fix(physics): remote bodies slide on steep faces instead of freezing (#32)
A remote observed in acdream landed on a sloped roof and froze; the server slid
on, the gap passed AP-87's 4 m threshold, and the body snapped — the visible
blip. Live probe capture, two adjacent ticks 63 ms apart:
t=88420671 rsInContact=True rsOnWalkable=False rsIsOnGround=True
bodyCpNz=0.6097 floorZ=0.6642 steep=True gravity=True
vel=(2.146,2.264,-3.549)
t=88420734 contact=True onWalkable=True <- forced against the sweep
gravity=False <- cleared
velBeforeZero=(2.146,2.264,0.000)
moved=0.0000 <- and every tick after
The roof is 52.4 degrees against a 48.4 degree limit, so acdream's classifier
was CORRECT and was then overruled. Four independent links each froze the body
on their own: a per-tick force of Contact|OnWalkable, a per-tick velocity zero,
a Gravity clear at landing, and a landing edge testing IsOnGround
(= inContact || ...) instead of OnWalkable. The tick called
HandleAllCollisions alone — the tail of SetPositionInternal without its prefix.
Retail simulates remotes locally and derives these bits rather than asserting
them: CPhysics::UseTime @0x00509950 iterates the whole object table;
update_object @0x00515D10 gates only on parent/cell/FROZEN with no
is_player fork; SetPositionInternal @0x00515330 sets CONTACT from
contact_plane_valid @0x00515430 and ON_WALKABLE from contact_plane.N.z vs
floor_z @0x00515465-@0x0051548E before handle_all_collisions @0x005154FE;
set_on_walkable @0x00511310 fires HitGround @0x00511364 / LeaveGround
@0x00511346 edge-triggered with no ownership gate; calc_acceleration
@0x00510950 zeroes only when CONTACT && ON_WALKABLE && !Sledding @0x0051096B;
calc_friction @0x0050EE70 returns at its first line when ON_WALKABLE is clear.
acdream had copied retail's airborne no-op WITHOUT retail's local simulation.
The fix is mostly deletion: stop forging the transients, stop discarding the
authoritative velocity, stop clearing Gravity, and route the remote tick
through the same SetPositionInternal commit TickHidden and the local player
already use, with the landing edge derived from the sweep's own OnWalkable.
AP-87's threshold and conditions and InterpolationManager's node_fail_counter
snap-to-tail are deliberately untouched — this removes the CAUSE of the
divergence rather than weakening the backstop.
Cross-checked against ACE: its only creature-side VectorUpdate emitters are the
jump broadcast and spell projectiles, so integrating the wire velocity cannot
double-move a walking remote; and PhysicsGlobals.DefaultState already carries
Gravity, so deleting the manufactured State |= Gravity is safe.
Register: AP-81 narrowed (its GRAVITY half retired outright), AP-87 annotated,
AP-139 filed (the interpolation-queue clear on the landing edge), AP-140 filed
(the two routing gates select snap-vs-interpolate on walkability where retail
uses CONTACT — adjust_offset @0x00555D30 gates on transient_state & 1
@0x00555D52). AP-140's follow-up is deliberately shaped as "point the two gates
at Body.InContact", NOT "re-derive Airborne", which would perturb five writers
and collide with a pinned RemoteTeleportPlacementTests assertion.
Three gaps recorded in #32 rather than papered over: the new LeaveGround
dispatch is untested for chatter; a persistently !Ok transition can latch a
remote airborne; and — the visual-gate watch item — the deleted forge was a
blanket guarantee of Contact|OnWalkable, and contact_allows_move @0x00528dd0
silently refuses action animations without both, which is the literal root
cause of closed #270. Retail-correct on a steep face, a regression anywhere
else.
10 discriminating tests over a real PhysicsEngine landblock whose contact
normal Z is 0.61 against FloorZ 0.6642 — the live roof's exact relationship.
Suite 11,019 passed / 4 skipped / 0 failed. Includes the temporary
ACDREAM_PROBE_REMOTE_LANDING / ACDREAM_PROBE_REMOTE_SLIDE probe family that
produced the capture above; strip with the family.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f058dfc9f9
commit
204d0ae047
11 changed files with 3103 additions and 245 deletions
157
docs/ISSUES.md
157
docs/ISSUES.md
|
|
@ -9609,12 +9609,161 @@ other pieces that are either incomplete or unverified for remotes:
|
|||
contact-plane-derived slide, not the terrain-only approximation AD-10
|
||||
already flags as a divergence.
|
||||
|
||||
No fix has been applied for this observation — the investigation stopped
|
||||
here per project policy (no workarounds without approval) and instrumented a
|
||||
probe (`ACDREAM_PROBE_REMOTE_LANDING`, `PhysicsDiagnostics.cs`) instead. See
|
||||
The investigation stopped there per project policy and instrumented a probe
|
||||
(`ACDREAM_PROBE_REMOTE_LANDING`, then the `[remote-slide-*]` family in
|
||||
`PhysicsDiagnostics.cs`). See
|
||||
`docs/research/2026-08-04-remote-landing-investigation.md` for the companion
|
||||
Bug A (falling-animation-lingers) hypothesis set and the probe's decision
|
||||
table.
|
||||
table, and `docs/research/2026-08-04-bug-b-remote-slide-diagnosis.md` for the
|
||||
full four-link chain.
|
||||
|
||||
**Bug B FIXED 2026-08-04 (awaiting the user's two-client visual gate).** The
|
||||
live capture settled it: two adjacent ticks 63 ms apart on a 52.4-degree roof
|
||||
(contact-plane `Normal.Z` 0.6097 against `FloorZ` 0.6642) showed the sweep
|
||||
reporting `rsInContact=True rsOnWalkable=False`, and the tick then committing
|
||||
`contact=True onWalkable=True gravity=False velBeforeZero=(2.146,2.264,0.000)`
|
||||
and `moved=0.0000` on every tick afterwards. **acdream's classifier was
|
||||
correct and was being overruled.** Four independent writes did it, and all four
|
||||
are gone:
|
||||
|
||||
1. `RuntimeRemotePhysicsUpdater.Tick` asserted
|
||||
`TransientState |= Contact | OnWalkable` on every tick a remote was not
|
||||
flagged airborne. Retail writes CONTACT_TS from
|
||||
`collision_info.contact_plane_valid` (`CPhysicsObj::SetPositionInternal`
|
||||
@0x00515330, 0x00515430) and routes ON_WALKABLE_TS through
|
||||
`set_on_walkable` (@0x00511310) purely on
|
||||
`contact_plane.N.z >= PhysicsGlobals::floor_z` (0x00515465-0x0051548E).
|
||||
With both bits forced, `calc_acceleration` (@0x00510950) returned zero
|
||||
acceleration and `calc_friction` (@0x0050EE70) — whose entire body sits
|
||||
inside `transient_state & 2` — could not engage either.
|
||||
2. The same block zeroed `Body.Velocity`. Retail's `MoveOrTeleport`
|
||||
(@0x00516330) never reads or writes a remote's wire velocity at all; the
|
||||
zeroing discarded both the authoritative `0xF74E` vector and everything
|
||||
gravity had accumulated.
|
||||
3. The tick consumed only `ResolveResult.Position/CellId/IsOnGround` and called
|
||||
`HandleAllCollisions` bare — the TAIL of `SetPositionInternal` without its
|
||||
prefix. The sweep's own `InContact`/`OnWalkable` were never committed, and
|
||||
the landing edge was decided from `IsOnGround`, which is `inContact || ...`
|
||||
and is therefore TRUE on a steep contact. The tick now runs the full
|
||||
`PhysicsObjUpdate.CommitSetPositionTransition` sequence (contact prefix ->
|
||||
`set_on_walkable` edge -> `handle_all_collisions` @0x005154FE), gated on
|
||||
`Ok && candidateMoved` exactly like `PlayerMovementController` and retail
|
||||
`UpdateObjectInternal` (pc:283657).
|
||||
4. Both landing blocks cleared `PhysicsStateFlags.Gravity`. Retail never
|
||||
toggles GRAVITY_PS on a ground edge: the `CPhysicsObj` constructor seeds it
|
||||
(state `0x400C08` @0x00512508) and `set_state` (@0x00514DD0) assigns the
|
||||
description's state wholesale, post-processing only lighting/nodraw/hidden.
|
||||
The matching `State |= Gravity` in the VectorUpdate jump handler is deleted
|
||||
too, so the bit is now wire-owned end to end.
|
||||
|
||||
Consequential cleanups in the same change: `MovementManager::HitGround` now has
|
||||
the single retail source it has in the binary — the `set_on_walkable(1)`
|
||||
edge — so the packet-side landing block no longer dispatches its own
|
||||
(which would have double-fired the landing re-apply); and `RemoteMotion.Airborne`
|
||||
is now derived on the SetPositionInternal commit from the committed
|
||||
`Body.OnWalkable`, which is the project's one existing definition of the flag
|
||||
(`PlayerMovementController.IsAirborne`, the spawn settle,
|
||||
`RemoteTeleportPlacement`, `TickHidden`). Only the fact it derives FROM moved,
|
||||
from the hand-rolled `IsOnGround` test to the sweep's contact-plane result.
|
||||
|
||||
**Known follow-up, deliberately not changed here — now register row AP-140.**
|
||||
Because `Airborne` remains `!OnWalkable`, a remote sliding on a steep face is
|
||||
classified airborne, so an accepted grounded Position takes the
|
||||
`AirborneSnap`/landing-snap arm and hard-snaps to the server position at UP
|
||||
cadence instead of feeding the interpolation queue. Retail's predicate for that
|
||||
same decision is the CONTACT transient, not walkability
|
||||
(`InterpolationManager::adjust_offset` @0x00555D30 gates its whole body on
|
||||
`transient_state & 1` @0x00555D52), so a retail body in contact with a
|
||||
non-walkable face keeps interpolating. The two disagree on exactly one state,
|
||||
and this fix turned that state from unreachable (the deleted forge made every
|
||||
non-airborne remote walkable by construction) into ordinary — which is why it
|
||||
is now a filed divergence rather than an unremarked one. The bound is one UP
|
||||
interval to the authoritative position, and the between-packet motion is now a
|
||||
genuine local slide rather than a freeze, so the composite reads as continuous.
|
||||
|
||||
**The follow-up slice is re-shaped (2026-08-04 review): do NOT re-derive
|
||||
`Airborne` from CONTACT.** That was prepared and backed out here because it
|
||||
perturbs all five `Airborne = !Body.OnWalkable` writers and contradicts a
|
||||
pinned assertion in
|
||||
`RemoteTeleportPlacementTests.Apply_PendingGroundToSteepContact_RestoresSourceWalkabilityForFirstAcceleration`
|
||||
(`InContact: true, OnWalkable: false` → `Assert.True(remote.Airborne)`). The
|
||||
right change is smaller: point the **two routing gates**
|
||||
(`ApplyRemoteContactRouting`'s `if (remote.Airborne)` and `OnPosition`'s
|
||||
player-remote `if (rmState.Airborne)`) at `remote.Body.InContact` directly and
|
||||
leave the `Airborne` flag alone. That is the literal retail predicate at the
|
||||
one place the predicate is used, and it touches no existing test.
|
||||
|
||||
**AP-87's 4 m snap is deliberately untouched.** It is the #184
|
||||
invisible-but-solid backstop; this change removes the CAUSE of the divergence
|
||||
that made it fire, and the expected consequence is that it fires far less often.
|
||||
`InterpolationManager`'s `node_fail_counter > 3` stall snap is likewise
|
||||
untouched — the capture confirmed `producer=ap87-4m`, so the stall snap was
|
||||
never the producer here, and it is a faithful port of
|
||||
`InterpolationManager::UseTime` @0x00555f20.
|
||||
|
||||
Register: **AP-81** narrowed (its whole GRAVITY half retired), **AP-87**
|
||||
annotated, **AP-139** filed for the interpolation-queue clear the deleted
|
||||
landing block used to own, **AP-140** filed at the review for the
|
||||
walkability-vs-CONTACT routing predicate above. Coverage:
|
||||
`tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs`
|
||||
(10 tests over a synthetic constant-gradient ramp, each individually
|
||||
discriminated against a reverted fix).
|
||||
|
||||
**Recorded gaps from the 2026-08-04 Opus review — the fix PASSED; these are
|
||||
known, deliberately unfixed, and none of them was changed in the tightening
|
||||
pass that recorded them.**
|
||||
|
||||
- **The `LeaveGround` dispatch is new and unbounded.** The
|
||||
`previousOnWalkable && !finalOnWalkable` arm calls
|
||||
`MotionInterpreter.LeaveGround()`, which is a per-remote dispatch acdream
|
||||
never made before. It is retail-shaped (`CMotionInterp::LeaveGround`
|
||||
@0x00528B00 — creature gate @0x00528B36, Gravity-state gate, then the
|
||||
velocity install @0x00528B66), but note what it DOES: `GetLeaveGroundVelocity`
|
||||
(@0x005280c0) **replaces** the body's velocity with `get_state_velocity()`
|
||||
plus a jump Z, then `RemoveLinkAnimations` + `apply_current_movement`
|
||||
re-dispatches motion. Nothing bounds how often it can fire: the suite bounds
|
||||
the HitGround edge at exactly 1
|
||||
(`WalkableLandingStillLandsAndFiresTheGroundEdgeOnce`) but has no
|
||||
counterpart for LeaveGround, and noisy geometry — a
|
||||
walkable lip alternating with a steep face across the sweep — could chatter
|
||||
the edge and re-dispatch motion every tick. Compare the #270 lesson in
|
||||
`claude-memory/project_physics_collision_digest.md`: a per-stats-refresh
|
||||
`ReportExhaustion` re-dispatch produced 490 spurious stance re-queues in one
|
||||
session; **never re-add a per-tick re-apply.** A LeaveGround-count bound is
|
||||
the missing test.
|
||||
- **The primary watch item for the visual gate: action animations on remotes
|
||||
that fail to establish contact.** The deleted per-tick force was, in
|
||||
practice, a blanket guarantee that every non-airborne remote carried
|
||||
`Contact | OnWalkable`. `contact_allows_move` (@0x00528dd0) **silently
|
||||
refuses action animations** for a body lacking both — that is the exact root
|
||||
cause of closed issue **#270** ("monster attacks but the animation never
|
||||
fires", "stuck in cast pose"). Post-fix, any remote whose sweep fails to
|
||||
establish contact loses its attack/cast animations. On a 52.4-degree roof
|
||||
that is retail-correct and is the point of the change. Anywhere else it is
|
||||
the `feedback_latent_bug_masked_by_fallback` shape: the forge was masking
|
||||
contact failures, and removing it exposes every one of them. **Watch for
|
||||
missing attack/cast animations on ordinary flat ground during the two-client
|
||||
gate; if any appear, the bug is in contact establishment, not in this fix.**
|
||||
- **A remote whose transition keeps failing never re-derives `Airborne`.** The
|
||||
whole SetPositionInternal commit is gated on `Ok && candidateMoved`, and
|
||||
`rm.Airborne = !rm.Body.OnWalkable` is assigned only inside it, while the
|
||||
packet-side landing block no longer clears `Airborne` either. A remote whose
|
||||
transition keeps returning `!Ok` — the #116/#182 wedge class — therefore
|
||||
stays flagged airborne indefinitely: it keeps integrating gravity, and every
|
||||
grounded UP hard-snaps it back, producing sink-and-snap jitter at UP cadence.
|
||||
Bounded (each snap is to the authoritative position) and its reachability is
|
||||
unproven, but it is new with this change and did not exist while the forge
|
||||
ran.
|
||||
|
||||
Still open on this row: the two dependencies named above. **#173**'s remote
|
||||
collision-velocity reflect now genuinely runs on a steep contact (the old code
|
||||
passed `IsOnGround` as `nowOnWalkable`, which suppressed the reflect exactly
|
||||
where retail forces it), but its visual gate is still unrun. **AD-10**'s
|
||||
terrain-only slope projection still cannot see building geometry; it is now
|
||||
correctly gated OFF while the body is not on walkable ground, so a roof slide
|
||||
is driven by gravity plus the sweep's own plane projection rather than by that
|
||||
approximation. The retail-strict `step_up_slide`/`cliff_slide` audit that this
|
||||
row was originally filed for is unchanged.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue