Resolve final STINIT object payloads

This commit is contained in:
gamer147
2026-07-23 09:56:18 -04:00
parent d1565d8e18
commit e7091f5680
7 changed files with 43 additions and 17 deletions

View File

@@ -296,7 +296,17 @@ runtime state by the object's sprite height to select its vertical source row. T
(type 26, `出撃の旗`). All deployment-flag values are 2, and all 63 enemies linked to those flags are also
faction 2, consistent with OBINIT's `敵の増援地点` description; because the spawn branch accepts type 26
without comparing those values, the field remains the directly proven object state rather than a guessed
faction id. Only three type-27 (`異界の門`) first-payload cells remain raw.
faction id.
The final three populated tagged cells are understood as engine-dead authoring data rather than a hidden
type-27 (`異界の門`) parameter. STINIT explicitly writes `2` to the first payload cell for object slot 22
in stages 52--54. Type 27 has no OBINIT state-row mode, so FIELD's fresh-stage initializer does not copy
that value to runtime state; the generic interaction dispatch also has no type-27 branch. Its dedicated
turn path reads only the object's active flag, type, reinforcement interval/limit, and coordinates, then
forces ADDEN enemy slot 0. ADDEN hardcodes EBINIT unit 465 (`漂着した異界の姫/BOSS`) into runtime entity
slot 49; on success FIELD changes to BGM 10 and reports `異界の姫が漂着した!`. Neither `0xe73bb` nor
`0xe73ed` participates. The join therefore preserves the three explicit writes under
`ignored_payload_fields` instead of inventing a semantic name or leaving them unresolved.
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
@@ -323,8 +333,9 @@ confirmed parallel buffers plus both prerequisite tables are also assembled into
`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 three undecoded type-27 payload cells stay attached under
`unknown_fields`, so this convenience view loses no evidence or invents names.
three engine-dead type-27 payload writes stay attached under `ignored_payload_fields`; there are no
remaining populated object payloads 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
weight, movement/battle routine rows, and `first_clear_only` replay gating. Raw footer metadata stays in
@@ -378,8 +389,8 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape
STINIT's mixed stage records now have machine-readable investigation surfaces, including joined object
placements and enemy spawns. STINIT's universal object schedule, consumer-proven tagged payload
families, faction-gated hazards/barriers, initialized object states, OBINIT definition join, and
first-clear enemy gate are decoded; next trace the three remaining raw type-27 payloads only where its
special-spawn path distinguishes them. Isolate EBINIT's remaining voice/action
first-clear enemy gate are decoded, and its last three populated tagged cells are proven ignored by the
type-27 special-spawn path. 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.

View File

@@ -755,8 +755,9 @@ forbidden story prerequisites. Generated records now carry a joined `semantic_fi
2,312 object placements across 66 stages. FIELD resolves the universal reinforcement interval/limit and
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. Its state-row mode and
FIELD/DRAWOBJ initialization path resolve 78 initial object states on types 11, 17, and 26; only three
type-27 payload cells remain raw. The separate 30-cell enemy family is also decoded:
FIELD/DRAWOBJ initialization path resolve 78 initial object states on types 11, 17, and 26. FIELD's
dedicated special-spawn path proves the last three populated type-27 tagged writes are ignored by the
engine; they remain preserved as provenance. 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
@@ -770,8 +771,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.
- The three remaining STINIT tagged payloads on object type 27, plus other data schemas when
their runtime consumers make them necessary.
- 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
only provides the execution framework.

View File

@@ -76,8 +76,9 @@ across 66 stages. It also exposes 604 reinforcement schedules and consumer-prove
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. OBINIT's state-row mode resolves another 78 initial
object states on types 11, 17, and 26; only three type-27 payload cells remain raw. A second join assembles
all 1,378 populated enemy slots
object states on types 11, 17, and 26. The last three populated tagged writes, all type 27, are preserved
as ignored authoring data after FIELD's dedicated spawn path proves it never reads them. 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.

View File

@@ -55,7 +55,7 @@ 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 <MES> [OUTNAME]` | `<MES>.BIN``build/data/<OUTNAME>.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`, 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), 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 <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus OBINIT for STINIT, matching `<MES>.BIN` when supported, and `build/globals.json``build/data/<OUTNAME>.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, 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. 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 <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus OBINIT for STINIT, matching `<MES>.BIN` when supported, and `build/globals.json``build/data/<OUTNAME>.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 <TABLE> [--build] [--limit N] [--message-query REGEX]` | `build/data/<TABLE>.json` + corpus → stdout; with `--build`, `build/data/<TABLE>-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, 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}` |

View File

@@ -54,7 +54,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 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, non-triggering-faction, and initial-object-state payloads while preserving only three unresolved type-27 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.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.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).

View File

@@ -646,6 +646,12 @@ def attach_stage_object_placements(
and definition["uses_runtime_state_sprite_row"]
and "0xe73bb" in payload):
obj["initial_object_state_id"] = payload.pop("0xe73bb")
elif type_id == 27 and payload:
# FIELD's dedicated otherworld-gate branch consumes the common
# schedule and coordinates, then calls ADDEN's hard-coded slot-0
# special-unit path. It never reads either tagged payload cell.
obj["ignored_payload_fields"] = payload
payload = {}
unknown = payload
if unknown:
obj["unknown_fields"] = unknown
@@ -781,7 +787,8 @@ def write_data_index(data_dir: Path) -> None:
"STINIT additionally joins confirmed parallel buffers into per-slot `object_placements`",
"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`.",
"engine-dead tagged writes remain in `ignored_payload_fields` and unresolved",
"type-specific/mode parameters remain in `unknown_fields`.",
"Use `tools/init_table_profile.py <TABLE> --build` to generate value/population and",
"direct-consumer evidence.",
"",

View File

@@ -186,10 +186,17 @@ def test_real_mixed_table() -> None:
check(spike["initial_object_state_id"] == 2
and "non_triggering_faction_id" not in spike,
"STINIT spikes expose state without inventing the excluded faction gate")
ignored_objects = [
obj for obj in all_objects if "ignored_payload_fields" in obj
]
check(len(ignored_objects) == 3
and {obj["type_id"] for obj in ignored_objects} == {27}
and {tuple(obj["ignored_payload_fields"].items()) for obj in ignored_objects}
== {(("0xe73bb", 2),)},
"STINIT preserves explicitly written but engine-ignored type-27 payloads")
unknown_objects = [obj for obj in all_objects if "unknown_fields" in obj]
check(len(unknown_objects) == 3
and {obj["type_id"] for obj in unknown_objects} == {27},
"STINIT preserves unresolved tagged object payloads as raw evidence")
check(not unknown_objects,
"STINIT has no unresolved populated tagged object payloads")
extract_init.attach_stage_enemy_spawns(records)
first_spawn = records[0]["enemy_spawns"][0]
check(first_spawn == {