using System; using Dalamud.Plugin.Services; using LLib.Gear; using Microsoft.Extensions.Logging; using Questionable.Data; using Questionable.Functions; namespace Questionable.Controller.Steps.Common; internal static class RepairGear { internal sealed record SelfRepairTask : ITask { public override string ToString() { return "SelfRepair"; } } internal sealed record MenderRepairTask(MenderNpc Npc) : ITask { public override string ToString() { return $"MenderRepair({Npc.DataId})"; } } internal enum EState { Interacting, SelectingMenu, OpeningWindow, WaitingForAddon, FiringRepair, Confirming, Settling, Closing, WaitingForClose } internal abstract class RepairExecutorBase(IGameGui gameGui, ILogger logger) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor where T : class, ITask { private const long StateTimeoutMs = 5000L; private const long SettleMs = 1000L; private const long PollIntervalMs = 250L; private const int MaxAttempts = 3; private EState _state; private long _stateDeadline; private long _nextPollAt; private int? _conditionBefore; private int _attempt; protected IGameGui GameGui { get; } = gameGui; protected ILogger Logger { get; } = logger; protected abstract EState InitialState { get; } protected abstract bool UpdateCustomState(EState state); protected sealed override bool Start() { _conditionBefore = GearConditionHelper.GetLowestConditionPercent(); _attempt = 1; EnterState(InitialState); _nextPollAt = 0L; return true; } public sealed override ETaskResult Update() { long tickCount = Environment.TickCount64; if (tickCount < _nextPollAt) { return ETaskResult.StillRunning; } _nextPollAt = tickCount + 250; if (tickCount > _stateDeadline) { if (_state != EState.Settling) { Logger.LogWarning("Repair timed out in state {State} ({Diagnostic}), giving up", _state, RepairActions.DescribeRepairAddon(GameGui)); RepairActions.TryCloseRepairWindow(GameGui); return Finish(); } EnterState(EState.Closing); } switch (_state) { case EState.OpeningWindow: if (RepairActions.TryOpenRepairWindow()) { EnterState(EState.WaitingForAddon); } break; case EState.WaitingForAddon: if (RepairActions.IsRepairAddonReady(GameGui)) { EnterState(EState.FiringRepair); } break; case EState.FiringRepair: if (RepairActions.TryRepairAll(GameGui)) { EnterState(EState.Confirming); } break; case EState.Confirming: if (RepairActions.TryConfirmDialog(GameGui)) { EnterState(EState.Settling); } else if (HasConditionImproved()) { EnterState(EState.Settling); } else { RepairActions.TryRepairAll(GameGui); } break; case EState.Closing: if (RepairActions.TryCloseRepairWindow(GameGui)) { EnterState(EState.WaitingForClose); } break; case EState.WaitingForClose: if (!RepairActions.IsRepairAddonReady(GameGui)) { return Finish(); } break; default: if (!UpdateCustomState(_state)) { throw new TaskException($"Unhandled repair state {_state}"); } break; case EState.Settling: break; } return ETaskResult.StillRunning; } protected void EnterState(EState state) { _state = state; _stateDeadline = Environment.TickCount64 + ((state == EState.Settling) ? 1000 : 5000); } private bool HasConditionImproved() { int? conditionBefore = _conditionBefore; if (conditionBefore.HasValue) { int valueOrDefault = conditionBefore.GetValueOrDefault(); conditionBefore = GearConditionHelper.GetLowestConditionPercent(); if (conditionBefore.HasValue) { int valueOrDefault2 = conditionBefore.GetValueOrDefault(); return valueOrDefault2 > valueOrDefault; } } return false; } private ETaskResult Finish() { int? lowestConditionPercent = GearConditionHelper.GetLowestConditionPercent(); if (HasConditionImproved()) { Logger.LogInformation("Repair complete, lowest condition {Before}% -> {After}%", _conditionBefore, lowestConditionPercent); return ETaskResult.TaskComplete; } if (_attempt < 3) { _attempt++; Logger.LogWarning("Repair pass ended without improving condition (still {After}%), retrying ({Attempt}/{Max})", lowestConditionPercent, _attempt, 3); RepairActions.TryCloseRepairWindow(GameGui); EnterState(InitialState); return ETaskResult.StillRunning; } Logger.LogWarning("Repair finished without improving condition (still {After}%)", lowestConditionPercent); return ETaskResult.TaskComplete; } public void StopNow() { RepairActions.TryCloseRepairWindow(GameGui); } } internal sealed class SelfRepairExecutor : RepairExecutorBase { protected override EState InitialState => EState.OpeningWindow; public SelfRepairExecutor(IGameGui gameGui, ILogger logger) : base(gameGui, (ILogger)logger) { } protected override bool UpdateCustomState(EState state) { return false; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed class MenderRepairExecutor : RepairExecutorBase { protected override EState InitialState => EState.Interacting; public MenderRepairExecutor(IGameGui gameGui, GameFunctions gameFunctions, IDataManager dataManager, ILogger logger) { _003CgameFunctions_003EP = gameFunctions; _003CdataManager_003EP = dataManager; base._002Ector(gameGui, (ILogger)logger); } protected override bool UpdateCustomState(EState state) { switch (state) { case EState.Interacting: if (_003CgameFunctions_003EP.InteractWith(base.Task.Npc.DataId)) { EnterState(EState.SelectingMenu); } return true; case EState.SelectingMenu: { if (RepairActions.IsRepairAddonReady(base.GameGui)) { EnterState(EState.FiringRepair); return true; } int? num = RepairActions.FindRepairIndex(_003CdataManager_003EP, base.Task.Npc.DataId); if (!num.HasValue) { base.Logger.LogWarning("NPC {DataId} has no repair menu entry", base.Task.Npc.DataId); EnterState(EState.Closing); return true; } if (RepairActions.TrySelectRepairFromMenu(base.GameGui, num.Value)) { EnterState(EState.WaitingForAddon); } return true; } default: return false; } } public override bool ShouldInterruptOnDamage() { return true; } } }