From 6e95ea29a9fbd40f4c95e843ce1ac11c14bc73b8 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 22:25:41 -0400 Subject: [PATCH] Decode TRINIT training actions --- docs/global-reference.md | 59 ++-- docs/name-resolution.md | 8 +- docs/phase-a-slice-plan.md | 38 ++- docs/script-inventory.md | 7 + docs/tools-reference.md | 9 +- docs/vm-mapping-plan.md | 5 +- tools/extract_init.py | 455 ++++++++++++++++++++++++++++++- tools/init_table_profile.py | 86 +++++- tools/test_extract_init.py | 85 ++++++ tools/test_globals.py | 8 + tools/test_init_table_profile.py | 54 ++++ vm-map/globals.toml | 348 +++++++++++++++++++++-- 12 files changed, 1097 insertions(+), 65 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 64c10bc..5798b1e 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5049 globals (335 curated, 4714 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5052 globals (361 curated, 4691 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -26,7 +26,15 @@ | `0x671f` | item_tuning_facility_progress | high | investigation | Progress toward the next equipment-tuning facility level. IMPROVE adds the selected item's tuning-level increases, applies row 0 of facility_level_progress_thresholds, and carries or clamps progress when the level rises. | | `0x6720` | alchemy_level_progress | high | investigation | Progress toward the next alchemy level. Each successful ALCHEMY synthesis increments it; reaching the current threshold raises alchemy_level and resets this counter. | | `0x6721` | magic_facility_progress | high | investigation | Progress toward the next magic/research facility level. USEMAGIC adds the selected action's progress award and applies row 2 of facility_level_progress_thresholds; MAGIC renders the same current/threshold pair. | +| `0x6722` | familiar_alignment | high | investigation | The familiar's alignment/personality axis. TRAIN decodes TRINIT minimum and maximum gates by subtracting 100 from the stored threshold, matching the locked hints' kind-versus-evil wording, and applies training_action_alignment_delta_hundredths through the paired fractional accumulator. DRAWCHP renders the signed value and EVOLVE/scene scripts use the same axis. | +| `0x6723` | familiar_alignment_fraction | high | investigation | Fractional accumulator paired with familiar_alignment. TRAIN adds the selected action's hundredths delta, applies the integral quotient and probabilistic remainder, and preserves the remaining fraction. | +| `0x6724` | familiar_training_progress | high | investigation | Cumulative familiar training/sexual-magic progress. TRAIN tests it against TRINIT's minimum/maximum progress gates, then applies training_action_training_progress_delta_hundredths through the paired fractional accumulator and clamps the result to 99. DRAWCHP and EVOLVE display or compare the same value. | +| `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. | | `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. | | `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. | @@ -43,6 +51,7 @@ |---|---|---|---|---| | `0x6d3` | story_event_flags | high | investigation | Shared one-based story/event flag bank. Progression, recruitment, item, and stage logic read or write individual cells; STINIT object prerequisite ids are decremented by one before SETOBJ tests this array. | | `0x2e49` | character_voice_suppressed | high | investigation | Base of the per-character voice enable/suppress settings. INITCONFIG zero-fills all 13 cells and registers each with the shared profile service; LOADCONFIG restores them. CONFIG indexes the table to preview a character voice and write 0/1. ROOM reads cell 0 before assigning its selected greeter's greeting/farewell voice ids, so the port's former scalar interpretation of zero-int-range (writing 13 into the base cell) suppressed those voices on every natural boot. This names the script-visible setting array without choosing a persistence backend for op 0x1a2/0x1a3. | +| `0x453b` | training_action_text | high | investigation | TRINIT's six-string row for each of 21 training/sexual-magic actions. TRAIN renders columns 0..2 for the action description and cost/reward summary, or columns 3..5 for the unmet-condition hint. Columns: 0=description_line_1, 1=description_line_2, 2=description_line_3, 3=locked_hint_line_1, 4=locked_hint_line_2, 5=locked_hint_line_3. | | `0x65ce` | skill_acquired_flags | high | investigation | Persistent acquired-skill flags. ADDSKILL sets the selected skill after resolving the unit's equipped-skill slots; FORT checks the flag before granting a skill; CHMENU combines it with skill_change_catalog_eligible to build the available skill-change catalog. | | `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. | | `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. | @@ -70,6 +79,7 @@ | `0x53643` | entity_movement_routine_progress | high | investigation | Per-runtime-entity movement-routine progress counters. FIELD clears all fifty rows, movement providers increment the current step, and MVRTN compares it with movement_routine_minimum_progress_counts before enabling a step. | | `0x53e13` | entity_carried_item_ids | high | investigation | Two carried/drop-item slots per runtime entity. SETEN initializes slot 0 from the unit's starting equipment item, FIELD fills an empty or matching slot when treasure is collected, and RTN_M009 requires an empty slot or an item-id match before approaching a chest/treasure object. Columns: 0=slot_0, 1=slot_1. | | `0x53e77` | entity_carried_item_counts | high | investigation | Quantities for the two per-entity carried/drop-item slots. SETEN seeds a starting item with quantity one; FIELD adds treasure quantities to the matching slot and caps the result at 999. Columns: 0=slot_0, 1=slot_1. | +| `0x53ede` | training_action_unlock_flags | high | investigation | Persistent per-action discovered/unlocked state maintained by TRAIN after evaluating eligibility. TRAIN uses it while choosing the locked-versus-known menu presentation, GAMESTART restores every cell, and SAVE persists the same block. | | `0x56223` | unit_story_speaker_seen_flags | high | investigation | Scene scripts set and persist a unit's cell when drawing that speaker's name. CONFIG reloads the twelve CVINIT-mapped unit cells and enables each named character-voice control only after its speaker has been encountered. | | `0x5660b` | skill_info_revealed_flags | high | investigation | Persistent skill-information visibility flags. ADDSKILL sets the selected skill, BTL marks every equipped skill when it is observed in combat, and INFOIT suppresses a skill's icon/handler-driven details until this flag is nonzero. This is broader than skill_acquired_flags. | | `0x56738` | entity_battle_routine_random_rolls | high | investigation | Per-runtime-entity battle-step random rolls. FIELD and BTL fill every active entity's twenty cells with random-modulo-100; BTRTN executes a step when the matching roll is below battle_routine_activation_percents. | @@ -242,6 +252,25 @@ | `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. | | `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. | +| `0x155bfb` | training_action_forbidden_story_flag_ids | high | investigation | Reserved three-slot exclusion table paired with training_action_required_story_flag_ids. TRAIN rejects an action when any populated flag equals one; shipped TRINIT leaves all 63 cells zero. Columns: 0=forbidden_story_flag_id_1, 1=forbidden_story_flag_id_2, 2=forbidden_story_flag_id_3. | +| `0x155c3a` | training_action_minimum_unit_levels | high | investigation | Minimum familiar level for each TRINIT action. TRAIN compares the selected unit's level and rejects values below the populated threshold. | +| `0x155c4f` | training_action_maximum_unit_levels | high | investigation | Maximum familiar level for each TRINIT action. TRAIN rejects levels above a populated threshold; shipped TRINIT leaves this reserved family empty. | +| `0x155c64` | training_action_minimum_alignment_thresholds_encoded | high | investigation | Encoded lower alignment/personality gates. TRAIN subtracts 100 from a populated value and requires familiar_alignment to be at least that signed threshold; the five shipped gates correspond to the locked hints' kind-personality requirements. | +| `0x155c79` | training_action_maximum_alignment_thresholds_encoded | high | investigation | Encoded upper alignment/personality gates. TRAIN subtracts 100 and requires familiar_alignment not to exceed the signed threshold; the five shipped gates correspond to evil-personality requirements. | +| `0x155c8e` | training_action_minimum_progress | high | investigation | Minimum familiar_training_progress required by each TRINIT action. TRAIN rejects the action when the current value is below a populated threshold. | +| `0x155ca3` | training_action_maximum_progress | high | investigation | Upper training-progress gate paired with training_action_minimum_progress. TRAIN enforces populated values; shipped TRINIT leaves all 21 cells zero. | +| `0x155cb8` | training_action_minimum_unit_stats | high | investigation | Ten-column minimum-stat gate using the first ten unit-stat ABI columns. TRAIN compares each populated cell with the selected familiar's current stat; shipped TRINIT leaves the table empty. Columns: 0=accuracy, 1=evasion, 2=physical_attack, 3=physical_defense, 4=magic_attack, 5=magic_defense, 6=speed, 7=luck, 8=critical_chance, 9=capture_power. | +| `0x155d8a` | training_action_maximum_unit_stats | high | investigation | Ten-column maximum-stat gate paired with training_action_minimum_unit_stats. TRAIN enforces populated cells; shipped TRINIT leaves the table empty. Columns: 0=accuracy, 1=evasion, 2=physical_attack, 3=physical_defense, 4=magic_attack, 5=magic_defense, 6=speed, 7=luck, 8=critical_chance, 9=capture_power. | +| `0x155e5c` | training_action_required_item_ids | high | investigation | Optional item prerequisite for each TRINIT action. TRAIN requires a nonzero inventory count and displays the joined item name; the item is a gate, not consumed by this path. | +| `0x155e71` | training_action_required_skill_ids | high | investigation | Optional acquired-skill prerequisite. TRAIN requires the corresponding skill_acquired_flags cell; shipped TRINIT leaves all 21 cells zero. | +| `0x155e86` | training_action_spirit_deltas | high | investigation | Signed current_spirit delta for each TRINIT action. TRAIN rejects an action whose post-delta spirit would be negative, displays the negated value as its cost, and applies the delta before scene dispatch. | +| `0x155e9b` | training_action_unit_stat_deltas | high | investigation | Fourteen-column familiar stat effects for each TRINIT action. TRAIN adds populated values through the unit stat-growth ABI, carries fractional growth, applies caps, and invokes SHOWGROW when any stat changes. Columns: 0=accuracy, 1=evasion, 2=physical_attack, 3=physical_defense, 4=magic_attack, 5=magic_defense, 6=speed, 7=luck, 8=critical_chance, 9=capture_power, 10=movement, 11=max_hp, 12=max_sp, 13=max_fs. | +| `0x155fc1` | training_action_alignment_deltas_hundredths | high | investigation | Alignment/personality change for each TRINIT action in hundredths. TRAIN combines it with familiar_alignment_fraction, applies the integral and stochastic fractional change to familiar_alignment, and clamps the result to -99..99. | +| `0x155fd6` | training_action_progress_deltas_hundredths | high | investigation | Training-progress gain for each TRINIT action in hundredths. TRAIN combines it with familiar_training_progress_fraction, advances familiar_training_progress with fractional probability, and clamps the result to 99. | +| `0x155feb` | training_action_awarded_skill_ids | high | investigation | Optional skill granted by a TRINIT action. TRAIN checks whether it is new, marks the reward path, and passes the id to ADDSKILL; the extractor joins all eight populated ids to SKINIT names. | +| `0x156000` | training_action_awarded_item_ids | high | investigation | Optional item granted by a TRINIT action. TRAIN suppresses already-owned key/story rewards, displays the joined item name, and calls ADDITEM for the three populated shipped actions. | +| `0x156015` | training_action_event_story_flag_ids | high | investigation | Per-action event sequence selected by the prior execution count. TRAIN copies the chosen id to scjump_decision_out and dispatches it through SCINIT; GAMESTART marks every slot below a restored count in story_event_flags. Repeated ids intentionally reuse a scene, and the first zero defines the execution cap. Columns: 0=execution_1, 1=execution_2, 2=execution_3, 3=execution_4, 4=execution_5, 5=execution_6, 6=execution_7, 7=execution_8, 8=execution_9, 9=execution_10. | | `0x1561f6` | magic_action_information_handler_script_ids | high | investigation | MAINIT handler column indexed by current_magic_action_id. MAGIC and STUDY call the selected packed script id; every shipped action routes to MAMES, whose text dispatcher has authored descriptions for ids 1..9 only. | | `0x156214` | alchemy_recipe_output_item_ids | high | investigation | Output item id indexed by sparse alchemy recipe id. ALCHEMY scans recipe ids 0..999, treats a nonzero cell as a populated recipe, and adds one copy of this item after a successful synthesis. | | `0x1565fc` | alchemy_recipe_minimum_levels | high | investigation | Minimum alchemy level indexed by recipe id. ALCHEMY exposes a populated recipe only when this value is less than or equal to alchemy_level. | @@ -280,7 +309,6 @@ | `0x3ad6` | — | low | auto-shape | array | | `0x4315` | — | low | auto-shape | array | | `0x4379` | — | low | auto-shape | array | -| `0x6727` | — | low | auto-shape | array | | `0x6804` | — | low | auto-shape | array | | `0x6868` | — | low | auto-shape | array | | `0x6994` | — | low | auto-shape | array | @@ -298,7 +326,6 @@ | `0x52225` | — | low | auto-shape | array | | `0x52257` | — | low | auto-shape | array | | `0x522bb` | — | low | auto-shape | array | -| `0x53ede` | — | low | auto-shape | array | | `0x55e3b` | — | low | auto-shape | array | | `0x56b52` | — | low | auto-shape | array | | `0x56f6d` | — | low | auto-shape | array | @@ -381,19 +408,6 @@ | `0x155b98` | — | low | auto-shape | array | | `0x155b9e` | — | low | auto-shape | array | | `0x155ba4` | — | low | auto-shape | array | -| `0x155c3a` | — | low | auto-shape | array | -| `0x155c4f` | — | low | auto-shape | array | -| `0x155c64` | — | low | auto-shape | array | -| `0x155c79` | — | low | auto-shape | array | -| `0x155c8e` | — | low | auto-shape | array | -| `0x155ca3` | — | low | auto-shape | array | -| `0x155e5c` | — | low | auto-shape | array | -| `0x155e71` | — | low | auto-shape | array | -| `0x155e86` | — | low | auto-shape | array | -| `0x155fc1` | — | low | auto-shape | array | -| `0x155fd6` | — | low | auto-shape | array | -| `0x155feb` | — | low | auto-shape | array | -| `0x156000` | — | low | auto-shape | array | | `0x1560e8` | — | low | auto-shape | array | | `0x156106` | — | low | auto-shape | array | | `0x156124` | — | low | auto-shape | array | @@ -413,6 +427,7 @@ |---|---|---|---|---| | `0x32f1` | difficulty_index | high | investigation | GAMESTART's three-way difficulty selection. SETEN uses the zero-based index to select difficulty stat adjustments; FIELD indexes three-column stage-enemy arrays with it, and SETOBJ tests it against each object's difficulty mask. | | `0x6718` | selected_party_slot | high | investigation | Current/selected slot in the 100-entry party-unit arrays. UNITECH chooses a free slot here before populating it; CHMENU replaces it with the selected sorted roster slot, then uses it to index party_slot_flags, party_slot_character_id, and companion per-slot tables. A natural New Game enters SC0000 with slot 2 selected. | +| `0x53edd` | selected_training_action_id | high | investigation | TRAIN's selected action row. It indexes all TRINIT eligibility, effect, text, award, and event arrays during detail rendering and execution. | | `0x66713` | acting_entity_index | high | investigation | Entity whose movement or battle turn is being resolved. FIELD sets it before MVRTN; RTN_M providers use it for faction, position, resources, selected action, and routine progress. Together with target_entity_index it forms the attacker/target pair passed into battle. | | `0x66714` | target_entity_index | high | investigation | Selected target/opponent entity. RTN_M051/052 choose and write an active foreign-faction target; FIELD pairs it with acting_entity_index for movement presentation and battle setup. | | `0x66715` | current_unit_id | high | investigation | Shared current-unit selector used by character growth and setup scripts. CCINIT keys every class-change rule on this value; CALCCC, ADDEXP, SETEN, SALLY, and related scripts use it to index unit definitions and persistent per-unit state. | @@ -443,16 +458,12 @@ | `0x3231` | game_mode | med | inference | Game-mode/phase selector in the 0x3231-0x3234 progression-state cluster (chapter_mode is 0x3234). Enum 1..9, written by the gameplay scripts (AIM/ALCHEMY/BTL/BUNKI), branch-read in 136 scenes. Distinct from chapter; likely current sub-mode/screen. INFERENCE — confirm with a listing/sweep before relying on the exact meaning. | | `0x3234` | chapter_mode | high | investigation | Progression chapter/mode selector. SCJUMP's top-level switch keys on it; branch-read by progression scripts (FIELD etc.), not directly by SC/SP scenes. | | `0x3275` | — | med | auto-shape | TODO: confirm. Branch-read in 136 scenes / 143 scripts; compared against [0]; writers=['CAMP.BIN', 'DEBUGADV.BIN', 'DEBUGADV2.BIN', 'FIELD.BIN']. | -| `0x6722` | — | med | auto-shape | TODO: confirm. Branch-read in 13 scenes / 18 scripts; compared against [0, 1, 5, 99]; writers=['GAMESTART.BIN', 'SALLY.BIN', 'SC0010.BIN', 'SC0140.BIN']. | -| `0x6724` | — | med | auto-shape | TODO: confirm. Branch-read in 4 scenes / 10 scripts; compared against [0, 25, 50, 99]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | | `0x7679` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0, 1, 4, 8, 16, 256, 512, 1024]; writers=['FIELD.BIN']. | | `0x767a` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0]; writers=['DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN', 'FIELD.BIN']. | | `0x767b` | — | med | auto-shape | TODO: confirm. Branch-read in 136 scenes / 150 scripts; compared against [0, 1]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | | `0x767d` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 9 scripts; compared against [0, 1, 2, 3]; writers=['CAMP.BIN', 'DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN']. | | `0x204f5` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [1, 2, 4, 8, 16, 32, 64]; writers=['FIELD.BIN']. | | `0x204f7` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [1, 2, 4, 6]; writers=['DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN', 'GAMESTART.BIN']. | -| `0x20530` | — | med | auto-shape | TODO: confirm. Branch-read in 14 scenes / 22 scripts; compared against [0]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | -| `0x20534` | — | med | auto-shape | TODO: confirm. Branch-read in 14 scenes / 24 scripts; compared against [0, 10, 100, 999]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | | `0x53ef4` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [0]; writers=['CAMP.BIN']. | | `0x55e37` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [0]; writers=['EVOLVE.BIN', 'STUDY.BIN']. | | `0x55e38` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 9 scripts; compared against [0, 1, 2]; writers=['CHMENU.BIN', 'INFO.BIN', 'INFOAF.BIN', 'INFOCH.BIN']. | @@ -3509,7 +3520,6 @@ | `0x4537` | — | med | auto-shape | string-table (written by CTINIT) | | `0x4538` | — | med | auto-shape | string-table (written by CTINIT) | | `0x4539` | — | med | auto-shape | string-table (written by CTINIT) | -| `0x453b` | — | med | auto-shape | string-table (written by TRINIT) | | `0x453c` | — | med | auto-shape | string-table (written by TRINIT) | | `0x4541` | — | med | auto-shape | string-table (written by TRINIT) | | `0x4542` | — | med | auto-shape | string-table (written by TRINIT) | @@ -3734,7 +3744,6 @@ | `0x7e54` | — | med | auto-shape | record-table[stride 50] | | `0x4e3d7` | — | med | auto-shape | record-table[stride 14] | | `0x53a2b` | — | med | auto-shape | record-table[stride 20] | -| `0x53edd` | — | low | auto-shape | index/counter? | | `0x53ef5` | — | low | auto-shape | index/counter? | | `0x53ef7` | — | med | auto-shape | record-table[stride 8] | | `0x57356` | — | med | auto-shape | current-entity-index? | @@ -5083,12 +5092,6 @@ | `0x15288e` | — | med | auto-shape | record-table[stride 6] | | `0x153ffe` | — | med | auto-shape | record-table[stride 6] | | `0x155baa` | — | med | auto-shape | record-table[stride 3] | -| `0x155bbc` | — | med | auto-shape | record-table[stride 3] | -| `0x155bfb` | — | med | auto-shape | record-table[stride 3] | -| `0x155cb8` | — | med | auto-shape | record-table[stride 10] | -| `0x155d8a` | — | med | auto-shape | record-table[stride 10] | -| `0x155e9b` | — | med | auto-shape | record-table[stride 14] | -| `0x156015` | — | med | auto-shape | record-table[stride 10] | | `0x15a75b` | — | med | auto-shape | record-table[stride 21] | | `0x15a785` | — | med | auto-shape | record-table[stride 3] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index df5c28d..9d4a583 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -683,9 +683,11 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape while LAINIT closes all twenty shipped terrain definitions: names/effects, rendering/topology columns, ten combat-stat columns, traversal-skill requirements, and shared map-texture assets. SPINIT closes HMODE's eight-by-fifteen thumbnail-page-to-scene registry, with all 118 scripts and - eight INIT2 sheets resolved. With RTINIT's used-provider surface closed and - ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES classified, audit TRINIT's compact - ritual/action registry next; + 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; 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 1464a9a..5ef2f1d 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3040,5 +3040,41 @@ semantics without inventing a stronger name for opcode `0x19d`. Regressions protect the 8-by-15 geometry, 118/120 population, exact two-cell gap, all eight INIT2 thumbnail joins, all 118 SYS4INI script joins, and complete instruction accounting. -**Next:** audit TRINIT's 365-instruction ritual/action registry by its direct consumers; it is the +**Next:** audit TRINIT's 365-instruction training-action registry by its direct consumers; it is the next compact mixed string/numeric INIT surface. + +## Data-semantics sidebar: TRINIT training actions (2026-07-23) + +TRINIT is TRAIN's complete 21-row training/sexual-magic action registry. Its 75 string writes form +six slots per row at `0x453b`: three available-description/cost/reward lines followed by three +locked-condition hint lines. The numeric storage is one contiguous block from `0x155bbc` through +`0x1560e6`, divided into nineteen parallel or row-major families. The dedicated schema classifies +all 365 instructions exactly: 75 strings, 289 static integer writes, and `exit`. + +TRAIN proves the eligibility half of the schema. Each action can require up to three story flags, +minimum/maximum unit level, signed familiar alignment thresholds encoded with a +100 bias, +minimum/maximum training progress, ten minimum/maximum combat stats, one inventory item, and one +acquired skill. The shipped data uses thirteen story-flag cells, one minimum-level gate, five +minimum and five maximum alignment gates, fourteen minimum-progress gates, and eight required +items; the forbidden-flag, maximum-level, maximum-progress, stat-bound, and required-skill families +are reserved but empty. All item gates resolve to ITINIT. + +The effect half stores a negative 精気 delta, fourteen unit-stat deltas, signed alignment and +training-progress changes in hundredths, optional skill/item awards, and ten event ids. TRAIN checks +and deducts the spirit cost, applies the unit-stat growth ABI, carries fractional alignment/progress, +and grants the selected reward. All eight skill awards resolve to SKINIT and all three item awards +resolve to ITINIT. The new global names also identify current/maximum spirit, familiar alignment and +its fraction, training progress and its fraction, total execution count, and the 21 per-action counts. + +The 75 populated event cells resolve through SCINIT to `SC0800`, `SC0810`, `SC0820`, `SC0830`, +`SC0840`, or `SC0850` paths. TRAIN indexes the row by the prior per-action execution count, copies +the selected value to the SCJUMP decision output, increments the count, and treats the first zero as +the execution cap. GAMESTART independently confirms the dual story-flag role: after restoring each +saved count it marks every preceding event id complete. Repeated ids intentionally reuse a scene. + +Regressions protect the complete write accounting, all text/gate/effect populations, representative +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. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index daca325..62a1a4d 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -142,6 +142,13 @@ the matching `SO027A.AGF` through `SO027H.AGF` thumbnail sheets, and all populat `SP*.BIN` resources. HMODE filters cells through opcode `0x19d` and indirectly call-scripts the selected available entry. +TRINIT defines TRAIN's 21 training/sexual-magic actions. Each row owns six available/locked text +slots plus eligibility, spirit-cost, fourteen-stat, alignment, training-progress, reward, and +ten-event fields in one contiguous numeric block. TRAIN consumes the complete schema; GAMESTART +replays event ids below each restored execution count as completed story flags. All eight item +requirements, eight skill awards, three item awards, and 75 event cells resolve through +ITINIT/SKINIT/SCINIT. + 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 a7d9a5a..e720292 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -57,7 +57,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited * | `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, 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 profiling. | run each directly | — | +| `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 | — | | `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, @@ -109,6 +109,13 @@ address records. It joins the page rows to INIT2's `SO027A.AGF` through `SO027H. resolves every populated cell to its `SP*.BIN` resource, preserves the final two zero slots, and records HMODE's opcode-0x19d availability-filter plus indirect-call contract. +Name-mode TRINIT bypasses the generic record-boundary heuristic and emits 21 training/sexual-magic +actions. Each row preserves six available/locked text slots and the contiguous eligibility, spirit-cost, +fourteen-stat, alignment, training-progress, award, and ten-event numeric families. Required and awarded +ids join through ITINIT/SKINIT; all 75 event cells join through SCINIT, and the output records TRAIN's +execution path plus GAMESTART's restored-story-flag contract. Its profile summarizes text/gate/effect +populations, execution-limit distribution, and event dispatch coverage. + 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 203b7fa..226666d 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], **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], **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. @@ -64,8 +64,9 @@ - [x] **2.3g — Extract `MPINIT`'s stage-terrain atlas.** Footer specialization classifies all 1,472 row copies as columns 1..50 of one stride-53 sparse atlas, with destination-derived grid Y=2..1600 and 127 implicit-zero rows. FIELD proves that STINIT2's tile bounds are doubled before copying into the mutable current-stage grid. The schema joins 66 stage definitions to 53 unique rectangles, preserves eight shared-map groups and 47 border-context cells, and resolves terrain ids through LAINIT's names, texture slots, area-fill flags, and layout classes. - [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.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, and SPINIT H-scene gallery are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, audit TRINIT's compact mixed string/numeric ritual/action registry, 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, 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. ## 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 56b9ed1..ebca411 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -29,6 +29,10 @@ MPINIT is a special footer-mode terrain atlas: each footer copy owns the fifty authored cells of one 53-cell half-tile grid row. STINIT2's per-stage tile bounds select rectangles after multiplying both coordinates by two. +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. + 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. @@ -677,6 +681,31 @@ H_SCENE_GALLERY_PAGE_COUNT = 8 H_SCENE_GALLERY_SLOTS_PER_PAGE = 15 H_SCENE_GALLERY_THUMBNAIL_BASE = 0x66421 +TRAINING_ACTION_STRING_BASE = 0x453B +TRAINING_ACTION_STRING_STRIDE = 6 +TRAINING_ACTION_COUNT = 21 +TRAINING_ACTION_ARRAYS = { + "required_story_flag_ids": (0x155BBC, 3), + "forbidden_story_flag_ids": (0x155BFB, 3), + "minimum_unit_level": (0x155C3A, 1), + "maximum_unit_level": (0x155C4F, 1), + "minimum_alignment_encoded": (0x155C64, 1), + "maximum_alignment_encoded": (0x155C79, 1), + "minimum_training_progress": (0x155C8E, 1), + "maximum_training_progress": (0x155CA3, 1), + "minimum_unit_stats": (0x155CB8, 10), + "maximum_unit_stats": (0x155D8A, 10), + "required_item_id": (0x155E5C, 1), + "required_skill_id": (0x155E71, 1), + "spirit_delta": (0x155E86, 1), + "unit_stat_deltas": (0x155E9B, 14), + "alignment_delta_hundredths": (0x155FC1, 1), + "training_progress_delta_hundredths": (0x155FD6, 1), + "awarded_skill_id": (0x155FEB, 1), + "awarded_item_id": (0x156000, 1), + "event_story_flag_ids": (0x156015, 10), +} + def resolve(name: str) -> Path: for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"): @@ -3784,6 +3813,416 @@ def extract_h_scene_gallery(scr): } +def extract_training_actions(scr): + """Extract TRINIT's 21 training/sexual-magic action definitions.""" + string_cells: dict[tuple[int, int], str] = {} + numeric_cells = { + field_name: {} + for field_name in TRAINING_ACTION_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] + index = destination - TRAINING_ACTION_STRING_BASE + capacity = ( + TRAINING_ACTION_COUNT + * TRAINING_ACTION_STRING_STRIDE + ) + if not 0 <= index < capacity: + raise ValueError( + f"{scr.path.name}: training string write " + f"0x{destination:x} outside the {capacity}-cell table" + ) + action_id, column = divmod( + index, TRAINING_ACTION_STRING_STRIDE + ) + value = scr.strings[ins.args[1][1]][0] + _store_unique( + string_cells, (action_id, column), value, action_id + ) + classified_offsets.add(ins.offset) + string_write_count += 1 + continue + + write = _static_global_write(ins) + if write is None: + continue + static_write_count += 1 + destination, value = write + if not isinstance(value, int): + raise ValueError( + f"{scr.path.name}: non-static training value " + f"at 0x{ins.offset:x}" + ) + for field_name, (base, stride) in TRAINING_ACTION_ARRAYS.items(): + index = destination - base + if 0 <= index < TRAINING_ACTION_COUNT * stride: + action_id, column = divmod(index, stride) + _store_unique( + numeric_cells[field_name], + (action_id, column), + value, + action_id, + ) + classified_offsets.add(ins.offset) + break + else: + raise ValueError( + f"{scr.path.name}: unclassified training write " + f"0x{destination:x} at 0x{ins.offset:x}" + ) + + exit_offsets = { + ins.offset + for ins in scr.instructions + if sys4load.display_label(ins.opcode) == "exit" + } + classified_offsets.update(exit_offsets) + 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) + ) + if len(exit_offsets) != 1: + raise ValueError( + f"{scr.path.name}: expected one exit, found {len(exit_offsets)}" + ) + + item_records, _ = extract_name(sys4load.load(resolve("ITINIT"))) + item_names = { + record["id"]: record["name"] for record in item_records + } + skill_records, _ = extract_name(sys4load.load(resolve("SKINIT"))) + skill_names = { + record["id"]: record["name"] for record in skill_records + } + dispatch_records, _ = extract_dispatch( + sys4load.load(resolve("SCINIT")) + ) + event_dispatch = { + record["id"]: record for record in dispatch_records + } + + def values(field_name: str, action_id: int) -> list[int]: + _, stride = TRAINING_ACTION_ARRAYS[field_name] + cells = numeric_cells[field_name] + return [ + cells.get((action_id, column), 0) + for column in range(stride) + ] + + def scalar(field_name: str, action_id: int) -> int: + return values(field_name, action_id)[0] + + records = [] + for action_id in range(TRAINING_ACTION_COUNT): + description_lines = [ + string_cells.get((action_id, column)) + for column in range(3) + ] + locked_hint_lines = [ + string_cells.get((action_id, column)) + for column in range(3, 6) + ] + description_lines = [ + line for line in description_lines if line is not None + ] + locked_hint_lines = [ + line for line in locked_hint_lines if line is not None + ] + + raw_fields = {} + raw_record_fields = {} + raw_string_fields = {} + for column in range(TRAINING_ACTION_STRING_STRIDE): + cell = (action_id, column) + if cell in string_cells: + raw_string_fields[ + f"0x{TRAINING_ACTION_STRING_BASE:x}/" + f"{TRAINING_ACTION_STRING_STRIDE}/{column}" + ] = string_cells[cell] + for field_name, (base, stride) in TRAINING_ACTION_ARRAYS.items(): + for column in range(stride): + cell = (action_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 = [ + value + for value in values( + "required_story_flag_ids", action_id + ) + if value + ] + forbidden_flags = [ + value + for value in values( + "forbidden_story_flag_ids", action_id + ) + if value + ] + minimum_stats = { + UNIT_STAT_COLUMNS[column]: value + for column, value in enumerate( + values("minimum_unit_stats", action_id) + ) + if value + } + maximum_stats = { + UNIT_STAT_COLUMNS[column]: value + for column, value in enumerate( + values("maximum_unit_stats", action_id) + ) + if value + } + stat_deltas = { + UNIT_STAT_COLUMNS[column]: value + for column, value in enumerate( + values("unit_stat_deltas", action_id) + ) + if value + } + event_ids = values("event_story_flag_ids", action_id) + events = [] + for slot, event_id in enumerate(event_ids): + if not event_id: + continue + dispatch = event_dispatch.get(event_id, {}) + events.append({ + "slot": slot, + "story_flag_id": event_id, + "script_resource_id": dispatch.get( + "script_resource_id", 0 + ), + "script_name": dispatch.get("script_name", ""), + }) + + required_item_id = scalar("required_item_id", action_id) + required_skill_id = scalar("required_skill_id", action_id) + awarded_item_id = scalar("awarded_item_id", action_id) + awarded_skill_id = scalar("awarded_skill_id", action_id) + spirit_delta = scalar("spirit_delta", action_id) + minimum_alignment_encoded = scalar( + "minimum_alignment_encoded", action_id + ) + maximum_alignment_encoded = scalar( + "maximum_alignment_encoded", action_id + ) + alignment_delta = scalar( + "alignment_delta_hundredths", action_id + ) + training_delta = scalar( + "training_progress_delta_hundredths", action_id + ) + + eligibility = { + "required_story_flag_ids": required_flags, + "forbidden_story_flag_ids": forbidden_flags, + "minimum_unit_stats": minimum_stats, + "maximum_unit_stats": maximum_stats, + } + for field_name in ( + "minimum_unit_level", + "maximum_unit_level", + "minimum_training_progress", + "maximum_training_progress", + ): + value = scalar(field_name, action_id) + if value: + eligibility[field_name] = value + if minimum_alignment_encoded: + eligibility["minimum_alignment"] = ( + minimum_alignment_encoded - 100 + ) + if maximum_alignment_encoded: + eligibility["maximum_alignment"] = ( + maximum_alignment_encoded - 100 + ) + if required_item_id: + eligibility.update({ + "required_item_id": required_item_id, + "required_item_name": item_names.get( + required_item_id, "" + ), + }) + if required_skill_id: + eligibility.update({ + "required_skill_id": required_skill_id, + "required_skill_name": skill_names.get( + required_skill_id, "" + ), + }) + + effects = { + "spirit_delta": spirit_delta, + "spirit_cost": -spirit_delta, + "unit_stat_deltas": stat_deltas, + "alignment_delta_hundredths": alignment_delta, + "training_progress_delta_hundredths": training_delta, + } + if awarded_skill_id: + effects.update({ + "awarded_skill_id": awarded_skill_id, + "awarded_skill_name": skill_names.get( + awarded_skill_id, "" + ), + }) + if awarded_item_id: + effects.update({ + "awarded_item_id": awarded_item_id, + "awarded_item_name": item_names.get( + awarded_item_id, "" + ), + }) + + records.append({ + "id": action_id, + "name": f"training_action_{action_id:02d}", + "description_lines": description_lines, + "description": "".join(description_lines), + "locked_hint_lines": locked_hint_lines, + "locked_hint": "".join(locked_hint_lines), + "eligibility": eligibility, + "effects": effects, + "event_story_flag_ids": event_ids, + "execution_limit": len(events), + "events": events, + "fields": raw_fields, + "record_fields": raw_record_fields, + "string_fields": raw_string_fields, + }) + + string_key = f"0x{TRAINING_ACTION_STRING_BASE:x}" + array_layouts = { + string_key: {"stride": TRAINING_ACTION_STRING_STRIDE}, + **{ + f"0x{base:x}": {"stride": stride} + for base, stride in TRAINING_ACTION_ARRAYS.values() + if stride > 1 + }, + } + semantic_names = { + string_key: "training_action_text", + **{ + f"0x{base:x}": f"training_action_{field_name}" + for field_name, (base, _) in TRAINING_ACTION_ARRAYS.items() + }, + } + schema_field_semantics = {} + for column in range(TRAINING_ACTION_STRING_STRIDE): + family = ( + "description_line" if column < 3 else "locked_hint_line" + ) + ordinal = column + 1 if column < 3 else column - 2 + schema_field_semantics[ + f"{string_key}/{TRAINING_ACTION_STRING_STRIDE}/{column}" + ] = f"training_action_{family}_{ordinal}" + for field_name, (base, stride) in TRAINING_ACTION_ARRAYS.items(): + key = f"0x{base:x}" + if stride == 1: + schema_field_semantics[key] = semantic_names[key] + continue + for column in range(stride): + schema_field_semantics[ + f"{key}/{stride}/{column}" + ] = f"training_action_{field_name}.column_{column}" + + event_values = [ + event["story_flag_id"] + for record in records + for event in record["events"] + ] + authored_cell_counts = { + field_name: len(cells) + for field_name, cells in numeric_cells.items() + } + return records, { + "schema": "training-action-definitions", + "reserved_record_count": TRAINING_ACTION_COUNT, + "string_table_base": string_key, + "string_stride": TRAINING_ACTION_STRING_STRIDE, + "numeric_block_start": ( + f"0x{TRAINING_ACTION_ARRAYS['required_story_flag_ids'][0]:x}" + ), + "numeric_block_end_exclusive": "0x1560e7", + "array_layouts": array_layouts, + "schema_field_semantics": schema_field_semantics, + "semantic_array_names": semantic_names, + "authored_numeric_cell_counts": authored_cell_counts, + "string_write_count": string_write_count, + "static_write_count": static_write_count, + "classified_static_write_count": sum( + len(cells) for cells in numeric_cells.values() + ), + "classified_instruction_count": len(classified_offsets), + "required_item_join_count": sum( + bool(record["eligibility"].get("required_item_name")) + for record in records + ), + "awarded_item_join_count": sum( + bool(record["effects"].get("awarded_item_name")) + for record in records + ), + "awarded_skill_join_count": sum( + bool(record["effects"].get("awarded_skill_name")) + for record in records + ), + "event_cell_count": len(event_values), + "distinct_event_story_flag_ids": sorted(set(event_values)), + "resolved_event_dispatch_count": sum( + bool(event["script_name"]) + for record in records + for event in record["events"] + ), + "runtime_contract": { + "selected_action_id": "0x53edd", + "availability_state_by_action": "0x53ede", + "familiar_alignment": "0x6722", + "familiar_alignment_fraction": "0x6723", + "training_progress": "0x6724", + "training_progress_fraction": "0x6725", + "total_execution_count": "0x6726", + "execution_count_by_action": "0x6727", + "current_spirit": "0x20530", + "maximum_spirit": "0x20534", + }, + "consumer_contract": { + "TRAIN.BIN": ( + "evaluates every eligibility family, renders the available " + "or locked three-line text, deducts spirit, applies fourteen-" + "stat/alignment/training effects, awards items or skills, " + "increments per-action execution counts, and dispatches the " + "event id selected by the prior execution count" + ), + "GAMESTART.BIN": ( + "restores all event story flags in slots below each saved " + "per-action execution count so prior training scenes remain " + "completed after load" + ), + }, + } + + def _map_stage_definitions() -> list[dict]: """Read the STINIT2 records that own all four terrain-atlas bounds.""" stage_scr = sys4load.load(resolve("STINIT2")) @@ -4399,6 +4838,11 @@ def write_data_index(data_dir: Path) -> None: "joins every page to its INIT2 SO027 thumbnail sheet, resolves all 118 populated", "scene resources, and retains the two implicit empty cells in the final page.", "", + "TRINIT's dedicated training-action schema exposes 21 six-line text rows and", + "the contiguous eligibility/cost/effect/award/event block consumed by TRAIN.", + "Item and skill ids join to ITINIT/SKINIT; all 75 event slots join through", + "SCINIT, and GAMESTART's restored-story-flag contract remains 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", @@ -4459,7 +4903,14 @@ def main() -> int: raise SystemExit(str(error)) from error scr = sys4load.load(resolve(name)) - mode = mode_arg or detect_mode(scr) + if mode_arg is not None: + mode = mode_arg + elif name == "TRINIT": + # TRINIT's six-column sparse string matrix is not the generic + # one-name-per-record layout expected by name-mode auto-detection. + mode = "name" + else: + mode = detect_mode(scr) extractor = { "name": extract_name, "numeric": extract_numeric, @@ -4491,6 +4942,8 @@ def main() -> int: extractor = extract_voice_configuration elif mode == "name" and name == "LAINIT": extractor = extract_terrain_definitions + elif mode == "name" and name == "TRINIT": + extractor = extract_training_actions 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 19de5d1..5b49f16 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -365,6 +365,67 @@ def profile_h_scene_gallery(data: dict) -> dict: } +def profile_training_actions(data: dict) -> dict: + """Summarize TRINIT's training-action gates, effects, and events.""" + if data.get("schema") != "training-action-definitions": + return {} + records = data.get("records", []) + return { + "action_count": len(records), + "string_line_count": data.get("string_write_count", 0), + "description_line_count": sum( + len(record.get("description_lines", [])) + for record in records + ), + "locked_hint_line_count": sum( + len(record.get("locked_hint_lines", [])) + for record in records + ), + "required_story_flag_cell_count": data.get( + "authored_numeric_cell_counts", {} + ).get("required_story_flag_ids", 0), + "required_item_count": sum( + "required_item_id" in record.get("eligibility", {}) + for record in records + ), + "minimum_alignment_gate_count": sum( + "minimum_alignment" in record.get("eligibility", {}) + for record in records + ), + "maximum_alignment_gate_count": sum( + "maximum_alignment" in record.get("eligibility", {}) + for record in records + ), + "minimum_training_gate_count": sum( + "minimum_training_progress" + in record.get("eligibility", {}) + for record in records + ), + "stat_delta_cell_count": data.get( + "authored_numeric_cell_counts", {} + ).get("unit_stat_deltas", 0), + "awarded_item_count": sum( + "awarded_item_id" in record.get("effects", {}) + for record in records + ), + "awarded_skill_count": sum( + "awarded_skill_id" in record.get("effects", {}) + for record in records + ), + "event_cell_count": data.get("event_cell_count", 0), + "distinct_event_count": len( + data.get("distinct_event_story_flag_ids", []) + ), + "resolved_event_dispatch_count": data.get( + "resolved_event_dispatch_count", 0 + ), + "execution_limits": dict(sorted(collections.Counter( + str(record.get("execution_limit", 0)) + for record in records + ).items(), key=lambda item: int(item[0]))), + } + + def profile_messages(data: dict) -> dict: """Summarize the joined player-facing message evidence.""" records = data["records"] @@ -492,7 +553,29 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str: f"- records: {data['record_count']}", f"- populated fields: {len(rows)}", ] - if h_gallery_profile := profile_h_scene_gallery(data): + if training_profile := profile_training_actions(data): + lines.extend([ + f"- training actions: {training_profile['action_count']}", + f"- display text lines: " + f"{training_profile['description_line_count']} available + " + f"{training_profile['locked_hint_line_count']} locked", + f"- eligibility cells: " + f"{training_profile['required_story_flag_cell_count']} required " + f"story flags, {training_profile['required_item_count']} items, " + f"{training_profile['minimum_alignment_gate_count']} minimum + " + f"{training_profile['maximum_alignment_gate_count']} maximum " + f"alignment gates, " + f"{training_profile['minimum_training_gate_count']} training gates", + f"- effect cells: {training_profile['stat_delta_cell_count']} stat " + f"deltas, {training_profile['awarded_skill_count']} skill awards, " + f"{training_profile['awarded_item_count']} item awards", + f"- event slots: {training_profile['event_cell_count']} across " + f"{training_profile['distinct_event_count']} distinct story flags " + f"({training_profile['resolved_event_dispatch_count']} dispatches " + f"resolved)", + f"- execution limits: {training_profile['execution_limits']}", + ]) + elif h_gallery_profile := profile_h_scene_gallery(data): lines.extend([ f"- geometry: {h_gallery_profile['page_count']} pages × " f"{h_gallery_profile['slots_per_page']} slots", @@ -667,6 +750,7 @@ def main() -> int: "map_atlas_profile": profile_map_atlas(data), "terrain_definition_profile": profile_terrain_definitions(data), "h_scene_gallery_profile": profile_h_scene_gallery(data), + "training_action_profile": profile_training_actions(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 783fbf5..3a69928 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1572,6 +1572,90 @@ def test_h_scene_gallery() -> None: ) +def test_training_actions() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["TRINIT.BIN"]) + records, meta = extract_init.extract_training_actions(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 21 + and meta["reserved_record_count"] == 21 + and meta["string_stride"] == 6 + and meta["numeric_block_start"] == "0x155bbc" + and meta["numeric_block_end_exclusive"] == "0x1560e7", + "TRINIT exposes its 21-row text and contiguous numeric geometry", + ) + check( + meta["string_write_count"] == 75 + and meta["static_write_count"] == 289 + and meta["classified_static_write_count"] == 289 + and meta["classified_instruction_count"] == 365, + "TRINIT classifies every text, numeric, and exit instruction", + ) + check( + by_id[0]["description_lines"] + == [ + "使い魔と性魔術を行い、能力を高める。", + "精気20必要。『捕獲攻撃』獲得。", + ] + and by_id[1]["locked_hint_lines"] + == [ + "使い魔の成長や特別なアイテムが必要の", + "ようだ……。", + "作る為の方法と材料は……。", + ] + and by_id[15]["description_lines"][2] + == "さらに最大精気+2。", + "TRINIT preserves the available and locked three-line text families", + ) + check( + by_id[0]["eligibility"]["minimum_unit_level"] == 3 + and by_id[4]["eligibility"]["minimum_alignment"] == 15 + and by_id[13]["eligibility"]["maximum_alignment"] == -75 + and by_id[13]["eligibility"]["minimum_training_progress"] == 45 + and by_id[1]["eligibility"]["required_item_name"] + == "マタタビの媚薬", + "TRINIT decodes level, alignment, training, and ITINIT gates", + ) + check( + by_id[0]["effects"]["spirit_cost"] == 20 + and by_id[0]["effects"]["unit_stat_deltas"] + == { + "physical_attack": 7, + "physical_defense": 4, + "speed": 8, + "luck": 2, + "max_hp": 15, + "max_sp": 12, + "max_fs": 6, + } + and by_id[0]["effects"]["awarded_skill_name"] == "捕獲攻撃" + and by_id[13]["effects"]["alignment_delta_hundredths"] == -2000 + and by_id[13]["effects"]["awarded_item_name"] == "死王の喚石", + "TRINIT joins spirit, stat, alignment, skill, and item effects", + ) + check( + meta["event_cell_count"] == 75 + and len(meta["distinct_event_story_flag_ids"]) == 38 + and meta["resolved_event_dispatch_count"] == 75 + and by_id[0]["execution_limit"] == 6 + and by_id[0]["event_story_flag_ids"] + == [800, 830, 830, 830, 830, 830, 0, 0, 0, 0] + and by_id[0]["events"][0]["script_name"] == "SC0800.BIN" + and by_id[0]["events"][1]["script_name"] == "SC0830.BIN", + "TRINIT event slots join to SCINIT and retain repeat-scene limits", + ) + check( + meta["required_item_join_count"] == 8 + and meta["awarded_item_join_count"] == 3 + and meta["awarded_skill_join_count"] == 8 + and meta["authored_numeric_cell_counts"]["unit_stat_deltas"] == 95 + and meta["authored_numeric_cell_counts"]["event_story_flag_ids"] + == 75, + "TRINIT accounts for every definition join and populated field family", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1788,6 +1872,7 @@ if __name__ == "__main__": test_voice_configuration() test_terrain_definitions() test_h_scene_gallery() + test_training_actions() test_map_terrain_atlas() test_condition_definitions() test_field_semantics() diff --git a/tools/test_globals.py b/tools/test_globals.py index ef51df6..4cee427 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -78,6 +78,14 @@ def test_load_and_lint(): and entries[0x1561f6]["name"] == "magic_action_information_handler_script_ids", "MAINIT/MAMES action state is curated") + check(entries[0x453b]["name"] == "training_action_text" + and entries[0x155bbc]["name"] + == "training_action_required_story_flag_ids" + and entries[0x155e9b]["columns"]["13"] == "max_fs" + and entries[0x156015]["columns"]["9"] == "execution_10" + and entries[0x6722]["name"] == "familiar_alignment" + and entries[0x6727]["name"] == "training_action_execution_counts", + "TRINIT/TRAIN action 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 81b41c0..fd88243 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -267,6 +267,60 @@ def main() -> int: assert "- geometry: 8 pages × 15 slots" in rendered_h_gallery assert "- populated scenes: 118/120" in rendered_h_gallery + training_fixture = { + "table": "TRAINING", + "mode": "name", + "schema": "training-action-definitions", + "record_count": 3, + "string_write_count": 7, + "authored_numeric_cell_counts": { + "required_story_flag_ids": 2, + "unit_stat_deltas": 4, + }, + "event_cell_count": 6, + "distinct_event_story_flag_ids": [800, 801, 830], + "resolved_event_dispatch_count": 6, + "records": [ + { + "description_lines": ["one", "cost"], + "locked_hint_lines": [], + "eligibility": {}, + "effects": {"awarded_skill_id": 1}, + "execution_limit": 3, + }, + { + "description_lines": ["two", "cost"], + "locked_hint_lines": ["locked"], + "eligibility": { + "required_item_id": 35, + "minimum_alignment": 10, + "minimum_training_progress": 5, + }, + "effects": {"awarded_item_id": 51}, + "execution_limit": 2, + }, + { + "description_lines": ["three"], + "locked_hint_lines": ["locked", "more"], + "eligibility": {"maximum_alignment": -20}, + "effects": {}, + "execution_limit": 1, + }, + ], + } + training_summary = profile.profile_training_actions(training_fixture) + assert training_summary["action_count"] == 3 + assert training_summary["description_line_count"] == 5 + assert training_summary["locked_hint_line_count"] == 3 + assert training_summary["required_item_count"] == 1 + assert training_summary["minimum_alignment_gate_count"] == 1 + assert training_summary["maximum_alignment_gate_count"] == 1 + assert training_summary["stat_delta_cell_count"] == 4 + assert training_summary["execution_limits"] == {"1": 1, "2": 1, "3": 1} + rendered_training = profile.render_markdown(training_fixture, [], 40) + assert "- training actions: 3" in rendered_training + assert "- event slots: 6 across 3 distinct story flags" in rendered_training + messages = profile.profile_messages(fixture) assert messages["population"] == 1 assert messages["coverage"] == 1 / 3 diff --git a/vm-map/globals.toml b/vm-map/globals.toml index a348624..cbed073 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -2273,25 +2273,25 @@ depends_on = ["0x671e", "0xab8b2"] [[global]] address = "0x6722" -name = "" -category = "story-flag" +name = "familiar_alignment" +category = "counter" type = "int" -value_domain = "one of {0, 1, 5, 99}" -usage = "TODO: confirm. Branch-read in 13 scenes / 18 scripts; compared against [0, 1, 5, 99]; writers=['GAMESTART.BIN', 'SALLY.BIN', 'SC0010.BIN', 'SC0140.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "-99..99; negative is evil/chaotic, positive is kind/lawful" +usage = "The familiar's alignment/personality axis. TRAIN decodes TRINIT minimum and maximum gates by subtracting 100 from the stored threshold, matching the locked hints' kind-versus-evil wording, and applies training_action_alignment_delta_hundredths through the paired fractional accumulator. DRAWCHP renders the signed value and EVOLVE/scene scripts use the same axis." +source = "investigation" +confidence = "high" +depends_on = ["0x6723", "0x155c64", "0x155c79", "0x155fc1"] [[global]] address = "0x6724" -name = "" -category = "story-flag" +name = "familiar_training_progress" +category = "counter" type = "int" -value_domain = "one of {0, 25, 50, 99}" -usage = "TODO: confirm. Branch-read in 4 scenes / 10 scripts; compared against [0, 25, 50, 99]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "0..99" +usage = "Cumulative familiar training/sexual-magic progress. TRAIN tests it against TRINIT's minimum/maximum progress gates, then applies training_action_training_progress_delta_hundredths through the paired fractional accumulator and clamps the result to 99. DRAWCHP and EVOLVE display or compare the same value." +source = "investigation" +confidence = "high" +depends_on = ["0x6725", "0x155c8e", "0x155ca3", "0x155fd6"] [[global]] address = "0x7679" @@ -2383,25 +2383,25 @@ depends_on = [] [[global]] address = "0x20530" -name = "" -category = "story-flag" +name = "current_spirit" +category = "counter" type = "int" -value_domain = "{0,1}" -usage = "TODO: confirm. Branch-read in 14 scenes / 22 scripts; compared against [0]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "0..maximum_spirit" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x20534", "0x155e86"] [[global]] address = "0x20534" -name = "" -category = "story-flag" +name = "maximum_spirit" +category = "counter" type = "int" -value_domain = "one of {0, 10, 100, 999}" -usage = "TODO: confirm. Branch-read in 14 scenes / 24 scripts; compared against [0, 10, 100, 999]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "nonnegative spirit capacity; debug clamp 0..999" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x20530"] [[global]] address = "0x53ef4" @@ -3662,3 +3662,295 @@ usage = "AFINIT's three facility-progression rows. IMPROVE uses row 0 for equipm source = "investigation" confidence = "high" depends_on = ["0x671c", "0x671d", "0x671e", "0x671f", "0x6720", "0x6721"] + +[[global]] +address = "0x453b" +name = "training_action_text" +category = "data-table" +type = "string[21][6]" +columns = { "0" = "description_line_1", "1" = "description_line_2", "2" = "description_line_3", "3" = "locked_hint_line_1", "4" = "locked_hint_line_2", "5" = "locked_hint_line_3" } +value_domain = "player-facing Japanese text; empty cells are unused lines" +usage = "TRINIT's six-string row for each of 21 training/sexual-magic actions. TRAIN renders columns 0..2 for the action description and cost/reward summary, or columns 3..5 for the unmet-condition hint." +source = "investigation" +confidence = "high" +depends_on = ["0x53edd", "0x53ede"] + +[[global]] +address = "0x155bbc" +name = "training_action_required_story_flag_ids" +category = "data-table" +type = "int[21][3]" +columns = { "0" = "required_story_flag_id_1", "1" = "required_story_flag_id_2", "2" = "required_story_flag_id_3" } +value_domain = "one-based story flag id; zero means no requirement" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x6d3"] + +[[global]] +address = "0x155bfb" +name = "training_action_forbidden_story_flag_ids" +category = "data-table" +type = "int[21][3]" +columns = { "0" = "forbidden_story_flag_id_1", "1" = "forbidden_story_flag_id_2", "2" = "forbidden_story_flag_id_3" } +value_domain = "one-based story flag id; zero means no exclusion" +usage = "Reserved three-slot exclusion table paired with training_action_required_story_flag_ids. TRAIN rejects an action when any populated flag equals one; shipped TRINIT leaves all 63 cells zero." +source = "investigation" +confidence = "high" +depends_on = ["0x6d3"] + +[[global]] +address = "0x155c3a" +name = "training_action_minimum_unit_levels" +category = "data-table" +type = "int[21]" +value_domain = "unit level; zero means no lower bound" +usage = "Minimum familiar level for each TRINIT action. TRAIN compares the selected unit's level and rejects values below the populated threshold." +source = "investigation" +confidence = "high" +depends_on = ["0x6930"] + +[[global]] +address = "0x155c4f" +name = "training_action_maximum_unit_levels" +category = "data-table" +type = "int[21]" +value_domain = "unit level; zero means no upper bound" +usage = "Maximum familiar level for each TRINIT action. TRAIN rejects levels above a populated threshold; shipped TRINIT leaves this reserved family empty." +source = "investigation" +confidence = "high" +depends_on = ["0x6930"] + +[[global]] +address = "0x155c64" +name = "training_action_minimum_alignment_thresholds_encoded" +category = "data-table" +type = "int[21]" +value_domain = "100 + signed alignment threshold; zero means no lower bound" +usage = "Encoded lower alignment/personality gates. TRAIN subtracts 100 from a populated value and requires familiar_alignment to be at least that signed threshold; the five shipped gates correspond to the locked hints' kind-personality requirements." +source = "investigation" +confidence = "high" +depends_on = ["0x6722"] + +[[global]] +address = "0x155c79" +name = "training_action_maximum_alignment_thresholds_encoded" +category = "data-table" +type = "int[21]" +value_domain = "100 + signed alignment threshold; zero means no upper bound" +usage = "Encoded upper alignment/personality gates. TRAIN subtracts 100 and requires familiar_alignment not to exceed the signed threshold; the five shipped gates correspond to evil-personality requirements." +source = "investigation" +confidence = "high" +depends_on = ["0x6722"] + +[[global]] +address = "0x155c8e" +name = "training_action_minimum_progress" +category = "data-table" +type = "int[21]" +value_domain = "familiar training progress 0..99; zero means no lower bound" +usage = "Minimum familiar_training_progress required by each TRINIT action. TRAIN rejects the action when the current value is below a populated threshold." +source = "investigation" +confidence = "high" +depends_on = ["0x6724"] + +[[global]] +address = "0x155ca3" +name = "training_action_maximum_progress" +category = "data-table" +type = "int[21]" +value_domain = "familiar training progress 0..99; zero means no upper bound" +usage = "Upper training-progress gate paired with training_action_minimum_progress. TRAIN enforces populated values; shipped TRINIT leaves all 21 cells zero." +source = "investigation" +confidence = "high" +depends_on = ["0x6724"] + +[[global]] +address = "0x155cb8" +name = "training_action_minimum_unit_stats" +category = "data-table" +type = "int[21][10]" +columns = { "0" = "accuracy", "1" = "evasion", "2" = "physical_attack", "3" = "physical_defense", "4" = "magic_attack", "5" = "magic_defense", "6" = "speed", "7" = "luck", "8" = "critical_chance", "9" = "capture_power" } +value_domain = "minimum unit stat; zero means no lower bound" +usage = "Ten-column minimum-stat gate using the first ten unit-stat ABI columns. TRAIN compares each populated cell with the selected familiar's current stat; shipped TRINIT leaves the table empty." +source = "investigation" +confidence = "high" +depends_on = ["0x69f8"] + +[[global]] +address = "0x155d8a" +name = "training_action_maximum_unit_stats" +category = "data-table" +type = "int[21][10]" +columns = { "0" = "accuracy", "1" = "evasion", "2" = "physical_attack", "3" = "physical_defense", "4" = "magic_attack", "5" = "magic_defense", "6" = "speed", "7" = "luck", "8" = "critical_chance", "9" = "capture_power" } +value_domain = "maximum unit stat; zero means no upper bound" +usage = "Ten-column maximum-stat gate paired with training_action_minimum_unit_stats. TRAIN enforces populated cells; shipped TRINIT leaves the table empty." +source = "investigation" +confidence = "high" +depends_on = ["0x69f8"] + +[[global]] +address = "0x155e5c" +name = "training_action_required_item_ids" +category = "data-table" +type = "int[21]" +value_domain = "ITINIT item id; zero means no item requirement" +usage = "Optional item prerequisite for each TRINIT action. TRAIN requires a nonzero inventory count and displays the joined item name; the item is a gate, not consumed by this path." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x155e71" +name = "training_action_required_skill_ids" +category = "data-table" +type = "int[21]" +value_domain = "SKINIT skill id; zero means no skill requirement" +usage = "Optional acquired-skill prerequisite. TRAIN requires the corresponding skill_acquired_flags cell; shipped TRINIT leaves all 21 cells zero." +source = "investigation" +confidence = "high" +depends_on = ["0x65ce"] + +[[global]] +address = "0x155e86" +name = "training_action_spirit_deltas" +category = "data-table" +type = "int[21]" +value_domain = "negative 精気 cost; shipped values -90..-20" +usage = "Signed current_spirit delta for each TRINIT action. TRAIN rejects an action whose post-delta spirit would be negative, displays the negated value as its cost, and applies the delta before scene dispatch." +source = "investigation" +confidence = "high" +depends_on = ["0x20530"] + +[[global]] +address = "0x155e9b" +name = "training_action_unit_stat_deltas" +category = "data-table" +type = "int[21][14]" +columns = { "0" = "accuracy", "1" = "evasion", "2" = "physical_attack", "3" = "physical_defense", "4" = "magic_attack", "5" = "magic_defense", "6" = "speed", "7" = "luck", "8" = "critical_chance", "9" = "capture_power", "10" = "movement", "11" = "max_hp", "12" = "max_sp", "13" = "max_fs" } +value_domain = "signed additive unit stat delta; zero means unchanged" +usage = "Fourteen-column familiar stat effects for each TRINIT action. TRAIN adds populated values through the unit stat-growth ABI, carries fractional growth, applies caps, and invokes SHOWGROW when any stat changes." +source = "investigation" +confidence = "high" +depends_on = ["0x69f8"] + +[[global]] +address = "0x155fc1" +name = "training_action_alignment_deltas_hundredths" +category = "data-table" +type = "int[21]" +value_domain = "signed hundredths of familiar alignment; shipped -2000..300" +usage = "Alignment/personality change for each TRINIT action in hundredths. TRAIN combines it with familiar_alignment_fraction, applies the integral and stochastic fractional change to familiar_alignment, and clamps the result to -99..99." +source = "investigation" +confidence = "high" +depends_on = ["0x6722", "0x6723"] + +[[global]] +address = "0x155fd6" +name = "training_action_progress_deltas_hundredths" +category = "data-table" +type = "int[21]" +value_domain = "positive hundredths of training progress; shipped 100..300" +usage = "Training-progress gain for each TRINIT action in hundredths. TRAIN combines it with familiar_training_progress_fraction, advances familiar_training_progress with fractional probability, and clamps the result to 99." +source = "investigation" +confidence = "high" +depends_on = ["0x6724", "0x6725"] + +[[global]] +address = "0x155feb" +name = "training_action_awarded_skill_ids" +category = "data-table" +type = "int[21]" +value_domain = "SKINIT skill id; zero means no award" +usage = "Optional skill granted by a TRINIT action. TRAIN checks whether it is new, marks the reward path, and passes the id to ADDSKILL; the extractor joins all eight populated ids to SKINIT names." +source = "investigation" +confidence = "high" +depends_on = ["0x65ce", "0xa6e59"] + +[[global]] +address = "0x156000" +name = "training_action_awarded_item_ids" +category = "data-table" +type = "int[21]" +value_domain = "ITINIT item id; zero means no award" +usage = "Optional item granted by a TRINIT action. TRAIN suppresses already-owned key/story rewards, displays the joined item name, and calls ADDITEM for the three populated shipped actions." +source = "investigation" +confidence = "high" +depends_on = ["0x8c877"] + +[[global]] +address = "0x156015" +name = "training_action_event_story_flag_ids" +category = "data-table" +type = "int[21][10]" +columns = { "0" = "execution_1", "1" = "execution_2", "2" = "execution_3", "3" = "execution_4", "4" = "execution_5", "5" = "execution_6", "6" = "execution_7", "7" = "execution_8", "8" = "execution_9", "9" = "execution_10" } +value_domain = "story flag / SCINIT decision id; zero ends the action's execution sequence" +usage = "Per-action event sequence selected by the prior execution count. TRAIN copies the chosen id to scjump_decision_out and dispatches it through SCINIT; GAMESTART marks every slot below a restored count in story_event_flags. Repeated ids intentionally reuse a scene, and the first zero defines the execution cap." +source = "investigation" +confidence = "high" +depends_on = ["0x53edd", "0x62ccf", "0x6727", "0x6d3", "0x87a57"] + +[[global]] +address = "0x6723" +name = "familiar_alignment_fraction" +category = "counter" +type = "int" +value_domain = "signed fractional hundredths carried between alignment changes" +usage = "Fractional accumulator paired with familiar_alignment. TRAIN adds the selected action's hundredths delta, applies the integral quotient and probabilistic remainder, and preserves the remaining fraction." +source = "investigation" +confidence = "high" +depends_on = ["0x6722", "0x155fc1"] + +[[global]] +address = "0x6725" +name = "familiar_training_progress_fraction" +category = "counter" +type = "int" +value_domain = "fractional hundredths carried between training-progress gains" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x6724", "0x155fd6"] + +[[global]] +address = "0x6726" +name = "training_action_total_execution_count" +category = "counter" +type = "int" +value_domain = "nonnegative total completed training actions" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x6727"] + +[[global]] +address = "0x6727" +name = "training_action_execution_counts" +category = "counter" +type = "int[21]" +value_domain = "0..10 completed executions by TRINIT action id" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x156015"] + +[[global]] +address = "0x53edd" +name = "selected_training_action_id" +category = "index-pointer" +type = "int" +value_domain = "TRINIT action id 0..20" +usage = "TRAIN's selected action row. It indexes all TRINIT eligibility, effect, text, award, and event arrays during detail rendering and execution." +source = "investigation" +confidence = "high" +depends_on = ["0x453b", "0x155bbc", "0x156015"] + +[[global]] +address = "0x53ede" +name = "training_action_unlock_flags" +category = "data-table" +type = "int[21]" +value_domain = "0/1 by TRINIT action id" +usage = "Persistent per-action discovered/unlocked state maintained by TRAIN after evaluating eligibility. TRAIN uses it while choosing the locked-versus-known menu presentation, GAMESTART restores every cell, and SAVE persists the same block." +source = "investigation" +confidence = "high" +depends_on = ["0x53edd"]