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

@ -5482,6 +5482,27 @@ outdoors at the angle that previously erased it.
# Recently closed
## #150 — Open doors still blocked at the threshold (ethereal target not skipped in the step-down pass)
**Status:** DONE (2026-06-25) — after the retail collision sweep, an OPEN door
(ETHEREAL_PS 0x4 set on Use) still stopped the player with a small residual block at
the sill ("can-sized cylinder on the ground threshold"), even though the door swings
open visually. Root: 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 (Layer-1 Path-1 + the Layer-2
override) but the **step-down pass had no escape** and its `Collided` result survived
as the threshold block. Retail's `CPhysicsObj::FindObjCollisions` (pc:276795-276806)
SKIPS an ethereal target entirely 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 animation is purely visual).
Fix: port that exact branch — `if ((state & 0x4) || (mover.Ethereal && (state & 0x1)
== 0)) && sp.StepDown → continue`. `TransitionTypes.cs` per-object loop. Live-verified
(user "Yes it works"; the door now blocks 0× while open [0x1000C], still blocks while
closed [0x10008]; pre-fix it blocked 217× while open). Core suite green (1595/0). The
earlier "animate the door collision" theory was WRONG and dropped — the user correctly
noted that if collision followed the swung panel you'd bump the panel in its open
position, which retail does not do. Likely also advances the door half of #137.
## #149 — BSP-less landblock statics (torches/braziers/lamp-posts) were walk-through
**Status:** DONE (2026-06-25, `4cf6eeb`) — town props placed as landblock stabs

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