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); }