Skip to content

Packet generator quests#24

Open
Seltraeh wants to merge 14 commits into
decompfrontier:mainfrom
Seltraeh:packet-generator-quests
Open

Packet generator quests#24
Seltraeh wants to merge 14 commits into
decompfrontier:mainfrom
Seltraeh:packet-generator-quests

Conversation

@Seltraeh
Copy link
Copy Markdown
Contributor

Rebased to the packet generator
Added:

  • Friend + Friend Unit (Takes strongest leader on update)
  • Summoning added back from Tom's commit
  • Missions reward units now
  • Evolution
  • Fusion
  • Town + Facilities
  • Cutscenes
  • Gaining units is client correct now and removes the "New" badge from older units
  • Leveling up and user stats that are associated to the users current level
  • Energy consumption and recharging

Seltraeh and others added 14 commits April 19, 2026 20:36
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant