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; /// /// Installed-dat inventory that PINS A NEGATIVE: interior soundscapes (inn /// talk-and-laughter) are NOT dat-authored on objects. Investigated from the /// 2026-08-08 listening-gate finding ("I still dont hear the specific ambient /// sound when I enter a inn"), and the data answered decisively: /// /// /// No interior cell in the town landblock places a static /// whose Setup carries an ambient-slot sound table (first test). /// No Setup in the ENTIRE portal dat (5,935 scanned) /// references an ambient-slot table at all (second test). /// Yet 23 sound tables carrying ONLY Ambient1..8 /// slots exist (third test) — pure soundscape banks with nothing client-side /// pointing at them. /// /// /// /// Conclusion: those banks are WIRE-BOUND — the server attaches one to an /// emitter object via CreateObject's sound-table field and fires the ambient /// slots over 0xF750 (ACE: EmoteType.Sound heartbeat emotes → /// GameMessageSound). The client side of that path is slice A3 and is /// live; whether chatter is heard depends on the server's world data actually /// emitting it. A probed session (ACDREAM_PROBE_SOUND_WIRE=1) against /// the local ACE received ZERO 0xF750 events across a town walkabout — the /// silence is server content, not a client drop. Skips cleanly when dats are /// absent. /// /// public sealed class EnvCellSoundEmitterInventoryTests { private readonly ITestOutputHelper _out; public EnvCellSoundEmitterInventoryTests(ITestOutputHelper output) => _out = output; [Fact] public void HoltburgInteriors_CarryNoDatAuthoredAmbientEmitters() { string? datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) return; using var dats = new DatCollection(new DatCollectionOptions { DatDirectory = datDir, AccessType = DatAccessType.Read, }); // Holtburg landblock: envcells are 0xA9B4_0100 upward. const uint landblock = 0xA9B40000u; var setupTables = new Dictionary(); // setup -> soundtable var emitterCells = new List<(uint Cell, uint Setup, uint Table, string Slots)>(); for (uint cellId = landblock | 0x0100u; cellId <= (landblock | 0x01FFu); cellId++) { EnvCell? cell; try { cell = dats.Get(cellId); } catch { continue; } if (cell is null) continue; foreach (var stab in cell.StaticObjects) { // Stab ids reference either a Setup (0x02...) or a bare GfxObj // (0x01...); only Setups can carry a DefaultSoundTable. if ((stab.Id & 0xFF000000u) != 0x02000000u) continue; if (!setupTables.TryGetValue(stab.Id, out uint tableDid)) { Setup? setup = dats.Get(stab.Id); tableDid = setup?.DefaultSoundTable?.DataId ?? 0u; setupTables[stab.Id] = tableDid; } if (tableDid == 0u) continue; SoundTable? table = dats.Get(tableDid); if (table is null) continue; var ambientSlots = table.Sounds.Keys .Where(sound => (uint)sound is >= 0x46u and <= 0x4Eu) .OrderBy(sound => (uint)sound) .ToList(); if (ambientSlots.Count == 0) continue; emitterCells.Add(( cellId, stab.Id, tableDid, string.Join(",", ambientSlots))); } } foreach (var row in emitterCells) { _out.WriteLine( $"cell=0x{row.Cell:X8} setup=0x{row.Setup:X8} " + $"table=0x{row.Table:X8} slots=[{row.Slots}]"); } _out.WriteLine($"total emitter placements: {emitterCells.Count}"); // The NEGATIVE is the finding: interior ambience is not dat-authored // on placed objects — it is wire-bound by the server (see the class // summary). If this ever becomes non-empty the dat set changed and the // interior-ambience conclusion must be revisited. Assert.Empty(emitterCells); } [Fact] public void PortalDat_SetupsWithAmbientSlotSoundTables() { string? datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) return; using var dats = new DatCollection(new DatCollectionOptions { DatDirectory = datDir, AccessType = DatAccessType.Read, }); int scanned = 0; var hits = new List(); foreach (var entry in dats.Portal.Tree .Where(e => e.Id >= 0x02000000u && e.Id <= 0x0200FFFFu)) { Setup? setup; try { setup = dats.Portal.Get(entry.Id); } catch { continue; } if (setup is null) continue; scanned++; uint tableDid = setup.DefaultSoundTable?.DataId ?? 0u; if (tableDid == 0u) continue; SoundTable? table = dats.Get(tableDid); if (table is null) continue; var ambient = table.Sounds.Keys .Where(s => (uint)s is >= 0x46u and <= 0x4Eu) .OrderBy(s => (uint)s) .ToList(); if (ambient.Count == 0) continue; hits.Add( $"setup=0x{entry.Id:X8} table=0x{tableDid:X8} " + $"mtable=0x{(setup.DefaultMotionTable?.DataId ?? 0):X8} " + $"script=0x{(setup.DefaultScript?.DataId ?? 0):X8} " + $"stable-slots=[{string.Join(",", ambient)}]"); } foreach (string hit in hits) _out.WriteLine(hit); _out.WriteLine($"setups scanned: {scanned}; ambient-slot setups: {hits.Count}"); } [Fact] public void SoundTables_WithAmbientSlots_ExistForWireBinding() { string? datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) return; using var dats = new DatCollection(new DatCollectionOptions { DatDirectory = datDir, AccessType = DatAccessType.Read, }); int tables = 0; var hits = new List(); foreach (var entry in dats.Portal.Tree .Where(e => e.Id >= 0x20000000u && e.Id <= 0x2000FFFFu)) { SoundTable? table; try { table = dats.Get(entry.Id); } catch { continue; } if (table is null) continue; tables++; var ambient = table.Sounds.Keys .Where(s => (uint)s is >= 0x46u and <= 0x4Eu) .OrderBy(s => (uint)s) .ToList(); if (ambient.Count == 0) continue; hits.Add( $"table=0x{entry.Id:X8} ambient-slots=[{string.Join(",", ambient)}] " + $"total-slots={table.Sounds.Count}"); } foreach (string hit in hits) _out.WriteLine(hit); _out.WriteLine($"tables scanned: {tables}; with ambient slots: {hits.Count}"); } }