fix(physics): #150 — skip ethereal targets in the step-down pass (open doors fully passable)

An OPEN door (ETHEREAL_PS 0x4 set on Use) still stopped the player with a
residual threshold block after the collision sweep, despite swinging open
visually.

The resolver runs two collision passes per step: the main sweep and a
step-down (foot-sphere "is there floor?") sub-pass. acdream tested the
ethereal door in BOTH; the main pass cleared it (BSPQuery Path 1 + the
Layer-2 override) but the step-down pass had no escape, leaving a Collided
result at the sill -- the "can-sized cylinder on the ground threshold".

Retail's CPhysicsObj::FindObjCollisions (pc:276795-276806) SKIPS an ethereal
target when sphere_path.step_down != 0 -- it only tests it in the main pass.
So an open door is fully passable everywhere; the swung panel's position is
irrelevant (ethereal = no collision; the swing animation is purely visual).

Port that branch verbatim: an ethereal-for-this-test target (target state &
0x4, OR mover-ethereal vs a non-static target) is `continue`d when
sp.StepDown is set.

Live-verified (user confirmed): the door now blocks 0x while open (0x1000C),
still blocks while closed (0x10008); pre-fix it blocked 217x while open.
Core suite green (1595/0).

(An earlier "animate the door collision" theory was wrong and dropped -- if
collision tracked the swung panel you'd bump the panel in its open position,
which retail does not do. The user caught it.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-25 12:20:54 +02:00
parent 2ac7ea776f
commit aa7c0b5c31
2 changed files with 47 additions and 9 deletions

View file

@ -2599,15 +2599,32 @@ public sealed class Transition
if (CollisionExemption.ShouldSkip(obj.State, obj.Flags, ObjectInfo.State))
continue;
// Task 3 (2026-06-24): set obstruction_ethereal on SpherePath before
// the shape dispatch. Retail: CPhysicsObj::FindObjCollisions pc:276806 /
// 0x0050f0c9 — `ebx->sphere_path.obstruction_ethereal = var_c` where
// var_c=1 when the target has ETHEREAL_PS (0x4). The flag is consumed by
// BSPQuery Path 1 (BSPTREE::find_collisions pc:323742) to route through
// sphere_intersects_solid instead of the blocking paths, making the player
// passable through the open door's BSP (no solid leaf at the opening).
// Cleared after each per-object test below (retail pc:276989 / 0x0050f31e).
sp.ObstructionEthereal = (obj.State & 0x4u) != 0;
// Retail CPhysicsObj::FindObjCollisions ethereal branch
// (pc:276795-276806 / 0x0050f0a2-0x0050f0c9). A target counts as
// "ethereal for this test" when it has ETHEREAL_PS (0x4) OR the mover
// is ethereal against a NON-static target ((state & 1) == 0):
//
// if ((state & 4) || (mover.ethereal && (state & 1) == 0)) {
// var_c = 1;
// if (sphere_path.step_down == 0) goto do_test; // main pass
// // step_down != 0 → fall through = SKIP this target entirely
// } else { var_c = 0; do_test: obstruction_ethereal = var_c; ...test... }
//
// The step-down SKIP (pc:276799 — no `goto` when step_down != 0) is what
// makes an OPEN DOOR fully passable: the main pass tests it with
// obstruction_ethereal=1 (BSPQuery Path 1 + the Layer-2 override below
// force it to OK), and the foot-sphere step-down sub-pass NEVER TESTS IT
// at all. acdream previously tested ethereal targets in BOTH passes with
// no step-down escape, so the step-down pass left a residual threshold
// block ("can" at the open door's sill) that retail does not have
// (#137 / live capture 2026-06-25: 217 blocks all while door state=0x1000C).
// obstruction_ethereal (= retail var_c) is consumed by BSPQuery Path 1
// (BSPTREE::find_collisions pc:323742); cleared after each test (pc:276989).
bool etherealForTest = (obj.State & 0x4u) != 0
|| (ObjectInfo.Ethereal && (obj.State & 0x1u) == 0);
if (etherealForTest && sp.StepDown)
continue; // retail pc:276799 — ethereal target not tested in step-down
sp.ObstructionEthereal = etherealForTest;
// L.2a slice 3 (2026-05-12): snapshot collision-normal state so
// we can tell whether THIS object's BSP/CylSphere test produced a