Decode STINIT enemy spawn semantics

This commit is contained in:
gamer147
2026-07-23 08:40:22 -04:00
parent 4e42b57970
commit 668e138507
9 changed files with 344 additions and 41 deletions

View File

@@ -1,7 +1,7 @@
<!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build -->
# Global Variable Reference (generated)
5030 globals (167 curated, 4863 auto shape-inferred). Source of truth: `vm-map/globals.toml`.
5030 globals (181 curated, 4849 auto shape-inferred). Source of truth: `vm-map/globals.toml`.
## choice-output
@@ -115,6 +115,20 @@
| `0xe7483` | stage_object_difficulty_mask | high | investigation | Per-object difficulty inclusion mask. SETOBJ uses check-bit with difficulty_index and rejects the object slot when the selected difficulty bit is absent. |
| `0xe74b5` | stage_object_required_story_flags | high | investigation | Seven positive prerequisites per stage object. SETOBJ subtracts one from every populated id and suppresses the object unless the corresponding story_event_flags cell equals 1. Columns: 0=required_flag_1, 1=required_flag_2, 2=required_flag_3, 3=required_flag_4, 4=required_flag_5, 5=required_flag_6, 6=required_flag_7. |
| `0xe7613` | stage_object_forbidden_story_flags | high | investigation | Five negative prerequisites per stage object. SETOBJ subtracts one from every populated id and suppresses the object when the corresponding story_event_flags cell equals 1. Columns: 0=forbidden_flag_1, 1=forbidden_flag_2, 2=forbidden_flag_3, 3=forbidden_flag_4, 4=forbidden_flag_5. |
| `0xe773f` | stage_enemy_spawn_tile_x | high | investigation | Direct X coordinate for a current-stage enemy slot. FIELD copies it into the runtime unit position when stage_enemy_spawn_object_slot is zero; object-linked alternatives instead inherit the referenced object's coordinates. |
| `0xe775d` | stage_enemy_spawn_tile_y | high | investigation | Direct Y coordinate paired with stage_enemy_spawn_tile_x. FIELD and ADDEN copy the pair into the runtime unit position; object-linked alternatives instead inherit the referenced object's coordinates. |
| `0xe777b` | stage_enemy_spawn_object_slot | high | investigation | Optional object-placement anchor for an enemy spawn. FIELD resolves nonzero entries through stage_object_type_id and uses the matching stage_object_tile_x/Y location; zero selects stage_enemy_spawn_tile_x/Y directly. |
| `0xe7799` | stage_enemy_faction_id | high | investigation | Per-spawn faction/team selector. FIELD and ADDEN copy it into the runtime unit's faction field, use it to choose the entity-index band, and test the same value against map occupancy masks. |
| `0xe77b7` | stage_enemy_difficulty_mask | high | investigation | Per-spawn difficulty inclusion mask. FIELD uses check-bit with difficulty_index and rejects a slot when the selected difficulty bit is absent. |
| `0xe77f3` | stage_enemy_random_selection_weight | high | investigation | Weight for stage-enemy alternatives. FIELD groups eligible nonzero-weight slots by their stage_enemy_spawn_object_slot, sums the weights, makes a weighted random choice within each eligible group, and directly materializes zero-weight slots. |
| `0xe7811` | stage_enemy_unit_id | high | investigation | Unit-definition id for each current-stage enemy slot. FIELD rejects zero slots, while FIELD and ADDEN pass populated ids to SETEN to copy the corresponding EBINIT definition into a runtime entity. |
| `0xe782f` | stage_enemy_min_level | high | investigation | Scenario-specific minimum level for each stage enemy. After applying party-level auto-scaling, SETEN raises the runtime level to this floor before applying the maximum-level clamp. |
| `0xe784d` | stage_enemy_max_level | high | investigation | Scenario-specific maximum level for each stage enemy. SETEN uses a positive value as the upper clamp unless the debug/config override is active; otherwise it falls back to the unit definition's unit_level_cap. |
| `0xe786b` | stage_enemy_auto_level_scale_divisor | high | investigation | Scenario override for enemy auto-level scaling. SETEN divides the positive gap between the scenario base level and unit_starting_level by this value before adding it to the runtime level; zero would fall back to unit_auto_level_scale_divisor. |
| `0xe7889` | stage_enemy_movement_routine_set_ids | high | investigation | Three difficulty-specific movement-AI routine-set ids per stage enemy. SETEN copies the row into the runtime unit, and MVRTN selects the difficulty_index column as its movement routine table row. Columns: 0=difficulty_0, 1=difficulty_1, 2=difficulty_2. |
| `0xe78e3` | stage_enemy_battle_routine_set_ids | high | investigation | Optional three-column battle-AI routine override per stage enemy. SETEN copies a populated row into the runtime unit or falls back to the unit definition, and BTRTN selects the difficulty_index column as its battle routine table row. Columns: 0=difficulty_0, 1=difficulty_1, 2=difficulty_2. |
| `0xe793d` | stage_enemy_required_story_flags | high | investigation | Seven positive prerequisites per stage enemy. FIELD subtracts one from every populated id and suppresses the spawn unless the corresponding story_event_flags cell equals 1. Columns: 0=required_flag_1, 1=required_flag_2, 2=required_flag_3, 3=required_flag_4, 4=required_flag_5, 5=required_flag_6, 6=required_flag_7. |
| `0xe7a0f` | stage_enemy_forbidden_story_flags | high | investigation | Five negative prerequisites per stage enemy. FIELD subtracts one from every populated id and suppresses the spawn when the corresponding story_event_flags cell equals 1. Columns: 0=forbidden_flag_1, 1=forbidden_flag_2, 2=forbidden_flag_3, 3=forbidden_flag_4, 4=forbidden_flag_5. |
| `0x5` | — | low | auto-shape | array |
| `0xd2` | — | low | auto-shape | array |
| `0xd7` | — | low | auto-shape | array |
@@ -265,17 +279,7 @@
| `0xe741f` | — | low | auto-shape | array |
| `0xe7451` | — | low | auto-shape | array |
| `0xe770d` | — | low | auto-shape | array |
| `0xe773f` | — | low | auto-shape | array |
| `0xe775d` | — | low | auto-shape | array |
| `0xe777b` | — | low | auto-shape | array |
| `0xe7799` | — | low | auto-shape | array |
| `0xe77b7` | — | low | auto-shape | array |
| `0xe77d5` | — | low | auto-shape | array |
| `0xe77f3` | — | low | auto-shape | array |
| `0xe7811` | — | low | auto-shape | array |
| `0xe782f` | — | low | auto-shape | array |
| `0xe784d` | — | low | auto-shape | array |
| `0xe786b` | — | low | auto-shape | array |
| `0xe7e8d` | — | low | auto-shape | array |
| `0xe8275` | — | low | auto-shape | array |
| `0xebd0d` | — | low | auto-shape | array |
@@ -367,6 +371,7 @@
| `0xa6e59` | current_skill_id | high | investigation | Shared skill-id argument/selection slot. Skill menus and combat scripts write the chosen skill id and use it to index SKINIT arrays; SKMES compares it against all 131 skill ids to select the matching player-facing title and description. |
| `0xeff75` | current_entity_index_hi | med | inference | High-purity current-entity row index (purity 0.95 in the auto shape map); dominant 2D-table row selector. |
| `0x152616` | current_entity_index | med | investigation | Primary current-entity row index (RECOVER-confirmed; purity 0.51, 363 row-index uses). |
| `0x152617` | current_stage_enemy_spawn_slot | high | investigation | Current STINIT enemy-template slot. FIELD selects slots 1..29 while materializing stage units, SETEN records the slot on the runtime entity and reads every parallel enemy buffer through it, and ADDEN uses slot 0 for its special generated unit. |
## story-flag
@@ -412,7 +417,6 @@
| `0xcc9f3` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against []; writers=['CALCSCOPE.BIN']. |
| `0xccc21` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0]; writers=['CHMENU.BIN']. |
| `0xeff77` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [0, 2, 3]; writers=['BTRTN.BIN', 'MVRTN.BIN', 'RTN_B001.BIN', 'RTN_B002.BIN']. |
| `0x152617` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0]; writers=['CLOSE.BIN', 'FIELD.BIN', 'INFOEN.BIN', 'READY.BIN']. |
| `0x1560e7` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0, 1, 2, 3, 4, 5, 6, 7]; writers=['MAGIC.BIN', 'SELACT.BIN', 'SUMMON.BIN']. |
| `0x15a094` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against [0]; writers=['ALCHEMY.BIN', 'EVOLVE.BIN', 'SELSTAGE.BIN', 'STUDY.BIN']. |
| `0x15a095` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against [0]; writers=['INFO.BIN', 'INFOCH.BIN', 'MENU.BIN']. |
@@ -5019,10 +5023,6 @@
| `0xe6afe` | — | med | auto-shape | record-table[stride 10] |
| `0xe6fe2` | — | med | auto-shape | record-table[stride 3] |
| `0xe71d6` | — | med | auto-shape | record-table[stride 3] |
| `0xe7889` | — | med | auto-shape | record-table[stride 3] |
| `0xe78e3` | — | med | auto-shape | record-table[stride 3] |
| `0xe793d` | — | med | auto-shape | record-table[stride 7] |
| `0xe7a0f` | — | med | auto-shape | record-table[stride 5] |
| `0xe865d` | — | med | auto-shape | record-table[stride 7] |
| `0xea1b5` | — | med | auto-shape | record-table[stride 7] |
| `0xee035` | — | med | auto-shape | record-table[stride 3] |

