From 7689de8caf2bd5e94f2885a3fcaa26499fa4f838 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Thu, 16 Apr 2026 10:10:14 -0400 Subject: [PATCH 1/6] Automated testing for Geo IPs for certain lobbies, which launch into specific Single region based lobbies, Updated Ping display in real time across users and display it inline --- relaytestapp/src/app.cpp | 420 ++++++++++++++++++++++++++++++++-- relaytestapp/src/app.h | 3 + relaytestapp/src/game.cpp | 30 ++- relaytestapp/src/globals.cpp | 5 + relaytestapp/src/globals.h | 56 ++++- relaytestapp/src/lobby.cpp | 155 +++++++++++-- relaytestapp/src/mainMenu.cpp | 140 +++++++++++- 7 files changed, 746 insertions(+), 63 deletions(-) diff --git a/relaytestapp/src/app.cpp b/relaytestapp/src/app.cpp index 7a566d9..3aa7054 100644 --- a/relaytestapp/src/app.cpp +++ b/relaytestapp/src/app.cpp @@ -68,6 +68,10 @@ static void onRelayConnected(); static bool isDisconnecting = false; +// Tracks the EdgeGap beacon region chosen for the current lobby attempt. +// Set when we pick the best un-tested region; recorded to geoTestedRegions on ROOM_READY. +static std::string s_geoTestRegion; + // brainCloud RTT Connection callbacks class RTTConnectCallback final : public BrainCloud::IRTTConnectCallback { @@ -132,6 +136,7 @@ class RelayCallback final : public BrainCloud::IRelayCallback public: void relayCallback(int netId, const uint8_t *bytes, int size) override { + if (isDisconnecting) return; Json::Value json; Json::Reader reader; std::string str((const char *)bytes, size); @@ -146,6 +151,7 @@ class RelaySystemCallback final : public BrainCloud::IRelaySystemCallback public: void relaySystemCallback(const std::string &jsonResponse) override { + if (isDisconnecting) return; Json::Value json; Json::Reader reader; reader.parse(jsonResponse, json); @@ -300,31 +306,176 @@ void onLoggedIn() })); } -// RTT connected. Go to main menu screen +// Shared parameters used by both findOrCreateLobby and findOrCreateLobbyWithPingData +static const char* LOBBY_ALGO = "{\"strategy\":\"ranged-absolute\",\"alignment\":\"center\",\"ranges\":[1000]}"; +static const char* LOBBY_FILTER = "{}"; +static const char* LOBBY_SETTINGS = "{}"; + +// Build the extra JSON for lobby join/ready calls. +// Always includes colorIndex; includes per-region ping results when available. +static std::string buildExtraJson() +{ + Json::Value extra; + extra["colorIndex"] = state.user.colorIndex; + if (!state.pingData.empty()) + { + Json::Value pings; + for (const auto& kv : state.pingData) + pings[kv.first] = kv.second; + extra["pings"] = pings; + } + Json::FastWriter writer; + auto s = writer.write(extra); + while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) + s.pop_back(); + return s; +} + +// Standard findOrCreateLobby (no ping-aware matchmaking) +static void doFindOrCreateLobby(const std::string &lobbyType) +{ + pBCWrapper->getLobbyService()->findOrCreateLobby( + lobbyType, + 0, // rating + 1, // max steps + LOBBY_ALGO, + LOBBY_FILTER, + {}, // other users + LOBBY_SETTINGS, + false, // ready + buildExtraJson(), + settings.teamCode, + new BCCallback( + [](const Json::Value &) { /* success comes via RTT onLobbyEvent */ }, + [](const std::string &status_message) + { + errorAndReturnToMenu("Failed to find lobby:\n" + status_message); + })); +} + +// findOrCreateLobby using collected ping data for region-aware matchmaking +static void doFindOrCreateLobbyWithPingData(const std::string &lobbyType) +{ + pBCWrapper->getLobbyService()->findOrCreateLobbyWithPingData( + lobbyType, + 0, // rating + 1, // max steps + LOBBY_ALGO, + LOBBY_FILTER, + {}, // other users + LOBBY_SETTINGS, + false, // ready + buildExtraJson(), + settings.teamCode, + new BCCallback( + [](const Json::Value &) { /* success comes via RTT onLobbyEvent */ }, + [](const std::string &status_message) + { + errorAndReturnToMenu("Failed to find lobby:\n" + status_message); + })); +} + +// RTT connected — always collect ping data so all members share their latencies +// in the lobby/game UI. usePingData only controls whether ping-aware matchmaking is used. void onRTTConnected() { state.user.cxId = pBCWrapper->getRTTService()->getRTTConnectionId(); - // Find lobby - pBCWrapper->getLobbyService()->findOrCreateLobby( - settings.lobbyType, // lobby type - 0, // rating - 1, // max steps - "{\"strategy\":\"ranged-absolute\",\"alignment\":\"center\",\"ranges\":[1000]}", // algorithm - "{}", // filters - {}, // other users - "{}", // settings - false, // ready - "{\"colorIndex\":" + std::to_string(state.user.colorIndex) + "}", // extra - settings.teamCode, // team code - new BCCallback( // callback - [](const Json::Value &result) // Success + // If RTT auto-reconnects while we are already in the lobby or game (e.g. after + // a brief network hiccup), do not restart the region-ping + findOrCreateLobby + // flow — the player is already placed and we just need the RTT channel back. + if (state.screenState != ScreenState::JoiningLobby) + return; + + loading_status = "Getting regions..."; + pBCWrapper->getLobbyService()->getRegionsForLobbies( + {settings.lobbyType}, + new BCCallback( + [](const Json::Value &result) { - // Success of lobby found will be in the event onLobbyEvent + if (isDisconnecting) return; + + // Collect region names so app_update() can show per-region progress + state.expectedPingRegions.clear(); + const auto ®ionPingData = result["data"]["regionPingData"]; + if (!regionPingData.isNull()) + { + for (const auto ®ion : regionPingData.getMemberNames()) + state.expectedPingRegions.push_back(region); + std::sort(state.expectedPingRegions.begin(), state.expectedPingRegions.end()); + } + + pBCWrapper->getLobbyService()->pingRegions( + new BCCallback( + [](const Json::Value &) + { + if (isDisconnecting) return; + state.pingData = pBCWrapper->getLobbyService()->getPingData(); + state.expectedPingRegions.clear(); // stop per-frame polling + + // For EdgeGap, route to the best region-specific lobby type. + // EdgeGap geo-test: cycle through only the regions that have a + // defined specific lobby type. Unmapped ping regions are ignored + // entirely — they have no EdgeGap lobby to route to. + std::string lobbyType = settings.lobbyType; + s_geoTestRegion.clear(); + if (isEdgeGapLobby(settings.lobbyType) && !state.pingData.empty()) + { + std::string bestRegion; + int bestPing = INT_MAX; + // Pick fastest un-tested region that has a defined EdgeGap lobby + for (const auto &kv : state.pingData) + { + if (edgeGapRegionToLobbyType(kv.first).empty()) continue; + const auto &tested = state.geoTestedRegions; + bool alreadyTested = std::find(tested.begin(), tested.end(), kv.first) != tested.end(); + if (!alreadyTested && kv.second < bestPing) + { + bestPing = kv.second; + bestRegion = kv.first; + } + } + // All defined lobbies tested — wrap around to global fastest mapped region + if (bestRegion.empty()) + { + bestPing = INT_MAX; + for (const auto &kv : state.pingData) + { + if (edgeGapRegionToLobbyType(kv.first).empty()) continue; + if (kv.second < bestPing) { bestPing = kv.second; bestRegion = kv.first; } + } + printf("[GeoTest] All EdgeGap lobbies tested — wrapping to %s (%dms)\n", + bestRegion.c_str(), bestPing); + } + else + { + printf("[GeoTest] Routing to %s (%dms), %d/%d tested\n", + bestRegion.c_str(), bestPing, + (int)state.geoTestedRegions.size(), + (int)state.pingData.size()); + } + s_geoTestRegion = bestRegion; + lobbyType = edgeGapRegionToLobbyType(bestRegion); + } + + if (settings.usePingData) + doFindOrCreateLobbyWithPingData(lobbyType); + else + doFindOrCreateLobby(lobbyType); + }, + [](const std::string &) + { + // Ping failed — fall back gracefully to standard lobby creation + if (isDisconnecting) return; + state.expectedPingRegions.clear(); + doFindOrCreateLobby(settings.lobbyType); + })); }, - [](const std::string &status_message) // Error + [](const std::string &) { - errorAndReturnToMenu("Failed to find lobby:\n" + status_message); + // Region lookup failed — proceed without ping data + if (isDisconnecting) return; + doFindOrCreateLobby(settings.lobbyType); })); } @@ -341,8 +492,13 @@ static void errorAndReturnToMenu(const std::string &message) // Reset state but keep the user logged in User user = state.user; + auto pingData = state.pingData; + auto geoTestedRegions = state.geoTestedRegions; + s_geoTestRegion.clear(); state = State(); state.user = user; + state.pingData = pingData; + state.geoTestedRegions = geoTestedRegions; state.screenState = ScreenState::MainMenu; errorMessage = message; @@ -463,6 +619,16 @@ static void onRelayConnected() { ++state.roundNumber; + // Auto geo test: relay connect confirms the region is reachable. + // Record the connect time; the update loop will set pendingGeoTestDisconnect + // after a 2.5s soak so we confirm the connection is fully stable. + if (settings.autoGeoTest) + { + printf("[GeoTest] Relay connected — soaking for 2.5s before disconnect\n"); + state.geoTestRelayConnectTime = std::chrono::steady_clock::now(); + return; + } + if (state.lobby.ownerCxId == state.user.cxId) { // Owner is the authoritative source for start time @@ -573,6 +739,10 @@ static void onRelayMessage(int netId, const Json::Value &json) state.gameStartTime = json["data"]["startTime"].asInt64(); state.roundNumber = json["data"]["round"].asInt(); } + else if (op == "relay_ping") + { + member.activePing = json["data"]["ping"].asInt(); + } break; } } @@ -597,6 +767,96 @@ void app_update() { pBCWrapper->runCallbacks(); + // While ping phase is active, poll incremental results each frame and + // update the loading status so the user sees per-region progress live. + if (!state.expectedPingRegions.empty()) + { + auto snapshot = pBCWrapper->getLobbyService()->getPingData(); + std::string status; + for (const auto ®ion : state.expectedPingRegions) + { + if (!status.empty()) status += "\n"; + auto it = snapshot.find(region); + if (it != snapshot.end()) + { + if (it->second >= 999) + status += " " + region + ": T/O"; + else + status += " " + region + ": " + std::to_string(it->second) + " ms"; + } + else + { + status += " " + region + ": pinging..."; + } + } + loading_status = status; + } + + // Geo test soak timer: wait 2.5s after relay connect before disconnecting, + // so the connection is confirmed fully stable before moving to the next region. + if (state.geoTestRelayConnectTime != std::chrono::steady_clock::time_point{}) + { + auto elapsed = std::chrono::steady_clock::now() - state.geoTestRelayConnectTime; + if (elapsed >= std::chrono::milliseconds(2500)) + { + printf("[GeoTest] 2.5s soak complete — triggering disconnect\n"); + state.geoTestRelayConnectTime = {}; + state.pendingGeoTestDisconnect = true; + } + } + + // Deferred auto geo test disconnect — relay connect was confirmed and the + // soak period has elapsed. Tear down gracefully so the next region test + // starts with a clean slate: + // 1. endMatch() — signal the relay server we are done (graceful close) + // 2. deregister + relay disconnect + // 3. leaveLobby() — remove us from the brainCloud lobby so the next + // findOrCreateLobby can place us in a fresh one + // 4. RTT teardown + state reset + if (state.pendingGeoTestDisconnect) + { + state.pendingGeoTestDisconnect = false; + isDisconnecting = true; // suppress any in-flight relay callbacks + + printf("[GeoTest] Graceful disconnect — %d region(s) tested so far\n", + (int)state.geoTestedRegions.size()); + + // 1. Gracefully end the relay match + app_endMatch(); + + // 2. Deregister relay callbacks and disconnect from relay server + pBCWrapper->getRelayService()->deregisterRelayCallback(); + pBCWrapper->getRelayService()->deregisterSystemCallback(); + pBCWrapper->getRelayService()->disconnect(); + + // 3. Leave the brainCloud lobby so the next test gets a fresh lobby + if (!state.lobby.lobbyId.empty()) + pBCWrapper->getLobbyService()->leaveLobby(state.lobby.lobbyId, nullptr); + + // 4. Tear down RTT + pBCWrapper->getRTTService()->deregisterAllRTTCallbacks(); + pBCWrapper->getRTTService()->disableRTT(); + + // Preserve geo test data and user info through the state reset + User user = state.user; + auto appLobbies = state.appLobbies; + int splotchDurationSec = state.splotchDurationSec; + auto pingData = state.pingData; + auto geoTestedRegions = state.geoTestedRegions; + s_geoTestRegion.clear(); + state = State(); + state.user = user; + state.user.isAlive = false; + state.user.isReady = false; + state.appLobbies = appLobbies; + state.splotchDurationSec = splotchDurationSec; + state.pingData = pingData; + state.geoTestedRegions = geoTestedRegions; + state.screenState = ScreenState::MainMenu; + + return; // state has been reset; skip rest of this frame's game update + } + // Deferred END_MATCH disconnect — safe to call here, after callbacks have returned if (state.pendingEndMatch) { @@ -614,7 +874,7 @@ void app_update() state.user.isReady = true; pBCWrapper->getLobbyService()->updateReady( state.lobby.lobbyId, true, - "{\"colorIndex\":" + std::to_string(state.user.colorIndex) + "}", + buildExtraJson(), nullptr); } } @@ -914,6 +1174,7 @@ static Lobby parseLobby(const Json::Value &lobbyJson, const std::string &lobbyId lobby.lobbyId = lobbyId; lobby.ownerCxId = lobbyJson["ownerCxId"].asString(); + const auto &jsonMembers = lobbyJson["members"]; for (const auto &jsonMember : jsonMembers) { @@ -921,11 +1182,52 @@ static Lobby parseLobby(const Json::Value &lobbyJson, const std::string &lobbyId user.cxId = jsonMember["cxId"].asString(); user.name = jsonMember["name"].asString(); user.colorIndex = jsonMember["extra"]["colorIndex"].asInt(); + + // Parse ping data shared via extra JSON (included by all clients after pinging) + const auto &jsonPings = jsonMember["extra"]["pings"]; + if (jsonPings.isObject()) + { + for (const auto ®ion : jsonPings.getMemberNames()) + user.pings[region] = jsonPings[region].asInt(); + } + if (user.cxId == state.user.cxId) user.allowSendTo = false; lobby.members.push_back(user); } + // Infer server region: region with the lowest mean ping across all members. + // 999 (timeout) is included in the average — unreachable regions naturally score high. + // Falls back to our own pingData for self if member.pings isn't populated yet. + { + std::map> totals; // region -> {sum_ms, count} + for (const auto &m : lobby.members) + { + const std::map *pPings = m.pings.empty() ? nullptr : &m.pings; + std::map selfPings; + if (!pPings && m.cxId == state.user.cxId && !state.pingData.empty()) + { + selfPings = state.pingData; + pPings = &selfPings; + } + if (!pPings) continue; + for (const auto &kv : *pPings) + { + totals[kv.first].first += kv.second; + totals[kv.first].second += 1; + } + } + std::string bestRegion; + int bestAvg = 1000; // above any valid value (max real ping is 999) + for (const auto &kv : totals) + { + if (kv.second.second == 0) continue; + int avg = (int)(kv.second.first / kv.second.second); + if (avg < bestAvg) { bestAvg = avg; bestRegion = kv.first; } + } + lobby.regionId = bestRegion; + } + return lobby; } @@ -972,6 +1274,7 @@ static void onLobbyEvent(const Json::Value &eventJson) if (state.screenState == ScreenState::JoiningLobby) { state.screenState = ScreenState::Lobby; + state.geoTestLobbyArrivalTime = std::chrono::steady_clock::now(); // Non-host users auto-ready when arriving at the lobby so the host can // start the round immediately without waiting for others to click Ready. @@ -981,7 +1284,7 @@ static void onLobbyEvent(const Json::Value &eventJson) state.user.isReady = true; pBCWrapper->getLobbyService()->updateReady( state.lobby.lobbyId, true, - "{\"colorIndex\":" + std::to_string(state.user.colorIndex) + "}", + buildExtraJson(), nullptr); } } @@ -996,8 +1299,11 @@ static void onLobbyEvent(const Json::Value &eventJson) printf("[DEBUG] DISBANDED reason code=%d (RTT_ROOM_READY=%d)\n", reasonCode, RTT_ROOM_READY); if (reasonCode != RTT_ROOM_READY) { - // Disbanded for any other reason than ROOM_READY, means we failed to launch the game. - app_closeGame(); + // Disbanded for any reason other than ROOM_READY means the server failed to launch. + // Show the error so autoJoin doesn't silently loop back in. + const std::string desc = jsonData["reason"]["desc"].asString(); + const std::string msg = jsonData["msg"].asString(); + errorAndReturnToMenu(desc + (msg.empty() ? "" : "\n" + msg)); } } else if (operation == "MATCHMAKING_IN_PROGRESS") @@ -1042,6 +1348,34 @@ static void onLobbyEvent(const Json::Value &eventJson) { loading_status = "Connecting..."; state.server = parseServer(jsonData); + + // Record which region was actually launched for the geo test. + // EdgeGap: region was chosen client-side and stored in s_geoTestRegion. + // V2 / GameLift / others: extract the region prefix from the lobbyId + // (format "region:LobbyType:N") — the server chose it via ping-aware routing. + { + std::string region = s_geoTestRegion.empty() + ? regionFromLobbyId(state.server.lobbyId) + : s_geoTestRegion; + if (!region.empty()) + { + // Only record once per region + const auto &tested = state.geoTestedRegions; + if (std::find(tested.begin(), tested.end(), region) == tested.end()) + { + state.geoTestedRegions.push_back(region); + printf("[GeoTest] Recorded region: %s (total: %d)\n", + region.c_str(), (int)state.geoTestedRegions.size()); + } + else + { + printf("[GeoTest] Region %s already recorded — server routed to same region\n", + region.c_str()); + } + } + s_geoTestRegion.clear(); + } + startGame(); } } @@ -1118,12 +1452,17 @@ void app_cancelLobby() User user = state.user; auto appLobbies = state.appLobbies; int splotchDurationSec = state.splotchDurationSec; + auto pingData = state.pingData; + auto geoTestedRegions = state.geoTestedRegions; + s_geoTestRegion.clear(); state = State(); state.user = user; state.user.isAlive = false; state.user.isReady = false; state.appLobbies = appLobbies; state.splotchDurationSec = splotchDurationSec; + state.pingData = pingData; + state.geoTestedRegions = geoTestedRegions; state.screenState = ScreenState::MainMenu; } @@ -1141,12 +1480,17 @@ void app_closeGame() User user = state.user; auto appLobbies = state.appLobbies; int splotchDurationSec = state.splotchDurationSec; + auto pingData = state.pingData; + auto geoTestedRegions = state.geoTestedRegions; + s_geoTestRegion.clear(); state = State(); state.user = user; state.user.isAlive = false; state.user.isReady = false; state.appLobbies = appLobbies; state.splotchDurationSec = splotchDurationSec; + state.pingData = pingData; + state.geoTestedRegions = geoTestedRegions; state.screenState = ScreenState::MainMenu; } @@ -1160,7 +1504,7 @@ void app_startGame() pBCWrapper->getLobbyService()->updateReady( state.lobby.lobbyId, state.user.isReady, - "{\"colorIndex\":" + std::to_string(state.user.colorIndex) + "}"); + buildExtraJson()); } // User changes his player color @@ -1179,7 +1523,7 @@ void app_changeUserColor(int colorIndex) pBCWrapper->getLobbyService()->updateReady( state.lobby.lobbyId, state.user.isReady, - "{\"colorIndex\":" + std::to_string(state.user.colorIndex) + "}"); + buildExtraJson()); } static uint64_t getPlayerMask() @@ -1290,3 +1634,31 @@ void app_clearSplotches() (const uint8_t *)str.data(), (int)str.length(), true, false, (BrainCloud::eRelayChannel)0); } + +// Broadcast our current relay RTT to all other players. +// Called periodically from game_update() so every client can see each other's live ping. +void app_broadcastRelayPing() +{ + int ping = pBCWrapper->getRelayService()->getPing(); + + // Update own entry immediately — no need to wait for a relay echo + for (auto &member : state.lobby.members) + { + if (member.cxId == state.user.cxId) + { + member.activePing = ping; + break; + } + } + + Json::Value json; + json["op"] = "relay_ping"; + json["data"]["ping"] = ping; + Json::FastWriter writer; + auto str = writer.write(json); + pBCWrapper->getRelayService()->sendToAll( + (const uint8_t *)str.data(), (int)str.length(), + false, // unreliable — stale pings are harmless to drop + false, + (BrainCloud::eRelayChannel)0); +} diff --git a/relaytestapp/src/app.h b/relaytestapp/src/app.h index c3f06f1..43e072a 100644 --- a/relaytestapp/src/app.h +++ b/relaytestapp/src/app.h @@ -69,3 +69,6 @@ void app_shockwave(const Point& pos); // Host clears all splotches on every client void app_clearSplotches(); + +// Broadcast our current relay ping to all other players (called periodically in-game) +void app_broadcastRelayPing(); diff --git a/relaytestapp/src/game.cpp b/relaytestapp/src/game.cpp index 28966a0..e6ae667 100644 --- a/relaytestapp/src/game.cpp +++ b/relaytestapp/src/game.cpp @@ -25,6 +25,7 @@ #include "globals.h" // C/C++ includes +#include #include #include #include @@ -42,19 +43,32 @@ void game_update() ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize); - ImGui::Text("Player mask"); + ImGui::Text("Players"); { ImGui::Indent(); - ImGui::TextDisabled("Only affect shockwaves"); + if (!state.lobby.regionId.empty()) + ImGui::TextDisabled("Est. region: %s", state.lobby.regionId.c_str()); + ImGui::TextDisabled("Mask = shockwave targets only"); for (auto& user : state.lobby.members) { auto color = COLORS[user.colorIndex % NUM_COLORS]; ImGui::PushStyleColor(ImGuiCol_Text, color); std::string label = user.name; - if (user.cxId == state.lobby.ownerCxId) label += " [Host]"; - if (user.cxId == state.user.cxId) label += " (Echo)"; + if (user.cxId == state.lobby.ownerCxId) label += " [H]"; + if (user.cxId == state.user.cxId) label += " (me)"; ImGui::Checkbox(label.c_str(), &user.allowSendTo); ImGui::PopStyleColor(); + ImGui::SameLine(); + char pingBuf[16]; + if (user.activePing < 0) + ImGui::TextDisabled("..."); + else if (user.activePing >= 999) + ImGui::TextDisabled("T/O"); + else + { + snprintf(pingBuf, sizeof(pingBuf), "%d ms", user.activePing); + ImGui::TextDisabled("%s", pingBuf); + } } ImGui::Unindent(); } @@ -309,13 +323,19 @@ void game_update() } #if RESEND_AT_60_FPS - // Send mouse position at 60 fps + // Send mouse position at 60 fps and broadcast relay ping every 2 seconds static auto lastTime = std::chrono::high_resolution_clock::now(); + static auto lastPingBroadcastTime = std::chrono::high_resolution_clock::now(); auto now = std::chrono::high_resolution_clock::now();; if (now - lastTime >= std::chrono::microseconds(1000000 / 60)) { lastTime = now; app_mouseMoved({state.mouseX, state.mouseY}); } + if (now - lastPingBroadcastTime >= std::chrono::seconds(2)) + { + lastPingBroadcastTime = now; + app_broadcastRelayPing(); + } #endif } diff --git a/relaytestapp/src/globals.cpp b/relaytestapp/src/globals.cpp index 9c0aa47..6e8d1f2 100644 --- a/relaytestapp/src/globals.cpp +++ b/relaytestapp/src/globals.cpp @@ -109,6 +109,10 @@ bool loadConfigs() { settings.teamCode = value; } + else if (strcmp(key, "usePingData") == 0) + { + settings.usePingData = std::stoi(value) != 0; + } } fclose(pFile); } @@ -166,6 +170,7 @@ void saveConfigs() fprintf(pFile, "protocol = %i\n", (int)settings.protocol); fprintf(pFile, "lobbyType = %s\n", settings.lobbyType.c_str()); fprintf(pFile, "teamCode = %s\n", settings.teamCode.c_str()); + fprintf(pFile, "usePingData = %i\n", settings.usePingData ? 1 : 0); fprintf(pFile, "autoLogin = %i\n", settings.autoLogin ? 1 : 0); fclose(pFile); } diff --git a/relaytestapp/src/globals.h b/relaytestapp/src/globals.h index 02e6b21..280eaf7 100644 --- a/relaytestapp/src/globals.h +++ b/relaytestapp/src/globals.h @@ -26,6 +26,7 @@ // C/C++ includes #include +#include #include #include #include @@ -121,6 +122,8 @@ struct User bool isAlive = false; bool allowSendTo = true; Point pos = {0, 0}; + std::map pings; /* Pre-game region survey latencies shared via lobby extra (ms) */ + int activePing = -1; /* Live relay-server RTT broadcast during gameplay (ms); -1 = not yet received */ }; // Lobby @@ -128,6 +131,7 @@ struct Lobby { std::string lobbyId; std::string ownerCxId; + std::string regionId; /* Region extracted from lobbyId prefix (e.g. "na-east") */ std::vector members; }; @@ -170,16 +174,22 @@ struct State std::vector shockwaves; /* Players' created shockwaves */ std::vector splotches; /* Persistent splotches left by shockwaves */ std::vector appLobbies; /* Lobby types fetched from AllLobbyTypes global property */ + std::map pingData; /* Our measured region latencies (ms), preserved across sessions */ + std::vector expectedPingRegions; /* Regions currently being pinged; empty when not in ping phase */ + std::vector geoTestedRegions; /* EdgeGap regions already launched into during geo test cycling */ int mouseX = 0; int mouseY = 0; - long long gameStartTime = 0; /* ms since epoch when current round started (0 = not in game) */ - int roundNumber = 0; /* Increments each relay round within the same lobby session */ - bool pendingEndMatch = false; /* Deferred END_MATCH disconnect (cannot call disconnect inside relay callback) */ - int splotchDurationSec = -1; /* -1 = forever; from SplotchDuration global property */ + long long gameStartTime = 0; /* ms since epoch when current round started (0 = not in game) */ + int roundNumber = 0; /* Increments each relay round within the same lobby session */ + bool pendingEndMatch = false; /* Deferred END_MATCH disconnect (cannot call disconnect inside relay callback) */ + bool pendingGeoTestDisconnect = false; /* Deferred auto-geo-test disconnect after relay connect confirmed */ + std::chrono::steady_clock::time_point geoTestLobbyArrivalTime; /* When we entered Lobby state during a geo test (for 1.5s auto-start delay) */ + std::chrono::steady_clock::time_point geoTestRelayConnectTime; /* When relay connected during a geo test (for 2.5s soak before disconnect) */ + int splotchDurationSec = -1; /* -1 = forever; from SplotchDuration global property */ }; // Change this one line to switch the default lobby type everywhere. -static const std::string DEFAULT_LOBBY_TYPE = "CursorPartyGameLift"; +static const std::string DEFAULT_LOBBY_TYPE = "CursorPartyCursorPartyV2"; struct Settings { @@ -194,6 +204,8 @@ struct Settings bool multiInstance = false; /* true when launched with instance/count args */ bool autoJoin = false; bool autoLogin = true; + bool usePingData = false; + bool autoGeoTest = false; /* Automatically cycle through all EdgeGap regions; disconnects after each relay connect */ BrainCloud::eRelayConnectionType protocol = BrainCloud::eRelayConnectionType::UDP; std::string lobbyType = DEFAULT_LOBBY_TYPE; std::string teamCode = "all"; /* "all" for non-team lobbies, "alpha"/"beta" for team lobbies */ @@ -220,6 +232,40 @@ inline int maxLobbyMembers(const std::string &lobbyType) return isCursorPartyLobby(lobbyType) ? 40 : 8; } +// Extracts the region prefix from a brainCloud lobbyId (format: "region:LobbyType:N"). +// Returns empty string if the lobbyId doesn't follow that convention. +inline std::string regionFromLobbyId(const std::string &lobbyId) +{ + auto pos = lobbyId.find(':'); + return (pos != std::string::npos && pos > 0) ? lobbyId.substr(0, pos) : ""; +} + +// True when the lobby type is the generic EdgeGap umbrella type. +// Selecting this type causes the app to ping EdgeGap beacon regions and then +// route to the best region-specific CP_E_* lobby type automatically. +inline bool isEdgeGapLobby(const std::string &lobbyType) +{ + return lobbyType == "CursorPartyEdgeGap"; +} + +// Maps an EdgeGap beacon region name to its corresponding specific lobby type. +// Returns an empty string if the region is unknown (caller should fall back to +// the original lobby type in that case). +inline std::string edgeGapRegionToLobbyType(const std::string ®ion) +{ + static const std::map kMap = { + {"asia-east", "CursorPartyEdgeGap_AsiaEast"}, + {"asia-south", "CursorPartyEdgeGap_AsiaSouth"}, + {"europe-central", "CursorPartyEdgeGap_Europe_Central"}, + {"na-east", "CursorPartyEdgeGap_NorthAmerica_East"}, + {"na-west", "CursorPartyEdgeGap_NorthAmerica_West"}, + {"sa-central", "CursorPartyEdgeGap_SouthAmerica_Central"}, + {"us-south", "CursorPartyEdgeGap_UnitedStates_South"}, + }; + auto it = kMap.find(region); + return it != kMap.end() ? it->second : ""; +} + // Main application state instance extern State state; diff --git a/relaytestapp/src/lobby.cpp b/relaytestapp/src/lobby.cpp index 87831ca..7856fb2 100644 --- a/relaytestapp/src/lobby.cpp +++ b/relaytestapp/src/lobby.cpp @@ -23,6 +23,7 @@ #include "globals.h" // C/C++ includes +#include #include #include @@ -35,10 +36,10 @@ void lobby_update() ImVec2((float)width / 2.0f, (float)height / 2.0f + 10.0f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::Begin("Lobby", nullptr, - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoTitleBar); + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoTitleBar); // Leave lobby if (ImGui::Button("Leave")) @@ -49,19 +50,31 @@ void lobby_update() // We're the boss, so we can start the game if (state.user.cxId == state.lobby.ownerCxId) { - ImGui::SameLine(); - if (ImGui::Button("Start")) + if (settings.autoGeoTest) { - app_startGame(); + // Auto-start after a 1.5s delay so the lobby state settles + auto elapsed = std::chrono::steady_clock::now() - state.geoTestLobbyArrivalTime; + if (elapsed >= std::chrono::milliseconds(1500)) + { + app_startGame(); + } + } + else + { + ImGui::SameLine(); + if (ImGui::Button("Start")) + { + app_startGame(); + } } } // Color picker: 10 per row, centered in the window { float buttonSize = ImGui::GetFrameHeight(); - float spacing = ImGui::GetStyle().ItemSpacing.x; - float gridWidth = 10.0f * buttonSize + 9.0f * spacing; - float startX = (ImGui::GetWindowSize().x - gridWidth) * 0.5f; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float gridWidth = 10.0f * buttonSize + 9.0f * spacing; + float startX = (ImGui::GetWindowSize().x - gridWidth) * 0.5f; for (int i = 0; i < NUM_COLORS; ++i) { if (i % 10 == 0) @@ -81,15 +94,17 @@ void lobby_update() state.lobby.ownerCxId != lastOwnerCxId) { lastMemberCount = state.lobby.members.size(); - lastOwnerCxId = state.lobby.ownerCxId; + lastOwnerCxId = state.lobby.ownerCxId; const float COL_PADDING = 24.0f; colWidth = 80.0f; - for (const auto& member : state.lobby.members) + for (const auto &member : state.lobby.members) { std::string label = member.name; - if (member.cxId == state.lobby.ownerCxId) label += " [Host]"; + if (member.cxId == state.lobby.ownerCxId) + label += " [Host]"; float w = ImGui::CalcTextSize(label.c_str()).x + COL_PADDING; - if (w > colWidth) colWidth = w; + if (w > colWidth) + colWidth = w; } } @@ -98,12 +113,14 @@ void lobby_update() ImGui::Dummy(ImVec2(totalColW, 0.0f)); // Helper: center a line of text in the current window - auto centerText = [](const std::string& s) { + auto centerText = [](const std::string &s) + { float tw = ImGui::CalcTextSize(s.c_str()).x; ImGui::SetCursorPosX((ImGui::GetWindowSize().x - tw) * 0.5f); ImGui::TextUnformatted(s.c_str()); }; - auto centerTextDisabled = [](const std::string& s) { + auto centerTextDisabled = [](const std::string &s) + { float tw = ImGui::CalcTextSize(s.c_str()).x; ImGui::SetCursorPosX((ImGui::GetWindowSize().x - tw) * 0.5f); ImGui::TextDisabled("%s", s.c_str()); @@ -113,6 +130,34 @@ void lobby_update() ImGui::Separator(); int maxMembers = maxLobbyMembers(settings.lobbyType); centerText("Lobby: " + state.lobby.lobbyId); + if (!state.lobby.regionId.empty()) + { + // Color the server region label based on how well it matches our ping data. + // Green : lobby region ping is within THRESHOLD of our best measured ping (good placement). + // Red : lobby region ping is significantly worse than best (likely wrong region). + // Grey : no ping data available to compare. + static const int REGION_MATCH_THRESHOLD_MS = 30; + ImVec4 regionColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); // grey default + if (!state.pingData.empty()) + { + auto lobbyIt = state.pingData.find(state.lobby.regionId); + if (lobbyIt != state.pingData.end()) + { + int lobbyPing = lobbyIt->second; + int bestPing = INT_MAX; + for (const auto &kv : state.pingData) + if (kv.second < bestPing) + bestPing = kv.second; + regionColor = (lobbyPing - bestPing <= REGION_MATCH_THRESHOLD_MS) + ? ImVec4(0.2f, 0.9f, 0.2f, 1.0f) // green: good region + : ImVec4(0.9f, 0.2f, 0.2f, 1.0f); // red: suboptimal region + } + } + const std::string regionLabel = "Server Region: " + state.lobby.regionId; + float tw = ImGui::CalcTextSize(regionLabel.c_str()).x; + ImGui::SetCursorPosX((ImGui::GetWindowSize().x - tw) * 0.5f); + ImGui::TextColored(regionColor, "%s", regionLabel.c_str()); + } centerTextDisabled("Players: " + std::to_string((int)state.lobby.members.size()) + " / " + std::to_string(maxMembers)); if (state.roundNumber > 0) @@ -123,13 +168,14 @@ void lobby_update() for (int i = 0; i < 3; ++i) ImGui::SetColumnWidth(i, colWidth); - for (const auto& member : state.lobby.members) + for (const auto &member : state.lobby.members) { std::string label = member.name; - if (member.cxId == state.lobby.ownerCxId) label += " [Host]"; - float textW = ImGui::CalcTextSize(label.c_str()).x; + if (member.cxId == state.lobby.ownerCxId) + label += " [Host]"; + float textW = ImGui::CalcTextSize(label.c_str()).x; float indent = (colWidth - textW) * 0.5f; - auto pos = ImGui::GetCursorPos(); + auto pos = ImGui::GetCursorPos(); // Drop shadow offset by 1px ImGui::SetCursorPos({pos.x + indent + 1, pos.y + 1}); ImGui::TextColored(ImVec4(0, 0, 0, 0.75f), "%s", label.c_str()); @@ -139,6 +185,75 @@ void lobby_update() } ImGui::Columns(); + // Ping data section — shown when at least one member has shared ping results + { + // Collect all unique region names across all members + our own data + std::vector regions; + auto addRegion = [&](const std::string &r) + { + if (std::find(regions.begin(), regions.end(), r) == regions.end()) + regions.push_back(r); + }; + for (const auto &kv : state.pingData) + addRegion(kv.first); + for (const auto &m : state.lobby.members) + for (const auto &kv : m.pings) + addRegion(kv.first); + std::sort(regions.begin(), regions.end()); + + if (!regions.empty()) + { + ImGui::Separator(); + centerText("Ping Data (ms)"); + + // Header row: region names + { + std::string header = " "; // name column indent + for (const auto &r : regions) + header += " " + r; + centerTextDisabled(header); + } + + // One row per member who has ping data + for (const auto &member : state.lobby.members) + { + // Prefer member.pings (shared via extra); fall back to state.pingData for self + const std::map *pPings = &member.pings; + std::map selfPings; + if (pPings->empty() && member.cxId == state.user.cxId && !state.pingData.empty()) + { + selfPings = state.pingData; + pPings = &selfPings; + } + if (pPings->empty()) + continue; + + std::string label = member.name; + if (member.cxId == state.lobby.ownerCxId) + label += " [Host]"; + // Pad name to fixed width for alignment + while ((int)label.size() < 16) + label += ' '; + label += ":"; + for (const auto &r : regions) + { + auto it = pPings->find(r); + char buf[16]; + if (it != pPings->end()) + snprintf(buf, sizeof(buf), it->second >= 999 ? " T/O" : " %5d", it->second); + else + snprintf(buf, sizeof(buf), " -"); + label += buf; + } + // Highlight the row for the local user + if (member.cxId == state.user.cxId) + ImGui::TextColored(COLORS[member.colorIndex % NUM_COLORS], "%s", label.c_str()); + else + ImGui::TextDisabled("%s", label.c_str()); + } + } + } + ImGui::End(); } } diff --git a/relaytestapp/src/mainMenu.cpp b/relaytestapp/src/mainMenu.cpp index 1dc9cbe..e459c99 100644 --- a/relaytestapp/src/mainMenu.cpp +++ b/relaytestapp/src/mainMenu.cpp @@ -28,23 +28,24 @@ #include "mainMenu.h" -// Login dialog dimensions +// Main menu dialog width (height auto-sizes to content) #define DIALOG_WIDTH 400.0f -#define DIALOG_HEIGHT 150.0f // Draws a login dialog and update its logic void mainMenu_update() { - // Main menu window, centered + // Main menu window, horizontally centered, near vertical center. + // AlwaysAutoResize lets it grow when the geo test panel is visible. { ImGui::SetNextWindowPos(ImVec2( - (float)width / 2.0f - DIALOG_WIDTH / 2.0f, - (float)height / 2.0f - DIALOG_HEIGHT / 2.0f)); - ImGui::SetNextWindowSize(ImVec2(DIALOG_WIDTH, DIALOG_HEIGHT)); + (float)width / 2.0f - DIALOG_WIDTH / 2.0f, + (float)height / 2.0f - 100.0f), // anchor ~100px above center; window grows down + ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(DIALOG_WIDTH, 0)); // 0 height = auto ImGui::Begin("Main Menu", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoResize); + ImGuiWindowFlags_AlwaysAutoResize); // Protocol choice if (ImGui::Combo("Protocol", (int *)&settings.protocol, "UDP\0TCP\0WS\0WSS\0")) @@ -74,7 +75,8 @@ void mainMenu_update() } saveConfigs(); } - if (selected) ImGui::SetItemDefaultFocus(); + if (selected) + ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } @@ -91,12 +93,132 @@ void mainMenu_update() } } + // Use ping region data toggle + if (ImGui::Checkbox("With Ping Region Data", &settings.usePingData)) + { + saveConfigs(); + } + + // Auto geo test: EdgeGap cycles through all regions (client-side routing); + // V2/GameLift connect once and record whichever region the server chose. + if (ImGui::Checkbox("Auto Geo Test", &settings.autoGeoTest)) + saveConfigs(); + if (settings.autoGeoTest) + { + ImGui::SameLine(); + if (isEdgeGapLobby(settings.lobbyType)) + ImGui::TextDisabled("(cycles all regions)"); + else + ImGui::TextDisabled("(records server-chosen region)"); + } + + // Stop condition differs by type: + // EdgeGap — every region that has a defined specific lobby type has been tested + // V2/others — at least one region confirmed (server always picks the same fastest) + bool geoTestComplete = false; + if (settings.autoGeoTest && !state.pingData.empty()) + { + if (isEdgeGapLobby(settings.lobbyType)) + { + int mappable = 0; + for (const auto &kv : state.pingData) + if (!edgeGapRegionToLobbyType(kv.first).empty()) + ++mappable; + geoTestComplete = mappable > 0 && (int)state.geoTestedRegions.size() >= mappable; + } + else + { + geoTestComplete = !state.geoTestedRegions.empty(); + } + } + // Join a game - if (ImGui::Button("Play") || settings.autoJoin) + bool autoPlay = settings.autoJoin || (settings.autoGeoTest && !geoTestComplete); + if (ImGui::Button("Play") || autoPlay) { app_play(settings.protocol); } + if (geoTestComplete) + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Geo test complete!"); + + // ---- Geo Region Test Panel (visible once ping data is available) ---------- + if (!state.pingData.empty()) + { + ImGui::Separator(); + if (isEdgeGapLobby(settings.lobbyType)) + ImGui::TextDisabled("Geo Region Test (EdgeGap — cycles all regions)"); + else + ImGui::TextDisabled("Geo Region Test (records server-chosen region)"); + + // Sort all known regions by ping + std::vector> sorted; + for (const auto &kv : state.pingData) + sorted.push_back({kv.second, kv.first}); + std::sort(sorted.begin(), sorted.end()); + + const auto &tested = state.geoTestedRegions; + + if (isEdgeGapLobby(settings.lobbyType)) + { + // EdgeGap: only show regions that have a defined specific lobby type + std::vector> mapped; + for (const auto &p : sorted) + if (!edgeGapRegionToLobbyType(p.second).empty()) + mapped.push_back(p); + + bool allTested = !mapped.empty(); + for (const auto &p : mapped) + if (std::find(tested.begin(), tested.end(), p.second) == tested.end()) + { + allTested = false; + break; + } + if (allTested && !tested.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "All EdgeGap lobbies tested — will wrap around"); + + std::string nextRegion; + for (const auto &p : mapped) + if (std::find(tested.begin(), tested.end(), p.second) == tested.end()) + { + nextRegion = p.second; + break; + } + + for (const auto &p : mapped) + { + bool wasTested = std::find(tested.begin(), tested.end(), p.second) != tested.end(); + if (wasTested) + ImGui::TextDisabled("[done] %s (%dms)", p.second.c_str(), p.first); + else if (p.second == nextRegion) + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), ">>> %s (%dms)", p.second.c_str(), p.first); + else + ImGui::Text("[ ] %s (%dms)", p.second.c_str(), p.first); + } + } + else + { + // V2 / GameLift: show ping table; highlight which region the server confirmed + if (tested.empty()) + ImGui::TextDisabled("Run test to confirm server-chosen region"); + for (const auto &p : sorted) + { + bool confirmed = std::find(tested.begin(), tested.end(), p.second) != tested.end(); + if (confirmed) + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "[confirmed] %s (%dms)", p.second.c_str(), p.first); + else + ImGui::TextDisabled("[ ? ] %s (%dms)", p.second.c_str(), p.first); + } + } + + if (!tested.empty()) + { + if (ImGui::Button("Reset Geo Test")) + state.geoTestedRegions.clear(); + } + } + // ------------------------------------------------------------------------- + ImGui::End(); } } From 5bacaffe0b8a843b832c016b811d36d8438c2057 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Thu, 16 Apr 2026 11:49:29 -0400 Subject: [PATCH 2/6] GEO Ip checking against Gamelift and bC Relay Servers --- relaytestapp/src/app.cpp | 53 +++++++++++++++-------------- relaytestapp/src/globals.h | 64 +++++++++++++++++++++++++++++++++++ relaytestapp/src/mainMenu.cpp | 28 +++++++-------- 3 files changed, 106 insertions(+), 39 deletions(-) diff --git a/relaytestapp/src/app.cpp b/relaytestapp/src/app.cpp index 3aa7054..5f43fd5 100644 --- a/relaytestapp/src/app.cpp +++ b/relaytestapp/src/app.cpp @@ -413,20 +413,19 @@ void onRTTConnected() state.pingData = pBCWrapper->getLobbyService()->getPingData(); state.expectedPingRegions.clear(); // stop per-frame polling - // For EdgeGap, route to the best region-specific lobby type. - // EdgeGap geo-test: cycle through only the regions that have a - // defined specific lobby type. Unmapped ping regions are ignored - // entirely — they have no EdgeGap lobby to route to. + // For regional cycling lobbies (EdgeGap, GameLift), route to the + // best region-specific lobby type based on ping data. + // Unmapped ping regions are ignored entirely. std::string lobbyType = settings.lobbyType; s_geoTestRegion.clear(); - if (isEdgeGapLobby(settings.lobbyType) && !state.pingData.empty()) + if (isRegionalCyclingLobby(settings.lobbyType) && !state.pingData.empty()) { std::string bestRegion; int bestPing = INT_MAX; - // Pick fastest un-tested region that has a defined EdgeGap lobby + // Pick fastest un-tested region that has a defined specific lobby for (const auto &kv : state.pingData) { - if (edgeGapRegionToLobbyType(kv.first).empty()) continue; + if (regionToSpecificLobbyType(settings.lobbyType, kv.first).empty()) continue; const auto &tested = state.geoTestedRegions; bool alreadyTested = std::find(tested.begin(), tested.end(), kv.first) != tested.end(); if (!alreadyTested && kv.second < bestPing) @@ -441,11 +440,11 @@ void onRTTConnected() bestPing = INT_MAX; for (const auto &kv : state.pingData) { - if (edgeGapRegionToLobbyType(kv.first).empty()) continue; + if (regionToSpecificLobbyType(settings.lobbyType, kv.first).empty()) continue; if (kv.second < bestPing) { bestPing = kv.second; bestRegion = kv.first; } } - printf("[GeoTest] All EdgeGap lobbies tested — wrapping to %s (%dms)\n", - bestRegion.c_str(), bestPing); + printf("[GeoTest] All %s lobbies tested — wrapping to %s (%dms)\n", + settings.lobbyType.c_str(), bestRegion.c_str(), bestPing); } else { @@ -455,7 +454,7 @@ void onRTTConnected() (int)state.pingData.size()); } s_geoTestRegion = bestRegion; - lobbyType = edgeGapRegionToLobbyType(bestRegion); + lobbyType = regionToSpecificLobbyType(settings.lobbyType, bestRegion); } if (settings.usePingData) @@ -810,9 +809,10 @@ void app_update() // starts with a clean slate: // 1. endMatch() — signal the relay server we are done (graceful close) // 2. deregister + relay disconnect - // 3. leaveLobby() — remove us from the brainCloud lobby so the next - // findOrCreateLobby can place us in a fresh one - // 4. RTT teardown + state reset + // 3. RTT teardown + state reset + // NOTE: leaveLobby is intentionally skipped here. endMatch causes the server + // to disband the lobby immediately, so by the time leaveLobby would be sent + // the lobby is already gone and the call returns "Unrecognized lobby". if (state.pendingGeoTestDisconnect) { state.pendingGeoTestDisconnect = false; @@ -821,7 +821,7 @@ void app_update() printf("[GeoTest] Graceful disconnect — %d region(s) tested so far\n", (int)state.geoTestedRegions.size()); - // 1. Gracefully end the relay match + // 1. Gracefully end the relay match (server disbands the lobby) app_endMatch(); // 2. Deregister relay callbacks and disconnect from relay server @@ -829,11 +829,7 @@ void app_update() pBCWrapper->getRelayService()->deregisterSystemCallback(); pBCWrapper->getRelayService()->disconnect(); - // 3. Leave the brainCloud lobby so the next test gets a fresh lobby - if (!state.lobby.lobbyId.empty()) - pBCWrapper->getLobbyService()->leaveLobby(state.lobby.lobbyId, nullptr); - - // 4. Tear down RTT + // 3. Tear down RTT pBCWrapper->getRTTService()->deregisterAllRTTCallbacks(); pBCWrapper->getRTTService()->disableRTT(); @@ -1295,6 +1291,9 @@ static void onLobbyEvent(const Json::Value &eventJson) if (operation == "DISBANDED") { + // Lobby is gone — clear the id so any subsequent leaveLobby calls are suppressed. + state.lobby.lobbyId.clear(); + int reasonCode = jsonData["reason"]["code"].asInt(); printf("[DEBUG] DISBANDED reason code=%d (RTT_ROOM_READY=%d)\n", reasonCode, RTT_ROOM_READY); if (reasonCode != RTT_ROOM_READY) @@ -1448,12 +1447,14 @@ void app_cancelLobby() pBCWrapper->getRTTService()->deregisterAllRTTCallbacks(); pBCWrapper->getRTTService()->disableRTT(); - // Reset state but keep the user and app-level config around + // Reset state but keep the user and app-level config around. + // Manual cancel during an auto geo test clears the tested-region list so the + // next run starts fresh rather than resuming a partially-completed cycle. User user = state.user; auto appLobbies = state.appLobbies; int splotchDurationSec = state.splotchDurationSec; auto pingData = state.pingData; - auto geoTestedRegions = state.geoTestedRegions; + auto geoTestedRegions = settings.autoGeoTest ? std::vector{} : state.geoTestedRegions; s_geoTestRegion.clear(); state = State(); state.user = user; @@ -1466,7 +1467,7 @@ void app_cancelLobby() state.screenState = ScreenState::MainMenu; } -// Cleanly close the game. Go back to main menu but don't log +// Cleanly close the game. Go back to main menu but don't log out. void app_closeGame() { isDisconnecting = true; @@ -1476,12 +1477,14 @@ void app_closeGame() pBCWrapper->getRTTService()->deregisterAllRTTCallbacks(); pBCWrapper->getRTTService()->disableRTT(); - // Reset state but keep the user and app-level config around + // Reset state but keep the user and app-level config around. + // Manual leave during an auto geo test clears the tested-region list so the + // next run starts fresh rather than resuming a partially-completed cycle. User user = state.user; auto appLobbies = state.appLobbies; int splotchDurationSec = state.splotchDurationSec; auto pingData = state.pingData; - auto geoTestedRegions = state.geoTestedRegions; + auto geoTestedRegions = settings.autoGeoTest ? std::vector{} : state.geoTestedRegions; s_geoTestRegion.clear(); state = State(); state.user = user; diff --git a/relaytestapp/src/globals.h b/relaytestapp/src/globals.h index 280eaf7..ee78360 100644 --- a/relaytestapp/src/globals.h +++ b/relaytestapp/src/globals.h @@ -266,6 +266,70 @@ inline std::string edgeGapRegionToLobbyType(const std::string ®ion) return it != kMap.end() ? it->second : ""; } +// True when the lobby type is the generic GameLift umbrella type. +// Selecting this type causes the app to ping GameLift regions and then +// route to the best region-specific CursorPartyGameLift_* lobby type. +inline bool isGameLiftLobby(const std::string &lobbyType) +{ + return lobbyType == "CursorPartyGameLift"; +} + +// Maps a GameLift ping-region name to its corresponding specific lobby type. +// Returns an empty string if the region is unknown. +// NOTE: verify these region key strings against actual pingRegions() data for GameLift. +inline std::string gameLiftRegionToLobbyType(const std::string ®ion) +{ + static const std::map kMap = { + {"ca-central-1", "CursorPartyGameLift_canada"}, + {"eu-central-1", "CursorPartyGameLift_frankfurt"}, + {"eu-west-1", "CursorPartyGameLift_ireland"}, + {"us-west-2", "CursorPartyGameLift_oregon"}, + }; + auto it = kMap.find(region); + return it != kMap.end() ? it->second : ""; +} + +// True when the lobby type is the generic CursorPartyV2 umbrella type. +// Selecting this type causes the app to ping V2 regions and then +// route to the best region-specific CursorPartyV2_* lobby type. +inline bool isV2RegionalLobby(const std::string &lobbyType) +{ + return lobbyType == "CursorPartyV2"; +} + +// Maps a V2 ping-region name to its corresponding specific lobby type. +// Returns an empty string if the region is unknown. +// NOTE: verify these region key strings against actual pingRegions() data for CursorPartyV2. +inline std::string v2RegionToLobbyType(const std::string ®ion) +{ + static const std::map kMap = { + {"ca-central-1", "CursorPartyV2_canada"}, + {"eu-west-1", "CursorPartyV2_ireland"}, + {"us-west-2", "CursorPartyV2_oregon"}, + }; + auto it = kMap.find(region); + return it != kMap.end() ? it->second : ""; +} + +// True when the lobby type is a regional-cycling umbrella (EdgeGap, GameLift, or V2). +// These types use ping-based region cycling in the auto geo test. +// All other lobby types use passive recording (server-chosen region). +inline bool isRegionalCyclingLobby(const std::string &lobbyType) +{ + return isEdgeGapLobby(lobbyType) || isGameLiftLobby(lobbyType) || isV2RegionalLobby(lobbyType); +} + +// Maps a ping region to the specific lobby type for the given umbrella lobby. +// Dispatches to the correct regional map based on the umbrella type. +// Returns empty string if the region has no defined specific lobby. +inline std::string regionToSpecificLobbyType(const std::string &umbrellaLobby, const std::string ®ion) +{ + if (isEdgeGapLobby(umbrellaLobby)) return edgeGapRegionToLobbyType(region); + if (isGameLiftLobby(umbrellaLobby)) return gameLiftRegionToLobbyType(region); + if (isV2RegionalLobby(umbrellaLobby)) return v2RegionToLobbyType(region); + return ""; +} + // Main application state instance extern State state; diff --git a/relaytestapp/src/mainMenu.cpp b/relaytestapp/src/mainMenu.cpp index e459c99..2cbbb27 100644 --- a/relaytestapp/src/mainMenu.cpp +++ b/relaytestapp/src/mainMenu.cpp @@ -99,30 +99,30 @@ void mainMenu_update() saveConfigs(); } - // Auto geo test: EdgeGap cycles through all regions (client-side routing); - // V2/GameLift connect once and record whichever region the server chose. + // Auto geo test: EdgeGap/GameLift cycle through all regions (client-side routing); + // V2/others connect once and record whichever region the server chose. if (ImGui::Checkbox("Auto Geo Test", &settings.autoGeoTest)) saveConfigs(); if (settings.autoGeoTest) { ImGui::SameLine(); - if (isEdgeGapLobby(settings.lobbyType)) + if (isRegionalCyclingLobby(settings.lobbyType)) ImGui::TextDisabled("(cycles all regions)"); else ImGui::TextDisabled("(records server-chosen region)"); } // Stop condition differs by type: - // EdgeGap — every region that has a defined specific lobby type has been tested - // V2/others — at least one region confirmed (server always picks the same fastest) + // EdgeGap/GameLift — every region that has a defined specific lobby type has been tested + // V2/others — at least one region confirmed (server always picks the same fastest) bool geoTestComplete = false; if (settings.autoGeoTest && !state.pingData.empty()) { - if (isEdgeGapLobby(settings.lobbyType)) + if (isRegionalCyclingLobby(settings.lobbyType)) { int mappable = 0; for (const auto &kv : state.pingData) - if (!edgeGapRegionToLobbyType(kv.first).empty()) + if (!regionToSpecificLobbyType(settings.lobbyType, kv.first).empty()) ++mappable; geoTestComplete = mappable > 0 && (int)state.geoTestedRegions.size() >= mappable; } @@ -146,8 +146,8 @@ void mainMenu_update() if (!state.pingData.empty()) { ImGui::Separator(); - if (isEdgeGapLobby(settings.lobbyType)) - ImGui::TextDisabled("Geo Region Test (EdgeGap — cycles all regions)"); + if (isRegionalCyclingLobby(settings.lobbyType)) + ImGui::TextDisabled("Geo Region Test (cycles all regions)"); else ImGui::TextDisabled("Geo Region Test (records server-chosen region)"); @@ -159,12 +159,12 @@ void mainMenu_update() const auto &tested = state.geoTestedRegions; - if (isEdgeGapLobby(settings.lobbyType)) + if (isRegionalCyclingLobby(settings.lobbyType)) { - // EdgeGap: only show regions that have a defined specific lobby type + // Regional cycling: only show regions that have a defined specific lobby type std::vector> mapped; for (const auto &p : sorted) - if (!edgeGapRegionToLobbyType(p.second).empty()) + if (!regionToSpecificLobbyType(settings.lobbyType, p.second).empty()) mapped.push_back(p); bool allTested = !mapped.empty(); @@ -175,7 +175,7 @@ void mainMenu_update() break; } if (allTested && !tested.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "All EdgeGap lobbies tested — will wrap around"); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "All lobbies tested — will wrap around"); std::string nextRegion; for (const auto &p : mapped) @@ -198,7 +198,7 @@ void mainMenu_update() } else { - // V2 / GameLift: show ping table; highlight which region the server confirmed + // V2 / specific regional: show ping table; highlight which region the server confirmed if (tested.empty()) ImGui::TextDisabled("Run test to confirm server-chosen region"); for (const auto &p : sorted) From 05cee635c934595292a932a8679eaab584ceabf6 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Thu, 16 Apr 2026 14:13:37 -0400 Subject: [PATCH 3/6] Expand GEO results to confirm the geo locational region during its tests --- relaytestapp/src/app.cpp | 19 ++++++++++++++++-- relaytestapp/src/globals.h | 1 + relaytestapp/src/mainMenu.cpp | 37 +++++++++++++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/relaytestapp/src/app.cpp b/relaytestapp/src/app.cpp index 5f43fd5..787a37a 100644 --- a/relaytestapp/src/app.cpp +++ b/relaytestapp/src/app.cpp @@ -493,11 +493,13 @@ static void errorAndReturnToMenu(const std::string &message) User user = state.user; auto pingData = state.pingData; auto geoTestedRegions = state.geoTestedRegions; + auto geoTestResults = state.geoTestResults; s_geoTestRegion.clear(); state = State(); state.user = user; state.pingData = pingData; state.geoTestedRegions = geoTestedRegions; + state.geoTestResults = geoTestResults; state.screenState = ScreenState::MainMenu; errorMessage = message; @@ -818,8 +820,12 @@ void app_update() state.pendingGeoTestDisconnect = false; isDisconnecting = true; // suppress any in-flight relay callbacks - printf("[GeoTest] Graceful disconnect — %d region(s) tested so far\n", - (int)state.geoTestedRegions.size()); + // Capture the relay RTT now, before teardown, so the panel can compare + // it against the pre-game beacon ping to determine pass/fail. + int relayPingAtSoak = pBCWrapper->getRelayService()->getPing(); + + printf("[GeoTest] Graceful disconnect — %d region(s) tested so far (relay ping: %dms)\n", + (int)state.geoTestedRegions.size(), relayPingAtSoak); // 1. Gracefully end the relay match (server disbands the lobby) app_endMatch(); @@ -839,6 +845,10 @@ void app_update() int splotchDurationSec = state.splotchDurationSec; auto pingData = state.pingData; auto geoTestedRegions = state.geoTestedRegions; + auto geoTestResults = state.geoTestResults; + // Record observed relay ping for the region that was just tested + if (!geoTestedRegions.empty()) + geoTestResults[geoTestedRegions.back()] = (relayPingAtSoak > 0) ? relayPingAtSoak : -1; s_geoTestRegion.clear(); state = State(); state.user = user; @@ -848,6 +858,7 @@ void app_update() state.splotchDurationSec = splotchDurationSec; state.pingData = pingData; state.geoTestedRegions = geoTestedRegions; + state.geoTestResults = geoTestResults; state.screenState = ScreenState::MainMenu; return; // state has been reset; skip rest of this frame's game update @@ -1455,6 +1466,7 @@ void app_cancelLobby() int splotchDurationSec = state.splotchDurationSec; auto pingData = state.pingData; auto geoTestedRegions = settings.autoGeoTest ? std::vector{} : state.geoTestedRegions; + auto geoTestResults = settings.autoGeoTest ? std::map{} : state.geoTestResults; s_geoTestRegion.clear(); state = State(); state.user = user; @@ -1464,6 +1476,7 @@ void app_cancelLobby() state.splotchDurationSec = splotchDurationSec; state.pingData = pingData; state.geoTestedRegions = geoTestedRegions; + state.geoTestResults = geoTestResults; state.screenState = ScreenState::MainMenu; } @@ -1485,6 +1498,7 @@ void app_closeGame() int splotchDurationSec = state.splotchDurationSec; auto pingData = state.pingData; auto geoTestedRegions = settings.autoGeoTest ? std::vector{} : state.geoTestedRegions; + auto geoTestResults = settings.autoGeoTest ? std::map{} : state.geoTestResults; s_geoTestRegion.clear(); state = State(); state.user = user; @@ -1494,6 +1508,7 @@ void app_closeGame() state.splotchDurationSec = splotchDurationSec; state.pingData = pingData; state.geoTestedRegions = geoTestedRegions; + state.geoTestResults = geoTestResults; state.screenState = ScreenState::MainMenu; } diff --git a/relaytestapp/src/globals.h b/relaytestapp/src/globals.h index ee78360..3089a54 100644 --- a/relaytestapp/src/globals.h +++ b/relaytestapp/src/globals.h @@ -177,6 +177,7 @@ struct State std::map pingData; /* Our measured region latencies (ms), preserved across sessions */ std::vector expectedPingRegions; /* Regions currently being pinged; empty when not in ping phase */ std::vector geoTestedRegions; /* EdgeGap regions already launched into during geo test cycling */ + std::map geoTestResults; /* region -> relay RTT (ms) observed during geo test soak; -1 = not captured */ int mouseX = 0; int mouseY = 0; long long gameStartTime = 0; /* ms since epoch when current round started (0 = not in game) */ diff --git a/relaytestapp/src/mainMenu.cpp b/relaytestapp/src/mainMenu.cpp index 2cbbb27..455e516 100644 --- a/relaytestapp/src/mainMenu.cpp +++ b/relaytestapp/src/mainMenu.cpp @@ -189,7 +189,22 @@ void mainMenu_update() { bool wasTested = std::find(tested.begin(), tested.end(), p.second) != tested.end(); if (wasTested) - ImGui::TextDisabled("[done] %s (%dms)", p.second.c_str(), p.first); + { + auto resIt = state.geoTestResults.find(p.second); + if (resIt != state.geoTestResults.end() && resIt->second > 0) + { + int relayMs = resIt->second; + bool pass = relayMs <= p.first + 100; + if (pass) + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), + "[done] %s beacon:%dms relay:%dms PASS", p.second.c_str(), p.first, relayMs); + else + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), + "[done] %s beacon:%dms relay:%dms FAIL", p.second.c_str(), p.first, relayMs); + } + else + ImGui::TextDisabled("[done] %s (%dms)", p.second.c_str(), p.first); + } else if (p.second == nextRegion) ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), ">>> %s (%dms)", p.second.c_str(), p.first); else @@ -205,7 +220,22 @@ void mainMenu_update() { bool confirmed = std::find(tested.begin(), tested.end(), p.second) != tested.end(); if (confirmed) - ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "[confirmed] %s (%dms)", p.second.c_str(), p.first); + { + auto resIt = state.geoTestResults.find(p.second); + if (resIt != state.geoTestResults.end() && resIt->second > 0) + { + int relayMs = resIt->second; + bool pass = relayMs <= p.first + 100; + if (pass) + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), + "[confirmed] %s beacon:%dms relay:%dms PASS", p.second.c_str(), p.first, relayMs); + else + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), + "[confirmed] %s beacon:%dms relay:%dms FAIL", p.second.c_str(), p.first, relayMs); + } + else + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "[confirmed] %s (%dms)", p.second.c_str(), p.first); + } else ImGui::TextDisabled("[ ? ] %s (%dms)", p.second.c_str(), p.first); } @@ -214,7 +244,10 @@ void mainMenu_update() if (!tested.empty()) { if (ImGui::Button("Reset Geo Test")) + { state.geoTestedRegions.clear(); + state.geoTestResults.clear(); + } } } // ------------------------------------------------------------------------- From ab62c2a1b87f29f7c69d7362b14729288b9ee836 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Thu, 16 Apr 2026 16:14:43 -0400 Subject: [PATCH 4/6] Update app.cpp --- relaytestapp/src/app.cpp | 51 ++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/relaytestapp/src/app.cpp b/relaytestapp/src/app.cpp index 787a37a..d1e1d39 100644 --- a/relaytestapp/src/app.cpp +++ b/relaytestapp/src/app.cpp @@ -68,6 +68,14 @@ static void onRelayConnected(); static bool isDisconnecting = false; +// Incremented on every app_play() call. Each ping-flow lambda captures the value at +// creation time and checks it before acting. Any callback whose captured generation +// doesn't match the current one is from a previous play session and is silently dropped. +// This prevents RTT reconnects (which re-fire onRTTConnected) from stacking up duplicate +// getRegionsForLobbies/pingRegions flows, and also kills stale relay callbacks that fire +// after the session has already been torn down. +static int s_playGeneration = 0; + // Tracks the EdgeGap beacon region chosen for the current lobby attempt. // Set when we pick the best un-tested region; recorded to geoTestedRegions on ROOM_READY. static std::string s_geoTestRegion; @@ -123,10 +131,15 @@ class RelayConnectCallback final : public BrainCloud::IRelayConnectCallback void relayConnectFailure(const std::string &errorMessage) override { printf("[%d][DEBUG] Relay connect FAILURE: %s\n", settings.instanceIndex, errorMessage.c_str()); - if (!isDisconnecting) - { - errorAndReturnToMenu("Failed to connect to relay server:\n" + errorMessage); - } + if (isDisconnecting) + return; + // Only meaningful if we are actually in a state where relay should be connecting. + // Any other state (MainMenu, JoiningLobby, …) means this is a stale callback from + // a session we already tore down — suppress it so no false error popup appears. + if (state.screenState != ScreenState::Starting && + state.screenState != ScreenState::Game) + return; + errorAndReturnToMenu("Failed to connect to relay server:\n" + errorMessage); } }; @@ -387,13 +400,26 @@ void onRTTConnected() if (state.screenState != ScreenState::JoiningLobby) return; + // Guard: each app_play() increments s_playGeneration. If RTT reconnects mid-ping + // we must NOT start a second getRegionsForLobbies + pingRegions flow — that would + // stack concurrent HTTP callbacks and corrupt the heap. Track which generation + // already started the ping flow and bail if this reconnect is for the same session. + static int s_pingStartedGen = -1; + if (s_pingStartedGen == s_playGeneration) + return; + s_pingStartedGen = s_playGeneration; + + // Capture the current generation so the lambdas below can detect if app_play() + // was called again (starting a new session) before their HTTP response arrives. + const int gen = s_playGeneration; + loading_status = "Getting regions..."; pBCWrapper->getLobbyService()->getRegionsForLobbies( {settings.lobbyType}, new BCCallback( - [](const Json::Value &result) + [gen](const Json::Value &result) { - if (isDisconnecting) return; + if (isDisconnecting || gen != s_playGeneration) return; // Collect region names so app_update() can show per-region progress state.expectedPingRegions.clear(); @@ -407,9 +433,9 @@ void onRTTConnected() pBCWrapper->getLobbyService()->pingRegions( new BCCallback( - [](const Json::Value &) + [gen](const Json::Value &) { - if (isDisconnecting) return; + if (isDisconnecting || gen != s_playGeneration) return; state.pingData = pBCWrapper->getLobbyService()->getPingData(); state.expectedPingRegions.clear(); // stop per-frame polling @@ -462,18 +488,18 @@ void onRTTConnected() else doFindOrCreateLobby(lobbyType); }, - [](const std::string &) + [gen](const std::string &) { // Ping failed — fall back gracefully to standard lobby creation - if (isDisconnecting) return; + if (isDisconnecting || gen != s_playGeneration) return; state.expectedPingRegions.clear(); doFindOrCreateLobby(settings.lobbyType); })); }, - [](const std::string &) + [gen](const std::string &) { // Region lookup failed — proceed without ping data - if (isDisconnecting) return; + if (isDisconnecting || gen != s_playGeneration) return; doFindOrCreateLobby(settings.lobbyType); })); } @@ -1155,6 +1181,7 @@ void app_play(BrainCloud::eRelayConnectionType in_protocol) { settings.protocol = in_protocol; isDisconnecting = false; + ++s_playGeneration; // invalidate any in-flight callbacks from a previous session state.user.colorIndex = settings.colorIndex; // Clear stale lobby data so the loading screen shows a fresh lobbyId From 913a7fa5a8befa7d8ee75f05b6a84e507138f78d Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Fri, 17 Apr 2026 09:58:33 -0400 Subject: [PATCH 5/6] Added more regions for prod. --- relaytestapp/src/globals.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/relaytestapp/src/globals.h b/relaytestapp/src/globals.h index 3089a54..ed4d0df 100644 --- a/relaytestapp/src/globals.h +++ b/relaytestapp/src/globals.h @@ -304,7 +304,20 @@ inline bool isV2RegionalLobby(const std::string &lobbyType) inline std::string v2RegionToLobbyType(const std::string ®ion) { static const std::map kMap = { + // prod regions + {"us-west-1", "CursorPartyV2_california"}, {"ca-central-1", "CursorPartyV2_canada"}, + {"eu-central-1", "CursorPartyV2_frankfurt"}, + {"eu-south-1", "CursorPartyV2_milan"}, + {"ap-south-1", "CursorPartyV2_mumbai"}, + {"us-east-2", "CursorPartyV2_ohio"}, + {"eu-west-3", "CursorPartyV2_paris"}, + {"sa-east-1", "CursorPartyV2_south_america"}, + {"eu-south-2", "CursorPartyV2_spain"}, + {"eu-north-1", "CursorPartyV2_stockholm"}, + {"ap-southeast-2","CursorPartyV2_sydney"}, + {"ap-northeast-1","CursorPartyV2_tokyo"}, + // internal regions (absent on prod — skipped automatically when not returned by pingRegions) {"eu-west-1", "CursorPartyV2_ireland"}, {"us-west-2", "CursorPartyV2_oregon"}, }; From 4fa14988e04d6366229c636734f694064d44174b Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Fri, 17 Apr 2026 11:08:54 -0400 Subject: [PATCH 6/6] Additional ping fixes and region support --- relaytestapp/src/app.cpp | 28 +++++++++++++++++++------- relaytestapp/src/game.cpp | 7 ++++++- relaytestapp/src/lobby.cpp | 6 ++++-- relaytestapp/src/mainMenu.cpp | 38 +++++++++++++++++------------------ 4 files changed, 49 insertions(+), 30 deletions(-) diff --git a/relaytestapp/src/app.cpp b/relaytestapp/src/app.cpp index d1e1d39..0e3b968 100644 --- a/relaytestapp/src/app.cpp +++ b/relaytestapp/src/app.cpp @@ -388,8 +388,7 @@ static void doFindOrCreateLobbyWithPingData(const std::string &lobbyType) })); } -// RTT connected — always collect ping data so all members share their latencies -// in the lobby/game UI. usePingData only controls whether ping-aware matchmaking is used. +// RTT connected — kick off region ping if needed, then find/create lobby. void onRTTConnected() { state.user.cxId = pBCWrapper->getRTTService()->getRTTConnectionId(); @@ -409,6 +408,14 @@ void onRTTConnected() return; s_pingStartedGen = s_playGeneration; + // Skip region fetch + ping entirely when neither ping-aware matchmaking nor + // geo testing is requested — go straight to a plain lobby join. + if (!settings.usePingData && !settings.autoGeoTest) + { + doFindOrCreateLobby(settings.lobbyType); + return; + } + // Capture the current generation so the lambdas below can detect if app_play() // was called again (starting a new session) before their HTTP response arrives. const int gen = s_playGeneration; @@ -439,12 +446,12 @@ void onRTTConnected() state.pingData = pBCWrapper->getLobbyService()->getPingData(); state.expectedPingRegions.clear(); // stop per-frame polling - // For regional cycling lobbies (EdgeGap, GameLift), route to the - // best region-specific lobby type based on ping data. - // Unmapped ping regions are ignored entirely. + // In geo-test mode only: remap umbrella lobby types (EdgeGap, GameLift, V2) + // to a best-ping specific lobby. Without geo-test, use the selected + // lobby type directly with no remapping. std::string lobbyType = settings.lobbyType; s_geoTestRegion.clear(); - if (isRegionalCyclingLobby(settings.lobbyType) && !state.pingData.empty()) + if (settings.autoGeoTest && isRegionalCyclingLobby(settings.lobbyType) && !state.pingData.empty()) { std::string bestRegion; int bestPing = INT_MAX; @@ -517,12 +524,16 @@ static void errorAndReturnToMenu(const std::string &message) // Reset state but keep the user logged in User user = state.user; + auto appLobbies = state.appLobbies; + int splotchDurationSec = state.splotchDurationSec; auto pingData = state.pingData; auto geoTestedRegions = state.geoTestedRegions; auto geoTestResults = state.geoTestResults; s_geoTestRegion.clear(); state = State(); state.user = user; + state.appLobbies = appLobbies; + state.splotchDurationSec = splotchDurationSec; state.pingData = pingData; state.geoTestedRegions = geoTestedRegions; state.geoTestResults = geoTestResults; @@ -1259,7 +1270,10 @@ static Lobby parseLobby(const Json::Value &lobbyJson, const std::string &lobbyId int avg = (int)(kv.second.first / kv.second.second); if (avg < bestAvg) { bestAvg = avg; bestRegion = kv.first; } } - lobby.regionId = bestRegion; + // Prefer the actual server region encoded in the lobbyId (format "region:LobbyType:N"). + // Fall back to the lowest-mean-ping estimate only when the lobbyId has no region prefix. + std::string actual = regionFromLobbyId(lobbyId); + lobby.regionId = actual.empty() ? bestRegion : actual; } return lobby; diff --git a/relaytestapp/src/game.cpp b/relaytestapp/src/game.cpp index e6ae667..e719785 100644 --- a/relaytestapp/src/game.cpp +++ b/relaytestapp/src/game.cpp @@ -47,7 +47,12 @@ void game_update() { ImGui::Indent(); if (!state.lobby.regionId.empty()) - ImGui::TextDisabled("Est. region: %s", state.lobby.regionId.c_str()); + { + bool isActual = !regionFromLobbyId(state.lobby.lobbyId).empty(); + ImGui::TextDisabled("%s: %s", + isActual ? "Region" : "Est. region", + state.lobby.regionId.c_str()); + } ImGui::TextDisabled("Mask = shockwave targets only"); for (auto& user : state.lobby.members) { diff --git a/relaytestapp/src/lobby.cpp b/relaytestapp/src/lobby.cpp index 7856fb2..70d44e1 100644 --- a/relaytestapp/src/lobby.cpp +++ b/relaytestapp/src/lobby.cpp @@ -153,7 +153,8 @@ void lobby_update() : ImVec4(0.9f, 0.2f, 0.2f, 1.0f); // red: suboptimal region } } - const std::string regionLabel = "Server Region: " + state.lobby.regionId; + bool isActual = !regionFromLobbyId(state.lobby.lobbyId).empty(); + const std::string regionLabel = (isActual ? "Region: " : "Est. Region: ") + state.lobby.regionId; float tw = ImGui::CalcTextSize(regionLabel.c_str()).x; ImGui::SetCursorPosX((ImGui::GetWindowSize().x - tw) * 0.5f); ImGui::TextColored(regionColor, "%s", regionLabel.c_str()); @@ -185,7 +186,8 @@ void lobby_update() } ImGui::Columns(); - // Ping data section — shown when at least one member has shared ping results + // Ping data section — only shown when ping region data is enabled + if (settings.usePingData) { // Collect all unique region names across all members + our own data std::vector regions; diff --git a/relaytestapp/src/mainMenu.cpp b/relaytestapp/src/mainMenu.cpp index 455e516..6b4cf1a 100644 --- a/relaytestapp/src/mainMenu.cpp +++ b/relaytestapp/src/mainMenu.cpp @@ -53,33 +53,31 @@ void mainMenu_update() saveConfigs(); } - // Lobby type — populated dynamically from AllLobbyTypes global property - if (!state.appLobbies.empty()) + // Lobby type — populated dynamically from AllLobbyTypes global property. + // Always shown so it remains visible alongside the Play button even on error. + if (ImGui::BeginCombo("Lobby Type", settings.lobbyType.c_str())) { - if (ImGui::BeginCombo("Lobby Type", settings.lobbyType.c_str())) + for (const auto &lobbyType : state.appLobbies) { - for (const auto &lobbyType : state.appLobbies) + bool selected = (lobbyType == settings.lobbyType); + if (ImGui::Selectable(lobbyType.c_str(), selected)) { - bool selected = (lobbyType == settings.lobbyType); - if (ImGui::Selectable(lobbyType.c_str(), selected)) + settings.lobbyType = lobbyType; + if (settings.lobbyType.find("Team") == 0) { - settings.lobbyType = lobbyType; - if (settings.lobbyType.find("Team") == 0) - { - if (settings.teamCode == "all") - settings.teamCode = "alpha"; - } - else - { - settings.teamCode = "all"; - } - saveConfigs(); + if (settings.teamCode == "all") + settings.teamCode = "alpha"; + } + else + { + settings.teamCode = "all"; } - if (selected) - ImGui::SetItemDefaultFocus(); + saveConfigs(); } - ImGui::EndCombo(); + if (selected) + ImGui::SetItemDefaultFocus(); } + ImGui::EndCombo(); } // Team selection (only for Team lobby types)