Implement mounted append AUTORUN boot

This commit is contained in:
gamer147
2026-07-24 13:03:43 -04:00
parent 6c24ac5e5b
commit 7bac5a9108
16 changed files with 355 additions and 86 deletions

View File

@@ -223,7 +223,7 @@ scene-local numeric addressing mode.
selector with arithmetic `SAR 24`; the port rejects sign-bit selectors rather than guessing behavior for
ids that would index before AGE's mount table.
**Append bootstrap/data sequence (static content verified 2026-07-24; native launch boundary open).**
**Append bootstrap/data sequence (launch boundary verified 2026-07-24).**
Installed `APPEND01` contains 81 records, including 39 SYS4 scripts. Record zero is
`$1$AUTORUN.BIN` (`0x01000000`), a clean 43-instruction script. Its first 22 instructions call packed
records `0x01000001..0x01000016` in exact order: append fragments for EBINIT, CNINIT, ITINIT, SKINIT,
@@ -245,11 +245,16 @@ scene-local numeric addressing mode.
The separate `APPEND01-EBINIT` result preserves packed provenance and can be queried by unit id or name
with `init_table_profile.py --record`; it is not an ordered merged runtime image.
The base/loose script corpus contains no reference to packed id `0x01000000`. The shipped game must
therefore enter the append AUTORUN outside the visible base script call graph, but the exact native
launcher and its ordering relative to SYSTEM4/INIT2 have not yet been reversed. The current port can
resolve and execute append scripts when given their packed ids, but natural SYSTEM4 boot does not yet
launch mounted AUTORUNs. Do not treat VFS-B mount completion as append gameplay/bootstrap completion.
The base/loose script corpus contains no explicit reference to packed id `0x01000000` because INIT2
invokes native opcode `0x143` once at `0x17f`. Its handler scans mounted selectors 1..255 in ascending
order and queues `(selector << 24) | 0`, selecting record zero without a filename lookup. The engine
executes those queued scripts serially before resuming INIT2. This occurs after INIT2's 23 base
initializer calls and registry setup, and immediately before TUNE, proving base-then-append patch
order. The port now implements op `0x143` over the mounted-selector view exposed by
`Sys4ScriptProvider`, snapshots and sorts the selectors, constructs each packed record-zero id, and
executes the scripts serially through normal VM frames. Natural SYSTEM4 boot regression-proves
`BTANINIT2 -> $1$AUTORUN.BIN -> $1$EBINIT.BIN -> TUNE.BIN`; VFS mounting and append patch application
are therefore both active. Native queue/frame details are canonical in `docs/engine-re.md`.
3. **AGF decoder (VFS-C DONE).** `Age.Engine/Sys4/AgfDecoder.cs` decodes an opened AGF payload directly
to a tightly packed, top-down width/height + RGBA8 surface. The MIT-licensed GARbro
`ArcFormats/Eushully/ImageAGF.cs` provides a compact reference: `ACGF` (or zero) signature, type 1/2,

View File

@@ -6,9 +6,6 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| offset | name | type | note |
|---|---|---|---|
| `0x40c` | `sys4ini_count` | `int` | SYS4INI record count |
| `0x410` | `archive_name_table` | `void*` | archive-name table base (arc_id*0x100 indexes it) |
| `0x414` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base {name[64],arc_id,file_number,offset,size}; record = base + id*0x50 |
| `0x814` | `input_action_count` | `int` | logical action count (0..31); op 0xfe sets it, op 0x100 scans actions below it and uses callback slot count itself when the polled mask is empty |
| `0x898` | `joystick_physical_button_count` | `int` | WinMM JOYCAPS physical button count returned by op 0x106 |
| `0x89c` | `joystick_button_map` | `int` | base of 32-entry logical button-slot to physical joystick-button table; op 0x107 writes it, slot N emits action N+4 |
@@ -17,7 +14,6 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0x1428` | `keyboard_vk_action_map` | `int` | base of 256-entry Win32 virtual-key to logical action table consumed by keyboard polling |
| `0x1828` | `dik_to_vk_table` | `int` | base of 256-entry DirectInput DIK scan-code to Win32 virtual-key translation used by op 0x10c |
| `0x1c34` | `mouse_wheel_delta` | `int` | signed WM_MOUSEWHEEL delta accumulated by age_main_window_proc; op 0x10d returns and clears it |
| `0x3028` | `alt_pack_table` | `int` | call-script high-byte alternate pack table (unused by corpus) |
| `0x4d7c` | `shared_profile_state` | `void*` | embedded shared SAVE.DAT state object; owns profile integer/settings tables and container timing metadata |
| `0x5190` | `shared_profile_int_table` | `int` | open-addressing 12-byte string-key to 32-bit value table; op 0x1a2 stores, 0x1a3 loads, shared SAVE.DAT serializes it |
| `0x14d54` | `gfx_obj_ptr_table` | `void*` | per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c) |
@@ -88,8 +84,21 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0x6dbf0` | `voice_bgm_duck_control_flags` | `uint` | transient mask replaced by op 0x1cf; bit 0 suppresses automatic voice-triggered BGM attenuation |
| `0x6dbf4` | `message_skip_queued_voice_id` | `int` | latest voice resource deferred by op 0xc4 while run_state_flags message-skip bit is active |
| `0x6dbf8` | `message_skip_queued_voice_arg` | `int` | second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero |
| `0x6f89c` | `script_launch_queue` | `int` | embedded integer FIFO used for engine-level auxiliary/root script launches; data pointer and queue indices follow |
| `0x6f8a0` | `script_launch_queue_data` | `void*` | dword storage for packed script ids or negative frame-resume ids |
| `0x6f8a4` | `script_launch_queue_read_cursor` | `int` | next queued launch consumed by script_launch_queue_dispatch_next |
| `0x6f8a8` | `script_launch_queue_write_cursor` | `int` | one-past-last queued launch; op 0x143 appends mounted selector record-zero ids here |
| `0x6f8ac` | `script_launch_queue_capacity` | `int` | allocated dword capacity; initialized to 0x100 |
| `0x6f8b0` | `script_launch_queue_growth` | `int` | capacity growth quantum; initialized to 0x100 |
| `0x6f8b4` | `script_launch_queue_high_water` | `int` | highest consumed cursor retained across FIFO compaction |
| `0x6f8b8` | `script_launch_dispatch_active` | `int` | suppresses immediate dispatch while op 0x143 batches mounted append record-zero ids and while a queued script is active |
| `0x9928c` | `save_frame_boundary_index` | `int` | highest script-frame index included by numbered-save layouts 2/3; -1 falls back to cur_ctx_index; op 0x1ad marks current frame and op 0x2 clears after unwinding below it |
| `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) |
| `0x9c658` | `sys4ini_count` | `int` | SYS4INI record count at embedded FileDB+0x40c |
| `0x9c65c` | `archive_name_table` | `void*` | archive-name table base at embedded FileDB+0x410; arc_id*0x100 indexes it |
| `0x9c660` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base at embedded FileDB+0x414; record = base + id*0x50 |
| `0x9f274` | `mounted_aai_catalogs` | `void*` | base of 256-entry selector-keyed AAI catalog-pointer table inside the embedded FileDB; op 0x143 scans slots 1..255 from +0x9f278 |
| `0x9f278` | `mounted_aai_catalog_selector_1` | `void*` | selector-one cell and op 0x143 scan start; subsequent dwords are selector 2..255 |
| `0xa0cc0` | `screen_w` | `int` | screen width (640) |
| `0xa0cc4` | `screen_h` | `int` | screen height (480) |
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |

View File

@@ -209,13 +209,16 @@ the dispatch table (op `0x03` → `ctx[0x26c93+3]` = **`FUN_0041bc90`**), then t
magic, allocates per-frame code/local buffers from the header var-counts, reads the bytecode body,
and pushes a script frame (**stride 0x1e = 30 dwords**, indexed by `ctx[0x14f45]`). Returns to the
caller when the callee ends.
- **`FUN_0044f390`** (resolver — the key): `record = [ctx+0x414] + id*0x50`. The record is exactly the
- **`asset_open_indexed_entry@0x44f390`** (resolver — the key): its `this` is the embedded FileDB at
`EngineCtx+0x9c24c`, not EngineCtx itself. A base `record = [FileDB+0x414] + id*0x50`. The record is exactly the
**SYS4INI 80-byte layout** `{name[64], arc_id@0x40, file_number@0x44, offset@0x48, size@0x4c}`
(count = `[ctx+0x40c]`, archive-name table = `[ctx+0x410]`). It tries a **loose override first**
(count = `[FileDB+0x40c]`, archive-name table = `[FileDB+0x410]`; absolute EngineCtx fields
`+0x9c658/+0x9c65c/+0x9c660`). It tries a **loose override first**
(`CreateFileA` on `record.name` → the mod/patch hook point), else opens archive
`[record.arc_id*0x100 + ctx+0x410]`, `SetFilePointer` to `record.offset`, size = `record.size`.
High-byte-tagged ids (`id & 0xff000000`) select an alternate pack via `[ctx+0x3028]` — **unused by
the corpus** (0/297 ids carry a high byte).
`[record.arc_id*0x100 + FileDB+0x410]`, `SetFilePointer` to `record.offset`, size = `record.size`.
High-byte-tagged ids select `[FileDB+0x3028 + signed_selector*4]` and use the low 24 bits as the
selected AAI record. The base corpus has no explicit high-byte call-script operand; INIT2 op `0x143`
supplies mounted record-zero ids dynamically.
**So `call-script <id>` = a direct RAW index into the SYS4INI global file table** — the same table
`parse_sys4ini.py` reads, but indexed *without* skipping `@` placeholders (13208 records, 2
@@ -2172,15 +2175,36 @@ The callers pass their operands unchanged: `script_frame_load_resource@0x40e980`
fetches operand 1 and immediately forwards it to that helper. Thus scene-local and raw-fallback are not
native modes: ordinary resource operands are already universal packed SYS4INI/AAI ids.
**Unresolved append execution boundary (2026-07-24).** Static inspection now separates catalog mount
from append initialization. Installed selector 1 record zero is `$1$AUTORUN.BIN` at packed id
`0x01000000`; it calls the 22 append INIT fragments at `0x01000001..0x01000016`, then installs append
dispatch scripts/resources. No base or loose script contains a call to `0x01000000`, so append startup is
owned outside the visible base-script graph. The native function that chooses and launches mounted
AUTORUN records, and its exact ordering relative to SYSTEM4/INIT2, remain unnamed/unproven. A follow-up
should trace script-frame creation for packed id `0x01000000` during cold boot, then rename/comment and
save the responsible `/v2` function. Until then, “AAI mounted” must not be used as evidence that append
INIT deltas ran.
**Append execution boundary resolved (2026-07-24).** Append startup is exposed to bytecode through
`op_0x143_run_mounted_append_autoruns@0x4172f0`, not through an explicit
`call-script 0x01000000`. The handler scans the `FileDB+0x3028` mount-pointer table as
`EngineCtx.mounted_aai_catalogs[1..255]` (`ctx+0x9f278` onward). For each non-null selector it enqueues
`selector << 24`: the packed id for that catalog's record zero. It does not search for an AUTORUN
basename. The selector loop batches all ids under `script_launch_dispatch_active`, advances the caller
past the opcode, then dispatches the queue.
`script_launch_queue_enqueue@0x40f820` appends to the embedded FIFO at `ctx+0x6f89c`.
`script_launch_queue_dispatch_next@0x40f6e0` suspends the current frame, records return sentinel `-10`,
and loads positive packed ids into reserved interpreter frame 37. When that script reaches
`op_0x2_exit_or_return_frame@0x417940`, the sentinel path launches the next queued id if present;
otherwise it restores the suspended caller. Mounted record-zero scripts therefore execute serially in
ascending selector order.
The sole corpus use is `INIT2@0x17f`. SYSTEM4 has already performed its UI, configuration, input-map,
and preload setup before calling INIT2. INIT2 then calls its 23 base data initializers—EBINIT through
SCINIT plus BTANINIT2—performs its base global/array registrations, executes op `0x143`, and calls
TUNE only after the queued append scripts return. Installed selector 1 record zero is
`$1$AUTORUN.BIN` (`0x01000000`), so the exact shipped order is base definitions, append deltas and
registrations, then TUNE and the remainder of SYSTEM4 boot. Catalog mounting itself still occurs earlier,
when the native SYS4INI/FileDB loader calls `asset_mount_append_catalogs`; mount only establishes
addressability, while op `0x143` is the explicit patch-application boundary.
The port implements the same observable boundary without reproducing the reserved native frame or sentinel.
`IScriptProvider` exposes mounted selector identity; op `0x143` snapshots, deduplicates, and sorts it,
constructs `selector << 24`, and executes each resolved record-zero script synchronously through the normal
nested-frame path. That preserves batching, serial order, caller suspension, global-bank sharing, and
whole-stack halt/reload propagation. Focused VM tests cover two selectors and failure resolution, while the
natural SYSTEM4 integration path proves `BTANINIT2 -> $1$AUTORUN -> $1$EBINIT -> TUNE`.
SC0010 supplies a clean corpus proof outside SC0000's base-zero coincidence. Its `set-texture 0x21` must
open raw entry `0x21` (`SO013A.AGF`); adding SC0010's catalog position `0x11e` instead selects unrelated

View File

@@ -735,10 +735,11 @@ field/value/raw-coordinate triples. Regression coverage fixes the seven row ids
representative unit-81 level, stat, growth, and selector-keyed asset values.
This is deliberately a fragment view, not a synthesized boot image. `$1$CNINIT.BIN` supplies companion
display-name/voice-family writes, other INIT fragments may add related definitions, and native AUTORUN
ordering remains unresolved. Whole-pack enumeration and ordered base/append assignment merging should wait
until that engine-owned launch boundary is proven. The AAI format and verified internal call sequence are
canonical in `docs/asset-resolution-re.md`; command usage is canonical in `docs/tools-reference.md`.
display-name/voice-family writes and other INIT fragments add related definitions. The launch order is now
proven: INIT2 op `0x143` executes each mounted selector's record zero serially after base initialization
and before TUNE. Whole-pack extraction still needs explicit ordered merge/provenance support before this
focused fragment should be presented as a synthesized runtime image. The AAI format and verified sequence
are canonical in `docs/asset-resolution-re.md`; command usage is canonical in `docs/tools-reference.md`.
### Battle experience pipeline (2026-07-24)

View File

@@ -318,7 +318,7 @@ This is raw strlen(bytes), not a .NET UTF-16 character count. BUNKI compares all
### 0x3 `call-script` (call-script, argc 1)
- **summary:** load & call another SYS4 script by id; id = RAW index into the SYS4INI file table (asset-index). Pushes a script frame; returns to caller when the callee ends.
- **grounding:** source=investigation, confidence=high
- **evidence:** native-RE (Ghidra): handler FUN_0041bc90 -> loader FUN_0040e980 -> resolver FUN_0044f390 indexes an 80-byte record table (base [ctx+0x414], count [ctx+0x40c]) at base+id*0x50 = the SYS4INI record layout {name[64],arc_id@0x40,file_number@0x44,offset@0x48,size@0x4c}. Confirmed statically: all 297 distinct corpus call-script ids resolve to a .BIN script with a semantically-exact name (0x1ab->ADDITEM, 0x2ae7->MES, 0x143->BUNKI, 0x329d->CALCREVISE), 0 out-of-range, 0 pack-branch. See docs/engine-re.md + name-resolution.md #1.
- **evidence:** native-RE (Ghidra): handler FUN_0041bc90 -> loader script_frame_load_resource@0x40e980 -> resolver asset_open_indexed_entry@0x44f390 indexes an 80-byte record table (FileDB+0x414 base, FileDB+0x40c count) at base+id*0x50 = the SYS4INI record layout {name[64],arc_id@0x40,file_number@0x44,offset@0x48,size@0x4c}. The FileDB is embedded at EngineCtx+0x9c24c, making those EngineCtx+0x9c660/+0x9c658. Confirmed statically: all 297 distinct base-corpus call-script ids resolve to a .BIN script with a semantically-exact name (0x1ab->ADDITEM, 0x2ae7->MES, 0x143->BUNKI, 0x329d->CALCREVISE), 0 out-of-range. See docs/engine-re.md + name-resolution.md #1.
op 0x03 (call-script, argc 1): `call-script <id>`. RESOLVED — the id is a direct RAW index into
the SYS4INI global file table (the same table parse_sys4ini.py reads, but indexed WITHOUT skipping
@@ -329,11 +329,13 @@ Native mechanism (dispatch table `handler(op)=ctx[0x26c93+op]`, op 0x03 -> FUN_0
2. FUN_0040e980 (loader): opens the resource by id, reads the 0x20-byte SYS4 header, checks magic,
allocates per-frame code/local buffers from the header var-counts, reads the bytecode body,
pushes a script frame (stride 0x1e = 30 dwords, indexed by ctx[0x14f45]).
3. FUN_0044f390 (resolver): record = [ctx+0x414] + id*0x50. Tries a LOOSE OVERRIDE first
(CreateFileA on record.name -> mod/patch hook point), else opens archive [record.arc_id*0x100 +
ctx+0x410], SetFilePointer to record.offset, size = record.size.
(High-byte-tagged ids `id & 0xff000000` select an alternate pack via [ctx+0x3028]; UNUSED by the
corpus -- 0/297 ids have a high byte.)
3. asset_open_indexed_entry@0x44f390 (resolver): its `this` is the embedded FileDB, not EngineCtx.
Base record = [FileDB+0x414] + id*0x50. It tries a LOOSE OVERRIDE first (CreateFileA on
record.name -> mod/patch hook point), else opens archive [record.arc_id*0x100 + FileDB+0x410],
SetFilePointer to record.offset, size = record.size.
High-byte-tagged ids select [FileDB+0x3028 + signed_selector*4] and index the chosen AAI by the
low 24 bits. The base corpus has 0/297 explicit high-byte call-script operands; INIT2 op 0x143
supplies mounted record-zero ids dynamically.
Companion op 0x8f `call` is INTRA-script (a local JSR), not cross-script -- see its entry.
This also names the whole call graph statically (build/callscript-names.json).
@@ -440,6 +442,26 @@ Implemented as process-lifecycle state owned by the persistent VM: it begins at
- **grounding:** source=investigation, confidence=med
- **evidence:** Ghidra: handler 0x4299c0 (dispatch ctx[0x9b74c]=0x4299c0; created+typed EngineCtx*+annotated; Kelebek u0041F9C0 = VA-drift). Records the generic 9-dword instruction length; op2→local_204, op3→local_104, op4→local_208; (*DAT_005c6018)(8, ctx[0x54fe8], &local_210) → FUN_00425fb0(1,ret). DAT_005c6018: 6 xrefs all READ, no static writer; FUN_00405740 (screen-fade) calls it w/ cmd 3, branches on ret 1/2 = transition progress = native video service.
### 0x143 `run-mounted-append-autoruns` (u00415FB0, argc 0)
- **summary:** () - enqueue record zero from every mounted nonzero AAI selector in ascending selector order, then execute those packed scripts serially before resuming the caller.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x143_run_mounted_append_autoruns@0x4172f0 scans mounted_aai_catalogs[1..255] at EngineCtx+0x9f278, enqueues selector<<24 through script_launch_queue_enqueue@0x40f820 while dispatch is suppressed, advances the caller PC, then tail-dispatches through script_launch_queue_dispatch_next@0x40f6e0. Positive queue entries load in reserved frame 37 with return sentinel -10; op_0x2_exit_or_return_frame@0x417940 dispatches the next queued entry or restores the suspended caller. Corpus: the sole site is INIT2@0x17f, after its 23 base INIT children and global/array setup and immediately before TUNE.BIN.
The opcode does not look up an AUTORUN filename. For each non-null mounted catalog pointer in selector
slots 1 through 255, it constructs the packed id `selector << 24`, whose low 24-bit record index is zero.
At Himegari cold boot the native catalog loader has already mounted APPEND01 in selector 1 before SYSTEM4
starts. SYSTEM4 calls INIT2; INIT2 runs EBINIT through SCINIT plus BTANINIT2, completes its base registry
setup, executes this opcode, and resumes at its following TUNE call only after every queued record-zero
script returns. Installed selector 1 record zero is `$1$AUTORUN.BIN`, which applies the append INIT deltas
and registrations. This is a serial base-then-append patch boundary, not a filename overlay.
PORT = IMPLEMENTED. `IScriptProvider.MountedAppendSelectors` exposes mounted selector identity without
coupling the VM to the SYS4 catalog type. The opcode snapshots, deduplicates, and sorts selectors, constructs
each packed record-zero id, and runs the resolved script through the ordinary nested-frame machinery before
resuming INIT2. Focused tests protect ordering, packed-id construction, caller suspension, and unresolved
record-zero failure. Natural SYSTEM4 boot proves BTANINIT2 -> `$1$AUTORUN.BIN` -> `$1$EBINIT.BIN` -> TUNE.
### 0x199 `yield-adv-coroutine` (u00414D50, argc 0)
- **summary:** Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow.
- **grounding:** source=investigation, confidence=high
@@ -1197,10 +1219,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x143 `u00415FB0` (u00415FB0, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x144 `u004259D0` (u004259D0, argc 2)
- **summary:** —
- **grounding:** source=kelebek, confidence=low

View File

@@ -3345,9 +3345,12 @@ semantic-field/value/raw-coordinate table, with large resource values also rende
Regressions cover packed loading, the complete sparse id set, inherited geometry, representative unit-81
level/base-stat/growth fields, and its packed battle-sprite id.
This closes the immediate “inspect an append unit semantically” need without asserting that the runtime
has executed `$1$AUTORUN.BIN`. Ordered whole-pack merging and natural port bootstrap still depend on the
unresolved native AUTORUN launch boundary documented in `docs/asset-resolution-re.md`.
This closes the immediate “inspect an append unit semantically” need without asserting that the port
runtime has executed `$1$AUTORUN.BIN`. Native RE subsequently proved INIT2 op `0x143` as the
base-initializers → mounted record-zero scripts → TUNE boundary. The port now implements that natural
bootstrap: SYSTEM4 regression-proves `$1$AUTORUN.BIN` and `$1$EBINIT.BIN` execute between BTANINIT2 and
TUNE. Ordered whole-pack extraction/merged semantic projection remains separate tooling work; see
`docs/asset-resolution-re.md` and `docs/engine-re.md`.
## Data-semantics sidebar: battle experience rewards (2026-07-24)

View File

@@ -82,21 +82,25 @@ letting script-owned setup/cleanup surround child scenes, rather than invent an
protocol. Full process-start observation remains useful for profile/default and retained host-state evidence,
but is no longer needed to guess the script coordinator architecture.
The current headless C# runner already follows this root naturally: one SYSTEM4 run entered INITCONFIG,
INIT2 and all 23 of its data-initializer children, TUNE, INIT, and TITLE (28 nested script calls total), then
The current headless C# runner already follows this root naturally: one SYSTEM4 run enters INITCONFIG,
INIT2 and all 23 of its base data-initializer children, APPEND01's AUTORUN and 22 append initializer
children, TUNE, INIT, and TITLE (51 nested script calls total), then
remained in TITLE's input-poll loop because the diagnostic host supplies no user input. Direct opcode
coverage is 100% for all 23 data initializers, CALCARR, and TUNE; the remaining direct coverage is SYSTEM4
64/82, INIT2 9/12, TITLE 61/65, GAMESTART 43/47, and UNITECH 29/31. B0/B1 should therefore make the
64/82, INIT2 10/12, TITLE 61/65, GAMESTART 43/47, and UNITECH 29/31. B0/B1 should therefore make the
SYSTEM4-rooted path visible and interactive in Godot, then investigate only the gaps actually reached on
that route instead of treating every static gap as a prerequisite.
**Append boot caveat (2026-07-24).** The sequence and coverage above describe only the base/loose script
graph. Mounted `APPEND01` has a separate `$1$AUTORUN.BIN` at packed id `0x01000000`; that script calls 22
append INIT deltas and registers the append's class-change, SCJUMP, message, stage, and scenario resources.
The base corpus never references that packed id, and natural port boot currently mounts the catalog without
executing AUTORUN. Stage B2 is therefore not append-complete until native RE identifies the engine-owned
launch boundary and the port reproduces its ordering. The verified append-internal sequence and remaining
native question are canonical in `docs/asset-resolution-re.md` and `docs/engine-re.md`.
**Append boot implemented (2026-07-24).** Mounted `APPEND01` has
`$1$AUTORUN.BIN` at packed id `0x01000000`; that script calls 22 append INIT deltas and registers the
append's class-change, SCJUMP, message, stage, and scenario resources. INIT2's sole op `0x143` site,
after its 23 base initializer children and registry setup and before TUNE, scans mounted selectors in
ascending order and serially executes each selector's record zero. The port now snapshots the mounted
selectors through its script-provider seam, constructs each `selector << 24` id, and runs those scripts
through normal nested VM frames. A natural SYSTEM4 regression proves the shipped order
`BTANINIT2 -> $1$AUTORUN -> $1$EBINIT -> TUNE`; focused regressions cover multiple-selector ordering,
deduplication, caller resumption, and missing record zero. The packed sequence is canonical in
`docs/asset-resolution-re.md`; native queue/frame mechanics are canonical in `docs/engine-re.md`.
**Godot root landing (2026-07-20).** The no-argument Godot/run-godot path now starts SYSTEM4 directly and
does not apply the direct-SC0000 layout/surface bootstrap or the diagnostic `--boot` prefix. A windowed run
@@ -324,9 +328,10 @@ The boot path must cover two existing categories:
- System/session initialization currently approximated by `INITCONFIG`, `INIT2`, and `INIT`, including
host-visible side effects that `CaptureHost` discards.
- Game-data initialization represented by the `*INIT` family used by the headless boot/session tools.
- Mounted append initialization: execute each native-selected packed AUTORUN at the proven boot boundary,
preserving base-versus-append order and provenance rather than treating same-suffix INIT fragments as
filename replacements.
- Mounted append initialization: implement op `0x143` at INIT2's natural bytecode position by enumerating
mounted selectors 1..255 in ascending order and executing packed record zero serially, preserving
base-versus-append order and provenance rather than treating same-suffix INIT fragments as filename
replacements.
Completion evidence:

View File

@@ -0,0 +1,86 @@
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
public class AppendAutorunTests
{
private static Operand I(long value) => new(0, value);
private static Operand G(long address) => new(3, address);
private sealed class MountedProvider : IScriptProvider
{
private readonly IReadOnlyDictionary<long, Script> _scripts;
public MountedProvider(IReadOnlyList<int> selectors, IReadOnlyDictionary<long, Script> scripts)
{
MountedAppendSelectors = selectors;
_scripts = scripts;
}
public IReadOnlyList<int> MountedAppendSelectors { get; }
public List<long> Requests { get; } = new();
public Script? GetById(long id)
{
Requests.Add(id);
return _scripts.GetValueOrDefault(id);
}
}
[Fact]
public void RunMountedAppendAutoruns_ExecutesRecordZeroSeriallyBySelectorThenResumesCaller()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var first = ScriptAssembler.Assemble(table, "APPEND_ONE_AUTORUN", new List<(int, Operand[])>
{
(0x55, new[] { G(0x100), I(7) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var second = ScriptAssembler.Assemble(table, "APPEND_TWO_AUTORUN", new List<(int, Operand[])>
{
(0x50, new[] { G(0x100), G(0x100), I(5) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var root = ScriptAssembler.Assemble(table, "ROOT", new List<(int, Operand[])>
{
(0x143, Array.Empty<Operand>()),
(0x55, new[] { G(0x101), G(0x100) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var provider = new MountedProvider(new[] { 2, 1, 2 }, new Dictionary<long, Script>
{
[0x01000000] = first,
[0x02000000] = second,
});
var vm = new VirtualMachine(root, table, new RecordingHost(), provider: provider);
vm.Run();
Assert.Equal(new[] { 0x01000000L, 0x02000000L }, provider.Requests);
Assert.Equal(12, vm.Globals.GetValueOrDefault(0x100));
Assert.Equal(12, vm.Globals.GetValueOrDefault(0x101));
Assert.Equal(2, vm.CallScriptDispatches);
Assert.Equal("exit", vm.HaltReason);
}
[Fact]
public void RunMountedAppendAutoruns_HaltsWhenMountedRecordZeroIsNotAScript()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var root = ScriptAssembler.Assemble(table, "ROOT", new List<(int, Operand[])>
{
(0x143, Array.Empty<Operand>()),
(0x55, new[] { G(0x100), I(1) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var provider = new MountedProvider(new[] { 1 }, new Dictionary<long, Script>());
var vm = new VirtualMachine(root, table, new RecordingHost(), provider: provider);
vm.Run();
Assert.Equal(new[] { 0x01000000L }, provider.Requests);
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x100));
Assert.Equal("append-autorun-unresolved:0x1000000", vm.HaltReason);
}
}

View File

@@ -155,6 +155,16 @@ public class NaturalBootIntegrationTests
Assert.Contains("UNITECH.BIN", sink.Entered);
Assert.Contains("CALCARR.BIN", sink.Entered);
Assert.Equal("SC0000.BIN", sink.Entered[^1]);
int baseBtanInit2 = sink.Entered.IndexOf("BTANINIT2.BIN");
int appendAutorun = sink.Entered.IndexOf("$1$AUTORUN.BIN");
int appendEbInit = sink.Entered.IndexOf("$1$EBINIT.BIN");
int tune = sink.Entered.IndexOf("TUNE.BIN");
Assert.Equal(new[] { 1 }, boot.Scripts.MountedAppendSelectors);
Assert.True(baseBtanInit2 >= 0 && baseBtanInit2 < appendAutorun
&& appendAutorun < appendEbInit && appendEbInit < tune,
$"entered={string.Join(",", sink.Entered)}");
Assert.Equal(40, vm.Globals.GetValueOrDefault(0x7a37e + 81)); // append unit starting level
Assert.Equal(0x0100002d, vm.Globals.GetValueOrDefault(0x6fb86 + 81)); // packed battle sprite
Assert.Equal(1, vm.Globals.GetValueOrDefault(0));
Assert.Equal(0x22, vm.Globals.GetValueOrDefault(0x699));
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x6c1));

View File

@@ -25,6 +25,7 @@ public class Sys4ScriptProviderTests
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var provider = Sys4ScriptProvider.Load(table);
var append = provider.Catalog.AppendPacks[1];
Assert.Equal(new[] { 1 }, provider.MountedAppendSelectors);
var entry = append.Files.Single(e => e.Name == "$1$SC1260.BIN");
long packedId = 0x01000000L | (uint)entry.RawIndex;

View File

@@ -7,4 +7,8 @@ public interface IScriptProvider
{
/// <summary>The script for this id, or null if the id maps to no known script.</summary>
Script? GetById(long id);
/// <summary>Selectors of currently mounted append catalogs. Opcode 0x143 scans these in
/// ascending order and executes record zero from each catalog.</summary>
IReadOnlyList<int> MountedAppendSelectors => Array.Empty<int>();
}

View File

@@ -13,6 +13,7 @@ public sealed class Sys4ScriptProvider : IScriptProvider
public Sys4AssetCatalog Catalog { get; }
public IReadOnlyList<string> ScriptNames => Catalog.ScriptNames;
public IReadOnlyList<int> MountedAppendSelectors => Catalog.AppendPacks.Keys.ToArray();
public Sys4ScriptProvider(OpcodeTable table, Sys4AssetCatalog catalog, IAssetStore store)
{ _table = table; Catalog = catalog; _store = store; }

View File

@@ -1060,6 +1060,45 @@ public sealed class VirtualMachine
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
return pc + 1; // Returned / RanOff: resume caller
}
case "u00415FB0":
case "run-mounted-append-autoruns": // 0x143: selector slots 1..255, packed record zero
{
if (_provider == null) return pc + 1;
// Native first scans every mounted selector into its launch queue, then dispatches
// those packed scripts serially. Snapshot before running any child so script-side
// effects cannot change the current batch.
int[] selectors = _provider.MountedAppendSelectors
.Where(selector => selector is > 0 and <= 0xff)
.Distinct()
.Order()
.ToArray();
foreach (int selector in selectors)
{
if (_depth >= _o.CallDepthCap)
{
HaltReason ??= "call-depth-exceeded";
return HALT;
}
long id = (long)selector << 24;
CallScriptDispatches++;
var child = _provider.GetById(id);
_sink.Emit(TraceEvent.CallScript(id, child?.Name));
if (child == null)
{
HaltReason ??= $"append-autorun-unresolved:0x{id:x}";
return HALT;
}
int entry = child.IndexByOffset.TryGetValue(0, out int childEntry) ? childEntry : 0;
var outcome = RunFrame(new ExecFrame(child, entry), FrameCause.CallScript, id);
if (outcome == FrameOutcome.Halted) return HALT;
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD;
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
}
return pc + 1;
}
case "u00417E80":
case "preload-script-slot": // 0x06 (script_id, frame_slot), valid slots 0..39
{

View File

@@ -43,6 +43,7 @@ INFERRED: dict[int, dict] = {
0x10d: dict(name='consume-mouse-wheel-delta', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the accumulated signed mouse-wheel delta and clear it.'),
0x13a: dict(name='register-numeric-glyph-style', category='draw', noop=False, confidence='high', source='investigation', summary='Register one of 11 decimal-glyph atlas styles as (surface slot, source x/y, digit width/height).'),
0x140: dict(name='coroutine-label-yield', category='control', noop=False, confidence='med', source='investigation', summary="(out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls the NATIVE video/transition service (*DAT_005c6018)(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the video service. See engine-re.md §Scene-coroutine framework."),
0x143: dict(name='run-mounted-append-autoruns', category='control', noop=False, confidence='high', source='investigation', summary='() - enqueue record zero from every mounted nonzero AAI selector in ascending selector order, then execute those packed scripts serially before resuming the caller.'),
0x199: dict(name='yield-adv-coroutine', category='control', noop=False, confidence='high', source='investigation', summary='Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow.'),
0x19a: dict(name='get-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current all-message skip state set by op 0x88.'),
0x19b: dict(name='suspend-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary="() - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle."),

View File

@@ -7,21 +7,6 @@
struct_name = "EngineCtx"
size = 0xa1000
[[field]]
offset = 0x40c
name = "sys4ini_count"
type = "int"
note = "SYS4INI record count"
[[field]]
offset = 0x410
name = "archive_name_table"
type = "void*"
note = "archive-name table base (arc_id*0x100 indexes it)"
[[field]]
offset = 0x414
name = "sys4ini_records"
type = "void*"
note = "SYS4INI 80-byte record base {name[64],arc_id,file_number,offset,size}; record = base + id*0x50"
[[field]]
offset = 0x814
name = "input_action_count"
@@ -63,11 +48,6 @@ name = "mouse_wheel_delta"
type = "int"
note = "signed WM_MOUSEWHEEL delta accumulated by age_main_window_proc; op 0x10d returns and clears it"
[[field]]
offset = 0x3028
name = "alt_pack_table"
type = "int"
note = "call-script high-byte alternate pack table (unused by corpus)"
[[field]]
offset = 0x14d54
name = "gfx_obj_ptr_table"
type = "void*"
@@ -418,6 +398,46 @@ name = "message_skip_queued_voice_arg"
type = "int"
note = "second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero"
[[field]]
offset = 0x6f89c
name = "script_launch_queue"
type = "int"
note = "embedded integer FIFO used for engine-level auxiliary/root script launches; data pointer and queue indices follow"
[[field]]
offset = 0x6f8a0
name = "script_launch_queue_data"
type = "void*"
note = "dword storage for packed script ids or negative frame-resume ids"
[[field]]
offset = 0x6f8a4
name = "script_launch_queue_read_cursor"
type = "int"
note = "next queued launch consumed by script_launch_queue_dispatch_next"
[[field]]
offset = 0x6f8a8
name = "script_launch_queue_write_cursor"
type = "int"
note = "one-past-last queued launch; op 0x143 appends mounted selector record-zero ids here"
[[field]]
offset = 0x6f8ac
name = "script_launch_queue_capacity"
type = "int"
note = "allocated dword capacity; initialized to 0x100"
[[field]]
offset = 0x6f8b0
name = "script_launch_queue_growth"
type = "int"
note = "capacity growth quantum; initialized to 0x100"
[[field]]
offset = 0x6f8b4
name = "script_launch_queue_high_water"
type = "int"
note = "highest consumed cursor retained across FIFO compaction"
[[field]]
offset = 0x6f8b8
name = "script_launch_dispatch_active"
type = "int"
note = "suppresses immediate dispatch while op 0x143 batches mounted append record-zero ids and while a queued script is active"
[[field]]
offset = 0x9928c
name = "save_frame_boundary_index"
type = "int"
@@ -428,6 +448,31 @@ name = "dispatch_table"
type = "void*"
note = "opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4)"
[[field]]
offset = 0x9c658
name = "sys4ini_count"
type = "int"
note = "SYS4INI record count at embedded FileDB+0x40c"
[[field]]
offset = 0x9c65c
name = "archive_name_table"
type = "void*"
note = "archive-name table base at embedded FileDB+0x410; arc_id*0x100 indexes it"
[[field]]
offset = 0x9c660
name = "sys4ini_records"
type = "void*"
note = "SYS4INI 80-byte record base at embedded FileDB+0x414; record = base + id*0x50"
[[field]]
offset = 0x9f274
name = "mounted_aai_catalogs"
type = "void*"
note = "base of 256-entry selector-keyed AAI catalog-pointer table inside the embedded FileDB; op 0x143 scans slots 1..255 from +0x9f278"
[[field]]
offset = 0x9f278
name = "mounted_aai_catalog_selector_1"
type = "void*"
note = "selector-one cell and op 0x143 scan start; subsequent dwords are selector 2..255"
[[field]]
offset = 0xa0cc0
name = "screen_w"
type = "int"

View File

@@ -83,7 +83,7 @@ noop_headless = false
source = "investigation"
confidence = "high"
depends_on = []
evidence = "native-RE (Ghidra): handler FUN_0041bc90 -> loader FUN_0040e980 -> resolver FUN_0044f390 indexes an 80-byte record table (base [ctx+0x414], count [ctx+0x40c]) at base+id*0x50 = the SYS4INI record layout {name[64],arc_id@0x40,file_number@0x44,offset@0x48,size@0x4c}. Confirmed statically: all 297 distinct corpus call-script ids resolve to a .BIN script with a semantically-exact name (0x1ab->ADDITEM, 0x2ae7->MES, 0x143->BUNKI, 0x329d->CALCREVISE), 0 out-of-range, 0 pack-branch. See docs/engine-re.md + name-resolution.md #1."
evidence = "native-RE (Ghidra): handler FUN_0041bc90 -> loader script_frame_load_resource@0x40e980 -> resolver asset_open_indexed_entry@0x44f390 indexes an 80-byte record table (FileDB+0x414 base, FileDB+0x40c count) at base+id*0x50 = the SYS4INI record layout {name[64],arc_id@0x40,file_number@0x44,offset@0x48,size@0x4c}. The FileDB is embedded at EngineCtx+0x9c24c, making those EngineCtx+0x9c660/+0x9c658. Confirmed statically: all 297 distinct base-corpus call-script ids resolve to a .BIN script with a semantically-exact name (0x1ab->ADDITEM, 0x2ae7->MES, 0x143->BUNKI, 0x329d->CALCREVISE), 0 out-of-range. See docs/engine-re.md + name-resolution.md #1."
confirm_by = ""
details = """
op 0x03 (call-script, argc 1): `call-script <id>`. RESOLVED — the id is a direct RAW index into
@@ -95,11 +95,13 @@ Native mechanism (dispatch table `handler(op)=ctx[0x26c93+op]`, op 0x03 -> FUN_0
2. FUN_0040e980 (loader): opens the resource by id, reads the 0x20-byte SYS4 header, checks magic,
allocates per-frame code/local buffers from the header var-counts, reads the bytecode body,
pushes a script frame (stride 0x1e = 30 dwords, indexed by ctx[0x14f45]).
3. FUN_0044f390 (resolver): record = [ctx+0x414] + id*0x50. Tries a LOOSE OVERRIDE first
(CreateFileA on record.name -> mod/patch hook point), else opens archive [record.arc_id*0x100 +
ctx+0x410], SetFilePointer to record.offset, size = record.size.
(High-byte-tagged ids `id & 0xff000000` select an alternate pack via [ctx+0x3028]; UNUSED by the
corpus -- 0/297 ids have a high byte.)
3. asset_open_indexed_entry@0x44f390 (resolver): its `this` is the embedded FileDB, not EngineCtx.
Base record = [FileDB+0x414] + id*0x50. It tries a LOOSE OVERRIDE first (CreateFileA on
record.name -> mod/patch hook point), else opens archive [record.arc_id*0x100 + FileDB+0x410],
SetFilePointer to record.offset, size = record.size.
High-byte-tagged ids select [FileDB+0x3028 + signed_selector*4] and index the chosen AAI by the
low 24 bits. The base corpus has 0/297 explicit high-byte call-script operands; INIT2 op 0x143
supplies mounted record-zero ids dynamically.
Companion op 0x8f `call` is INTRA-script (a local JSR), not cross-script -- see its entry.
This also names the whole call graph statically (build/callscript-names.json).
"""
@@ -2975,14 +2977,29 @@ argc = 0
abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "u00415FB0"
category = "unknown"
summary = ""
name = "run-mounted-append-autoruns"
category = "control"
summary = "() - enqueue record zero from every mounted nonzero AAI selector in ascending selector order, then execute those packed scripts serially before resuming the caller."
noop_headless = false
source = "kelebek"
confidence = "low"
source = "investigation"
confidence = "high"
depends_on = []
evidence = ""
evidence = "Ghidra /v2: op_0x143_run_mounted_append_autoruns@0x4172f0 scans mounted_aai_catalogs[1..255] at EngineCtx+0x9f278, enqueues selector<<24 through script_launch_queue_enqueue@0x40f820 while dispatch is suppressed, advances the caller PC, then tail-dispatches through script_launch_queue_dispatch_next@0x40f6e0. Positive queue entries load in reserved frame 37 with return sentinel -10; op_0x2_exit_or_return_frame@0x417940 dispatches the next queued entry or restores the suspended caller. Corpus: the sole site is INIT2@0x17f, after its 23 base INIT children and global/array setup and immediately before TUNE.BIN."
details = """
The opcode does not look up an AUTORUN filename. For each non-null mounted catalog pointer in selector
slots 1 through 255, it constructs the packed id `selector << 24`, whose low 24-bit record index is zero.
At Himegari cold boot the native catalog loader has already mounted APPEND01 in selector 1 before SYSTEM4
starts. SYSTEM4 calls INIT2; INIT2 runs EBINIT through SCINIT plus BTANINIT2, completes its base registry
setup, executes this opcode, and resumes at its following TUNE call only after every queued record-zero
script returns. Installed selector 1 record zero is `$1$AUTORUN.BIN`, which applies the append INIT deltas
and registrations. This is a serial base-then-append patch boundary, not a filename overlay.
PORT = IMPLEMENTED. `IScriptProvider.MountedAppendSelectors` exposes mounted selector identity without
coupling the VM to the SYS4 catalog type. The opcode snapshots, deduplicates, and sorts selectors, constructs
each packed record-zero id, and runs the resolved script through the ordinary nested-frame machinery before
resuming INIT2. Focused tests protect ordering, packed-id construction, caller suspension, and unresolved
record-zero failure. Natural SYSTEM4 boot proves BTANINIT2 -> `$1$AUTORUN.BIN` -> `$1$EBINIT.BIN` -> TUNE.
"""
[[opcode]]
op = 0x144