feat(#184): Slice 2b — unify the remote player/NPC fork (players collide faithfully)
Collapse the two-path fork in RemotePhysicsUpdater.Tick: the former Path A (grounded PLAYER remotes advanced by the interp catch-up with ResolveWithTransition deliberately OMITTED, per the now-retired issue-#40 premise) is gone. Every remote -- player and NPC -- now runs the SAME per-tick catch-up + sweep + shadow-follows-resolved. Retail's UpdateObjectInternal (0x005156b0) has no player/remote fork; this is the faithful shape. Player remotes now get terrain-Z snap (no slope staircase), wall collision, and MONSTER collision -- previously (Path A) they skipped ALL collision. RETAIL PvP (adversarial review caught this): two non-PK players WALK THROUGH each other in AC (you can stand inside another non-PK player) -- they do NOT de-overlap. The remote-player mover now carries IsPlayer|EdgeSlide (mirroring the LOCAL player at PlayerMovementController), so CollisionExemption's PvP block exempts a non-PK pair, exactly as retail sets IsPlayer on every object's own transition (OBJECTINFO::init 0x0050cf30) and FindObjCollisions (pc:276812) exempts it. The first 2b draft passed bare EdgeSlide and de-overlapped players (MORE solid than retail); the 3-lens review flagged it. PK/PKLite/Impenetrable are not plumbed onto the remote mover yet -- the same M1.5 gap the local player carries (TS-23, extended). #40 proven dead in code (before the gate): PlayerVsMonster_DeOverlapsAndAbsorbsTheStallBlip drives the real ComputeOffset -> InterpolationManager catch-up (incl. the fail_count blip-to-tail) for a player mover converging on a monster and asserts de-overlap + maxSpike<0.30 -- the sweep absorbs the stall-blip. ConvergingPlayers_WalkThroughEachOther proves the PvP exemption (non-PK players pass through). Path B already ran the #40-feared config stably; #40 (May 2026) predates the CSphere/#137/#170/#171 rebuild. Placement (HIGH, review finding 3): the player UP routing gains the SAME placement-snap backstop Slice 1 gave NPCs (AP-87). Without it a UM-first player (RemoteMotion seeded to the spawn pos, then a first UP in a different cell) would sweep from a stale cell -> garbage -> the digest's invisible/misplaced-player bug. The 4 m bodyToTarget guard + !willBeDrTicked + dist>96 snap; near placed corrections still enqueue for smooth catch-up. Also seed Body.Position=worldPos at UP-handler RemoteMotion creation (mirrors the UM handler :5176) for the UP-first case. Coupled shadow edits (research finding 9): RETIRED the players-only raw-worldPos shadow sync -- now that players run the sweep + shadow-follows-resolved, the raw sync would re-snap a packed player's shadow into overlap each UP. Player shadows follow the RESOLVED body via the DR-tick loop + a new player UP-branch-tail SyncRemoteShadowToBody. Surviving player/NPC split (AP-88): the omega -- grounded PLAYERS keep the ObservedOmega-or-seqOmega world-frame (Concatenate) fallback ("rectangle when running circles"); NPCs + airborne keep ObservedOmega-only body-frame (Multiply). They commute for an upright body + yaw omega, so the fork is faithful. Register: TS-23 extended (remote-player mover PK gap); AP-86 updated (raw sync retired for players too, Where column fixed); AP-88 added (omega fork + eval-order note). Tests: Core 2623 / App 741 green, 0 warnings. 3-lens adversarial review + per-finding verification (10 agents); all 6 confirmed findings addressed (2 substantive: PvP mover flags + player placement-snap; 4 doc/cosmetic). VISUAL GATE (acceptance test) owed by the user -- NOTE the corrected expectation: (a) a player remote on a hill -- no slope staircase; (b) two packed player remotes -- they WALK THROUGH each other (retail PvP), NOT de-overlap (this corrects the design's original "players de-overlap" gate); (c) a player remote cannot stand inside a MONSTER (new: player-vs-monster collision); (d) remote walk/run/jump/land/turn UNCHANGED. Handoff: docs/research/2026-07-07-184-slice2-unify-extract-handoff.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1ac56cce9
commit
ddb5a96799
4 changed files with 303 additions and 309 deletions
|
|
@ -5654,21 +5654,17 @@ public sealed class GameWindow : IDisposable
|
|||
// (its body is the simulator, not a target). Retail does the
|
||||
// equivalent via SetPosition → change_cell → AddShadowObject
|
||||
// (acclient_2013_pseudo_c.txt:284276 / 281200 / 282862).
|
||||
if (update.Guid != _playerServerGuid && IsPlayerGuid(update.Guid))
|
||||
{
|
||||
// Remote PLAYERS only (#184): a player remote (Path A, no sweep) tracks
|
||||
// the server position closely, so syncing its shadow to the raw pos is
|
||||
// fine. NPC shadows are synced to the RESOLVED body instead — by the
|
||||
// NPC-branch tail below and the per-tick DR loop (SyncRemoteShadowToBody)
|
||||
// — because an NPC's body is de-overlapped BEHIND its raw server pos;
|
||||
// writing the raw pos here would snap the shadow into overlap for a frame
|
||||
// each UP and fight the de-overlap (the #184 review's :5669 finding).
|
||||
// BR-7: the wire position's full cell id seeds the re-flood
|
||||
// (retail SetPosition → calc_cross_cells from m_position).
|
||||
_physicsEngine.ShadowObjects.UpdatePosition(
|
||||
entity.Id, worldPos, rot, origin.X, origin.Y, p.LandblockId,
|
||||
seedCellId: p.LandblockId);
|
||||
}
|
||||
// #184 Slice 2b: the former players-only RAW-pos shadow sync is RETIRED.
|
||||
// It was a Slice-1 stopgap while grounded player remotes (old Path A) skipped
|
||||
// the sweep and tracked the server position closely. Now that Slice 2b runs
|
||||
// the SAME per-tick sweep + shadow-follows-resolved for players, writing the
|
||||
// raw (overlapping) server pos here would re-snap a packed player's shadow
|
||||
// into overlap once per UP and fight the in-tick de-overlap (research
|
||||
// finding 9). Player shadows now follow the RESOLVED body — via the DR-tick
|
||||
// loop (SyncRemoteShadowToBody, movement-gated) and the player UP-branch tail
|
||||
// below (first-UP / no-Sequencer case), exactly like NPCs. Local-player
|
||||
// broadphase still tests an up-to-date remote shadow; it is just the resolved
|
||||
// body now, not the raw wire pos.
|
||||
|
||||
// Track remote-entity motion for stop detection. Only record the
|
||||
// timestamp when position moved MEANINGFULLY (> 0.05m). Updates
|
||||
|
|
@ -5710,6 +5706,18 @@ public sealed class GameWindow : IDisposable
|
|||
// Hard-snap orientation on first spawn so the per-tick
|
||||
// slerp doesn't visibly rotate from Identity to truth.
|
||||
rmState.Body.Orientation = rot;
|
||||
// #184 Slice 2b: PLACE the body at the server position on creation,
|
||||
// mirroring the UM handler's seed (:5176 `Body.Position =
|
||||
// entity.Position`). A UP-first RemoteMotion (created here before any
|
||||
// UM) was left at the default (0,0,0). Path A never swept, so that
|
||||
// stale origin was harmless — it caught up gradually. Now that Slice
|
||||
// 2b runs the sweep for grounded PLAYERS too, an unplaced body would
|
||||
// sweep from (0,0,0) in the server cell that does not contain it →
|
||||
// garbage resolved pos → the digest's INVISIBLE/misplaced-body bug.
|
||||
// Seeding here is the root-cause fix (the UP creation path should
|
||||
// seed exactly like the UM path); worldPos == entity.Position (the
|
||||
// unconditional snap at the top of this handler already ran).
|
||||
rmState.Body.Position = worldPos;
|
||||
}
|
||||
|
||||
// L.3 M2 (2026-05-05): retail-faithful MoveOrTeleport routing for
|
||||
|
|
@ -5819,25 +5827,45 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
// ── GROUNDED ROUTING (CPhysicsObj::MoveOrTeleport) ────────────
|
||||
const float MaxPhysicsDistance = 96f;
|
||||
const float MaxPhysicsDistance = 96f; // retail player_distance far-snap
|
||||
const float BodySnapThreshold = 4f; // large correction / teleport / unplaced -> snap
|
||||
var localPlayerPos = _playerController?.Position ?? System.Numerics.Vector3.Zero;
|
||||
float dist = System.Numerics.Vector3.Distance(worldPos, localPlayerPos);
|
||||
// #184 Slice 2b: the player UP routing gains the SAME placement-snap
|
||||
// backstop the NPC routing got in Slice 1 (AP-87). Now that grounded
|
||||
// PLAYER remotes run the sweep, an unplaced / stale-cell body — a
|
||||
// UM-first RemoteMotion seeded to the spawn pos (:5176) then a first UP
|
||||
// in a DIFFERENT cell, which the UP-creation seed (:5720) does NOT cover
|
||||
// — would enqueue and the per-tick sweep would run from a cell that does
|
||||
// not contain the body -> garbage resolved pos -> the digest's
|
||||
// INVISIBLE/misplaced player. The 4 m bodyToTarget guard is the
|
||||
// LOAD-BEARING backstop (AP-87; firstUp via LastServerPosTime is a poor
|
||||
// signal for players — it is already set by the VEL_DIAG block above);
|
||||
// !willBeDrTicked snaps a no-Sequencer player whose queue nothing would
|
||||
// consume; dist>96 is retail's far-snap. Placed + near corrections still
|
||||
// enqueue for the smooth catch-up.
|
||||
float bodyToTarget = System.Numerics.Vector3.Distance(
|
||||
rmState.Body.Position, worldPos);
|
||||
bool willBeDrTicked =
|
||||
_animatedEntities.TryGetValue(entity.Id, out var aeDrPl)
|
||||
&& aeDrPl.Sequencer is not null;
|
||||
|
||||
if (dist > MaxPhysicsDistance)
|
||||
if (dist > MaxPhysicsDistance || !willBeDrTicked
|
||||
|| bodyToTarget > BodySnapThreshold)
|
||||
{
|
||||
// Beyond view bubble: SetPositionSimple slide-snap. Clear queue.
|
||||
// Beyond view bubble / large correction / unplaced body:
|
||||
// SetPositionSimple slide-snap. Clear queue.
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Within view bubble: enqueue waypoint for adjust_offset to walk to.
|
||||
// The per-frame TickAnimations player-remote path drives the
|
||||
// actual body advancement via InterpolationManager.AdjustOffset.
|
||||
// Pass body's current position so the InterpolationManager can
|
||||
// detect a far-distance enqueue (>100 m from body) and pre-arm
|
||||
// an immediate blip — avoids body drifting visibly toward a
|
||||
// far waypoint instead of teleporting to it.
|
||||
// Within view bubble, placed + near: enqueue waypoint for
|
||||
// adjust_offset to walk to. The per-frame TickAnimations player-
|
||||
// remote path drives the actual body advancement via
|
||||
// InterpolationManager.AdjustOffset. Pass body's current position so
|
||||
// the InterpolationManager can detect a far-distance enqueue (>100 m
|
||||
// from body) and pre-arm an immediate blip.
|
||||
float headingFromQuat = ExtractYawFromQuaternion(rot);
|
||||
rmState.Interp.Enqueue(
|
||||
worldPos,
|
||||
|
|
@ -5871,6 +5899,18 @@ public sealed class GameWindow : IDisposable
|
|||
// the near-enqueue branch this prevents a 1-frame teleport-then-
|
||||
// yank-back rubber-band as TickAnimations chases worldPos via the
|
||||
// queue.
|
||||
//
|
||||
// #184 Slice 2b: sync the player-remote shadow to the RESOLVED/placed
|
||||
// body (mirrors the NPC UP-branch tail). Now that grounded players run
|
||||
// the DR-tick sweep + shadow-follows-resolved, the retired raw-pos sync
|
||||
// (top of this handler) would have re-snapped the shadow into overlap
|
||||
// each UP; this keeps collision == render for the first UP (before any
|
||||
// DR tick) and for no-Sequencer players. rmState.CellId is the server
|
||||
// cell adopted above (:5735). The per-tick loop keeps it current
|
||||
// between UPs (movement-gated).
|
||||
if (rmState.CellId != 0)
|
||||
_remotePhysicsUpdater.SyncRemoteShadowToBody(
|
||||
entity.Id, rmState, _liveCenterX, _liveCenterY);
|
||||
entity.SetPosition(rmState.Body.Position);
|
||||
return;
|
||||
}
|
||||
|
|
@ -6105,8 +6145,8 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
// #184: sync the NPC shadow to the resolved/placed body (NOT the raw
|
||||
// server pos — the :5669 sync is players-only now) so collision ==
|
||||
// render and the de-overlap isn't snapped away for a frame each UP.
|
||||
// server pos — the raw-pos sync was RETIRED for players too in Slice 2b)
|
||||
// so collision == render and the de-overlap isn't snapped away each UP.
|
||||
// Covers the first UP (before any DR tick) and no-Sequencer NPCs (which
|
||||
// the per-tick loop skips). The per-tick loop keeps it current between
|
||||
// UPs, movement-gated. rmState.CellId is the server cell adopted above.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue