fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival. Portalling worked before this change and works after it — this is not a bug fix, EXCEPT that it found and fixed one dead-code production bug. THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the accepted destination at Place time, but TryBeginPortalReveal already consumes that slot at Aim time — so the arm was 100% dead code and every real portal Place refused with host-token-unavailable. Found only because we refused to accept 7 skipped tests instead of chasing the count to zero. RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING: SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with flags 0x1012, followed by PlayerPositionUpdated. BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed here (ConstrainTo @0x0045418A) and velocity is zeroed (set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook runs AFTER placement (@0x004538AE). THE THREE-ROUND DEFECT CHAIN, HONESTLY: - Round 1 released the player at the pre-teleport position while the anim stream marched on — the contract wrongly assumed Place re-fires (process rule 1's third occurrence this campaign). - Round 2's fix inferred commit from a global PendingCount, which three non-committing paths also clear — making the SAME bug complete cleanly and silently. Strictly worse than round 1: round 1 at least tripped portal-complete-before-materialized. - Round 3 latches the commit where it actually happens (ReconcileAndAcknowledgePortal), keyed on reveal generation and teleport sequence, via TryConsumePortalCommit. Two of the three required regression tests landed and are sabotage-verified on both hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted / HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted). The third (force-arm-takes-the-slot) was judged unnecessary on review: with the inference gone, PendingCount is only a "don't ask yet" guard at both gates, so a force operation occupying or vacating the slot no longer changes an input the commit decision reads — the case collapses into what the landed test already discriminates. THE B2/P3 RESOLUTION: both round-2 reviews were right about different branches of the same synchronous call. RuntimePlacementProjectionSubscription .OnPlacement acknowledges the FIFO head only when TryApply returns true; a Place whose portal authority went stale (transit ended/superseded while parked) used to return false, wedging every later entity's placement receipt behind it forever. Both sinks (RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink) now acknowledge-and-ignore a stale-authority Place instead of refusing it. The regression test (RuntimePlacementPresentationSinkTests .PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had been asserting the old, wrong `false` behaviour; it now asserts and sabotage-verifies the fix. Also lands: AP-144 (register discipline — the portal movement-event send reuses the stricter UsePositionFromServer gate where retail's SendMovementEvent is the looser autonomy_level != 0 test, diverging only at level 1, currently unreachable), AP-145 + issue #318 (the local-player collision-shadow presentation write bypasses its own publisher's ShadowObjects write via a direct cache .Set(), self-healing only once dedup diverges — filed, not fixed, pending a composition test), AD-42 deleted (its last citation retired by the canonical portal arm), AD-2 updated (the wait-cue's trigger predicate now covers a second cause), and two documentation corrections: the enter_world misattribution (both call sites are in SmartBox::HandleCreateObject, only one in the player branch — portal arrival is TeleportPlayer, not enter_world) and the stale "local player never reaches this path" comment on the generic-remote-render-pose write. Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing weakened. STILL OWED: the connected two-client gate, with ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines actually appear in the capture — and explicitly NOT scored as covering issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects directly). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cd3129e9d6
commit
e0f96a55bf
24 changed files with 5261 additions and 243 deletions
|
|
@ -97,16 +97,26 @@ internal sealed class HeadlessRuntimePlacementProjectionSink
|
|||
if (projection.Kind is not RuntimePlacementProjectionKind.Place)
|
||||
return false;
|
||||
|
||||
return record.PositionAuthorityVersion
|
||||
== token.PositionAuthorityVersion
|
||||
&& record.SpatialAuthorityVersion
|
||||
== token.SpatialAuthorityVersion
|
||||
&& record.PlacementCommitVersion
|
||||
== token.PlacementCommitVersion
|
||||
&& record.FullCellId == token.ExactCellId
|
||||
&& _runtime.TransitOwner.IsCurrentPlacementAuthority(
|
||||
if (record.PositionAuthorityVersion != token.PositionAuthorityVersion
|
||||
|| record.SpatialAuthorityVersion != token.SpatialAuthorityVersion
|
||||
|| record.PlacementCommitVersion != token.PlacementCommitVersion
|
||||
|| record.FullCellId != token.ExactCellId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_runtime.TransitOwner.IsCurrentPlacementAuthority(
|
||||
token.Portal,
|
||||
token.ExactCellId);
|
||||
token.ExactCellId))
|
||||
{
|
||||
// B2 review fix (2026-08-05): acknowledge-and-ignore, the same
|
||||
// shape and reasoning as the graphical sink's identical fix
|
||||
// (RuntimePlacementPresentationSink.TryApply) — a stale portal
|
||||
// authority must not wedge the ordered FIFO for every entity.
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasValidPortalShape(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using AcDream.Headless.Credentials;
|
|||
using AcDream.Headless.Diagnostics;
|
||||
using AcDream.Headless.Policies;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
|
@ -143,6 +144,16 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
private RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
|
||||
private AcDream.Core.Net.WorldSession? _currentSession;
|
||||
private HeadlessSessionWorldProjection? _worldProjection;
|
||||
/// <summary>
|
||||
/// A1/A3 review fix (2026-08-05): retained so <see cref="Tick"/> can
|
||||
/// pump <see cref="RuntimeLiveEntitySessionController.PumpPortalCompletion"/>
|
||||
/// alongside <see cref="_worldProjection"/>'s own
|
||||
/// <c>PumpFirstEntry</c> — a parked portal placement must retry on the
|
||||
/// host's own per-tick cadence rather than the completion sequence
|
||||
/// running unconditionally the instant it is first attempted. Reassigned
|
||||
/// on every reconnect exactly like <see cref="_worldProjection"/>.
|
||||
/// </summary>
|
||||
private RuntimeLiveEntitySessionController? _entities;
|
||||
/// <summary>C4 route 4b-1 (N3): the exact route <see cref="CreateEventRoute"/>
|
||||
/// last constructed, so <see cref="Tick"/> can republish the canonical
|
||||
/// placement FIFO every tick — mirrors the graphical host's per-frame
|
||||
|
|
@ -332,6 +343,12 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
// collision-generation progress and freshly accepted Creates both
|
||||
// surface here, mirroring the graphical per-frame retry phase.
|
||||
_worldProjection?.PumpFirstEntry();
|
||||
// A1/A3 review fix (2026-08-05): retry a parked portal completion
|
||||
// (see RuntimeLiveEntitySessionController.PumpPortalCompletion) on
|
||||
// the SAME per-tick cadence, after first-entry so a DeferredCell
|
||||
// wake first-entry's own pump just resolved is picked up the same
|
||||
// tick.
|
||||
_entities?.PumpPortalCompletion();
|
||||
// C4 route 4b-1 (N3): republish the canonical placement FIFO LAST,
|
||||
// same order as the graphical host's retry-lease callback (drives
|
||||
// first, retry last) — a declined Place left at the FIFO head by
|
||||
|
|
@ -621,6 +638,13 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
Radius: 0.48f,
|
||||
Height: 1.835f,
|
||||
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
|
||||
// D-T8 (temporary probe): labels every subsequent
|
||||
// PhysicsDiagnostics.LogLocalTeleportArrival line from THIS
|
||||
// process as headless — Runtime itself has no host-kind concept
|
||||
// (Slice K keeps it presentation-agnostic), so this is a
|
||||
// diagnostics-only label set once at composition time, not a
|
||||
// Runtime dependency.
|
||||
PhysicsDiagnostics.LocalTeleportHostKind = "headless";
|
||||
// C4 route 2: one drive controller per host, mirroring
|
||||
// _firstEntryDrive exactly — same persistent Runtime lifetime,
|
||||
// collision source, and clock.
|
||||
|
|
@ -633,7 +657,20 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
() => Runtime.PlayerIdentity.ServerGuid,
|
||||
() => Runtime.MovementOwner.Controller,
|
||||
() => Runtime.CharacterOwner.UsePositionFromServer,
|
||||
() => _currentSession);
|
||||
() => _currentSession,
|
||||
// C4 route 3: the portal arm's PlayerTeleported port needs
|
||||
// the J5.4 autorun latch owner, one level above the raw
|
||||
// controller.
|
||||
() => Runtime.MovementOwner,
|
||||
// A2/D-T2.4 review fix (2026-08-05): same wiring as the
|
||||
// graphical composition (SessionPlayerComposition.cs) — the
|
||||
// SAME idempotent query TryCompletePortal/PrepareDestination
|
||||
// themselves use.
|
||||
isPortalAuthorityCurrent: portal => Runtime.TransitOwner
|
||||
.CanPlacePortalDestination(
|
||||
portal.RevealGeneration,
|
||||
portal.TeleportSequence,
|
||||
portal.Projection.DestinationCell));
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
Runtime,
|
||||
content,
|
||||
|
|
@ -651,6 +688,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
Runtime.Generation.Value),
|
||||
worldProjection,
|
||||
_acceptedPositionDrive);
|
||||
_entities = entities;
|
||||
var route = new LiveSessionEventRouter(
|
||||
session,
|
||||
entities.CreateSink(),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using AcDream.Runtime.Entities;
|
|||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Headless.Hosting;
|
||||
|
||||
|
|
@ -585,9 +586,6 @@ internal sealed class HeadlessCollisionNeighborhood
|
|||
internal sealed class HeadlessSessionWorldProjection
|
||||
: IRuntimeDirectWorldProjection
|
||||
{
|
||||
private const float DefaultRadius = 0.48f;
|
||||
private const float DefaultHeight = 1.835f;
|
||||
|
||||
private readonly GameRuntime _runtime;
|
||||
private readonly IHeadlessCollisionNeighborhood _collision;
|
||||
private readonly RuntimeFirstEntryDriveController? _firstEntry;
|
||||
|
|
@ -749,90 +747,228 @@ internal sealed class HeadlessSessionWorldProjection
|
|||
controller.State = PlayerState.PortalSpace;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A1/A3 review fix (2026-08-05): true while a <c>DeferredCell</c> park
|
||||
/// from a PRIOR call to this method is outstanding for the local
|
||||
/// player's one possible pending drive operation. Mirrors
|
||||
/// <c>LocalPlayerTeleportController._awaitingDeferredWake</c> on the
|
||||
/// graphical side — avoids re-attempting
|
||||
/// <c>TryExecuteAcceptedPortalArrival</c> while parked (Runtime's own
|
||||
/// Begin would just refuse a second overlapping attempt with
|
||||
/// <c>Contention</c>) by polling
|
||||
/// <see cref="RuntimeAcceptedPositionDriveController.PendingCount"/>
|
||||
/// instead.
|
||||
/// </summary>
|
||||
private bool _awaitingPortalWake;
|
||||
|
||||
/// <summary>
|
||||
/// B3 review fix (2026-08-05): keys <see cref="_awaitingPortalWake"/> to
|
||||
/// the exact reveal it was armed for. The graphical twin
|
||||
/// (<c>_awaitingDeferredWake</c>) is naturally reset per teleport via
|
||||
/// <c>ResetTransit</c>; this class is constructed per SESSION, not per
|
||||
/// teleport, so without this a stale latch from reveal N could silently
|
||||
/// steal reveal N+1's <c>PrepareDestination</c> call into polling a park
|
||||
/// that belongs to a different, already-abandoned reveal — skipping the
|
||||
/// new reveal's placement attempt entirely.
|
||||
/// </summary>
|
||||
private long _awaitingPortalWakeGeneration;
|
||||
private ushort _awaitingPortalWakeSequence;
|
||||
|
||||
/// <summary>
|
||||
/// N3 review fix (2026-08-05): bounds how many consecutive
|
||||
/// <c>NotApplicable</c> attempts this host tolerates before treating the
|
||||
/// condition as unrecoverable. <c>NotApplicable</c> covers hydration-race
|
||||
/// transients (no canonical body yet, an active initial-Create residence
|
||||
/// still owning the record) as well as a genuinely stale reveal — unlike
|
||||
/// <c>Rejected</c>, it is not established to be permanent, and this host
|
||||
/// must survive K4's 30-session / two-hour endurance profile without a
|
||||
/// transient becoming fatal.
|
||||
/// </summary>
|
||||
private int _notApplicableRetryCount;
|
||||
private const int NotApplicableRetryBudget = 50;
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 3 (D-T6): the portal-arrival placement runs through the
|
||||
/// SAME canonical Runtime portal arm the graphical host drives
|
||||
/// (<see cref="RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival"/>),
|
||||
/// retiring the duplicate Resolve/ResolvePlacement/SetPosition authority
|
||||
/// this method used to own directly (D2;
|
||||
/// docs/research/2026-08-04-c4-route-3-contract.md D-T6).
|
||||
///
|
||||
/// <para>
|
||||
/// A1/A3 review fix (2026-08-05): the first pass discarded the arm's
|
||||
/// returned status entirely (<c>_ = ...</c>) and always reported
|
||||
/// success, so <c>RuntimeLiveEntitySessionController.TryCompletePortal</c>
|
||||
/// acknowledged a materialization that never happened on ANY refusal
|
||||
/// (architecture review A3). This method now:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>throws if no drive controller was wired — a
|
||||
/// composition regression must not silently disable placement, never
|
||||
/// pretend success (A3's second finding);</description></item>
|
||||
/// <item><description>does not even ATTEMPT the placement until
|
||||
/// <see cref="_collision"/> reports the destination resident — this
|
||||
/// host's narrow collision window makes a premature attempt a
|
||||
/// guaranteed <c>DeferredCell</c>, and <see cref="_collision"/>'s own
|
||||
/// readiness IS the signal <see cref="IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition"/>'s
|
||||
/// doc names as the precondition for a park to ever resolve;</description></item>
|
||||
/// <item><description>reports <c>IsCollisionReady: false</c> — never
|
||||
/// success — for every non-<c>Committed</c> outcome, so the caller's
|
||||
/// retry loop (<c>RuntimeLiveEntitySessionController.PumpPortalCompletion</c>,
|
||||
/// A1's headless-side fix) keeps calling this method instead of the
|
||||
/// completion sequence running against an unplaced body; a genuine
|
||||
/// <c>DeferredCell</c> is therefore never an error, only a wait — and
|
||||
/// throws only for the two statuses that mean something is actually
|
||||
/// wrong (<c>Rejected</c>/<c>NotApplicable</c> — the reveal itself is
|
||||
/// stale, or the local player has no canonical body, neither of which
|
||||
/// a headless bot can recover from by waiting).</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public RuntimeDestinationReadiness PrepareDestination(
|
||||
long revealGeneration,
|
||||
RuntimeTeleportDestination destination)
|
||||
RuntimeTeleportDestination destination,
|
||||
RuntimeWorldHostProjectionToken portal)
|
||||
{
|
||||
_collision.CenterOn(destination.CellId);
|
||||
if (_runtime.EntityObjects.Entities.TryGetActive(
|
||||
destination.EntityGuid,
|
||||
out RuntimeEntityRecord record))
|
||||
if (_acceptedPositionDrive is null)
|
||||
{
|
||||
ResynchronizeLocalPlayerForPortalArrival(record);
|
||||
throw new InvalidOperationException(
|
||||
"Headless portal placement requires a wired "
|
||||
+ "RuntimeAcceptedPositionDriveController - a composition "
|
||||
+ "regression must not silently disable placement (A3).");
|
||||
}
|
||||
if (_runtime.MovementOwner.Controller is { } controller)
|
||||
|
||||
// B3: a latch armed for a DIFFERENT reveal must not be consulted
|
||||
// for this one — fall through to a fresh attempt below instead.
|
||||
if (_awaitingPortalWake
|
||||
&& (_awaitingPortalWakeGeneration != revealGeneration
|
||||
|| _awaitingPortalWakeSequence != destination.TeleportSequence))
|
||||
{
|
||||
_awaitingPortalWake = false;
|
||||
}
|
||||
|
||||
bool committed;
|
||||
if (_awaitingPortalWake)
|
||||
{
|
||||
if (_acceptedPositionDrive.PendingCount != 0)
|
||||
{
|
||||
committed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// B1 review fix (2026-08-05): "not pending" is not
|
||||
// "committed" — the drive's own doc names a merge-time
|
||||
// Forget (an ordinary ACE broadcast arriving mid-park) as
|
||||
// the EXPECTED way a park resolves without committing.
|
||||
// TryConsumePortalCommit is the drive's OWN record of
|
||||
// whether ITS commit actually happened for this exact
|
||||
// reveal/sequence, latched only inside a real
|
||||
// ReconcileAndAcknowledgePortal call — never inferred.
|
||||
_awaitingPortalWake = false;
|
||||
committed = _acceptedPositionDrive.TryConsumePortalCommit(
|
||||
revealGeneration, destination.TeleportSequence);
|
||||
// A "no" here falls through to committed=false below; the
|
||||
// NEXT PrepareDestination call re-attempts fresh since
|
||||
// _awaitingPortalWake is now false and nothing is pending.
|
||||
}
|
||||
}
|
||||
else if (!_collision.IsReady(destination.CellId))
|
||||
{
|
||||
committed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var authority = new RuntimePortalPlacementAuthority(
|
||||
Present: true,
|
||||
RevealGeneration: revealGeneration,
|
||||
TeleportSequence: destination.TeleportSequence,
|
||||
Projection: portal);
|
||||
RuntimeAcceptedPositionExecutionStatus status =
|
||||
_acceptedPositionDrive.TryExecuteAcceptedPortalArrival(
|
||||
destination,
|
||||
authority);
|
||||
switch (status)
|
||||
{
|
||||
case RuntimeAcceptedPositionExecutionStatus.Committed:
|
||||
committed = true;
|
||||
_notApplicableRetryCount = 0;
|
||||
break;
|
||||
case RuntimeAcceptedPositionExecutionStatus.DeferredCell:
|
||||
_awaitingPortalWake = true;
|
||||
_awaitingPortalWakeGeneration = revealGeneration;
|
||||
_awaitingPortalWakeSequence = destination.TeleportSequence;
|
||||
committed = false;
|
||||
break;
|
||||
case RuntimeAcceptedPositionExecutionStatus.Contention:
|
||||
// Transient - some other operation still owns the
|
||||
// entity's placement token. Retried next pump; never a
|
||||
// hard error, matching the graphical arm's D-T5
|
||||
// refusal shape.
|
||||
committed = false;
|
||||
break;
|
||||
case RuntimeAcceptedPositionExecutionStatus.NotApplicable:
|
||||
// N3 review fix (2026-08-05): NotApplicable covers
|
||||
// hydration-race transients (record.PhysicsBody is
|
||||
// null, or an active initial-Create residence still
|
||||
// owns the record — RuntimeAcceptedPositionDriveController's
|
||||
// own guard) as well as a genuinely stale reveal; unlike
|
||||
// Rejected it is not established to be permanent.
|
||||
// Bounded, loud retry rather than an immediate throw —
|
||||
// this host must survive K4's 30-session/two-hour
|
||||
// endurance profile without a transient becoming fatal.
|
||||
_notApplicableRetryCount++;
|
||||
PhysicsDiagnostics.LogTeleport(
|
||||
"REFUSED",
|
||||
destination.CellId,
|
||||
$"cause=NotApplicable attempt={_notApplicableRetryCount}");
|
||||
if (_notApplicableRetryCount > NotApplicableRetryBudget)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Headless portal placement stayed NotApplicable "
|
||||
+ $"for {_notApplicableRetryCount} consecutive "
|
||||
+ "attempts (no canonical body, or an active "
|
||||
+ "initial-Create residence still owns the "
|
||||
+ "record) - exceeded the bounded retry budget.");
|
||||
}
|
||||
committed = false;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Headless portal placement refused with "
|
||||
+ $"status={status} even though the destination's "
|
||||
+ "collision neighborhood reported ready - the "
|
||||
+ "reveal itself is stale, not recoverable by "
|
||||
+ "waiting (contract §4 item 5 forbids "
|
||||
+ "acknowledging a materialization that did not "
|
||||
+ "happen).");
|
||||
}
|
||||
}
|
||||
|
||||
if (committed && _runtime.MovementOwner.Controller is { } controller)
|
||||
controller.State = PlayerState.InWorld;
|
||||
|
||||
bool ready = _collision.IsReady(destination.CellId);
|
||||
bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u;
|
||||
return new RuntimeDestinationReadiness(
|
||||
revealGeneration,
|
||||
destination.CellId,
|
||||
indoor,
|
||||
IsUnhydratable: !ready,
|
||||
// N2 review fix (2026-08-05): hardcoded false, NOT DERIVED.
|
||||
// AD-2's "loud unhydratable-placement path" (a claim beyond
|
||||
// NumCells) is a graphical-only concept today —
|
||||
// WorldRevealReadinessBarrier's render/composite-texture domains
|
||||
// have no headless analogue, so there is no local predicate this
|
||||
// no-window host could evaluate. A genuinely unhydratable
|
||||
// destination therefore reports IsCollisionReady=false forever
|
||||
// (via the bounded DeferredCell retry above) rather than taking
|
||||
// AD-2's loud path — headless does not model unhydratable
|
||||
// claims. If headless ever gains its own resident-cell-set
|
||||
// concept, derive the real predicate here instead of leaving
|
||||
// this hardcoded.
|
||||
IsUnhydratable: false,
|
||||
RequiredRenderRadius: indoor ? 0 : 1,
|
||||
IsRenderNeighborhoodReady: true,
|
||||
AreCompositeTexturesReady: true,
|
||||
IsCollisionReady: ready);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO-C4 (route 3): portal-arrival re-synchronization only. The
|
||||
/// route-1/8 initial-entry hand-copy (controller construction + first
|
||||
/// resolve/placement) was deleted at C3c — the first-entry conductor's
|
||||
/// publication chain owns it — but the portal route is unflipped, so its
|
||||
/// arrival re-resolve keeps today's exact behavior against the
|
||||
/// already-published controller until C4 routes it through
|
||||
/// RuntimePortalPlacementAuthority.
|
||||
/// </summary>
|
||||
private void ResynchronizeLocalPlayerForPortalArrival(
|
||||
RuntimeEntityRecord record)
|
||||
{
|
||||
if (record.ServerGuid
|
||||
!= _runtime.PlayerIdentity.ServerGuid
|
||||
|| record.Snapshot.Position is not { } position
|
||||
|| _runtime.MovementOwner.Controller is not { } controller)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_collision.CenterOn(position.LandblockId);
|
||||
Vector3 wirePosition = new(
|
||||
position.PositionX,
|
||||
position.PositionY,
|
||||
position.PositionZ);
|
||||
Quaternion orientation = new(
|
||||
position.RotationX,
|
||||
position.RotationY,
|
||||
position.RotationZ,
|
||||
position.RotationW);
|
||||
|
||||
ResolveResult resolved =
|
||||
_runtime.EntityObjects.Physics.Engine.Resolve(
|
||||
wirePosition,
|
||||
position.LandblockId,
|
||||
Vector3.Zero,
|
||||
100f);
|
||||
ResolveResult placement =
|
||||
_runtime.EntityObjects.Physics.Engine.ResolvePlacement(
|
||||
resolved.Position,
|
||||
resolved.CellId,
|
||||
DefaultRadius,
|
||||
DefaultHeight,
|
||||
controller.StepUpHeight,
|
||||
controller.StepDownHeight,
|
||||
ObjectInfoState.IsPlayer
|
||||
| ObjectInfoState.EdgeSlide,
|
||||
record.LocalEntityId ?? 0u);
|
||||
if (placement.Ok)
|
||||
resolved = placement;
|
||||
|
||||
controller.LocalEntityId = record.LocalEntityId ?? 0u;
|
||||
controller.SetPosition(
|
||||
resolved.Position,
|
||||
resolved.CellId,
|
||||
wirePosition);
|
||||
controller.SetBodyOrientation(orientation);
|
||||
IsCollisionReady: committed);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue