Attach the retail projectile driver to canonical LiveEntityRecord ownership, sharing one PhysicsBody and full-cell identity with RemoteMotion regardless of creation order. Apply timestamp-gated state, vector, and position corrections in place, preserve active ordinary-body behavior when Missile clears, and keep renderer, effects, shadows, and spatial buckets synchronized across loaded/pending transitions. Validate malformed packets before canonical timestamp mutation, validate adopted bodies from their current frame rather than stale CreateObject data, serialize late Setup resolution with streaming DAT reads, and preserve classification across clock anomalies. Retain shadow registrations through temporary leave-world residence while logical teardown remains generation-scoped. Add 31 App lifecycle/adversarial tests plus Core shadow suspension coverage, and synchronize the retail pseudocode, architecture, milestones, roadmap, and durable physics memory. Co-Authored-By: Codex <noreply@openai.com>
546 lines
21 KiB
C#
546 lines
21 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="ShadowObjectRegistry"/> — Task 7 cell-based
|
|
/// spatial index for object collision.
|
|
/// </summary>
|
|
public class ShadowObjectRegistryTests
|
|
{
|
|
private const uint LbId = 0xA9B40000u; // landblock prefix used throughout
|
|
private const float OffX = 0f;
|
|
private const float OffY = 0f;
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Register / TotalRegistered
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Register_SingleEntity_TotalRegisteredIsOne()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
|
Assert.Equal(1, reg.TotalRegistered);
|
|
}
|
|
|
|
[Fact]
|
|
public void Register_SameEntityTwice_NoDuplicate()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
|
reg.Register(1u, 0x01000001u, new Vector3(13f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId); // re-register (position update)
|
|
Assert.Equal(1, reg.TotalRegistered);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// GetObjectsInCell
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void GetObjectsInCell_EntityInCenter_ReturnedInExpectedCell()
|
|
{
|
|
// Entity at local (12, 12) = cell (0,0) = cellId prefix | 1.
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(42u, 0x01000002u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
|
|
|
uint cellId = LbId | 1u; // cx=0, cy=0 → 0*8+0+1 = 1
|
|
var objs = reg.GetObjectsInCell(cellId);
|
|
Assert.Single(objs);
|
|
Assert.Equal(42u, objs[0].EntityId);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetObjectsInCell_EntitySpanning2Cells_RegisteredInBoth()
|
|
{
|
|
// Entity at local (24, 12) with radius=2 spans cells cx=0 and cx=1 in X.
|
|
// Cell 0,0 = prefix|1; cell 1,0 = prefix|(1*8+0+1)=prefix|9.
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(7u, 0x01000003u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
|
|
|
|
uint cell00 = LbId | 1u; // cx=0, cy=0
|
|
uint cell10 = LbId | 9u; // cx=1, cy=0
|
|
|
|
Assert.Contains(reg.GetObjectsInCell(cell00), e => e.EntityId == 7u);
|
|
Assert.Contains(reg.GetObjectsInCell(cell10), e => e.EntityId == 7u);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Deregister
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Deregister_RemovesFromAllCells()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(5u, 0x01000004u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
|
|
reg.Deregister(5u);
|
|
|
|
Assert.Equal(0, reg.TotalRegistered);
|
|
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
|
|
Assert.Empty(reg.GetObjectsInCell(LbId | 9u));
|
|
}
|
|
|
|
[Fact]
|
|
public void Deregister_NonexistentEntity_NoThrow()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
// Should not throw.
|
|
reg.Deregister(999u);
|
|
}
|
|
|
|
[Fact]
|
|
public void Suspend_WithdrawsCellsAndUpdatePositionRestoresSameRegistration()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
const uint entityId = 23u;
|
|
reg.Register(
|
|
entityId,
|
|
0x01000004u,
|
|
new Vector3(12f, 12f, 50f),
|
|
Quaternion.Identity,
|
|
1f,
|
|
OffX,
|
|
OffY,
|
|
LbId);
|
|
|
|
Assert.True(reg.Suspend(entityId));
|
|
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
|
|
Assert.Equal(0, reg.TotalRegistered);
|
|
|
|
reg.UpdatePosition(
|
|
entityId,
|
|
new Vector3(36f, 12f, 50f),
|
|
Quaternion.Identity,
|
|
OffX,
|
|
OffY,
|
|
LbId,
|
|
seedCellId: LbId | 9u);
|
|
|
|
ShadowEntry restored = Assert.Single(reg.GetObjectsInCell(LbId | 9u));
|
|
Assert.Equal(entityId, restored.EntityId);
|
|
Assert.Equal(new Vector3(36f, 12f, 50f), restored.Position);
|
|
Assert.Equal(1, reg.TotalRegistered);
|
|
}
|
|
|
|
[Fact]
|
|
public void Suspend_StateChangeUpdatesRetainedRegistrationBeforeRestore()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
const uint entityId = 24u;
|
|
reg.Register(
|
|
entityId,
|
|
0x01000004u,
|
|
new Vector3(12f, 12f, 50f),
|
|
Quaternion.Identity,
|
|
1f,
|
|
OffX,
|
|
OffY,
|
|
LbId,
|
|
state: 0x40u,
|
|
seedCellId: LbId | 1u,
|
|
isStatic: false);
|
|
Assert.True(reg.Suspend(entityId));
|
|
|
|
reg.UpdatePhysicsState(entityId, 0x44u);
|
|
reg.UpdatePosition(
|
|
entityId,
|
|
new Vector3(12f, 12f, 50f),
|
|
Quaternion.Identity,
|
|
OffX,
|
|
OffY,
|
|
LbId,
|
|
seedCellId: LbId | 1u);
|
|
|
|
ShadowEntry restored = Assert.Single(reg.GetObjectsInCell(LbId | 1u));
|
|
Assert.Equal(0x44u, restored.State);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// RemoveLandblock
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void RemoveLandblock_ClearsAllEntitiesForThatBlock()
|
|
{
|
|
const uint otherLb = 0xAAAA0000u;
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
|
// Entity 2 lives in otherLb whose world origin is at X=192. Place it at
|
|
// world X=204 so localX = 204-192 = 12, which maps to cell (0,0) of otherLb.
|
|
reg.Register(2u, 0x01000002u, new Vector3(204f, 12f, 50f), Quaternion.Identity, 1f, 192f, 0f, otherLb);
|
|
|
|
reg.RemoveLandblock(LbId);
|
|
|
|
Assert.Equal(1, reg.TotalRegistered); // entity 2 (otherLb) survives
|
|
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveLandblock_DynamicRegistrationRefloodsAfterStreamingReload()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
const uint entityId = 31u;
|
|
reg.Register(
|
|
entityId,
|
|
0x01000005u,
|
|
new Vector3(12f, 12f, 50f),
|
|
Quaternion.Identity,
|
|
1f,
|
|
OffX,
|
|
OffY,
|
|
LbId,
|
|
seedCellId: LbId | 1u,
|
|
isStatic: false);
|
|
|
|
reg.RemoveLandblock(LbId);
|
|
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
|
|
|
|
reg.RefloodLandblock(LbId);
|
|
|
|
Assert.Contains(
|
|
reg.GetObjectsInCell(LbId | 1u),
|
|
entry => entry.EntityId == entityId);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Per-cell query surface (BR-7 / A6.P4 2026-06-11): GetObjectsInCell IS
|
|
// the query — retail CObjCell::find_obj_collisions iterates only the
|
|
// asked cell's shadow_object_list (Ghidra 0x0052b750). The former
|
|
// radial GetNearbyObjects sweep is deleted; cell membership is the
|
|
// broad phase.
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void PerCellQuery_EntityCell_ReturnsIt()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(10u, 0x01000005u, new Vector3(30f, 30f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
|
|
|
// local (30,30) → landcell (1,1) → 1*8+1+1 = 10.
|
|
var results = reg.GetObjectsInCell(LbId | 10u);
|
|
Assert.Single(results);
|
|
Assert.Equal(10u, results[0].EntityId);
|
|
}
|
|
|
|
[Fact]
|
|
public void PerCellQuery_FarCell_ReturnsEmpty()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
// Entity at local (12, 12) — cell (0,0).
|
|
reg.Register(11u, 0x01000006u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
|
|
|
// Cell (7,7) — far away. No spatial radius can reach across cells.
|
|
Assert.Empty(reg.GetObjectsInCell(LbId | 64u));
|
|
}
|
|
|
|
[Fact]
|
|
public void PerCellQuery_EntityInMultipleCells_OncePerCellList()
|
|
{
|
|
// Entity spans cells (0,0) and (1,0) (local X≈24, radius=2) via the
|
|
// flood's boundary-neighbor adds. Each overlapped cell's list holds
|
|
// the entry exactly once — retail tests an object once per iterated
|
|
// cell (no cross-cell dedup exists in find_obj_collisions).
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(20u, 0x01000007u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
|
|
|
|
Assert.Single(reg.GetObjectsInCell(LbId | 1u), e => e.EntityId == 20u);
|
|
Assert.Single(reg.GetObjectsInCell(LbId | 9u), e => e.EntityId == 20u);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Cell ID formula
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Theory]
|
|
[InlineData(0f, 0f, 1u)] // cell (0,0) → index 0*8+0+1 = 1
|
|
[InlineData(48f, 0f, 17u)] // cell (2,0) → 2*8+0+1 = 17
|
|
[InlineData(0f, 48f, 3u)] // cell (0,2) → 0*8+2+1 = 3
|
|
[InlineData(168f, 168f, 64u)] // cell (7,7) → 7*8+7+1 = 64
|
|
public void GetObjectsInCell_CellIdFormula_Correct(float lx, float ly, uint expectedLow)
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
// Small radius so entity sits in exactly one cell.
|
|
reg.Register(99u, 0x01000008u, new Vector3(lx + 0.5f, ly + 0.5f, 50f), Quaternion.Identity, 0.1f, OffX, OffY, LbId);
|
|
|
|
uint cellId = LbId | expectedLow;
|
|
var objs = reg.GetObjectsInCell(cellId);
|
|
Assert.Contains(objs, e => e.EntityId == 99u);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// UpdatePosition — Commit A of 2026-04-29 live-entity collision port
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void UpdatePosition_MovedEntity_NewCellOccupied()
|
|
{
|
|
// Entity starts at local (12, 12) — cell (0,0).
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(42u, 0x01000010u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 0.5f, OffX, OffY, LbId);
|
|
|
|
// Move to local (60, 12) — cell (2, 0). Same landblock.
|
|
reg.UpdatePosition(42u, new Vector3(60f, 12f, 50f), Quaternion.Identity, OffX, OffY, LbId);
|
|
|
|
// Old cell empty, new cell holds the entity.
|
|
Assert.Empty(reg.GetObjectsInCell(LbId | 1u)); // cell (0,0)
|
|
var newCell = reg.GetObjectsInCell(LbId | 17u); // cell (2,0) → 2*8+0+1
|
|
Assert.Single(newCell);
|
|
Assert.Equal(42u, newCell[0].EntityId);
|
|
Assert.Equal(1, reg.TotalRegistered); // not duplicated
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdatePosition_PreservesFlags()
|
|
{
|
|
// Register with PK flags + PhysicsState; UpdatePosition must keep them.
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(50u, 0x01000011u, new Vector3(12f, 12f, 50f), Quaternion.Identity,
|
|
0.5f, OffX, OffY, LbId,
|
|
state: 0x4u, // ETHEREAL_PS
|
|
flags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPK);
|
|
|
|
reg.UpdatePosition(50u, new Vector3(60f, 12f, 50f), Quaternion.Identity, OffX, OffY, LbId);
|
|
|
|
var newCell = reg.GetObjectsInCell(LbId | 17u);
|
|
Assert.Single(newCell);
|
|
Assert.Equal(0x4u, newCell[0].State);
|
|
Assert.Equal(EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPK, newCell[0].Flags);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdatePosition_UnregisteredEntity_NoOp()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
// Should not throw, should not register a new entity.
|
|
reg.UpdatePosition(99u, new Vector3(12f, 12f, 50f), Quaternion.Identity, OffX, OffY, LbId);
|
|
Assert.Equal(0, reg.TotalRegistered);
|
|
}
|
|
|
|
[Fact]
|
|
public void Register_WithStateAndFlags_StoredOnEntry()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(60u, 0x01000012u, new Vector3(12f, 12f, 50f), Quaternion.Identity,
|
|
0.5f, OffX, OffY, LbId,
|
|
state: 0x10u, // IGNORE_COLLISIONS_PS
|
|
flags: EntityCollisionFlags.IsImpenetrable);
|
|
|
|
var entry = reg.GetObjectsInCell(LbId | 1u)[0];
|
|
Assert.Equal(0x10u, entry.State);
|
|
Assert.Equal(EntityCollisionFlags.IsImpenetrable, entry.Flags);
|
|
}
|
|
|
|
[Fact]
|
|
public void Register_DefaultStateAndFlags_AreZeroAndNone()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(70u, 0x01000013u, new Vector3(12f, 12f, 50f), Quaternion.Identity,
|
|
0.5f, OffX, OffY, LbId);
|
|
|
|
var entry = reg.GetObjectsInCell(LbId | 1u)[0];
|
|
Assert.Equal(0u, entry.State);
|
|
Assert.Equal(EntityCollisionFlags.None, entry.Flags);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// UpdatePhysicsState — L.2g slice 1 (doors flip ETHEREAL post-spawn)
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void UpdatePhysicsState_FlipsEthereal_NextLookupSeesNewBits()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
const uint doorId = 0x000F4244u;
|
|
reg.Register(doorId, 0x020019FFu, new Vector3(12f, 12f, 50f),
|
|
Quaternion.Identity, 1f, OffX, OffY, LbId,
|
|
state: 0u, flags: EntityCollisionFlags.None);
|
|
|
|
var before = reg.AllEntriesForDebug().Single(e => e.EntityId == doorId);
|
|
Assert.Equal(0u, before.State);
|
|
|
|
reg.UpdatePhysicsState(doorId, 0x00000004u);
|
|
|
|
var after = reg.AllEntriesForDebug().Single(e => e.EntityId == doorId);
|
|
Assert.Equal(0x00000004u, after.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdatePhysicsState_UnregisteredEntity_IsNoOp()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.UpdatePhysicsState(0xDEADBEEFu, 0x00000004u);
|
|
Assert.Equal(0, reg.TotalRegistered);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdatePhysicsState_EntitySpanningMultipleCells_AllCellsUpdated()
|
|
{
|
|
var reg = new ShadowObjectRegistry();
|
|
reg.Register(99u, 0x01000099u, new Vector3(24f, 12f, 50f),
|
|
Quaternion.Identity, 2f, OffX, OffY, LbId,
|
|
state: 0u);
|
|
|
|
reg.UpdatePhysicsState(99u, 0x00000004u);
|
|
|
|
uint cellA = LbId | 1u;
|
|
uint cellB = LbId | (1u*8 + 0 + 1);
|
|
var inA = reg.GetObjectsInCell(cellA).Single(e => e.EntityId == 99u);
|
|
var inB = reg.GetObjectsInCell(cellB).Single(e => e.EntityId == 99u);
|
|
Assert.Equal(0x00000004u, inA.State);
|
|
Assert.Equal(0x00000004u, inB.State);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// BR-7 / A6.P4 (2026-06-11) — registration-side architecture pins.
|
|
// The former query-side compensations (the b3ce505 indoor gate, the
|
|
// portalReachableCells expansion, the isViewer exemption) are deleted;
|
|
// these pin the registration shapes that replaced them.
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Register_OutdoorSeed_DoorVisibleInItsOutdoorCell_NotInUnrelatedIndoorCell()
|
|
{
|
|
// Issue #99 architecture: a door registered at its outdoor cell is
|
|
// found by iterating THAT cell's list — which the Transition reaches
|
|
// from the indoor side when its sphere straddles the exit portal
|
|
// (the cell array spans both cells at the threshold; retail
|
|
// "covered twice" note, wf1-interior-collision.md). Without a
|
|
// building bridge in the flood cache, the door floods outdoor-only.
|
|
var reg = new ShadowObjectRegistry();
|
|
|
|
const uint doorEntityId = 0x000F4244u;
|
|
reg.Register(doorEntityId, 0x020019FFu, new Vector3(12f, 12f, 50f),
|
|
Quaternion.Identity, 1f, OffX, OffY, LbId,
|
|
ShadowCollisionType.Cylinder, cylHeight: 2.5f,
|
|
isStatic: false);
|
|
|
|
uint doorOutdoorCellId = LbId | 1u; // outdoor cell where door sits
|
|
uint vestibuleCellId = LbId | 0x0145u; // indoor cell on the other side
|
|
|
|
Assert.Contains(reg.GetObjectsInCell(doorOutdoorCellId), e => e.EntityId == doorEntityId);
|
|
Assert.DoesNotContain(reg.GetObjectsInCell(vestibuleCellId), e => e.EntityId == doorEntityId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Register_OutdoorFootprint_NeverLandsInInteriorCells()
|
|
{
|
|
// The #98 shape, now closed at REGISTRATION: an outdoor-positioned
|
|
// footprint (the old cottage GfxObj entry) floods into outdoor
|
|
// landcells only — without a building-bridge admission its spheres
|
|
// can never reach an interior cell's list, so a fully-interior
|
|
// cellar query structurally cannot see it. (In production the
|
|
// cottage SHELL additionally left the registry entirely — the
|
|
// per-LandCell building channel owns it.)
|
|
var reg = new ShadowObjectRegistry();
|
|
|
|
const uint cottageEntityId = 0xA9B47900u;
|
|
reg.Register(cottageEntityId, 0x01000A2Bu, new Vector3(12f, 12f, 90f),
|
|
Quaternion.Identity, 5.5f, OffX, OffY, LbId);
|
|
|
|
uint cellarCellId = LbId | 0x0146u;
|
|
Assert.Empty(reg.GetObjectsInCell(cellarCellId));
|
|
|
|
// And every cell it DID land in is outdoor.
|
|
foreach (var entry in reg.AllEntriesForDebug())
|
|
Assert.Equal(cottageEntityId, entry.EntityId);
|
|
}
|
|
|
|
[Fact]
|
|
public void RefloodLandblock_RerunsFloodAfterCellsHydrate()
|
|
{
|
|
// The streaming race (spec §7 Q3 / retail init_objects →
|
|
// recalc_cross_cells): an entity registered while its indoor seed
|
|
// cell was unhydrated stays pinned to {seed}; once the cell (with a
|
|
// portal neighbor) hydrates, RefloodLandblock re-runs the flood and
|
|
// the neighbor appears in the set.
|
|
var reg = new ShadowObjectRegistry();
|
|
var cache = new PhysicsDataCache();
|
|
reg.DataCache = cache;
|
|
|
|
const uint seedCell = 0xA9B40100u;
|
|
const uint neighborCell = 0xA9B40101u;
|
|
|
|
reg.Register(77u, 0x01000009u, new Vector3(2.0f, 0f, 2.5f),
|
|
Quaternion.Identity, 0.5f, OffX, OffY, LbId,
|
|
seedCellId: seedCell, isStatic: false);
|
|
|
|
// Unhydrated seed → registered under the claimed cell only.
|
|
Assert.Contains(reg.GetObjectsInCell(seedCell), e => e.EntityId == 77u);
|
|
Assert.Empty(reg.GetObjectsInCell(neighborCell));
|
|
|
|
// Hydrate the seed (portal to the neighbor at x=2.5) + the neighbor
|
|
// (leaf BSP admits the straddling sphere), then re-flood.
|
|
cache.RegisterCellStructForTest(seedCell,
|
|
BuildShadowCellSetTests_MakeCellWithPortalAtRightWall(Matrix4x4.Identity, 0x0101));
|
|
cache.RegisterCellStructForTest(neighborCell,
|
|
BuildShadowCellSetTests_MakeLeafCell(Matrix4x4.CreateTranslation(5f, 0f, 0f)));
|
|
|
|
reg.RefloodLandblock(LbId);
|
|
|
|
Assert.Contains(reg.GetObjectsInCell(seedCell), e => e.EntityId == 77u);
|
|
Assert.Contains(reg.GetObjectsInCell(neighborCell), e => e.EntityId == 77u);
|
|
}
|
|
|
|
// Local copies of the BuildShadowCellSetTests fixture helpers (kept
|
|
// private per test class by repo convention).
|
|
private static CellPhysics BuildShadowCellSetTests_MakeCellWithPortalAtRightWall(
|
|
Matrix4x4 worldTransform, ushort otherCellId)
|
|
{
|
|
var portalPoly = new ResolvedPolygon
|
|
{
|
|
Vertices = new[]
|
|
{
|
|
new Vector3(2.5f, -2.5f, 0f),
|
|
new Vector3(2.5f, 2.5f, 0f),
|
|
new Vector3(2.5f, 2.5f, 5f),
|
|
new Vector3(2.5f, -2.5f, 5f),
|
|
},
|
|
Plane = new System.Numerics.Plane(new Vector3(1, 0, 0), -2.5f),
|
|
NumPoints = 4,
|
|
SidesType = DatReaderWriter.Enums.CullMode.None,
|
|
};
|
|
|
|
Matrix4x4.Invert(worldTransform, out var inv);
|
|
return new CellPhysics
|
|
{
|
|
WorldTransform = worldTransform,
|
|
InverseWorldTransform = inv,
|
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
|
PortalPolygons = new Dictionary<ushort, ResolvedPolygon> { [10] = portalPoly },
|
|
Portals = new[]
|
|
{
|
|
new PortalInfo(otherCellId: otherCellId, polygonId: 10, flags: 0),
|
|
},
|
|
CellBSP = new DatReaderWriter.Types.CellBSPTree
|
|
{
|
|
Root = new DatReaderWriter.Types.CellBSPNode
|
|
{
|
|
Type = DatReaderWriter.Enums.BSPNodeType.Leaf,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
private static CellPhysics BuildShadowCellSetTests_MakeLeafCell(Matrix4x4 worldTransform)
|
|
{
|
|
Matrix4x4.Invert(worldTransform, out var inv);
|
|
return new CellPhysics
|
|
{
|
|
WorldTransform = worldTransform,
|
|
InverseWorldTransform = inv,
|
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
|
CellBSP = new DatReaderWriter.Types.CellBSPTree
|
|
{
|
|
Root = new DatReaderWriter.Types.CellBSPNode
|
|
{
|
|
Type = DatReaderWriter.Enums.BSPNodeType.Leaf,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
}
|