252 lines
9.3 KiB
C#
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,
|
|
};
|
|
}
|
|
}
|