From 9d5cc80ccef0b68e1880c2bd4a6bc1e892ab7e89 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 21:57:32 -0400 Subject: [PATCH] Decode SPINIT H-scene gallery --- docs/global-reference.md | 6 +- docs/name-resolution.md | 6 +- docs/phase-a-slice-plan.md | 25 +++++ docs/script-inventory.md | 6 ++ docs/tools-reference.md | 5 + docs/vm-mapping-plan.md | 5 +- tools/extract_init.py | 160 +++++++++++++++++++++++++++++++ tools/init_table_profile.py | 37 ++++++- tools/test_extract_init.py | 50 ++++++++++ tools/test_init_table_profile.py | 22 +++++ vm-map/globals.toml | 22 +++++ 11 files changed, 336 insertions(+), 8 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 0c3f3ea..64c10bc 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5049 globals (333 curated, 4716 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5049 globals (335 curated, 4714 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -87,6 +87,8 @@ | `0x64441` | gallery_thumbnail_slot_ids | high | investigation | CGINIT's slot within the selected 6x5 thumbnail atlas. CGMODE subtracts one, draws that one of thirty 126x95 cells, and reports the unlocked/total variant counts associated with the slot. | | `0x64c11` | gallery_variant_ordinals | high | investigation | CGINIT's ordering key for multiple full-size images behind one thumbnail slot. CGMODE subtracts one and stores the gallery record id at sheet[slot*100 + ordinal], then walks that ordered variant list in the image viewer. | | `0x66381` | gallery_thumbnail_sheet_asset_ids | high | investigation | CGMODE's configured thumbnail-sheet resources. INIT2 populates the first four cells with SO026A.AGF through SO026D.AGF and leaves the remaining six reserved; CGMODE compacts nonzero cells into its visible sheet list before applying gallery_thumbnail_sheet_ids. | +| `0x6638b` | h_scene_gallery_script_ids | high | investigation | SPINIT's H-scene gallery registry. HMODE compacts the eight configured thumbnail pages, scans each page's fifteen script slots, filters populated resources through opcode 0x19d, and call-scripts the selected available entry. Columns: 0=slot_0, 1=slot_1, 2=slot_2, 3=slot_3, 4=slot_4, 5=slot_5, 6=slot_6, 7=slot_7, 8=slot_8, 9=slot_9, 10=slot_10, 11=slot_11, 12=slot_12, 13=slot_13, 14=slot_14. | +| `0x66421` | h_scene_gallery_thumbnail_sheet_assets | high | investigation | INIT2's eight HMODE thumbnail-page assets. HMODE compacts nonzero configured pages and pairs each page index with the corresponding row of h_scene_gallery_script_ids. | | `0x665e3` | modal_annotation_horizontal_cells | high | investigation | Per-annotation horizontal anchor input shared by MES and SBUNKI. Each renderer multiplies the cell by 21 pixels and offsets it by half the annotation's rendered width. | | `0x66647` | modal_annotation_row_offsets | high | investigation | Per-annotation vertical row offset shared by MES and SBUNKI. Renderers multiply it by 30 pixels; a negative value additionally shifts the annotation left by one 21-pixel cell. | | `0x66716` | unit_voice_asset_ids | high | investigation | EBINIT per-unit voice bank for 116 voiced characters and variants. FIELD directly selects column 0 before WARPD, column 1 when an acting unit takes chest/treasure contents, and column 3 after occupying, losing, or sealing a stage objective. BTL selects the acting unit's columns 7..9 for ordinary attacks, 10 for an ordinary critical, odd columns 11/13/15 for skill uses, and even columns 12/14/16 for critical skills; the three variants follow its six-step exchange selector. If the target survives positive damage, BTL selects target columns 19/20/21 with 60/30/10 percent weights; column 22 belongs to a target reduced to zero HP, while actor column 23 is selected for a finishing blow. SHOWGROW selects column 24 after level gain. Populated columns 4..6 and 17..18 have no reachable selector in the shipped script corpus and are retained as explicit unused authoring slots; columns 17 and 18 duplicate columns 15 and 16 in all 116 populated rows. Columns: 0=warp, 1=treasure_capture, 3=objective_interaction, 4=unused_slot_4, 5=unused_slot_5, 6=unused_slot_6, 7=normal_attack_1, 8=normal_attack_2, 9=normal_attack_3, 10=critical_normal_attack, 11=skill_use_1, 12=critical_skill_1, 13=skill_use_2, 14=critical_skill_2, 15=skill_use_3, 16=critical_skill_3, 17=unused_slot_17, 18=unused_slot_18, 19=damage_reaction_1, 20=damage_reaction_2, 21=damage_reaction_3, 22=defeated, 23=finishing_blow, 24=level_up. | @@ -314,7 +316,6 @@ | `0x624a3` | — | low | auto-shape | array | | `0x653e1` | — | low | auto-shape | array | | `0x65bb1` | — | low | auto-shape | array | -| `0x66421` | — | low | auto-shape | array | | `0x6642d` | — | low | auto-shape | array | | `0x66442` | — | low | auto-shape | array | | `0x6650a` | — | low | auto-shape | array | @@ -5044,7 +5045,6 @@ | `0x64cdc` | — | low | auto-shape | cg-gallery-field? | | `0x64cdd` | — | low | auto-shape | cg-gallery-field? | | `0x64cde` | — | med | auto-shape | cg-gallery-field | -| `0x6638b` | — | med | auto-shape | record-table[stride 15] | | `0x81c96` | — | med | auto-shape | record-table[stride 3] | | `0x8284e` | — | med | auto-shape | record-table[stride 3] | | `0xc6077` | — | med | auto-shape | record-table[stride 27] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index a1dad18..df5c28d 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -682,8 +682,10 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape doubled-coordinate terrain atlas and joins 66 STINIT2 stage definitions to 53 unique rectangles, 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. - With RTINIT's used-provider surface closed and ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and - INFOMES/MES classified, audit SPINIT's stride-15 HMODE resource matrix next; + 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; 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 7167c98..1464a9a 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3017,3 +3017,28 @@ population, all thirteen combat-stat cells, all five SKINIT skill joins, and all **Next:** audit SPINIT's 118 populated cells as the stride-15 HMODE resource matrix; the generic numeric view currently fragments its eight logical rows into individual address-derived records. + +## Data-semantics sidebar: SPINIT H-scene gallery (2026-07-23) + +SPINIT's 118 numeric writes are a single `8 × 15` row-major registry at `0x6638b`, not 118 +independent records. Each row is one HMODE gallery page and each column is one thumbnail slot. The +first seven rows are full; the eighth authors slots 0..12 and leaves slots 13..14 as meaningful +zero/default cells. The dedicated schema classifies all 119 instructions including `exit` and +preserves both complete rows and authored raw-cell provenance. + +INIT2 supplies the parallel eight-page thumbnail-sheet registry at `0x66421`. All eight assets +resolve through SYS4INI as `SO027A.AGF` through `SO027H.AGF`; visual conversion confirms each is a +fifteen-thumbnail page matching SPINIT's row geometry. Every one of SPINIT's 118 nonzero values +resolves to an `SP*.BIN` script resource. + +HMODE compacts the configured thumbnail sheets into its visible page list, iterates all fifteen +SPINIT slots for each page, passes every populated resource through still-unnamed opcode `0x19d`, +and marks rejected cells unavailable. Selecting an available thumbnail retrieves the same matrix +cell and executes it with indirect `call-script`. This proves the page, slot, resource, and dispatch +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 compact mixed string/numeric INIT surface. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 58c0245..daca325 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -136,6 +136,12 @@ proves the stat ABI, MVSEEK/FIELD prove five SKINIT traversal requirements, and shared `MP000*.AGF` fallback assets to the stage texture slots. The dedicated view accounts for all 98 instructions and preserves ids 0, 5, and 6 as meaningful implicit-default rows. +SPINIT is HMODE's `8 × 15` H-scene gallery registry. Its 118 populated resource ids fill seven +complete pages and thirteen cells of the final page; the final two slots remain zero. INIT2 provides +the matching `SO027A.AGF` through `SO027H.AGF` thumbnail sheets, and all populated cells resolve to +`SP*.BIN` resources. HMODE filters cells through opcode `0x19d` and indirectly call-scripts the +selected available entry. + 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 83e8608..a7d9a5a 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -104,6 +104,11 @@ terrain arrays, the signed ten-column combat-stat matrix, and three implicit-def Required skill ids join to SKINIT names; shared texture-slot fallbacks join to SYS4INI AGF filenames. The profile summarizes those populations and the CALCBTPARAM/MVSEEK/FIELD/INFOAF/DRAWMAP contract. +Numeric-mode SPINIT emits eight complete fifteen-slot HMODE gallery pages instead of 118 fragmented +address records. It joins the page rows to INIT2's `SO027A.AGF` through `SO027H.AGF` thumbnail sheets, +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. + 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 29db023..203b7fa 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], **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], **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. @@ -63,8 +63,9 @@ - [x] **2.3f — Extract `CVINIT` character-voice settings.** Numeric specialization accounts for all 37 static writes as thirteen preview OGG assets, twelve CONFIG-setting-slot-to-EBINIT-unit joins, and the exact inverse unit-to-suppression-setting map. CONFIG proves preview, row-label, persisted speaker-seen unlock, and setting-edit behavior; the shared CNINIT/CVINIT voice chain proves the inverse map. - [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.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, and MPINIT stage-terrain atlas are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, decode SPINIT's 118 populated cells as HMODE's stride-15 resource matrix, 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, 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. ## 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 b6042ec..56b9ed1 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -672,6 +672,11 @@ MAP_TEXTURE_SLOT_COUNT = 20 TERRAIN_DEFINITION_SPAN = 30 TERRAIN_SHIPPED_ID_MAX = 19 +H_SCENE_GALLERY_SCRIPT_BASE = 0x6638B +H_SCENE_GALLERY_PAGE_COUNT = 8 +H_SCENE_GALLERY_SLOTS_PER_PAGE = 15 +H_SCENE_GALLERY_THUMBNAIL_BASE = 0x66421 + def resolve(name: str) -> Path: for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"): @@ -3630,6 +3635,155 @@ def _terrain_definitions(max_terrain_id: int) -> list[dict]: ] +def extract_h_scene_gallery(scr): + """Extract SPINIT's eight-page, fifteen-slot HMODE script registry.""" + values: dict[tuple[int, int], int] = {} + classified_offsets = set() + static_write_count = 0 + for ins in scr.instructions: + 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 H-gallery value " + f"at 0x{ins.offset:x}" + ) + index = destination - H_SCENE_GALLERY_SCRIPT_BASE + capacity = ( + H_SCENE_GALLERY_PAGE_COUNT + * H_SCENE_GALLERY_SLOTS_PER_PAGE + ) + if not 0 <= index < capacity: + raise ValueError( + f"{scr.path.name}: H-gallery write 0x{destination:x} " + f"outside the {capacity}-cell registry" + ) + page, slot = divmod(index, H_SCENE_GALLERY_SLOTS_PER_PAGE) + _store_unique(values, (page, slot), value, page) + classified_offsets.add(ins.offset) + + 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)}" + ) + + init_scr = sys4load.load(resolve("INIT2")) + thumbnail_assets = {} + for ins in init_scr.instructions: + write = _static_global_write(ins) + if write is None: + continue + destination, value = write + page = destination - H_SCENE_GALLERY_THUMBNAIL_BASE + if 0 <= page < H_SCENE_GALLERY_PAGE_COUNT: + _store_unique(thumbnail_assets, page, value, page) + expected_pages = set(range(H_SCENE_GALLERY_PAGE_COUNT)) + if set(thumbnail_assets) != expected_pages: + raise ValueError( + f"INIT2.BIN: H-gallery thumbnail pages are " + f"{sorted(thumbnail_assets)}, expected " + f"0..{H_SCENE_GALLERY_PAGE_COUNT - 1}" + ) + + names = callscript_names() + table_key = f"0x{H_SCENE_GALLERY_SCRIPT_BASE:x}" + records = [] + empty_cells = [] + for page in range(H_SCENE_GALLERY_PAGE_COUNT): + script_ids = [] + scenes = [] + raw_fields = {} + for slot in range(H_SCENE_GALLERY_SLOTS_PER_PAGE): + script_id = values.get((page, slot), 0) + script_ids.append(script_id) + if not script_id: + empty_cells.append({"page": page, "slot": slot}) + continue + script_name = names.get(script_id, "") + scenes.append({ + "slot": slot, + "script_resource_id": script_id, + "script_name": script_name, + }) + raw_fields[ + f"{table_key}/{H_SCENE_GALLERY_SLOTS_PER_PAGE}/{slot}" + ] = script_id + thumbnail_asset_id = thumbnail_assets[page] + records.append({ + "id": page, + "name": f"page_{page}", + "thumbnail_sheet_asset_id": thumbnail_asset_id, + "thumbnail_sheet_asset_name": names.get( + thumbnail_asset_id, "" + ), + "script_resource_ids": script_ids, + "scenes": scenes, + "record_fields": raw_fields, + }) + + return records, { + "schema": "h-scene-gallery-pages", + "page_count": H_SCENE_GALLERY_PAGE_COUNT, + "slots_per_page": H_SCENE_GALLERY_SLOTS_PER_PAGE, + "registry_capacity": ( + H_SCENE_GALLERY_PAGE_COUNT + * H_SCENE_GALLERY_SLOTS_PER_PAGE + ), + "populated_scene_count": len(values), + "empty_cells": empty_cells, + "script_registry_base": table_key, + "thumbnail_sheet_array_base": ( + f"0x{H_SCENE_GALLERY_THUMBNAIL_BASE:x}" + ), + "thumbnail_sheet_source": "INIT2.BIN", + "array_layouts": { + table_key: {"stride": H_SCENE_GALLERY_SLOTS_PER_PAGE}, + }, + "schema_field_semantics": { + table_key: "h_scene_gallery_script_ids", + }, + "static_write_count": static_write_count, + "classified_static_write_count": len(values), + "classified_instruction_count": len(classified_offsets), + "resolved_scene_script_count": sum( + bool(scene["script_name"]) + for record in records + for scene in record["scenes"] + ), + "resolved_thumbnail_sheet_count": sum( + bool(record["thumbnail_sheet_asset_name"]) + for record in records + ), + "consumer_contract": { + "HMODE.BIN": ( + "compact the eight configured INIT2 thumbnail pages, scan " + "their fifteen SPINIT script slots, filter each populated " + "resource through opcode 0x19d, and call-script the selected " + "available scene" + ), + }, + } + + def _map_stage_definitions() -> list[dict]: """Read the STINIT2 records that own all four terrain-atlas bounds.""" stage_scr = sys4load.load(resolve("STINIT2")) @@ -4241,6 +4395,10 @@ def write_data_index(data_dir: Path) -> None: "effect descriptions, four parallel topology/rendering arrays, the ten-column", "combat-stat matrix, SKINIT traversal-skill joins, and shared texture fallbacks.", "", + "SPINIT's dedicated H-scene gallery schema exposes eight fifteen-slot pages,", + "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.", + "", "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", @@ -4333,6 +4491,8 @@ def main() -> int: extractor = extract_voice_configuration elif mode == "name" and name == "LAINIT": extractor = extract_terrain_definitions + elif mode == "numeric" and name == "SPINIT": + extractor = extract_h_scene_gallery elif mode == "footer" and name == "MPINIT": extractor = extract_map_terrain_atlas recs, meta = extractor(scr) diff --git a/tools/init_table_profile.py b/tools/init_table_profile.py index 6d07fb0..19de5d1 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -346,6 +346,25 @@ def profile_terrain_definitions(data: dict) -> dict: } +def profile_h_scene_gallery(data: dict) -> dict: + """Summarize SPINIT's HMODE page/slot script registry.""" + if data.get("schema") != "h-scene-gallery-pages": + return {} + return { + "page_count": data.get("page_count", 0), + "slots_per_page": data.get("slots_per_page", 0), + "registry_capacity": data.get("registry_capacity", 0), + "populated_scene_count": data.get("populated_scene_count", 0), + "empty_cells": data.get("empty_cells", []), + "resolved_scene_script_count": data.get( + "resolved_scene_script_count", 0 + ), + "resolved_thumbnail_sheet_count": data.get( + "resolved_thumbnail_sheet_count", 0 + ), + } + + def profile_messages(data: dict) -> dict: """Summarize the joined player-facing message evidence.""" records = data["records"] @@ -473,7 +492,22 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str: f"- records: {data['record_count']}", f"- populated fields: {len(rows)}", ] - if terrain_profile := profile_terrain_definitions(data): + if 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", + f"- populated scenes: " + f"{h_gallery_profile['populated_scene_count']}/" + f"{h_gallery_profile['registry_capacity']}", + f"- resolved scripts: " + f"{h_gallery_profile['resolved_scene_script_count']}/" + f"{h_gallery_profile['populated_scene_count']}", + f"- resolved thumbnail sheets: " + f"{h_gallery_profile['resolved_thumbnail_sheet_count']}/" + f"{h_gallery_profile['page_count']}", + f"- empty cells: {h_gallery_profile['empty_cells']}", + ]) + elif terrain_profile := profile_terrain_definitions(data): lines.extend([ f"- shipped terrain ids: " f"{terrain_profile['shipped_record_count']} inside a " @@ -632,6 +666,7 @@ def main() -> int: "banked_profile": profile_banked(data), "map_atlas_profile": profile_map_atlas(data), "terrain_definition_profile": profile_terrain_definitions(data), + "h_scene_gallery_profile": profile_h_scene_gallery(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 dac53bf..783fbf5 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1523,6 +1523,55 @@ def test_terrain_definitions() -> None: ) +def test_h_scene_gallery() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["SPINIT.BIN"]) + check( + extract_init.detect_mode(script) == "numeric", + "SPINIT remains compatible with numeric-mode auto-detection", + ) + records, meta = extract_init.extract_h_scene_gallery(script) + check( + len(records) == 8 + and meta["page_count"] == 8 + and meta["slots_per_page"] == 15 + and meta["registry_capacity"] == 120, + "SPINIT exposes HMODE's eight-by-fifteen page geometry", + ) + check( + meta["static_write_count"] == 118 + and meta["classified_static_write_count"] == 118 + and meta["classified_instruction_count"] == 119 + and meta["empty_cells"] + == [{"page": 7, "slot": 13}, {"page": 7, "slot": 14}], + "SPINIT classifies every scene cell and the two implicit empty slots", + ) + check( + records[0]["thumbnail_sheet_asset_name"] == "SO027A.AGF" + and records[7]["thumbnail_sheet_asset_name"] == "SO027H.AGF" + and meta["resolved_thumbnail_sheet_count"] == 8, + "SPINIT pages join to all eight INIT2 HMODE thumbnail sheets", + ) + check( + records[0]["scenes"][0] + == { + "slot": 0, + "script_resource_id": 0x151E, + "script_name": "SP0800.BIN", + } + and records[2]["scenes"][3]["script_name"] == "SP1200.BIN" + and records[7]["scenes"][-1]["script_name"] == "SP0179.BIN" + and meta["resolved_scene_script_count"] == 118, + "SPINIT resolves every populated cell to its call-script resource", + ) + check( + len(records[7]["script_resource_ids"]) == 15 + and records[7]["script_resource_ids"][-2:] == [0, 0] + and len(records[7]["record_fields"]) == 13, + "SPINIT preserves complete rows beside authored raw-cell provenance", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1738,6 +1787,7 @@ if __name__ == "__main__": test_name_entry_palette() test_voice_configuration() test_terrain_definitions() + test_h_scene_gallery() test_map_terrain_atlas() test_condition_definitions() test_field_semantics() diff --git a/tools/test_init_table_profile.py b/tools/test_init_table_profile.py index 8b4c3d9..81b41c0 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -245,6 +245,28 @@ def main() -> int: assert "- shipped terrain ids: 20 inside a 30-row table" in rendered_terrain assert "- combat-stat cells: 13" in rendered_terrain + h_gallery_fixture = { + "table": "SP", + "mode": "numeric", + "schema": "h-scene-gallery-pages", + "record_count": 8, + "page_count": 8, + "slots_per_page": 15, + "registry_capacity": 120, + "populated_scene_count": 118, + "empty_cells": [{"page": 7, "slot": 13}, {"page": 7, "slot": 14}], + "resolved_scene_script_count": 118, + "resolved_thumbnail_sheet_count": 8, + "records": [], + } + h_gallery_summary = profile.profile_h_scene_gallery(h_gallery_fixture) + assert h_gallery_summary["page_count"] == 8 + assert h_gallery_summary["populated_scene_count"] == 118 + assert h_gallery_summary["resolved_thumbnail_sheet_count"] == 8 + rendered_h_gallery = profile.render_markdown(h_gallery_fixture, [], 40) + assert "- geometry: 8 pages × 15 slots" in rendered_h_gallery + assert "- populated scenes: 118/120" in rendered_h_gallery + 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 45e55a4..a348624 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -3087,6 +3087,28 @@ usage = "LAINIT's terrain vocabulary. MPINIT's atlas cells contain terrain ids 0 source = "investigation" confidence = "high" +[[global]] +address = "0x6638b" +name = "h_scene_gallery_script_ids" +category = "data-table" +type = "int[8][15]" +columns = { "0" = "slot_0", "1" = "slot_1", "2" = "slot_2", "3" = "slot_3", "4" = "slot_4", "5" = "slot_5", "6" = "slot_6", "7" = "slot_7", "8" = "slot_8", "9" = "slot_9", "10" = "slot_10", "11" = "slot_11", "12" = "slot_12", "13" = "slot_13", "14" = "slot_14" } +value_domain = "SYS4INI SP*.BIN resource id; 118 populated cells and two trailing zero cells" +usage = "SPINIT's H-scene gallery registry. HMODE compacts the eight configured thumbnail pages, scans each page's fifteen script slots, filters populated resources through opcode 0x19d, and call-scripts the selected available entry." +source = "investigation" +confidence = "high" +depends_on = ["0x66421"] + +[[global]] +address = "0x66421" +name = "h_scene_gallery_thumbnail_sheet_assets" +category = "data-table" +type = "int[8]" +value_domain = "SYS4INI resources SO027A.AGF through SO027H.AGF" +usage = "INIT2's eight HMODE thumbnail-page assets. HMODE compacts nonzero configured pages and pairs each page index with the corresponding row of h_scene_gallery_script_ids." +source = "investigation" +confidence = "high" + [[global]] address = "0x26d3" name = "terrain_effect_descriptions"