test(phys): A6.P3 #98 — comparison harness reproduces cottage-floor cap

Apparatus convergence. With the cottage GfxObj 0x01000A2B registered as
a ShadowEntry in BuildEngineWithCellarFixtures, the harness now reproduces
the live cap-event collision normal (cn=(0,0,-1)) exactly, ending the
"harness doesn't reproduce" divergence the prior session's findings doc
identified.

Concretely:
  * Adds a minimum-stub landblock (TerrainSurface at z=-1000) so
    TryGetLandblockContext succeeds at the cellar XY — production's
    FindObjCollisions early-returns without a landblock and would skip
    the cottage shadow query.
  * Adds RegisterCottageGfxObj that loads the 74-polygon cottage fixture
    via GfxObjDumpSerializer.Hydrate, then registers it at the cottage's
    world transform (translation (130.5, 11.5, 94.0) + 180° around Z,
    derived from the cellar cell's WorldTransform), matching
    GameWindow.cs:5893's landblock-baked-static registration shape.
  * LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered
    flips: the cap-normal reproduction is now enforced by
    LiveCompare_FirstCap_HarnessReproducesCottageFloorCapNormal.
  * The full per-field round-trip uncovered ONE residual divergence:
    live preserves +0.0266m of +X motion through the cap event (edge-
    slide along the floor in XY); harness blocks ALL motion at the cap.
    Captured by LiveCompare_FirstCap_ResidualXMotionDivergence_Docs...
    in documents-the-bug form so the next session has a concrete next
    target.

Fixture: tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json
(74 polygons, 6 downward-facing cottage-floor triangles at object-local
Z=0, BSP radius 13.989m matching the live [resolve-bldg] bspR=13.99).
Captured via launch-a6-issue98-cottage-gfxobj-dump.ps1.

In-isolation: all 12 CellarUpTrajectoryReplayTests + 4 GfxObjDumpRoundTripTests
+ 1 new PhysicsDiagnosticsTests pass.

Note on full-suite baseline: the full xUnit serial run shows 8–19
failures depending on order (pre-existing test interaction with shared
statics across PlayerMovementControllerTests, MotionInterpreterTests,
PositionManagerTests, etc.). The flakiness is independent of this
change — confirmed by stashing the harness changes and observing the
same flaky range. Investigating the static-state isolation problem is
out of scope for issue #98; tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-23 20:44:50 +02:00
parent cc3afbcbeb
commit 97fec19dbb
3 changed files with 2586 additions and 132 deletions

View file

@ -0,0 +1,46 @@
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = '1'
$env:ACDREAM_TEST_HOST = '127.0.0.1'
$env:ACDREAM_TEST_PORT = '9000'
$env:ACDREAM_TEST_USER = 'testaccount'
$env:ACDREAM_TEST_PASS = 'testpassword'
# A6.P3 #98 (2026-05-23 evening v2) — focused capture of the cottage
# GfxObj 0x01000A2B's full polygon table. ACDREAM_DUMP_GFXOBJS triggers
# a one-shot JSON dump the first time PhysicsDataCache.CacheGfxObj fires
# for the listed id. The dump lands in the tests' fixture directory under
# the worktree, so the harness can load it without copying.
#
# Reproduction steps for the user:
# 1. Run this script (it launches in the foreground; log streams to the
# file path below).
# 2. Log into +Acdream. The cottage GfxObj caches when the streaming
# worker walks the cottage building's mesh — which happens as soon
# as the cottage landblock enters the streaming N1 (near) tier.
# Holtburg's cottage (the one with the cellar) is at the spawn area,
# so just being in-world is enough.
# 3. Watch the log for "[gfxobj-dump] wrote 0x01000A2B polys=N → ..."
# then close the client.
#
# After the dump file exists at
# tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json
# come back to Claude to continue with the RegisterCottageGfxObj wiring.
$env:ACDREAM_DUMP_GFXOBJS = '0x01000A2B'
# Output dir is the relative fixture path; the dump infrastructure
# resolves it against the worktree current dir (Set-Location below).
$env:ACDREAM_DUMP_GFXOBJS_DIR = 'tests/AcDream.Core.Tests/Fixtures/issue98'
# Keep the cell-transit probe on so the launch log shows when the player
# enters cells — helps correlate the dump event with player position.
$env:ACDREAM_PROBE_CELL = '1'
$logPath = 'C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\a6-issue98-cottage-gfxobj-dump-launch.log'
Write-Host "Log path: $logPath"
Write-Host "Dump target: $env:ACDREAM_DUMP_GFXOBJS_DIR\0x01000A2B.gfxobj.json"
Write-Host ''
Write-Host 'After login, watch the log for [gfxobj-dump] then close the client.'
Set-Location 'C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c'
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug *> $logPath

File diff suppressed because it is too large Load diff

View file

@ -484,31 +484,105 @@ public class CellarUpTrajectoryReplayTests
/// First-cap event — the failing tick. Live engine reports cn=(0,0,-1), /// First-cap event — the failing tick. Live engine reports cn=(0,0,-1),
/// a downward-facing collision normal, capping the foot sphere at /// a downward-facing collision normal, capping the foot sphere at
/// world Z=92.74. Math: head sphere TOP reaches Z=94.0 (the cottage /// world Z=92.74. Math: head sphere TOP reaches Z=94.0 (the cottage
/// floor) when foot Z = 94.0 - sphereHeight = 92.80. So the head is /// floor) when foot Z = 94.0 - sphereHeight = 92.80. The head bumps
/// bumping the cottage floor from BELOW. /// the cottage floor from BELOW — NOT a step-up / AdjustOffset bug.
/// ///
/// <para> /// <para>
/// This is the actual #98 bug, NOT a step-up / AdjustOffset problem. /// Live capture's <c>[resolve]</c> probe pinpointed the blocking
/// Live capture's <c>[resolve]</c> probe pinpoints the blocking
/// entity: <c>obj=0xA9B47900</c> — a landblock-baked static building /// entity: <c>obj=0xA9B47900</c> — a landblock-baked static building
/// (the cottage GfxObj). The cottage's floor polygons live in this /// (the cottage GfxObj <c>0x01000A2B</c>). The cottage's floor polys
/// GfxObj, registered as a ShadowEntry, NOT in any of the cottage's /// live in this GfxObj as a ShadowEntry, NOT in any cottage cell.
/// cells. The harness's <see cref="BuildEngineWithCellarFixtures"/>
/// loads cell fixtures but does NOT register the cottage GfxObj, so
/// the harness fails to reproduce the cap — DOCUMENTED here as the
/// divergence pattern.
/// </para> /// </para>
/// ///
/// <para> /// <para>
/// Documents-the-bug pattern: passes WHILE the harness lacks the /// Apparatus-convergence form (2026-05-23 evening v2): with the
/// cottage GfxObj. When a future session adds the cottage GfxObj /// cottage GfxObj registered via <see cref="RegisterCottageGfxObj"/>,
/// (full polygon list extracted from the live <c>[poly-dump]</c> + /// the harness reproduces the live cn=(0,0,-1) cap event. This test
/// <c>[resolve-bldg]</c> probes), this test will start failing — /// enforces THAT specific reproduction. The post-cap position
/// the signal to flip it from documenting-the-bug to enforcing-the-fix. /// processing has a separate residual divergence — documented by
/// <see cref="LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation"/>.
/// </para> /// </para>
/// </summary> /// </summary>
[Fact] [Fact]
public void LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered() public void LiveCompare_FirstCap_HarnessReproducesCottageFloorCapNormal()
{
var (engine, _) = BuildEngineWithCellarFixtures();
var captured = LoadCapturedRecord(record =>
record.Result.CollisionNormalValid
&& record.Result.CollisionNormal.Z < -0.99f);
// Live must have cn=(0,0,-1) at this point — sanity check.
Assert.True(captured.Result.CollisionNormalValid,
"Captured record must have collisionNormalValid=true.");
Assert.True(captured.Result.CollisionNormal.Z < -0.99f,
$"Captured record must have downward collision normal; got " +
$"{captured.Result.CollisionNormal}.");
// Replay the call.
Assert.NotNull(captured.BodyBefore);
var body = SeedBodyFromSnapshot(captured.BodyBefore);
var harnessResult = engine.ResolveWithTransition(
currentPos: captured.Input.CurrentPos,
targetPos: captured.Input.TargetPos,
cellId: captured.Input.CellId,
sphereRadius: captured.Input.SphereRadius,
sphereHeight: captured.Input.SphereHeight,
stepUpHeight: captured.Input.StepUpHeight,
stepDownHeight: captured.Input.StepDownHeight,
isOnGround: captured.Input.IsOnGround,
body: body,
moverFlags: (ObjectInfoState)captured.Input.MoverFlags,
movingEntityId: captured.Input.MovingEntityId);
// Apparatus convergence: harness reproduces the cap-event collision
// normal exactly. If this fails, the cottage GfxObj registration
// has regressed (RegisterCottageGfxObj is broken, the dump fixture
// is stale, or the harness wiring lost the landblock context).
Assert.True(harnessResult.CollisionNormalValid,
"Harness must reproduce the live collision-normal-valid signal " +
"now that the cottage GfxObj is registered.");
Assert.True(harnessResult.CollisionNormal.Z < -0.99f,
$"Harness must reproduce the live downward-facing cap normal " +
$"(live cn={captured.Result.CollisionNormal}, harness cn={harnessResult.CollisionNormal}).");
}
/// <summary>
/// A6.P3 issue #98 (2026-05-23 evening v2) — documents-the-bug for the
/// residual divergence the apparatus surfaced. With the cottage GfxObj
/// registered, the harness reproduces the live cap-event collision
/// normal, but the POST-CAP POSITION processing diverges:
/// <list type="bullet">
/// <item>Live: full +X motion preserved (sphere slides +0.0266 m in X
/// along the cottage floor before the Y motion is capped).</item>
/// <item>Harness: ZERO X motion (sphere stays at its input X position,
/// only the Y component is blocked).</item>
/// </list>
///
/// <para>
/// Both X positions agree on Y=7.2243 and Z=92.7390. Only X differs:
/// live X=141.3865 (requested target), harness X=141.3599 (current = no
/// move). The requested delta was (+0.0266, -0.4022, 0); live applied
/// the +X portion, harness applied nothing.
/// </para>
///
/// <para>
/// Hypothesis (to investigate next session): live's response to a
/// cn=(0,0,-1) head-bump treats it as a Z-only constraint and lets the
/// XY component of the move complete via edge-slide. Harness's BSP path
/// is rejecting the WHOLE move vector when the cottage floor poly
/// intersects the head sphere, instead of computing a slid offset.
/// </para>
///
/// <para>
/// Documents-the-bug: PASSES today on the asserted residual magnitude.
/// When a future session fixes the post-cap edge-slide, harness X will
/// match live X — this test FAILS at that point, signaling that the
/// X divergence is closed and the test should be folded back into the
/// strict <see cref="AssertCallMatchesCapture"/> path.
/// </para>
/// </summary>
[Fact]
public void LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation()
{ {
var (engine, _) = BuildEngineWithCellarFixtures(); var (engine, _) = BuildEngineWithCellarFixtures();
var captured = LoadCapturedRecord(record => var captured = LoadCapturedRecord(record =>
@ -530,26 +604,20 @@ public class CellarUpTrajectoryReplayTests
moverFlags: (ObjectInfoState)captured.Input.MoverFlags, moverFlags: (ObjectInfoState)captured.Input.MoverFlags,
movingEntityId: captured.Input.MovingEntityId); movingEntityId: captured.Input.MovingEntityId);
// Live reported cn=(0,0,-1) blocking the climb at this point. // Live preserved the full +X motion through the cap event; harness
Assert.True(captured.Result.CollisionNormalValid, // blocked it. Y and Z agree.
"Captured record must have collisionNormalValid=true."); Assert.Equal(captured.Result.Position.Y, harnessResult.Position.Y, 4);
Assert.True(captured.Result.CollisionNormal.Z < -0.99f, Assert.Equal(captured.Result.Position.Z, harnessResult.Position.Z, 4);
$"Captured record must have downward collision normal; got " +
$"{captured.Result.CollisionNormal}.");
// Harness does NOT reproduce the live downward push because the float liveDeltaX = captured.Result.Position.X - captured.Input.CurrentPos.X;
// cottage GfxObj is not registered — the blocking polygon lives float harnessDeltaX = harnessResult.Position.X - captured.Input.CurrentPos.X;
// in static obj 0xA9B47900, which BuildEngineWithCellarFixtures
// intentionally skips today (RegisterStairRampGfxObj is commented Assert.True(liveDeltaX > 0.02f,
// out). When the cottage GfxObj's full polygon set is added to $"Live must show +X motion after cap (expected ~+0.0266 m, got {liveDeltaX:F4}).");
// the harness, this assertion will start to fail — flip the test Assert.True(MathF.Abs(harnessDeltaX) < 0.001f,
// to assert the live cn=(0,0,-1) round-trips at that point. $"Harness currently zeros X motion through the cap (expected ~0, got {harnessDeltaX:F4}). " +
Assert.False( "If this assertion starts failing because harness now preserves +X, the post-cap " +
harnessResult.CollisionNormalValid "edge-slide divergence is closed — fold this back into AssertCallMatchesCapture.");
&& harnessResult.CollisionNormal.Z < -0.99f,
"Harness should NOT reproduce the cottage-floor cap yet — " +
"if it does, the cottage GfxObj has been added and this test " +
"needs to flip to AssertCallMatchesCapture(engine, captured).");
} }
/// <summary> /// <summary>
@ -868,25 +936,44 @@ public class CellarUpTrajectoryReplayTests
cache.RegisterCellStructForTest(cellId, cellWithBsp); cache.RegisterCellStructForTest(cellId, cellWithBsp);
} }
// ── 2. NO landblock registered ────────────────────────────── // ── 2. Minimum landblock context for FindObjCollisions ──────
// Without a landblock, SampleTerrainWalkable returns null and // FindObjCollisions (TransitionTypes.cs:2153) early-returns
// FindEnvCollisions's outdoor-fallback path returns OK without // TransitionState.OK when TryGetLandblockContext fails for the
// running ValidateWalkable on stub terrain. This is the right // sphere XY. Without a landblock the harness can't query the
// shape for indoor-only tests — the cell's BSP would handle // cottage GfxObj's shadow entries — and that's where the
// collision if hydrated, and falling through to stub terrain // first-cap collision actually lives (live capture confirmed
// produces spurious (0,1,0) wall hits. FindObjCollisions also // obj=0xA9B47900 fires the cn=(0,0,-1) push).
// early-returns without landblock context (line 2153 of //
// TransitionTypes.cs), so the synthetic stair GfxObj is also // Register an EMPTY-terrain landblock 0xA9B40000 anchored at
// skipped — fine for the airborne-at-tick-1 isolation. // world origin (0,0). The landblock test
// (worldX >= 0 && worldX < 192) covers every harness sphere
// position (X≈141, Y≈7). TerrainSurface gets a flat far-below
// surface so SampleTerrainZ returns something the indoor BSP
// path never consults (FindEnvCollisions's indoor branch fires
// first when the cell has BSP). Outdoor-fallback queries are
// harmless because the cell's synthetic BSP returns Collided
// before terrain is checked.
var heights = new byte[81]; // 9x9 corners
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f; // far below cellar
var stubTerrain = new TerrainSurface(heights, heightTable);
engine.AddLandblock(
landblockId: 0xA9B40000u,
terrain: stubTerrain,
cells: Array.Empty<CellSurface>(),
portals: Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
// ── 3. Synthetic stair-piece GfxObj + ShadowEntry ────────── // ── 3. Cottage GfxObj 0x01000A2B from dumped fixture ────────
// Temporarily disabled while debugging the airborne-at-tick-1 // Live capture (2026-05-23 PM v2) attributes the first-cap event
// issue. Re-enable once the cell-BSP-is-null + landblock-stub // to obj=0xA9B47900 (entity 0x00A9B479 partIdx=0) — a landblock-
// interaction is understood, AND we have a way to register // baked static building registered as a ShadowEntry. The full
// the stair without needing a landblock (e.g., extend // polygon table was extracted via ACDREAM_DUMP_GFXOBJS=0x01000A2B
// FindObjCollisions to query cellScope-only shadows without // (issue #98 evening-v2 apparatus); 74 polygons including six
// landblock context). // downward-facing cottage-floor triangles at object-local Z=0
// RegisterStairRampGfxObj(engine, cache); // that the head sphere bumps from below at world Z=94.
RegisterCottageGfxObj(engine, cache);
return (engine, cache); return (engine, cache);
} }
@ -939,100 +1026,75 @@ public class CellarUpTrajectoryReplayTests
} }
/// <summary> /// <summary>
/// Constructs a synthetic GfxObj containing the cellar ramp polygon /// A6.P3 issue #98 (2026-05-23 evening v2). Loads the cottage GfxObj
/// in WORLD coordinates and registers it as a ShadowEntry scoped to /// <c>0x01000A2B</c> from the JSON fixture
/// the cellar cell. The polygon's vertices + normal are reproduced /// (<c>tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json</c>,
/// from the live capture's <c>[poly-dump]</c> data (commit pre-3f56915), /// produced via the <c>ACDREAM_DUMP_GFXOBJS</c> capture infrastructure),
/// transformed to world frame so the GfxObj can sit at world origin /// hydrates it as a <see cref="GfxObjPhysics"/> with a synthetic
/// with identity rotation/scale (simplifies the /// single-leaf BSP, and registers it as a ShadowEntry at the cottage's
/// FindObjCollisions local-to-world transform). /// world transform — the same shape production's GameWindow.cs:5893
/// registration uses for landblock-baked statics.
/// ///
/// <para> /// <para>
/// Live capture's local polygon vertices (in building frame): /// Transform values come from two evidence sources:
/// (0.8,-1.59,-1.5), (0.8,1.31,1.5), (-0.8,1.31,1.5), (-0.8,-1.59,-1.5). /// <list type="bullet">
/// Building's world transform: origin (141.5, 7.155, 92.455), 180° yaw /// <item>The cellar cell 0xA9B40147's WorldTransform has translation
/// around Z. After applying yaw + translation, world vertices are: /// (130.5, 11.5, 94.0) and a 3×3 with M11=M22=-1 / M33=+1
/// (140.7, 8.745, 90.955), (140.7, 5.845, 93.955), /// (a 180° rotation around Z). The cottage GfxObj sits at the
/// (142.3, 5.845, 93.955), (142.3, 8.745, 90.955). /// SAME world transform (its building origin is also at
/// World normal = (0, 0.719, 0.695), world d = -69.5035 — matches /// (130.5, 11.5, 94.0) per the existing [resolve-bldg] capture
/// the live cdb capture exactly. /// <c>entOrigin_lb=(130.5,11.5,94.0)</c>).</item>
/// <item>BoundingSphere radius from the dump's
/// <see cref="GfxObjDump.BoundingSphereRadius"/> — 13.989 m.
/// Matches the live <c>bspR=13.99</c> observed in the
/// [resolve-bldg] capture; cross-validation that the same
/// building is in play.</item>
/// </list>
/// </para>
///
/// <para>
/// Entity id <c>0x00A9B479</c> mirrors the live capture's
/// <c>obj=0xA9B47900</c> formula (entity.Id × 256 + partIdx=0). Using
/// the same id keeps any future probe correlation aligned with live
/// log conventions.
/// </para> /// </para>
/// </summary> /// </summary>
private static void RegisterStairRampGfxObj(PhysicsEngine engine, PhysicsDataCache cache) private static void RegisterCottageGfxObj(PhysicsEngine engine, PhysicsDataCache cache)
{ {
const ushort RampPolyId = 0x0008; const uint CottageGfxId = 0x01000A2Bu;
const uint StairGfxId = 0xDEADBEEFu; const uint CottageEntityId = 0x00A9B479u;
const uint StairEntityId = 0xC0FFEE00u;
// World-frame vertices (winding order preserved from live capture). var fixturePath = Path.Combine(FixtureDir, "0x01000A2B.gfxobj.json");
var v0 = new Vector3(140.7f, 8.745f, 90.955f); // ramp foot, X=-side Assert.True(File.Exists(fixturePath),
var v1 = new Vector3(140.7f, 5.845f, 93.955f); // ramp top, X=-side $"Cottage GfxObj fixture missing: {fixturePath}. Re-run live " +
var v2 = new Vector3(142.3f, 5.845f, 93.955f); // ramp top, X=+side $"capture with ACDREAM_DUMP_GFXOBJS=0x01000A2B.");
var v3 = new Vector3(142.3f, 8.745f, 90.955f); // ramp foot, X=+side
var verts = new[] { v0, v1, v2, v3 };
// Compute normal from cross(v1-v0, v3-v0). var dump = GfxObjDumpSerializer.Read(fixturePath);
var edge0 = v1 - v0; var physics = GfxObjDumpSerializer.Hydrate(dump);
var edge1 = v3 - v0; cache.RegisterGfxObjForTest(CottageGfxId, physics);
var normal = Vector3.Normalize(Vector3.Cross(edge0, edge1));
// Plane equation: N·p + d = 0 → d = -N·v0.
float d = -Vector3.Dot(normal, v0);
var resolved = new Dictionary<ushort, ResolvedPolygon> // World transform from the cellar cell's WorldTransform: translation
{ // (130.5, 11.5, 94.0) + 180° rotation around Z. The cottage GfxObj
[RampPolyId] = new ResolvedPolygon // shares this transform (it IS the cellar/cottage geometry).
{ var worldPos = new Vector3(130.5f, 11.5f, 94.0f);
Vertices = verts, var worldRot = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI);
Plane = new System.Numerics.Plane(normal, d),
NumPoints = 4,
SidesType = CullMode.Landblock,
},
};
// Minimal one-leaf BSP containing the ramp poly. Bounding sphere
// encompasses the polygon (center at poly centroid).
var leaf = new PhysicsBSPNode
{
Type = BSPNodeType.Leaf,
BoundingSphere = new Sphere
{
Origin = new Vector3(141.5f, 7.295f, 92.455f),
Radius = 3.0f,
},
};
leaf.Polygons.Add(RampPolyId);
var bspTree = new PhysicsBSPTree { Root = leaf };
var gfxPhysics = new GfxObjPhysics
{
BSP = bspTree,
PhysicsPolygons = new Dictionary<ushort, Polygon>(),
Vertices = new VertexArray(),
Resolved = resolved,
BoundingSphere = leaf.BoundingSphere,
};
cache.RegisterGfxObjForTest(StairGfxId, gfxPhysics);
// ShadowEntry: object at world origin (0,0,0), identity rotation,
// scale 1.0 — keeps the polygon's WORLD-frame vertices intact
// through the FindObjCollisions local-transform math.
// cellScope = CellarId so the entry is only queried when the sphere
// is in cellar cell (matches retail's per-cell shadow scoping for
// interior statics — Issue #91 family).
engine.ShadowObjects.Register( engine.ShadowObjects.Register(
entityId: StairEntityId, entityId: CottageEntityId,
gfxObjId: StairGfxId, gfxObjId: CottageGfxId,
worldPos: Vector3.Zero, worldPos: worldPos,
rotation: Quaternion.Identity, rotation: worldRot,
radius: 5.0f, radius: physics.BoundingSphere?.Radius ?? 14f,
worldOffsetX: 0f, worldOffsetX: 0f,
worldOffsetY: 0f, worldOffsetY: 0f,
landblockId: 0xA9B40000u, landblockId: 0xA9B40000u,
collisionType: ShadowCollisionType.BSP, collisionType: ShadowCollisionType.BSP,
scale: 1.0f, scale: 1.0f,
cellScope: CellarId); // Landblock-baked statics in production (GameWindow.cs:5899) use
// `entity.ParentCellId ?? 0u` — the cottage building has no
// ParentCellId (it's a top-level landblock static), so the
// scope is landblock-wide (cellScope=0).
cellScope: 0u);
} }
/// <summary> /// <summary>