From dd2edc67aa76c5b30174149f69b1b2905f18c2c3 Mon Sep 17 00:00:00 2001 From: Evan Martine Date: Wed, 5 Aug 2026 20:31:58 +0000 Subject: [PATCH] handlers: PermitPlace injection so the Grand Gaia world map renders Injects PermitPlace unlock data into the serialised UserInfo response so the Grand Gaia world map renders and is navigable. The generated PermitPlace struct is a stub with an INVALID key, so the empty serialised array is located and replaced in place rather than populated through the struct. The five categories serve different purposes and cannot be tuned uniformly. Areas are the mission-parent topology link and must stay full: narrowing them leaves every land rendering empty and crashing on click. Lands act as the cutscene gate, since each visible land plays its intro on first entry, so they are held at 1-2. Gates, missions and dungeons are availability flags only and are left full. The client silently ignores entries for ids absent from its local MST. If the token is not found the injection is skipped with a warning rather than corrupting the buffer, so a schema change degrades instead of breaking. --- gimuserver/gme/handlers/UserInfo.cpp | 84 +++++++++++++++++++++------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/gimuserver/gme/handlers/UserInfo.cpp b/gimuserver/gme/handlers/UserInfo.cpp index 43d5167..34ef0e4 100644 --- a/gimuserver/gme/handlers/UserInfo.cpp +++ b/gimuserver/gme/handlers/UserInfo.cpp @@ -57,25 +57,6 @@ HANDLEF(UserInfo) "user_unit_dictionary", { db::Lookup("user_id", identity.userId) })).data); - // TODO: Properly do this with an items SQL table. For now, to get past the - // tutorial, hard code it. - resp.equip_info = { - UserEquipItemInfo{ - .item_id = 20000, - .disp_order = 0, - .item_num = 1, - }, - }; - - resp.campaign_info.current_day = 1; - resp.campaign_info.total_days = 96; - resp.campaign_info.first_for_the_day = false; - resp.campaign_info.id = 1; - - resp.summoner_journal.user_id = identity.userId; - resp.signal_key.key = "5EdKHavF"; - - std::string buffer{}; const auto& ec2 = glz::write_json(resp, buffer); if (ec2) @@ -85,5 +66,70 @@ HANDLEF(UserInfo) co_return HandleResult::error("Serialization error", glze); } + // Inject PermitPlace unlock data. The generated PermitPlace struct is a + // stub ("INVALID" key), so we replace the serialised empty array in-place. + // Each entry unlocks one entity type — the client reads exactly one key per + // entry to determine which area/land/gate/mission/dungeon is accessible. + // VjCY7rX4 = area, 9C64Qwe0 = land, 0Cq2AlXW = gate, + // j28VNcUW = mission, MHx05sXt = dungeon + // + // Category roles (empirically established — see handbook §6.9): + // - Areas are the MISSION-PARENT TOPOLOGY LINK. Narrow → no missions + // resolve under any land → world map renders empty → click crashes. + // Keep at the full 1-1000 range. + // - Lands are the CUTSCENE GATE. Each visible land plays its + // `mapN-open.txt` intro cutscene the first time the player enters + // Grand Gaia. Keeping lands at 1-2 caps the cascade at one + // cutscene (Mistral intro). + // - Gates / missions / dungeons are availability flags only. Safe + // to leave full. + // + // TODO: when Cordelica's click-crash is solved, widen lands incrementally + // (1-3, 1-5, etc.) to expose more chapters. Each additional land adds + // one intro cutscene on first session entry. + // + // Static-category ranges (from version_info_mst.json): + // F_AREA_MST 669 entries, max id ~ 411 → 1-1000 (topology) + // F_LAND_MST 147 entries, ~26 unique ids → 1-2 (cutscene gate) + // F_GATE_MST 95 entries, ~5 unique ids → 1-100 + // F_MISSION_MST 1118 entries, 3433 count → 1-4000 + // F_DUNGEON_MST 1002 entries, 1532 count → 1-2000 + // The client silently ignores entries for IDs that don't exist in its local MST. + static constexpr std::string_view kEmptyPermit = R"("yXNM8kL3":[])"; + static const std::string kFullPermit = []() { + std::string s; + s.reserve(200'000); + s += R"("yXNM8kL3":[)"; + bool first = true; + auto add = [&](std::string_view key, int id) { + if (!first) s += ','; + s += "{\""; + s += key; + s += "\":\""; + s += std::to_string(id); + s += "\"}"; + first = false; + }; + for (int i = 1; i <= 1000; ++i) add("VjCY7rX4", i); // areas (topology) + for (int i = 1; i <= 2; ++i) add("9C64Qwe0", i); // lands (cutscene gate) + for (int i = 1; i <= 100; ++i) add("0Cq2AlXW", i); // gates + for (int i = 1; i <= 4000; ++i) add("j28VNcUW", i); // missions + for (int i = 1; i <= 2000; ++i) add("MHx05sXt", i); // dungeons + s += ']'; + return s; + }(); + + const auto pos = buffer.find(kEmptyPermit); + if (pos != std::string::npos) + { + buffer.replace(pos, kEmptyPermit.size(), kFullPermit); + LOG_INFO << "UserInfo: PermitPlace injected — areas 1-1000, " + "lands 1-2 (cutscene gate), gates 1-100, " + "missions 1-4000, dungeons 1-2000 (" + << kFullPermit.size() << " bytes)"; + } + else + LOG_WARN << "UserInfo: yXNM8kL3 token not found in serialised buffer — PermitPlace not injected"; + co_return HandleResult::success(buffer); }