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` |