using System; using System.Numerics; using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Common.Math; using Microsoft.Extensions.Logging; using Questionable.Data; using Questionable.Functions; namespace Questionable.Controller.Steps.Common; internal static class Mount { internal sealed record MountTask : ITask { public ushort TerritoryId { get; init; } public EMountIf MountIf { get; init; } public FFXIVClientStructs.FFXIV.Common.Math.Vector3? Position { get; } public MountTask(ushort TerritoryId, EMountIf MountIf, FFXIVClientStructs.FFXIV.Common.Math.Vector3? Position = null) { this.TerritoryId = TerritoryId; this.MountIf = MountIf; this.Position = ((MountIf == EMountIf.AwayFromPosition) ? new FFXIVClientStructs.FFXIV.Common.Math.Vector3?(Position ?? throw new ArgumentNullException("Position")) : ((FFXIVClientStructs.FFXIV.Common.Math.Vector3?)null)); base._002Ector(); } public bool ShouldRedoOnInterrupt() { return true; } public override string ToString() { return "Mount"; } [CompilerGenerated] public void Deconstruct(out ushort TerritoryId, out EMountIf MountIf, out FFXIVClientStructs.FFXIV.Common.Math.Vector3? Position) { TerritoryId = this.TerritoryId; MountIf = this.MountIf; Position = this.Position; } } internal sealed class MountEvaluator(GameFunctions gameFunctions, ICondition condition, TerritoryData territoryData, IClientState clientState, IObjectTable objectTable, ILogger logger) { public unsafe MountResult EvaluateMountState(MountTask task, bool dryRun, ref long retryAtMs) { if (condition[ConditionFlag.Mounted]) { return MountResult.DontMount; } LogLevel logLevel = ((!dryRun) ? LogLevel.Debug : LogLevel.None); if (!territoryData.CanUseMount(task.TerritoryId)) { logger.Log(logLevel, "Can't use mount in current territory {Id}", task.TerritoryId); return MountResult.DontMount; } if (gameFunctions.HasStatusPreventingMount()) { logger.Log(logLevel, "Can't mount due to status preventing sprint or mount"); return MountResult.DontMount; } if (task.MountIf == EMountIf.AwayFromPosition) { float num = System.Numerics.Vector3.Distance((FFXIVClientStructs.FFXIV.Common.Math.Vector3)(objectTable.LocalPlayer?.Position ?? ((System.Numerics.Vector3)FFXIVClientStructs.FFXIV.Common.Math.Vector3.Zero)), task.Position.GetValueOrDefault()); if (task.TerritoryId == clientState.TerritoryType && num < 30f && !FFXIVClientStructs.FFXIV.Client.Game.Conditions.Instance()->Diving) { logger.Log(logLevel, "Not using mount, as we're close to the target"); return MountResult.DontMount; } logger.Log(logLevel, "Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...", num, task.TerritoryId); } logger.Log(logLevel, "Want to use mount, trying (in territory {Id})...", task.TerritoryId); if (!condition[ConditionFlag.InCombat]) { if (dryRun) { retryAtMs = Environment.TickCount64 + 500; } return MountResult.Mount; } return MountResult.WhenOutOfCombat; } } internal sealed class MountExecutor(GameFunctions gameFunctions, ICondition condition, MountEvaluator mountEvaluator, ILogger logger) : TaskExecutor() { private bool _mountTriggered; private long _retryAtMs; protected override bool Start() { _mountTriggered = false; return mountEvaluator.EvaluateMountState(base.Task, dryRun: false, ref _retryAtMs) == MountResult.Mount; } public override ETaskResult Update() { if (_mountTriggered && !condition[ConditionFlag.Mounted] && Environment.TickCount64 > _retryAtMs) { logger.LogDebug("Not mounted, retrying..."); _mountTriggered = false; _retryAtMs = long.MaxValue; } if (!_mountTriggered) { if (gameFunctions.HasStatusPreventingMount()) { logger.LogDebug("Can't mount due to status preventing sprint or mount"); return ETaskResult.TaskComplete; } base.ProgressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount()); ResetProgressTimer(); _retryAtMs = Environment.TickCount64 + 5000; return ETaskResult.StillRunning; } if (condition[ConditionFlag.Mounted]) { ResetProgressTimer(); return ETaskResult.TaskComplete; } if (base.ProgressContext != null) { if (base.ProgressContext.WasInterrupted()) { logger.LogDebug("Mount was interrupted"); _mountTriggered = false; return ETaskResult.StillRunning; } if (base.ProgressContext.WasSuccessful()) { ResetProgressTimer(); } } return ETaskResult.StillRunning; } public override bool ShouldInterruptOnDamage() { return false; } } internal enum MountResult { DontMount, Mount, WhenOutOfCombat } internal sealed record UnmountTask : ITask { public bool ShouldRedoOnInterrupt() { return true; } public override string ToString() { return "Unmount"; } } internal sealed class UnmountExecutor(ICondition condition, ILogger logger, GameFunctions gameFunctions, IObjectTable objectTable) : TaskExecutor() { private bool _unmountTriggered; private long _continueAtMs; protected override bool Start() { if (!condition[ConditionFlag.Mounted]) { return false; } logger.LogDebug("Step explicitly wants no mount, trying to unmount..."); if (condition[ConditionFlag.InFlight]) { gameFunctions.Unmount(); _continueAtMs = Environment.TickCount64 + 1000; return true; } _unmountTriggered = gameFunctions.Unmount(); _continueAtMs = Environment.TickCount64 + 1000; return true; } public override ETaskResult Update() { if (Environment.TickCount64 < _continueAtMs) { return ETaskResult.StillRunning; } if (IsUnmounting()) { return ETaskResult.StillRunning; } if (!_unmountTriggered) { if (condition[ConditionFlag.InFlight]) { gameFunctions.Unmount(); } else { _unmountTriggered = gameFunctions.Unmount(); } _continueAtMs = Environment.TickCount64 + 1000; return ETaskResult.StillRunning; } if (condition[ConditionFlag.Mounted] && condition[ConditionFlag.InCombat]) { _unmountTriggered = gameFunctions.Unmount(); _continueAtMs = Environment.TickCount64 + 1000; return ETaskResult.StillRunning; } if (!condition[ConditionFlag.Mounted]) { return ETaskResult.TaskComplete; } return ETaskResult.StillRunning; } private unsafe bool IsUnmounting() { IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer != null) { BattleChara* address = (BattleChara*)localPlayer.Address; return (address->Mount.Flags & 1) == 1; } return false; } public override bool ShouldInterruptOnDamage() { return false; } } public enum EMountIf { Always, AwayFromPosition } }