Files
NX2navi/tools/check_platform.py
chrg6 7e07e13bac Add co-op, off by default: rooms through a relay, progress sync between the players' own saves, and a Co-op tab.
Each player keeps their own game and save. One player makes a room, the others join it with its secret, and story flags and, by the room's rules, items, songs, quest items, small keys (a shared pool), rupees and ammunition reach every save. The layer is a host thread with one UDP socket to a relay the players run themselves (tools/coop_relay.py, Python's standard library only); the messages between the players are sealed with a key from the room's secret (XChaCha20-Poly1305, Monocypher 4.0.2 vendored with its licence and a row in THIRD_PARTY_NOTICES.md), every change is signed by the player who made it, a player accepts a room by its members' keys and the key of its rules, the save is copied to coop-backup before anything is applied, and values are set through the game's own routines between frames. The Co-op tab makes, joins, accepts, removes and leaves, and the menu takes typed text for the name, the relay and the secret. This is the contributor's pull request #518 on this line.

Nothing reaches the network while co-op is off: the socket is opened, and the relay's name looked up, only after co-op is on, a relay is set and a room is made or joined. There is no default relay.

Left out of the pull request: the other players drawn in the world (the "puppets") with the copied engine they need. They were unfinished, added two patch sites to the game's per-frame update that could not be put through the differential tests here, and their file helpers clashed with the ones this line already has. So no position is sent: the POS message's codec stays, nothing sends it.

Changed against the pull request:
- The menu's test hook drives the Co-op tab only when it comes from the environment. From nx2navi.ini it could turn co-op on, join a room and accept its offer.
- The log's label for a room is hashed from the secret instead of being its first three characters.
- A game that has not saved yet has no save to copy, and takes nothing from the room until it has saved once; that used to count as a copy made.
- A PING's token no longer carries the PC's uptime.
- On Windows the maker's key file and its folder get an access list with the player's account alone.
- The tests that link the platform layer get the socket library, and the save test a stand-in for the title screen's hook.

Checked on Windows, where the pull request had never been compiled: the runtime builds and links; --probe-coop passes (sockets on 127.0.0.1, the codec against its byte vectors, a fuzz of every parser, two sessions through a hostile mock relay, a relay restart, the sync engine); tests/coop_relay_test.py passes its 61 tests, with the probe through the real relay on 127.0.0.1 at 0 and 30 % loss; tests/coop_page_check.py passes and its deliberate fault fails 48 checks. Not checked: two games in one room (one game runs at a time here), any real network, the tab in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 00:11:41 -05:00

