using System.Numerics; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; namespace AcDream.App.Input; /// /// Serializes input-originated output from the object-phase result and the /// periodic position report from post-inbound controller state. /// public sealed class LocalPlayerOutboundController { private readonly IMovementTruthDiagnosticSink _diagnostic; internal LocalPlayerOutboundController(IMovementTruthDiagnosticSink diagnostic) { _diagnostic = diagnostic ?? throw new ArgumentNullException(nameof(diagnostic)); } public LocalPlayerOutboundController( Action diagnostic) { _diagnostic = new DelegateMovementTruthDiagnosticSink(diagnostic); } /// /// Sends input-originated movement and jump packets before inbound /// dispatch. Retail emits these from input/object processing rather than /// from CommandInterpreter::UseTime. /// public void SendPreNetworkActions( WorldSession? session, PlayerMovementController controller, MovementResult movement, bool hidden) { if (session is null || hidden) return; if (!controller.TryGetOutboundPosition(out Position outboundPosition)) { return; } uint wireCellId = outboundPosition.ObjCellId; Vector3 wirePosition = outboundPosition.Frame.Origin; Quaternion wireRotation = outboundPosition.Frame.Orientation; if (movement.ShouldSendMovementEvent) TrySendMovement(session, controller, movement); if (movement.JumpExtent.HasValue && movement.JumpVelocity.HasValue) { uint sequence = session.NextGameActionSequence(); byte[] body = JumpAction.Build( gameActionSequence: sequence, extent: movement.JumpExtent.Value, velocity: movement.JumpVelocity.Value, cellId: wireCellId, position: wirePosition, rotation: wireRotation, instanceSequence: session.InstanceSequence, serverControlSequence: session.ServerControlSequence, teleportSequence: session.TeleportSequence, forcePositionSequence: session.ForcePositionSequence); session.SendGameAction(body); } } /// /// Ports the position-send slot in CommandInterpreter::UseTime /// (0x006B3BF0), which SmartBox calls after draining inbound /// events. The predicate and serialized frame therefore observe any /// accepted ForcePosition or teleport state from this update. /// public void SendPostNetworkPosition( WorldSession? session, PlayerMovementController controller, bool hidden) { if (session is null || hidden) return; if (!controller.TryGetOutboundPosition(out Position position)) { return; } uint wireCellId = position.ObjCellId; Vector3 wirePosition = position.Frame.Origin; Quaternion wireRotation = position.Frame.Orientation; if (!controller.ShouldSendPositionEvent( position, controller.ContactPlane, controller.SimTimeSeconds) || !controller.CanSendPositionEvent) { return; } MovementResult movement = controller.CapturePresentationResult(); byte contactByte = movement.IsOnGround ? (byte)1 : (byte)0; uint sequence = session.NextGameActionSequence(); byte[] body = AutonomousPosition.Build( gameActionSequence: sequence, cellId: wireCellId, position: wirePosition, rotation: wireRotation, instanceSequence: session.InstanceSequence, serverControlSequence: session.ServerControlSequence, teleportSequence: session.TeleportSequence, forcePositionSequence: session.ForcePositionSequence, lastContact: contactByte); _diagnostic.OnOutbound( "AP", sequence, movement, wirePosition, wireCellId, contactByte); session.SendGameAction(body); controller.NotePositionSent( position, controller.ContactPlane, controller.SimTimeSeconds); } /// /// Retail ForcePosition acknowledgement sent immediately after the local /// body is blipped. Unlike the periodic post-network sender, this bypasses /// the elapsed/difference predicate but still requires a valid grounded /// canonical frame. /// internal void SendImmediatePosition( WorldSession? session, PlayerMovementController? controller) { if (session is null || controller is null || !controller.CanSendPositionEvent) { return; } if (!controller.TryGetOutboundPosition(out Position outboundPosition)) return; uint cellId = outboundPosition.ObjCellId; Vector3 position = outboundPosition.Frame.Origin; Quaternion rotation = outboundPosition.Frame.Orientation; uint sequence = session.NextGameActionSequence(); byte[] body = AutonomousPosition.Build( gameActionSequence: sequence, cellId: cellId, position: position, rotation: rotation, instanceSequence: session.InstanceSequence, serverControlSequence: session.ServerControlSequence, teleportSequence: session.TeleportSequence, forcePositionSequence: session.ForcePositionSequence, lastContact: 1); session.SendGameAction(body); controller.NotePositionSent( outboundPosition, controller.ContactPlane, controller.SimTimeSeconds); } public bool TrySendMovement( WorldSession? session, PlayerMovementController? controller, MovementResult movement) { if (session is null || controller is null) return false; if (!controller.TryGetOutboundPosition(out Position outboundPosition)) { return false; } uint wireCellId = outboundPosition.ObjCellId; Vector3 wirePosition = outboundPosition.Frame.Origin; Quaternion wireRotation = outboundPosition.Frame.Orientation; byte contactByte = movement.IsOnGround ? (byte)1 : (byte)0; RawMotionState rawMotionState = BuildRawMotionState(movement); uint sequence = session.NextGameActionSequence(); byte[] body = MoveToState.Build( gameActionSequence: sequence, rawMotionState: rawMotionState, cellId: wireCellId, position: wirePosition, rotation: wireRotation, instanceSequence: session.InstanceSequence, serverControlSequence: session.ServerControlSequence, teleportSequence: session.TeleportSequence, forcePositionSequence: session.ForcePositionSequence, contact: contactByte != 0, standingLongjump: false); _diagnostic.OnOutbound( "MTS", sequence, movement, wirePosition, wireCellId, contactByte); session.SendGameAction(body); controller.NoteMovementSent( controller.SimTimeSeconds, movement.IsMouseLookMovementEvent); return true; } public static RawMotionState BuildRawMotionState(MovementResult movement) { HoldKey axisHoldKey = movement.IsRunning ? HoldKey.Run : HoldKey.None; return new RawMotionState { CurrentHoldKey = axisHoldKey, // CommandInterpreter::SendMovementEvent @ 0x006B4680 passes // CPhysicsObj::InqRawMotionState directly to MoveToStatePack. // RawMotionState::UnPack @ 0x0051EFC0 treats an absent style as // NonCombat, so this is state—not optional observer decoration. CurrentStyle = movement.CurrentStyle, ForwardCommand = movement.ForwardCommand ?? RawMotionState.Default.ForwardCommand, ForwardHoldKey = movement.ForwardCommand.HasValue ? axisHoldKey : HoldKey.Invalid, ForwardSpeed = movement.ForwardSpeed ?? RawMotionState.Default.ForwardSpeed, SidestepCommand = movement.SidestepCommand ?? RawMotionState.Default.SidestepCommand, SidestepHoldKey = movement.SidestepCommand.HasValue ? movement.SidestepUsesRunHold ? HoldKey.Run : axisHoldKey : HoldKey.Invalid, SidestepSpeed = movement.SidestepSpeed ?? RawMotionState.Default.SidestepSpeed, TurnCommand = movement.TurnCommand ?? RawMotionState.Default.TurnCommand, TurnHoldKey = movement.TurnCommand.HasValue ? movement.TurnUsesRunHold ? HoldKey.Run : axisHoldKey : HoldKey.Invalid, TurnSpeed = movement.TurnSpeed ?? RawMotionState.Default.TurnSpeed, }; } }