acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkProductionWorldDataTests.cs
Erik 75ea269d35 feat(physics): S2 chunk 4 - movement publishes from the transition's cells; children inherit at registration
Movement: CommitSetPosition's RefreshPositionRows/ReplacePositionRows and
the staged apply publish the retail render product from the exact cell
list collision just used, the transition's cell_array retail feeds
add_shadows_to_cells in CPhysicsObj::SetPositionInternal @0x00515330
(pseudo-C 283526-283539); the separate move-path bbox recompute is deleted.
calc_cross_cells @0x00515230 stays the distinct full-recompute path
(PhysicsShadowCommitAction.Recalculate).

Children (Contract B recursion): ShadowObjectRegistry.AttachChild/DetachChild
give an attached object the root's current cells as part entries only,
republished whenever the root's array changes, detached at withdrawal and
cascaded from the root's Deregister; nested attachment resolves to the root
with a bounded, cycle-safe chain. EquippedChildRenderController attaches at
realization (FromSetupRenderParts over the child's Setup) and detaches at
its single removal funnel. WalkProductionWorldData's dynamic sweep reads
TryGetRetailCellArray directly; the 64-hop parent-chain walk and its
FindParentLocalId plumbing are deleted. CollisionWorldState.Clear now
also clears the retail products.

Gates (implementer's isolated worktree at identical content): Release
build 0/0; Core 4,970/4,970; App hermetic 6,761/6,761; targeted
walk/child/live-entity/placement/comparator 166/166; Runtime 1,884/1,884.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:37:34 +02:00

475 lines
17 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Physics;
namespace AcDream.App.Tests.Rendering.Walk;
public sealed class WalkProductionWorldDataTests
{
[Fact]
public void BuildingShellBucketCellId_PortalLessBuildingUsesLandscapePositionCell()
{
const uint positionCellId = 0xF4180011u;
RenderProjectionRecord record = Record(
id: 0xCF418060u,
position: new Vector3(53.57f, 13.874f, 160f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0xCF418060u,
SourceId = 0x01001FD3u,
EffectCellId = positionCellId,
BuildingShellAnchorCellId = 0,
},
EntityPayload = new RenderEntityPayload() with
{
IsBuildingShell = true,
},
};
var building = new WalkBuilding
{
PositionCellId = positionCellId,
Portals = [],
};
Assert.Equal(
positionCellId,
WalkProductionWorldData.BuildingShellBucketCellId(in record));
Assert.Equal(
positionCellId,
WalkProductionWorldData.BuildingShellBucketCellId(building));
}
[Fact]
public void BuildingShellBucketCellId_PortalBearingBuildingKeepsInteriorAnchor()
{
const uint anchorCellId = 0xF4180112u;
RenderProjectionRecord record = Record(
id: 0xCF41805Fu,
position: new Vector3(36f, 13.8349f, 160f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0xCF41805Fu,
SourceId = 0x01001FB7u,
EffectCellId = 0xF4180009u,
BuildingShellAnchorCellId = anchorCellId,
},
EntityPayload = new RenderEntityPayload() with
{
IsBuildingShell = true,
},
};
var building = new WalkBuilding
{
PositionCellId = 0xF4180009u,
Portals =
[
new WalkBldPortal { OtherCellId = anchorCellId },
],
};
Assert.Equal(
anchorCellId,
WalkProductionWorldData.BuildingShellBucketCellId(in record));
Assert.Equal(
anchorCellId,
WalkProductionWorldData.BuildingShellBucketCellId(building));
}
[Fact]
public void BucketOutdoorRecord_UsesEveryOutdoorPhysicsShadowCellAcrossLandblockEdge()
{
RenderProjectionRecord record = Record(
id: 0x1234u,
position: new Vector3(191f, 191f, 0f));
var buckets = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketOutdoorRecord(
in record,
[0xF07F0040u, 0xF0800001u, 0xF4180101u],
buckets,
renderCenterLbX: 0xF0,
renderCenterLbY: 0x7F);
Assert.Equal([0xF07F0040u, 0xF0800001u], buckets.Keys.Order());
Assert.All(buckets.Values, bucket => Assert.Equal(record, Assert.Single(bucket)));
}
[Fact]
public void BucketOutdoorRecord_UnregisteredEffectFallsBackToRootPositionCell()
{
RenderProjectionRecord record = Record(
id: 0x5678u,
position: new Vector3(193f, 25f, 0f));
var buckets = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketOutdoorRecord(
in record,
Array.Empty<uint>(),
buckets,
renderCenterLbX: 0xEF,
renderCenterLbY: 0x7F);
Assert.Equal([0xF07F0002u], buckets.Keys);
Assert.Equal(record, Assert.Single(buckets[0xF07F0002u]));
}
[Fact]
public void BucketIndoorRecord_InstallsCrossCellPartInBothInteriorCells()
{
RenderProjectionRecord record = Record(
id: 0x48A02035u,
position: new Vector3(55.25f, -46.47f, -3f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x48A02035u,
ParentCellId = 0x8A02015Fu,
},
};
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketIndoorRecord(
in record,
[0x8A02015Fu, 0x8A02015Eu],
indoor,
outdoor);
Assert.Equal([0x8A02015Eu, 0x8A02015Fu], indoor.Keys.Order());
Assert.All(indoor.Values, bucket => Assert.Equal(record, Assert.Single(bucket)));
Assert.Empty(outdoor);
}
[Fact]
public void BucketIndoorRecord_CanCrossAnExitIntoLandscapeCell()
{
RenderProjectionRecord record = Record(
id: 0x48A02035u,
position: Vector3.Zero) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x48A02035u,
ParentCellId = 0x8A02015Fu,
},
};
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketIndoorRecord(
in record,
[0x8A02015Fu, 0x8A020021u],
indoor,
outdoor);
Assert.Equal(record, Assert.Single(indoor[0x8A02015Fu]));
Assert.Equal(record, Assert.Single(outdoor[0x8A020021u]));
}
[Fact]
public void BucketDynamicRecord_InstallsMultipartPlayerInEveryCrossedInteriorCell()
{
RenderProjectionRecord record = Record(
id: 0x000F4243u,
position: new Vector3(58.81f, -49.42f, -0.85f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x000F4243u,
ParentCellId = 0x8A02015Eu,
},
};
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketDynamicRecord(
in record,
[0x8A02015Eu, 0x8A02015Fu, 0x8A0201C1u],
indoor,
outdoor,
renderCenterLbX: 0x8A,
renderCenterLbY: 0x02);
Assert.Equal(
[0x8A02015Eu, 0x8A02015Fu, 0x8A0201C1u],
indoor.Keys.Order());
Assert.All(
indoor.Values,
bucket => Assert.Equal(record, Assert.Single(bucket)));
Assert.Empty(outdoor);
}
[Fact]
public void BucketDynamicRecord_UnregisteredInteriorEffectFallsBackToParentCell()
{
RenderProjectionRecord record = Record(
id: 0x00001234u,
position: Vector3.Zero) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x00001234u,
ParentCellId = 0x8A02015Fu,
},
};
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketDynamicRecord(
in record,
Array.Empty<uint>(),
indoor,
outdoor,
renderCenterLbX: 0x8A,
renderCenterLbY: 0x02);
Assert.Equal(record, Assert.Single(indoor[0x8A02015Fu]));
Assert.Empty(outdoor);
}
// Campaign OVERHAUL S2 chunk 4: ResolveDynamicRenderCells is now a direct
// TryGetRetailCellArray read for EVERY dynamic record, children included
// — ShadowObjectRegistry.AttachChild owns Contract B's child-inheritance
// recursion at the registry, so the render-side parent-chain walk this
// test used to exercise no longer exists.
private static ShadowShape ChildBsp(uint gfxObjId) =>
ShadowShape.Bsp(
gfxObjId,
Vector3.Zero,
Quaternion.Identity,
scale: 1f,
localGeometry: ShadowPartGeometry.Create(
new FlatCollisionSphere(Vector3.Zero, 1f), null));
[Fact]
public void ResolveDynamicRenderCells_AttachedChildReadsTheRegistrysRetailCellArrayDirectly()
{
var shadows = new ShadowObjectRegistry();
const uint rootId = 0x5000000Au;
const uint childId = 0x800045EEu;
IReadOnlyList<ShadowShape> rootParts = new[] { ChildBsp(0x02000001u) };
shadows.RegisterMultiPart(
rootId,
new Vector3(12f, 12f, 50f),
Quaternion.Identity,
rootParts,
state: 0u,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xF4180000u,
seedCellId: 0xF4180104u,
isStatic: false,
partArray: rootParts);
Assert.True(shadows.TryGetRetailCellArray(rootId, out IReadOnlyList<uint> rootCells));
Assert.NotEmpty(rootCells); // the fixture must actually exercise a flood
// No render-side parent walk any more: Contract B's inheritance is
// owned by the registry at attach time.
Assert.True(shadows.AttachChild(childId, rootId, new[] { ChildBsp(0x02000002u) }));
RenderProjectionRecord wand = Record(
id: childId,
position: Vector3.Zero) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = childId,
ParentCellId = 0xF4180104u,
},
EntityPayload = new RenderEntityPayload() with
{
CasterIdentity = RenderCasterIdentityKind.EquippedChild,
},
};
IReadOnlyList<uint> cells =
WalkProductionWorldData.ResolveDynamicRenderCells(
in wand,
id => shadows.TryGetRetailCellArray(id, out IReadOnlyList<uint> c)
? (true, c)
: (false, Array.Empty<uint>()),
shadows.GetOwnerCells,
out bool usedFallback);
Assert.False(usedFallback);
Assert.Equal(rootCells, cells);
}
[Fact]
public void ResolveDynamicRenderCells_UnregisteredEntityFallsBackToOwnerCells()
{
var shadows = new ShadowObjectRegistry();
const uint entityId = 0x00099Fu;
IReadOnlyList<ShadowShape> shapes = new[] { ChildBsp(0x02000003u) };
shadows.RegisterMultiPart(
entityId,
new Vector3(12f, 12f, 50f),
Quaternion.Identity,
shapes,
state: 0u,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xF4180000u,
seedCellId: 0xF4180104u,
isStatic: false);
// No partArray supplied above -> no retail cell array registered for
// this entity (item E of the S2 chunk-1 contract), so the ordinary
// collision-flood answer is the conservative fallback.
Assert.False(shadows.TryGetRetailCellArray(entityId, out _));
Assert.NotEmpty(shadows.GetOwnerCells(entityId));
RenderProjectionRecord record = Record(
id: entityId,
position: Vector3.Zero) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = entityId,
ParentCellId = 0xF4180104u,
},
};
IReadOnlyList<uint> cells =
WalkProductionWorldData.ResolveDynamicRenderCells(
in record,
id => shadows.TryGetRetailCellArray(id, out IReadOnlyList<uint> c)
? (true, c)
: (false, Array.Empty<uint>()),
shadows.GetOwnerCells,
out bool usedFallback);
Assert.True(usedFallback);
Assert.Equal(shadows.GetOwnerCells(entityId), cells);
}
// -----------------------------------------------------------------
// Campaign OVERHAUL S2 chunk 2: BeginFrame's indoor static sweep now
// borrows membership from ShadowObjectRegistry.TryGetRetailCellArray
// instead of rebuilding it (the deleted ResolveStaticRenderCells /
// ShadowObjectRegistry.ComputeStaticRenderCells pair). These two tests
// drive the REAL production path end to end — a real (bare, no-DAT)
// ShadowObjectRegistry, a real ArchRenderScene, and BeginFrame itself —
// rather than the hardcoded-cell-array style of the Bucket* tests above,
// which exercise only the (unchanged) bucketing primitive.
// -----------------------------------------------------------------
private static ShadowShape Bsp(uint gfxObjId, float radius = 1f) =>
ShadowShape.Bsp(
gfxObjId,
Vector3.Zero,
Quaternion.Identity,
scale: 1f,
localGeometry: ShadowPartGeometry.Create(
new FlatCollisionSphere(Vector3.Zero, radius), null));
private static RenderProjectionRecord IndoorStaticRecord(
uint entityId, uint sourceId, uint parentCellId) =>
new RenderProjectionRecord() with
{
Id = RenderProjectionId.FromRaw(entityId),
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
Transform = new RenderTransform(Matrix4x4.Identity),
PreviousTransform = new PreviousRenderTransform(Matrix4x4.Identity),
Residency = new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(parentCellId), 0x8A020000u, parentCellId),
Flags = RenderProjectionFlags.Draw,
Source = new RenderSourceMetadata() with
{
LocalEntityId = entityId,
SourceId = sourceId,
ParentCellId = parentCellId,
},
};
[Fact]
public void BeginFrame_IndoorStatic_RegisteredEntityUsesRegistryRetailCellArray()
{
const uint entityId = 0x48A02001u;
const uint retailCellId = 0x8A02015Fu;
// A DECOY parent cell, deliberately different from the registered
// retail array's cell, so the assertions below can only pass if the
// indoor sweep actually consulted the registry rather than falling
// back to the authored parent.
const uint decoyParentCellId = 0x8A0201C1u;
var shadows = new ShadowObjectRegistry();
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x01001234u) };
shadows.RegisterMultiPart(
entityId,
Vector3.Zero,
Quaternion.Identity,
parts,
state: 0u,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0x8A020000u,
seedCellId: retailCellId,
isStatic: true,
partArray: parts);
Assert.True(shadows.TryGetRetailCellArray(entityId, out IReadOnlyList<uint> retailCells));
Assert.Equal(new[] { retailCellId }, retailCells);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
IndoorStaticRecord(entityId, sourceId: 0x02000001u, decoyParentCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x8A020000u, renderCenterLbX: 0x8A, renderCenterLbY: 0x02);
Assert.Contains(
worldData.GetCellStatics(retailCellId).Records,
record => record.Id == projection.Id);
Assert.DoesNotContain(
worldData.GetCellStatics(decoyParentCellId).Records,
record => record.Id == projection.Id);
Assert.Equal(0, worldData.UnregisteredStaticRenderFallbackCount);
}
[Fact]
public void BeginFrame_IndoorStatic_UnregisteredEntityFallsBackToParentCellAndCountsFallback()
{
const uint entityId = 0x48A02002u;
const uint parentCellId = 0x8A02015Fu;
// Bare, empty registry: entityId is never registered, so
// TryGetRetailCellArray must answer false for it.
var shadows = new ShadowObjectRegistry();
Assert.False(shadows.TryGetRetailCellArray(entityId, out _));
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
IndoorStaticRecord(entityId, sourceId: 0x02000002u, parentCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x8A020000u, renderCenterLbX: 0x8A, renderCenterLbY: 0x02);
Assert.Contains(
worldData.GetCellStatics(parentCellId).Records,
record => record.Id == projection.Id);
Assert.Equal(1, worldData.UnregisteredStaticRenderFallbackCount);
}
private static RenderProjectionRecord Record(uint id, Vector3 position) =>
new RenderProjectionRecord() with
{
Id = RenderProjectionId.FromRaw(id),
Transform = new RenderTransform(Matrix4x4.CreateTranslation(position)),
Source = new RenderSourceMetadata() with { LocalEntityId = id },
};
}