fix(physics #145): Slice 2a — canonicalize outdoor seed + re-derive cell index every tick

The wire local is LANDBLOCK-relative [0,192); the cell low word = floor(local/24),
so a consistent (cell,local) pair must keep them in lockstep. Two retail-faithful
corrections vs the first pass:
 - SnapToCell canonicalizes the OUTDOOR seed via AdjustToOutside (retail
   SetPositionInternal/adjust_to_outside @0x00504A40 — the #107 'never trust a
   server (cell,pos) pair' protection). Indoor seeds stay verbatim (BSP-validated).
 - SyncCellPositionDelta calls AdjustToOutside on EVERY delta, not just on 192 m
   crossings, so intra-landblock 24 m cell-index changes track (needed by Slice 3
   membership). Idempotent within a cell.
Tests rewritten to verify both (the earlier test paired an inconsistent cell+local).
Core 1527 passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-21 10:55:27 +02:00
parent afe495b9e6
commit 7928b445ab
2 changed files with 59 additions and 42 deletions

View file

@ -117,46 +117,46 @@ public sealed class PhysicsBody
/// <summary>
/// Placement: set the world position AND seed <see cref="CellPosition"/> from the
/// wire's (cell, local) directly — NO streaming center, NO delta. Stores the wire
/// values verbatim; the caller has already decoded the wire into a consistent
/// (cellId, cellLocal) pair. Caller (Slice 2b) supplies the wire (cell, localX/Y/Z)
/// from the inbound position update / teleport arrival.
/// wire's (cell, local) — NO streaming center, NO delta. For an OUTDOOR cell the
/// seed is canonicalized via <see cref="LandDefs.AdjustToOutside"/> (retail
/// <c>SetPositionInternal</c>/<c>adjust_to_outside</c> @0x00504A40): the cell index
/// is re-derived from the landblock-local position (low word = floor(local/24)) and
/// the origin wrapped into [0,192). This is the #107 protection — never trust a
/// server (cell, pos) pair without re-deriving the cell. Indoor EnvCell claims
/// (low word >= 0x100) are validated by the BSP/spawn-gate path — seeded verbatim.
/// Caller (Slice 2b) supplies the wire (cell, localX/Y/Z) from the inbound position
/// update / teleport arrival. <paramref name="worldPos"/> stays authoritative for
/// the world frame; canonicalizing the (cell, local) decomposition leaves the world
/// point unchanged for an in-range local.
/// </summary>
public void SnapToCell(uint cellId, Vector3 worldPos, Vector3 cellLocal)
{
_position = worldPos;
CellPosition = new Position(cellId, new CellFrame(cellLocal, Orientation));
uint cell = cellId;
Vector3 local = cellLocal;
if ((cellId & 0xFFFFu) is >= 1u and <= 0x40u)
LandDefs.AdjustToOutside(ref cell, ref local);
CellPosition = new Position(cell, new CellFrame(local, Orientation));
}
// Mirror a world-position translation into the cell-relative frame. Velocity is
// frame-invariant under translation, so the same delta applies to the local origin;
// AdjustToOutside re-wraps into [0,192) and bumps the cell on a landblock crossing.
// Only for a SEEDED OUTDOOR cell (player). Unseeded bodies (ObjCellId==0, e.g. remote
// entities) and indoor cells are left untouched here.
// We only invoke AdjustToOutside when the local coordinate leaves [0,192) — i.e. on
// an actual landblock-boundary crossing. Within the same landblock the cell id is left
// unchanged (the wire seed is authoritative for which cell index we occupy).
// frame-invariant under translation, so the same delta applies to the local origin.
// AdjustToOutside then recomputes the cell index from the local (intra-landblock 24 m
// cell crossings) AND wraps + bumps the landblock on a 192 m crossing — the outdoor
// membership + canonicalization in one call (get_outside_lcoord + lcoord_to_gid +
// [0,192) wrap). Idempotent within a cell. Runs only for a SEEDED OUTDOOR cell;
// unseeded bodies (ObjCellId==0, e.g. remote entities) and indoor cells are skipped.
private void SyncCellPositionDelta(Vector3 delta)
{
uint cell = CellPosition.ObjCellId;
if ((cell & 0xFFFFu) is not (>= 1u and <= 0x40u))
return;
Vector3 local = CellPosition.Frame.Origin + delta;
// Only call AdjustToOutside on a landblock-boundary crossing (local X or Y
// leaves [0, BlockLength)). Within the block the cell index is kept as-is.
if (local.X < 0f || local.X >= LandDefs.BlockLength ||
local.Y < 0f || local.Y >= LandDefs.BlockLength)
{
uint adjusted = cell;
if (LandDefs.AdjustToOutside(ref adjusted, ref local))
CellPosition = new Position(adjusted, new CellFrame(local, CellPosition.Frame.Orientation));
// else: map-edge (AdjustToOutside failed) — leave CellPosition unchanged.
// Slice 3 owns proper map-edge membership; this slice is behaviour-neutral.
}
else
{
CellPosition = new Position(cell, new CellFrame(local, CellPosition.Frame.Orientation));
}
uint adjusted = cell;
if (LandDefs.AdjustToOutside(ref adjusted, ref local))
CellPosition = new Position(adjusted, new CellFrame(local, CellPosition.Frame.Orientation));
// else: map edge (AdjustToOutside failed) — leave CellPosition unchanged.
// Slice 3 owns proper map-edge membership; this slice is behaviour-neutral.
}
/// <summary>Orientation quaternion (struct offsets 0x600x80 column matrix).</summary>