refactor(input): extract the gameplay frame
This commit is contained in:
parent
0bc9fda9de
commit
c557038353
24 changed files with 2433 additions and 559 deletions
|
|
@ -34,8 +34,17 @@ public sealed class GameWindow : IDisposable
|
|||
private CameraController? _cameraController;
|
||||
private IMouse? _capturedMouse;
|
||||
private IDatReaderWriter? _dats;
|
||||
private float _lastMouseX;
|
||||
private float _lastMouseY;
|
||||
private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new();
|
||||
private float _lastMouseX
|
||||
{
|
||||
get => _pointerPosition.X;
|
||||
set => _pointerPosition.X = value;
|
||||
}
|
||||
private float _lastMouseY
|
||||
{
|
||||
get => _pointerPosition.Y;
|
||||
set => _pointerPosition.Y = value;
|
||||
}
|
||||
private Shader? _meshShader;
|
||||
private TextureCache? _textureCache;
|
||||
/// <summary>Phase N.4+: WB-backed rendering pipeline adapter. Always non-null
|
||||
|
|
@ -156,6 +165,8 @@ public sealed class GameWindow : IDisposable
|
|||
private LiveEntityAnimationScheduler _liveAnimationScheduler = null!;
|
||||
private LiveEntityAnimationPresenter _animationPresenter = null!;
|
||||
private readonly AcDream.App.Input.LocalPlayerProjectionController _localPlayerProjection;
|
||||
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
||||
_movementTruthDiagnostics;
|
||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||
private readonly AcDream.App.Input.RetailLocalPlayerFrameController _localPlayerFrame;
|
||||
private AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator = null!;
|
||||
|
|
@ -434,6 +445,10 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
|
||||
private AcDream.App.UI.UiHost? _uiHost;
|
||||
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
|
||||
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
|
||||
_combatAttackOperations = new();
|
||||
private readonly AcDream.App.Combat.CombatFeedbackSlot
|
||||
_combatFeedback = new();
|
||||
private AcDream.App.Combat.CombatAttackController? _combatAttackController;
|
||||
private AcDream.App.Combat.CombatTargetController? _combatTargetController;
|
||||
private AcDream.App.UI.ItemInteractionController? _itemInteractionController;
|
||||
|
|
@ -452,11 +467,8 @@ public sealed class GameWindow : IDisposable
|
|||
// _panelHost does. Self-subscribes to CombatState in its ctor, so
|
||||
// disposing isn't required (panel host holds the only ref).
|
||||
private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _debugVm;
|
||||
// DevToolsEnabled + DumpMoveTruthEnabled now read through _options
|
||||
// (RuntimeOptions.DevTools / DumpMoveTruth). Kept the same names for
|
||||
// local readability via expression-bodied properties.
|
||||
// DevToolsEnabled reads through typed RuntimeOptions.
|
||||
private bool DevToolsEnabled => _options.DevTools;
|
||||
private bool DumpMoveTruthEnabled => _options.DumpMoveTruth;
|
||||
|
||||
// Slice 3: the reset transaction remains cached by the host, while the
|
||||
// lifecycle controller owns each exact event/command/session generation.
|
||||
|
|
@ -530,8 +542,17 @@ public sealed class GameWindow : IDisposable
|
|||
get => _playerControllerSlot.Controller;
|
||||
set => _playerControllerSlot.Controller = value;
|
||||
}
|
||||
private AcDream.App.Rendering.ChaseCamera? _chaseCamera;
|
||||
private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera;
|
||||
private readonly AcDream.App.Input.ChaseCameraInputState _chaseCameraInput = new();
|
||||
private AcDream.App.Rendering.ChaseCamera? _chaseCamera
|
||||
{
|
||||
get => _chaseCameraInput.Legacy;
|
||||
set => _chaseCameraInput.Legacy = value;
|
||||
}
|
||||
private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera
|
||||
{
|
||||
get => _chaseCameraInput.Retail;
|
||||
set => _chaseCameraInput.Retail = value;
|
||||
}
|
||||
private readonly AcDream.App.Input.LocalPlayerModeState _localPlayerMode = new();
|
||||
private bool _playerMode
|
||||
{
|
||||
|
|
@ -549,21 +570,8 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
||||
_characterOptions1 =
|
||||
AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1.Default;
|
||||
private MovementTruthOutbound? _lastMovementTruthOutbound;
|
||||
private readonly AcDream.App.Physics.LocalPlayerShadowState _localPlayerShadow = new();
|
||||
|
||||
private readonly record struct MovementTruthOutbound(
|
||||
string Kind,
|
||||
uint Sequence,
|
||||
System.DateTime TimeUtc,
|
||||
System.Numerics.Vector3 LocalWorldPosition,
|
||||
uint LocalCellId,
|
||||
System.Numerics.Vector3 WirePosition,
|
||||
uint WireCellId,
|
||||
bool IsOnGround,
|
||||
byte ContactByte,
|
||||
System.Numerics.Vector3 Velocity);
|
||||
|
||||
// K-fix7 (2026-04-26): server-authoritative Run + Jump skill values
|
||||
// received from PlayerDescription. -1 = "not yet received, fall back
|
||||
// to the controller's default (env-var or hardcoded 200/300)".
|
||||
|
|
@ -577,36 +585,25 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase D.2b-C — live character-sheet assembly + raise flow (extracted
|
||||
// feature class; GameWindow only wires it). Null unless ACDREAM_RETAIL_UI=1.
|
||||
private AcDream.App.UI.Layout.CharacterSheetProvider? _characterSheetProvider;
|
||||
// K.1b: this field is RESERVED — written when entering / leaving player
|
||||
// mode and previously fed mouse-X into MovementInput.MouseDeltaX. Now
|
||||
// never consumed by MovementInput (mouse never drives character yaw —
|
||||
// K.1b regression-prevention). Kept around as plumbing for the future
|
||||
// K.2 MMB-mouse-look path which will re-enable mouse → character-yaw
|
||||
// when MMB is held. The pragma silences the dead-write warning until K.2
|
||||
// wires the read-side back in.
|
||||
#pragma warning disable CS0414 // assigned but never used — see comment above
|
||||
private float _playerMouseDeltaX;
|
||||
#pragma warning restore CS0414
|
||||
|
||||
// Mouse sensitivity multipliers — one per camera mode because the visual
|
||||
// feel is very different. Adjust via F8 / F9 for whichever mode is
|
||||
// currently active. Chase default is low because the character + camera
|
||||
// rotating together is overwhelming at fly speeds.
|
||||
private float _sensChase = 0.15f;
|
||||
private float _sensChase
|
||||
{
|
||||
get => _chaseCameraInput.Sensitivity;
|
||||
set => _chaseCameraInput.Sensitivity = value;
|
||||
}
|
||||
private float _sensFly = 1.0f;
|
||||
private float _sensOrbit = 1.0f;
|
||||
|
||||
// Right-mouse-button held → free-orbit the chase camera around the
|
||||
// player without turning the character. Release leaves the camera at
|
||||
// the orbited position (no snap back).
|
||||
private bool _rmbHeld;
|
||||
|
||||
// K-fix1 (2026-04-26): autorun is a TOGGLE — Press Q to start
|
||||
// forward-running, press Q again (or any movement-cancel key like
|
||||
// X / S / Backward / Forward) to stop. Mirrors retail's
|
||||
// AutoRun action. While true, MovementInput.Forward is forced
|
||||
// true regardless of W's state.
|
||||
private bool _autoRunActive;
|
||||
private bool _rmbHeld
|
||||
{
|
||||
get => _chaseCameraInput.RmbOrbitHeld;
|
||||
}
|
||||
|
||||
// Phase K.2 — auto-enter player mode after a successful login. Armed
|
||||
// by ApplyLiveSessionEnteredWorld; ticked from
|
||||
|
|
@ -617,21 +614,6 @@ public sealed class GameWindow : IDisposable
|
|||
// the bool here.
|
||||
private AcDream.App.Input.PlayerModeAutoEntry? _playerModeAutoEntry;
|
||||
|
||||
// Phase K.2 — MMB-hold instant mouse-look state. Live throughout
|
||||
// the session; flips Active on Press/Release. Defense-in-depth on
|
||||
// ImGui's WantCaptureMouse — the dispatcher already filters, but
|
||||
// OnWantCaptureMouseChanged also suspends the state if a panel
|
||||
// claims focus mid-hold.
|
||||
private AcDream.UI.Abstractions.Input.MouseLookState? _mouseLook;
|
||||
// Tracks the previous WantCaptureMouse value so we can fire the
|
||||
// changed-edge callback once per transition (vs every frame).
|
||||
private bool _lastWantCaptureMouse;
|
||||
// Cursor mode prior to entering MMB mouse-look. Restored on
|
||||
// release so the user lands back in the same camera mode as
|
||||
// before (raw for chase/fly, normal for orbit). Set non-null while
|
||||
// mouse-look is active.
|
||||
private Silk.NET.Input.CursorMode? _mouseLookSavedCursorMode;
|
||||
|
||||
// Phase K.1b — single input path. Every keyboard/mouse-button reaction
|
||||
// flows through InputDispatcher.Fired (see OnInputAction below) or
|
||||
// IsActionHeld (per-frame polling for movement). The legacy direct
|
||||
|
|
@ -641,6 +623,11 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Input.SilkKeyboardSource? _kbSource;
|
||||
private AcDream.App.Input.SilkMouseSource? _mouseSource;
|
||||
private AcDream.UI.Abstractions.Input.InputDispatcher? _inputDispatcher;
|
||||
private readonly AcDream.App.Input.RetainedUiInputCaptureSlot _retainedInputCapture;
|
||||
private readonly AcDream.App.Input.CompositeInputCaptureSource _inputCapture;
|
||||
private readonly AcDream.App.Input.DispatcherMovementInputSource _movementInput;
|
||||
private AcDream.App.Input.IMouseLookCursor? _mouseLookCursor;
|
||||
private AcDream.App.Input.GameplayInputFrameController? _gameplayInputFrame;
|
||||
// K.1c: load user-customized bindings from %LOCALAPPDATA%\acdream\keybinds.json,
|
||||
// falling back to the retail-faithful defaults if the file is missing
|
||||
// or corrupt. This is THE single source of truth for the keymap at
|
||||
|
|
@ -750,6 +737,12 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
|
||||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_retainedInputCapture = new AcDream.App.Input.RetainedUiInputCaptureSlot();
|
||||
_inputCapture = new AcDream.App.Input.CompositeInputCaptureSource(
|
||||
new AcDream.App.Input.DevToolsInputCaptureSource(options.DevTools),
|
||||
_retainedInputCapture);
|
||||
_movementInput = new AcDream.App.Input.DispatcherMovementInputSource(
|
||||
_inputCapture);
|
||||
_datDir = options.DatDir;
|
||||
_worldGameState = worldGameState;
|
||||
_worldEvents = worldEvents;
|
||||
|
|
@ -785,16 +778,17 @@ public sealed class GameWindow : IDisposable
|
|||
_liveEntities?.RebucketLiveEntity(serverGuid, landblockId),
|
||||
isCurrentVisibleProjection: IsCurrentVisibleLocalPlayerProjection,
|
||||
suspendShadow: SuspendLocalPlayerShadow);
|
||||
_movementTruthDiagnostics =
|
||||
new AcDream.App.Input.MovementTruthDiagnosticController(
|
||||
options.DumpMoveTruth,
|
||||
_playerControllerSlot,
|
||||
_localPlayerIdentity);
|
||||
_localPlayerOutbound = new AcDream.App.Input.LocalPlayerOutboundController(
|
||||
DumpMovementTruthOutbound);
|
||||
_movementTruthDiagnostics);
|
||||
_localPlayerFrame = new AcDream.App.Input.RetailLocalPlayerFrameController(
|
||||
canPresentPlayer: CanAdvanceLocalPlayer,
|
||||
getController: () => _playerController,
|
||||
captureInput: () =>
|
||||
{
|
||||
_playerMouseDeltaX = 0f;
|
||||
return CaptureMovementInput();
|
||||
},
|
||||
movementInput: _movementInput,
|
||||
resolveLocalEntityId: () =>
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var entity)
|
||||
? entity.Id
|
||||
|
|
@ -904,30 +898,16 @@ public sealed class GameWindow : IDisposable
|
|||
_kbSource = new AcDream.App.Input.SilkKeyboardSource(firstKb);
|
||||
_mouseSource = new AcDream.App.Input.SilkMouseSource(
|
||||
firstMouse,
|
||||
wantCaptureMouse: () => (DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse)
|
||||
|| (_uiHost?.Root.WantsMouse ?? false),
|
||||
wantCaptureKeyboard: () => (DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard)
|
||||
|| (_uiHost?.Root.WantsKeyboard ?? false));
|
||||
_mouseSource.ModifierProbe = () => _kbSource.CurrentModifiers;
|
||||
_inputCapture,
|
||||
_kbSource);
|
||||
_inputDispatcher = new AcDream.UI.Abstractions.Input.InputDispatcher(
|
||||
_kbSource, _mouseSource, _keyBindings);
|
||||
_movementInput.Bind(_inputDispatcher);
|
||||
_mouseLookCursor = new AcDream.App.Input.SilkMouseLookCursor(firstMouse);
|
||||
_inputDispatcher.Fired += OnInputAction;
|
||||
Combat.CombatModeChanged += SetInputCombatScope;
|
||||
SetInputCombatScope(Combat.CurrentMode);
|
||||
|
||||
// Retail CameraSet::ToggleMouseLook / Rotate (0x00457490 /
|
||||
// 0x00458310): the callback submits a filtered horizontal
|
||||
// adjustment to the player's MotionInterpreter. It must never
|
||||
// mutate visible Yaw directly; doing so leaves ACE's authoritative
|
||||
// heading stale and makes targeted attacks skip their server turn.
|
||||
_mouseLook = new AcDream.UI.Abstractions.Input.MouseLookState(
|
||||
applyHorizontalAdjustment: adjustment =>
|
||||
{
|
||||
_playerController?.SubmitMouseTurnAdjustment(
|
||||
adjustment,
|
||||
CaptureMovementInput());
|
||||
});
|
||||
|
||||
// Phase K.2 — auto-enter player mode after EnterWorld
|
||||
// succeeds. Predicates close over GameWindow state; the
|
||||
// entry callback flips into player mode via the same code
|
||||
|
|
@ -975,12 +955,12 @@ public sealed class GameWindow : IDisposable
|
|||
if (_playerMode && _cameraController.IsChaseMode && _chaseCamera is not null)
|
||||
{
|
||||
float sens = _sensChase;
|
||||
if (_mouseLook is not null && _mouseLook.Active)
|
||||
if (_gameplayInputFrame?.MouseLookActive == true)
|
||||
{
|
||||
// DirectInput may deliver several device records before
|
||||
// one retail GetInput pass. Accumulate raw motion here;
|
||||
// the update loop filters the aggregate exactly once.
|
||||
_mouseLook.QueueDelta(dx, dy);
|
||||
_gameplayInputFrame.QueueRawMouseDelta(dx, dy);
|
||||
}
|
||||
else if (_rmbHeld)
|
||||
{
|
||||
|
|
@ -1269,6 +1249,7 @@ public sealed class GameWindow : IDisposable
|
|||
// it fires, so the chase camera doesn't snap on top of
|
||||
// the fly camera mid-inspection.
|
||||
_debugVm.ToggleFlyMode = ToggleFlyOrChase;
|
||||
_combatFeedback.ViewModel = _debugVm;
|
||||
_debugPanel = new AcDream.UI.Abstractions.Panels.Debug.DebugPanel(_debugVm);
|
||||
_panelHost.Register(_debugPanel);
|
||||
|
||||
|
|
@ -1439,6 +1420,7 @@ public sealed class GameWindow : IDisposable
|
|||
_panelHost = null;
|
||||
_vitalsVm = null;
|
||||
_vitalsPanel = null;
|
||||
_combatFeedback.ViewModel = null;
|
||||
_debugVm = null;
|
||||
_debugPanel = null;
|
||||
_chatPanel = null;
|
||||
|
|
@ -1626,14 +1608,7 @@ public sealed class GameWindow : IDisposable
|
|||
// semantics even when the retail renderer is disabled.
|
||||
_combatAttackController = new AcDream.App.Combat.CombatAttackController(
|
||||
Combat,
|
||||
CanStartLiveCombatAttack,
|
||||
SendLiveCombatAttack,
|
||||
prepareAttackRequest: PreparePlayerForAttackRequest,
|
||||
sendCancelAttack: () => LiveSession?.SendCancelAttack(),
|
||||
isDualWield: () => _playerController?.Motion.InterpretedState.CurrentStyle
|
||||
== AcDream.Core.Combat.CombatInputPlanner.DualWieldCombatStyle,
|
||||
playerReadyForAttack: IsPlayerReadyForCombatAttack,
|
||||
autoRepeatAttack: () => _persistedGameplay.AutoRepeatAttack);
|
||||
_combatAttackOperations);
|
||||
_combatTargetController = new AcDream.App.Combat.CombatTargetController(
|
||||
Combat,
|
||||
_selection,
|
||||
|
|
@ -1702,6 +1677,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
_vitalsVm ??= new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat, LocalPlayer);
|
||||
_uiHost = new AcDream.App.UI.UiHost(_gl, shadersDir, _debugFont);
|
||||
_retainedInputCapture.Root = _uiHost.Root;
|
||||
_uiHost.Root.UiLocked = _persistedGameplay.LockUI;
|
||||
var cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(
|
||||
_itemInteractionController,
|
||||
|
|
@ -1732,7 +1708,7 @@ public sealed class GameWindow : IDisposable
|
|||
accountName: () => LiveSession?.Characters?.AccountName
|
||||
?? _options.LiveUser
|
||||
?? string.Empty,
|
||||
stopCompletely: PreparePlayerForAttackRequest,
|
||||
stopCompletely: _combatAttackOperations.PrepareAttackRequest,
|
||||
sendUntargeted: spellId => LiveSession?.SendCastUntargetedSpell(spellId),
|
||||
sendTargeted: (target, spellId) => LiveSession?.SendCastTargetedSpell(target, spellId),
|
||||
displayMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||
|
|
@ -2631,7 +2607,7 @@ public sealed class GameWindow : IDisposable
|
|||
() => LiveSession,
|
||||
PublishLocalPhysicsTimestamps,
|
||||
AimTeleportDestination,
|
||||
DumpMovementTruthServerEcho);
|
||||
_movementTruthDiagnostics);
|
||||
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
|
||||
_liveEntities,
|
||||
() => _playerServerGuid,
|
||||
|
|
@ -2652,6 +2628,41 @@ public sealed class GameWindow : IDisposable
|
|||
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
||||
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
|
||||
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
||||
_combatAttackOperations.Bind(
|
||||
new AcDream.App.Combat.LiveCombatAttackOperations(
|
||||
Combat,
|
||||
new AcDream.App.Combat.CombatAttackTargetSource(
|
||||
_selection,
|
||||
_liveEntities!,
|
||||
Objects,
|
||||
_localPlayerIdentity),
|
||||
_gameplaySettings,
|
||||
_playerControllerSlot,
|
||||
_localPlayerOutbound,
|
||||
_liveSessionController,
|
||||
_liveSessionController,
|
||||
_combatFeedback));
|
||||
AcDream.App.Input.MouseLookController? mouseLookController =
|
||||
_mouseSource is not null && _mouseLookCursor is not null
|
||||
? new AcDream.App.Input.MouseLookController(
|
||||
_mouseSource,
|
||||
_pointerPosition,
|
||||
_localPlayerMode,
|
||||
_playerControllerSlot,
|
||||
_cameraController!,
|
||||
_chaseCameraInput,
|
||||
_movementInput,
|
||||
_localPlayerOutbound,
|
||||
_liveSessionController,
|
||||
_mouseLookCursor,
|
||||
new AcDream.App.Input.EnvironmentInputMonotonicClock())
|
||||
: null;
|
||||
_gameplayInputFrame = new AcDream.App.Input.GameplayInputFrameController(
|
||||
_inputDispatcher,
|
||||
_movementInput,
|
||||
mouseLookController,
|
||||
new AcDream.App.Input.CombatAttackInputFrameAdapter(
|
||||
_combatAttackController!));
|
||||
_streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
|
||||
_options.LiveMode,
|
||||
_localPlayerMode,
|
||||
|
|
@ -2801,12 +2812,7 @@ public sealed class GameWindow : IDisposable
|
|||
});
|
||||
|
||||
private void ResetSessionMouseCapture()
|
||||
{
|
||||
bool wasActive = _mouseLook?.Active == true;
|
||||
_mouseLook?.Release();
|
||||
if (wasActive || _mouseLookSavedCursorMode.HasValue)
|
||||
RestoreCursorAfterMouseLook();
|
||||
}
|
||||
=> _gameplayInputFrame?.ResetSession();
|
||||
|
||||
private void ResetSessionPlayerPresentation()
|
||||
{
|
||||
|
|
@ -2822,10 +2828,7 @@ public sealed class GameWindow : IDisposable
|
|||
_playerHost = null;
|
||||
_chaseCamera = null;
|
||||
_retailChaseCamera = null;
|
||||
_playerMouseDeltaX = 0f;
|
||||
_rmbHeld = false;
|
||||
_autoRunActive = false;
|
||||
_lastMovementTruthOutbound = null;
|
||||
_movementTruthDiagnostics.ResetSession();
|
||||
_localPlayerShadow.Clear();
|
||||
_spawnClaimRangeMemo = null;
|
||||
}
|
||||
|
|
@ -3583,7 +3586,7 @@ public sealed class GameWindow : IDisposable
|
|||
|| !_teleportTransit.CanBegin(teleportSequence))
|
||||
return;
|
||||
|
||||
EndMouseLookAndRestoreCursor();
|
||||
_gameplayInputFrame?.EndMouseLook();
|
||||
// A fresh sequence is a new logical transit. Discard the prior
|
||||
// destination and presentation before this sequence can observe them;
|
||||
// its destination PositionUpdate may arrive on a later network tick.
|
||||
|
|
@ -3856,49 +3859,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Input callbacks feed the current object tick. Packets already read
|
||||
// by Core.Net remain queued until the retail object/physics phase has
|
||||
// consumed that input and published its final pose.
|
||||
_inputDispatcher?.Tick();
|
||||
|
||||
// Phase K.2 — mouse-look is an input source for this object's
|
||||
// movement tick, so sample it before the retail CPhysics/network
|
||||
// barrier just like held keyboard actions.
|
||||
if (_mouseLook is not null)
|
||||
{
|
||||
bool wantCaptureMouse = IsUiCapturingMouse();
|
||||
if (wantCaptureMouse != _lastWantCaptureMouse)
|
||||
{
|
||||
if (wantCaptureMouse && _mouseLook.Active)
|
||||
EndMouseLookAndRestoreCursor();
|
||||
_mouseLook.OnWantCaptureMouseChanged(wantCaptureMouse);
|
||||
_lastWantCaptureMouse = wantCaptureMouse;
|
||||
}
|
||||
|
||||
float nowSeconds = (float)(Environment.TickCount64 / 1000.0);
|
||||
if (_mouseLook.TryTakeRawSample(nowSeconds, out float rawX, out float rawY))
|
||||
{
|
||||
// GetInput synthesizes (0,0) only after >0.2 s idle.
|
||||
// MouseLookHandler stops drift before filtering that sample;
|
||||
// the filter tail may then start a smaller Rotate again.
|
||||
if (rawX == 0f && rawY == 0f)
|
||||
_playerController?.StopMouseDrift(CaptureMovementInput());
|
||||
|
||||
(float filteredX, float filteredY) =
|
||||
AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera
|
||||
&& _retailChaseCamera is not null
|
||||
? _retailChaseCamera.FilterMouseDelta(
|
||||
rawX,
|
||||
rawY,
|
||||
weight: 0.5f,
|
||||
nowSec: nowSeconds)
|
||||
: (rawX, rawY);
|
||||
_mouseLook.ApplyDelta(filteredX, _sensChase);
|
||||
if (_retailChaseCamera is not null)
|
||||
_retailChaseCamera.AdjustPitch(filteredY * 0.003f * _sensChase);
|
||||
else
|
||||
_chaseCamera?.AdjustPitch(filteredY * 0.003f * _sensChase);
|
||||
}
|
||||
}
|
||||
|
||||
_combatAttackController?.Tick();
|
||||
_gameplayInputFrame!.Tick(frameTiming);
|
||||
|
||||
// Drain pending live-session traffic AFTER streaming so any incoming
|
||||
// CreateObject events find their landblock already loaded in
|
||||
|
|
@ -4043,10 +4004,7 @@ public sealed class GameWindow : IDisposable
|
|||
// character yaw (regression-prevention per K.1b plan §D);
|
||||
// MouseDeltaX is hardcoded 0f. RMB held still pans the chase
|
||||
// camera (handled in the mouse-move handler via _rmbHeld).
|
||||
// The _playerMouseDeltaX field is preserved as plumbing for the
|
||||
// future MMB-mouse-look behavior coming back in K.2.
|
||||
if (_inputDispatcher is null) return;
|
||||
_playerMouseDeltaX = 0f; // defensive: ensure no leakage even if some path writes it
|
||||
|
||||
// Retail-style held-key offset integration. Only active when
|
||||
// retail chase is selected; legacy camera ignores these.
|
||||
|
|
@ -4069,7 +4027,7 @@ public sealed class GameWindow : IDisposable
|
|||
// walk speed.
|
||||
// * Q = AUTORUN TOGGLE: pressing Q latches forward-running
|
||||
// until Q is pressed again. Handled in OnInputAction; here
|
||||
// we just OR _autoRunActive into the Forward flag.
|
||||
// DispatcherMovementInputSource owns the autorun latch.
|
||||
// * Mouse never drives character yaw (K.1b regression-prevention).
|
||||
if (!_localPlayerFrame.TryGetPresentationAfterNetwork(out var playerFrame))
|
||||
return;
|
||||
|
|
@ -4128,82 +4086,12 @@ public sealed class GameWindow : IDisposable
|
|||
&& _inputDispatcher is not null
|
||||
&& !(DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard);
|
||||
|
||||
private void DumpMovementTruthOutbound(
|
||||
string kind,
|
||||
uint sequence,
|
||||
AcDream.App.Input.MovementResult result,
|
||||
System.Numerics.Vector3 wirePosition,
|
||||
uint wireCellId,
|
||||
byte contactByte)
|
||||
{
|
||||
if (!DumpMoveTruthEnabled) return;
|
||||
|
||||
var velocity = _playerController?.BodyVelocity ?? System.Numerics.Vector3.Zero;
|
||||
_lastMovementTruthOutbound = new MovementTruthOutbound(
|
||||
kind,
|
||||
sequence,
|
||||
System.DateTime.UtcNow,
|
||||
result.Position,
|
||||
result.CellId,
|
||||
wirePosition,
|
||||
wireCellId,
|
||||
result.IsOnGround,
|
||||
contactByte,
|
||||
velocity);
|
||||
|
||||
Console.WriteLine(System.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)}"));
|
||||
}
|
||||
|
||||
private void DumpMovementTruthServerEcho(
|
||||
AcDream.Core.Net.WorldSession.EntityPositionUpdate update,
|
||||
System.Numerics.Vector3 serverWorldPosition)
|
||||
{
|
||||
if (!DumpMoveTruthEnabled || update.Guid != _playerServerGuid) return;
|
||||
|
||||
var now = System.DateTime.UtcNow;
|
||||
var localPosition = _playerController?.Position;
|
||||
var localCellId = _playerController?.CellId;
|
||||
var deltaLocal = localPosition.HasValue
|
||||
? serverWorldPosition - localPosition.Value
|
||||
: (System.Numerics.Vector3?)null;
|
||||
|
||||
string localText = localPosition.HasValue ? Fmt(localPosition.Value) : "-";
|
||||
string localCellText = localCellId.HasValue
|
||||
? System.FormattableString.Invariant($"0x{localCellId.Value:X8}")
|
||||
: "-";
|
||||
string deltaLocalText = deltaLocal.HasValue ? Fmt(deltaLocal.Value) : "-";
|
||||
string deltaLocalLen = deltaLocal.HasValue
|
||||
? System.FormattableString.Invariant($"{deltaLocal.Value.Length():F3}")
|
||||
: "-";
|
||||
|
||||
string lastText = "-";
|
||||
if (_lastMovementTruthOutbound is { } last)
|
||||
{
|
||||
var deltaOut = serverWorldPosition - last.LocalWorldPosition;
|
||||
var ageMs = (now - last.TimeUtc).TotalMilliseconds;
|
||||
lastText = System.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 = _playerController?.State.ToString() ?? "-";
|
||||
string velocityText = update.Velocity.HasValue ? Fmt(update.Velocity.Value) : "-";
|
||||
|
||||
Console.WriteLine(System.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={deltaLocalLen} serverVel={velocityText} state={state} lastOut={lastText}"));
|
||||
}
|
||||
|
||||
private static string Fmt(System.Numerics.Vector3 v) =>
|
||||
System.FormattableString.Invariant($"({v.X:F3},{v.Y:F3},{v.Z:F3})");
|
||||
|
||||
private static string FmtCmd(uint? command) =>
|
||||
command.HasValue
|
||||
? System.FormattableString.Invariant($"0x{command.Value:X8}")
|
||||
: "-";
|
||||
|
||||
private void OnCameraModeChanged(bool _modeBool)
|
||||
{
|
||||
if (_mouseLook?.Active == true
|
||||
if (_gameplayInputFrame?.MouseLookActive == true
|
||||
&& _cameraController?.IsChaseMode != true)
|
||||
{
|
||||
EndMouseLookAndRestoreCursor();
|
||||
_gameplayInputFrame?.EndMouseLook();
|
||||
}
|
||||
|
||||
if (_input is null) return;
|
||||
|
|
@ -4218,8 +4106,7 @@ public sealed class GameWindow : IDisposable
|
|||
// the only correct gate. Cursor visible by default in chase /
|
||||
// orbit modes; Raw cursor only in fly mode (continuous
|
||||
// look-and-fly affordance). Mouse-look (raw mode) when MMB is
|
||||
// held is handled separately by HideCursorForMouseLook /
|
||||
// RestoreCursorAfterMouseLook.
|
||||
// held is handled separately by the gameplay input owner.
|
||||
bool needsRawCursor = _cameraController?.IsFlyMode == true;
|
||||
mouse.Cursor.CursorMode = needsRawCursor ? CursorMode.Raw : CursorMode.Normal;
|
||||
_capturedMouse = needsRawCursor ? mouse : null;
|
||||
|
|
@ -6805,8 +6692,13 @@ public sealed class GameWindow : IDisposable
|
|||
= AcDream.UI.Abstractions.Panels.Settings.DisplaySettings.Default;
|
||||
private AcDream.UI.Abstractions.Panels.Settings.AudioSettings _persistedAudio
|
||||
= AcDream.UI.Abstractions.Panels.Settings.AudioSettings.Default;
|
||||
private readonly AcDream.App.Combat.GameplaySettingsState
|
||||
_gameplaySettings = new();
|
||||
private AcDream.UI.Abstractions.Panels.Settings.GameplaySettings _persistedGameplay
|
||||
= AcDream.UI.Abstractions.Panels.Settings.GameplaySettings.Default;
|
||||
{
|
||||
get => _gameplaySettings.Value;
|
||||
set => _gameplaySettings.Value = value;
|
||||
}
|
||||
private AcDream.UI.Abstractions.Panels.Settings.ChatSettings _persistedChat
|
||||
= AcDream.UI.Abstractions.Panels.Settings.ChatSettings.Default;
|
||||
private AcDream.UI.Abstractions.Panels.Settings.CharacterSettings _persistedCharacter
|
||||
|
|
@ -7111,61 +7003,14 @@ public sealed class GameWindow : IDisposable
|
|||
// chords also fire Press on key-down + Release on key-up; we
|
||||
// ignore the in-between Hold ticks here (the mouse-move handler
|
||||
// checks _rmbHeld each frame anyway).
|
||||
if (action == AcDream.UI.Abstractions.Input.InputAction.AcdreamRmbOrbitHold)
|
||||
{
|
||||
if (activation == AcDream.UI.Abstractions.Input.ActivationType.Press)
|
||||
_rmbHeld = _playerMode && _cameraController?.IsChaseMode == true;
|
||||
else if (activation == AcDream.UI.Abstractions.Input.ActivationType.Release)
|
||||
_rmbHeld = false;
|
||||
if (_gameplayInputFrame?.HandlePointerAction(action, activation) == true)
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase K.2 — MMB-hold instant mouse-look. Press hides the
|
||||
// cursor + activates yaw drive; release restores. WantCapture
|
||||
// edge handling lives in OnUpdate; only Press needs to read it
|
||||
// for the initial gate (defense in depth — the dispatcher
|
||||
// already filters on WantCaptureMouse in OnMouseDown).
|
||||
if (action == AcDream.UI.Abstractions.Input.InputAction.CameraInstantMouseLook)
|
||||
{
|
||||
if (_mouseLook is null) return;
|
||||
if (activation == AcDream.UI.Abstractions.Input.ActivationType.Press)
|
||||
{
|
||||
if (!_playerMode
|
||||
|| _cameraController?.IsChaseMode != true
|
||||
|| _playerController is not { State: AcDream.App.Input.PlayerState.InWorld })
|
||||
return;
|
||||
|
||||
bool wcm = IsUiCapturingMouse();
|
||||
float nowSeconds = (float)(Environment.TickCount64 / 1000.0);
|
||||
_mouseLook.Press(_lastMouseX, _lastMouseY, wcm, nowSeconds);
|
||||
if (_mouseLook.Active)
|
||||
{
|
||||
if (_playerController is not { } controller
|
||||
|| !controller.BeginMouseLook(CaptureMovementInput()))
|
||||
{
|
||||
// Keep capture ownership atomic with the movement
|
||||
// transition. A portal/state change between the outer
|
||||
// gate and this call must not leave a hidden cursor
|
||||
// with no active movement owner.
|
||||
_mouseLook.Release();
|
||||
return;
|
||||
}
|
||||
|
||||
// ToggleMouseLook calls SendMovementEvent at the input
|
||||
// boundary. This also preserves down+up events that occur
|
||||
// before the next physics update.
|
||||
TrySendPlayerMovementEvent(
|
||||
controller.CaptureMovementResult(mouseLookEvent: false));
|
||||
HideCursorForMouseLook();
|
||||
}
|
||||
}
|
||||
else if (activation == AcDream.UI.Abstractions.Input.ActivationType.Release)
|
||||
{
|
||||
EndMouseLookAndRestoreCursor();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ScrollUp / ScrollDown — emit by InputDispatcher.OnScroll on every
|
||||
// wheel tick. Press is the only activation type for wheel.
|
||||
if (action == AcDream.UI.Abstractions.Input.InputAction.ScrollUp
|
||||
|
|
@ -7180,12 +7025,10 @@ public sealed class GameWindow : IDisposable
|
|||
// combat before the movement command enters CommandInterpreter. This
|
||||
// notification does not consume the input; the movement owner below
|
||||
// still receives it normally.
|
||||
_combatAttackController?.HandleMovementInput(action, activation);
|
||||
|
||||
// Retail attack actions consume their full transition stream: key-down
|
||||
// starts the power build and key-up commits it. Handle them before the
|
||||
// generic Press-only gate below.
|
||||
if (_combatAttackController?.HandleInputAction(action, activation) == true)
|
||||
if (_gameplayInputFrame?.HandleCombatAction(action, activation) == true)
|
||||
return;
|
||||
|
||||
// Every other action fires on Press only (no Release / Hold side-
|
||||
|
|
@ -7212,22 +7055,8 @@ public sealed class GameWindow : IDisposable
|
|||
// — retail-faithful: any deliberate movement input wins. (Pressing
|
||||
// Forward AGAIN does NOT cancel — retail's autorun stays active
|
||||
// even when you press W; the two stack.)
|
||||
if (action == AcDream.UI.Abstractions.Input.InputAction.MovementRunLock)
|
||||
{
|
||||
_autoRunActive = !_autoRunActive;
|
||||
if (_gameplayInputFrame?.HandlePressedMovementAction(action) == true)
|
||||
return;
|
||||
}
|
||||
if (_autoRunActive
|
||||
&& (action == AcDream.UI.Abstractions.Input.InputAction.MovementBackup
|
||||
|| action == AcDream.UI.Abstractions.Input.InputAction.MovementStop
|
||||
|| action == AcDream.UI.Abstractions.Input.InputAction.MovementStrafeLeft
|
||||
|| action == AcDream.UI.Abstractions.Input.InputAction.MovementStrafeRight))
|
||||
{
|
||||
_autoRunActive = false;
|
||||
// Fall through — these actions still need their normal handling
|
||||
// (e.g. Stop is currently a no-op in the switch, but keeping the
|
||||
// fall-through means future logic fires).
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
|
|
@ -7313,7 +7142,7 @@ public sealed class GameWindow : IDisposable
|
|||
_cameraController.ToggleFly(); // exit fly, release cursor
|
||||
else if (_playerMode)
|
||||
{
|
||||
EndMouseLookAndRestoreCursor();
|
||||
_gameplayInputFrame?.EndMouseLook();
|
||||
_playerMode = false;
|
||||
_cameraController?.ExitChaseMode();
|
||||
_playerController = null;
|
||||
|
|
@ -7347,97 +7176,10 @@ public sealed class GameWindow : IDisposable
|
|||
_debugVm?.AddToast(text);
|
||||
}
|
||||
|
||||
private bool IsPlayerReadyForCombatAttack()
|
||||
{
|
||||
if (_playerController is null)
|
||||
return false;
|
||||
var motion = _playerController.Motion.InterpretedState;
|
||||
return AcDream.Core.Combat.CombatInputPlanner.PlayerInReadyPositionForAttack(
|
||||
Combat.CurrentMode,
|
||||
motion.CurrentStyle,
|
||||
motion.ForwardCommand);
|
||||
}
|
||||
|
||||
private bool CanStartLiveCombatAttack()
|
||||
{
|
||||
if (_liveSessionController?.IsInWorld != true)
|
||||
return false;
|
||||
|
||||
if (!AcDream.Core.Combat.CombatInputPlanner.SupportsTargetedAttack(Combat.CurrentMode))
|
||||
{
|
||||
_debugVm?.AddToast("Enter melee or missile combat first");
|
||||
Console.WriteLine("combat: attack ignored; not in melee/missile combat mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint? target = GetSelectedOrClosestCombatTarget();
|
||||
if (target is null)
|
||||
{
|
||||
_debugVm?.AddToast("No monster target");
|
||||
Console.WriteLine("combat: attack ignored; no creature target found");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SendLiveCombatAttack(
|
||||
AcDream.Core.Combat.AttackHeight height,
|
||||
float power)
|
||||
{
|
||||
if (!CanStartLiveCombatAttack())
|
||||
return false;
|
||||
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (session is null)
|
||||
return false;
|
||||
|
||||
uint target = _selection.SelectedObjectId!.Value;
|
||||
power = Math.Clamp(power, 0f, 1f);
|
||||
if (Combat.CurrentMode == AcDream.Core.Combat.CombatMode.Missile)
|
||||
{
|
||||
session.SendMissileAttack(target, height, power);
|
||||
Console.WriteLine($"combat: missile attack target=0x{target:X8} height={height} accuracy={power:F2}");
|
||||
}
|
||||
else
|
||||
{
|
||||
session.SendMeleeAttack(target, height, power);
|
||||
Console.WriteLine($"combat: melee attack target=0x{target:X8} height={height} power={power:F2}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>ClientCombatSystem::StartAttackRequest</c> (0x0056C040)
|
||||
/// calls <c>MaybeStopCompletely</c>, whose successful path sends the
|
||||
/// stopped movement state synchronously. Doing this in the host preserves
|
||||
/// wire ordering: ACE sees the final player heading before the later
|
||||
/// targeted attack request.
|
||||
/// </summary>
|
||||
private void PreparePlayerForAttackRequest()
|
||||
{
|
||||
if (_playerController is not { } controller
|
||||
|| !controller.PrepareForAttackRequest())
|
||||
return;
|
||||
|
||||
TrySendPlayerMovementEvent(
|
||||
controller.CaptureMovementResult(mouseLookEvent: false));
|
||||
}
|
||||
|
||||
internal static AcDream.Core.Physics.RawMotionState BuildOutboundRawMotionState(
|
||||
AcDream.App.Input.MovementResult result) =>
|
||||
AcDream.App.Input.LocalPlayerOutboundController.BuildRawMotionState(result);
|
||||
|
||||
private bool TrySendPlayerMovementEvent(AcDream.App.Input.MovementResult result)
|
||||
=> _localPlayerOutbound.TrySendMovement(
|
||||
LiveSession,
|
||||
_playerController,
|
||||
result);
|
||||
|
||||
private uint? GetSelectedOrClosestCombatTarget()
|
||||
=> _selectionInteractions?.GetSelectedOrClosestCombatTarget(
|
||||
_persistedGameplay.AutoTarget);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves retail's combat-camera target. ClientCombatSystem::
|
||||
/// UpdateTargetTracking (0x0056A950) enables CameraSet::TrackTarget only
|
||||
|
|
@ -7531,12 +7273,11 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
EndMouseLookAndRestoreCursor();
|
||||
_gameplayInputFrame?.EndMouseLook();
|
||||
_cameraController?.ExitChaseMode();
|
||||
_playerController = null;
|
||||
_chaseCamera = null;
|
||||
_retailChaseCamera = null;
|
||||
_playerMouseDeltaX = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8009,11 +7750,6 @@ public sealed class GameWindow : IDisposable
|
|||
Aspect = _window!.Size.X / (float)_window.Size.Y,
|
||||
CollisionProbe = new AcDream.App.Rendering.PhysicsCameraCollisionProbe(_physicsEngine),
|
||||
};
|
||||
// K.1b: _playerMouseDeltaX is no longer consumed by
|
||||
// MovementInput, but we still reset it here so any stale
|
||||
// accumulated value from a previous session doesn't leak
|
||||
// into a future code path that re-enables mouse-yaw.
|
||||
_playerMouseDeltaX = 0f;
|
||||
_cameraController?.EnterChaseMode(_chaseCamera, _retailChaseCamera);
|
||||
// K-fix1 (2026-04-26): latch the "we have entered chase at least
|
||||
// once" flag so the live-mode pre-login render gate stops
|
||||
|
|
@ -8024,103 +7760,6 @@ public sealed class GameWindow : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Both presentation stacks participate in the same gameplay-input gate.
|
||||
/// A retained retail panel capturing the pointer must suspend instant
|
||||
/// mouse-look just as an ImGui developer panel does.
|
||||
/// </summary>
|
||||
private bool IsUiCapturingMouse()
|
||||
=> (DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse)
|
||||
|| (_uiHost?.Root.WantsMouse ?? false);
|
||||
|
||||
private AcDream.App.Input.MovementInput CaptureMovementInput()
|
||||
{
|
||||
if (_inputDispatcher is null)
|
||||
return default;
|
||||
|
||||
bool walking = _inputDispatcher.IsActionHeld(
|
||||
AcDream.UI.Abstractions.Input.InputAction.MovementWalkMode);
|
||||
bool forward = _inputDispatcher.IsActionHeld(
|
||||
AcDream.UI.Abstractions.Input.InputAction.MovementForward);
|
||||
return new AcDream.App.Input.MovementInput(
|
||||
Forward: forward || _autoRunActive,
|
||||
Backward: _inputDispatcher.IsActionHeld(
|
||||
AcDream.UI.Abstractions.Input.InputAction.MovementBackup),
|
||||
StrafeLeft: _inputDispatcher.IsActionHeld(
|
||||
AcDream.UI.Abstractions.Input.InputAction.MovementStrafeLeft),
|
||||
StrafeRight: _inputDispatcher.IsActionHeld(
|
||||
AcDream.UI.Abstractions.Input.InputAction.MovementStrafeRight),
|
||||
TurnLeft: _inputDispatcher.IsActionHeld(
|
||||
AcDream.UI.Abstractions.Input.InputAction.MovementTurnLeft),
|
||||
TurnRight: _inputDispatcher.IsActionHeld(
|
||||
AcDream.UI.Abstractions.Input.InputAction.MovementTurnRight),
|
||||
Run: !walking,
|
||||
Jump: _inputDispatcher.IsActionHeld(
|
||||
AcDream.UI.Abstractions.Input.InputAction.MovementJump));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single idempotent teardown for every mouse-look exit path. Retail sends
|
||||
/// the stopped movement state synchronously; doing so before nulling the
|
||||
/// player controller preserves ACE's final authoritative heading.
|
||||
/// </summary>
|
||||
private void EndMouseLookAndRestoreCursor()
|
||||
{
|
||||
bool stateWasActive = _mouseLook?.Active == true;
|
||||
_mouseLook?.Release();
|
||||
|
||||
if (_playerController is { } controller
|
||||
&& controller.EndMouseLook(CaptureMovementInput()))
|
||||
{
|
||||
TrySendPlayerMovementEvent(
|
||||
controller.CaptureMovementResult(mouseLookEvent: false));
|
||||
}
|
||||
|
||||
if (stateWasActive || _mouseLookSavedCursorMode.HasValue)
|
||||
RestoreCursorAfterMouseLook();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase K.2: hide the system cursor while MMB instant mouse-look is
|
||||
/// held. Saves the previous CursorMode so <see cref="RestoreCursorAfterMouseLook"/>
|
||||
/// can put it back exactly. Skips when no mouse / no input — tests
|
||||
/// and headless runs stay clean.
|
||||
/// </summary>
|
||||
private void HideCursorForMouseLook()
|
||||
{
|
||||
if (_input is null) return;
|
||||
var mouse = _input.Mice.FirstOrDefault();
|
||||
if (mouse is null) return;
|
||||
// Save previous mode (Normal in orbit, Raw in chase/fly) so the
|
||||
// exact pre-hold mode is restored on release.
|
||||
_mouseLookSavedCursorMode = mouse.Cursor.CursorMode;
|
||||
mouse.Cursor.CursorMode = CursorMode.Hidden;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase K.2: restore the saved cursor mode after MMB instant
|
||||
/// mouse-look ends. Called from the Release branch and from the
|
||||
/// WantCaptureMouse-edge suspend path so the cursor never gets
|
||||
/// stuck hidden.
|
||||
/// </summary>
|
||||
private void RestoreCursorAfterMouseLook()
|
||||
{
|
||||
if (_input is null) return;
|
||||
var mouse = _input.Mice.FirstOrDefault();
|
||||
if (mouse is null) return;
|
||||
if (_mouseLookSavedCursorMode is { } saved)
|
||||
{
|
||||
mouse.Cursor.CursorMode = saved;
|
||||
_mouseLookSavedCursorMode = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Defense in depth: never observed the saved value, fall
|
||||
// back to Normal so the user always gets a visible cursor.
|
||||
mouse.Cursor.CursorMode = CursorMode.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// K.1b: F8/F9 sensitivity adjust extracted into a helper. Multiplies
|
||||
/// the currently-active mode's sensitivity (chase / fly / orbit) by the
|
||||
|
|
@ -8389,11 +8028,12 @@ public sealed class GameWindow : IDisposable
|
|||
]),
|
||||
new ResourceShutdownStage("session dependents",
|
||||
[
|
||||
new("mouse capture", EndMouseLookAndRestoreCursor),
|
||||
new("mouse capture", () => _gameplayInputFrame?.EndMouseLook()),
|
||||
new("retail UI", () =>
|
||||
{
|
||||
_retailUiRuntime?.Dispose();
|
||||
_retailUiRuntime = null;
|
||||
_retainedInputCapture.Root = null;
|
||||
_uiHost = null;
|
||||
}),
|
||||
new("combat target", () =>
|
||||
|
|
@ -8540,7 +8180,7 @@ public sealed class GameWindow : IDisposable
|
|||
private void OnFocusChanged(bool focused)
|
||||
{
|
||||
if (!focused)
|
||||
EndMouseLookAndRestoreCursor();
|
||||
_gameplayInputFrame?.EndMouseLook();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue