Fix packed raw menu sound effects

This commit is contained in:
gamer147
2026-07-20 18:39:38 -04:00
parent 9582731f6f
commit ec185c9561
8 changed files with 40 additions and 24 deletions

View File

@@ -117,7 +117,7 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
catalog id in the graphics surface, so that surface remains stable after a nested helper returns or a
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` +
4. **Audio.** **BGM/voice/SFX wired; VFS bytes complete (2026-07-11); packed-raw SFX corrected 2026-07-20.** `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
@@ -219,13 +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 (scripts/textures complete; SFX resolver correction pending).** `ResourceMap.ResolveTexture` preserves
4. **Runtime consumers (complete for scripts, textures, and current audio families).** `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 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.
Godot no longer reads `build/textures/*.BMP`. BGM direct-name, scene-local voice, and packed-raw SFX
entries are opened through the same `IAssetStore`. `ResourceMap.ResolveSoundEffect` owns op `0xb4`'s
`ResolvePacked` lookup and audio filtering; the existing Godot WAV/channel path consumes its result.
Extraction and conversion tools remain diagnostics.
### Acceptance gates

View File

@@ -1795,14 +1795,16 @@ raw catalog id `0x28` to `E0808.WAV`, loads channel 0, and `0xb5@0xc2e` starts i
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
**TITLE SFX packed-raw correction (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.
each preceding load recorded `file:null`: `GodotAdvHost.LoadSoundEffect` applied the active script's
scene-local manifest, and TITLE has only 14 local entries. `ResourceMap.ResolveSoundEffect` now uses the
native packed lookup and filters the selected record to OGG/WAV before the existing byte/player path.
The corrected synchronized trace resolves and starts `SE020`, `SE015`, and `SE013` through
TITLE→GAMESTART→TITLE, and the user confirmed hover, activation, and cancel sounds are audible. BGM remains
independent through its direct-name resolver; no callback, mixer, or channel workaround was needed.
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

View File

@@ -130,7 +130,7 @@
### 0xb4 `sfx-load` (play-sound-effect, argc 2)
- **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 /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.
- **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. The pre-fix port trace reported file=null under scene-manifest resolution; after switching Godot to ResolveSoundEffect/ResolvePacked, a synchronized TITLE->GAMESTART->TITLE trace resolves and starts SE020/SE015/SE013, confirmed audible by the user.
### 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.

View File

@@ -99,11 +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 B1B3; 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.
**TITLE SFX packed-raw correction (2026-07-20).** Hover and activation callbacks were already executing their
scripted `0xb5` starts. The load failed earlier because op `0xb4` uses universal packed SYS4INI/AAI ids,
while Godot treated them as active-script manifest ids. The new packed-raw resolver maps TITLE's
`0x2aea`/`SE020.WAV` hover, `0x3321`/`SE015.WAV` activation, and GAMESTART's `0x2aeb`/`SE013.WAV` cancel
through the existing channel players. A synchronized TITLEGAMESTART→TITLE trace records every load/start
with its filename, and manual validation confirms they are audible; BGM remains unaffected.
## Stage B1 — Persistent session and scene coordinator

View File

@@ -168,7 +168,7 @@ public class Sys4AssetStoreTests
}
[Fact]
public void Sc0000BgmVoiceAndSfxPayloadsReadDirectlyFromArchives()
public void Sc0000AndTitleAudioPayloadsReadDirectlyFromTheirNativeAddressSpaces()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
var archiveOnly = new Sys4AssetStore(catalog, Paths.GameDir);
@@ -182,13 +182,20 @@ public class Sys4AssetStoreTests
Assert.Equal("MAN999.OGG", voice?.Name);
AssertOgg(resources.ReadAudio(voice!));
var sfx = resources.Resolve("SC0000", 0x28);
var sfx = resources.ResolveSoundEffect(0x28);
Assert.Equal("E0808.WAV", sfx?.Name);
var wav = resources.ReadAudio(sfx!);
Assert.Equal("E0808.WAV", wav.Name);
Assert.Equal("RIFF", Encoding.ASCII.GetString(wav.Bytes, 0, 4));
Assert.Equal("WAVE", Encoding.ASCII.GetString(wav.Bytes, 8, 4));
Assert.Null(resources.Resolve("TITLE", 0x2aea));
Assert.Equal("SE020.WAV", resources.ResolveSoundEffect(0x2aea)?.Name);
Assert.Equal("SE013.WAV", resources.ResolveSoundEffect(0x2aeb)?.Name);
Assert.Equal("SE015.WAV", resources.ResolveSoundEffect(0x3321)?.Name);
Assert.Null(resources.ResolveSoundEffect(0x337e)); // SO001.AGF is not audio.
Assert.Null(resources.ResolveSoundEffect(0x02000000)); // unmounted append selector.
Assert.Throws<InvalidDataException>(() => resources.ReadAudio(catalog.ResolveName("SO001.AGF")!));
}

View File

@@ -1,9 +1,8 @@
namespace Age.Engine.Sys4;
/// <summary>
/// Compatibility facade over the runtime SYS4 catalog. SYS4INI's file list is sectioned (one per scene: SCxxxx.BIN + its
/// cross-archive asset manifest); file_number is the index within a section. So a bytecode
/// resId resolves as files[section_base(scene) + resId] -- unified for graphics and audio.
/// Compatibility facade over the runtime SYS4 catalog. Scene-local graphics/voice/movie ids resolve
/// through the executing script's manifest; BGM uses direct names; SFX/cursors use universal packed ids.
/// See docs/asset-resolution-re.md.
/// </summary>
public sealed class ResourceMap
@@ -75,6 +74,13 @@ public sealed class ResourceMap
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
}
/// <summary>Resolve opcode 0xb4's universal packed SYS4INI/AAI id to an audio entry.</summary>
public AssetEntry? ResolveSoundEffect(long packedRawId)
{
var entry = _catalog.ResolvePacked(packedRawId);
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
}
/// <summary>Read a catalog-resolved OGG/WAV payload through the loose-first ALF/AAI byte store.</summary>
public AudioPayload ReadAudio(AssetEntry entry)
{

View File

@@ -748,7 +748,7 @@ public sealed class GodotAdvHost : IHost
public void LoadSoundEffect(long resourceId, int channel)
{
if ((uint)channel >= (uint)_sfxNames.Length) return;
var asset = _res.Resolve(CurrentScene, resourceId);
var asset = _res.ResolveSoundEffect(resourceId);
var audio = asset != null ? LoadAudio(asset) : null;
_sfxNames[channel] = audio?.Name;
_timeline?.Event("sfx-load", new() { ["resource"] = resourceId, ["channel"] = channel,

View File

@@ -1736,7 +1736,7 @@ noop_headless = false
source = "investigation"
confidence = "high"
depends_on = []
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."
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. The pre-fix port trace reported file=null under scene-manifest resolution; after switching Godot to ResolveSoundEffect/ResolvePacked, a synchronized TITLE->GAMESTART->TITLE trace resolves and starts SE020/SE015/SE013, confirmed audible by the user."
[[opcode.semantics.args]]
i = 1