View File

@@ -239,7 +239,7 @@ fallbacks are deliberately limited to the still-unresolved voice reactions and S
three fields remain wholly anonymous (`0x7843e` and two suspicious sparse writes into runtime table
`0x4e693/300`).
### STINIT mixed stage records (2026-07-22)
### STINIT mixed stage records (2026-07-23)
STINIT is not a name table. Its preamble allocates 29 fixed global work buffers, then 74 sparse branches
compare `scjump_progress_a` with stage ids 1 through 170. Each selected branch populates the same current-
@@ -271,14 +271,29 @@ type id. SETOBJ tests the `{3,4,7}` masks in `0xe7483` against GAMESTART's three
then applies seven required and five forbidden one-based ids from `0xe74b5`/`0xe7613` against the shared
`story_event_flags` bank.
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.
`0xe7811` selects the EBINIT unit, `0xe7799` is its faction, `0xe773f`/`0xe775d` are direct tile
coordinates, and `0xe777b` optionally anchors the unit to a stage-object slot. FIELD checks the
three-bit difficulty mask in `0xe77b7`, uses `0xe77f3` as a weighted-random alternative value, and applies
the seven required plus five forbidden story flags in `0xe793d`/`0xe7a0f`. SETEN proves `0xe782f`,
`0xe784d`, and `0xe786b` are the scenario level floor, cap, and party-level scaling divisor. Finally,
the three-value footer rows in `0xe7889` and optional `0xe78e3` become difficulty-specific movement and
battle routine-set ids selected by MVRTN/BTRTN.
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, and populated positive/negative story prerequisites. The four still-unresolved object-parameter
buffers remain attached under `unknown_fields`, so this convenience view loses no evidence or invents names.
The next structural batch is the separate 30-slot enemy-spawn schema and its difficulty-dependent footer
arrays.
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, and movement/battle routine rows. Raw footer metadata stays in `footer_arrays`, while its
`semantic_fields` value is the copied row itself. The one remaining spawn-mode cell (`0xe77d5`, always 2
when populated) stays under `unknown_fields`: FIELD ties it to already-cleared-stage state, but the precise
player-facing nuance is not yet strong enough to name. The next STINIT batch is the four type-specific
object parameters plus that isolated spawn-mode gate.
### The curated registry — `vm-map/globals.toml` (2026-07-07)
@@ -325,11 +340,11 @@ are *not* story flags — the miner over-tags them; they are recategorized `unkn
The v1 map labels *shapes and tables*; the next increments add *meaning*, cheapest first:
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. Next assemble STINIT's
30-slot enemy-spawn schema through FIELD/SETEN/ADDEN, including its three-difficulty footer arrays; trace
the four remaining specialized object parameters only when their type-specific 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.
STINIT's mixed stage records now have machine-readable investigation surfaces, including joined object
placements and enemy spawns. Next trace STINIT's four remaining specialized object parameters and its
isolated spawn-mode gate only where 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.
3. **Label 2D record tables by their readers** — cross-reference which scripts read each

View File

@@ -749,11 +749,14 @@ The remaining EBINIT unknowns are now the unread `0x7843e` enum and the signed m
enemy AI appears to live outside the static EBINIT schema. STINIT's separate mixed parser is now complete:
all 74 sparse stage ids retain their victory/defeat strings, six scalars, fixed-buffer cells, and 1,396
footer-array copies. AIM/FIELD/DRAWCHP consumers establish the four condition slots, stage BGM, turn limit,
and turn-limit outcome. The next pass establishes target clear turns, performance reward, replay behavior,
and turn-limit outcome. Further passes establish target clear turns, performance reward, replay behavior,
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 while retaining four unresolved type-specific parameters by raw
address. The separate 30-slot enemy-spawn family remains the next evidence-driven batch.
address. 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; only one replay-linked spawn-mode field remains raw.
Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks:
@@ -763,8 +766,8 @@ 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 enemy-spawn and specialized object-parameter semantics, plus other data schemas when their
runtime consumers make them necessary.
- Remaining STINIT specialized object-parameter semantics and the isolated spawn-mode gate, 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
only provides the execution framework.

View File

@@ -72,7 +72,10 @@ buffer rather than parallel per-id arrays. Its generated records preserve four m
six scalars, cells in 29 preallocated buffers, and all 1,396 length-prefixed footer-array copies. Confirmed
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; unknown type-specific parameters remain raw, and the enemy-spawn buffers remain separate.
across 66 stages. 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, and
difficulty-specific movement/battle routine sets. Unknown type-specific object parameters and the isolated
enemy spawn-mode field remain raw.
### Message/string tables (`*MES`)
`ITMES` (64 KB — item text), `VIMES` (43 KB), `EIMES` (37 KB), `SKMES` (31 KB — skill

View File

@@ -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 <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`. 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; raw keys 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 matching `<MES>.BIN` when supported + `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`. 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 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 matching `<MES>.BIN` when supported + `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 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, STINIT object/enemy joins, 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

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)** [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 now cover conditions, BGM, turn/replay/clear-reward settings, and map texture overrides; four parallel object fields plus 7 required/5 forbidden story prerequisites assemble into 2,312 object placements. 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; four parallel object fields plus 7 required/5 forbidden story prerequisites assemble into 2,312 object placements. The 30-cell enemy family now contributes 1,378 joined spawns with unit/faction, placement, difficulty/story gates, level scaling, weighted selection, and difficulty-specific movement/battle routine sets. 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

@@ -15,7 +15,7 @@
Records are {id, name?, desc?, fields:{"0x<col_base>": value}} or, for footer tables,
{id, global_addr, footer_off, values:[...]}. Column addresses are raw engine globals;
naming them (attack, cost, …) needs the engine global-var map — later work.
confirmed names come from the generated engine global registry while raw keys remain provenance.
Usage: py -3.11 -X utf8 tools/extract_init.py <TABLE> [OUTNAME] [--mode name|numeric|footer|mixed]
"""
@@ -484,6 +484,11 @@ def field_semantics(
records: list[dict], array_layouts: dict[str, dict] | None = None
) -> dict[str, str]:
"""Map raw extracted field keys to canonical semantic names when available."""
footer_keys = {
key
for record in records
for key in record.get("footer_arrays", {})
}
keys = {
key
for record in records
@@ -510,6 +515,10 @@ def field_semantics(
layout = (array_layouts or {}).get(f"0x{int(parts[0], 16):x}", {})
if stride := layout.get("stride"):
row, column = divmod(index, stride)
if key in footer_keys:
# A footer copy owns the complete row beginning at this offset.
name = f"{name}.row_{row}"
else:
column_name = entry.get("columns", {}).get(
str(column), f"column_{column}"
)
@@ -539,7 +548,9 @@ def attach_semantic_fields(records: list[dict], semantics: dict[str, str]) -> No
raise ValueError(
f"record {record['id']}: duplicate semantic field {semantic_name}"
)
semantic_fields[semantic_name] = value
semantic_fields[semantic_name] = (
value["values"] if container == "footer_arrays" else value
)
if semantic_fields:
record["semantic_fields"] = semantic_fields
else:
@@ -593,6 +604,78 @@ def attach_stage_object_placements(records: list[dict]) -> None:
record["object_placements"] = objects
def attach_stage_enemy_spawns(records: list[dict]) -> None:
"""Assemble STINIT's parallel enemy buffers into modder-facing slot records."""
direct_fields = {
"unit_id": "0xe7811",
"faction_id": "0xe7799",
"difficulty_mask": "0xe77b7",
"min_level": "0xe782f",
"max_level": "0xe784d",
"auto_level_scale_divisor": "0xe786b",
}
optional_fields = {
"tile_x": "0xe773f",
"tile_y": "0xe775d",
"object_slot": "0xe777b",
"random_selection_weight": "0xe77f3",
}
routine_fields = {
"movement_routine_set_ids": ("0xe7889", 3),
"battle_routine_set_ids": ("0xe78e3", 3),
}
unknown_bases = ("0xe77d5",)
for record in records:
fields = record.get("array_fields", {})
footer_arrays = record.get("footer_arrays", {})
spawns = []
# Slot zero is reserved by ADDEN for its synthesized special-unit path.
for slot in range(1, 30):
unit_key = f"{direct_fields['unit_id']}/{slot}"
if unit_key not in fields:
continue
spawn = {"slot": slot}
for semantic_name, base in direct_fields.items():
key = f"{base}/{slot}"
# Faction zero is the buffer default and is meaningful to SETEN.
if key in fields:
spawn[semantic_name] = fields[key]
elif semantic_name == "faction_id":
spawn[semantic_name] = 0
for semantic_name, base in optional_fields.items():
if (key := f"{base}/{slot}") in fields:
spawn[semantic_name] = fields[key]
for semantic_name, (base, stride) in routine_fields.items():
key = f"{base}/{slot * stride}"
if key in footer_arrays:
spawn[semantic_name] = footer_arrays[key]["values"]
required = [
fields[key]
for column in range(7)
if (key := f"0xe793d/{slot * 7 + column}") in fields
and fields[key] > 0
]
forbidden = [
fields[key]
for column in range(5)
if (key := f"0xe7a0f/{slot * 5 + column}") in fields
and fields[key] > 0
]
if required:
spawn["required_story_flags"] = required
if forbidden:
spawn["forbidden_story_flags"] = forbidden
unknown = {
base: fields[f"{base}/{slot}"]
for base in unknown_bases
if f"{base}/{slot}" in fields
}
if unknown:
spawn["unknown_fields"] = unknown
spawns.append(spawn)
record["enemy_spawns"] = spawns
def write_data_index(data_dir: Path) -> None:
"""Regenerate the disposable build/data index from current table JSONs."""
tables = []
@@ -640,12 +723,13 @@ def write_data_index(data_dir: Path) -> None:
"short description stored by the INIT script.",
"Top-level `field_semantics` maps raw array/row-column keys to canonical machine-readable",
"names from `vm-map/globals.toml`; each record's generated `semantic_fields` is the joined",
"name-keyed convenience view. Raw keys remain intact as bytecode provenance.",
"name-keyed convenience view. Complete footer copies expose their row values there while",
"raw keys and footer metadata remain intact as bytecode provenance.",
"",
"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 the confirmed parallel object buffers into per-slot",
"`object_placements`; unresolved type-specific parameters remain in `unknown_fields`.",
"STINIT additionally joins confirmed parallel buffers into per-slot `object_placements`",
"and `enemy_spawns`; 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.",
"",
@@ -697,6 +781,7 @@ def main() -> int:
attach_semantic_fields(recs, semantics)
if mode == "mixed" and name == "STINIT":
attach_stage_object_placements(recs)
attach_stage_enemy_spawns(recs)
out = {"table": name, "source": scr.path.name, "magic": scr.magic, "mode": mode,
"record_count": len(recs), **meta,
"field_columns": cols if mode != "footer" else None,

View File

@@ -120,6 +120,27 @@ def test_real_mixed_table() -> None:
)
check(stage2_slot3["required_story_flags"] == [902],
"STINIT object placements join positive story prerequisites")
extract_init.attach_stage_enemy_spawns(records)
first_spawn = records[0]["enemy_spawns"][0]
check(first_spawn == {
"slot": 1,
"unit_id": 205,
"faction_id": 2,
"difficulty_mask": 7,
"min_level": 1,
"max_level": 10,
"auto_level_scale_divisor": 1,
"object_slot": 2,
"movement_routine_set_ids": [1, 1, 1],
"forbidden_story_flags": [11],
"unknown_fields": {"0xe77d5": 2},
}, "STINIT joins confirmed enemy buffers into one spawn record")
stage1_slot2 = records[0]["enemy_spawns"][1]
check(stage1_slot2["random_selection_weight"] == 1
and stage1_slot2["object_slot"] == 5,
"STINIT preserves weighted object-linked enemy alternatives")
check(sum(len(record["enemy_spawns"]) for record in records) == 1378,
"STINIT assembles every populated enemy spawn slot")
def test_real_message_tables() -> None:
@@ -211,6 +232,18 @@ def test_field_semantics() -> None:
== "stage_object_required_story_flags.row_3.required_flag_1",
"mixed row buffers expose row and column semantics",
)
check(
stage_semantics["0xe7889/3"]
== "stage_enemy_movement_routine_set_ids.row_1",
"enemy footer copies expose whole-row semantics",
)
extract_init.attach_semantic_fields(stages, stage_semantics)
check(
stages[0]["semantic_fields"][
"stage_enemy_movement_routine_set_ids.row_1"
] == [1, 1, 1],
"semantic footer fields expose row values without provenance wrappers",
)
if __name__ == "__main__":

View File

@@ -1682,6 +1682,16 @@ usage = "GAMESTART's three-way difficulty selection. SETEN uses the zero-based i
source = "investigation"
confidence = "high"
[[global]]
address = "0x152617"
name = "current_stage_enemy_spawn_slot"
category = "index-pointer"
type = "int"
value_domain = "0..29; slot 0 is ADDEN's synthesized special-unit path"
usage = "Current STINIT enemy-template slot. FIELD selects slots 1..29 while materializing stage units, SETEN records the slot on the runtime entity and reads every parallel enemy buffer through it, and ADDEN uses slot 0 for its special generated unit."
source = "investigation"
confidence = "high"
[[global]]
address = "0x6d3"
name = "story_event_flags"
@@ -1796,3 +1806,157 @@ usage = "Five negative prerequisites per stage object. SETOBJ subtracts one from
source = "investigation"
confidence = "high"
depends_on = ["0x6d3"]
[[global]]
address = "0xe773f"
name = "stage_enemy_spawn_tile_x"
category = "data-table"
type = "int[30]"
value_domain = "map tile X coordinate, 1..25 when populated"
usage = "Direct X coordinate for a current-stage enemy slot. FIELD copies it into the runtime unit position when stage_enemy_spawn_object_slot is zero; object-linked alternatives instead inherit the referenced object's coordinates."
source = "investigation"
confidence = "high"
[[global]]
address = "0xe775d"
name = "stage_enemy_spawn_tile_y"
category = "data-table"
type = "int[30]"
value_domain = "map tile Y coordinate, 3..799 when populated"
usage = "Direct Y coordinate paired with stage_enemy_spawn_tile_x. FIELD and ADDEN copy the pair into the runtime unit position; object-linked alternatives instead inherit the referenced object's coordinates."
source = "investigation"
confidence = "high"
depends_on = ["0xe773f"]
[[global]]
address = "0xe777b"
name = "stage_enemy_spawn_object_slot"
category = "data-table"
type = "int[30]"
value_domain = "stage object slot 2..46; zero means use the direct tile coordinates"
usage = "Optional object-placement anchor for an enemy spawn. FIELD resolves nonzero entries through stage_object_type_id and uses the matching stage_object_tile_x/Y location; zero selects stage_enemy_spawn_tile_x/Y directly."
source = "investigation"
confidence = "high"
depends_on = ["0xe7325", "0xe7357", "0xe7389"]
[[global]]
address = "0xe7799"
name = "stage_enemy_faction_id"
category = "data-table"
type = "int[30]"
value_domain = "{0,2,3}"
usage = "Per-spawn faction/team selector. FIELD and ADDEN copy it into the runtime unit's faction field, use it to choose the entity-index band, and test the same value against map occupancy masks."
source = "investigation"
confidence = "high"
[[global]]
address = "0xe77b7"
name = "stage_enemy_difficulty_mask"
category = "data-table"
type = "int[30]"
value_domain = "7 in the shipped table; bit 0/1/2 enables the spawn on the corresponding difficulty"
usage = "Per-spawn difficulty inclusion mask. FIELD uses check-bit with difficulty_index and rejects a slot when the selected difficulty bit is absent."
source = "investigation"
confidence = "high"
depends_on = ["0x32f1"]
[[global]]
address = "0xe77f3"
name = "stage_enemy_random_selection_weight"
category = "data-table"
type = "int[30]"
value_domain = "{0,1,2,3,10,20,30}; zero means spawn directly"
usage = "Weight for stage-enemy alternatives. FIELD groups eligible nonzero-weight slots by their stage_enemy_spawn_object_slot, sums the weights, makes a weighted random choice within each eligible group, and directly materializes zero-weight slots."
source = "investigation"
confidence = "high"
depends_on = ["0xe777b"]
[[global]]
address = "0xe7811"
name = "stage_enemy_unit_id"
category = "data-table"
type = "int[30]"
value_domain = "EBINIT unit ids 1..869; zero means unused slot"
usage = "Unit-definition id for each current-stage enemy slot. FIELD rejects zero slots, while FIELD and ADDEN pass populated ids to SETEN to copy the corresponding EBINIT definition into a runtime entity."
source = "investigation"
confidence = "high"
[[global]]
address = "0xe782f"
name = "stage_enemy_min_level"
category = "data-table"
type = "int[30]"
value_domain = "1..99"
usage = "Scenario-specific minimum level for each stage enemy. After applying party-level auto-scaling, SETEN raises the runtime level to this floor before applying the maximum-level clamp."
source = "investigation"
confidence = "high"
[[global]]
address = "0xe784d"
name = "stage_enemy_max_level"
category = "data-table"
type = "int[30]"
value_domain = "1..99"
usage = "Scenario-specific maximum level for each stage enemy. SETEN uses a positive value as the upper clamp unless the debug/config override is active; otherwise it falls back to the unit definition's unit_level_cap."
source = "investigation"
confidence = "high"
depends_on = ["0x7a766"]
[[global]]
address = "0xe786b"
name = "stage_enemy_auto_level_scale_divisor"
category = "data-table"
type = "int[30]"
value_domain = "{1,2}"
usage = "Scenario override for enemy auto-level scaling. SETEN divides the positive gap between the scenario base level and unit_starting_level by this value before adding it to the runtime level; zero would fall back to unit_auto_level_scale_divisor."
source = "investigation"
confidence = "high"
depends_on = ["0x7a37e", "0x7ab4e"]
[[global]]
address = "0xe7889"
name = "stage_enemy_movement_routine_set_ids"
category = "data-table"
type = "int[30][3]"
columns = { "0" = "difficulty_0", "1" = "difficulty_1", "2" = "difficulty_2" }
value_domain = "movement-routine set ids 1..176"
usage = "Three difficulty-specific movement-AI routine-set ids per stage enemy. SETEN copies the row into the runtime unit, and MVRTN selects the difficulty_index column as its movement routine table row."
source = "investigation"
confidence = "high"
depends_on = ["0x32f1", "0x152617"]
[[global]]
address = "0xe78e3"
name = "stage_enemy_battle_routine_set_ids"
category = "data-table"
type = "int[30][3]"
columns = { "0" = "difficulty_0", "1" = "difficulty_1", "2" = "difficulty_2" }
value_domain = "battle-routine set ids 2..7; zero row falls back to the unit definition"
usage = "Optional three-column battle-AI routine override per stage enemy. SETEN copies a populated row into the runtime unit or falls back to the unit definition, and BTRTN selects the difficulty_index column as its battle routine table row."
source = "investigation"
confidence = "high"
depends_on = ["0x32f1", "0x152617"]
[[global]]
address = "0xe793d"
name = "stage_enemy_required_story_flags"
category = "data-table"
type = "int[30][7]"
columns = { "0" = "required_flag_1", "1" = "required_flag_2", "2" = "required_flag_3", "3" = "required_flag_4", "4" = "required_flag_5", "5" = "required_flag_6", "6" = "required_flag_7" }
value_domain = "one-based story_event_flags ids; zero means no requirement"
usage = "Seven positive prerequisites per stage enemy. FIELD subtracts one from every populated id and suppresses the spawn unless the corresponding story_event_flags cell equals 1."
source = "investigation"
confidence = "high"
depends_on = ["0x6d3"]
[[global]]
address = "0xe7a0f"
name = "stage_enemy_forbidden_story_flags"
category = "data-table"
type = "int[30][5]"
columns = { "0" = "forbidden_flag_1", "1" = "forbidden_flag_2", "2" = "forbidden_flag_3", "3" = "forbidden_flag_4", "4" = "forbidden_flag_5" }
value_domain = "one-based story_event_flags ids; zero means no exclusion"
usage = "Five negative prerequisites per stage enemy. FIELD subtracts one from every populated id and suppresses the spawn when the corresponding story_event_flags cell equals 1."
source = "investigation"
confidence = "high"
depends_on = ["0x6d3"]