fix(phys): #111 - a validated indoor claim is authoritative at the snap; stop the whole-landblock bestCell floor-pick from clobbering it

The [snap] apparatus (issue111-snap1.log) caught the mechanism live: ACE
restored a CLEAN pair (0xA9B40171, on-floor) which AdjustPosition validated -
and the legacy Resolve then committed 0xA9B4013F instead: its bestCell floor-
pick scans EVERY CellSurface in the landblock (123 at Holtburg) for "any floor
under this XY nearest currentZ" and breaks same-height ties by iteration
order. The wrong cell then fails containment on the first movement -> outdoor
demote inside the building -> the #111 transparent interior. This free-pick
also explains the earlier "committed verbatim" mystery (the winning tie
happened to echo the input pair) AND seeded the ACE poison loop: our outbound
heartbeats reported the clobbered cell, ACE persisted it, and the NEXT login
inherited it (this run's [spawn-adjust] rejecting 0xA9B4013F is exactly that
echo coming back).

Fix (retail SetPositionInternal shape): when AdjustPosition VALIDATED an
indoor claim, the cell choice is settled - the snap grounds Z onto the
validated cell's own floor (find_valid_position's settle role, :283426) and
returns; it never re-picks the cell from floor geometry. Claims whose cell has
no own floor surface under the XY (thresholds, stair lips) fall through to
the legacy path unchanged; mover-shaped calls (delta != 0, tests) untouched.

[snap] diagnostic kept (snaps only - one line per login/teleport).
Baseline: Core 1381+4 pre-existing #99 failures+1 skip; App/UI/Net green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-10 14:12:21 +02:00
parent 383af0ab5f
commit 5f1eb7c4b1

View file

@ -582,6 +582,13 @@ public sealed class PhysicsEngine
var candidatePos = currentPos + new Vector3(delta.X, delta.Y, 0f);
// #111 apparatus: one [snap] line per Resolve call (entry + teleport
// arrival only — low volume, permanent). The gate-3/4/5 runs committed
// ACE's restored pair VERBATIM through this method while every read
// path should have changed Z or cell — this line answers which branch
// actually ran. Remove or demote to env-gate once #111 closes.
bool snapDiag = (delta.X == 0f && delta.Y == 0f);
// Find the landblock this candidate position falls in.
// #106 follow-up (2026-06-09): capture its high-16 prefix — every
// computed cell id below is returned FULL (lbPrefix | low). The old
@ -605,11 +612,47 @@ public sealed class PhysicsEngine
}
if (physics is null)
{
if (snapDiag)
Console.WriteLine(System.FormattableString.Invariant(
$"[snap] claim=0x{cellId:X8} pos=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) branch=NO-LANDBLOCK (lbs={_landblocks.Count}) -> verbatim"));
return new ResolveResult(candidatePos, cellId, IsOnGround: false);
}
float localCandX = candidatePos.X - physics.WorldOffsetX;
float localCandY = candidatePos.Y - physics.WorldOffsetY;
// #111 (2026-06-10): a VALIDATED indoor claim is AUTHORITATIVE for the
// cell — retail SetPositionInternal commits the AdjustPosition cell and
// only settles Z (CheckPositionInternal → find_valid_position, :283426);
// it never re-picks the cell from floor geometry. The legacy bestCell
// floor-pick below scans EVERY CellSurface in the landblock (123 at
// Holtburg) and breaks same-height ties by iteration order — on a live
// login it clobbered ACE's clean, validated claim 0xA9B40171 with
// 0xA9B4013F (issue111-snap1.log), putting the player in a wrong cell
// → outdoor demote on first movement → transparent interior (#111).
// Snap shape only (zero delta): ground Z onto the validated claim's own
// floor when it has one under this XY; cells without their own floor
// surface here (thresholds, stair lips) fall through to the legacy path.
if (snapDiag && adjustedFound && (cellId & 0xFFFFu) >= 0x0100u)
{
CellSurface? claimSurface = null;
foreach (var c in physics.Cells)
{
if ((c.CellId & 0xFFFFu) == (cellId & 0xFFFFu)) { claimSurface = c; break; }
}
float? claimFloorZ = claimSurface?.SampleFloorZ(candidatePos.X, candidatePos.Y);
if (claimFloorZ is not null)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[snap] claim=0x{cellId:X8} pos=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) VALIDATED -> grounded to its floor z={claimFloorZ.Value:F3}"));
return new ResolveResult(
new Vector3(candidatePos.X, candidatePos.Y, claimFloorZ.Value),
lbPrefix | (cellId & 0xFFFFu),
IsOnGround: true);
}
}
// Check if the candidate position falls on any indoor cell floor.
// Pick the cell whose floor Z is closest to the entity's current Z.
CellSurface? bestCell = null;
@ -757,6 +800,9 @@ public sealed class PhysicsEngine
// Step-height enforcement: block upward movement that exceeds the limit.
float zDelta = targetZ - currentPos.Z;
if (snapDiag)
Console.WriteLine(System.FormattableString.Invariant(
$"[snap] claim=0x{cellId:X8} pos=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) cells={physics.Cells.Count} bestCell=0x{(bestCell?.CellId ?? 0u):X8} bestZ={(bestCellZ?.ToString("F3") ?? "none")} terrainZ={terrainZ:F3} indoor={currentlyIndoor} -> targetZ={targetZ:F3} targetCell=0x{(lbPrefix | (targetCellId & 0xFFFFu)):X8} stepReject={zDelta > stepUpHeight}"));
if (zDelta > stepUpHeight)
{
// Too steep to step up — reject horizontal movement.