Packet generator quests#24
Open
Seltraeh wants to merge 14 commits into
Open
Conversation
- Replace Visual Studio 17 2022 generator with Ninja Multi-Config in CMakePresets.json so the build works with any VS version (2022, 2026+) without requiring a specific platform toolset. Adds paired buildPresets for Debug and Release configurations. - Wire packet-generator into CMake: add_custom_command regenerates gimuserver/packets/all.hpp from KDL schemas whenever any .kdl file changes. Removes the hand-run generate.py script (marked for deletion by upstream). - standalone_frontend: accept optional config path as argv[1] and chdir to its parent so all relative paths in config.json resolve correctly regardless of launch working directory. Debug builds bake the absolute deploy/ path at compile time so F5 from VS works without configuring a launch profile. - Add BUILD.md (build prerequisites, preset cheatsheet, troubleshooting), MIGRATION_NOTES.md (old-vs-new architecture diff, KDL cookbook, porting order for 288 old handlers), launch.vs.json, and rebuild.bat.
- Wire UnitMst auto_cache and LoadJson from deploy/system/unit.json; fix all unit.kdl field types to match F_UNIT_MST string-quoted format - Port user_units schema migrations (CreateUserUnitsTable, AddStats, AddSphereSlots, AddFavoriteFlg); fix migration ordering by switching MigrationMap from unordered_map to vector to preserve declaration order - Add GimuServer::SeedDefaultUnits: seeds 100 units from UnitMst into user_units on first boot (idempotent); runs after migrations in registerBeginningAdvice; randomises unit_type_id 1-6 per unit - Rewrite UserInfo handler to query user_units from DB instead of returning a single hardcoded unit - Add stub handlers: UnitFavorite (persists favorite_flg to DB), UnitEvo (stub), ChallengeArenaResetInfo (stub, Zw3WIoWu/KlwYMGF1) - Update MIGRATION_NOTES.md with new handler count, schema state, and outstanding unit.json blocker notes
Handler additions: - UnitMix (Mw08CIg2): XP fusion with Great/Super success multipliers, zel debit, full ReinforcementInfoResponse + UserTeamInfo response - UnitSell (Ri3uTq9b): DELETE user_units rows, credit zel - UnitEvo (0gUSE84e): evolve unit, preserve IMP stats, send all five I82p0wCL response fields including t9FEW2KC (original MST id) to prevent client crash during evo animation scene - UnitFavorite/UnitEvo already registered; align column formatting DB / userinfo: - Migration 21042026_SeedDefaultUserInfo: insert default row (level=900, zel/karma=99M, paid_gems=99k, username=DecompDev) - Migration 22042026_SetFreeGems: one-shot bump free_gems=95000 - Initialize.cpp: replace hardcoded login_info with DB read - UserInfo.cpp: replace hardcoded team_info with full DB read; level-gated caps (AP, deck_cost, friend count) from user_level.json MST cache via UserLevelMst lookup packet-generator submodule: - Bump to 27cc6d6 (unit net KDLs + mst/unit.kdl type/doc fixes)
Handler additions:
- TownFacilityUpdate (8v43tz7g / rq7Yd1nG): persists complete desired
facility+location level state sent by the client; deducts karma from
userinfo. Uses lenient Glaze read (error_on_unknown_keys=false) to
tolerate the IKqx1Cn9/KeC10fuL envelope keys the client always sends.
- TownUpdate (CuQ5oB8U / w1eo2ZDJ): stub returning {}; keeps session
alive on in-town navigation. Does not fire on town entry.
- EventTokenInfo (f49als4D / 94lDsgh4): stub returning {}; prevents
session-close when entering the Event Bazaar sub-area.
DB / seeding:
- Migration 25042026_CreateUserTownTables: user_town_facilities and
user_town_locations tables (user_id, id, lv, karma).
- Migration 25042026_RemoveEventTownFacilities: cleans facility ids
>= 1000 from existing databases (event-only, no client sprites).
- GimuServer::SeedDefaultTown: seeds one row per MST facility (id < 1000)
and location at lv=1 on first boot, mirroring SeedDefaultUnits.
Called from standalone_frontend/main.cpp after migrations run.
UserInfo:
- Populates town_facility_info (YRgx49WG) and town_location_info
(yj46Q2xw) from DB on every UserInfo request.
- Emits a synthetic town_location_detail (s8TCo2MS) entry for each
location (epoch datetime, tap_cnt=0). The client null-dereferences
the detail for every location in the info array; an empty detail
array crashes the town scene loader.
Asset fix (deploy-only, no rebuild):
- Copied sam/MapVillage_iph5/* -> game_content/content/_dlcbundle/MapVillage/*
stripping the MapVillage_iph5_ prefix. Client requests assets from
_dlcbundle/MapVillage/ and crashes on null texture when they 404.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…le engine
This commit brings the server from Town-complete to a working Quest entry
path. The Grand Gaia world map now loads, mission tiles render, and the
campaign subsystem has Phase-1 handler coverage.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WHAT WORKS NOW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Quest → Grand Gaia selection UI (PermitPlace unlocks all map entities)
- Grand Gaia world map loads after opening cutscene (no more BjAt1D6b crash)
- All 5 Grand Gaia land zones visible: Mistral, Cordelica, Palmyna, Lizeria,
Agni Region
- Campaign menu opens and shows mission tiles
- Mission battle starts and ends (mission-10 data only — see Known Issues)
- Party deck edits persist across sessions (DeckEdit full implementation)
- Brave Burst activation works (skill_lv seeded correctly in unit table)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEW HANDLERS (all registered in GmeControllerHandlers.cpp)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Quest / world-map entry:
AreaInfo Zds63G5y YfAh7gqojdXEtFR1 Entry point for Quest menu
Campaign subsystem (Phase 1 — see plan file for Phase 2):
CampaignStart 6Y0gaPQN WM6yr4ej Open Campaign; return mission list
CampaignMissionGet RSm6p2d4 5jzXN7AH Per-mission progress/state
CampaignDeckGet C3a0VnQK q2ZtYJ6P Campaign party deck list
CampaignBattleStart h1RjcD3S 4CKoVAq0 Lock deck, begin battle
CampaignBattleEnd pTNB6yw3 t06HFsXP Persist clear flag, credit zel/exp
CampaignReceipt 5Imq3wC0 4DAgP80B Claim mission rewards
CampaignEnd jF9Kkro4 4X9tBSg8 Exit Campaign (stub {})
Post-battle:
MissionEnd 9TvyNR5H oINq0rfUFPx5MgmT Battle result ack
FixGiftInfo gLRIn74v 15gTE9ft Gift inbox stub
Grand Gaia entry sequence (all stub {}):
DungeonEventUpdate BjAt1D6b k5EiNe9x Fires after opening cutscene
GetScenarioPlayingInfo VRfsv4e3 Bh4WqR01 Cutscene viewed-state query
UpdatePermitPlaceInfo 1MJT6L3W 3zip5Htw Post-area permit refresh
UpdateEventInfo rCB7ZI8x L1o4eGbi Active event data update
Chronology 5o8ZlDGX SNrhAG29 Chronology timeline feature
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DB MIGRATIONS (MigrationManager.cpp)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25042026_CreateUserCampaignTables
- user_campaign_missions (state: 0=locked, 1=available, 2=cleared)
- user_campaign_decks
- user_campaign_state (active mission + battle seed + saved checkpoint)
25042026_SeedCampaignMissions
- Seeds 3 placeholder missions (10001-10003) as available for dev account
27042026_SetGemsTo4200
- Clamps paid_gems/free_gems to 4200; values in the tens-of-thousands
overflow the client's gem counter and get silently reset to 0
27042026_CreateUserPartyDecks
- user_party_decks table (deck_type, deck_num, user_unit_id, member_type,
disp_order); written by DeckEdit, read back by UserInfo
28042026_BroadenCampaignMissions
- Seeds missions 1-200 as available under the correct UUID (0839899613932562)
so the client's campaign MST can match node IDs regardless of numbering
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
KEY HANDLER CHANGES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
UserInfo.cpp
- Fixed user ID from legacy "12345678" to "0839899613932562" throughout
- Party decks now loaded from user_party_decks DB table; falls back to a
generated default on first login before any DeckEdit writes
- PermitPlace (yXNM8kL3) injection: all world-map entities unlocked
(Areas 1-1000, Lands 1-200, Gates 1-100, Missions 1-4000, Dungeons 1-2000)
DeckEdit.cpp
- Full implementation (was stub): DELETE + INSERT for the submitted deck slot,
echo the saved entries back under "dX7S2Lc1"
MissionStart.cpp
- Full battle-engine seed implementation using hardcoded mission-10 data
- Returns 5 enemy waves with Glaze-serialized MST structs (MsBattleGroup,
MsBattleMonsterGroup, MsMonster, MsAI, MsSkill, etc.)
- Reads actual mission_id from request (uses "10" as fallback, not hardcoded)
MissionEnd.cpp (new)
- Accepts post-battle result acknowledgement; credits zel to userinfo;
returns UserTeamInfo + CampaignBattleEnd payload
GimuServer.cpp
- skill_lv and extra_skill_lv now set to 10 for any unit with a skill_id > 0
(was always 0, which prevented BB gauge from filling in battle)
Initialize.cpp / UnitMix.cpp / UnitEvo.cpp / UnitSell.cpp
- Removed stale kInfoId "12345678" alias; all DB queries now use kUserId
"0839899613932562" consistently
TownFacilityUpdate.cpp
- Switched to KDL-generated TownFacilityUpdateReq (removes local struct copies)
GmeControllerHandlers.cpp
- Added std::exception catch alongside DrogonDbException catch so any
unhandled C++ exception is logged and returned as a Close error rather
than silently crashing the coroutine
DebugCli.cpp / DebugCli.hpp (new)
- Standalone-frontend interactive debug CLI for DB inspection and
handler replay during development
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
KNOWN ISSUES (deferred to next session)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[1] Cutscene replay on every Grand Gaia session
GetScenarioPlayingInfo (VRfsv4e3) returns {}; client sees "no scenarios
viewed" and replays all area-unlock cutscenes (mapN-open.txt) on every
entry. Fix requires: user_scenario_progress table + DB read/write in
GetScenarioPlayingInfo. Reverse GetScenarioPlayingInfoResponse from
readparam_analysis.json before implementing response format.
Reference: F_SCENARIO_MST (308 entries), F_SCENARIO_CATEGORY_MST (189).
[2] MissionStart hardcodes mission-10 battle data for all missions
All missions launch with the same 5-wave enemy set from the original
test capture (mission 10, Grand Gaia chapter 1). Tapping any mission
other than mission-10 causes the client to download incorrect battle
assets and crash. Fix requires loading battle wave data from the MST
(F_MISSION_MST / F_BATTLE_GROUP_MST / F_MONSTER_MST) keyed on the
actual mission_id from the request.
[3] Campaign handler responses are stubs
CampaignBattleEnd, CampaignReceipt, and CampaignStart return minimal
data. Real reward/receipt logic and MST-driven mission catalog need
to be wired up once MissionStart is fixed (issues #1 and #2 first).
[4] area-unlock cutscene download hang (client quirk, not server bug)
On cold asset cache the client hangs downloading /content/event/mapN-open.txt
one file at a time; requires relaunch to continue. Workaround: Windows
app reset clears the cache for a clean re-download sequence.
Bumps the submodule pointer to commit 02ebab4 on the new packet-generator-quests branch, which adds five MST KDL schemas required for the quest/campaign/battle subsystem: assets/mst/mission.kdl — F_MISSION_MST (parts 1 & 2) assets/mst/mission_ep3.kdl — F_MISSION_EP3_MST (Ep3 params) assets/mst/mission_npc_unit.kdl — F_MISSION_NPC_UNIT_MST (NPC stats) assets/mst/mission_script.kdl — F_MISSION_SCRIPT_MST (cutscene scripts) assets/mst/skill.kdl — F_SKILL_MST (all 9 parts merged) Also remove three temporary Town-debugging scripts (check_town.ps1, check_town2.ps1, check_town3.ps1) — Town is stable and these are no longer needed.
…ionStart fixes
Players can now enter Grand Gaia, see Mistral's land map, click into any of
Mistral's areas (including end-game ones like Volcano Eldent), and play missions
to completion. Cutscenes are limited to 1 per session (the Mistral intro).
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PERMIT PLACE — CATEGORY ROLES (THE KEY DISCOVERY)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Three weeks of experiments revealed that PermitPlace's five categories serve
DIFFERENT purposes — narrowing them has opposite effects:
Areas → mission-parent topology link. Narrow → no missions visible
in ANY land (renders empty,
click crashes).
FULL → missions resolve normally.
Lands → cutscene gate. Each visible land plays its `mapN-open.txt`
intro the first time the player enters Grand Gaia.
Narrow → 1 cutscene. Full 1-200 → all cutscenes cascade.
(Folder name `sam/Map4Coldelica/` confirms `mapN` = land N.)
Gates/Dungeons/Missions → just availability flags; safe to leave full.
Stable winning configuration committed in UserInfo.cpp:
Areas 1-1000 (FULL — topology requirement)
Lands 1-2 (cutscene gate — exposes Mistral + Cordelica)
Gates 1-100 (full)
Missions 1-4000 (full)
Dungeons 1-2000 (full)
Earlier attempts to limit cutscenes by narrowing AREAS broke mission
visibility in every land for weeks — that approach was wrong.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MISSIONSTART — NESTED PARSE BUG FIXED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Captured request payload showed mission_id is nested inside 9Q1Lq5FS[0],
NOT at the top level as our struct declared. Glaze's lenient mode tolerated
the unknown root keys but couldn't find mission_id, so it stayed empty and
the fallback "10" fired for every mission. Mission 10 worked by accident;
mission 11+ produced response-says-10/request-says-11 mismatches and crashed
the client on battle load.
Fix:
- New nested MissionStartItem struct keyed on j28VNcUW + Z0Y4RoD7
- MissionStartReq wraps it under 9Q1Lq5FS
- missionIdNum patched into all 5 MsBattleGroup initialisers so the inner
battle-wave headers also match the requested mission
Wave content is still mission-10's captured enemies (no F_MISSION_MST
loading yet — that's the next milestone) but the headers match so the
client renders and the battle plays cleanly through end-of-mission.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CAMPAIGNBATTLEEND — NEXT-MISSION UNLOCK CHAIN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
After CampaignBattleEnd marks a mission state=2 (cleared), it now also
INSERT OR IGNOREs `mission_id + 1` with state=1 (available). Wrapped in
try/catch (std::stoi can throw on event campaigns with alphabetic IDs).
Harmless when the next ID already exists; useful when the player chains
through sequential story missions.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MIGRATION HISTORY (MigrationManager.cpp)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Two new migrations bracket the failed mission-restriction experiment:
06052026_RestrictToFirstThreeMissions
Hypothesis: cascading cutscenes come from too many unlocked missions.
Action: DELETE user_campaign_missions, INSERT only 1/2/3.
Result: hypothesis WRONG. Cutscenes still played; also crashed on
second Grand Gaia entry because lands/areas were broadly unlocked but
contained zero visible missions. Kept in history (the migration ran
on existing DBs).
07052026_RestoreMissionCatalog
Recovery: re-seeds missions 1-50 under user 0839899613932562 so the
campaign menu has tiles and the world map has navigable content.
INSERT OR IGNORE preserves any already-cleared missions.
07052026_RestrictToStoryMissions10_11_12
Followup: restricts campaign menu (not world map — that's PermitPlace's
job) to mission IDs 10/11/12 — the actual story tile IDs captured from
live MissionStart requests.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WHAT'S WORKING NOW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ Town subsystem (full, untouched)
✓ Unit Mix / Evo / Sell (full, untouched)
✓ Grand Gaia entry → 1 cutscene (Mistral intro) → land chooser
✓ Mistral land click → land map renders → all areas visible as "New"
✓ Tap any Mistral area (including end-game like Volcano Eldent) → its area
cutscene plays → mission tiles visible with "New" indicator
✓ Tap any mission → battle starts → battle plays through → end screen → home
✓ Mission rewards credited (zel/karma/exp) → HUD updates
✓ Next-mission unlock chain (clear N → unlock N+1)
✓ DB-backed party deck persistence
✓ Brave Burst gauge filling (skill_lv=10 seed in user_units)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
KNOWN LIMITATIONS (NEXT TASKS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[1] CORDELICA click → soft crash to home
Mistral renders fine; clicking Cordelica (land 2) crashes. Likely a
specific data dependency (cutscene script, NPC unit, area asset) we
have for Mistral but not Cordelica. Pivot here next.
[2] All missions show the SAME enemy waves (mission-10's data)
MissionStart hardcodes battle_groups, monster_groups, monsters, AI,
and skills from a mission-10 live capture. Headers (mission_id field
in MsBattleGroup) are dynamic and match the request, so the client
renders without crashing — but the enemies, boss, and rewards are
always mission-10's. Fix: load F_MISSION_MST + F_BATTLE_GROUP_MST +
F_MONSTER_MST from decoded deploy/system/*.json and key the response
off req.missionIdNum.
[3] Cutscenes replay every session
With lands 1-2 the cascade is limited to 1 cutscene per session, but
that one cutscene still plays every time the player relaunches.
Investigated and ruled out: user_scenario_info / user_special_scenario_info
/ clear_mission_info / GetScenarioPlayingInfo. The gate appears to
be either client-local cache or a structural field requiring binary
reverse-engineering to locate. Quality-of-life only, not a blocker.
[4] Campaign battle data MST loading
Once [2] is solved, CampaignBattleEnd / CampaignReceipt can graduate
from hard-coded zel rewards to per-mission reward tables from
F_MISSION_MST. Campaign Phase 2 handlers (Save/Restart/DeckEdit/
ItemEdit/TieupCampaign) stay as stubs.
…TODOs
Now that the PermitPlace category-role discovery has landed and missions are
playable end-to-end, the running-experiment comment blocks in UserInfo.cpp
are no longer accurate. Replaced them with a concise "final state"
explanation that describes WHY the current configuration works, and added
explicit `TODO:` markers for the remaining open items so `git grep TODO`
surfaces them as a backlog.
UserInfo.cpp
- Dropped the "EXPERIMENTAL: cutscene already viewed injection —
ABANDONED" block (24 lines) — collapsed into a single TODO
- Dropped the "CUTSCENE-CASCADE EXPERIMENT (running)" block (38 lines)
plus the trailing "ABANDONED: UT1SVg59" block (18 lines) — replaced
with the steady-state Category Roles summary that references the
handbook for the full dead-end list
- Loop comments simplified (no more "FULL — was 1-50" git-history
breadcrumbs)
MissionStart.cpp
- Added explicit TODO above battle_groups initialiser pointing at the
F_MISSION_MST / F_BATTLE_GROUP_MST / F_MONSTER_MST loading path
MissionEnd.cpp / CampaignBattleEnd.cpp
- Replaced "placeholder" comments on the hardcoded reward constants
with TODO markers cross-referencing each other
No behavioural change.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ADDENDUM — detailed cleanup & TODO inventory
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Before/after sketch of each block touched:
────── UserInfo.cpp ── login_info scenario fields ──────
- 25 lines of "EXPERIMENTAL ... ABANDONED" narrative re-tracing two
format experiments (comma list, colon-pair) that proved cutscenes
aren't gated by user_scenario_info / user_special_scenario_info.
+ 8-line TODO: cutscene-replay persistence — refers handbook §7.4.5
for the full dead-end list, notes that GetScenarioPlayingInfo
(VRfsv4e3) never fires during normal play, concludes binary
reverse-engineering is the only path forward, flags this as
quality-of-life not blocking.
+ The two field assignments (= "") stay — they were correct.
────── UserInfo.cpp ── PermitPlace injection ──────
- 38-line "CUTSCENE-CASCADE EXPERIMENT (running)" block containing the
iteration history (areas 1-5 → 1-15 → 1-50, lands 1-2 → 1-200, etc.)
with the running hypotheses.
- 18-line "ABANDONED: UT1SVg59 cleared-mission injection" trailing
block recording the 1118-entry UT1SVg59 dead-end test.
- "FULL — was 1-50" / "FULL — was 1-2" suffixes on each
for-loop comment. These were migration-narrative breadcrumbs from
a single working session and are git-log information, not
code-level information.
+ 22-line Category Roles steady-state summary covering:
* Areas are mission-parent TOPOLOGY (must stay full)
* Lands are the CUTSCENE GATE (each visible land = 1 intro cutscene)
* Gates/missions/dungeons are availability flags only
* Reference to handbook §6.9 for empirical derivation
+ 5-line TODO: widen lands incrementally after Cordelica fix
+ 6-line TODO at end-of-function: Cordelica click-crash investigation
(diff dlc_404.log before/after, look for land-2-specific handler we
haven't implemented)
+ Loop comments simplified to bare category labels.
────── MissionStart.cpp ── battle_groups initialiser ──────
- "Wave content (group_id, monster_groups) stays as captured mission-10
data; until F_MISSION_MST loading is wired up..." — accurate but
buried in a normal comment paragraph.
+ Explicit TODO block above the initialiser pointing at the MST
loading path: F_MISSION_MST + F_BATTLE_GROUP_MST + F_MONSTER_MST,
decoded via scripts/mstdec.py from deploy/game_content/mst/Ver*.dat
into deploy/system/*.json, then read at boot in ServerCache.cpp.
────── MissionEnd.cpp ── kMissionZelReward et al. ──────
- "Fixed rewards (placeholder until real per-mission MST is wired):
+500 zel, +100 karma, +100 exp"
+ Explicit TODO referencing the MissionStart and CampaignBattleEnd
MST loading TODOs as a paired effort.
────── CampaignBattleEnd.cpp ── kBattleZelReward ──────
- "// placeholder until real MST rewards" inline tag.
+ Explicit TODO: pairs with the MissionStart MST loading task, lookup
zel + karma + exp per mission_id from F_MISSION_MST once parsed.
────── Net change ──────
+53 / -86 lines (≈40% reduction in narrative comment volume), no
runtime behavior change.
────── TODO inventory after this commit ──────
git grep TODO gimuserver/gme/UserInfo.cpp gimuserver/gme/MissionStart.cpp \
gimuserver/gme/MissionEnd.cpp gimuserver/gme/CampaignBattleEnd.cpp
UserInfo.cpp:50 cutscene-replay persistence (deferred,
needs client binary reverse-engineering)
UserInfo.cpp:275 widen lands beyond 1-2 once Cordelica
click-crash is solved (each adds 1 intro
cutscene on first session entry)
UserInfo.cpp:322 investigate Cordelica click-crash — likely
a per-land asset/NPC/script dependency we
have for Mistral but not Cordelica
MissionStart.cpp:339 load real per-mission battle waves from
F_MISSION_MST + F_BATTLE_GROUP_MST +
F_MONSTER_MST (currently every mission
ships mission-10's captured live data)
MissionEnd.cpp:12 replace fixed rewards with per-mission MST
lookup (paired with MissionStart TODO)
CampaignBattleEnd.cpp:21 replace fixed reward with per-mission MST
lookup (paired with MissionStart TODO)
────── Suggested execution order for the backlog ──────
1. UserInfo.cpp:322 — Cordelica click-crash. Standalone bug; doesn't
need any new infrastructure. Capture log + dlc_404 diff during a
reproduction, identify the missing dependency, fix.
2. MissionStart.cpp:339 — F_MISSION_MST loading. Largest task: decode
the .dat files via scripts/mstdec.py, write a ServerCache loader,
thread mission_id into the battle_groups / monster_groups lookup.
Unblocks the two reward TODOs as a freebie.
3. MissionEnd.cpp:12 + CampaignBattleEnd.cpp:21 — fall out of (2).
4. UserInfo.cpp:275 — widen lands 1-3, 1-5, etc. once (1) is fixed.
Per-land cutscenes are the cost.
5. UserInfo.cpp:50 — cutscene-replay persistence. Deferred until
someone has binary access (libgame.so / BraveFrontier.exe decomp).
…allengeArenaResetInfo cooldown Cherry-picks the three upstream packet-generator commits (5472598 / d2893ba / 61fedcc) onto our quests-branch submodule, then wires the matching handler fixes into gimuserver/gme/ on the server side. Reference: upstream PR decompfrontier#23 (Tom2096). ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DISCOVERED CLIENT BEHAVIOUR (the actual headline) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ The PR uncovered two important client-side patterns that we should treat as general principles, not just BadgeInfo/ChallengeArenaResetInfo trivia: (1) WRAPPER STRUCT vs INNER STRUCT — the dispatch-key trap When a KDL schema defines `XxxInfo` + `XxxInfoResp` (with XxxInfoResp having one field of type XxxInfo keyed under a dispatch hash), the WRAPPER is what handlers must serialize. Sending the inner struct directly puts the inner field-key at the root of the response body, where the client's dispatch pipeline doesn't recognise it. The handler appears successful but no UI update fires — silent drop. (2) THE CONNECT-SCENE POLL CASCADE Handlers the client polls during the connect scene (notably ChallengeArenaResetInfo) have THREE escalating failure modes: - unregistered → GmeError{cmd=Close} → 50 ms retry (~20 req/s spam) - registered but `{}` → parse OK, needServerRefresh() trips on zero-valued timestamps → 1 second retry - registered with future-dated daily_cooling_end → no retry until the timestamp expires Formula: server_time + now() - local_snapshot > daily_cooling_end Resolution: 1-second tick from time(). Epoch zero is treated as "expired now". ALWAYS set future timestamps on cooling/refresh response fields. Both patterns are documented in the handbook (§6.11 and §6.12) for future reference. The "stubs return {} for now" default we've been using is actively harmful for any handler the client polls — we should audit all handlers currently returning {} to see if any of them are causing similar unmetered request rates in deploy/log/. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WHAT CHANGED ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ packet-generator submodule (cherry-picked, 3 commits) 5472598 BadgeInfoResp wrapper struct with dispatch key h23iRjGN d2893ba ChallengeArenaResetInfo + ChallengeArenaResetInfoResp wrapper (dispatch key t6bRQfln) 61fedcc Move BadgeInfoResp and ChallengeArenaResetInfoResp into handlers.kdl; switch timestamp fields to datetime-unix::str These commits are now on Seltraeh/packet-generator packet-generator-quests at 1192091; the submodule pointer bump in this commit picks them up. On next cmake build the regenerated gimuserver/packets/all.hpp will contain BadgeInfoResp, ChallengeArenaResetInfo, and ChallengeArenaResetInfoResp with the correct glz::meta specializations. gimuserver/gme/BadgeInfo.cpp Switched `::BadgeInfo resp{}` → `::BadgeInfoResp resp{}` so the serialized response carries the h23iRjGN dispatch key at the root instead of u7Htm9P4. Inline comment cross-references PR decompfrontier#23. gimuserver/gme/ChallengeArenaResetInfo.cpp Replaced the empty `{}` stub with the PR decompfrontier#23 logic: resp.reset_info.server_time = now (ms precision) resp.reset_info.daily_cooling_end = server_time + 1h Other timestamps (daily_cooling_start, weekly_reset_time) stay at epoch — the client doesn't gate on those for this handler's polling logic. TODO marker placed for deriving the real cooldown end from the arena reset schedule instead of a fixed +1h offset. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WHAT'S NEXT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [1] Audit all handlers currently returning `{}`. Anything the client treats as a cooldown/refresh-style poll will spam at 1 req/s indefinitely under the empty-handler regime documented above. Top candidates to check (high frequency in deploy/log/): - Zw3WIoWu ChallengeArenaResetInfo (FIXED in this commit) - any other handler showing >2 calls/min in a single session [2] BadgeInfoResp default values. Our current BadgeInfo struct ships everything at 0, which means no badges/scenarios/new content indicators. Wire it to user_campaign_missions.state count and F_SCENARIO_MST so the home-screen "New" badges reflect actual player progress. [3] Continue the original backlog (Cordelica click-crash, F_MISSION_MST loading, etc. — see existing TODOs).
Pulls in commit 592f699 from Seltraeh/packet-generator packet-generator-quests, which addresses every comment on decompfrontier/packet-generator PR decompfrontier#21: * 5 of 6 unit-position MST fields reverted to [i32::str]::sep(comma) after auditing all 2291 unit.json rows (no decimals anywhere — the earlier 'may contain floats' claim was incorrect) * 3 fields kept as str with hard evidence in the doc string: 5SUvj4tM overdriveStats — trailing comma in real values CEeqs63b bad_state_resists — contains literal space char KC3Jk8Br summon_image_pos — 56/2291 rows have decimal floats * UnitFavoriteReq/Resp + UnitEvoReq/Resp moved from net/unit.kdl into net/handlers.kdl per project convention; UnitFavoriteEntry stays in unit.kdl as a shared type * favorite field doc clarifies why i32::str instead of bool::int / bool::str (wire format is quoted decimal 1/0; no bool modifier matches that exact byte sequence) No server-side handler changes needed — UnitFavorite.cpp already binds e.favorite as an integer via execSqlCoro, which round-trips correctly through i32::str on both sides.
Picks up commit eb3de83 from Seltraeh/packet-generator
packet-generator-quests, which corrects FIVE wrong MST hash declarations
discovered during a systematic audit:
* assets/mst/unit.kdl UnitMst hash 2r9cNSdt (was JYFGe9y6 — that
was actually F_UNIT_EXP_PATTERN_MST's hash; VERIFIED against
deploy/system/unit.json top-level key)
* assets/mst/mission.kdl hash oXeC1Ak9 (was 2I9V0o6J — that was
a CampaignMissionInfoResponse network response, is_mst=FALSE)
* assets/mst/mission_ep3.kdl hash vE0Qdg33 (was a duplicate of the
above wrong hash; IDA confirms this is MissionEp3MstResponse)
* assets/mst/mission_npc_unit.kdl added UNVERIFIED marker — hash
1e6jxQzf is only a field key in IDA, never a top-level MST hash
* assets/mst/mission_script.kdl added UNVERIFIED marker — hash
N4XVE1uA is heavily dual-use across multiple BF wire contexts
* assets/mst/skill.kdl added UNVERIFIED marker — 8aiBoHg5 in IDA
is UnitSkillMstResponse, not a standalone SkillMstResponse
Methodology (now codified in BF_OFFLINE_SERVER_HANDBOOK.md §3.3):
1. grep response_mappings.json for the hash
2. head -c 30 deploy/system/<file>.json to see real top-level key
3. UNVERIFIED marker if neither check resolves it
No C++ handler changes — handlers either inject directly via
string-replace (PermitPlace pattern) or use generated structs that
won't break from a hash relabel.
Big-picture: the MissionStart / MissionEnd flow is now schema-driven
end-to-end via IDA-decoded response classes (committed to the
packet-generator submodule). The server credits real per-mission
zel/karma from the client's rXvA1E5y request block, deducts the real
F_MISSION_MST stamina cost on start, levels the player up with proper
chunk-subtraction semantics, refills energy on level-up, and emits the
F5Vs19mb MissionRewardResponse with the cascade-control fields
(LvupFlg / BeforeLv) populated so single-step level-up animations
play without the broken Lv1->Lv2->Lv3 cascade we kept hitting.
gimuserver/gme/MissionStart.cpp
- Energy cost from F_MISSION_MST via ServerCache::missionMst() —
mission_id lookup against the 3433-row table. Mission 1 costs 3
(previously hardcoded 10). Falls back to 10 for synthetic IDs.
- active_deck UPDATE split out of the failing combined UPDATE; uses
request's Z0Y4RoD7 directly.
gimuserver/gme/MissionEnd.cpp
- Parses request via the generated MissionEndReq struct (KDL-driven
typed access instead of substring scans).
- Credits real zel/karma from rXvA1E5y; falls back to seed values if
missing.
- Level-up loop: per-level chunk subtraction (while exp >=
chunk[level+1], exp -= chunk, level += 1). Refills energy to the
new level's max_action_point on level-up.
- action_rest_timer = 180 (3 min, empirical) to stop the
"Energy is full." misrender when below cap.
- F5Vs19mb response now sends all 19 fields with real values
(or empty placeholders) including LvupFlg + BeforeLv which the
client uses to gate the level-up animation cascade. *UpType
badge fields kept commented for future offline-event support.
- UT1SVg59 emits the full 7-field UserClearMissionInfo shape with
placeholders for the per-mission clear-history stats.
gimuserver/drogon/ServerCache.{hpp,cpp}
- missionMst() loader for deploy/system/mission.json (wrapper key
oXeC1Ak9), 3433 rows from F_MISSION_MST_{1,2}.
gimuserver/drogon/ServerCacheMst.hpp
- MissionMstCache type registration.
gimuserver/gme/DeckEdit.cpp
- DELETE every (deck_type, deck_num) slot referenced in the request
before INSERTing; previously DELETEd only req.deck[0]'s slot which
let the other 9 slots collide on PRIMARY KEY and silently fail.
- Persists active_deck from Ti62XfZK.Z0Y4RoD7 so home-screen team
selection survives logout.
gimuserver/db/MigrationManager.cpp
- Three migrations for the level-up / exp drift fixes (see handbook
section 8.27 for the back-and-forth history).
- Final state: seed level=900, exp=1009680 ("almost ready to ding
901" per per-level-chunk semantics).
deploy/system/mission.json (NEW)
- 3433-row merged F_MISSION_MST_1 + F_MISSION_MST_2.
deploy/system/unit.json (NEW), gacha_categories.json (NEW),
gacha_info.json, summon_tickets_v2.json
- MST data now properly tracked (was hidden by overly-broad
deploy/ ignore rule before the .gitignore fix).
.gitignore
- Replaced blanket `deploy/` with targeted ignores for gme.sqlite /
log/ / game_content/ / config.json — MST data under deploy/system/
now committable.
- Added tools/ida/ (private IDA audit artifacts).
standalone_frontend/DebugCli.cpp, gimuserver/gme/{UnitMix,UnitEvo,
UnitSell,CampaignBattleEnd,CampaignReceipt,UserInfo,GatchaList,
GachaAction,UnitSelectorGachaTicket}.cpp
- Zel/karma caps bumped from 99,000,000 to 99,999,999 (seed values
stay at 99M). Misc handler refactors flowing from the audit.
Open follow-ups in code as TODO comments: per-mission clear-history
table for real UT1SVg59 values; qC2tJs4E incremental UserUnitInfo
push for the post-mission "unit obtained" animation (currently items
render but units don't); recharge scheduler for the action_rest_timer.
## Unit drops end-to-end (handbook §8.31)
MissionEnd now credits dropped units to user_units and triggers both the
"obtained unit X" card animation and the first-encounter encyclopedia
unlock screen:
- Parses req.battle_result.unit_drops (triples like "30030:1:1,...")
- Batched INSERT + SELECT last_insert_rowid pattern (§6.14 — prevents
the SQLite single-worker wedge that earlier per-row inserts hit)
- Emits qC2tJs4E UserUnitInfo (incremental — same readParam class as
4ceMWH6k but with the append-flag set)
- Emits GV81ctzR UserUnitDictionary for any MST id not already in
user_units pre-INSERT (suffix-strips _100 evolved forms so they
collapse onto the base id; deduped within the response)
## Reinforce / Friend slot (handbook §8.32, §8.33, §8.34)
The squad-select reinforce slot is now populated. The Reinforcement
picker reads from FriendInfoList (populated by tojMy68W) and
ReinforcementInfoList (populated by xZH6EIQ7); we dual-emit both from
FriendGet to cover any UI path the binary takes.
- FriendGet returns {"xZH6EIQ7":[ReinforcementInfo],"tojMy68W":[FriendInfo]}
with a placeholder "DecompFriend" entry built from the player's
active-deck leader (falls back to highest-level unit). user_unit_id
set to a fake out-of-range value (999999) so post-battle cleanup
can't accidentally delete a real user_units row
- MissionStart + CampaignBattleStart also emit tojMy68W with the same
data (defensive; some UI paths may read FriendInfoList directly
during MissionStart's response phase)
- Earlier xZH6EIQ7 / T_FIXED_REINFORCEMENT emissions from
MissionStart removed — they're FriendGet's job now
- FriendInfo.element converted from user_units.element string back to
int (FriendInfo's typed setter takes uint32_t, NOT a string like
UserUnitInfo — see §8.34 for the contextual-overload table)
Confirmed working: squad-select picker shows DecompFriend; mission
battle includes the friend's unit in slot 6; Social tab Friend Info
dialog renders correctly.
## MST porting (handbook §8.37)
- 51 deploy/system/ JSONs renamed to *_mst.json (e.g. unit.json ->
unit_mst.json) for clearer cross-referencing against the IDA-decoded
MstResponse class names. All ServerCache.cpp load paths updated;
ServerCache.hpp / MigrationManager.cpp / UserInfo.cpp /
GachaAction.cpp / GatchaList.cpp use the new names
- 23 new MST ports added (skill_mst — 33k rows merged from
F_SKILL_MST_1..9, leader_skill_mst, item_mst, dungeon_mst,
area_mst, unit_evo_mst, unit_cgs_mst, etc.). On disk only — the
matching <Name>MstCache C++ types only exist when the
packet-generator KDL has them, and most of these don't have KDL
yet. Tracked as future work in handbook §8.15
## OfflineModController (frame-rate cap)
New /offline_mod/fps_cap endpoint returns the configured frame-rate
cap to the offline-proxy libcurl shim at startup. Reads from
plugins[0].config.server.fps_cap in deploy/config.json (default 60).
Required by the proxy's MyRender hook so the client doesn't burn CPU
at uncapped FPS in windowed mode.
## Release build / APPX deployment (handbook §9, BUILD.md)
The release-win32 preset (PROXYAPPX frontend, x86 target, embedded
into the BF game's APPX package) builds cleanly for the first time:
- vcpkg.json: drop drogon[ctl] feature. The drogon port marks ctl as
supports: "native"; vcpkg refuses to install on x86-windows-static
(cross-compile triplet). drogon_ctl is a CLI scaffolding helper
not referenced anywhere in our codebase
- game_frontend/bootstrap_windows.cpp: MSVC requires __declspec(dllexport)
BEFORE the return type, not between __stdcall and the function name.
The __APPX__-gated block had never been compiled before because
debug-win64 uses STANDALONE (no __APPX__ define) — the C2059 only
surfaced on the first release build
- new rebuild_release.bat — parallel to rebuild.bat but invokes
VsDevCmd.bat -arch=x86 -host_arch=amd64 (not -arch=amd64) and uses
the release-win32 preset. Auto-wipes partial CMakeCache on
reconfigure to self-heal the "compiler not found" cascade that
earlier failed vcpkg installs left behind
- BUILD.md gets a Release / APPX deployment build section + three
new troubleshooting entries (drogon[ctl], Ninja/compiler symptoms,
C2059 declspec)
## Submodule
Bumps packet-generator to bring in:
- net/friends.kdl: new FriendInfo (50 fields)
- net/reinforcement_info.kdl: ReinforcementInfo (47) + FixedReinforcementInfo (21)
- net/handlers.kdl: FriendGetResp dual emission, removes ReinforcementInfo stub
- Various MST KDL doc/type touch-ups from IDA audit cross-reference
See the submodule commit for the full audit-driven schema decode.
Pivots rebuild_release.bat from the niche release-win32 APPX build (which
embeds the server into the BF game's APPX package) to the much more
useful standalone-portable workflow. APPX is still documented in
BUILD.md for the rare case it's needed; rebuild_release.bat now builds
the everyday "drop a folder on a friend's machine and run gimuserverw.exe"
deliverable.
CMakeLists.txt:
- Add `install()` rules behind `if (STANDALONE)` to stage a portable
bundle (gimuserverw.exe + DLLs + config.json + system/) at the prefix
passed to `cmake --install`. game_content/ is intentionally NOT
bundled — it's game-owned static assets the operator drops in
separately. gme.sqlite is auto-created on first run by
MigrationManager. PDBs are excluded.
- DLL globbing uses an `install(CODE ...)` snippet that pulls whatever
applocal.ps1 already staged beside the exe — `$<TARGET_RUNTIME_DLLS>`
misses transitive deps like brotli / openssl / zlib that drogon
pulls in. applocal.ps1 needs dumpbin on PATH at build time, which
rebuild_release.bat ensures by sourcing VsDevCmd.bat.
- Disable glaze::glaze's vcpkg-imposed /GL and /LTCG. Those flags
propagate to every consumer, and MSVC's LTCG code-gen pass chokes on
the coroutine HANDLEF bodies in UserInfo.cpp / GachaAction.cpp with
C4737 -> LNK1257 ("invalid record in IL"). Tradeoff: lose cross-TU
inlining from glaze, gain a Release build that actually links.
rebuild_release.bat:
- 3-step flow: configure (debug-win64) -> build Release ->
`cmake --install` to out/build/release-win64/
- Wipes the dist dir between runs so deleted/renamed system files
don't linger
- Auto-detects partial / failed previous configure and wipes
CMakeCache before re-running (same self-heal as before)
- Sources `VsDevCmd.bat -arch=amd64 -host_arch=amd64` itself so it
works from any prompt
BUILD.md:
- Reorders the release sections: "Portable standalone release
(rebuild_release.bat)" first as the main path, "APPX deployment
build (release-win32)" second as the niche manual recipe
- Documents what's in the staged bundle vs what's NOT bundled and why
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebased to the packet generator
Added: