acdream/tests/AcDream.Core.Tests/Physics/A6P7DispatchRulesTests.cs
Erik 888272aad1 fix(phys): A6.P7 — retail-binary cyl-vs-BSP dispatch (HAS_PHYSICS_BSP_PS gate)
Closes the door-cyl phantom slide where a sphere approaching a closed
cottage door at NE/SE headings could be blocked by the cyl's radial
normal contaminating the slide tangent into the slab face (live
evidence in door-a6p6-v2.utf8.log: 12 resolves with
cn=(0.86,0.51,0) attributed to door entity 0x000F4245).

Retail anchor: CPhysicsObj::FindObjCollisions at
acclient_2013_pseudo_c.txt:276861 dispatches BINARILY between
BSP-only and cyl+sphere based on HAS_PHYSICS_BSP_PS (0x10000 in
acclient.h:2833). For non-PvP, non-missile movers — every M1.5
scope walking-vs-static scenario — an entity with the flag set
tests its BSP exclusively; the foot cyl is never tested. ACE
confirms the truth table at PhysicsObj.cs:412-450 (HasPhysicsBSP,
missileIgnore, exemption).

Our dispatcher iterated every ShadowEntry independently and tested
both the cyl AND the BSP for a closed door. Cyl was registered
first (FromSetup walk order), and its diagonal radial slide normal
"won" attribution at the early-return on first non-OK. Result was
out=in for tangential motion along the door face.

Changes (~15 LOC + 7 unit tests):
- PhysicsStateFlags.HasPhysicsBsp = 0x00010000 (PhysicsBody.cs)
- Transition.BspOnlyDispatch(uint state) static predicate
  (TransitionTypes.cs) — mirrors retail's branch with M1.5 scope
  defaults (ebp_1 and eax_12 treated as false; wire PvP / missile
  refinements when those scopes ship)
- Per-entry guard in FindObjCollisions cyl/sphere branch
  (TransitionTypes.cs:2433) — continue when BspOnlyDispatch fires,
  with [cyl-skip-bsp] diagnostic line gated on ProbeBuildingEnabled
- A6P7DispatchRulesTests (7 tests, all GREEN): flag value + 6
  parameterized predicate cases

Verification: 14-test keep-green list from the 2026-05-25 handoff
passes (5 BSPQueryTests.FindCollisions_Path5_*, 2 CellTransitTests.A6P5_*,
2 DoorCollisionApparatusTests.Apparatus_DeadCenter_*,
5 DoorBugTrajectoryReplayTests, 1
CellarUpTrajectoryReplayTests.LiveCompare_FirstCap_FixClosesCottageFloorCap).
Total: 20/20 pass including the new 7-test predicate suite.

The DocumentsBug test (Apparatus_Grounded_50cmOffCenter) fails
post-fix BUT was already failing pre-fix in the worktree baseline
(verified by stashing the fix and re-running — same failure mode:
sphere blocks at start with floor normal (0,0,1)). Not in the
keep-green list, so this is a known pre-existing condition; the
test's own header comment instructs flipping the assertion when
the fix lands.

Investigation:
docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md

Needs visual verification at Holtburg cottage door (NE/SE approach
should now slide smoothly along the door face — zero [cyl-test]
log lines attributed to door entity, replaced by [cyl-skip-bsp]).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:35:32 +02:00

78 lines
3.1 KiB
C#

using AcDream.Core.Physics;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// A6.P7 (2026-05-25) — retail-binary dispatch rule. Retail's
/// <c>CPhysicsObj::FindObjCollisions</c> at
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt:276861</c>
/// dispatches BINARILY between "BSP-only" and "cyl + sphere" based on
/// the <c>HAS_PHYSICS_BSP_PS</c> flag (bit 16, hex 0x10000) in
/// <c>PhysicsState</c>. The flag is defined in
/// <c>docs/research/named-retail/acclient.h:2833</c> and mirrored in
/// ACE at <c>references/ACE/Source/ACE.Entity/Enum/PhysicsState.cs:24</c>
/// as <c>HasPhysicsBSP = 0x00010000</c>.
///
/// <para>
/// For non-PvP, non-missile movers (M1.5 scope — walking-vs-static), an
/// entity with HAS_PHYSICS_BSP_PS in its state tests its BSP exclusively
/// — the foot cyl is NEVER tested. The closed cottage door (state
/// <c>0x10008</c> = <c>STATIC | REPORT_COLLISIONS | HAS_PHYSICS_BSP</c>)
/// hits this branch.
/// </para>
///
/// <para>
/// Pre-A6.P7: our dispatcher iterates every <c>ShadowEntry</c>
/// independently and tests both the cyl AND the BSP for a door. The
/// cyl's radial normal contaminates the slide direction at NE/SE
/// approach headings (see
/// <c>docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md</c>).
/// </para>
///
/// <para>
/// Post-A6.P7: the dispatcher consults
/// <see cref="Transition.BspOnlyDispatch(uint)"/> on each cyl/sphere
/// entry and skips it when the entity has BSP, matching retail.
/// </para>
/// </summary>
public class A6P7DispatchRulesTests
{
/// <summary>
/// Retail bit constant <c>HAS_PHYSICS_BSP_PS = 0x10000</c>
/// (acclient.h:2833) and ACE's <c>HasPhysicsBSP = 0x00010000</c>
/// (PhysicsState.cs:24) must match the value we expose on
/// <see cref="PhysicsStateFlags"/>.
/// </summary>
[Fact]
public void PhysicsStateFlags_HasPhysicsBsp_Is_Bit_16()
{
Assert.Equal(0x00010000u, (uint)PhysicsStateFlags.HasPhysicsBsp);
}
/// <summary>
/// The dispatch predicate maps directly from retail's branch at
/// acclient_2013_pseudo_c.txt:276861:
/// <code>
/// ((state &amp; 0x10000) == 0 || ebp_1 != 0 || eax_12 != 0)
/// → cyl + sphere path
/// else
/// → BSP-only path
/// </code>
/// For M1.5 scope <c>ebp_1</c> (PvP-target-player) and <c>eax_12</c>
/// (missile_ignore) are treated as false. The predicate reduces to:
/// "BSP-only iff <c>HAS_PHYSICS_BSP_PS</c> is set".
/// </summary>
[Theory]
[InlineData(0x00010008u, true)] // closed cottage door — STATIC | REPORT | HAS_BSP
[InlineData(0x00010000u, true)] // bare HAS_BSP
[InlineData(0x00110000u, true)] // HAS_BSP | CLOAKED
[InlineData(0x00000008u, false)] // creature — REPORT_COLLISIONS only
[InlineData(0x00000000u, false)] // empty state
[InlineData(0x0000FFFFu, false)] // every bit BELOW 0x10000 set
public void BspOnlyDispatch_RespectsHasPhysicsBspFlag(
uint entityState, bool expected)
{
Assert.Equal(expected, Transition.BspOnlyDispatch(entityState));
}
}