Migrate audio payloads onto asset VFS

This commit is contained in:
gamer147
2026-07-11 11:01:11 -04:00
parent e0ca7936e2
commit 233f791c10
9 changed files with 132 additions and 64 deletions

View File

@@ -350,14 +350,13 @@ sealed class AudioTraceHost : IHost
public AudioTraceHost(ResourceMap res, string scene) { _res = res; _scene = scene; }
public void PlayBgm(long id) // BGM: direct name, not the manifest
{
var path = _res.BgmPathById(id);
Events.Add(("play-bgm", id, path != null ? $"DATA3 {System.IO.Path.GetFileName(path)}" : $"BGM{id:D3}.OGG <missing>"));
var entry = _res.ResolveBgm(id);
Events.Add(("play-bgm", id, entry != null ? $"{entry.Archive} {entry.Name}" : $"BGM{id:D3}.OGG <missing>"));
}
public void PlayVoice(long id) // voice: per-scene manifest
{
var e = _res.Resolve(_scene, id);
Events.Add(("play-voice", id, e == null ? "<unresolved>"
: $"{e.Archive} {e.Name}" + (ResourceMap.AudioPath(e) == null ? " [NO FILE]" : "")));
Events.Add(("play-voice", id, e == null ? "<unresolved>" : $"{e.Archive} {e.Name}"));
}
public void ShowText(int offset, string text) { }
public void WaitForInput() { }

View File

@@ -167,6 +167,31 @@ public class Sys4AssetStoreTests
}
}
[Fact]
public void Sc0000BgmVoiceAndSfxPayloadsReadDirectlyFromArchives()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
var archiveOnly = new Sys4AssetStore(catalog, Paths.GameDir);
var resources = new ResourceMap(catalog, archiveOnly);
var bgm = resources.ResolveBgm(5);
Assert.Equal("BGM005.OGG", bgm?.Name);
AssertOgg(resources.ReadAudio(bgm!));
var voice = resources.Resolve("SC0000", 0x24);
Assert.Equal("MAN999.OGG", voice?.Name);
AssertOgg(resources.ReadAudio(voice!));
var sfx = resources.Resolve("SC0000", 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.Throws<InvalidDataException>(() => resources.ReadAudio(catalog.ResolveName("SO001.AGF")!));
}
[Fact]
public void AllInstalledLooseScriptOverridesShadowArchiveCopies()
{
@@ -235,4 +260,10 @@ public class Sys4AssetStoreTests
stream.CopyTo(copy);
return copy.ToArray();
}
private static void AssertOgg(AudioPayload payload)
{
Assert.EndsWith(".OGG", payload.Name, StringComparison.OrdinalIgnoreCase);
Assert.Equal("OggS", Encoding.ASCII.GetString(payload.Bytes, 0, 4));
}
}

View File

@@ -4,7 +4,7 @@ namespace Age.Engine.Sys4;
/// 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.
/// See docs/asset-resolution-re.md. Extracted paths remain temporary graphics/audio backends only.
/// See docs/asset-resolution-re.md.
/// </summary>
public sealed class ResourceMap
{
@@ -40,27 +40,29 @@ public sealed class ResourceMap
public AssetEntry? ResolveName(string name) => _catalog.ResolveName(name);
/// <summary>
/// Resolve a BGM id to its OGG path. BGM is addressed by DIRECT LITERAL NAME (BGM{id:D3}.OGG), NOT the
/// Resolve a BGM id to its catalog entry. BGM is addressed by DIRECT LITERAL NAME (BGM{id:D3}.OGG), NOT the
/// per-scene section manifest that voices/textures use. Confirmed by ear (play-bgm 5->BGM005, 8->BGM008)
/// and by the play-bgm 0x23->BGM035 case: BGM035 is a real standalone track (the BGM set skips 030-034),
/// which the manifest mis-resolved to a graphics entry. See docs/asset-resolution-re.md.
/// </summary>
public string? BgmPathById(long id)
public AssetEntry? ResolveBgm(long id)
{
var name = $"BGM{id:D3}.OGG";
var f = _catalog.ResolveName(name);
return f == null ? null : AudioPath(f);
var entry = _catalog.ResolveName(name);
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
}
/// <summary>Loose extracted OGG/WAV path for an audio asset (extracted/DATA{n}/{name}), or null.
/// Used by the current bootstrap audio backend; voices and SFX use the scene manifest via Resolve.</summary>
public static string? AudioPath(AssetEntry a)
/// <summary>Read a catalog-resolved OGG/WAV payload through the loose-first ALF/AAI byte store.</summary>
public AudioPayload ReadAudio(AssetEntry entry)
{
if (!a.Name.EndsWith(".OGG", StringComparison.OrdinalIgnoreCase) &&
!a.Name.EndsWith(".WAV", StringComparison.OrdinalIgnoreCase)) return null;
var dir = a.Archive.EndsWith(".ALF", StringComparison.OrdinalIgnoreCase)
? a.Archive[..^4] : a.Archive; // "DATA3.ALF" -> "DATA3"
var audio = Path.Combine(Paths.Extracted, dir, a.Name);
return File.Exists(audio) ? audio : null;
if (entry.IsPlaceholder || !IsAudio(entry))
throw new InvalidDataException($"not an OGG/WAV asset: {entry.Name}");
return new AudioPayload(entry.Name, _store.ReadAll(entry));
}
private static bool IsAudio(AssetEntry entry)
=> entry.Name.EndsWith(".OGG", StringComparison.OrdinalIgnoreCase)
|| entry.Name.EndsWith(".WAV", StringComparison.OrdinalIgnoreCase);
}
public sealed record AudioPayload(string Name, byte[] Bytes);