using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.Core.Meshing; using Xunit; namespace AcDream.Core.Tests.Rendering.Wb; public class EntityClassificationCacheTests { [Fact] public void TryGet_EmptyCache_ReturnsFalse() { var cache = new EntityClassificationCache(); bool found = cache.TryGet(entityId: 42, out var entry); Assert.False(found); Assert.Null(entry); } [Fact] public void Populate_ThenTryGet_ReturnsBatchesInOrder() { var cache = new EntityClassificationCache(); var batches = new[] { MakeCachedBatch(ibo: 1, firstIndex: 0, indexCount: 6, texHandle: 0xAA), MakeCachedBatch(ibo: 1, firstIndex: 6, indexCount: 6, texHandle: 0xBB), }; cache.Populate(entityId: 100, landblockHint: 0xA9B40000u, batches); Assert.True(cache.TryGet(100, out var entry)); Assert.NotNull(entry); Assert.Equal(100u, entry!.EntityId); Assert.Equal(0xA9B40000u, entry.LandblockHint); Assert.Equal(batches, entry.Batches); } [Fact] public void Populate_OverridesExistingEntry() { var cache = new EntityClassificationCache(); cache.Populate(100, 0u, new[] { MakeCachedBatch(1, 0, 6, 0xAA) }); cache.Populate(100, 0u, new[] { MakeCachedBatch(2, 0, 12, 0xCC) }); Assert.True(cache.TryGet(100, out var entry)); Assert.NotNull(entry); Assert.Single(entry!.Batches); Assert.Equal(0xCCu, entry.Batches[0].BindlessTextureHandle); } [Fact] public void Count_TracksLiveEntries() { var cache = new EntityClassificationCache(); Assert.Equal(0, cache.Count); cache.Populate(1, 0u, new[] { MakeCachedBatch(1, 0, 6, 0xAA) }); Assert.Equal(1, cache.Count); cache.Populate(2, 0u, new[] { MakeCachedBatch(2, 0, 6, 0xAA) }); Assert.Equal(2, cache.Count); // Re-populate same id — should not double-count. cache.Populate(1, 0u, new[] { MakeCachedBatch(3, 0, 6, 0xBB) }); Assert.Equal(2, cache.Count); } [Fact] public void Populate_WithEmptyBatches_StoresEmptyEntry() { var cache = new EntityClassificationCache(); cache.Populate(entityId: 7, landblockHint: 0u, System.Array.Empty()); Assert.True(cache.TryGet(7, out var entry)); Assert.NotNull(entry); Assert.Empty(entry!.Batches); } [Fact] public void Populate_SetupMultiPart_StoresFlatBatchPerSubPart() { // Synthetic Setup with 3 subParts × 2 batches each = 6 flat entries. // This pins the spec §3 Q4 decision: pre-flatten Setup multi-parts at // populate time so the per-frame hot path is branchless. var cache = new EntityClassificationCache(); var batches = new CachedBatch[6]; for (int subPart = 0; subPart < 3; subPart++) for (int b = 0; b < 2; b++) { batches[subPart * 2 + b] = MakeCachedBatch( ibo: (uint)(subPart + 1), firstIndex: (uint)(b * 6), indexCount: 6, texHandle: (ulong)(0x100 + subPart * 2 + b)); } cache.Populate(99, 0u, batches); Assert.True(cache.TryGet(99, out var entry)); Assert.NotNull(entry); Assert.Equal(6, entry!.Batches.Length); Assert.Equal(0x100u, entry.Batches[0].BindlessTextureHandle); Assert.Equal(0x105u, entry.Batches[5].BindlessTextureHandle); } private static CachedBatch MakeCachedBatch( uint ibo, uint firstIndex, int indexCount, ulong texHandle) { var key = new GroupKey( Ibo: ibo, FirstIndex: firstIndex, BaseVertex: 0, IndexCount: indexCount, BindlessTextureHandle: texHandle, TextureLayer: 0, Translucency: TranslucencyKind.Opaque); return new CachedBatch(key, texHandle, Matrix4x4.Identity); } }