diff --git a/docs/asset-resolution-re.md b/docs/asset-resolution-re.md index f8d4fdf..ba1a3fd 100644 --- a/docs/asset-resolution-re.md +++ b/docs/asset-resolution-re.md @@ -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, diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index 7909c93..fd0b65b 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -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) | diff --git a/docs/engine-re.md b/docs/engine-re.md index 01bc3d5..81f9055 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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 ` = 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 diff --git a/docs/name-resolution.md b/docs/name-resolution.md index e724049..ac662a9 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -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) diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 3c986db..53c387a 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -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 `. 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 diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 096ea26..6076008 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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) diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index 4710a85..3364b6d 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -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: diff --git a/engine/Age.Engine.Tests/AppendAutorunTests.cs b/engine/Age.Engine.Tests/AppendAutorunTests.cs new file mode 100644 index 0000000..8bf5986 --- /dev/null +++ b/engine/Age.Engine.Tests/AppendAutorunTests.cs @@ -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 _scripts; + + public MountedProvider(IReadOnlyList selectors, IReadOnlyDictionary scripts) + { + MountedAppendSelectors = selectors; + _scripts = scripts; + } + + public IReadOnlyList MountedAppendSelectors { get; } + public List 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()), + }, Array.Empty()); + var second = ScriptAssembler.Assemble(table, "APPEND_TWO_AUTORUN", new List<(int, Operand[])> + { + (0x50, new[] { G(0x100), G(0x100), I(5) }), + (0x2, Array.Empty()), + }, Array.Empty()); + var root = ScriptAssembler.Assemble(table, "ROOT", new List<(int, Operand[])> + { + (0x143, Array.Empty()), + (0x55, new[] { G(0x101), G(0x100) }), + (0x2, Array.Empty()), + }, Array.Empty()); + var provider = new MountedProvider(new[] { 2, 1, 2 }, new Dictionary + { + [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()), + (0x55, new[] { G(0x100), I(1) }), + (0x2, Array.Empty()), + }, Array.Empty()); + var provider = new MountedProvider(new[] { 1 }, new Dictionary()); + 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); + } +} diff --git a/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs b/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs index 3d6ae08..10fd558 100644 --- a/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs +++ b/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs @@ -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)); diff --git a/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs b/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs index 887da15..3596316 100644 --- a/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs +++ b/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs @@ -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; diff --git a/engine/Age.Engine/Hosting/IScriptProvider.cs b/engine/Age.Engine/Hosting/IScriptProvider.cs index bb75aaa..1edeaae 100644 --- a/engine/Age.Engine/Hosting/IScriptProvider.cs +++ b/engine/Age.Engine/Hosting/IScriptProvider.cs @@ -7,4 +7,8 @@ public interface IScriptProvider { /// The script for this id, or null if the id maps to no known script. Script? GetById(long id); + + /// Selectors of currently mounted append catalogs. Opcode 0x143 scans these in + /// ascending order and executes record zero from each catalog. + IReadOnlyList MountedAppendSelectors => Array.Empty(); } diff --git a/engine/Age.Engine/Sys4/Sys4ScriptProvider.cs b/engine/Age.Engine/Sys4/Sys4ScriptProvider.cs index afee641..100324d 100644 --- a/engine/Age.Engine/Sys4/Sys4ScriptProvider.cs +++ b/engine/Age.Engine/Sys4/Sys4ScriptProvider.cs @@ -13,6 +13,7 @@ public sealed class Sys4ScriptProvider : IScriptProvider public Sys4AssetCatalog Catalog { get; } public IReadOnlyList ScriptNames => Catalog.ScriptNames; + public IReadOnlyList MountedAppendSelectors => Catalog.AppendPacks.Keys.ToArray(); public Sys4ScriptProvider(OpcodeTable table, Sys4AssetCatalog catalog, IAssetStore store) { _table = table; Catalog = catalog; _store = store; } diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index c2a905f..c620078 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -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 { diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index b2e7c01..2b12e3b 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -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."), diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index a840982..aa70f8e 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -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" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index b4b46bd..5aa4276 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -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 `. 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