forked from aly/qstbak
311 lines
11 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|