1
0
Fork 0
forked from aly/qstbak
qstbak/Questionable/Questionable.Controller.Steps.Common/RepairGear.cs
2026-08-17 20:29:32 +10:00

273 lines
6.5 KiB
C#

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<T>(IGameGui gameGui, ILogger logger) : TaskExecutor<T>(), 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<SelfRepairTask>
{
protected override EState InitialState => EState.OpeningWindow;
public SelfRepairExecutor(IGameGui gameGui, ILogger<SelfRepairExecutor> logger)
: base(gameGui, (ILogger)logger)
{
}
protected override bool UpdateCustomState(EState state)
{
return false;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed class MenderRepairExecutor : RepairExecutorBase<MenderRepairTask>
{
protected override EState InitialState => EState.Interacting;
public MenderRepairExecutor(IGameGui gameGui, GameFunctions gameFunctions, IDataManager dataManager, ILogger<MenderRepairExecutor> 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;
}
}
}