diff --git a/docs/global-reference.md b/docs/global-reference.md index 4664d01..fae9622 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -4277 globals (430 curated, 3847 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +4277 globals (431 curated, 3846 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -60,6 +60,7 @@ | `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. | | `0x6930` | unit_current_levels | high | investigation | Persistent current level by playable-unit id. CCINIT compares current_unit_id's row with each promotion threshold; ADDEXP increments and reports the same value. | | `0x69f8` | unit_current_stats | high | investigation | Persistent fourteen-stat row for each playable unit, using the same column order as unit_base_stats. CALCCC adds class_change_stat_bonuses to the current unit's row and clamps each result to the shared stat caps. Columns: 0=accuracy, 1=evasion, 2=physical_attack, 3=physical_defense, 4=magic_attack, 5=magic_defense, 6=speed, 7=luck, 8=critical_chance, 9=capture_power, 10=movement, 11=max_hp, 12=max_sp, 13=max_fs. | +| `0x6f70` | unit_stat_growth_fractions | high | investigation | Persistent fractional-growth row paired with each playable unit's unit_current_stats. ADDEXP adds unit_stat_growth_rates, TRAIN adds training_action_stat_growth_hundredths, and UNITECH applies level catch-up growth; each path awards the quotient divided by 100 to the current stat and retains the remainder modulo 100. GAMESTART serializes the rows, while GAMECLEAR and EVOLVE copy them with the rest of the persistent unit record. Columns: 0=accuracy, 1=evasion, 2=physical_attack, 3=physical_defense, 4=magic_attack, 5=magic_defense, 6=speed, 7=luck, 8=critical_chance, 9=capture_power, 10=movement, 11=max_hp, 12=max_sp, 13=max_fs. | | `0x74e8` | unit_skill_ids | high | investigation | Persistent four-skill row for each playable unit. CALCCC copies positive class_change_skill_awards into the first three slots after a promotion; ADDEXP compares the before/after row to report learned or replaced skills. Columns: 0=skill_slot_1, 1=skill_slot_2, 2=skill_slot_3, 3=skill_slot_4. | | `0x7684` | stage_clear_state | high | investigation | Persistent per-stage completion state indexed by stage id. STAGECLEAR sets the current stage cell to 1, while FORT, SELSTAGE, and FIELD use zero versus one to distinguish an uncleared mission from a cleared replay. | | `0x141a4` | stage_object_runtime_state | high | investigation | Per-stage, per-object-slot runtime state. FIELD initializes mode-1 objects from STINIT's first tagged payload on a fresh stage and updates capturable-object ownership; DRAWOBJ uses it as the vertical sprite-row index. RTN_M015 treats the state of Magic Pillar types 2..4 as their controlling faction and targets pillars whose value differs from the acting entity's faction. | @@ -3760,7 +3761,6 @@ | `0x24ce` | — | high | auto-shape | skill-desc-table | | `0x32f0` | — | med | auto-shape | current-entity-index? | | `0x3ebe` | — | med | auto-shape | record-table[stride 10] | -| `0x6f70` | — | med | auto-shape | record-table[stride 14] | | `0x7e54` | — | med | auto-shape | record-table[stride 50] | | `0x4e3d7` | — | med | auto-shape | record-table[stride 14] | | `0x53a2b` | — | med | auto-shape | record-table[stride 20] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index 0c86591..50ed5ca 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -710,9 +710,10 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape RTINIT's used-provider surface closed, ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined, INFOMES/MES classified, STINIT2's engine-dead authoring tier retained at medium confidence, and the 309-script ADV primary/alternate/transition surface-slot registry named, the selected - movement-route grid reconstructed, and CALCDMG/BTL's two-side-by-300 triggered-passive - matrix plus actor HP-recovery output closed, continue with consumer-led tables that still - have shipped readers; + movement-route grid reconstructed, CALCDMG/BTL's two-side-by-300 triggered-passive + matrix plus actor HP-recovery output closed, and the persistent 100-by-14 unit stat-growth + fraction table joined to level-up, training, catch-up, save/load, and unit-copy consumers, + continue with consumer-led tables that still have shipped readers; never assign one universal meaning to a parameter bank whose meaning varies by provider selector. 2. **Label remaining 2D record tables by their readers** — cross-reference which scripts read each `rec[sN]` table and infer purpose from context, preserving reserved rows and sparse cells. RECOVER's diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 780ccd4..1b99a0f 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3298,3 +3298,33 @@ presentation consumers, and the actor-recovery producer/consumer pair. **Next:** investigate the highest-use remaining auto-shaped row table, the stride-14 table at `G[0x6f70]` (27 references across ADDEXP, EVOLVE, GAMECLEAR, GAMESTART, TRAIN, and UNITECH). + +## Data-semantics sidebar: persistent unit stat-growth fractions (2026-07-24) + +The former stride-14 auto table at `G[0x6f70]` is the `100 x 14` +`unit_stat_growth_fractions` table. It is the fractional half of the persistent playable-unit stat +record: the preceding `unit_current_stats` table has the same 100-row geometry, and both use the +standard accuracy-through-max-FS fourteen-column ABI. The table ends exactly where the following +100-by-4 `unit_skill_ids` block begins. + +Three independent growth paths prove the value scale and role. ADDEXP adds the unit definition's +`unit_stat_growth_rates`; TRAIN adds the selected action's +`training_action_stat_growth_hundredths`; and UNITECH multiplies the definition growth rate by its +level catch-up factor. Each path divides the accumulated cell by 100, adds that quotient to the +matching `unit_current_stats` cell, and retains the remainder modulo 100. ADDEXP and TRAIN can also +convert the surviving positive fraction into a probabilistic extra point, while all three paths +clamp the integer result to the shared fourteen-stat caps. + +The fractions are durable unit state rather than temporary growth work. GAMESTART serializes each +cell immediately after its matching current-stat cell. GAMECLEAR copies complete fourteen-cell +rows during its unit-record transfer and registers all 100 rows with the shared profile; EVOLVE +copies the same rows between Lily forms. The complete corpus surface is exactly 27 two-dimensional +lookups across these six scripts. + +Evidence regressions protect all 27 offsets and their stride, the three source/add/divide/modulo +growth paths, the current-stat join, both save/load sites, both complete-row copy paths, and the +shared-profile registration. + +**Next:** investigate the highest-use remaining auto-shaped row table, the stride-10 table at +`G[0x3ebe]` (21 references across CALCREVISE, CHMENU, DRAWTIP, GAMECLEAR, GAMESTART, IMPROVE, and +TUNE). diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index bdd1af1..12d8a90 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -72,8 +72,9 @@ - [x] **2.3o — Name the ADV layer surface-slot registry.** `G[0x3239]` is an eight-by-three primary/alternate/transition surface bank initialized by 143 scripts and read across 309. The common CG loader proves double-buffer selection and transition staging; INIT2 supplies the paired retained-object handles, and the companion resource, layer-index, live-slot, and handle globals are curated. - [x] **2.3p — Name the selected movement-route grid.** `G[0xc6077]` is a reserved 1000-by-27 work grid carrying the one-cell-wide route copied from MVSEEK's remaining-step scores. SETROUTE proves the cardinal backtrace, FIELD proves route rendering and execution, SELACT proves move-before-action use, and RTN_M002..018 prove candidate-endpoint testing. The shared map target coordinates and INIT2 cardinal vectors are curated with it. - [x] **2.3q — Name the triggered passive-skill matrix.** `G[0x15261f]` is a two-side-by-300 SKINIT-indexed activation matrix rebuilt for each CALCDMG exchange. CALCDMG gates owned category-4 passives by actor/target role, proc chance, species/boss/movement/action conditions, and pairwise exclusions; BTL uses the surviving flags for passive icons, reaction animations, and passive combat behavior. The adjacent actor HP-recovery output is curated with it. +- [x] **2.3r — Name persistent unit stat-growth fractions.** `G[0x6f70]` is the 100-by-14 fractional-hundredths table paired with `unit_current_stats`. ADDEXP, TRAIN, and UNITECH add definition, action, and catch-up growth, carry each quotient divided by 100 into the matching current stat, preserve the modulo-100 remainder, and clamp against the shared stat caps. GAMESTART serializes it, while GAMECLEAR and EVOLVE copy complete rows with the persistent unit record. - [x] **2.4 — Partial global-var map BUILT + wired into the disassembler.** `tools/global_map.py` → `build/global-var-map.{json,md}` (16,354/49,435 globals labelled: string tables, `*INIT` field arrays, 122 record tables w/ strides, current-entity index pointers). `sys4load` renders the labels inline (`=rec[s30]`, `=current-entity-index?`). See `docs/name-resolution.md`. -- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: RTINIT is closed at 1,043/1,043 movement steps; the RECOVER/ILINIT condition ABI, ALINIT recipes, AFINIT affinity/progression tables, CTINIT name palette, CVINIT character-voice registry, LAINIT terrain definitions, MPINIT stage-terrain atlas, SPINIT H-scene gallery, TRINIT training actions, CDINIT/CDINIT2 card selection/effects, BTANINIT/BTANINIT2 battle animations, STINIT2's complete stage surface, the 309-script ADV layer surface-slot registry, the selected movement-route grid, and the battle triggered-passive matrix are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, continue remaining consumer-led 2D table naming. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, the shipped RTINIT movement-provider join, and the six completed message joins are resolved. +- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: RTINIT is closed at 1,043/1,043 movement steps; the RECOVER/ILINIT condition ABI, ALINIT recipes, AFINIT affinity/progression tables, CTINIT name palette, CVINIT character-voice registry, LAINIT terrain definitions, MPINIT stage-terrain atlas, SPINIT H-scene gallery, TRINIT training actions, CDINIT/CDINIT2 card selection/effects, BTANINIT/BTANINIT2 battle animations, STINIT2's complete stage surface, the 309-script ADV layer surface-slot registry, the selected movement-route grid, the battle triggered-passive matrix, and persistent unit stat-growth fractions are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, investigate stride-10 `G[0x3ebe]`, the highest-use remaining auto-shaped row table. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, the shipped RTINIT movement-provider join, and the six completed message joins are resolved. ## Phase 3 — Name the unnamed opcodes *(top ~20 BEFORE Phase 4; the rest on demand)* diff --git a/tools/test_globals.py b/tools/test_globals.py index e2d37d9..dedb5e9 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -31,6 +31,10 @@ def test_load_and_lint(): "entity activity and faction arrays are curated") check(entries[0x4e693]["name"] == "entity_skill_flags", "per-entity skill flags are curated") + check(entries[0x6f70]["name"] == "unit_stat_growth_fractions" + and entries[0x6f70]["type"] == "int[100][14]" + and entries[0x6f70]["columns"]["13"] == "max_fs", + "persistent unit stat-growth fractions are curated") check(entries[0x53e13]["name"] == "entity_carried_item_ids" and entries[0x53e77]["name"] == "entity_carried_item_counts", "per-entity carried-item slots are curated") @@ -417,6 +421,105 @@ def test_battle_triggered_passive_skill_flags_evidence(): if name == "BTL.BIN"), "CALCDMG computes actor recovery and BTL adds it to the acting entity's HP") +def test_unit_stat_growth_fractions_evidence(): + target = 0x6f70 + expected_offsets = { + "ADDEXP.BIN": {0x1ea, 0x200, 0x212, 0x241, 0x24c, 0x263, 0x2a0}, + "EVOLVE.BIN": {0xa2a, 0xa3a}, + "GAMECLEAR.BIN": {0x331, 0x341, 0x417}, + "GAMESTART.BIN": {0x942, 0xac5}, + "TRAIN.BIN": {0x901, 0x917, 0x929, 0x958, 0x963, 0x975, 0x9a5, 0x9db}, + "UNITECH.BIN": {0x403, 0x40e, 0x420, 0x44f, 0x45a}, + } + refs = [] + loaded = {} + for name, path in paths.scripts().items(): + if path.read_bytes()[:8] != b"SYS4422 ": + continue + script = sys4load.load(path) + loaded[name] = script + refs.extend( + (name, instruction) + for instruction in script.instructions + if (3, target) in instruction.args + ) + + actual_offsets = {} + for name, instruction in refs: + actual_offsets.setdefault(name, set()).add(instruction.offset) + check(actual_offsets == expected_offsets + and len(refs) == 27 + and all(instruction.opcode == 0x12c + and instruction.args[3] == (0, 14) + for _, instruction in refs), + "stat-growth fractions have exactly 27 stride-14 lookups in six scripts") + + instructions = { + name: {instruction.offset: instruction for instruction in loaded[name].instructions} + for name in expected_offsets + } + growth_paths = { + "ADDEXP.BIN": { + "source": (0x1f5, 0x7e5e6), + "add": 0x20b, + "divide": 0x21d, + "current": 0x224, + "modulo": 0x257, + }, + "TRAIN.BIN": { + "source": (0x90c, 0x155e9b), + "add": 0x922, + "divide": 0x934, + "current": 0x93b, + "modulo": 0x96e, + }, + "UNITECH.BIN": { + "source": (0x3f1, 0x7e5e6), + "add": 0x419, + "divide": 0x42b, + "current": 0x432, + "modulo": 0x465, + }, + } + growth_mechanics_ok = True + for name, path in growth_paths.items(): + script = instructions[name] + source_offset, source_address = path["source"] + growth_mechanics_ok &= ( + script[source_offset].opcode == 0x12c + and script[source_offset].args[1] == (3, source_address) + and script[path["add"]].label == "add" + and script[path["divide"]].label == "div" + and script[path["divide"]].args[-1] == (0, 100) + and script[path["current"]].opcode == 0x12c + and script[path["current"]].args[1] == (3, 0x69f8) + and script[path["modulo"]].label == "mod" + and script[path["modulo"]].args[-1] == (0, 100) + ) + check(growth_mechanics_ok, + "level-up, training, and catch-up growth carry hundredths into current stats") + + persistence_ok = all( + instructions["GAMESTART.BIN"][offset + 0xb].label == "string-lookup-set" + for offset in expected_offsets["GAMESTART.BIN"] + ) + for name, first, second, copy in ( + ("GAMECLEAR.BIN", 0x331, 0x341, 0x351), + ("EVOLVE.BIN", 0xa2a, 0xa3a, 0xa4a), + ): + script = instructions[name] + persistence_ok &= ( + script[first + 0xb].opcode == 0x63 + and script[second + 0xb].opcode == 0x63 + and script[copy].opcode == 0x1b0 + and script[copy].args[-1] == (0, 14) + ) + persistence_ok &= ( + instructions["GAMECLEAR.BIN"][0x422].opcode == 0x1a2 + ) + check(persistence_ok, + "save/load, game-clear, and evolution preserve the complete fractional rows") + def test_merge_precedence(): curated, _ = G.load_toml(paths.VM_MAP / "globals.toml") auto = G.load_auto(paths.BUILD / "global-var-map.json") @@ -482,6 +585,7 @@ if __name__ == "__main__": test_adv_layer_surface_slot_evidence() test_selected_movement_route_grid_evidence() test_battle_triggered_passive_skill_flags_evidence() + test_unit_stat_growth_fractions_evidence() test_merge_precedence() test_sys4load_labels_from_registry() test_miner_finds_known_flags() diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 4b81408..2f59675 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -2152,6 +2152,18 @@ usage = "Persistent fourteen-stat row for each playable unit, using the same col source = "investigation" confidence = "high" +[[global]] +address = "0x6f70" +name = "unit_stat_growth_fractions" +category = "data-table" +type = "int[100][14]" +columns = { "0" = "accuracy", "1" = "evasion", "2" = "physical_attack", "3" = "physical_defense", "4" = "magic_attack", "5" = "magic_defense", "6" = "speed", "7" = "luck", "8" = "critical_chance", "9" = "capture_power", "10" = "movement", "11" = "max_hp", "12" = "max_sp", "13" = "max_fs" } +value_domain = "signed fractional hundredths carried between stat changes; normalized remainder magnitude below 100" +usage = "Persistent fractional-growth row paired with each playable unit's unit_current_stats. ADDEXP adds unit_stat_growth_rates, TRAIN adds training_action_stat_growth_hundredths, and UNITECH applies level catch-up growth; each path awards the quotient divided by 100 to the current stat and retains the remainder modulo 100. GAMESTART serializes the rows, while GAMECLEAR and EVOLVE copy them with the rest of the persistent unit record." +source = "investigation" +confidence = "high" +depends_on = ["0x69f8", "0x7e5e6", "0x155e9b"] + [[global]] address = "0x74e8" name = "unit_skill_ids"