diff --git a/docs/global-reference.md b/docs/global-reference.md index fae9622..984f069 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -4277 globals (431 curated, 3846 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +4278 globals (435 curated, 3843 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -33,6 +33,7 @@ | `0x6726` | training_action_total_execution_count | high | investigation | TRAIN increments this once after every successful action, independently of the selected action's own execution count. DRAWCHP displays it with the familiar's other training statistics and SAVE persists it. | | `0x6727` | training_action_execution_counts | high | investigation | Per-action completed execution counts. TRAIN uses the prior count as the column in training_action_event_story_flag_ids, increments the selected cell, and treats a zero next event as the cap; GAMESTART restores each count and replays its completed event flags. | | `0x204f4` | current_stage_turn | high | investigation | FIELD initializes this to 1, increments it after end-of-turn processing, compares it with STINIT's stage turn limit and timed object schedules, and displays it through DRAWCHP. Card generation divides it by each CDINIT growth interval to increase that card's weighted-selection share. | +| `0x204f8` | party_reference_level | high | investigation | FIELD's enemy auto-level reference. It averages all eligible party units when at most five exist, otherwise the five highest-level units. SETEN raises scalable enemies above unit_starting_level by the positive gap divided by the selected auto-level divisor before applying stage level clamps. Difficulty separately adds -5/0/+5 stat-growth iterations and does not change this stored runtime level. | | `0x2052e` | scjump_progress_b | med | inference | Second SCJUMP switch input (1223 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | | `0x20530` | current_spirit | high | investigation | Current 精気 (spirit/essence) resource. TRAIN rejects a selected action when adding its negative TRINIT spirit delta would fall below zero, deducts the cost on execution, and passes the updated value to DRAWCHP. Field and scene reward paths update the same resource and clamp it to maximum_spirit. | | `0x20534` | maximum_spirit | high | investigation | Maximum 精気 capacity paired with current_spirit. TRAIN clamps the post-cost current value against it, DRAWCHP renders the current/maximum gauge, and field/scene reward paths raise or restore the same capacity. | @@ -44,7 +45,6 @@ | `0xcc9f2` | usable_action_min_range | high | investigation | CALCSCOPE initializes this from the equipped/default attack's minimum range and widens it for usable offensive and healing skills. It is the lower bound shared by the offensive and healing action-scope tables. | | `0xcc9f3` | usable_action_max_range | high | investigation | CALCSCOPE initializes this from the equipped/default attack's maximum range and widens it for usable offensive and healing skills. ATSEEK bounds its action-range flood fill with this value; RTN_M051/052 and RTN_M061 use it as their upper scan bound. | | `0xe6c5d` | scjump_progress_d | med | inference | SCJUMP switch input (168 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | -| `0x152618` | scjump_progress_c | med | inference | Third SCJUMP switch input (530 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | | `0x152619` | battle_exchange_step | high | investigation | BTL's six-step alternating exchange counter. Dividing it by two selects the first, second, or third ordinary-attack/skill voice variant and related animation slots. | ## data-table @@ -59,6 +59,7 @@ | `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. | | `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. | +| `0x6994` | unit_experience_progress | high | investigation | Persistent experience progress by playable-unit id. ADDEXP adds the current award, animates the increase to 100, raises unit_current_levels, resets this cell to zero, and repeats while award remains and the unit is below unit_level_cap. | | `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. | @@ -66,7 +67,9 @@ | `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. | | `0x20543` | tile_faction_traversal_masks | high | investigation | Row-major tile traversal permissions. Movement checks the acting entity's faction bit before enqueuing a tile in movement-limited MVSEEK searches. RTN_M013 builds either one selected faction bit or all bits except the actor's, requires the current tile not to match that set, and approaches the nearest reachable tile whose mask does. | | `0x341ab` | current_stage_terrain_grid | high | investigation | Current stage's mutable terrain grid. FIELD clears all 2,000 rows and copies the selected stage_terrain_atlas rectangle; rendering, minimap, occupancy, battle, and movement scripts read it, while SETLAND/DELLAND alter cells and RESETLAND restores atlas values. | +| `0x4dfbd` | entity_unit_definition_ids | high | investigation | Definition id backing each runtime map entity. SETEN writes current_unit_id when materializing an enemy; BTL uses the opposing entity's value to read unit_experience_reward and drop tables, while ADDEXP uses the faction-1 participant's value to select persistent playable-unit state. | | `0x4e021` | entity_runtime_flags | high | investigation | Per-runtime-entity state flags. RTN_M007 requires bit 0 when selecting an injured ally, and MVSEEK mode 2 requires it before masking a foreign entity's occupied terrain cell. | +| `0x4e053` | entity_levels | high | investigation | Displayed/runtime level for each map entity. SETEN begins enemies at unit_starting_level, applies party-reference auto-scaling and stage minimum/maximum clamps, and BTL subtracts the surviving faction-1 participant's level from the opponent's level to choose the experience multiplier bracket. | | `0x4e085` | entity_current_resources | high | investigation | Per-runtime-entity current resources. DRAWCHP/DRAWENP render columns 0..2 against max-stat columns 11..13, skill/item resource deltas modify the same three columns, and movement search compares path cost with current_fs. Columns: 0=current_hp, 1=current_sp, 2=current_fs. | | `0x4e11b` | entity_effective_stats | high | investigation | Per-runtime-entity effective fourteen-stat row after CALCREVISE applies base data, equipment, skills, and conditions. Combat/UI consumers use columns 0..10 directly; columns 11..13 are the maxima paired with entity_current_resources HP/SP/FS. 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. | | `0x4e693` | entity_skill_flags | high | investigation | Per-runtime-entity skill-state row. CALCREVISE and CALCSCOPE mark equipped skills, combat/field consumers test specific skill ids, and RTN_M009 requires skill 22 (Unlock) before treating an unopened type-7 chest as a treasure target. | @@ -141,7 +144,7 @@ | `0x7ab4e` | unit_auto_level_scale_divisor | high | investigation | Default level-scaling divisor for 192 enemy definitions. SETEN uses a scenario override when supplied, otherwise divides the difference between scenario level and unit_starting_level by this value, adds the result to the runtime level, and clamps it to unit_level_cap. | | `0x7af36` | unit_base_stats | high | investigation | EBINIT fourteen-column base-stat record copied wholesale into each runtime unit by SETEN, UNITECH, and SALLY. Its layout matches item_stat_modifiers: columns 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 10 movement, 11 max HP, 12 max SP, and 13 max FS; accuracy/evasion and other unpopulated columns begin at zero. 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. | | `0x7e5e6` | unit_stat_growth_rates | high | investigation | EBINIT per-level growth record with the same fourteen-column layout as unit_base_stats. ADDEXP adds each rate to a fractional accumulator, divides by 100 to award whole stat points, and retains the remainder; SETEN and UNITECH apply the same rates when materializing units above their starting level. 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. | -| `0x83406` | unit_experience_reward | high | investigation | Base experience award for 164 enemy records. BTL scales this value by the victor/defeated level difference and difficulty mode, stores the result in the ADDEXP input channel, and invokes ADDEXP for the winning unit. | +| `0x83406` | unit_experience_reward | high | investigation | Per-opponent base experience progress. After each battle exchange, BTL awards a surviving faction-1 participant against the opposing unit definition. If the opponent survives, enemy-minus-player level differences >=3/2/1/<=0 select 40/30/20/10 percent. If the opponent is defeated, differences >=3/2/1/0/-1/-2/<=-3 select 250/200/150/100/75/40/20 percent. Integer division truncates. The result enters ADDEXP, whose normally cleared modifier bits can suppress, double, or multiply it by ten. A cell with no writer in the ordered INIT sequence remains zero; append unit 900 is such a cell and therefore awards zero at every bracket. | | `0x837ee` | unit_drop_item_ids | high | investigation | Enemy drop table. Values cross-resolve to ITINIT items (for example treasure puttetto drops bronze/silver/gold coins and planet stones); BTL rolls and awards the rows, while INFOEN displays the possible drops. Columns: 0=drop_1_item_id, 1=drop_2_item_id, 2=drop_3_item_id, 3=drop_4_item_id, 4=drop_5_item_id, 5=drop_6_item_id, 6=drop_7_item_id, 7=drop_8_item_id. | | `0x8572e` | unit_drop_chance_percent | high | investigation | Per-slot drop chance paired with unit_drop_item_ids. BTL draws random-modulo 100 for each populated slot and awards the item when the result is below this value; only columns 0..4 are populated in shipped EBINIT. Columns: 0=drop_1_percent, 1=drop_2_percent, 2=drop_3_percent, 3=drop_4_percent, 4=drop_5_percent, 5=drop_6_percent, 6=drop_7_percent, 7=drop_8_percent. | | `0x8766e` | unit_large_battle_sprite | high | investigation | Presentation flag for 33 large demons, dragons, gods, and their variants. BTL and INFOEN use it to anchor the CB battle figure at the lower screen edge and omit the ordinary-unit framing treatment required by smaller sprites. | @@ -366,7 +369,6 @@ | `0x3ad6` | — | low | auto-shape | array | | `0x6804` | — | low | auto-shape | array | | `0x6868` | — | low | auto-shape | array | -| `0x6994` | — | low | auto-shape | array | | `0x7a6c` | — | low | auto-shape | array | | `0x204fb` | — | low | auto-shape | array | | `0x2052f` | — | low | auto-shape | array | @@ -374,9 +376,7 @@ | `0x20537` | — | low | auto-shape | array | | `0x2053b` | — | low | auto-shape | array | | `0x2053f` | — | low | auto-shape | array | -| `0x4dfbd` | — | low | auto-shape | array | | `0x4dfef` | — | low | auto-shape | array | -| `0x4e053` | — | low | auto-shape | array | | `0x521f3` | — | low | auto-shape | array | | `0x52225` | — | low | auto-shape | array | | `0x52257` | — | low | auto-shape | array | @@ -3747,6 +3747,7 @@ | `0xab8e9` | class_change_stat_bonuses | high | investigation | Fourteen-cell class-change accumulator using the unit stat column order. CALCCC zeroes it, CCINIT adds the selected rule's bonuses, and CALCCC adds the result to unit_current_stats with per-column caps. EVOLVE separately clears it and calls CCINIT to preview the next Lily form's movement bonus. 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. | | `0xab8f7` | class_change_skill_awards | high | investigation | Four-cell class-change skill output. CALCCC zeroes the buffer, CCINIT writes awarded skill ids, and CALCCC copies positive values from the first three cells into unit_skill_ids before ADDEXP reports the changes. Columns: 0=skill_slot_1, 1=skill_slot_2, 2=skill_slot_3, 3=skill_slot_4. | | `0xab8fb` | class_change_state_work | high | investigation | Ten-cell working copy of the current unit's unit_class_change_state row. CCINIT sets the slot belonging to an awarded rule; CALCCC manages the block copy between this buffer and persistent per-unit state. Columns: 0=promotion_slot_1, 1=promotion_slot_2, 2=promotion_slot_3, 3=promotion_slot_4, 4=promotion_slot_5, 5=promotion_slot_6, 6=promotion_slot_7, 7=promotion_slot_8, 8=promotion_slot_9, 9=promotion_slot_10. | +| `0x152618` | battle_outcome_flags | high | investigation | BTL derives the low three bits from the acting and target entities' post-exchange HP. Mask 4 selects the nonlethal experience table; otherwise BTL uses the defeat table. FIELD consumes and combines the result during post-battle flow, then passes it to SCJUMP, whose former progress-C comparisons are battle-outcome decisions. | | `0x15261c` | battle_hit_result | high | investigation | CALCDMG initializes this to miss, changes it to hit after the accuracy gate, and then to critical after the critical-chance gate. BTL uses the same result to choose presentation, cut-ins, and critical voice columns; -1 is written only by a synthetic pre-battle path. | | `0x15261d` | battle_hp_delta | high | investigation | CALCDMG's signed HP result. BTL subtracts it from the target's current HP, so positive values deal damage and negative values heal; zero and result state gate damage reactions and defeat handling. | | `0x15261e` | battle_actor_hp_recovery | high | investigation | CALCDMG's actor-side HP recovery output. Absorb (SKINIT skill 40) contributes half of positive battle_hp_delta with a minimum of one, and the target's HP-absorption condition can add its level-scaled amount capped at the damage dealt. BTL adds the result to the actor's current HP, renders the green recovery number, and clamps the resource through the ordinary post-battle path. | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index 674d14a..e724049 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -740,6 +740,57 @@ ordering remains unresolved. Whole-pack enumeration and ordered base/append assi until that engine-owned launch boundary is proven. The AAI format and verified internal call sequence are canonical in `docs/asset-resolution-re.md`; command usage is canonical in `docs/tools-reference.md`. +### Battle experience pipeline (2026-07-24) + +BTL awards experience after every completed combat exchange, not only after a kill and not once per +individual strike in a multi-hit animation. It first chooses the surviving faction-1 participant, reads +the opposing runtime entity's EBINIT definition id through `entity_unit_definition_ids`, and loads that +definition's `unit_experience_reward`. The level difference is: + +`opponent entity level - faction-1 entity level` + +BTL selects a percentage from one of two integer tables: + +| level difference | opponent survives | opponent defeated | +|---:|---:|---:| +| `>= 3` | 40% | 250% | +| `2` | 30% | 200% | +| `1` | 20% | 150% | +| `0` | 10% | 100% | +| `-1` | 10% | 75% | +| `-2` | 10% | 40% | +| `<= -3` | 10% | 20% | + +The award is `unit_experience_reward * percentage / 100` with integer truncation. BTL derives the +opponent-survived branch from `battle_outcome_flags`: bit 0 means the target was defeated, bit 1 means +the actor was defeated, and bit 2 means both survived. The surviving player-side participant receives +the result through ADDEXP. ADDEXP rejects zero awards or units already at `unit_level_cap`, adds the +award to persistent `unit_experience_progress`, and carries any remainder through successive 100-point +level-up boundaries. Normally cleared configuration/debug bits `0x10`, `0x20`, and `0x40` respectively +suppress, double, or multiply the award by ten; no shipped base or append script enables them during +ordinary play. + +An omitted EBINIT reward write is not a general fragment-level declaration of zero: additive INIT +fragments can intentionally leave an earlier value unchanged. For append unit 900 +`ヘタレアースマン`, however, the base EBINIT has no unit-900 row and no base or append writer targets +its reward cell. The zero-initialized global therefore remains zero, so every level bracket awards +zero. The append profile correctly leaves `unit_experience_reward` absent because that is the authored +write provenance; the effective value is zero only after considering the complete writer history. + +Append stage 200 explains `キングプテテット`'s observed 6–10 experience per exchange without a +special battle flag. Unit 901 (`キングプテテット本体/BOSS`) has base reward 50, while unit 905 +(`キングプテテット分身/BOSS`) has base reward 20. The stage places one body and two splits and clamps +all three to levels 70..99. Consequently, a surviving body awards 20/15/10/5 at level differences +`>=3`/`2`/`1`/`<=0`, while a surviving split awards 8/6/4/2. The commonly seen 10 can therefore be +the body one level above the attacker, and 6 can be a split two levels above. + +FIELD computes `party_reference_level` as the integer average of every eligible party member when +there are at most five, or the five highest levels otherwise. SETEN starts an enemy from +`unit_starting_level`, raises scalable enemies toward that party reference using the stage/unit divisor, +then applies the stage minimum and maximum clamps. Difficulty separately contributes -5/0/+5 stat-growth +iterations to enemy stats; it does not alter the stored `entity_levels` value used by BTL and therefore +does not directly select an experience percentage. + Re-run `tools/global_map.py` after each increment; `sys4load` picks up the new labels automatically (it reads `build/global-var-map.json` at load). diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index e0953fe..096ea26 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3348,3 +3348,32 @@ level/base-stat/growth fields, and its packed battle-sprite id. This closes the immediate “inspect an append unit semantically” need without asserting that the runtime has executed `$1$AUTORUN.BIN`. Ordered whole-pack merging and natural port bootstrap still depend on the unresolved native AUTORUN launch boundary documented in `docs/asset-resolution-re.md`. + +## Data-semantics sidebar: battle experience rewards (2026-07-24) + +BTL now proves the complete experience-award path. After each combat exchange it identifies a surviving +faction-1 participant, reads the opposing runtime entity's EBINIT definition and base +`unit_experience_reward`, and applies an enemy-minus-player runtime-level bracket. Surviving opponents use +40/30/20/10 percent for differences `>=3`/`2`/`1`/`<=0`; defeated opponents use +250/200/150/100/75/40/20 percent for `>=3`/`2`/`1`/`0`/`-1`/`-2`/`<=-3`. Integer division truncates. +ADDEXP then advances the persistent 0..99 progress cell, carries excess through level-ups, and stops at +the EBINIT level cap. + +The append examples close both motivating questions. Unit 900 `ヘタレアースマン` has no reward write, +no base unit-900 definition, and no earlier writer for that cell, so its effective zero-initialized reward +is zero. Unit 901 `キングプテテット本体/BOSS` writes 50 and unit 905 +`キングプテテット分身/BOSS` writes 20. Append stage 200 places one body and two splits with level +clamps 70..99, producing nonlethal body awards 20/15/10/5 and split awards 8/6/4/2 across the four level +brackets. The observed 6–10 per exchange is therefore ordinary table behavior, not a King-specific flag. + +FIELD and SETEN establish the level input: eligible party levels are averaged (top five when necessary), +enemy definitions auto-scale upward from their starting level, and stage minimum/maximum clamps apply. +Difficulty's -5/0/+5 adjustment affects stat-growth iterations separately and does not change the runtime +level BTL compares. The global registry now names the runtime definition ids, runtime entity levels, +party reference level, persistent experience progress, and BTL outcome flags; evidence regressions protect +the formula constants, ADDEXP gates and modifiers, level construction, append rewards, and stage-200 spawn +composition. + +**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/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index ba18e73..947f235 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -259,7 +259,7 @@ event may compose intentionally. Mechanical overlap is valuable evidence, not a Improve runtime diagnosis by attributing normal-API behavior to the responsible mod: registered hook points and callback order, named globals/state read or changed, event values before/after each callback, asset/script resolution, cancellation/control-flow overrides and exceptions. An optional development -trace should make chains such as `base 50 -> difficulty mod 35 -> double-exp mod 70` explicit. Raw VM +trace should make chains such as `base 50 -> level-gap mod 25 -> double-exp mod 50` explicit. Raw VM access remains less observable and should be marked advanced/opaque rather than falsely analyzed. Raw opcode hooks remain available as the expert escape hatch, but their filters should be declarative diff --git a/docs/scjump-progression.md b/docs/scjump-progression.md index 0861234..e054dc0 100644 --- a/docs/scjump-progression.md +++ b/docs/scjump-progression.md @@ -8,7 +8,8 @@ readable, VM-verified table. - **Top switch** on `0x3234` (`chapter_mode`, 1..9) dispatches to a per-chapter block (`1→0x81, 2→0x9f, 3→0x1ded, 4→0x1e6d, 5→0x54c1, 6→0x93c4, 7→0x1e868, 8→0x2aa42, 9→0x2b8a3`). - Each chapter block is an **acyclic tree** (0 back-edges) of comparisons on progress counters - (`scjump_progress_a/b/c/d` = `0x4dfbc`/`0x2052e`/`0x152618`/`0xe6c5d`), story flags, **and a native + (`scjump_progress_a/b/d` = `0x4dfbc`/`0x2052e`/`0xe6c5d`), `battle_outcome_flags` + (`0x152618`; low bits target-defeated/actor-defeated/both-survived), story flags, **and a native computed value** (see below). - At each of **1755 decision sites** it writes `0x0 = 1` (decision-valid) and `0x62ccf = ` (847 distinct values, 0..1693). diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index 12d8a90..9634a6a 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -73,8 +73,9 @@ - [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.3s — Decode battle experience rewards.** BTL awards after each completed exchange using the opponent definition's EBINIT base reward and enemy-minus-player runtime-level brackets: nonlethal 40/30/20/10 percent and defeat 250/200/150/100/75/40/20 percent, truncated to an integer. ADDEXP applies normally cleared suppression/multiplier bits and advances persistent 0..99 progress through capped level-ups. Append unit 900's unwritten reward is proven effective zero; stage 200's level-70+ unit-901 body (50) and unit-905 splits (20) explain the ordinary 6–10 per-exchange King Pterodactyl awards. - [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, 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. +- [ ] **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, persistent unit stat-growth fractions, and battle experience pipeline 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_extract_init.py b/tools/test_extract_init.py index 751e2b6..39f0417 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -140,6 +140,32 @@ def test_append_ebinit_fragment() -> None: by_id[81]["semantic_fields"]["unit_battle_sprite_asset_id"] == 0x0100002D, "append unit 81 retains its selector-keyed battle sprite resource id", ) + check( + "unit_experience_reward" not in by_id[900]["semantic_fields"] + and by_id[901]["semantic_fields"]["unit_experience_reward"] == 50 + and by_id[905]["semantic_fields"]["unit_experience_reward"] == 20, + "append Earthman omits XP while both King Pterodactyl definitions set rewards", + ) + + +def test_append_king_pterodactyl_stage() -> None: + records, _ = extract_init.extract_mixed( + extract_init.load_packed_script(0x01000050) + ) + extract_init.attach_stage_enemy_spawns(records) + stage = next(record for record in records if record["id"] == 200) + spawns = stage["enemy_spawns"] + + check( + [spawn["unit_id"] for spawn in spawns] + == [901, 900, 900, 900, 900, 900, 905, 905], + "append stage 200 places the King body, five Earthmen, and two split bodies", + ) + check( + [(spawn["min_level"], spawn["max_level"]) for spawn in spawns] + == [(70, 99)] + [(1, 50)] * 5 + [(70, 99)] * 2, + "append stage 200 keeps both King definitions in the level-70-to-99 bracket", + ) def test_character_profiles() -> None: @@ -2331,6 +2357,7 @@ def test_field_semantics() -> None: if __name__ == "__main__": test_real_name_tables() test_append_ebinit_fragment() + test_append_king_pterodactyl_stage() test_character_profiles() test_magic_actions() test_static_negative_write() diff --git a/tools/test_globals.py b/tools/test_globals.py index dedb5e9..46a9537 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -57,6 +57,12 @@ def test_load_and_lint(): and entries[0x66714]["name"] == "target_entity_index" and entries[0x52289]["name"] == "entity_selected_action_ids", "AI actor, target, and selected action state are curated") + check(entries[0x4dfbd]["name"] == "entity_unit_definition_ids" + and entries[0x4e053]["name"] == "entity_levels" + and entries[0x6994]["name"] == "unit_experience_progress" + and entries[0x204f8]["name"] == "party_reference_level" + and entries[0x152618]["name"] == "battle_outcome_flags", + "battle experience and outcome state are curated") check(entries[0xcca08]["name"] == "offensive_action_attack_elements" and entries[0xab5ba]["name"] == "attack_element_effectiveness_percent", @@ -520,6 +526,94 @@ def test_unit_stat_growth_fractions_evidence(): check(persistence_ok, "save/load, game-clear, and evolution preserve the complete fractional rows") +def test_battle_experience_evidence(): + loaded = { + name: { + instruction.offset: instruction + for instruction in sys4load.load(paths.scripts()[name]).instructions + } + for name in ( + "BTL.BIN", "ADDEXP.BIN", "FIELD.BIN", "GAMESTART.BIN", "SETEN.BIN" + ) + } + btl = loaded["BTL.BIN"] + + check( + btl[0x3667].args[1] == (3, 0x4dfbd) + and btl[0x366e].args[1] == (3, 0x83406) + and btl[0x367a].args[1] == (3, 0x4e053) + and btl[0x3681].args[1] == (3, 0x4e053) + and btl[0x3688].label == "sub", + "BTL selects the opponent definition reward and computes the runtime level gap", + ) + check( + btl[0x35ae].args[1] == (3, 0x4e085) + and btl[0x35cf].args[1] == (3, 0x4e085) + and btl[0x35c7].args == [(3, 0x152618), (0, 0)] + and btl[0x35e8].args == [(3, 0x152618), (0, 1)] + and btl[0x35f0].args == [(3, 0x152618), (0, 2)] + and btl[0x368f].args == [(9, 49), (3, 0x152618), (0, 4)], + "BTL derives defeated/survived outcome bits from both combatants' HP", + ) + + nonlethal = { + 0x36ab: 40, + 0x36c1: 30, + 0x36d7: 20, + 0x36df: 10, + } + defeated = { + 0x36f5: 250, + 0x370b: 200, + 0x3721: 150, + 0x3737: 100, + 0x3754: 75, + 0x3771: 40, + 0x3779: 20, + } + check( + all(btl[offset].args == [(9, 7), (0, percent)] + for offset, percent in nonlethal.items()) + and all(btl[offset].args == [(9, 7), (0, percent)] + for offset, percent in defeated.items()) + and btl[0x377e].label == "mul" + and btl[0x3785].args[-1] == (0, 100) + and btl[0x3791].label == "call-script", + "BTL's nonlethal and defeat percentage tables feed integer-divided ADDEXP awards", + ) + + addexp = loaded["ADDEXP.BIN"] + check( + addexp[0x52].args == [(9, 1), (3, 0x62ccb)] + and addexp[0x57].args[-1] == (0, 0x10) + and addexp[0x6d].args[-1] == (0, 0x20) + and addexp[0x85].args[-1] == (0, 0x40) + and loaded["GAMESTART.BIN"][0xf07].args == [(3, 0x3302), (0, 4)] + and loaded["GAMESTART.BIN"][0xf0c].args == [(3, 0x3302), (0, 5)] + and loaded["GAMESTART.BIN"][0xf11].args == [(3, 0x3302), (0, 6)] + and addexp[0x9a].label == "gr" + and addexp[0x148].args[1] == (3, 0x6994) + and addexp[0x164].args[1] == (3, 0x6994) + and addexp[0x156].args[-1] == (0, 100), + "ADDEXP applies modifier bits, rejects zero, and advances the 0..99 progress cell", + ) + + field = loaded["FIELD.BIN"] + seten = loaded["SETEN.BIN"] + check( + field[0x5e8].args == [(3, 0x204f8), (0, 0)] + and field[0x63e].args[1] == (3, 0x6930) + and field[0x705].args[1] == (3, 0x6930) + and field[0x6b7].args[1] == (3, 0x204f8) + and field[0x70c].args[1] == (3, 0x204f8) + and field[0x6c6].args[-1] == (0, 5) + and field[0x71b].args[1] == (3, 0x204f8) + and seten[0x2be].args[1] == (3, 0x204f8) + and seten[0x2e1].args[1] == (3, 0x204f8) + and seten[0x2ef].args[1] == (3, 0x4e053), + "FIELD builds the party reference level and SETEN uses it for enemy runtime levels", + ) + def test_merge_precedence(): curated, _ = G.load_toml(paths.VM_MAP / "globals.toml") auto = G.load_auto(paths.BUILD / "global-var-map.json") @@ -586,6 +680,7 @@ if __name__ == "__main__": test_selected_movement_route_grid_evidence() test_battle_triggered_passive_skill_flags_evidence() test_unit_stat_growth_fractions_evidence() + test_battle_experience_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 2f59675..e370e8c 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -731,8 +731,8 @@ address = "0x83406" name = "unit_experience_reward" category = "data-table" type = "int[1000]" -value_domain = "10..100 base experience" -usage = "Base experience award for 164 enemy records. BTL scales this value by the victor/defeated level difference and difficulty mode, stores the result in the ADDEXP input channel, and invokes ADDEXP for the winning unit." +value_domain = "0 implicit/no award; populated values 10..100" +usage = "Per-opponent base experience progress. After each battle exchange, BTL awards a surviving faction-1 participant against the opposing unit definition. If the opponent survives, enemy-minus-player level differences >=3/2/1/<=0 select 40/30/20/10 percent. If the opponent is defeated, differences >=3/2/1/0/-1/-2/<=-3 select 250/200/150/100/75/40/20 percent. Integer division truncates. The result enters ADDEXP, whose normally cleared modifier bits can suppress, double, or multiply it by ten. A cell with no writer in the ordered INIT sequence remains zero; append unit 900 is such a cell and therefore awards zero at every bracket." source = "investigation" confidence = "high" @@ -1364,6 +1364,28 @@ source = "investigation" confidence = "high" depends_on = ["0x522ed", "0x5231f", "0x52351"] +[[global]] +address = "0x4dfbd" +name = "entity_unit_definition_ids" +category = "data-table" +type = "int[50]" +value_domain = "EBINIT unit definition id" +usage = "Definition id backing each runtime map entity. SETEN writes current_unit_id when materializing an enemy; BTL uses the opposing entity's value to read unit_experience_reward and drop tables, while ADDEXP uses the faction-1 participant's value to select persistent playable-unit state." +source = "investigation" +confidence = "high" +depends_on = ["0x66715", "0x83406"] + +[[global]] +address = "0x4e053" +name = "entity_levels" +category = "data-table" +type = "int[50]" +value_domain = "runtime unit level 1..99" +usage = "Displayed/runtime level for each map entity. SETEN begins enemies at unit_starting_level, applies party-reference auto-scaling and stage minimum/maximum clamps, and BTL subtracts the surviving faction-1 participant's level from the opponent's level to choose the experience multiplier bracket." +source = "investigation" +confidence = "high" +depends_on = ["0x7a37e", "0xe782f", "0xe784d", "0x204f8"] + [[global]] address = "0x4e085" name = "entity_current_resources" @@ -2141,6 +2163,17 @@ usage = "Persistent current level by playable-unit id. CCINIT compares current_u source = "investigation" confidence = "high" +[[global]] +address = "0x6994" +name = "unit_experience_progress" +category = "data-table" +type = "int[100]" +value_domain = "0..99 progress toward the next level; 100 is the transient level-up boundary" +usage = "Persistent experience progress by playable-unit id. ADDEXP adds the current award, animates the increase to 100, raises unit_current_levels, resets this cell to zero, and repeats while award remains and the unit is below unit_level_cap." +source = "investigation" +confidence = "high" +depends_on = ["0x6930", "0x7a766", "0x83406"] + [[global]] address = "0x69f8" name = "unit_current_stats" @@ -2887,28 +2920,6 @@ source = "auto-shape" confidence = "med" depends_on = [] -[[global]] -address = "0x152617" -name = "" -category = "story-flag" -type = "int" -value_domain = "{0,1}" -usage = "TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0]; writers=['CLOSE.BIN', 'FIELD.BIN', 'INFOEN.BIN', 'READY.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] - -[[global]] -address = "0x152618" -name = "" -category = "story-flag" -type = "int" -value_domain = "one of {1, 2, 4, 7}" -usage = "TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [1, 2, 4, 7]; writers=['FIELD.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] - [[global]] address = "0x1560e7" name = "current_magic_action_id" @@ -3011,14 +3022,14 @@ depends_on = [] [[global]] address = "0x152618" -name = "scjump_progress_c" -category = "counter" +name = "battle_outcome_flags" +category = "unknown" type = "int" -value_domain = "progress index" -usage = "Third SCJUMP switch input (530 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage." -source = "inference" -confidence = "med" -depends_on = [] +value_domain = "low bits: 1 = target defeated, 2 = actor defeated, 4 = both survived; combinations may accumulate during FIELD resolution" +usage = "BTL derives the low three bits from the acting and target entities' post-exchange HP. Mask 4 selects the nonlethal experience table; otherwise BTL uses the defeat table. FIELD consumes and combines the result during post-battle flow, then passes it to SCJUMP, whose former progress-C comparisons are battle-outcome decisions." +source = "investigation" +confidence = "high" +depends_on = ["0x4e085", "0x66713", "0x66714"] [[global]] address = "0xe6c5d" @@ -3162,6 +3173,17 @@ usage = "GAMESTART's three-way difficulty selection. SETEN uses the zero-based i source = "investigation" confidence = "high" +[[global]] +address = "0x204f8" +name = "party_reference_level" +category = "counter" +type = "int" +value_domain = "integer mean of eligible party-unit levels" +usage = "FIELD's enemy auto-level reference. It averages all eligible party units when at most five exist, otherwise the five highest-level units. SETEN raises scalable enemies above unit_starting_level by the positive gap divided by the selected auto-level divisor before applying stage level clamps. Difficulty separately adds -5/0/+5 stat-growth iterations and does not change this stored runtime level." +source = "investigation" +confidence = "high" +depends_on = ["0x673c", "0x67a0", "0x6930"] + [[global]] address = "0x152617" name = "current_stage_enemy_spawn_slot"