fix(phys): #106 follow-up — legacy Resolve returns full prefixed cell ids (teleport bare-id wedge)

The #106 live gate run was sabotaged by a pre-existing bug the corrective
ACE teleport exposed: PhysicsEngine.Resolve (the legacy Phase-D resolve,
still used by the teleport-arrival snap at GameWindow.cs:4869 and the
player-mode-entry snap at :11295) returned BARE low-word cell ids on
every computed exit (ComputeOutdoorCellId, bestCell.CellId & 0xFFFF,
nextCellIndex, enterCellIndex). The teleport committed 0x0000013F into
PlayerMovementController.CellId, and a bare indoor id wedges the entire
membership chain:

- GetCellStruct(0x0000013F) misses (cells are keyed full-prefix) -> no
  indoor wall BSP -> walk through walls;
- the b3ce505 #98 gate reads "indoor primary" -> outdoor object radial
  sweep skipped -> NO object collision anywhere in the world;
- BuildCellSetAndPickContaining early-returns an unresolvable id forever
  (block 0x0000 is a real far-NW map block) -> membership frozen;
- render root never resolves -> interiors draw empty when stepping in.

Probe evidence: probe-cell-106-gate.log has exactly 2 [cell-transit]
lines for the whole session, both reason=teleport — the second one
(0xA9B3003C -> 0x0000013F) is the wedge. This is the L.2e "player CellId
tracked as bare low byte" finding (2026-05-12) finally biting; prefix
survival until now was a race artifact — Resolve only preserved the full
id when the landblock had not streamed in yet (passthrough exit), which
is why login snaps usually came out prefixed.

Fix: capture the matched landblock's key in Resolve's containment loop
and return lbPrefix | (targetCellId & 0xFFFF) on the computed exit —
the same full-32-bit convention Resolve's own doc comment states for
its inputs, and what both production callers (player snaps) require.
The passthrough exits (no landblock / step-reject) still return the
caller's id unchanged.

Tests: Resolve_IndoorStay_ReturnsFullPrefixedCellId (the teleport
shape, red pre-fix) + Resolve_OutdoorStay_ReturnsFullPrefixedCellId;
Resolve_LeaveIndoorCell_TransitionsToOutdoor's unmasked
`CellId < 0x100` assertion codified the bare behavior — now masked +
asserts the prefix. Full suite: 294+218+420 green; Core 1371 green +
the same 4 pre-existing door/#99-era failures + 1 skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-09 23:34:30 +02:00
parent 7078264291
commit 23adc9c9df
3 changed files with 78 additions and 3 deletions

View file

@ -463,7 +463,14 @@ public sealed class PhysicsEngine
var candidatePos = currentPos + new Vector3(delta.X, 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
// bare-low-word returns wedged the membership chain whenever a caller
// committed them (the teleport-arrival snap wrote 0x0000013F: an
// unresolvable indoor id → no wall BSP, #98 gate reads "indoor
// primary" and kills the outdoor object sweep → no collision at all).
LandblockPhysics? physics = null;
uint lbPrefix = 0u;
foreach (var kvp in _landblocks)
{
var lb = kvp.Value;
@ -472,6 +479,7 @@ public sealed class PhysicsEngine
if (localX >= 0 && localX < 192f && localY >= 0 && localY < 192f)
{
physics = lb;
lbPrefix = kvp.Key & 0xFFFF0000u;
break;
}
}
@ -637,7 +645,7 @@ public sealed class PhysicsEngine
return new ResolveResult(
new Vector3(candidatePos.X, candidatePos.Y, targetZ),
targetCellId,
lbPrefix | (targetCellId & 0xFFFFu),
IsOnGround: true);
}