using System.Reflection; using System.Numerics; using AcDream.App.Streaming; using AcDream.Content; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using Xunit.Sdk; namespace AcDream.App.Tests.Streaming; public sealed class LandblockBuildFactoryTests { private const uint LandblockId = 0xA9B4FFFFu; private const uint AdjacentLandblockId = 0xA9B5FFFFu; private static readonly LandblockBuildOrigin HoltburgOrigin = new(0xA9, 0xB4); [Fact] public void BuildFar_ReturnsOnlyHeightmapAndCapturedOrigin() { var dat = CreateDat(out RecordingDatProxy proxy); var heightmap = new LandBlock { Id = LandblockId }; proxy.Add(LandblockId, heightmap); var factory = Factory(dat, new object()); LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadFar)); Assert.NotNull(result); Assert.Same(heightmap, result.Landblock.Heightmap); Assert.Empty(result.Landblock.Entities); Assert.Same(PhysicsDatBundle.Empty, result.Landblock.PhysicsDats); Assert.Null(result.EnvCells); Assert.Equal(HoltburgOrigin, result.Origin); Assert.Equal([(typeof(LandBlock), LandblockId)], proxy.Reads); } [Fact] public void Build_RejectsUnspecifiedOriginBeforeReadingDat() { var dat = CreateDat(out RecordingDatProxy proxy); var factory = Factory(dat, new object()); Assert.Throws(() => factory.Build( new LandblockBuildRequest( LandblockId, LandblockStreamJobKind.LoadFar, Generation: 1, Origin: default))); Assert.Empty(proxy.Reads); } [Fact] public void Build_MissingHeightmapReturnsNullWithoutPartialPayload() { var dat = CreateDat(out RecordingDatProxy proxy); var factory = Factory(dat, new object()); LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadFar)); Assert.Null(result); Assert.Equal([(typeof(LandBlock), LandblockId)], proxy.Reads); } [Fact] public void BuildNear_MissingLandblockInfoReturnsCompleteEmptyNearPayload() { var dat = CreateDat(out RecordingDatProxy proxy); proxy.Add(LandblockId, new LandBlock { Id = LandblockId }); var factory = Factory(dat, new object()); LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadNear)); Assert.NotNull(result); Assert.Empty(result.Landblock.Entities); var envCells = Assert.IsType( result.EnvCells); Assert.Empty(envCells.VisibilityCells); Assert.Empty(envCells.Shells); var physics = Assert.IsType(result.Landblock.PhysicsDats); Assert.Null(physics.Info); Assert.Empty(physics.EnvCells); } [Fact] public void BuildNear_MissingEnvCellSkipsOnlyThatCellWithoutPartialVisibilityEntry() { var dat = CreateDat(out RecordingDatProxy proxy); AddNearFixture(proxy, LandblockId, environmentId: 1, cellCount: 2); uint missingCell = (LandblockId & 0xFFFF0000u) | 0x0101u; proxy.Remove(missingCell); var factory = Factory(dat, new object()); LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadNear)); Assert.NotNull(result); var envCells = Assert.IsType( result.EnvCells); Assert.Equal( [(LandblockId & 0xFFFF0000u) | 0x0100u], envCells.VisibilityCells.Select(cell => cell.CellId)); var physics = Assert.IsType(result.Landblock.PhysicsDats); Assert.Single(physics.EnvCells); Assert.DoesNotContain(missingCell, physics.EnvCells.Keys); } [Fact] public async Task Build_SerializesCompleteNearTransactionsOnSharedGate() { var dat = CreateDat(out RecordingDatProxy proxy); AddNearFixture(proxy, LandblockId, environmentId: 1); AddNearFixture(proxy, AdjacentLandblockId, environmentId: 2); object gate = new(); var factory = Factory(dat, gate); Assert.NotNull(factory.Build(Request( LandblockId, LandblockStreamJobKind.LoadNear, generation: 1, HoltburgOrigin))); (Type Type, uint Id)[] firstSequence = proxy.Reads.ToArray(); proxy.ClearReads(); Assert.NotNull(factory.Build(Request( AdjacentLandblockId, LandblockStreamJobKind.LoadNear, generation: 1, new LandblockBuildOrigin(0xA9, 0xB5)))); (Type Type, uint Id)[] secondSequence = proxy.Reads.ToArray(); proxy.ClearReads(); proxy.ReadDelay = TimeSpan.FromMilliseconds(5); Task first = Task.Run(() => factory.Build(Request( LandblockId, LandblockStreamJobKind.LoadNear, generation: 2, HoltburgOrigin))); Task second = Task.Run(() => factory.Build(Request( AdjacentLandblockId, LandblockStreamJobKind.LoadNear, generation: 3, new LandblockBuildOrigin(0xA9, 0xB5)))); LandblockBuild?[] results = await Task.WhenAll(first, second); Assert.All(results, Assert.NotNull); Assert.Equal(1, proxy.MaxConcurrentReads); (Type Type, uint Id)[] combined = proxy.Reads.ToArray(); Assert.True( combined.SequenceEqual(firstSequence.Concat(secondSequence)) || combined.SequenceEqual(secondSequence.Concat(firstSequence)), "Near-build DAT reads interleaved instead of remaining one serialized transaction."); } [Fact] public async Task Build_UsesTheSuppliedSharedReaderGate() { var dat = CreateDat(out RecordingDatProxy proxy); proxy.Add(LandblockId, new LandBlock { Id = LandblockId }); object gate = new(); var factory = Factory(dat, gate); using var started = new ManualResetEventSlim(); Monitor.Enter(gate); Task build; try { build = Task.Run(() => { started.Set(); return factory.Build(Request(LandblockStreamJobKind.LoadFar)); }); Assert.True(started.Wait(TimeSpan.FromSeconds(2))); Assert.False(proxy.ReadObserved.Wait(TimeSpan.FromMilliseconds(100))); } finally { Monitor.Exit(gate); } Assert.NotNull(await build); Assert.True(proxy.ReadObserved.IsSet); } [Theory] [InlineData(LandblockStreamJobKind.LoadNear)] [InlineData(LandblockStreamJobKind.PromoteToNear)] public void Build_NearAndPromoteIncludeEveryValidZeroSubmeshVisibilityCell( LandblockStreamJobKind kind) { var dat = CreateDat(out RecordingDatProxy proxy); AddNearFixture(proxy, LandblockId, environmentId: 1, cellCount: 2); var factory = Factory(dat, new object()); LandblockBuild? result = factory.Build(Request(kind)); Assert.NotNull(result); var envCells = Assert.IsType( result.EnvCells); var physics = Assert.IsType(result.Landblock.PhysicsDats); Assert.Equal( [(LandblockId & 0xFFFF0000u) | 0x0100u, (LandblockId & 0xFFFF0000u) | 0x0101u], envCells.VisibilityCells.Select(cell => cell.CellId)); Assert.Empty(envCells.Shells); Assert.NotNull(physics.Info); Assert.Equal(2, physics.EnvCells.Count); Assert.Single(physics.Environments); } [Fact] public void ConstructorSnapshotsTheRetailHeightTable() { var dat = CreateDat(out _); var heights = Enumerable.Range(0, 256).Select(value => (float)value).ToArray(); var factory = new LandblockBuildFactory( dat, new object(), heights, new PhysicsDataCache()); heights[42] = -1f; var snapshot = Assert.IsType(typeof(LandblockBuildFactory) .GetField("_heightTable", BindingFlags.Instance | BindingFlags.NonPublic)! .GetValue(factory)); Assert.NotSame(heights, snapshot); Assert.Equal(42f, snapshot[42]); } [Fact] public void ConstructorRejectsIncompleteRetailHeightTable() { var dat = CreateDat(out _); Assert.Throws(() => new LandblockBuildFactory( dat, new object(), new float[255], new PhysicsDataCache())); } [Fact] public void Build_ExceptionReleasesSharedGateForTheNextTransaction() { var dat = CreateDat(out RecordingDatProxy proxy); proxy.Add(LandblockId, new LandBlock { Id = LandblockId }); proxy.ExceptionToThrow = new InvalidDataException("fixture read failure"); var factory = Factory(dat, new object()); Assert.Throws(() => factory.Build(Request(LandblockStreamJobKind.LoadFar))); proxy.ExceptionToThrow = null; Assert.NotNull(factory.Build(Request(LandblockStreamJobKind.LoadFar))); } [Fact] public void FactorySurface_HasNoRendererWorldStateOrLiveOriginDependency() { Type type = typeof(LandblockBuildFactory); Type[] dependencyTypes = type .GetFields(BindingFlags.Instance | BindingFlags.NonPublic) .Select(field => field.FieldType) .Concat(type.GetConstructors().SelectMany(ctor => ctor.GetParameters().Select(parameter => parameter.ParameterType))) .ToArray(); Assert.DoesNotContain(dependencyTypes, dependency => dependency.FullName?.Contains("GameWindow", StringComparison.Ordinal) == true); Assert.DoesNotContain(dependencyTypes, dependency => dependency.FullName?.Contains("GpuWorldState", StringComparison.Ordinal) == true); Assert.DoesNotContain(dependencyTypes, dependency => dependency.FullName?.Contains("LiveWorldOrigin", StringComparison.Ordinal) == true); Assert.DoesNotContain(dependencyTypes, dependency => dependency.Namespace?.StartsWith("Silk.NET", StringComparison.Ordinal) == true); } [Fact] public void InstalledDatNearBuild_IsDeterministicAndClosesEntityPhysicsPayload() { string? datDirectory = ResolveDatDirectory(); if (datDirectory is null) throw SkipException.ForSkip("Installed retail DATs are required."); using var dat = new BoundedTestDatCollection(datDirectory); var bounded = (IDatReaderWriter)dat; Region? region = bounded.Get(0x13000000u); float[]? heights = region?.LandDefs.LandHeightTable; Assert.NotNull(heights); Assert.True(heights.Length >= 256); var factory = new LandblockBuildFactory( bounded, new object(), heights, new PhysicsDataCache()); LandblockBuildRequest request = Request(LandblockStreamJobKind.LoadNear, generation: 4); LandblockBuild? first = factory.Build(request); LandblockBuild? second = factory.Build(request); Assert.NotNull(first); Assert.NotNull(second); Assert.Equal(HoltburgOrigin, first.Origin); Assert.NotNull(first.EnvCells); Assert.NotEmpty(first.Landblock.Entities); AssertEntityBuildsEqual(first.Landblock.Entities, second.Landblock.Entities); Assert.Equal( first.EnvCells.VisibilityCells.Select(cell => cell.CellId), second.EnvCells!.VisibilityCells.Select(cell => cell.CellId)); Assert.Equal( first.EnvCells.Shells.Select(shell => shell.GeometryId), second.EnvCells.Shells.Select(shell => shell.GeometryId)); LandBlockInfo? info = bounded.Get( (LandblockId & 0xFFFF0000u) | 0xFFFEu); Assert.NotNull(info); var expectedVisibilityCells = new List(); uint firstCellId = (LandblockId & 0xFFFF0000u) | 0x0100u; for (uint offset = 0; offset < info.NumCells; offset++) { uint cellId = firstCellId + offset; EnvCell? cell = bounded.Get(cellId); if (cell is null || cell.EnvironmentId == 0) continue; DatReaderWriter.DBObjs.Environment? environment = bounded.Get( 0x0D000000u | cell.EnvironmentId); if (environment?.Cells.ContainsKey(cell.CellStructure) == true) expectedVisibilityCells.Add(cellId); } Assert.Equal( expectedVisibilityCells, first.EnvCells.VisibilityCells.Select(cell => cell.CellId)); PhysicsDatBundle physics = Assert.IsType( first.Landblock.PhysicsDats); Assert.NotNull(physics.Info); foreach (WorldEntity entity in first.Landblock.Entities) { if ((entity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u) Assert.Contains(entity.SourceGfxObjOrSetupId, physics.Setups.Keys); foreach (MeshRef mesh in entity.MeshRefs) Assert.Contains(mesh.GfxObjId, physics.GfxObjs.Keys); } } private static LandblockBuildFactory Factory(IDatReaderWriter dat, object gate) => new(dat, gate, new float[256], new PhysicsDataCache()); private static LandblockBuildRequest Request( LandblockStreamJobKind kind, ulong generation = 1) => new(LandblockId, kind, generation, HoltburgOrigin); private static LandblockBuildRequest Request( uint landblockId, LandblockStreamJobKind kind, ulong generation, LandblockBuildOrigin origin) => new(landblockId, kind, generation, origin); private static void AddNearFixture( RecordingDatProxy proxy, uint landblockId, ushort environmentId, uint cellCount = 1) { proxy.Add(landblockId, new LandBlock { Id = landblockId }); proxy.Add( (landblockId & 0xFFFF0000u) | 0xFFFEu, new LandBlockInfo { NumCells = cellCount }); var environment = new DatReaderWriter.DBObjs.Environment { Id = 0x0D000000u | environmentId, }; for (uint offset = 0; offset < cellCount; offset++) { ushort structure = checked((ushort)(offset + 1)); uint cellId = (landblockId & 0xFFFF0000u) | (0x0100u + offset); proxy.Add(cellId, new EnvCell { Id = cellId, EnvironmentId = environmentId, CellStructure = structure, Position = new Frame { Orientation = Quaternion.Identity, }, }); environment.Cells[structure] = EmptyCellStruct(); } proxy.Add(environment.Id, environment); } private static CellStruct EmptyCellStruct() => new() { VertexArray = new VertexArray { Vertices = new Dictionary(), }, }; private static IDatReaderWriter CreateDat(out RecordingDatProxy proxy) { IDatReaderWriter dat = DispatchProxy.Create(); proxy = (RecordingDatProxy)(object)dat; return dat; } private static void AssertEntityBuildsEqual( IReadOnlyList expected, IReadOnlyList actual) { Assert.Equal(expected.Count, actual.Count); for (int i = 0; i < expected.Count; i++) { WorldEntity left = expected[i]; WorldEntity right = actual[i]; Assert.Equal(left.Id, right.Id); Assert.Equal(left.SourceGfxObjOrSetupId, right.SourceGfxObjOrSetupId); Assert.Equal(left.Position, right.Position); Assert.Equal(left.Rotation, right.Rotation); Assert.Equal(left.ParentCellId, right.ParentCellId); Assert.Equal(left.EffectCellId, right.EffectCellId); Assert.Equal(left.MeshRefs.Count, right.MeshRefs.Count); for (int part = 0; part < left.MeshRefs.Count; part++) Assert.Equal(left.MeshRefs[part], right.MeshRefs[part]); } } private static string? ResolveDatDirectory() { string? configured = System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); if (!string.IsNullOrWhiteSpace(configured) && Directory.Exists(configured)) return configured; const string installed = @"C:\Turbine\Asheron's Call"; if (Directory.Exists(installed)) return installed; string documents = Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); return Directory.Exists(documents) ? documents : null; } private class RecordingDatProxy : DispatchProxy { private readonly Dictionary<(Type Type, uint Id), object> _objects = new(); private readonly object _gate = new(); private int _activeReads; internal List<(Type Type, uint Id)> Reads { get; } = new(); internal int MaxConcurrentReads { get; private set; } internal TimeSpan ReadDelay { get; set; } internal Exception? ExceptionToThrow { get; set; } internal ManualResetEventSlim ReadObserved { get; } = new(); internal void Add(uint id, T value) where T : class => _objects[(typeof(T), id)] = value; internal void Remove(uint id) where T : class => _objects.Remove((typeof(T), id)); internal void ClearReads() { lock (_gate) { Reads.Clear(); MaxConcurrentReads = 0; } } protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { if (targetMethod?.Name == "Get" && args is [uint id]) { Type type = targetMethod.ReturnType; int active = Interlocked.Increment(ref _activeReads); ReadObserved.Set(); lock (_gate) { Reads.Add((type, id)); MaxConcurrentReads = Math.Max(MaxConcurrentReads, active); } try { if (ReadDelay > TimeSpan.Zero) Thread.Sleep(ReadDelay); if (ExceptionToThrow is { } error) throw error; return _objects.TryGetValue((type, id), out object? value) ? value : null; } finally { Interlocked.Decrement(ref _activeReads); } } if (targetMethod?.ReturnType == typeof(void)) return null; if (targetMethod?.ReturnType.IsValueType == true) return Activator.CreateInstance(targetMethod.ReturnType); return null; } } }