From 5f1eb7c4b19338544611da1d5c2c3193822274fe Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 10 Jun 2026 14:12:21 +0200 Subject: [PATCH] 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 --- src/AcDream.Core/Physics/PhysicsEngine.cs | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 24edafac..437cb422 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -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.