Restore complete numbered save state
This commit is contained in:
@@ -16,6 +16,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
|||||||
| `0x1c34` | `mouse_wheel_delta` | `int` | signed WM_MOUSEWHEEL delta accumulated by age_main_window_proc; op 0x10d returns and clears it |
|
| `0x1c34` | `mouse_wheel_delta` | `int` | signed WM_MOUSEWHEEL delta accumulated by age_main_window_proc; op 0x10d returns and clears it |
|
||||||
| `0x4d7c` | `shared_profile_state` | `void*` | embedded shared SAVE.DAT state object; owns profile integer/settings tables and container timing metadata |
|
| `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 |
|
| `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 |
|
||||||
|
| `0x144e0` | `sfx_channel_resource_ids` | `int` | base of ten packed resource ids retained by the sound-effect facade; op 0xb4 loads a channel, op 0xb6 clears it, and numbered-save layouts restore then reopen every positive id |
|
||||||
| `0x14d54` | `gfx_obj_ptr_table` | `void*` | per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c) |
|
| `0x14d54` | `gfx_obj_ptr_table` | `void*` | per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c) |
|
||||||
| `0x14e08` | `gfx_default_object_slot` | `int` | op 0x80 selected slot; op 0x1d9 substitutes it when its explicit object-slot operand is zero |
|
| `0x14e08` | `gfx_default_object_slot` | `int` | op 0x80 selected slot; op 0x1d9 substitutes it when its explicit object-slot operand is zero |
|
||||||
| `0x14ea0` | `text_line_spacing` | `int` | extra pixel leading between text lines; defaults to 6, op 0x8b writes it |
|
| `0x14ea0` | `text_line_spacing` | `int` | extra pixel leading between text lines; defaults to 6, op 0x8b writes it |
|
||||||
@@ -99,6 +100,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
|||||||
| `0x9c660` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base at embedded FileDB+0x414; record = base + id*0x50 |
|
| `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 |
|
| `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 |
|
| `0x9f278` | `mounted_aai_catalog_selector_1` | `void*` | selector-one cell and op 0x143 scan start; subsequent dwords are selector 2..255 |
|
||||||
|
| `0xa0b84` | `current_bgm_track_id` | `int` | direct-name BGM track id retained by the music facade; op 0xbf starts/replaces it, ops 0xc0/0xc3 get/set it, and numbered-save layouts restore it |
|
||||||
| `0xa0cc0` | `screen_w` | `int` | logical screen width; constructor/default registry uses 640, then the SYS4INI SCREENX setting overrides it (Himegari 800) |
|
| `0xa0cc0` | `screen_w` | `int` | logical screen width; constructor/default registry uses 640, then the SYS4INI SCREENX setting overrides it (Himegari 800) |
|
||||||
| `0xa0cc4` | `screen_h` | `int` | logical screen height; constructor/default registry uses 480, then the SYS4INI SCREENY setting overrides it (Himegari 600) |
|
| `0xa0cc4` | `screen_h` | `int` | logical screen height; constructor/default registry uses 480, then the SYS4INI SCREENY setting overrides it (Himegari 600) |
|
||||||
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |
|
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |
|
||||||
|
|||||||
@@ -1751,12 +1751,30 @@ diagnostic/extended-mode surface.
|
|||||||
|
|
||||||
The port tracks the native global banks separately at runtime, captures the marked frame chain, serializes
|
The port tracks the native global banks separately at runtime, captures the marked frame chain, serializes
|
||||||
live surfaces and retained objects, and reloads host textures from the 20-byte surface records. Full load
|
live surfaces and retained objects, and reloads host textures from the 20-byte surface records. Full load
|
||||||
first replaces banks/history/gfx, unwinds the obsolete managed call chain, runs `CALLBACK_LOAD.BIN` when
|
first replaces each serialized mutable bank prefix while preserving initialization-authored cells beyond
|
||||||
|
its count, restores history/gfx and retained audio, unwinds the obsolete managed call chain, runs
|
||||||
|
`CALLBACK_LOAD.BIN` when
|
||||||
the mounted script provider resolves it, starts the saved root at its `0xae` rendezvous, recursively
|
the mounted script provider resolves it, starts the saved root at its `0xae` rendezvous, recursively
|
||||||
reconstructs child frames, resumes parents after their saved T2 call sites,
|
reconstructs child frames, resumes parents after their saved T2 call sites,
|
||||||
and finally resumes the terminal frame at its T1 boundary. Successful `0x19e` also flushes shared
|
and finally resumes the terminal frame at its T1 boundary. Successful `0x19e` also flushes shared
|
||||||
`SAVE.DAT`/`RT.DAT`, matching `context_state_serialize`.
|
`SAVE.DAT`/`RT.DAT`, matching `context_state_serialize`.
|
||||||
|
|
||||||
|
The prefix boundary is observable in the installed file: integer count `0x6241b` stops before the
|
||||||
|
initialization-authored unit/stage definition tables, and string count `0x315` stops exactly before
|
||||||
|
`unit_story_display_names`. Native `save_data_deserialize_and_begin_restore@0x40fd10` zeroes only those
|
||||||
|
counted regions; the port's former whole-dictionary clear erased the definitions needed by CHMENU,
|
||||||
|
unit-management, and SELSTAGE after load.
|
||||||
|
|
||||||
|
The fixed audio fields are also resolved. Payload `+0x008` is
|
||||||
|
`EngineCtx.current_bgm_track_id` (`ctx+0xa0b84`), maintained by
|
||||||
|
`bgm_play_track@0x464750` and queried by op `0xc0`; the installed FORT save contains `0x18`, matching
|
||||||
|
FORT's BGM024 command immediately before its T1 resume boundary. Payload `+0x00c..+0x033` copies
|
||||||
|
`EngineCtx.sfx_channel_resource_ids[10]` (`ctx+0x144e0`), and
|
||||||
|
`sfx_reload_saved_channels@0x482640` reopens each positive packed id after deserialization. The port now
|
||||||
|
tracks both lifecycles through `0xbf`/`0xc2` and `0xb4`/`0xb6`, serializes them, and restores the active
|
||||||
|
track plus saved SFX resources. The new EngineCtx fields and function annotations are applied to the
|
||||||
|
saved `/v2` image.
|
||||||
|
|
||||||
### Opcode `0xae` continues numbered-save stack restoration (2026-07-20)
|
### Opcode `0xae` continues numbered-save stack restoration (2026-07-20)
|
||||||
|
|
||||||
Opcode `0xae` is the load-side rendezvous paired with serialized script-frame state. Its handler,
|
Opcode `0xae` is the load-side rendezvous paired with serialized script-frame state. Its handler,
|
||||||
@@ -1776,6 +1794,16 @@ coroutine-resume or call boundaries, including SC0000's main-loop resume sequenc
|
|||||||
The port now implements both branches: an ordinary one-instruction no-op outside restoration, and the
|
The port now implements both branches: an ordinary one-instruction no-op outside restoration, and the
|
||||||
T1/T2/T3-driven managed-frame reconstruction described above while a full numbered load is active.
|
T1/T2/T3-driven managed-frame reconstruction described above while a full numbered load is active.
|
||||||
|
|
||||||
|
The first interactive installed-save continuation exposed a dispatch-label integration error rather than
|
||||||
|
a native semantic error. Although `0xae` had this mapped semantic record and a VM case, its opcode-table
|
||||||
|
label still used upstream placeholder `u00415130`; the VM switches on the label, so the active case was
|
||||||
|
unreachable. The saved root entered `SYSTEM4.BIN` with restore state intact, but `0xae` fell through and
|
||||||
|
SYSTEM4 ran its ordinary `LOGO.BIN → OP.BIN → INIT.BIN → TITLE.BIN` boot path. The source label is now
|
||||||
|
`continue-save-load-stack-restore`. A real installed-save gate executes beyond the rendezvous and proves
|
||||||
|
`SYSTEM4.BIN → FORT.BIN → CHMENU.BIN` restoration to the gameplay poll. The synthetic two-frame gate
|
||||||
|
also asserts that its child enters with `FrameCause.SaveRestore`, preventing its ordinary call site from
|
||||||
|
masking another inactive-branch regression.
|
||||||
|
|
||||||
### ADV read-message Skip and shared `RT.DAT` history (2026-07-18)
|
### ADV read-message Skip and shared `RT.DAT` history (2026-07-18)
|
||||||
|
|
||||||
Read-message Skip is backed by an engine-owned `ReadTextDB`, not a VM-global flag and not ordinary numbered
|
Read-message Skip is backed by an engine-owned `ReadTextDB`, not a VM-global flag and not ordinary numbered
|
||||||
|
|||||||
@@ -167,6 +167,11 @@ Port status (2026-07-24): after the blocking host releases this wait, the VM que
|
|||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry. Diagnostic: `Age.Cli audio SC0000.BIN`.
|
- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry. Diagnostic: `Age.Cli audio SC0000.BIN`.
|
||||||
|
|
||||||
|
### 0xc0 `get-current-bgm-track` (get-current-bgm-track, argc 1)
|
||||||
|
- **summary:** (track_out) - return the direct-name BGM track id retained by the music facade; the same value is restored from numbered saves.
|
||||||
|
- **grounding:** source=investigation, confidence=high
|
||||||
|
- **evidence:** Ghidra /v2 op_0xc0_get_current_bgm_track@0x428440 writes EngineCtx.current_bgm_track_id (+0xa0b84) to operand 1. op 0xbf's bgm_play_track retains the selected direct-name id at the same music-facade field; layout-3 serialization copies it to payload +0x008 and restoration copies it back. Installed SAVE00 stores 0x18, matching FORT's BGM024.
|
||||||
|
|
||||||
### 0xc2 `fade-bgm` (u0041D2B0, argc 2)
|
### 0xc2 `fade-bgm` (u0041D2B0, argc 2)
|
||||||
- **summary:** (target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.
|
- **summary:** (target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
@@ -397,12 +402,12 @@ Implemented as a whole-stack root-reload boundary in the persistent VM. A reques
|
|||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra /v2: op_0xa3_handler@0x420060 formats operand 1, queries value_dispatch_lookup@0x419290, writes the matched or operand-2 default PC into the current frame, and clears the command type. Corpus pairs it with 0xa1/0xa2 in 12 generic switch sequences.
|
- **evidence:** Ghidra /v2: op_0xa3_handler@0x420060 formats operand 1, queries value_dispatch_lookup@0x419290, writes the matched or operand-2 default PC into the current frame, and clears the command type. Corpus pairs it with 0xa1/0xa2 in 12 generic switch sequences.
|
||||||
|
|
||||||
### 0xae `continue-save-load-stack-restore` (u00415130, argc 0)
|
### 0xae `continue-save-load-stack-restore` (continue-save-load-stack-restore, argc 0)
|
||||||
- **summary:** () - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.
|
- **summary:** () - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra /v2: op_0xae_continue_save_load_stack_restore@0x416790 first tests ctx+0x53d24 (set by save_data_deserialize_and_begin_restore@0x40fd10). When clear it returns. When set, it selects the serialized frame layout through set:SaveVersion1/2, restores the current PC from that layout's saved return/call target, advances through contexts with script_frame_restore_saved_layout@0x40f2d0, and clears the restore flag on reaching the saved terminal context. Its 305 corpus sites overwhelmingly follow coroutine-resume/call boundaries, which provide the rendezvous points used while reconstructing the stack.
|
- **evidence:** Ghidra /v2: op_0xae_continue_save_load_stack_restore@0x416790 first tests ctx+0x53d24 (set by save_data_deserialize_and_begin_restore@0x40fd10). When clear it returns. When set, it selects the serialized frame layout through set:SaveVersion1/2, restores the current PC from that layout's saved return/call target, advances through contexts with script_frame_restore_saved_layout@0x40f2d0, and clears the restore flag on reaching the saved terminal context. Its 305 corpus sites overwhelmingly follow coroutine-resume/call boundaries, which provide the rendezvous points used while reconstructing the stack.
|
||||||
|
|
||||||
Layout 3 frame d259 indexes SYS4 T1 read-message reset sites, d260 indexes T2 call-script sites, and the saved local return stack indexes T3 local-call sites. Port status (2026-07-24): the active path reconstructs the saved recursive frame chain and resumes the terminal frame at its T1 boundary.
|
Layout 3 frame d259 indexes SYS4 T1 read-message reset sites, d260 indexes T2 call-script sites, and the saved local return stack indexes T3 local-call sites. Port status (2026-07-24): the active path reconstructs the saved recursive frame chain and resumes the terminal frame at its T1 boundary. The installed SAVE00 continuation gate proves SYSTEM4 -> FORT restoration reaches FORT's CHMENU gameplay poll; the synthetic gate asserts the child frame enters with SaveRestore rather than ordinary CallScript cause.
|
||||||
|
|
||||||
### 0xc8 `sleep` (sleep, argc 1)
|
### 0xc8 `sleep` (sleep, argc 1)
|
||||||
- **summary:** Pause the current script for <duration> milliseconds while retained presentation continues.
|
- **summary:** Pause the current script for <duration> milliseconds while retained presentation continues.
|
||||||
@@ -1267,10 +1272,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
|
|||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|
||||||
### 0xc0 `u00415620` (u00415620, argc 1)
|
|
||||||
- **summary:** —
|
|
||||||
- **grounding:** source=kelebek, confidence=low
|
|
||||||
|
|
||||||
### 0xc5 `u0041D4A0` (u0041D4A0, argc 2)
|
### 0xc5 `u0041D4A0` (u0041D4A0, argc 2)
|
||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|||||||
@@ -3546,6 +3546,57 @@ and unnamed retained-gfx fields outside the confirmed Himegari compatibility cla
|
|||||||
This documentation-only closeout does not change runtime persistence. The next functional check remains
|
This documentation-only closeout does not change runtime persistence. The next functional check remains
|
||||||
continuing from the installed slot through the restored gameplay frame.
|
continuing from the installed slot through the restored gameplay frame.
|
||||||
|
|
||||||
|
### Persistence implementation step 7 — installed gameplay continuation correction (2026-07-24)
|
||||||
|
|
||||||
|
Interactive loading of slot 000 reached `SYSTEM4.BIN` as a restore but then played `LOGO.BIN` and
|
||||||
|
`OP.BIN` before returning to a broken TITLE state. The installed-save regression had stopped on frame
|
||||||
|
entry, before executing the saved root's `0xae`, while the synthetic two-frame test accidentally made
|
||||||
|
the same child call through its ordinary script path.
|
||||||
|
|
||||||
|
The restore state and decoded frames were correct (`SYSTEM4.BIN` root plus terminal `FORT.BIN`). The
|
||||||
|
actual integration error was the opcode registry: `0xae` had the implemented semantic case but retained
|
||||||
|
placeholder label `u00415130`, and VM dispatch selects cases by label. It therefore behaved as an
|
||||||
|
unhandled stub. The source label is now `continue-save-load-stack-restore` and generated opcode
|
||||||
|
references were rebuilt.
|
||||||
|
|
||||||
|
The synthetic regression now requires the child to enter with `FrameCause.SaveRestore` and rejects an
|
||||||
|
ordinary `CallScript` entry. A new read-only installed-save continuation gate traverses the real
|
||||||
|
`SAVE.BIN → CALLBACK_LOAD.BIN → SYSTEM4.BIN → FORT.BIN → CHMENU.BIN` path and stops at FORT's stable
|
||||||
|
gameplay poll; the original AppData files remain untouched.
|
||||||
|
|
||||||
|
Validation: all 387 engine tests pass, opcode lint reports zero errors/warnings, the Godot C# build has
|
||||||
|
zero warnings, and the threaded headless run reports `SELFTEST OK`. The next acceptance action is a
|
||||||
|
visual retry of slot 000 in the rebuilt port.
|
||||||
|
|
||||||
|
### Persistence implementation step 8 — preserve initialized banks and restore retained audio (2026-07-24)
|
||||||
|
|
||||||
|
The first successful FORT continuation exposed a second load-state boundary. CHMENU retained party-slot
|
||||||
|
shells but lacked names/definition data, SELSTAGE listed no stages, unit-management was blank, and the
|
||||||
|
TITLE BGM continued playing. Reaching FORT through the developer launcher populated the same consumers,
|
||||||
|
localizing the issue to restoration rather than their rendering opcodes.
|
||||||
|
|
||||||
|
Layout 3 stores mutable prefixes, not the complete initialized typed banks. The installed integer count
|
||||||
|
is `0x6241b`, before unit/stage definitions including `0x66716` and `0xe8275`; the installed string count
|
||||||
|
is `0x315`, exactly where `unit_story_display_names` begins. Native deserialization zeroes only the
|
||||||
|
counted prefixes. The port instead cleared whole dictionaries, erasing the initialization-authored tail.
|
||||||
|
Full load now removes and replaces only keys below each saved count, including saved zeroes, and preserves
|
||||||
|
every initialized key at or above the boundary.
|
||||||
|
|
||||||
|
Native RE also resolves the fixed audio words. Payload `+0x008` is the current direct-name BGM track;
|
||||||
|
the installed value `0x18` matches FORT's skipped BGM024 instruction. The following ten DWORDs are packed
|
||||||
|
SFX resource ids; the installed save retains SE015 and SE020 in channels 1 and 2. VM audio operations now
|
||||||
|
track these values, numbered saves emit them, and full restoration replaces the BGM plus reloads retained
|
||||||
|
SFX channels. Opcode `0xc0` is mapped as the current-BGM query. Ghidra `/v2` names/comments the BGM
|
||||||
|
getter/setter/player and saved-SFX reload worker, includes the two new EngineCtx fields, and is saved.
|
||||||
|
|
||||||
|
Regressions cover saved-zero replacement, preservation at integer/string prefix boundaries, BGM/SFX
|
||||||
|
round trips, the `0xc0` query, and installed SAVE00 continuation with its matching append-install mask.
|
||||||
|
Validation: all 387 engine tests pass, opcode and EngineCtx generation/lints are clean, the Godot C#
|
||||||
|
build has zero warnings, and the threaded headless run reports `SELFTEST OK`. Manual slot-000 acceptance
|
||||||
|
then confirmed that CHMENU, unit management, SELSTAGE, and FORT audio populate correctly. The remaining
|
||||||
|
visible load-specific discrepancy is the reusable choice-box frame losing its top winged flourish after
|
||||||
|
restoration; that is the next independent investigation slice.
|
||||||
|
|
||||||
## Data-semantics sidebar: focused append EBINIT inspection (2026-07-24)
|
## Data-semantics sidebar: focused append EBINIT inspection (2026-07-24)
|
||||||
|
|
||||||
The static INIT surface now accepts a universal packed script id for focused append inspection.
|
The static INIT surface now accepts a universal packed script id for focused append inspection.
|
||||||
|
|||||||
@@ -473,9 +473,13 @@ import/export and the packed-script/T1 ReadTextDB queue/commit/query lifecycle a
|
|||||||
including `message:ReadTextSkip` ops `0x1ca`/`0x1cb` and state query `0x1cc`. Numbered active-frame state
|
including `message:ReadTextSkip` ops `0x1ca`/`0x1cb` and state query `0x1cc`. Numbered active-frame state
|
||||||
is now implemented in native layout 3: metadata query, paired `.DAT`/`.STH` lifecycle, exact native BMP
|
is now implemented in native layout 3: metadata query, paired `.DAT`/`.STH` lifecycle, exact native BMP
|
||||||
thumbnail I/O, six global banks, retained surface/gfx state, history, and nested frame restoration through
|
thumbnail I/O, six global banks, retained surface/gfx state, history, and nested frame restoration through
|
||||||
the `0xae` rendezvous. The real `SAVE.BIN` script is covered end to end for listing and loading, including
|
the `0xae` rendezvous. The real `SAVE.BIN` script is covered end to end for listing and loading; the
|
||||||
a read-only installed-save compatibility gate. JSON inspection/export, namespaced mod data, and migrations
|
read-only installed-save gate now continues through `CALLBACK_LOAD`, reconstructs
|
||||||
remain additive extended-mode work rather than 1.0 compatibility requirements.
|
`SYSTEM4.BIN → FORT.BIN`, and reaches FORT's `CHMENU` gameplay poll. Full restoration replaces only the
|
||||||
|
serialized mutable bank prefixes (preserving initialized unit/stage/string definitions), restores the
|
||||||
|
retained BGM/SFX state, and rebuilds graphics/history/frame state. JSON inspection/export,
|
||||||
|
namespaced mod data, and migrations remain additive extended-mode work rather than 1.0 compatibility
|
||||||
|
requirements.
|
||||||
|
|
||||||
### Phase C — Externalize & modding foundation
|
### Phase C — Externalize & modding foundation
|
||||||
- Add **editable named data overlays** mapped explicitly onto the VM's `*INIT`-produced state; external
|
- Add **editable named data overlays** mapped explicitly onto the VM's `*INIT`-produced state; external
|
||||||
|
|||||||
@@ -198,8 +198,8 @@ The decoded layout-3 body begins:
|
|||||||
|---:|---:|---|
|
|---:|---:|---|
|
||||||
| `0x000` | 4 | terminal saved-frame index, called `cutoff`; frame count is `cutoff + 1` |
|
| `0x000` | 4 | terminal saved-frame index, called `cutoff`; frame count is `cutoff + 1` |
|
||||||
| `0x004` | 4 | saved frame-owner/context word |
|
| `0x004` | 4 | saved frame-owner/context word |
|
||||||
| `0x008` | 4 | engine-state word |
|
| `0x008` | 4 | current direct-name BGM track id |
|
||||||
| `0x00c` | `0x28` | ten engine/context state DWORDs |
|
| `0x00c` | `0x28` | ten packed SFX resource ids, one per retained channel |
|
||||||
| `0x034` | `0x4b0` | 100 resource-reload records of three DWORDs |
|
| `0x034` | `0x4b0` | 100 resource-reload records of three DWORDs |
|
||||||
| `0x4e4` | `0x4e20` | 1,000 surface-reload records of 20 bytes |
|
| `0x4e4` | `0x4e20` | 1,000 surface-reload records of 20 bytes |
|
||||||
| `0x5304` | `(cutoff + 1) * 0x414` | saved script-frame records |
|
| `0x5304` | `(cutoff + 1) * 0x414` | saved script-frame records |
|
||||||
@@ -233,6 +233,13 @@ The integer and float arrays follow their counts. String count comes from bank c
|
|||||||
begins with an additional `string_blob_dwords` followed by that many bytes of concatenated NUL-terminated
|
begins with an additional `string_blob_dwords` followed by that many bytes of concatenated NUL-terminated
|
||||||
CP932 strings and DWORD padding. The three pointer-family DWORD arrays follow.
|
CP932 strings and DWORD padding. The three pointer-family DWORD arrays follow.
|
||||||
|
|
||||||
|
These counts are serialized mutable-prefix lengths, not declarations that every typed runtime bank ends
|
||||||
|
there. Native deserialization zeroes and replaces only each counted prefix. Initialization-authored
|
||||||
|
definitions after the prefix remain live: in Himegari the saved integer prefix ends at `0x6241b`, before
|
||||||
|
unit/stage definition tables such as `0x66716` and `0xe8275`, while the saved string prefix ends at
|
||||||
|
`0x315`, exactly where `unit_story_display_names` begins. Clearing the whole runtime dictionary during
|
||||||
|
import therefore destroys data that is intentionally absent from the numbered file.
|
||||||
|
|
||||||
Retained graphics then uses:
|
Retained graphics then uses:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -252,7 +259,8 @@ records actually written (`0x2e1 + object_count * 0x2d8` DWORDs in the graphics
|
|||||||
zero/slack bytes after the meaningful range record.
|
zero/slack bytes after the meaningful range record.
|
||||||
|
|
||||||
The installed `SAVE00.DAT` validates the complete layout-3 decode: cutoff 1, global-bank counts
|
The installed `SAVE00.DAT` validates the complete layout-3 decode: cutoff 1, global-bank counts
|
||||||
`[402459,1,789,1,1,1]`, and 211 retained graphics objects.
|
`[402459,1,789,1,1,1]`, current BGM id `0x18`, retained SFX ids `0x3321` (channel 1) and
|
||||||
|
`0x2aea` (channel 2), and 211 retained graphics objects.
|
||||||
|
|
||||||
#### Appended text-history tail
|
#### Appended text-history tail
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ public class NativeNumberedSaveCodecTests
|
|||||||
NativeNumberedSaveState state = NativeNumberedSaveCodec.Empty(frames) with
|
NativeNumberedSaveState state = NativeNumberedSaveCodec.Empty(frames) with
|
||||||
{
|
{
|
||||||
SavedFrameOwner = 7,
|
SavedFrameOwner = 7,
|
||||||
EngineState = 9,
|
BgmTrackId = 9,
|
||||||
StateWords = Enumerable.Range(10, 10).ToArray(),
|
SoundEffectResourceIds = Enumerable.Range(10, 10).ToArray(),
|
||||||
IntegerGlobals = new[] { 12, -3, 0x12345678 },
|
IntegerGlobals = new[] { 12, -3, 0x12345678 },
|
||||||
FloatGlobals = new[] { BitConverter.SingleToInt32Bits(1.25f) },
|
FloatGlobals = new[] { BitConverter.SingleToInt32Bits(1.25f) },
|
||||||
StringGlobals = new[] { "姫狩り", "", "save" },
|
StringGlobals = new[] { "姫狩り", "", "save" },
|
||||||
@@ -38,8 +38,8 @@ public class NativeNumberedSaveCodecTests
|
|||||||
NativeNumberedSaveState decoded = NativeNumberedSaveCodec.Decode(encoded);
|
NativeNumberedSaveState decoded = NativeNumberedSaveCodec.Decode(encoded);
|
||||||
|
|
||||||
Assert.Equal(state.SavedFrameOwner, decoded.SavedFrameOwner);
|
Assert.Equal(state.SavedFrameOwner, decoded.SavedFrameOwner);
|
||||||
Assert.Equal(state.EngineState, decoded.EngineState);
|
Assert.Equal(state.BgmTrackId, decoded.BgmTrackId);
|
||||||
Assert.Equal(state.StateWords, decoded.StateWords);
|
Assert.Equal(state.SoundEffectResourceIds, decoded.SoundEffectResourceIds);
|
||||||
Assert.Equal(state.Frames[0].ParentContext, decoded.Frames[0].ParentContext);
|
Assert.Equal(state.Frames[0].ParentContext, decoded.Frames[0].ParentContext);
|
||||||
Assert.Equal(state.Frames[0].ScriptId, decoded.Frames[0].ScriptId);
|
Assert.Equal(state.Frames[0].ScriptId, decoded.Frames[0].ScriptId);
|
||||||
Assert.Equal(state.Frames[0].ReturnIndices, decoded.Frames[0].ReturnIndices);
|
Assert.Equal(state.Frames[0].ReturnIndices, decoded.Frames[0].ReturnIndices);
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ public class NumberedSaveVmTests
|
|||||||
var store = new DirectoryNativeDatStore(root, Identity);
|
var store = new DirectoryNativeDatStore(root, Identity);
|
||||||
Script script = WithPackedId(ScriptAssembler.Assemble(Table, "SAVE_TEST.BIN",
|
Script script = WithPackedId(ScriptAssembler.Assemble(Table, "SAVE_TEST.BIN",
|
||||||
[
|
[
|
||||||
|
(0xbf, [new Operand(Immediate, 24)]),
|
||||||
|
(0xc0, [new Operand(GlobalInt, 0x21)]),
|
||||||
|
(0xb4, [new Operand(Immediate, 0x3321), new Operand(Immediate, 1)]),
|
||||||
(0x1ad, Array.Empty<Operand>()),
|
(0x1ad, Array.Empty<Operand>()),
|
||||||
(0x19e, [new Operand(GlobalInt, 0x20), new Operand(Immediate, 2)]),
|
(0x19e, [new Operand(GlobalInt, 0x20), new Operand(Immediate, 2)]),
|
||||||
(0x2, Array.Empty<Operand>()),
|
(0x2, Array.Empty<Operand>()),
|
||||||
@@ -40,10 +43,13 @@ public class NumberedSaveVmTests
|
|||||||
vm.Run();
|
vm.Run();
|
||||||
|
|
||||||
Assert.Equal(0, vm.Globals[0x20]);
|
Assert.Equal(0, vm.Globals[0x20]);
|
||||||
|
Assert.Equal(24, vm.Globals[0x21]);
|
||||||
NativeNumberedSaveFile file = store.LoadNumberedFile(2)!;
|
NativeNumberedSaveFile file = store.LoadNumberedFile(2)!;
|
||||||
NativeNumberedSaveState state = NativeNumberedSaveCodec.Decode(file.Document.Payload);
|
NativeNumberedSaveState state = NativeNumberedSaveCodec.Decode(file.Document.Payload);
|
||||||
Assert.Equal(0x6241b, state.IntegerGlobals.Count);
|
Assert.Equal(0x6241b, state.IntegerGlobals.Count);
|
||||||
Assert.Equal(456, state.IntegerGlobals[0x123]);
|
Assert.Equal(456, state.IntegerGlobals[0x123]);
|
||||||
|
Assert.Equal(24, state.BgmTrackId);
|
||||||
|
Assert.Equal(0x3321, state.SoundEffectResourceIds[1]);
|
||||||
Assert.Equal("姫狩り", state.StringGlobals[4]);
|
Assert.Equal("姫狩り", state.StringGlobals[4]);
|
||||||
Assert.Equal(0x77u, state.Frames.Single().ScriptId);
|
Assert.Equal(0x77u, state.Frames.Single().ScriptId);
|
||||||
Assert.Contains(state.GfxObjects, item => item.Handle == 100);
|
Assert.Contains(state.GfxObjects, item => item.Handle == 100);
|
||||||
@@ -97,6 +103,12 @@ public class NumberedSaveVmTests
|
|||||||
new NativeSavedScriptFrame(0, 0x89, Array.Empty<int>(), -1, -1),
|
new NativeSavedScriptFrame(0, 0x89, Array.Empty<int>(), -1, -1),
|
||||||
]) with
|
]) with
|
||||||
{
|
{
|
||||||
|
BgmTrackId = 24,
|
||||||
|
SoundEffectResourceIds =
|
||||||
|
[
|
||||||
|
0, 0x3321, 0x2aea, 0, 0,
|
||||||
|
0, 0, 0, 0, 0,
|
||||||
|
],
|
||||||
IntegerGlobals = DenseIntBank(0x124, (0x123, 456)),
|
IntegerGlobals = DenseIntBank(0x124, (0x123, 456)),
|
||||||
FloatGlobals = [BitConverter.SingleToInt32Bits(3.5f)],
|
FloatGlobals = [BitConverter.SingleToInt32Bits(3.5f)],
|
||||||
StringGlobals = ["復帰"],
|
StringGlobals = ["復帰"],
|
||||||
@@ -113,8 +125,10 @@ public class NumberedSaveVmTests
|
|||||||
1, NativeNumberedSaveCodec.Encode(state), NativeTextHistoryCodec.Encode(history),
|
1, NativeNumberedSaveCodec.Encode(state), NativeTextHistoryCodec.Encode(history),
|
||||||
NativeSystemTime.FromLocalDateTime(DateTime.Now), 123);
|
NativeSystemTime.FromLocalDateTime(DateTime.Now), 123);
|
||||||
var liveHistory = new AdvTextHistory();
|
var liveHistory = new AdvTextHistory();
|
||||||
|
var trace = new RecordingTraceSink();
|
||||||
|
var host = new RecordingHost();
|
||||||
var vm = new VirtualMachine(
|
var vm = new VirtualMachine(
|
||||||
loader, Table, new RecordingHost(), provider: new MapProvider(
|
loader, Table, host, provider: new MapProvider(
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
[0x88] = resumed,
|
[0x88] = resumed,
|
||||||
@@ -124,22 +138,42 @@ public class NumberedSaveVmTests
|
|||||||
{
|
{
|
||||||
["CALLBACK_LOAD.BIN"] = callback,
|
["CALLBACK_LOAD.BIN"] = callback,
|
||||||
}),
|
}),
|
||||||
textHistory: liveHistory, nativeDatStore: store);
|
sink: trace, textHistory: liveHistory, nativeDatStore: store);
|
||||||
|
vm.Globals[0x10] = 999;
|
||||||
vm.Globals[0x123] = 999;
|
vm.Globals[0x123] = 999;
|
||||||
|
vm.Globals[0x124] = 777;
|
||||||
|
vm.GlobalStrings[0] = "stale";
|
||||||
|
vm.GlobalStrings[1] = "static-unit-name";
|
||||||
|
|
||||||
vm.Run();
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.False(vm.Globals.ContainsKey(0x10));
|
||||||
Assert.Equal(456, vm.Globals[0x123]);
|
Assert.Equal(456, vm.Globals[0x123]);
|
||||||
|
Assert.Equal(777, vm.Globals[0x124]);
|
||||||
Assert.Equal(456, vm.Globals[0x500]);
|
Assert.Equal(456, vm.Globals[0x500]);
|
||||||
Assert.Equal(456, vm.Globals[0x501]);
|
Assert.Equal(456, vm.Globals[0x501]);
|
||||||
Assert.Equal(1, vm.Globals[0x502]);
|
Assert.Equal(1, vm.Globals[0x502]);
|
||||||
Assert.Equal("復帰", vm.GlobalStrings[0]);
|
Assert.Equal("復帰", vm.GlobalStrings[0]);
|
||||||
Assert.Equal(0x123, vm.GlobalPointers[0]);
|
Assert.Equal(0x123, vm.GlobalPointers[0]);
|
||||||
|
Assert.Equal("static-unit-name", vm.GlobalStrings[1]);
|
||||||
|
Assert.Equal([24L], host.BgmTracks);
|
||||||
|
Assert.Equal(
|
||||||
|
[(0x3321L, 1), (0x2aeaL, 2)],
|
||||||
|
host.SfxLoads);
|
||||||
|
Assert.Equal(Enumerable.Range(0, 10), host.SfxReleases);
|
||||||
Assert.Equal("履歴復帰", liveHistory.Records.Single().Text);
|
Assert.Equal("履歴復帰", liveHistory.Records.Single().Text);
|
||||||
Assert.Equal(3, vm.Gfx.QuerySlot(100));
|
Assert.Equal(3, vm.Gfx.QuerySlot(100));
|
||||||
RenderObject restoredObject = Assert.Single(vm.Gfx.SnapshotVisibleObjects());
|
RenderObject restoredObject = Assert.Single(vm.Gfx.SnapshotVisibleObjects());
|
||||||
Assert.Equal(0x1234, restoredObject.SurfaceResId);
|
Assert.Equal(0x1234, restoredObject.SurfaceResId);
|
||||||
Assert.Equal((50, 60), (restoredObject.DstX, restoredObject.DstY));
|
Assert.Equal((50, 60), (restoredObject.DstX, restoredObject.DstY));
|
||||||
|
Assert.Contains(trace.Events, item =>
|
||||||
|
item.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter
|
||||||
|
&& item.Name == "CHILD.BIN"
|
||||||
|
&& item.Cause == Age.Engine.Diagnostics.FrameCause.SaveRestore);
|
||||||
|
Assert.DoesNotContain(trace.Events, item =>
|
||||||
|
item.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter
|
||||||
|
&& item.Name == "CHILD.BIN"
|
||||||
|
&& item.Cause == Age.Engine.Diagnostics.FrameCause.CallScript);
|
||||||
Assert.Equal("exit", vm.HaltReason);
|
Assert.Equal("exit", vm.HaltReason);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ public class SaveUiIntegrationTests
|
|||||||
{
|
{
|
||||||
private sealed class MenuReadyException : Exception;
|
private sealed class MenuReadyException : Exception;
|
||||||
private sealed class InstalledResumeReachedException : Exception;
|
private sealed class InstalledResumeReachedException : Exception;
|
||||||
|
private sealed class InstalledGameplayPollReachedException : Exception;
|
||||||
|
private sealed class InstalledRootReloadReachedException(string message) : Exception(message);
|
||||||
|
|
||||||
private sealed class StopAtFirstMenuPollHost : RecordingHost
|
private sealed class StopAtFirstMenuPollHost : RecordingHost
|
||||||
{
|
{
|
||||||
@@ -19,7 +21,7 @@ public class SaveUiIntegrationTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ClickFirstSlotHost : RecordingHost
|
private class ClickFirstSlotHost : RecordingHost
|
||||||
{
|
{
|
||||||
private long _now;
|
private long _now;
|
||||||
private bool _pressed;
|
private bool _pressed;
|
||||||
@@ -45,6 +47,53 @@ public class SaveUiIntegrationTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class InstalledContinuationState(int expectedRestoreFrames)
|
||||||
|
{
|
||||||
|
public int RestoreFrameCount;
|
||||||
|
public bool TerminalRestoreEntered => RestoreFrameCount >= expectedRestoreFrames;
|
||||||
|
public readonly List<string> Frames = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ContinueInstalledLoadHost(InstalledContinuationState state) : ClickFirstSlotHost
|
||||||
|
{
|
||||||
|
public override void WaitForInput(
|
||||||
|
int layoutSlot, Func<bool> serviceInputCallback, Func<AdvAutoWaitState> autoWaitState)
|
||||||
|
{
|
||||||
|
if (state.TerminalRestoreEntered) throw new InstalledGameplayPollReachedException();
|
||||||
|
base.WaitForInput(layoutSlot, serviceInputCallback, autoWaitState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Sleep(long duration)
|
||||||
|
{
|
||||||
|
base.Sleep(duration);
|
||||||
|
if (state.TerminalRestoreEntered) throw new InstalledGameplayPollReachedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class InstalledContinuationSink(InstalledContinuationState state) : ITraceSink
|
||||||
|
{
|
||||||
|
public bool TracingSteps => false;
|
||||||
|
|
||||||
|
public void Emit(in TraceEvent item)
|
||||||
|
{
|
||||||
|
if (item.Kind == TraceEventKind.FrameEnter)
|
||||||
|
{
|
||||||
|
state.Frames.Add($"+{item.Name}:{item.Cause}");
|
||||||
|
if (item.Cause == FrameCause.SaveRestore
|
||||||
|
&& !string.Equals(item.Name, "CALLBACK_LOAD.BIN",
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
state.RestoreFrameCount++;
|
||||||
|
if (item.Cause == FrameCause.RootReload)
|
||||||
|
throw new InstalledRootReloadReachedException(
|
||||||
|
string.Join(", ", state.Frames));
|
||||||
|
}
|
||||||
|
else if (item.Kind == TraceEventKind.FrameExit)
|
||||||
|
{
|
||||||
|
state.Frames.Add($"-{item.Name}:{item.Text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class SaveUiProvider(
|
private sealed class SaveUiProvider(
|
||||||
Sys4ScriptProvider native,
|
Sys4ScriptProvider native,
|
||||||
Script? resumed,
|
Script? resumed,
|
||||||
@@ -224,12 +273,50 @@ public class SaveUiIntegrationTests
|
|||||||
sharedProfile: sharedProfile,
|
sharedProfile: sharedProfile,
|
||||||
nativeDatStore: store);
|
nativeDatStore: store);
|
||||||
host.Vm = vm;
|
host.Vm = vm;
|
||||||
|
vm.Globals[0x4] = unchecked((uint)sharedProfile.LoadInteger(0x5c3));
|
||||||
vm.Globals[0x6241b] = 1;
|
vm.Globals[0x6241b] = 1;
|
||||||
vm.Globals[0x696] = 0;
|
vm.Globals[0x696] = 0;
|
||||||
|
|
||||||
Assert.Throws<InstalledResumeReachedException>(() => vm.Run());
|
Assert.Throws<InstalledResumeReachedException>(() => vm.Run());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InstalledSave00ContinuesToRestoredGameplayPollWhenPresent()
|
||||||
|
{
|
||||||
|
string root = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Eushully", "姫狩りダンジョンマイスター", "SAVE");
|
||||||
|
if (!File.Exists(Path.Combine(root, "SAVE00.DAT"))) return;
|
||||||
|
|
||||||
|
var scripts = Sys4ScriptProvider.Load(Table);
|
||||||
|
var store = new DirectoryNativeDatStore(root, Identity);
|
||||||
|
NativeNumberedSaveState numbered = NativeNumberedSaveCodec.Decode(
|
||||||
|
store.LoadNumberedFile(0)!.Document.Payload);
|
||||||
|
var state = new InstalledContinuationState(numbered.Frames.Count);
|
||||||
|
var host = new ContinueInstalledLoadHost(state);
|
||||||
|
var sharedProfile = new SharedProfile();
|
||||||
|
Assert.True(sharedProfile.Load(store));
|
||||||
|
var vm = new VirtualMachine(
|
||||||
|
scripts.RequireByName("SAVE.BIN"), Table, host,
|
||||||
|
new VmOptions(MaxSteps: 1_000_000), scripts,
|
||||||
|
new InstalledContinuationSink(state),
|
||||||
|
sharedProfile: sharedProfile,
|
||||||
|
nativeDatStore: store);
|
||||||
|
host.Vm = vm;
|
||||||
|
vm.Globals[0x4] = unchecked((uint)sharedProfile.LoadInteger(0x5c3));
|
||||||
|
vm.Globals[0x6241b] = 1;
|
||||||
|
vm.Globals[0x696] = 0;
|
||||||
|
|
||||||
|
Exception? outcome = Record.Exception(() => vm.Run());
|
||||||
|
Assert.True(outcome is InstalledGameplayPollReachedException,
|
||||||
|
$"outcome={outcome?.GetType().Name ?? "<none>"}:{outcome?.Message}; " +
|
||||||
|
$"halt={vm.HaltReason}; steps={vm.Steps}; waits={host.Waits}; " +
|
||||||
|
$"movies={string.Join(",", host.ModalMovies.Select(item => item.Resource))}; " +
|
||||||
|
$"saved={string.Join(", ", numbered.Frames.Select((frame, index) =>
|
||||||
|
$"{index}:0x{frame.ScriptId:x}/resume={frame.ResumeIndex}/call={frame.CallTargetIndex}"))}; " +
|
||||||
|
$"frames={string.Join(", ", state.Frames)}");
|
||||||
|
}
|
||||||
|
|
||||||
private static Script WithPackedId(Script source, uint packedId)
|
private static Script WithPackedId(Script source, uint packedId)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ internal class RecordingHost : IHost
|
|||||||
public readonly List<int> SfxStarts = new();
|
public readonly List<int> SfxStarts = new();
|
||||||
public readonly List<(int Channel, int StartMode, long DelayMs)> ScheduledSfxStarts = new();
|
public readonly List<(int Channel, int StartMode, long DelayMs)> ScheduledSfxStarts = new();
|
||||||
public readonly List<int> SfxReleases = new();
|
public readonly List<int> SfxReleases = new();
|
||||||
|
public readonly List<long> BgmTracks = new();
|
||||||
public readonly List<(int Target, long Duration)> BgmFades = new();
|
public readonly List<(int Target, long Duration)> BgmFades = new();
|
||||||
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
|
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
|
||||||
public System.Action? OnPlayMovie;
|
public System.Action? OnPlayMovie;
|
||||||
@@ -149,7 +150,7 @@ internal class RecordingHost : IHost
|
|||||||
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy)
|
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy)
|
||||||
=> TextureDraws.Add((slot, sx, sy, w, h, dx, dy));
|
=> TextureDraws.Add((slot, sx, sy, w, h, dx, dy));
|
||||||
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
|
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
|
||||||
public void PlayBgm(long id) { }
|
public void PlayBgm(long id) => BgmTracks.Add(id);
|
||||||
public void PlayVoice(long id) => Voices.Add(id);
|
public void PlayVoice(long id) => Voices.Add(id);
|
||||||
public void PlayVoice(long id, int playbackVariant)
|
public void PlayVoice(long id, int playbackVariant)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ public sealed record NativeSavedGfxObject(long Handle, byte[] Record);
|
|||||||
|
|
||||||
public sealed record NativeNumberedSaveState(
|
public sealed record NativeNumberedSaveState(
|
||||||
int SavedFrameOwner,
|
int SavedFrameOwner,
|
||||||
int EngineState,
|
int BgmTrackId,
|
||||||
IReadOnlyList<int> StateWords,
|
IReadOnlyList<int> SoundEffectResourceIds,
|
||||||
byte[] ResourceRecords,
|
byte[] ResourceRecords,
|
||||||
byte[] SurfaceRecords,
|
byte[] SurfaceRecords,
|
||||||
IReadOnlyList<NativeSavedScriptFrame> Frames,
|
IReadOnlyList<NativeSavedScriptFrame> Frames,
|
||||||
@@ -30,7 +30,7 @@ public sealed record NativeNumberedSaveState(
|
|||||||
int RangeTransformCount,
|
int RangeTransformCount,
|
||||||
byte[] RangeTransformRecord)
|
byte[] RangeTransformRecord)
|
||||||
{
|
{
|
||||||
public const int StateWordCount = 10;
|
public const int SoundEffectChannelCount = 10;
|
||||||
public const int ResourceRecordsSize = 300 * 4;
|
public const int ResourceRecordsSize = 300 * 4;
|
||||||
public const int SurfaceRecordsSize = 20_000;
|
public const int SurfaceRecordsSize = 20_000;
|
||||||
public const int GfxRecordSize = 0x2d4;
|
public const int GfxRecordSize = 0x2d4;
|
||||||
@@ -65,8 +65,8 @@ public static class NativeNumberedSaveCodec
|
|||||||
|
|
||||||
WriteInt(payload, 0, cutoff);
|
WriteInt(payload, 0, cutoff);
|
||||||
WriteInt(payload, 4, state.SavedFrameOwner);
|
WriteInt(payload, 4, state.SavedFrameOwner);
|
||||||
WriteInt(payload, 8, state.EngineState);
|
WriteInt(payload, 8, state.BgmTrackId);
|
||||||
WriteIntList(payload, 0x0c, state.StateWords);
|
WriteIntList(payload, 0x0c, state.SoundEffectResourceIds);
|
||||||
state.ResourceRecords.CopyTo(payload, 0x34);
|
state.ResourceRecords.CopyTo(payload, 0x34);
|
||||||
state.SurfaceRecords.CopyTo(payload, 0x4e4);
|
state.SurfaceRecords.CopyTo(payload, 0x4e4);
|
||||||
|
|
||||||
@@ -119,7 +119,8 @@ public static class NativeNumberedSaveCodec
|
|||||||
int fixedBytes = checked(0x5718 + cutoff * FrameSize);
|
int fixedBytes = checked(0x5718 + cutoff * FrameSize);
|
||||||
Require(payload, 0, fixedBytes, "numbered-save fixed state");
|
Require(payload, 0, fixedBytes, "numbered-save fixed state");
|
||||||
|
|
||||||
int[] stateWords = ReadInts(payload, 0x0c, NativeNumberedSaveState.StateWordCount);
|
int[] soundEffects = ReadInts(
|
||||||
|
payload, 0x0c, NativeNumberedSaveState.SoundEffectChannelCount);
|
||||||
byte[] resources = payload.Slice(0x34, NativeNumberedSaveState.ResourceRecordsSize).ToArray();
|
byte[] resources = payload.Slice(0x34, NativeNumberedSaveState.ResourceRecordsSize).ToArray();
|
||||||
byte[] surfaces = payload.Slice(0x4e4, NativeNumberedSaveState.SurfaceRecordsSize).ToArray();
|
byte[] surfaces = payload.Slice(0x4e4, NativeNumberedSaveState.SurfaceRecordsSize).ToArray();
|
||||||
var frames = new NativeSavedScriptFrame[cutoff + 1];
|
var frames = new NativeSavedScriptFrame[cutoff + 1];
|
||||||
@@ -167,14 +168,14 @@ public static class NativeNumberedSaveCodec
|
|||||||
byte[] rangeRecord = payload.Slice(at + 8, gfxRecordSize).ToArray();
|
byte[] rangeRecord = payload.Slice(at + 8, gfxRecordSize).ToArray();
|
||||||
|
|
||||||
return new NativeNumberedSaveState(
|
return new NativeNumberedSaveState(
|
||||||
ReadInt(payload, 4), ReadInt(payload, 8), stateWords, resources, surfaces, frames,
|
ReadInt(payload, 4), ReadInt(payload, 8), soundEffects, resources, surfaces, frames,
|
||||||
integers, floats, strings, pointers, pointerStrings, localPointerScratch, objects,
|
integers, floats, strings, pointers, pointerStrings, localPointerScratch, objects,
|
||||||
rangeFirst, rangeCount, rangeRecord);
|
rangeFirst, rangeCount, rangeRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NativeNumberedSaveState Empty(IReadOnlyList<NativeSavedScriptFrame> frames)
|
public static NativeNumberedSaveState Empty(IReadOnlyList<NativeSavedScriptFrame> frames)
|
||||||
=> new(
|
=> new(
|
||||||
0, 0, new int[NativeNumberedSaveState.StateWordCount],
|
0, 0, new int[NativeNumberedSaveState.SoundEffectChannelCount],
|
||||||
new byte[NativeNumberedSaveState.ResourceRecordsSize],
|
new byte[NativeNumberedSaveState.ResourceRecordsSize],
|
||||||
new byte[NativeNumberedSaveState.SurfaceRecordsSize],
|
new byte[NativeNumberedSaveState.SurfaceRecordsSize],
|
||||||
frames, Array.Empty<int>(), Array.Empty<int>(), Array.Empty<string>(),
|
frames, Array.Empty<int>(), Array.Empty<int>(), Array.Empty<string>(),
|
||||||
@@ -239,8 +240,8 @@ public static class NativeNumberedSaveCodec
|
|||||||
private static void ValidateState(NativeNumberedSaveState state)
|
private static void ValidateState(NativeNumberedSaveState state)
|
||||||
{
|
{
|
||||||
if (state.Frames.Count == 0) throw new InvalidDataException("A numbered save requires at least one frame.");
|
if (state.Frames.Count == 0) throw new InvalidDataException("A numbered save requires at least one frame.");
|
||||||
if (state.StateWords.Count != NativeNumberedSaveState.StateWordCount)
|
if (state.SoundEffectResourceIds.Count != NativeNumberedSaveState.SoundEffectChannelCount)
|
||||||
throw new InvalidDataException("Numbered-save state word count must be 10.");
|
throw new InvalidDataException("Numbered-save sound-effect channel count must be 10.");
|
||||||
if (state.ResourceRecords.Length != NativeNumberedSaveState.ResourceRecordsSize)
|
if (state.ResourceRecords.Length != NativeNumberedSaveState.ResourceRecordsSize)
|
||||||
throw new InvalidDataException("Numbered-save resource table must be 1,200 bytes.");
|
throw new InvalidDataException("Numbered-save resource table must be 1,200 bytes.");
|
||||||
if (state.SurfaceRecords.Length != NativeNumberedSaveState.SurfaceRecordsSize)
|
if (state.SurfaceRecords.Length != NativeNumberedSaveState.SurfaceRecordsSize)
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ public sealed class VirtualMachine
|
|||||||
private NativeNumberedSaveState? _loadedNumberedState;
|
private NativeNumberedSaveState? _loadedNumberedState;
|
||||||
private NativeNumberedSaveState? _retainedNativeNumberedState;
|
private NativeNumberedSaveState? _retainedNativeNumberedState;
|
||||||
private int _restoreFrameIndex = -1;
|
private int _restoreFrameIndex = -1;
|
||||||
|
private long _currentBgmTrackId;
|
||||||
|
private readonly long[] _loadedSoundEffectResourceIds =
|
||||||
|
new long[NativeNumberedSaveState.SoundEffectChannelCount];
|
||||||
private uint _accumulatedPlaySeconds;
|
private uint _accumulatedPlaySeconds;
|
||||||
private readonly long _sessionStartTimestamp;
|
private readonly long _sessionStartTimestamp;
|
||||||
private ExecFrame? _debugActiveFrame;
|
private ExecFrame? _debugActiveFrame;
|
||||||
@@ -819,6 +822,9 @@ public sealed class VirtualMachine
|
|||||||
var gfx = NativeGfxPersistenceCodec.Capture(Gfx);
|
var gfx = NativeGfxPersistenceCodec.Capture(Gfx);
|
||||||
return basis with
|
return basis with
|
||||||
{
|
{
|
||||||
|
BgmTrackId = unchecked((int)_currentBgmTrackId),
|
||||||
|
SoundEffectResourceIds = _loadedSoundEffectResourceIds
|
||||||
|
.Select(id => unchecked((int)id)).ToArray(),
|
||||||
Frames = frames,
|
Frames = frames,
|
||||||
IntegerGlobals = DenseValues(Globals, himegariIntegerCount),
|
IntegerGlobals = DenseValues(Globals, himegariIntegerCount),
|
||||||
FloatGlobals = DenseValues(GlobalFloats, himegariFloatCount),
|
FloatGlobals = DenseValues(GlobalFloats, himegariFloatCount),
|
||||||
@@ -870,21 +876,24 @@ public sealed class VirtualMachine
|
|||||||
|
|
||||||
private void ApplyNumberedState(NativeNumberedSaveState state)
|
private void ApplyNumberedState(NativeNumberedSaveState state)
|
||||||
{
|
{
|
||||||
Globals.Clear();
|
ReplaceDensePrefix(
|
||||||
for (int i = 0; i < state.IntegerGlobals.Count; i++)
|
Globals, state.IntegerGlobals.Select(value => (long)value).ToArray(), value => value != 0);
|
||||||
if (state.IntegerGlobals[i] != 0) Globals[i] = state.IntegerGlobals[i];
|
ReplaceDensePrefix(
|
||||||
GlobalFloats.Clear();
|
GlobalFloats, state.FloatGlobals.Select(value => (long)value).ToArray(), value => value != 0);
|
||||||
for (int i = 0; i < state.FloatGlobals.Count; i++)
|
ReplaceDensePrefix(GlobalStrings, state.StringGlobals, value => value.Length != 0);
|
||||||
if (state.FloatGlobals[i] != 0) GlobalFloats[i] = state.FloatGlobals[i];
|
ReplaceDensePrefix(GlobalPointers, state.PointerGlobals, value => value != 0);
|
||||||
GlobalStrings.Clear();
|
ReplaceDensePrefix(GlobalStringPointers, state.PointerStrings, value => value != 0);
|
||||||
for (int i = 0; i < state.StringGlobals.Count; i++)
|
|
||||||
if (state.StringGlobals[i].Length != 0) GlobalStrings[i] = state.StringGlobals[i];
|
_currentBgmTrackId = unchecked((uint)state.BgmTrackId);
|
||||||
GlobalPointers.Clear();
|
if (_currentBgmTrackId == 0) _host.FadeBgm(0, 0);
|
||||||
for (int i = 0; i < state.PointerGlobals.Count; i++)
|
else _host.PlayBgm(_currentBgmTrackId);
|
||||||
if (state.PointerGlobals[i] != 0) GlobalPointers[i] = state.PointerGlobals[i];
|
for (int channel = 0; channel < _loadedSoundEffectResourceIds.Length; channel++)
|
||||||
GlobalStringPointers.Clear();
|
{
|
||||||
for (int i = 0; i < state.PointerStrings.Count; i++)
|
_host.ReleaseSoundEffect(channel);
|
||||||
if (state.PointerStrings[i] != 0) GlobalStringPointers[i] = state.PointerStrings[i];
|
long resourceId = unchecked((uint)state.SoundEffectResourceIds[channel]);
|
||||||
|
_loadedSoundEffectResourceIds[channel] = resourceId;
|
||||||
|
if (resourceId != 0) _host.LoadSoundEffect(resourceId, channel);
|
||||||
|
}
|
||||||
|
|
||||||
GfxPersistenceSnapshot gfxSnapshot = NativeGfxPersistenceCodec.Decode(state);
|
GfxPersistenceSnapshot gfxSnapshot = NativeGfxPersistenceCodec.Decode(state);
|
||||||
for (int slot = 0; slot < 1000; slot++) _host.ReleaseSurface(slot);
|
for (int slot = 0; slot < 1000; slot++) _host.ReleaseSurface(slot);
|
||||||
@@ -895,6 +904,15 @@ public sealed class VirtualMachine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ReplaceDensePrefix<T>(
|
||||||
|
Dictionary<int, T> bank, IReadOnlyList<T> values, Func<T, bool> retain)
|
||||||
|
{
|
||||||
|
foreach (int key in bank.Keys.Where(key => (uint)key < (uint)values.Count).ToArray())
|
||||||
|
bank.Remove(key);
|
||||||
|
for (int i = 0; i < values.Count; i++)
|
||||||
|
if (retain(values[i])) bank[i] = values[i];
|
||||||
|
}
|
||||||
|
|
||||||
private Script ResolveSavedScript(NativeSavedScriptFrame frame)
|
private Script ResolveSavedScript(NativeSavedScriptFrame frame)
|
||||||
{
|
{
|
||||||
if (_s.PackedId == frame.ScriptId) return _s;
|
if (_s.PackedId == frame.ScriptId) return _s;
|
||||||
@@ -2193,7 +2211,13 @@ public sealed class VirtualMachine
|
|||||||
case "release-transient-surfaces": // 0x23d: native fixed range [42,1000)
|
case "release-transient-surfaces": // 0x23d: native fixed range [42,1000)
|
||||||
Gfx.ReleaseSurfaceRange(42, 1000 - 42);
|
Gfx.ReleaseSurfaceRange(42, 1000 - 42);
|
||||||
_host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1;
|
_host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1;
|
||||||
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
|
case "play-bgm":
|
||||||
|
_currentBgmTrackId = Read(a[0]);
|
||||||
|
_host.PlayBgm(_currentBgmTrackId);
|
||||||
|
return pc + 1;
|
||||||
|
case "get-current-bgm-track":
|
||||||
|
Write(a[0], _currentBgmTrackId);
|
||||||
|
return pc + 1;
|
||||||
case "play-voice":
|
case "play-voice":
|
||||||
_autoVoicePending = true;
|
_autoVoicePending = true;
|
||||||
TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle);
|
TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle);
|
||||||
@@ -2208,15 +2232,33 @@ public sealed class VirtualMachine
|
|||||||
case "schedule-voice-playback": // 0x2c0: replace the pending delayed combat voice request
|
case "schedule-voice-playback": // 0x2c0: replace the pending delayed combat voice request
|
||||||
_host.ScheduleVoicePlayback(Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
|
_host.ScheduleVoicePlayback(Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
|
||||||
case "play-sound-effect": // 0xb4 / semantics: sfx-load
|
case "play-sound-effect": // 0xb4 / semantics: sfx-load
|
||||||
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
{
|
||||||
|
long resourceId = Read(a[0]);
|
||||||
|
int channel = (int)Read(a[1]);
|
||||||
|
if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length)
|
||||||
|
_loadedSoundEffectResourceIds[channel] = resourceId;
|
||||||
|
_host.LoadSoundEffect(resourceId, channel);
|
||||||
|
return pc + 1;
|
||||||
|
}
|
||||||
case "u0041D050": // 0xb5 / semantics: sfx-start
|
case "u0041D050": // 0xb5 / semantics: sfx-start
|
||||||
_host.StartSoundEffect((int)Read(a[0])); return pc + 1;
|
_host.StartSoundEffect((int)Read(a[0])); return pc + 1;
|
||||||
case "u0041D080": // 0xb6 / semantics: sfx-release
|
case "u0041D080": // 0xb6 / semantics: sfx-release
|
||||||
_host.ReleaseSoundEffect((int)Read(a[0])); return pc + 1;
|
{
|
||||||
|
int channel = (int)Read(a[0]);
|
||||||
|
if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length)
|
||||||
|
_loadedSoundEffectResourceIds[channel] = 0;
|
||||||
|
_host.ReleaseSoundEffect(channel);
|
||||||
|
return pc + 1;
|
||||||
|
}
|
||||||
case "schedule-sfx-start": // 0x2bf / native SetDelay(channel, start mode, delay ms)
|
case "schedule-sfx-start": // 0x2bf / native SetDelay(channel, start mode, delay ms)
|
||||||
_host.ScheduleSoundEffectStart((int)Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
|
_host.ScheduleSoundEffectStart((int)Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
|
||||||
case "u0041D2B0": // 0xc2 / semantics: fade-bgm
|
case "u0041D2B0": // 0xc2 / semantics: fade-bgm
|
||||||
_host.FadeBgm((int)Read(a[0]), Read(a[1])); return pc + 1;
|
{
|
||||||
|
int targetPercent = (int)Read(a[0]);
|
||||||
|
_host.FadeBgm(targetPercent, Read(a[1]));
|
||||||
|
if (targetPercent == 0) _currentBgmTrackId = 0;
|
||||||
|
return pc + 1;
|
||||||
|
}
|
||||||
case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000
|
case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "get-initial-root-run": // 0x130 (out)
|
case "get-initial-root-run": // 0x130 (out)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ INFERRED: dict[int, dict] = {
|
|||||||
0x93: dict(name='cancel-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary="Reset the current frame's hotspot registry/input wait and clear native run-state bit 0x00800000. Used before opening History, Menu, or HIDEWIN flows."),
|
0x93: dict(name='cancel-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary="Reset the current frame's hotspot registry/input wait and clear native run-state bit 0x00800000. Used before opening History, Menu, or HIDEWIN flows."),
|
||||||
0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'),
|
0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'),
|
||||||
0x97: dict(name='bind-hotspot-key', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y)(w)(h)(logical_action) - find the already-registered rectangle with identical bounds and bind a logical input action to its activation callback.'),
|
0x97: dict(name='bind-hotspot-key', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y)(w)(h)(logical_action) - find the already-registered rectangle with identical bounds and bind a logical input action to its activation callback.'),
|
||||||
0xae: dict(name='continue-save-load-stack-restore', category='control', noop=False, confidence='high', source='investigation', summary='() - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.'),
|
|
||||||
0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9."),
|
0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9."),
|
||||||
0xb5: dict(name='sfx-start', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.'),
|
0xb5: dict(name='sfx-start', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.'),
|
||||||
0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'),
|
0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'),
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ name = "mouse_wheel_delta"
|
|||||||
type = "int"
|
type = "int"
|
||||||
note = "signed WM_MOUSEWHEEL delta accumulated by age_main_window_proc; op 0x10d returns and clears it"
|
note = "signed WM_MOUSEWHEEL delta accumulated by age_main_window_proc; op 0x10d returns and clears it"
|
||||||
[[field]]
|
[[field]]
|
||||||
|
offset = 0x144e0
|
||||||
|
name = "sfx_channel_resource_ids"
|
||||||
|
type = "int"
|
||||||
|
note = "base of ten packed resource ids retained by the sound-effect facade; op 0xb4 loads a channel, op 0xb6 clears it, and numbered-save layouts restore then reopen every positive id"
|
||||||
|
[[field]]
|
||||||
offset = 0x14d54
|
offset = 0x14d54
|
||||||
name = "gfx_obj_ptr_table"
|
name = "gfx_obj_ptr_table"
|
||||||
type = "void*"
|
type = "void*"
|
||||||
@@ -473,6 +478,11 @@ name = "mounted_aai_catalog_selector_1"
|
|||||||
type = "void*"
|
type = "void*"
|
||||||
note = "selector-one cell and op 0x143 scan start; subsequent dwords are selector 2..255"
|
note = "selector-one cell and op 0x143 scan start; subsequent dwords are selector 2..255"
|
||||||
[[field]]
|
[[field]]
|
||||||
|
offset = 0xa0b84
|
||||||
|
name = "current_bgm_track_id"
|
||||||
|
type = "int"
|
||||||
|
note = "direct-name BGM track id retained by the music facade; op 0xbf starts/replaces it, ops 0xc0/0xc3 get/set it, and numbered-save layouts restore it"
|
||||||
|
[[field]]
|
||||||
offset = 0xa0cc0
|
offset = 0xa0cc0
|
||||||
name = "screen_w"
|
name = "screen_w"
|
||||||
type = "int"
|
type = "int"
|
||||||
|
|||||||
@@ -1716,7 +1716,7 @@ observed_types = ["imm"]
|
|||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0xae
|
op = 0xae
|
||||||
label = "u00415130"
|
label = "continue-save-load-stack-restore"
|
||||||
argc = 0
|
argc = 0
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
@@ -1724,7 +1724,7 @@ abi_source = "kelebek+decode-validated"
|
|||||||
name = "continue-save-load-stack-restore"
|
name = "continue-save-load-stack-restore"
|
||||||
category = "control"
|
category = "control"
|
||||||
summary = "() - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op."
|
summary = "() - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op."
|
||||||
details = "Layout 3 frame d259 indexes SYS4 T1 read-message reset sites, d260 indexes T2 call-script sites, and the saved local return stack indexes T3 local-call sites. Port status (2026-07-24): the active path reconstructs the saved recursive frame chain and resumes the terminal frame at its T1 boundary."
|
details = "Layout 3 frame d259 indexes SYS4 T1 read-message reset sites, d260 indexes T2 call-script sites, and the saved local return stack indexes T3 local-call sites. Port status (2026-07-24): the active path reconstructs the saved recursive frame chain and resumes the terminal frame at its T1 boundary. The installed SAVE00 continuation gate proves SYSTEM4 -> FORT restoration reaches FORT's CHMENU gameplay poll; the synthetic gate asserts the child frame enters with SaveRestore rather than ordinary CallScript cause."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
@@ -1901,23 +1901,23 @@ observed_types = ["imm", "g-int", "l-int", "l-ptr"]
|
|||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0xc0
|
op = 0xc0
|
||||||
label = "u00415620"
|
label = "get-current-bgm-track"
|
||||||
argc = 1
|
argc = 1
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "u00415620"
|
name = "get-current-bgm-track"
|
||||||
category = "unknown"
|
category = "audio"
|
||||||
summary = ""
|
summary = "(track_out) - return the direct-name BGM track id retained by the music facade; the same value is restored from numbered saves."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "low"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = ""
|
evidence = "Ghidra /v2 op_0xc0_get_current_bgm_track@0x428440 writes EngineCtx.current_bgm_track_id (+0xa0b84) to operand 1. op 0xbf's bgm_play_track retains the selected direct-name id at the same music-facade field; layout-3 serialization copies it to payload +0x008 and restoration copies it back. Installed SAVE00 stores 0x18, matching FORT's BGM024."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "current BGM track id output"
|
||||||
observed_types = ["l-int"]
|
observed_types = ["l-int"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
|
|||||||
Reference in New Issue
Block a user