96 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""Fails when runtime code outside runtime/platform/ uses the Win32 API directly.
Runtime code goes through runtime/platform/platform.h. The Win32 back ends of the window system are Win32 by design and
are not scanned: files named *_win.c / *_win.cpp (runtime/ and runtime/ui/), whose SDL counterparts (*_sdl.*) serve
other hosts. Any other file that still uses Win32 goes into ALLOWED with the reason; a file in ALLOWED that no longer
matches is noted, so the entry gets removed.
Usage: python tools/check_platform.py [--root DIR] (DIR holds runtime/; default: the repository root)
"""
import argparse
import re
import sys
from pathlib import Path
# Per-file allowlist of shared files that still use Win32, with the reason. Empty: every shared file is portable.
ALLOWED = {}
INCLUDE = re.compile(r'^\s*#\s*include\s*[<"](windows|winsock2|ws2tcpip|synchapi|memoryapi|processthreadsapi|'
r'fileapi|handleapi|libloaderapi|winbase|winnt)\.h[>"]', re.M)
CALLS = [
"CreateFile[AW]?", "ReadFile", "WriteFile", "CloseHandle", "DeleteFile[AW]?", "MoveFileEx[AW]?", "CreateDirectory[AW]?",
"FindFirstFile[AW]?", "GetFileAttributes[AW]?", "GetFileSizeEx", "SetFilePointerEx", "SetEndOfFile",
"CreatePipe", "CreateProcess[AW]?", "TerminateProcess", "GetModuleFileName[AW]?", "GetEnvironmentVariable[AW]?",
"VirtualAlloc(Ex)?", "VirtualFree", "VirtualProtect", "VirtualQuery", "MapViewOfFile[A-Za-z0-9]*", "CreateFileMapping[AW]?",
"GetWriteWatch", "ResetWriteWatch", "AddVectoredExceptionHandler", "RemoveVectoredExceptionHandler",
"CreateThread", "_beginthread(ex)?", "GetCurrentThread(Id)?", "SetThreadAffinityMask", "SetThreadDescription",
"SetThreadPriority", "WaitForSingleObject(Ex)?", "WaitForMultipleObjects", "CreateEvent[AW]?", "CreateSemaphore[AW]?",
"CreateMutex[AW]?", "CreateWaitableTimer(Ex)?[AW]?", "SetWaitableTimer(Ex)?", "Sleep(Ex)?", "SwitchToThread",
"QueryPerformanceCounter", "QueryPerformanceFrequency", "GetTickCount(64)?", "GetSystemTimePreciseAsFileTime",
"GetSystemInfo", "GetLogicalProcessorInformation(Ex)?", "SRWLOCK", "CONDITION_VARIABLE", "CRITICAL_SECTION",
"InitializeCriticalSection[A-Za-z]*", "EnterCriticalSection", "LeaveCriticalSection",
"(Acquire|Release)SRWLock(Exclusive|Shared)", "WakeAllConditionVariable", "WakeConditionVariable",
"SleepConditionVariableSRW", "InitOnceExecuteOnce", "INIT_ONCE", "Interlocked[A-Za-z0-9]*", "TlsAlloc", "FlsAlloc",
"LoadLibrary[AW]?", "GetProcAddress", "FreeLibrary", "HANDLE", "DWORD", "LPVOID", "WINAPI",
"SuspendThread", "ResumeThread", "GetThreadContext", "SetThreadContext", "OpenThread", "CreateFiber(Ex)?",
"ConvertThreadToFiber(Ex)?", "ConvertFiberToThread", "SwitchToFiber", "DeleteFiber", "BCrypt[A-Za-z]*",
"_aligned_malloc", "_aligned_free", "_stricmp", "_strnicmp", "_strdup", "_wfopen", "_fseeki64", "_ftelli64",
"localtime_s", "strtok_s", "GetModuleHandle(Ex)?[AW]?", "ExitProcess", "SetEnvironmentVariable[AW]?", "_putenv(_s)?",
"GetLastError", "MultiByteToWideChar", "WideCharToMultiByte", "HWND", "LARGE_INTEGER", "FILETIME",
"MAX_PATH", "GetActiveProcessorCount", "timeBeginPeriod",
"WSAStartup", "WSAPoll", "WSAIoctl", "WSAGetLastError", "closesocket", "ioctlsocket", "SOCKET",
"sockaddr(_in6?|_storage)?",
]
CALL_RE = re.compile(r"\b(" + "|".join(CALLS) + r")\b")
def strip(text):
"""Blank comments and string literals, keeping line numbers."""
def blank(m):
return re.sub(r"[^\n]", " ", m.group(0))
text = re.sub(r"/\*.*?\*/", blank, text, flags=re.S)
text = re.sub(r"//[^\n]*", blank, text)
return re.sub(r'"(?:\\.|[^"\\\n])*"', blank, text)
def scan(path):
raw = path.read_text(encoding="utf-8", errors="replace")
hits = [("include", m.group(0).strip()) for m in INCLUDE.finditer(raw)]
code = strip(raw)
hits += [("call", m.group(1)) for m in CALL_RE.finditer(code)]
return hits
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--root", default=str(Path(__file__).resolve().parent.parent))
ap.add_argument("--runtime", default=None, help="runtime dir (default ROOT/runtime)")
a = ap.parse_args()
rt = Path(a.runtime) if a.runtime else Path(a.root) / "runtime"
bad, remaining = [], {}
files = [p for pat in ("*.c", "*.h", "*.cpp", "ui/*.cpp", "ui/*.h") for p in rt.glob(pat)]
for p in sorted(files):
if p.stem.endswith("_win"):
continue
hits = scan(p)
if p.name in ALLOWED:
remaining[p.name] = len(hits)
elif hits:
bad.append((p.name, hits))
for name, hits in bad:
kinds = sorted({h[1] for h in hits})
print(f"FAIL runtime/{name}: {len(hits)} Win32 uses outside runtime/platform/ ({', '.join(kinds[:8])})")
stale = [n for n, c in remaining.items() if c == 0]
for n in stale:
print(f"note runtime/{n}: allowlisted but no Win32 use left; remove it from ALLOWED")
print("remaining Win32 uses per allowlisted file:")
for n, c in sorted(remaining.items(), key=lambda kv: (-kv[1], kv[0])):
print(f" {c:5d} runtime/{n} ({ALLOWED[n]})")
print(f"total {sum(remaining.values())} in {sum(1 for c in remaining.values() if c)} files")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())