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:
parent
78e5758185
commit
3361a8d776
7 changed files with 382 additions and 30 deletions
192
docs/research/2026-06-24-obstruction-ethereal-pseudocode.md
Normal file
192
docs/research/2026-06-24-obstruction-ethereal-pseudocode.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# obstruction_ethereal — set / clear / consume contract
|
||||
|
||||
**Research date:** 2026-06-24
|
||||
**Oracle:** `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
**Phase:** Task 3 of the collision-inclusion verbatim-retail port
|
||||
|
||||
---
|
||||
|
||||
## 1. Gate 1 — `CPhysicsObj::FindObjCollisions` (pc:276782 / 0x0050f050)
|
||||
|
||||
```
|
||||
// pc:276782
|
||||
if (state & 4) AND (state & 0x10): // ETHEREAL_PS | IGNORE_COLLISIONS_PS
|
||||
return 1 // OK_TS — instant-skip, no further work
|
||||
// else fall through: ETHEREAL-alone goes into the block below
|
||||
|
||||
// pc:276802
|
||||
int32_t var_c;
|
||||
if ((state_2 & 4) != 0 // target ETHEREAL
|
||||
|| (ebx->object_info.ethereal != 0 // mover is ethereal
|
||||
&& (state_2 & 1) == 0)): // AND target not REPORTS_COLLISIONS
|
||||
var_c = 1
|
||||
if (sphere_path.step_down == 0):
|
||||
goto label_50f0c9 // set obstruction_ethereal = 1 and continue
|
||||
else:
|
||||
var_c = 0
|
||||
// label_50f0c9:
|
||||
ebx->sphere_path.obstruction_ethereal = var_c // pc:276806
|
||||
// ... continue with shape dispatch (FindObjCollisions body: BSP / Sphere / Cyl) ...
|
||||
|
||||
// At the END of the FindObjCollisions body (after all shape tests):
|
||||
// pc:276989 / 0x0050f31e:
|
||||
ebx->sphere_path.obstruction_ethereal = 0 // clear after each object's test
|
||||
```
|
||||
|
||||
**Translation (C#):**
|
||||
- `ShouldSkip` returns `true` ONLY when `(targetState & 0x4) != 0 && (targetState & 0x10) != 0`.
|
||||
- ETHEREAL-alone falls through. Before the shape dispatch (BSP/Sphere/Cyl call), set
|
||||
`sp.ObstructionEthereal = true` when `(targetState & 0x4) != 0`.
|
||||
- After the shape dispatch, retail clears the flag (`= 0`). In acdream we clear it at the
|
||||
END of the per-target loop iteration (mirrors the per-object clear at pc:276989).
|
||||
|
||||
**Note on `step_down` gate:** retail's `var_c` assignment also checks `step_down == 0`
|
||||
before jumping to `label_50f0c9`. When `step_down != 0`, var_c stays 1 but hits the
|
||||
`else var_c = 0` branch at pc:276804 (which sets var_c = 0). Reading the full
|
||||
branching tree: the ONLY path that sets `obstruction_ethereal = 1` is when
|
||||
`(state_2 & 4) != 0` (target ETHEREAL) AND `step_down == 0`. For acdream's
|
||||
transitional player insert, `step_down` is false at the outer loop entry, so the flag
|
||||
fires when target is ETHEREAL. We match retail exactly by gating on target ETHEREAL
|
||||
(the `(obj.State & 0x4) != 0` check in the loop).
|
||||
|
||||
---
|
||||
|
||||
## 2. D5 clear — `CEnvCell::find_env_collisions` (pc:309580 / 0x0052c144)
|
||||
|
||||
```
|
||||
// 0x0052c144:
|
||||
arg2->sphere_path.obstruction_ethereal = 0;
|
||||
// ... then: BSP dispatch for the ENV cell walls (not objects) ...
|
||||
```
|
||||
|
||||
**Translation (C#):** At the top of `FindEnvCollisions`, clear `sp.ObstructionEthereal = false`
|
||||
before any BSP dispatch. The ENV path clears it because ENV walls are always solid — the
|
||||
flag only applies to object (CPhysicsObj) tests. This prevents a stale flag from a
|
||||
prior object loop from weakening ENV wall tests.
|
||||
|
||||
---
|
||||
|
||||
## 3. Consume site 1 — `CSphere::intersects_sphere` (pc:321692 / 0x00537ae4)
|
||||
|
||||
```
|
||||
// pc:321692:
|
||||
if (obstruction_ethereal != 0 || insert_type == PLACEMENT_INSERT):
|
||||
// sphere_intersects_solid test — allows passage if sphere is NOT inside solid
|
||||
if (collides_with_sphere(center, pos, radius_sum)):
|
||||
return 2 // COLLIDED — solid containment
|
||||
// else: sphere is NOT inside solid, passage allowed — no push-back
|
||||
else if (step_down == 0):
|
||||
// normal walkable / check_walkable / slide path (the BLOCKING path)
|
||||
...
|
||||
```
|
||||
|
||||
**What this means:** when `obstruction_ethereal` (target is passable-ethereal) or
|
||||
`PLACEMENT_INSERT`, the test only fails if the sphere FULLY OVERLAPS the solid region
|
||||
of the sphere target. In practice for a Sphere-type entity with no BSP this means
|
||||
you can walk through it as long as your sphere center isn't inside its solid — which
|
||||
is almost never true during normal movement. The blocker path (slide/push-back) is
|
||||
bypassed entirely.
|
||||
|
||||
**acdream scope note:** `SphereCollision` in `TransitionTypes.cs` does NOT currently
|
||||
implement this gate; it always does the slide path. That is a separate task (Task 5 or
|
||||
equivalent). For Task 3 scope: BSP objects are the priority for opened doors; the
|
||||
Sphere path is a separate port.
|
||||
|
||||
---
|
||||
|
||||
## 4. Consume site 2 — `BSPTREE::find_collisions` (pc:323742 / 0x0053a496)
|
||||
|
||||
```
|
||||
// pc:323742:
|
||||
if (insert_type == PLACEMENT_INSERT || obstruction_ethereal != 0):
|
||||
// sphere_intersects_solid with bldg_check center_solid flag
|
||||
ebp_4 = 1
|
||||
if (bldg_check != 0):
|
||||
ebp_4 = (hits_interior_cell == 0) // center_solid weakens inside buildings
|
||||
if (root_node->sphere_intersects_solid(localspace_sphere, ebp_4)):
|
||||
return 2 // COLLIDED
|
||||
if (num_sphere > 1):
|
||||
if (root_node->sphere_intersects_solid(localspace_sphere[1], ebp_4)):
|
||||
return 2
|
||||
else:
|
||||
// check_walkable / step_down / normal blocking path
|
||||
...
|
||||
```
|
||||
|
||||
**This is the BSPQuery Path 1 at `BSPQuery.cs:1717`.** The existing code:
|
||||
```csharp
|
||||
if (path.InsertType == InsertType.Placement || obj.Ethereal)
|
||||
```
|
||||
`obj.Ethereal` is `ObjectInfo.Ethereal` (the MOVER's ethereal flag) which is NEVER
|
||||
set — it's always false. The correct translation of `obstruction_ethereal` is
|
||||
`path.ObstructionEthereal` (on `SpherePath`).
|
||||
|
||||
**Fix:** change to:
|
||||
```csharp
|
||||
if (path.InsertType == InsertType.Placement || path.ObstructionEthereal)
|
||||
```
|
||||
|
||||
This is the ONLY consume site for Task 3 scope (BSP objects). For the BSP path:
|
||||
when the target is ETHEREAL, `path.ObstructionEthereal` is set before calling
|
||||
`BSPQuery.FindCollisions`, so Path 1 fires instead of the normal blocking path.
|
||||
Path 1 uses `sphere_intersects_solid` which only returns COLLIDED if the player
|
||||
is inside a BSP solid leaf — during forward movement toward a door (which has no
|
||||
solid wall when open), the BSP finds no solid intersection, returns OK, player passes
|
||||
through.
|
||||
|
||||
---
|
||||
|
||||
## 5. Consume site 3 — `CCylSphere::intersects_sphere` (pc:324573 / 0x0053b4a0)
|
||||
|
||||
```
|
||||
// pc:324573:
|
||||
if (insert_type == PLACEMENT_INSERT || obstruction_ethereal != 0):
|
||||
// collides_with_sphere test (3D overlap only)
|
||||
if (CCylSphere::collides_with_sphere(this, global_sphere, ...)):
|
||||
return // early-out, no Collided return — void return here
|
||||
// else: no collision, return (void — passable)
|
||||
else:
|
||||
// step_down / check_walkable / normal blocking path
|
||||
...
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Summary: set / clear / consume flow for Task 3
|
||||
|
||||
```
|
||||
FindObjCollisionsLoop():
|
||||
for each obj in cell.shadow_entries:
|
||||
// Gate 1: instant-skip needs BOTH bits
|
||||
if obj.State has ETHEREAL AND IGNORE_COLLISIONS:
|
||||
continue // pass through, no obstruction_ethereal
|
||||
|
||||
// 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
|
||||
|
||||
// Clear after per-object test (retail pc:276989)
|
||||
sp.ObstructionEthereal = false
|
||||
|
||||
FindEnvCollisions():
|
||||
sp.ObstructionEthereal = false // D5 clear — ENV walls are always solid
|
||||
// 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue