diff --git a/docs/global-reference.md b/docs/global-reference.md index 5085e8f..49f58d0 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5046 globals (293 curated, 4753 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5046 globals (298 curated, 4748 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -71,6 +71,11 @@ | `0x5f0ed` | scene_decision_seen_flags | high | investigation | Persistent seen-state for progression decisions. Numbered scene scripts set the current scjump_decision_out cell after playback and synchronize it through the persistence opcode; INFOVO uses VIINIT's one-based prerequisites to unlock glossary topics. | | `0x624bf` | unit_voice_family_unit_ids | high | investigation | CNINIT's sparse unit-id keyed voice-family normalization map. Story, history, field, and battle voice paths map the current unit variant through this table before selecting the family-level voice-suppression state; 175 of 277 authored rows alias a variant to another representative unit id. | | `0x628a7` | unit_voice_suppression_flag_ids | high | investigation | Second stage of the shared voice-eligibility chain. Story, history, field, and battle paths normalize the current unit through unit_voice_family_unit_ids, index this table by that representative unit id, and suppress voice playback when the selected character_voice_suppressed cell is nonzero. | +| `0x62cd1` | gallery_image_assets | high | investigation | CGINIT's sparse gallery asset rows. CGMODE tests and displays column 0 as the full-size gallery image. SAVE and SELSTAGE scan column 0 for the current image and, when column 1 is populated, load that 112x84 preview instead of capturing the current screen. Columns: 0=gallery_image_asset_id, 1=save_stage_preview_asset_id. | +| `0x63c71` | gallery_thumbnail_sheet_ids | high | investigation | CGINIT's gallery thumbnail-atlas selector. CGMODE subtracts one, remaps the configured sheet through gallery_thumbnail_sheet_asset_ids, and groups every populated gallery record under that sheet. | +| `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. | | `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. | @@ -284,12 +289,8 @@ | `0x624a3` | — | low | auto-shape | array | | `0x62c8f` | — | low | auto-shape | array | | `0x62cad` | — | low | auto-shape | array | -| `0x63c71` | — | low | auto-shape | array | -| `0x64441` | — | low | auto-shape | array | -| `0x64c11` | — | low | auto-shape | array | | `0x653e1` | — | low | auto-shape | array | | `0x65bb1` | — | low | auto-shape | array | -| `0x66381` | — | low | auto-shape | array | | `0x66421` | — | low | auto-shape | array | | `0x6642d` | — | low | auto-shape | array | | `0x66442` | — | low | auto-shape | array | @@ -3727,7 +3728,6 @@ | `0x62436` | — | low | auto-shape | index/counter? | | `0x62450` | — | low | auto-shape | index/counter? | | `0x62ccb` | — | low | auto-shape | index/counter? | -| `0x62cd1` | — | med | auto-shape | record-table[stride 2] | | `0x62cd3` | — | low | auto-shape | cg-gallery-field? | | `0x62cd5` | — | low | auto-shape | cg-gallery-field? | | `0x62cd7` | — | low | auto-shape | cg-gallery-field? | diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 461dd3d..5814792 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -2855,3 +2855,30 @@ and the generated profile has collapsed from 242 pseudo-fields to the two true c **Next:** audit CGINIT's large numeric output. Determine whether its apparent 1,304-field surface is a legitimate gallery/resource matrix or another ownership artifact before assigning any new semantics. + +## Data-semantics sidebar: CGINIT gallery registry (2026-07-23) + +CGINIT's former 379-record/1,304-field numeric output was an ownership error. The script contains 3,941 +static writes forming 851 sparse gallery-image rows, ids 1..855 with four gaps, inside a reserved +2,000-row layout. Every row has a full-size gallery image, a thumbnail sheet id, a thumbnail slot id, +and a variant ordinal; 537 rows additionally carry the optional preview used by SAVE and SELSTAGE. +The dedicated schema classifies every write and reduces the generated profile to these five real fields. + +CGMODE proves the presentation model. INIT2 configures four sheets with `SO026A.AGF` through +`SO026D.AGF`; each resource is a 6-by-5 atlas of thirty 126-by-95 thumbnail cells. CGMODE compacts the +enabled sheets, groups gallery records by their one-based sheet and slot, and orders the full-size images +behind a slot by the one-based variant ordinal (1..23 in shipped data). It tests each primary image's +unlock state, reports unlocked and total variants per thumbnail, and displays the selected full-size +asset. SAVE and SELSTAGE independently scan the same primary image column and use the optional second +asset when it is a 112-by-84 preview, otherwise falling back to a captured frame. + +The generated rows join all primary, preview, and sheet asset ids to SYS4INI-derived filenames while +retaining the raw `0x62cd1/2/{0,1}`, `0x63c71`, `0x64441`, and `0x64c11` keys. The curated global +registry names those arrays plus CGMODE's ten-cell configured sheet-resource vector. Regressions cover +the complete write accounting, sparse id range, four atlases, optional previews, representative image +joins, and raw-to-semantic field projection. + +**Next:** profile the remaining generic/footer INIT outputs by evidence density. MPINIT is structurally +regular but still semantically shallow; the smaller CTINIT/CVINIT/AFINIT families may offer faster +reader-proven joins. Rank them by malformed field count and direct-consumer coverage before choosing the +next dedicated schema. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index fccb166..9b08814 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -95,6 +95,13 @@ All 135 packed resource ids resolve to numbered SC scripts. The generated view p decision ids and joins every one of SCJUMP's 847 live decision ids; 844 final chapter tags agree, while three legacy/stale mismatches remain explicit. +CGINIT's generic numeric parse formerly reported 379 records and 1,304 address-derived fields; that was +another ownership artifact. Its 3,941 writes actually define 851 sparse gallery-image ids in a reserved +2,000-row layout: a full-size image asset, an optional 112-by-84 SAVE/SELSTAGE preview, one of four +`SO026A`--`SO026D` thumbnail sheets, one of the sheet's thirty atlas cells, and a one-based variant +ordinal behind that thumbnail. CGMODE supplies the grouping and ordering evidence and consumes every +record; the dedicated extraction joins all asset ids to filenames while retaining the raw arrays. + 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 a14734e..74c9e9e 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. 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, `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/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. 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, and CNINIT unit-name/voice-family 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 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, and CGINIT gallery 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 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, @@ -72,6 +72,14 @@ ITINIT→ITMES, SKINIT→SKMES, VIINIT→VIMES, EBINIT→EIMES, CIINIT→CIMES, intentionally sparse across unrelated unit definitions; MAINIT intentionally lacks MAMES bodies for growth ritual ids 10 and 11. +Numeric-mode CGINIT has its own consumer-proven specialization rather than the generic stride-one +heuristic. It emits 851 sparse rows in the reserved 2,000-row layout, classifies all 3,941 integer +writes into one two-column image/preview table plus thumbnail sheet, 30-cell atlas slot, and variant +ordinal arrays, and joins the packed asset ids through `build/callscript-names.json`. The four thumbnail +sheets are read from INIT2's ten-cell CGMODE configuration and exposed as the 6-by-5 `SO026A.AGF` +through `SO026D.AGF` atlases. The optional preview column +is identified from SAVE/SELSTAGE's 112-by-84 consumer path; all raw addresses remain present. + ## VM | Tool | Purpose | Run | Reads → Writes | diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index 2baa1fa..8400d9f 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 (379 CG entries)** [numeric: index-keyed columns], **MPINIT (1472 map records)** [footer: length-prefixed arrays], **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], **MPINIT (1472 map records)** [footer: length-prefixed arrays], **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. diff --git a/tools/extract_init.py b/tools/extract_init.py index 836478f..3160631 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -579,6 +579,15 @@ CONDITION_STAT_COLUMNS = ( ) CONDITION_RESOURCE_COLUMNS = ("hp", "sp", "fs") +GALLERY_ASSET_TABLE_BASE = 0x62CD1 +GALLERY_RECORD_SPAN = 2000 +GALLERY_ASSET_STRIDE = 2 +GALLERY_THUMBNAIL_SHEET_ARRAY_BASE = 0x63C71 +GALLERY_THUMBNAIL_SLOT_ARRAY_BASE = 0x64441 +GALLERY_VARIANT_ORDINAL_ARRAY_BASE = 0x64C11 +GALLERY_THUMBNAIL_SHEET_CONFIG_BASE = 0x66381 +GALLERY_THUMBNAIL_SHEET_CONFIG_SPAN = 10 + RECOVER_CURRENT_ENTITY = 0x152616 RECOVER_EFFECTIVE_STATS = 0x4E11B RECOVER_CURRENT_RESOURCES = 0x4E085 @@ -1842,6 +1851,220 @@ def extract_numeric(scr): return records, {"primary_index_base": f"0x{base:x}", "record_span": n} +def extract_gallery_definitions(scr): + """Extract CGINIT's sparse gallery-image registry. + + CGINIT owns one 2,000-by-2 asset table and three parallel 2,000-cell + classification arrays. CGMODE uses the latter as a thumbnail-sheet, + 30-cell atlas slot, and per-slot variant ordinal; SAVE and SELSTAGE use + the optional second asset as a 112-by-84 preview of the first. + """ + records_by_id: dict[int, dict] = {} + static_write_count = 0 + classified_write_count = 0 + + def record_for(record_id: int) -> dict: + if not (1 <= record_id < GALLERY_RECORD_SPAN): + raise ValueError( + f"{scr.path.name}: gallery id {record_id} outside reserved span" + ) + return records_by_id.setdefault(record_id, { + "id": record_id, + "fields": {}, + "record_fields": {}, + }) + + for ins in scr.instructions: + write = _static_global_write(ins) + if write is None: + if sys4load.display_label(ins.opcode) != "exit": + raise ValueError( + f"{scr.path.name}: unclassified instruction at 0x{ins.offset:x}" + ) + continue + static_write_count += 1 + destination, value = write + if not isinstance(value, int): + raise ValueError( + f"{scr.path.name}: non-static gallery value at 0x{ins.offset:x}" + ) + + relative = destination - GALLERY_ASSET_TABLE_BASE + if 0 <= relative < GALLERY_RECORD_SPAN * GALLERY_ASSET_STRIDE: + record_id, column = divmod(relative, GALLERY_ASSET_STRIDE) + record = record_for(record_id) + _store_unique( + record["record_fields"], + ( + f"0x{GALLERY_ASSET_TABLE_BASE:x}/" + f"{GALLERY_ASSET_STRIDE}/{column}" + ), + value, + record_id, + ) + classified_write_count += 1 + continue + + scalar_arrays = ( + GALLERY_THUMBNAIL_SHEET_ARRAY_BASE, + GALLERY_THUMBNAIL_SLOT_ARRAY_BASE, + GALLERY_VARIANT_ORDINAL_ARRAY_BASE, + ) + for base in scalar_arrays: + record_id = destination - base + if 1 <= record_id < GALLERY_RECORD_SPAN: + _store_unique( + record_for(record_id)["fields"], + f"0x{base:x}", + value, + record_id, + ) + classified_write_count += 1 + break + else: + raise ValueError( + f"{scr.path.name}: unclassified gallery write " + f"0x{destination:x} at 0x{ins.offset:x}" + ) + + names = callscript_names() + sheet_asset_ids = gallery_thumbnail_sheet_assets() + records = [records_by_id[record_id] for record_id in sorted(records_by_id)] + required_scalar_keys = { + f"0x{GALLERY_THUMBNAIL_SHEET_ARRAY_BASE:x}", + f"0x{GALLERY_THUMBNAIL_SLOT_ARRAY_BASE:x}", + f"0x{GALLERY_VARIANT_ORDINAL_ARRAY_BASE:x}", + } + primary_key = ( + f"0x{GALLERY_ASSET_TABLE_BASE:x}/{GALLERY_ASSET_STRIDE}/0" + ) + preview_key = ( + f"0x{GALLERY_ASSET_TABLE_BASE:x}/{GALLERY_ASSET_STRIDE}/1" + ) + for record in records: + if set(record["fields"]) != required_scalar_keys: + raise ValueError( + f"{scr.path.name}: gallery id {record['id']} has incomplete scalars" + ) + if primary_key not in record["record_fields"]: + raise ValueError( + f"{scr.path.name}: gallery id {record['id']} has no image asset" + ) + sheet_id = record["fields"][ + f"0x{GALLERY_THUMBNAIL_SHEET_ARRAY_BASE:x}" + ] + slot_id = record["fields"][ + f"0x{GALLERY_THUMBNAIL_SLOT_ARRAY_BASE:x}" + ] + variant_ordinal = record["fields"][ + f"0x{GALLERY_VARIANT_ORDINAL_ARRAY_BASE:x}" + ] + if sheet_id not in sheet_asset_ids: + raise ValueError( + f"{scr.path.name}: gallery id {record['id']} has bad sheet {sheet_id}" + ) + if not (1 <= slot_id <= 30 and variant_ordinal >= 1): + raise ValueError( + f"{scr.path.name}: gallery id {record['id']} has bad " + f"slot/variant {slot_id}/{variant_ordinal}" + ) + image_asset_id = record["record_fields"][primary_key] + sheet_asset_id = sheet_asset_ids[sheet_id] + record.update({ + "gallery_image_asset_id": image_asset_id, + "gallery_image_asset_name": names.get(image_asset_id, ""), + "thumbnail_sheet_id": sheet_id, + "thumbnail_sheet_asset_id": sheet_asset_id, + "thumbnail_sheet_asset_name": names.get(sheet_asset_id, ""), + "thumbnail_slot_id": slot_id, + "variant_ordinal": variant_ordinal, + }) + if preview_asset_id := record["record_fields"].get(preview_key): + record["save_stage_preview_asset_id"] = preview_asset_id + record["save_stage_preview_asset_name"] = names.get( + preview_asset_id, "" + ) + + populated_ids = set(records_by_id) + populated_min = min(populated_ids) + populated_max = max(populated_ids) + sheet_definitions = [] + for sheet_id, asset_id in sheet_asset_ids.items(): + sheet_definitions.append({ + "id": sheet_id, + "asset_id": asset_id, + "asset_name": names.get(asset_id, ""), + "atlas_columns": 6, + "atlas_rows": 5, + "slot_count": 30, + }) + return records, { + "record_span": GALLERY_RECORD_SPAN, + "populated_id_range": [populated_min, populated_max], + "id_gaps_within_populated_range": [ + record_id + for record_id in range(populated_min, populated_max + 1) + if record_id not in populated_ids + ], + "asset_table_base": f"0x{GALLERY_ASSET_TABLE_BASE:x}", + "asset_table_stride": GALLERY_ASSET_STRIDE, + "thumbnail_sheet_array_base": ( + f"0x{GALLERY_THUMBNAIL_SHEET_ARRAY_BASE:x}" + ), + "thumbnail_slot_array_base": ( + f"0x{GALLERY_THUMBNAIL_SLOT_ARRAY_BASE:x}" + ), + "variant_ordinal_array_base": ( + f"0x{GALLERY_VARIANT_ORDINAL_ARRAY_BASE:x}" + ), + "record_field_columns": [primary_key, preview_key], + "static_write_count": static_write_count, + "classified_static_write_count": classified_write_count, + "preview_asset_count": sum( + preview_key in record["record_fields"] for record in records + ), + "thumbnail_sheets": sheet_definitions, + "thumbnail_sheet_configuration": { + "source": "INIT2.BIN", + "base": f"0x{GALLERY_THUMBNAIL_SHEET_CONFIG_BASE:x}", + "reserved_span": GALLERY_THUMBNAIL_SHEET_CONFIG_SPAN, + }, + "consumer_contract": { + "gallery": ( + "CGMODE groups records by thumbnail sheet and one of its " + "thirty atlas slots, orders variants by the one-based ordinal, " + "tests the primary image's unlock state, and displays it." + ), + "save_stage_preview": ( + "SAVE and SELSTAGE match the current image against the primary " + "asset and use the optional second asset as a 112x84 preview." + ), + }, + } + + +@cache +def gallery_thumbnail_sheet_assets() -> dict[int, int]: + """Read CGMODE's enabled thumbnail-sheet assets from INIT2.""" + script = sys4load.load(resolve("INIT2")) + assets = {} + for ins in script.instructions: + write = _static_global_write(ins) + if write is None: + continue + destination, value = write + index = destination - GALLERY_THUMBNAIL_SHEET_CONFIG_BASE + if ( + 0 <= index < GALLERY_THUMBNAIL_SHEET_CONFIG_SPAN + and isinstance(value, int) + and value + ): + assets[index + 1] = value + if not assets: + raise ValueError("INIT2.BIN: no configured CGMODE thumbnail sheets") + return assets + + @cache def callscript_names() -> dict[int, str]: """Load the generated packed script-resource id join.""" @@ -2543,6 +2766,11 @@ def write_data_index(data_dir: Path) -> None: "Every row joins both its own EBINIT definition and the representative voice-family", "definition; deliberately empty names and variant aliases remain explicit.", "", + "CGINIT's dedicated gallery schema exposes 851 sparse ids in a reserved 2,000-row", + "layout. Each row joins its full-size image asset, one of four 30-cell thumbnail", + "atlases, its atlas slot and variant ordinal, and the optional 112-by-84 preview", + "used by SAVE and SELSTAGE. Raw global-array provenance remains beside these joins.", + "", "Mixed-mode tables preserve the sparse selector id, branch offset, condition strings,", "scalar fields, cells within preallocated buffers, and length-prefixed footer arrays.", "STINIT additionally joins confirmed parallel buffers into per-slot `object_placements`", @@ -2618,6 +2846,8 @@ def main() -> int: extractor = extract_magic_actions elif mode == "name" and name == "ILINIT": extractor = extract_condition_definitions + elif mode == "numeric" and name == "CGINIT": + extractor = extract_gallery_definitions recs, meta = extractor(scr) if mode == "name" and name in MESSAGE_TABLES: message_name = MESSAGE_TABLES[name] diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 78355ce..4d80b52 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -970,6 +970,70 @@ def test_character_names() -> None: ) +def test_gallery_definitions() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["CGINIT.BIN"]) + check( + extract_init.detect_mode(script) == "numeric", + "CGINIT remains compatible with numeric-mode auto-detection", + ) + records, meta = extract_init.extract_gallery_definitions(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 851 + and meta["record_span"] == 2000 + and meta["populated_id_range"] == [1, 855] + and meta["id_gaps_within_populated_range"] == [205, 585, 603, 604], + "CGINIT extracts 851 sparse gallery rows from its reserved 2,000 ids", + ) + check( + meta["static_write_count"] == 3941 + and meta["classified_static_write_count"] == 3941 + and meta["preview_asset_count"] == 537, + "CGINIT classifies every integer write and all optional previews", + ) + check( + [sheet["asset_name"] for sheet in meta["thumbnail_sheets"]] + == ["SO026A.AGF", "SO026B.AGF", "SO026C.AGF", "SO026D.AGF"] + and all(sheet["slot_count"] == 30 for sheet in meta["thumbnail_sheets"]), + "CGINIT joins the four configured 6x5 thumbnail atlases", + ) + check( + by_id[1]["gallery_image_asset_id"] == 5407 + and by_id[1]["gallery_image_asset_name"] == "EV001AA.AGF" + and by_id[1]["thumbnail_sheet_id"] == 1 + and by_id[1]["thumbnail_slot_id"] == 1 + and by_id[1]["variant_ordinal"] == 3, + "CGINIT row 1 joins its full image and thumbnail placement", + ) + check( + by_id[4]["save_stage_preview_asset_id"] == 11266 + and by_id[4]["save_stage_preview_asset_name"] == "EVM001BA.AGF" + and "save_stage_preview_asset_id" not in by_id[1], + "CGINIT preserves optional SAVE/SELSTAGE preview assets", + ) + + semantics = extract_init.field_semantics(records) + check( + semantics == { + "0x62cd1/2/0": "gallery_image_assets.gallery_image_asset_id", + "0x62cd1/2/1": "gallery_image_assets.save_stage_preview_asset_id", + "0x63c71": "gallery_thumbnail_sheet_ids", + "0x64441": "gallery_thumbnail_slot_ids", + "0x64c11": "gallery_variant_ordinals", + }, + "CGINIT raw arrays join to their canonical semantic names", + ) + extract_init.attach_semantic_fields(records, semantics) + check( + by_id[4]["semantic_fields"][ + "gallery_image_assets.save_stage_preview_asset_id" + ] == 11266 + and by_id[4]["semantic_fields"]["gallery_thumbnail_slot_ids"] == 1, + "CGINIT retains raw addresses beside one semantic field view", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1179,6 +1243,7 @@ if __name__ == "__main__": test_message_infrastructure() test_message_join() test_character_names() + test_gallery_definitions() test_condition_definitions() test_field_semantics() if FAILS: diff --git a/vm-map/globals.toml b/vm-map/globals.toml index a81ab08..18589ff 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -1409,6 +1409,62 @@ source = "investigation" confidence = "high" depends_on = ["0x2e49"] +[[global]] +address = "0x62cd1" +name = "gallery_image_assets" +category = "data-table" +type = "int[2000][2]" +columns = { "0" = "gallery_image_asset_id", "1" = "save_stage_preview_asset_id" } +value_domain = "packed asset resource ids; preview zero/unwritten means capture the current screen" +usage = "CGINIT's sparse gallery asset rows. CGMODE tests and displays column 0 as the full-size gallery image. SAVE and SELSTAGE scan column 0 for the current image and, when column 1 is populated, load that 112x84 preview instead of capturing the current screen." +source = "investigation" +confidence = "high" +depends_on = ["0x63c71", "0x64441", "0x64c11"] + +[[global]] +address = "0x63c71" +name = "gallery_thumbnail_sheet_ids" +category = "data-table" +type = "int[2000]" +value_domain = "one-based thumbnail-sheet id 1..4" +usage = "CGINIT's gallery thumbnail-atlas selector. CGMODE subtracts one, remaps the configured sheet through gallery_thumbnail_sheet_asset_ids, and groups every populated gallery record under that sheet." +source = "investigation" +confidence = "high" +depends_on = ["0x66381", "0x64441"] + +[[global]] +address = "0x64441" +name = "gallery_thumbnail_slot_ids" +category = "data-table" +type = "int[2000]" +value_domain = "one-based thumbnail-atlas slot 1..30" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x63c71", "0x64c11"] + +[[global]] +address = "0x64c11" +name = "gallery_variant_ordinals" +category = "data-table" +type = "int[2000]" +value_domain = "one-based variant ordinal 1..23; zero/unwritten means no gallery record" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x62cd1", "0x63c71", "0x64441"] + +[[global]] +address = "0x66381" +name = "gallery_thumbnail_sheet_asset_ids" +category = "data-table" +type = "int[10]" +value_domain = "packed thumbnail-atlas asset resource id; zero disables a sheet" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x63c71"] + [[global]] address = "0x52383" name = "entity_condition_levels"