using AcDream.Core.World; using AcDream.App.Physics; using AcDream.App.World; namespace AcDream.App.Input; internal interface ILocalPlayerProjectionRuntime { WorldEntity? ResolveEntity(); int LiveCenterX { get; } int LiveCenterY { get; } void SyncShadow(WorldEntity entity, uint cellId); void Rebucket(uint serverGuid, uint landblockId); bool IsCurrentVisibleProjection(WorldEntity entity); void SuspendShadow(WorldEntity entity); } internal sealed class LiveLocalPlayerProjectionRuntime : ILocalPlayerProjectionRuntime { private readonly LiveEntityRuntime _liveEntities; private readonly ILocalPlayerIdentitySource _identity; private readonly LiveWorldOriginState _origin; private readonly LocalPlayerShadowSynchronizer _shadow; public LiveLocalPlayerProjectionRuntime( LiveEntityRuntime liveEntities, ILocalPlayerIdentitySource identity, LiveWorldOriginState origin, LocalPlayerShadowSynchronizer shadow) { _liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities)); _identity = identity ?? throw new ArgumentNullException(nameof(identity)); _origin = origin ?? throw new ArgumentNullException(nameof(origin)); _shadow = shadow ?? throw new ArgumentNullException(nameof(shadow)); } public WorldEntity? ResolveEntity() => _liveEntities.TryGetWorldEntity(_identity.ServerGuid, out var entity) ? entity : null; public int LiveCenterX => _origin.CenterX; public int LiveCenterY => _origin.CenterY; public void SyncShadow(WorldEntity entity, uint cellId) => _shadow.Sync(entity, cellId); public void Rebucket(uint serverGuid, uint landblockId) => _liveEntities.RebucketLiveEntity(serverGuid, landblockId); public bool IsCurrentVisibleProjection(WorldEntity entity) => _shadow.IsCurrentVisibleProjection(entity); public void SuspendShadow(WorldEntity entity) => _shadow.Suspend(entity); } /// /// Projects the canonical local physics body into world rendering and spatial /// buckets without advancing it. /// public sealed class LocalPlayerProjectionController { private readonly ILocalPlayerProjectionRuntime _runtime; internal LocalPlayerProjectionController(ILocalPlayerProjectionRuntime runtime) => _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); public void Project( PlayerMovementController controller, MovementResult movement, bool hidden) { WorldEntity? entity = _runtime.ResolveEntity(); if (entity is null) return; entity.SetPosition(movement.RenderPosition); entity.ParentCellId = movement.CellId; // Retail's root is a complete Frame. Project the authoritative body // quaternion directly so pitch/roll from a server correction or DAT // root frame cannot be flattened by the presentation layer. entity.Rotation = controller.BodyOrientation; uint currentLandblock; if (movement.CellId != 0 && (movement.CellId & 0xFFFFu) >= 0x0100u) { // Indoor positions belong to the cell's landblock. Dungeon frame // origins can be negative, so render XYZ is not a landblock key. currentLandblock = (movement.CellId & 0xFFFF0000u) | 0xFFFFu; } else { System.Numerics.Vector3 position = controller.Position; int landblockX = _runtime.LiveCenterX + (int)Math.Floor(position.X / 192f); int landblockY = _runtime.LiveCenterY + (int)Math.Floor(position.Y / 192f); currentLandblock = (uint)((landblockX << 24) | (landblockY << 16) | 0xFFFF); } // The teleport owner alone projects the destination while the local // controller deliberately retains its frozen source cell. if (controller.State == PlayerState.PortalSpace) return; // SetPositionInternal commits the complete root before changing cell // membership, then replaces collision rows only while the object still // owns a loaded cell. Rebucket is a synchronous callback boundary: it // can move this incarnation to pending or replace the GUID entirely. _runtime.Rebucket(entity.ServerGuid, currentLandblock); if (hidden || movement.CellId == 0 || !_runtime.IsCurrentVisibleProjection(entity)) { _runtime.SuspendShadow(entity); return; } _runtime.SyncShadow(entity, movement.CellId); } }