Files
psxrecomp/runtime/src/netplay_exit_reason.c
Alexandros Mandravillis c1a9e52894 netplay: say why a start failed
A failed start printed "built without recomp-net, or bind/peer invalid" for every cause. A player whose system held the UDP port read first that the build had no netplay.

The library answers a refused bind, a listen address it cannot read and a peer address it cannot use with the same -1. main.cpp now does those steps again (it reads the listen address as the library does, looks up a name, binds without address reuse) and reports the system's answer. The sentence names a cause only when the system does: address in use, access denied (a port range Windows keeps for Hyper-V or WSL, a port below 1024 elsewhere), address not available. Any other error gets the number and the system's text. The peer address is named only when the listen address opened and the peer address cannot be read. The build is named only when it has no netplay.

The sentences are in netplay_exit_reason.c with unit cases; a source guard checks the wiring in main.cpp.
2026-10-02 19:13:57 +03:00

169 lines
7.4 KiB
C

/* Why a netplay match ended, in words. See netplay_exit_reason.h. */
#include "netplay_exit_reason.h"
#include <stddef.h>
#include <stdio.h>
#include <string.h>
typedef struct {
const char *origin;
const char *text;
} NetplayExitReason;
static const NetplayExitReason k_reasons[] = {
{ "netplay_boot_mismatch",
"The match ended: the players' consoles started differently "
"(different BIOS image or boot settings)." },
{ "netplay_peer_disconnect",
"The match ended: the other player left or stopped responding." },
{ "netplay_load_failed",
"The match ended: a player could not load the shared save state." },
{ "netplay_load_stall",
"The match ended: loading the shared save state timed out." },
{ "netplay_link_stall",
"The match ended: could not connect to the other player in time." },
{ "netplay_admit_stall",
"The match ended: the players' games stopped running in step." },
};
const char *netplay_exit_reason_text(const char *origin)
{
size_t i;
if (!origin) return NULL;
for (i = 0; i < sizeof(k_reasons) / sizeof(k_reasons[0]); ++i) {
if (strcmp(k_reasons[i].origin, origin) == 0)
return k_reasons[i].text;
}
return NULL;
}
/* What a refused bind means, from the system's error number. The numbers of
* Windows, Linux and macOS do not overlap for these three causes. */
enum {
BIND_ERROR_OTHER = 0,
BIND_ERROR_IN_USE, /* WSAEADDRINUSE 10048, EADDRINUSE 98 (Linux), 48 (macOS) */
BIND_ERROR_NOT_ALLOWED, /* WSAEACCES 10013, EACCES 13 */
BIND_ERROR_NO_SUCH_ADDR /* WSAEADDRNOTAVAIL 10049, EADDRNOTAVAIL 99 (Linux), 49 (macOS) */
};
static int bind_error_kind(int sys_error)
{
switch (sys_error) {
case 10048: case 98: case 48: return BIND_ERROR_IN_USE;
case 10013: case 13: return BIND_ERROR_NOT_ALLOWED;
case 10049: case 99: case 49: return BIND_ERROR_NO_SUCH_ADDR;
default: return BIND_ERROR_OTHER;
}
}
/* The system's own text goes into a sentence only when it is short and plain
* ASCII: a text in the system's language and code page is not shown as
* broken letters, and a long one is not cut in the middle. */
static int sys_text_fits(const char *text)
{
size_t n = 0;
if (!text || !text[0]) return 0;
for (; text[n]; ++n) {
if ((unsigned char)text[n] < 0x20 || (unsigned char)text[n] > 0x7e)
return 0;
}
return n <= 80;
}
void netplay_start_failure_text(int start_rc, int has_netplay,
const char *bind_hostport,
const char *peer_hostport, int bind_probe,
int sys_error, const char *sys_text,
char *out, size_t cap)
{
char address[64] = "";
char system[112] = "";
const char *port = "";
const char *colon;
if (!out || !cap) return;
out[0] = '\0';
if (!bind_hostport) bind_hostport = "";
if (!peer_hostport) peer_hostport = "";
/* "address:port": the port is what follows the last colon. No address
* means every address of this computer. */
colon = strrchr(bind_hostport, ':');
if (colon && (size_t)(colon - bind_hostport) < sizeof(address)) {
memcpy(address, bind_hostport, (size_t)(colon - bind_hostport));
address[colon - bind_hostport] = '\0';
port = colon + 1;
if (!address[0]) snprintf(address, sizeof(address), "0.0.0.0");
}
/* "(system error N)" or "(system error N: the system's text)". */
if (sys_text_fits(sys_text))
snprintf(system, sizeof(system), "(system error %d: %s)", sys_error, sys_text);
else
snprintf(system, sizeof(system), "(system error %d)", sys_error);
if (!has_netplay) {
snprintf(out, cap, "This build has no netplay: it was built without "
"the netplay library.");
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED &&
bind_error_kind(sys_error) == BIND_ERROR_NOT_ALLOWED) {
/* Access denied. The system does not hand the port out: a port range
* that Windows keeps for Hyper-V or WSL, a port another program
* holds for itself alone on Windows, or a port below 1024 elsewhere.
* "Another program holds it" would send the player looking for a
* program that may not exist; another port helps in every case. */
snprintf(out, cap,
"Netplay could not open UDP port %.5s on %.15s (system error "
"%d). The operating system does not allow this port. Choose "
"another port and start again.",
port, address, sys_error);
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED &&
bind_error_kind(sys_error) == BIND_ERROR_IN_USE) {
snprintf(out, cap,
"Netplay could not open UDP port %.5s on %.15s (system error "
"%d). Another program or the operating system holds that "
"port. Choose another port and start again.",
port, address, sys_error);
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED &&
bind_error_kind(sys_error) == BIND_ERROR_NO_SUCH_ADDR) {
/* No port is held and another port does not help: the listen address
* is not an address of this computer (a copied command line, a
* network that is down). */
snprintf(out, cap,
"Netplay could not listen on %.15s (system error %d). That "
"address is not one of this computer's addresses. Check the "
"listen address and start again.",
address, sys_error);
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED) {
/* Any other answer: the number and the system's text, no cause. */
snprintf(out, cap, "Netplay could not open UDP port %.5s on %.15s %s.",
port, address, system);
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_NO_SOCKET) {
snprintf(out, cap, "Netplay could not open a network socket %s.", system);
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_BAD_ADDRESS) {
snprintf(out, cap,
"Netplay could not start: \"%.60s\" is not an address and a "
"port to listen on.", bind_hostport);
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_PEER_BAD) {
snprintf(out, cap,
"Netplay could not start: the other player's address "
"\"%.60s\" could not be used.", peer_hostport);
} else if (start_rc == -3) {
/* The listen address opened and the peer address reads well, or
* nothing was tried: no cause is known. */
snprintf(out, cap,
"Netplay could not start its connection on %.60s.",
bind_hostport);
} else if (start_rc == -4) {
/* Five causes share this code (a missing peer or relay setting, a
* relay start that failed, a build without the relay library); the
* code does not say which. */
snprintf(out, cap, "Netplay could not start the online connection.");
} else {
snprintf(out, cap, "Netplay could not start (code %d).", start_rc);
}
}
int netplay_boot_mismatch_final(uint32_t mismatch_since_ms, uint32_t now_ms,
uint32_t grace_ms)
{
if (!mismatch_since_ms) return 0;
return (uint32_t)(now_ms - mismatch_since_ms) >= grace_ms;
}