using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter.DBObjs; using Xunit; namespace AcDream.Core.Tests.Rendering.Wb; public sealed class EntitySpawnAdapterTests { // ── Happy-path: server-spawned entity ───────────────────────────────── [Fact] public void OnCreate_ServerSpawnedEntity_RegistersAnimatedEntityState() { var cache = new CapturingTextureCache(); var adapter = MakeAdapter(cache); var entity = MakeEntity(id: 1, serverGuid: 0xDEAD0001u); var state = adapter.OnCreate(entity); Assert.NotNull(state); Assert.Same(state, adapter.GetState(0xDEAD0001u)); } [Fact] public void OnCreate_ServerSpawnedEntity_SequencerIsNotNull() { var adapter = MakeAdapter(); var entity = MakeEntity(id: 1, serverGuid: 0xDEAD0002u); var state = adapter.OnCreate(entity); Assert.NotNull(state!.Sequencer); } // ── Atlas-tier filter ───────────────────────────────────────────────── [Fact] public void OnCreate_ProceduralEntity_ReturnsNullAndRegistersNothing() { var cache = new CapturingTextureCache(); var adapter = MakeAdapter(cache); // ServerGuid == 0 → atlas-tier, must not be processed here. var entity = MakeEntity(id: 2, serverGuid: 0u); var state = adapter.OnCreate(entity); Assert.Null(state); Assert.Null(adapter.GetState(0u)); // No texture decode should have been triggered. Assert.Empty(cache.Calls); } // ── Palette-override texture decode ─────────────────────────────────── [Fact] public void OnCreate_WithPaletteOverrideAndSurfaceOverrides_TriggersTextureCacheDecode() { var cache = new CapturingTextureCache(); var adapter = MakeAdapter(cache); var palette = new PaletteOverride( BasePaletteId: 0x04001234u, SubPalettes: new[] { new PaletteOverride.SubPaletteRange(0x04002000u, 0, 2), }); // Entity carries two parts each with one surface override. var entity = new WorldEntity { Id = 10, ServerGuid = 0xBEEF0001u, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, PaletteOverride = palette, MeshRefs = new[] { new MeshRef(0x01000010u, Matrix4x4.Identity) { SurfaceOverrides = new Dictionary { { 0x08000100u, 0u }, // surfaceId → origTex (0 = none) }, }, new MeshRef(0x01000020u, Matrix4x4.Identity) { SurfaceOverrides = new Dictionary { { 0x08000200u, 0x05000300u }, // with origTex override }, }, }, }; adapter.OnCreate(entity); // One call per surface-with-override: (0x08000100, null) and (0x08000200, 0x05000300). Assert.Equal(2, cache.Calls.Count); Assert.Contains(cache.Calls, c => c.SurfaceId == 0x08000100u && c.OrigTexOverride == null && c.Palette == palette); Assert.Contains(cache.Calls, c => c.SurfaceId == 0x08000200u && c.OrigTexOverride == 0x05000300u && c.Palette == palette); } [Fact] public void OnCreate_WithPaletteOverrideButNoSurfaceOverrides_DoesNotCallCache() { // Surfaces without SurfaceOverrides == null are decoded lazily at draw // time; the adapter only pre-warms what it knows at spawn time. var cache = new CapturingTextureCache(); var adapter = MakeAdapter(cache); var entity = new WorldEntity { Id = 11, ServerGuid = 0xBEEF0002u, SourceGfxObjOrSetupId = 0x02000002u, Position = Vector3.Zero, Rotation = Quaternion.Identity, PaletteOverride = new PaletteOverride(0x04001235u, Array.Empty()), // MeshRef with NO SurfaceOverrides. MeshRefs = new[] { new MeshRef(0x01000011u, Matrix4x4.Identity) }, }; adapter.OnCreate(entity); Assert.Empty(cache.Calls); } [Fact] public void OnCreate_WithoutPaletteOverride_DoesNotCallCache() { var cache = new CapturingTextureCache(); var adapter = MakeAdapter(cache); var entity = MakeEntity(id: 12, serverGuid: 0xBEEF0003u); adapter.OnCreate(entity); Assert.Empty(cache.Calls); } // ── OnRemove ───────────────────────────────────────────────────────── [Fact] public void OnRemove_ReleasesPerEntityState() { var adapter = MakeAdapter(); var entity = MakeEntity(id: 20, serverGuid: 0xCAFE0001u); adapter.OnCreate(entity); Assert.NotNull(adapter.GetState(0xCAFE0001u)); adapter.OnRemove(0xCAFE0001u); Assert.Null(adapter.GetState(0xCAFE0001u)); } [Fact] public void OnRemove_UnknownGuid_NoOps() { var adapter = MakeAdapter(); // Must not throw. adapter.OnRemove(0xDEADBEEFu); } // ── Multiple entities ───────────────────────────────────────────────── [Fact] public void OnCreate_MultipleEntities_EachGetsOwnState() { var adapter = MakeAdapter(); var e1 = MakeEntity(id: 30, serverGuid: 0x11110001u); var e2 = MakeEntity(id: 31, serverGuid: 0x11110002u); var s1 = adapter.OnCreate(e1); var s2 = adapter.OnCreate(e2); Assert.NotNull(s1); Assert.NotNull(s2); Assert.NotSame(s1, s2); Assert.Same(s1, adapter.GetState(0x11110001u)); Assert.Same(s2, adapter.GetState(0x11110002u)); } [Fact] public void OnRemove_OnlyReleasesTargetGuid() { var adapter = MakeAdapter(); var e1 = MakeEntity(id: 40, serverGuid: 0x22220001u); var e2 = MakeEntity(id: 41, serverGuid: 0x22220002u); adapter.OnCreate(e1); adapter.OnCreate(e2); adapter.OnRemove(0x22220001u); Assert.Null(adapter.GetState(0x22220001u)); Assert.NotNull(adapter.GetState(0x22220002u)); } // ── Helpers ─────────────────────────────────────────────────────────── private static EntitySpawnAdapter MakeAdapter(ITextureCachePerInstance? cache = null) { cache ??= new CapturingTextureCache(); return new EntitySpawnAdapter(cache, _ => MakeSequencer()); } private static WorldEntity MakeEntity(uint id, uint serverGuid) => new WorldEntity { Id = id, ServerGuid = serverGuid, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = new[] { new MeshRef(0x01000001u, Matrix4x4.Identity) }, }; private static AnimationSequencer MakeSequencer() => new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader()); // ── Mocks / stubs ───────────────────────────────────────────────────── /// /// Capture every call to GetOrUploadWithPaletteOverride so tests can /// assert without a live GL context. /// private sealed class CapturingTextureCache : ITextureCachePerInstance { public readonly record struct Call(uint SurfaceId, uint? OrigTexOverride, PaletteOverride Palette); public List Calls { get; } = new(); public uint GetOrUploadWithPaletteOverride( uint surfaceId, uint? overrideOrigTextureId, PaletteOverride paletteOverride) { Calls.Add(new Call(surfaceId, overrideOrigTextureId, paletteOverride)); return 1u; // Fake GL handle. } } private sealed class NullAnimationLoader : IAnimationLoader { public Animation? LoadAnimation(uint id) => null; } }