qstbak/LLib/LLib.Movement/MovementOverrideHook.cs
2026-08-17 20:29:32 +10:00

589 lines
16 KiB
C#

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<MovementOverrideHook> _logger;
private readonly IObjectTable _objectTable;
private readonly IGameConfig _gameConfig;
private RMIWalkIsInputEnabledDelegate? _isInputEnabled1;
private RMIWalkIsInputEnabledDelegate? _isInputEnabled2;
private Hook<RMIWalkDelegate>? _rmiWalkHook;
private Hook<RMIFlyDelegate>? _rmiFlyHook;
private Hook<MoveControlIsInputActiveDelegate>? _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<bool>? 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<MovementOverrideHook> 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<RMIWalkIsInputEnabledDelegate>(num);
nint num2 = sigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 75 03 88 47 3F");
_isInputEnabled2 = Marshal.GetDelegateForFunctionPointer<RMIWalkIsInputEnabledDelegate>(num2);
_rmiWalkHook = interop.HookFromSignature<RMIWalkDelegate>("E8 ?? ?? ?? ?? 80 7B 3E 00 48 8D 3D", RMIWalkDetour);
_rmiWalkHook.Enable();
_rmiFlyHook = interop.HookFromSignature<RMIFlyDelegate>("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<MoveControlIsInputActiveDelegate>("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<bool>? 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<bool>? 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;
}
}