From fafc0b65d958416060df3e4034e2d68b72b13bfa Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 6 Aug 2026 06:37:46 +0200 Subject: [PATCH] fix(physics): adopt the settle's resolved cell across an indoor seam (#276 remainder) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SpawnPlacementSettler committed settle.Position but discarded settle.CellId, so a compressed first-gravity-frame settle that crossed a cell boundary left the body's cell at the placement cell until some later resolve corrected it. Now committed through the same guarded channel the per-tick resolve writeback uses (RuntimeOrdinaryPhysicsUpdater): resolved cell when the transition reports one, source cell otherwise, never a zeroed residency. Retail anchor: CPhysicsObj::SetPositionInternal(CTransition const*) 0x00515330 commits both sphere_path.curr_pos.objcell_id and its frame, including EnvCells. WHERE THE DEFECT ACTUALLY BIT — #276's own framing is half wrong, and the half it misses is the whole fix. PhysicsBody.Position's setter already mirrors the world delta into the landblock-local frame and lets LandDefs.AdjustToOutside recompute the 24 m cell index from it, so an outdoor->outdoor settle already landed the right cell and dropping settle.CellId cost nothing there. It cannot do that for an EnvCell: an EnvCell id is not derivable from a position, so the mirror deliberately PRESERVES it. settle.CellId is therefore the only carrier of a cell identity across an indoor seam. The live defect is the issue's parenthetical ("outdoor/EnvCell seam, stacked EnvCells"), not its main clause — and the change is consequently a no-op on the outdoor path that dominates production, corrective only at the seam. That finding is what made the test possible. The three existing settler tests build bodies with NO CellPosition and pass identically with or without this change — shipping against them would have repeated C5b finding D3, a test that passed with its own change reverted. The new test seeds an EnvCell id over plain outdoor terrain instead, so the stale-id preservation is the discriminator and no EnvCell geometry fixture is needed. Sabotage-verified: restoring `body.Position = settle.Position` fails exactly the new test (1 failed / 4) and leaves the other three green — confirming both that the new test discriminates and that the old ones never could. Core suite 4,263 passed / 1 skipped / 0 failed, +1 for the new test. Still open and unverified, deliberately not claimed closed: whether the remote spawn-seed caller (LiveEntityNetworkUpdateController) hands in a body that carries a CellPosition at all. CommitTransitionPosition early-returns on a zero cell, so this fix is an inert no-op there and #276's remote half may survive. The C3c local first-entry caller is confirmed — it passes activation.Body.CellPosition.ObjCellId. Scoping detail in docs/research/2026-08-06-276-remainder-scoping.md. Co-Authored-By: Claude Opus 4.8 --- .../Physics/SpawnPlacementSettler.cs | 23 +++++++- .../Physics/SpawnPlacementSettlerTests.cs | 55 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/AcDream.Core/Physics/SpawnPlacementSettler.cs b/src/AcDream.Core/Physics/SpawnPlacementSettler.cs index 86263c87..519a6146 100644 --- a/src/AcDream.Core/Physics/SpawnPlacementSettler.cs +++ b/src/AcDream.Core/Physics/SpawnPlacementSettler.cs @@ -58,7 +58,28 @@ public static class SpawnPlacementSettler if (!settle.Ok || !settle.InContact) return false; - body.Position = settle.Position; + // #276: adopt the settle's RESOLVED cell, not just its position. + // Retail's CPhysicsObj::SetPositionInternal(CTransition const*) + // @0x00515330 commits both sphere_path.curr_pos.objcell_id and its + // frame, including EnvCells. + // + // A bare `body.Position = settle.Position` is sufficient only while + // the body is OUTDOOR: that setter mirrors the world delta into the + // landblock-local frame and LandDefs.AdjustToOutside then recomputes + // the 24 m cell index from it. It cannot do so for an EnvCell — an + // EnvCell id is not derivable from a position, so the mirror + // deliberately preserves it (PhysicsBody.cs, the `not (>= 1 and + // <= 0x40)` arm). settle.CellId is therefore the ONLY carrier of a + // cell identity across an indoor seam, and dropping it stranded the + // body in the placement cell until some later resolve corrected it. + // + // Identical guarded shape to the per-tick resolve writeback + // (RuntimeOrdinaryPhysicsUpdater): a transition reporting no cell + // falls back to the source cell rather than zeroing residency. + uint resolvedCellId = settle.CellId != 0 + ? settle.CellId + : cellId; + body.CommitTransitionPosition(resolvedCellId, settle.Position); PhysicsObjUpdate.CommitSetPositionTransition( body, settle.InContact, diff --git a/tests/AcDream.Core.Tests/Physics/SpawnPlacementSettlerTests.cs b/tests/AcDream.Core.Tests/Physics/SpawnPlacementSettlerTests.cs index a3e60a1c..2563d12e 100644 --- a/tests/AcDream.Core.Tests/Physics/SpawnPlacementSettlerTests.cs +++ b/tests/AcDream.Core.Tests/Physics/SpawnPlacementSettlerTests.cs @@ -75,6 +75,61 @@ public sealed class SpawnPlacementSettlerTests Assert.Equal(originalPosition, body.Position); } + /// + /// #276: the settle must adopt the transition's RESOLVED cell, not only + /// its position. Retail's + /// CPhysicsObj::SetPositionInternal(CTransition const*) + /// (0x00515330) commits both curr_pos.objcell_id and the + /// frame. + /// + /// + /// The body is seeded with an ENVCELL id over plain outdoor terrain, + /// which is what makes this discriminating. A bare + /// body.Position = settle.Position mirrors the world delta into + /// the local frame and lets AdjustToOutside recompute the cell — + /// but only for outdoor ids; the mirror deliberately PRESERVES an + /// EnvCell id, because one cannot be derived from a position. So the old + /// code leaves the stale EnvCell in place and only the resolved-cell + /// adoption reaches the outdoor cell the body physically settled in. + /// This is the "outdoor/EnvCell seam" half of #276, in the outward + /// direction. + /// + /// + [Fact] + public void SettleLeavingAnEnvCell_AdoptsTheTransitionsResolvedOutdoorCell() + { + const uint StaleEnvCell = Landblock | 0x0100u; + PhysicsEngine engine = BuildFlatEngine(); + PhysicsBody body = AirborneBody(new Vector3(12f, 12f, 0.25f)); + body.StageDormantCellFrame( + StaleEnvCell, + body.Position, + body.Position); + Assert.Equal(StaleEnvCell, body.CellPosition.ObjCellId); + + bool settled = SpawnPlacementSettler.TrySettle( + engine, + body, + body.Position, + Cell, + Radius, + Height, + ObjectInfoState.EdgeSlide, + movingEntityId: 0x70000001u, + static () => { }, + static () => { }); + + Assert.True(settled); + Assert.True(body.InContact); + + // The discriminating assertion: the body no longer claims the EnvCell + // it never physically occupied, and its cell is an OUTDOOR landcell + // (low word 1..0x40) of the same landblock it settled in. + Assert.NotEqual(StaleEnvCell, body.CellPosition.ObjCellId); + Assert.Equal(Landblock, body.CellPosition.ObjCellId & 0xFFFF0000u); + Assert.InRange(body.CellPosition.ObjCellId & 0xFFFFu, 1u, 0x40u); + } + private static bool TrySettle(PhysicsEngine engine, PhysicsBody body) => SpawnPlacementSettler.TrySettle( engine,