qstbak/Questionable/Questionable.Controller.Steps.Interactions/UseItem.cs
2026-08-17 20:29:32 +10:00

311 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Movement;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
using SmartNav.Model;
namespace Questionable.Controller.Steps.Interactions;
internal static class UseItem
{
internal sealed class Factory(IClientState clientState, TerritoryData territoryData, ILogger<Factory> logger) : ITaskFactory
{
public unsafe IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((interactionType == EInteractionType.SinglePlayerDuty || interactionType == EInteractionType.CompleteQuest) ? true : false)
{
if (!step.ItemId.HasValue)
{
return Array.Empty<ITask>();
}
}
else if (step.InteractionType != EInteractionType.UseItem)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.ItemId, "step.ItemId");
if (step.ItemId == 30362)
{
if (InventoryManager.Instance()->GetInventoryItemCount(step.ItemId.Value, isHq: false, checkEquipped: true, checkArmory: true, 0) == 0)
{
return CreateVesperBayFallbackTask();
}
UseOnSelf useOnSelf = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
int num = sequence.Steps.IndexOf(step) + 1;
Vector3? position = (((num < sequence.Steps.Count) ? sequence.Steps[num] : null) ?? step).Position;
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[4]
{
useOnSelf,
new WaitCondition.Task(() => clientState.TerritoryType == 140, "Wait(territory: " + territoryData.GetNameAndId(140u) + ")"),
new Mount.MountTask(140, position.HasValue ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always, position),
new MoveTask(140, new Vector3(-408.92343f, 23.167036f, -351.16223f), null, MountRequired: false, DismountRequired: false, 0.25f, null, DisableNavmesh: true, false, Fly: false, Land: false, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.WalkTo)
});
}
Mount.UnmountTask unmountTask = new Mount.UnmountTask();
if (step.GroundTarget == true)
{
ITask task;
if (step.DataId.HasValue)
{
task = new UseOnGround(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
}
else
{
ArgumentNullException.ThrowIfNull(step.Position, "step.Position");
task = new UseOnPosition(quest.Id, step.Position.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
}
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[3]
{
unmountTask,
new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(0.5)),
task
});
}
if (step.DataId.HasValue)
{
UseOnObject useOnObject = new UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, useOnObject });
}
UseOnSelf useOnSelf2 = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, useOnSelf2 });
}
private IEnumerable<ITask> CreateVesperBayFallbackTask()
{
logger.LogWarning("No vesper bay aetheryte tickets in inventory, navigating via ferry in Limsa instead");
uint npcId = 1003540u;
ushort territoryId = 129;
Vector3 destination = new Vector3(-360.9217f, 8f, 38.92566f);
yield return new AetheryteTeleport.Task(EAetheryteLocation.Limsa, territoryId);
yield return new AethernetRide.Task(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
yield return new WaitAtEnd.WaitDelay();
uint? dataId = npcId;
bool? sprint = false;
yield return new MoveTask(territoryId, destination, null, MountRequired: false, DismountRequired: false, null, dataId, DisableNavmesh: false, sprint, Fly: false, Land: false, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.WalkTo);
yield return new Interact.Task(npcId, null, EInteractionType.None, SkipMarkerCheck: true);
}
}
internal interface IUseItemBase : ITask
{
ElementId? QuestId { get; }
uint ItemId { get; }
IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
bool StartingCombat { get; }
}
internal abstract class UseItemExecutorBase<T>(QuestFunctions questFunctions, ICondition condition, ILogger logger) : TaskExecutor<T>(), IToastAware, ITaskExecutor where T : class, IUseItemBase
{
private const long ErrorToastWindowMs = 2000L;
private bool _usedItem;
private bool _errorToastReceived;
private long _continueAt;
private long _usedItemAt;
protected uint ItemId => base.Task.ItemId;
protected abstract bool UseItem();
protected unsafe override bool Start()
{
InventoryManager* intPtr = InventoryManager.Instance();
if (intPtr == null)
{
throw new TaskException("No InventoryManager");
}
if (intPtr->GetInventoryItemCount(ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) == 0)
{
throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)");
}
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_usedItemAt = Environment.TickCount64;
_continueAt = Environment.TickCount64 + GetRetryDelayMs();
return true;
}
public override ETaskResult Update()
{
if (base.Task.QuestId is QuestId elementId && QuestWorkUtils.HasCompletionFlags(base.Task.CompletionQuestVariablesFlags))
{
QuestProgressInfo questProgressInfo = _003CquestFunctions_003EP.GetQuestProgressInfo(elementId);
if (questProgressInfo != null && QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo))
{
ResetProgressTimer();
return ETaskResult.TaskComplete;
}
}
if (Environment.TickCount64 <= _continueAt)
{
return ETaskResult.StillRunning;
}
if (base.Task.StartingCombat && _003Ccondition_003EP[ConditionFlag.InCombat])
{
ResetProgressTimer();
return ETaskResult.TaskComplete;
}
if (base.ProgressContext != null)
{
if (base.ProgressContext.WasInterrupted())
{
_003Clogger_003EP.LogDebug("Item use {ItemId} was interrupted", ItemId);
_usedItem = false;
_errorToastReceived = false;
return ETaskResult.StillRunning;
}
if (base.ProgressContext.WasSuccessful())
{
_errorToastReceived = false;
ResetProgressTimer();
}
}
if (_usedItem && _errorToastReceived)
{
_003Clogger_003EP.LogDebug("Item {ItemId} use failed (error toast received), retrying", ItemId);
_usedItem = false;
_errorToastReceived = false;
return ETaskResult.StillRunning;
}
if (!_usedItem)
{
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_usedItemAt = Environment.TickCount64;
_continueAt = Environment.TickCount64 + GetRetryDelayMs();
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
private long GetRetryDelayMs()
{
if (ItemId == 30362)
{
return 11000L;
}
return 5000L;
}
public bool OnErrorToast(SeString message)
{
if (_usedItem && Environment.TickCount64 - _usedItemAt < 2000)
{
_003Clogger_003EP.LogDebug("Error toast during item use: {Message}", message);
_errorToastReceived = true;
}
return false;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed record UseOnGround(ElementId? QuestId, uint DataId, uint ItemId, IList<QuestWorkValue?> CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask
{
public override string ToString()
{
return $"UseItem({ItemId} on ground at {DataId})";
}
}
internal sealed class UseOnGroundExecutor : UseItemExecutorBase<UseOnGround>
{
public UseOnGroundExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<UseOnGroundExecutor> logger)
{
_003CgameFunctions_003EP = gameFunctions;
base._002Ector(questFunctions, condition, (ILogger)logger);
}
protected override bool UseItem()
{
return _003CgameFunctions_003EP.UseItemOnGround(base.Task.DataId, base.ItemId);
}
}
internal sealed record UseOnPosition(ElementId? QuestId, Vector3 Position, uint ItemId, IList<QuestWorkValue?> CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask
{
public override string ToString()
{
return $"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})";
}
}
internal sealed class UseOnPositionExecutor : UseItemExecutorBase<UseOnPosition>
{
public UseOnPositionExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<UseOnPosition> logger)
{
_003CgameFunctions_003EP = gameFunctions;
base._002Ector(questFunctions, condition, (ILogger)logger);
}
protected override bool UseItem()
{
return _003CgameFunctions_003EP.UseItemOnPosition(base.Task.Position, base.ItemId);
}
}
internal sealed record UseOnObject(ElementId? QuestId, uint DataId, uint ItemId, IList<QuestWorkValue?> CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask
{
public override string ToString()
{
return $"UseItem({ItemId} on {DataId})";
}
}
internal sealed class UseOnObjectExecutor : UseItemExecutorBase<UseOnObject>
{
public UseOnObjectExecutor(QuestFunctions questFunctions, GameFunctions gameFunctions, ICondition condition, ILogger<UseOnObject> logger)
{
_003CgameFunctions_003EP = gameFunctions;
base._002Ector(questFunctions, condition, (ILogger)logger);
}
protected override bool UseItem()
{
return _003CgameFunctions_003EP.UseItem(base.Task.DataId, base.ItemId);
}
}
internal sealed record UseOnSelf(ElementId? QuestId, uint ItemId, IList<QuestWorkValue?> CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask
{
public override string ToString()
{
return $"UseItem({ItemId})";
}
}
internal sealed class UseOnSelfExecutor : UseItemExecutorBase<UseOnSelf>
{
public UseOnSelfExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<UseOnSelf> logger)
{
_003CgameFunctions_003EP = gameFunctions;
base._002Ector(questFunctions, condition, (ILogger)logger);
}
protected override bool UseItem()
{
return _003CgameFunctions_003EP.UseItem(base.ItemId);
}
}
}