Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.
Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.
D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.
Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.
Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.
A1 ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
creature — a regression this slice introduced while closing a
structurally identical hole. Now maps to NearInterpolate; switch made
total with a throwing default proven unreachable.
R1 D2's write-nothing shape existed on the player arm only; NPC packets
fell through and wrote the body. Retail makes no player/NPC distinction.
R2 report_collision_end(this,1) @0x00514F31 was bound to
ShadowObjects.Suspend, a port of a DIFFERENT retail function
(remove_shadows_from_cells) that teleport_hook never calls. Now routes
to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
ForceEnd in an admission-blocking transaction so a DoCollisionEnd
callback cannot recreate the contact table.
R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
(~1,000+ m/s) and planned a run cycle from it. Both the install and
RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.
BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.
Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.
Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.
Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).
Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.
Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.
Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.
Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).
STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
435 lines
18 KiB
C#
435 lines
18 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Physics;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using AcDream.Core.World;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
|
|
namespace AcDream.App.Tests.Physics;
|
|
|
|
/// <summary>
|
|
/// C4 route 4a: behavioural proof that the generic top-of-<c>OnPosition</c>
|
|
/// render-pose write no longer double-writes a remote whose accepted Position
|
|
/// classifies to
|
|
/// <see cref="RuntimeAuthoritativePositionDisposition.NoPositionOperation"/> or
|
|
/// <see cref="RuntimeAuthoritativePositionDisposition.Interpolate"/>, and
|
|
/// still writes it for every classification route 4a does not own.
|
|
///
|
|
/// <para>
|
|
/// These tests call the PRODUCTION decision-and-mutation entry point
|
|
/// (<see cref="LiveEntityNetworkUpdateController.TryApplyGenericRemoteRenderPose"/>)
|
|
/// against a real <see cref="WorldEntity"/> and real
|
|
/// <see cref="RuntimeAuthoritativePositionRouteClassifier"/> output. Nothing
|
|
/// is re-derived in the test body: revert the suppression inside that method
|
|
/// and the two suppression tests fail; widen it to <c>route is not null</c>
|
|
/// and four of the five legacy-write tests fail (the fifth,
|
|
/// <see cref="NoClassificationAtAll_StillTakesTheLegacyRenderPoseWrite"/>,
|
|
/// passes a null route and so survives that particular widening — it
|
|
/// discriminates the OTHER direction, an over-narrow guard). That is the gap
|
|
/// #292 recorded for route 2's source-text pin.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The suite also pins the ORDERING carve-out
|
|
/// (<see cref="LiveEntityNetworkUpdateController.ApplyRemoteContactRouting"/>):
|
|
/// an airborne body's contact packet classifies <c>Interpolate</c>, so
|
|
/// precedence — not classification — is the only thing keeping a landing
|
|
/// creature planting instead of gliding.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class LiveEntityNetworkRemoteSteadyStateIntegrationTests
|
|
{
|
|
private const uint OldCell = 0x01010001u;
|
|
private const uint WireCell = 0x01010002u;
|
|
private static readonly Vector3 OldPosition = new(5f, 5f, 5f);
|
|
private static readonly Vector3 WirePosition = new(6f, 7f, 8f);
|
|
|
|
[Fact]
|
|
public void NoPositionOperation_LeavesTheRenderEntityPoseAndCellUntouched()
|
|
{
|
|
// Retail's airborne no-op (D1): MoveOrTeleport @0x00516330 returns 0
|
|
// for arg4 == 0 and writes nothing. CommittedCellId is nonzero so the
|
|
// cell-less SetPosition branch cannot be what is selected here.
|
|
RuntimeAuthoritativePositionRoute route =
|
|
Classify(hasContact: false, playerDistance: 1f);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
|
|
route.Disposition);
|
|
|
|
AssertRenderPoseSuppressed(route);
|
|
}
|
|
|
|
[Fact]
|
|
public void Interpolate_LeavesTheRenderEntityPoseAndCellUntouched()
|
|
{
|
|
// Retail's near InterpolateTo queue @0x005163AF. The resolved body,
|
|
// not the raw wire packet, is the branch tail's only writer of the
|
|
// render entity — writing the wire pose here first would be the
|
|
// second writer route 2's original defect consisted of.
|
|
RuntimeAuthoritativePositionRoute route =
|
|
Classify(hasContact: true, playerDistance: 10f);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
route.Disposition);
|
|
|
|
AssertRenderPoseSuppressed(route);
|
|
}
|
|
|
|
[Fact]
|
|
public void FarSetPositionSimple_StillTakesTheLegacyRenderPoseWrite()
|
|
{
|
|
RuntimeAuthoritativePositionRoute route =
|
|
Classify(hasContact: true, playerDistance: 200f);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
|
route.Disposition);
|
|
|
|
AssertRenderPoseWritten(route);
|
|
}
|
|
|
|
[Fact]
|
|
public void CellLessRemote_StillTakesTheLegacyRenderPoseWrite()
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = Classify(
|
|
hasContact: false,
|
|
playerDistance: 1f,
|
|
committedCellId: 0u);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
route.Disposition);
|
|
|
|
AssertRenderPoseWritten(route);
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectedAuthority_StillTakesTheLegacyRenderPoseWrite()
|
|
{
|
|
RuntimeAuthoritativePositionRoute route = Classify(
|
|
hasContact: true,
|
|
playerDistance: 10f,
|
|
disposition: PositionTimestampDisposition.Rejected);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.RejectedAuthority,
|
|
route.Disposition);
|
|
|
|
AssertRenderPoseWritten(route);
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectedData_StillTakesTheLegacyRenderPoseWrite()
|
|
{
|
|
RuntimeAuthoritativePositionRoute route =
|
|
Classify(hasContact: true, playerDistance: float.NaN);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.RejectedData,
|
|
route.Disposition);
|
|
|
|
AssertRenderPoseWritten(route);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoClassificationAtAll_StillTakesTheLegacyRenderPoseWrite()
|
|
{
|
|
// The login window (no local-player controller yet) and a canonical
|
|
// record that has not claimed a local id both produce a null route.
|
|
// Its doc comment promises the legacy path runs unchanged — so the
|
|
// write must still happen.
|
|
AssertRenderPoseWritten(null);
|
|
}
|
|
|
|
// ── Ordering: the airborne snap outranks route 4a's near branch ─────────
|
|
|
|
[Fact]
|
|
public void AirborneBodyWithAContactPacket_SnapsInsteadOfEnqueuing()
|
|
{
|
|
// The NPC LANDING packet: the client still believes the body airborne
|
|
// and ACE reports it in contact, close by. That classifies
|
|
// Interpolate — and AP-87's conditions are all benign (0.5 m, sampled,
|
|
// DR-ticked) — so route 4a's branch would ENQUEUE it. It must SNAP:
|
|
// a drudge knocked off a ledge plants, it does not glide to the
|
|
// ground over a packet interval.
|
|
RuntimeAuthoritativePositionRoute route =
|
|
Classify(hasContact: true, playerDistance: 10f);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
route.Disposition);
|
|
|
|
using var fixture = new RemotePlacementDriveFixture();
|
|
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
|
fixture.AddRemote(0x70005001u, new Vector3(12f, 14f, 7f));
|
|
remote.Body.Position = new Vector3(10f, 10f, 5f);
|
|
// AP-140 (retired 2026-08-04): free flight is retail's CONTACT_TS-clear
|
|
// (`InterpolationManager::adjust_offset` gates on `transient_state & 1`
|
|
// @0x00555D52), not the client `Airborne` walkability flag. Both are
|
|
// stated so the test reads the same before and after that change.
|
|
remote.Airborne = true;
|
|
remote.Body.TransientState = TransientStateFlags.Active;
|
|
var landing = new Vector3(10.5f, 10f, 5f); // 0.5 m — well within 4 m.
|
|
|
|
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
|
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
|
fixture.Drive,
|
|
record,
|
|
remote,
|
|
route,
|
|
landing,
|
|
Quaternion.Identity,
|
|
willBeDrTicked: true,
|
|
runTeleportHook: () => true);
|
|
|
|
Assert.Equal(
|
|
LiveEntityNetworkUpdateController.RemoteContactArm.AirborneSnap,
|
|
routing.Arm);
|
|
Assert.Equal(landing, remote.Body.Position);
|
|
}
|
|
|
|
[Fact]
|
|
public void GroundedBodyWithANearContactPacket_TakesRoute4asInterpolateBranch()
|
|
{
|
|
// The same packet on a body the client already considers grounded is
|
|
// route 4a's own branch: queued for the per-tick catch-up, body
|
|
// untouched. This is what proves the airborne test above is a
|
|
// PRECEDENCE carve-out and not a blanket disable of route 4a.
|
|
using var fixture = new RemotePlacementDriveFixture();
|
|
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
|
fixture.AddRemote(0x70005002u, new Vector3(12f, 14f, 7f));
|
|
remote.Body.Position = new Vector3(10f, 10f, 5f);
|
|
remote.Airborne = false;
|
|
var target = new Vector3(10.5f, 10f, 5f);
|
|
|
|
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
|
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
|
fixture.Drive,
|
|
record,
|
|
remote,
|
|
Classify(hasContact: true, playerDistance: 10f),
|
|
target,
|
|
Quaternion.Identity,
|
|
willBeDrTicked: true,
|
|
runTeleportHook: () => true);
|
|
|
|
Assert.Equal(
|
|
LiveEntityNetworkUpdateController.RemoteContactArm
|
|
.SteadyStateInterpolate,
|
|
routing.Arm);
|
|
Assert.Equal(new Vector3(10f, 10f, 5f), remote.Body.Position);
|
|
}
|
|
|
|
// ── AP-140 (retired 2026-08-04): the gate's predicate is CONTACT ────────
|
|
|
|
/// <summary>
|
|
/// The divergence AP-140 recorded, as behaviour. A remote in CONTACT with
|
|
/// a NON-walkable face — sliding down a steep roof — is
|
|
/// <c>Airborne == true</c> by the client flag's own definition
|
|
/// (<c>!Body.OnWalkable</c>), and used to be hard-snapped at
|
|
/// UpdatePosition cadence. Retail interpolates it:
|
|
/// <c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its entire
|
|
/// body on <c>transient_state & 1</c> @0x00555D52, and bit 0 is
|
|
/// <c>CONTACT_TS</c> (acclient.h:3690), not <c>ON_WALKABLE_TS</c> (0x2).
|
|
///
|
|
/// <para>
|
|
/// This is the ONE state on which the two predicates disagree, so it is
|
|
/// the only test that can discriminate the fix: point the gate back at
|
|
/// <c>remote.Airborne</c> and this test alone fails, with
|
|
/// <c>AirborneSnap</c> and a moved body.
|
|
/// </para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void SteepContactBody_InterpolatesInsteadOfSnapping()
|
|
{
|
|
using var fixture = new RemotePlacementDriveFixture();
|
|
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
|
fixture.AddRemote(0x70005003u, new Vector3(12f, 14f, 7f));
|
|
var before = new Vector3(10f, 10f, 5f);
|
|
remote.Body.Position = before;
|
|
// Contact with a face too steep to stand on: CONTACT set, ON_WALKABLE
|
|
// clear — exactly what the sweep's SetPositionInternal commit derives
|
|
// on a 52-degree roof since Bug B (204d0ae0) deleted the per-tick
|
|
// walkability forge that used to make this state unreachable.
|
|
remote.Body.TransientState =
|
|
TransientStateFlags.Active | TransientStateFlags.Contact;
|
|
Assert.True(remote.Body.InContact);
|
|
Assert.False(remote.Body.OnWalkable);
|
|
// The client flag agrees with its five writers, all `!Body.OnWalkable`.
|
|
remote.Airborne = !remote.Body.OnWalkable;
|
|
Assert.True(remote.Airborne);
|
|
|
|
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
|
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
|
fixture.Drive,
|
|
record,
|
|
remote,
|
|
Classify(hasContact: true, playerDistance: 10f),
|
|
before + new Vector3(0.5f, 0f, 0f),
|
|
Quaternion.Identity,
|
|
willBeDrTicked: true,
|
|
runTeleportHook: () => true);
|
|
|
|
Assert.Equal(
|
|
LiveEntityNetworkUpdateController.RemoteContactArm
|
|
.SteadyStateInterpolate,
|
|
routing.Arm);
|
|
// The interpolate arm enqueues a waypoint and leaves the body to the
|
|
// per-tick catch-up; the snap arm would have written it directly.
|
|
Assert.Equal(before, remote.Body.Position);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The regression guard on the other side of the same gate: a genuinely
|
|
/// free-flying remote — no contact with anything — must still take the
|
|
/// hard snap. Retail's <c>adjust_offset</c> writes nothing at all with
|
|
/// <c>CONTACT_TS</c> clear, so the queued waypoint cannot advance the
|
|
/// body and the authoritative position is the only thing that can.
|
|
/// Widen the gate to "always interpolate" and this test fails.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FreeFlightBodyWithNoContact_StillSnaps()
|
|
{
|
|
using var fixture = new RemotePlacementDriveFixture();
|
|
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
|
fixture.AddRemote(0x70005004u, new Vector3(12f, 14f, 7f));
|
|
remote.Body.Position = new Vector3(10f, 10f, 5f);
|
|
remote.Body.TransientState = TransientStateFlags.Active;
|
|
Assert.False(remote.Body.InContact);
|
|
remote.Airborne = !remote.Body.OnWalkable;
|
|
var landing = new Vector3(10.5f, 10f, 5f);
|
|
|
|
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
|
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
|
fixture.Drive,
|
|
record,
|
|
remote,
|
|
Classify(hasContact: true, playerDistance: 10f),
|
|
landing,
|
|
Quaternion.Identity,
|
|
willBeDrTicked: true,
|
|
runTeleportHook: () => true);
|
|
|
|
Assert.Equal(
|
|
LiveEntityNetworkUpdateController.RemoteContactArm.AirborneSnap,
|
|
routing.Arm);
|
|
Assert.Equal(landing, remote.Body.Position);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The unchanged third state: contact with WALKABLE ground. Both
|
|
/// predicates agree here, and both before and after AP-140's fix the
|
|
/// packet interpolates. Pinned so a future edit cannot "simplify" the
|
|
/// gate into something that only satisfies the two tests above.
|
|
/// </summary>
|
|
[Fact]
|
|
public void WalkableGroundedBody_IsUnchangedAndStillInterpolates()
|
|
{
|
|
using var fixture = new RemotePlacementDriveFixture();
|
|
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
|
fixture.AddRemote(0x70005005u, new Vector3(12f, 14f, 7f));
|
|
var before = new Vector3(10f, 10f, 5f);
|
|
remote.Body.Position = before;
|
|
remote.Body.TransientState = TransientStateFlags.Active
|
|
| TransientStateFlags.Contact
|
|
| TransientStateFlags.OnWalkable;
|
|
remote.Airborne = !remote.Body.OnWalkable;
|
|
Assert.False(remote.Airborne);
|
|
|
|
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
|
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
|
fixture.Drive,
|
|
record,
|
|
remote,
|
|
Classify(hasContact: true, playerDistance: 10f),
|
|
before + new Vector3(0.5f, 0f, 0f),
|
|
Quaternion.Identity,
|
|
willBeDrTicked: true,
|
|
runTeleportHook: () => true);
|
|
|
|
Assert.Equal(
|
|
LiveEntityNetworkUpdateController.RemoteContactArm
|
|
.SteadyStateInterpolate,
|
|
routing.Arm);
|
|
Assert.Equal(before, remote.Body.Position);
|
|
}
|
|
|
|
private static void AssertRenderPoseSuppressed(
|
|
RuntimeAuthoritativePositionRoute? route)
|
|
{
|
|
WorldEntity entity = MakeEntity();
|
|
var beforeRotation = entity.Rotation;
|
|
|
|
bool written = LiveEntityNetworkUpdateController
|
|
.TryApplyGenericRemoteRenderPose(
|
|
entity,
|
|
route,
|
|
WirePosition,
|
|
WireCell,
|
|
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 1.1f));
|
|
|
|
Assert.False(written);
|
|
Assert.Equal(OldPosition, entity.Position);
|
|
Assert.Equal(OldCell, entity.ParentCellId);
|
|
Assert.Equal(beforeRotation, entity.Rotation);
|
|
}
|
|
|
|
private static void AssertRenderPoseWritten(
|
|
RuntimeAuthoritativePositionRoute? route)
|
|
{
|
|
WorldEntity entity = MakeEntity();
|
|
var wireRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 1.1f);
|
|
|
|
bool written = LiveEntityNetworkUpdateController
|
|
.TryApplyGenericRemoteRenderPose(
|
|
entity,
|
|
route,
|
|
WirePosition,
|
|
WireCell,
|
|
wireRotation);
|
|
|
|
Assert.True(written);
|
|
Assert.Equal(WirePosition, entity.Position);
|
|
Assert.Equal(WireCell, entity.ParentCellId);
|
|
Assert.Equal(wireRotation, entity.Rotation);
|
|
}
|
|
|
|
private static WorldEntity MakeEntity() => new()
|
|
{
|
|
Id = 1u,
|
|
SourceGfxObjOrSetupId = 1u,
|
|
Position = OldPosition,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
ParentCellId = OldCell,
|
|
};
|
|
|
|
private const uint Cell = 0x0101FFFFu;
|
|
|
|
private static RuntimeAuthoritativePositionRoute Classify(
|
|
bool hasContact,
|
|
float playerDistance,
|
|
uint committedCellId = OldCell,
|
|
PositionTimestampDisposition disposition =
|
|
PositionTimestampDisposition.Apply) =>
|
|
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(
|
|
new RuntimeAcceptedPositionRouteRequest(
|
|
new RuntimeAuthoritativePositionAuthority(
|
|
new RuntimeGenerationToken(7),
|
|
new RuntimeEntityKey(0x70000001u, 3),
|
|
PositionAuthorityVersion: 11UL,
|
|
AcceptedPositionSequence: 20,
|
|
PreviousTeleportSequence: 10,
|
|
AcceptedTeleportSequence: 10,
|
|
disposition),
|
|
RuntimePositionEntityKind.Remote,
|
|
RuntimeAcceptedPositionSource.PositionEvent,
|
|
new CreateObject.ServerPosition(
|
|
Cell, 10f, 20f, 30f, 1f, 0f, 0f, 0f),
|
|
PlacementFrame: 0u,
|
|
PositionPackVelocity: Vector3.Zero,
|
|
committedCellId,
|
|
hasContact,
|
|
playerDistance,
|
|
UsePositionFromServer: false,
|
|
HasAnimations: false,
|
|
default));
|
|
}
|