feat(render): S2 chunk 2 - render statics borrow the registry's retail cell array
WalkProductionWorldData no longer floods; its indoor and outdoor static sweeps read ShadowObjectRegistry.TryGetRetailCellArray (retail's calc_cross_cells_static @0x00515160 -> AddPartsShadow @0x00517e40 CELLARRAY, computed once at registration). The App-owned render flood (ResolveStaticRenderCells, its fingerprint cache, the primitive-Setup special case) and Core's ComputeStaticRenderCells are deleted. The one remaining fallback, an entity the physics publisher has not registered yet while the projection journal already published it, buckets to the authored parent cell and is counted per frame (UnregisteredStaticRenderFallbackCount) for chunk 5 to judge on the connected route. The Facility stair pin now registers at the projection's own entity id (the old pure-function test never carried identity) and reads the retail array; the installed-DAT comparator compares retail against collision. Gates (run in the implementer's isolated worktree at identical content): Release build 0/0; Core Physics 2,202/2,202; App hermetic 6,760/6,760 (the two added WalkProductionWorldData tests); installed-DAT walk/flood/stair family 24/24; Runtime 1,884/1,884. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
2b40c0bb38
commit
707d2803a4
10 changed files with 392 additions and 227 deletions
|
|
@ -33,16 +33,19 @@ namespace AcDream.App.Tests.Physics;
|
|||
/// <c>PublishStaticEntity</c> branches, plus the
|
||||
/// <see cref="ShadowShapeBuilder.FromStaticRenderParts"/> part-array input —
|
||||
/// against a registry backed by the real <see cref="PhysicsDataCache"/>, then
|
||||
/// prints and compares three answers per fixture: retail's CELLARRAY
|
||||
/// (<see cref="ShadowObjectRegistry.TryGetRetailCellArray"/>), today's
|
||||
/// COLLISION cells (<see cref="ShadowObjectRegistry.GetOwnerCells"/>), and
|
||||
/// today's OLD RENDER cells
|
||||
/// (<see cref="ShadowObjectRegistry.ComputeStaticRenderCells"/> over the
|
||||
/// SAME part array and seed). Chunk 1's ownership-map claim is that retail
|
||||
/// and old-render share one primitive for a BSP-routed static, so this
|
||||
/// asserts that equality; a retail-vs-collision difference is EXPECTED
|
||||
/// evidence for chunk 3 (decorative, non-colliding parts) and is printed,
|
||||
/// never asserted.
|
||||
/// prints and compares two answers per fixture: retail's CELLARRAY
|
||||
/// (<see cref="ShadowObjectRegistry.TryGetRetailCellArray"/>) and today's
|
||||
/// COLLISION cells (<see cref="ShadowObjectRegistry.GetOwnerCells"/>). Chunk
|
||||
/// 2 deleted the third "old render cells" column
|
||||
/// (<c>ShadowObjectRegistry.ComputeStaticRenderCells</c>) along with its
|
||||
/// only production consumer (<c>WalkProductionWorldData.ResolveStaticRenderCells</c>).
|
||||
/// Every fixture here is BSP-bearing, and chunk 1's evidence
|
||||
/// (`docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md` §5)
|
||||
/// found retail == collision == old-render exactly for all five as a
|
||||
/// structural consequence of the shared bbox-route primitive — so this
|
||||
/// asserts retail == collision directly; a genuine divergence (expected only
|
||||
/// for a fixture mixing colliding and non-colliding parts, none of which
|
||||
/// exist among these fixtures) remains chunk 3 evidence.
|
||||
/// </summary>
|
||||
[Trait("Lane", "InstalledDat")]
|
||||
public sealed class RetailCellArrayComparatorInstalledDatTests
|
||||
|
|
@ -118,11 +121,10 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
|
||||
/// <summary>
|
||||
/// Verbatim copy of <c>ShadowObjectRegistry.DeriveOutdoorSeed</c> (private
|
||||
/// there) — needed here so the SAME resolved seed feeds both
|
||||
/// <see cref="ShadowObjectRegistry.RegisterMultiPart"/> and the
|
||||
/// independent <see cref="ShadowObjectRegistry.ComputeStaticRenderCells"/>
|
||||
/// recompute below (that method requires a non-zero seed directly; it
|
||||
/// does not derive one).
|
||||
/// there) — needed here so an outdoor fixture with no explicit
|
||||
/// <c>seedCellId</c> resolves the SAME seed
|
||||
/// <see cref="ShadowObjectRegistry.RegisterMultiPart"/> would derive
|
||||
/// internally, for printing alongside the registered result.
|
||||
/// </summary>
|
||||
private static uint DeriveOutdoorSeedForTest(
|
||||
Vector3 worldPos, float worldOffsetX, float worldOffsetY, uint landblockId)
|
||||
|
|
@ -141,7 +143,6 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
uint EntityId,
|
||||
IReadOnlyList<uint> RetailCells,
|
||||
IReadOnlyList<uint> CollisionCells,
|
||||
IReadOnlyList<uint> OldRenderCells,
|
||||
RetailCellArrayRoute Route,
|
||||
int PartCount,
|
||||
int BspShapeCount,
|
||||
|
|
@ -154,7 +155,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
/// <c>LandblockPhysicsPublisher.PublishStaticEntity</c> uses — plus the
|
||||
/// chunk-1b whole-part-array side product
|
||||
/// (<see cref="ShadowShapeBuilder.FromStaticRenderParts"/>), then reads
|
||||
/// back and prints all three comparator answers.
|
||||
/// back and prints both comparator answers.
|
||||
/// </summary>
|
||||
private ComparatorOutcome RegisterAndCompare(
|
||||
string label,
|
||||
|
|
@ -232,20 +233,16 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<uint> oldRenderCells = resolvedSeed != 0u && partArray.Count > 0
|
||||
? engine.ShadowObjects.ComputeStaticRenderCells(resolvedSeed, worldPos, worldRot, partArray)
|
||||
: Array.Empty<uint>();
|
||||
|
||||
engine.ShadowObjects.TryGetRetailCellArray(entityId, out IReadOnlyList<uint> retailCells);
|
||||
IReadOnlyList<uint> collisionCells = engine.ShadowObjects.GetOwnerCells(entityId);
|
||||
RetailCellArrayRoute route = engine.ShadowObjects.GetRetailCellArrayRoute(entityId);
|
||||
|
||||
PrintOutcome(
|
||||
label, entityId, retailCells, collisionCells, oldRenderCells, route,
|
||||
label, entityId, retailCells, collisionCells, route,
|
||||
partArray.Count, bspShapes.Count, hasPhysicsBsp, registered, resolvedSeed);
|
||||
|
||||
return new ComparatorOutcome(
|
||||
label, entityId, retailCells, collisionCells, oldRenderCells, route,
|
||||
label, entityId, retailCells, collisionCells, route,
|
||||
partArray.Count, bspShapes.Count, registered);
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +251,6 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
uint entityId,
|
||||
IReadOnlyList<uint> retailCells,
|
||||
IReadOnlyList<uint> collisionCells,
|
||||
IReadOnlyList<uint> oldRenderCells,
|
||||
RetailCellArrayRoute route,
|
||||
int partCount,
|
||||
int bspShapeCount,
|
||||
|
|
@ -271,7 +267,6 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
$" parts={partCount} bspShapes={bspShapeCount} hasPhysicsBsp={hasPhysicsBsp} collisionRegistered={collisionRegistered} route={route}"));
|
||||
_out.WriteLine($" retail n={retailCells.Count} {CellSet(retailCells)}");
|
||||
_out.WriteLine($" collision n={collisionCells.Count} {CellSet(collisionCells)}");
|
||||
_out.WriteLine($" oldRender n={oldRenderCells.Count} {CellSet(oldRenderCells)}");
|
||||
|
||||
foreach (uint cellId in retailCells)
|
||||
{
|
||||
|
|
@ -287,7 +282,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
if (retailOnly.Any() || collisionOnly.Any())
|
||||
{
|
||||
_out.WriteLine(
|
||||
$" retail-vs-collision difference (EXPECTED evidence, not asserted): "
|
||||
$" retail-vs-collision difference: "
|
||||
+ $"retailOnly={CellSet(retailOnly)} collisionOnly={CellSet(collisionOnly)}");
|
||||
}
|
||||
}
|
||||
|
|
@ -316,10 +311,10 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
worldPos, worldRot, worldOffsetX, worldOffsetY, landblockId, seedCellId);
|
||||
}
|
||||
|
||||
private static void AssertSameCells(IReadOnlyList<uint> retail, IReadOnlyList<uint> oldRender)
|
||||
private static void AssertSameCells(IReadOnlyList<uint> retail, IReadOnlyList<uint> collision)
|
||||
{
|
||||
Assert.Equal(
|
||||
oldRender.OrderBy(id => id).ToArray(),
|
||||
collision.OrderBy(id => id).ToArray(),
|
||||
retail.OrderBy(id => id).ToArray());
|
||||
}
|
||||
|
||||
|
|
@ -328,7 +323,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void FacilityHubStair_RetailCellArrayMatchesOldRenderCells()
|
||||
public void FacilityHubStair_RetailCellArrayMatchesCollisionCells()
|
||||
{
|
||||
string? datDir = ResolveDatDir();
|
||||
if (datDir is null) Assert.Fail(SkipMessage);
|
||||
|
|
@ -366,7 +361,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
|
||||
Assert.Contains(FacilityHub | 0x015Fu, outcome.RetailCells);
|
||||
Assert.Contains(FacilityHub | 0x015Eu, outcome.RetailCells);
|
||||
AssertSameCells(outcome.RetailCells, outcome.OldRenderCells);
|
||||
AssertSameCells(outcome.RetailCells, outcome.CollisionCells);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
|
@ -389,7 +384,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void CathedralRamp_RetailCellArrayMatchesOldRenderCells()
|
||||
public void CathedralRamp_RetailCellArrayMatchesCollisionCells()
|
||||
{
|
||||
string? datDir = ResolveDatDir();
|
||||
if (datDir is null) Assert.Fail(SkipMessage);
|
||||
|
|
@ -434,7 +429,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
landblockId: Cathedral,
|
||||
seedCellId: parentCellId);
|
||||
|
||||
AssertSameCells(outcome.RetailCells, outcome.OldRenderCells);
|
||||
AssertSameCells(outcome.RetailCells, outcome.CollisionCells);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
|
@ -442,7 +437,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void NeftetFormation_RetailCellArrayMatchesOldRenderCells()
|
||||
public void NeftetFormation_RetailCellArrayMatchesCollisionCells()
|
||||
{
|
||||
string? datDir = ResolveDatDir();
|
||||
if (datDir is null) Assert.Fail(SkipMessage);
|
||||
|
|
@ -475,7 +470,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
landblockId: NeftetLandblock,
|
||||
seedCellId: 0u);
|
||||
|
||||
AssertSameCells(outcome.RetailCells, outcome.OldRenderCells);
|
||||
AssertSameCells(outcome.RetailCells, outcome.CollisionCells);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
|
@ -490,7 +485,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
// square_length (#334) — the sphere is typically much looser (a first
|
||||
// pass keyed on BoundingSphere.Radius picked the #334 Neftet formation
|
||||
// itself, whose 69 m sphere radius crosses 0 m at its 63.78 m origin but
|
||||
// whose real 96 m box does not — see NeftetFormation_RetailCellArrayMatchesOldRenderCells,
|
||||
// whose real 96 m box does not — see NeftetFormation_RetailCellArrayMatchesCollisionCells,
|
||||
// which already covers that object; this scan explicitly excludes it so
|
||||
// fixture 4 stays independent of fixture 3).
|
||||
// ---------------------------------------------------------------
|
||||
|
|
@ -521,7 +516,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void OutdoorLandblockEdgeCrosser_RetailCellArrayMatchesOldRenderCells()
|
||||
public void OutdoorLandblockEdgeCrosser_RetailCellArrayMatchesCollisionCells()
|
||||
{
|
||||
string? datDir = ResolveDatDir();
|
||||
if (datDir is null) Assert.Fail(SkipMessage);
|
||||
|
|
@ -574,7 +569,7 @@ public sealed class RetailCellArrayComparatorInstalledDatTests
|
|||
$" cells in a NEIGHBOR landblock (edge-crossing evidence): "
|
||||
+ $"[{string.Join(",", foreignLandblockCells.Select(id => FormattableString.Invariant($"0x{id:X8}")))}]");
|
||||
|
||||
AssertSameCells(outcome.RetailCells, outcome.OldRenderCells);
|
||||
AssertSameCells(outcome.RetailCells, outcome.CollisionCells);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -167,8 +167,29 @@ public class Issue177StairDescentCameraFloodTests
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S2 chunk 2: retargeted from the deleted
|
||||
/// <c>ShadowObjectRegistry.ComputeStaticRenderCells</c> pure-function
|
||||
/// side channel to the production
|
||||
/// <see cref="ShadowObjectRegistry.RegisterMultiPart"/> transaction
|
||||
/// (the SAME BSP-collision + <c>partArray:</c> recipe
|
||||
/// <c>LandblockPhysicsPublisher.PublishStaticEntity</c> uses, and the
|
||||
/// same one <c>RetailCellArrayComparatorInstalledDatTests</c>' fixture 1
|
||||
/// exercises for this exact Setup) plus
|
||||
/// <see cref="ShadowObjectRegistry.TryGetRetailCellArray"/>. The entity
|
||||
/// id now MUST match the projection's
|
||||
/// <see cref="RenderSourceMetadata.LocalEntityId"/> — chunk 2's
|
||||
/// <see cref="WalkProductionWorldData"/> indoor sweep looks the retail
|
||||
/// CELLARRAY up BY that id, unlike the deleted method's seed+position
|
||||
/// pure function, which never carried entity identity. The former
|
||||
/// "WithoutCollisionRows" name no longer describes production: a part
|
||||
/// array is always supplied at the SAME registration as the collision
|
||||
/// shapes now (chunk 1b), so <c>GetOwnerCells</c> for this BSP-bearing
|
||||
/// Setup reflects that registration — and, per the comparator's fixture-1
|
||||
/// evidence, equals the retail CELLARRAY exactly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FacilityStairAssembly_RegisterAcross015FTo015EWithoutCollisionRows()
|
||||
public void FacilityStairAssembly_RegisterAcross015FTo015E()
|
||||
{
|
||||
string? datDir = ResolveDatDir();
|
||||
if (datDir is null)
|
||||
|
|
@ -199,6 +220,8 @@ public class Issue177StairDescentCameraFloodTests
|
|||
stair.Frame.Origin.Z);
|
||||
Quaternion rootRotation = stair.Frame.Orientation;
|
||||
|
||||
List<ShadowShape> bspShapes = ShadowShapeBuilder.FromLandblockBspParts(
|
||||
meshRefs, isBuildingShell: false, cache.GetGfxObj);
|
||||
List<ShadowShape> parts = ShadowShapeBuilder.FromStaticRenderParts(
|
||||
meshRefs,
|
||||
cache.GetGfxObj,
|
||||
|
|
@ -206,14 +229,25 @@ public class Issue177StairDescentCameraFloodTests
|
|||
out bool hasPhysicsBsp);
|
||||
Assert.True(hasPhysicsBsp);
|
||||
Assert.NotEmpty(parts);
|
||||
Assert.NotEmpty(bspShapes);
|
||||
|
||||
const uint syntheticOwner = 0x7F00DEADu;
|
||||
Assert.Empty(engine.ShadowObjects.GetOwnerCells(syntheticOwner));
|
||||
IReadOnlyList<uint> cells = engine.ShadowObjects.ComputeStaticRenderCells(
|
||||
FacilityHub | 0x015Fu,
|
||||
const uint entityId = 0x48A02000u;
|
||||
engine.ShadowObjects.RegisterMultiPart(
|
||||
entityId,
|
||||
rootPosition,
|
||||
rootRotation,
|
||||
parts);
|
||||
bspShapes,
|
||||
state: 0u,
|
||||
flags: EntityCollisionFlags.None,
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f,
|
||||
landblockId: FacilityHub,
|
||||
seedCellId: FacilityHub | 0x015Fu,
|
||||
isStatic: true,
|
||||
partArray: parts);
|
||||
|
||||
Assert.True(engine.ShadowObjects.TryGetRetailCellArray(
|
||||
entityId, out IReadOnlyList<uint> cells));
|
||||
|
||||
ShadowShape part = parts[0];
|
||||
ShadowPartBox box = ShadowPartBox.FromShape(
|
||||
|
|
@ -229,11 +263,21 @@ public class Issue177StairDescentCameraFloodTests
|
|||
|
||||
Assert.Contains(FacilityHub | 0x015Fu, cells);
|
||||
Assert.Contains(FacilityHub | 0x015Eu, cells);
|
||||
Assert.Empty(engine.ShadowObjects.GetOwnerCells(syntheticOwner));
|
||||
|
||||
// The registration carries collision shapes at the SAME transaction
|
||||
// now (production always does — chunk 1b threaded the part array
|
||||
// through the SAME RegisterMultiPart call the collision dispatch
|
||||
// uses), so GetOwnerCells reflects that registration instead of
|
||||
// staying empty; retail == collision for this BSP-bearing Setup
|
||||
// (RetailCellArrayComparatorInstalledDatTests fixture 1), so the two
|
||||
// sets agree exactly.
|
||||
Assert.Equal(
|
||||
cells.OrderBy(id => id),
|
||||
engine.ShadowObjects.GetOwnerCells(entityId).OrderBy(id => id));
|
||||
|
||||
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionId projectionId = RenderProjectionId.FromRaw(0x48A02000u);
|
||||
RenderProjectionId projectionId = RenderProjectionId.FromRaw(entityId);
|
||||
RenderTransform transform = RenderTransform.FromRoot(
|
||||
rootPosition,
|
||||
rootRotation,
|
||||
|
|
@ -252,7 +296,7 @@ public class Issue177StairDescentCameraFloodTests
|
|||
Flags = RenderProjectionFlags.Draw,
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x48A02000u,
|
||||
LocalEntityId = entityId,
|
||||
SourceId = setupId,
|
||||
ParentCellId = FacilityHub | 0x015Fu,
|
||||
},
|
||||
|
|
@ -272,6 +316,7 @@ public class Issue177StairDescentCameraFloodTests
|
|||
Assert.Contains(
|
||||
worldData.GetCellStatics(FacilityHub | 0x015Eu).Records,
|
||||
record => record.Id == projectionId);
|
||||
Assert.Equal(0, worldData.UnregisteredStaticRenderFallbackCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
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;
|
||||
|
||||
|
|
@ -269,6 +271,121 @@ public sealed class WalkProductionWorldDataTests
|
|||
Assert.Equal([0xF4180106u, 0xF4180104u], 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -92,12 +92,14 @@ public class ShadowObjectRegistryRetailCellArrayTests
|
|||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// The bbox route is ComputeStaticRenderCells's own primitive, and
|
||||
// supplying a part array never touches the existing collision product.
|
||||
// The bbox route (find_bbox_cell_list 0x00510fc0) is the SAME primitive
|
||||
// the collision flood uses for a single BSP-shape part array fed as both
|
||||
// `shapes` and `partArray`, and supplying a part array never touches the
|
||||
// existing collision product.
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void RegisterMultiPart_WithPartArray_BoundingBoxRouteMatchesComputeStaticRenderCells()
|
||||
public void RegisterMultiPart_WithPartArray_BoundingBoxRouteMatchesCollisionFloodForTheSamePartArray()
|
||||
{
|
||||
var withPartArray = new ShadowObjectRegistry();
|
||||
var withoutPartArray = new ShadowObjectRegistry();
|
||||
|
|
@ -113,11 +115,16 @@ public class ShadowObjectRegistryRetailCellArrayTests
|
|||
|
||||
Assert.Equal(RetailCellArrayRoute.BoundingBox, withPartArray.GetRetailCellArrayRoute(entityId));
|
||||
Assert.True(withPartArray.TryGetRetailCellArray(entityId, out var retailCells));
|
||||
Assert.NotEmpty(retailCells); // the fixture must actually exercise a flood
|
||||
|
||||
IReadOnlyList<uint> expected =
|
||||
withPartArray.ComputeStaticRenderCells(CellA, Pos, Quaternion.Identity, parts);
|
||||
Assert.Equal(expected, retailCells);
|
||||
Assert.NotEmpty(expected); // the fixture must actually exercise a flood
|
||||
// Retail's bbox route and RegisterMultiPart's own BSP collision
|
||||
// dispatch share one primitive (CPhysicsObj::find_bbox_cell_list
|
||||
// 0x00510fc0) when fed the identical single-part array as both
|
||||
// `shapes` and `partArray`, so the two products agree exactly. This
|
||||
// is the independent ground truth now that ComputeStaticRenderCells
|
||||
// (Campaign OVERHAUL S2 chunk 2) is deleted along with its only
|
||||
// production consumer.
|
||||
Assert.Equal(withPartArray.GetOwnerCells(entityId), retailCells);
|
||||
|
||||
// Supplying a part array must not perturb the pre-existing collision
|
||||
// product — same registration, same result either way.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue