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) <noreply@anthropic.com>
133 lines
6 KiB
C#
133 lines
6 KiB
C#
namespace AcDream.Core.Physics;
|
|
|
|
/// <summary>
|
|
/// The retail-faithful exemption gate at the top of
|
|
/// <c>CPhysicsObj::FindObjCollisions</c>. Decides — based on the moving
|
|
/// object's <see cref="ObjectInfoState"/> bits and the target's raw
|
|
/// <c>PhysicsState</c> + decoded <see cref="EntityCollisionFlags"/> —
|
|
/// whether collision against the target should be skipped entirely
|
|
/// (return <c>OK_TS</c>) or proceed to broad-phase / shape dispatch.
|
|
///
|
|
/// <para>
|
|
/// Ported from the named retail decompilation:
|
|
/// </para>
|
|
/// <list type="bullet">
|
|
/// <item><c>acclient_2013_pseudo_c.txt:276782</c> — target
|
|
/// <c>ETHEREAL_PS=0x4 & IGNORE_COLLISIONS_PS=0x10</c>: walk through.</item>
|
|
/// <item><c>acclient_2013_pseudo_c.txt:276787</c> — viewer mover vs
|
|
/// creature target: walk through (camera ray ignores creatures).</item>
|
|
/// <item><c>acclient_2013_pseudo_c.txt:276971</c> — mover with
|
|
/// <c>IGNORE_CREATURES (state & 0x400)</c> vs creature target:
|
|
/// walk through.</item>
|
|
/// <item><c>acclient_2013_pseudo_c.txt:276807-276839</c> — PvP rule:
|
|
/// <para>
|
|
/// If both are players: skip <em>unless</em> target is Impenetrable,
|
|
/// or both are PK, or both are PKLite. Mismatched PK status (PK vs
|
|
/// non-PK, PK vs PKLite) is exempted — players in different pools
|
|
/// pass through each other, matching retail muscle memory.
|
|
/// </para>
|
|
/// </item>
|
|
/// </list>
|
|
///
|
|
/// <para>
|
|
/// Cross-checked against ACE
|
|
/// <c>references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:381-405</c>
|
|
/// (line-for-line C# port of the same logic). Note: ACE adds
|
|
/// <c>state.HasFlag(IsImpenetrable)</c> (mover-impenetrable) to the
|
|
/// collide list; retail's pseudo-C only checks the target's
|
|
/// <c>IsImpenetrable()</c>. acdream follows retail.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class CollisionExemption
|
|
{
|
|
private const uint ETHEREAL_PS = 0x4u; // acclient.h:2819
|
|
private const uint IGNORE_COLLISIONS_PS = 0x10u; // acclient.h:2821
|
|
|
|
/// <summary>
|
|
/// Should the moving object skip collision testing against this
|
|
/// target entirely? Returns <c>true</c> if exempt (no further
|
|
/// shape dispatch).
|
|
/// </summary>
|
|
/// <param name="targetState">Raw retail <c>PhysicsState</c> bits
|
|
/// captured at <c>CreateObject</c> time (ETHEREAL/IGNORE/etc.).</param>
|
|
/// <param name="targetFlags">Decoded
|
|
/// <see cref="EntityCollisionFlags"/> from the target's PWD bitfield
|
|
/// plus its <c>ItemType</c>-derived <c>IsCreature</c> bit.</param>
|
|
/// <param name="moverState">The moving object's
|
|
/// <see cref="ObjectInfoState"/> — typically the local player's
|
|
/// IsPlayer + (PK/PKLite/Impenetrable bits if known) flags.</param>
|
|
public static bool ShouldSkip(uint targetState, EntityCollisionFlags targetFlags,
|
|
ObjectInfoState moverState)
|
|
{
|
|
// 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.
|
|
bool moverIsViewer = (moverState & ObjectInfoState.IsViewer) != 0;
|
|
bool targetIsCreature = (targetFlags & EntityCollisionFlags.IsCreature) != 0;
|
|
if (moverIsViewer && targetIsCreature)
|
|
return true;
|
|
|
|
// 3. IGNORE_CREATURES mover + creature target → walk through.
|
|
// acclient_2013_pseudo_c.txt:276971.
|
|
bool moverIgnoresCreatures = (moverState & ObjectInfoState.IgnoreCreatures) != 0;
|
|
if (moverIgnoresCreatures && targetIsCreature)
|
|
return true;
|
|
|
|
// 4. PvP exemption block.
|
|
// acclient_2013_pseudo_c.txt:276807-276839.
|
|
bool moverIsPlayer = (moverState & ObjectInfoState.IsPlayer) != 0;
|
|
bool targetIsPlayer = (targetFlags & EntityCollisionFlags.IsPlayer) != 0;
|
|
if (moverIsPlayer && targetIsPlayer)
|
|
{
|
|
// Tentatively exempt (retail `ebp_1 = 1`). Then disqualify
|
|
// if any of the COLLIDE conditions hold.
|
|
bool collide = false;
|
|
|
|
// 4a. Impenetrable target → collide.
|
|
// acclient_2013_pseudo_c.txt:276826.
|
|
if ((targetFlags & EntityCollisionFlags.IsImpenetrable) != 0)
|
|
collide = true;
|
|
|
|
// 4b. Both PK → collide.
|
|
// acclient_2013_pseudo_c.txt:276832-276836.
|
|
if (!collide
|
|
&& (moverState & ObjectInfoState.IsPK) != 0
|
|
&& (targetFlags & EntityCollisionFlags.IsPK) != 0)
|
|
{
|
|
collide = true;
|
|
}
|
|
|
|
// 4c. Both PKLite → collide.
|
|
// acclient_2013_pseudo_c.txt:276837.
|
|
if (!collide
|
|
&& (moverState & ObjectInfoState.IsPKLite) != 0
|
|
&& (targetFlags & EntityCollisionFlags.IsPKLite) != 0)
|
|
{
|
|
collide = true;
|
|
}
|
|
|
|
if (!collide)
|
|
return true; // exempt — non-PK pair walks through
|
|
}
|
|
|
|
return false; // proceed to broad-phase + shape dispatch
|
|
}
|
|
}
|