fix(physics): #42 skip self in FindObjCollisions — airborne XY drift
Root cause confirmed via two-run diagnostic and the named-retail decomp:
the airborne sweep was colliding with the moving entity's OWN ShadowEntry
because FindObjCollisions had no self-skip filter. Live entities (local
player, remotes) register a Cylinder in ShadowObjectRegistry on spawn
(GameWindow.cs:2545) and UpdatePosition tracks its world position each
tick, so the moving sphere's own cylinder is always at the body's
position. Without a gate, CylinderCollision sees the sphere overlapping
its own cylinder volume and slides the sphere ~1m horizontally on every
frame the path produces non-zero motion.
Why grounded mostly hides it and airborne exposes it:
- Stationary grounded → numSteps=0, TransitionalInsert never runs.
- Walking grounded → push fires but motion escapes the cyl radius and
the deflection blends into normal motion.
- Stationary airborne (jump) → pure +Z motion; the cyl push is the
only horizontal contribution and manifests as a clean ~1m drift.
Run-2 evidence (launch-42-r2.log) — 152 [SWEEP-OBJ] events, every one
with type=Cylinder, gfxObj=0x02000001 (humanoid setup), R=0.679,
H=1.835, at obj.Position EXACTLY matching the body's pre.Position. Run
1 had already ruled out H1 (cpN=(0,0,1) flat, no slope projection).
Retail does the same skip — CObjCell::find_obj_collisions at
named-retail acclient_2013_pseudo_c.txt:308931:
if ((physobj->parent == 0 && physobj != arg2->object_info.object))
`arg2->object_info.object` is the OBJECTINFO::object self-pointer set
by OBJECTINFO::init at acclient_2013_pseudo_c.txt:274435. Our port
mirrors this with an EntityId-based filter:
- ObjectInfo gains a SelfEntityId field (default 0 = no filter).
- ResolveWithTransition gains an optional `uint movingEntityId = 0`
parameter that sets it.
- FindObjCollisions skips entries whose EntityId matches
SelfEntityId when the id is non-zero.
- PlayerMovementController gains a LocalEntityId property; GameWindow
refreshes it per-tick from `_entitiesByServerGuid[_playerServerGuid]`.
- GameWindow's airborne-remote ResolveWithTransition call site passes
`movingEntityId: kv.Key` (kv.Key is the local entity id keying
`_animatedEntities`, same id used at the spawn-time
ShadowObjects.Register).
Default 0 keeps tests and one-shot callers (no registered ShadowEntry)
working unchanged.
Lock-the-fix unit test:
`PhysicsEngineTests.ResolveWithTransition_SelfShadowEntry_NotPushedWhenIdMatches`
registers a humanoid Cylinder at the body's exact position (matching
GameWindow's spawn pattern), then asserts that:
- movingEntityId=0 (control) → unfiltered XY drift > 0.5m
- movingEntityId=registered id (fix) → XY drift ≈ 0
Diagnostic wiring (a36369d + this commit's [SWEEP-OBJ] addition) stays
in tree, env-var gated (ACDREAM_AIRBORNE_DIAG=1) so it produces no
output in normal use but lets us verify the fix on the live client and
debug future regressions.
Build: green. Tests: 355 pass, 6 fail (all pre-existing per the handoff
prompt — verified by stashing this change; the BSPStepUp C3 failure is
on the prior commit too).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a36369d8ca
commit
ec59a08db5
5 changed files with 179 additions and 3 deletions
|
|
@ -130,6 +130,17 @@ public sealed class PlayerMovementController
|
|||
public Vector3 Position => _body.Position;
|
||||
public uint CellId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local-player entity id used to skip self-collision in the
|
||||
/// airborne sweep. GameWindow updates this whenever the local
|
||||
/// `+Acdream` entity (re)spawns. Default 0 = no filter (matches
|
||||
/// retail's CObjCell::find_obj_collisions self-skip when the
|
||||
/// caller's OBJECTINFO::object pointer is null). Without this the
|
||||
/// sweep collides with its own ShadowEntry registered at
|
||||
/// GameWindow.cs:2545 — see #42.
|
||||
/// </summary>
|
||||
public uint LocalEntityId { get; set; }
|
||||
|
||||
public bool IsAirborne => !_body.OnWalkable;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -558,7 +569,12 @@ public sealed class PlayerMovementController
|
|||
// through other non-PK players, which is retail's default for
|
||||
// ACE's character creation defaults too).
|
||||
moverFlags: AcDream.Core.Physics.ObjectInfoState.IsPlayer
|
||||
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide);
|
||||
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide,
|
||||
// Fix #42: skip self in FindObjCollisions. Wired by GameWindow
|
||||
// when the local player entity spawns (or stays 0 in tests, in
|
||||
// which case there's no registered ShadowEntry to collide with
|
||||
// anyway).
|
||||
movingEntityId: LocalEntityId);
|
||||
|
||||
// L.4-diag (2026-04-30): trace position transitions so we can see
|
||||
// whether the body is actually moving frame-to-frame on the steep
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue