using System; using System.Collections.Generic; using System.Numerics; using DatReaderWriter.DBObjs; namespace AcDream.Core.Audio; /// /// Builds the live ambient set from the region file's authored data — retail's /// Ambient::InitSounds plus CLandBlock::add_ambient_sounds @ /// 0x50AC10 and LScape::add_ambient_sounds. /// /// /// Where ambients come from. Entirely region.dat: /// Region.SoundInfo.STBDesc[] holds the AmbientSTBDesc entries, /// Region.SceneInfo.SceneTypes[i].StbIndex points into that list, and /// Region.TerrainInfo.TerrainTypes[t].SceneTypes[s] points at the scene /// type. 0xFFFFFFFF means none. There is no separate ambient dat range — /// AmbientSTBDesc.STBId is an ordinary SoundTable DID. /// /// /// /// Granularity. Selection is per LAND CELL, not per landblock: retail /// walks the 8×8 cells of each landblock, decodes that cell's terrain word to /// (terrainType, sceneIndex), and positions the contribution at the /// cell's SW vertex. Only the 3×3 landblock ring around the viewer contributes /// (LScape::add_ambient_sounds feeds blocks whose /// get_block_orient is ring ≤ 1). /// /// /// /// Indoors is silent. CEnvCell::add_ambient_sounds exists in the /// PDB but is ICF-folded onto a bare ret, and the EnvCell format carries /// no sound field — corroborated independently by two research lanes. Dungeon /// silence is retail-correct; any indoor ambient would be a new feature needing /// a divergence row, not a port. /// /// public sealed class AmbientSoundGatherer { /// Land cells per landblock side. public const int CellsPerSide = 8; /// Terrain-word entries per landblock side (a 9×9 vertex grid). private const int VerticesPerSide = 9; /// Retail's "no entry" sentinel in the scene/STB index chain. private const uint NoIndex = 0xFFFFFFFFu; private readonly AmbientSoundScheduler _scheduler; public AmbientSoundGatherer(AmbientSoundScheduler scheduler) => _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler)); /// /// Rebuild the ambient set for a listener standing at /// — the listener's LANDBLOCK-LOCAL /// position, x and y in [0, 192), as retail's Position carries /// it. supplies the terrain words for each /// landblock in the 3×3 ring, keyed by landblock id; a missing entry simply /// contributes nothing. /// /// /// Frames matter here. Offsets are computed the way retail's /// Position::get_offset / LandDefs::get_block_offset do — /// landblock delta plus in-block coordinates — NOT by differencing absolute /// world coordinates. acdream's live Position is in a streamed frame /// rebased on the streaming centre, so subtracting it from an absolute cell /// coordinate yields tens of kilometres and culls every contribution. The /// streamed-frame listener position is still needed for PLAYBACK, but it is /// a separate value carried by the scheduler. /// /// public void Rebuild( Region region, uint viewerLandblockId, Vector3 listenerLocalPosition, Func landblocks, double now) { ArgumentNullException.ThrowIfNull(region); ArgumentNullException.ThrowIfNull(landblocks); _scheduler.BeginRebuild(); uint viewerX = viewerLandblockId >> 24; uint viewerY = (viewerLandblockId >> 16) & 0xFFu; for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { long blockX = viewerX + dx; long blockY = viewerY + dy; if (blockX < 0 || blockX > 0xFF || blockY < 0 || blockY > 0xFF) continue; uint landblockId = ((uint)blockX << 24) | ((uint)blockY << 16) | 0xFFFFu; ushort[]? terrain = landblocks(landblockId); if (terrain is null || terrain.Length < VerticesPerSide * VerticesPerSide) continue; ContributeLandblock( region, dx, dy, terrain, listenerLocalPosition); } } _scheduler.EndRebuild(now); } private void ContributeLandblock( Region region, int blockDeltaX, int blockDeltaY, ushort[] terrain, Vector3 listenerLocalPosition) { // The ring neighbour's origin RELATIVE to the listener's own landblock. const float landblockLength = CellsPerSide * AmbientSoundConstants.LandCellLength; float blockOriginX = blockDeltaX * landblockLength; float blockOriginY = blockDeltaY * landblockLength; // 8x8 CELLS, not the 9x9 vertex grid: each cell contributes once, at its // south-west vertex. for (int x = 0; x < CellsPerSide; x++) { for (int y = 0; y < CellsPerSide; y++) { ushort raw = terrain[(x * VerticesPerSide) + y]; uint terrainType = (uint)((raw >> 2) & 0x1F); uint sceneIndex = (uint)((raw >> 11) & 0x1F); if (!TryResolveStbDesc(region, terrainType, sceneIndex, out var stb)) continue; // Retail positions each contribution at the land cell's SW // vertex. Z stays planar: CalcDir ignores Z outright and // CalcWeight's Z term is the terrain height difference, which we // do not sample here (see the register row). var offset = new Vector3( blockOriginX + (x * AmbientSoundConstants.LandCellLength) - listenerLocalPosition.X, blockOriginY + (y * AmbientSoundConstants.LandCellLength) - listenerLocalPosition.Y, 0f); // Cheap reject before touching the descriptor list: beyond 120 m // the weight is zero and retail's AddSound gate drops it. if (offset.LengthSquared() > AmbientSoundConstants.MaxDistanceSq) continue; // Ambient::AddSound @ 0x551610 adds this cell's weight to the // shared denominator ONCE, then feeds every descriptor in the // table. Adding it per descriptor would divide each bed's // crossfade by the entry count and push quiet beds under the // 0.03 audibility floor. _scheduler.ContributeCell(offset, stb!, static (stb, index) => { var sound = stb.AmbientSounds[index]; return new AmbientSoundDescriptor( (SoundId)(uint)sound.SType, sound.Volume, sound.BaseChance, sound.MinRate, sound.MaxRate); }); } } } /// /// terrain type → scene type → STB descriptor, with retail's /// 0xFFFFFFFF "none" sentinel honoured at each hop. /// private static bool TryResolveStbDesc( Region region, uint terrainType, uint sceneIndex, out DatReaderWriter.Types.AmbientSTBDesc? stb) { stb = null; var terrainTypes = region.TerrainInfo?.TerrainTypes; if (terrainTypes is null || terrainType >= terrainTypes.Count) return false; var sceneTypes = terrainTypes[(int)terrainType].SceneTypes; if (sceneIndex >= sceneTypes.Count) return false; uint sceneTypeIndex = sceneTypes[(int)sceneIndex]; var sceneList = region.SceneInfo?.SceneTypes; if (sceneTypeIndex == NoIndex || sceneList is null || sceneTypeIndex >= sceneList.Count) return false; uint stbIndex = sceneList[(int)sceneTypeIndex].StbIndex; var descriptors = region.SoundInfo?.STBDesc; if (stbIndex == NoIndex || descriptors is null || stbIndex >= descriptors.Count) return false; var candidate = descriptors[(int)stbIndex]; if (candidate.STBId == 0 || candidate.AmbientSounds.Count == 0) return false; stb = candidate; return true; } }