refactor(input): extract the gameplay frame
This commit is contained in:
parent
0bc9fda9de
commit
c557038353
24 changed files with 2433 additions and 559 deletions
83
src/AcDream.App/Input/DispatcherMovementInputSource.cs
Normal file
83
src/AcDream.App/Input/DispatcherMovementInputSource.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
internal interface IMovementInputSource
|
||||
{
|
||||
MovementInput Capture();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns held movement sampling and the retail autorun latch. The input
|
||||
/// dispatcher remains the only keyboard/mouse-button state source.
|
||||
/// </summary>
|
||||
internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
||||
{
|
||||
private readonly IInputCaptureSource? _capture;
|
||||
private InputDispatcher? _dispatcher;
|
||||
|
||||
public DispatcherMovementInputSource(IInputCaptureSource? capture = null) =>
|
||||
_capture = capture;
|
||||
|
||||
public bool AutoRunActive { get; private set; }
|
||||
|
||||
public void Bind(InputDispatcher dispatcher)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
if (_dispatcher is not null && !ReferenceEquals(_dispatcher, dispatcher))
|
||||
throw new InvalidOperationException(
|
||||
"The movement input source is already bound to another dispatcher.");
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
public MovementInput Capture()
|
||||
{
|
||||
// Devtools owns the whole gameplay keyboard while active, including
|
||||
// a latched autorun. Retained chat owns physical key state only;
|
||||
// retail's autorun latch continues until an explicit cancel action.
|
||||
if (_capture?.DevToolsWantCaptureKeyboard == true)
|
||||
return default;
|
||||
|
||||
if (_dispatcher is not { } dispatcher)
|
||||
return default;
|
||||
|
||||
bool walking = dispatcher.IsActionHeld(InputAction.MovementWalkMode);
|
||||
bool forward = dispatcher.IsActionHeld(InputAction.MovementForward);
|
||||
return new MovementInput(
|
||||
Forward: forward || AutoRunActive,
|
||||
Backward: dispatcher.IsActionHeld(InputAction.MovementBackup),
|
||||
StrafeLeft: dispatcher.IsActionHeld(InputAction.MovementStrafeLeft),
|
||||
StrafeRight: dispatcher.IsActionHeld(InputAction.MovementStrafeRight),
|
||||
TurnLeft: dispatcher.IsActionHeld(InputAction.MovementTurnLeft),
|
||||
TurnRight: dispatcher.IsActionHeld(InputAction.MovementTurnRight),
|
||||
Run: !walking,
|
||||
Jump: dispatcher.IsActionHeld(InputAction.MovementJump));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the press-only autorun policy at the same point in the semantic
|
||||
/// action pipeline as the former GameWindow body.
|
||||
/// </summary>
|
||||
/// <returns>True when the action is fully consumed.</returns>
|
||||
public bool HandlePressedAction(InputAction action)
|
||||
{
|
||||
if (action == InputAction.MovementRunLock)
|
||||
{
|
||||
AutoRunActive = !AutoRunActive;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (AutoRunActive && action is (
|
||||
InputAction.MovementBackup
|
||||
or InputAction.MovementStop
|
||||
or InputAction.MovementStrafeLeft
|
||||
or InputAction.MovementStrafeRight))
|
||||
{
|
||||
AutoRunActive = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ResetSession() => AutoRunActive = false;
|
||||
}
|
||||
87
src/AcDream.App/Input/GameplayInputFrameController.cs
Normal file
87
src/AcDream.App/Input/GameplayInputFrameController.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
internal interface ICombatInputFrameController
|
||||
{
|
||||
void Tick();
|
||||
void HandleMovementInput(InputAction action, ActivationType activation);
|
||||
bool HandleInputAction(InputAction action, ActivationType activation);
|
||||
}
|
||||
|
||||
internal sealed class CombatAttackInputFrameAdapter : ICombatInputFrameController
|
||||
{
|
||||
private readonly CombatAttackController _owner;
|
||||
|
||||
public CombatAttackInputFrameAdapter(CombatAttackController owner) =>
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
|
||||
public void Tick() => _owner.Tick();
|
||||
|
||||
public void HandleMovementInput(InputAction action, ActivationType activation) =>
|
||||
_owner.HandleMovementInput(action, activation);
|
||||
|
||||
public bool HandleInputAction(InputAction action, ActivationType activation) =>
|
||||
_owner.HandleInputAction(action, activation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns the complete pre-object input frame: held semantic dispatch, one raw
|
||||
/// mouse-look sample, and combat attack intent.
|
||||
/// </summary>
|
||||
internal sealed class GameplayInputFrameController : IGameplayInputFramePhase
|
||||
{
|
||||
private readonly InputDispatcher? _dispatcher;
|
||||
private readonly DispatcherMovementInputSource _movement;
|
||||
private readonly IMouseLookInputFrameController? _mouseLook;
|
||||
private readonly ICombatInputFrameController _combat;
|
||||
|
||||
public GameplayInputFrameController(
|
||||
InputDispatcher? dispatcher,
|
||||
DispatcherMovementInputSource movement,
|
||||
IMouseLookInputFrameController? mouseLook,
|
||||
ICombatInputFrameController combat)
|
||||
{
|
||||
_dispatcher = dispatcher;
|
||||
_movement = movement ?? throw new ArgumentNullException(nameof(movement));
|
||||
_mouseLook = mouseLook;
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
}
|
||||
|
||||
public bool MouseLookActive => _mouseLook?.Active == true;
|
||||
|
||||
public void Tick(UpdateFrameTiming timing)
|
||||
{
|
||||
_ = timing;
|
||||
_dispatcher?.Tick();
|
||||
_mouseLook?.Tick();
|
||||
_combat.Tick();
|
||||
}
|
||||
|
||||
public bool HandlePointerAction(InputAction action, ActivationType activation) =>
|
||||
_mouseLook?.HandlePointerAction(action, activation) == true;
|
||||
|
||||
public bool HandleCombatAction(InputAction action, ActivationType activation)
|
||||
{
|
||||
// ACCmdInterp::HandleNewForwardMovement @ 0x0058B1F0 aborts
|
||||
// automatic combat before the movement command enters the interpreter.
|
||||
_combat.HandleMovementInput(action, activation);
|
||||
return _combat.HandleInputAction(action, activation);
|
||||
}
|
||||
|
||||
public bool HandlePressedMovementAction(InputAction action) =>
|
||||
_movement.HandlePressedAction(action);
|
||||
|
||||
public void QueueRawMouseDelta(float dx, float dy) =>
|
||||
_mouseLook?.QueueRawDelta(dx, dy);
|
||||
|
||||
public void EndMouseLook() => _mouseLook?.EndForLifecycle();
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
_mouseLook?.ResetSession();
|
||||
_movement.ResetSession();
|
||||
}
|
||||
}
|
||||
74
src/AcDream.App/Input/InputCaptureSources.cs
Normal file
74
src/AcDream.App/Input/InputCaptureSources.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
internal interface IInputCaptureSource
|
||||
{
|
||||
bool WantCaptureMouse { get; }
|
||||
bool WantCaptureKeyboard { get; }
|
||||
bool DevToolsWantCaptureKeyboard { get; }
|
||||
}
|
||||
|
||||
internal sealed class DevToolsInputCaptureSource
|
||||
{
|
||||
private readonly bool _enabled;
|
||||
|
||||
public DevToolsInputCaptureSource(bool enabled) => _enabled = enabled;
|
||||
|
||||
public bool WantCaptureMouse =>
|
||||
_enabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse;
|
||||
|
||||
public bool WantCaptureKeyboard =>
|
||||
_enabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard;
|
||||
}
|
||||
|
||||
internal sealed class RetainedUiInputCaptureSlot
|
||||
{
|
||||
public UiRoot? Root { get; set; }
|
||||
|
||||
public bool WantCaptureMouse => Root?.WantsMouse ?? false;
|
||||
public bool WantCaptureKeyboard => Root?.WantsKeyboard ?? false;
|
||||
}
|
||||
|
||||
internal sealed class CompositeInputCaptureSource : IInputCaptureSource
|
||||
{
|
||||
private readonly DevToolsInputCaptureSource _devTools;
|
||||
private readonly RetainedUiInputCaptureSlot _retained;
|
||||
|
||||
public CompositeInputCaptureSource(
|
||||
DevToolsInputCaptureSource devTools,
|
||||
RetainedUiInputCaptureSlot retained)
|
||||
{
|
||||
_devTools = devTools ?? throw new ArgumentNullException(nameof(devTools));
|
||||
_retained = retained ?? throw new ArgumentNullException(nameof(retained));
|
||||
}
|
||||
|
||||
public bool WantCaptureMouse =>
|
||||
_devTools.WantCaptureMouse || _retained.WantCaptureMouse;
|
||||
|
||||
public bool WantCaptureKeyboard =>
|
||||
DevToolsWantCaptureKeyboard || _retained.WantCaptureKeyboard;
|
||||
|
||||
public bool DevToolsWantCaptureKeyboard =>
|
||||
_devTools.WantCaptureKeyboard;
|
||||
}
|
||||
|
||||
internal sealed class DelegateInputCaptureSource : IInputCaptureSource
|
||||
{
|
||||
private readonly Func<bool> _wantCaptureMouse;
|
||||
private readonly Func<bool> _wantCaptureKeyboard;
|
||||
|
||||
public DelegateInputCaptureSource(
|
||||
Func<bool> wantCaptureMouse,
|
||||
Func<bool> wantCaptureKeyboard)
|
||||
{
|
||||
_wantCaptureMouse = wantCaptureMouse
|
||||
?? throw new ArgumentNullException(nameof(wantCaptureMouse));
|
||||
_wantCaptureKeyboard = wantCaptureKeyboard
|
||||
?? throw new ArgumentNullException(nameof(wantCaptureKeyboard));
|
||||
}
|
||||
|
||||
public bool WantCaptureMouse => _wantCaptureMouse();
|
||||
public bool WantCaptureKeyboard => _wantCaptureKeyboard();
|
||||
public bool DevToolsWantCaptureKeyboard => false;
|
||||
}
|
||||
|
|
@ -11,13 +11,18 @@ namespace AcDream.App.Input;
|
|||
/// </summary>
|
||||
public sealed class LocalPlayerOutboundController
|
||||
{
|
||||
private readonly Action<string, uint, MovementResult, Vector3, uint, byte> _diagnostic;
|
||||
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 = diagnostic
|
||||
?? throw new ArgumentNullException(nameof(diagnostic));
|
||||
_diagnostic = new DelegateMovementTruthDiagnosticSink(diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -107,7 +112,7 @@ public sealed class LocalPlayerOutboundController
|
|||
teleportSequence: session.TeleportSequence,
|
||||
forcePositionSequence: session.ForcePositionSequence,
|
||||
lastContact: contactByte);
|
||||
_diagnostic(
|
||||
_diagnostic.OnOutbound(
|
||||
"AP",
|
||||
sequence,
|
||||
movement,
|
||||
|
|
@ -193,7 +198,7 @@ public sealed class LocalPlayerOutboundController
|
|||
forcePositionSequence: session.ForcePositionSequence,
|
||||
contact: contactByte != 0,
|
||||
standingLongjump: false);
|
||||
_diagnostic(
|
||||
_diagnostic.OnOutbound(
|
||||
"MTS",
|
||||
sequence,
|
||||
movement,
|
||||
|
|
|
|||
262
src/AcDream.App/Input/MouseLookController.cs
Normal file
262
src/AcDream.App/Input/MouseLookController.cs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
using AcDream.App.Net;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
internal interface IInputMonotonicClock
|
||||
{
|
||||
float NowSeconds { get; }
|
||||
}
|
||||
|
||||
internal sealed class EnvironmentInputMonotonicClock : IInputMonotonicClock
|
||||
{
|
||||
public float NowSeconds => (float)(Environment.TickCount64 / 1000.0);
|
||||
}
|
||||
|
||||
internal interface IPointerPositionSource
|
||||
{
|
||||
float X { get; }
|
||||
float Y { get; }
|
||||
}
|
||||
|
||||
internal sealed class PointerPositionState : IPointerPositionSource
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
}
|
||||
|
||||
internal interface IMouseLookCursor
|
||||
{
|
||||
bool HasSavedMode { get; }
|
||||
void Hide();
|
||||
void Restore();
|
||||
}
|
||||
|
||||
internal interface IMouseLookInputFrameController
|
||||
{
|
||||
bool Active { get; }
|
||||
bool HandlePointerAction(InputAction action, ActivationType activation);
|
||||
void QueueRawDelta(float dx, float dy);
|
||||
void Tick();
|
||||
void EndAndRestoreCursor();
|
||||
void EndForLifecycle();
|
||||
void ResetSession();
|
||||
}
|
||||
|
||||
internal sealed class SilkMouseLookCursor : IMouseLookCursor
|
||||
{
|
||||
private readonly IMouse _mouse;
|
||||
private CursorMode? _savedMode;
|
||||
|
||||
public SilkMouseLookCursor(IMouse mouse) =>
|
||||
_mouse = mouse ?? throw new ArgumentNullException(nameof(mouse));
|
||||
|
||||
public bool HasSavedMode => _savedMode.HasValue;
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_savedMode = _mouse.Cursor.CursorMode;
|
||||
_mouse.Cursor.CursorMode = CursorMode.Hidden;
|
||||
}
|
||||
|
||||
public void Restore()
|
||||
{
|
||||
_mouse.Cursor.CursorMode = _savedMode ?? CursorMode.Normal;
|
||||
_savedMode = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current chase-camera input targets. GameWindow keeps compatibility
|
||||
/// properties over this one slot until checkpoint F moves camera ownership.
|
||||
/// </summary>
|
||||
internal sealed class ChaseCameraInputState
|
||||
{
|
||||
public ChaseCamera? Legacy { get; set; }
|
||||
public RetailChaseCamera? Retail { get; set; }
|
||||
public float Sensitivity { get; set; } = 0.15f;
|
||||
public bool RmbOrbitHeld { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns instant mouse-look state, raw-sample timing/filtering, movement
|
||||
/// transition sends, and cursor capture across every exit path.
|
||||
/// </summary>
|
||||
internal sealed class MouseLookController : IMouseLookInputFrameController
|
||||
{
|
||||
private readonly IMouseSource _mouseSource;
|
||||
private readonly IPointerPositionSource _pointer;
|
||||
private readonly ILocalPlayerModeSource _playerMode;
|
||||
private readonly ILocalPlayerControllerSource _playerController;
|
||||
private readonly CameraController _camera;
|
||||
private readonly ChaseCameraInputState _chase;
|
||||
private readonly IMovementInputSource _movementInput;
|
||||
private readonly LocalPlayerOutboundController _outbound;
|
||||
private readonly ILiveWorldSessionSource _session;
|
||||
private readonly IMouseLookCursor _cursor;
|
||||
private readonly IInputMonotonicClock _clock;
|
||||
private readonly MouseLookState _state;
|
||||
private bool _lastWantCaptureMouse;
|
||||
|
||||
public MouseLookController(
|
||||
IMouseSource mouseSource,
|
||||
IPointerPositionSource pointer,
|
||||
ILocalPlayerModeSource playerMode,
|
||||
ILocalPlayerControllerSource playerController,
|
||||
CameraController camera,
|
||||
ChaseCameraInputState chase,
|
||||
IMovementInputSource movementInput,
|
||||
LocalPlayerOutboundController outbound,
|
||||
ILiveWorldSessionSource session,
|
||||
IMouseLookCursor cursor,
|
||||
IInputMonotonicClock clock)
|
||||
{
|
||||
_mouseSource = mouseSource ?? throw new ArgumentNullException(nameof(mouseSource));
|
||||
_pointer = pointer ?? throw new ArgumentNullException(nameof(pointer));
|
||||
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
|
||||
_playerController = playerController
|
||||
?? throw new ArgumentNullException(nameof(playerController));
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_chase = chase ?? throw new ArgumentNullException(nameof(chase));
|
||||
_movementInput = movementInput
|
||||
?? throw new ArgumentNullException(nameof(movementInput));
|
||||
_outbound = outbound ?? throw new ArgumentNullException(nameof(outbound));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_cursor = cursor ?? throw new ArgumentNullException(nameof(cursor));
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
_state = new MouseLookState(ApplyHorizontalAdjustment);
|
||||
}
|
||||
|
||||
public bool Active => _state.Active;
|
||||
|
||||
public bool HandlePointerAction(InputAction action, ActivationType activation)
|
||||
{
|
||||
if (action == InputAction.AcdreamRmbOrbitHold)
|
||||
{
|
||||
if (activation == ActivationType.Press)
|
||||
_chase.RmbOrbitHeld = _playerMode.IsPlayerMode && _camera.IsChaseMode;
|
||||
else if (activation == ActivationType.Release)
|
||||
_chase.RmbOrbitHeld = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action != InputAction.CameraInstantMouseLook)
|
||||
return false;
|
||||
|
||||
if (activation == ActivationType.Press)
|
||||
Begin();
|
||||
else if (activation == ActivationType.Release)
|
||||
EndAndRestoreCursor();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void QueueRawDelta(float dx, float dy) => _state.QueueDelta(dx, dy);
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
bool wantCaptureMouse = _mouseSource.WantCaptureMouse;
|
||||
if (wantCaptureMouse != _lastWantCaptureMouse)
|
||||
{
|
||||
if (wantCaptureMouse && _state.Active)
|
||||
EndAndRestoreCursor();
|
||||
_state.OnWantCaptureMouseChanged(wantCaptureMouse);
|
||||
_lastWantCaptureMouse = wantCaptureMouse;
|
||||
}
|
||||
|
||||
float nowSeconds = _clock.NowSeconds;
|
||||
if (!_state.TryTakeRawSample(nowSeconds, out float rawX, out float rawY))
|
||||
return;
|
||||
|
||||
PlayerMovementController? controller = _playerController.Controller;
|
||||
if (rawX == 0f && rawY == 0f)
|
||||
controller?.StopMouseDrift(_movementInput.Capture());
|
||||
|
||||
(float filteredX, float filteredY) =
|
||||
CameraDiagnostics.UseRetailChaseCamera && _chase.Retail is { } retail
|
||||
? retail.FilterMouseDelta(rawX, rawY, weight: 0.5f, nowSec: nowSeconds)
|
||||
: (rawX, rawY);
|
||||
_state.ApplyDelta(filteredX, _chase.Sensitivity);
|
||||
if (_chase.Retail is { } retailCamera)
|
||||
retailCamera.AdjustPitch(filteredY * 0.003f * _chase.Sensitivity);
|
||||
else
|
||||
_chase.Legacy?.AdjustPitch(filteredY * 0.003f * _chase.Sensitivity);
|
||||
}
|
||||
|
||||
public void EndAndRestoreCursor()
|
||||
{
|
||||
bool stateWasActive = _state.Active;
|
||||
_state.Release();
|
||||
|
||||
PlayerMovementController? controller = _playerController.Controller;
|
||||
if (controller is not null && controller.EndMouseLook(_movementInput.Capture()))
|
||||
{
|
||||
_outbound.TrySendMovement(
|
||||
_session.CurrentSession,
|
||||
controller,
|
||||
controller.CaptureMovementResult(mouseLookEvent: false));
|
||||
}
|
||||
|
||||
if (stateWasActive || _cursor.HasSavedMode)
|
||||
_cursor.Restore();
|
||||
}
|
||||
|
||||
public void EndForLifecycle()
|
||||
{
|
||||
EndAndRestoreCursor();
|
||||
_chase.RmbOrbitHeld = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Session teardown releases presentation capture without emitting a
|
||||
/// movement packet into the ending session, matching the prior reset edge.
|
||||
/// </summary>
|
||||
public void ResetSession()
|
||||
{
|
||||
bool stateWasActive = _state.Active;
|
||||
_state.Release();
|
||||
_chase.RmbOrbitHeld = false;
|
||||
_lastWantCaptureMouse = false;
|
||||
if (stateWasActive || _cursor.HasSavedMode)
|
||||
_cursor.Restore();
|
||||
}
|
||||
|
||||
private void Begin()
|
||||
{
|
||||
PlayerMovementController? controller = _playerController.Controller;
|
||||
if (!_playerMode.IsPlayerMode
|
||||
|| !_camera.IsChaseMode
|
||||
|| controller is not { State: PlayerState.InWorld })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float nowSeconds = _clock.NowSeconds;
|
||||
_state.Press(
|
||||
_pointer.X,
|
||||
_pointer.Y,
|
||||
_mouseSource.WantCaptureMouse,
|
||||
nowSeconds);
|
||||
if (!_state.Active)
|
||||
return;
|
||||
|
||||
if (!controller.BeginMouseLook(_movementInput.Capture()))
|
||||
{
|
||||
_state.Release();
|
||||
return;
|
||||
}
|
||||
|
||||
_outbound.TrySendMovement(
|
||||
_session.CurrentSession,
|
||||
controller,
|
||||
controller.CaptureMovementResult(mouseLookEvent: false));
|
||||
_cursor.Hide();
|
||||
}
|
||||
|
||||
private void ApplyHorizontalAdjustment(float adjustment) =>
|
||||
_playerController.Controller?.SubmitMouseTurnAdjustment(
|
||||
adjustment,
|
||||
_movementInput.Capture());
|
||||
}
|
||||
161
src/AcDream.App/Input/MovementTruthDiagnosticController.cs
Normal file
161
src/AcDream.App/Input/MovementTruthDiagnosticController.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Net;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
internal interface IMovementTruthDiagnosticSink
|
||||
{
|
||||
void OnOutbound(
|
||||
string kind,
|
||||
uint sequence,
|
||||
MovementResult result,
|
||||
Vector3 wirePosition,
|
||||
uint wireCellId,
|
||||
byte contactByte);
|
||||
|
||||
void OnServerEcho(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
Vector3 serverWorldPosition);
|
||||
|
||||
void ResetSession();
|
||||
}
|
||||
|
||||
internal sealed class MovementTruthDiagnosticController
|
||||
: IMovementTruthDiagnosticSink
|
||||
{
|
||||
private readonly bool _enabled;
|
||||
private readonly ILocalPlayerControllerSource _player;
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private MovementTruthOutbound? _lastOutbound;
|
||||
|
||||
private readonly record struct MovementTruthOutbound(
|
||||
string Kind,
|
||||
uint Sequence,
|
||||
DateTime TimeUtc,
|
||||
Vector3 LocalWorldPosition,
|
||||
Vector3 WirePosition,
|
||||
uint WireCellId,
|
||||
bool IsOnGround,
|
||||
byte ContactByte,
|
||||
Vector3 Velocity);
|
||||
|
||||
public MovementTruthDiagnosticController(
|
||||
bool enabled,
|
||||
ILocalPlayerControllerSource player,
|
||||
ILocalPlayerIdentitySource identity)
|
||||
{
|
||||
_enabled = enabled;
|
||||
_player = player ?? throw new ArgumentNullException(nameof(player));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
}
|
||||
|
||||
public void OnOutbound(
|
||||
string kind,
|
||||
uint sequence,
|
||||
MovementResult result,
|
||||
Vector3 wirePosition,
|
||||
uint wireCellId,
|
||||
byte contactByte)
|
||||
{
|
||||
if (!_enabled)
|
||||
return;
|
||||
|
||||
Vector3 velocity = _player.Controller?.BodyVelocity ?? Vector3.Zero;
|
||||
_lastOutbound = new MovementTruthOutbound(
|
||||
kind,
|
||||
sequence,
|
||||
DateTime.UtcNow,
|
||||
result.Position,
|
||||
wirePosition,
|
||||
wireCellId,
|
||||
result.IsOnGround,
|
||||
contactByte,
|
||||
velocity);
|
||||
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"move-truth OUT kind={kind} seq={sequence} local={Fmt(result.Position)} localCell=0x{result.CellId:X8} wire={Fmt(wirePosition)} wireCell=0x{wireCellId:X8} grounded={result.IsOnGround} contact={contactByte} vel={Fmt(velocity)} f={FmtCmd(result.ForwardCommand)} s={FmtCmd(result.SidestepCommand)} t={FmtCmd(result.TurnCommand)}"));
|
||||
}
|
||||
|
||||
public void OnServerEcho(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
Vector3 serverWorldPosition)
|
||||
{
|
||||
if (!_enabled || update.Guid != _identity.ServerGuid)
|
||||
return;
|
||||
|
||||
DateTime now = DateTime.UtcNow;
|
||||
PlayerMovementController? controller = _player.Controller;
|
||||
Vector3? localPosition = controller?.Position;
|
||||
uint? localCellId = controller?.CellId;
|
||||
Vector3? deltaLocal = localPosition.HasValue
|
||||
? serverWorldPosition - localPosition.Value
|
||||
: null;
|
||||
|
||||
string localText = localPosition.HasValue ? Fmt(localPosition.Value) : "-";
|
||||
string localCellText = localCellId.HasValue
|
||||
? FormattableString.Invariant($"0x{localCellId.Value:X8}")
|
||||
: "-";
|
||||
string deltaLocalText = deltaLocal.HasValue ? Fmt(deltaLocal.Value) : "-";
|
||||
string deltaLocalLength = deltaLocal.HasValue
|
||||
? FormattableString.Invariant($"{deltaLocal.Value.Length():F3}")
|
||||
: "-";
|
||||
|
||||
string lastText = "-";
|
||||
if (_lastOutbound is { } last)
|
||||
{
|
||||
Vector3 deltaOut = serverWorldPosition - last.LocalWorldPosition;
|
||||
double ageMs = (now - last.TimeUtc).TotalMilliseconds;
|
||||
lastText = FormattableString.Invariant(
|
||||
$"{last.Kind}:{last.Sequence} ageMs={ageMs:F0} outGrounded={last.IsOnGround} outContact={last.ContactByte} outCell=0x{last.WireCellId:X8} deltaOut={Fmt(deltaOut)} distOut={deltaOut.Length():F3}");
|
||||
}
|
||||
|
||||
string state = controller?.State.ToString() ?? "-";
|
||||
string velocityText = update.Velocity.HasValue
|
||||
? Fmt(update.Velocity.Value)
|
||||
: "-";
|
||||
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"move-truth ECHO guid=0x{update.Guid:X8} server={Fmt(serverWorldPosition)} serverCell=0x{update.Position.LandblockId:X8} local={localText} localCell={localCellText} deltaLocal={deltaLocalText} distLocal={deltaLocalLength} serverVel={velocityText} state={state} lastOut={lastText}"));
|
||||
}
|
||||
|
||||
public void ResetSession() => _lastOutbound = null;
|
||||
|
||||
private static string Fmt(Vector3 value) =>
|
||||
FormattableString.Invariant(
|
||||
$"({value.X:F3},{value.Y:F3},{value.Z:F3})");
|
||||
|
||||
private static string FmtCmd(uint? command) =>
|
||||
command.HasValue
|
||||
? FormattableString.Invariant($"0x{command.Value:X8}")
|
||||
: "-";
|
||||
}
|
||||
|
||||
internal sealed class DelegateMovementTruthDiagnosticSink
|
||||
: IMovementTruthDiagnosticSink
|
||||
{
|
||||
private readonly Action<string, uint, MovementResult, Vector3, uint, byte>
|
||||
_outbound;
|
||||
|
||||
public DelegateMovementTruthDiagnosticSink(
|
||||
Action<string, uint, MovementResult, Vector3, uint, byte> outbound) =>
|
||||
_outbound = outbound ?? throw new ArgumentNullException(nameof(outbound));
|
||||
|
||||
public void OnOutbound(
|
||||
string kind,
|
||||
uint sequence,
|
||||
MovementResult result,
|
||||
Vector3 wirePosition,
|
||||
uint wireCellId,
|
||||
byte contactByte) =>
|
||||
_outbound(kind, sequence, result, wirePosition, wireCellId, contactByte);
|
||||
|
||||
public void OnServerEcho(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
Vector3 serverWorldPosition)
|
||||
{
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
{
|
||||
private readonly Func<bool> _canPresentPlayer;
|
||||
private readonly Func<PlayerMovementController?> _getController;
|
||||
private readonly Func<MovementInput> _captureInput;
|
||||
private readonly IMovementInputSource _movementInput;
|
||||
private readonly Func<uint> _resolveLocalEntityId;
|
||||
private readonly Action _handleTargeting;
|
||||
private readonly Func<bool> _isHidden;
|
||||
|
|
@ -53,7 +53,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
public RetailLocalPlayerFrameController(
|
||||
Func<bool> canPresentPlayer,
|
||||
Func<PlayerMovementController?> getController,
|
||||
Func<MovementInput> captureInput,
|
||||
IMovementInputSource movementInput,
|
||||
Func<uint> resolveLocalEntityId,
|
||||
Action handleTargeting,
|
||||
Func<bool> isHidden,
|
||||
|
|
@ -67,8 +67,8 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
?? throw new ArgumentNullException(nameof(canPresentPlayer));
|
||||
_getController = getController
|
||||
?? throw new ArgumentNullException(nameof(getController));
|
||||
_captureInput = captureInput
|
||||
?? throw new ArgumentNullException(nameof(captureInput));
|
||||
_movementInput = movementInput
|
||||
?? throw new ArgumentNullException(nameof(movementInput));
|
||||
_resolveLocalEntityId = resolveLocalEntityId
|
||||
?? throw new ArgumentNullException(nameof(resolveLocalEntityId));
|
||||
_handleTargeting = handleTargeting
|
||||
|
|
@ -134,7 +134,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
|
||||
MovementResult movement = hidden
|
||||
? controller.TickHidden(deltaSeconds, _handleTargeting)
|
||||
: controller.Update(deltaSeconds, _captureInput(), _handleTargeting);
|
||||
: controller.Update(deltaSeconds, _movementInput.Capture(), _handleTargeting);
|
||||
|
||||
_project(controller, movement, hidden);
|
||||
_sendPreNetwork(controller, movement, hidden);
|
||||
|
|
|
|||
|
|
@ -11,18 +11,15 @@ namespace AcDream.App.Input;
|
|||
/// <see cref="InputDispatcher"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// We don't link Hexa.NET.ImGui or ImGuiNET directly here — the
|
||||
/// constructor takes two delegates so the App.Rendering layer can
|
||||
/// proxy <c>ImGui.GetIO().WantCaptureMouse</c> via whichever ImGui
|
||||
/// package is currently active without leaking the type onto the
|
||||
/// abstraction interface.
|
||||
/// Production receives a typed capture source, keeping retained-UI and
|
||||
/// developer-UI ownership outside the Silk adapter. The public delegate
|
||||
/// overload remains as a compatibility seam for standalone consumers.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class SilkMouseSource : IMouseSource
|
||||
{
|
||||
private readonly IMouse _mouse;
|
||||
private readonly Func<bool> _wantCaptureMouse;
|
||||
private readonly Func<bool> _wantCaptureKeyboard;
|
||||
private readonly IInputCaptureSource _capture;
|
||||
private float _lastX;
|
||||
private float _lastY;
|
||||
private bool _haveLastPos;
|
||||
|
|
@ -32,10 +29,21 @@ public sealed class SilkMouseSource : IMouseSource
|
|||
public event Action<float, float>? MouseMove;
|
||||
public event Action<float>? Scroll;
|
||||
|
||||
/// <summary>Caller-supplied probe for the current modifier mask. Reused
|
||||
/// from the keyboard source so mouse events carry consistent modifier
|
||||
/// state.</summary>
|
||||
public Func<ModifierMask> ModifierProbe { get; set; } = () => ModifierMask.None;
|
||||
/// <summary>The keyboard source that owns the current modifier mask, so
|
||||
/// mouse events and keyboard events resolve identical chords.</summary>
|
||||
public IKeyboardSource? ModifierSource { get; set; }
|
||||
|
||||
internal SilkMouseSource(
|
||||
IMouse mouse,
|
||||
IInputCaptureSource capture,
|
||||
IKeyboardSource modifierSource)
|
||||
{
|
||||
_mouse = mouse ?? throw new ArgumentNullException(nameof(mouse));
|
||||
_capture = capture ?? throw new ArgumentNullException(nameof(capture));
|
||||
ModifierSource = modifierSource
|
||||
?? throw new ArgumentNullException(nameof(modifierSource));
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
public SilkMouseSource(
|
||||
IMouse mouse,
|
||||
|
|
@ -43,11 +51,16 @@ public sealed class SilkMouseSource : IMouseSource
|
|||
Func<bool> wantCaptureKeyboard)
|
||||
{
|
||||
_mouse = mouse ?? throw new ArgumentNullException(nameof(mouse));
|
||||
_wantCaptureMouse = wantCaptureMouse ?? throw new ArgumentNullException(nameof(wantCaptureMouse));
|
||||
_wantCaptureKeyboard = wantCaptureKeyboard ?? throw new ArgumentNullException(nameof(wantCaptureKeyboard));
|
||||
_capture = new DelegateInputCaptureSource(
|
||||
wantCaptureMouse,
|
||||
wantCaptureKeyboard);
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
_mouse.MouseDown += (_, btn) => MouseDown?.Invoke(btn, ModifierProbe());
|
||||
_mouse.MouseUp += (_, btn) => MouseUp?.Invoke(btn, ModifierProbe());
|
||||
private void Subscribe()
|
||||
{
|
||||
_mouse.MouseDown += (_, btn) => MouseDown?.Invoke(btn, ReadModifiers());
|
||||
_mouse.MouseUp += (_, btn) => MouseUp?.Invoke(btn, ReadModifiers());
|
||||
_mouse.MouseMove += (_, pos) =>
|
||||
{
|
||||
float dx, dy;
|
||||
|
|
@ -69,8 +82,11 @@ public sealed class SilkMouseSource : IMouseSource
|
|||
_mouse.Scroll += (_, scroll) => Scroll?.Invoke(scroll.Y);
|
||||
}
|
||||
|
||||
private ModifierMask ReadModifiers() =>
|
||||
ModifierSource?.CurrentModifiers ?? ModifierMask.None;
|
||||
|
||||
public bool IsHeld(MouseButton button) => _mouse.IsButtonPressed(button);
|
||||
|
||||
public bool WantCaptureMouse => _wantCaptureMouse();
|
||||
public bool WantCaptureKeyboard => _wantCaptureKeyboard();
|
||||
public bool WantCaptureMouse => _capture.WantCaptureMouse;
|
||||
public bool WantCaptureKeyboard => _capture.WantCaptureKeyboard;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue