using System; using System.Numerics; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; using Questionable.Controller.GameUi; using Questionable.Functions; using SmartNav; namespace Questionable.Controller.Steps.Shared; internal static class WarpInteract { internal sealed record Task(uint NpcDataId, uint WarpRowId, ushort SourceTerritoryId, ushort DestTerritoryId) : ITask { public bool ShouldRedoOnInterrupt() { return true; } public override string ToString() { return $"WarpInteract({NpcDataId}, warp={WarpRowId}, {SourceTerritoryId}->{DestTerritoryId})"; } } internal sealed class DoWarpInteract(GameFunctions gameFunctions, InteractionUiController interactionUiController, ICondition condition, IClientState clientState, IObjectTable objectTable, WarpFailureTracker warpFailureTracker, ILogger logger) : TaskExecutor(), IConditionChangeAware, ITaskExecutor, IStoppableTaskExecutor { private enum EWarpPhase { None, Unmounting, Interacting, WaitingForSettle } private EWarpPhase _phase; private Vector3 _startPosition; private long _interactingStartedAt; private long _settleAt; private bool _abortToReroute; protected override bool Start() { _phase = EWarpPhase.None; _abortToReroute = false; _startPosition = objectTable.LocalPlayer?.Position ?? Vector3.Zero; warpFailureTracker.RecordAttempt(base.Task.WarpRowId); if (warpFailureTracker.IsBlacklisted(base.Task.WarpRowId)) { logger.LogWarning("WarpInteract: warp {WarpRowId} blacklisted after repeated failures, retrying step to re-route", base.Task.WarpRowId); _abortToReroute = true; return true; } interactionUiController.SmartNavWarpRowId = base.Task.WarpRowId; interactionUiController.SmartNavDestTerritoryId = base.Task.DestTerritoryId; IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.NpcDataId); if (gameObject == null) { logger.LogDebug("WarpInteract: NPC with DataId {DataId} not found yet, waiting", base.Task.NpcDataId); return true; } if (condition[ConditionFlag.Mounted]) { logger.LogDebug("WarpInteract: Unmounting before warp interaction"); gameFunctions.Unmount(); _phase = EWarpPhase.Unmounting; return true; } if (gameFunctions.InteractWith(gameObject)) { logger.LogDebug("WarpInteract: Interacting with NPC {DataId}", base.Task.NpcDataId); _phase = EWarpPhase.Interacting; _interactingStartedAt = Environment.TickCount64; } return true; } public override ETaskResult Update() { if (_abortToReroute) { ClearContext(); return ETaskResult.RetryStep; } switch (_phase) { case EWarpPhase.Unmounting: { if (condition[ConditionFlag.Mounted]) { return ETaskResult.StillRunning; } IGameObject gameObject2 = gameFunctions.FindObjectByDataId(base.Task.NpcDataId); if (gameObject2 == null) { logger.LogDebug("WarpInteract: NPC not found after unmount, waiting"); _phase = EWarpPhase.None; return ETaskResult.StillRunning; } if (gameFunctions.InteractWith(gameObject2)) { logger.LogDebug("WarpInteract: Interacting with NPC {DataId} after unmount", base.Task.NpcDataId); _phase = EWarpPhase.Interacting; _interactingStartedAt = Environment.TickCount64; } return ETaskResult.StillRunning; } case EWarpPhase.Interacting: { if (clientState.TerritoryType != base.Task.SourceTerritoryId) { logger.LogDebug("WarpInteract: Territory changed to {Territory}, warp successful", clientState.TerritoryType); warpFailureTracker.RecordSuccess(base.Task.WarpRowId); _phase = EWarpPhase.WaitingForSettle; _settleAt = Environment.TickCount64 + 500; return ETaskResult.StillRunning; } IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer != null && Vector3.Distance(localPlayer.Position, _startPosition) > 2f) { logger.LogDebug("WarpInteract: Position changed significantly, same-zone warp detected"); warpFailureTracker.RecordSuccess(base.Task.WarpRowId); _phase = EWarpPhase.WaitingForSettle; _settleAt = Environment.TickCount64 + 500; return ETaskResult.StillRunning; } if (Environment.TickCount64 - _interactingStartedAt > 30000) { logger.LogWarning("WarpInteract: Timed out waiting for warp transition, retrying step"); ClearContext(); return ETaskResult.RetryStep; } return ETaskResult.StillRunning; } case EWarpPhase.WaitingForSettle: if (Environment.TickCount64 >= _settleAt) { logger.LogDebug("WarpInteract: Warp complete"); ClearContext(); return ETaskResult.TaskComplete; } return ETaskResult.StillRunning; case EWarpPhase.None: { IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.NpcDataId); if (gameObject == null) { return ETaskResult.StillRunning; } _startPosition = objectTable.LocalPlayer?.Position ?? _startPosition; if (condition[ConditionFlag.Mounted]) { gameFunctions.Unmount(); _phase = EWarpPhase.Unmounting; return ETaskResult.StillRunning; } if (gameFunctions.InteractWith(gameObject)) { logger.LogDebug("WarpInteract: Interacting with NPC {DataId}", base.Task.NpcDataId); _phase = EWarpPhase.Interacting; _interactingStartedAt = Environment.TickCount64; } return ETaskResult.StillRunning; } default: return ETaskResult.StillRunning; } } public void OnConditionChange(ConditionFlag flag, bool value) { bool flag2 = _phase == EWarpPhase.Interacting; if (flag2) { bool flag3 = (uint)(flag - 31) <= 1u; flag2 = flag3; } if (flag2 && value) { logger.LogDebug("WarpInteract: Interaction confirmed via condition flag"); } } public override bool ShouldInterruptOnDamage() { return true; } public void StopNow() { ClearContext(); } private void ClearContext() { interactionUiController.SmartNavWarpRowId = null; interactionUiController.SmartNavDestTerritoryId = null; } } }