using System;
using System.Collections.Generic;
using System.Linq;
using AcDream.Core.Tests.Conformance;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Audio;
///
/// Resolves retail's UI sound table from the installed dats and pins the answer.
///
///
/// ClientUISystem::GetUISoundTable @ 0x00563FB0 does NOT hold a
/// hard-coded DID — it calls DBObj::GetByEnum @ 0x00415490, and
/// DBCache::GetDIDFromEnum @ 0x00413940 resolves the answer through
/// two EnumIDMap hops (DB_TYPE_DID_MAPPER, DID range
/// 0x25xxxxxx, fetched as cache type 0x26): the master map keyed by
/// the enum INDEX yields a per-slot map, which keyed by the TYPE yields the
/// concrete DID. Because that chain lives in the dats rather than the binary,
/// the only honest way to learn the DID is to walk it — which is what this
/// does, instead of guessing a plausible 0x20xxxxxx value.
///
///
///
/// Walking it against the installed dats resolves master 0x25000000 →
/// slot-7 map 0x25000010 → 0x2000004B, and that table contains
/// exactly the 32 UI_* slots (UI_EnterPortal 0x6A through
/// UI_Thunder6 0x8A) — content that confirms the walk independently of
/// the decode.
///
///
///
/// Skips cleanly when the dats are absent (CI), matching every other
/// installed-dat test in this suite.
///
///
public sealed class UiSoundTableResolutionTests
{
///
/// The key the second enum hop uses. `DBObj::GetByEnum` @ 0x00415490 takes
/// (type, idx, cache) and passes (type, idx) to the enum chain, then hands
/// the resolved DID to `DBCache::Get(did, cache)`. Walking the real dats
/// shows this hop is keyed by values in the 0x1000000x space, and
/// 0x10000003 is the one whose value lands in the SoundTable DID range —
/// so 0x10000003 is the `type` argument and the 0x22 in the decode is the
/// CACHE type (`CLOCache(cache, CSoundTable::Allocator, 0x22)`), not a
/// lookup key. The lane-5 note transposed the two.
///
private const uint UiSoundTableTypeKey = 0x10000003u;
/// The enum slot GetUISoundTable asks for.
private const uint UiSoundTableEnumSlot = 7u;
///
/// The DID the chain resolves to in the shipped dats. Pinned so a dat
/// change or a regression in the walk is caught rather than silently
/// producing a different sound bank.
///
public const uint ExpectedUiSoundTableDid = 0x2000004Bu;
private readonly ITestOutputHelper _out;
public UiSoundTableResolutionTests(ITestOutputHelper output) => _out = output;
[Fact]
public void UiSoundTable_ResolvesThroughTheEnumIdMapChain()
{
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null)
return; // dats absent (CI) — nothing to resolve
using var dats = new DatCollection(new DatCollectionOptions
{
DatDirectory = datDir,
AccessType = DatAccessType.Read,
});
var candidates = new List<(uint MapId, uint PerTypeMapId, uint SoundTableDid)>();
for (uint id = 0x25000000u; id <= 0x2500FFFFu; id++)
{
EnumIDMap? master;
try
{
master = dats.Get(id);
}
catch
{
continue; // not an EnumIDMap / unreadable
}
if (master is null)
continue;
// Retail's order, from the decode: the MASTER map is keyed by the
// enumIndex (arg4, = 7) and yields a per-slot map; that map is then
// keyed by the fileType (arg3, = 0x22) and yields the DID. Reading
// the two hops the other way round finds nothing.
if (!master.ClientEnumToID.TryGetValue(UiSoundTableEnumSlot, out uint perSlot)
|| perSlot == 0)
{
continue;
}
EnumIDMap? perSlotMap;
try
{
perSlotMap = dats.Get(perSlot);
}
catch
{
continue;
}
if (perSlotMap is null)
continue;
_out.WriteLine(
$"master 0x{id:X8} -> slot-{UiSoundTableEnumSlot} map 0x{perSlot:X8} " +
$"({perSlotMap.ClientEnumToID.Count} type keys)");
foreach (var slot in perSlotMap.ClientEnumToID.OrderBy(kv => kv.Key))
_out.WriteLine($" typeKey 0x{slot.Key:X8} -> 0x{slot.Value:X8}");
if (perSlotMap.ClientEnumToID.TryGetValue(UiSoundTableTypeKey, out uint did)
&& did != 0)
{
candidates.Add((id, perSlot, did));
}
}
Assert.NotEmpty(candidates);
// Every master map that carries the chain must agree on the answer;
// retail follows exactly one, so a disagreement would mean the walk is
// wrong rather than that retail is ambiguous.
uint resolved = candidates[0].SoundTableDid;
Assert.All(candidates, c => Assert.Equal(resolved, c.SoundTableDid));
_out.WriteLine($"RESOLVED UI sound table DID = 0x{resolved:X8}");
Assert.Equal(ExpectedUiSoundTableDid, resolved);
// It must be a real SoundTable in the 0x20xxxxxx range and it must load.
Assert.InRange(resolved, 0x20000000u, 0x2000FFFFu);
SoundTable? table = dats.Get(resolved);
Assert.NotNull(table);
Assert.NotEmpty(table!.Sounds);
_out.WriteLine($"UI sound table 0x{resolved:X8} carries {table.Sounds.Count} slots:");
foreach (var kv in table.Sounds.OrderBy(kv => (uint)kv.Key))
_out.WriteLine($" {kv.Key} ({(uint)kv.Key:X2}) -> {kv.Value.Entries.Count} entries");
// The content check: every slot must be a UI_* slot, and the three the
// named decode calls out by name must be present.
Assert.All(
table.Sounds.Keys,
slot => Assert.InRange((uint)slot, 0x6Au, 0x8Au));
Assert.Contains(DatReaderWriter.Enums.Sound.UI_EnterPortal, table.Sounds.Keys);
Assert.Contains(DatReaderWriter.Enums.Sound.UI_ExitPortal, table.Sounds.Keys);
Assert.Contains(DatReaderWriter.Enums.Sound.UI_Roar, table.Sounds.Keys);
Assert.Contains(DatReaderWriter.Enums.Sound.UI_Thunder6, table.Sounds.Keys);
}
}