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

@ -2986,6 +2986,15 @@ public sealed class Transition
/// </summary>
private TransitionState SphereCollision(ShadowEntry obj, SpherePath sp)
{
// Consume site 1 — CSphere::intersects_sphere @ 0x00537ae4 (pc:321692).
// When obstruction_ethereal is set (target is ETHEREAL-alone, state & 0x4),
// the retail function is void and all paths in the ethereal branch return
// without producing a COLLIDED/Slid result — the player is fully passable.
// We mirror this by returning OK immediately, skipping all blocking paths.
// Retail ref: acclient_2013_pseudo_c.txt:321692.
if (sp.ObstructionEthereal)
return TransitionState.OK;
var ci = CollisionInfo;
Vector3 sphereCurrPos = sp.GlobalCurrCenter[0].Origin;
Vector3 sphereCheckPos = sp.GlobalSphere[0].Origin;
@ -3090,6 +3099,15 @@ public sealed class Transition
/// </summary>
private TransitionState CylinderCollision(ShadowEntry obj, SpherePath sp, PhysicsEngine engine)
{
// Consume site 2 — CCylSphere::intersects_sphere @ 0x0053b4a0 (pc:324573).
// When obstruction_ethereal is set (target is ETHEREAL-alone, state & 0x4),
// the retail function is void and all paths in the ethereal branch return
// without producing a COLLIDED/Slid result — the player is fully passable.
// We mirror this by returning OK immediately, skipping all blocking paths.
// Retail ref: acclient_2013_pseudo_c.txt:324573.
if (sp.ObstructionEthereal)
return TransitionState.OK;
var ci = CollisionInfo;
var oi = ObjectInfo;
Vector3 sphereCurrPos = sp.GlobalCurrCenter[0].Origin;