diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs
index d3c25c4..8084231 100644
--- a/src/AcDream.App/Input/PlayerMovementController.cs
+++ b/src/AcDream.App/Input/PlayerMovementController.cs
@@ -413,7 +413,13 @@ public sealed class PlayerMovementController
stepUpHeight: StepUpHeight,
stepDownHeight: 0.04f, // retail default
isOnGround: _body.OnWalkable,
- body: _body); // persist ContactPlane across frames for slope tracking
+ body: _body, // persist ContactPlane across frames for slope tracking
+ // Commit C 2026-04-29 — local player is always IsPlayer.
+ // The PK/PKLite/Impenetrable bits come from PlayerDescription's
+ // PlayerKillerStatus property; not yet parsed (non-PK pair → walks
+ // through other non-PK players, which is retail's default for
+ // ACE's character creation defaults too).
+ moverFlags: AcDream.Core.Physics.ObjectInfoState.IsPlayer);
// Apply resolved position.
_body.Position = resolveResult.Position;
diff --git a/src/AcDream.Core/Physics/CollisionExemption.cs b/src/AcDream.Core/Physics/CollisionExemption.cs
new file mode 100644
index 0000000..89b368d
--- /dev/null
+++ b/src/AcDream.Core/Physics/CollisionExemption.cs
@@ -0,0 +1,123 @@
+namespace AcDream.Core.Physics;
+
+///
+/// The retail-faithful exemption gate at the top of
+/// CPhysicsObj::FindObjCollisions. Decides — based on the moving
+/// object's bits and the target's raw
+/// PhysicsState + decoded —
+/// whether collision against the target should be skipped entirely
+/// (return OK_TS) or proceed to broad-phase / shape dispatch.
+///
+///
+/// Ported from the named retail decompilation:
+///
+///
+/// - acclient_2013_pseudo_c.txt:276782 — target
+/// ETHEREAL_PS=0x4 & IGNORE_COLLISIONS_PS=0x10: walk through.
+/// - acclient_2013_pseudo_c.txt:276787 — viewer mover vs
+/// creature target: walk through (camera ray ignores creatures).
+/// - acclient_2013_pseudo_c.txt:276971 — mover with
+/// IGNORE_CREATURES (state & 0x400) vs creature target:
+/// walk through.
+/// - acclient_2013_pseudo_c.txt:276807-276839 — PvP rule:
+///
+/// If both are players: skip unless 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.
+///
+///
+///
+///
+///
+/// Cross-checked against ACE
+/// references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:381-405
+/// (line-for-line C# port of the same logic). Note: ACE adds
+/// state.HasFlag(IsImpenetrable) (mover-impenetrable) to the
+/// collide list; retail's pseudo-C only checks the target's
+/// IsImpenetrable(). acdream follows retail.
+///
+///
+public static class CollisionExemption
+{
+ private const uint ETHEREAL_PS = 0x4u; // acclient.h:2819
+ private const uint IGNORE_COLLISIONS_PS = 0x10u; // acclient.h:2821
+
+ ///
+ /// Should the moving object skip collision testing against this
+ /// target entirely? Returns true if exempt (no further
+ /// shape dispatch).
+ ///
+ /// Raw retail PhysicsState bits
+ /// captured at CreateObject time (ETHEREAL/IGNORE/etc.).
+ /// Decoded
+ /// from the target's PWD bitfield
+ /// plus its ItemType-derived IsCreature bit.
+ /// The moving object's
+ /// — typically the local player's
+ /// IsPlayer + (PK/PKLite/Impenetrable bits if known) flags.
+ 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)
+ {
+ 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
+ }
+}
diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs
index bb26c54..4a93403 100644
--- a/src/AcDream.Core/Physics/PhysicsEngine.cs
+++ b/src/AcDream.Core/Physics/PhysicsEngine.cs
@@ -388,13 +388,21 @@ public sealed class PhysicsEngine
float sphereRadius, float sphereHeight,
float stepUpHeight, float stepDownHeight,
bool isOnGround,
- PhysicsBody? body = null)
+ PhysicsBody? body = null,
+ ObjectInfoState moverFlags = ObjectInfoState.None)
{
var transition = new Transition();
transition.ObjectInfo.StepUpHeight = stepUpHeight;
transition.ObjectInfo.StepDownHeight = stepDownHeight;
transition.ObjectInfo.StepDown = true;
+ // Commit C 2026-04-29 — caller-supplied mover flags drive the
+ // retail PvP exemption block in FindObjCollisions. The local
+ // player passes IsPlayer (and PK/PKLite/Impenetrable when known
+ // from PlayerDescription); remote dead-reckoning passes None
+ // (matches non-player movement, all targets collide).
+ transition.ObjectInfo.State |= moverFlags;
+
if (isOnGround)
transition.ObjectInfo.State |= ObjectInfoState.Contact | ObjectInfoState.OnWalkable;
diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs
index be451f9..57d6e6d 100644
--- a/src/AcDream.Core/Physics/TransitionTypes.cs
+++ b/src/AcDream.Core/Physics/TransitionTypes.cs
@@ -807,6 +807,17 @@ public sealed class Transition
if (distToCurr > maxReach)
continue;
+ // Commit C 2026-04-29 — retail exemption block at the top of
+ // CPhysicsObj::FindObjCollisions
+ // (acclient_2013_pseudo_c.txt:276782-276839,276971). Skips
+ // ethereal+ignore-collisions, viewer-vs-creature,
+ // IGNORE_CREATURES movers, and the player-vs-player PvP
+ // pass-through (non-PK pair / mismatched PK). Static
+ // landblock entries register with State=0 and Flags=None,
+ // so this is a cheap fall-through for them.
+ if (CollisionExemption.ShouldSkip(obj.State, obj.Flags, ObjectInfo.State))
+ continue;
+
TransitionState result;
if (obj.CollisionType == ShadowCollisionType.BSP)
diff --git a/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs b/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs
new file mode 100644
index 0000000..b843165
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Physics/CollisionExemptionTests.cs
@@ -0,0 +1,169 @@
+using AcDream.Core.Physics;
+using Xunit;
+
+namespace AcDream.Core.Tests.Physics;
+
+///
+/// Unit tests for — Commit C of the
+/// 2026-04-29 live-entity collision port. Covers retail's
+/// CPhysicsObj::FindObjCollisions exemption block, ported
+/// line-for-line from
+/// docs/research/named-retail/acclient_2013_pseudo_c.txt:276782-276839,276971.
+///
+///
+/// Behaviour matrix (target / mover columns):
+///
+///
+/// MoverTargetSkip?
+/// - anyETHEREAL+IGNORE_COLLISIONSYES (early-out)
+/// - IsViewerIsCreatureYES (camera ray-test passes through)
+/// - IGNORE_CREATURESIsCreatureYES (mover walks through creatures)
+/// - IsPlayerIsPlayer (no PK)YES (non-PK pair walks through)
+/// - IsPlayer + IsPKIsPlayer + IsPKNO (PK pair collides)
+/// - IsPlayer + IsPKLiteIsPlayer + IsPKLiteNO (PKLite pair collides)
+/// - IsPlayerIsPlayer + IsImpenetrableNO (Impenetrable target always collides)
+/// - IsPlayer + IsPKIsPlayer (no PK)YES (mismatched PK skip)
+/// - IsPlayerIsCreature (NPC)NO (player vs NPC always collides)
+///
+///
+public class CollisionExemptionTests
+{
+ private const uint ETHEREAL_PS = 0x4u;
+ private const uint IGNORE_COLLISIONS_PS = 0x10u;
+
+ [Fact]
+ public void EtherealAndIgnoreCollisions_AlwaysSkipped()
+ {
+ // Target with both bits set is exempted from any mover.
+ Assert.True(CollisionExemption.ShouldSkip(
+ targetState: ETHEREAL_PS | IGNORE_COLLISIONS_PS,
+ targetFlags: EntityCollisionFlags.None,
+ moverState: ObjectInfoState.IsPlayer));
+ }
+
+ [Fact]
+ public void EtherealOnly_NotSkipped()
+ {
+ // 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(
+ targetState: ETHEREAL_PS,
+ targetFlags: EntityCollisionFlags.None,
+ moverState: ObjectInfoState.IsPlayer));
+ }
+
+ [Fact]
+ public void Viewer_VsCreature_Skipped()
+ {
+ // Camera-ray viewer transitions through creatures.
+ Assert.True(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsCreature,
+ moverState: ObjectInfoState.IsViewer));
+ }
+
+ [Fact]
+ public void Viewer_VsNonCreature_NotSkipped()
+ {
+ Assert.False(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.None,
+ moverState: ObjectInfoState.IsViewer));
+ }
+
+ [Fact]
+ public void IgnoreCreatures_VsCreature_Skipped()
+ {
+ // Per acclient_2013_pseudo_c.txt:276971 — an arrow with
+ // IGNORE_CREATURES doesn't get blocked by the very monster it's
+ // tracking towards (until missile_ignore filters its target).
+ Assert.True(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsCreature,
+ moverState: ObjectInfoState.IgnoreCreatures));
+ }
+
+ [Fact]
+ public void NonPkPlayer_VsNonPkPlayer_Skipped()
+ {
+ // The user-visible payoff: two ordinary players walk through each
+ // other instead of blocking.
+ Assert.True(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsPlayer,
+ moverState: ObjectInfoState.IsPlayer));
+ }
+
+ [Fact]
+ public void Pk_VsPk_NotSkipped()
+ {
+ // Two PK players collide.
+ Assert.False(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPK,
+ moverState: ObjectInfoState.IsPlayer | ObjectInfoState.IsPK));
+ }
+
+ [Fact]
+ public void PkLite_VsPkLite_NotSkipped()
+ {
+ Assert.False(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPKLite,
+ moverState: ObjectInfoState.IsPlayer | ObjectInfoState.IsPKLite));
+ }
+
+ [Fact]
+ public void Pk_VsNonPk_Skipped()
+ {
+ // Mismatched PK status: still exempt — only matching pair collides.
+ Assert.True(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsPlayer,
+ moverState: ObjectInfoState.IsPlayer | ObjectInfoState.IsPK));
+ }
+
+ [Fact]
+ public void Pk_VsPkLite_Skipped()
+ {
+ // PK and PKLite are different pools — pair doesn't match.
+ Assert.True(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPKLite,
+ moverState: ObjectInfoState.IsPlayer | ObjectInfoState.IsPK));
+ }
+
+ [Fact]
+ public void ImpenetrableTarget_VsAnyPlayer_NotSkipped()
+ {
+ // Impenetrable target ("Free" PK status) always collides with
+ // any player mover — regardless of mover's PK state.
+ Assert.False(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsImpenetrable,
+ moverState: ObjectInfoState.IsPlayer));
+ }
+
+ [Fact]
+ public void Player_VsCreature_NotSkipped()
+ {
+ // PvP exemption only applies player-on-player. Player vs creature
+ // (NPC, monster) is the normal blocking case.
+ Assert.False(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsCreature,
+ moverState: ObjectInfoState.IsPlayer));
+ }
+
+ [Fact]
+ public void NonPlayerMover_VsPlayer_NotSkipped()
+ {
+ // PvP rule requires BOTH to be players. Mover is not a player
+ // (e.g., dead-reckoned remote NPC) → no exemption applies.
+ Assert.False(CollisionExemption.ShouldSkip(
+ targetState: 0u,
+ targetFlags: EntityCollisionFlags.IsPlayer,
+ moverState: ObjectInfoState.None));
+ }
+}