From 16a03f16e84f49d45945ff11baa49e2d8b444f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 18 Oct 2026 09:15:51 +0200 Subject: [PATCH] Adhoc: Restore the inited flags from savestates Loading a savestate kept the current session's netAdhocInited, netAdhocctlInited and matching-inited flags rather than the saved ones. Loaded into a fresh boot, a state saved with adhocctl initialized (but not connected) then failed every adhocctl call with NOT_INITIALIZED. MGS Peace Walker shuts ad hoc down before a cutscene and retried sceNetAdhocctlDisconnect forever, leaving the screen black. Take the flags from the state, and bring the host side in line: start the friendFinder and log in again when the state has adhocctl up and we don't, stop it in the opposite case. Also save the product code, which that login sends. Also add a game.state.save debugger command, used to reproduce this. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/Debugger/WebSocket/GameSubscriber.cpp | 29 +++++ Core/Debugger/WebSocket/GameSubscriber.h | 1 + Core/HLE/sceNetAdhoc.cpp | 132 ++++++++++++++------- Core/HLE/sceNetAdhocMatching.cpp | 10 -- Core/HLE/sceNetAdhocMatching.h | 2 - docs/WebSocketDebugger.md | 2 +- 6 files changed, 117 insertions(+), 59 deletions(-) diff --git a/Core/Debugger/WebSocket/GameSubscriber.cpp b/Core/Debugger/WebSocket/GameSubscriber.cpp index 9cebba3038..4c1c223a7d 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.cpp +++ b/Core/Debugger/WebSocket/GameSubscriber.cpp @@ -60,6 +60,7 @@ DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map) { map["game.reset"] = &WebSocketGameReset; map["game.status"] = &WebSocketGameStatus; map["game.state.load"] = &WebSocketGameStateLoad; + map["game.state.save"] = &WebSocketGameStateSave; map["game.speed.get"] = &WebSocketGameSpeedGet; map["game.speed.set"] = &WebSocketGameSpeedSet; map["version"] = &WebSocketVersion; @@ -156,6 +157,34 @@ void WebSocketGameStateLoad(DebuggerRequest &req) { req.Respond(); } +// Save a savestate file (game.state.save) +// +// Like game.state.load, the save is queued and happens at the end of the next frame the CPU runs. +// +// Parameters: +// - path: string, path to write the .ppst file to. +// +// Response (same event name) with no extra data, once queued. +void WebSocketGameStateSave(DebuggerRequest &req) { + std::string path; + if (!req.ParamString("path", &path)) + return; + + bool running = false; + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + }); + if (!running) + return req.Fail("Game not running"); + + SaveState::Save(Path(path), -1, [](SaveState::Status status, std::string_view message, std::string_view) { + if (status == SaveState::Status::FAILURE) { + ERROR_LOG(Log::System, "game.state.save: %.*s", STR_VIEW(message)); + } + }); + req.Respond(); +} + // Percentages are relative to 60 FPS, matching how the in-app settings present the alternative // speeds (see GameSettingsScreen.cpp) - so a client and the UI agree on what "200%" means. static int PercentToFps(int percent) { diff --git a/Core/Debugger/WebSocket/GameSubscriber.h b/Core/Debugger/WebSocket/GameSubscriber.h index 1e340c5fdf..220ba2ecf2 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.h +++ b/Core/Debugger/WebSocket/GameSubscriber.h @@ -24,6 +24,7 @@ DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map); void WebSocketGameReset(DebuggerRequest &req); void WebSocketGameStatus(DebuggerRequest &req); void WebSocketGameStateLoad(DebuggerRequest &req); +void WebSocketGameStateSave(DebuggerRequest &req); void WebSocketGameSpeedGet(DebuggerRequest &req); void WebSocketGameSpeedSet(DebuggerRequest &req); void WebSocketVersion(DebuggerRequest &req); diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index aeb8202d42..0e21b8e807 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -333,8 +333,6 @@ void discardMatchingEvents(); void DoNetAdhocMatchingInited(PointerWrap &p); void DoNetAdhocMatchingThreads(PointerWrap &p); void ZeroNetAdhocMatchingThreads(); -void SaveNetAdhocMatchingInited(); -void RestoreNetAdhocMatchingInited(); bool __NetAdhocConnected() { return netAdhocInited && netAdhocctlInited && (adhocctlState == ADHOCCTL_STATE_CONNECTED || adhocctlState == ADHOCCTL_STATE_GAMEMODE); @@ -1803,14 +1801,72 @@ int WaitBlockingAdhocSocket(u64 threadSocketId, int type, int pspSocketId, void* return SCE_NET_ADHOC_ERROR_TIMEOUT; } +static void SetupAdhocRelayMode() { + switch ((AdhocServerRelayMode)g_Config.iAdhocServerRelayMode) { + case AdhocServerRelayMode::Auto: + serverHasRelay = false; + // Fetch data mode from server list + if (AdhocGetServerDataMode(g_Config.sProAdhocServer) == AdhocDataMode::AemuPostoffice) { + serverHasRelay = true; + trackingRelayFailure = false; + relayDisabled = false; + relayFirstConnect = true; + } + break; + case AdhocServerRelayMode::AlwaysOn: + serverHasRelay = true; + trackingRelayFailure = false; + relayDisabled = false; + relayFirstConnect = true; + break; + default: + serverHasRelay = false; + break; + } + + if (serverHasRelay) { + aemu_post_office_init(); + } +} + +static void StartFriendFinder() { + // TODO: Merging friendFinder (real) thread to AdhocThread (fake) thread on PSP side + if (!friendFinderRunning) { + // Set before the thread starts, not by it: shutting down before it got going used to clear + // the flag first, and then the thread set it again and never stopped, hanging the join. + if (friendFinderThread.joinable()) { + friendFinderThread.join(); + } + friendFinderRunning = true; + friendFinderThread = std::thread(friendFinder); + } +} + +// Stops the friendFinder thread and drops the connection to the Adhoc Server, leaving the PSP side +// (handlers, AdhocThread) alone. +static void StopFriendFinder() { + friendFinderRunning = false; + if (friendFinderThread.joinable()) { + friendFinderThread.join(); + } + + int32_t peercount = 0; + freeFriendsRecursive(friends, &peercount); + friends = NULL; + + g_adhocServerConnected = false; + shutdown((int)metasocket, SD_BOTH); + closesocket((int)metasocket); + metasocket = (int)INVALID_SOCKET; +} + void __NetAdhocDoState(PointerWrap &p) { - auto s = p.Section("sceNetAdhoc", 1, 9); + auto s = p.Section("sceNetAdhoc", 1, 10); if (!s) return; auto cur_netAdhocInited = netAdhocInited; auto cur_netAdhocctlInited = netAdhocctlInited; - SaveNetAdhocMatchingInited(); Do(p, netAdhocInited); Do(p, netAdhocctlInited); @@ -1894,6 +1950,10 @@ void __NetAdhocDoState(PointerWrap &p) { } else if (p.mode == p.MODE_READ) { adhocctlRequests.clear(); } + if (s >= 10) { + // The login after a load sends it, so a state loaded into a fresh boot needs it. + p.DoVoid(&product_code, (int)sizeof(product_code)); + } if (p.mode == p.MODE_READ) { // Discard leftover events @@ -1906,13 +1966,26 @@ void __NetAdhocDoState(PointerWrap &p) { deleteAllAdhocSockets(); discardMatchingEvents(); - // Let's not change "Inited" value when Loading SaveState to prevent memory & port leaks - RestoreNetAdhocMatchingInited(); - - netAdhocctlInited = cur_netAdhocctlInited; - netAdhocInited = cur_netAdhocInited; - - isAdhocctlNeedLogin = false; + // The game remembers which libraries it initialized, so the "Inited" flags have to come + // from the state too. Bring the host side (relay, friendFinder, server login) in line. + // Keeping the current flags instead left a state saved with adhocctl inited, loaded into a + // fresh boot, failing every call with NOT_INITIALIZED; MGS Peace Walker retried + // sceNetAdhocctlDisconnect forever when starting a cutscene. + if (netAdhocInited && !cur_netAdhocInited) { + SetupAdhocRelayMode(); + } + if (netAdhocctlInited && !cur_netAdhocctlInited) { + isAdhocctlNeedLogin = true; + StartFriendFinder(); + } else { + if (!netAdhocctlInited && cur_netAdhocctlInited) { + // The thread sets adhocctlState on its way out, so keep the loaded one. + const int loadedState = adhocctlState; + StopFriendFinder(); + adhocctlState = loadedState; + } + isAdhocctlNeedLogin = false; + } } } @@ -1963,31 +2036,7 @@ u32 sceNetAdhocInit() { // Since we are deleting GameMode Master here, we should probably need to make sure GameMode resources all cleared too. deleteAllGMB(); - switch ((AdhocServerRelayMode)g_Config.iAdhocServerRelayMode) { - case AdhocServerRelayMode::Auto: - serverHasRelay = false; - // Fetch data mode from server list - if (AdhocGetServerDataMode(g_Config.sProAdhocServer) == AdhocDataMode::AemuPostoffice) { - serverHasRelay = true; - trackingRelayFailure = false; - relayDisabled = false; - relayFirstConnect = true; - } - break; - case AdhocServerRelayMode::AlwaysOn: - serverHasRelay = true; - trackingRelayFailure = false; - relayDisabled = false; - relayFirstConnect = true; - break; - default: - serverHasRelay = false; - break; - } - - if (serverHasRelay) { - aemu_post_office_init(); - } + SetupAdhocRelayMode(); auto n = GetI18NCategory(I18NCat::NETWORKING); if (g_Config.bEnableWlan) { @@ -2036,16 +2085,7 @@ int sceNetAdhocctlInit(int stackSize, int prio, u32 productAddr) { __KernelStartThread(threadAdhocID, 0, 0); } - // TODO: Merging friendFinder (real) thread to AdhocThread (fake) thread on PSP side - if (!friendFinderRunning) { - // Set before the thread starts, not by it: shutting down before it got going used to clear - // the flag first, and then the thread set it again and never stopped, hanging the join. - if (friendFinderThread.joinable()) { - friendFinderThread.join(); - } - friendFinderRunning = true; - friendFinderThread = std::thread(friendFinder); - } + StartFriendFinder(); // Need to make sure to be connected to Adhoc Server (indicated by networkInited) before returning to prevent GTA VCS failed to create/join a group and unable to see any game room int us = adhocDefaultDelay; diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index e1cee8dfd2..75396d848b 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -46,8 +46,6 @@ std::deque matchingEvents; bool netAdhocMatchingInited; int adhocMatchingEventDelay = 30000; //30000 -static bool savedNetAdhocMatchingInited; // the storage to use during the savestating routine in sceNetAdhoc.cpp - void DoNetAdhocMatchingInited(PointerWrap &p) { Do(p, netAdhocMatchingInited); } @@ -62,14 +60,6 @@ void ZeroNetAdhocMatchingThreads() { } } -void SaveNetAdhocMatchingInited() { - savedNetAdhocMatchingInited = netAdhocMatchingInited; -} - -void RestoreNetAdhocMatchingInited() { - netAdhocMatchingInited = savedNetAdhocMatchingInited; -} - int netAdhocMatchingStarted = 0; diff --git a/Core/HLE/sceNetAdhocMatching.h b/Core/HLE/sceNetAdhocMatching.h index a262a66995..156e8b57fc 100644 --- a/Core/HLE/sceNetAdhocMatching.h +++ b/Core/HLE/sceNetAdhocMatching.h @@ -36,8 +36,6 @@ int NetAdhocMatching_Term(); void DoNetAdhocMatchingInited(PointerWrap &p); void DoNetAdhocMatchingThreads(PointerWrap &p); void ZeroNetAdhocMatchingThreads(); -void SaveNetAdhocMatchingInited(); -void RestoreNetAdhocMatchingInited(); void Register_sceNetAdhocMatching(); void __NetAdhocMatchingInit(); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index c098208aa8..4a25bd0249 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -270,7 +270,7 @@ file - this is just an index. | Category | Events | File | |---|---|---| -| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file, applied at the end of the next frame the CPU runs), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `version` | `GameSubscriber.cpp` | +| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file, applied at the end of the next frame the CPU runs), `game.state.save` (the same for saving one), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `version` | `GameSubscriber.cpp` | | CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status` (reports `ticks` plus `us`, emulated microseconds, and `clockHz` - use `us` to line up with wall-clock timings, since games change the clock frequency and the ticks-per-second ratio isn't fixed), `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` | | Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop), `cpu.nextHLE` | `SteppingSubscriber.cpp` | | Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.regBreakpoint.add/update/remove/list` (break when a register is written to, by any instruction anywhere - currently GPRs only; interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` |