From a6e4b5709f1937b8c1f05a433f2fdfffa24d4aee Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 13 May 2026 18:27:06 +0200 Subject: [PATCH] fix(phys L.2g slice 1b): widen CollisionExemption to ETHEREAL alone B.4b visual test confirmed the L.2g slice 1 handoff's open question: ACE's Door.Open() broadcasts state=0x0001000C (HasPhysicsBSP | Ethereal | ReportCollisions), NOT the state=0x14+ that retail servers send (Ethereal | IgnoreCollisions). The L.2g pipeline correctly mutates ShadowObjectRegistry with the new state, but CollisionExemption.ShouldSkip required both bits and the door stayed solid. Retail (acclient_2013_pseudo_c.txt:276782) wraps FindObjCollisions in `if NOT (state & ETHEREAL && state & IGNORE_COLLISIONS)`. ETHEREAL alone takes a different retail path at line 276795 that sets sphere_path.obstruction_ethereal = 1 and lets downstream movement allow passage despite the contact. We haven't ported that downstream path yet. Pragmatic shortcut: widen the early-out to ETHEREAL alone so doors become passable when ACE flips the bit. Retail-server broadcasts still hit the same branch correctly (both bits set implies ETHEREAL). Compatible with both server styles. Renames test EtherealOnly_NotSkipped -> EtherealOnly_Skipped and flips its assertion. 13 CollisionExemption tests pass; full suite 1046 pass / 8 pre-existing baseline fail (unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Physics/CollisionExemption.cs | 24 +++++++++++++------ .../Physics/CollisionExemptionTests.cs | 14 +++++++---- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/AcDream.Core/Physics/CollisionExemption.cs b/src/AcDream.Core/Physics/CollisionExemption.cs index 89b368d..2e66751 100644 --- a/src/AcDream.Core/Physics/CollisionExemption.cs +++ b/src/AcDream.Core/Physics/CollisionExemption.cs @@ -59,14 +59,24 @@ public static class CollisionExemption public static bool ShouldSkip(uint targetState, EntityCollisionFlags targetFlags, ObjectInfoState moverState) { - // 1. Target ETHEREAL + IGNORE_COLLISIONS → walk through. - // acclient_2013_pseudo_c.txt:276782 — wraps the entire body of - // FindObjCollisions; we hoist it as the first early-out. - if ((targetState & ETHEREAL_PS) != 0 - && (targetState & IGNORE_COLLISIONS_PS) != 0) - { + // 1. Target ETHEREAL → walk through. + // Retail (acclient_2013_pseudo_c.txt:276782) requires BOTH + // ETHEREAL_PS (0x4) AND IGNORE_COLLISIONS_PS (0x10) to wrap + // the entire body of FindObjCollisions and skip collision. + // ETHEREAL alone takes a different retail path (line 276795 + // sets sphere_path.obstruction_ethereal = 1 and downstream + // movement allows passage despite the contact). We haven't + // ported that downstream path yet. + // + // L.2g slice 1b (2026-05-13): ACE's Door.Open() sends only + // ETHEREAL (state=0x0001000C observed live), not the + // ETHEREAL|IGNORE_COLLISIONS combo retail servers broadcast. + // Pragmatic shortcut: exempt on ETHEREAL alone so doors + // become passable when ACE flips the bit. Retail-server + // broadcasts (state=0x14+) still hit this branch correctly + // because both bits set implies ETHEREAL set. + if ((targetState & ETHEREAL_PS) != 0) return true; - } // 2. Viewer mover + creature target → walk through. // acclient_2013_pseudo_c.txt:276787-276790. diff --git a/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs b/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs index b843165..3950bd9 100644 --- a/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs +++ b/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs @@ -42,12 +42,16 @@ public class CollisionExemptionTests } [Fact] - public void EtherealOnly_NotSkipped() + public void EtherealOnly_Skipped() { - // Target with ETHEREAL but NOT IGNORE_COLLISIONS does not bail - // out at the first gate — collision proceeds. (Step-down marks - // obstruction_ethereal, but does not exempt.) - Assert.False(CollisionExemption.ShouldSkip( + // L.2g slice 1b (2026-05-13): ETHEREAL alone exempts collision. + // Retail (acclient_2013_pseudo_c.txt:276782) required both bits, + // but ACE's Door.Open() broadcasts ETHEREAL alone — observed + // live: state=0x0001000C (HasPhysicsBSP | Ethereal | ReportCollisions). + // Pragmatic shortcut: widen the early-out to ETHEREAL alone so + // doors become passable when ACE flips the bit. Retail-server + // broadcasts (state=0x14+) still hit the same branch correctly. + Assert.True(CollisionExemption.ShouldSkip( targetState: ETHEREAL_PS, targetFlags: EntityCollisionFlags.None, moverState: ObjectInfoState.IsPlayer));