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:
parent
01f4791e95
commit
670f307c84
7 changed files with 140 additions and 12 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue