From a6aa29f2a0d4ebd3a95ee15d338ef0a5a2cb03cf Mon Sep 17 00:00:00 2001 From: Si Yuan He Date: Sun, 10 May 2026 22:51:34 -0700 Subject: [PATCH 1/3] Fix BadgeInfo response wrapping under h23iRjGN dispatch key --- .gitignore | 4 ++++ gimuserver/gme/BadgeInfo.cpp | 2 +- packet-generator | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3a4759e..1cfe52f 100644 --- a/.gitignore +++ b/.gitignore @@ -486,3 +486,7 @@ vcpkg_installed/ .vscode generated/ .cmake/ + +# Project-specific +CONTEXT.md +out/ diff --git a/gimuserver/gme/BadgeInfo.cpp b/gimuserver/gme/BadgeInfo.cpp index b72092a..2d8c691 100644 --- a/gimuserver/gme/BadgeInfo.cpp +++ b/gimuserver/gme/BadgeInfo.cpp @@ -3,7 +3,7 @@ HANDLEF(BadgeInfo) { - ::BadgeInfo resp{}; + ::BadgeInfoResp resp{}; std::string buffer{}; const auto& ec = glz::write_json(resp, buffer); if (ec) diff --git a/packet-generator b/packet-generator index 37256a2..5472598 160000 --- a/packet-generator +++ b/packet-generator @@ -1 +1 @@ -Subproject commit 37256a25049637980fcac4c7f5fafa1654b45d2e +Subproject commit 54725984f4d4505a2c6c2993ade0a8252163be1a From 941f6cbe4fbab4c539b12ecf041e75156e6bbb00 Mon Sep 17 00:00:00 2001 From: Si Yuan He Date: Mon, 11 May 2026 01:49:18 -0700 Subject: [PATCH 2/3] Add ChallengeArenaResetInfo handler for Zw3WIoWu (stops spam loop) --- gimuserver/gme/ChallengeArenaResetInfo.cpp | 47 ++++++++++++++++++++++ gimuserver/gme/GmeControllerHandlers.cpp | 1 + gimuserver/gme/Handlers.hpp | 1 + packet-generator | 2 +- 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 gimuserver/gme/ChallengeArenaResetInfo.cpp diff --git a/gimuserver/gme/ChallengeArenaResetInfo.cpp b/gimuserver/gme/ChallengeArenaResetInfo.cpp new file mode 100644 index 0000000..edbf315 --- /dev/null +++ b/gimuserver/gme/ChallengeArenaResetInfo.cpp @@ -0,0 +1,47 @@ +#include "App.hpp" +#include "Handlers.hpp" + +// ChallengeArenaResetInfo — "Zw3WIoWu" / key "KlwYMGF1" +// +// Spam loop (before this handler was registered): +// 1. How it starts: +// Parent scene calls createConnectSceneToHome() (0xE9B6A0), which creates a +// ChallengeArenaResetInfoConnectScene. On activation, initConnect() (0xE9B7A8) +// calls needServerRefresh() (0xE7FA00). Since all internal fields are zero, +// this always returns true, triggering accessPhpChallengeArena() (0x16082E0). +// +// 2. Why no handler caused infinite retry: +// The server hit default: in getHandler(), returning GmeError{cmd=Close}. +// checkConnectResult() (0xE9B828) saw the error and returned false. +// updateEvent() never reached changeScene(), so the connect scene never +// transitioned away. The game loop kept calling initConnect() on this +// still-alive scene, producing an endless request stream at ~50ms intervals. +// +// 3. Why changeScene breaks the loop: +// This is a transient bridge scene. Its only job is to fetch the response, +// then hand off to a permanent arena scene via changeScene() and self-destruct. +// When the server returned Close, the transition never happened, so the scene +// stayed alive forever. error -4000 in noticeOK() (0xE9B8A8) is the escape +// hatch that force-transitions without checking response validity, but our +// Close error did not map to -4000. +// +// 4. Future retriggers: +// Once a response populates the timestamp fields, needServerRefresh() will +// eventually return true again after enough time passes. At that point the +// parent scene recreates the bridge scene, which sends a single request. +// This is normal behavior, not a spam loop. + +HANDLEF(ChallengeArenaResetInfo) +{ + // Currently returns empty timestamps — client retries ~every 1s until real values are provided. + // TODO: populate with real timestamps + ::ChallengeArenaResetInfoResp resp{}; + + std::string buffer{}; + const auto& ec = glz::write_json(resp, buffer); + if (ec) { + co_return HandleResult::error("Serialization error", glz::format_error(ec, buffer)); + } + + co_return HandleResult::success(buffer); +} \ No newline at end of file diff --git a/gimuserver/gme/GmeControllerHandlers.cpp b/gimuserver/gme/GmeControllerHandlers.cpp index 27f3458..de5661b 100644 --- a/gimuserver/gme/GmeControllerHandlers.cpp +++ b/gimuserver/gme/GmeControllerHandlers.cpp @@ -65,6 +65,7 @@ static GmeHandler getHandler(std::string_view cmd) REGISTER("MfZyu1q9", Initialize, "EmcshnQoDr20TZz1"); + REGISTER("Zw3WIoWu", ChallengeArenaResetInfo, "KlwYMGF1"); REGISTER("nJ3A7qFp", BadgeInfo, "bGxX67KB"); REGISTER("uYF93Mhc", ControlCenterEnter, "d0k6LGUu"); REGISTER("m2Ve9PkJ", DeckEdit, "d7UuQsq8"); diff --git a/gimuserver/gme/Handlers.hpp b/gimuserver/gme/Handlers.hpp index c3bce4d..debda14 100644 --- a/gimuserver/gme/Handlers.hpp +++ b/gimuserver/gme/Handlers.hpp @@ -81,6 +81,7 @@ namespace GmeHandlers { HANDLE(Initialize); HANDLE(BadgeInfo); + HANDLE(ChallengeArenaResetInfo); HANDLE(ControlCenterEnter); HANDLE(DeckEdit); HANDLE(FriendGet); diff --git a/packet-generator b/packet-generator index 5472598..d2893ba 160000 --- a/packet-generator +++ b/packet-generator @@ -1 +1 @@ -Subproject commit 54725984f4d4505a2c6c2993ade0a8252163be1a +Subproject commit d2893ba3c3129cd3cf876ee24e8701f853d6ca9c From f9d80abccefd2938827145cf26dadf7da73e7d7f Mon Sep 17 00:00:00 2001 From: Si Yuan He Date: Tue, 12 May 2026 00:50:03 -0700 Subject: [PATCH 3/3] ChallengeArenaResetInfo: populate daily_cooling_end with +1h to block client retry spam --- gimuserver/gme/ChallengeArenaResetInfo.cpp | 60 +++++++++++----------- packet-generator | 2 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/gimuserver/gme/ChallengeArenaResetInfo.cpp b/gimuserver/gme/ChallengeArenaResetInfo.cpp index edbf315..d553965 100644 --- a/gimuserver/gme/ChallengeArenaResetInfo.cpp +++ b/gimuserver/gme/ChallengeArenaResetInfo.cpp @@ -1,46 +1,48 @@ #include "App.hpp" #include "Handlers.hpp" -// ChallengeArenaResetInfo — "Zw3WIoWu" / key "KlwYMGF1" +// ChallengeArenaResetInfo // -// Spam loop (before this handler was registered): -// 1. How it starts: -// Parent scene calls createConnectSceneToHome() (0xE9B6A0), which creates a -// ChallengeArenaResetInfoConnectScene. On activation, initConnect() (0xE9B7A8) -// calls needServerRefresh() (0xE7FA00). Since all internal fields are zero, -// this always returns true, triggering accessPhpChallengeArena() (0x16082E0). +// By default the server returns GmeError{cmd=Close}, which the +// client interpreted as a connection failure. The connect scene never +// transitioned away, so the game loop re-fired the request every frame. // -// 2. Why no handler caused infinite retry: -// The server hit default: in getHandler(), returning GmeError{cmd=Close}. -// checkConnectResult() (0xE9B828) saw the error and returned false. -// updateEvent() never reached changeScene(), so the connect scene never -// transitioned away. The game loop kept calling initConnect() on this -// still-alive scene, producing an endless request stream at ~50ms intervals. +// With this handler returning empty timestamps (instead of an error), the client parses +// a valid response and stores server_time + a local snapshot. needServerRefresh() computes: // -// 3. Why changeScene breaks the loop: -// This is a transient bridge scene. Its only job is to fetch the response, -// then hand off to a permanent arena scene via changeScene() and self-destruct. -// When the server returned Close, the transition never happened, so the scene -// stayed alive forever. error -4000 in noticeOK() (0xE9B8A8) is the escape -// hatch that force-transitions without checking response validity, but our -// Close error did not map to -4000. +// server_time + now() - local_snapshot > daily_cooling_end // -// 4. Future retriggers: -// Once a response populates the timestamp fields, needServerRefresh() will -// eventually return true again after enough time passes. At that point the -// parent scene recreates the bridge scene, which sends a single request. -// This is normal behavior, not a spam loop. - +// Assume fields start at zero. After parsing the empty response, server_time stays 0, +// local_snapshot is set to the current time (via time()), and daily_cooling_end stays 0. +// So the check becomes: 0 + now() - local_snapshot > 0. +// +// Since local_snapshot == the time() value from just after parsing, this is false until +// now() ticks forward to the next second. +// +// To stop the client from retrying every second, we'll set daily_cooling_end to an hour in the +// future. +// +// TODO: figure out the actual intended cooldown time and use that instead. HANDLEF(ChallengeArenaResetInfo) { - // Currently returns empty timestamps — client retries ~every 1s until real values are provided. - // TODO: populate with real timestamps + using namespace std::chrono; + using namespace std::chrono_literals; + ::ChallengeArenaResetInfoResp resp{}; + // I've tested this with 5 seconds as well, which causes the client to retry every 6 seconds. + // This makes sense since its an explicit greater-than check. + // Using an hour here as a placeholder to prevent the client from constantly retrying and polluting + // our logs (and our disks :D). + resp.reset_info.server_time = floor(system_clock::now()); + resp.reset_info.daily_cooling_end = resp.reset_info.server_time + 1h; + std::string buffer{}; const auto& ec = glz::write_json(resp, buffer); if (ec) { - co_return HandleResult::error("Serialization error", glz::format_error(ec, buffer)); + const auto& glze = glz::format_error(ec, buffer); + LOG_DEBUG << "Gme ChallengeArenaResetInfo Error during JSON writing: " << glze; + co_return HandleResult::error("Serialization error", glze); } co_return HandleResult::success(buffer); diff --git a/packet-generator b/packet-generator index d2893ba..61fedcc 160000 --- a/packet-generator +++ b/packet-generator @@ -1 +1 @@ -Subproject commit d2893ba3c3129cd3cf876ee24e8701f853d6ca9c +Subproject commit 61fedcc3275d4abc209ba997f980d51010c87453