feat(render): Campaign OVERHAUL S2 chunk 5 + closeout — registry is the only render membership owner
Chunk 5 (consumer cutover): WalkProductionWorldData's per-cell views are borrowed from ShadowObjectRegistry.GetRetailPartEntriesInCell and resolved through RenderSceneQuery.TryGetByLocalEntityId; every render-side sweep, bucket, parent-cell and root-position fallback is deleted (AD-116 for the one-frame registry→scene window, counted in UnregisteredRenderMembershipCount). A live entity with visual parts but no collision geometry registers render-only (LiveEntityCollisionBuilder computes the part array before the empty-shapes gate). Closeout fixes found while landing it: - RefloodOwnerForLandblock forwards the retained part array — a reflood is retail's recalc_cross_cells over the SAME CPartArray; without it every owner touched by a landblock replacement commit lost its render membership. - Non-colliding DAT statics register render-only from BOTH publishers (LandblockPhysicsPublisher.PublishStaticEntity, LandblockPhysicsContentBuilder.RegisterRenderOnlyStatic). The G2 self-gate pixel diff caught them vanishing (Facility Hub wall panels): retail floods every object regardless of collision (CEnvCell::init_static_objects 0x0052c350, add_shadows_to_cells 0x00514ae0). - S2 dual review fix batch (arch + retail lens, lead-verified): Suspend clears the retail product (remove_shadows_from_cells 0x00511230 is one transaction); AttachChild/DetachChild advance the mutation revision so a prepared SetPosition cannot clobber a child's rows; an attached child never floods on its own re-registration; RemoveLandblock and the non-rooted RetireOwnerFromLandblock prune retail rows (render-only statics end with their landblock); a render-only owner's no-cell-array commit republishes at its destination cell (AD-117); an empty non-null part array is treated as null; per-move closures/LINQ replaced by index loops; EnvCell shells stay out of the scene's LocalEntityId index (payload-less records); the index predicate compares the id; the dead per-cell scene indices are deleted. Register: AD-116 (chunk 5), AD-117 (four residual Contract A/B readings). Evidence: s2-membership-ownership-map.md §8 (chunk 5) and §9 (closeout). Gates (Release): Core 4,984/4,984; Content 214/214; Runtime 1,884/1,884; App hermetic lane 6,760/6,760; App InstalledDat lane 217 pass / 1 skip / 2 pre-existing #383 layout-fixture failures; App Windows lane 1/1. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
f30039d3d9
commit
c94a1a407e
20 changed files with 2141 additions and 1026 deletions
|
|
@ -867,4 +867,388 @@ public class ShadowObjectRegistryRetailCellArrayTests
|
|||
Assert.True(reg.TryGetRetailCellArray(childId, out var childCells));
|
||||
Assert.Equal(rootCellsAfter, childCells);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Campaign OVERHAUL S2 chunk 5 (item B): a live entity whose collision
|
||||
// dispatch produced NO shapes but whose Setup still has a visual part
|
||||
// array (the retail short-lived spell/visual effect-object case) still
|
||||
// registers for render — Contract A's cylsphere-vs-bbox TEST reads the
|
||||
// COLLISION shapes (none here, so it is always false) and the flood
|
||||
// unconditionally falls through to the bbox route over the WHOLE part
|
||||
// array. No ShadowEntry collision row is ever published; only the
|
||||
// retail CELLARRAY and per-cell RetailPartEntry rows are.
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void RegisterMultiPart_EmptyShapesWithPartArray_RegistersRenderOnlyNoCollisionRows()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x50u;
|
||||
IReadOnlyList<ShadowShape> partArray = new[] { Bsp(0x0100_0060u, radius: 2f) };
|
||||
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: partArray);
|
||||
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var cells));
|
||||
Assert.Equal(new[] { CellA }, cells);
|
||||
Assert.Equal(RetailCellArrayRoute.BoundingBox, reg.GetRetailCellArrayRoute(entityId));
|
||||
|
||||
var entries = reg.GetRetailPartEntriesInCell(CellA)
|
||||
.Where(e => e.EntityId == entityId).ToList();
|
||||
Assert.Single(entries);
|
||||
Assert.Equal(0x0100_0060u, entries[0].GfxObjId);
|
||||
|
||||
// No collision row anywhere: shadow_object_list gets nothing for an
|
||||
// object with no part to contribute to it (Contract B).
|
||||
Assert.Empty(reg.GetOwnerCells(entityId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMultiPart_EmptyShapesAndNoPartArray_StillDeregisters()
|
||||
{
|
||||
// Byte-for-byte prior behavior: a caller that supplies neither
|
||||
// collision shapes nor a part array is a genuine deregistration, not
|
||||
// a render-only registration.
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x51u;
|
||||
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x0100_0061u) };
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, parts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: parts);
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out _));
|
||||
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true);
|
||||
|
||||
Assert.False(reg.TryGetRetailCellArray(entityId, out var cells));
|
||||
Assert.Empty(cells);
|
||||
Assert.Empty(reg.GetOwnerCells(entityId));
|
||||
Assert.Empty(reg.GetRetailPartEntriesInCell(CellA));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMultiPart_RenderOnly_KeepWhenEmptyPreservesThePriorRegistration()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x52u;
|
||||
IReadOnlyList<ShadowShape> partArray = new[] { Bsp(0x0100_0062u, radius: 2f) };
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: partArray);
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var before));
|
||||
Assert.Equal(new[] { CellA }, before);
|
||||
|
||||
// landblockId 0 (no seedCellId either) => DeriveOutdoorSeed returns 0
|
||||
// => the retail keep-when-empty gate (pc:283540): neither product is
|
||||
// touched.
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, landblockId: 0u,
|
||||
isStatic: true, partArray: partArray);
|
||||
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var after));
|
||||
Assert.Equal(before, after);
|
||||
Assert.NotEmpty(reg.GetRetailPartEntriesInCell(CellA));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMultiPart_RenderOnly_DeregisterClearsEveryProduct()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x53u;
|
||||
IReadOnlyList<ShadowShape> partArray = new[] { Bsp(0x0100_0063u, radius: 2f) };
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: partArray);
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out _));
|
||||
|
||||
reg.Deregister(entityId);
|
||||
|
||||
Assert.False(reg.TryGetRetailCellArray(entityId, out var cells));
|
||||
Assert.Empty(cells);
|
||||
Assert.Empty(reg.GetRetailPartEntriesInCell(CellA));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePosition_RenderOnly_RecomputesTheRetailCellArrayAtTheNewPosition()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x54u;
|
||||
IReadOnlyList<ShadowShape> partArray = new[] { Bsp(0x0100_0064u, radius: 2f) };
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: partArray);
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var before));
|
||||
Assert.Equal(new[] { CellA }, before);
|
||||
|
||||
var moved = new Vector3(42f, 12f, 50f); // 30 m away in X -> CellB
|
||||
reg.UpdatePosition(entityId, moved, Quaternion.Identity, OffX, OffY, LbId);
|
||||
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var after));
|
||||
Assert.Equal(new[] { CellB }, after);
|
||||
Assert.DoesNotContain(
|
||||
reg.GetRetailPartEntriesInCell(CellA),
|
||||
e => e.EntityId == entityId);
|
||||
var movedEntries = reg.GetRetailPartEntriesInCell(CellB)
|
||||
.Where(e => e.EntityId == entityId).ToList();
|
||||
Assert.Single(movedEntries);
|
||||
Assert.Equal(0x0100_0064u, movedEntries[0].GfxObjId);
|
||||
// Still no collision row after the move.
|
||||
Assert.Empty(reg.GetOwnerCells(entityId));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Chunk 5 closeout: a streaming reflood (RefloodOwnerForLandblock, the
|
||||
// path landblock replacement commits and the Content builder's
|
||||
// post-publication reflood both take) is retail's recalc_cross_cells
|
||||
// over the SAME CPartArray, so the retail render product survives it.
|
||||
// Before this pin the reflood re-registered from the collision shapes
|
||||
// only and every reflooded owner vanished from the walk.
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void RefloodLandblock_KeepsTheRetailProductOfACollisionOwnerWithAPartArray()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x60u;
|
||||
IReadOnlyList<ShadowShape> collision = new[] { Bsp(0x0100_0070u, radius: 2f) };
|
||||
IReadOnlyList<ShadowShape> parts = new[]
|
||||
{
|
||||
Bsp(0x0100_0070u, radius: 2f),
|
||||
Bsp(0x0100_0071u, localPosition: new Vector3(0.5f, 0f, 0f), radius: 1f),
|
||||
};
|
||||
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, collision,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: parts);
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var before));
|
||||
var beforeEntries = reg.GetRetailPartEntriesInCell(CellA)
|
||||
.Where(e => e.EntityId == entityId).Select(e => e.GfxObjId).ToList();
|
||||
Assert.Equal(new[] { 0x0100_0070u, 0x0100_0071u }, beforeEntries);
|
||||
|
||||
reg.RefloodLandblock(LbId);
|
||||
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var after));
|
||||
Assert.Equal(before, after);
|
||||
Assert.Equal(RetailCellArrayRoute.BoundingBox, reg.GetRetailCellArrayRoute(entityId));
|
||||
var afterEntries = reg.GetRetailPartEntriesInCell(CellA)
|
||||
.Where(e => e.EntityId == entityId).Select(e => e.GfxObjId).ToList();
|
||||
Assert.Equal(beforeEntries, afterEntries);
|
||||
// The collision product is untouched by the reflood as well.
|
||||
Assert.Equal(new[] { CellA }, reg.GetOwnerCells(entityId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefloodLandblock_KeepsARenderOnlyOwner()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x61u;
|
||||
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x0100_0072u, radius: 2f) };
|
||||
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: parts);
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out _));
|
||||
|
||||
reg.RefloodLandblock(LbId);
|
||||
|
||||
// Still registered, still render-only, still in its cell: the empty
|
||||
// collision shape list plus the retained part array is the
|
||||
// render-only registration, not the "nothing supplied" deregistration.
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var cells));
|
||||
Assert.Equal(new[] { CellA }, cells);
|
||||
var entries = reg.GetRetailPartEntriesInCell(CellA)
|
||||
.Where(e => e.EntityId == entityId).ToList();
|
||||
Assert.Single(entries);
|
||||
Assert.Equal(0x0100_0072u, entries[0].GfxObjId);
|
||||
Assert.Empty(reg.GetOwnerCells(entityId));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// S2 review fix round (2026-09-03): lifetime symmetry of the retail
|
||||
// render product. remove_shadows_from_cells (0x00511230) removes the
|
||||
// CShadowObj row AND the AddPartsShadow rows in one transaction, and
|
||||
// recurses through children — every acdream path that drops one product
|
||||
// drops the other.
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
private static int CountRows(ShadowObjectRegistry reg, uint cellId, uint entityId)
|
||||
=> reg.GetRetailPartEntriesInCell(cellId).Count(e => e.EntityId == entityId);
|
||||
|
||||
[Fact]
|
||||
public void Suspend_ClearsTheRetailProduct_AndTheUnsuspendingMoveRepublishesIt()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x70u;
|
||||
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x0100_0080u, radius: 2f) };
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, parts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: false, partArray: parts);
|
||||
Assert.Equal(1, CountRows(reg, CellA, entityId));
|
||||
|
||||
Assert.True(reg.Suspend(entityId));
|
||||
|
||||
Assert.False(reg.TryGetRetailCellArray(entityId, out _));
|
||||
Assert.Equal(0, CountRows(reg, CellA, entityId));
|
||||
Assert.Empty(reg.GetOwnerCells(entityId));
|
||||
// The route and part array are retained for the republish.
|
||||
Assert.Equal(RetailCellArrayRoute.BoundingBox, reg.GetRetailCellArrayRoute(entityId));
|
||||
|
||||
// A .None commit republishes at the RETAINED (suspended) cells for
|
||||
// both products — the un-suspend path.
|
||||
reg.CommitSetPosition(
|
||||
entityId, Pos, Quaternion.Identity,
|
||||
seedCellId: CellA, worldOffsetX: OffX, worldOffsetY: OffY,
|
||||
action: PhysicsShadowCommitAction.None,
|
||||
crossCellIds: ImmutableArray<uint>.Empty);
|
||||
|
||||
Assert.Equal(new[] { CellA }, reg.GetOwnerCells(entityId));
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var cells));
|
||||
Assert.Equal(new[] { CellA }, cells);
|
||||
Assert.Equal(1, CountRows(reg, CellA, entityId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttachChild_AndDetachChild_AdvanceTheMutationRevision()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint rootId = 0x71u;
|
||||
const uint childId = 0x72u;
|
||||
IReadOnlyList<ShadowShape> rootParts = new[] { Bsp(0x0100_0081u, radius: 2f) };
|
||||
reg.RegisterMultiPart(rootId, Pos, Quaternion.Identity, rootParts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: false, partArray: rootParts);
|
||||
|
||||
ulong before = reg.MutationRevision;
|
||||
Assert.True(reg.AttachChild(childId, rootId, new[] { Bsp(0x0100_0082u) }));
|
||||
Assert.True(reg.MutationRevision > before);
|
||||
|
||||
before = reg.MutationRevision;
|
||||
Assert.True(reg.DetachChild(childId));
|
||||
Assert.True(reg.MutationRevision > before);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMultiPart_OfAnAttachedChild_KeepsInheritingTheRootsCells()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint rootId = 0x73u;
|
||||
const uint childId = 0x74u;
|
||||
IReadOnlyList<ShadowShape> rootParts = new[] { Bsp(0x0100_0083u, radius: 2f) };
|
||||
reg.RegisterMultiPart(rootId, Pos, Quaternion.Identity, rootParts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: false, partArray: rootParts);
|
||||
Assert.True(reg.AttachChild(childId, rootId, new[] { Bsp(0x0100_0084u) }));
|
||||
Assert.Equal(1, CountRows(reg, CellA, childId));
|
||||
|
||||
// An appearance update re-registers the child at its own (different)
|
||||
// position with a NEW part array: retail never floods a child, so it
|
||||
// keeps the root's CELLARRAY and only its parts change.
|
||||
IReadOnlyList<ShadowShape> newParts = new[] { Bsp(0x0100_0085u, radius: 1f) };
|
||||
reg.RegisterMultiPart(childId, new Vector3(42f, 12f, 50f), Quaternion.Identity, newParts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellB, isStatic: false, partArray: newParts);
|
||||
|
||||
Assert.True(reg.TryGetRetailCellArray(childId, out var childCells));
|
||||
Assert.Equal(new[] { CellA }, childCells);
|
||||
Assert.Equal(0, CountRows(reg, CellB, childId));
|
||||
var rows = reg.GetRetailPartEntriesInCell(CellA).Where(e => e.EntityId == childId).ToList();
|
||||
Assert.Single(rows);
|
||||
Assert.Equal(0x0100_0085u, rows[0].GfxObjId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_ClearsTheRetailProduct_IncludingRenderOnlyStatics()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint collidingId = 0x75u;
|
||||
const uint renderOnlyId = 0x76u;
|
||||
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x0100_0086u, radius: 2f) };
|
||||
reg.RegisterMultiPart(collidingId, Pos, Quaternion.Identity, parts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: parts);
|
||||
reg.RegisterMultiPart(renderOnlyId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: true, partArray: parts);
|
||||
Assert.Equal(1, CountRows(reg, CellA, renderOnlyId));
|
||||
|
||||
reg.RemoveLandblock(LbId);
|
||||
|
||||
Assert.False(reg.TryGetRetailCellArray(collidingId, out _));
|
||||
Assert.False(reg.TryGetRetailCellArray(renderOnlyId, out _));
|
||||
Assert.Empty(reg.GetRetailPartEntriesInCell(CellA));
|
||||
// Both statics ended with their landblock — nothing left to suspend.
|
||||
Assert.False(reg.Suspend(collidingId));
|
||||
Assert.False(reg.Suspend(renderOnlyId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetireOwnerFromLandblock_PrunesTheRetailRowsOfANonRootedOwner()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x77u;
|
||||
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x0100_0087u, radius: 2f) };
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, parts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: false, partArray: parts);
|
||||
Assert.Equal(1, CountRows(reg, CellA, entityId));
|
||||
|
||||
reg.RetireOwnerFromLandblock(entityId, LbId);
|
||||
|
||||
Assert.Equal(0, CountRows(reg, CellA, entityId));
|
||||
Assert.False(reg.TryGetRetailCellArray(entityId, out _));
|
||||
Assert.Empty(reg.GetOwnerCells(entityId));
|
||||
// A live owner survives the prefix retirement (it is still logically
|
||||
// alive for the reload reflood), so it can still be suspended.
|
||||
Assert.True(reg.Suspend(entityId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceMultiPartPayload_WithAnEmptyPartArray_KeepsThePriorRows()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x78u;
|
||||
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x0100_0088u, radius: 2f) };
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, parts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: false, partArray: parts);
|
||||
|
||||
reg.ReplaceMultiPartPayload(entityId, Pos, Quaternion.Identity, parts,
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: false, partArray: Array.Empty<ShadowShape>());
|
||||
|
||||
// Never a cell-array claim with no drawable rows behind it.
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var cells));
|
||||
Assert.Equal(new[] { CellA }, cells);
|
||||
Assert.Equal(1, CountRows(reg, CellA, entityId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommitSetPosition_None_MovesARenderOnlyOwnerToItsDestinationCell()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x79u;
|
||||
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x0100_0089u, radius: 1f) };
|
||||
reg.RegisterMultiPart(entityId, Pos, Quaternion.Identity, Array.Empty<ShadowShape>(),
|
||||
state: 0u, flags: EntityCollisionFlags.None, OffX, OffY, LbId,
|
||||
seedCellId: CellA, isStatic: false, partArray: parts);
|
||||
Assert.Equal(1, CountRows(reg, CellA, entityId));
|
||||
|
||||
// A render-only owner has no collision cells to republish from, so
|
||||
// its "cell did not change" commit would otherwise freeze it at its
|
||||
// registration cells forever (retail review F3): it travels with its
|
||||
// destination cell instead (AD-117).
|
||||
reg.CommitSetPosition(
|
||||
entityId, new Vector3(42f, 12f, 50f), Quaternion.Identity,
|
||||
seedCellId: CellB, worldOffsetX: OffX, worldOffsetY: OffY,
|
||||
action: PhysicsShadowCommitAction.None,
|
||||
crossCellIds: ImmutableArray<uint>.Empty);
|
||||
|
||||
Assert.True(reg.TryGetRetailCellArray(entityId, out var cells));
|
||||
Assert.Equal(new[] { CellB }, cells);
|
||||
Assert.Equal(0, CountRows(reg, CellA, entityId));
|
||||
Assert.Equal(1, CountRows(reg, CellB, entityId));
|
||||
Assert.Empty(reg.GetOwnerCells(entityId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue