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>
475 lines
17 KiB
C#
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 },
|
|
};
|
|
}
|