fix(physics): Task 3 follow-up — obstruction_ethereal consume for Cylinder+Sphere shapes

Retail oracle greps confirmed:
- CSphere::intersects_sphere @ 0x00537ae4 (pc:321692): the ethereal branch
  is `void __thiscall` — all paths return void (no COLLIDED). The function
  performs a proximity check only; no blocking result is produced.
- CCylSphere::intersects_sphere @ 0x0053b4a0 (pc:324573): same void-return
  pattern — ethereal branch calls collides_with_sphere (check only, no slide),
  all returns are void = passable.

Change: added `if (sp.ObstructionEthereal) return TransitionState.OK` at the
top of SphereCollision and CylinderCollision in TransitionTypes.cs, mirroring
the void-return semantics of both retail functions. The existing per-object
clear at pc:276989 (line 2837) still fires after the early OK return.

Before this fix: an ethereal-alone NPC/ghost with a Cylinder or Sphere shadow
shape would BLOCK the player (regression introduced when Task 3 made ETHEREAL-
alone fall through ShouldSkip instead of instant-skipping). After: all three
shape types — BSP (via BSPQuery Path 1), Sphere, and Cylinder — correctly pass
through when obstruction_ethereal is set.

Tests: added 4 tests to ObstructionEtherealTests.cs verifying:
- Ethereal Cylinder → passable (sweep passes through, no CollisionNormalValid)
- Ethereal Sphere → passable (same)
- Non-ethereal Cylinder → still blocks (regression guard)
- Non-ethereal Sphere → still blocks (regression guard)
Full Core suite: 1584 pass, 0 fail, 2 skip (pre-existing dat skips).

Pseudocode doc updated with confirmed cyl/sphere ethereal contracts and the
complete set/clear/consume flow summary.

Retail refs:
- CSphere::intersects_sphere @ 0x00537ae4 / acclient_2013_pseudo_c.txt:321692
- CCylSphere::intersects_sphere @ 0x0053b4a0 / acclient_2013_pseudo_c.txt:324573

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

View file

@ -150,15 +150,43 @@ else:
...
```
**acdream scope note:** `CylinderCollision` in `TransitionTypes.cs` does NOT implement
this gate. That is a separate task. Cylinder-type shadow entries for ETHEREAL objects
would not be blocked by the BSP path (BSP is separate), so the ETHEREAL Cyl gate
primarily matters for objects that are purely Cylinder (no BSP) — which is less common
than BSP objects. Task 3 scope covers BSP.
**acdream implementation (Task 3 follow-up, 2026-06-24):** `CylinderCollision` in
`TransitionTypes.cs` now implements this gate as `if (sp.ObstructionEthereal) return OK`
at the top of the method — mirroring exactly how `SphereCollision` (consume site 1) is
handled. The retail function is `void __thiscall`, so all returns in the ethereal branch
are void (= OK). An ethereal Cylinder (e.g. an NPC ghost) is now fully passable.
---
## Summary: set / clear / consume flow for Task 3
## 3b. Consume site 1 — `CSphere::intersects_sphere` (pc:321692) — full contract
The full decomp at pc:321692 / 0x537ae4 confirms:
```
// pc:321692:
if (obstruction_ethereal != 0 || insert_type == PLACEMENT_INSERT):
// distSq check — if sphere NOT overlapping: return (void = passable)
if (distSq < radiusSumSq): // overlapping:
// calls collides_with_sphere on sphere[1] if num_sphere > 1
// returns void — no COLLIDED
return // void = passable in all cases
else:
// step_down / check_walkable / slide blocking paths
```
**Key:** the `CSphere::intersects_sphere` function is `void __thiscall` — there is NO
`return 2` (COLLIDED) from the ethereal branch. The inner `collides_with_sphere` call
(for the penetrating case) also produces no blocking result (it may contribute to a
side-channel contact; it does NOT stop the player). The player is passable in ALL
sub-cases of the ethereal branch.
**acdream implementation (Task 3 follow-up, 2026-06-24):** `SphereCollision` implements
`if (sp.ObstructionEthereal) return OK` at the top. The inner overlap/slide path is
bypassed — matches the void-return semantics of the retail ethereal branch.
---
## Summary: set / clear / consume flow (complete after Task 3 follow-up)
```
FindObjCollisionsLoop():
@ -170,10 +198,10 @@ FindObjCollisionsLoop():
// Set flag for ETHEREAL-alone
sp.ObstructionEthereal = (obj.State & 0x4) != 0
// Shape dispatch (BSP path):
// BSPQuery.FindCollisions → Path 1 checks ObstructionEthereal
// When ObstructionEthereal=true → sphere_intersects_solid (passable)
// When ObstructionEthereal=false → normal blocking paths
// Shape dispatch — ALL three shapes now consume the flag:
// BSP: BSPQuery.FindCollisions Path 1 → sphere_intersects_solid (passable)
// Sphere: SphereCollision → if (ObstructionEthereal) return OK
// Cylinder: CylinderCollision → if (ObstructionEthereal) return OK
// Clear after per-object test (retail pc:276989)
sp.ObstructionEthereal = false
@ -183,10 +211,12 @@ FindEnvCollisions():
// BSP dispatch against cell walls ...
```
**Contracts:**
- ETHEREAL-only target (`state = 0x0001000C`): `ShouldSkip` returns false → flag set to
true → BSP Path 1 fires → `sphere_intersects_solid` test → player walks through open
door's BSP (no solid leaf at the opening) → OK → passable.
- Non-ethereal wall: flag = false → Path 1 DOES NOT fire → normal blocking paths → player
stopped.
- ENV cell walls: flag cleared before BSP dispatch → ENV BSP always uses the blocking path.
**Contracts (complete):**
- ETHEREAL-alone BSP target (e.g. open door): `ShouldSkip` = false → flag set → BSP
Path 1 fires → `sphere_intersects_solid` → player walks through open door. Passable.
- ETHEREAL-alone Sphere target (e.g. ghost NPC with sphere shape): flag set →
`SphereCollision` returns OK immediately. Passable.
- ETHEREAL-alone Cylinder target (e.g. ghost NPC with cyl shape): flag set →
`CylinderCollision` returns OK immediately. Passable.
- Non-ethereal wall/object: flag = false → normal blocking paths. Blocked.
- ENV cell walls: flag cleared before BSP dispatch → ENV BSP always blocking. Blocked.