From 35080a086ef48426bd8371638912b5adb9a27879 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 23:17:47 -0400 Subject: [PATCH] Decode battle animation tables --- docs/global-reference.md | 40 +- docs/name-resolution.md | 6 +- docs/phase-a-slice-plan.md | 46 ++ docs/script-inventory.md | 14 +- docs/tools-reference.md | 20 +- docs/vm-mapping-plan.md | 5 +- tools/extract_init.py | 738 +++++++++++++++++++++++++++++++ tools/init_table_profile.py | 133 +++++- tools/test_extract_init.py | 165 +++++++ tools/test_globals.py | 10 + tools/test_init_table_profile.py | 64 +++ vm-map/globals.toml | 212 +++++++++ 12 files changed, 1424 insertions(+), 29 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 727b88b..9c81746 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5054 globals (384 curated, 4670 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5054 globals (403 curated, 4651 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -272,6 +272,24 @@ | `0x1525b2` | card_generation_card_ids | high | investigation | Card ids parallel to card_generation_weight_schedules. FIELD scans slots 0..99, filters each nonzero id through the CDINIT2 story-flag rows, and performs cumulative weighted random selection. CDINIT clears only the first 50 slots even though its largest authored list reaches slot 75. | | `0x152877` | battle_entity_indices | high | investigation | The two runtime entity rows participating in BTL. battle_actor_side_index and battle_target_side_index select these cells before BTL and CALCDMG access HP, unit definitions, skills, animation state, and voice banks. Columns: 0=side_0, 1=side_1. | | `0x152879` | battle_selected_skill_ids | high | investigation | Selected skill for each battle side. BTL displays a positive entry through the skill-name table and selects skill-use voice columns; CALCDMG uses the same id for skill parameters. A zero entry follows the equipped/default ordinary-attack path. Columns: 0=side_0, 1=side_1. | +| `0x15288e` | battle_animation_effect_ids | high | investigation | BTANINIT2's sparse battle-animation composition table. BTANINIT expands the selected row into six runtime effect work slots. BTL anchors requested effects in slots 0 and 1 to the actor and slots 2 through 5 to the target; shipped data never populates slot 4. Columns: 0=effect_slot_0, 1=effect_slot_1, 2=effect_slot_2, 3=effect_slot_3, 4=reserved_effect_slot_4, 5=effect_slot_5. | +| `0x153ffe` | battle_animation_effect_start_delays_ms | high | investigation | Per-effect start delays paired with battle_animation_effect_ids. BTL passes each populated value to movie/sprite animation setup and adds it to the effect-local sound and hit-pulse offsets. Shipped data authors delays only for slots 2, 3, and 5. Columns: 0=effect_slot_0, 1=effect_slot_1, 2=effect_slot_2, 3=effect_slot_3, 4=reserved_effect_slot_4, 5=effect_slot_5. | +| `0x15576e` | battle_animation_duration_ms | high | investigation | Overall timing boundary for one BTANINIT2 animation row. BTL uses it to schedule delayed battle voice and to begin the 500 ms HP interpolation after the main animation. The passive/defeat auxiliary rows 801..809 intentionally omit it. | +| `0x155b56` | battle_effect_visual_asset_ids | high | investigation | BTANINIT clears and materializes the selected animation's six visual resources. BTL opens mode-0 values as nonblocking MPEG-in-AGF movies and mode-1/2 values as sprite sheets. All 202 effect definitions resolve through SYS4INI. | +| `0x155b5c` | battle_effect_visual_mode_ids | high | investigation | BTANINIT visual playback mode per materialized effect slot. BTL routes zero through play-movie-to-surface, mode 1 through set-texture with green color key 0x00ff00, and mode 2 through set-texture with no color key. | +| `0x155b62` | battle_effect_additive_blend_flags | high | investigation | When nonzero, BTL applies gfx-draw-color mode 1 (SRCALPHA/ONE additive glow) to the materialized battle-effect surface. The flag is populated on all 186 movie effects and the sole green-colorkey sprite-sheet effect. | +| `0x155b68` | battle_effect_widths | high | investigation | Width of the destination surface BTL creates for each materialized battle effect. It also participates in centering the surface before applying battle_effect_offset_x_pixels. | +| `0x155b6e` | battle_effect_heights | high | investigation | Height of the destination surface BTL creates for each materialized battle effect. It also participates in centering the surface before applying battle_effect_offset_y_pixels. | +| `0x155b74` | battle_effect_atlas_column_counts | high | investigation | Sprite-sheet column count passed by BTL to animate-gfx-srcrect-target. The sixteen sprite-sheet definitions pair it with battle_effect_atlas_frame_counts and a finite duration. | +| `0x155b7a` | battle_effect_atlas_row_counts | high | investigation | Authored sprite-sheet row count. It agrees with ceil(frame_count / column_count) for all sixteen sprite effects, but BTL never reads this array and instead passes the already-authored frame count and column count to animate-gfx-srcrect-target. | +| `0x155b80` | battle_effect_atlas_frame_counts | high | investigation | Total sprite-sheet frame count passed by BTL to animate-gfx-srcrect-target. Shipped values equal atlas columns times authored rows. | +| `0x155b86` | battle_effect_durations_ms | high | investigation | Per-effect playback duration. BTANINIT authors it for sixteen sprite-sheet effects; for movie effects BTL overwrites the slot with query-surface-stop-time-ms. BTL uses start delay plus this duration when extending the animation timeline. | +| `0x155b8c` | battle_effect_combatant_anchor_flags | high | investigation | When nonzero, BTL anchors effect slots 0 and 1 to the actor-side combatant and slots 2 through 5 to the target-side combatant. Zero uses the battlefield center. | +| `0x155b92` | battle_effect_offset_x_pixels | high | investigation | Horizontal offset added after BTL centers the effect surface on its selected combatant or the battlefield. | +| `0x155b98` | battle_effect_offset_y_pixels | high | investigation | Vertical offset added after BTL centers the effect surface on its selected combatant or the battlefield. | +| `0x155b9e` | battle_effect_sound_asset_ids | high | investigation | Optional WAV loaded by BTL for a materialized effect slot. BTL schedules playback at the animation row's effect start delay plus battle_effect_sound_delays_ms. All 199 populated ids resolve through SYS4INI. | +| `0x155ba4` | battle_effect_sound_delays_ms | high | investigation | Effect-local sound delay added to battle_animation_effect_start_delays_ms before BTL schedules the loaded WAV channel. | +| `0x155baa` | battle_effect_hit_pulse_offsets_ms | high | investigation | Up to three BTL hit-feedback pulse offsets per materialized effect slot. BTL adds the animation row's effect start delay, fires each pulse within a 100 ms window, and tracks it once per slot/column. Shipped BTANINIT populates only pulse_1, at 100 ms, on 26 effects. Columns: 0=pulse_1, 1=pulse_2, 2=pulse_3. | | `0x155bbc` | training_action_required_story_flag_ids | high | investigation | Up to three prerequisite story flags per TRINIT action. TRAIN subtracts one before indexing story_event_flags and rejects the action unless every populated flag equals one. Columns: 0=required_story_flag_id_1, 1=required_story_flag_id_2, 2=required_story_flag_id_3. | | `0x155bfb` | training_action_forbidden_story_flag_ids | high | investigation | Reserved three-slot exclusion table paired with training_action_required_story_flag_ids. TRAIN rejects an action when any populated flag equals one; shipped TRINIT leaves all 63 cells zero. Columns: 0=forbidden_story_flag_id_1, 1=forbidden_story_flag_id_2, 2=forbidden_story_flag_id_3. | | `0x155c3a` | training_action_minimum_unit_levels | high | investigation | Minimum familiar level for each TRINIT action. TRAIN compares the selected unit's level and rejects values below the populated threshold. | @@ -400,21 +418,6 @@ | `0x152885` | — | low | auto-shape | array | | `0x152887` | — | low | auto-shape | array | | `0x152889` | — | low | auto-shape | array | -| `0x15576e` | — | low | auto-shape | array | -| `0x155b56` | — | low | auto-shape | array | -| `0x155b5c` | — | low | auto-shape | array | -| `0x155b62` | — | low | auto-shape | array | -| `0x155b68` | — | low | auto-shape | array | -| `0x155b6e` | — | low | auto-shape | array | -| `0x155b74` | — | low | auto-shape | array | -| `0x155b7a` | — | low | auto-shape | array | -| `0x155b80` | — | low | auto-shape | array | -| `0x155b86` | — | low | auto-shape | array | -| `0x155b8c` | — | low | auto-shape | array | -| `0x155b92` | — | low | auto-shape | array | -| `0x155b98` | — | low | auto-shape | array | -| `0x155b9e` | — | low | auto-shape | array | -| `0x155ba4` | — | low | auto-shape | array | | `0x1560e8` | — | low | auto-shape | array | | `0x156106` | — | low | auto-shape | array | | `0x156124` | — | low | auto-shape | array | @@ -449,6 +452,7 @@ | `0x152617` | current_stage_enemy_spawn_slot | high | investigation | Current STINIT enemy-template slot. FIELD selects slots 1..29 while materializing stage units, SETEN records the slot on the runtime entity and reads every parallel enemy buffer through it, and ADDEN uses slot 0 for its special generated unit. | | `0x15261a` | battle_actor_side_index | high | investigation | Index of the acting side in BTL's two-entry battle arrays. CALCDMG derives battle_target_side_index as 1 minus this value; BTL uses the actor's unit-definition row for attack, skill, critical, and finishing-blow voices. | | `0x15261b` | battle_target_side_index | high | investigation | Index of the target side in BTL's two-entry battle arrays. CALCDMG sets it to 1 - battle_actor_side_index; BTL applies battle_hp_delta to this side and uses its unit-definition row for damage and defeated voices. | +| `0x15288c` | selected_battle_animation_id | high | investigation | CALCDMG selects a skill_battle_animation_id or an equipped item's item_weapon_class, and BTL also writes passive reaction ids 801..808 and hardcoded defeat id 809. BTANINIT uses this value to select one six-effect timeline. | | `0x1560e7` | current_magic_action_id | high | investigation | Shared selected magic/research/growth action id. MAGIC and STUDY index MAINIT's parallel columns with it, FIELD and USEMAGIC consume the selected action, and MAMES dispatches ids 1..9 to their untitled help descriptions. | | `0x15a095` | information_tab_index | high | investigation | Selected INFO screen tab. INFO dispatches the five tab scripts with it; INFOMES uses values 0..2 as the column of information_message_handler_script_ids for INFOCH, INFOEN, and INFOVO. | | `0x15a117` | current_character_profile_id | high | investigation | INFOCH's selected character-information row. It indexes CIINIT's name, unit, portrait, and placement arrays; CIMES compares the same id against all 24 profiles to dispatch the character biography. | @@ -5090,10 +5094,6 @@ | `0x15261f` | — | med | auto-shape | record-table[stride 300] | | `0x152878` | — | low | auto-shape | index/counter? | | `0x15288b` | — | low | auto-shape | index/counter? | -| `0x15288c` | — | med | auto-shape | current-entity-index? | -| `0x15288e` | — | med | auto-shape | record-table[stride 6] | -| `0x153ffe` | — | med | auto-shape | record-table[stride 6] | -| `0x155baa` | — | med | auto-shape | record-table[stride 3] | | `0x15a75b` | — | med | auto-shape | record-table[stride 21] | | `0x15a785` | — | med | auto-shape | record-table[stride 3] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index 1cc6e79..48f8f79 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -687,8 +687,10 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape slots, all eligibility/cost/stat/alignment/progress/award families, and 75 event cells joined through SCINIT and GAMESTART's restored story flags. CDINIT/CDINIT2 close FIELD's nine weighted generation lists and 81 typed card effects, including story gates and item/event/condition/visual - joins. With RTINIT's used-provider surface closed and ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined - and INFOMES/MES classified, audit BTANINIT/BTANINIT2's battle-animation registry next; + joins. BTANINIT/BTANINIT2 close 122 battle-animation timelines and 202 visual/audio/hit-pulse + effects, with skill and weapon-class selection joined. With RTINIT's used-provider surface closed + and ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES classified, audit STINIT2's + remaining stage-definition columns next; 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 0db29e6..5072e65 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3147,3 +3147,49 @@ FIELD/STAGECLEAR consumer contract. **Next:** audit BTANINIT and BTANINIT2 together. Their current generic outputs fragment the battle-animation registry across address-derived fields, making the consumer-side row geometry the next data-structure target. + +## Data-semantics sidebar: BTANINIT/BTANINIT2 battle animations (2026-07-23) + +BTANINIT2 is three parallel reserved 1,000-row arrays, not 28 records with 387 unrelated fields. +Its 1,017 writes define 122 sparse battle-animation ids: six effect slots at `0x15288e`, six paired +start-delay slots at `0x153ffe`, and one total-duration cell at `0x15576e`. The dedicated extractor +classifies all 1,018 instructions exactly and resolves all 573 populated effect references across +186 distinct BTANINIT definitions. + +The shipped slot geometry is deliberate. Slots zero and one have 114 effect references each and +are actor-side when an effect requests combatant anchoring; slots two and three have 113 each and +are target-side; slot five has 119; slot four is never populated. Only slots two, three, and five +carry authored start delays. One hundred eleven rows are complete attack timelines with a +500--1,700 ms total duration. Eleven are auxiliary rows: two incomplete/unjoined authored attacks +and passive/defeat rows 801--809, which are invoked for isolated overlays and intentionally omit the +main duration. + +CALCDMG selects rows 1--21 from the equipped item's `item_weapon_class` for normal attacks or reads +`skill_battle_animation_id` for skills. The join accounts for 101 populated skill references across +98 animation rows, including passive reactions 801--808. BTL selects hardcoded row 809 for defeat. +Four authored animation rows—205, 221, 222, and 224—have no shipped item, skill, or hardcoded +consumer and remain explicit rather than being discarded. + +BTANINIT is the paired effect catalog and runtime materializer. It clears fourteen six-cell work +arrays plus one `6 × 3` hit-pulse table, reads the selected BTANINIT2 row, and dispatches each slot's +effect id through 203 cases: zero as the empty sentinel and 202 populated definitions. The dedicated +extractor classifies all 4,472 instructions, including 1,917 definition writes. The catalog contains +186 nonblocking MPEG-in-AGF movie effects, one green-colorkey sprite sheet, and fifteen opaque sprite +sheets. All 202 visual resources resolve to `MVB*.AGF` or `AM*.AGF`; all 199 populated sound ids +resolve to WAV files. + +BTL proves the complete materialized work ABI: playback mode, additive blend, destination width and +height, actor/target or battlefield-center anchor, pixel offsets, sprite atlas columns/frame count +and finite duration, delayed sound, and up to three hit-pulse offsets. Shipped data populates only +the first hit-pulse column, always at 100 ms, on 26 effects. The sixteen authored sprite-atlas row +counts agree with the explicit frame/column values, but BTL never reads them. Sixteen complete +BTANINIT effect definitions are unreferenced by BTANINIT2 and remain preserved. + +Regressions protect both scripts' complete instruction accounting, reserved geometry, every +effect/delay/duration population, all 573 definition joins, the SKINIT and ITINIT selections, +passive/defeat rows, visual/audio resolution, movie/sprite behavior, hit-pulse timing, and the +unreferenced authored surfaces. + +**Next:** audit STINIT2's remaining generic 321-row stage-definition surface. MPINIT already proves +its tile-bound columns; the next pass should join the rest of its stage metadata to FIELD and the +stage-selection/initialization consumers. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 35e7b5c..0ead06a 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -49,7 +49,8 @@ Large, table-like scripts — prime candidates for data extraction: - `SCINIT` (88 KB), `CGINIT` (79 KB — CG gallery), `ITINIT` (70 KB — items), `RTINIT` (67 KB — movement/battle routine-step banks), `CCINIT` (41 KB — class-change rule program), `SKINIT` (37 KB — skills), `CDINIT` (31 KB — weighted card-generation lists), `CDINIT2` - (card definitions and effects), `BTANINIT` (105 KB — battle animations) + (card definitions and effects), `BTANINIT` (105 KB — battle-effect definitions), `BTANINIT2` + (six-slot battle-animation timelines) - Smaller: `AFINIT`, `ALINIT`, `CIINIT`, `CNINIT`, `CTINIT`, `CVINIT`, `ILINIT`, `LAINIT`, `MAINIT`, `OBINIT` (46 object definitions), `SPINIT`, `TRINIT`, `VIINIT` @@ -161,6 +162,17 @@ resource recovery, trap, and random warp. All item, event, condition, and visual through ITINIT, SCINIT, ILINIT, and SYS4INI. FIELD reads two of the three authored required-story-flag columns; eighteen third-column values are preserved as engine-dead data. +BTANINIT2 defines 122 sparse battle-animation ids in a reserved 1,000-row layout. Each row owns up +to six BTANINIT effect ids, their start delays, and an overall duration. CALCDMG selects rows 1--21 +from ITINIT weapon classes or a skill's explicit animation id; 101 SKINIT references join to 98 +rows, passive reactions occupy 801--808, and BTL hardcodes defeat row 809. + +BTANINIT materializes the selected timeline into BTL's six-slot work record. Its 202 effect +definitions resolve 186 movie visuals and sixteen sprite sheets, 199 optional WAV sounds, blend and +anchor flags, surface geometry and offsets, sprite-atlas timing, sound delay, and hit-pulse timing. +All populated visual/audio resources resolve through SYS4INI; sixteen authored effects are +unreferenced by BTANINIT2. + RTINIT is a seventh banked shape. Its 3,336 writes populate twenty parallel `1000 × 20` banks: ten movement banks consumed by MVRTN/RTN_M providers and ten battle banks consumed by BTRTN/RTN_B providers. The generated 172 routine-set records assemble 1,043 movement steps and fourteen battle steps, join diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 89a6ca9..28f144e 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -55,9 +55,9 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited * |---|---|---|---| | `extract_phase2.py` | Batch: disassembly + text corpora for every script. | `extract_phase2.py` | corpus → `build/disasm/*.asm`, `build/text/{dialogue.jsonl,strings.jsonl,*.strings.txt}`, `build/manifest.json` | | `extract_message_table.py` | Discover repeated global-id dispatch chains in either fallthrough-body (ITMES/SKMES/MAMES) or branch-target-body (VIMES/EIMES/CIMES) form, reconstruct player-facing text and furigana, preserve guard/body offsets, and emit an ID-keyed message table. Title-bearing tables expose `title`/`description`; EIMES exposes its untitled lines as `summary`/`strategy`; CIMES exposes one untitled multiline `biography`; MAMES exposes untitled `description` text. | `extract_message_table.py [OUTNAME]` | `.BIN` → `build/data/.json` | -| `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch / banked shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate direct and negative-value writes, and separate parallel `fields` from linked row-major `record_fields`; when flat global ranges overlap, a parallel base established by other records wins over a coincidental row-table cell calculation. Dedicated name-mode schemas recover VIINIT's pre-title glossary prerequisites and CIINIT's profile name, backing-unit, optional portrait, and reserved placement columns. Footer-mode MPINIT recovers its sparse 53-column doubled-coordinate terrain atlas, joins 66 STINIT2 stage rectangles, and resolves terrain ids through LAINIT. Mixed tables recover selector-dispatched records, condition strings, scalars, preallocated buffer cells, consumer-confirmed strides, and length-prefixed footer arrays; STINIT additionally assembles confirmed buffers into `object_placements` and `enemy_spawns`, joins object type names and available descriptions from OBINIT, and exposes consumer-proven tagged payloads (teleport, treasure, card, initial faction, non-triggering hazard/barrier faction, and OBINIT-gated initial object state), preserves engine-dead type-27 writes under `ignored_payload_fields`, and exposes reinforcement schedules and first-clear-only enemy gates. Rules mode decodes CCINIT's source-ordered class-change predicates and output accumulators, joining EBINIT unit names, SKINIT skill names, titles, named stat bonuses, cost deltas, and applied-state slots. Dispatch mode decodes SCINIT's paired decision-to-packed-scene and authored-chapter arrays, preserving source-order overwrites, resolving SYS4INI script names, and cross-checking chapter tags against decoded SCJUMP paths. Banked mode decodes RTINIT's twenty parallel 1000-by-20 movement/battle routine banks, retains all assignments and overwrites, assembles final per-slot steps, resolves provider selectors to RTN_M/RTN_B scripts, and applies selector-scoped schemas where proven. Current schemas cover all nineteen used providers, RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061, including progress-only, randomized roaming, object-slot, coordinate, enemy/ally, treasure and Magic Pillar search, Healing Feather, waypoint, faction-terrain, retreat, reachable normal-attack routing, immediate offensive target/action selection, and immediate allied healing. Parameterless behavior schemas publish an empty `parameter_fields` mapping while still joining `provider_behavior`, target selection, action selection, and completion rules. Provider defaults such as M004's unwritten slot 0, M010's zero/HP resource index, and M013's any-foreign-faction filter are projected explicitly and counted separately from populated source cells. Authored cells proven unread by M001/M008 remain under per-step `ignored_movement_parameters` and contribute to top-level `ignored_movement_parameter_count`; they are not silently discarded or assigned invented meanings. Raw provider banks remain intact, and top-level `movement_provider_parameter_schemas` documents the projection. ITINIT, SKINIT, VIINIT, EBINIT, and CIINIT join their matching MES text. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py [OUTNAME] [--mode …]` | `
.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, STINIT2/LAINIT for MPINIT, CDINIT2/STINIT for CDINIT, ITINIT/SCINIT/ILINIT and `build/callscript-names.json` for CDINIT2, `build/callscript-names.json` for RTINIT/SCINIT, `build/scjump-decisions.json` for SCINIT, matching `.BIN` when supported, and `build/globals.json` → `build/data/.json`, `build/data/README.md` | -| `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/footer/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. MPINIT's footer specialization summarizes atlas geometry, implicit-zero rows, terrain ids, stage-rectangle joins, sharing, and border cells; CDINIT summarizes selector/list sizes, CDINIT2 joins and story gates, STINIT references, and the runtime scan/clear geometry; CDINIT2 summarizes card-type and effect populations plus item/event/condition/visual join coverage. Name tables report message coverage for title/description, summary/strategy, biography, and description-only layouts; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards; dispatch tables summarize assignments, overwrites, script-name resolution, and SCJUMP chapter agreement; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` searches every supported message field beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py
[--build] [--limit N] [--message-query REGEX]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,md}` | -| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based, mixed selector-dispatched, CCINIT rule, SCINIT dispatch, RTINIT banked, ILINIT condition-matrix, CNINIT unit-name/voice-family, CGINIT gallery, ALINIT alchemy-recipe, AFINIT affinity/progression, CTINIT name-entry-palette, CVINIT character-voice, TRINIT training-action, CDINIT card-generation, CDINIT2 card-effect, and MPINIT terrain-atlas extraction; RECOVER ABI validation; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked/map/training/card profiling. | run each directly | — | +| `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch / banked shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate direct and negative-value writes, and separate parallel `fields` from linked row-major `record_fields`; when flat global ranges overlap, a parallel base established by other records wins over a coincidental row-table cell calculation. Dedicated name-mode schemas recover VIINIT's pre-title glossary prerequisites and CIINIT's profile name, backing-unit, optional portrait, and reserved placement columns. Footer-mode MPINIT recovers its sparse 53-column doubled-coordinate terrain atlas, joins 66 STINIT2 stage rectangles, and resolves terrain ids through LAINIT. Mixed tables recover selector-dispatched records, condition strings, scalars, preallocated buffer cells, consumer-confirmed strides, and length-prefixed footer arrays; STINIT additionally assembles confirmed buffers into `object_placements` and `enemy_spawns`, joins object type names and available descriptions from OBINIT, and exposes consumer-proven tagged payloads (teleport, treasure, card, initial faction, non-triggering hazard/barrier faction, and OBINIT-gated initial object state), preserves engine-dead type-27 writes under `ignored_payload_fields`, and exposes reinforcement schedules and first-clear-only enemy gates. Rules mode decodes CCINIT's source-ordered class-change predicates and output accumulators, joining EBINIT unit names, SKINIT skill names, titles, named stat bonuses, cost deltas, and applied-state slots. Dispatch mode decodes SCINIT's paired decision-to-packed-scene and authored-chapter arrays, preserving source-order overwrites, resolving SYS4INI script names, and cross-checking chapter tags against decoded SCJUMP paths. Banked mode decodes RTINIT's twenty parallel 1000-by-20 movement/battle routine banks, retains all assignments and overwrites, assembles final per-slot steps, resolves provider selectors to RTN_M/RTN_B scripts, and applies selector-scoped schemas where proven. Current schemas cover all nineteen used providers, RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061, including progress-only, randomized roaming, object-slot, coordinate, enemy/ally, treasure and Magic Pillar search, Healing Feather, waypoint, faction-terrain, retreat, reachable normal-attack routing, immediate offensive target/action selection, and immediate allied healing. Parameterless behavior schemas publish an empty `parameter_fields` mapping while still joining `provider_behavior`, target selection, action selection, and completion rules. Provider defaults such as M004's unwritten slot 0, M010's zero/HP resource index, and M013's any-foreign-faction filter are projected explicitly and counted separately from populated source cells. Authored cells proven unread by M001/M008 remain under per-step `ignored_movement_parameters` and contribute to top-level `ignored_movement_parameter_count`; they are not silently discarded or assigned invented meanings. Raw provider banks remain intact, and top-level `movement_provider_parameter_schemas` documents the projection. ITINIT, SKINIT, VIINIT, EBINIT, and CIINIT join their matching MES text. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py
[OUTNAME] [--mode …]` | `
.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, STINIT2/LAINIT for MPINIT, CDINIT2/STINIT for CDINIT, ITINIT/SCINIT/ILINIT and `build/callscript-names.json` for CDINIT2, BTANINIT/SKINIT/ITINIT and `build/callscript-names.json` for BTANINIT2, BTANINIT2 and `build/callscript-names.json` for BTANINIT, `build/callscript-names.json` for RTINIT/SCINIT, `build/scjump-decisions.json` for SCINIT, matching `.BIN` when supported, and `build/globals.json` → `build/data/.json`, `build/data/README.md` | +| `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/footer/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. MPINIT's footer specialization summarizes atlas geometry, implicit-zero rows, terrain ids, stage-rectangle joins, sharing, and border cells; CDINIT summarizes selector/list sizes, CDINIT2 joins and story gates, STINIT references, and the runtime scan/clear geometry; CDINIT2 summarizes card-type and effect populations plus item/event/condition/visual join coverage; BTANINIT/BTANINIT2 summarize effect modes/resources, six-slot timeline geometry, delays, duration, skill/weapon joins, and unreferenced authoring. Name tables report message coverage for title/description, summary/strategy, biography, and description-only layouts; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards; dispatch tables summarize assignments, overwrites, script-name resolution, and SCJUMP chapter agreement; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` searches every supported message field beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py
[--build] [--limit N] [--message-query REGEX]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,md}` | +| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based, mixed selector-dispatched, CCINIT rule, SCINIT dispatch, RTINIT banked, ILINIT condition-matrix, CNINIT unit-name/voice-family, CGINIT gallery, ALINIT alchemy-recipe, AFINIT affinity/progression, CTINIT name-entry-palette, CVINIT character-voice, TRINIT training-action, CDINIT card-generation, CDINIT2 card-effect, BTANINIT/BTANINIT2 battle-animation, and MPINIT terrain-atlas extraction; RECOVER ABI validation; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked/map/training/card/battle-animation profiling. | run each directly | — | | `global_map.py` | Build the partial global-variable name map from static evidence. | `global_map.py` | corpus + `build/data/` → `build/global-var-map.{json,md}` | `extract_init.py` also has name-mode specializations for VIINIT's sparse 200-row glossary table, @@ -135,6 +135,20 @@ SYS4INI `MVS*.AGF` resources. Ranged effects expose the authored minimum and max an equal pair is a fixed value. The consumer contract also records FIELD's rendering/effect order and STAGECLEAR's deferred spendable-point award. +Numeric-mode BTANINIT2 emits 122 sparse battle-animation rows inside three reserved 1,000-row +arrays: six effect ids, six paired start delays, and one overall duration. Every one of its 573 +effect references joins to BTANINIT. The output also joins 101 SKINIT skill references across 98 +animations and ITINIT weapon classes to normal-attack rows 1 through 21; passive reactions 801--808, +hardcoded defeat row 809, four unjoined authored rows, and the unused slot 4 remain explicit. + +Numeric-mode BTANINIT decodes its conditional effect-id program into 202 definitions rather than +one generic scalar. Each definition retains the six-slot work-array writes for visual resource and +mode, additive blend, surface geometry, combatant anchoring and offsets, sprite-atlas timing, +optional WAV and delay, and three reserved hit-pulse offsets. All 202 AGF and 199 populated WAV ids +resolve through SYS4INI. The schema records BTL's movie/sprite playback and actor/target slot +contract, the engine-dead authored atlas-row count, and sixteen definitions not referenced by +BTANINIT2. + Footer-mode MPINIT classifies all 1,472 copies as fifty-cell rows within one stride-53 terrain atlas. It derives grid Y from each destination, retains 127 implicit-zero gaps, and joins STINIT2's inclusive tile bounds after the consumer-proven two-times coordinate conversion. The output contains 66 named diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index 173c43c..a76071b 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -53,7 +53,7 @@ - [x] **2.0 — Project structure.** Established `docs/`, `build/{disasm,text,data,scripts-json}/`, `godot/`; game install stays read-only in place. Also relaxed the loader magic check to the `SYS4` family (`SYS4424` patch scripts now parse — was silently skipping 5 scripts). - [x] **2.1 — Text corpora.** `tools/extract_phase2.py` → 481/481 scripts: full disassembly (`build/disasm/*.asm`), per-script strings, `build/text/dialogue.jsonl` (**30,057 show-text lines** — the translation corpus), `build/text/strings.jsonl` (38,449 strings tagged by source opcode), `build/manifest.json`. -- [x] **2.2 — `*INIT` data and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`/`dispatch`/`banked`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions), CIINIT (24 character-information profiles)** [name: sparse one-based name/description/fields, with CIINIT's dedicated profile registry], **CGINIT (851 sparse gallery images)** [numeric: full image, optional save/stage preview, thumbnail sheet/slot, and variant ordinal in a reserved 2,000-row layout], **ALINIT (107 sparse alchemy recipes)** [numeric: output, level/story gates, point cost, and four paired ingredient slots in a reserved 1,000-row layout], **AFINIT (13 authored affinity rows plus tuning/facility curves)** [name/footer specialization], **CTINIT (five 70-cell name-entry pages)** [string-matrix specialization], **CVINIT (13 character-voice settings)** [numeric: preview assets, slot-to-unit joins, and inverse suppression-setting map], **LAINIT (20 shipped terrain definitions)** [name: sparse strings, topology/rendering, combat-stat, traversal-skill, and texture-fallback joins], **MPINIT (1,472 sparse terrain rows joined to 66 stage maps)** [footer: 53-column doubled-coordinate atlas], **SPINIT (eight 15-slot H-scene gallery pages)** [numeric: INIT2 thumbnail-sheet and SYS4INI scene joins], **TRINIT (21 training actions)** [name/string-matrix: eligibility, cost, stat/alignment/progress effects, rewards, and ten event slots], **CDINIT (nine weighted card-generation lists)** [numeric/dispatch specialization], **CDINIT2 (81 card definitions)** [name: story gates, typed effects, and item/event/condition/visual joins], **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays], **CCINIT (71 class-change rules over 33 units)** [rules: unit/level/state predicates plus title/cost/stat/skill effects], **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history], and **RTINIT (172 routine sets)** [banked: twenty movement/battle step banks, provider joins, and overwrite history]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. +- [x] **2.2 — `*INIT` data and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`/`dispatch`/`banked`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions), CIINIT (24 character-information profiles)** [name: sparse one-based name/description/fields, with CIINIT's dedicated profile registry], **CGINIT (851 sparse gallery images)** [numeric: full image, optional save/stage preview, thumbnail sheet/slot, and variant ordinal in a reserved 2,000-row layout], **ALINIT (107 sparse alchemy recipes)** [numeric: output, level/story gates, point cost, and four paired ingredient slots in a reserved 1,000-row layout], **AFINIT (13 authored affinity rows plus tuning/facility curves)** [name/footer specialization], **CTINIT (five 70-cell name-entry pages)** [string-matrix specialization], **CVINIT (13 character-voice settings)** [numeric: preview assets, slot-to-unit joins, and inverse suppression-setting map], **LAINIT (20 shipped terrain definitions)** [name: sparse strings, topology/rendering, combat-stat, traversal-skill, and texture-fallback joins], **MPINIT (1,472 sparse terrain rows joined to 66 stage maps)** [footer: 53-column doubled-coordinate atlas], **SPINIT (eight 15-slot H-scene gallery pages)** [numeric: INIT2 thumbnail-sheet and SYS4INI scene joins], **TRINIT (21 training actions)** [name/string-matrix: eligibility, cost, stat/alignment/progress effects, rewards, and ten event slots], **CDINIT (nine weighted card-generation lists)** [numeric/dispatch specialization], **CDINIT2 (81 card definitions)** [name: story gates, typed effects, and item/event/condition/visual joins], **BTANINIT (202 battle effects)** [numeric/dispatch: visual/audio/atlas/hit-pulse work records], **BTANINIT2 (122 battle-animation timelines)** [numeric: six effect ids, delays, duration, and skill/weapon joins], **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays], **CCINIT (71 class-change rules over 33 units)** [rules: unit/level/state predicates plus title/cost/stat/skill effects], **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history], and **RTINIT (172 routine sets)** [banked: twenty movement/battle step banks, provider joins, and overwrite history]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. - [x] **2.3 — Extract `STINIT`'s 74 sparse stage records.** The mixed mode identifies the dominant `scjump_progress_a` dispatch, recovers 29 preallocated buffer layouts (including six consumer-confirmed row strides), and keeps four condition strings, six scalars, fixed-buffer writes, and all 1,396 footer-array copies separated by stage id. Profiles supply population/value and direct-consumer evidence. Header/map semantics cover conditions, BGM, turn/replay/clear-reward settings, and map texture overrides. The object join assembles 2,312 placements with type/position/gates, OBINIT names and available descriptions, 604 reinforcement schedules, and typed initial-faction, teleport, treasure, card-list, non-triggering-faction, and initial-object-state payloads; FIELD's dedicated special-spawn path proves the final three populated type-27 tagged writes are engine-dead, so they remain visible as ignored provenance rather than unresolved semantics. The 30-cell enemy family contributes 1,378 joined spawns with unit/faction, placement, difficulty/story gates, level scaling, weighted selection, difficulty-specific movement/battle routine sets, and 485 first-clear-only gates. Raw address views remain alongside generated `semantic_fields`. - [x] **2.3a — Extract `CCINIT`'s 71 class-change rules.** Rules mode preserves source-order guards and detects the unit id, minimum level, clear applied-state slot, selected title/level, deployment-cost delta, named fourteen-stat bonuses, awarded SKINIT skills, and state slot set by each rule. EBINIT/SKINIT definition joins and the common global semantic projection coexist with raw addresses. CALCCC/ADDEXP establish the apply/report ABI; EVOLVE explains the two level-independent empty-title Lily rules. The generated profile covers 33 units, 69 titled rules, 30 skill awards, three used promotion slots, and 19 populated output fields. - [x] **2.3b — Extract `SCINIT`'s scene-dispatch registry.** Dispatch mode recognizes 2,179 alternating writes to two 10,000-cell arrays, preserves all 710 overwritten decision ids, and emits 1,209 final rows. The primary column maps decisions to 135 packed SYS4INI ids, all resolved to numbered SC scripts; the parallel column is authored chapter metadata. All 847 live SCJUMP decisions join to rows and 844 final chapter tags agree with the independently decoded paths; three legacy/stale mismatches remain explicit. @@ -67,8 +67,9 @@ - [x] **2.3j — Extract `TRINIT`'s training-action registry.** Name/string-matrix specialization classifies all 365 instructions into 21 six-text-slot actions and nineteen contiguous numeric families. TRAIN proves story/level/alignment/progress/stat/item/skill gates, spirit and fourteen-stat effects, fractional alignment/progress changes, rewards, execution counts, and event dispatch. ITINIT/SKINIT resolve every gate and reward; all 75 event cells resolve through SCINIT, while GAMESTART proves their restored story-flag role. - [x] **2.3k — Extract `CDINIT`'s card-generation lists.** Numeric/dispatch specialization classifies all 1,565 instructions into nine selectors and 383 weighted candidates, joins every card to CDINIT2, and links seven live selectors to 246 STINIT card objects. FIELD proves the turn-scaled cumulative selection rule, 100-slot scan, 50-slot clear prefix, and two-column effective required-story-gate contract. - [x] **2.3l — Extract `CDINIT2`'s card definitions.** Name specialization classifies all 558 instructions into 81 cards inside a reserved 100-row registry. FIELD proves six type-specific effect paths and the engine-dead third required-flag column; ITINIT, SCINIT, ILINIT, and SYS4INI resolve all item, event, condition, and visual references, while STAGECLEAR proves the deferred point-award accumulator. +- [x] **2.3m — Extract `BTANINIT`/`BTANINIT2`'s battle-animation registry.** BTANINIT2 specialization classifies all 1,018 instructions into 122 sparse six-effect timelines, 573 effect references, paired start delays, and 111 complete durations. BTANINIT dispatch specialization classifies all 4,472 instructions into 202 visual/audio/hit-pulse definitions. BTL proves movie/sprite playback, blend, anchor, geometry, sound, and pulse semantics; SKINIT/ITINIT resolve skill and weapon-class selection. - [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, and CDINIT/CDINIT2 card selection/effects are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, audit BTANINIT/BTANINIT2's battle-animation registry and direct consumers, then use Frida only for semantics static consumers cannot settle. 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, and BTANINIT/BTANINIT2 battle animations are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, audit STINIT2's remaining stage-definition columns and consumers, then use Frida only for semantics static consumers cannot settle. 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/extract_init.py b/tools/extract_init.py index 45c3c78..593582f 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -42,6 +42,11 @@ CDINIT2 is a special name-mode registry: 81 cards occupy one contiguous 100-row definition block with story gates, item/event/point rewards, ranged HP/SP/FS and spirit effects, conditions, warp behavior, and visual assets. +BTANINIT2 is a special numeric-mode registry: 122 sparse battle animations +occupy three reserved 1,000-row arrays for six effect ids, six start delays, +and one total duration. BTANINIT dispatches 202 effect ids into BTL's six-slot +visual/audio/hit-pulse work record. + Records are {id, name?, desc?, fields:{"0x": value}} or, for footer tables, {id, global_addr, footer_off, values:[...]}. Column addresses are raw engine globals; confirmed names come from the generated engine global registry while raw keys remain provenance. @@ -70,6 +75,7 @@ T_GLOBAL_INT = 3 T_GLOBAL_STRING = 5 T_IMM = 0 T_LOCAL_INT = 9 +T_LOCAL_PTR = 12 CURRENT_UNIT_ID = 0x66715 CURRENT_UNIT_LEVELS = 0x6930 @@ -755,6 +761,53 @@ CARD_TYPE_NAMES = { } CARD_RESOURCE_COLUMNS = ("hp", "sp", "fs") +BATTLE_ANIMATION_SELECTOR = 0x15288C +BATTLE_ANIMATION_EFFECT_ID_BASE = 0x15288E +BATTLE_ANIMATION_EFFECT_DELAY_BASE = 0x153FFE +BATTLE_ANIMATION_DURATION_BASE = 0x15576E +BATTLE_ANIMATION_CAPACITY = 1000 +BATTLE_ANIMATION_SLOT_COUNT = 6 +BATTLE_EFFECT_HIT_PULSE_COUNT = 3 +BATTLE_EFFECT_WORK_ARRAYS = { + "visual_asset_id": (0x155B56, 1), + "visual_mode_id": (0x155B5C, 1), + "additive_blend_flag": (0x155B62, 1), + "width": (0x155B68, 1), + "height": (0x155B6E, 1), + "atlas_column_count": (0x155B74, 1), + "atlas_row_count": (0x155B7A, 1), + "atlas_frame_count": (0x155B80, 1), + "duration_ms": (0x155B86, 1), + "combatant_anchor_flag": (0x155B8C, 1), + "offset_x": (0x155B92, 1), + "offset_y": (0x155B98, 1), + "sound_asset_id": (0x155B9E, 1), + "sound_delay_ms": (0x155BA4, 1), + "hit_pulse_offsets_ms": (0x155BAA, BATTLE_EFFECT_HIT_PULSE_COUNT), +} +BATTLE_EFFECT_VISUAL_MODES = { + 0: "movie", + 1: "green_colorkey_sprite_sheet", + 2: "opaque_sprite_sheet", +} +BATTLE_EFFECT_SEMANTIC_NAMES = { + "visual_asset_id": "battle_effect_visual_asset_ids", + "visual_mode_id": "battle_effect_visual_mode_ids", + "additive_blend_flag": "battle_effect_additive_blend_flags", + "width": "battle_effect_widths", + "height": "battle_effect_heights", + "atlas_column_count": "battle_effect_atlas_column_counts", + "atlas_row_count": "battle_effect_atlas_row_counts", + "atlas_frame_count": "battle_effect_atlas_frame_counts", + "duration_ms": "battle_effect_durations_ms", + "combatant_anchor_flag": "battle_effect_combatant_anchor_flags", + "offset_x": "battle_effect_offset_x_pixels", + "offset_y": "battle_effect_offset_y_pixels", + "sound_asset_id": "battle_effect_sound_asset_ids", + "sound_delay_ms": "battle_effect_sound_delays_ms", + "hit_pulse_offsets_ms": "battle_effect_hit_pulse_offsets_ms", +} + def resolve(name: str) -> Path: for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"): @@ -4664,6 +4717,682 @@ def extract_card_definitions(scr): } +def extract_battle_effect_definitions(scr): + """Extract BTANINIT's effect-id dispatch into six-slot BTL work fields.""" + instructions = scr.instructions + labels = [ + sys4load.display_label(ins.opcode) for ins in instructions + ] + classified_offsets = set() + + clear_layout = [ + (base, ( + BATTLE_ANIMATION_SLOT_COUNT * stride + if stride > 1 else BATTLE_ANIMATION_SLOT_COUNT + )) + for base, stride in BATTLE_EFFECT_WORK_ARRAYS.values() + ] + if len(clear_layout) != 15: + raise AssertionError("battle-effect work layout must have 15 arrays") + for index, (base, count) in enumerate(clear_layout): + ins = instructions[index] + expected_args = [ + (T_GLOBAL_INT, base), + (T_IMM, count), + ] + if labels[index] != "copy-to-global" or ins.args != expected_args: + raise ValueError( + f"{scr.path.name}: unexpected work clear at " + f"0x{ins.offset:x}: {labels[index]} {ins.args!r}" + ) + classified_offsets.add(ins.offset) + + control_labels = ( + "mov", + "jmp", + "add", + "lt", + "jcc", + "call", + "jmp", + "jmp", + "lookup-array-2d", + "mov", + ) + control_start = len(clear_layout) + for relative, expected_label in enumerate(control_labels): + ins = instructions[control_start + relative] + if labels[control_start + relative] != expected_label: + raise ValueError( + f"{scr.path.name}: unexpected dispatch control at " + f"0x{ins.offset:x}: expected {expected_label}, got " + f"{labels[control_start + relative]}" + ) + classified_offsets.add(ins.offset) + + input_lookup = instructions[control_start + 8] + expected_lookup_args = [ + (T_LOCAL_PTR, 0), + (T_GLOBAL_INT, BATTLE_ANIMATION_EFFECT_ID_BASE), + (T_GLOBAL_INT, BATTLE_ANIMATION_SELECTOR), + (T_IMM, BATTLE_ANIMATION_SLOT_COUNT), + (T_LOCAL_INT, 1), + ] + if input_lookup.args != expected_lookup_args: + raise ValueError( + f"{scr.path.name}: unexpected effect-id input lookup " + f"{input_lookup.args!r}" + ) + + cases = {} + cursor = control_start + len(control_labels) + while cursor < len(instructions) and labels[cursor] == "eq": + compare = instructions[cursor] + if ( + len(compare.args) != 3 + or compare.args[0] != (T_LOCAL_INT, 2) + or compare.args[1] != (T_LOCAL_INT, 0) + or compare.args[2][0] != T_IMM + ): + raise ValueError( + f"{scr.path.name}: malformed effect comparison at " + f"0x{compare.offset:x}" + ) + effect_id = compare.args[2][1] + if effect_id in cases: + raise ValueError( + f"{scr.path.name}: duplicate effect id {effect_id}" + ) + classified_offsets.add(compare.offset) + cursor += 1 + + branch = instructions[cursor] + if labels[cursor] != "jcc": + raise ValueError( + f"{scr.path.name}: missing branch after effect " + f"{effect_id}" + ) + classified_offsets.add(branch.offset) + cursor += 1 + + values = {} + raw_fields = {} + raw_record_fields = {} + while labels[cursor] != "ret": + lookup = instructions[cursor] + if labels[cursor] not in { + "lookup-array", "lookup-array-2d" + }: + raise ValueError( + f"{scr.path.name}: unexpected effect {effect_id} " + f"instruction at 0x{lookup.offset:x}: " + f"{labels[cursor]}" + ) + cursor += 1 + write = instructions[cursor] + if ( + labels[cursor] != "mov" + or write.args[0] != (T_LOCAL_PTR, 0) + or write.args[1][0] != T_IMM + ): + raise ValueError( + f"{scr.path.name}: malformed effect {effect_id} " + f"write at 0x{write.offset:x}" + ) + + base = lookup.args[1][1] + field_name = next(( + name + for name, (candidate_base, _) in ( + BATTLE_EFFECT_WORK_ARRAYS.items() + ) + if candidate_base == base + ), None) + if field_name is None: + raise ValueError( + f"{scr.path.name}: effect {effect_id} writes " + f"unknown work base 0x{base:x}" + ) + _, stride = BATTLE_EFFECT_WORK_ARRAYS[field_name] + if stride == 1: + if ( + labels[cursor - 1] != "lookup-array" + or lookup.args != [ + (T_LOCAL_PTR, 0), + (T_GLOBAL_INT, base), + (T_LOCAL_INT, 1), + ] + ): + raise ValueError( + f"{scr.path.name}: malformed {field_name} lookup " + f"for effect {effect_id}" + ) + key = field_name + raw_fields[f"0x{base:x}"] = write.args[1][1] + else: + if ( + labels[cursor - 1] != "lookup-array-2d" + or lookup.args[:4] != [ + (T_LOCAL_PTR, 0), + (T_GLOBAL_INT, base), + (T_LOCAL_INT, 1), + (T_IMM, stride), + ] + or lookup.args[4][0] != T_IMM + ): + raise ValueError( + f"{scr.path.name}: malformed {field_name} lookup " + f"for effect {effect_id}" + ) + column = lookup.args[4][1] + if not 0 <= column < stride: + raise ValueError( + f"{scr.path.name}: effect {effect_id} " + f"{field_name} column {column} out of range" + ) + key = f"{field_name}.{column}" + raw_record_fields[ + f"0x{base:x}/{stride}/{column}" + ] = write.args[1][1] + + _store_unique( + values, key, write.args[1][1], effect_id + ) + classified_offsets.update((lookup.offset, write.offset)) + cursor += 1 + + classified_offsets.add(instructions[cursor].offset) + cursor += 1 + cases[effect_id] = { + "values": values, + "fields": raw_fields, + "record_fields": raw_record_fields, + } + + expected_tail = ( + "comment", + "instruction-marker-noop", + "ret", + "exit", + ) + if tuple(labels[cursor:]) != expected_tail: + raise ValueError( + f"{scr.path.name}: unexpected dispatch tail " + f"{labels[cursor:]!r}" + ) + classified_offsets.update( + ins.offset for ins in instructions[cursor:] + ) + if len(classified_offsets) != len(instructions): + raise ValueError( + f"{scr.path.name}: classified {len(classified_offsets)}/" + f"{len(instructions)} instructions" + ) + if cases.get(0, {}).get("values"): + raise ValueError( + f"{scr.path.name}: effect id zero must remain the empty sentinel" + ) + + asset_names = callscript_names() + animation_references = collections.defaultdict(set) + animation_scr = sys4load.load(resolve("BTANINIT2")) + for ins in animation_scr.instructions: + write = _static_global_write(ins) + if write is None: + continue + destination, value = write + index = destination - BATTLE_ANIMATION_EFFECT_ID_BASE + if not ( + 0 <= index + < BATTLE_ANIMATION_CAPACITY * BATTLE_ANIMATION_SLOT_COUNT + ): + continue + animation_id, _ = divmod( + index, BATTLE_ANIMATION_SLOT_COUNT + ) + animation_references[value].add(animation_id) + + records = [] + for effect_id, case in sorted(cases.items()): + if effect_id == 0: + continue + values = case["values"] + visual_asset_id = values["visual_asset_id"] + visual_mode_id = values["visual_mode_id"] + atlas = {} + for field_name, output_name in ( + ("atlas_column_count", "columns"), + ("atlas_row_count", "authored_rows"), + ("atlas_frame_count", "frame_count"), + ("duration_ms", "duration_ms"), + ): + if field_name in values: + atlas[output_name] = values[field_name] + + sound_asset_id = values.get("sound_asset_id", 0) + hit_pulses = [ + values.get(f"hit_pulse_offsets_ms.{column}", 0) + for column in range(BATTLE_EFFECT_HIT_PULSE_COUNT) + ] + record = { + "id": effect_id, + "visual_asset_id": visual_asset_id, + "visual_asset_name": asset_names.get( + visual_asset_id, "" + ), + "visual_mode_id": visual_mode_id, + "visual_mode": BATTLE_EFFECT_VISUAL_MODES.get( + visual_mode_id, "" + ), + "additive_blend": bool( + values.get("additive_blend_flag", 0) + ), + "width": values["width"], + "height": values["height"], + "anchor": ( + "slot_combatant" + if values.get("combatant_anchor_flag", 0) + else "battlefield_center" + ), + "offset_x": values["offset_x"], + "offset_y": values["offset_y"], + "atlas": atlas, + "sound_asset_id": sound_asset_id, + "sound_asset_name": ( + asset_names.get(sound_asset_id, "") + if sound_asset_id else "" + ), + "sound_delay_ms": values.get("sound_delay_ms", 0), + "hit_pulse_offsets_ms": [ + value for value in hit_pulses if value + ], + "referenced_animation_ids": sorted( + animation_references.get(effect_id, ()) + ), + "fields": case["fields"], + "record_fields": case["record_fields"], + } + records.append(record) + + unreferenced = [ + record["id"] + for record in records + if not record["referenced_animation_ids"] + ] + schema_field_semantics = {} + semantic_array_names = {} + for field_name, (base, stride) in ( + BATTLE_EFFECT_WORK_ARRAYS.items() + ): + semantic_name = BATTLE_EFFECT_SEMANTIC_NAMES[field_name] + semantic_array_names[f"0x{base:x}"] = semantic_name + if stride == 1: + schema_field_semantics[f"0x{base:x}"] = semantic_name + else: + for column in range(stride): + schema_field_semantics[ + f"0x{base:x}/{stride}/{column}" + ] = f"{semantic_name}.pulse_{column + 1}" + + return records, { + "schema": "battle-effect-definitions", + "effect_definition_count": len(records), + "zero_effect_id_is_empty": True, + "runtime_work_slot_count": BATTLE_ANIMATION_SLOT_COUNT, + "hit_pulse_capacity_per_slot": BATTLE_EFFECT_HIT_PULSE_COUNT, + "definition_write_count": sum( + len(record["fields"]) + len(record["record_fields"]) + for record in records + ), + "classified_instruction_count": len(classified_offsets), + "visual_mode_counts": dict(sorted(collections.Counter( + record["visual_mode"] for record in records + ).items())), + "resolved_visual_asset_count": sum( + bool(record["visual_asset_name"]) for record in records + ), + "resolved_sound_asset_count": sum( + bool(record["sound_asset_name"]) for record in records + ), + "sound_effect_count": sum( + bool(record["sound_asset_id"]) for record in records + ), + "hit_pulse_effect_count": sum( + bool(record["hit_pulse_offsets_ms"]) for record in records + ), + "sprite_sheet_effect_count": sum( + bool(record["atlas"]) for record in records + ), + "engine_dead_atlas_row_count": sum( + "authored_rows" in record["atlas"] for record in records + ), + "referenced_effect_definition_count": ( + len(records) - len(unreferenced) + ), + "unreferenced_effect_definition_ids": unreferenced, + "array_layouts": { + f"0x{base:x}": {"stride": stride} + for base, stride in BATTLE_EFFECT_WORK_ARRAYS.values() + if stride > 1 + }, + "schema_field_semantics": schema_field_semantics, + "semantic_array_names": semantic_array_names, + "consumer_contract": { + "BTANINIT2.BIN": ( + "selects up to six effect ids and their start delays for " + "each sparse battle-animation row" + ), + "BTL.BIN": ( + "draws a movie or sprite-sheet surface, applies optional " + "additive blending, anchors slots 0/1 to the actor and " + "slots 2..5 to the target when requested, schedules WAV " + "start, and consumes up to three hit-pulse offsets" + ), + }, + } + + +def extract_battle_animations(scr): + """Extract BTANINIT2's sparse 1000-row battle-animation timeline.""" + cells = { + "effect_ids": {}, + "effect_start_delays_ms": {}, + "duration_ms": {}, + } + classified_offsets = set() + static_write_count = 0 + layouts = { + "effect_ids": ( + BATTLE_ANIMATION_EFFECT_ID_BASE, + BATTLE_ANIMATION_SLOT_COUNT, + ), + "effect_start_delays_ms": ( + BATTLE_ANIMATION_EFFECT_DELAY_BASE, + BATTLE_ANIMATION_SLOT_COUNT, + ), + "duration_ms": (BATTLE_ANIMATION_DURATION_BASE, 1), + } + + for ins in scr.instructions: + if sys4load.display_label(ins.opcode) == "exit": + classified_offsets.add(ins.offset) + continue + write = _static_global_write(ins) + if write is None: + continue + static_write_count += 1 + destination, value = write + for field_name, (base, stride) in layouts.items(): + index = destination - base + if not ( + 0 <= index < BATTLE_ANIMATION_CAPACITY * stride + ): + continue + animation_id, column = divmod(index, stride) + if animation_id == 0: + raise ValueError( + f"{scr.path.name}: writes reserved animation row zero " + f"at 0x{ins.offset:x}" + ) + _store_unique( + cells[field_name], + (animation_id, column), + value, + animation_id, + ) + classified_offsets.add(ins.offset) + break + else: + raise ValueError( + f"{scr.path.name}: unclassified animation write " + f"0x{destination:x} at 0x{ins.offset:x}" + ) + + unclassified = [ + f"0x{ins.offset:x}" + for ins in scr.instructions + if ins.offset not in classified_offsets + ] + if unclassified: + raise ValueError( + f"{scr.path.name}: unclassified instructions " + + ", ".join(unclassified) + ) + + effect_records, effect_meta = extract_battle_effect_definitions( + sys4load.load(resolve("BTANINIT")) + ) + effects_by_id = { + record["id"]: record for record in effect_records + } + + skill_records, _ = extract_name( + sys4load.load(resolve("SKINIT")) + ) + skill_uses = collections.defaultdict(list) + for record in skill_records: + animation_id = record.get("fields", {}).get("0xaaa1e", 0) + if animation_id: + skill_uses[animation_id].append({ + "skill_id": record["id"], + "skill_name": record["name"], + }) + + item_records, _ = extract_name( + sys4load.load(resolve("ITINIT")) + ) + weapon_class_items = collections.defaultdict(list) + for record in item_records: + weapon_class = record.get("fields", {}).get("0xa6689", 0) + if weapon_class: + weapon_class_items[weapon_class].append({ + "item_id": record["id"], + "item_name": record["name"], + }) + + animation_ids = sorted({ + animation_id + for field_cells in cells.values() + for animation_id, _ in field_cells + }) + records = [] + resolved_effect_reference_count = 0 + for animation_id in animation_ids: + raw_record_fields = {} + slots = [] + for slot in range(BATTLE_ANIMATION_SLOT_COUNT): + effect_key = (animation_id, slot) + if effect_key not in cells["effect_ids"]: + continue + effect_id = cells["effect_ids"][effect_key] + delay_key = (animation_id, slot) + start_delay_ms = cells[ + "effect_start_delays_ms" + ].get(delay_key, 0) + effect = effects_by_id.get(effect_id, {}) + if effect: + resolved_effect_reference_count += 1 + raw_record_fields[ + f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}/" + f"{BATTLE_ANIMATION_SLOT_COUNT}/{slot}" + ] = effect_id + if delay_key in cells["effect_start_delays_ms"]: + raw_record_fields[ + f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}/" + f"{BATTLE_ANIMATION_SLOT_COUNT}/{slot}" + ] = start_delay_ms + slots.append({ + "slot": slot, + "anchor_side": ( + "actor" if slot < 2 else "target" + ), + "effect_id": effect_id, + "start_delay_ms": start_delay_ms, + "effect": { + key: value + for key, value in effect.items() + if key not in { + "fields", + "record_fields", + "referenced_animation_ids", + } + }, + }) + + duration_key = (animation_id, 0) + duration_ms = cells["duration_ms"].get(duration_key, 0) + raw_fields = {} + if duration_key in cells["duration_ms"]: + raw_fields[ + f"0x{BATTLE_ANIMATION_DURATION_BASE:x}" + ] = duration_ms + uses = [] + if 1 <= animation_id <= 21: + uses.append("normal_attack_weapon_class") + if skill_uses.get(animation_id): + uses.append("skill") + if animation_id == 809: + uses.append("defeat") + if not uses: + uses.append("unjoined_authored") + records.append({ + "id": animation_id, + "duration_ms": duration_ms, + "effect_slots": slots, + "uses": uses, + "skill_uses": skill_uses.get(animation_id, []), + "weapon_class_items": weapon_class_items.get( + animation_id, [] + ) if 1 <= animation_id <= 21 else [], + "fields": raw_fields, + "record_fields": raw_record_fields, + }) + + effect_reference_values = list(cells["effect_ids"].values()) + delay_columns = collections.Counter( + column + for _, column in cells["effect_start_delays_ms"] + ) + effect_columns = collections.Counter( + column for _, column in cells["effect_ids"] + ) + unjoined_animation_ids = [ + record["id"] for record in records + if record["uses"] == ["unjoined_authored"] + ] + return records, { + "schema": "battle-animation-timelines", + "reserved_record_count": BATTLE_ANIMATION_CAPACITY, + "authored_record_count": len(records), + "effect_slots_per_record": BATTLE_ANIMATION_SLOT_COUNT, + "effect_id_array_base": ( + f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}" + ), + "effect_start_delay_array_base": ( + f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}" + ), + "duration_array_base": ( + f"0x{BATTLE_ANIMATION_DURATION_BASE:x}" + ), + "static_write_count": static_write_count, + "classified_instruction_count": len(classified_offsets), + "effect_reference_count": len(effect_reference_values), + "distinct_effect_id_count": len(set(effect_reference_values)), + "resolved_effect_reference_count": ( + resolved_effect_reference_count + ), + "effect_slot_populations": { + str(column): effect_columns.get(column, 0) + for column in range(BATTLE_ANIMATION_SLOT_COUNT) + }, + "delay_slot_populations": { + str(column): delay_columns.get(column, 0) + for column in range(BATTLE_ANIMATION_SLOT_COUNT) + }, + "duration_cell_count": len(cells["duration_ms"]), + "complete_timeline_count": sum( + bool(record["duration_ms"]) for record in records + ), + "auxiliary_timeline_count": sum( + not record["duration_ms"] for record in records + ), + "skill_reference_count": sum( + len(record["skill_uses"]) for record in records + ), + "skill_animation_count": sum( + bool(record["skill_uses"]) for record in records + ), + "weapon_class_animation_count": 21, + "unjoined_authored_animation_ids": unjoined_animation_ids, + "effect_definition_count": effect_meta[ + "effect_definition_count" + ], + "unreferenced_effect_definition_ids": effect_meta[ + "unreferenced_effect_definition_ids" + ], + "array_layouts": { + f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}": { + "stride": BATTLE_ANIMATION_SLOT_COUNT + }, + f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}": { + "stride": BATTLE_ANIMATION_SLOT_COUNT + }, + }, + "schema_field_semantics": { + **{ + f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}/" + f"{BATTLE_ANIMATION_SLOT_COUNT}/{column}": ( + "battle_animation_effect_ids." + f"effect_slot_{column}" + ) + for column in range(BATTLE_ANIMATION_SLOT_COUNT) + }, + **{ + f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}/" + f"{BATTLE_ANIMATION_SLOT_COUNT}/{column}": ( + "battle_animation_effect_start_delays_ms." + f"effect_slot_{column}" + ) + for column in range(BATTLE_ANIMATION_SLOT_COUNT) + }, + f"0x{BATTLE_ANIMATION_DURATION_BASE:x}": ( + "battle_animation_duration_ms" + ), + }, + "semantic_array_names": { + f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}": ( + "battle_animation_effect_ids" + ), + f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}": ( + "battle_animation_effect_start_delays_ms" + ), + f"0x{BATTLE_ANIMATION_DURATION_BASE:x}": ( + "battle_animation_duration_ms" + ), + }, + "consumer_contract": { + "CALCDMG.BIN": ( + "selects a skill's animation id or the equipped item's " + "weapon-class id for an ordinary attack" + ), + "BTL.BIN": ( + "calls BTANINIT for the selected row, starts each populated " + "effect at its six-slot delay, schedules audio and hit " + "pulses, and uses the row duration to stage voice and HP " + "interpolation; animation 809 is the hardcoded defeat row" + ), + "SKINIT.BIN": ( + "provides 101 skill references across 98 distinct animation " + "rows, including passive reaction rows 801 through 808" + ), + "ITINIT.BIN": ( + "provides the weapon-class ids that select normal-attack " + "animation rows 1 through 21" + ), + }, + } + + def extract_card_generation_lists(scr): """Extract CDINIT's selector-dispatched weighted card candidate lists.""" instructions = scr.instructions @@ -5705,6 +6434,11 @@ def write_data_index(data_dir: Path) -> None: "six effect types, and raw array coordinates remain together; item, event, condition,", "and visual ids join through ITINIT, SCINIT, ILINIT, and SYS4INI resources.", "", + "BTANINIT2's dedicated battle-animation schema exposes 122 sparse timelines in", + "three reserved 1,000-row arrays: six effect ids, six start delays, and total", + "duration. BTANINIT's paired schema decodes 202 effect ids into BTL's six-slot", + "movie/sprite, blend, geometry, audio, and hit-pulse work record.", + "", "MPINIT's dedicated terrain-atlas schema exposes 1,472 authored rows of a sparse", "53-column half-tile grid. It joins STINIT2's doubled tile-bound rectangles to 66", "stage definitions, preserves implicit-zero rows and raw footer provenance, and", @@ -5814,6 +6548,10 @@ def main() -> int: extractor = extract_card_definitions elif mode == "numeric" and name == "CDINIT": extractor = extract_card_generation_lists + elif mode == "numeric" and name == "BTANINIT": + extractor = extract_battle_effect_definitions + elif mode == "numeric" and name == "BTANINIT2": + extractor = extract_battle_animations elif mode == "numeric" and name == "SPINIT": extractor = extract_h_scene_gallery elif mode == "footer" and name == "MPINIT": diff --git a/tools/init_table_profile.py b/tools/init_table_profile.py index 72f82a8..06c5565 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -494,6 +494,90 @@ def profile_card_definitions(data: dict) -> dict: } +def profile_battle_effect_definitions(data: dict) -> dict: + """Summarize BTANINIT's visual/audio/hit-pulse effect catalog.""" + if data.get("schema") != "battle-effect-definitions": + return {} + return { + "effect_definition_count": data.get( + "effect_definition_count", 0 + ), + "runtime_work_slot_count": data.get( + "runtime_work_slot_count", 0 + ), + "visual_mode_counts": data.get("visual_mode_counts", {}), + "resolved_visual_asset_count": data.get( + "resolved_visual_asset_count", 0 + ), + "sound_effect_count": data.get("sound_effect_count", 0), + "resolved_sound_asset_count": data.get( + "resolved_sound_asset_count", 0 + ), + "sprite_sheet_effect_count": data.get( + "sprite_sheet_effect_count", 0 + ), + "hit_pulse_effect_count": data.get( + "hit_pulse_effect_count", 0 + ), + "referenced_effect_definition_count": data.get( + "referenced_effect_definition_count", 0 + ), + "unreferenced_effect_definition_ids": data.get( + "unreferenced_effect_definition_ids", [] + ), + "engine_dead_atlas_row_count": data.get( + "engine_dead_atlas_row_count", 0 + ), + } + + +def profile_battle_animations(data: dict) -> dict: + """Summarize BTANINIT2's sparse six-slot animation timelines.""" + if data.get("schema") != "battle-animation-timelines": + return {} + return { + "authored_record_count": data.get( + "authored_record_count", 0 + ), + "reserved_record_count": data.get( + "reserved_record_count", 0 + ), + "effect_reference_count": data.get( + "effect_reference_count", 0 + ), + "distinct_effect_id_count": data.get( + "distinct_effect_id_count", 0 + ), + "resolved_effect_reference_count": data.get( + "resolved_effect_reference_count", 0 + ), + "effect_slot_populations": data.get( + "effect_slot_populations", {} + ), + "delay_slot_populations": data.get( + "delay_slot_populations", {} + ), + "complete_timeline_count": data.get( + "complete_timeline_count", 0 + ), + "auxiliary_timeline_count": data.get( + "auxiliary_timeline_count", 0 + ), + "skill_reference_count": data.get( + "skill_reference_count", 0 + ), + "skill_animation_count": data.get( + "skill_animation_count", 0 + ), + "weapon_class_animation_count": data.get( + "weapon_class_animation_count", 0 + ), + "unjoined_authored_animation_ids": data.get( + "unjoined_authored_animation_ids", [] + ), + } + + def profile_messages(data: dict) -> dict: """Summarize the joined player-facing message evidence.""" records = data["records"] @@ -621,7 +705,54 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str: f"- records: {data['record_count']}", f"- populated fields: {len(rows)}", ] - if definition_profile := profile_card_definitions(data): + if effect_profile := profile_battle_effect_definitions(data): + lines.extend([ + f"- effect definitions: " + f"{effect_profile['effect_definition_count']} for " + f"{effect_profile['runtime_work_slot_count']} runtime slots", + f"- visual modes: {effect_profile['visual_mode_counts']}", + f"- visual assets resolved: " + f"{effect_profile['resolved_visual_asset_count']}/" + f"{effect_profile['effect_definition_count']}", + f"- sound assets resolved: " + f"{effect_profile['resolved_sound_asset_count']}/" + f"{effect_profile['sound_effect_count']}", + f"- sprite-sheet/hit-pulse effects: " + f"{effect_profile['sprite_sheet_effect_count']}/" + f"{effect_profile['hit_pulse_effect_count']}", + f"- referenced definitions: " + f"{effect_profile['referenced_effect_definition_count']}/" + f"{effect_profile['effect_definition_count']}; unreferenced " + f"{effect_profile['unreferenced_effect_definition_ids']}", + f"- engine-dead authored atlas-row cells: " + f"{effect_profile['engine_dead_atlas_row_count']}", + ]) + elif animation_profile := profile_battle_animations(data): + lines.extend([ + f"- battle animations: " + f"{animation_profile['authored_record_count']}/" + f"{animation_profile['reserved_record_count']} rows", + f"- effect references: " + f"{animation_profile['effect_reference_count']} across " + f"{animation_profile['distinct_effect_id_count']} definitions " + f"({animation_profile['resolved_effect_reference_count']} " + f"resolved)", + f"- populated effect slots: " + f"{animation_profile['effect_slot_populations']}", + f"- populated delay slots: " + f"{animation_profile['delay_slot_populations']}", + f"- full/auxiliary timelines: " + f"{animation_profile['complete_timeline_count']}/" + f"{animation_profile['auxiliary_timeline_count']}", + f"- SKINIT joins: " + f"{animation_profile['skill_reference_count']} skills across " + f"{animation_profile['skill_animation_count']} animations", + f"- weapon-class rows: " + f"{animation_profile['weapon_class_animation_count']}", + f"- unjoined authored rows: " + f"{animation_profile['unjoined_authored_animation_ids']}", + ]) + elif definition_profile := profile_card_definitions(data): lines.extend([ f"- card definitions: {definition_profile['card_count']}/" f"{definition_profile['reserved_record_count']} rows", diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index d106d0b..adf6ae5 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1810,6 +1810,169 @@ def test_card_definitions() -> None: ) +def test_battle_effect_definitions() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["BTANINIT.BIN"]) + check( + extract_init.detect_mode(script) == "numeric", + "BTANINIT remains compatible with numeric-mode auto-detection", + ) + records, meta = extract_init.extract_battle_effect_definitions( + script + ) + by_id = {record["id"]: record for record in records} + check( + len(records) == 202 + and meta["runtime_work_slot_count"] == 6 + and meta["hit_pulse_capacity_per_slot"] == 3 + and meta["zero_effect_id_is_empty"], + "BTANINIT exposes 202 sparse effect ids and the six-slot work ABI", + ) + check( + meta["definition_write_count"] == 1917 + and meta["classified_instruction_count"] == 4472, + "BTANINIT classifies every clear, branch, work write, and exit", + ) + check( + meta["visual_mode_counts"] + == { + "green_colorkey_sprite_sheet": 1, + "movie": 186, + "opaque_sprite_sheet": 15, + } + and meta["sprite_sheet_effect_count"] == 16, + "BTANINIT separates movie and sprite-sheet effect paths", + ) + check( + by_id[1]["visual_asset_name"] == "MVB001.AGF" + and by_id[1]["sound_asset_name"] == "A0124.WAV" + and by_id[1]["additive_blend"] + and by_id[1]["anchor"] == "slot_combatant" + and by_id[1]["width"] == 280 + and by_id[1]["height"] == 352, + "BTANINIT effect 1 joins its movie, sound, blend, and geometry", + ) + check( + by_id[24]["visual_asset_name"] == "AM994.AGF" + and by_id[24]["visual_mode"] + == "green_colorkey_sprite_sheet" + and by_id[24]["atlas"] + == { + "columns": 4, + "authored_rows": 4, + "frame_count": 16, + "duration_ms": 800, + } + and by_id[131]["visual_mode"] == "opaque_sprite_sheet" + and by_id[131]["atlas"]["frame_count"] == 28, + "BTANINIT preserves sprite-atlas geometry and duration", + ) + check( + by_id[915]["hit_pulse_offsets_ms"] == [100] + and by_id[915]["record_fields"]["0x155baa/3/0"] == 100 + and meta["hit_pulse_effect_count"] == 26, + "BTANINIT exposes BTL's hit-pulse timing rows", + ) + check( + meta["resolved_visual_asset_count"] == 202 + and meta["sound_effect_count"] == 199 + and meta["resolved_sound_asset_count"] == 199, + "BTANINIT resolves every populated AGF and WAV resource", + ) + check( + meta["referenced_effect_definition_count"] == 186 + and meta["unreferenced_effect_definition_ids"] + == [ + 24, 806, 807, 901, 902, 903, 904, 905, + 906, 907, 910, 995, 996, 997, 998, 999, + ], + "BTANINIT retains all sixteen unreferenced authored effects", + ) + + +def test_battle_animations() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["BTANINIT2.BIN"]) + check( + extract_init.detect_mode(script) == "numeric", + "BTANINIT2 remains compatible with numeric-mode auto-detection", + ) + records, meta = extract_init.extract_battle_animations(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 122 + and meta["reserved_record_count"] == 1000 + and meta["effect_slots_per_record"] == 6, + "BTANINIT2 exposes 122 sparse rows in its reserved timeline registry", + ) + check( + meta["static_write_count"] == 1017 + and meta["classified_instruction_count"] == 1018 + and meta["effect_reference_count"] == 573 + and meta["distinct_effect_id_count"] == 186 + and meta["resolved_effect_reference_count"] == 573, + "BTANINIT2 classifies every write and resolves every effect id", + ) + check( + meta["effect_slot_populations"] + == { + "0": 114, "1": 114, "2": 113, + "3": 113, "4": 0, "5": 119, + } + and meta["delay_slot_populations"] + == { + "0": 0, "1": 0, "2": 111, + "3": 111, "4": 0, "5": 111, + }, + "BTANINIT2 preserves its actor/target slot and delay geometry", + ) + check( + by_id[1]["duration_ms"] == 670 + and [ + (slot["slot"], slot["effect_id"], slot["start_delay_ms"]) + for slot in by_id[1]["effect_slots"] + ] + == [ + (0, 958, 0), + (1, 958, 0), + (2, 1, 200), + (3, 1, 200), + (5, 921, 470), + ] + and by_id[1]["weapon_class_items"] + == [{"item_id": 901, "item_name": "素手"}], + "BTANINIT2 row 1 joins the unarmed attack timeline and item class", + ) + check( + by_id[101]["skill_uses"] + == [{"skill_id": 101, "skill_name": "急所射撃"}] + and by_id[101]["effect_slots"][2]["effect"][ + "visual_asset_name" + ] == "MVB101.AGF" + and meta["skill_reference_count"] == 101 + and meta["skill_animation_count"] == 98, + "BTANINIT2 joins all SKINIT animation references", + ) + check( + by_id[801]["skill_uses"] + == [{"skill_id": 33, "skill_name": "見切り"}] + and by_id[801]["duration_ms"] == 0 + and by_id[809]["uses"] == ["defeat"] + and by_id[809]["effect_slots"][0]["effect"][ + "visual_asset_name" + ] == "AM050.AGF" + and meta["complete_timeline_count"] == 111 + and meta["auxiliary_timeline_count"] == 11, + "BTANINIT2 distinguishes full timelines and passive/defeat auxiliaries", + ) + check( + meta["unjoined_authored_animation_ids"] + == [205, 221, 222, 224] + and by_id[205]["uses"] == ["unjoined_authored"], + "BTANINIT2 preserves four authored rows without shipped consumers", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -2029,6 +2192,8 @@ if __name__ == "__main__": test_training_actions() test_card_generation_lists() test_card_definitions() + test_battle_effect_definitions() + test_battle_animations() test_map_terrain_atlas() test_condition_definitions() test_field_semantics() diff --git a/tools/test_globals.py b/tools/test_globals.py index 7a8baf3..a963525 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -103,6 +103,16 @@ def test_load_and_lint(): and entries[0x4dfbb]["name"] == "stage_card_spendable_point_bonus", "CDINIT2/FIELD card-effect state is curated") + check(entries[0x15288c]["name"] == "selected_battle_animation_id" + and entries[0x15288e]["columns"]["4"] + == "reserved_effect_slot_4" + and entries[0x153ffe]["name"] + == "battle_animation_effect_start_delays_ms" + and entries[0x15576e]["name"] == "battle_animation_duration_ms" + and entries[0x155b5c]["name"] == "battle_effect_visual_mode_ids" + and entries[0x155b7a]["name"] == "battle_effect_atlas_row_counts" + and entries[0x155baa]["columns"]["2"] == "pulse_3", + "BTANINIT/BTANINIT2 battle-animation state is curated") check(entries[0x15a095]["name"] == "information_tab_index" and entries[0x15a096]["name"] == "information_message_handled" and entries[0x15a097]["name"] diff --git a/tools/test_init_table_profile.py b/tools/test_init_table_profile.py index 104b58b..fc4e56a 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -350,6 +350,70 @@ def main() -> int: assert "- card definitions: 3/100 rows" in rendered_definitions assert "- engine-dead third required flags: 1" in rendered_definitions + effect_fixture = { + "table": "EFFECTS", + "mode": "numeric", + "schema": "battle-effect-definitions", + "record_count": 4, + "effect_definition_count": 4, + "runtime_work_slot_count": 6, + "visual_mode_counts": {"movie": 3, "opaque_sprite_sheet": 1}, + "resolved_visual_asset_count": 4, + "sound_effect_count": 3, + "resolved_sound_asset_count": 3, + "sprite_sheet_effect_count": 1, + "hit_pulse_effect_count": 2, + "referenced_effect_definition_count": 3, + "unreferenced_effect_definition_ids": [999], + "engine_dead_atlas_row_count": 1, + "records": [{}, {}, {}, {}], + } + effect_summary = profile.profile_battle_effect_definitions( + effect_fixture + ) + assert effect_summary["effect_definition_count"] == 4 + assert effect_summary["visual_mode_counts"]["movie"] == 3 + rendered_effects = profile.render_markdown( + effect_fixture, [], 40 + ) + assert "- effect definitions: 4 for 6 runtime slots" in rendered_effects + assert "- sound assets resolved: 3/3" in rendered_effects + + animation_fixture = { + "table": "ANIMATIONS", + "mode": "numeric", + "schema": "battle-animation-timelines", + "record_count": 3, + "authored_record_count": 3, + "reserved_record_count": 1000, + "effect_reference_count": 12, + "distinct_effect_id_count": 8, + "resolved_effect_reference_count": 12, + "effect_slot_populations": { + "0": 2, "1": 2, "2": 2, "3": 2, "4": 0, "5": 4, + }, + "delay_slot_populations": { + "0": 0, "1": 0, "2": 2, "3": 2, "4": 0, "5": 2, + }, + "complete_timeline_count": 2, + "auxiliary_timeline_count": 1, + "skill_reference_count": 2, + "skill_animation_count": 2, + "weapon_class_animation_count": 21, + "unjoined_authored_animation_ids": [205], + "records": [{}, {}, {}], + } + animation_summary = profile.profile_battle_animations( + animation_fixture + ) + assert animation_summary["authored_record_count"] == 3 + assert animation_summary["auxiliary_timeline_count"] == 1 + rendered_animations = profile.render_markdown( + animation_fixture, [], 40 + ) + assert "- battle animations: 3/1000 rows" in rendered_animations + assert "- full/auxiliary timelines: 2/1" in rendered_animations + training_fixture = { "table": "TRAINING", "mode": "name", diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 0ac3e30..7805dd7 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -3966,6 +3966,218 @@ source = "investigation" confidence = "high" depends_on = ["0x1519f8"] +[[global]] +address = "0x15288c" +name = "selected_battle_animation_id" +category = "index-pointer" +type = "int" +value_domain = "BTANINIT2 animation id 1..809; zero means no animation" +usage = "CALCDMG selects a skill_battle_animation_id or an equipped item's item_weapon_class, and BTL also writes passive reaction ids 801..808 and hardcoded defeat id 809. BTANINIT uses this value to select one six-effect timeline." +source = "investigation" +confidence = "high" +depends_on = ["0xaaa1e", "0xa6689", "0x15288e"] + +[[global]] +address = "0x15288e" +name = "battle_animation_effect_ids" +category = "data-table" +type = "int[1000][6]" +columns = { "0" = "effect_slot_0", "1" = "effect_slot_1", "2" = "effect_slot_2", "3" = "effect_slot_3", "4" = "reserved_effect_slot_4", "5" = "effect_slot_5" } +value_domain = "BTANINIT effect ids 1..966; zero means no effect" +usage = "BTANINIT2's sparse battle-animation composition table. BTANINIT expands the selected row into six runtime effect work slots. BTL anchors requested effects in slots 0 and 1 to the actor and slots 2 through 5 to the target; shipped data never populates slot 4." +source = "investigation" +confidence = "high" +depends_on = ["0x15288c", "0x153ffe", "0x155b56"] + +[[global]] +address = "0x153ffe" +name = "battle_animation_effect_start_delays_ms" +category = "data-table" +type = "int[1000][6]" +columns = { "0" = "effect_slot_0", "1" = "effect_slot_1", "2" = "effect_slot_2", "3" = "effect_slot_3", "4" = "reserved_effect_slot_4", "5" = "effect_slot_5" } +value_domain = "non-negative milliseconds; zero means start immediately" +usage = "Per-effect start delays paired with battle_animation_effect_ids. BTL passes each populated value to movie/sprite animation setup and adds it to the effect-local sound and hit-pulse offsets. Shipped data authors delays only for slots 2, 3, and 5." +source = "investigation" +confidence = "high" +depends_on = ["0x15288e", "0x15576e"] + +[[global]] +address = "0x15576e" +name = "battle_animation_duration_ms" +category = "data-table" +type = "int[1000]" +value_domain = "non-negative milliseconds; shipped complete timelines use 500..1700" +usage = "Overall timing boundary for one BTANINIT2 animation row. BTL uses it to schedule delayed battle voice and to begin the 500 ms HP interpolation after the main animation. The passive/defeat auxiliary rows 801..809 intentionally omit it." +source = "investigation" +confidence = "high" +depends_on = ["0x15288c", "0x15288e"] + +[[global]] +address = "0x155b56" +name = "battle_effect_visual_asset_ids" +category = "data-table" +type = "int[6]" +value_domain = "SYS4INI MVB*.AGF or AM*.AGF resource id; zero means empty slot" +usage = "BTANINIT clears and materializes the selected animation's six visual resources. BTL opens mode-0 values as nonblocking MPEG-in-AGF movies and mode-1/2 values as sprite sheets. All 202 effect definitions resolve through SYS4INI." +source = "investigation" +confidence = "high" +depends_on = ["0x15288e", "0x155b5c"] + +[[global]] +address = "0x155b5c" +name = "battle_effect_visual_mode_ids" +category = "data-table" +type = "int[6]" +value_domain = "0=movie, 1=green-colorkey sprite sheet, 2=opaque sprite sheet" +usage = "BTANINIT visual playback mode per materialized effect slot. BTL routes zero through play-movie-to-surface, mode 1 through set-texture with green color key 0x00ff00, and mode 2 through set-texture with no color key." +source = "investigation" +confidence = "high" +depends_on = ["0x155b56", "0x155b74", "0x155b80", "0x155b86"] + +[[global]] +address = "0x155b62" +name = "battle_effect_additive_blend_flags" +category = "data-table" +type = "int[6]" +value_domain = "{0,1}" +usage = "When nonzero, BTL applies gfx-draw-color mode 1 (SRCALPHA/ONE additive glow) to the materialized battle-effect surface. The flag is populated on all 186 movie effects and the sole green-colorkey sprite-sheet effect." +source = "investigation" +confidence = "high" +depends_on = ["0x155b56"] + +[[global]] +address = "0x155b68" +name = "battle_effect_widths" +category = "data-table" +type = "int[6]" +value_domain = "surface width in pixels; shipped values 208..600" +usage = "Width of the destination surface BTL creates for each materialized battle effect. It also participates in centering the surface before applying battle_effect_offset_x_pixels." +source = "investigation" +confidence = "high" +depends_on = ["0x155b6e", "0x155b92"] + +[[global]] +address = "0x155b6e" +name = "battle_effect_heights" +category = "data-table" +type = "int[6]" +value_domain = "surface height in pixels; shipped values 208..400" +usage = "Height of the destination surface BTL creates for each materialized battle effect. It also participates in centering the surface before applying battle_effect_offset_y_pixels." +source = "investigation" +confidence = "high" +depends_on = ["0x155b68", "0x155b98"] + +[[global]] +address = "0x155b74" +name = "battle_effect_atlas_column_counts" +category = "data-table" +type = "int[6]" +value_domain = "4..7; zero for movie effects" +usage = "Sprite-sheet column count passed by BTL to animate-gfx-srcrect-target. The sixteen sprite-sheet definitions pair it with battle_effect_atlas_frame_counts and a finite duration." +source = "investigation" +confidence = "high" +depends_on = ["0x155b5c", "0x155b80", "0x155b86"] + +[[global]] +address = "0x155b7a" +name = "battle_effect_atlas_row_counts" +category = "data-table" +type = "int[6]" +value_domain = "2..5; zero for movie effects" +usage = "Authored sprite-sheet row count. It agrees with ceil(frame_count / column_count) for all sixteen sprite effects, but BTL never reads this array and instead passes the already-authored frame count and column count to animate-gfx-srcrect-target." +source = "investigation" +confidence = "high" +depends_on = ["0x155b74", "0x155b80"] + +[[global]] +address = "0x155b80" +name = "battle_effect_atlas_frame_counts" +category = "data-table" +type = "int[6]" +value_domain = "10..28; zero for movie effects" +usage = "Total sprite-sheet frame count passed by BTL to animate-gfx-srcrect-target. Shipped values equal atlas columns times authored rows." +source = "investigation" +confidence = "high" +depends_on = ["0x155b74", "0x155b7a", "0x155b86"] + +[[global]] +address = "0x155b86" +name = "battle_effect_durations_ms" +category = "data-table" +type = "int[6]" +value_domain = "333..934 for authored sprite effects; movie duration is queried into the same work cell" +usage = "Per-effect playback duration. BTANINIT authors it for sixteen sprite-sheet effects; for movie effects BTL overwrites the slot with query-surface-stop-time-ms. BTL uses start delay plus this duration when extending the animation timeline." +source = "investigation" +confidence = "high" +depends_on = ["0x153ffe", "0x155b5c"] + +[[global]] +address = "0x155b8c" +name = "battle_effect_combatant_anchor_flags" +category = "data-table" +type = "int[6]" +value_domain = "{0,1}" +usage = "When nonzero, BTL anchors effect slots 0 and 1 to the actor-side combatant and slots 2 through 5 to the target-side combatant. Zero uses the battlefield center." +source = "investigation" +confidence = "high" +depends_on = ["0x155b92", "0x155b98"] + +[[global]] +address = "0x155b92" +name = "battle_effect_offset_x_pixels" +category = "data-table" +type = "int[6]" +value_domain = "0, 35, or 54 pixels" +usage = "Horizontal offset added after BTL centers the effect surface on its selected combatant or the battlefield." +source = "investigation" +confidence = "high" +depends_on = ["0x155b68", "0x155b8c"] + +[[global]] +address = "0x155b98" +name = "battle_effect_offset_y_pixels" +category = "data-table" +type = "int[6]" +value_domain = "0, 77, 80, or 100 pixels" +usage = "Vertical offset added after BTL centers the effect surface on its selected combatant or the battlefield." +source = "investigation" +confidence = "high" +depends_on = ["0x155b6e", "0x155b8c"] + +[[global]] +address = "0x155b9e" +name = "battle_effect_sound_asset_ids" +category = "data-table" +type = "int[6]" +value_domain = "SYS4INI WAV resource id; zero means no sound" +usage = "Optional WAV loaded by BTL for a materialized effect slot. BTL schedules playback at the animation row's effect start delay plus battle_effect_sound_delays_ms. All 199 populated ids resolve through SYS4INI." +source = "investigation" +confidence = "high" +depends_on = ["0x153ffe", "0x155ba4"] + +[[global]] +address = "0x155ba4" +name = "battle_effect_sound_delays_ms" +category = "data-table" +type = "int[6]" +value_domain = "0..600 milliseconds" +usage = "Effect-local sound delay added to battle_animation_effect_start_delays_ms before BTL schedules the loaded WAV channel." +source = "investigation" +confidence = "high" +depends_on = ["0x153ffe", "0x155b9e"] + +[[global]] +address = "0x155baa" +name = "battle_effect_hit_pulse_offsets_ms" +category = "data-table" +type = "int[6][3]" +columns = { "0" = "pulse_1", "1" = "pulse_2", "2" = "pulse_3" } +value_domain = "positive effect-local milliseconds; zero means no pulse" +usage = "Up to three BTL hit-feedback pulse offsets per materialized effect slot. BTL adds the animation row's effect start delay, fires each pulse within a 100 ms window, and tracks it once per slot/column. Shipped BTANINIT populates only pulse_1, at 100 ms, on 26 effects." +source = "investigation" +confidence = "high" +depends_on = ["0x153ffe"] + [[global]] address = "0x4379" name = "card_definition_result_messages"