/** ** Supermodel ** A Sega Model 3 Arcade Emulator. ** Copyright 2011-2020 Bart Trzynadlowski, Nik Henson, Ian Curtis, ** Harry Tuttle, and Spindizzi ** ** This file is part of Supermodel. ** ** Supermodel is free software: you can redistribute it and/or modify it under ** the terms of the GNU General Public License as published by the Free ** Software Foundation, either version 3 of the License, or (at your option) ** any later version. ** ** Supermodel is distributed in the hope that it will be useful, but WITHOUT ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ** more details. ** ** You should have received a copy of the GNU General Public License along ** with Supermodel. If not, see . **/ #include #include #include "Supermodel.h" #include "SimNetBoard.h" // these make 16-bit read/writes much neater #define RAM16 *(uint16_t*)&RAM #define CommRAM16 *(uint16_t*)&CommRAM static const uint64_t netGUID = 0x5bf177da34872; inline bool CSimNetBoard::IsGame(const char* gameName) { return (m_gameInfo.name == gameName) || (m_gameInfo.parent == gameName); } CSimNetBoard::CSimNetBoard(const Util::Config::Node& config) : m_config(config) { } CSimNetBoard::~CSimNetBoard(void) { m_quit = true; if (m_connectThread.joinable()) m_connectThread.join(); } void CSimNetBoard::SaveState(CBlockFile* SaveState) { } void CSimNetBoard::LoadState(CBlockFile* SaveState) { } bool CSimNetBoard::Init(uint8_t* netRAMPtr, uint8_t* netBufferPtr) { RAM = netRAMPtr; Buffer = netBufferPtr; CommRAM = Buffer; externalCommRAM = Buffer + 0x10000; m_attached = m_gameInfo.netboard_present && m_config["Network"].ValueAs(); if (!m_attached) return 0; if (IsGame("daytona2") || IsGame("harley") || IsGame("scud") || IsGame("srally2") || IsGame("skichamp") || IsGame("spikeout") || IsGame("spikeofe")) m_gameType = GameType::one; else if (IsGame("lemans24") || IsGame("von2") || IsGame("dirtdvls")) m_gameType = GameType::two; else return ErrorLog("Game not recognized or supported"); m_state = State::start; //netsocks port_in = m_config["PortIn"].ValueAs(); port_out = m_config["PortOut"].ValueAs(); addr_out = m_config["AddressOut"].ValueAs(); nets = std::make_unique(addr_out, port_out); netr = std::make_unique(port_in); return 0; } void CSimNetBoard::RunFrame(void) { if (!IsRunning()) return; switch (m_state) { case State::start: if (!m_connected && !m_connectThread.joinable()) m_connectThread = std::thread(&CSimNetBoard::ConnectProc, this); m_status0 = 0; m_status1 = IsGame("dirtdvls") ? 0x4004 : 0xe000; m_state = State::init; break; case State::init: memset(CommRAM, 0, 0x20000); if (m_gameType == GameType::one) { if (m_status0 & 0x8000) // has main board changed this register? { m_IRQ2ack |= 0x01; // simulate IRQ 2 ack if (m_status0 == 0xf000) { // initialization complete m_status1 = 0; CommRAM16[0x72] = FLIPENDIAN16(0x1); // is this necessary? m_state = State::testing; } m_status0 = 0; // 0 should work for all init subroutines } } else { // type 2 performs initialization on its own m_status1 = 0; m_state = State::testing; m_counter = 0; } break; case State::testing: if (m_gameType == GameType::one) { m_status0 += 1; // type 1 games require this to be incremented every frame if (!m_connected) break; uint8_t numMachines, machineIndex; if (RAM16[0x400] == 0) // master { // flush receive buffer while (netr->CheckDataAvailable()) { netr->Receive(); } // check all linked instances have the same GUID nets->Send(&netGUID, sizeof(netGUID)); auto& recv_data = netr->Receive(); if (recv_data.empty()) break; uint64_t testGUID; memcpy(&testGUID, &recv_data[0], recv_data.size()); if (testGUID != netGUID) testGUID = 0; // send the GUID for one more loop nets->Send(&testGUID, sizeof(testGUID)); netr->Receive(); if (testGUID != netGUID) { ErrorLog("unable to verify connection. Make sure all machines are using same build!"); m_state = State::error; break; } // master has an index of zero machineIndex = 0; nets->Send(&machineIndex, sizeof(machineIndex)); // receive back the number of other linked machines recv_data = netr->Receive(); if (recv_data.empty()) break; numMachines = recv_data[0]; // send the number of other linked machines nets->Send(&numMachines, sizeof(numMachines)); netr->Receive(); } else { // receive GUID from the previous machine and check it matches auto& recv_data = netr->Receive(); if (recv_data.empty()) break; uint64_t testGUID; memcpy(&testGUID, &recv_data[0], recv_data.size()); if (testGUID != netGUID) testGUID = 0; nets->Send(&testGUID, sizeof(testGUID)); // one more time, in case a later machine has a GUID mismatch recv_data = netr->Receive(); if (recv_data.empty()) break; memcpy(&testGUID, &recv_data[0], recv_data.size()); if (testGUID != netGUID) testGUID = 0; nets->Send(&testGUID, sizeof(testGUID)); if (testGUID != netGUID) { ErrorLog("unable to verify connection. Make sure all machines are using same build!"); m_state = State::error; break; } // receive the previous machine's index, increment it, send it to the next machine recv_data = netr->Receive(); if (recv_data.empty()) break; machineIndex = recv_data[0] + 1; nets->Send(&machineIndex, sizeof(machineIndex)); // receive the number of other linked machines and forward it on recv_data = netr->Receive(); if (recv_data.empty()) break; numMachines = recv_data[0]; nets->Send(&numMachines, sizeof(numMachines)); } // if there are no other linked machines, only continue if Supermodel is linked to itself // there might be more than one machine set to master which would cause glitches if ((numMachines == 0) && ((port_in != port_out) || (addr_out.compare("127.0.0.1") != 0))) { ErrorLog("no slave machines detected. Make sure only one machine is set to master!"); m_state = State::error; break; } m_numMachines = numMachines + 1; m_status0 = 0; // supposed to cycle between 0 and 1 (also 2 for Daytona 2); doesn't seem to matter m_status1 = 0x2021 + (numMachines * 0x20) + machineIndex; CommRAM16[0x0] = RAM16[0x400]; // 0 if master, 1 if slave CommRAM16[0x2] = numMachines; CommRAM16[0x4] = machineIndex; m_counter = 0; CommRAM16[0x6] = 0; m_segmentSize = RAM16[0x404]; // don't know if these are actually required, but it never hurts to include them CommRAM16[0x8] = FLIPENDIAN16(0x100 + m_segmentSize); CommRAM16[0xa] = FLIPENDIAN16(RAM16[0x402] - m_segmentSize - 1); CommRAM16[0xc] = FLIPENDIAN16(0x100); CommRAM16[0xe] = FLIPENDIAN16(RAM16[0x402] - m_segmentSize + 0x200); m_state = State::ready; } else { if (!m_connected) break; // we have to track both playable and non-playable machines for type 2 struct { uint8_t total; uint8_t playable; } numMachines, machineIndex; if (RAM16[0x200] == 0) // master { // flush receive buffer while (netr->CheckDataAvailable()) netr->Receive(); // check all linked instances have the same GUID nets->Send(&netGUID, sizeof(netGUID)); auto& recv_data = netr->Receive(); if (recv_data.empty()) break; uint64_t testGUID; memcpy(&testGUID, &recv_data[0], recv_data.size()); if (testGUID != netGUID) testGUID = 0; // send the GUID for one more loop nets->Send(&testGUID, sizeof(testGUID)); netr->Receive(); if (testGUID != netGUID) { ErrorLog("unable to verify connection. Make sure all machines are using same build!"); m_state = State::error; break; } // master has indices set to zero machineIndex.total = 0, machineIndex.playable = 0; nets->Send(&machineIndex, sizeof(machineIndex)); // receive back the number of other linked machines recv_data = netr->Receive(); if (recv_data.empty()) break; memcpy(&numMachines, &recv_data[0], recv_data.size()); // send the number of other linked machines nets->Send(&numMachines, sizeof(numMachines)); netr->Receive(); } else if (RAM16[0x200] < 0x8000) // slave { // receive GUID from the previous machine and check it matches auto& recv_data = netr->Receive(); if (recv_data.empty()) break; uint64_t testGUID; memcpy(&testGUID, &recv_data[0], recv_data.size()); if (testGUID != netGUID) testGUID = 0; nets->Send(&testGUID, sizeof(testGUID)); // one more time, in case a later machine has a GUID mismatch recv_data = netr->Receive(); if (recv_data.empty()) break; memcpy(&testGUID, &recv_data[0], recv_data.size()); if (testGUID != netGUID) testGUID = 0; nets->Send(&testGUID, sizeof(testGUID)); if (testGUID != netGUID) { ErrorLog("unable to verify connection. Make sure all machines are using same build!"); m_state = State::error; break; } // receive the indices of the previous machine and increment them recv_data = netr->Receive(); if (recv_data.empty()) break; memcpy(&machineIndex, &recv_data[0], recv_data.size()); machineIndex.total++, machineIndex.playable++; // send our indices to the next machine nets->Send(&machineIndex, sizeof(machineIndex)); // receive the number of machines recv_data = netr->Receive(); if (recv_data.empty()) break; memcpy(&numMachines, &recv_data[0], recv_data.size()); // forward the number of machines nets->Send(&numMachines, sizeof(numMachines)); } else { // relay/satellite // receive GUID from the previous machine and check it matches auto& recv_data = netr->Receive(); if (recv_data.empty()) break; uint64_t testGUID; memcpy(&testGUID, &recv_data[0], recv_data.size()); if (testGUID != netGUID) testGUID = 0; nets->Send(&testGUID, sizeof(testGUID)); // one more time, in case a later machine has a GUID mismatch recv_data = netr->Receive(); if (recv_data.empty()) break; memcpy(&testGUID, &recv_data[0], recv_data.size()); if (testGUID != netGUID) testGUID = 0; nets->Send(&testGUID, sizeof(testGUID)); if (testGUID != netGUID) { ErrorLog("unable to verify connection. Make sure all machines are using same build!"); m_state = State::error; break; } // receive the indices of the previous machine; don't increment the playable index recv_data = netr->Receive(); if (recv_data.empty()) break; memcpy(&machineIndex, &recv_data[0], recv_data.size()); machineIndex.total++; // send our indices to the next machine nets->Send(&machineIndex, sizeof(machineIndex)); // receive the number of machines recv_data = netr->Receive(); if (recv_data.empty()) break; memcpy(&numMachines, &recv_data[0], recv_data.size()); // forward the number of machines nets->Send(&numMachines, sizeof(numMachines)); // indicate that this machine is a relay/satellite if (!IsGame("dirtdvls")) machineIndex.playable |= 0x80; } // if there are no other linked machines, only continue if Supermodel is linked to itself // there might be more than one machine set to master which would cause glitches if ((numMachines.total == 0) && ((port_in != port_out) || (addr_out.compare("127.0.0.1") != 0))) { ErrorLog("no slave machines detected. Make sure only one machine is set to master!"); if (IsGame("dirtdvls")) m_status1 = 0x8085; // seems like the netboard code writers really liked their CPU model numbers m_state = State::error; break; } m_numMachines = numMachines.total + 1; m_status0 = 5; // probably not necessary if (IsGame("dirtdvls")) m_status1 = (numMachines.playable << 4) | machineIndex.playable | 0x7400; else m_status1 = (numMachines.playable << 8) | machineIndex.playable; CommRAM16[0x0] = RAM16[0x200]; // master/slave/relay status CommRAM16[0x2] = (numMachines.playable << 8) | numMachines.total; CommRAM16[0x4] = (machineIndex.playable << 8) | machineIndex.total; m_counter = 0; CommRAM16[0x6] = 0; m_segmentSize = RAM16[0x204]; // don't know if these are actually required, but it never hurts to include them CommRAM16[0x8] = FLIPENDIAN16(0x100 + m_segmentSize); CommRAM16[0xa] = FLIPENDIAN16(RAM16[0x206]); CommRAM16[0xc] = FLIPENDIAN16(0x100); CommRAM16[0xe] = FLIPENDIAN16(RAM16[0x206] + 0x80); m_state = State::ready; } break; case State::ready: m_counter++; CommRAM16[0x6] = FLIPENDIAN16(m_counter); // we only send what we need to; helps cut down on bandwidth // each machine has to receive back its own data (TODO: copy this data manually?) for (int i = 0; i < m_numMachines; i++) { nets->Send(CommRAM + 0x100 + i * m_segmentSize, m_segmentSize); auto& recv_data = netr->Receive(); if (recv_data.size() == 0) { // link broken - send an "empty" packet to alert other machines nets->Send(nullptr, 0); m_state = State::error; if (m_gameType == GameType::one) m_status1 = 0x40; // send "link broken" message to mainboard break; } memcpy(CommRAM + 0x100 + (i + 1) * m_segmentSize, recv_data.data(), recv_data.size()); } // swap CommRAM banks if (m_commbank) { m_commbank = false; CommRAM = Buffer; externalCommRAM = Buffer + 0x10000; } else { m_commbank = true; CommRAM = Buffer + 0x10000; externalCommRAM = Buffer; } break; case State::error: // do nothing break; } } void CSimNetBoard::Reset(void) { // if netboard was active, send an "empty" packet so the other machines don't get stuck waiting for data if (m_state == State::ready) { nets->Send(nullptr, 0); netr->Receive(); } m_running = false; m_state = State::start; } bool CSimNetBoard::IsAttached(void) { return m_attached; } bool CSimNetBoard::IsRunning(void) { return m_attached && m_running; } void CSimNetBoard::GetGame(Game gameInfo) { m_gameInfo = gameInfo; } void CSimNetBoard::ConnectProc(void) { using namespace std::chrono_literals; if (m_connected) return; printf("Connecting to %s:%i ..\n", addr_out.c_str(), port_out); // wait until TCPSend has connected to the next machine while (!nets->Connect()) { if (m_quit) return; } // wait until TCPReceive has accepted a connection from the previous machine while (!netr->Connected()) { if (m_quit) return; std::this_thread::sleep_for(1ms); } printf("Successfully connected.\n"); m_connected = true; } uint8_t CSimNetBoard::ReadCommRAM8(unsigned addr) { return externalCommRAM[addr]; } uint16_t CSimNetBoard::ReadCommRAM16(unsigned addr) { return *(uint16_t*)&externalCommRAM[addr]; } uint32_t CSimNetBoard::ReadCommRAM32(unsigned addr) { return *(uint32_t*)&externalCommRAM[addr]; } void CSimNetBoard::WriteCommRAM8(unsigned addr, uint8_t data) { externalCommRAM[addr] = data; } void CSimNetBoard::WriteCommRAM16(unsigned addr, uint16_t data) { *(uint16_t*)&externalCommRAM[addr] = data; } void CSimNetBoard::WriteCommRAM32(unsigned addr, uint32_t data) { *(uint32_t*)&externalCommRAM[addr] = data; } uint16_t CSimNetBoard::ReadIORegister(unsigned reg) { if (!IsRunning()) return 0; switch (reg) { case 0x00: return m_IRQ2ack; case 0x88: return m_status0; case 0x8a: return m_status1; default: ErrorLog("read from unknown IO register 0x%02x", reg); return 0; } } void CSimNetBoard::WriteIORegister(unsigned reg, uint16_t data) { switch (reg) { case 0x00: m_IRQ2ack = data; break; case 0x88: m_status0 = data; break; case 0x8a: m_status1 = data; break; case 0xc0: if (data == 0) Reset(); m_running = (data != 0); break; default: ErrorLog("write to unknown IO register 0x%02x", reg); } }