diff --git a/docs/global-reference.md b/docs/global-reference.md index 29a4c53..727b88b 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5053 globals (370 curated, 4683 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5054 globals (384 curated, 4670 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -36,6 +36,7 @@ | `0x2052e` | scjump_progress_b | med | inference | Second SCJUMP switch input (1223 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | | `0x20530` | current_spirit | high | investigation | Current 精気 (spirit/essence) resource. TRAIN rejects a selected action when adding its negative TRINIT spirit delta would fall below zero, deducts the cost on execution, and passes the updated value to DRAWCHP. Field and scene reward paths update the same resource and clamp it to maximum_spirit. | | `0x20534` | maximum_spirit | high | investigation | Maximum 精気 capacity paired with current_spirit. TRAIN clamps the post-cost current value against it, DRAWCHP renders the current/maximum gauge, and field/scene reward paths raise or restore the same capacity. | +| `0x4dfbb` | stage_card_spendable_point_bonus | high | investigation | FIELD clears this on stage entry and adds CDINIT2 type-3 card bonuses. STAGECLEAR adds the accumulated amount to its computed stage award before increasing shared_spendable_points. | | `0x4dfbc` | scjump_progress_a | med | inference | Dominant SCJUMP switch input (1609 comparison reads) — a per-chapter story-progress counter/position the progression machine branches on. INFERENCE from SCJUMP usage; confirm exact meaning via a listing/playthrough. | | `0x665d6` | modal_message_line_count | high | investigation | Number of populated strings in modal_message_lines. Dozens of menu/gameplay producers append at lines[count] and increment this value; MES renders the resulting non-selecting modal and clears the count, while SBUNKI consumes the same buffer as selectable options. | | `0x665e2` | modal_annotation_count | high | investigation | Number of populated modal_annotation_texts entries. MES and SBUNKI iterate this many annotations and clear it on dismissal; no direct shipped producer was found. | @@ -252,8 +253,21 @@ | `0x12a8f8` | battle_routine_parameter_1 | high | investigation | RTINIT battle bank 12. RTN_B004 uses it to choose an entry from the prepared battle-action candidate table. | | `0x147db8` | battle_routine_required_story_flag_ids | high | investigation | RTINIT battle bank 18. BTRTN subtracts one and rejects the step when the referenced story flag is not set. | | `0x14cbd8` | battle_routine_forbidden_story_flag_ids | high | investigation | RTINIT battle bank 19. BTRTN subtracts one and rejects the step when the referenced story flag is set. | +| `0x1519f9` | card_definition_type_ids | high | investigation | CDINIT2 behavior class by card id. FIELD applies the common populated effect arrays, uses type 6 for the random valid-tile warp path, and uses type 1 to dispatch the card's event after showing its result. | | `0x151a5d` | card_definition_required_story_flag_ids | high | investigation | CDINIT2 card eligibility rows. FIELD checks only columns 0 and 1 before admitting a card to weighted selection. CDINIT2 nevertheless authors column 2 for eighteen cards; those third requirements are engine-dead in the shipped FIELD loop and are retained separately by the extractor. Columns: 0=required_flag_1, 1=required_flag_2, 2=engine_dead_required_flag_3. | | `0x151b89` | card_definition_forbidden_story_flag_ids | high | investigation | CDINIT2 card exclusion rows. FIELD checks columns 0 and 1 and rejects a card when either populated flag is set; the shipped definitions populate only column 0. Columns: 0=forbidden_flag_1, 1=forbidden_flag_2, 2=reserved_forbidden_flag_3. | +| `0x151cb5` | card_definition_awarded_item_ids | high | investigation | Optional item granted by FIELD through ADDITEM after card selection. All 24 populated ids resolve through ITINIT. | +| `0x151d19` | card_definition_event_story_flag_ids | high | investigation | Event dispatched for type-1 cards after FIELD displays the card result. All 40 populated ids resolve through SCINIT. | +| `0x151d7d` | card_definition_stage_clear_point_bonuses | high | investigation | Type-3 card reward accumulated into stage_card_spendable_point_bonus. STAGECLEAR adds it to the normal performance-adjusted award before updating shared_spendable_points; shipped small/medium/large values are 5, 10, and 15. | +| `0x151de1` | card_definition_minimum_resource_recovery | high | investigation | Lower bounds for type-4 HP/SP/FS card recovery. FIELD starts each resource delta at this value and, when the paired upper bound is larger, adds a random value below their difference. Columns: 0=hp, 1=sp, 2=fs. | +| `0x151f0d` | card_definition_maximum_resource_recovery | high | investigation | Upper bounds paired with card_definition_minimum_resource_recovery. Equal bounds produce the fixed minimum; larger bounds produce a uniformly selected value from minimum through upper-bound-minus-one. Columns: 0=hp, 1=sp, 2=fs. | +| `0x152039` | card_definition_minimum_spirit_recovery | high | investigation | Lower bound for type-4 familiar-spirit recovery. FIELD selects the value with the same minimum/exclusive-upper algorithm as HP/SP/FS and clamps current_spirit to maximum_spirit. | +| `0x15209d` | card_definition_maximum_spirit_recovery | high | investigation | Upper bound paired with card_definition_minimum_spirit_recovery. Equal bounds produce the fixed minimum; larger bounds exclude this upper value. | +| `0x152101` | card_definition_minimum_resource_damage | high | investigation | Lower bounds for type-5 trap damage. FIELD subtracts these randomized HP/SP/FS amounts from the active entity through the same resource-delta path used for recovery. Columns: 0=hp, 1=sp, 2=fs. | +| `0x15222d` | card_definition_maximum_resource_damage | high | investigation | Upper bounds paired with card_definition_minimum_resource_damage. Equal bounds produce fixed damage; larger bounds exclude this upper value. Columns: 0=hp, 1=sp, 2=fs. | +| `0x152359` | card_definition_condition_ids | high | investigation | Condition applied by type-5 trap cards through ADDILL. The three populated rows resolve to paralysis, poison, and water_flow. | +| `0x1523bd` | card_definition_condition_levels | high | investigation | Condition level paired with card_definition_condition_ids and passed to ADDILL. All three shipped condition traps use level 1. | +| `0x152421` | card_definition_visual_asset_ids | high | investigation | Card-result visual selected by FIELD before applying and presenting the effect. All 81 authored rows resolve to MVS*.AGF assets, with related card families sharing artwork. | | `0x152486` | card_generation_weight_schedules | high | investigation | CDINIT's selector-specific candidate weights, parallel to card_generation_card_ids. FIELD computes base_weight + floor(current_stage_turn / growth_interval_turns) * growth_weight, with a zero interval selecting the fixed base path. The shipped lists populate 11..75 one-based slots. Columns: 0=base_weight, 1=growth_interval_turns, 2=growth_weight. | | `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. | @@ -378,15 +392,6 @@ | `0xed865` | — | low | auto-shape | array | | `0xeebed` | — | low | auto-shape | array | | `0xefb8d` | — | low | auto-shape | array | -| `0x1519f9` | — | low | auto-shape | array | -| `0x151cb5` | — | low | auto-shape | array | -| `0x151d19` | — | low | auto-shape | array | -| `0x151d7d` | — | low | auto-shape | array | -| `0x152039` | — | low | auto-shape | array | -| `0x15209d` | — | low | auto-shape | array | -| `0x152359` | — | low | auto-shape | array | -| `0x1523bd` | — | low | auto-shape | array | -| `0x152421` | — | low | auto-shape | array | | `0x15287b` | — | low | auto-shape | array | | `0x15287d` | — | low | auto-shape | array | | `0x15287f` | — | low | auto-shape | array | @@ -5082,10 +5087,6 @@ | `0xea1b5` | — | med | auto-shape | record-table[stride 7] | | `0xee035` | — | med | auto-shape | record-table[stride 3] | | `0xeefd5` | — | med | auto-shape | record-table[stride 3] | -| `0x151de1` | — | med | auto-shape | record-table[stride 3] | -| `0x151f0d` | — | med | auto-shape | record-table[stride 3] | -| `0x152101` | — | med | auto-shape | record-table[stride 3] | -| `0x15222d` | — | med | auto-shape | record-table[stride 3] | | `0x15261f` | — | med | auto-shape | record-table[stride 300] | | `0x152878` | — | low | auto-shape | index/counter? | | `0x15288b` | — | low | auto-shape | index/counter? | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index 9d4a583..1cc6e79 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -685,9 +685,10 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape SPINIT closes HMODE's eight-by-fifteen thumbnail-page-to-scene registry, with all 118 scripts and eight INIT2 sheets resolved. TRINIT closes TRAIN's 21 training/sexual-magic actions: six text slots, all eligibility/cost/stat/alignment/progress/award families, and 75 event cells joined - through SCINIT and GAMESTART's restored story flags. With RTINIT's used-provider surface closed and - ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES classified, audit CDINIT's card-generation - registry next; + 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; 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 822e0e5..0db29e6 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3108,5 +3108,42 @@ Regressions protect every selector and entry count, exact instruction accounting effective/ignored flag joins, STINIT object joins, raw parallel addresses, the 100/50 scan-clear asymmetry, and the original missing-list warning. -**Next:** audit CDINIT2's full 81-card effect schema. CDINIT has now named its selection-facing -surface, but FIELD's post-selection effect arrays remain the clearest unresolved INIT structure. +## Data-semantics sidebar: CDINIT2 card definitions and effects (2026-07-23) + +CDINIT2 is FIELD's complete 81-card definition registry inside one reserved 100-row block. The +dedicated extractor classifies all 558 instructions exactly: 162 name/result-message strings, +395 numeric writes, and `exit`. Its numeric block is contiguous from `0x1519f9` through `0x152484` +and preserves the raw arrays for card type, required and forbidden story flags, awarded item, +event story flag, stage-clear point bonus, ranged HP/SP/FS recovery and damage, spirit recovery, +condition id/level, and visual asset. + +The shipped type distribution is forty story events, twenty-four item awards, six resource +recoveries, three stage-clear point bonuses, seven traps, and one random warp. FIELD filters each +candidate through two required and three forbidden story-flag cells before selection. CDINIT2 also +authors eighteen values in a third required-flag column, but FIELD's eligibility loop never reads +that column; these remain explicit as engine-dead authoring data rather than being promoted into the +effective gate. All 81 visual ids resolve through SYS4INI to `MVS*.AGF` assets, all 24 item rewards +join through ITINIT, all 40 event ids join through SCINIT, and all three condition-bearing traps join +through ILINIT. + +After selection, FIELD renders the card's visual and result message, then applies the type-specific +effect. Resource and spirit ranges use an authored minimum plus `random(maximum - minimum)` when the +maximum exceeds the minimum, so the stored maximum is exclusive for a non-fixed range and equality +means a fixed value. Resource recovery and trap damage address HP, SP, and FS independently; spirit +recovery clamps the current value into its valid range; condition traps call ADDILL with the authored +condition level; and the random-warp card chooses a valid destination tile. Story-event cards dispatch +their joined SCINIT event after the result message. Item cards call ADDITEM. Point cards accumulate +their award in `stage_card_spendable_point_bonus`; STAGECLEAR adds that accumulator to the stage award +before updating `shared_spendable_points`. + +Representative definitions protect each path: card 1 restores HP/SP/FS by 2--4, card 4 restores +spirit by 2--4, card 7 awards five stage-clear points, card 10 dispatches SC0490, card 50 performs +ranged resource damage, card 52 applies level-one paralysis, card 55 inflicts fixed ten SP damage, +card 57 is the random warp, card 58 awards a bronze coin, and card 81 renders `MVS107.AGF`. +Regressions protect every string and numeric write, the 81-of-100 geometry, type distribution, +effective/ignored gate split, all four external joins, representative effect projections, and the +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. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 62a1a4d..35e7b5c 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -47,8 +47,9 @@ Large, table-like scripts — prime candidates for data extraction: - `EBINIT` (338 KB) — enemy battle data - `MPINIT` (330 KB) — maps - `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), - `BTANINIT` (105 KB — battle animations) + `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) - Smaller: `AFINIT`, `ALINIT`, `CIINIT`, `CNINIT`, `CTINIT`, `CVINIT`, `ILINIT`, `LAINIT`, `MAINIT`, `OBINIT` (46 object definitions), `SPINIT`, `TRINIT`, `VIINIT` @@ -149,6 +150,17 @@ replays event ids below each restored execution count as completed story flags. requirements, eight skill awards, three item awards, and 75 event cells resolve through ITINIT/SKINIT/SCINIT. +CDINIT defines nine weighted card-generation lists with 383 total candidates. FIELD selects the +list through STINIT type-28 card objects, filters CDINIT2 story gates, applies the current-stage-turn +weight curve, and performs cumulative random selection across a 100-slot work buffer. The native +script clears only the first 50 slots even though its largest authored list contains 75. + +CDINIT2 defines 81 cards inside a reserved 100-row registry. Its names and result messages pair with +six consumer-proven effect types: story event, item award, deferred stage-clear point bonus, ranged +resource recovery, trap, and random warp. All item, event, condition, and visual references resolve +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. + 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 3d1dc33..89a6ca9 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, `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. 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, 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, `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 | — | | `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, @@ -126,6 +126,15 @@ cumulative weighted random selection across its 100-slot scan. The schema separa eighteen CDINIT2 third-column required flags that FIELD never reads, plus the shipped 50-slot clear prefix even though selector 160 authors 75 slots. +Name-mode CDINIT2 emits all 81 card definitions within the reserved 100-row registry. It preserves +the two text cells and every raw numeric array while projecting FIELD's six card types: story event, +item award, stage-clear point bonus, resource recovery, trap, and random warp. Required/forbidden +story gates distinguish FIELD's two effective required columns from the authored but unread third +column. Item ids join to ITINIT, event ids to SCINIT, condition ids to ILINIT, and all visual ids to +SYS4INI `MVS*.AGF` resources. Ranged effects expose the authored minimum and maximum-exclusive bound; +an equal pair is a fixed value. The consumer contract also records FIELD's rendering/effect order +and STAGECLEAR's deferred spendable-point award. + 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 226666d..173c43c 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], **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], **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. @@ -65,8 +65,10 @@ - [x] **2.3h — Extract `LAINIT`'s terrain definitions.** Name specialization classifies all 98 instructions into twenty shipped terrain ids inside a reserved thirty-row registry: seventeen names, five effect descriptions, four parallel terrain arrays, a ten-column signed combat-stat matrix, five SKINIT traversal/reveal requirements, and ten SYS4INI-resolved shared texture fallbacks. CALCBTPARAM, MVSEEK, FIELD, INFOAF, and DRAWMAP prove the complete consumer contract. - [x] **2.3i — Extract `SPINIT`'s H-scene gallery registry.** Numeric specialization classifies all 118 writes as eight fifteen-slot HMODE pages with two implicit trailing zero cells. Every row joins to INIT2's `SO027A` through `SO027H` thumbnail sheet and every populated cell resolves to an `SP*.BIN` resource. HMODE proves the opcode-0x19d availability-filter and indirect-call contract. - [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.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, and TRINIT training actions are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, audit CDINIT's card-generation registry and its STINIT/FIELD 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, 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. ## 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 8ed24ee..45c3c78 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -38,6 +38,10 @@ generation lists populate a parallel card-id array and three-column weight schedule. FIELD filters the joined CDINIT2 definitions by story flags and uses the current stage turn to grow each candidate's weighted-selection share. +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. + 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. @@ -717,9 +721,39 @@ CARD_GENERATION_WEIGHT_STRIDE = 3 CARD_GENERATION_CARD_ID_BASE = 0x1525B2 CARD_GENERATION_SCAN_CAPACITY = 100 CARD_GENERATION_CLEAR_COUNT = 50 +CARD_DEFINITION_NAME_BASE = 0x4315 +CARD_DEFINITION_RESULT_BASE = 0x4379 CARD_REQUIRED_STORY_FLAG_BASE = 0x151A5D CARD_FORBIDDEN_STORY_FLAG_BASE = 0x151B89 CARD_STORY_FLAG_STRIDE = 3 +CARD_DEFINITION_COUNT = 81 +CARD_DEFINITION_CAPACITY = 100 +CARD_DEFINITION_ARRAYS = { + "type_id": (0x1519F9, 1), + "required_story_flag_ids": (0x151A5D, 3), + "forbidden_story_flag_ids": (0x151B89, 3), + "awarded_item_id": (0x151CB5, 1), + "event_story_flag_id": (0x151D19, 1), + "stage_clear_point_bonus": (0x151D7D, 1), + "minimum_resource_recovery": (0x151DE1, 3), + "maximum_resource_recovery": (0x151F0D, 3), + "minimum_spirit_recovery": (0x152039, 1), + "maximum_spirit_recovery": (0x15209D, 1), + "minimum_resource_damage": (0x152101, 3), + "maximum_resource_damage": (0x15222D, 3), + "condition_id": (0x152359, 1), + "condition_level": (0x1523BD, 1), + "visual_asset_id": (0x152421, 1), +} +CARD_TYPE_NAMES = { + 1: "story_event", + 2: "item_award", + 3: "stage_clear_point_bonus", + 4: "resource_recovery", + 5: "trap", + 6: "random_warp", +} +CARD_RESOURCE_COLUMNS = ("hp", "sp", "fs") def resolve(name: str) -> Path: @@ -4238,6 +4272,398 @@ def extract_training_actions(scr): } +def extract_card_definitions(scr): + """Extract CDINIT2's complete 81-card definition and effect registry.""" + string_cells = {} + numeric_cells = { + field_name: {} + for field_name in CARD_DEFINITION_ARRAYS + } + classified_offsets = set() + string_write_count = 0 + static_write_count = 0 + + for ins in scr.instructions: + if ( + ins.opcode == SET_STRING + and len(ins.args) >= 2 + and ins.args[0][0] == T_GLOBAL_STRING + ): + destination = ins.args[0][1] + if ( + CARD_DEFINITION_NAME_BASE + < destination + <= CARD_DEFINITION_NAME_BASE + CARD_DEFINITION_COUNT + ): + card_id = destination - CARD_DEFINITION_NAME_BASE + field_name = "name" + elif ( + CARD_DEFINITION_RESULT_BASE + < destination + <= CARD_DEFINITION_RESULT_BASE + CARD_DEFINITION_COUNT + ): + card_id = destination - CARD_DEFINITION_RESULT_BASE + field_name = "result_message" + else: + raise ValueError( + f"{scr.path.name}: unclassified string destination " + f"0x{destination:x} at 0x{ins.offset:x}" + ) + _store_unique( + string_cells, + (card_id, field_name), + scr.strings[ins.args[1][1]][0], + card_id, + ) + classified_offsets.add(ins.offset) + string_write_count += 1 + continue + + write = _static_global_write(ins) + if write is not None: + static_write_count += 1 + destination, value = write + if not isinstance(value, int): + raise ValueError( + f"{scr.path.name}: non-static card value " + f"at 0x{ins.offset:x}" + ) + for field_name, (base, stride) in ( + CARD_DEFINITION_ARRAYS.items() + ): + index = destination - base + if not ( + 0 <= index < CARD_DEFINITION_CAPACITY * stride + ): + continue + card_id, column = divmod(index, stride) + if not 1 <= card_id <= CARD_DEFINITION_COUNT: + raise ValueError( + f"{scr.path.name}: write to reserved card row " + f"{card_id} at 0x{ins.offset:x}" + ) + _store_unique( + numeric_cells[field_name], + (card_id, column), + value, + card_id, + ) + classified_offsets.add(ins.offset) + break + else: + raise ValueError( + f"{scr.path.name}: unclassified numeric destination " + f"0x{destination:x} at 0x{ins.offset:x}" + ) + continue + + if sys4load.display_label(ins.opcode) == "exit": + classified_offsets.add(ins.offset) + + 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) + ) + + item_records, _ = extract_name(sys4load.load(resolve("ITINIT"))) + item_names = { + record["id"]: record["name"] for record in item_records + } + dispatch_records, _ = extract_dispatch( + sys4load.load(resolve("SCINIT")) + ) + event_dispatch = { + record["id"]: record for record in dispatch_records + } + condition_records, _ = extract_condition_definitions( + sys4load.load(resolve("ILINIT")) + ) + condition_names = { + record["id"]: record["condition"] + for record in condition_records + } + asset_names = callscript_names() + + def values(field_name: str, card_id: int) -> list[int]: + _, stride = CARD_DEFINITION_ARRAYS[field_name] + return [ + numeric_cells[field_name].get((card_id, column), 0) + for column in range(stride) + ] + + def scalar(field_name: str, card_id: int) -> int: + return values(field_name, card_id)[0] + + records = [] + for card_id in range(1, CARD_DEFINITION_COUNT + 1): + raw_fields = {} + raw_record_fields = {} + for field_name, (base, stride) in ( + CARD_DEFINITION_ARRAYS.items() + ): + for column in range(stride): + cell = (card_id, column) + if cell not in numeric_cells[field_name]: + continue + value = numeric_cells[field_name][cell] + if stride == 1: + raw_fields[f"0x{base:x}"] = value + else: + raw_record_fields[ + f"0x{base:x}/{stride}/{column}" + ] = value + + required_flags = values( + "required_story_flag_ids", card_id + ) + forbidden_flags = values( + "forbidden_story_flag_ids", card_id + ) + eligibility = { + "required_story_flag_ids": [ + value for value in required_flags[:2] if value + ], + "forbidden_story_flag_ids": [ + value for value in forbidden_flags[:2] if value + ], + } + if required_flags[2]: + eligibility["ignored_required_story_flag_ids"] = [ + required_flags[2] + ] + + effects = {} + awarded_item_id = scalar("awarded_item_id", card_id) + if awarded_item_id: + effects.update({ + "awarded_item_id": awarded_item_id, + "awarded_item_name": item_names.get( + awarded_item_id, "" + ), + }) + event_id = scalar("event_story_flag_id", card_id) + if event_id: + dispatch = event_dispatch.get(event_id, {}) + effects.update({ + "event_story_flag_id": event_id, + "event_script_resource_id": dispatch.get( + "script_resource_id", 0 + ), + "event_script_name": dispatch.get("script_name", ""), + }) + point_bonus = scalar("stage_clear_point_bonus", card_id) + if point_bonus: + effects["stage_clear_spendable_point_bonus"] = point_bonus + + for prefix, minimum_field, maximum_field in ( + ( + "resource_recovery", + "minimum_resource_recovery", + "maximum_resource_recovery", + ), + ( + "resource_damage", + "minimum_resource_damage", + "maximum_resource_damage", + ), + ): + minimums = values(minimum_field, card_id) + maximums = values(maximum_field, card_id) + ranges = { + resource: { + "minimum": minimums[column], + "maximum_exclusive": maximums[column], + } + for column, resource in enumerate( + CARD_RESOURCE_COLUMNS + ) + if minimums[column] or maximums[column] + } + if ranges: + effects[prefix] = ranges + + minimum_spirit = scalar( + "minimum_spirit_recovery", card_id + ) + maximum_spirit = scalar( + "maximum_spirit_recovery", card_id + ) + if minimum_spirit or maximum_spirit: + effects["spirit_recovery"] = { + "minimum": minimum_spirit, + "maximum_exclusive": maximum_spirit, + } + condition_id = scalar("condition_id", card_id) + if condition_id: + effects.update({ + "condition_id": condition_id, + "condition": condition_names.get(condition_id, ""), + "condition_level": scalar( + "condition_level", card_id + ), + }) + type_id = scalar("type_id", card_id) + if type_id == 6: + effects["random_warp"] = True + + visual_asset_id = scalar("visual_asset_id", card_id) + records.append({ + "id": card_id, + "name": string_cells[(card_id, "name")], + "result_message": string_cells[ + (card_id, "result_message") + ], + "type_id": type_id, + "type": CARD_TYPE_NAMES.get(type_id, ""), + "eligibility": eligibility, + "effects": effects, + "visual_asset_id": visual_asset_id, + "visual_asset_name": asset_names.get(visual_asset_id, ""), + "fields": raw_fields, + "record_fields": raw_record_fields, + "string_fields": { + f"0x{CARD_DEFINITION_NAME_BASE:x}": ( + string_cells[(card_id, "name")] + ), + f"0x{CARD_DEFINITION_RESULT_BASE:x}": ( + string_cells[(card_id, "result_message")] + ), + }, + }) + + array_layouts = { + f"0x{base:x}": {"stride": stride} + for base, stride in CARD_DEFINITION_ARRAYS.values() + if stride > 1 + } + schema_field_semantics = { + f"0x{CARD_DEFINITION_NAME_BASE:x}": "card_definition_names", + f"0x{CARD_DEFINITION_RESULT_BASE:x}": ( + "card_definition_result_messages" + ), + } + semantic_names = { + "type_id": "card_definition_type_ids", + "required_story_flag_ids": ( + "card_definition_required_story_flag_ids" + ), + "forbidden_story_flag_ids": ( + "card_definition_forbidden_story_flag_ids" + ), + "awarded_item_id": "card_definition_awarded_item_ids", + "event_story_flag_id": ( + "card_definition_event_story_flag_ids" + ), + "stage_clear_point_bonus": ( + "card_definition_stage_clear_point_bonuses" + ), + "minimum_resource_recovery": ( + "card_definition_minimum_resource_recovery" + ), + "maximum_resource_recovery": ( + "card_definition_maximum_resource_recovery" + ), + "minimum_spirit_recovery": ( + "card_definition_minimum_spirit_recovery" + ), + "maximum_spirit_recovery": ( + "card_definition_maximum_spirit_recovery" + ), + "minimum_resource_damage": ( + "card_definition_minimum_resource_damage" + ), + "maximum_resource_damage": ( + "card_definition_maximum_resource_damage" + ), + "condition_id": "card_definition_condition_ids", + "condition_level": "card_definition_condition_levels", + "visual_asset_id": "card_definition_visual_asset_ids", + } + for field_name, (base, stride) in ( + CARD_DEFINITION_ARRAYS.items() + ): + key = f"0x{base:x}" + if stride == 1: + schema_field_semantics[key] = semantic_names[field_name] + continue + columns = ( + ("required_flag_1", "required_flag_2", + "engine_dead_required_flag_3") + if field_name == "required_story_flag_ids" + else + ("forbidden_flag_1", "forbidden_flag_2", + "reserved_forbidden_flag_3") + if field_name == "forbidden_story_flag_ids" + else CARD_RESOURCE_COLUMNS + ) + for column, column_name in enumerate(columns): + schema_field_semantics[ + f"{key}/{stride}/{column}" + ] = f"{semantic_names[field_name]}.{column_name}" + + return records, { + "schema": "card-definitions", + "reserved_record_count": CARD_DEFINITION_CAPACITY, + "authored_record_count": CARD_DEFINITION_COUNT, + "numeric_block_start": "0x1519f9", + "numeric_block_end_exclusive": "0x152485", + "array_layouts": array_layouts, + "schema_field_semantics": schema_field_semantics, + "semantic_array_names": { + f"0x{base:x}": semantic_names[field_name] + for field_name, (base, _) in ( + CARD_DEFINITION_ARRAYS.items() + ) + }, + "string_write_count": string_write_count, + "static_write_count": static_write_count, + "classified_instruction_count": len(classified_offsets), + "type_counts": dict(sorted(collections.Counter( + record["type"] for record in records + ).items())), + "awarded_item_join_count": sum( + bool(record["effects"].get("awarded_item_name")) + for record in records + ), + "event_dispatch_join_count": sum( + bool(record["effects"].get("event_script_name")) + for record in records + ), + "condition_join_count": sum( + bool(record["effects"].get("condition")) + for record in records + ), + "visual_asset_join_count": sum( + bool(record["visual_asset_name"]) for record in records + ), + "ignored_required_story_flag_count": sum( + bool(record["eligibility"].get( + "ignored_required_story_flag_ids" + )) + for record in records + ), + "consumer_contract": { + "FIELD.BIN": ( + "filters required/forbidden story flags, applies randomized " + "HP/SP/FS recovery or damage, spirit recovery, item awards, " + "stage-clear spendable-point bonuses, conditions, event " + "dispatch, or random warp by card type, draws the visual " + "asset, and presents the name/result text" + ), + "STAGECLEAR.BIN": ( + "adds FIELD's accumulated card point bonus to the ordinary " + "stage-clear award before increasing shared_spendable_points" + ), + }, + } + + def extract_card_generation_lists(scr): """Extract CDINIT's selector-dispatched weighted card candidate lists.""" instructions = scr.instructions @@ -5274,6 +5700,11 @@ def write_data_index(data_dir: Path) -> None: "gates, and links the seven used selectors back to STINIT type-28 stage objects.", "FIELD's turn-scaled weight formula and 100-slot selection scan remain explicit.", "", + "CDINIT2's dedicated card-definition schema exposes all 81 cards in the reserved", + "100-row registry. Names, result messages, effective and engine-dead story gates,", + "six effect types, and raw array coordinates remain together; item, event, condition,", + "and visual ids join through ITINIT, SCINIT, ILINIT, and SYS4INI resources.", + "", "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", @@ -5379,6 +5810,8 @@ def main() -> int: extractor = extract_terrain_definitions elif mode == "name" and name == "TRINIT": extractor = extract_training_actions + elif mode == "name" and name == "CDINIT2": + extractor = extract_card_definitions elif mode == "numeric" and name == "CDINIT": extractor = extract_card_generation_lists elif mode == "numeric" and name == "SPINIT": diff --git a/tools/init_table_profile.py b/tools/init_table_profile.py index ad999a7..72f82a8 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -469,6 +469,31 @@ def profile_card_generation_lists(data: dict) -> dict: } +def profile_card_definitions(data: dict) -> dict: + """Summarize CDINIT2's card categories, effects, and joins.""" + if data.get("schema") != "card-definitions": + return {} + records = data.get("records", []) + return { + "card_count": len(records), + "reserved_record_count": data.get("reserved_record_count", 0), + "type_counts": data.get("type_counts", {}), + "awarded_item_join_count": data.get( + "awarded_item_join_count", 0 + ), + "event_dispatch_join_count": data.get( + "event_dispatch_join_count", 0 + ), + "condition_join_count": data.get("condition_join_count", 0), + "visual_asset_join_count": data.get( + "visual_asset_join_count", 0 + ), + "ignored_required_story_flag_count": data.get( + "ignored_required_story_flag_count", 0 + ), + } + + def profile_messages(data: dict) -> dict: """Summarize the joined player-facing message evidence.""" records = data["records"] @@ -596,7 +621,22 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str: f"- records: {data['record_count']}", f"- populated fields: {len(rows)}", ] - if card_profile := profile_card_generation_lists(data): + if definition_profile := profile_card_definitions(data): + lines.extend([ + f"- card definitions: {definition_profile['card_count']}/" + f"{definition_profile['reserved_record_count']} rows", + f"- types: {definition_profile['type_counts']}", + f"- joined effects: " + f"{definition_profile['awarded_item_join_count']} items, " + f"{definition_profile['event_dispatch_join_count']} events, " + f"{definition_profile['condition_join_count']} conditions", + f"- visual assets resolved: " + f"{definition_profile['visual_asset_join_count']}/" + f"{definition_profile['card_count']}", + f"- engine-dead third required flags: " + f"{definition_profile['ignored_required_story_flag_count']}", + ]) + elif card_profile := profile_card_generation_lists(data): lines.extend([ f"- card-generation lists: {card_profile['list_count']}", f"- weighted entries: {card_profile['entry_count']} across " @@ -817,6 +857,7 @@ def main() -> int: "h_scene_gallery_profile": profile_h_scene_gallery(data), "training_action_profile": profile_training_actions(data), "card_generation_profile": profile_card_generation_lists(data), + "card_definition_profile": profile_card_definitions(data), "columns": sorted(rows, key=lambda row: ( int(row["base"], 16), row["stride"] or 0, row["column"] or 0 )), diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 4edb9d4..d106d0b 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1737,6 +1737,79 @@ def test_card_generation_lists() -> None: ) +def test_card_definitions() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["CDINIT2.BIN"]) + records, meta = extract_init.extract_card_definitions(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 81 + and meta["reserved_record_count"] == 100 + and meta["numeric_block_start"] == "0x1519f9" + and meta["numeric_block_end_exclusive"] == "0x152485", + "CDINIT2 exposes 81 cards in one contiguous reserved 100-row block", + ) + check( + meta["string_write_count"] == 162 + and meta["static_write_count"] == 395 + and meta["classified_instruction_count"] == 558, + "CDINIT2 classifies every name, result, numeric, and exit instruction", + ) + check( + meta["type_counts"] + == { + "item_award": 24, + "random_warp": 1, + "resource_recovery": 6, + "stage_clear_point_bonus": 3, + "story_event": 40, + "trap": 7, + }, + "CDINIT2 partitions every card into its FIELD behavior type", + ) + check( + by_id[1]["effects"]["resource_recovery"]["hp"] + == {"minimum": 2, "maximum_exclusive": 5} + and by_id[4]["effects"]["spirit_recovery"] + == {"minimum": 2, "maximum_exclusive": 5} + and by_id[7]["effects"]["stage_clear_spendable_point_bonus"] + == 5, + "CDINIT2 exposes recovery ranges and stage-clear point bonuses", + ) + check( + by_id[10]["effects"]["event_story_flag_id"] == 490 + and by_id[10]["effects"]["event_script_name"] == "SC0490.BIN" + and by_id[58]["effects"]["awarded_item_name"] + == "ブロンズコイン", + "CDINIT2 resolves event and item rewards through SCINIT and ITINIT", + ) + check( + by_id[52]["effects"]["condition"] == "paralysis" + and by_id[52]["effects"]["condition_level"] == 1 + and by_id[55]["effects"]["resource_damage"]["sp"] + == {"minimum": 10, "maximum_exclusive": 10} + and by_id[57]["effects"]["random_warp"], + "CDINIT2 decodes trap conditions, fixed damage, and random warp", + ) + check( + by_id[18]["eligibility"]["required_story_flag_ids"] + == [901, 863] + and by_id[18]["eligibility"][ + "ignored_required_story_flag_ids" + ] == [51] + and meta["ignored_required_story_flag_count"] == 18, + "CDINIT2 separates FIELD's live gates from the engine-dead third flag", + ) + check( + meta["awarded_item_join_count"] == 24 + and meta["event_dispatch_join_count"] == 40 + and meta["condition_join_count"] == 3 + and meta["visual_asset_join_count"] == 81 + and by_id[81]["visual_asset_name"] == "MVS107.AGF", + "CDINIT2 resolves every item, event, condition, and visual join", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1955,6 +2028,7 @@ if __name__ == "__main__": test_h_scene_gallery() test_training_actions() test_card_generation_lists() + test_card_definitions() test_map_terrain_atlas() test_condition_definitions() test_field_semantics() diff --git a/tools/test_globals.py b/tools/test_globals.py index 6debcb5..7a8baf3 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -94,6 +94,15 @@ def test_load_and_lint(): == "engine_dead_required_flag_3" and entries[0x204f4]["name"] == "current_stage_turn", "CDINIT/FIELD card-generation state is curated") + check(entries[0x1519f9]["name"] == "card_definition_type_ids" + and entries[0x151de1]["columns"]["2"] == "fs" + and entries[0x15222d]["name"] + == "card_definition_maximum_resource_damage" + and entries[0x152359]["name"] == "card_definition_condition_ids" + and entries[0x152421]["name"] == "card_definition_visual_asset_ids" + and entries[0x4dfbb]["name"] + == "stage_card_spendable_point_bonus", + "CDINIT2/FIELD card-effect 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 088cbad..104b58b 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -196,7 +196,7 @@ def main() -> int: {"stage_ids": [32, 33, 34]}, {"stage_ids": [35, 36]}, ], - "records": [], + "records": [{}, {}, {}], } map_summary = profile.profile_map_atlas(map_fixture) assert map_summary["row_stride"] == 53 @@ -320,6 +320,36 @@ def main() -> int: assert "- card-generation lists: 2" in rendered_cards assert "- runtime scan/clear prefix: 100/50 slots" in rendered_cards + definition_fixture = { + "table": "CARDDEFS", + "mode": "name", + "schema": "card-definitions", + "record_count": 3, + "reserved_record_count": 100, + "type_counts": { + "item_award": 1, + "resource_recovery": 1, + "trap": 1, + }, + "awarded_item_join_count": 1, + "event_dispatch_join_count": 0, + "condition_join_count": 1, + "visual_asset_join_count": 3, + "ignored_required_story_flag_count": 1, + "records": [{}, {}, {}], + } + definition_summary = profile.profile_card_definitions( + definition_fixture + ) + assert definition_summary["card_count"] == 3 + assert definition_summary["reserved_record_count"] == 100 + assert definition_summary["visual_asset_join_count"] == 3 + rendered_definitions = profile.render_markdown( + definition_fixture, [], 40 + ) + assert "- card definitions: 3/100 rows" in rendered_definitions + assert "- engine-dead third required flags: 1" in rendered_definitions + training_fixture = { "table": "TRAINING", "mode": "name", diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 1093f9d..0ac3e30 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -4056,3 +4056,161 @@ usage = "Card ids parallel to card_generation_weight_schedules. FIELD scans slot source = "investigation" confidence = "high" depends_on = ["0x1519f8", "0x152485", "0x152486"] + +[[global]] +address = "0x4dfbb" +name = "stage_card_spendable_point_bonus" +category = "counter" +type = "int" +value_domain = "nonnegative accumulated point bonus for the active stage" +usage = "FIELD clears this on stage entry and adds CDINIT2 type-3 card bonuses. STAGECLEAR adds the accumulated amount to its computed stage award before increasing shared_spendable_points." +source = "investigation" +confidence = "high" +depends_on = ["0x151d7d", "0x671b"] + +[[global]] +address = "0x1519f9" +name = "card_definition_type_ids" +category = "data-table" +type = "int[100]" +value_domain = "1=story event, 2=item award, 3=stage-clear point bonus, 4=recovery, 5=trap, 6=random warp" +usage = "CDINIT2 behavior class by card id. FIELD applies the common populated effect arrays, uses type 6 for the random valid-tile warp path, and uses type 1 to dispatch the card's event after showing its result." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8"] + +[[global]] +address = "0x151cb5" +name = "card_definition_awarded_item_ids" +category = "data-table" +type = "int[100]" +value_domain = "ITINIT item id; zero means no item award" +usage = "Optional item granted by FIELD through ADDITEM after card selection. All 24 populated ids resolve through ITINIT." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8", "0x8c877"] + +[[global]] +address = "0x151d19" +name = "card_definition_event_story_flag_ids" +category = "data-table" +type = "int[100]" +value_domain = "story flag / SCINIT decision id; zero means no event" +usage = "Event dispatched for type-1 cards after FIELD displays the card result. All 40 populated ids resolve through SCINIT." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8", "0x62ccf"] + +[[global]] +address = "0x151d7d" +name = "card_definition_stage_clear_point_bonuses" +category = "data-table" +type = "int[100]" +value_domain = "nonnegative shared-spendable-point bonus" +usage = "Type-3 card reward accumulated into stage_card_spendable_point_bonus. STAGECLEAR adds it to the normal performance-adjusted award before updating shared_spendable_points; shipped small/medium/large values are 5, 10, and 15." +source = "investigation" +confidence = "high" +depends_on = ["0x4dfbb", "0x671b"] + +[[global]] +address = "0x151de1" +name = "card_definition_minimum_resource_recovery" +category = "data-table" +type = "int[100][3]" +columns = { "0" = "hp", "1" = "sp", "2" = "fs" } +value_domain = "nonnegative minimum HP/SP/FS recovery" +usage = "Lower bounds for type-4 HP/SP/FS card recovery. FIELD starts each resource delta at this value and, when the paired upper bound is larger, adds a random value below their difference." +source = "investigation" +confidence = "high" +depends_on = ["0x151f0d"] + +[[global]] +address = "0x151f0d" +name = "card_definition_maximum_resource_recovery" +category = "data-table" +type = "int[100][3]" +columns = { "0" = "hp", "1" = "sp", "2" = "fs" } +value_domain = "exclusive upper HP/SP/FS recovery bound when greater than the minimum" +usage = "Upper bounds paired with card_definition_minimum_resource_recovery. Equal bounds produce the fixed minimum; larger bounds produce a uniformly selected value from minimum through upper-bound-minus-one." +source = "investigation" +confidence = "high" +depends_on = ["0x151de1"] + +[[global]] +address = "0x152039" +name = "card_definition_minimum_spirit_recovery" +category = "data-table" +type = "int[100]" +value_domain = "nonnegative minimum familiar spirit recovery" +usage = "Lower bound for type-4 familiar-spirit recovery. FIELD selects the value with the same minimum/exclusive-upper algorithm as HP/SP/FS and clamps current_spirit to maximum_spirit." +source = "investigation" +confidence = "high" +depends_on = ["0x15209d", "0x20530", "0x20534"] + +[[global]] +address = "0x15209d" +name = "card_definition_maximum_spirit_recovery" +category = "data-table" +type = "int[100]" +value_domain = "exclusive upper spirit-recovery bound when greater than the minimum" +usage = "Upper bound paired with card_definition_minimum_spirit_recovery. Equal bounds produce the fixed minimum; larger bounds exclude this upper value." +source = "investigation" +confidence = "high" +depends_on = ["0x152039"] + +[[global]] +address = "0x152101" +name = "card_definition_minimum_resource_damage" +category = "data-table" +type = "int[100][3]" +columns = { "0" = "hp", "1" = "sp", "2" = "fs" } +value_domain = "nonnegative minimum HP/SP/FS loss" +usage = "Lower bounds for type-5 trap damage. FIELD subtracts these randomized HP/SP/FS amounts from the active entity through the same resource-delta path used for recovery." +source = "investigation" +confidence = "high" +depends_on = ["0x15222d"] + +[[global]] +address = "0x15222d" +name = "card_definition_maximum_resource_damage" +category = "data-table" +type = "int[100][3]" +columns = { "0" = "hp", "1" = "sp", "2" = "fs" } +value_domain = "exclusive upper HP/SP/FS damage bound when greater than the minimum" +usage = "Upper bounds paired with card_definition_minimum_resource_damage. Equal bounds produce fixed damage; larger bounds exclude this upper value." +source = "investigation" +confidence = "high" +depends_on = ["0x152101"] + +[[global]] +address = "0x152359" +name = "card_definition_condition_ids" +category = "data-table" +type = "int[100]" +value_domain = "ILINIT condition id; zero means no condition" +usage = "Condition applied by type-5 trap cards through ADDILL. The three populated rows resolve to paralysis, poison, and water_flow." +source = "investigation" +confidence = "high" +depends_on = ["0x1523bd"] + +[[global]] +address = "0x1523bd" +name = "card_definition_condition_levels" +category = "data-table" +type = "int[100]" +value_domain = "ILINIT condition level; zero means no condition" +usage = "Condition level paired with card_definition_condition_ids and passed to ADDILL. All three shipped condition traps use level 1." +source = "investigation" +confidence = "high" +depends_on = ["0x152359"] + +[[global]] +address = "0x152421" +name = "card_definition_visual_asset_ids" +category = "data-table" +type = "int[100]" +value_domain = "SYS4INI AGF resource id" +usage = "Card-result visual selected by FIELD before applying and presenting the effect. All 81 authored rows resolve to MVS*.AGF assets, with related card families sharing artwork." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8"]