refactor(physics): delete legacy PhysicsEngine.Resolve/ResolvePlacement/HasCellSurface (C5a, AP-1/AD-1)
Member-wise deletion of the three legacy resolver members named in docs/research/2026-08-05-c5a-contract.md: PhysicsEngine.Resolve, PhysicsEngine.HasCellSurface, and PhysicsEngine.ResolvePlacement. An exhaustive receiver census over src/ found zero production callers of any of the three — every production placement writer already reaches the canonical PhysicsEngine.SetPosition transaction exclusively through RuntimeSetPositionState (three call sites total). The deletion is purely member-wise: IsSpawnCellReady and AdjustPosition, which shared the same source region as the deleted members, are preserved byte-identical — every remaining production caller of either (including PhysicsCameraCollisionProbe, AdjustPosition's sole surviving production caller) is unaffected. Companion changes: - PlayerMovementController's 3-argument SetPosition test overload is renamed to SeedPlacementForTest (internal) and CommitPreparedPosition is deleted; 83 call sites across 19 test files were mechanically renamed to match. - Seven pinned test dispositions from the contract are executed: 3.1 (PhysicsEngineTests.cs: 11 legacy-resolver tests deleted, 6 ResolveWithTransition tests kept), 3.2/3.3/3.4 (re-point to canonical SetPosition, with TransitionScratchDifferentialTests.cs additionally gaining positive IsCommitted assertions after each bitwise comparison so the differential proves a placement actually committed, not just that two possibly-uncommitted results match), 3.5 (Runtime rename), and 3.6 (PlayerMovementPlacementTransactionTests.cs rewritten — its xmldoc now states plainly that the render-root publish moved to RuntimeSetPositionState.cs, but the sticky-release relocation claim was false and is retracted; this disposition's coverage loss is the sticky release path, not silently absorbed elsewhere). - Stale `PhysicsEngine.Resolve`/`Resolve` doc citations in CellTransit.cs, PlayerMovementController.cs, and HeadlessSessionWorldProjection.cs are corrected to name the surviving canonical entry points by symbol (SetPosition, AdjustSetPosition/AdjustPosition, ResolveWithTransition) rather than fragile line numbers. Retires AP-1 and AD-1 in docs/architecture/retail-divergence-register.md: both rows described production zero-delta placement routing remaining on the legacy resolver pending the Slice 4B2/4B route cutover; that resolver no longer exists, so the condition each row tracked is now structurally false rather than merely narrowed. AP-145 (routed through the prior commit) and this commit's AP-1/AD-1 together bring the section counts to 101 AP / 47 AD active rows. Builds on the AP-145 fix (previous commit) — this commit's staged tree was independently rebuilt and its four suites independently rerun on top of that commit before this commit was created, in addition to the combined rebuild/rerun below. Full-solution build: 0 errors (21 pre-existing warnings, all unrelated). Suite results (combined tree): Core 4270/4271 passed (1 skip; the single DatSoundCacheTests concurrent-decode-dedup failure is a known load-sensitive race, confirmed passing standalone and unrelated to this change), Runtime 1176/1176, Headless 86/86, App 4132/4135 (3 skips). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f8e55ba5e4
commit
6921a02744
34 changed files with 508 additions and 946 deletions
|
|
@ -15,7 +15,7 @@ public sealed class LocalPlayerProjectionControllerTests
|
|||
Quaternion.CreateFromAxisAngle(Vector3.UnitX, 0.7f)
|
||||
* Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.4f));
|
||||
var movement = new PlayerMovementController(new PhysicsEngine());
|
||||
movement.SetPosition(Vector3.Zero, cellId, Vector3.Zero);
|
||||
movement.SeedPlacementForTest(Vector3.Zero, cellId, Vector3.Zero);
|
||||
movement.SetBodyOrientation(complete);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
|
|
@ -53,7 +53,7 @@ public sealed class LocalPlayerProjectionControllerTests
|
|||
{
|
||||
const uint cellId = 0x02020001u;
|
||||
var movement = new PlayerMovementController(new PhysicsEngine());
|
||||
movement.SetPosition(new Vector3(12f, 8f, 3f), cellId, Vector3.Zero);
|
||||
movement.SeedPlacementForTest(new Vector3(12f, 8f, 3f), cellId, Vector3.Zero);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 8u,
|
||||
|
|
@ -88,7 +88,7 @@ public sealed class LocalPlayerProjectionControllerTests
|
|||
{
|
||||
const uint cellId = 0x02020001u;
|
||||
var movement = new PlayerMovementController(new PhysicsEngine());
|
||||
movement.SetPosition(new Vector3(12f, 8f, 3f), cellId, Vector3.Zero);
|
||||
movement.SeedPlacementForTest(new Vector3(12f, 8f, 3f), cellId, Vector3.Zero);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 9u,
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ public sealed class MouseLookControllerTests
|
|||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001u, new Vector3(96f, 96f, 50f));
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,47 @@ public sealed class PlayerMovementPlacementTransactionTests
|
|||
private const uint PriorCell = 0x0101_0101u;
|
||||
private const uint DestinationCell = 0x0101_0102u;
|
||||
|
||||
/// <summary>
|
||||
/// C5a (2026-08-05) re-point: <c>PlayerMovementController.CommitPreparedPosition</c>
|
||||
/// is deleted (zero production callers — production placement now arms
|
||||
/// the login-entry leash via <c>ArmConstraintLeashAtCommittedPlacement</c>,
|
||||
/// called from <c>RuntimeLocalPlayerPhysicsPublicationState.ArmFirstEntryConstraintLeash</c>
|
||||
/// AFTER <c>RuntimeSetPositionState.TryApplyDormantLocalActivationFinalCommit</c>
|
||||
/// has already published the shared current cell — see that method's
|
||||
/// doc comment). Architecture-review correction (A5, 2026-08-05): the two
|
||||
/// deleted invariants did NOT relocate symmetrically.
|
||||
/// <b>Render-root publish DID move</b> — it happens inside
|
||||
/// <c>RuntimeSetPositionState.TryApplyDormantLocalActivationFinalCommit</c>
|
||||
/// itself, via <c>_physics.Engine.UpdatePlayerCurrCell(result.CellId)</c>
|
||||
/// (<c>RuntimeSetPositionState.cs:2774</c>).
|
||||
/// <b>Sticky-target release did NOT move anywhere</b> — <c>grep -rn
|
||||
/// "UnStick" src/AcDream.Runtime/</c> returns zero call sites on this
|
||||
/// path; the only local-player <c>UnStick</c> left is
|
||||
/// <c>PlayerMovementController.SetPositionCore</c>'s
|
||||
/// <c>if (publishSharedState) PositionManager?.UnStick();</c>, and
|
||||
/// <c>PreparePositionForCommit</c> passes <c>publishSharedState: false</c>.
|
||||
/// The unstick-at-first-entry-commit behaviour <c>CommitPreparedPosition</c>
|
||||
/// used to perform is simply gone — it was dead code (the method had zero
|
||||
/// production callers before this slice deleted it), so nothing regresses
|
||||
/// today, but no layer pins the invariant "first-entry commit releases any
|
||||
/// sticky target" any more. That is disposition 3.6's one real coverage
|
||||
/// loss (see the C5a commit message).
|
||||
/// This App-layer fixture (a bare <see cref="PlayerMovementController"/> +
|
||||
/// <see cref="EntityPhysicsHost"/> pair, no Runtime activation pipeline)
|
||||
/// cannot drive the Runtime final-commit transaction without
|
||||
/// disproportionate fixture growth — recorded here as the explicit C5a
|
||||
/// scope decision rather than silently dropped.
|
||||
/// What THIS test still proves, unchanged: <see cref="PlayerMovementController.PreparePositionForCommit"/>
|
||||
/// defers the render-root publish exactly as before (<c>controller.CellId</c>
|
||||
/// updates immediately; <c>physics.DataCache.CellGraph.CurrCell</c> and
|
||||
/// the sticky target stay untouched), and the surviving production
|
||||
/// commit-arm, <see cref="PlayerMovementController.ArmConstraintLeashAtCommittedPlacement"/>,
|
||||
/// arms the constraint leash without independently touching either —
|
||||
/// neither the render-root publish (a DIFFERENT layer's job now) nor the
|
||||
/// sticky release (nobody's job any more).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PreparedPosition_DoesNotPublishRenderRootOrUnstickUntilCommit()
|
||||
public void PreparedPosition_DefersRenderRootPublish_CommitArmsLeashOnly()
|
||||
{
|
||||
PhysicsEngine physics = PhysicsWithCells(PriorCell, DestinationCell);
|
||||
physics.UpdatePlayerCurrCell(PriorCell);
|
||||
|
|
@ -38,11 +77,19 @@ public sealed class PlayerMovementPlacementTransactionTests
|
|||
Assert.Equal(DestinationCell, controller.CellId);
|
||||
Assert.Equal(PriorCell, physics.DataCache!.CellGraph.CurrCell!.Id);
|
||||
Assert.Equal(TargetGuid, player.PositionManager.GetStickyObjectId());
|
||||
Assert.False(player.PositionManager.IsFullyConstrained());
|
||||
Assert.Null(player.PositionManager.Constraint);
|
||||
|
||||
controller.CommitPreparedPosition();
|
||||
controller.ArmConstraintLeashAtCommittedPlacement();
|
||||
|
||||
Assert.Equal(DestinationCell, physics.DataCache.CellGraph.CurrCell!.Id);
|
||||
Assert.Equal(0u, player.PositionManager.GetStickyObjectId());
|
||||
// The leash is armed...
|
||||
Assert.NotNull(player.PositionManager.Constraint);
|
||||
Assert.True(player.PositionManager.Constraint!.IsConstrained);
|
||||
// ...but the surviving App-layer commit does NOT publish the render
|
||||
// root or release the sticky target — those now happen inside the
|
||||
// Runtime dormant-activation final commit, before this method runs.
|
||||
Assert.Equal(PriorCell, physics.DataCache.CellGraph.CurrCell!.Id);
|
||||
Assert.Equal(TargetGuid, player.PositionManager.GetStickyObjectId());
|
||||
}
|
||||
|
||||
private static PhysicsEngine PhysicsWithCells(params uint[] cellIds)
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
|
||||
var clock = new RetailObjectQuantumClock();
|
||||
var controller = new PlayerMovementController(engine, clock);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001u, new Vector3(96f, 96f, 50f));
|
||||
Assert.True(clock.IsActive);
|
||||
return controller;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public sealed class PlayerInteractionMovementSinkTests
|
|||
MovementType expectedType)
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
controller.SetPosition(Vector3.Zero, Cell);
|
||||
controller.SeedPlacementForTest(Vector3.Zero, Cell, Vector3.Zero);
|
||||
bool nonAutonomousAtTargetInstall = false;
|
||||
int cancellations = 0;
|
||||
double targetQuantum = 0d;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public sealed class PlayerMovementHiddenTests
|
|||
const uint cellId = 0x01010001u;
|
||||
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
controller.SetPosition(Vector3.Zero, cellId, Vector3.Zero);
|
||||
controller.SeedPlacementForTest(Vector3.Zero, cellId, Vector3.Zero);
|
||||
|
||||
var hosts = new Dictionary<uint, IPhysicsObjHost>();
|
||||
EntityPhysicsHost? playerHost = null;
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ public sealed class CameraFrameControllerTests
|
|||
0f,
|
||||
0f);
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001u, new Vector3(96f, 96f, 50f));
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
Assert.Null(harness.Presentation.BeginProjection);
|
||||
|
||||
harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine());
|
||||
harness.Mode.Controller.SetPosition(
|
||||
harness.Mode.Controller.SeedPlacementForTest(
|
||||
Vector3.Zero,
|
||||
0x20210001u,
|
||||
Vector3.Zero);
|
||||
|
|
@ -117,7 +117,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
Assert.Empty(harness.Streaming.Reservations);
|
||||
|
||||
harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine());
|
||||
harness.Mode.Controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
||||
harness.Mode.Controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
||||
harness.Controller.Tick(0.016f);
|
||||
|
||||
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
||||
|
|
@ -156,7 +156,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
harness.Mode.RebuildOnEnter = () =>
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
||||
controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
||||
return controller;
|
||||
};
|
||||
harness.Controller.Tick(0.016f);
|
||||
|
|
@ -353,7 +353,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
// AddSyntheticIndoorCell doc comment for why CommitLandblockCollision
|
||||
// alone (terrain-only) cannot make it spawn-ready.
|
||||
harness.AddSyntheticIndoorCell(0x30310100u);
|
||||
harness.Mode.Controller!.SetPosition(
|
||||
harness.Mode.Controller!.SeedPlacementForTest(
|
||||
Vector3.Zero,
|
||||
0x20210001u,
|
||||
Vector3.Zero);
|
||||
|
|
@ -649,7 +649,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
// body/cell/orientation BEFORE this suffix runs. Reproduce that
|
||||
// pre-state directly rather than re-resolving it here - Place() no
|
||||
// longer resolves anything (D-T4).
|
||||
controllerSlot.Controller.SetPosition(position, cell, position);
|
||||
controllerSlot.Controller.SeedPlacementForTest(position, cell, position);
|
||||
controllerSlot.Controller.SetBodyOrientation(rotation);
|
||||
var cameras = new ChaseCameraInputState
|
||||
{
|
||||
|
|
@ -727,14 +727,14 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
{
|
||||
Controller = new PlayerMovementController(new PhysicsEngine()),
|
||||
};
|
||||
controllerSlot.Controller.SetPosition(
|
||||
controllerSlot.Controller.SeedPlacementForTest(
|
||||
Vector3.Zero,
|
||||
sourceCell,
|
||||
Vector3.Zero);
|
||||
// C4 route 3: reproduce the canonical Runtime commit's pre-state
|
||||
// (body/cell already resolved) - Place() no longer resolves it.
|
||||
var destinationPosition = new Vector3(12f, 24f, 6f);
|
||||
controllerSlot.Controller.SetPosition(
|
||||
controllerSlot.Controller.SeedPlacementForTest(
|
||||
destinationPosition,
|
||||
destinationCell,
|
||||
destinationPosition);
|
||||
|
|
@ -1303,7 +1303,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
{
|
||||
_order = order;
|
||||
Controller = new PlayerMovementController(new PhysicsEngine());
|
||||
Controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
||||
Controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
||||
}
|
||||
|
||||
public PlayerMovementController? Controller { get; set; }
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ public sealed class StreamingFrameControllerTests
|
|||
var fixture = new Fixture(initializeOrigin: true);
|
||||
fixture.Mode.IsPlayerMode = true;
|
||||
var player = new PlayerMovementController(new PhysicsEngine());
|
||||
player.SetPosition(
|
||||
player.SeedPlacementForTest(
|
||||
new Vector3(383f, -0.01f, 0f),
|
||||
0x0A140001u,
|
||||
new Vector3(1f, 1f, 0f));
|
||||
|
|
@ -159,7 +159,7 @@ public sealed class StreamingFrameControllerTests
|
|||
{
|
||||
State = PlayerState.PortalSpace,
|
||||
};
|
||||
player.SetPosition(
|
||||
player.SeedPlacementForTest(
|
||||
new Vector3(1000f, -1000f, 0f),
|
||||
0x0A140001u,
|
||||
new Vector3(1f, 1f, 0f));
|
||||
|
|
@ -179,7 +179,7 @@ public sealed class StreamingFrameControllerTests
|
|||
var fixture = new Fixture(initializeOrigin: true);
|
||||
fixture.Mode.IsPlayerMode = true;
|
||||
var player = new PlayerMovementController(new PhysicsEngine());
|
||||
player.SetPosition(
|
||||
player.SeedPlacementForTest(
|
||||
new Vector3(1000f, -1000f, 0f),
|
||||
0x0A140001u,
|
||||
new Vector3(1f, 1f, 0f));
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ public class DispatcherToMovementIntegrationTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f; // facing +X
|
||||
|
||||
var kb = new FakeKb();
|
||||
|
|
@ -117,7 +117,7 @@ public class DispatcherToMovementIntegrationTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
|
||||
var kb = new FakeKb();
|
||||
|
|
@ -165,12 +165,12 @@ public class DispatcherToMovementIntegrationTests
|
|||
// frame's MouseDeltaX doesn't leak into the second run via Yaw.
|
||||
var engineA = MakeFlatEngine();
|
||||
var ctrlA = new PlayerMovementController(engineA);
|
||||
ctrlA.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
ctrlA.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
ctrlA.Yaw = 0f;
|
||||
|
||||
var engineB = MakeFlatEngine();
|
||||
var ctrlB = new PlayerMovementController(engineB);
|
||||
ctrlB.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
ctrlB.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
ctrlB.Yaw = 0f;
|
||||
|
||||
var inputZero = new MovementInput(Forward: true, MouseDeltaX: 0f);
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ public class PlayerMoveToCutoverTests
|
|||
controller.Motion.RemoveLinkAnimations = () => seq.Manager.HandleEnterWorld();
|
||||
controller.Motion.InitializeMotionTables = () => seq.Manager.InitializeState();
|
||||
controller.Motion.CheckForCompletedMotions = seq.Manager.CheckForCompletedMotions;
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f; // heading 90 = facing +X
|
||||
|
||||
const uint selfGuid = 0x5000000Au;
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ public class W6EdgeDrivenMovementTests
|
|||
controller.Motion.RemoveLinkAnimations = () => s.Manager.HandleEnterWorld();
|
||||
controller.Motion.InitializeMotionTables = () => s.Manager.InitializeState();
|
||||
controller.Motion.CheckForCompletedMotions = s.Manager.CheckForCompletedMotions;
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
return controller;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ public class CellarUpTrajectoryReplayTests : IDisposable
|
|||
{
|
||||
var (engine, _) = BuildEngineWithCellarFixtures();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(InitialSphereWorld, CellarId);
|
||||
controller.SeedPlacementForTest(InitialSphereWorld, CellarId, InitialSphereWorld);
|
||||
|
||||
var settled = controller.Update(1f / 60f, new MovementInput());
|
||||
var basePos = settled.Position;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
|
|
@ -6,6 +7,23 @@ namespace AcDream.Core.Tests.Physics;
|
|||
/// <summary>
|
||||
/// Enter-world overlap conformance for retail
|
||||
/// <c>CTransition::find_placement_pos</c> (0x0050BA50).
|
||||
///
|
||||
/// <para>
|
||||
/// C5a (2026-08-05) re-point: the legacy <c>PhysicsEngine.ResolvePlacement</c>
|
||||
/// this test originally drove is deleted (zero production callers). Canonical
|
||||
/// <c>PhysicsEngine.SetPosition</c> reaches the SAME ring-search machinery —
|
||||
/// <c>SetPositionInternal</c> sets <c>InsertType.Placement</c> before calling
|
||||
/// <c>Transition.FindValidPosition</c>, which (for any non-<c>Transition</c>
|
||||
/// insert type) dispatches to <c>FindPlacementPosition</c>
|
||||
/// (<c>TransitionTypes.cs</c>), and THAT method calls this exact
|
||||
/// <c>FindPlacementPos</c> ring search after its initial-placement insert —
|
||||
/// so this is not a new code path, only a new entry point onto the one the
|
||||
/// legacy method used directly. Sphere shape is reconstructed as the
|
||||
/// two-sphere capsule the legacy scalar <c>(radius, height)</c> overload
|
||||
/// built internally (origin (0,0,radius) + (0,0,height-radius), both
|
||||
/// radius-sized), since canonical <c>SetPosition</c> takes a pre-built
|
||||
/// sphere list rather than a radius/height pair.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class InitialPlacementOverlapTests
|
||||
{
|
||||
|
|
@ -14,6 +32,7 @@ public sealed class InitialPlacementOverlapTests
|
|||
private const uint PlayerId = 0x50000001u;
|
||||
private const uint MonsterId = 0x50000002u;
|
||||
private const float Radius = 0.48f;
|
||||
private const float SphereHeight = 1.835f;
|
||||
|
||||
[Fact]
|
||||
public void PlayerReloggingInsideMonster_SearchesOutToNearestClearRing()
|
||||
|
|
@ -39,17 +58,30 @@ public sealed class InitialPlacementOverlapTests
|
|||
RegisterSphere(engine, MonsterId, monsterCenter,
|
||||
EntityCollisionFlags.IsCreature);
|
||||
|
||||
ResolveResult result = engine.ResolvePlacement(
|
||||
savedFeet,
|
||||
Cell,
|
||||
sphereRadius: Radius,
|
||||
sphereHeight: 1.835f,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: PlayerId);
|
||||
// The mover's own two-sphere capsule, matching the legacy scalar
|
||||
// InitPath(sphereRadius: 0.48, sphereHeight: 1.835) reconstruction:
|
||||
// a foot sphere at (0,0,radius) and a head sphere at
|
||||
// (0,0,height-radius), both radius-sized.
|
||||
ImmutableArray<FlatCollisionSphere> spheres = ImmutableArray.Create(
|
||||
new FlatCollisionSphere(new Vector3(0f, 0f, Radius), Radius),
|
||||
new FlatCollisionSphere(new Vector3(0f, 0f, SphereHeight - Radius), Radius));
|
||||
|
||||
Assert.True(result.Ok);
|
||||
PhysicsSetPositionResult result = engine.SetPosition(
|
||||
new PhysicsSetPositionRequest(
|
||||
Position: savedFeet,
|
||||
Orientation: Quaternion.Identity,
|
||||
CellId: Cell,
|
||||
CellLocalPosition: savedFeet,
|
||||
Spheres: spheres,
|
||||
Scale: 1f,
|
||||
StepUpHeight: 0.4f,
|
||||
StepDownHeight: 0.4f,
|
||||
MoverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
MovingEntityId: PlayerId,
|
||||
Flags: PhysicsSetPositionFlags.Placement
|
||||
| PhysicsSetPositionFlags.Slide));
|
||||
|
||||
Assert.True(result.IsCommitted);
|
||||
Assert.True(result.InContact);
|
||||
Assert.True(result.OnWalkable);
|
||||
Assert.Equal(Cell, result.ContactPlaneCellId);
|
||||
|
|
|
|||
|
|
@ -9,32 +9,49 @@ using Xunit;
|
|||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// #133 (Bug A) — the validated-claim placement branch of
|
||||
/// <see cref="PhysicsEngine.Resolve"/> must return the VALIDATED claim's own
|
||||
/// full cell id, NOT <c>lbPrefix | (cellId & 0xFFFF)</c>.
|
||||
/// #133 (Bug A) — a validated dungeon claim's landblock prefix is
|
||||
/// AUTHORITATIVE; a candidate world position that also falls inside a
|
||||
/// neighbouring RESIDENT landblock's <c>[0,192)</c> local bounds must NOT
|
||||
/// re-stamp the claim with that neighbour's prefix.
|
||||
///
|
||||
/// <para>
|
||||
/// C5a (2026-08-05) re-point: the legacy <c>PhysicsEngine.Resolve</c> this
|
||||
/// test originally drove is deleted (zero production callers — every
|
||||
/// production placement writer reaches canonical
|
||||
/// <c>PhysicsEngine.SetPosition</c> through <c>RuntimeSetPositionState</c>).
|
||||
/// The defect this test pins lived entirely inside <c>Resolve</c>'s own
|
||||
/// <c>lbPrefix</c> resident-landblock scan (find a resident block whose
|
||||
/// <c>[0,192)</c> bounds contain the candidate XY, then stamp
|
||||
/// <c>lbPrefix | (cellId & 0xFFFF)</c> — the scan has no notion that
|
||||
/// the claim's OWN landblock can legitimately fail its own bounds test,
|
||||
/// which is exactly what a dungeon EnvCell's negative local Y does). The
|
||||
/// canonical <c>SetPosition</c>/<c>SetPositionInternal</c> pipeline has no
|
||||
/// equivalent lbPrefix scan: a validated claim (<c>AdjustSeedCell</c>
|
||||
/// confirms the seed cell contains the point) is committed with ITS OWN
|
||||
/// cell id, full stop — the defect class cannot recur there by
|
||||
/// construction. This test now proves that positively: drive the SAME
|
||||
/// geometry as the original #133 capture (dungeon claim
|
||||
/// <c>0x00070143</c> at dungeon-local <c>(70,-60,0.01)</c>, with a
|
||||
/// still-resident Holtburg neighbour block whose <c>[0,192)</c> bounds
|
||||
/// contain the same world XY) through canonical
|
||||
/// <c>PhysicsEngine.SetPosition</c> and assert the committed cell keeps
|
||||
/// the <c>0x0007</c> prefix.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <c>lbPrefix</c> is found by scanning resident landblocks for one whose
|
||||
/// <c>[0,192)</c> local bounds contain the candidate XY. A dungeon EnvCell's
|
||||
/// local Y can be NEGATIVE relative to its own landblock (the live capture:
|
||||
/// server teleport to dungeon cell <c>0x00070143</c> at local <c>(70,-60,0.01)</c>).
|
||||
/// The dungeon landblock fails the <c>localY >= 0</c> bounds test, so the loop
|
||||
/// instead matches a still-resident NEIGHBOURING block (a Holtburg landblock
|
||||
/// whose world bounds happen to contain the same XY) and sets
|
||||
/// <c>lbPrefix = 0xA9B30000</c>. The old code then returned
|
||||
/// The dungeon landblock fails the <c>localY >= 0</c> bounds test, so the
|
||||
/// legacy scan instead matched a still-resident NEIGHBOURING block (a
|
||||
/// Holtburg landblock whose world bounds happen to contain the same XY) and
|
||||
/// set <c>lbPrefix = 0xA9B30000</c>. The old code then returned
|
||||
/// <c>0xA9B30000 | 0x0143 = 0xA9B30143</c>, re-stamping the validated dungeon
|
||||
/// claim with the wrong landblock — the client mis-resolved the player into
|
||||
/// Holtburg and spammed ACE with rejected moves
|
||||
/// (<c>movement pre-validation failed from 00070143 to A9B30143</c>).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The validated claim's prefix is authoritative; a position falling in a
|
||||
/// neighbouring resident landblock must not re-stamp it. This test reproduces
|
||||
/// the exact geometry of the capture (dungeon claim in landblock <c>0x0007</c>,
|
||||
/// candidate XY also inside resident Holtburg <c>0xA9B3</c>) and asserts the
|
||||
/// returned cell keeps its <c>0x0007</c> prefix.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class Issue133DungeonTeleportPrefixTests
|
||||
{
|
||||
|
|
@ -53,13 +70,29 @@ public class Issue133DungeonTeleportPrefixTests
|
|||
{
|
||||
var engine = BuildEngine();
|
||||
|
||||
// Zero delta = the snap shape (teleport arrival). cellId is the dungeon
|
||||
// claim; the candidate XY also falls inside the resident Holtburg block.
|
||||
var result = engine.Resolve(SpawnPos, DungeonCellId, delta: Vector3.Zero, stepUpHeight: 0.5f);
|
||||
// Zero-delta teleport arrival — canonical SetPosition's placement
|
||||
// shape for a server-restored (cell, position) pair. The dungeon
|
||||
// claim is the seed cell; the candidate XY also falls inside the
|
||||
// resident Holtburg block's [0,192) bounds.
|
||||
PhysicsSetPositionResult result = engine.SetPosition(
|
||||
new PhysicsSetPositionRequest(
|
||||
Position: SpawnPos,
|
||||
Orientation: Quaternion.Identity,
|
||||
CellId: DungeonCellId,
|
||||
CellLocalPosition: SpawnPos,
|
||||
Spheres: default,
|
||||
Scale: 1f,
|
||||
StepUpHeight: 0.4f,
|
||||
StepDownHeight: 0.4f,
|
||||
Flags: PhysicsSetPositionFlags.Placement
|
||||
| PhysicsSetPositionFlags.Teleport
|
||||
| PhysicsSetPositionFlags.Slide));
|
||||
|
||||
Assert.True(result.IsOnGround);
|
||||
Assert.True(result.IsCommitted);
|
||||
// The validated claim's prefix is authoritative — high word stays 0x0007,
|
||||
// NOT re-stamped to the neighbouring Holtburg 0xA9B3.
|
||||
// NOT re-stamped to the neighbouring Holtburg 0xA9B3. Canonical
|
||||
// SetPosition has no lbPrefix scan to get this wrong in the first
|
||||
// place; this assertion is the positive proof of that.
|
||||
Assert.Equal(DungeonCellId, result.CellId);
|
||||
Assert.Equal(DungeonLandblock, result.CellId & 0xFFFF0000u);
|
||||
}
|
||||
|
|
@ -73,12 +106,13 @@ public class Issue133DungeonTeleportPrefixTests
|
|||
|
||||
// The dungeon cell: a Leaf CellBSP contains any point, so AdjustPosition
|
||||
// validates the claim (returns it with found=true). Its Resolved set has
|
||||
// one walkable floor polygon at z=0 under the spawn XY so the #111
|
||||
// validated-claim branch grounds onto it.
|
||||
// one walkable floor polygon at z=0 under the spawn XY so placement
|
||||
// grounds onto it.
|
||||
cache.RegisterCellStructForTest(DungeonCellId, MakeDungeonCell());
|
||||
|
||||
// Resident Holtburg block at world origin: its [0,192) bounds CONTAIN the
|
||||
// candidate XY (70,70). This is the block the lbPrefix loop wrongly matched.
|
||||
// candidate XY (70,70). This is the block the legacy lbPrefix loop wrongly
|
||||
// matched.
|
||||
engine.AddLandblock(
|
||||
landblockId: HoltburgLandblock,
|
||||
terrain: FlatTerrain(),
|
||||
|
|
@ -89,7 +123,7 @@ public class Issue133DungeonTeleportPrefixTests
|
|||
|
||||
// The dungeon's own landblock, offset so the candidate XY produces a
|
||||
// NEGATIVE dungeon-local Y (70 - 130 = -60) → it FAILS the [0,192) bounds
|
||||
// test, which is exactly why the old code fell through to the Holtburg
|
||||
// test, which is exactly why the legacy code fell through to the Holtburg
|
||||
// prefix. Registered so the scenario is faithful (a resident dungeon block
|
||||
// whose local bounds don't cover the EnvCell's negative-Y position).
|
||||
engine.AddLandblock(
|
||||
|
|
|
|||
|
|
@ -31,192 +31,6 @@ public class PhysicsEngineTests
|
|||
return engine;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_ZeroDeltaSnapTrace_IsExplicitlyOptIn()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var diagnostics = new List<string>();
|
||||
|
||||
Assert.Null(engine.DiagnosticLog);
|
||||
_ = engine.Resolve(
|
||||
Vector3.Zero,
|
||||
cellId: 0xA9B40001u,
|
||||
delta: Vector3.Zero,
|
||||
stepUpHeight: 0.5f);
|
||||
|
||||
engine.DiagnosticLog = diagnostics.Add;
|
||||
_ = engine.Resolve(
|
||||
Vector3.Zero,
|
||||
cellId: 0xA9B40001u,
|
||||
delta: Vector3.Zero,
|
||||
stepUpHeight: 0.5f);
|
||||
|
||||
Assert.Single(diagnostics);
|
||||
Assert.StartsWith(
|
||||
"[snap] claim=0xA9B40001",
|
||||
diagnostics[0],
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_FlatTerrain_ZMatchesTerrain()
|
||||
{
|
||||
var engine = MakeFlatEngine(terrainZ: 50f);
|
||||
|
||||
var result = engine.Resolve(
|
||||
new Vector3(96f, 96f, 50f), cellId: 0x0001, delta: new Vector3(1f, 0f, 0f),
|
||||
stepUpHeight: 2f);
|
||||
|
||||
Assert.Equal(50f, result.Position.Z, precision: 1);
|
||||
Assert.True(result.IsOnGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_WalkUpSmallSlope_Accepted()
|
||||
{
|
||||
// Heights slope from 50 to 52 across X — small enough for step height.
|
||||
var heights = new byte[81];
|
||||
for (int x = 0; x < 9; x++)
|
||||
for (int y = 0; y < 9; y++)
|
||||
heights[x * 9 + y] = (byte)(50 + x / 4); // gentle slope
|
||||
|
||||
var engine = new PhysicsEngine();
|
||||
var terrain = new TerrainSurface(heights, LinearHeightTable());
|
||||
engine.AddLandblock(0xA9B4FFFFu, terrain, Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
var result = engine.Resolve(
|
||||
new Vector3(48f, 96f, 50f), cellId: 0x0001, delta: new Vector3(48f, 0f, 0f),
|
||||
stepUpHeight: 5f);
|
||||
|
||||
Assert.True(result.IsOnGround);
|
||||
Assert.True(result.Position.Z >= 50f); // moved uphill
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_StepUpExceedsHeight_MovementBlocked()
|
||||
{
|
||||
// Heights jump sharply: left half = 50, right half = 100.
|
||||
var heights = new byte[81];
|
||||
for (int x = 0; x < 9; x++)
|
||||
for (int y = 0; y < 9; y++)
|
||||
heights[x * 9 + y] = (byte)(x < 5 ? 50 : 100);
|
||||
|
||||
var engine = new PhysicsEngine();
|
||||
var terrain = new TerrainSurface(heights, LinearHeightTable());
|
||||
engine.AddLandblock(0xA9B4FFFFu, terrain, Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
// Try to walk from the low side to the high side.
|
||||
var result = engine.Resolve(
|
||||
new Vector3(96f, 96f, 50f), cellId: 0x0001, delta: new Vector3(48f, 0f, 0f),
|
||||
stepUpHeight: 2f);
|
||||
|
||||
// Movement should be blocked — Z delta (50→100) exceeds step height (2).
|
||||
Assert.Equal(96f, result.Position.X, precision: 1); // didn't move
|
||||
Assert.True(result.IsOnGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_OutdoorThroughPortal_TransitionsToIndoor()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var terrain = new TerrainSurface(FlatHeightmap(50), LinearHeightTable());
|
||||
|
||||
// A CellSurface for the indoor cell with floor at Z=50.
|
||||
var cellVerts = new Dictionary<ushort, Vector3>
|
||||
{
|
||||
[0] = new(40f, 40f, 50f),
|
||||
[1] = new(60f, 40f, 50f),
|
||||
[2] = new(60f, 60f, 50f),
|
||||
[3] = new(40f, 60f, 50f),
|
||||
};
|
||||
var cellPolys = new List<List<short>> { new() { 0, 1, 2, 3 } };
|
||||
var cell = new CellSurface(0x0100, cellVerts, cellPolys);
|
||||
|
||||
// A portal plane at X=45 (vertical plane facing +X).
|
||||
// OwnerCellId = 0x0100 (the indoor cell), TargetCellId = 0xFFFF (faces outdoor).
|
||||
// From outside, walking through this portal enters OwnerCellId.
|
||||
var portal = PortalPlane.FromVertices(
|
||||
new Vector3(45f, 40f, 45f),
|
||||
new Vector3(45f, 60f, 45f),
|
||||
new Vector3(45f, 60f, 55f),
|
||||
targetCellId: 0xFFFF, ownerCellId: 0x0100, flags: 0);
|
||||
|
||||
engine.AddLandblock(0xA9B4FFFFu, terrain, new[] { cell }, new[] { portal },
|
||||
worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
// Walk from X=40 (outdoor) through X=45 (portal) to X=50 (indoor).
|
||||
var result = engine.Resolve(
|
||||
new Vector3(40f, 50f, 50f), cellId: 0x0001, delta: new Vector3(10f, 0f, 0f),
|
||||
stepUpHeight: 5f);
|
||||
|
||||
// Should have transitioned to indoor cell 0x0100.
|
||||
Assert.Equal(0x0100u, result.CellId & 0xFFFFu);
|
||||
Assert.True(result.IsOnGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_IndoorThroughExitPortal_TransitionsToOutdoor()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var terrain = new TerrainSurface(FlatHeightmap(50), LinearHeightTable());
|
||||
|
||||
var cellVerts = new Dictionary<ushort, Vector3>
|
||||
{
|
||||
[0] = new(40f, 40f, 50f),
|
||||
[1] = new(60f, 40f, 50f),
|
||||
[2] = new(60f, 60f, 50f),
|
||||
[3] = new(40f, 60f, 50f),
|
||||
};
|
||||
var cellPolys = new List<List<short>> { new() { 0, 1, 2, 3 } };
|
||||
var cell = new CellSurface(0x0100, cellVerts, cellPolys);
|
||||
|
||||
// Same portal geometry — OwnerCellId = 0x0100, TargetCellId = 0xFFFF (outdoor exit).
|
||||
var portal = PortalPlane.FromVertices(
|
||||
new Vector3(45f, 40f, 45f),
|
||||
new Vector3(45f, 60f, 45f),
|
||||
new Vector3(45f, 60f, 55f),
|
||||
targetCellId: 0xFFFF, ownerCellId: 0x0100, flags: 0);
|
||||
|
||||
engine.AddLandblock(0xA9B4FFFFu, terrain, new[] { cell }, new[] { portal },
|
||||
worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
// Walk from X=50 (indoor) through X=45 (portal) to X=40 (outdoor).
|
||||
var result = engine.Resolve(
|
||||
new Vector3(50f, 50f, 50f), cellId: 0x0100, delta: new Vector3(-10f, 0f, 0f),
|
||||
stepUpHeight: 5f);
|
||||
|
||||
// Should have transitioned to outdoor.
|
||||
Assert.True((result.CellId & 0xFFFFu) < 0x0100u);
|
||||
Assert.True(result.IsOnGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_LandblockBoundary_PicksAdjacentTerrain()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
|
||||
// Landblock A: flat at Z=50, offset at X=0.
|
||||
var terrainA = new TerrainSurface(FlatHeightmap(50), LinearHeightTable());
|
||||
engine.AddLandblock(0xA9B4FFFFu, terrainA, Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(), worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
// Landblock B: flat at Z=60, offset at X=192 (adjacent east).
|
||||
var terrainB = new TerrainSurface(FlatHeightmap(60), LinearHeightTable());
|
||||
engine.AddLandblock(0xAAB4FFFFu, terrainB, Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(), worldOffsetX: 192f, worldOffsetY: 0f);
|
||||
|
||||
// Walk from X=190 (landblock A) across to X=194 (landblock B).
|
||||
var result = engine.Resolve(
|
||||
new Vector3(190f, 96f, 50f), cellId: 0x0001, delta: new Vector3(4f, 0f, 0f),
|
||||
stepUpHeight: 15f);
|
||||
|
||||
// Should be at Z=60 (landblock B's terrain) and position X≈194.
|
||||
Assert.Equal(60f, result.Position.Z, precision: 1);
|
||||
Assert.True(result.Position.X > 192f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWithTransition_OutdoorCellBoundary_UpdatesLowCellId()
|
||||
{
|
||||
|
|
@ -368,102 +182,6 @@ public class PhysicsEngineTests
|
|||
Assert.Equal(0xAAB40001u, result.CellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_LeaveIndoorCell_TransitionsToOutdoor()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var terrain = new TerrainSurface(FlatHeightmap(50), LinearHeightTable());
|
||||
|
||||
var cellVerts = new Dictionary<ushort, Vector3>
|
||||
{
|
||||
[0] = new(40f, 40f, 55f),
|
||||
[1] = new(60f, 40f, 55f),
|
||||
[2] = new(60f, 60f, 55f),
|
||||
[3] = new(40f, 60f, 55f),
|
||||
};
|
||||
var cellPolys = new List<List<short>> { new() { 0, 1, 2, 3 } };
|
||||
var cell = new CellSurface(0x0100, cellVerts, cellPolys);
|
||||
|
||||
engine.AddLandblock(0xA9B4FFFFu, terrain, new[] { cell }, Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
// Start inside the cell, walk out.
|
||||
var result = engine.Resolve(
|
||||
new Vector3(50f, 50f, 55f), cellId: 0x0100, delta: new Vector3(-20f, 0f, 0f),
|
||||
stepUpHeight: 10f);
|
||||
|
||||
// Should transition back to outdoor. (#106 follow-up: masked compare —
|
||||
// Resolve now returns FULL prefixed cell ids; the old unmasked
|
||||
// `CellId < 0x0100` assertion codified the bare-low-word bug.)
|
||||
Assert.True((result.CellId & 0xFFFFu) < 0x0100u);
|
||||
Assert.Equal(0xA9B40000u, result.CellId & 0xFFFF0000u);
|
||||
Assert.Equal(50f, result.Position.Z, precision: 1);
|
||||
Assert.True(result.IsOnGround);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #106 follow-up (2026-06-09): the live boundary-walk gate was sabotaged by
|
||||
/// the teleport-arrival snap (GameWindow.cs:4869) receiving a BARE indoor
|
||||
/// cell id from Resolve (`0x0000013F`). A bare indoor id wedges the whole
|
||||
/// membership chain: GetCellStruct misses (no wall BSP), the #98 gate reads
|
||||
/// "indoor primary" and skips the outdoor object sweep (no collision with
|
||||
/// anything), and FindCellSet can never re-resolve a malformed id. Resolve
|
||||
/// MUST return the matched landblock's full 32-bit cell id on every
|
||||
/// computed exit — the same convention its own inputs use.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_IndoorStay_ReturnsFullPrefixedCellId()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var terrain = new TerrainSurface(FlatHeightmap(50), LinearHeightTable());
|
||||
|
||||
var cellVerts = new Dictionary<ushort, Vector3>
|
||||
{
|
||||
[0] = new(40f, 40f, 55f),
|
||||
[1] = new(60f, 40f, 55f),
|
||||
[2] = new(60f, 60f, 55f),
|
||||
[3] = new(40f, 60f, 55f),
|
||||
};
|
||||
var cellPolys = new List<List<short>> { new() { 0, 1, 2, 3 } };
|
||||
var cell = new CellSurface(0x0100, cellVerts, cellPolys);
|
||||
|
||||
engine.AddLandblock(0xA9B4FFFFu, terrain, new[] { cell }, Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
// The teleport shape: full indoor cell id in, zero delta (pure snap).
|
||||
var result = engine.Resolve(
|
||||
new Vector3(50f, 50f, 55f), cellId: 0xA9B40100u, delta: Vector3.Zero,
|
||||
stepUpHeight: 5f);
|
||||
|
||||
Assert.Equal(0xA9B40100u, result.CellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_OutdoorStay_ReturnsFullPrefixedCellId()
|
||||
{
|
||||
var engine = MakeFlatEngine(terrainZ: 50f);
|
||||
|
||||
var result = engine.Resolve(
|
||||
new Vector3(96f, 96f, 50f), cellId: 0xA9B40029u, delta: new Vector3(1f, 0f, 0f),
|
||||
stepUpHeight: 2f);
|
||||
|
||||
// (97, 96) is over grid (4, 4) → low = 4*8+4+1 = 0x25, prefixed.
|
||||
Assert.Equal(0xA9B40025u, result.CellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_NoSurfaceUnderEntity_NotOnGround()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
// No landblocks loaded — entity is floating in void.
|
||||
|
||||
var result = engine.Resolve(
|
||||
new Vector3(0f, 0f, 100f), cellId: 0x0001, delta: Vector3.Zero,
|
||||
stepUpHeight: 2f);
|
||||
|
||||
Assert.False(result.IsOnGround);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #42 lock — when the moving entity's own ShadowEntry is registered
|
||||
/// in <see cref="ShadowObjectRegistry"/> at the body's exact position
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -176,57 +177,95 @@ public sealed class TransitionScratchDifferentialTests
|
|||
Coverage: CoverageKind.Failure));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C5a (2026-08-05) re-point: the legacy <c>PhysicsEngine.ResolvePlacement</c>
|
||||
/// this arm originally drove is deleted (zero production callers).
|
||||
/// Canonical <c>PhysicsEngine.SetPosition</c> reaches the exact same
|
||||
/// <c>FindPlacementPos</c> ring search — <c>SetPositionInternal</c> sets
|
||||
/// <c>InsertType.Placement</c> before <c>Transition.FindValidPosition</c>,
|
||||
/// which dispatches to <c>FindPlacementPosition</c>, which calls
|
||||
/// <c>FindPlacementPos</c> — so this is the same scratch-reuse surface
|
||||
/// under a new entry point, not a new code path. The bitwise
|
||||
/// fresh-vs-reused comparison and the second-identity leak check are both
|
||||
/// preserved verbatim, now via <see cref="AssertSetPositionBitwise"/>
|
||||
/// over <see cref="PhysicsSetPositionResult"/>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ReusedScratch_MatchesFreshPlacementSearch()
|
||||
{
|
||||
PhysicsEngine fresh = BuildPlacementEngine(reuse: false);
|
||||
PhysicsEngine reused = BuildPlacementEngine(reuse: true);
|
||||
|
||||
ResolveResult expected = fresh.ResolvePlacement(
|
||||
new Vector3(10f, 10f, 0f),
|
||||
Cell,
|
||||
0.48f,
|
||||
1.835f,
|
||||
0.40f,
|
||||
0.40f,
|
||||
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
0x50000101u);
|
||||
ResolveResult actual = reused.ResolvePlacement(
|
||||
new Vector3(10f, 10f, 0f),
|
||||
Cell,
|
||||
0.48f,
|
||||
1.835f,
|
||||
0.40f,
|
||||
0.40f,
|
||||
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
0x50000101u);
|
||||
PhysicsSetPositionResult expected = fresh.SetPosition(
|
||||
PlacementRequest(
|
||||
new Vector3(10f, 10f, 0f),
|
||||
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
0x50000101u));
|
||||
PhysicsSetPositionResult actual = reused.SetPosition(
|
||||
PlacementRequest(
|
||||
new Vector3(10f, 10f, 0f),
|
||||
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
0x50000101u));
|
||||
|
||||
AssertResolveBitwise(expected, actual, "placement");
|
||||
AssertSetPositionBitwise(expected, actual, "placement");
|
||||
// A6 (architecture review, 2026-08-05): the bitwise comparison alone
|
||||
// proves fresh and reused AGREE, not that either actually placed
|
||||
// anything — a future regression that makes SetPosition fail
|
||||
// IDENTICALLY on both engines would leave this differential green
|
||||
// while the scratch-reuse surface it exists to guard goes
|
||||
// unexercised. Assert the positive fact the comparison itself can't:
|
||||
// this placement committed.
|
||||
Assert.True(expected.IsCommitted, "fresh engine did not commit the placement");
|
||||
Assert.True(actual.IsCommitted, "reused engine did not commit the placement");
|
||||
|
||||
// A second placement with a different self identity proves that the
|
||||
// previous mover and collision-GUID list cannot leak through the lease.
|
||||
expected = fresh.ResolvePlacement(
|
||||
new Vector3(11f, 10f, 0f),
|
||||
Cell,
|
||||
0.48f,
|
||||
1.835f,
|
||||
0.40f,
|
||||
0.40f,
|
||||
ObjectInfoState.EdgeSlide,
|
||||
0x80000102u);
|
||||
actual = reused.ResolvePlacement(
|
||||
new Vector3(11f, 10f, 0f),
|
||||
Cell,
|
||||
0.48f,
|
||||
1.835f,
|
||||
0.40f,
|
||||
0.40f,
|
||||
ObjectInfoState.EdgeSlide,
|
||||
0x80000102u);
|
||||
expected = fresh.SetPosition(
|
||||
PlacementRequest(
|
||||
new Vector3(11f, 10f, 0f),
|
||||
ObjectInfoState.EdgeSlide,
|
||||
0x80000102u));
|
||||
actual = reused.SetPosition(
|
||||
PlacementRequest(
|
||||
new Vector3(11f, 10f, 0f),
|
||||
ObjectInfoState.EdgeSlide,
|
||||
0x80000102u));
|
||||
|
||||
AssertResolveBitwise(expected, actual, "placement after hostile identity");
|
||||
AssertSetPositionBitwise(expected, actual, "placement after hostile identity");
|
||||
Assert.True(
|
||||
expected.IsCommitted,
|
||||
"fresh engine did not commit the second placement");
|
||||
Assert.True(
|
||||
actual.IsCommitted,
|
||||
"reused engine did not commit the second placement");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the two-sphere capsule request the legacy scalar
|
||||
/// <c>InitPath(sphereRadius: 0.48, sphereHeight: 1.835)</c> reconstructed
|
||||
/// internally (origin (0,0,radius) + (0,0,height-radius), both
|
||||
/// radius-sized), so canonical <c>SetPosition</c> sees the identical
|
||||
/// mover shape the legacy <c>ResolvePlacement</c> call did.
|
||||
/// </summary>
|
||||
private static PhysicsSetPositionRequest PlacementRequest(
|
||||
Vector3 position,
|
||||
ObjectInfoState moverFlags,
|
||||
uint movingEntityId) => new(
|
||||
Position: position,
|
||||
Orientation: Quaternion.Identity,
|
||||
CellId: Cell,
|
||||
CellLocalPosition: position,
|
||||
Spheres: ImmutableArray.Create(
|
||||
new FlatCollisionSphere(new Vector3(0f, 0f, 0.48f), 0.48f),
|
||||
new FlatCollisionSphere(new Vector3(0f, 0f, 1.835f - 0.48f), 0.48f)),
|
||||
Scale: 1f,
|
||||
StepUpHeight: 0.40f,
|
||||
StepDownHeight: 0.40f,
|
||||
MoverFlags: moverFlags,
|
||||
MovingEntityId: movingEntityId,
|
||||
Flags: PhysicsSetPositionFlags.Placement
|
||||
| PhysicsSetPositionFlags.Slide);
|
||||
|
||||
private static void RunSequence(
|
||||
Func<bool, PhysicsEngine> buildEngine,
|
||||
params ResolveSpec[] specs)
|
||||
|
|
@ -441,6 +480,66 @@ public sealed class TransitionScratchDifferentialTests
|
|||
Assert.Equal(expected.ContactPlaneIsWater, actual.ContactPlaneIsWater);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bitwise sibling of <see cref="AssertResolveBitwise"/> for
|
||||
/// <see cref="PhysicsSetPositionResult"/>, used by the re-pointed
|
||||
/// <see cref="ReusedScratch_MatchesFreshPlacementSearch"/> (C5a). Every
|
||||
/// field is compared — float-bearing members bitwise (a stale scratch
|
||||
/// leaking a different -0.0/+0.0 sign bit would otherwise pass a
|
||||
/// value-equality check), enum/bool/id members by value, and the
|
||||
/// immutable id arrays by ordered sequence.
|
||||
/// </summary>
|
||||
private static void AssertSetPositionBitwise(
|
||||
PhysicsSetPositionResult expected,
|
||||
PhysicsSetPositionResult actual,
|
||||
string context)
|
||||
{
|
||||
Assert.Equal(expected.Error, actual.Error);
|
||||
Assert.Equal(expected.Residence, actual.Residence);
|
||||
AssertVectorBitwise(expected.Position, actual.Position, context);
|
||||
AssertQuaternionBitwise(expected.Orientation, actual.Orientation, context);
|
||||
Assert.Equal(expected.CellId, actual.CellId);
|
||||
AssertVectorBitwise(
|
||||
expected.CellLocalPosition,
|
||||
actual.CellLocalPosition,
|
||||
$"{context}.CellLocalPosition");
|
||||
Assert.Equal(expected.InContact, actual.InContact);
|
||||
Assert.Equal(expected.OnWalkable, actual.OnWalkable);
|
||||
AssertPlaneBitwise(expected.ContactPlane, actual.ContactPlane, context);
|
||||
Assert.Equal(expected.ContactPlaneCellId, actual.ContactPlaneCellId);
|
||||
Assert.Equal(expected.ContactPlaneIsWater, actual.ContactPlaneIsWater);
|
||||
Assert.Equal(expected.SlidingNormalValid, actual.SlidingNormalValid);
|
||||
AssertVectorBitwise(
|
||||
expected.SlidingNormal,
|
||||
actual.SlidingNormal,
|
||||
$"{context}.SlidingNormal");
|
||||
Assert.Equal(expected.CollisionNormalValid, actual.CollisionNormalValid);
|
||||
AssertVectorBitwise(
|
||||
expected.CollisionNormal,
|
||||
actual.CollisionNormal,
|
||||
$"{context}.CollisionNormal");
|
||||
Assert.Equal(expected.FramesStationaryFall, actual.FramesStationaryFall);
|
||||
Assert.Equal(expected.CollidedWithEnvironment, actual.CollidedWithEnvironment);
|
||||
Assert.Equal(expected.CollisionHandlerResult, actual.CollisionHandlerResult);
|
||||
Assert.Equal(expected.CellChanged, actual.CellChanged);
|
||||
Assert.Equal(expected.ShadowAction, actual.ShadowAction);
|
||||
// ImmutableArray<T>.Equals(ImmutableArray<T>) compares the BACKING
|
||||
// ARRAY REFERENCE, not the elements — the fresh and reused engines
|
||||
// never share a backing array even when the contents match, so
|
||||
// Assert.Equal on the bare ImmutableArray would false-fail. Compare
|
||||
// as plain arrays (regular array Equals/sequence comparison) instead.
|
||||
Assert.Equal(ToArrayOrEmpty(expected.CrossCellIds), ToArrayOrEmpty(actual.CrossCellIds));
|
||||
Assert.Equal(
|
||||
ToArrayOrEmpty(expected.CollidedObjectIds),
|
||||
ToArrayOrEmpty(actual.CollidedObjectIds));
|
||||
Assert.Equal(
|
||||
ToArrayOrEmpty(expected.QueriedCellIds),
|
||||
ToArrayOrEmpty(actual.QueriedCellIds));
|
||||
}
|
||||
|
||||
private static uint[] ToArrayOrEmpty(ImmutableArray<uint> array) =>
|
||||
array.IsDefault ? Array.Empty<uint>() : array.ToArray();
|
||||
|
||||
private static void AssertCoverage(ResolveSpec spec, ResolveResult result)
|
||||
{
|
||||
switch (spec.Coverage)
|
||||
|
|
|
|||
|
|
@ -402,9 +402,9 @@ public sealed class HeadlessSessionHostTests
|
|||
PlayerMovementController controller =
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(48f, 49f, 50f),
|
||||
0xA9B40001u);
|
||||
0xA9B40001u, new Vector3(48f, 49f, 50f));
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
|
|
@ -533,9 +533,9 @@ public sealed class HeadlessSessionHostTests
|
|||
PlayerMovementController controller =
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(48f, 49f, 50f),
|
||||
0xA9B40001u);
|
||||
0xA9B40001u, new Vector3(48f, 49f, 50f));
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
|
|
@ -727,9 +727,9 @@ public sealed class HeadlessSessionHostTests
|
|||
PlayerMovementController controller =
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(48f, 49f, 50f),
|
||||
0xA9B40001u);
|
||||
0xA9B40001u, new Vector3(48f, 49f, 50f));
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
|
|
@ -878,9 +878,9 @@ public sealed class HeadlessSessionHostTests
|
|||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(48f, 49f, 50f),
|
||||
0xA9B40001u);
|
||||
0xA9B40001u, new Vector3(48f, 49f, 50f));
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
|
|
@ -1583,7 +1583,7 @@ public sealed class HeadlessSessionHostTests
|
|||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(96f, 97f, 50f),
|
||||
0xA9B40001u,
|
||||
new Vector3(96f, 97f, 50f));
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ public sealed class LocalPlayerImmediatePositionTests
|
|||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
var player = new PlayerMovementController(engine);
|
||||
player.SetPosition(
|
||||
player.SeedPlacementForTest(
|
||||
new Vector3(96f, 97f, 50f),
|
||||
0xA9B40001u,
|
||||
new Vector3(96f, 97f, 50f));
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ public sealed class PlayerMouseLookMovementTests
|
|||
worldOffsetY: 0f);
|
||||
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001u, new Vector3(96f, 96f, 50f));
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
|
||||
var result = controller.Update(0.016f, new MovementInput());
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ public class PlayerMovementControllerTests
|
|||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
var rest = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(rest, 0x0001);
|
||||
controller.SeedPlacementForTest(rest, 0x0001, rest);
|
||||
|
||||
// Settle one frame so the resolver establishes its rest state, then
|
||||
// capture the baseline the body must hold.
|
||||
|
|
@ -126,7 +126,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
|
||||
// Walk forward ~0.5 s, then release.
|
||||
|
|
@ -157,7 +157,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f; // facing +X
|
||||
|
||||
// L.5 physics-tick gate (235de33, 2026-04-30): Update() integrates
|
||||
|
|
@ -181,7 +181,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
controller.Yaw = 0f;
|
||||
controller.AttachAnimationRootMotionSource((_, _) => { });
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
controller.Yaw = 0f;
|
||||
controller.ObjectScale = 2f;
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
|
|
@ -220,7 +220,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
controller.Yaw = 0f;
|
||||
int advances = 0;
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
|
|
@ -247,7 +247,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
controller.Yaw = 0f; // body local +Y faces world +X
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
{
|
||||
|
|
@ -273,7 +273,7 @@ public class PlayerMovementControllerTests
|
|||
public void Update_AttachedAnimationFrame_PreservesCompleteNonCommutingOrientation()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
Quaternion initial = Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1.1f);
|
||||
Quaternion delta = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.9f);
|
||||
controller.SetBodyOrientation(initial);
|
||||
|
|
@ -297,7 +297,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
controller.Yaw = 0f;
|
||||
int sample = 0;
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
|
|
@ -331,7 +331,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
int advances = 0;
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
{
|
||||
|
|
@ -356,8 +356,8 @@ public class PlayerMovementControllerTests
|
|||
var combined = new PlayerMovementController(MakeFlatEngine());
|
||||
var split = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
combined.SetPosition(start, 0x0001);
|
||||
split.SetPosition(start, 0x0001);
|
||||
combined.SeedPlacementForTest(start, 0x0001, start);
|
||||
split.SeedPlacementForTest(start, 0x0001, start);
|
||||
int combinedHooks = 0;
|
||||
int splitHooks = 0;
|
||||
|
||||
|
|
@ -391,7 +391,7 @@ public class PlayerMovementControllerTests
|
|||
public void TickHidden_DoesNotAdvancePartArrayButStillProcessesHooks()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
int advances = 0;
|
||||
int hookPasses = 0;
|
||||
controller.AttachAnimationRootMotionSource(
|
||||
|
|
@ -412,7 +412,7 @@ public class PlayerMovementControllerTests
|
|||
public void InvalidElapsed_VisibleFrameIsPurePresentationRead()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
float initialTime = controller.SimTimeSeconds;
|
||||
float initialYaw = controller.Yaw;
|
||||
Vector3 initialPosition = controller.Position;
|
||||
|
|
@ -452,7 +452,7 @@ public class PlayerMovementControllerTests
|
|||
public void InvalidElapsed_HiddenFrameDoesNotAdvanceClockOrManagerTail()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
float initialTime = controller.SimTimeSeconds;
|
||||
int targetPasses = 0;
|
||||
|
||||
|
|
@ -475,7 +475,7 @@ public class PlayerMovementControllerTests
|
|||
public void Update_AirbornePartArrayFrame_SuppressesOriginButPreservesOrientation()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Update(1f, new MovementInput(Jump: true));
|
||||
|
||||
Vector3 beforeRelease = controller.Position;
|
||||
|
|
@ -506,7 +506,7 @@ public class PlayerMovementControllerTests
|
|||
public void Update_AttachedAnimationTurn_IsNotAppliedByASecondYawIntegrator()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
controller.AttachAnimationRootMotionSource((dt, frame) =>
|
||||
{
|
||||
|
|
@ -531,7 +531,7 @@ public class PlayerMovementControllerTests
|
|||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
controller.Yaw = 0f;
|
||||
|
||||
var firstTick = controller.Update(ObjectTick, new MovementInput(Forward: true));
|
||||
|
|
@ -554,14 +554,14 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
|
||||
controller.Update(ObjectTick, new MovementInput(Forward: true));
|
||||
controller.Update(PhysicsBody.MinQuantum * 0.5f, new MovementInput(Forward: true));
|
||||
|
||||
var snapped = new Vector3(120f, 80f, 50f);
|
||||
controller.SetPosition(snapped, 0x0001);
|
||||
controller.SeedPlacementForTest(snapped, 0x0001, snapped);
|
||||
var result = controller.Update(PhysicsBody.MinQuantum * 0.5f, new MovementInput());
|
||||
|
||||
Assert.Equal(snapped, result.Position);
|
||||
|
|
@ -572,7 +572,7 @@ public class PlayerMovementControllerTests
|
|||
public void CommitCanonicalForcePositionFrame_ReconcilesPoseWithoutStoppingActiveMotion()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
controller.Update(ObjectTick, new MovementInput(Forward: true));
|
||||
Vector3 velocity = controller.BodyVelocity;
|
||||
|
|
@ -616,7 +616,7 @@ public class PlayerMovementControllerTests
|
|||
var world = new Vector3(12f, 12f, 50f);
|
||||
var wireLocal = new Vector3(12f, 12f, 50f);
|
||||
|
||||
controller.SetPosition(world, 0xA9B40031u, wireLocal);
|
||||
controller.SeedPlacementForTest(world, 0xA9B40031u, wireLocal);
|
||||
|
||||
Assert.Equal(0xA9B40001u, controller.CellId);
|
||||
Assert.Equal(controller.CellId, controller.CellPosition.ObjCellId);
|
||||
|
|
@ -628,7 +628,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
int animationAdvances = 0;
|
||||
int hookPasses = 0;
|
||||
|
|
@ -656,7 +656,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
|
||||
var result = controller.Update(
|
||||
|
|
@ -680,14 +680,14 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
|
||||
var walkInput = new MovementInput { Forward = true };
|
||||
var walkResult = controller.Update(1.0f, walkInput);
|
||||
float walkDist = walkResult.Position.X - 96f;
|
||||
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
|
||||
var runInput = new MovementInput { Forward = true, Run = true };
|
||||
var runResult = controller.Update(1.0f, runInput);
|
||||
|
|
@ -701,7 +701,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
float initialYaw = controller.Yaw;
|
||||
|
||||
var input = new MovementInput { TurnRight = true };
|
||||
|
|
@ -715,7 +715,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
|
||||
// First frame: idle (no input).
|
||||
controller.Update(0.016f, new MovementInput());
|
||||
|
|
@ -732,7 +732,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
|
||||
// Charged jump: hold for a full charge (1s dt), then release to fire.
|
||||
// A full charge gives enough Vz that the player clears the 0.05-unit
|
||||
|
|
@ -749,7 +749,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
|
||||
Assert.True(controller.CanSendPositionEvent);
|
||||
|
||||
|
|
@ -765,7 +765,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
|
||||
Assert.Equal(default, controller.JumpCharge);
|
||||
|
||||
|
|
@ -783,7 +783,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
|
||||
// Charged jump: hold for a full charge, then release.
|
||||
controller.Update(1.0f, new MovementInput(Jump: true)); // full charge
|
||||
|
|
@ -829,7 +829,7 @@ public class PlayerMovementControllerTests
|
|||
// One step at walk speed will cross into the low region where terrain drops
|
||||
// ~28 units — more than StepUpHeight=5, triggering the ledge-fall.
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(118f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(118f, 96f, 50f), 0x0001, new Vector3(118f, 96f, 50f));
|
||||
controller.Yaw = 0f; // facing +X
|
||||
|
||||
// Single step — should trigger airborne state because terrain drops sharply.
|
||||
|
|
@ -915,7 +915,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var (controller, _) = MakeControllerWithHost();
|
||||
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001); // low16 < 0x0100 -> outdoor
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f)); // low16 < 0x0100 -> outdoor
|
||||
|
||||
ConstraintManager cm = controller.PositionManager!.Constraint!;
|
||||
Assert.True(cm.IsConstrained);
|
||||
|
|
@ -935,9 +935,9 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var (controller, _) = MakeControllerWithHost();
|
||||
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(10f, 10f, 5f),
|
||||
0x01000105u); // low16 = 0x0105 >= 0x0100 -> indoor (verbatim, no outdoor canonicalization)
|
||||
0x01000105u, new Vector3(10f, 10f, 5f)); // low16 = 0x0105 >= 0x0100 -> indoor (verbatim, no outdoor canonicalization)
|
||||
|
||||
ConstraintManager cm = controller.PositionManager!.Constraint!;
|
||||
Assert.Equal(5.0f, cm.ConstraintDistanceStart);
|
||||
|
|
@ -948,7 +948,7 @@ public class PlayerMovementControllerTests
|
|||
public void SetPosition_Teleport_TearsDownAndRearmsAPreviouslyFullyConstrainedLeash()
|
||||
{
|
||||
var (controller, _) = MakeControllerWithHost();
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
ConstraintManager cm = controller.PositionManager!.Constraint!;
|
||||
|
||||
// Synthetically over-strain the leash with a tight band (production
|
||||
|
|
@ -958,7 +958,7 @@ public class PlayerMovementControllerTests
|
|||
cm.AdjustOffset(new MotionDeltaFrame { Origin = new Vector3(5f, 0f, 0f) }, quantum: 0.1);
|
||||
Assert.True(controller.PositionManager.IsFullyConstrained());
|
||||
|
||||
controller.SetPosition(new Vector3(150f, 150f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(150f, 150f, 50f), 0x0001, new Vector3(150f, 150f, 50f));
|
||||
|
||||
// retail teleport_hook's UnConstrain, followed by the fresh re-arm at
|
||||
// the new position, clears the stale over-strained state.
|
||||
|
|
@ -971,7 +971,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var (controller, _) = MakeControllerWithHost();
|
||||
var initial = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(initial, 0x0001);
|
||||
controller.SeedPlacementForTest(initial, 0x0001, initial);
|
||||
controller.Update(ObjectTick, new MovementInput(Forward: true));
|
||||
Vector3 velocityBeforeCommit = controller.BodyVelocity;
|
||||
Assert.NotEqual(Vector3.Zero, velocityBeforeCommit); // sanity: actually moving
|
||||
|
|
@ -1002,7 +1002,7 @@ public class PlayerMovementControllerTests
|
|||
public void Update_ConstraintArmedInBand_TapersALargeRootMotionOffsetOnTheSecondTick()
|
||||
{
|
||||
var (controller, _) = MakeControllerWithHost();
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
ConstraintManager cm = controller.PositionManager!.Constraint!;
|
||||
cm.ConstrainTo(controller.CellPosition, startDistance: 1f, maxDistance: 10f);
|
||||
|
||||
|
|
@ -1034,7 +1034,7 @@ public class PlayerMovementControllerTests
|
|||
public void Update_ConstraintOverstrained_PushesIsFullyConstrainedOntoBodyAndBlocksJump()
|
||||
{
|
||||
var (controller, _) = MakeControllerWithHost();
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
ConstraintManager cm = controller.PositionManager!.Constraint!;
|
||||
cm.ConstrainTo(controller.CellPosition, startDistance: 1f, maxDistance: 2f);
|
||||
|
||||
|
|
@ -1069,7 +1069,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
controller.Yaw = 0f;
|
||||
// Fixed per-tick local-forward delta, matching
|
||||
// Update_AttachedAnimationRootDelta_DrivesGroundedBodyAtObjectScale's
|
||||
|
|
@ -1145,7 +1145,7 @@ public class PlayerMovementControllerTests
|
|||
default));
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.TickHidden(
|
||||
1f / 60f));
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.SetPosition(
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.SeedPlacementForTest(
|
||||
Vector3.One,
|
||||
0xA9B40021u,
|
||||
Vector3.One));
|
||||
|
|
@ -1155,7 +1155,12 @@ public class PlayerMovementControllerTests
|
|||
candidate.CaptureMovementResult(mouseLookEvent: false));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
candidate.NoteMovementSent(1f));
|
||||
Assert.Throws<InvalidOperationException>(candidate.CommitPreparedPosition);
|
||||
// C5a (2026-08-05): CommitPreparedPosition is deleted (zero production
|
||||
// callers); ArmConstraintLeashAtCommittedPlacement is its production
|
||||
// replacement and carries the SAME EnsurePublishedForRuntimeOperation
|
||||
// guard at its own entry, so this re-points rather than drops.
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
candidate.ArmConstraintLeashAtCommittedPlacement);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
candidate.ApplyPhysicsState(PhysicsStateFlags.Frozen));
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.LocalEntityId = 2u);
|
||||
|
|
@ -1194,7 +1199,7 @@ public class PlayerMovementControllerTests
|
|||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
controller.Yaw = 0f;
|
||||
controller.Motion.DefaultSink = new FakeAnimationDispatchSink();
|
||||
// No root-motion displacement contributed -- isolates the residual
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public sealed class PlayerOutboundPositionTests
|
|||
// The render origin remains A9B1, so a point 22.56 m into A9B2 is
|
||||
// represented at world Y=214.56. The wire frame must remain the
|
||||
// carried A9B2-local 22.56, never the render-space 214.56.
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(116.07f, 214.56f, 83.76f),
|
||||
0xA9B20021u,
|
||||
new Vector3(116.07f, 22.56f, 83.76f));
|
||||
|
|
@ -42,7 +42,7 @@ public sealed class PlayerOutboundPositionTests
|
|||
public void OutboundPosition_PreservesCompleteAuthoritativeQuaternion()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(116.07f, 214.56f, 83.76f),
|
||||
0xA9B20021u,
|
||||
new Vector3(116.07f, 22.56f, 83.76f));
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public sealed class RuntimeLocalPlayerMovementStateTests
|
|||
{
|
||||
LocalEntityId = 0x50000001u,
|
||||
};
|
||||
controller.SetPosition(
|
||||
controller.SeedPlacementForTest(
|
||||
new Vector3(11f, 12f, 13f),
|
||||
0xA9B40001u,
|
||||
new Vector3(11f, 12f, 13f));
|
||||
|
|
@ -140,8 +140,8 @@ public sealed class RuntimeLocalPlayerMovementStateTests
|
|||
second.Controller = secondController;
|
||||
|
||||
first.Execute(RuntimeMovementCommand.ToggleRunLock);
|
||||
firstController.SetPosition(Vector3.One, 0xA9B40001u, Vector3.One);
|
||||
secondController.SetPosition(
|
||||
firstController.SeedPlacementForTest(Vector3.One, 0xA9B40001u, Vector3.One);
|
||||
secondController.SeedPlacementForTest(
|
||||
new Vector3(2f),
|
||||
0xA9B50001u,
|
||||
new Vector3(2f));
|
||||
|
|
|
|||
|
|
@ -2970,7 +2970,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
|||
1f / 60f));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
controller.SuspendObjectUpdate(1f / 60f));
|
||||
Assert.Throws<InvalidOperationException>(() => controller.SetPosition(
|
||||
Assert.Throws<InvalidOperationException>(() => controller.SeedPlacementForTest(
|
||||
Vector3.One,
|
||||
Cell,
|
||||
Vector3.One));
|
||||
|
|
@ -3003,8 +3003,12 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
|||
controller.PrepareForAttackRequest());
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
controller.RequestPosture(MotionCommand.Ready));
|
||||
// C5a (2026-08-05): CommitPreparedPosition is deleted (zero production
|
||||
// callers); ArmConstraintLeashAtCommittedPlacement is its production
|
||||
// replacement and carries the SAME EnsurePublishedForRuntimeOperation
|
||||
// guard at its own entry, so this re-points rather than drops.
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
controller.CommitPreparedPosition());
|
||||
controller.ArmConstraintLeashAtCommittedPlacement());
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
controller.PreparePositionForCommit(Vector3.One, Cell, Vector3.One));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue