// Phase A8 — verify the WbDrawDispatcher EntitySet partition. // // The pure-data WalkEntitiesForTest helper iterates a flat entity list and // returns the IDs that survive the EntitySet filter + visibleCellIds gate. // EntitySet.IndoorOnly should include only entities with ParentCellId, // EntitySet.OutdoorOnly only entities with null ParentCellId, and // EntitySet.All (the default) should match the pre-A8 behavior. using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.Core.World; using Xunit; namespace AcDream.Core.Tests.Rendering.Wb; public class WbDrawDispatcherEntitySetTests { private static WorldEntity Indoor(uint id, uint cellId) => new() { Id = id, SourceGfxObjOrSetupId = 0x01000001u, ParentCellId = cellId, MeshRefs = new List { new() { GfxObjId = 0x01000001u }, }, Position = Vector3.Zero, Rotation = System.Numerics.Quaternion.Identity, }; private static WorldEntity Outdoor(uint id) => new() { Id = id, SourceGfxObjOrSetupId = 0x01000001u, ParentCellId = null, MeshRefs = new List { new() { GfxObjId = 0x01000001u }, }, Position = Vector3.Zero, Rotation = System.Numerics.Quaternion.Identity, }; [Fact] public void EntitySet_IndoorOnly_DropsOutdoorEntities() { var entities = new List { Indoor(0x10000001, 0xA9B40143), Outdoor(0x10000002), Indoor(0x10000003, 0xA9B40144), }; var visible = new HashSet { 0xA9B40143u, 0xA9B40144u }; var result = WbDrawDispatcher.WalkEntitiesForTest( entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorOnly); Assert.Equal(2, result.Count); Assert.Contains(0x10000001u, result); Assert.Contains(0x10000003u, result); Assert.DoesNotContain(0x10000002u, result); } [Fact] public void EntitySet_OutdoorOnly_KeepsOnlyNullParentCellId() { var entities = new List { Indoor(0x10000001, 0xA9B40143), Outdoor(0x10000002), Outdoor(0x10000003), }; var result = WbDrawDispatcher.WalkEntitiesForTest( entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.OutdoorOnly); Assert.Equal(2, result.Count); Assert.Contains(0x10000002u, result); Assert.Contains(0x10000003u, result); Assert.DoesNotContain(0x10000001u, result); } [Fact] public void EntitySet_All_MatchesPreA8Behavior() { var entities = new List { Indoor(0x10000001, 0xA9B40143), Outdoor(0x10000002), Indoor(0x10000003, 0xA9B40999), // not in visibleCellIds }; var visible = new HashSet { 0xA9B40143u }; var result = WbDrawDispatcher.WalkEntitiesForTest( entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.All); // Pre-A8: visibleCellIds gates indoor entities, outdoor entities pass. Assert.Equal(2, result.Count); Assert.Contains(0x10000001u, result); Assert.Contains(0x10000002u, result); Assert.DoesNotContain(0x10000003u, result); } // --------------------------------------------------------------------- // Phase A8 fix (post-visual-verification): animated entities (player, // NPCs, monsters) are live server-spawned objects with ParentCellId == null. // Without these tests, they would be classified as outdoor scenery and // stencil-gated by the OutdoorOnly pass — causing the character to // disappear when the camera enters a building. Fix: animatedEntityIds // overrides the ParentCellId-based partition. Animated entities always // belong in the IndoorOnly pass, never in OutdoorOnly. // --------------------------------------------------------------------- [Fact] public void EntitySet_IndoorOnly_IncludesAnimatedEntitiesEvenWithNullParentCellId() { var entities = new List { Indoor(0x10000001, 0xA9B40143), Outdoor(0x10000002), // static outdoor scenery Outdoor(0x40000005), // animated (player/NPC) }; var visible = new HashSet { 0xA9B40143u }; var animated = new HashSet { 0x40000005u }; var result = WbDrawDispatcher.WalkEntitiesForTest( entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorOnly, animatedEntityIds: animated); // Indoor entity passes via ParentCellId.HasValue. // Outdoor scenery (0x10000002) fails — not animated, no ParentCellId. // Animated entity (0x40000005) passes via animatedEntityIds override. Assert.Equal(2, result.Count); Assert.Contains(0x10000001u, result); Assert.Contains(0x40000005u, result); Assert.DoesNotContain(0x10000002u, result); } [Fact] public void EntitySet_OutdoorOnly_ExcludesAnimatedEntities() { var entities = new List { Outdoor(0x10000002), // static outdoor scenery Outdoor(0x40000005), // animated (player/NPC) Outdoor(0x40000006), // animated (NPC) }; var animated = new HashSet { 0x40000005u, 0x40000006u }; var result = WbDrawDispatcher.WalkEntitiesForTest( entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.OutdoorOnly, animatedEntityIds: animated); // Only static outdoor scenery passes the OutdoorOnly partition. // Animated entities are explicitly excluded so they don't get // stencil-gated (they're drawn in the IndoorOnly pass instead). Assert.Single(result); Assert.Contains(0x10000002u, result); Assert.DoesNotContain(0x40000005u, result); Assert.DoesNotContain(0x40000006u, result); } }