Builds the machinery route 4b-2 and 4b-3 will flip on, and changes no remote behaviour: it has no production caller, so RemotePlacementDrivePendingCount is provably 0 and IsConverged is unchanged. Five pieces: a per-entity remote placement owner (RuntimeRemotePlacementDriveController), a Position-time service-window guard with a Runtime interface plus BOTH host implementations, N3's headless RetryPending pump, parked-count observability in the ownership ledger, and the service-window optimisation that avoids parks we can cheaply predict. Landed alone because it is where the park-withdraws-the-entity failure was decided; that decision is fixed at the source in the preceding commit and must not share a review signal with a behaviour flip. Two parts of route 2's controller are deliberately NOT ported, both verified against retail rather than assumed. There is no ack: SendPositionEvent is called only inside HandleReceivedPosition's local-player FORCE_POSITION gate @0x0045400C-@0x00454091, and the remote arm @0x0045414D has no equivalent. There is no re-issue funnel: retail never re-attempts a position it could not apply — stale timestamps merely bump error_count @0x004542AC — and re-issuing packet N after N+1 has merged would apply a pose the newer packet already superseded, which is correct for a one-shot ForcePosition and wrong for a 5-10 Hz stream. The service-window guard is an OPTIMISATION, not the correctness mechanism. The original contract had it the other way round, justified by a claim that retail cannot represent "arrived but not placeable" — false, and corrected in the review findings: retail's GotoLostCell/reenter_visibility path represents it exactly. A pre-flight guard also cannot be complete, because Core defers on the entity's CURRENT cell, on the swept QueriedCellIds footprint spanning neighbouring landblocks, and on residency evaluated after AdjustToOutside — conditions only Core can see. Review found and this commit fixes: DetachRoute cleared two maps of LIVE Core operations without cancelling them (route 2's AbandonPending is the correct mirror, not the first-entry controller) and its test asserted that blindness as convergence; the headless predicate answered "can ever publish" rather than "is published", and after the first fix still matched only 1 of the 9 landblocks this host publishes; OwnsPlacement admitted remote top-level Creates until gated on the Teleport flag as well as the disposition; Advance re-submitted without re-checking the window; and four comments cited a report that did not exist. Contract item 6 is met by the structural proof, not the earlier test: HasOldPrefixPlacementDebt refuses collision-prefix mutation permission before ParkCollisionResidents is ever entered, so its overlap throw is unreachable. That same mechanism is the unbounded stall filed as #310, which 4b-1 does not bound — it only avoids widening it. #311 files the remaining per-tick allocation in RetryPendingProjections; the early-out for the empty-FIFO case landed via a new HasPendingReceipts accessor so hosts still never touch .Placements. directly. Gates: complete Release solution 10,973 passed / 4 skipped / 0 failed (baseline 10,938). Four review rounds; every fix discrimination-verified by revert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1072 lines
48 KiB
C#
1072 lines
48 KiB
C#
using System.Numerics;
|
|
using AcDream.Content;
|
|
using AcDream.Content.Pak;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
using AcDream.Runtime.Session;
|
|
|
|
namespace AcDream.Runtime.Tests.Session;
|
|
|
|
/// <summary>
|
|
/// C4 route 4b-1: focused tests for the Runtime-owned, per-entity remote
|
|
/// placement infrastructure. This route wires NO production caller — every
|
|
/// test below constructs an already-classified <see cref="RuntimeAuthoritativePositionRoute"/>
|
|
/// directly (the same shape
|
|
/// <see cref="RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition"/>
|
|
/// would hand a real caller) so each scenario is exercised precisely, mirroring
|
|
/// <c>RuntimeSetPositionStateTests</c>' bare-<see cref="RuntimeEntityObjectLifetime"/>
|
|
/// fixture rather than the full <c>GameRuntime</c>/<c>LiveSessionHost</c>
|
|
/// harness route 2's tests use — this controller has no local-player
|
|
/// controller, outbound session, or generation dependency to bootstrap.
|
|
///
|
|
/// Each test was verified to actually discriminate its own fix by temporarily
|
|
/// reverting the corresponding guard/omission in
|
|
/// <see cref="RuntimeRemotePlacementDriveController"/> and confirming the
|
|
/// matching test failed, then restoring it.
|
|
/// </summary>
|
|
public sealed class RuntimeRemotePlacementDriveControllerTests
|
|
{
|
|
private const uint SourceLandblock = 0xB1000000u;
|
|
private const uint SourceCell = SourceLandblock | 0x0001u;
|
|
private const uint DestinationLandblock = 0xB2000000u;
|
|
private const uint DestinationCell = DestinationLandblock | 0x0001u;
|
|
private const float SpawnHeight = 7f;
|
|
|
|
[Fact]
|
|
public void NotApplicable_WhenDispositionIsNotOwnedByThisRoute()
|
|
{
|
|
foreach (RuntimeAuthoritativePositionDisposition disposition in
|
|
new[]
|
|
{
|
|
RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
|
|
RuntimeAuthoritativePositionDisposition.RejectedAuthority,
|
|
RuntimeAuthoritativePositionDisposition.RejectedData,
|
|
RuntimeAuthoritativePositionDisposition.AwaitFreshPosition,
|
|
})
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003001u);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record, disposition, DestinationCell);
|
|
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.NotApplicable,
|
|
drive.TryExecuteAcceptedRemotePosition(record, route));
|
|
Assert.Equal(0, drive.PendingCount);
|
|
AssertConverged(lifetime);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void NotApplicable_WhenTheEntityHasNoCanonicalBody()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003002u);
|
|
// Deliberately never attach a body.
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell);
|
|
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.NotApplicable,
|
|
drive.TryExecuteAcceptedRemotePosition(record, route));
|
|
}
|
|
|
|
/// <summary>
|
|
/// B6 review fix: <c>OwnsPlacement</c> keyed on <c>Disposition</c> alone
|
|
/// would claim this route too — the classifier emits
|
|
/// <c>SetPositionSimple</c> for the LOCAL PLAYER's FORCE_POSITION/
|
|
/// teleport branches (<c>RuntimeSetPositionOperationKind.LocalAuthoritative</c>),
|
|
/// not only for remotes. The static predicate itself is exercised
|
|
/// directly (no entity/body needed) since it takes only the route.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative()
|
|
{
|
|
// RuntimeSetPositionOperationKind is internal, so a public [Theory]
|
|
// cannot take it as a parameter (CS0051) — iterate directly instead,
|
|
// mirroring NotApplicable_WhenDispositionIsNotOwnedByThisRoute's own
|
|
// foreach-over-internal-enum shape.
|
|
foreach (RuntimeSetPositionOperationKind operationKind in
|
|
new[]
|
|
{
|
|
RuntimeSetPositionOperationKind.InitialLogin,
|
|
RuntimeSetPositionOperationKind.LocalAuthoritative,
|
|
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
|
|
})
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x7000300Fu);
|
|
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
|
DestinationCell,
|
|
operationKind: operationKind);
|
|
|
|
Assert.False(
|
|
RuntimeRemotePlacementDriveController.OwnsPlacement(route));
|
|
|
|
// End to end: this controller must decline the SAME route as
|
|
// NotApplicable, not merely the static predicate in isolation.
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive =
|
|
CreateDrive(lifetime, window);
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.NotApplicable,
|
|
drive.TryExecuteAcceptedRemotePosition(record, route));
|
|
Assert.Equal(0, drive.PendingCount);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// C2-4 review fix (delta round): the <c>OperationKind is RemoteAuthoritative</c>
|
|
/// guard alone is STILL not exact — <c>RuntimeAuthoritativePositionRouteClassifier
|
|
/// .ClassifyCreate</c> emits <c>Disposition = SetPosition</c> AND
|
|
/// <c>OperationKind = RemoteAuthoritative</c> for a REMOTE top-level
|
|
/// initial Create too (only the LOCAL PLAYER's Create maps to
|
|
/// <c>InitialLogin</c>), so both the disposition and operation-kind
|
|
/// guards pass for a Create-shaped route. Retail's own flag choice is
|
|
/// what actually distinguishes them: a Create carries
|
|
/// <c>InitialCreateFlags</c> (<c>Placement|Slide</c>, no
|
|
/// <c>Teleport</c>); every remote accepted-Position SetPosition/
|
|
/// SetPositionSimple route carries <c>AuthoritativeTeleportFlags</c>
|
|
/// (<c>Teleport|Slide|SendPositionEvent</c>). Route 4b-1 is a
|
|
/// POSITION-only route — the first-entry conductor owns every Create —
|
|
/// so a Create-shaped route reaching this controller must be declined.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OwnsPlacement_FalseForARemoteTopLevelCreateShapedRoute()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003014u);
|
|
|
|
RuntimeAuthoritativePositionRoute createRoute = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell,
|
|
operationKind: RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
setPositionFlags: PhysicsSetPositionFlags.Placement
|
|
| PhysicsSetPositionFlags.Slide);
|
|
|
|
Assert.False(
|
|
RuntimeRemotePlacementDriveController.OwnsPlacement(createRoute));
|
|
|
|
// End to end: this controller must decline the SAME route as
|
|
// NotApplicable, not merely the static predicate in isolation.
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive =
|
|
CreateDrive(lifetime, window);
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.NotApplicable,
|
|
drive.TryExecuteAcceptedRemotePosition(record, createRoute));
|
|
Assert.Equal(0, drive.PendingCount);
|
|
|
|
// The genuine remote accepted-Position shape (SAME disposition, SAME
|
|
// operation kind, Teleport-flagged) must still be owned — the fix
|
|
// must not have over-corrected into refusing everything.
|
|
RuntimeAuthoritativePositionRoute positionRoute = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell);
|
|
Assert.True(
|
|
RuntimeRemotePlacementDriveController.OwnsPlacement(positionRoute));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The central decision's core pin: a destination this host's service
|
|
/// window does not currently cover must be refused, not parked — no
|
|
/// operation may open at all, so the body's own <c>InWorld</c>/object
|
|
/// clock are never touched.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Refused_WhenDestinationIsNotWithinServiceWindow_NoOperationOpensAndBodyStaysInWorld()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003003u);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
// The service window allows nothing — mirrors a destination
|
|
// landblock this host has never collision-published.
|
|
var window = new FakeServiceWindow();
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell);
|
|
|
|
RuntimeRemotePlacementExecutionStatus status =
|
|
drive.TryExecuteAcceptedRemotePosition(record, route);
|
|
|
|
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Refused, status);
|
|
Assert.True(body.InWorld);
|
|
Assert.True(record.ObjectClock.IsActive);
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
AssertConverged(lifetime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The exact §"central decision" sequence the contract's acceptance
|
|
/// section pins: attempt a placement whose destination is refused, then
|
|
/// deliver the entity's NEXT accepted Position — which, whatever it
|
|
/// classifies to, ALWAYS runs <c>RuntimeSetPositionState.Forget</c> for
|
|
/// this entity first
|
|
/// (<see cref="RuntimeEntityObjectLifetime.TryApplyPosition"/>, called
|
|
/// unconditionally regardless of disposition). Because nothing was ever
|
|
/// parked in the first step, that unconditional Forget has nothing to
|
|
/// discard, and the entity is STILL in the world afterward — the
|
|
/// invisible-and-intangible failure this route exists to prevent.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CentralDecision_RefusedPlacementThenNextAcceptedPositionForgetsNothing_EntityStaysInWorld()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003004u);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
var window = new FakeServiceWindow();
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell);
|
|
|
|
// Packet N: destination not placeable now.
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Refused,
|
|
drive.TryExecuteAcceptedRemotePosition(record, route));
|
|
Assert.True(body.InWorld);
|
|
Assert.True(record.ObjectClock.IsActive);
|
|
|
|
// Packet N+1 ~150 ms later: whatever it classifies to (here,
|
|
// Interpolate — the caller never reaches this controller at all for
|
|
// that disposition), production's merge unconditionally Forgets any
|
|
// in-flight operation for this entity FIRST. Simulated directly
|
|
// (mirrors RuntimeAcceptedPositionDriveControllerTests'
|
|
// Equal_ClearsPendingWithoutReissuing... fixture, which drives the
|
|
// same production Forget call without a full second network
|
|
// round-trip).
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
lifetime.Physics.SetPosition.Forget(record);
|
|
if (cancellation.IsValid)
|
|
lifetime.Physics.SetPosition.PublishCancellation(cancellation);
|
|
|
|
Assert.True(body.InWorld);
|
|
Assert.True(record.ObjectClock.IsActive);
|
|
Assert.Equal(SourceCell, record.FullCellId);
|
|
AssertConverged(lifetime);
|
|
}
|
|
|
|
[Fact]
|
|
public void Committed_WhenDestinationIsWithinServiceWindowAndCollisionGenerationCommitted()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003005u);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
var destination = new Vector3(12f, 14f, SpawnHeight);
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell,
|
|
destination);
|
|
|
|
RuntimeRemotePlacementExecutionStatus status =
|
|
drive.TryExecuteAcceptedRemotePosition(record, route);
|
|
|
|
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
|
|
// Runtime's world-frame resolution shifts an authored landblock-local
|
|
// position into Runtime's continuous world frame
|
|
// (resolveWorldOffsetFromRuntimeFrame: true) — DestinationLandblock
|
|
// sits +192m on X from SourceLandblock per CommitLandblockCollision.
|
|
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
|
|
Assert.Equal(0, drive.PendingCount);
|
|
// No host subscription is wired in this bare fixture — drain the
|
|
// Place receipt exactly like production's placement-projection
|
|
// subscription would, so the operation itself (not this drive's own
|
|
// ledger) also converges to zero.
|
|
DrainPlacementFifo(lifetime);
|
|
Assert.Equal(
|
|
0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
AssertConverged(lifetime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// B4 review fix: <c>SubmitAndResolve</c>'s Committed branch returns and
|
|
/// retains nothing in <c>_pending</c>, but when nothing synchronously
|
|
/// consumes-and-acknowledges the Place receipt (this bare fixture has no
|
|
/// host subscription wired — production's declined-sink retry scenario
|
|
/// is the identical shape), the operation stays live in Core's
|
|
/// <c>_operations</c> map until something later calls
|
|
/// <c>AcknowledgeProjection</c>. Before this fix
|
|
/// <c>RemotePlacementDrivePendingCount</c> was blind to that live
|
|
/// operation the instant <c>Committed</c> was returned. This proves it is
|
|
/// now visible, and that it still converges to zero once the receipt is
|
|
/// actually acknowledged.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Committed_UnacknowledgedOperationStaysVisibleInTheLedgerUntilAcknowledged()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003010u);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
var destination = new Vector3(12f, 14f, SpawnHeight);
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell,
|
|
destination);
|
|
|
|
RuntimeRemotePlacementExecutionStatus status =
|
|
drive.TryExecuteAcceptedRemotePosition(record, route);
|
|
|
|
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
|
|
// No host subscription is wired in this bare fixture, so the Place
|
|
// receipt is still genuinely unacknowledged — the ledger must SEE it.
|
|
Assert.Equal(
|
|
1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
Assert.Equal(
|
|
1,
|
|
lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
|
|
|
|
DrainPlacementFifo(lifetime);
|
|
|
|
Assert.Equal(
|
|
0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
AssertConverged(lifetime);
|
|
}
|
|
|
|
[Fact]
|
|
public void Contention_WhenTheEntityAlreadyOwnsAnActiveOperation()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003006u);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
Vector3 positionBefore = body.Position;
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
// An external placement authority (portal/teleport/another route)
|
|
// already owns this entity's SetPosition operation.
|
|
RuntimeEntityPlacementToken displaced = lifetime.Physics.SetPosition
|
|
.TryBeginExclusiveAuthoredPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(displaced.IsValid);
|
|
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell);
|
|
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedRemotePosition(record, route));
|
|
Assert.Equal(positionBefore, body.Position);
|
|
Assert.Equal(0, drive.PendingCount);
|
|
|
|
RuntimePlacementCancellationReceipt cancellation = lifetime.Physics
|
|
.SetPosition.ForgetExactPlacement(displaced);
|
|
if (cancellation.IsValid)
|
|
lifetime.Physics.SetPosition.PublishCancellation(cancellation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-entity independence: two remotes interleaved must not share any
|
|
/// state — one entity's Contention must never block the other's Begin,
|
|
/// and each converges to zero independently.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PerEntityIndependence_TwoRemotesInterleavedDoNotBlockEachOther()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
RuntimeEntityRecord first = CreateRemoteRecord(lifetime, 0x70003007u);
|
|
RuntimeEntityRecord second = CreateRemoteRecord(lifetime, 0x70003008u);
|
|
PhysicsBody firstBody = AttachBody(lifetime, first, SourceCell);
|
|
PhysicsBody secondBody = AttachBody(lifetime, second, SourceCell);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
// First entity's own operation is still outstanding (an external
|
|
// authority holds it) when the second entity's Position arrives.
|
|
RuntimeEntityPlacementToken firstDisplaced = lifetime.Physics.SetPosition
|
|
.TryBeginExclusiveAuthoredPlacement(
|
|
first,
|
|
first.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(firstDisplaced.IsValid);
|
|
|
|
RuntimeAuthoritativePositionRoute firstRoute = MakeRoute(
|
|
first, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
|
|
RuntimeAuthoritativePositionRoute secondRoute = MakeRoute(
|
|
second, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell,
|
|
new Vector3(20f, 22f, SpawnHeight));
|
|
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedRemotePosition(first, firstRoute));
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Committed,
|
|
drive.TryExecuteAcceptedRemotePosition(second, secondRoute));
|
|
|
|
Assert.Equal(
|
|
new Vector3(20f, 22f, SpawnHeight) + new Vector3(192f, 0f, 0f),
|
|
secondBody.Position);
|
|
Assert.Equal(0, drive.PendingCount);
|
|
|
|
RuntimePlacementCancellationReceipt cancellation = lifetime.Physics
|
|
.SetPosition.ForgetExactPlacement(firstDisplaced);
|
|
if (cancellation.IsValid)
|
|
lifetime.Physics.SetPosition.PublishCancellation(cancellation);
|
|
DrainPlacementFifo(lifetime);
|
|
_ = firstBody;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-entity independence, the shape route 2's single <c>_pending</c>
|
|
/// slot cannot express: TWO entities each hold their OWN outstanding
|
|
/// preparation retry at the same time. Route 2's
|
|
/// <c>RetainPending</c> throws if a second live entry would displace the
|
|
/// first; this per-key map must track both simultaneously and let each
|
|
/// resolve (or die) independently.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PerEntityIndependence_TwoConcurrentPreparationRetriesDoNotCollide()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
RuntimeEntityRecord first = CreateRemoteRecord(
|
|
lifetime, 0x7000300Du, setupTableId: 0x02000001u);
|
|
RuntimeEntityRecord second = CreateRemoteRecord(
|
|
lifetime, 0x7000300Eu, setupTableId: 0x02000001u);
|
|
AttachBody(lifetime, first, SourceCell);
|
|
AttachBody(lifetime, second, SourceCell);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
RuntimeAuthoritativePositionRoute firstRoute = MakeRoute(
|
|
first, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
|
|
RuntimeAuthoritativePositionRoute secondRoute = MakeRoute(
|
|
second, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
|
|
|
|
// Both entities' Setup assets are unresolved (Missing) — both retry.
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedRemotePosition(first, firstRoute));
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedRemotePosition(second, secondRoute));
|
|
Assert.Equal(2, drive.PendingCount);
|
|
|
|
// The FIRST entity's retry dies (an unrelated accepted Position for
|
|
// IT ALONE forgets it); the second must remain completely
|
|
// unaffected — a shared/overwriting single slot could not represent
|
|
// this.
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
lifetime.Physics.SetPosition.Forget(first);
|
|
if (cancellation.IsValid)
|
|
lifetime.Physics.SetPosition.PublishCancellation(cancellation);
|
|
|
|
drive.Advance();
|
|
|
|
Assert.Equal(1, drive.PendingCount);
|
|
// The second entity's own operation is still tracked — the first's
|
|
// cancellation removed exactly one operation, not both.
|
|
Assert.Equal(
|
|
1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
|
|
// Cleanup: retire the second entity's still-live retry too.
|
|
RuntimePlacementCancellationReceipt secondCancellation =
|
|
lifetime.Physics.SetPosition.Forget(second);
|
|
if (secondCancellation.IsValid)
|
|
{
|
|
lifetime.Physics.SetPosition
|
|
.PublishCancellation(secondCancellation);
|
|
}
|
|
drive.Advance();
|
|
Assert.Equal(0, drive.PendingCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// B3 review fix: a retained preparation retry must re-check the SAME
|
|
/// service-window guard the entry point uses before <c>Advance()</c>
|
|
/// resubmits it — a destination that retired its collision publication in
|
|
/// the frames since the retry was retained must be dropped, not
|
|
/// resubmitted. Discriminates from the OLD unconditional resubmit:
|
|
/// resubmitting against the SAME always-<c>Missing</c> collision source
|
|
/// returns the SAME retryable status, so without the re-check the entry
|
|
/// would simply be re-retained (<c>PendingCount</c> stays 1) even though
|
|
/// the window already forbids the destination — never converging.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Advance_DropsRetainedRetryWhenTheServiceWindowNoLongerCoversItsDestination()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(
|
|
lifetime, 0x70003011u, setupTableId: 0x02000001u);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
|
|
|
|
// The Setup asset is unresolved (Missing) — the first attempt
|
|
// retries rather than commits, retaining the entry.
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedRemotePosition(record, route));
|
|
Assert.Equal(1, drive.PendingCount);
|
|
Assert.Equal(
|
|
1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
|
|
// The remote wandered back out of range between this retry being
|
|
// retained and the next host cadence pump.
|
|
window.Forbid(DestinationLandblock);
|
|
|
|
drive.Advance();
|
|
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Equal(
|
|
0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
AssertConverged(lifetime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Currency: a preparation-retry entry left tracking a token some OTHER
|
|
/// caller already Forgot must not leak once this entity's NEXT packet
|
|
/// takes a path that itself never touches <c>_pending</c> (here, a
|
|
/// service-window refusal) — proving the self-heal is doing real work,
|
|
/// not merely being masked by the retry branch's own dictionary
|
|
/// overwrite (which would hide the leak if this test only ever re-hit
|
|
/// the SAME retryable status).
|
|
/// </summary>
|
|
[Fact]
|
|
public void StalePreparationRetry_SelfHealsRatherThanLeakingWhenTheNextPacketNeverTouchesPending()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(
|
|
lifetime, 0x70003009u, setupTableId: 0x02000001u);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
// A Setup asset that never resolves (Missing) — the first attempt
|
|
// through this collision source retries rather than commits.
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
RuntimeAuthoritativePositionRoute route = MakeRoute(
|
|
record, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
|
|
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedRemotePosition(record, route));
|
|
Assert.Equal(1, drive.PendingCount);
|
|
|
|
// An unrelated accepted Position for this SAME entity Forgets the
|
|
// retained retry before it ever resolves — the mundane 5-10 Hz case.
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
lifetime.Physics.SetPosition.Forget(record);
|
|
if (cancellation.IsValid)
|
|
lifetime.Physics.SetPosition.PublishCancellation(cancellation);
|
|
|
|
// The destination is no longer serviceable by the NEXT packet (the
|
|
// remote wandered back out of range) — the Refused branch never
|
|
// assigns or clears _pending itself, so only the entry-point
|
|
// self-heal can retire the dead entry left behind above.
|
|
window.Forbid(DestinationLandblock);
|
|
RuntimeAuthoritativePositionRoute refusedRoute = MakeRoute(
|
|
record, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
|
|
|
|
RuntimeRemotePlacementExecutionStatus status =
|
|
drive.TryExecuteAcceptedRemotePosition(record, refusedRoute);
|
|
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Refused, status);
|
|
Assert.Equal(0, drive.PendingCount);
|
|
AssertConverged(lifetime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C2-1 review fix (delta round): the retained <c>_pending</c> entry
|
|
/// holds a LIVE Core operation (already begun via
|
|
/// <c>TryBeginExclusiveAuthoredPlacement</c>, sitting at
|
|
/// <c>AwaitingPreparation</c>) — clearing this controller's own map on
|
|
/// detach is not enough; the operation itself must be cancelled or it
|
|
/// pins its landblock prefix forever (docs/ISSUES.md #310). Asserting
|
|
/// <c>SetPositionOperationCount == 0</c> in addition to the ledger going
|
|
/// to zero is what actually proves the Core operation died, not just
|
|
/// that this controller stopped watching it — sibling tests in this file
|
|
/// (<c>Committed_...</c>, <c>ParkCollisionResidents_...</c>) already
|
|
/// check this dimension; this one previously did not, and would have
|
|
/// passed even with the old clear-only <c>DetachRoute</c>.
|
|
/// </summary>
|
|
[Fact]
|
|
public void LedgerConverges_AfterDetachRouteClearsTrackedEntries()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
RuntimeEntityRecord record = CreateRemoteRecord(
|
|
lifetime, 0x7000300Au, setupTableId: 0x02000001u);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
var route = new object();
|
|
drive.AttachRoute(route);
|
|
|
|
RuntimeAuthoritativePositionRoute positionRoute = MakeRoute(
|
|
record, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedRemotePosition(record, positionRoute));
|
|
Assert.Equal(1, drive.PendingCount);
|
|
Assert.Equal(
|
|
1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
|
|
drive.DetachRoute(route);
|
|
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
|
|
Assert.Equal(
|
|
0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C2-1 review fix (delta round): the OTHER map — <c>_awaitingAcknowledgement</c>
|
|
/// — holds an equally live Core operation (a published, unacknowledged
|
|
/// <c>Place</c> sitting at <c>AwaitingCommitAcknowledgement</c>), and the
|
|
/// prior <c>DetachRoute</c> left it live the same way. Mirrors the
|
|
/// <c>_pending</c> case above for the SECOND map DetachRoute must cancel.
|
|
/// </summary>
|
|
[Fact]
|
|
public void LedgerConverges_AfterDetachRouteCancelsAnUnacknowledgedCommit()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003012u);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
var route = new object();
|
|
drive.AttachRoute(route);
|
|
|
|
RuntimeAuthoritativePositionRoute positionRoute = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell,
|
|
new Vector3(12f, 14f, SpawnHeight));
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Committed,
|
|
drive.TryExecuteAcceptedRemotePosition(record, positionRoute));
|
|
// No host subscription is wired in this bare fixture, so the Place
|
|
// receipt is still genuinely unacknowledged — the SAME shape the
|
|
// production declined-sink FIFO retry class leaves behind.
|
|
Assert.Equal(
|
|
1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
Assert.Equal(
|
|
1, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
|
|
|
|
drive.DetachRoute(route);
|
|
|
|
Assert.Equal(
|
|
0, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
|
|
Assert.Equal(
|
|
0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C2-1 review fix (delta round), disposal safety:
|
|
/// <c>CountLiveAwaitingAcknowledgement</c> calls
|
|
/// <c>RuntimeSetPositionState.IsPlacementCurrent</c>, whose first
|
|
/// statement is <c>EnsureNotDisposed</c> — it THROWS once disposed. A
|
|
/// post-<c>Dispose()</c> <c>CaptureOwnership()</c> read is the designed
|
|
/// contract (<c>GameWindowLifetime.DisposeGameRuntime</c>:
|
|
/// <c>runtime.Dispose(); runtime.CaptureOwnership();</c>), so this
|
|
/// dimension must survive it too. Deliberately leaves the entry
|
|
/// UNDRAINED at dispose time — a genuine leak, exactly what this ledger
|
|
/// exists to report — to prove the disposed branch reports it rather
|
|
/// than hiding it behind a thrown exception.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CountLiveAwaitingAcknowledgement_SurvivesReadAfterDisposeWithoutThrowing()
|
|
{
|
|
var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003013u);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
var window = new FakeServiceWindow();
|
|
window.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
|
|
RuntimeAuthoritativePositionRoute positionRoute = MakeRoute(
|
|
record,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell,
|
|
new Vector3(12f, 14f, SpawnHeight));
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Committed,
|
|
drive.TryExecuteAcceptedRemotePosition(record, positionRoute));
|
|
|
|
lifetime.Dispose();
|
|
int pendingAfterDispose = -1;
|
|
Exception? thrown = Record.Exception(() =>
|
|
pendingAfterDispose =
|
|
lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
|
|
|
|
Assert.Null(thrown);
|
|
Assert.Equal(1, pendingAfterDispose);
|
|
}
|
|
|
|
[Fact]
|
|
public void AttachRoute_ThrowsForADifferentRouteWhileTheFirstIsStillLive()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
var window = new FakeServiceWindow();
|
|
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
|
|
var firstRoute = new object();
|
|
var secondRoute = new object();
|
|
drive.AttachRoute(firstRoute);
|
|
|
|
Assert.Throws<InvalidOperationException>(
|
|
() => drive.AttachRoute(secondRoute));
|
|
|
|
drive.DetachRoute(firstRoute);
|
|
drive.AttachRoute(secondRoute);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gate item: <c>ParkCollisionResidents</c> throws on overlap for every
|
|
/// spatial root in a retiring prefix that holds an active operation
|
|
/// (<c>RuntimeSetPositionState.cs:3387-3395</c>). This is the concrete
|
|
/// proof it stays unreachable under this route's design — both after a
|
|
/// refusal (no operation ever opened) and after a normal commit
|
|
/// (operation retired to zero), the SAME prefix can be "retired" without
|
|
/// throwing.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ParkCollisionResidents_StaysUnreachable_AfterRefusalAndAfterCommit()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
|
|
CommitLandblockCollision(lifetime, DestinationLandblock);
|
|
RuntimeEntityRecord refusedEntity = CreateRemoteRecord(lifetime, 0x7000300Bu);
|
|
AttachBody(lifetime, refusedEntity, DestinationCell);
|
|
RuntimeEntityRecord committedEntity = CreateRemoteRecord(lifetime, 0x7000300Cu);
|
|
AttachBody(lifetime, committedEntity, SourceCell);
|
|
|
|
var refusingWindow = new FakeServiceWindow();
|
|
RuntimeRemotePlacementDriveController refusingDrive =
|
|
CreateDrive(lifetime, refusingWindow);
|
|
RuntimeAuthoritativePositionRoute refusedRoute = MakeRoute(
|
|
refusedEntity,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell);
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Refused,
|
|
refusingDrive.TryExecuteAcceptedRemotePosition(refusedEntity, refusedRoute));
|
|
|
|
var allowingWindow = new FakeServiceWindow();
|
|
allowingWindow.Allow(DestinationLandblock);
|
|
RuntimeRemotePlacementDriveController committingDrive =
|
|
CreateDrive(lifetime, allowingWindow);
|
|
RuntimeAuthoritativePositionRoute committedRoute = MakeRoute(
|
|
committedEntity,
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
DestinationCell,
|
|
new Vector3(30f, 30f, SpawnHeight));
|
|
Assert.Equal(
|
|
RuntimeRemotePlacementExecutionStatus.Committed,
|
|
committingDrive.TryExecuteAcceptedRemotePosition(
|
|
committedEntity, committedRoute));
|
|
// No host subscription is wired in this bare fixture — drain the
|
|
// Place receipt exactly like production's placement-projection
|
|
// subscription would, converging the operation to zero the same way
|
|
// a live host's synchronous consumption does.
|
|
DrainPlacementFifo(lifetime);
|
|
|
|
// Both entities now sit resident in the destination prefix with NO
|
|
// active operation. Retiring that prefix must not throw.
|
|
Exception? thrown = Record.Exception(() =>
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
DestinationLandblock, includeOutdoorCells: true));
|
|
Assert.Null(thrown);
|
|
}
|
|
|
|
// ── Fixture ──────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Stands in for the production placement-projection subscription this
|
|
/// bare fixture never wires — a committed placement's <c>Place</c>
|
|
/// receipt would otherwise sit unacknowledged forever, exactly like
|
|
/// <c>RuntimeAcceptedPositionDriveControllerTests.DrainPlacementFifo</c>.
|
|
/// </summary>
|
|
private static void DrainPlacementFifo(RuntimeEntityObjectLifetime lifetime)
|
|
{
|
|
while (lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot head))
|
|
{
|
|
if (!lifetime.Physics.SetPosition.AcknowledgeProjection(head.Token))
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void AssertConverged(RuntimeEntityObjectLifetime lifetime)
|
|
{
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
|
|
}
|
|
|
|
private static RuntimeRemotePlacementDriveController CreateDrive(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
IRuntimeRemotePlacementServiceWindow window) =>
|
|
new(
|
|
lifetime,
|
|
new GameRuntimeClock(),
|
|
new UnusedCollisionSource(),
|
|
window);
|
|
|
|
private static RuntimeAuthoritativePositionRoute MakeRoute(
|
|
RuntimeEntityRecord record,
|
|
RuntimeAuthoritativePositionDisposition disposition,
|
|
uint destinationCellId,
|
|
Vector3? destinationPosition = null,
|
|
RuntimeSetPositionOperationKind operationKind =
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
PhysicsSetPositionFlags? setPositionFlags = null)
|
|
{
|
|
// Publishes the destination into the record's accepted Snapshot —
|
|
// exactly what the upstream merge
|
|
// (RuntimeEntityObjectLifetime.TryApplyPosition) already did before a
|
|
// real caller would ever reach this controller — so the controller's
|
|
// own service-window read (record.Snapshot.Position.LandblockId)
|
|
// observes the SAME destination the route was classified against.
|
|
Vector3 position = destinationPosition ?? new Vector3(10f, 10f, SpawnHeight);
|
|
record.Snapshot = record.Snapshot with
|
|
{
|
|
Position = new CreateObject.ServerPosition(
|
|
destinationCellId,
|
|
position.X,
|
|
position.Y,
|
|
position.Z,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f),
|
|
};
|
|
|
|
var authority = new RuntimeAuthoritativePositionAuthority(
|
|
new RuntimeGenerationToken(1),
|
|
record.Key!.Value,
|
|
record.PositionAuthorityVersion,
|
|
AcceptedPositionSequence: 2,
|
|
PreviousTeleportSequence: 0,
|
|
AcceptedTeleportSequence: 0,
|
|
PositionTimestampDisposition.Apply);
|
|
|
|
bool performsSetPosition = disposition is
|
|
RuntimeAuthoritativePositionDisposition.SetPosition
|
|
or RuntimeAuthoritativePositionDisposition.SetPositionSimple;
|
|
|
|
return new RuntimeAuthoritativePositionRoute(
|
|
authority,
|
|
disposition,
|
|
operationKind,
|
|
setPositionFlags ?? (performsSetPosition
|
|
? PhysicsSetPositionFlags.Teleport
|
|
| PhysicsSetPositionFlags.Slide
|
|
: PhysicsSetPositionFlags.None),
|
|
PlacementFrame: 0u,
|
|
UnparentBeforeRouting: true,
|
|
ApplyPlacementFrameBeforeRouting: false,
|
|
LeaveWorld: false,
|
|
TeleportHookPhase: RuntimeTeleportHookPhase.None,
|
|
StopInterpolating: false,
|
|
ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
|
|
PreserveHeading: false,
|
|
ZeroVelocity: false,
|
|
SendPositionImmediately: false,
|
|
CollisionBatchEligible: true);
|
|
}
|
|
|
|
private static RuntimeEntityRecord CreateRemoteRecord(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
uint guid,
|
|
uint? setupTableId = null)
|
|
{
|
|
RuntimeEntityRecord record = lifetime.RegisterEntity(
|
|
Spawn(guid, setupTableId)).Canonical!;
|
|
lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
|
|
return record;
|
|
}
|
|
|
|
private static PhysicsBody AttachBody(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityRecord record,
|
|
uint cellId)
|
|
{
|
|
lifetime.Entities.SetFullCell(
|
|
record, cellId, (cellId & 0xFFFF0000u) | 0xFFFFu);
|
|
var body = new PhysicsBody
|
|
{
|
|
Position = new Vector3(10f, 10f, SpawnHeight),
|
|
Orientation = Quaternion.Identity,
|
|
LastUpdateTime = 1d,
|
|
State = PhysicsStateFlags.Gravity,
|
|
TransientState = TransientStateFlags.Active,
|
|
};
|
|
body.SnapToCell(cellId, body.Position, body.Position);
|
|
lifetime.Entities.SetPhysicsBody(record, body);
|
|
record.ObjectClock.Activate();
|
|
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
|
return body;
|
|
}
|
|
|
|
private static WorldSession.EntitySpawn Spawn(uint guid, uint? setupTableId) =>
|
|
new(
|
|
guid,
|
|
new CreateObject.ServerPosition(
|
|
SourceCell, 10f, 10f, SpawnHeight, 1f, 0f, 0f, 0f),
|
|
setupTableId,
|
|
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
|
|
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
|
|
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
BasePaletteId: null,
|
|
ObjScale: null,
|
|
Name: "remote",
|
|
ItemType: null,
|
|
MotionState: null,
|
|
MotionTableId: 0x09000001u);
|
|
|
|
private static PhysicsEngine FlatEngine()
|
|
{
|
|
var engine = new PhysicsEngine
|
|
{
|
|
DataCache = new PhysicsDataCache(),
|
|
};
|
|
engine.AddLandblock(
|
|
SourceLandblock,
|
|
new TerrainSurface(new byte[81], new float[256]),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f,
|
|
worldOffsetY: 0f);
|
|
return engine;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors <c>RuntimeAcceptedPositionDriveControllerTests.CommitLandblockCollision</c>
|
|
/// exactly, against a bare <see cref="RuntimeEntityObjectLifetime"/>
|
|
/// instead of a full <c>GameRuntime</c>. Also observes the SOURCE
|
|
/// landblock's world frame once — Runtime's world-frame resolution is
|
|
/// arithmetic thereafter, so a destination landblock never needs its own
|
|
/// call (route 2's own fixture comment).
|
|
/// </summary>
|
|
private static void CommitLandblockCollision(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
uint landblockId)
|
|
{
|
|
var heights = new byte[81];
|
|
Array.Fill(heights, (byte)SpawnHeight);
|
|
var heightTable = new float[256];
|
|
for (int index = 0; index < heightTable.Length; index++)
|
|
heightTable[index] = index;
|
|
lifetime.Physics.ObserveLocalWorldFrame(
|
|
SourceCell, teleportAdvanced: false);
|
|
lifetime.Physics.SetPosition.BeginCollisionGeneration(landblockId, 1UL);
|
|
lifetime.Physics.Engine.AddLandblock(
|
|
landblockId,
|
|
new TerrainSurface(heights, heightTable),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 192f,
|
|
worldOffsetY: 0f);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
landblockId, 1UL, ready: true);
|
|
}
|
|
|
|
private sealed class FakeServiceWindow : IRuntimeRemotePlacementServiceWindow
|
|
{
|
|
private readonly HashSet<uint> _within = [];
|
|
|
|
internal void Allow(uint landblockId) =>
|
|
_within.Add(Canonical(landblockId));
|
|
|
|
internal void Forbid(uint landblockId) =>
|
|
_within.Remove(Canonical(landblockId));
|
|
|
|
public bool IsWithinServiceWindow(uint landblockId) =>
|
|
_within.Contains(Canonical(landblockId));
|
|
|
|
private static uint Canonical(uint landblockId) =>
|
|
(landblockId & 0xFFFF0000u) | 0xFFFFu;
|
|
}
|
|
|
|
private sealed class UnusedCollisionSource : IPreparedCollisionSource
|
|
{
|
|
public PreparedAssetPresence ProbeCollision(
|
|
PakAssetType type,
|
|
uint sourceFileId) =>
|
|
PreparedAssetPresence.Available;
|
|
|
|
public PreparedCollisionReadResult<FlatSetupCollision> ReadSetupCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
PreparedCollisionReadResult<FlatSetupCollision>.Missing;
|
|
|
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset> ReadGfxObjCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset> ReadCellStructureCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public PreparedCollisionReadResult<FlatEnvCellTopology> ReadEnvCellTopology(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public PreparedCollisionSourceStats CollisionStats => default;
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|