acdream/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs
Erik 55f26f2a9c feat(render): Phase A8 R2 — WbDrawDispatcher.EntitySet taxonomy partition
Reshapes the dormant EntitySet enum from binary IndoorOnly/OutdoorOnly to
a three-way taxonomy-aware partition:

  IndoorPass     — cell mesh + cell statics + building shells
                   (ParentCellId.HasValue OR IsBuildingShell), live-dynamic
                   excluded
  OutdoorScenery — outdoor scenery only (ParentCellId == null AND
                   !IsBuildingShell), live-dynamic excluded
  LiveDynamic    — ServerGuid != 0 (player, NPCs, dropped items)

Centralizes the membership predicate in EntityMatchesSet to keep the three
call sites (two in WalkEntitiesInto, one in WalkEntitiesForTest) DRY.

R1's IsBuildingShell flag is now consumed at render time. Integration into
the render frame ships in R3.

Tests rebuilt from scratch — 7 cases cover the new partition truth table.
Existing dispatcher tests (Tier 1 cache, etc.) continue to pass under the
default EntitySet.All.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:42:09 +02:00

214 lines
7.6 KiB
C#

// Phase A8 — verify the WbDrawDispatcher EntitySet partition (taxonomy-aware).
//
// The pure-data WalkEntitiesForTest helper iterates a flat entity list and
// returns the IDs that survive the EntitySet filter + visibleCellIds gate.
//
// EntitySet.IndoorPass — ParentCellId.HasValue OR IsBuildingShell,
// and NOT live-dynamic (ServerGuid == 0).
// Building shells render unconditionally indoors;
// live-dynamic flows through LiveDynamic instead.
// EntitySet.OutdoorScenery — ParentCellId == null AND !IsBuildingShell
// AND not live-dynamic.
// EntitySet.LiveDynamic — ServerGuid != 0 (player, NPCs, dropped items,
// idle doors after animation). Drawn last with
// stencil disabled.
// EntitySet.All — pre-A8 behavior (visibleCellIds gates indoor;
// outdoor entities pass through).
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 CellEnt(uint id, uint cellId) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x01000001u,
ParentCellId = cellId,
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
private static WorldEntity OutdoorScenery(uint id) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x01000001u,
ParentCellId = null,
IsBuildingShell = false,
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
private static WorldEntity BuildingShell(uint id) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x02000001u,
ParentCellId = null,
IsBuildingShell = true,
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
private static WorldEntity LiveDynamic(uint id, uint serverGuid) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x02000001u,
ServerGuid = serverGuid,
ParentCellId = null,
IsBuildingShell = false,
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
[Fact]
public void IndoorPass_IncludesCellEntities()
{
var entities = new List<WorldEntity>
{
CellEnt(0x10000001, 0xA9B40143),
OutdoorScenery(0x10000002),
CellEnt(0x10000003, 0xA9B40144),
};
var visible = new HashSet<uint> { 0xA9B40143u, 0xA9B40144u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Equal(2, result.Count);
Assert.Contains(0x10000001u, result);
Assert.Contains(0x10000003u, result);
Assert.DoesNotContain(0x10000002u, result);
}
[Fact]
public void IndoorPass_IncludesBuildingShells_EvenWithNullParentCellId()
{
var entities = new List<WorldEntity>
{
BuildingShell(0xC0000001), // cottage wall
OutdoorScenery(0xC0000002), // tree
CellEnt(0x40000001, 0xA9B40143),
};
var visible = new HashSet<uint> { 0xA9B40143u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Equal(2, result.Count);
Assert.Contains(0xC0000001u, result); // building shell included
Assert.Contains(0x40000001u, result); // cell entity included
Assert.DoesNotContain(0xC0000002u, result); // tree excluded
}
[Fact]
public void IndoorPass_ExcludesLiveDynamic()
{
var entities = new List<WorldEntity>
{
CellEnt(0x40000001, 0xA9B40143),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
};
var visible = new HashSet<uint> { 0xA9B40143u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Single(result);
Assert.Contains(0x40000001u, result);
Assert.DoesNotContain(0x10000001u, result); // live-dynamic excluded
}
[Fact]
public void OutdoorScenery_ExcludesBuildingShells()
{
var entities = new List<WorldEntity>
{
BuildingShell(0xC0000001), // cottage wall — excluded
OutdoorScenery(0xC0000002), // tree — included
CellEnt(0x40000001, 0xA9B40143), // cell — excluded
};
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.OutdoorScenery);
Assert.Single(result);
Assert.Contains(0xC0000002u, result);
Assert.DoesNotContain(0xC0000001u, result);
Assert.DoesNotContain(0x40000001u, result);
}
[Fact]
public void OutdoorScenery_ExcludesLiveDynamic()
{
var entities = new List<WorldEntity>
{
OutdoorScenery(0xC0000001),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
};
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.OutdoorScenery);
Assert.Single(result);
Assert.Contains(0xC0000001u, result);
Assert.DoesNotContain(0x10000001u, result);
}
[Fact]
public void LiveDynamic_IncludesOnlyServerSpawned()
{
var entities = new List<WorldEntity>
{
OutdoorScenery(0xC0000001),
BuildingShell(0xC0000002),
CellEnt(0x40000001, 0xA9B40143),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
LiveDynamic(0x10000002, serverGuid: 0x50000456u),
};
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.LiveDynamic);
Assert.Equal(2, result.Count);
Assert.Contains(0x10000001u, result);
Assert.Contains(0x10000002u, result);
Assert.DoesNotContain(0xC0000001u, result);
Assert.DoesNotContain(0xC0000002u, result);
Assert.DoesNotContain(0x40000001u, result);
}
[Fact]
public void All_MatchesPreA8Behavior()
{
var entities = new List<WorldEntity>
{
CellEnt(0x40000001, 0xA9B40143),
OutdoorScenery(0xC0000001),
BuildingShell(0xC0000002),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
CellEnt(0x40000002, 0xA9B40999), // not in visibleCellIds
};
var visible = new HashSet<uint> { 0xA9B40143u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.All);
// Pre-A8: visibleCellIds gates indoor entities only; outdoor entities
// (regardless of building/scenery/live-dynamic) pass through.
Assert.Equal(4, result.Count);
Assert.Contains(0x40000001u, result);
Assert.Contains(0xC0000001u, result);
Assert.Contains(0xC0000002u, result);
Assert.Contains(0x10000001u, result);
Assert.DoesNotContain(0x40000002u, result);
}
}