Implement mounted append AUTORUN boot
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
86
engine/Age.Engine.Tests/AppendAutorunTests.cs
Normal file
86
engine/Age.Engine.Tests/AppendAutorunTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user