using System; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Config; using Dalamud.Hooking; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Common.Math; using Microsoft.Extensions.Logging; namespace LLib.Movement; public sealed class MovementOverrideHook : IDisposable { private unsafe delegate bool RMIWalkIsInputEnabledDelegate(void* self); private unsafe delegate void RMIWalkDelegate(void* self, float* sumLeft, float* sumForward, float* sumTurnLeft, byte* haveBackwardOrStrafe, byte* a6, byte bAdditiveUnk); private unsafe delegate void RMIFlyDelegate(void* self, PlayerMoveControllerFlyInput* result); private unsafe delegate byte MoveControlIsInputActiveDelegate(void* self, byte inputSourceFlags); private const float DefaultPrecision = 0.1f; private const float StallDistanceThreshold = 0.3f; private const long StallTimeThresholdMs = 500L; private const long StallCooldownMs = 500L; private const long StallContinuityGapMs = 250L; private const long UserInputFreshnessMs = 250L; private const float MisdirectionThresholdRad = (float)Math.PI / 4f; private static readonly uint[] MisdirectionStatusIds = new uint[4] { 1422u, 2936u, 3694u, 3909u }; private readonly ILogger _logger; private readonly IObjectTable _objectTable; private readonly IGameConfig _gameConfig; private RMIWalkIsInputEnabledDelegate? _isInputEnabled1; private RMIWalkIsInputEnabledDelegate? _isInputEnabled2; private Hook? _rmiWalkHook; private Hook? _rmiFlyHook; private Hook? _mcIsInputActiveHook; private unsafe float* _forcedMovementDirection; private bool? _forcedControlState; private System.Numerics.Vector3? _desiredDirection; private System.Numerics.Vector3? _desiredPosition; private System.Numerics.Vector3? _desiredFlyPosition; private System.Numerics.Vector3 _stallCheckPosition; private long _stallCheckStartMs; private long _stallCooldownUntilMs; private long _lastDriveMs; private float _flyStallBestDist; private long _flyStallImprovementMs; private long _flyStallCooldownUntilMs; private long _flyLastDriveMs; private bool _legacyMode; private long _lastUserInputAtMs; public Func? IsExternalPathfindingActive { get; set; } public bool IsAvailable { get; private set; } public bool IsActive { get; private set; } public bool UserInputDetected => Environment.TickCount64 - _lastUserInputAtMs < 250; public float Precision { get; set; } = 0.1f; public System.Numerics.Vector3? DesiredDirection { get { return _desiredDirection; } set { if (!(_desiredDirection == value)) { _desiredDirection = value; if (value.HasValue) { _desiredPosition = null; } ResetStallTracking(); } } } public System.Numerics.Vector3? DesiredPosition { get { return _desiredPosition; } set { if (!(_desiredPosition == value)) { _desiredPosition = value; if (value.HasValue) { _desiredDirection = null; } ResetStallTracking(); } } } public System.Numerics.Vector3? DesiredFlyPosition { get { return _desiredFlyPosition; } set { if (!(_desiredFlyPosition == value)) { _desiredFlyPosition = value; ResetFlyStallTracking(); } } } public MovementOverrideHook(ILogger logger, IObjectTable objectTable, IGameConfig gameConfig, ISigScanner sigScanner, IGameInteropProvider interop, bool enabled = true) { _logger = logger; _objectTable = objectTable; _gameConfig = gameConfig; _gameConfig.UiControlChanged += OnGameConfigChanged; UpdateLegacyMode(); TryInstallHook(sigScanner, interop, enabled); } private unsafe void TryInstallHook(ISigScanner sigScanner, IGameInteropProvider interop, bool enabled) { if (!enabled) { _logger.LogInformation("MovementOverrideHook is disabled - skipping hook install"); IsAvailable = false; return; } try { nint num = sigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 38 43 3C"); _isInputEnabled1 = Marshal.GetDelegateForFunctionPointer(num); nint num2 = sigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 75 03 88 47 3F"); _isInputEnabled2 = Marshal.GetDelegateForFunctionPointer(num2); _rmiWalkHook = interop.HookFromSignature("E8 ?? ?? ?? ?? 80 7B 3E 00 48 8D 3D", RMIWalkDetour); _rmiWalkHook.Enable(); _rmiFlyHook = interop.HookFromSignature("E8 ?? ?? ?? ?? 0F B6 0D ?? ?? ?? ?? B8", RMIFlyDetour); _rmiFlyHook.Enable(); IsAvailable = true; _logger.LogInformation("MovementOverrideHook installed: RMIWalk=0x{Walk:X}, RMIFly=0x{Fly:X}, IsEnabled1=0x{E1:X}, IsEnabled2=0x{E2:X}", _rmiWalkHook.Address, _rmiFlyHook.Address, num, num2); } catch (Exception ex) { if (IsTrampolineExhaustion(ex)) { _logger.LogWarning(ex, "Failed to install MovementOverrideHook: the hook backend found the function but could not place a trampoline near it. That space fills up over repeated plugin reloads in one game session and is never returned - restart the game client to clear it. Fast movement override is unavailable until then - callers will fall back to navmesh."); } else { _logger.LogWarning(ex, "Failed to install MovementOverrideHook (signature scan or hook creation failed). Fast movement override is unavailable - callers will fall back to navmesh. This usually means the game was patched; update the plugin when a new version is available."); } IsAvailable = false; _isInputEnabled1 = null; _isInputEnabled2 = null; _rmiWalkHook?.Dispose(); _rmiWalkHook = null; _rmiFlyHook?.Dispose(); _rmiFlyHook = null; return; } try { nint num3 = (nint)(_forcedMovementDirection = (float*)sigScanner.GetStaticAddressFromSig("F3 0F 11 0D ?? ?? ?? ?? 48 85 DB")); _mcIsInputActiveHook = interop.HookFromSignature("E8 ?? ?? ?? ?? 84 C0 74 09 84 DB 74 1A", MCIsInputActiveDetour); _mcIsInputActiveHook.Enable(); _logger.LogInformation("MovementOverrideHook misdirection primitives installed: MCIsInputActive=0x{Mc:X}, ForcedMovementDirection=0x{Fmd:X}", _mcIsInputActiveHook.Address, num3); } catch (Exception exception) { _logger.LogWarning(exception, "Failed to install MovementOverrideHook misdirection primitives (MCIsInputActive / ForcedMovementDirection). Core movement override is still active; the detour will bail during misdirection mechanics rather than drive through the input rotation."); _forcedMovementDirection = null; _mcIsInputActiveHook?.Dispose(); _mcIsInputActiveHook = null; } } private static bool IsTrampolineExhaustion(Exception ex) { if (!ex.Message.Contains("MemoryBuffer", StringComparison.Ordinal)) { return ex.Message.Contains("memory location", StringComparison.OrdinalIgnoreCase); } return true; } private unsafe byte MCIsInputActiveDetour(void* self, byte inputSourceFlags) { if (_forcedControlState.HasValue) { return _forcedControlState.Value ? ((byte)1) : ((byte)0); } return _mcIsInputActiveHook.Original(self, inputSourceFlags); } private unsafe void RMIFlyDetour(void* self, PlayerMoveControllerFlyInput* result) { _rmiFlyHook.Original(self, result); bool flag = result->Forward != 0f || result->Left != 0f || result->Up != 0f; if (flag) { _lastUserInputAtMs = Environment.TickCount64; } if (!IsAvailable || flag) { return; } Func? isExternalPathfindingActive = IsExternalPathfindingActive; if (isExternalPathfindingActive != null && isExternalPathfindingActive()) { return; } IPlayerCharacter localPlayer = _objectTable.LocalPlayer; if (localPlayer == null) { return; } System.Numerics.Vector3? desiredFlyPosition = _desiredFlyPosition; if (!desiredFlyPosition.HasValue) { return; } System.Numerics.Vector3 valueOrDefault = desiredFlyPosition.GetValueOrDefault(); System.Numerics.Vector3 position = localPlayer.Position; System.Numerics.Vector3 vector = valueOrDefault - position; float num = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z; if (num < Precision * Precision) { _desiredFlyPosition = null; ResetFlyStallTracking(); return; } float num3; if (_legacyMode) { float? num2 = TryGetCameraAzimuth(); if (!num2.HasValue) { return; } num3 = num2.Value + (float)Math.PI; } else { num3 = localPlayer.Rotation; } long tickCount = Environment.TickCount64; if (tickCount >= _flyStallCooldownUntilMs) { float num4 = MathF.Sqrt(num); if (_flyStallImprovementMs == 0L || tickCount - _flyLastDriveMs > 250 || num4 < _flyStallBestDist - 0.3f) { _flyStallBestDist = num4; _flyStallImprovementMs = tickCount; } else if (tickCount - _flyStallImprovementMs >= 500) { _logger.LogDebug("MovementOverride fly watchdog tripped: no progress toward target ({Dist:F2}y away) within {Elapsed}ms, clearing", num4, tickCount - _flyStallImprovementMs); _desiredFlyPosition = null; ResetFlyStallTracking(); _flyStallCooldownUntilMs = tickCount + 500; return; } _flyLastDriveMs = tickCount; float x = MathF.Atan2(vector.X, vector.Z) - num3; result->Forward = MathF.Cos(x); result->Left = MathF.Sin(x); float x2 = MathF.Sqrt(vector.X * vector.X + vector.Z * vector.Z); result->Up = MathF.Atan2(vector.Y, x2); } } private static bool PlayerHasMisdirection(IPlayerCharacter player) { StatusList statusList = player.StatusList; for (int i = 0; i < statusList.Length; i++) { IStatus status = statusList[i]; if (status == null) { continue; } uint statusId = status.StatusId; for (int j = 0; j < MisdirectionStatusIds.Length; j++) { if (statusId == MisdirectionStatusIds[j]) { return true; } } } return false; } private unsafe void RMIWalkDetour(void* self, float* sumLeft, float* sumForward, float* sumTurnLeft, byte* haveBackwardOrStrafe, byte* a6, byte bAdditiveUnk) { if (bAdditiveUnk == 0) { _forcedControlState = null; } _rmiWalkHook.Original(self, sumLeft, sumForward, sumTurnLeft, haveBackwardOrStrafe, a6, bAdditiveUnk); if (bAdditiveUnk != 0) { return; } bool flag = *sumLeft != 0f || *sumForward != 0f; if (flag) { _lastUserInputAtMs = Environment.TickCount64; } if (!IsAvailable) { IsActive = false; return; } if (flag) { IsActive = false; return; } Func? isExternalPathfindingActive = IsExternalPathfindingActive; if (isExternalPathfindingActive != null && isExternalPathfindingActive()) { IsActive = false; return; } if (_isInputEnabled1 == null || _isInputEnabled2 == null) { IsActive = false; return; } if (!_isInputEnabled1(self) || !_isInputEnabled2(self)) { IsActive = false; return; } IPlayerCharacter localPlayer = _objectTable.LocalPlayer; if (localPlayer == null) { IsActive = false; return; } System.Numerics.Vector3 position = localPlayer.Position; System.Numerics.Vector3? vector = ComputeDesiredWorldDirection(position); if (!vector.HasValue) { IsActive = false; return; } long tickCount = Environment.TickCount64; if (tickCount < _stallCooldownUntilMs) { IsActive = false; return; } float referenceRotation; if (_legacyMode) { float? num = TryGetCameraAzimuth(); if (!num.HasValue) { IsActive = false; return; } referenceRotation = num.Value + (float)Math.PI; } else { referenceRotation = localPlayer.Rotation; } var (num2, num3) = WorldToPlayerRelative(vector.Value, referenceRotation); if (PlayerHasMisdirection(localPlayer)) { if (_forcedMovementDirection == null || _mcIsInputActiveHook == null) { IsActive = false; return; } float num4 = MathF.Atan2(vector.Value.X, vector.Value.Z); float forcedMovementDirection = *_forcedMovementDirection; float num5; for (num5 = num4 - forcedMovementDirection; num5 > (float)Math.PI; num5 -= (float)Math.PI * 2f) { } for (; num5 < -(float)Math.PI; num5 += (float)Math.PI * 2f) { } bool flag2 = MathF.Abs(num5) <= (float)Math.PI / 4f; _forcedControlState = flag2; if (!flag2) { *sumLeft = 0f; *sumForward = 0f; IsActive = false; return; } } if (_stallCheckStartMs == 0L || tickCount - _lastDriveMs > 250) { _stallCheckPosition = position; _stallCheckStartMs = tickCount; } else if (tickCount - _stallCheckStartMs >= 500) { float num6 = position.X - _stallCheckPosition.X; float num7 = position.Z - _stallCheckPosition.Z; float num8 = num6 * num6 + num7 * num7; if (num8 < 0.09f) { _logger.LogDebug("MovementOverride stall watchdog tripped: player moved {Dist:F2}y over {Elapsed}ms, clearing", MathF.Sqrt(num8), tickCount - _stallCheckStartMs); _desiredDirection = null; _desiredPosition = null; ResetStallTracking(); _stallCooldownUntilMs = tickCount + 500; _forcedControlState = null; IsActive = false; return; } _stallCheckPosition = position; _stallCheckStartMs = tickCount; } _lastDriveMs = tickCount; *sumLeft = num2; *sumForward = num3; IsActive = true; } private void OnGameConfigChanged(object? sender, ConfigChangeEvent evt) { UpdateLegacyMode(); } private void UpdateLegacyMode() { uint value; bool flag = _gameConfig.UiControl.TryGetUInt("MoveMode", out value) && value == 1; if (flag != _legacyMode) { _legacyMode = flag; _logger.LogInformation("Legacy movement mode is now {State}", flag ? "enabled" : "disabled"); } } private unsafe static float? TryGetCameraAzimuth() { CameraManager* ptr = CameraManager.Instance(); if (ptr == null) { return null; } FFXIVClientStructs.FFXIV.Client.Game.Camera* activeCamera = ptr->GetActiveCamera(); if (activeCamera == null) { return null; } FFXIVClientStructs.FFXIV.Client.Graphics.Render.Camera* renderCamera = activeCamera->SceneCamera.RenderCamera; if (renderCamera == null) { return null; } FFXIVClientStructs.FFXIV.Common.Math.Matrix4x4 viewMatrix = renderCamera->ViewMatrix; return MathF.Atan2(viewMatrix.M13, viewMatrix.M33); } public void Reset() { _desiredDirection = null; _desiredPosition = null; _desiredFlyPosition = null; ResetStallTracking(); ResetFlyStallTracking(); _stallCooldownUntilMs = 0L; _flyStallCooldownUntilMs = 0L; _forcedControlState = null; IsActive = false; } private void ResetStallTracking() { _stallCheckPosition = default(System.Numerics.Vector3); _stallCheckStartMs = 0L; _lastDriveMs = 0L; } private void ResetFlyStallTracking() { _flyStallBestDist = 0f; _flyStallImprovementMs = 0L; _flyLastDriveMs = 0L; } private System.Numerics.Vector3? ComputeDesiredWorldDirection(System.Numerics.Vector3 playerPosition) { System.Numerics.Vector3? desiredDirection = _desiredDirection; if (desiredDirection.HasValue) { System.Numerics.Vector3 valueOrDefault = desiredDirection.GetValueOrDefault(); float num = valueOrDefault.X * valueOrDefault.X + valueOrDefault.Z * valueOrDefault.Z; if (num < 1E-08f) { return null; } float num2 = MathF.Sqrt(num); return new System.Numerics.Vector3(valueOrDefault.X / num2, 0f, valueOrDefault.Z / num2); } desiredDirection = _desiredPosition; if (desiredDirection.HasValue) { System.Numerics.Vector3 valueOrDefault2 = desiredDirection.GetValueOrDefault(); float num3 = valueOrDefault2.X - playerPosition.X; float num4 = valueOrDefault2.Z - playerPosition.Z; float num5 = num3 * num3 + num4 * num4; if (num5 < Precision * Precision) { _desiredPosition = null; ResetStallTracking(); return null; } float num6 = MathF.Sqrt(num5); return new System.Numerics.Vector3(num3 / num6, 0f, num4 / num6); } return null; } private static (float left, float forward) WorldToPlayerRelative(System.Numerics.Vector3 worldDir, float referenceRotation) { float num = MathF.Sin(referenceRotation); float num2 = MathF.Cos(referenceRotation); float item = worldDir.X * num + worldDir.Z * num2; return (left: worldDir.X * num2 - worldDir.Z * num, forward: item); } public void Dispose() { _gameConfig.UiControlChanged -= OnGameConfigChanged; _mcIsInputActiveHook?.Disable(); _mcIsInputActiveHook?.Dispose(); _mcIsInputActiveHook = null; _rmiFlyHook?.Disable(); _rmiFlyHook?.Dispose(); _rmiFlyHook = null; _rmiWalkHook?.Disable(); _rmiWalkHook?.Dispose(); _rmiWalkHook = null; } }