From 7a9fe52a9da0e3931bac6e8fdde2df064896d3ff Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 20 Jul 2026 18:32:56 -0400 Subject: [PATCH] Document packed raw SFX addressing --- docs/asset-resolution-re.md | 33 +++++++++++++++++++-------------- docs/engine-re.md | 16 +++++++++++++--- docs/opcode-reference.md | 4 ++-- docs/phase-b-framework.md | 6 ++++++ tools/age_opcodes_himegari.py | 2 +- vm-map/opcodes.toml | 6 +++--- 6 files changed, 44 insertions(+), 23 deletions(-) diff --git a/docs/asset-resolution-re.md b/docs/asset-resolution-re.md index e09d9dd..908d37c 100644 --- a/docs/asset-resolution-re.md +++ b/docs/asset-resolution-re.md @@ -115,8 +115,9 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b The active manifest is frame-local, not fixed to the root scene: every VM call frame brackets host work with its script context. `set-texture` resolves the local id at load time and retains the normalized raw catalog id in the graphics surface, so that surface remains stable after a nested helper returns or a - sibling script becomes active. Voice, SFX, and movie calls likewise resolve against the executing frame. -4. **Audio.** **✅ WIRED (2026-07-06), VFS bytes complete (2026-07-11).** `IHost.PlayBgm/PlayVoice` + + sibling script becomes active. Voice and movie calls likewise resolve against the executing frame; + SFX is the separate universal packed-id family documented below. +4. **Audio.** **BGM/voice wired; VFS bytes complete (2026-07-11); packed-raw SFX correction pending.** `IHost.PlayBgm/PlayVoice` + VM dispatch (`play-bgm` 0xbf / `play-voice` 0xc4, both argc 1); `ResourceMap.ReadAudio` opens the resolved catalog entry through `IAssetStore`; `GodotAdvHost` passes the bytes to `Main`'s players (`AudioStreamOggVorbis.LoadFromBuffer`; BGM loops, voice interrupt-on-new). Non-Godot hosts no-op it @@ -132,8 +133,9 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b to a graphics entry. Implemented as `ResourceMap.ResolveBgm(id)`; `GodotAdvHost.PlayBgm` uses it. The prior "Frida-confirmed play-bgm 5→BGM006" record was a mis-attribution. - Lily silent = correct (form-gated on `G[0xa57/0xa58/0xa59]`, unseeded). Scene-local SFX WAV entries use - the same byte path while retaining the existing channel lifecycle. See `docs/phase-a-slice-plan.md`. + Lily silent = correct (form-gated on `G[0xa57/0xa58/0xa59]`, unseeded). SFX WAV entries use universal + packed catalog ids through the same byte store while retaining the existing channel lifecycle. See + `docs/phase-a-slice-plan.md`. Diagnostic: `Age.Cli audio `. 5. **Movies** (`OP`/`MVB`, MPEG) — SC0000 `0x236` now resolves its scene-local id through the same catalog and reads owned payload bytes through `IAssetStore`; see the movie section below. @@ -150,19 +152,21 @@ rendering what the executed bytecode + the map produce (never a hardcoded image) A2b-background: **steps 1–3 landed.** Step 1 = `build/asset-index.json`. Step 2 = **`resId → files[section_base(scene) + resId]`** via SYS4INI per-scene sections (`tools/resolve_asset.py` + -`build/asset-sections.json`) — no runtime capture, all archives/types + audio. Step 3 = **first-pass +`build/asset-sections.json`) — no runtime capture, all archives/types plus scene-local voice. Step 3 = **first-pass render** (ResourceMap + GodotAdvHost texture ops → TextureRect compositing): the full-screen event-CG layer renders end-to-end from the bytecode. Remaining (next chunk): the **graphics geometry/blend subsystem** — native geometry ops (`0x208` + sprite position/animation) so sprites/`BG*` position, plus alpha/blend for fades + chromakey. See `docs/phase-a-slice-plan.md` (A2b). Audio (step 4): **`play-voice` uses the manifest** (`files[base+id]`); **`play-bgm` uses direct names** (`BGM{id:03d}.OGG`) — NOT unified. -## Native SFX resource proof (2026-07-11) +## Native SFX resource proof (2026-07-11; addressing corrected 2026-07-20) -SFX uses the same scene-local rule as graphics and voice: `files[section_base(scene)+resource_id]`. -The matching native trace at SC0000 `0xc29` captures resource `0x28`, channel 0; static resolution yields -`DATA1/E0808.WAV`, and the port trace resolves the same file. The following `0xc31` preload uses the same -resource on native secondary channel 4. `play-bgm` remains the separate direct-name exception. +SFX uses a universal packed catalog id, not the scene-local graphics/voice rule. A zero high byte directly +indexes SYS4INI; a nonzero high byte selects the matching AAI mount and uses the low 24-bit index. The +matching native trace at SC0000 `0xc29` captures raw id `0x28`, channel 0, which is +`DATA1/E0808.WAV`; SC0000 being the first section previously hid the distinction. TITLE makes it decisive: +`0x2aea` is raw `SE020.WAV` for hover, `0x3321` is raw `SE015.WAV` for activation, and neither fits TITLE's +14-entry manifest. `play-bgm` remains the separate direct-name family. The Phase-A backend now resolves the OGG/WAV catalog entry and opens it through `IAssetStore`; Godot decodes the returned bytes into its existing BGM, voice, and fixed SC0000 SFX channel players. The earlier @@ -215,12 +219,13 @@ store. on-disk `BinExtractALF.exe` are validation references; the Kelebek repository exposes no clear license, so its code should not be copied without clarification. The focused `LzssDecoder` is shared with `Sys4AssetCatalog`; raw and compressed information/pixel/ACIF sections use the same bounded primitive. -4. **Runtime consumers (complete for scripts, textures, and audio).** `ResourceMap.ResolveTexture` preserves +4. **Runtime consumers (scripts/textures complete; SFX resolver correction pending).** `ResourceMap.ResolveTexture` preserves scene-local resolution and falls back to universal raw ids for SYSTEM4 assets; `GodotAdvHost` caches decoded RGBA surfaces by catalog identity and supplies synchronous dimensions to opcode `0x208`. - Godot no longer reads `build/textures/*.BMP`. BGM direct-name entries plus scene-local voice/SFX entries - are opened through the same `IAssetStore`; Godot decodes their OGG/WAV bytes in process. Extraction and - conversion tools remain diagnostics. + Godot no longer reads `build/textures/*.BMP`. BGM direct-name and scene-local voice entries are opened + through the same `IAssetStore`; the SFX WAV/player path is also present, but `GodotAdvHost` still applies + scene-local resolution where op `0xb4` requires `ResolvePacked`. SC0000 worked because its raw ids happen + to coincide with the first section. Extraction and conversion tools remain diagnostics. ### Acceptance gates diff --git a/docs/engine-re.md b/docs/engine-re.md index b2ede68..bee8302 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -1768,8 +1768,9 @@ the shortcut carries the same state in the meantime. The three SFX opcodes are a retained channel lifecycle, not immediate fire-and-forget calls. Handler resolution and the saved `/v2` names are: -- `0xb4` `op_0xb4_sfx_load@0x4201d0` -> `sfx_channel_load@0x482500`: `(resource_id, channel)` opens the - scene-local SYS4 entry and replaces the channel decoder/buffer without starting it. The manager supports +- `0xb4` `op_0xb4_sfx_load@0x4201d0` -> `sfx_channel_load@0x482500`: `(packed_raw_resource_id, channel)` + opens the universal SYS4INI entry directly when the high byte is zero, or the selected AAI append entry + when it is nonzero, then replaces the channel decoder/buffer without starting it. The manager supports 13 slots (`0..12`); SC0000 deliberately resets and uses the `0..9` subset. - `0xb5` `op_0xb5_sfx_start_once@0x420210` -> `sfx_channel_start@0x4825d0`: starts the loaded channel with logical loop mode 0. Adjacent op `0xba`, not this slice, passes mode 1. @@ -1789,11 +1790,20 @@ five handlers. The bounded port does not yet import native audio preferences, so uses unity gain and centered pan rather than hard-coding the captured user's setting. The native trace in `build/native-sfx-trace.jsonl` captures SC0000's first pair: `0xb4@0xc29` resolves -resource `0x28` to `E0808.WAV`, loads channel 0, and `0xb5@0xc2e` starts it in the same millisecond. The next +raw catalog id `0x28` to `E0808.WAV`, loads channel 0, and `0xb5@0xc2e` starts it in the same millisecond. The next `0xb4@0xc31` preloads the same WAV into engine-owned secondary channel 4 for a later service start. The scratch global `G[0x6242d]` is maintained outside script-visible writes; the SC0000 port profile exposes it as an external value of 4 rather than pretending the script assigned it. +**TITLE SFX silence root cause (2026-07-20).** TITLE preloads raw id `0x2aea` (`SE020.WAV`) into channel 2 +and executes `0xb5(2)` whenever mouse/joy selection changes. Each activation path loads raw id `0x3321` +(`SE015.WAV`) into channel 1 and immediately starts it; GAMESTART additionally uses raw `0x2aeb` +(`SE013.WAV`). A synchronized Godot trace captured repeated hover starts and the Game Start activation, but +each preceding load recorded `file:null`. `GodotAdvHost.LoadSoundEffect` currently applies the active +script's scene-local manifest, and TITLE has only 14 local entries, so all three large raw ids fail before +the existing WAV/player path. BGM remains audible because it uses its separate direct-name resolver. The +bounded fix is a scene-independent packed-raw SFX resolver, not changes to callbacks, mixing, or channels. + That later service start is opcode `0x2bf`, now identified as the facade's `SetDelay` operation. `op_0x2bf_schedule_sfx_start@0x425240` passes `(channel,start_mode,delay_ms)` to `sfx_set_delay@0x482720` on the inline sound facade at `ctx+0x14024`. The worker accepts channels `0..9` diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 33b701e..7133d87 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -128,9 +128,9 @@ ## audio ### 0xb4 `sfx-load` (play-sound-effect, argc 2) -- **summary:** (resource_id)(channel) — synchronously resolve/open the scene-manifest asset and replace the channel's decoded sound buffer without starting playback. Native manager supports channels 0..12; SC0000 uses 0..9. +- **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. - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra op 0xb4 handler 0x4201d0 -> sfx_channel_load@0x482500 -> asset_open@0x44f390 + sound_decode_channel@0x483360. Native trace: SC0000 0xc29 loads resource 0x28 into channel 0; resource resolves by section_base+id to E0808.WAV; completion precedes 0xb5 in the same millisecond. +- **evidence:** Ghidra /v2: op_0xb4_sfx_load@0x4201d0 -> sfx_channel_load@0x482500 -> asset_open_indexed_entry@0x44f390 + sound_decode_channel@0x483360. The opener indexes the base catalog directly for a zero high byte and selects mounted_aai[high_byte] otherwise. SC0000 0xc29 uses raw 0x28 = E0808.WAV. TITLE independently proves the addressing rule: raw 0x2aea = SE020.WAV is preloaded on channel 2 and started on every hover-selection change; raw 0x3321 = SE015.WAV is loaded/started on activation. A synchronized port trace executes those loads and starts but reports file=null because Godot incorrectly applies TITLE's 14-entry scene manifest. ### 0xb5 `sfx-start` (u0041D050, argc 1) - **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. diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index dc8ece1..24c6193 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -99,6 +99,12 @@ SYSTEM4's computed resource id `G[0x699]=0x22`; `G[0]=1` and the script-produced single-scene diagnostic harness. This lands the boot/title/New Game entry half of B1–B3; proving a completed scene return plus boundary cleanup still belongs to B1 completion. +**TITLE SFX investigation (2026-07-20).** Hover and activation callbacks are already executing their +scripted `0xb5` starts. The load fails earlier because op `0xb4` uses universal packed SYS4INI/AAI ids, +while Godot currently treats them as active-script manifest ids. TITLE's `0x2aea`/`SE020.WAV` hover and +`0x3321`/`SE015.WAV` activation loads therefore resolve null; BGM is unaffected. The next bounded correction +is a packed-raw SFX resolver plus TITLE/GAMESTART regression coverage, with no mixer redesign indicated. + ## Stage B1 — Persistent session and scene coordinator Replace the single-SC0000-root assumption with an application-owned session that runs SYSTEM4 as its root. diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index 3bf999f..6a6fd17 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -15,7 +15,7 @@ INFERRED: dict[int, dict] = { 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)(input_bit) - find the already-registered rectangle with identical bounds and bind an input/key bit number to it.'), 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="(resource_id)(channel) — synchronously resolve/open the scene-manifest asset and replace the channel's decoded sound buffer without starting playback. 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.'), 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.'), 0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', 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.'), diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 0d7c1d1..3e59e4d 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -1731,16 +1731,16 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "sfx-load" category = "audio" -summary = "(resource_id)(channel) — synchronously resolve/open the scene-manifest asset and replace the channel's decoded sound buffer without starting playback. Native manager supports channels 0..12; SC0000 uses 0..9." +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." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "Ghidra op 0xb4 handler 0x4201d0 -> sfx_channel_load@0x482500 -> asset_open@0x44f390 + sound_decode_channel@0x483360. Native trace: SC0000 0xc29 loads resource 0x28 into channel 0; resource resolves by section_base+id to E0808.WAV; completion precedes 0xb5 in the same millisecond." +evidence = "Ghidra /v2: op_0xb4_sfx_load@0x4201d0 -> sfx_channel_load@0x482500 -> asset_open_indexed_entry@0x44f390 + sound_decode_channel@0x483360. The opener indexes the base catalog directly for a zero high byte and selects mounted_aai[high_byte] otherwise. SC0000 0xc29 uses raw 0x28 = E0808.WAV. TITLE independently proves the addressing rule: raw 0x2aea = SE020.WAV is preloaded on channel 2 and started on every hover-selection change; raw 0x3321 = SE015.WAV is loaded/started on activation. A synchronized port trace executes those loads and starts but reports file=null because Godot incorrectly applies TITLE's 14-entry scene manifest." [[opcode.semantics.args]] i = 1 -role = "resource_id" +role = "packed raw resource id" observed_types = ["imm", "g-int", "l-ptr"] [[opcode.semantics.args]]