fix(physics): #137 Task 3 — port obstruction_ethereal verbatim; retire AD-7 shim

Retail CPhysicsObj::FindObjCollisions (0x0050f050) only instant-skips when
BOTH ETHEREAL_PS (0x4) AND IGNORE_COLLISIONS_PS (0x10) are set (pc:276782).
ETHEREAL-alone sets sphere_path.obstruction_ethereal=1 (pc:276806) and
continues to the shape dispatch. BSPTREE::find_collisions (0x0053a496) routes
Path 1 (sphere_intersects_solid) when the flag is set (pc:323742): the open
door has no solid leaf at the doorway, so the test returns OK → player passes
through. CEnvCell::find_env_collisions (0x0052c144) clears the flag first so
ENV walls are never weakened (pc:309580, "D5 clear").

Changes:
- CollisionExemption.ShouldSkip: require BOTH bits for Gate-1 early-out
  (previously ETHEREAL alone returned true — the AD-7 shim). Divergence
  register row AD-7 deleted.
- SpherePath: add ObstructionEthereal field (mirrors retail
  SPHEREPATH.obstruction_ethereal).
- FindObjCollisionsInternal loop: set sp.ObstructionEthereal=(target&0x4)!=0
  before shape dispatch; clear it after (per-object clear pc:276989).
  Also clear at the null-BSP continue site to keep flag clean.
- FindEnvCollisions: clear sp.ObstructionEthereal=false at top (D5 clear
  pc:309580) — ENV cell walls are always solid.
- BSPQuery.FindCollisions Path 1: change `obj.Ethereal` (ObjectInfo.Ethereal,
  always false — dead code) to `path.ObstructionEthereal`. Gate now correctly
  mirrors retail pc:323742: PLACEMENT_INSERT || obstruction_ethereal.

Consume site change (BSPQuery.cs before/after):
  BEFORE: if (path.InsertType == InsertType.Placement || obj.Ethereal)
  AFTER:  if (path.InsertType == InsertType.Placement || path.ObstructionEthereal)
Mirrors retail pc:323742 exactly. obj.Ethereal was dead code (ObjectInfo.Ethereal
is never set true anywhere); the correct flag is SpherePath.ObstructionEthereal.

Tests: 1580 pass / 0 fail / 2 skip (was 1576/0/2 + 4 new in ObstructionEtherealTests.
CellarUp, CornerFlood, DoorCollision, HouseExitWalk all green — no wall regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-24 19:20:02 +02:00
parent 78e5758185
commit 3361a8d776
7 changed files with 382 additions and 30 deletions

View file

@ -1712,9 +1712,20 @@ public static class BSPQuery
Vector3 L2W(Vector3 v) => Vector3.Transform(v, localToWorld);
// ----------------------------------------------------------------
// Path 1: Placement or Ethereal → sphere_intersects_solid
// Path 1: Placement or obstruction_ethereal → sphere_intersects_solid
// ----------------------------------------------------------------
if (path.InsertType == InsertType.Placement || obj.Ethereal)
// Retail BSPTREE::find_collisions pc:323742 / 0x0053a496:
// `if (insert_type == PLACEMENT_INSERT || obstruction_ethereal != 0)`
// obstruction_ethereal is a per-object flag set by FindObjCollisions
// (pc:276806) when the target is ETHEREAL-alone (ETHEREAL_PS=0x4 set,
// IGNORE_COLLISIONS_PS=0x10 NOT set). When set, Path 1 fires instead of
// the blocking paths: sphere_intersects_solid only returns COLLIDED if
// the player sphere is inside a BSP solid leaf. For an open door (no
// solid wall at the doorway), no solid leaf intersects → OK → passable.
// Previously obj.Ethereal (ObjectInfo.Ethereal, always false) was here —
// the correct flag is SpherePath.ObstructionEthereal, set per target.
// Task 3 (2026-06-24) / retail divergence register AD-7 retired.
if (path.InsertType == InsertType.Placement || path.ObstructionEthereal)
{
// BR-7 / A6.P4 (2026-06-11): retail weakens the solid test
// against BUILDING shells while the path engages interior cells