fix(physics): keep remote placement and targeting in one world frame

CreateObject positions are landblock-local, but Runtime first-entry previously submitted remotes with a zero world offset. Runtime now owns the accepted local-player world-frame center and converts remote placements before SetPosition. The local physics host also publishes body.Position rather than CellPosition's landblock-local origin, so TargetManager no longer directs monsters toward a phantom player position. User gate: monster/static placement, chase, and attacks accepted outside Tusker Barracks.
This commit is contained in:
Erik 2026-08-03 08:59:31 +02:00
parent 01f4791e95
commit 670f307c84
7 changed files with 140 additions and 12 deletions

View file

@ -556,6 +556,18 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection) Func<RuntimeEntityRecord, Exception?>? retirePriorProjection)
{ {
EnsureNotDisposed(); EnsureNotDisposed();
if (beginInitialResidence
&& isLocalPlayer
&& (incoming.Physics?.Position ?? incoming.Position)
is { LandblockId: not 0u } initialPlayerPosition)
{
// The accepted local Create establishes the shared world frame
// before any remote first-entry conductor converts its authored
// landblock-local coordinates.
Physics.ObserveLocalWorldFrame(
initialPlayerPosition.LandblockId,
teleportAdvanced: false);
}
if (_sessionClearInProgress) if (_sessionClearInProgress)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
@ -1508,6 +1520,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
if (!deferredKnown) if (!deferredKnown)
return false; return false;
if (isLocalPlayer
&& disposition is not PositionTimestampDisposition.Rejected)
{
Physics.ObserveLocalWorldFrame(
update.Position.LandblockId,
timestamps.TeleportAdvanced);
}
if (disposition is PositionTimestampDisposition.Rejected if (disposition is PositionTimestampDisposition.Rejected
&& !timestampMutation) && !timestampMutation)
{ {
@ -1556,6 +1576,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
return known; return known;
} }
if (isLocalPlayer
&& disposition is not PositionTimestampDisposition.Rejected)
{
Physics.ObserveLocalWorldFrame(
update.Position.LandblockId,
timestamps.TeleportAdvanced);
}
bool acceptedPosition = bool acceptedPosition =
disposition is not PositionTimestampDisposition.Rejected; disposition is not PositionTimestampDisposition.Rejected;
if (disposition is PositionTimestampDisposition.Apply) if (disposition is PositionTimestampDisposition.Apply)

View file

@ -346,7 +346,8 @@ internal sealed class RuntimeRemoteFirstEntryState
lease.Route.SetPositionFlags, lease.Route.SetPositionFlags,
collisionSource, collisionSource,
gameTime, gameTime,
out RuntimeSetPositionCommand command); out RuntimeSetPositionCommand command,
resolveWorldOffsetFromRuntimeFrame: true);
if (moverStatus if (moverStatus
== RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable) == RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable)
{ {

View file

@ -243,7 +243,16 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
motion, motion,
stopCompletely: () => stopCompletely: () =>
_ = controller.StopCompletelyAtPhysicsObjectBoundary(), _ = controller.StopCompletelyAtPhysicsObjectBoundary(),
getPosition: () => body.CellPosition, // App/Runtime movement managers use one normalized world
// coordinate frame. PhysicsBody.CellPosition deliberately
// retains retail's landblock-local origin for cell transit;
// publishing it here made every remote TargetManager chase a
// different point whenever the player and world origin were
// not the same landblock.
getPosition: () => new Position(
body.CellPosition.ObjCellId,
body.Position,
body.Orientation),
getHeading: () => MoveToMath.GetHeading(body.Orientation), getHeading: () => MoveToMath.GetHeading(body.Orientation),
setHeading: (heading, _) => body.Orientation = setHeading: (heading, _) => body.Orientation =
MoveToMath.SetHeading(body.Orientation, heading), MoveToMath.SetHeading(body.Orientation, heading),
@ -268,7 +277,10 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
}; };
physicsHost = new EntityPhysicsHost( physicsHost = new EntityPhysicsHost(
record.ServerGuid, record.ServerGuid,
getPosition: () => body.CellPosition, getPosition: () => new Position(
body.CellPosition.ObjCellId,
body.Position,
body.Orientation),
getVelocity: () => body.Velocity, getVelocity: () => body.Velocity,
getRadius: () => preparedActivation.Radius, getRadius: () => preparedActivation.Radius,
inContact: () => body.InContact, inContact: () => body.InContact,

View file

@ -448,6 +448,7 @@ public sealed class RuntimePhysicsState : IDisposable
private int _collisionMutationThreadId; private int _collisionMutationThreadId;
private long _nextCollisionPreparationSequence; private long _nextCollisionPreparationSequence;
private ulong _collisionWorldAuthority = 1UL; private ulong _collisionWorldAuthority = 1UL;
private uint _worldFrameCenterLandblockId;
private readonly List<Action<RuntimeCollisionGenerationCommitted>> private readonly List<Action<RuntimeCollisionGenerationCommitted>>
_collisionGenerationCommittedObservers = new(); _collisionGenerationCommittedObservers = new();
private bool _disposed; private bool _disposed;
@ -524,6 +525,52 @@ public sealed class RuntimePhysicsState : IDisposable
internal double PlacementSimulationTime(double fallback) => internal double PlacementSimulationTime(double fallback) =>
_gameClock?.SimulationTimeSeconds ?? fallback; _gameClock?.SimulationTimeSeconds ?? fallback;
/// <summary>
/// Establishes the landblock that maps to world-frame XY (0,0). The
/// local-player Create initializes it before remote first-entry work;
/// only an accepted teleport moves it afterward. Ordinary walking across
/// a landblock boundary does not rebase the streamed world.
/// </summary>
internal void ObserveLocalWorldFrame(
uint fullCellId,
bool teleportAdvanced)
{
EnsureNotDisposed();
if (fullCellId == 0u)
return;
if (_worldFrameCenterLandblockId == 0u || teleportAdvanced)
{
_worldFrameCenterLandblockId =
(fullCellId & 0xFFFF0000u) | 0xFFFFu;
}
}
/// <summary>
/// Converts a retail landblock-local network frame into the Runtime's
/// current world frame without consulting presentation or waiting for
/// the destination collision package.
/// </summary>
internal bool TryGetWorldFrameOffset(
uint fullCellId,
out float worldOffsetX,
out float worldOffsetY)
{
if (_worldFrameCenterLandblockId == 0u || fullCellId == 0u)
{
worldOffsetX = 0f;
worldOffsetY = 0f;
return false;
}
int centerX = (int)((_worldFrameCenterLandblockId >> 24) & 0xFFu);
int centerY = (int)((_worldFrameCenterLandblockId >> 16) & 0xFFu);
int landblockX = (int)((fullCellId >> 24) & 0xFFu);
int landblockY = (int)((fullCellId >> 16) & 0xFFu);
worldOffsetX = (landblockX - centerX) * 192f;
worldOffsetY = (landblockY - centerY) * 192f;
return true;
}
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() public RuntimePhysicsOwnershipSnapshot CaptureOwnership()
{ {
RuntimeSetPositionOwnershipSnapshot setPosition = RuntimeSetPositionOwnershipSnapshot setPosition =
@ -1315,6 +1362,7 @@ public sealed class RuntimePhysicsState : IDisposable
_collisionAdmissions.Clear(); _collisionAdmissions.Clear();
SetPosition.ResetSession(); SetPosition.ResetSession();
CollisionReports.ResetSession(); CollisionReports.ResetSession();
_worldFrameCenterLandblockId = 0u;
AdvanceCollisionWorldAuthority(); AdvanceCollisionWorldAuthority();
Volatile.Write(ref _collisionMutationThreadId, 0); Volatile.Write(ref _collisionMutationThreadId, 0);
} }
@ -2007,6 +2055,7 @@ public sealed class RuntimePhysicsState : IDisposable
_collisionPrefixMutations.Clear(); _collisionPrefixMutations.Clear();
_collisionAdmissions.Clear(); _collisionAdmissions.Clear();
_collisionGenerations.Clear(); _collisionGenerations.Clear();
_worldFrameCenterLandblockId = 0u;
CellCommitted = null; CellCommitted = null;
_collisionGenerationCommittedObservers.Clear(); _collisionGenerationCommittedObservers.Clear();
_disposed = true; _disposed = true;

View file

@ -57,7 +57,8 @@ internal readonly record struct RuntimeSetPositionMoverPreparation(
uint ScatterAttempts = 0u, uint ScatterAttempts = 0u,
float ShadowWorldOffsetX = 0f, float ShadowWorldOffsetX = 0f,
float ShadowWorldOffsetY = 0f, float ShadowWorldOffsetY = 0f,
RuntimePortalPlacementAuthority Portal = default); RuntimePortalPlacementAuthority Portal = default,
bool ResolveWorldOffsetFromRuntimeFrame = false);
/// <summary> /// <summary>
/// Pure preparation port of the mover inputs consumed by retail /// Pure preparation port of the mover inputs consumed by retail

View file

@ -1522,6 +1522,32 @@ internal sealed class RuntimeSetPositionState : IDisposable
.RetrySetupUnavailable; .RetrySetupUnavailable;
} }
RuntimeSetPositionMoverPreparation effectivePreparation = preparation;
if (preparation.ResolveWorldOffsetFromRuntimeFrame)
{
// CreateObject/Position origins are local to their authored
// landblock. Resolve them through Runtime's accepted world frame,
// which is initialized by the local-player Create and advanced
// only by authoritative teleport transitions. This is available
// before streaming publishes the target collision generation, so
// remote admission cannot starve world loading while still using
// the exact same coordinate frame that generation will publish.
if (!_physics.TryGetWorldFrameOffset(
authority.AcceptedPosition.LandblockId,
out float worldOffsetX,
out float worldOffsetY))
{
return RuntimeSetPositionMoverPreparationStatus
.RetrySetupUnavailable;
}
effectivePreparation = preparation with
{
ShadowWorldOffsetX = worldOffsetX,
ShadowWorldOffsetY = worldOffsetY,
};
}
if (!RuntimeSetPositionMoverPreparer.TryBuild( if (!RuntimeSetPositionMoverPreparer.TryBuild(
operation.Record, operation.Record,
authority.AcceptedPosition, authority.AcceptedPosition,
@ -1529,7 +1555,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
operation.Kind, operation.Kind,
operation.Portal, operation.Portal,
authority.VelocityAuthorityVersion, authority.VelocityAuthorityVersion,
preparation, effectivePreparation,
out command) out command)
|| !IsStructurallyValid(command.Physics)) || !IsStructurallyValid(command.Physics))
{ {
@ -1586,7 +1612,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
float scatterRadiusY = 0f, float scatterRadiusY = 0f,
uint scatterAttempts = 0u, uint scatterAttempts = 0u,
float shadowWorldOffsetX = 0f, float shadowWorldOffsetX = 0f,
float shadowWorldOffsetY = 0f) float shadowWorldOffsetY = 0f,
bool resolveWorldOffsetFromRuntimeFrame = false)
{ {
outcome = default; outcome = default;
@ -1606,7 +1633,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
scatterRadiusY, scatterRadiusY,
scatterAttempts, scatterAttempts,
shadowWorldOffsetX, shadowWorldOffsetX,
shadowWorldOffsetY); shadowWorldOffsetY,
resolveWorldOffsetFromRuntimeFrame);
if (status != RuntimeSetPositionMoverPreparationStatus.Prepared) if (status != RuntimeSetPositionMoverPreparationStatus.Prepared)
return status; return status;
@ -1644,7 +1672,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
float scatterRadiusY = 0f, float scatterRadiusY = 0f,
uint scatterAttempts = 0u, uint scatterAttempts = 0u,
float shadowWorldOffsetX = 0f, float shadowWorldOffsetX = 0f,
float shadowWorldOffsetY = 0f) float shadowWorldOffsetY = 0f,
bool resolveWorldOffsetFromRuntimeFrame = false)
{ {
EnsureNotDisposed(); EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record); ArgumentNullException.ThrowIfNull(record);
@ -1683,7 +1712,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
scatterAttempts, scatterAttempts,
shadowWorldOffsetX, shadowWorldOffsetX,
shadowWorldOffsetY, shadowWorldOffsetY,
portal); portal,
resolveWorldOffsetFromRuntimeFrame);
return PrepareMover(token, preparation, out command); return PrepareMover(token, preparation, out command);
} }

