From 85b0e3f66f720781964366fc00894fc5799363c2 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 22:41:32 -0400 Subject: [PATCH] Decode CDINIT card generation lists --- docs/global-reference.md | 21 +- docs/phase-a-slice-plan.md | 36 ++- docs/tools-reference.md | 16 +- tools/extract_init.py | 437 +++++++++++++++++++++++++++++++ tools/init_table_profile.py | 68 ++++- tools/test_extract_init.py | 82 ++++++ tools/test_globals.py | 8 + tools/test_init_table_profile.py | 53 ++++ vm-map/globals.toml | 114 +++++++- 9 files changed, 813 insertions(+), 22 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 5798b1e..29a4c53 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5052 globals (361 curated, 4691 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5053 globals (370 curated, 4683 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -32,6 +32,7 @@ | `0x6725` | familiar_training_progress_fraction | high | investigation | Fractional accumulator paired with familiar_training_progress. TRAIN adds the action's hundredths delta, advances the integer value by the quotient and probabilistic remainder, and preserves the remaining fraction. | | `0x6726` | training_action_total_execution_count | high | investigation | TRAIN increments this once after every successful action, independently of the selected action's own execution count. DRAWCHP displays it with the familiar's other training statistics and SAVE persists it. | | `0x6727` | training_action_execution_counts | high | investigation | Per-action completed execution counts. TRAIN uses the prior count as the column in training_action_event_story_flag_ids, increments the selected cell, and treats a zero next event as the cap; GAMESTART restores each count and replays its completed event flags. | +| `0x204f4` | current_stage_turn | high | investigation | FIELD initializes this to 1, increments it after end-of-turn processing, compares it with STINIT's stage turn limit and timed object schedules, and displays it through DRAWCHP. Card generation divides it by each CDINIT growth interval to increase that card's weighted-selection share. | | `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. | @@ -212,6 +213,7 @@ | `0xe7325` | stage_object_tile_x | high | investigation | X coordinate for each current-stage object slot. DRAWOBJ converts it to centered map-space pixels; FIELD and CALCOCC use it with stage_object_tile_y for object interaction and occupancy. | | `0xe7357` | stage_object_tile_y | high | investigation | Y coordinate for each current-stage object slot. DRAWOBJ converts it to centered map-space pixels; FIELD and CALCOCC use it with stage_object_tile_x for object interaction and occupancy. | | `0xe7389` | stage_object_type_id | high | investigation | Object-definition id for each current-stage object slot. SETOBJ decides whether the slot exists, while DRAWOBJ and FIELD use the id to select shared object graphics, dimensions, animation, collision, and behavior metadata. | +| `0xe73bb` | stage_object_primary_payload | high | investigation | STINIT's primary per-object payload array. FIELD interprets it by the parallel object type: initial faction for Magic Pillars, teleport destination X, item id, card-generation list id for type 28, non-triggering faction for hazards/barriers, or initial object state for the consumer-proven stateful types. | | `0xe741f` | stage_object_reinforcement_interval_turns | high | investigation | Per-object reinforcement schedule. FIELD compares the current turn and the object's runtime spawn count against this interval to calculate due weighted enemy spawns; object type 27 uses the same value as its one-shot trigger turn. | | `0xe7451` | stage_object_reinforcement_spawn_limit | high | investigation | Maximum number of scheduled units materialized through a stage object. FIELD stops the object's reinforcement path once its runtime spawn counter reaches this value; object type 27 stores one for its one-shot special spawn. | | `0xe7483` | stage_object_difficulty_mask | high | investigation | Per-object difficulty inclusion mask. SETOBJ uses check-bit with difficulty_index and rejects the object slot when the selected difficulty bit is absent. | @@ -250,6 +252,10 @@ | `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. | +| `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. | +| `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. | | `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. | | `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. | @@ -307,8 +313,6 @@ | `0x3306` | — | low | auto-shape | array | | `0x36ee` | — | low | auto-shape | array | | `0x3ad6` | — | low | auto-shape | array | -| `0x4315` | — | low | auto-shape | array | -| `0x4379` | — | low | auto-shape | array | | `0x6804` | — | low | auto-shape | array | | `0x6868` | — | low | auto-shape | array | | `0x6994` | — | low | auto-shape | array | @@ -364,7 +368,6 @@ | `0xccc2f` | — | low | auto-shape | array | | `0xe7305` | — | low | auto-shape | array | | `0xe730e` | — | low | auto-shape | array | -| `0xe73bb` | — | low | auto-shape | array | | `0xe73ed` | — | low | auto-shape | array | | `0xe770d` | — | low | auto-shape | array | | `0xe7e8d` | — | low | auto-shape | array | @@ -384,7 +387,6 @@ | `0x152359` | — | low | auto-shape | array | | `0x1523bd` | — | low | auto-shape | array | | `0x152421` | — | low | auto-shape | array | -| `0x1525b2` | — | low | auto-shape | array | | `0x15287b` | — | low | auto-shape | array | | `0x15287d` | — | low | auto-shape | array | | `0x15287f` | — | low | auto-shape | array | @@ -436,6 +438,8 @@ | `0xaac76` | current_condition_id | high | investigation | Shared condition selector consumed by CALCILL, ADDILL, ADDILLSUB, DISARM, FIELD, and related condition handlers. It indexes the 30-column runtime condition rows and ILINIT definition arrays. | | `0xeff75` | current_routine_set_id | high | investigation | Shared RTINIT row selector. MVRTN loads the current entity's difficulty-selected movement routine set; BTRTN loads its battle routine set. Both then iterate routine_step_index across the selected twenty-slot row. | | `0xeff76` | routine_step_index | high | investigation | Shared RTINIT step selector. MVRTN and BTRTN iterate it from zero through nineteen and use it as the column index in every routine bank and matching per-entity runtime row. | +| `0x1519f8` | current_card_id | high | investigation | FIELD's selected card-definition row. Card generation copies a surviving CDINIT card id here, uses it to test CDINIT2 story-flag gates, retains it through weighted selection, and indexes the chosen card's effects, graphics, name, and result text. | +| `0x152485` | current_card_generation_list_id | high | investigation | Selector consumed by CDINIT. FIELD loads it from the current STINIT type-28 card object's payload before rebuilding the parallel card-id and weight-schedule buffers; selectors 55 and 94 are authored but have no shipped STINIT object reference. | | `0x152616` | current_entity_index | med | investigation | Primary current-entity row index (RECOVER-confirmed; purity 0.51, 363 row-index uses). | | `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. | @@ -501,6 +505,8 @@ | `0x27ba` | stage_victory_condition_2 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders nonempty values after stage_victory_condition_1, and FIELD copies the slot into the current mission-condition display. | | `0x27bb` | stage_defeat_condition_1 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders this line first in the defeat-condition section, and FIELD copies it into the current mission-condition display. | | `0x27bc` | stage_defeat_condition_2 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders nonempty values after stage_defeat_condition_1, and FIELD copies the slot into the current mission-condition display. | +| `0x4315` | card_definition_names | high | investigation | Card names populated by CDINIT2. FIELD indexes this table with current_card_id when presenting the card selected from CDINIT's weighted generation list. | +| `0x4379` | card_definition_result_messages | high | investigation | Short player-facing result text paired with card_definition_names. FIELD displays the selected card's row after applying its CDINIT2 effect. | | `0x43dd` | name_entry_character_palette | high | investigation | INPUTNAME's five 70-cell character pages: row 0 hiragana, row 1 katakana, row 2 full-width Latin letters, row 3 Arabic/Kanji/Roman/circled numerals, and row 4 symbols. Cursor slots 70..74 select the page; selecting a populated cell copies it into the seven-character name buffer. | | `0x45b9` | magic_action_names | high | investigation | MAINIT's one-based action-name array: eleven shipped records occupy ids 1..11 in a reserved 30-cell span. MAGIC, STUDY, and EVOLVE render the selected entry. | | `0x45d7` | character_profile_names | high | investigation | CIINIT writes the character-information screen's 24 display names. INFOCH lists them by profile id, masks unrevealed rows, and uses the same id to select the backing unit metadata and CIMES biography. | @@ -3718,7 +3724,6 @@ | `0x3301` | — | low | inference | Config/settings global (INITCONFIG.BIN writer, scene-reach 0) — NOT a scene story flag; miner over-tagged it. Branch-read in 6 scripts; compared against [1, 2]. | | `0x3303` | — | low | inference | Config/settings global (CONFIG.BIN/INITCONFIG.BIN writer, scene-reach 0) — NOT a scene story flag; miner over-tagged it. Branch-read in 8 scripts; compared against [1]. | | `0x3304` | — | low | inference | Config/settings global (CONFIG.BIN/INITCONFIG.BIN writer, scene-reach 0) — NOT a scene story flag; miner over-tagged it. Branch-read in 13 scripts; compared against [1]. | -| `0x204f4` | — | low | inference | Map/field display state (FIELD.BIN writer; used in DRAWCHP draw call u00422460 and `div 0x204f4/3`). A numeric display parameter, NOT a story flag. Branch-read in 2 scenes. | | `0x6249e` | — | low | inference | Graphics-subsystem state in the 0x624xx gfx-object range (set via `mov 0x6249e,` alongside `mov 0x62450,`). Likely a draw/slot parameter, NOT a story flag. Branch-read in 78 scenes. | | `0xab8e7` | class_change_selected_level | high | investigation | CALCCC clears this accumulator before invoking the class-change rule scripts. Each eligible CCINIT rule replaces it only when its threshold is higher than the current selection; nonzero then signals CALCCC and ADDEXP to apply and report the chosen promotion. | | `0xab8e8` | class_change_deployment_cost_delta | high | investigation | Shared class-change output initialized by CALCCC and incremented by the selected CCINIT rule. CALCCC adds it to unit_deployment_cost_adjustments and ADDEXP reports the signed cost change. | @@ -5077,14 +5082,10 @@ | `0xea1b5` | — | med | auto-shape | record-table[stride 7] | | `0xee035` | — | med | auto-shape | record-table[stride 3] | | `0xeefd5` | — | med | auto-shape | record-table[stride 3] | -| `0x1519f8` | — | med | auto-shape | current-entity-index? | -| `0x151a5d` | — | med | auto-shape | record-table[stride 3] | -| `0x151b89` | — | 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] | -| `0x152486` | — | 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/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 5ef2f1d..822e0e5 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3076,5 +3076,37 @@ Regressions protect the complete write accounting, all text/gate/effect populati signed thresholds and stat rows, every ITINIT/SKINIT join, all 75 SCINIT joins, and the observed one/three/four/six-execution limit distribution. -**Next:** audit CDINIT's card-generation registry and its STINIT/FIELD consumers; it is the clearest -remaining INIT table already joined to a decoded stage-object payload. +## Data-semantics sidebar: CDINIT card-generation lists (2026-07-23) + +CDINIT is not a 1,149-row numeric table. It is a nine-way dispatch on +`current_card_generation_list_id` (`0x152485`) that repopulates two parallel runtime buffers: +100 card-id slots at `0x1525b2` and 100 three-column weight rows at `0x152486`. The dedicated +extractor classifies all 1,565 instructions exactly: a three-instruction clear prelude, nine +selector tests and branches, 383 four-write candidate entries, nine terminal jumps, the missing-list +comment/marker, and `exit`. The selector ids are 1, 11, 31, 41, 55, 61, 71, 94, and 160, containing +11, 36, 38, 51, 26, 52, 64, 30, and 75 entries respectively. + +FIELD supplies the selector from the active STINIT type-28 card object's primary payload. Seven +selectors are referenced by 246 such objects across 50 stage definitions; selectors 55 and 94 are +authored but unreferenced by the shipped STINIT corpus. All 383 candidate references resolve to the +81 CDINIT2 card definitions, including their display names and result text. + +The three per-entry values are `base_weight`, `growth_interval_turns`, and `growth_weight`. For every +card that passes its CDINIT2 story-flag gates, FIELD computes +`base_weight + floor(current_stage_turn / growth_interval_turns) * growth_weight`; a zero interval +would select the fixed base path, although all shipped entries use a nonzero interval. It then chooses +one card by cumulative weighted random selection. FIELD tests only required-flag columns zero and one: +CDINIT2 authors a third required flag on eighteen cards, producing 54 occurrences across the generation +lists, but that column is engine-dead in the shipped eligibility loop and remains separately labeled. + +Two runtime geometry facts are intentionally not normalized away. FIELD scans 100 candidate slots, +while CDINIT's prelude clears only the first 50 card ids and first 50 weight rows. Selector 160 then +authors 75 slots. The extracted schema records the scan capacity, clear prefix, and complete authored +extent so a reimplementation can reproduce or deliberately resolve that native stale-tail risk. + +Regressions protect every selector and entry count, exact instruction accounting, CDINIT2 names and +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. diff --git a/docs/tools-reference.md b/docs/tools-reference.md index e720292..3d1dc33 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, `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. 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, 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 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, `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 | — | | `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, @@ -116,6 +116,16 @@ ids join through ITINIT/SKINIT; all 75 event cells join through SCINIT, and the execution path plus GAMESTART's restored-story-flag contract. Its profile summarizes text/gate/effect populations, execution-limit distribution, and event dispatch coverage. +Numeric-mode CDINIT bypasses the generic fragmented-address view and emits nine selector-dispatched +card-generation lists with 383 candidate entries. Each one-based slot joins its CDINIT2 card id, +display name, result text, effective required/forbidden story flags, and three FIELD-proven weight +parameters. STINIT type-28 objects join back to seven live selectors; authored selectors 55 and 94 +remain explicit as unreferenced data. FIELD's effective weight is +`base_weight + floor(current_stage_turn / growth_interval_turns) * growth_weight`, followed by +cumulative weighted random selection across its 100-slot scan. The schema separately preserves the +eighteen CDINIT2 third-column required flags that FIELD never reads, plus the shipped 50-slot clear +prefix even though selector 160 authors 75 slots. + 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/tools/extract_init.py b/tools/extract_init.py index ebca411..8ed24ee 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -33,6 +33,11 @@ TRINIT is a special name-mode registry: 21 training/sexual-magic actions each own six display-text slots and a contiguous block of eligibility, cost, effect, award, and ten-slot event arrays consumed by TRAIN and restored by GAMESTART. +CDINIT is a special numeric-mode registry: nine selector-dispatched card +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. + 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. @@ -706,6 +711,16 @@ TRAINING_ACTION_ARRAYS = { "event_story_flag_ids": (0x156015, 10), } +CARD_GENERATION_SELECTOR = 0x152485 +CARD_GENERATION_WEIGHT_BASE = 0x152486 +CARD_GENERATION_WEIGHT_STRIDE = 3 +CARD_GENERATION_CARD_ID_BASE = 0x1525B2 +CARD_GENERATION_SCAN_CAPACITY = 100 +CARD_GENERATION_CLEAR_COUNT = 50 +CARD_REQUIRED_STORY_FLAG_BASE = 0x151A5D +CARD_FORBIDDEN_STORY_FLAG_BASE = 0x151B89 +CARD_STORY_FLAG_STRIDE = 3 + def resolve(name: str) -> Path: for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"): @@ -4223,6 +4238,417 @@ def extract_training_actions(scr): } +def extract_card_generation_lists(scr): + """Extract CDINIT's selector-dispatched weighted card candidate lists.""" + instructions = scr.instructions + classified_offsets = set() + cursor = 0 + + expected_prelude = ( + ( + "mul", + [ + (T_LOCAL_INT, 0), + (T_IMM, CARD_GENERATION_CLEAR_COUNT), + (T_IMM, CARD_GENERATION_WEIGHT_STRIDE), + ], + ), + ( + "copy-to-global", + [ + (T_GLOBAL_INT, CARD_GENERATION_WEIGHT_BASE), + (T_LOCAL_INT, 0), + ], + ), + ( + "copy-to-global", + [ + (T_GLOBAL_INT, CARD_GENERATION_CARD_ID_BASE), + (T_IMM, CARD_GENERATION_CLEAR_COUNT), + ], + ), + ) + for expected_label, expected_args in expected_prelude: + if cursor >= len(instructions): + raise ValueError(f"{scr.path.name}: truncated CDINIT prelude") + ins = instructions[cursor] + label = sys4load.display_label(ins.opcode) + if label != expected_label or ins.args != expected_args: + raise ValueError( + f"{scr.path.name}: unexpected prelude instruction " + f"at 0x{ins.offset:x}: {label} {ins.args}" + ) + classified_offsets.add(ins.offset) + cursor += 1 + + list_cells: dict[int, dict[int, dict[str, int]]] = {} + branch_offsets: dict[int, int] = {} + while cursor < len(instructions): + ins = instructions[cursor] + if sys4load.display_label(ins.opcode) != "eq": + break + if ( + len(ins.args) != 3 + or ins.args[0] != (T_LOCAL_INT, 0) + or ins.args[1] != (T_GLOBAL_INT, CARD_GENERATION_SELECTOR) + or ins.args[2][0] != T_IMM + ): + raise ValueError( + f"{scr.path.name}: malformed selector test at 0x{ins.offset:x}" + ) + selector = ins.args[2][1] + if selector in list_cells: + raise ValueError( + f"{scr.path.name}: duplicate selector {selector}" + ) + list_cells[selector] = {} + branch_offsets[selector] = ins.offset + classified_offsets.add(ins.offset) + cursor += 1 + + if cursor >= len(instructions): + raise ValueError( + f"{scr.path.name}: selector {selector} lacks a branch" + ) + branch = instructions[cursor] + if ( + sys4load.display_label(branch.opcode) != "jcc" + or branch.args[:2] + != [(T_LOCAL_INT, 0), (T_IMM, 0xFFFFFFFF)] + ): + raise ValueError( + f"{scr.path.name}: malformed selector branch " + f"at 0x{branch.offset:x}" + ) + classified_offsets.add(branch.offset) + cursor += 1 + + while cursor < len(instructions): + write_ins = instructions[cursor] + if sys4load.display_label(write_ins.opcode) != "mov": + break + write = _static_global_write(write_ins) + if write is None or not isinstance(write[1], int): + raise ValueError( + f"{scr.path.name}: non-static list write " + f"at 0x{write_ins.offset:x}" + ) + destination, value = write + if ( + CARD_GENERATION_CARD_ID_BASE + < destination + < CARD_GENERATION_CARD_ID_BASE + + CARD_GENERATION_SCAN_CAPACITY + ): + slot = destination - CARD_GENERATION_CARD_ID_BASE + field_name = "card_id" + elif ( + CARD_GENERATION_WEIGHT_BASE + <= destination + < CARD_GENERATION_WEIGHT_BASE + + CARD_GENERATION_SCAN_CAPACITY + * CARD_GENERATION_WEIGHT_STRIDE + ): + index = destination - CARD_GENERATION_WEIGHT_BASE + slot, column = divmod( + index, CARD_GENERATION_WEIGHT_STRIDE + ) + field_name = ( + "base_weight", + "growth_interval_turns", + "growth_weight", + )[column] + else: + raise ValueError( + f"{scr.path.name}: unclassified list write " + f"0x{destination:x} at 0x{write_ins.offset:x}" + ) + if slot == 0: + raise ValueError( + f"{scr.path.name}: selector {selector} writes reserved " + f"slot zero at 0x{write_ins.offset:x}" + ) + entry = list_cells[selector].setdefault(slot, {}) + if field_name in entry: + raise ValueError( + f"{scr.path.name}: selector {selector} slot {slot} " + f"overwrites {field_name}" + ) + entry[field_name] = value + classified_offsets.add(write_ins.offset) + cursor += 1 + + if cursor >= len(instructions): + raise ValueError( + f"{scr.path.name}: selector {selector} lacks terminal jump" + ) + terminal = instructions[cursor] + if sys4load.display_label(terminal.opcode) != "jmp": + raise ValueError( + f"{scr.path.name}: selector {selector} lacks terminal jump " + f"at 0x{terminal.offset:x}" + ) + classified_offsets.add(terminal.offset) + cursor += 1 + + if not list_cells: + raise ValueError(f"{scr.path.name}: no card-generation selectors") + + fallback_comment = "" + while cursor < len(instructions): + ins = instructions[cursor] + label = sys4load.display_label(ins.opcode) + if label == "comment": + fallback_comment = scr.strings[ins.args[0][1]][0] + elif label not in ("instruction-marker-noop", "exit"): + raise ValueError( + f"{scr.path.name}: unclassified trailing instruction " + f"{label} at 0x{ins.offset:x}" + ) + classified_offsets.add(ins.offset) + cursor += 1 + + unclassified = [ + f"0x{ins.offset:x}" + for ins in instructions + if ins.offset not in classified_offsets + ] + if unclassified: + raise ValueError( + f"{scr.path.name}: unclassified instructions " + + ", ".join(unclassified) + ) + + required_fields = { + "card_id", + "base_weight", + "growth_interval_turns", + "growth_weight", + } + for selector, cells in list_cells.items(): + expected_slots = list(range(1, len(cells) + 1)) + if sorted(cells) != expected_slots: + raise ValueError( + f"{scr.path.name}: selector {selector} has non-contiguous " + f"slots {sorted(cells)}" + ) + for slot, entry in cells.items(): + if set(entry) != required_fields: + raise ValueError( + f"{scr.path.name}: selector {selector} slot {slot} " + f"has fields {sorted(entry)}, expected " + f"{sorted(required_fields)}" + ) + + card_scr = sys4load.load(resolve("CDINIT2")) + card_records, _ = extract_name(card_scr) + card_definitions = { + record["id"]: { + "name": record.get("name") or "", + "description": record.get("desc") or "", + "required_story_flag_ids": [], + "forbidden_story_flag_ids": [], + "ignored_required_story_flag_ids": [], + } + for record in card_records + } + flag_cells = { + "required_story_flag_ids": {}, + "forbidden_story_flag_ids": {}, + } + for ins in card_scr.instructions: + write = _static_global_write(ins) + if write is None or not isinstance(write[1], int): + continue + destination, value = write + for field_name, base in ( + ("required_story_flag_ids", CARD_REQUIRED_STORY_FLAG_BASE), + ("forbidden_story_flag_ids", CARD_FORBIDDEN_STORY_FLAG_BASE), + ): + index = destination - base + if not ( + 0 + <= index + < CARD_GENERATION_SCAN_CAPACITY * CARD_STORY_FLAG_STRIDE + ): + continue + card_id, column = divmod(index, CARD_STORY_FLAG_STRIDE) + flag_cells[field_name][(card_id, column)] = value + break + for card_id, definition in card_definitions.items(): + for field_name in flag_cells: + definition[field_name] = [ + flag_cells[field_name].get((card_id, column), 0) + for column in range(2) + if flag_cells[field_name].get((card_id, column), 0) + ] + ignored_required = flag_cells[ + "required_story_flag_ids" + ].get((card_id, 2), 0) + if ignored_required: + definition["ignored_required_story_flag_ids"] = [ + ignored_required + ] + + stages, _ = extract_mixed(sys4load.load(resolve("STINIT"))) + attach_stage_object_placements(stages) + references_by_selector: dict[int, dict[int, list[int]]] = ( + collections.defaultdict(lambda: collections.defaultdict(list)) + ) + for stage in stages: + for obj in stage.get("object_placements", []): + selector = obj.get("card_generation_list_id") + if selector is None: + continue + references_by_selector[selector][stage["id"]].append( + obj["slot"] + ) + + records = [] + all_card_ids = set() + resolved_card_reference_count = 0 + for selector in sorted(list_cells): + entries = [] + for slot, raw_entry in sorted(list_cells[selector].items()): + card_id = raw_entry["card_id"] + all_card_ids.add(card_id) + definition = card_definitions.get(card_id, {}) + if definition.get("name"): + resolved_card_reference_count += 1 + entries.append({ + "slot": slot, + "card_id": card_id, + "card_name": definition.get("name", ""), + "card_description": definition.get("description", ""), + "base_weight": raw_entry["base_weight"], + "growth_interval_turns": raw_entry[ + "growth_interval_turns" + ], + "growth_weight": raw_entry["growth_weight"], + "required_story_flag_ids": definition.get( + "required_story_flag_ids", [] + ), + "forbidden_story_flag_ids": definition.get( + "forbidden_story_flag_ids", [] + ), + "ignored_required_story_flag_ids": definition.get( + "ignored_required_story_flag_ids", [] + ), + "source_addresses": { + "card_id": ( + f"0x{CARD_GENERATION_CARD_ID_BASE + slot:x}" + ), + "base_weight": ( + f"0x{CARD_GENERATION_WEIGHT_BASE + slot * 3:x}" + ), + "growth_interval_turns": ( + f"0x{CARD_GENERATION_WEIGHT_BASE + slot * 3 + 1:x}" + ), + "growth_weight": ( + f"0x{CARD_GENERATION_WEIGHT_BASE + slot * 3 + 2:x}" + ), + }, + }) + stage_references = [ + { + "stage_id": stage_id, + "object_slots": sorted(object_slots), + } + for stage_id, object_slots + in sorted(references_by_selector.get(selector, {}).items()) + ] + records.append({ + "id": selector, + "name": f"card_generation_list_{selector}", + "branch_offset": f"0x{branch_offsets[selector]:x}", + "entry_count": len(entries), + "stage_object_references": stage_references, + "entries": entries, + "fields": {}, + }) + + used_selectors = sorted( + selector + for selector in list_cells + if selector in references_by_selector + ) + entry_count = sum(len(cells) for cells in list_cells.values()) + return records, { + "schema": "card-generation-lists", + "selector_global": f"0x{CARD_GENERATION_SELECTOR:x}", + "card_id_array_base": f"0x{CARD_GENERATION_CARD_ID_BASE:x}", + "weight_schedule_table_base": ( + f"0x{CARD_GENERATION_WEIGHT_BASE:x}" + ), + "weight_schedule_stride": CARD_GENERATION_WEIGHT_STRIDE, + "runtime_scan_capacity": CARD_GENERATION_SCAN_CAPACITY, + "cleared_entry_prefix": CARD_GENERATION_CLEAR_COUNT, + "selector_ids": sorted(list_cells), + "used_selector_ids": used_selectors, + "unreferenced_selector_ids": sorted( + set(list_cells) - set(used_selectors) + ), + "entry_count": entry_count, + "distinct_card_ids": sorted(all_card_ids), + "resolved_card_reference_count": resolved_card_reference_count, + "ignored_required_story_flag_definition_count": sum( + bool(definition["ignored_required_story_flag_ids"]) + for definition in card_definitions.values() + ), + "ignored_required_story_flag_entry_count": sum( + bool(entry["ignored_required_story_flag_ids"]) + for record in records + for entry in record["entries"] + ), + "stage_definition_reference_count": sum( + len(stage_map) + for stage_map in references_by_selector.values() + ), + "stage_object_reference_count": sum( + len(object_slots) + for stage_map in references_by_selector.values() + for object_slots in stage_map.values() + ), + "fallback_comment": fallback_comment, + "classified_instruction_count": len(classified_offsets), + "semantic_array_names": { + f"0x{CARD_GENERATION_SELECTOR:x}": ( + "current_card_generation_list_id" + ), + f"0x{CARD_GENERATION_CARD_ID_BASE:x}": ( + "card_generation_card_ids" + ), + f"0x{CARD_GENERATION_WEIGHT_BASE:x}": ( + "card_generation_weight_schedules" + ), + }, + "weight_formula": ( + "base_weight + floor(current_stage_turn / " + "growth_interval_turns) * growth_weight; when " + "growth_interval_turns is zero, use base_weight" + ), + "consumer_contract": { + "FIELD.BIN": ( + "load the STINIT type-28 object's card-generation list, " + "scan 100 candidate slots, discard empty card ids and " + "CDINIT2 definitions whose required/forbidden story flags " + "fail, compute the current-turn-adjusted weight, and select " + "one surviving card by cumulative weighted random choice" + ), + "CDINIT2.BIN": ( + "provides card names, result text, effects, graphics, and " + "the required/forbidden story-flag rows used by FIELD; " + "FIELD tests only columns zero and one, leaving the eighteen " + "authored required-flag values in column two engine-dead" + ), + "STINIT.BIN": ( + "type-28 stage objects supply the selector id consumed by " + "CDINIT" + ), + }, + } + + def _map_stage_definitions() -> list[dict]: """Read the STINIT2 records that own all four terrain-atlas bounds.""" stage_scr = sys4load.load(resolve("STINIT2")) @@ -4843,6 +5269,11 @@ def write_data_index(data_dir: Path) -> None: "Item and skill ids join to ITINIT/SKINIT; all 75 event slots join through", "SCINIT, and GAMESTART's restored-story-flag contract remains explicit.", "", + "CDINIT's dedicated card-generation schema exposes nine sparse selector lists,", + "joins their 383 weighted candidate slots to CDINIT2 card names and story-flag", + "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.", + "", "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", @@ -4909,6 +5340,10 @@ def main() -> int: # TRINIT's six-column sparse string matrix is not the generic # one-name-per-record layout expected by name-mode auto-detection. mode = "name" + elif name == "CDINIT": + # CDINIT's selector branches look like one fragmented numeric table + # to the generic parallel-array detector. + mode = "numeric" else: mode = detect_mode(scr) extractor = { @@ -4944,6 +5379,8 @@ def main() -> int: extractor = extract_terrain_definitions elif mode == "name" and name == "TRINIT": extractor = extract_training_actions + elif mode == "numeric" and name == "CDINIT": + extractor = extract_card_generation_lists 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 5b49f16..ad999a7 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -426,6 +426,49 @@ def profile_training_actions(data: dict) -> dict: } +def profile_card_generation_lists(data: dict) -> dict: + """Summarize CDINIT's weighted, selector-dispatched card lists.""" + if data.get("schema") != "card-generation-lists": + return {} + entries = [ + entry + for record in data.get("records", []) + for entry in record.get("entries", []) + ] + return { + "list_count": len(data.get("records", [])), + "entry_count": data.get("entry_count", 0), + "distinct_card_count": len(data.get("distinct_card_ids", [])), + "resolved_card_reference_count": data.get( + "resolved_card_reference_count", 0 + ), + "used_selector_count": len(data.get("used_selector_ids", [])), + "unreferenced_selector_ids": data.get( + "unreferenced_selector_ids", [] + ), + "stage_definition_reference_count": data.get( + "stage_definition_reference_count", 0 + ), + "stage_object_reference_count": data.get( + "stage_object_reference_count", 0 + ), + "story_flag_gated_entry_count": sum( + bool(entry.get("required_story_flag_ids")) + or bool(entry.get("forbidden_story_flag_ids")) + for entry in entries + ), + "ignored_required_story_flag_entry_count": data.get( + "ignored_required_story_flag_entry_count", 0 + ), + "runtime_scan_capacity": data.get("runtime_scan_capacity", 0), + "cleared_entry_prefix": data.get("cleared_entry_prefix", 0), + "list_entry_counts": { + str(record["id"]): record.get("entry_count", 0) + for record in data.get("records", []) + }, + } + + def profile_messages(data: dict) -> dict: """Summarize the joined player-facing message evidence.""" records = data["records"] @@ -553,7 +596,29 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str: f"- records: {data['record_count']}", f"- populated fields: {len(rows)}", ] - if training_profile := profile_training_actions(data): + if 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 " + f"{card_profile['distinct_card_count']} distinct cards " + f"({card_profile['resolved_card_reference_count']} CDINIT2 " + f"references resolved)", + f"- selector usage: {card_profile['used_selector_count']} used; " + f"unreferenced {card_profile['unreferenced_selector_ids']}", + f"- STINIT joins: " + f"{card_profile['stage_object_reference_count']} type-28 objects " + f"across {card_profile['stage_definition_reference_count']} " + f"stage definitions", + f"- story-flag-gated entries: " + f"{card_profile['story_flag_gated_entry_count']} effective; " + f"{card_profile['ignored_required_story_flag_entry_count']} " + f"carry an engine-dead third required flag", + f"- runtime scan/clear prefix: " + f"{card_profile['runtime_scan_capacity']}/" + f"{card_profile['cleared_entry_prefix']} slots", + f"- entries by selector: {card_profile['list_entry_counts']}", + ]) + elif training_profile := profile_training_actions(data): lines.extend([ f"- training actions: {training_profile['action_count']}", f"- display text lines: " @@ -751,6 +816,7 @@ def main() -> int: "terrain_definition_profile": profile_terrain_definitions(data), "h_scene_gallery_profile": profile_h_scene_gallery(data), "training_action_profile": profile_training_actions(data), + "card_generation_profile": profile_card_generation_lists(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 3a69928..4edb9d4 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1656,6 +1656,87 @@ def test_training_actions() -> None: ) +def test_card_generation_lists() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["CDINIT.BIN"]) + check( + extract_init.detect_mode(script) == "numeric", + "CDINIT remains compatible with numeric-mode auto-detection", + ) + records, meta = extract_init.extract_card_generation_lists(script) + by_id = {record["id"]: record for record in records} + check( + list(by_id) == [1, 11, 31, 41, 55, 61, 71, 94, 160] + and [record["entry_count"] for record in records] + == [11, 36, 38, 51, 26, 52, 64, 30, 75], + "CDINIT exposes all nine selector branches and their candidate counts", + ) + check( + meta["entry_count"] == 383 + and len(meta["distinct_card_ids"]) == 81 + and meta["resolved_card_reference_count"] == 383 + and meta["classified_instruction_count"] == 1565, + "CDINIT classifies every instruction and resolves every card reference", + ) + first = by_id[1]["entries"][0] + check( + first["slot"] == 1 + and first["card_id"] == 1 + and first["card_name"] == "癒しのカード・小" + and first["base_weight"] == 25 + and first["growth_interval_turns"] == 5 + and first["growth_weight"] == 1 + and first["source_addresses"] + == { + "card_id": "0x1525b3", + "base_weight": "0x152489", + "growth_interval_turns": "0x15248a", + "growth_weight": "0x15248b", + }, + "CDINIT retains the parallel card-id and weight-table provenance", + ) + gated = next( + entry + for entry in by_id[11]["entries"] + if entry["card_id"] == 14 + ) + check( + gated["card_name"] == "使い魔のカード" + and gated["required_story_flag_ids"] == [901] + and gated["forbidden_story_flag_ids"] == [861], + "CDINIT entries join CDINIT2 names and FIELD story-flag gates", + ) + ignored_gate = next( + entry + for entry in by_id[41]["entries"] + if entry["card_id"] == 18 + ) + check( + meta["ignored_required_story_flag_definition_count"] == 18 + and meta["ignored_required_story_flag_entry_count"] == 54 + and ignored_gate["required_story_flag_ids"] == [901, 863] + and ignored_gate["ignored_required_story_flag_ids"] == [51], + "CDINIT distinguishes FIELD's two live required flags from column three", + ) + check( + meta["used_selector_ids"] == [1, 11, 31, 41, 61, 71, 160] + and meta["unreferenced_selector_ids"] == [55, 94] + and meta["stage_definition_reference_count"] == 50 + and meta["stage_object_reference_count"] == 246 + and by_id[1]["stage_object_references"] + == [{"stage_id": 1, "object_slots": [8]}], + "CDINIT joins every used list back to STINIT type-28 stage objects", + ) + check( + meta["runtime_scan_capacity"] == 100 + and meta["cleared_entry_prefix"] == 50 + and by_id[160]["entry_count"] == 75 + and meta["fallback_comment"] + == "カード発生リストの設定が不足しています", + "CDINIT preserves its 100-slot scan, 50-slot clear, and fallback warning", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1873,6 +1954,7 @@ if __name__ == "__main__": test_terrain_definitions() test_h_scene_gallery() test_training_actions() + test_card_generation_lists() test_map_terrain_atlas() test_condition_definitions() test_field_semantics() diff --git a/tools/test_globals.py b/tools/test_globals.py index 4cee427..6debcb5 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -86,6 +86,14 @@ def test_load_and_lint(): and entries[0x6722]["name"] == "familiar_alignment" and entries[0x6727]["name"] == "training_action_execution_counts", "TRINIT/TRAIN action state is curated") + check(entries[0x152485]["name"] == "current_card_generation_list_id" + and entries[0x152486]["columns"]["2"] == "growth_weight" + and entries[0x1525b2]["name"] == "card_generation_card_ids" + and entries[0x1519f8]["name"] == "current_card_id" + and entries[0x151a5d]["columns"]["2"] + == "engine_dead_required_flag_3" + and entries[0x204f4]["name"] == "current_stage_turn", + "CDINIT/FIELD card-generation 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 fd88243..088cbad 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -267,6 +267,59 @@ def main() -> int: assert "- geometry: 8 pages × 15 slots" in rendered_h_gallery assert "- populated scenes: 118/120" in rendered_h_gallery + card_fixture = { + "table": "CARDS", + "mode": "numeric", + "schema": "card-generation-lists", + "record_count": 2, + "entry_count": 3, + "distinct_card_ids": [1, 14], + "resolved_card_reference_count": 3, + "used_selector_ids": [1], + "unreferenced_selector_ids": [55], + "stage_definition_reference_count": 1, + "stage_object_reference_count": 2, + "runtime_scan_capacity": 100, + "cleared_entry_prefix": 50, + "records": [ + { + "id": 1, + "entry_count": 2, + "entries": [ + { + "card_id": 1, + "required_story_flag_ids": [], + "forbidden_story_flag_ids": [], + }, + { + "card_id": 14, + "required_story_flag_ids": [901], + "forbidden_story_flag_ids": [861], + }, + ], + }, + { + "id": 55, + "entry_count": 1, + "entries": [ + { + "card_id": 1, + "required_story_flag_ids": [], + "forbidden_story_flag_ids": [], + }, + ], + }, + ], + } + card_summary = profile.profile_card_generation_lists(card_fixture) + assert card_summary["list_count"] == 2 + assert card_summary["entry_count"] == 3 + assert card_summary["story_flag_gated_entry_count"] == 1 + assert card_summary["list_entry_counts"] == {"1": 2, "55": 1} + rendered_cards = profile.render_markdown(card_fixture, [], 40) + assert "- card-generation lists: 2" in rendered_cards + assert "- runtime scan/clear prefix: 100/50 slots" in rendered_cards + training_fixture = { "table": "TRAINING", "mode": "name", diff --git a/vm-map/globals.toml b/vm-map/globals.toml index cbed073..1093f9d 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -2339,13 +2339,13 @@ depends_on = [] [[global]] address = "0x204f4" -name = "" -category = "unknown" +name = "current_stage_turn" +category = "counter" type = "int" -value_domain = "small int" -usage = "Map/field display state (FIELD.BIN writer; used in DRAWCHP draw call u00422460 and `div 0x204f4/3`). A numeric display parameter, NOT a story flag. Branch-read in 2 scenes." -source = "inference" -confidence = "low" +value_domain = "positive one-based turn number while a stage is active" +usage = "FIELD initializes this to 1, increments it after end-of-turn processing, compares it with STINIT's stage turn limit and timed object schedules, and displays it through DRAWCHP. Card generation divides it by each CDINIT growth interval to increase that card's weighted-selection share." +source = "investigation" +confidence = "high" depends_on = [] [[global]] @@ -3954,3 +3954,105 @@ usage = "Persistent per-action discovered/unlocked state maintained by TRAIN aft source = "investigation" confidence = "high" depends_on = ["0x53edd"] + +[[global]] +address = "0x4315" +name = "card_definition_names" +category = "string-table" +type = "string[100]" +value_domain = "CDINIT2 display name by one-based card id" +usage = "Card names populated by CDINIT2. FIELD indexes this table with current_card_id when presenting the card selected from CDINIT's weighted generation list." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8"] + +[[global]] +address = "0x4379" +name = "card_definition_result_messages" +category = "string-table" +type = "string[100]" +value_domain = "CDINIT2 result text by one-based card id" +usage = "Short player-facing result text paired with card_definition_names. FIELD displays the selected card's row after applying its CDINIT2 effect." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8"] + +[[global]] +address = "0x1519f8" +name = "current_card_id" +category = "index-pointer" +type = "int" +value_domain = "CDINIT2 card id 1..81" +usage = "FIELD's selected card-definition row. Card generation copies a surviving CDINIT card id here, uses it to test CDINIT2 story-flag gates, retains it through weighted selection, and indexes the chosen card's effects, graphics, name, and result text." +source = "investigation" +confidence = "high" +depends_on = ["0x4315", "0x4379", "0x1525b2"] + +[[global]] +address = "0x151a5d" +name = "card_definition_required_story_flag_ids" +category = "data-table" +type = "int[100][3]" +columns = { "0" = "required_flag_1", "1" = "required_flag_2", "2" = "engine_dead_required_flag_3" } +value_domain = "story flag id; zero means no requirement" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8", "0x6d3"] + +[[global]] +address = "0x151b89" +name = "card_definition_forbidden_story_flag_ids" +category = "data-table" +type = "int[100][3]" +columns = { "0" = "forbidden_flag_1", "1" = "forbidden_flag_2", "2" = "reserved_forbidden_flag_3" } +value_domain = "story flag id; zero means no exclusion" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8", "0x6d3"] + +[[global]] +address = "0x152485" +name = "current_card_generation_list_id" +category = "index-pointer" +type = "int" +value_domain = "one of {1, 11, 31, 41, 55, 61, 71, 94, 160}" +usage = "Selector consumed by CDINIT. FIELD loads it from the current STINIT type-28 card object's payload before rebuilding the parallel card-id and weight-schedule buffers; selectors 55 and 94 are authored but have no shipped STINIT object reference." +source = "investigation" +confidence = "high" +depends_on = ["0xe73bb"] + +[[global]] +address = "0xe73bb" +name = "stage_object_primary_payload" +category = "data-table" +type = "int[50]" +value_domain = "object-type-tagged payload" +usage = "STINIT's primary per-object payload array. FIELD interprets it by the parallel object type: initial faction for Magic Pillars, teleport destination X, item id, card-generation list id for type 28, non-triggering faction for hazards/barriers, or initial object state for the consumer-proven stateful types." +source = "investigation" +confidence = "high" +depends_on = [] + +[[global]] +address = "0x152486" +name = "card_generation_weight_schedules" +category = "data-table" +type = "int[100][3]" +columns = { "0" = "base_weight", "1" = "growth_interval_turns", "2" = "growth_weight" } +value_domain = "nonnegative weighted-selection parameters" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x152485", "0x1525b2", "0x204f4"] + +[[global]] +address = "0x1525b2" +name = "card_generation_card_ids" +category = "data-table" +type = "int[100]" +value_domain = "CDINIT2 card id; zero means empty candidate slot" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x1519f8", "0x152485", "0x152486"]