# 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 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. --- ## 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(): 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 — 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 FindEnvCollisions(): sp.ObstructionEthereal = false // D5 clear — ENV walls are always solid // BSP dispatch against cell walls ... ``` **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.