CollisionExemption checked only the TARGET's IsImpenetrable, and the class doc
asserted "retail's pseudo-C only checks the target's IsImpenetrable(); acdream
follows retail" while blaming ACE for checking both. That was backwards: ACE was
retail-faithful and acdream was missing half the check.
Retail short-circuits on EITHER the mover's own state & IS_IMPENETRABLE (0x80)
OR the target's IsImpenetrable(); either alone exempts. Verified at the byte
level rather than from the decompiler's rendering — Binary Ninja shows the mover
test as `int16_t state_1 ... if (state_1 < 0)`, which reads like a 0x8000 test,
but decoding the PDB-paired binary at the mapped offset gives:
8b 43 04 mov eax,[ebx+4] ; mover object_info.state
f6 c4 01 test ah,1 ; 0x100 IsPlayer
84 c0 test al,al ; sign bit of AL = state & 0x80
78 3d js ... ; -> collide
`test al, al; js` is a byte-level sign test on AL, i.e. 0x80, not 0x8000.
Corroborated downstream in the same block (`test ah,8` = 0x800 IsPK,
`test ah,0x10` = 0x1000 IsPKLite) and by OBJECTINFO::init @0x0050cf30 setting
state |= 0x80 from the object's own IsImpenetrable().
Also corrected: ACCWeenieObject::IsImpenetrable @0x0058c8c0 returns
(_bitfield >> 0x15) & 1 — retail genuinely conflates BF_FREE_PKSTATUS with
"impenetrable", so acdream's FromPwdBitfield decode was already right.
Both retail arms set collide, so ordering between them is semantically free and
a misreading here could only ever produce spurious collisions, never a
walk-through.
Found while investigating #297; not symptom-causing on its own. No divergence
row: this retires a missing port rather than introducing a deviation, and
nothing in the register or the collision digest's DO-NOT-RETRY tables covers it.
Gates: complete Release solution 10,887 passed / 4 skipped / 0 failed
(baseline 10,867/4/0). Adversarial + retail-conformance review PASS on this
change specifically. Both new tests discrimination-verified by reverting the
branch and confirming failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
7.1 KiB
C#
146 lines
7.1 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). #299: retail's pseudo-C
|
|
/// (<c>acclient_2013_pseudo_c.txt:276824-276827</c>) checks the MOVER's own
|
|
/// <c>state & IS_IMPENETRABLE (0x80)</c> — the sign-bit test on the
|
|
/// truncated 16-bit <c>ebx->object_info.state</c> the decompiler renders as
|
|
/// <c>if (state_1 < 0)</c>, sitting immediately before the target's
|
|
/// <c>IsImpenetrable()</c> check at the same nesting level — in addition to
|
|
/// the target's own <c>IsImpenetrable()</c>, either alone disqualifying the
|
|
/// tentative PvP exemption. ACE's port has both; acdream previously had only
|
|
/// the target half. The bit position is confirmed against
|
|
/// <c>OBJECTINFO::init@0x0050cf30</c> (<c>this->state |= 0x80</c> when
|
|
/// <c>weenie_obj->IsImpenetrable()</c>) and ACE's own
|
|
/// <c>ObjectInfoState.IsImpenetrable = 0x80</c>
|
|
/// (<c>ACE.Server/Physics/ObjectInfo.cs:17</c>) — the earlier comment here
|
|
/// blaming ACE for an addition was itself the divergence; ACE was
|
|
/// retail-faithful and acdream was missing the mover half.
|
|
/// </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 + IGNORE_COLLISIONS → instant-skip.
|
|
// Retail (acclient_2013_pseudo_c.txt:276782):
|
|
// `if ((state & 4) AND (state & 0x10)) return 1`
|
|
// BOTH bits are required. ETHEREAL-alone takes the retail
|
|
// `obstruction_ethereal` path instead (pc:276806): the flag is
|
|
// set to 1 on the SpherePath and the shape test still runs,
|
|
// but BSP Path 1 (sphere_intersects_solid) weakens solid-
|
|
// containment so the player passes through the open door.
|
|
// ACE's Door.Open() broadcasts ETHEREAL only (0x0001000C) —
|
|
// this faithful port makes open doors passable via the BSP
|
|
// sphere_intersects_solid path (no solid leaf at the opening),
|
|
// which subsumes the former AD-7 shim. Divergence register
|
|
// row AD-7 retired in the same commit as this change.
|
|
if ((targetState & ETHEREAL_PS) != 0 && (targetState & IGNORE_COLLISIONS_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 OR impenetrable mover → collide.
|
|
// acclient_2013_pseudo_c.txt:276824-276827 — retail checks
|
|
// BOTH: the mover's own state&0x80 (IS_IMPENETRABLE) short-
|
|
// circuits first, then the target's IsImpenetrable(). #299:
|
|
// acdream previously ported only the target half.
|
|
if ((moverState & ObjectInfoState.IsImpenetrable) != 0)
|
|
collide = true;
|
|
if (!collide && (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
|
|
}
|
|
}
|