Match retail's update ordering so object animation, particles, and scripts advance before inbound teleport state is applied. Separate input-originated movement from post-network autonomous position output, and reconcile presentation without a second physics tick so recall cannot resume after arrival. Co-authored-by: OpenAI Codex <codex@openai.com>
209 lines
7.5 KiB
C#
209 lines
7.5 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 Func<float, Quaternion> _toWireRotation;
|
|
private readonly Action<string, uint, MovementResult, Vector3, uint, byte> _diagnostic;
|
|
|
|
public LocalPlayerOutboundController(
|
|
Func<float, Quaternion> toWireRotation,
|
|
Action<string, uint, MovementResult, Vector3, uint, byte> diagnostic)
|
|
{
|
|
_toWireRotation = toWireRotation
|
|
?? throw new ArgumentNullException(nameof(toWireRotation));
|
|
_diagnostic = diagnostic
|
|
?? throw new ArgumentNullException(nameof(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;
|
|
|
|
Quaternion wireRotation = _toWireRotation(controller.Yaw);
|
|
if (!controller.TryGetOutboundPosition(
|
|
wireRotation,
|
|
out uint wireCellId,
|
|
out Vector3 wirePosition))
|
|
{
|
|
return;
|
|
}
|
|
|
|
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;
|
|
|
|
Quaternion wireRotation = _toWireRotation(controller.Yaw);
|
|
if (!controller.TryGetOutboundPosition(
|
|
wireRotation,
|
|
out uint wireCellId,
|
|
out Vector3 wirePosition))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var position = new Position(wireCellId, wirePosition, wireRotation);
|
|
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(
|
|
"AP",
|
|
sequence,
|
|
movement,
|
|
wirePosition,
|
|
wireCellId,
|
|
contactByte);
|
|
session.SendGameAction(body);
|
|
controller.NotePositionSent(
|
|
position,
|
|
controller.ContactPlane,
|
|
controller.SimTimeSeconds);
|
|
}
|
|
|
|
public bool TrySendMovement(
|
|
WorldSession? session,
|
|
PlayerMovementController? controller,
|
|
MovementResult movement)
|
|
{
|
|
if (session is null || controller is null)
|
|
return false;
|
|
|
|
Quaternion wireRotation = _toWireRotation(controller.Yaw);
|
|
if (!controller.TryGetOutboundPosition(
|
|
wireRotation,
|
|
out uint wireCellId,
|
|
out Vector3 wirePosition))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
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(
|
|
"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,
|
|
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,
|
|
};
|
|
}
|
|
}
|