using AcDream.Core.World; namespace AcDream.App.Input; /// /// Projects the canonical local physics body into world rendering and spatial /// buckets without advancing it. /// public sealed class LocalPlayerProjectionController { private readonly Func _resolveEntity; private readonly Func _liveCenterX; private readonly Func _liveCenterY; private readonly Action _syncShadow; private readonly Action _rebucket; private readonly Func _isCurrentVisibleProjection; private readonly Action _suspendShadow; public LocalPlayerProjectionController( Func resolveEntity, Func liveCenterX, Func liveCenterY, Action syncShadow, Action rebucket, Func isCurrentVisibleProjection, Action suspendShadow) { _resolveEntity = resolveEntity ?? throw new ArgumentNullException(nameof(resolveEntity)); _liveCenterX = liveCenterX ?? throw new ArgumentNullException(nameof(liveCenterX)); _liveCenterY = liveCenterY ?? throw new ArgumentNullException(nameof(liveCenterY)); _syncShadow = syncShadow ?? throw new ArgumentNullException(nameof(syncShadow)); _rebucket = rebucket ?? throw new ArgumentNullException(nameof(rebucket)); _isCurrentVisibleProjection = isCurrentVisibleProjection ?? throw new ArgumentNullException(nameof(isCurrentVisibleProjection)); _suspendShadow = suspendShadow ?? throw new ArgumentNullException(nameof(suspendShadow)); } public void Project( PlayerMovementController controller, MovementResult movement, bool hidden) { WorldEntity? entity = _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 = _liveCenterX() + (int)Math.Floor(position.X / 192f); int landblockY = _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. _rebucket(entity.ServerGuid, currentLandblock); if (hidden || movement.CellId == 0 || !_isCurrentVisibleProjection(entity)) { _suspendShadow(entity); return; } _syncShadow(entity, movement.CellId); } }