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) {