acdream/src/AcDream.Core/Physics/CollisionExemption.cs
Erik 3361a8d776 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>
2026-06-24 19:20:02 +02:00

130 lines
5.9 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 &amp; 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 &amp; 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 + 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 → 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
}
}