mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
1d98dd5d30
A match started from the launcher closed the program when the start failed, with the reason on a console the player does not see. It now ends through netplay_soft_exit, keeps the sentence for the status line and goes back to the room before the scheduler runs, so the game is not booted offline. No session is open at that point, so no BYE is sent. A command-line start still exits with code 1.
126 lines
7.9 KiB
Python
126 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Guard that a failed netplay start says what failed.
|
|
|
|
The start printed "built without recomp-net, or bind/peer invalid" for every
|
|
failure. A player whose system held the UDP port read first that the build
|
|
had no netplay, and a match started from the launcher closed the program with
|
|
no text. main.cpp now does the library's steps again, reports the system's
|
|
answer, and takes a launcher match back to the room with the sentence. The
|
|
sentences are tested in test_netplay_exit_reason.c; a refused bind needs a
|
|
held port and the launcher path needs a room, so that main.cpp probes, prints,
|
|
stops a command-line start and returns a launcher match is checked in the
|
|
source.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import re
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
MAIN = (ROOT / "runtime" / "src" / "main.cpp").read_text(encoding="utf-8")
|
|
|
|
# The old sentence, which named the build for every failure, is gone.
|
|
assert "built without recomp-net, " not in MAIN, "the start failure still blames the build first"
|
|
|
|
starts = [m.start() for m in re.finditer(r"psx_netplay_start\(&net_cfg\)", MAIN)]
|
|
assert len(starts) == 1, "main.cpp must start a netplay session in exactly one place"
|
|
after = MAIN[starts[0] : starts[0] + 900]
|
|
|
|
# A failed start asks for the sentence, stops a command-line start with code 1,
|
|
# and takes a launcher match back to the room with the sentence kept for the
|
|
# status line.
|
|
failed = after.index("if (nrc != 0) {")
|
|
block = after[failed : after.index("} else {", failed)]
|
|
assert "netplay_start_failure(nrc, net_cfg)" in block, "the failure is not explained"
|
|
assert re.search(r"if \(!g_netplay_from_lobby\)\s*return 1;", block), "a command-line start must stop with exit code 1"
|
|
ends = block.index('netplay_soft_exit("netplay_start_failed");')
|
|
assert block.index("return 1;") < ends, "a command-line start must stop before the return to the room"
|
|
assert "g_netplay_exit_reason_text = why;" in block[ends:], (
|
|
"the sentence must be kept for the launcher's status line after the soft exit"
|
|
)
|
|
assert "psx_lobby_set_last_error(g_netplay_exit_reason_text);" in MAIN, "the soft return no longer shows the reason"
|
|
|
|
# The guest is not entered after a failed launcher start: the return to the
|
|
# lobby is taken before the scheduler runs.
|
|
runs = [m.start() for m in re.finditer(r"(?m)^\s*psx_scheduler_run\(&cpu\);", MAIN)]
|
|
assert len(runs) == 1, "expected one scheduler entry"
|
|
assert re.search(
|
|
r"if \(psx_return_to_lobby_requested\(\) && g_netplay_from_lobby\)\s*goto soft_return_lobby;\s*$",
|
|
MAIN[runs[0] - 260 : runs[0]],
|
|
), "a failed launcher start would boot the game offline: no return to the lobby before the scheduler"
|
|
|
|
# The explanation: the bind is tried again for a LAN start only, the system's
|
|
# answer goes into the sentence and the log line, and the build is named from
|
|
# the build's own definition.
|
|
helper = MAIN[MAIN.index("static const char* netplay_start_failure(int nrc") :]
|
|
helper = helper[: helper.index("\n}\n") + 3]
|
|
assert re.search(r"#if defined\(PSX_HAS_RECOMP_NET\)\s*const int netplay_built = 1;\s*#else\s*const int netplay_built = 0;",
|
|
helper), "whether the build has netplay must come from the build"
|
|
assert re.search(r"\(netplay_built && nrc == -3\)\s*\? netplay_bind_probe\(cfg\.bind_hostport, cfg\.peer_hostport, tried, sizeof\(tried\),\s*"
|
|
r"&sys_error, sys_text, sizeof\(sys_text\)\)\s*: NETPLAY_BIND_NOT_TRIED;", helper), (
|
|
"the bind must be tried again for a LAN start, and only then"
|
|
)
|
|
# The sentence names the address that was bound, and gets the system's number and text.
|
|
assert re.search(r"tried\[0\] \? tried : cfg\.bind_hostport,\s*cfg\.peer_hostport, bind_probe, sys_error, sys_text,", helper), (
|
|
"the sentence must get the bound address, the probe result and the system's answer"
|
|
)
|
|
assert "netplay_start_failure_text(nrc, netplay_built," in helper, "the sentence is not built"
|
|
assert "std::fprintf(stderr" in helper and "netplay start failed (%d)" in helper and "[system: " in helper, (
|
|
"the log line must carry the sentence and the system's text"
|
|
)
|
|
|
|
# The probe binds without address reuse, on both platforms, and keeps the
|
|
# system's error number.
|
|
probe = MAIN[MAIN.index("static int netplay_bind_probe(") : MAIN.index("static const char* netplay_start_failure(int nrc")]
|
|
assert "SO_REUSEADDR" not in probe, "the probe must not ask for address reuse: a held port would go unseen"
|
|
assert "WSAGetLastError()" in probe and "errno" in probe, "the system's error number is not read on both platforms"
|
|
assert probe.count("bind(s, (const sockaddr*)&addr, sizeof(addr))") == 2, "the probe must bind on both platforms"
|
|
assert "closesocket(s);" in probe and "close(s);" in probe, "the probe must close its socket"
|
|
assert probe.count("result = NETPLAY_BIND_NO_SOCKET;") == 2, "a socket that could not be made is not a refused port"
|
|
|
|
# A sentence says a cause only when the probe found it.
|
|
# The listen address is read the way the library reads it: a name is looked up, so a held port behind a name
|
|
# is found, and no address before the colon means every address. The probe never skips the bind.
|
|
reader = MAIN[MAIN.index("static int netplay_hostport_addr(") : MAIN.index("/* Why a LAN netplay start failed")]
|
|
assert "getaddrinfo(host, nullptr, &hints, &found)" in reader, "a host name must be looked up before the bind"
|
|
assert re.search(r'if \(!host\[0\] \|\| std::strcmp\(host, "0\.0\.0\.0"\) == 0\) \{\s*out->sin_addr\.s_addr = htonl\(INADDR_ANY\);',
|
|
reader), "no address before the colon must mean every address, as in the library"
|
|
assert "NETPLAY_BIND_NOT_TRIED" not in probe, "the probe must not leave a listen address untried"
|
|
assert re.search(r"if \(netplay_hostport_addr\(bind_hostport, &addr\) != 0\)\s*return NETPLAY_BIND_BAD_ADDRESS;", probe), (
|
|
"a listen address the library refuses must be reported as such"
|
|
)
|
|
# The peer address is read only after the listen address opened, and is named only when it cannot be read.
|
|
opened = probe.index("if (result != NETPLAY_BIND_OK)")
|
|
peer = probe.index("netplay_hostport_addr(peer_hostport, &peer) != 0 || peer.sin_port == 0")
|
|
assert opened < peer < probe.index("return NETPLAY_BIND_PEER_BAD;"), "the peer address is judged before the listen address"
|
|
|
|
REASON = (ROOT / "runtime" / "src" / "netplay_exit_reason.c").read_text(encoding="utf-8")
|
|
text = REASON[REASON.index("void netplay_start_failure_text(") :]
|
|
text = text[: text.index("\n}\n")]
|
|
# Each cause the system names has its own sentence, chosen by the number; any other number gets the neutral one.
|
|
order = [
|
|
"bind_error_kind(sys_error) == BIND_ERROR_NOT_ALLOWED",
|
|
"The operating system does not allow this port.",
|
|
"bind_error_kind(sys_error) == BIND_ERROR_IN_USE",
|
|
"holds that ",
|
|
"bind_error_kind(sys_error) == BIND_ERROR_NO_SUCH_ADDR",
|
|
"address is not one of this computer's addresses.",
|
|
"start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED) {",
|
|
'Netplay could not open UDP port %.5s on %.15s %s."',
|
|
"bind_probe == NETPLAY_BIND_PEER_BAD",
|
|
"the other player's address ",
|
|
]
|
|
at = [text.index(piece) for piece in order]
|
|
assert at == sorted(at), "the start-failure sentences are not chosen in the expected order"
|
|
assert text.count("the other player's address ") == 1, "the peer address is blamed in more than one case"
|
|
assert re.search(r"case 10048: case 98: case 48:\s*return BIND_ERROR_IN_USE;", REASON), "address in use: Windows, Linux, macOS"
|
|
assert re.search(r"case 10013: case 13:\s*return BIND_ERROR_NOT_ALLOWED;", REASON), "access denied: Windows, POSIX"
|
|
assert re.search(r"case 10049: case 99: case 49:\s*return BIND_ERROR_NO_SUCH_ADDR;", REASON), (
|
|
"address not available: Windows, Linux, macOS"
|
|
)
|
|
# The online failure names no log and no setting: the code does not say which of its causes it was.
|
|
online = text[text.index("start_rc == -4") :]
|
|
online = online[: online.index("} else {")]
|
|
assert "The log" not in online and "setting" not in online.split("*/")[-1], "the online sentence says more than the code knows"
|
|
|
|
print("netplay start failure wiring test: PASS")
|