Retail re-cells children when their parent crosses a cell, recursively, to unbounded depth. acdream did it from a RENDER tick, so headless parented children were cell-less forever and the canonical cell had two writers. This slice makes Runtime the sole authority and demotes App's tick to presentation-only. Contract: docs/research/2026-08-04-c4-route-7-contract.md; the research that unblocked it is docs/research/2026-08-04-retail-parent-cell-propagation.md (ca96ea5e). Retail: SetPositionInternal @0x00515330 branches on `this->cell == curr_cell` @0x0051536d; the changed branch reaches change_cell @0x00513390, whose delegates leave_cell @0x00510f50 and enter_cell @0x00510ed0 self-recurse over children and write the FULL identity (add_object @0x00510ee2, objcell_id @0x00510f1e, part-array cell id @0x00510f2b, cell pointer @0x00510f35). change_cell itself has no child loop. THE TRAP, recorded because it nearly shipped: the depth-1 loop @0x0051539c-0x005153d8 is the SAME-CELL fast path (objcell_id and part-array id only, deliberately not the cell pointer), NOT the propagation. An implementer who finds it first concludes "depth-1, id-only" and strands every equipped item at a landblock boundary — the #184 class. The clincher against that reading: update_object @0x00515d10 early-returns on `parent != 0` @0x00515d40, so a child never runs its own physics tick and parent propagation is the ONLY mechanism maintaining its cell. Route 7 performs NO placement (DoPickupEvent @0x00452240 = unset_parent + leave_world; DoParentEvent @0x00452290 = set_parent + SetPlacementFrame), so it arms ConstrainTo nowhere — the leash rule INVERTS relative to routes 2/4/5, and both reviewers confirmed nothing arms. Propagation is an ITERATIVE WORKLIST, not recursion. The first implementation recursed with a depth-64 cap; both reviews independently found the cap left a truncated tail at a stale NON-ZERO cell — permanently unrecoverable, logged only under a probe flag, and on the withdraw path exactly the #184 shape AP-142 clause (a) exists to reject. Shipping a fresh #184 instance inside the slice that fixes stranded children was not acceptable, so the cap was removed rather than tuned. The worklist retires the cap, the constant, its register clause, and the failure mode together. Termination: every record on the stack is already at the target pair, so nothing can be pushed twice and a hostile A->B->A cycle collapses without a visited set. The child write deliberately bypasses the public RuntimeEntityDirectory .SetFullCell and calls the record method directly. This is LOAD-BEARING: the public method re-enters PropagateFullCellToChildren, which opens with _propagationWorklist.Clear() — routing children through it mid-drain would wipe the shared stack and silently drop every unprocessed sibling. Any future side effect added to the public SetFullCell must be mirrored by hand at that call site. Deliberate divergence, recorded not disguised: retail's removal path leaves children with a null cell pointer but a STALE nonzero objcell_id @0x005133c1. acdream does not reproduce it, because FullCellId != 0 is the liveness predicate at 45+ sites — faithful porting would mark dead children live. AP-142 records this; clause (d) records that acdream cannot gate propagation on HasPartArray the way enter_cell gates on part_array @0x00510ed8, because the flag's only writers are graphical and headless never sets it — the reason is Slice J LAYERING, not a semantic difference (retail's part_array is itself a mesh-construction product, single assignment site makeAnimObject @0x0050e930 -> CPartArray::CreateSetup @0x0050e93e). D7 adopts retail's unset_parent-before-leave_world order @0x0045227f -> @0x00452286, applied to BOTH pickup paths including the dormant executor replay. Its inertness was verified by reverting it and finding all 12 propagation tests still green — reported honestly rather than papered over with a manufactured test, and independently confirmed by both reviewers. ClassifyLeaveWorld and its request/cause types are DELETED: retail has no classification here, and method-per-cause IS the retail dispatch shape. Wiring it would have forced a vacuous teleport-sequence predicate with the #307 shape. Two review rounds plus a coordinator-required third pass; 5 MAJORs. One was a handoff failure worth recording: enter_cell's part_array guard was correctly identified as load-bearing by the research, dropped by the contract when it enumerated the writes, and inherited as an omission by the code — a right finding that evaporated across two handoffs with nobody re-reading the source. Another was a test that survived deleting the entire behaviour it claimed to pin, because its assertion read a field written unconditionally one line earlier. NoProjection is structurally unreachable from TickChild (TryResolveExactAttachment performs a strictly stronger form of the same guard one call earlier). Kept as a fail-safe, unit-tested directly, and documented in two places rather than wrapped in a fabricated end-to-end test. Headless regression test — the direct gate for this defect, which FAILED before this work because no code path existed: RuntimeLiveEntitySessionControllerTests .DirectSink_D5_StandaloneParentEventCommitsChildToParentsExactCell. Probe: ACDREAM_PROBE_CHILD_CELL=1 emits [child-cell] lines at all four write sites (attach / headless-attach / propagate / withdraw / delete). TEMPORARY. Complete Release suite MEASURED at 11,079 passed / 4 skipped / 0 failed (baseline 11,063 atcff52c44, +16). An allocation flake appeared once under load and was proven NOT this slice by reachability — RuntimeCollisionReportingState contains zero SetFullCell and zero ParentAttachments references. STILL OWED: the two-client connected gate (equip/unequip, carry across landblock boundaries, pickup, loot, reconnect) with ACDREAM_PROBE_CHILD_CELL=1, and a session counts only if [child-cell] cause=propagate lines appear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
533 lines
21 KiB
C#
533 lines
21 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
|
|
namespace AcDream.Runtime.Tests.Physics;
|
|
|
|
public sealed class RuntimeAuthoritativePositionRouteClassifierTests
|
|
{
|
|
[Theory]
|
|
[InlineData(RuntimePositionEntityKind.LocalPlayer,
|
|
RuntimeSetPositionOperationKind.InitialLogin,
|
|
RuntimeTeleportHookPhase.AfterEnterWorld)]
|
|
[InlineData(RuntimePositionEntityKind.Remote,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
RuntimeTeleportHookPhase.None)]
|
|
[InlineData(RuntimePositionEntityKind.Projectile,
|
|
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
|
|
RuntimeTeleportHookPhase.None)]
|
|
internal void TopLevelCreate_AlwaysUsesRetailEnterWorldFlags(
|
|
RuntimePositionEntityKind kind,
|
|
RuntimeSetPositionOperationKind expectedKind,
|
|
RuntimeTeleportHookPhase expectedHookPhase)
|
|
{
|
|
RuntimeAuthoritativePositionRoute route =
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate(
|
|
new RuntimeCreatePositionRouteRequest(
|
|
Authority(),
|
|
kind,
|
|
RuntimeCreateResidenceKind.TopLevel,
|
|
Position(),
|
|
new RuntimePositionPlacementFacts(
|
|
PhysicsStateFlags.Static
|
|
| PhysicsStateFlags.Hidden
|
|
| PhysicsStateFlags.NoDraw,
|
|
HasAuthoredMoverShape: false)));
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
route.Disposition);
|
|
Assert.Equal(expectedKind, route.OperationKind);
|
|
Assert.Equal(0x11u, (uint)route.SetPositionFlags);
|
|
Assert.Equal(expectedHookPhase, route.TeleportHookPhase);
|
|
Assert.True(route.PerformsSetPosition);
|
|
Assert.False(route.CollisionBatchEligible);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(RuntimeCreateResidenceKind.Parented)]
|
|
[InlineData(RuntimeCreateResidenceKind.PickedUp)]
|
|
internal void NewParentedOrPickedUpCreate_RemainsCelllessAndAwaitsPosition(
|
|
RuntimeCreateResidenceKind residence)
|
|
{
|
|
RuntimeAuthoritativePositionRoute route =
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate(
|
|
new RuntimeCreatePositionRouteRequest(
|
|
Authority(),
|
|
RuntimePositionEntityKind.Remote,
|
|
residence,
|
|
AcceptedWirePosition: null,
|
|
default));
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition,
|
|
route.Disposition);
|
|
Assert.False(route.LeaveWorld);
|
|
Assert.False(route.PerformsSetPosition);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(InvalidPositions))]
|
|
internal void CreateAndPositionRejectMalformedAuthoritativeFrames(
|
|
CreateObject.ServerPosition invalid)
|
|
{
|
|
RuntimeAuthoritativePositionRoute create =
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate(
|
|
new RuntimeCreatePositionRouteRequest(
|
|
Authority(),
|
|
RuntimePositionEntityKind.Remote,
|
|
RuntimeCreateResidenceKind.TopLevel,
|
|
invalid,
|
|
default));
|
|
RuntimeAuthoritativePositionRoute update = ClassifyRemote(
|
|
position: invalid,
|
|
committedCellId: Cell,
|
|
hasContact: true,
|
|
playerDistance: 1f);
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedData,
|
|
create.Disposition);
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedData,
|
|
update.Disposition);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(RuntimeAcceptedPositionSource.PositionEvent)]
|
|
[InlineData(RuntimeAcceptedPositionSource.SameIncarnationCreate)]
|
|
internal void AcceptedPosition_UnparentsAndAppliesDefaultZeroPlacementBeforeRoute(
|
|
RuntimeAcceptedPositionSource source)
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyRemote(
|
|
source: source,
|
|
placementFrame: null,
|
|
committedCellId: Cell,
|
|
hasContact: true,
|
|
playerDistance: 10f);
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
route.Disposition);
|
|
Assert.True(route.UnparentBeforeRouting);
|
|
Assert.True(route.ApplyPlacementFrameBeforeRouting);
|
|
Assert.Equal(0u, route.PlacementFrame);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnimatedAcceptedPosition_UnparentsButSkipsPlacementFrameReset()
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyRemote(
|
|
committedCellId: Cell,
|
|
hasContact: true,
|
|
playerDistance: 10f,
|
|
hasAnimations: true);
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
route.Disposition);
|
|
Assert.True(route.UnparentBeforeRouting);
|
|
Assert.False(route.ApplyPlacementFrameBeforeRouting);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(95.999f, RuntimeAuthoritativePositionDisposition.Interpolate)]
|
|
[InlineData(96f, RuntimeAuthoritativePositionDisposition.SetPositionSimple)]
|
|
internal void SameIncarnationCreate_ForcesContactRoute(
|
|
float distance,
|
|
RuntimeAuthoritativePositionDisposition expected)
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyRemote(
|
|
source: RuntimeAcceptedPositionSource.SameIncarnationCreate,
|
|
committedCellId: Cell,
|
|
hasContact: false,
|
|
playerDistance: distance);
|
|
|
|
Assert.Equal(expected, route.Disposition);
|
|
Assert.False(route.ConstrainBeforeRouting);
|
|
Assert.True(route.ConstrainAfterRouting);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(ushort.MaxValue, 0)]
|
|
[InlineData(0x8000, 0)]
|
|
internal void RemoteFreshTeleport_UsesWrapSafeTimestampAndRetailHook(
|
|
ushort previousTeleport,
|
|
ushort acceptedTeleport)
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyRemote(
|
|
authority: Authority(previousTeleport, acceptedTeleport),
|
|
committedCellId: Cell,
|
|
hasContact: false,
|
|
playerDistance: float.NaN);
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
route.Disposition);
|
|
Assert.Equal(0x1012u, (uint)route.SetPositionFlags);
|
|
Assert.Equal(RuntimeTeleportHookPhase.BeforePositionOperation,
|
|
route.TeleportHookPhase);
|
|
Assert.True(route.ConstrainAfterRouting);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoteCellless_UsesCommittedResidenceNotAcceptedWireCell()
|
|
{
|
|
CreateObject.ServerPosition accepted = Position(Cell, 200f);
|
|
|
|
RuntimeAuthoritativePositionRoute route = ClassifyRemote(
|
|
position: accepted,
|
|
committedCellId: null,
|
|
hasContact: true,
|
|
playerDistance: 1f);
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
route.Disposition);
|
|
Assert.Equal(RuntimeTeleportHookPhase.BeforePositionOperation,
|
|
route.TeleportHookPhase);
|
|
Assert.Equal(Cell, accepted.LandblockId);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoteNoContact_PerformsNoPositionOperationOrConstraint()
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyRemote(
|
|
committedCellId: Cell,
|
|
hasContact: false,
|
|
playerDistance: float.NaN);
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.NoPositionOperation,
|
|
route.Disposition);
|
|
Assert.False(route.ConstrainAfterRouting);
|
|
Assert.False(route.PerformsSetPosition);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0f, RuntimeAuthoritativePositionDisposition.Interpolate, false)]
|
|
[InlineData(95.999f, RuntimeAuthoritativePositionDisposition.Interpolate, false)]
|
|
[InlineData(96f, RuntimeAuthoritativePositionDisposition.SetPositionSimple, true)]
|
|
[InlineData(500f, RuntimeAuthoritativePositionDisposition.SetPositionSimple, true)]
|
|
internal void RemoteContact_UsesExactNinetySixBoundary(
|
|
float distance,
|
|
RuntimeAuthoritativePositionDisposition expected,
|
|
bool stopInterpolating)
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyRemote(
|
|
committedCellId: Cell,
|
|
hasContact: true,
|
|
playerDistance: distance);
|
|
|
|
Assert.Equal(expected, route.Disposition);
|
|
Assert.Equal(stopInterpolating, route.StopInterpolating);
|
|
Assert.True(route.ConstrainAfterRouting);
|
|
Assert.Equal(stopInterpolating ? 0x1012u : 0u,
|
|
(uint)route.SetPositionFlags);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(float.NaN)]
|
|
[InlineData(float.PositiveInfinity)]
|
|
[InlineData(float.NegativeInfinity)]
|
|
[InlineData(-0.001f)]
|
|
internal void RemoteContact_RejectsMalformedDerivedDistance(float distance)
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyRemote(
|
|
committedCellId: Cell,
|
|
hasContact: true,
|
|
playerDistance: distance);
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedData,
|
|
route.Disposition);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemotePositionPackVelocity_IsDeadRoutingInput()
|
|
{
|
|
RuntimeAcceptedPositionRouteRequest request = RemoteRequest(
|
|
committedCellId: Cell,
|
|
hasContact: true,
|
|
playerDistance: 10f) with
|
|
{
|
|
PositionPackVelocity = new Vector3(float.NaN, float.PositiveInfinity, 7f),
|
|
};
|
|
|
|
RuntimeAuthoritativePositionRoute route =
|
|
RuntimeAuthoritativePositionRouteClassifier
|
|
.ClassifyAcceptedPosition(request);
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
route.Disposition);
|
|
Assert.True(route.Accepted);
|
|
}
|
|
|
|
[Fact]
|
|
public void LocalFreshForce_PreservesHeadingBlipsAndAcknowledgesImmediately()
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyLocal(
|
|
Authority(
|
|
previousTeleport: 10,
|
|
acceptedTeleport: 10,
|
|
disposition: PositionTimestampDisposition.ForcePosition));
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
|
route.Disposition);
|
|
Assert.Equal(0x1012u, (uint)route.SetPositionFlags);
|
|
Assert.True(route.PreserveHeading);
|
|
Assert.True(route.SendPositionImmediately);
|
|
Assert.False(route.UnparentBeforeRouting);
|
|
Assert.False(route.ApplyPlacementFrameBeforeRouting);
|
|
Assert.False(route.ConstrainAfterRouting);
|
|
}
|
|
|
|
[Fact]
|
|
public void ForcePosition_IsRejectedForRemoteOrUnequalTeleport()
|
|
{
|
|
RuntimeAuthoritativePositionAuthority force = Authority(
|
|
10, 10, PositionTimestampDisposition.ForcePosition);
|
|
RuntimeAuthoritativePositionRoute remote = ClassifyRemote(authority: force);
|
|
RuntimeAuthoritativePositionRoute unequal = ClassifyLocal(
|
|
Authority(10, 11, PositionTimestampDisposition.ForcePosition));
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedAuthority,
|
|
remote.Disposition);
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedAuthority,
|
|
unequal.Disposition);
|
|
}
|
|
|
|
[Fact]
|
|
public void LocalFreshTeleport_SetsPositionConstrainsAndZerosVelocity()
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyLocal(
|
|
Authority(ushort.MaxValue, 0));
|
|
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
|
route.Disposition);
|
|
Assert.Equal(0x1012u, (uint)route.SetPositionFlags);
|
|
Assert.True(route.UnparentBeforeRouting);
|
|
Assert.True(route.ApplyPlacementFrameBeforeRouting);
|
|
Assert.True(route.ConstrainAfterRouting);
|
|
Assert.True(route.ZeroVelocity);
|
|
Assert.Equal(RuntimeTeleportHookPhase.AfterPositionOperation,
|
|
route.TeleportHookPhase);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, false, false, RuntimeAuthoritativePositionDisposition.NoPositionOperation)]
|
|
[InlineData(false, true, true, RuntimeAuthoritativePositionDisposition.NoPositionOperation)]
|
|
[InlineData(true, false, true, RuntimeAuthoritativePositionDisposition.NoPositionOperation)]
|
|
[InlineData(true, true, false, RuntimeAuthoritativePositionDisposition.Interpolate)]
|
|
[InlineData(true, true, true, RuntimeAuthoritativePositionDisposition.Interpolate)]
|
|
internal void LocalOrdinary_AlwaysConstrainsAndInterpolatesOnlyByOptionAndContact(
|
|
bool usePositionFromServer,
|
|
bool hasContact,
|
|
bool hasVelocity,
|
|
RuntimeAuthoritativePositionDisposition expected)
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = ClassifyLocal(
|
|
Authority(),
|
|
usePositionFromServer,
|
|
hasContact,
|
|
hasVelocity
|
|
? new Vector3(float.NaN, float.PositiveInfinity, 1f)
|
|
: null);
|
|
|
|
Assert.Equal(expected, route.Disposition);
|
|
Assert.True(route.ConstrainBeforeRouting);
|
|
Assert.False(route.ConstrainAfterRouting);
|
|
}
|
|
|
|
[Fact]
|
|
public void HiddenAndNoDrawDoNotGatePlacement_ButReportingRemainsSeparate()
|
|
{
|
|
RuntimePositionPlacementFacts visible = new(
|
|
PhysicsStateFlags.None,
|
|
HasAuthoredMoverShape: true);
|
|
RuntimePositionPlacementFacts noDraw = visible with
|
|
{
|
|
PhysicsState = PhysicsStateFlags.NoDraw,
|
|
};
|
|
RuntimePositionPlacementFacts hidden = visible with
|
|
{
|
|
PhysicsState = PhysicsStateFlags.Hidden,
|
|
};
|
|
RuntimePositionPlacementFacts ignored = visible with
|
|
{
|
|
PhysicsState = PhysicsStateFlags.IgnoreCollisions,
|
|
};
|
|
|
|
RuntimeAuthoritativePositionRoute visibleRoute =
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate(
|
|
new RuntimeCreatePositionRouteRequest(
|
|
Authority(),
|
|
RuntimePositionEntityKind.Remote,
|
|
RuntimeCreateResidenceKind.TopLevel,
|
|
Position(),
|
|
visible));
|
|
RuntimeAuthoritativePositionRoute noDrawRoute =
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate(
|
|
new RuntimeCreatePositionRouteRequest(
|
|
Authority(),
|
|
RuntimePositionEntityKind.Remote,
|
|
RuntimeCreateResidenceKind.TopLevel,
|
|
Position(),
|
|
noDraw));
|
|
RuntimeAuthoritativePositionRoute hiddenRoute =
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate(
|
|
new RuntimeCreatePositionRouteRequest(
|
|
Authority(),
|
|
RuntimePositionEntityKind.Remote,
|
|
RuntimeCreateResidenceKind.TopLevel,
|
|
Position(),
|
|
hidden));
|
|
RuntimeAuthoritativePositionRoute ignoredRoute =
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate(
|
|
new RuntimeCreatePositionRouteRequest(
|
|
Authority(),
|
|
RuntimePositionEntityKind.Remote,
|
|
RuntimeCreateResidenceKind.TopLevel,
|
|
Position(),
|
|
ignored));
|
|
|
|
Assert.All([visibleRoute, noDrawRoute, hiddenRoute, ignoredRoute], route =>
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
route.Disposition));
|
|
Assert.True(visibleRoute.CollisionBatchEligible);
|
|
Assert.True(noDrawRoute.CollisionBatchEligible);
|
|
Assert.False(hiddenRoute.CollisionBatchEligible);
|
|
Assert.True(ignoredRoute.CollisionBatchEligible);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProjectilePosition_UsesRemoteMoveOrTeleportClassification()
|
|
{
|
|
RuntimeAcceptedPositionRouteRequest request = RemoteRequest(
|
|
committedCellId: Cell,
|
|
hasContact: true,
|
|
playerDistance: 96f) with
|
|
{
|
|
EntityKind = RuntimePositionEntityKind.Projectile,
|
|
};
|
|
|
|
RuntimeAuthoritativePositionRoute route =
|
|
RuntimeAuthoritativePositionRouteClassifier
|
|
.ClassifyAcceptedPosition(request);
|
|
|
|
Assert.Equal(RuntimeSetPositionOperationKind.ProjectileAuthoritative,
|
|
route.OperationKind);
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
|
route.Disposition);
|
|
}
|
|
|
|
[Fact]
|
|
public void StaleOrMalformedAuthorityIsRejectedWithoutRouting()
|
|
{
|
|
RuntimeAuthoritativePositionRoute stale = ClassifyRemote(
|
|
authority: Authority(previousTeleport: 0, acceptedTeleport: ushort.MaxValue));
|
|
RuntimeAuthoritativePositionRoute wrongGeneration = ClassifyRemote(
|
|
authority: Authority() with { Generation = default });
|
|
RuntimeAuthoritativePositionRoute wrongEntity = ClassifyRemote(
|
|
authority: Authority() with { Entity = default });
|
|
RuntimeAuthoritativePositionRoute wrongVersion = ClassifyRemote(
|
|
authority: Authority() with { PositionAuthorityVersion = 0UL });
|
|
|
|
Assert.All([stale, wrongGeneration, wrongEntity, wrongVersion], route =>
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedAuthority,
|
|
route.Disposition));
|
|
}
|
|
|
|
public static TheoryData<CreateObject.ServerPosition> InvalidPositions => new()
|
|
{
|
|
Position(cell: 0u),
|
|
Position(x: float.NaN),
|
|
Position(x: float.PositiveInfinity),
|
|
Position(rotationW: 0f),
|
|
Position(rotationW: float.NaN),
|
|
};
|
|
|
|
private const uint Cell = 0x0101FFFFu;
|
|
|
|
private static RuntimeAuthoritativePositionAuthority Authority(
|
|
ushort previousTeleport = 10,
|
|
ushort acceptedTeleport = 10,
|
|
PositionTimestampDisposition disposition = PositionTimestampDisposition.Apply) =>
|
|
new(
|
|
new RuntimeGenerationToken(7),
|
|
new RuntimeEntityKey(0x70000001u, 3),
|
|
PositionAuthorityVersion: 11UL,
|
|
AcceptedPositionSequence: 20,
|
|
previousTeleport,
|
|
acceptedTeleport,
|
|
disposition);
|
|
|
|
private static RuntimeAuthoritativePositionRoute ClassifyRemote(
|
|
RuntimeAuthoritativePositionAuthority? authority = null,
|
|
RuntimeAcceptedPositionSource source = RuntimeAcceptedPositionSource.PositionEvent,
|
|
CreateObject.ServerPosition? position = null,
|
|
uint? placementFrame = 0u,
|
|
uint? committedCellId = Cell,
|
|
bool hasContact = true,
|
|
float playerDistance = 10f,
|
|
bool hasAnimations = false) =>
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(
|
|
RemoteRequest(
|
|
authority,
|
|
source,
|
|
position,
|
|
placementFrame,
|
|
committedCellId,
|
|
hasContact,
|
|
playerDistance,
|
|
hasAnimations));
|
|
|
|
private static RuntimeAcceptedPositionRouteRequest RemoteRequest(
|
|
RuntimeAuthoritativePositionAuthority? authority = null,
|
|
RuntimeAcceptedPositionSource source = RuntimeAcceptedPositionSource.PositionEvent,
|
|
CreateObject.ServerPosition? position = null,
|
|
uint? placementFrame = 0u,
|
|
uint? committedCellId = Cell,
|
|
bool hasContact = true,
|
|
float playerDistance = 10f,
|
|
bool hasAnimations = false) =>
|
|
new(
|
|
authority ?? Authority(),
|
|
RuntimePositionEntityKind.Remote,
|
|
source,
|
|
position ?? Position(),
|
|
placementFrame,
|
|
PositionPackVelocity: Vector3.One,
|
|
committedCellId,
|
|
hasContact,
|
|
playerDistance,
|
|
UsePositionFromServer: false,
|
|
hasAnimations,
|
|
default);
|
|
|
|
private static RuntimeAuthoritativePositionRoute ClassifyLocal(
|
|
RuntimeAuthoritativePositionAuthority authority,
|
|
bool usePositionFromServer = false,
|
|
bool hasContact = true,
|
|
Vector3? velocity = null) =>
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(
|
|
new RuntimeAcceptedPositionRouteRequest(
|
|
authority,
|
|
RuntimePositionEntityKind.LocalPlayer,
|
|
RuntimeAcceptedPositionSource.PositionEvent,
|
|
Position(),
|
|
PlacementFrame: null,
|
|
PositionPackVelocity: velocity,
|
|
CommittedCellId: Cell,
|
|
hasContact,
|
|
PlayerDistance: 0f,
|
|
usePositionFromServer,
|
|
HasAnimations: false,
|
|
default));
|
|
|
|
private static CreateObject.ServerPosition Position(
|
|
uint cell = Cell,
|
|
float x = 10f,
|
|
float rotationW = 1f) =>
|
|
new(
|
|
cell,
|
|
x,
|
|
20f,
|
|
30f,
|
|
rotationW,
|
|
0f,
|
|
0f,
|
|
0f);
|
|
}
|