From 1d1afcd562476d3f467f8f4a60ebb4eabd6eaa09 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 10 May 2026 19:22:50 +0200 Subject: [PATCH] feat(render #53): wire EntityClassificationCache.InvalidateEntity at despawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GameWindow.RemoveLiveEntityByServerGuid now invalidates the entity's cache entry next to the existing _animatedEntities.Remove(). Fires for DeleteObject (0xF747) and the dedup leg of ObjDescEvent (0xF625). Adds test #15 (despawn-respawn under reused id repopulates fresh) per spec section 7.5 — pins the audit's ObjDescEvent-as-despawn-respawn contract. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 1 + .../Wb/EntityClassificationCacheTests.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index b401553..2ea6f5d 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2945,6 +2945,7 @@ public sealed class GameWindow : IDisposable _worldState.RemoveEntityByServerGuid(serverGuid); _worldGameState.RemoveById(existingEntity.Id); _animatedEntities.Remove(existingEntity.Id); + _classificationCache.InvalidateEntity(existingEntity.Id); _physicsEngine.ShadowObjects.Deregister(existingEntity.Id); // Dead-reckon state is keyed by SERVER guid (not local id) so we diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs index 4766461..bc05262 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs @@ -171,6 +171,28 @@ public class EntityClassificationCacheTests Assert.Equal(1, cache.Count); } + [Fact] + public void DespawnRespawn_UnderReusedId_RepopulatesFresh() + { + // Pins the audit's ObjDescEvent contract (audit section 1): + // ObjDescEvent is despawn + respawn (with a NEW local entity.Id), + // never an in-place mutation. Even when an id IS reused + // (theoretical — _liveEntityIdCounter is monotonic in practice), + // the cache must serve fresh data after invalidation. + var cache = new EntityClassificationCache(); + var batchesV1 = new[] { MakeCachedBatch(1, 0, 6, 0xAA) }; + var batchesV2 = new[] { MakeCachedBatch(2, 6, 12, 0xCC) }; + + cache.Populate(100, 0xA9B40000u, batchesV1); + cache.InvalidateEntity(100); + cache.Populate(100, 0xA9B40000u, batchesV2); + + Assert.True(cache.TryGet(100, out var entry)); + Assert.NotNull(entry); + Assert.Equal(batchesV2, entry!.Batches); + Assert.Equal(0xCCu, entry.Batches[0].BindlessTextureHandle); + } + private static CachedBatch MakeCachedBatch( uint ibo, uint firstIndex, int indexCount, ulong texHandle) {