Fix packed raw menu sound effects
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 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.
|
||||
**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 TITLE→GAMESTART→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
|
||||
|
||||
|
||||
@@ -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")!));
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user