Add a pure Core projectile driver for retail clock quanta, final-state acceleration, 50-unit velocity clamping, world-space angular integration, full-3D AlignPath, and oriented Setup-local sphere sweeps. Preserve exact success versus set_frame-only failure semantics, independent Contact/OnWalkable state, and full-cell identity across all landblock directions. Port OBJECTINFO::missile_ignore with explicit weenie/creature metadata, remove the invented transition-step cap, retain PathClipped without arming PerfectClip, and keep authoritative target identity optional. Add malformed-shape/clock, thin-wall, terrain, target-exclusion, frame-spike, failure-state, and boundary conformance coverage. Retire TS-2 and synchronize the architecture, milestones, roadmap, research oracle, and durable physics memory. Co-Authored-By: Codex <noreply@openai.com>
106 lines
4.1 KiB
C#
106 lines
4.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>
|
||
/// After the whole-object PvP and missile exemption gates, 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 & 0x10000) == 0 || ebp_1 != 0 || eax_12 != 0)
|
||
/// → cyl + sphere path
|
||
/// else
|
||
/// → BSP-only path
|
||
/// </code>
|
||
/// The PvP and missile terms are whole-object early exits in the port.
|
||
/// Once shape dispatch is reached, the predicate is therefore:
|
||
/// "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));
|
||
}
|
||
|
||
/// <summary>
|
||
/// A non-missile mover does not trigger the retail missile exemption.
|
||
/// Retail oracle: <c>OBJECTINFO::missile_ignore</c> pc:274385;
|
||
/// dispatch use pc:276858–276861.
|
||
/// </summary>
|
||
[Fact]
|
||
public void NonMissileMover_DoesNotIgnoreOrdinaryTarget()
|
||
{
|
||
var mover = new ObjectInfo();
|
||
Assert.False(mover.MissileIgnore(
|
||
targetEntityId: 1u,
|
||
targetPhysicsState: 0u,
|
||
targetFlags: EntityCollisionFlags.HasWeenie));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Guard: a BSP-flagged entity that reaches shape dispatch still uses
|
||
/// only its BSP — A6.P7 door behavior remains unchanged.
|
||
/// This protects the A6.P7 cottage-door / dungeon-wall fix.
|
||
/// </summary>
|
||
[Fact]
|
||
public void BspOnlyDispatch_DoorStateStillDispatchesBspOnly()
|
||
{
|
||
// Cottage door state from A6.P7 investigation: 0x10008 = STATIC | REPORT | HAS_BSP
|
||
const uint doorState = 0x00010008u;
|
||
Assert.True(Transition.BspOnlyDispatch(doorState));
|
||
}
|
||
}
|