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;
}
}