/* Copyright 2022 flyinghead This file is part of Flycast. Flycast 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 2 of the License, or (at your option) any later version. Flycast 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 Flycast. If not, see . */ #include "naomi_network.h" #include "hw/naomi/naomi_flashrom.h" #include "cfg/option.h" #include "stdclass.h" #include "oslib/oslib.h" #include "oslib/i18n.h" using namespace i18n; #include #include #include NaomiNetwork naomiNetwork; bool NaomiNetwork::init() { if (!config::NetworkEnable) return false; if (config::EnableUPnP) { miniupnp.Init(); miniupnp.AddPortMapping(config::LocalPort, true); } createSocket(); return true; } void NaomiNetwork::createSocket() { sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock == INVALID_SOCKET) { ERROR_LOG(NETWORK, "Socket creation failed: errno %d", get_last_error()); throw Exception("Socket creation failed"); } int option = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&option, sizeof(option)); sockaddr_in serveraddr{}; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(config::LocalPort); if (::bind(sock, (sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) { ERROR_LOG(NETWORK, "NaomiServer: bind() failed. errno=%d", get_last_error()); closesocket(sock); throw Exception("Socket bind failed"); } set_non_blocking(sock); // Allow broadcast packets to be sent int broadcast = 1; if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (const char *)&broadcast, sizeof(broadcast)) == -1) WARN_LOG(NETWORK, "setsockopt(SO_BROADCAST) failed. errno=%d", get_last_error()); } bool NaomiNetwork::startNetwork() { if (!init()) return false; slotId = 0; slotCount = 0; slaves.clear(); using namespace std::chrono; if (config::ActAsServer) { enableNetworkBroadcast(true); const auto timeout = seconds(20); NOTICE_LOG(NETWORK, "Waiting for slave connections"); steady_clock::time_point start_time = steady_clock::now(); while (steady_clock::now() - start_time < timeout) { if (networkStopping) return false; std::string notif; if (slaves.empty()) { notif = T("Waiting for players..."); } else { notif = strprintf(translatePlural("%d player connected. Waiting...", "%d players connected. Waiting...", slaves.size()), (int)slaves.size()); } os_notify(notif.c_str(), timeout.count() * 2000); poll(); if ((maxSlots != 0 && slaves.size() == (unsigned)maxSlots - 1) || (_startNow && !slaves.empty())) break; std::this_thread::sleep_for(milliseconds(20)); } enableNetworkBroadcast(false); if (!slaves.empty()) { NOTICE_LOG(NETWORK, "Master starting: %zd slaves", slaves.size()); _startNow = true; slotCount = slaves.size() + 1; Packet packet(Start); packet.start.nodeCount = slotCount; for (auto& slave : slaves) send(&slave.addr, &packet, packet.size()); nextPeer = slaves[0].addr; os_notify(T("Starting game"), 2000); setNaomiNetworkConfig(0, slotCount); return true; } os_notify(T("No player connected"), 8000); } else { const auto timeout = seconds(30); serverIp = INADDR_BROADCAST; u16 serverPort = SERVER_PORT; if (!config::NetworkServer.get().empty()) { auto pos = config::NetworkServer.get().find_last_of(':'); std::string server; if (pos != std::string::npos) { serverPort = atoi(config::NetworkServer.get().substr(pos + 1).c_str()); server = config::NetworkServer.get().substr(0, pos); } else server = config::NetworkServer; addrinfo *resultAddr; if (getaddrinfo(server.c_str(), 0, nullptr, &resultAddr)) WARN_LOG(NETWORK, "Server %s is unknown", server.c_str()); else { for (addrinfo *ptr = resultAddr; ptr != nullptr; ptr = ptr->ai_next) if (ptr->ai_family == AF_INET) { serverIp = ((sockaddr_in *)ptr->ai_addr)->sin_addr.s_addr; break; } freeaddrinfo(resultAddr); } } NOTICE_LOG(NETWORK, "Connecting to server"); os_notify(T("Connecting to server"), 10000); steady_clock::time_point start_time = steady_clock::now(); while (!networkStopping && !_startNow && steady_clock::now() - start_time < timeout) { if (slotId == 0) { Packet packet(SyncReq); sockaddr_in serverAddr{}; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(serverPort); serverAddr.sin_addr.s_addr = serverIp; send(&serverAddr, &packet, packet.size()); } std::this_thread::sleep_for(milliseconds(10)); poll(); } if (!networkStopping && _startNow) { setNaomiNetworkConfig(slotId, slotCount, config::NaomiSatellite && satelliteSupported); return true; } } return false; } bool NaomiNetwork::receive(const sockaddr_in *addr, const Packet *packet, u32 size) { DEBUG_LOG(NETWORK, "Received port %d pckt %d size %x", ntohs(addr->sin_port), packet->type, size - (u32)packet->size(0)); switch (packet->type) { case SyncReq: if (!config::ActAsServer) break; if (!_startNow) { Slave *slave = nullptr; for (auto& s : slaves) if (s.addr.sin_port == addr->sin_port && s.addr.sin_addr.s_addr == addr->sin_addr.s_addr) { slave = &s; break; } if (slave == nullptr) { if (maxSlots != 0 && slaves.size() >= (unsigned)maxSlots - 1) { INFO_LOG(NETWORK, "Server is full. Sending NAK"); Packet reply(NAck); send(addr, &reply, reply.size()); break; } slaves.push_back(Slave()); slave = &slaves.back(); slave->state = 0; // unused slave->addr = *addr; } Packet reply(SyncReply); reply.sync.nodeId = (u16)(slave - &slaves[0] + 1); if (slave - &slaves[0] + 1 < (int)slaves.size()) { Slave *nextSlave = &slaves[slave - &slaves[0] + 1]; reply.sync.nextNodeIp = nextSlave->addr.sin_addr.s_addr; reply.sync.nextNodePort = nextSlave->addr.sin_port; } else { //FIXME local ip? reply.sync.nextNodeIp = 0; reply.sync.nextNodePort = htons(config::LocalPort); } send(addr, &reply, reply.size()); if (reply.sync.nodeId > 1) { // notify previous slave of nextNode change reply.sync.nextNodeIp = addr->sin_addr.s_addr; reply.sync.nextNodePort = addr->sin_port; reply.sync.nodeId--; slave = &slaves[reply.sync.nodeId - 1]; send(&slave->addr, &reply, reply.size()); } } else { INFO_LOG(NETWORK, "Server has already started. Sending NAK"); Packet reply(NAck); send(addr, &reply, reply.size()); } break; case SyncReply: if (!config::ActAsServer && !_startNow) { serverIp = addr->sin_addr.s_addr; slotId = packet->sync.nodeId; nextPeer.sin_family = AF_INET; nextPeer.sin_port = packet->sync.nextNodePort; nextPeer.sin_addr.s_addr = packet->sync.nextNodeIp == 0 ? addr->sin_addr.s_addr : packet->sync.nextNodeIp; std::string notif = strprintf(T("Connected as slot %d"), slotId); os_notify(notif.c_str(), 2000); } break; case Start: if (!_startNow) { slotCount = packet->start.nodeCount; sendAck(addr); _startNow = true; } break; case Data: if (!receivedData.empty()) INFO_LOG(NETWORK, "Received packet overwritten"); receivedData.resize(size - packet->size(0)); memcpy(receivedData.data(), packet->data.payload, receivedData.size()); packetNumber = packet->data.packetNumber; // TODO? sendAck(peer, port); return true; case Ack: break; case NAck: WARN_LOG(NETWORK, "NAK received"); throw Exception("Server has already started"); break; default: WARN_LOG(NETWORK, "Unknown packet type %d", packet->type); throw Exception("Unknown packet type "); break; } return false; } // Sets the game network config using MIE eeprom or bbsram: // Node -1 disables network // Node 0 is master, nodes 1+ are slave void setNaomiNetworkConfig(int node, int nodeCount, bool satellite) { const std::string& gameId = settings.content.gameId; if (gameId == "ALIEN FRONT") { // no way to disable the network write_naomi_eeprom(0x3f, node == 0 ? 0 : 1); } else if (gameId == "MOBILE SUIT GUNDAM JAPAN" // gundmct || gameId == "MOBILE SUIT GUNDAM DELUXE JAPAN") // gundmxgd { write_naomi_eeprom(0x38, node == -1 ? 2 : node == 0 ? 0 : 1); } else if (gameId == " BIOHAZARD GUN SURVIVOR2") { write_naomi_flash(0x21c, node == 0 ? 0 : 1); // CPU ID - 1 write_naomi_flash(0x22a, node == -1 ? 0 : 1); // comm link on } else if (gameId == "HEAVY METAL JAPAN") { write_naomi_eeprom(0x31, node == -1 ? 0 : node == 0 ? 1 : 2); } else if (gameId == "OUTTRIGGER JAPAN") { write_naomi_flash(0x21a, node == -1 ? 0 : 1); // network on write_naomi_flash(0x21b, node); // node id } else if (gameId == "SLASHOUT JAPAN VERSION") { write_naomi_eeprom(0x30, node + 1); } else if (gameId == "SPAWN JAPAN") { write_naomi_eeprom(0x44, node == -1 ? 0 : node == 0 ? 1 : 2); // the game wants the region there or it resets the eeprom write_naomi_eeprom(0x30, config::Region); } else if (gameId == "SPIKERS BATTLE JAPAN VERSION") { write_naomi_eeprom(0x30, node == -1 ? 0 : node == 0 ? 1 : 2); } else if (gameId == "VIRTUAL-ON ORATORIO TANGRAM") { u8 v; switch (node) { case -1: v = 3; break; case 0: v = 0; break; default: if (satellite) v = 2; else v = 1; break; } write_naomi_eeprom(0x45, v); } else if (gameId == "WAVE RUNNER GP") { write_naomi_eeprom(0x33, node); write_naomi_eeprom(0x35, node == -1 ? 2 : node == 0 ? 0 : 1); } else if (gameId == "WORLD KICKS") { write_naomi_flash(0x224, node == -1 ? 0 : 1); // network on write_naomi_flash(0x220, node == 0 ? 0 : 1); // node id } else if (gameId == "CLUB KART IN JAPAN" && settings.content.fileName.substr(0, 6) != "clubkp") { u8 v; switch (node) { case -1: v = 0; break; case 0: v = 1; break; default: if (satellite) v = 3; else v = 2; break; } write_naomi_eeprom(0x34, v); if (node != -1) { // car # if (settings.content.fileName.substr(0, 7) == "clubkrt") { u8 b = read_naomi_eeprom(0x4a) & 0xf8; write_naomi_eeprom(0x4a, b | node); } else { // clubk2k3 u8 b = read_naomi_eeprom(0x3d) & 0xc7; write_naomi_eeprom(0x3d, b | (node << 3)); } } } else if (gameId == "INITIAL D" || gameId == "INITIAL D Ver.2" || gameId == "INITIAL D Ver.3") { u8 b = read_naomi_eeprom(0x34) & 0xcf; write_naomi_eeprom(0x34, (node == -1 ? 0x00 : node == 0 ? 0x10 : 0x20) | b); } else if (gameId == "THE KING OF ROUTE66") { u8 b = read_naomi_eeprom(0x3d) & 0xf; write_naomi_eeprom(0x3d, (node == -1 ? 0x40 : node == 0 ? 0x50 : 0x60) | b); } else if (gameId == "MAXIMUM SPEED") { configure_maxspeed_flash(node != -1, node == 0); } else if (gameId == "F355 CHALLENGE JAPAN" && settings.content.fileName != "f355") { write_naomi_flash(0x230, node == -1 ? 0 : node == 0 ? 1 : 2); if (node != -1) // car number (0 to 7) write_naomi_flash(0x231, node); // 0x233: cabinet type (0 deluxe, 1 twin) write_naomi_flash(0x233, config::MultiboardSlaves >= 2 ? 0 : 1); } else if (gameId == "SEGA TETRIS") { write_naomi_eeprom(0x50, node + 1); } else if (gameId.substr(0, 6) == " DERBY") { if (read_naomi_eeprom(3) == 'B' && read_naomi_eeprom(4) == 'D' && read_naomi_eeprom(5) == 'Y') { // derbyoc2: DOC 2 v2.1 if (node == -1 && config::MultiboardSlaves <= 1 && !settings.naomi.slave) // no link (satellite) write_naomi_eeprom(0x30, read_naomi_eeprom(0x30) | 1); else write_naomi_eeprom(0x30, read_naomi_eeprom(0x30) & 0xfe); } else { // DOC (BAX0) // DOC 2000 (BBX0) // DOC WE (BEF0) if (config::MultiboardSlaves <= 1 && !settings.naomi.slave) { if (node == -1) // no link write_naomi_eeprom(0x45, (read_naomi_eeprom(0x45) & 0xfc) | 3); else // satellite write_naomi_eeprom(0x45, (read_naomi_eeprom(0x45) & 0xfc) | 2); } else { // Can't disable the network on the main screen write_naomi_eeprom(0x45, (read_naomi_eeprom(0x45) & 0xfc)); } if (node != -1 && nodeCount < 5) WARN_LOG(NETWORK,"Derby Owners Club doesn't support less than 4 satellites"); write_naomi_eeprom(0x4e, (read_naomi_eeprom(0x4e) & 0xf8) | std::max(nodeCount - 2, 3)); } } else if (gameId.substr(0, 4) == "WCCF" && node != -1) { // wccf420e: // offset 34h left proj=0, right proj=1, sat0=a, sat1=b, ... // offset 36h // max sat 4: 86 (1 * 4), 46 (2 + 2) // max sat 6: 84 (4 + 2), 85 (2 * 3) // max sat 8: 80 (or 00) (2 * 4), 81 (3 + 3 + 2), 82 (3 + 2 + 3), 83 (2 + 3 + 3) // msb is show numbers on/off write_naomi_eeprom(0x34, node); /* Not really needed if (nodeCount >= 8) nodeCount = 0x80; // 2 rows * 4 sats else if (nodeCount >= 6) nodeCount = 0x85; // 2 rows * 3 sats else nodeCount = 0x86; // 1 row * 4 sats write_naomi_eeprom(0x36, nodeCount); */ } } // Returns a pair (max nodes, satellite supported) std::pair naomiNetworkMaxNodes() { // gameId -> (max nodes, satellite supported) static std::map> games { { "ALIEN FRONT", { 4, false } }, { "MOBILE SUIT GUNDAM JAPAN", { 4, false } }, { "MOBILE SUIT GUNDAM DELUXE JAPAN", { 4, false } }, { " BIOHAZARD GUN SURVIVOR2", { 2, false } }, { "HEAVY METAL JAPAN", { 2, false } }, { "OUTTRIGGER JAPAN", { 4, false } }, { "SLASHOUT JAPAN VERSION", { 4, false } }, { "SPAWN JAPAN", { 4, false } }, { "SPIKERS BATTLE JAPAN VERSION", { 4, false } }, { "VIRTUAL-ON ORATORIO TANGRAM", { 3, true } }, { "WAVE RUNNER GP", { 4, false } }, { "WORLD KICKS", { 2, false } }, { "F355 CHALLENGE JAPAN", { 8, false } }, { "SEGA TETRIS", { 2, false } }, { " DERBY OWNERS CLUB WE ---------", { 9, false } }, { " DERBY OWNERS CLUB ------------", { 9, false } }, { " DERBY OWNERS CLUB II-----------", { 9, false } }, // Naomi 2 { "CLUB KART IN JAPAN", { 8, true } }, { "INITIAL D", { 2, false } }, { "INITIAL D Ver.2", { 2, false } }, { "INITIAL D Ver.3", { 2, false } }, { "THE KING OF ROUTE66", { 2, false } }, { "SEGA DRIVING SIMULATOR", { 3, false } }, }; if (!config::NetworkEnable || settings.naomi.slave) return {}; if (settings.content.fileName.substr(0, 6) == "clubkp" || settings.content.fileName == "f355") // Club Kart Prize and F355 (vanilla) don't support networking return {}; auto it = games.find(settings.content.gameId); if (it == games.end()) return {}; else return it->second; }