View file

@ -250,7 +250,11 @@ internal sealed class RuntimeFirstEntryDriveController
// Contention — nothing more this pump can do synchronously. // Contention — nothing more this pump can do synchronously.
return; return;
} }
if (!TryCompleteContinuationPlacement(key, pending.Record)) if (!TryCompleteContinuationPlacement(
key,
pending.Record,
resolveWorldOffsetFromRuntimeFrame:
!pending.IsLocalPlayer))
return; return;
// A continuation placement progressed — re-Advance so the // A continuation placement progressed — re-Advance so the
// executor can consume the acknowledged completion and keep // executor can consume the acknowledged completion and keep
@ -265,7 +269,8 @@ internal sealed class RuntimeFirstEntryDriveController
/// </summary> /// </summary>
private bool TryCompleteContinuationPlacement( private bool TryCompleteContinuationPlacement(
RuntimeEntityKey key, RuntimeEntityKey key,
RuntimeEntityRecord record) RuntimeEntityRecord record,
bool resolveWorldOffsetFromRuntimeFrame)
{ {
RuntimeSetPositionState setPosition = RuntimeSetPositionState setPosition =
_entityObjects.Physics.SetPosition; _entityObjects.Physics.SetPosition;
@ -311,7 +316,9 @@ internal sealed class RuntimeFirstEntryDriveController
route.SetPositionFlags, route.SetPositionFlags,
_collisionSource, _collisionSource,
_clock.SimulationTimeSeconds, _clock.SimulationTimeSeconds,
out RuntimeSetPositionOutcome outcome); out RuntimeSetPositionOutcome outcome,
resolveWorldOffsetFromRuntimeFrame:
resolveWorldOffsetFromRuntimeFrame);
if (status != RuntimeSetPositionMoverPreparationStatus.Prepared) if (status != RuntimeSetPositionMoverPreparationStatus.Prepared)
{ {
// RetrySetupUnavailable retries on a later pump; a rejected // RetrySetupUnavailable retries on a later pump; a rejected