589 lines
16 KiB
C#
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;
|
|
}
|
|
}
|