acdream/src/AcDream.App/Input/LocalPlayerOutboundController.cs

252 lines
9.3 KiB
C#

using System.Numerics;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
namespace AcDream.App.Input;
/// <summary>
/// Serializes input-originated output from the object-phase result and the
/// periodic position report from post-inbound controller state.
/// </summary>
public sealed class LocalPlayerOutboundController
{
private readonly IMovementTruthDiagnosticSink _diagnostic;
internal LocalPlayerOutboundController(IMovementTruthDiagnosticSink diagnostic)
{
_diagnostic = diagnostic
?? throw new ArgumentNullException(nameof(diagnostic));
}
public LocalPlayerOutboundController(
Action<string, uint, MovementResult, Vector3, uint, byte> diagnostic)
{
_diagnostic = new DelegateMovementTruthDiagnosticSink(diagnostic);
}
/// <summary>
/// Sends input-originated movement and jump packets before inbound
/// dispatch. Retail emits these from input/object processing rather than
/// from <c>CommandInterpreter::UseTime</c>.
/// </summary>
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);
}
}
/// <summary>
/// Ports the position-send slot in <c>CommandInterpreter::UseTime</c>
/// (<c>0x006B3BF0</c>), which SmartBox calls after draining inbound
/// events. The predicate and serialized frame therefore observe any
/// accepted ForcePosition or teleport state from this update.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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,
};
}
}