diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs index f8a47c86..cd9e5a59 100644 --- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs +++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs @@ -556,6 +556,18 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable Func? retirePriorProjection) { 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) { throw new InvalidOperationException( @@ -1508,6 +1520,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable if (!deferredKnown) return false; + if (isLocalPlayer + && disposition is not PositionTimestampDisposition.Rejected) + { + Physics.ObserveLocalWorldFrame( + update.Position.LandblockId, + timestamps.TeleportAdvanced); + } + if (disposition is PositionTimestampDisposition.Rejected && !timestampMutation) { @@ -1556,6 +1576,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable return known; } + if (isLocalPlayer + && disposition is not PositionTimestampDisposition.Rejected) + { + Physics.ObserveLocalWorldFrame( + update.Position.LandblockId, + timestamps.TeleportAdvanced); + } + bool acceptedPosition = disposition is not PositionTimestampDisposition.Rejected; if (disposition is PositionTimestampDisposition.Apply) diff --git a/src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs b/src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs index 9563881d..90ad5c61 100644 --- a/src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs +++ b/src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs @@ -346,7 +346,8 @@ internal sealed class RuntimeRemoteFirstEntryState lease.Route.SetPositionFlags, collisionSource, gameTime, - out RuntimeSetPositionCommand command); + out RuntimeSetPositionCommand command, + resolveWorldOffsetFromRuntimeFrame: true); if (moverStatus == RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable) { diff --git a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs index 4080bc11..b84b0d37 100644 --- a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs +++ b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs @@ -243,7 +243,16 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable motion, stopCompletely: () => _ = 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), setHeading: (heading, _) => body.Orientation = MoveToMath.SetHeading(body.Orientation, heading), @@ -268,7 +277,10 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable }; physicsHost = new EntityPhysicsHost( record.ServerGuid, - getPosition: () => body.CellPosition, + getPosition: () => new Position( + body.CellPosition.ObjCellId, + body.Position, + body.Orientation), getVelocity: () => body.Velocity, getRadius: () => preparedActivation.Radius, inContact: () => body.InContact, diff --git a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs index b5ee2bf8..77554eee 100644 --- a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs +++ b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs @@ -448,6 +448,7 @@ public sealed class RuntimePhysicsState : IDisposable private int _collisionMutationThreadId; private long _nextCollisionPreparationSequence; private ulong _collisionWorldAuthority = 1UL; + private uint _worldFrameCenterLandblockId; private readonly List> _collisionGenerationCommittedObservers = new(); private bool _disposed; @@ -524,6 +525,52 @@ public sealed class RuntimePhysicsState : IDisposable internal double PlacementSimulationTime(double fallback) => _gameClock?.SimulationTimeSeconds ?? fallback; + /// + /// 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. + /// + internal void ObserveLocalWorldFrame( + uint fullCellId, + bool teleportAdvanced) + { + EnsureNotDisposed(); + if (fullCellId == 0u) + return; + if (_worldFrameCenterLandblockId == 0u || teleportAdvanced) + { + _worldFrameCenterLandblockId = + (fullCellId & 0xFFFF0000u) | 0xFFFFu; + } + } + + /// + /// Converts a retail landblock-local network frame into the Runtime's + /// current world frame without consulting presentation or waiting for + /// the destination collision package. + /// + 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() { RuntimeSetPositionOwnershipSnapshot setPosition = @@ -1315,6 +1362,7 @@ public sealed class RuntimePhysicsState : IDisposable _collisionAdmissions.Clear(); SetPosition.ResetSession(); CollisionReports.ResetSession(); + _worldFrameCenterLandblockId = 0u; AdvanceCollisionWorldAuthority(); Volatile.Write(ref _collisionMutationThreadId, 0); } @@ -2007,6 +2055,7 @@ public sealed class RuntimePhysicsState : IDisposable _collisionPrefixMutations.Clear(); _collisionAdmissions.Clear(); _collisionGenerations.Clear(); + _worldFrameCenterLandblockId = 0u; CellCommitted = null; _collisionGenerationCommittedObservers.Clear(); _disposed = true; diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionMoverPreparation.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionMoverPreparation.cs index e2f97302..9c46f161 100644 --- a/src/AcDream.Runtime/Physics/RuntimeSetPositionMoverPreparation.cs +++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionMoverPreparation.cs @@ -57,7 +57,8 @@ internal readonly record struct RuntimeSetPositionMoverPreparation( uint ScatterAttempts = 0u, float ShadowWorldOffsetX = 0f, float ShadowWorldOffsetY = 0f, - RuntimePortalPlacementAuthority Portal = default); + RuntimePortalPlacementAuthority Portal = default, + bool ResolveWorldOffsetFromRuntimeFrame = false); /// /// Pure preparation port of the mover inputs consumed by retail diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs index 39378bda..0f6dcee0 100644 --- a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs +++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs @@ -1522,6 +1522,32 @@ internal sealed class RuntimeSetPositionState : IDisposable .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( operation.Record, authority.AcceptedPosition, @@ -1529,7 +1555,7 @@ internal sealed class RuntimeSetPositionState : IDisposable operation.Kind, operation.Portal, authority.VelocityAuthorityVersion, - preparation, + effectivePreparation, out command) || !IsStructurallyValid(command.Physics)) { @@ -1586,7 +1612,8 @@ internal sealed class RuntimeSetPositionState : IDisposable float scatterRadiusY = 0f, uint scatterAttempts = 0u, float shadowWorldOffsetX = 0f, - float shadowWorldOffsetY = 0f) + float shadowWorldOffsetY = 0f, + bool resolveWorldOffsetFromRuntimeFrame = false) { outcome = default; @@ -1606,7 +1633,8 @@ internal sealed class RuntimeSetPositionState : IDisposable scatterRadiusY, scatterAttempts, shadowWorldOffsetX, - shadowWorldOffsetY); + shadowWorldOffsetY, + resolveWorldOffsetFromRuntimeFrame); if (status != RuntimeSetPositionMoverPreparationStatus.Prepared) return status; @@ -1644,7 +1672,8 @@ internal sealed class RuntimeSetPositionState : IDisposable float scatterRadiusY = 0f, uint scatterAttempts = 0u, float shadowWorldOffsetX = 0f, - float shadowWorldOffsetY = 0f) + float shadowWorldOffsetY = 0f, + bool resolveWorldOffsetFromRuntimeFrame = false) { EnsureNotDisposed(); ArgumentNullException.ThrowIfNull(record); @@ -1683,7 +1712,8 @@ internal sealed class RuntimeSetPositionState : IDisposable scatterAttempts, shadowWorldOffsetX, shadowWorldOffsetY, - portal); + portal, + resolveWorldOffsetFromRuntimeFrame); return PrepareMover(token, preparation, out command); } diff --git a/src/AcDream.Runtime/Session/RuntimeFirstEntryDriveController.cs b/src/AcDream.Runtime/Session/RuntimeFirstEntryDriveController.cs index 43acaf53..921e15ea 100644 --- a/src/AcDream.Runtime/Session/RuntimeFirstEntryDriveController.cs +++ b/src/AcDream.Runtime/Session/RuntimeFirstEntryDriveController.cs @@ -250,7 +250,11 @@ internal sealed class RuntimeFirstEntryDriveController // Contention — nothing more this pump can do synchronously. return; } - if (!TryCompleteContinuationPlacement(key, pending.Record)) + if (!TryCompleteContinuationPlacement( + key, + pending.Record, + resolveWorldOffsetFromRuntimeFrame: + !pending.IsLocalPlayer)) return; // A continuation placement progressed — re-Advance so the // executor can consume the acknowledged completion and keep @@ -265,7 +269,8 @@ internal sealed class RuntimeFirstEntryDriveController /// private bool TryCompleteContinuationPlacement( RuntimeEntityKey key, - RuntimeEntityRecord record) + RuntimeEntityRecord record, + bool resolveWorldOffsetFromRuntimeFrame) { RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition; @@ -311,7 +316,9 @@ internal sealed class RuntimeFirstEntryDriveController route.SetPositionFlags, _collisionSource, _clock.SimulationTimeSeconds, - out RuntimeSetPositionOutcome outcome); + out RuntimeSetPositionOutcome outcome, + resolveWorldOffsetFromRuntimeFrame: + resolveWorldOffsetFromRuntimeFrame); if (status != RuntimeSetPositionMoverPreparationStatus.Prepared) { // RetrySetupUnavailable retries on a later pump; a rejected