using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Conditions; 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.Common; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Interactions; internal static class UseItem { internal sealed class Factory(IClientState clientState, TerritoryData territoryData, ILogger logger) : ITaskFactory { public unsafe IEnumerable 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(); } } else if (step.InteractionType != EInteractionType.UseItem) { return Array.Empty(); } 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); Vector3? position = (sequence.Steps.Skip(num + 1).FirstOrDefault() ?? step).Position; return new global::_003C_003Ez__ReadOnlyArray(new ITask[4] { useOnSelf, new WaitCondition.Task(() => clientState.TerritoryType == 140, "Wait(territory: " + territoryData.GetNameAndId(140) + ")"), 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, 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(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(new ITask[2] { unmountTask, useOnObject }); } UseOnSelf useOnSelf2 = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags); return new global::_003C_003Ez__ReadOnlyArray(new ITask[2] { unmountTask, useOnSelf2 }); } private IEnumerable 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 AetheryteShortcut.Task(null, null, EAetheryteLocation.Limsa, territoryId); yield return new Questionable.Controller.Steps.Shared.AethernetShortcut.Task(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist); yield return new WaitAtEnd.WaitDelay(); uint? dataId = npcId; bool? sprint = false; yield return new MoveTask(territoryId, destination, null, 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 CompletionQuestVariablesFlags { get; } bool StartingCombat { get; } } internal abstract class UseItemExecutorBase(QuestFunctions questFunctions, ICondition condition, ILogger logger) : TaskExecutor() where T : class, IUseItemBase { private bool _usedItem; private DateTime _continueAt; private int _itemCount; private ElementId? QuestId => base.Task.QuestId; protected uint ItemId => base.Task.ItemId; private IList CompletionQuestVariablesFlags => base.Task.CompletionQuestVariablesFlags; private bool StartingCombat => base.Task.StartingCombat; protected abstract bool UseItem(); protected unsafe override bool Start() { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { throw new TaskException("No InventoryManager"); } _itemCount = ptr->GetInventoryItemCount(ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0); if (_itemCount == 0) { throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)"); } base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem()); _continueAt = DateTime.Now.Add(GetRetryDelay()); return true; } public unsafe override ETaskResult Update() { if (QuestId is QuestId elementId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags)) { QuestProgressInfo questProgressInfo = _003CquestFunctions_003EP.GetQuestProgressInfo(elementId); if (questProgressInfo != null && QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questProgressInfo)) { return ETaskResult.TaskComplete; } } if (DateTime.Now <= _continueAt) { return ETaskResult.StillRunning; } if (StartingCombat && _003Ccondition_003EP[ConditionFlag.InCombat]) { return ETaskResult.TaskComplete; } if (ItemId == 30362 && _usedItem) { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { _003Clogger_003EP.LogWarning("InventoryManager is not available"); return ETaskResult.StillRunning; } if (ptr->GetInventoryItemCount(ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) == _itemCount) { _003Clogger_003EP.LogInformation("Attempted to use vesper bay aetheryte ticket, but it didn't consume an item - reattempting next frame"); _usedItem = false; return ETaskResult.StillRunning; } } if (!_usedItem) { base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem()); _continueAt = DateTime.Now.Add(GetRetryDelay()); return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } private TimeSpan GetRetryDelay() { if (ItemId == 30362) { return TimeSpan.FromSeconds(11L); } return TimeSpan.FromSeconds(5L); } public override bool ShouldInterruptOnDamage() { return true; } } internal sealed record UseOnGround(ElementId? QuestId, uint DataId, uint ItemId, IList CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask { public override string ToString() { return $"UseItem({ItemId} on ground at {DataId})"; } } internal sealed class UseOnGroundExecutor : UseItemExecutorBase { public UseOnGroundExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger 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 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 { public UseOnPositionExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger 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 CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask { public override string ToString() { return $"UseItem({ItemId} on {DataId})"; } } internal sealed class UseOnObjectExecutor : UseItemExecutorBase { public UseOnObjectExecutor(QuestFunctions questFunctions, GameFunctions gameFunctions, ICondition condition, ILogger 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 CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask { public override string ToString() { return $"UseItem({ItemId})"; } } internal sealed class UseOnSelfExecutor : UseItemExecutorBase { public UseOnSelfExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger logger) { _003CgameFunctions_003EP = gameFunctions; base._002Ector(questFunctions, condition, (ILogger)logger); } protected override bool UseItem() { return _003CgameFunctions_003EP.UseItem(base.ItemId); } } }