diff --git a/docs/global-reference.md b/docs/global-reference.md index 624b67d..91b5e62 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5030 globals (185 curated, 4845 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5030 globals (187 curated, 4843 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -153,8 +153,6 @@ | `0x26a4` | — | low | auto-shape | array | | `0x26b5` | — | low | auto-shape | array | | `0x26d3` | — | low | auto-shape | array | -| `0x26f1` | — | low | auto-shape | array | -| `0x2755` | — | low | auto-shape | array | | `0x27bd` | — | low | auto-shape | array | | `0x3276` | — | low | auto-shape | array | | `0x328a` | — | low | auto-shape | array | @@ -425,6 +423,8 @@ | address | name | conf | source | usage | |---|---|---|---|---| +| `0x26f1` | object_type_names | high | investigation | OBINIT writes the authoritative object names. FIELD, SETOBJ, and DRAWOBJ use STINIT's object type id to select these definitions; extract_init joins the names to stage object placements. | +| `0x2755` | object_type_descriptions | high | investigation | OBINIT writes the short object descriptions displayed by the field object-information path. extract_init joins populated descriptions to STINIT object placements by type id. | | `0x27b9` | stage_victory_condition_1 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders this line first in the victory-condition section, and FIELD copies it into the current mission-condition display. | | `0x27ba` | stage_victory_condition_2 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders nonempty values after stage_victory_condition_1, and FIELD copies the slot into the current mission-condition display. | | `0x27bb` | stage_defeat_condition_1 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders this line first in the defeat-condition section, and FIELD copies it into the current mission-condition display. | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index d442fb2..a13090f 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -280,10 +280,21 @@ variants: - types 6 and 36: teleport `destination_tile_x` / `destination_tile_y`; - types 7 and 8: treasure `item_id` / `item_quantity`, passed to ADDITEM; - type 28: `card_generation_list_id`, passed to CDINIT. +- types 18--25: `non_triggering_faction_id` in the first cell when populated. FIELD suppresses the + hazard/barrier interaction when the entering unit's faction equals this value. This accounts for 220 initial-owner values, 229 teleport destinations, 626 treasure pairs, and 246 card -list ids. Another 185 placements across object types 11, 17--21, 25--27 retain one raw payload cell because -their type-specific meaning is not yet separable from the consumer behavior. +list ids. The faction-gate branch resolves another 104 cells on populated types 18--21 and 25. Type 17 +(`針`, spikes) is explicitly outside FIELD's faction comparison, so its 16 populated values remain raw +rather than borrowing the neighboring hazard meaning. In total, 81 placements across object types 11, +17, 26, and 27 still retain one raw payload cell because their type-specific meaning is not yet separable +from the consumer behavior. + +OBINIT is the authoritative object-definition table: 46 one-based records provide the type names, and 34 +provide short player-facing effect descriptions used by the field object-information path. The STINIT join now +adds `type_name` to every placement and `type_description` when populated while retaining `type_id`; +top-level `object_definition_table: "OBINIT"` records the join provenance. This is intentionally separate +from payload decoding: a known display label does not by itself establish the meaning of a tagged cell. The enemy pass follows the separate 30-cell family through FIELD, SETEN, ADDEN, MVRTN, and BTRTN. Slot zero is reserved for ADDEN's synthesized special-unit path; the stage table populates slots 1 through 29. @@ -301,9 +312,10 @@ set and the spawn's cell equals 2. The joined view exposes all 485 populated cas Generated INIT records now retain their raw `fields`/`record_fields`/buffer keys and additionally expose a flat `semantic_fields` projection joined through the top-level `field_semantics` map. For STINIT, the four confirmed parallel buffers plus both prerequisite tables are also assembled into 2,312 -`object_placements` across 66 stages. Each placement contains its slot, type, tile coordinates, difficulty -mask, populated positive/negative story prerequisites, optional reinforcement schedule, and the decoded -type-tagged payload variants above. The remaining 185 undecoded payload cells stay attached under +`object_placements` across 66 stages. Each placement contains its slot, numeric type plus OBINIT +name/available description, tile coordinates, difficulty mask, populated positive/negative story +prerequisites, optional reinforcement schedule, and the decoded type-tagged payload variants above. The +remaining 81 undecoded payload cells stay attached under `unknown_fields`, so this convenience view loses no evidence or invents names. The same records now contain 1,378 joined `enemy_spawns` across 66 stages, with unit/faction, direct or object-linked placement data when present, difficulty and story gates, level rules, random-selection @@ -357,8 +369,9 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape 1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, the confirmed EBINIT row layouts, and STINIT's mixed stage records now have machine-readable investigation surfaces, including joined object placements and enemy spawns. STINIT's universal object schedule, four consumer-proven tagged payload - variants, and first-clear enemy gate are decoded; next trace the 185 remaining raw object payloads only - where their type-specific runtime branches distinguish them. Isolate EBINIT's remaining voice/action + families, faction-gated hazards/barriers, OBINIT definition join, and first-clear enemy gate are decoded; + next trace the 81 remaining raw object payloads on types 11, 17, 26, and 27 only where their + type-specific runtime branches distinguish them. Isolate EBINIT's remaining voice/action slots under the same rule. Preserve explicit joins and do not infer meaning from column position alone. 2. **Extend message-table joins beyond the completed ITMES/SKMES pair** (`VIMES`, other id dispatchers, …) and fold in other `set-string`/`copy-to-global` writers not covered by the `*INIT` set. diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index 4f222d6..790de0a 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -753,10 +753,11 @@ and turn-limit outcome. Further passes establish target clear turns, performance the 20-slot map-texture override list, object type/tile/difficulty fields, and seven required plus five forbidden story prerequisites. Generated records now carry a joined `semantic_fields` view and assemble 2,312 object placements across 66 stages. FIELD resolves the universal reinforcement interval/limit and -type-tagged initial-faction, teleport, treasure, and card-list payloads; 185 payload cells on other object -types remain raw. The separate 30-cell enemy family is also decoded: FIELD/SETEN/ADDEN establish unit/faction, -direct or object-linked placement, difficulty/story gates, level floor/cap/scaling, weighted selection, -and three difficulty-specific movement/battle routine-set ids. The generated view assembles all 1,378 +type-tagged initial-faction, teleport, treasure, card-list, and non-triggering-faction payloads. OBINIT +supplies authoritative names for all 46 object types and descriptions for 34; 81 payload cells on types 11, +17, 26, and 27 remain raw. The separate 30-cell enemy family is also decoded: FIELD/SETEN/ADDEN establish +unit/faction, direct or object-linked placement, difficulty/story gates, level floor/cap/scaling, weighted +selection, and three difficulty-specific movement/battle routine-set ids. The generated view assembles all 1,378 populated enemy slots across 66 stages, including 485 enemies suppressed after the stage's first clear. Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks: @@ -767,7 +768,7 @@ Once the natural spine and first gameplay loop are trustworthy, broaden in indep - Unit statistics, equipment, inventory, skills, magic, heroine forms, and progression. - Combat resolution presentation, enemy turns/AI services, and win/loss transitions. - A full chapter of ADV and the scene types encountered between gameplay segments. -- Remaining STINIT tagged payloads on object types 11, 17--21, and 25--27, plus other data schemas when +- Remaining STINIT tagged payloads on object types 11, 17, 26, and 27, plus other data schemas when their runtime consumers make them necessary. The high-level Phase B direction remains canonical in `docs/remake-architecture-and-roadmap.md`; this file diff --git a/docs/script-inventory.md b/docs/script-inventory.md index eef1239..ebc89b1 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -50,7 +50,7 @@ Large, table-like scripts — prime candidates for data extraction: `RTINIT` (67 KB), `CCINIT` (41 KB), `SKINIT` (37 KB — skills), `CDINIT` (31 KB), `BTANINIT` (105 KB — battle animations) - Smaller: `AFINIT`, `ALINIT`, `CIINIT`, `CNINIT`, `CTINIT`, `CVINIT`, `ILINIT`, - `LAINIT`, `MAINIT`, `OBINIT`, `SPINIT`, `TRINIT`, `VIINIT` + `LAINIT`, `MAINIT`, `OBINIT` (46 object definitions), `SPINIT`, `TRINIT`, `VIINIT` **Extracted-table correction and semantic pilot (2026-07-22).** Name-mode INIT tables use sparse, one-based runtime ids over fixed reserved array spans, not a new record whenever the string destination @@ -73,8 +73,10 @@ six scalars, cells in 29 preallocated buffers, and all 1,396 length-prefixed foo stage field meanings and the evidence workflow live in `docs/name-resolution.md`. The first semantic join now assembles the confirmed type/X/Y/difficulty and story-prerequisite buffers into 2,312 object placements across 66 stages. It also exposes 604 reinforcement schedules and consumer-proven tagged payload variants -for initial factions, teleport destinations, treasure item/count pairs, and card-generation lists; 185 -other type-specific payload cells remain raw. A second join assembles all 1,378 populated enemy slots +for initial factions, teleport destinations, treasure item/count pairs, card-generation lists, and 104 +hazard/barrier non-triggering faction gates. Every placement joins its type name and, where populated, its +effect description from OBINIT's 46 definitions; 81 type-specific payload cells on types 11, 17, 26, and +27 remain raw. A second join assembles all 1,378 populated enemy slots across the same 66 stages, including unit/faction, placement, difficulty/story gates, level rules, weighted alternatives, difficulty-specific movement/battle routine sets, and 485 first-clear-only replay gates. diff --git a/docs/tools-reference.md b/docs/tools-reference.md index f6f646b..80e36e4 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -55,9 +55,9 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited * |---|---|---|---| | `extract_phase2.py` | Batch: disassembly + text corpora for every script. | `extract_phase2.py` | corpus → `build/disasm/*.asm`, `build/text/{dialogue.jsonl,strings.jsonl,*.strings.txt}`, `build/manifest.json` | | `extract_message_table.py` | Discover a repeated global-id dispatch chain such as ITMES/SKMES, reconstruct player-facing title/description lines (including furigana surface text and readings), and emit an ID-keyed message table with bytecode provenance. | `extract_message_table.py [OUTNAME]` | `.BIN` → `build/data/.json` | -| `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer / mixed 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`. 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`, including type-tagged teleport/treasure/card/initial-faction payloads, reinforcement schedules, and first-clear-only enemy gates. ITINIT and SKINIT join ITMES/SKMES messages. 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 matching `.BIN` when supported + `build/globals.json` → `build/data/.json`, `build/data/README.md` | +| `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer / mixed 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`. 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, and non-triggering hazard/barrier faction), reinforcement schedules, and first-clear-only enemy gates. ITINIT and SKINIT join ITMES/SKMES messages. 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 OBINIT for STINIT, 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 table: message coverage, per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. `--message-query REGEX` shows every matching name/message 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 and mixed selector-dispatched INIT extraction, MES reconstruction/joins, footer-array accounting and semantic projection, STINIT object/enemy joins, and field/message profiling. | run each directly | — | +| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based and mixed selector-dispatched INIT extraction, MES reconstruction/joins, footer-array accounting and semantic projection, OBINIT object-definition extraction, STINIT object/enemy joins and tagged-payload behavior, and field/message 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}` | ## VM diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index 1b526a9..961fcdd 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -53,8 +53,8 @@ - [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 tables → JSON.** `tools/extract_init.py` auto-detects table shape (`name`/`numeric`/`footer`/`mixed`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units)** [name: sparse one-based name/description/fields], **CGINIT (379 CG entries)** [numeric: index-keyed columns], **MPINIT (1472 map records)** [footer: length-prefixed arrays], and **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays]. 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, 604 reinforcement schedules, and typed initial-faction, teleport, treasure, and card-list payloads while preserving 185 unresolved tagged cells. 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.2 — `*INIT` data tables → JSON.** `tools/extract_init.py` auto-detects table shape (`name`/`numeric`/`footer`/`mixed`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions)** [name: sparse one-based name/description/fields], **CGINIT (379 CG entries)** [numeric: index-keyed columns], **MPINIT (1472 map records)** [footer: length-prefixed arrays], and **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays]. 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, and non-triggering-faction payloads while preserving 81 unresolved tagged cells on types 11, 17, 26, and 27. 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.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: fold in `*MES` writers; label 2D record tables by their reader scripts. Then Frida to name *which stat* each field is. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Also deferred: `call-script` id→name resolution (engine-level — SCJUMP.BIN decode or Frida; see `docs/name-resolution.md` #1). diff --git a/tools/extract_init.py b/tools/extract_init.py index ae700a2..ee86405 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -5,7 +5,7 @@ name — records keyed by a name string. Each record: set-string(name), static field writes, set-string(desc). Arrays indexed by record id in lockstep (+1/record). - (SKINIT skills, ITINIT items, EBINIT units) + (SKINIT skills, ITINIT items, EBINIT units, OBINIT object definitions) numeric— column table with NO names: mov/copy-to-global into parallel int arrays, keyed by an incrementing index column. (CGINIT gallery) footer — copy-local-array (op 0x64) bulk-loads length-prefixed arrays from the file @@ -394,6 +394,19 @@ def extract_name(scr): "desc_array_bases": {k: f"0x{v:x}" for k, v in sorted(desc_bases.items())}} +@cache +def object_type_definitions() -> dict[int, dict]: + """Load OBINIT's authoritative names and descriptions keyed by object type id.""" + records, _ = extract_name(sys4load.load(resolve("OBINIT"))) + return { + record["id"]: { + "name": record["name"], + **({"description": record["desc"]} if record.get("desc") else {}), + } + for record in records + } + + def _int_writes(scr): """Ordered (addr, value_arg) for global-int mov / copy-to-global.""" out = [] @@ -557,8 +570,12 @@ def attach_semantic_fields(records: list[dict], semantics: dict[str, str]) -> No record.pop("semantic_fields", None) -def attach_stage_object_placements(records: list[dict]) -> None: +def attach_stage_object_placements( + records: list[dict], definitions: dict[int, dict] | None = None +) -> None: """Assemble STINIT's parallel object buffers into modder-facing slot records.""" + if definitions is None: + definitions = object_type_definitions() known_fields = { "type_id": "0xe7389", "tile_x": "0xe7325", @@ -574,8 +591,15 @@ def attach_stage_object_placements(records: list[dict]) -> None: type_key = f"{known_fields['type_id']}/{slot}" if type_key not in fields: continue - obj = {"slot": slot} + type_id = fields[type_key] + obj = {"slot": slot, "type_id": type_id} + if definition := definitions.get(type_id): + obj["type_name"] = definition["name"] + if description := definition.get("description"): + obj["type_description"] = description for semantic_name, base in known_fields.items(): + if semantic_name == "type_id": + continue if (key := f"{base}/{slot}") in fields: obj[semantic_name] = fields[key] required = [ @@ -599,7 +623,6 @@ def attach_stage_object_placements(records: list[dict]) -> None: for base in ("0xe73bb", "0xe73ed") if (key := f"{base}/{slot}") in fields } - type_id = obj["type_id"] if type_id in (1, 2, 3, 4) and "0xe73bb" in payload: obj["initial_faction_id"] = payload.pop("0xe73bb") elif type_id in (6, 36): @@ -614,6 +637,8 @@ def attach_stage_object_placements(records: list[dict]) -> None: obj["item_quantity"] = payload.pop("0xe73ed") elif type_id == 28 and "0xe73bb" in payload: obj["card_generation_list_id"] = payload.pop("0xe73bb") + elif 18 <= type_id <= 25 and "0xe73bb" in payload: + obj["non_triggering_faction_id"] = payload.pop("0xe73bb") unknown = payload if unknown: obj["unknown_fields"] = unknown @@ -747,7 +772,9 @@ def write_data_index(data_dir: Path) -> None: "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`", - "and `enemy_spawns`; unresolved type-specific/mode parameters remain in `unknown_fields`.", + "and `enemy_spawns`. Its object type ids join to OBINIT's authoritative names and", + "available descriptions; consumer-proven tagged payload variants receive semantic names while", + "unresolved type-specific/mode parameters remain in `unknown_fields`.", "Use `tools/init_table_profile.py
--build` to generate value/population and", "direct-consumer evidence.", "", @@ -798,6 +825,7 @@ def main() -> int: semantics = field_semantics(recs, meta.get("array_layouts")) attach_semantic_fields(recs, semantics) if mode == "mixed" and name == "STINIT": + meta["object_definition_table"] = "OBINIT" attach_stage_object_placements(recs) attach_stage_enemy_spawns(recs) out = {"table": name, "source": scr.path.name, "magic": scr.magic, "mode": mode, diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 1065aeb..9fca846 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -29,6 +29,7 @@ def test_real_name_tables() -> None: "SKINIT.BIN": (300, 131), "ITINIT.BIN": (1000, 287), "EBINIT.BIN": (1000, 277), + "OBINIT.BIN": (100, 46), } scripts = paths.scripts() for name, (span, count) in expected.items(): @@ -55,6 +56,14 @@ def test_real_name_tables() -> None: check(by_id[108]["record_fields"]["0x906f9/30/8"] == -5, "ITINIT item 108 preserves its paralysis-removal delta") + objects, _ = extract_init.extract_name(sys4load.load(scripts["OBINIT.BIN"])) + object_by_id = {record["id"]: record for record in objects} + check(object_by_id[17]["name"] == "針" + and object_by_id[17]["desc"] == "HP-2", + "OBINIT object 17 preserves its name and effect description") + check(object_by_id[28]["name"] == "カード取得", + "OBINIT object ids provide authoritative STINIT type labels") + def test_static_negative_write() -> None: class Instruction: @@ -110,6 +119,8 @@ def test_real_mixed_table() -> None: check(first_object == { "slot": 1, "type_id": 1, + "type_name": "拠点", + "type_description": "▲命中・回避・防御", "tile_x": 13, "tile_y": 1, "difficulty_mask": 7, @@ -132,6 +143,8 @@ def test_real_mixed_table() -> None: ) check(stage1_slot8["card_generation_list_id"] == 1, "STINIT card objects expose their generation-list id") + check(stage1_slot8["type_name"] == "カード取得", + "STINIT object placements join OBINIT type names") stage1_slot9 = next( obj for obj in records[0]["object_placements"] if obj["slot"] == 9 ) @@ -146,7 +159,20 @@ def test_real_mixed_table() -> None: all_objects = [ obj for record in records for obj in record["object_placements"] ] - check(sum("unknown_fields" in obj for obj in all_objects) == 185, + check(sum("non_triggering_faction_id" in obj for obj in all_objects) == 104, + "STINIT hazard/barrier payloads expose their FIELD-proven faction gate") + check({ + obj["non_triggering_faction_id"] + for obj in all_objects + if "non_triggering_faction_id" in obj + } == {1, 2, 3}, "STINIT faction gates retain all observed faction ids") + spike = next( + obj for obj in all_objects + if obj["type_id"] == 17 and "unknown_fields" in obj + ) + check(spike["unknown_fields"] == {"0xe73bb": 2}, + "STINIT spike payload remains raw because FIELD excludes type 17 from the gate") + check(sum("unknown_fields" in obj for obj in all_objects) == 81, "STINIT preserves unresolved tagged object payloads as raw evidence") extract_init.attach_stage_enemy_spawns(records) first_spawn = records[0]["enemy_spawns"][0] diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 56fb725..faa78bf 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -1602,6 +1602,26 @@ source = "inference" confidence = "med" depends_on = [] +[[global]] +address = "0x26f1" +name = "object_type_names" +category = "string-table" +type = "string[100]" +value_domain = "OBINIT object definition names for ids 1..46" +usage = "OBINIT writes the authoritative object names. FIELD, SETOBJ, and DRAWOBJ use STINIT's object type id to select these definitions; extract_init joins the names to stage object placements." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x2755" +name = "object_type_descriptions" +category = "string-table" +type = "string[100]" +value_domain = "optional OBINIT player-facing object behavior/effect descriptions for ids 1..46" +usage = "OBINIT writes the short object descriptions displayed by the field object-information path. extract_init joins populated descriptions to STINIT object placements by type id." +source = "investigation" +confidence = "high" + [[global]] address = "0x27b9" name = "stage_victory_condition_1"