Files
Flyinghead 8542ff2dc4 naomi network QoL improvements. fix jvs eeprom regression
Support for satellite nodes (vonot, club kart)
Start network game when known max of nodes is reached.
Add Start Now button to network sync wait window.

Ignore MultiboardSlaves option if not active when loading eeprom. Rely
on game id instead.
2026-06-02 16:24:14 +02:00

281 lines
7.1 KiB
C++

/*
Copyright 2023 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "hw/sh4/modules/modules.h"
#include "net_platform.h"
#include "cfg/option.h"
#include "miniupnp.h"
#include "hw/sh4/sh4_sched.h"
#include "net_handshake.h"
#include "ice.h"
#include "oslib/i18n.h"
#include <deque>
class NullModemPipe : public SerialPort::Pipe
{
public:
class Exception : public FlycastException
{
public:
Exception(const std::string& reason) : FlycastException(reason) {}
};
// Serial TX
void write(u8 data) override
{
u8 packet[2] = { 'D', data };
int rc = sendto(sock, (const char *)&packet[0], sizeof(packet), 0, (const sockaddr *)&peerAddress, sizeof(peerAddress));
if (rc != sizeof(packet))
ERROR_LOG(NETWORK, "sendto: %d errno %d", rc, get_last_error());
DEBUG_LOG(NETWORK, "Write %02x %c (buf rx %d)", data, data, (int)rxBuffer.size());
}
void sendBreak() override
{
const char b = 'B';
int rc = sendto(sock, &b, 1, 0, (const sockaddr *)&peerAddress, sizeof(peerAddress));
if (rc != 1)
ERROR_LOG(NETWORK, "sendto: %d errno %d", rc, get_last_error());
DEBUG_LOG(NETWORK, "Send Break");
}
// RX buffer Size
int available() override {
poll();
checkBreak();
int realSize = 0;
for (u32 b : rxBuffer)
if (b != (u32)~0)
realSize++;
return realSize;
}
// Serial RX
u8 read() override
{
poll();
if (rxBuffer.empty()) {
WARN_LOG(NETWORK, "NetPipe: empty read");
return 0;
}
u8 b = rxBuffer.front();
rxBuffer.pop_front();
DEBUG_LOG(NETWORK, "Read %02x (buf rx %d)", b, (int)rxBuffer.size());
checkBreak();
return b;
}
~NullModemPipe() {
shutdown();
}
bool init()
{
if (config::EnableUPnP) {
miniupnp.Init();
miniupnp.AddPortMapping(config::LocalPort, true);
}
createSocket();
SCIFSerialPort::Instance().setPipe(this);
return true;
}
void shutdown()
{
enableNetworkBroadcast(false);
if (VALID(sock))
closesocket(sock);
sock = INVALID_SOCKET;
if (SCIFSerialPort::Instance().getPipe() == this)
SCIFSerialPort::Instance().setPipe(nullptr);
}
private:
void checkBreak()
{
if (!rxBuffer.empty() && rxBuffer.front() == (u32)~0) {
SCIFSerialPort::Instance().receiveBreak();
rxBuffer.pop_front();
}
}
void poll()
{
if (lastPoll == sh4_sched_now64())
return;
lastPoll = sh4_sched_now64();
u8 data[0x100];
sockaddr_in addr;
while (true)
{
socklen_t len = sizeof(addr);
int rc = recvfrom(sock, (char *)data, sizeof(data), 0, (sockaddr *)&addr, &len);
if (rc == -1)
{
int error = get_last_error();
if (error == L_EWOULDBLOCK || error == L_EAGAIN)
break;
#ifdef _WIN32
if (error == WSAECONNRESET)
// Happens if the previous send resulted in an ICMP Port Unreachable message
break;
#endif
throw Exception("Receive error: errno " + std::to_string(error));
}
if (peerAddress.sin_addr.s_addr == INADDR_BROADCAST)
{
if (addr.sin_port != htons(config::LocalPort) || !is_local_address(addr.sin_addr.s_addr))
{
peerAddress.sin_addr.s_addr = addr.sin_addr.s_addr;
peerAddress.sin_port = addr.sin_port;
enableNetworkBroadcast(false);
NOTICE_LOG(NETWORK, "Data received from peer %x:%d", htonl(addr.sin_addr.s_addr), htons(addr.sin_port));
}
else {
// this is coming from us so ignore it
continue;
}
}
if (rc == 2)
{
if (data[0] != 'D') {
ERROR_LOG(NETWORK, "Unexpected packet '%c'", data[0]);
}
else
{
rxBuffer.push_back(data[1]);
if (!dataReceived)
{
dataReceived = true;
char name[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr.sin_addr, name, sizeof(name));
std::string s(name);
s += ":" + std::to_string(htons(addr.sin_port));
os_notify(i18n::T("Network connected"), 5000, s.c_str());
}
}
}
else if (rc == 1)
{
if (data[0] != 'B')
ERROR_LOG(NETWORK, "Unexpected packet '%c'", data[0]);
else
rxBuffer.push_back(~0);
}
}
}
void 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());
peerAddress.sin_family = AF_INET;
peerAddress.sin_addr.s_addr = INADDR_BROADCAST;
peerAddress.sin_port = htons(defaultNaomiServerPort());
if (!config::NetworkServer.get().empty()
// ignore server name if acting as server (maxspeed)
&& (!config::ActAsServer || settings.platform.isConsole()))
{
auto pos = config::NetworkServer.get().find_last_of(':');
std::string server;
if (pos != std::string::npos) {
peerAddress.sin_port = htons(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) {
peerAddress.sin_addr.s_addr = ((sockaddr_in *)ptr->ai_addr)->sin_addr.s_addr;
break;
}
freeaddrinfo(resultAddr);
}
}
else {
enableNetworkBroadcast(true);
}
}
sock_t sock = INVALID_SOCKET;
MiniUPnP miniupnp;
std::deque<u32> rxBuffer;
sockaddr_in peerAddress{};
u64 lastPoll = 0;
bool dataReceived = false;
};
class BattleCableHandshake : public NetworkHandshake
{
public:
std::future<bool> start() override
{
std::promise<bool> promise;
if (ice::getState() != ice::Playing)
promise.set_value(pipe.init());
else
promise.set_value(true);
return promise.get_future();
}
void stop() override {
pipe.shutdown();
}
bool canStartNow() override {
return true;
}
void startNow() override {}
private:
NullModemPipe pipe;
};