punish v6.8.18.0

This commit is contained in:
alydev 2025-10-09 07:47:19 +10:00
commit e786325cda
322 changed files with 554232 additions and 0 deletions

View file

@ -0,0 +1,277 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Action
{
internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Action)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.Action, "step.Action");
ITask task = OnObject(step.DataId, quest, step.Action.Value, step.CompletionQuestVariablesFlags);
if (step.Action.Value.RequiresMount())
{
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(task);
}
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2]
{
new Mount.UnmountTask(),
task
});
}
public static ITask OnObject(uint? dataId, Quest quest, EAction action, List<QuestWorkValue?>? completionQuestVariablesFlags)
{
if ((uint)(action - 2265) <= 2u)
{
ArgumentNullException.ThrowIfNull(dataId, "dataId");
return new UseMudraOnObject(dataId.Value, action);
}
return new UseOnObject(dataId, quest, action, completionQuestVariablesFlags);
}
}
internal sealed record UseOnObject(uint? DataId, Quest? Quest, EAction Action, List<QuestWorkValue?>? CompletionQuestVariablesFlags) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"Action({Action})";
}
}
internal sealed class UseOnObjectExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ILogger<UseOnObject> logger) : TaskExecutor<UseOnObject>()
{
private bool _usedAction;
private DateTime _continueAt = DateTime.MinValue;
protected override bool Start()
{
if (base.Task.DataId.HasValue)
{
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId.Value);
if (gameObject == null)
{
logger.LogWarning("No game object with dataId {DataId}", base.Task.DataId);
return false;
}
if (gameObject.IsTargetable)
{
if (base.Task.Action == EAction.Diagnosis && gameFunctions.HasStatus(EStatus.Eukrasia) && GameFunctions.RemoveStatus(EStatus.Eukrasia))
{
_continueAt = DateTime.Now.AddSeconds(2.0);
return true;
}
_usedAction = gameFunctions.UseAction(gameObject, base.Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
}
return true;
}
_usedAction = gameFunctions.UseAction(base.Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
}
public override ETaskResult Update()
{
if (DateTime.Now <= _continueAt)
{
return ETaskResult.StillRunning;
}
if (!_usedAction)
{
if (base.Task.DataId.HasValue)
{
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId.Value);
if (gameObject == null || !gameObject.IsTargetable)
{
return ETaskResult.StillRunning;
}
_usedAction = gameFunctions.UseAction(gameObject, base.Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5);
}
else
{
_usedAction = gameFunctions.UseAction(base.Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5);
}
return ETaskResult.StillRunning;
}
if (base.Task.Quest != null && base.Task.CompletionQuestVariablesFlags != null && QuestWorkUtils.HasCompletionFlags(base.Task.CompletionQuestVariablesFlags))
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(base.Task.Quest.Id);
if (questProgressInfo == null || !QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed record UseMudraOnObject(uint DataId, EAction Action) : ITask
{
public override string ToString()
{
return $"Mudra({Action})";
}
}
internal sealed class UseMudraOnObjectExecutor(GameFunctions gameFunctions, ILogger<UseMudraOnObject> logger) : TaskExecutor<UseMudraOnObject>()
{
private static readonly ReadOnlyDictionary<EAction, Dictionary<EAction, EAction>> Combos = new Dictionary<EAction, Dictionary<EAction, EAction>>
{
{
EAction.FumaShuriken,
new Dictionary<EAction, EAction> {
{
EAction.Ninjutsu,
EAction.Ten
} }
},
{
EAction.Raiton,
new Dictionary<EAction, EAction>
{
{
EAction.Ninjutsu,
EAction.Ten
},
{
EAction.FumaShuriken,
EAction.Chi
}
}
},
{
EAction.Katon,
new Dictionary<EAction, EAction>
{
{
EAction.Ninjutsu,
EAction.Chi
},
{
EAction.FumaShuriken,
EAction.Ten
}
}
}
}.AsReadOnly();
private DateTime _continueAt = DateTime.MinValue;
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
{
return ETaskResult.StillRunning;
}
EAction adjustedActionId = (EAction)ActionManager.Instance()->GetAdjustedActionId(2260u);
if (adjustedActionId == EAction.RabbitMedium)
{
_continueAt = DateTime.Now.AddSeconds(1.0);
return ETaskResult.StillRunning;
}
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId);
if (gameObject == null || !gameObject.IsTargetable)
{
return ETaskResult.StillRunning;
}
if (adjustedActionId == base.Task.Action)
{
_continueAt = DateTime.Now.AddSeconds(0.25);
if (!gameFunctions.UseAction(gameObject, base.Task.Action))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
if (Combos.TryGetValue(base.Task.Action, out Dictionary<EAction, EAction> value))
{
if (value.TryGetValue(adjustedActionId, out var value2))
{
_continueAt = DateTime.Now.AddSeconds(0.25);
gameFunctions.UseAction(value2);
return ETaskResult.StillRunning;
}
_continueAt = DateTime.Now.AddSeconds(0.25);
return ETaskResult.StillRunning;
}
logger.LogError("Unable to find relevant combo for {Action}", base.Task.Action);
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record TriggerStatusIfMissing(EStatus Status, EAction Action) : ITask
{
public override string ToString()
{
return $"TriggerStatus({Status})";
}
}
internal sealed class TriggerStatusIfMissingExecutor(GameFunctions gameFunctions) : TaskExecutor<TriggerStatusIfMissing>()
{
protected override bool Start()
{
if (gameFunctions.HasStatus(base.Task.Status))
{
return false;
}
gameFunctions.UseAction(base.Task.Action);
return true;
}
public override ETaskResult Update()
{
if (!gameFunctions.HasStatus(base.Task.Status))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,74 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class AetherCurrent
{
internal sealed class Factory(AetherCurrentData aetherCurrentData, IChatGui chatGui) : SimpleTaskFactory()
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AttuneAetherCurrent)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
ArgumentNullException.ThrowIfNull(step.AetherCurrentId, "step.AetherCurrentId");
if (!aetherCurrentData.IsValidAetherCurrent(step.TerritoryId, step.AetherCurrentId.Value))
{
chatGui.PrintError($"Aether current with id {step.AetherCurrentId} is referencing an invalid aether current, will skip attunement", "Questionable", 576);
return null;
}
return new Attune(step.DataId.Value, step.AetherCurrentId.Value);
}
}
internal sealed record Attune(uint DataId, uint AetherCurrentId) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"AttuneAetherCurrent({AetherCurrentId})";
}
}
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : TaskExecutor<Attune>()
{
protected override bool Start()
{
if (!gameFunctions.IsAetherCurrentUnlocked(base.Task.AetherCurrentId))
{
logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", base.Task.AetherCurrentId, base.Task.DataId);
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(base.Task.DataId, ObjectKind.EventObj));
return true;
}
logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", base.Task.AetherCurrentId, base.Task.DataId);
return false;
}
public override ETaskResult Update()
{
if (!gameFunctions.IsAetherCurrentUnlocked(base.Task.AetherCurrentId))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,67 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class AethernetShard
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AttuneAethernetShard)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.AethernetShard, "step.AethernetShard");
return new Attune(step.AethernetShard.Value);
}
}
internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"AttuneAethernetShard({AetheryteLocation})";
}
}
internal sealed class DoAttune(AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, ILogger<DoAttune> logger) : TaskExecutor<Attune>()
{
protected override bool Start()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", base.Task.AetheryteLocation);
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith((uint)base.Task.AetheryteLocation, ObjectKind.Aetheryte));
return true;
}
logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", base.Task.AetheryteLocation);
return false;
}
public override ETaskResult Update()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,67 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Aetheryte
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AttuneAetheryte)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.Aetheryte, "step.Aetheryte");
return new Attune(step.Aetheryte.Value);
}
}
internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"AttuneAetheryte({AetheryteLocation})";
}
}
internal sealed class DoAttune(AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, ILogger<DoAttune> logger) : TaskExecutor<Attune>()
{
protected override bool Start()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
logger.LogInformation("Attuning to aetheryte {Aetheryte}", base.Task.AetheryteLocation);
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith((uint)base.Task.AetheryteLocation, ObjectKind.Aetheryte));
return true;
}
logger.LogInformation("Already attuned to aetheryte {Aetheryte}", base.Task.AetheryteLocation);
return false;
}
public override ETaskResult Update()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,70 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class AetheryteFreeOrFavored
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.RegisterFreeOrFavoredAetheryte)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.Aetheryte, "step.Aetheryte");
return new Register(step.Aetheryte.Value);
}
}
internal sealed record Register(EAetheryteLocation AetheryteLocation) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"RegisterFreeOrFavoredAetheryte({AetheryteLocation})";
}
}
internal sealed class DoRegister(AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, ILogger<DoRegister> logger) : TaskExecutor<Register>()
{
protected override bool Start()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
throw new TaskException($"Aetheryte {base.Task.AetheryteLocation} is not attuned");
}
if (aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(base.Task.AetheryteLocation) == AetheryteRegistrationResult.NotPossible)
{
logger.LogInformation("Could not register aetheryte {AetheryteLocation} as free or favored", base.Task.AetheryteLocation);
return false;
}
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith((uint)base.Task.AetheryteLocation, ObjectKind.Aetheryte));
return true;
}
public override ETaskResult Update()
{
if (aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(base.Task.AetheryteLocation) != AetheryteRegistrationResult.NotPossible)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Combat
{
internal sealed class Factory(GameFunctions gameFunctions) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Combat)
{
yield break;
}
ArgumentNullException.ThrowIfNull(step.EnemySpawnType, "step.EnemySpawnType");
if (gameFunctions.GetMountId() != 128 && gameFunctions.GetMountId() != 147)
{
yield return new Mount.UnmountTask();
}
if (step.CombatDelaySecondsAtStart.HasValue)
{
yield return new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(step.CombatDelaySecondsAtStart.Value));
}
switch (step.EnemySpawnType)
{
case EEnemySpawnType.AfterInteraction:
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
yield return new Interact.Task(step.DataId.Value, quest, EInteractionType.None, SkipMarkerCheck: true);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.AfterItemUse:
ArgumentNullException.ThrowIfNull(step.ItemId, "step.ItemId");
if (step.GroundTarget == true)
{
if (step.DataId.HasValue)
{
yield return new UseItem.UseOnGround(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, StartingCombat: true);
}
else
{
ArgumentNullException.ThrowIfNull(step.Position, "step.Position");
yield return new UseItem.UseOnPosition(quest.Id, step.Position.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, StartingCombat: true);
}
}
else if (step.DataId.HasValue)
{
yield return new UseItem.UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, StartingCombat: true);
}
else
{
yield return new UseItem.UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags, StartingCombat: true);
}
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.AfterAction:
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
ArgumentNullException.ThrowIfNull(step.Action, "step.Action");
if (!step.Action.Value.RequiresMount())
{
yield return new Mount.UnmountTask();
}
yield return new Action.UseOnObject(step.DataId.Value, null, step.Action.Value, null);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.AfterEmote:
ArgumentNullException.ThrowIfNull(step.Emote, "step.Emote");
yield return new Mount.UnmountTask();
if (step.DataId.HasValue)
{
yield return new Emote.UseOnObject(step.Emote.Value, step.DataId.Value);
}
else
{
yield return new Emote.UseOnSelf(step.Emote.Value);
}
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.AutoOnEnterArea:
if (!step.CombatDelaySecondsAtStart.HasValue)
{
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
}
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.OverworldEnemies:
case EEnemySpawnType.FateEnemies:
case EEnemySpawnType.FinishCombatIfAny:
yield return CreateTask(quest, sequence, step);
break;
default:
throw new ArgumentOutOfRangeException("step", $"Unknown spawn type {step.EnemySpawnType}");
}
}
private static Task CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
ArgumentNullException.ThrowIfNull(step.EnemySpawnType, "step.EnemySpawnType");
bool isLastStep = sequence.Steps.Last() == step;
return CreateTask(quest.Id, sequence.Sequence, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds, step.CompletionQuestVariablesFlags, step.ComplexCombatData, step.CombatItemUse);
}
internal static Task CreateTask(ElementId? elementId, int sequence, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags, IList<ComplexCombatData> complexCombatData, CombatItemUse? combatItemUse)
{
return new Task(new CombatController.CombatData
{
ElementId = elementId,
Sequence = sequence,
CompletionQuestVariablesFlags = completionQuestVariablesFlags,
SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(),
CombatItemUse = combatItemUse
}, completionQuestVariablesFlags, isLastStep);
}
}
internal sealed record Task(CombatController.CombatData CombatData, IList<QuestWorkValue?> CompletionQuestVariableFlags, bool IsLastStep) : ITask
{
public override string ToString()
{
if (CombatData.SpawnType == EEnemySpawnType.FinishCombatIfAny)
{
return "HandleCombat(wait: not in combat, optional)";
}
if (QuestWorkUtils.HasCompletionFlags(CompletionQuestVariableFlags))
{
return "HandleCombat(wait: QW flags)";
}
if (IsLastStep)
{
return "HandleCombat(wait: next sequence)";
}
return "HandleCombat(wait: not in combat)";
}
}
internal sealed class HandleCombat(CombatController combatController, QuestFunctions questFunctions) : TaskExecutor<Task>()
{
private CombatController.EStatus _status;
protected override bool Start()
{
return combatController.Start(base.Task.CombatData);
}
public override ETaskResult Update()
{
_status = combatController.Update();
if (_status != CombatController.EStatus.Complete)
{
return ETaskResult.StillRunning;
}
if (QuestWorkUtils.HasCompletionFlags(base.Task.CompletionQuestVariableFlags) && base.Task.CombatData.ElementId is QuestId elementId)
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(elementId);
if (questProgressInfo == null)
{
return ETaskResult.StillRunning;
}
if (QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariableFlags, questProgressInfo))
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
if (base.Task.IsLastStep)
{
return ETaskResult.StillRunning;
}
combatController.Stop("Combat task complete");
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Dive
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Dive)
{
return null;
}
return new Task();
}
}
internal sealed class Task : ITask
{
public override string ToString()
{
return "Dive";
}
}
internal sealed class DoDive(ICondition condition, ILogger<DoDive> logger) : AbstractDelayedTaskExecutor<Task>(TimeSpan.FromSeconds(5L))
{
private readonly Queue<(uint Type, nint Key)> _keysToPress = new Queue<(uint, nint)>();
private int _attempts;
protected override bool StartInternal()
{
if (condition[ConditionFlag.Diving])
{
return false;
}
if (condition[ConditionFlag.Mounted] || condition[ConditionFlag.Swimming])
{
Descend();
return true;
}
throw new TaskException("You aren't swimming, so we can't dive.");
}
public unsafe override ETaskResult Update()
{
if (_keysToPress.TryDequeue(out (uint, nint) result))
{
if (result.Item1 == 0)
{
return ETaskResult.StillRunning;
}
logger.LogDebug("{Action} key {KeyCode:X2}", (result.Item1 == 256) ? "Pressing" : "Releasing", result.Item2);
NativeMethods.SendMessage((nint)Device.Instance()->hWnd, result.Item1, result.Item2, IntPtr.Zero);
return ETaskResult.StillRunning;
}
return base.Update();
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
protected override ETaskResult UpdateInternal()
{
if (condition[ConditionFlag.Diving])
{
return ETaskResult.TaskComplete;
}
if (_attempts >= 3)
{
throw new TaskException("Please dive manually.");
}
Descend();
_attempts++;
return ETaskResult.StillRunning;
}
private unsafe void Descend()
{
UIInputData.Keybind keybind = default(UIInputData.Keybind);
Utf8String* name = Utf8String.FromString("MOVE_DESCENT");
UIInputData.Instance()->GetKeybindByName(name, (Keybind*)(&keybind));
logger.LogInformation("Dive keybind: {Key1} + {Modifier1}, {Key2} + {Modifier2}", keybind.Key, keybind.Modifier, keybind.AltKey, keybind.AltModifier);
int num = 2;
List<List<nint>> list = new List<List<nint>>(num);
CollectionsMarshal.SetCount(list, num);
Span<List<nint>> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
span[num2] = GetKeysToPress(keybind.Key, keybind.Modifier);
num2++;
span[num2] = GetKeysToPress(keybind.AltKey, keybind.AltModifier);
List<nint> list2 = (from x in list
where x != null
select (x)).MinBy((List<nint> x) => x.Count);
if (list2 == null || list2.Count == 0)
{
throw new TaskException("No useable keybind found for diving");
}
foreach (nint item in list2)
{
_keysToPress.Enqueue((256u, item));
_keysToPress.Enqueue((0u, 0));
_keysToPress.Enqueue((0u, 0));
}
for (int num3 = 0; num3 < 5; num3++)
{
_keysToPress.Enqueue((0u, 0));
}
list2.Reverse();
foreach (nint item2 in list2)
{
_keysToPress.Enqueue((257u, item2));
}
}
}
private static class NativeMethods
{
public const uint WM_KEYUP = 257u;
public const uint WM_KEYDOWN = 256u;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
public static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, nint lParam);
}
private static List<nint>? GetKeysToPress(SeVirtualKey key, ModifierFlag modifier)
{
List<nint> list = new List<nint>();
if (modifier.HasFlag(ModifierFlag.Ctrl))
{
list.Add(17);
}
if (modifier.HasFlag(ModifierFlag.Shift))
{
list.Add(16);
}
if (modifier.HasFlag(ModifierFlag.Alt))
{
list.Add(18);
}
nint num = (nint)key;
if (num == 0)
{
return null;
}
list.Add(num);
return list;
}
}

View file

@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using LLib.Gear;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Duty
{
internal sealed class Factory(AutoDutyIpc autoDutyIpc) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Duty)
{
yield break;
}
ArgumentNullException.ThrowIfNull(step.DutyOptions, "step.DutyOptions");
uint contentFinderConditionId;
int dutyMode;
if (autoDutyIpc.IsConfiguredToRunContent(step.DutyOptions))
{
contentFinderConditionId = step.DutyOptions.ContentFinderConditionId;
ElementId id = quest.Id;
if (id is QuestId)
{
ushort value = id.Value;
if (value >= 357 && value <= 360)
{
dutyMode = 2;
goto IL_00b2;
}
}
dutyMode = 1;
goto IL_00b2;
}
if (!step.DutyOptions.LowPriority)
{
yield return new OpenDutyFinderTask(step.DutyOptions.ContentFinderConditionId);
}
yield break;
IL_00b2:
yield return new StartAutoDutyTask(contentFinderConditionId, (AutoDutyIpc.DutyMode)dutyMode);
yield return new WaitAutoDutyTask(step.DutyOptions.ContentFinderConditionId);
if (!QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
{
yield return new WaitAtEnd.WaitNextStepOrSequence();
}
}
}
internal sealed record StartAutoDutyTask(uint ContentFinderConditionId, AutoDutyIpc.DutyMode DutyMode) : ITask
{
public override string ToString()
{
return $"StartAutoDuty({ContentFinderConditionId}, {DutyMode})";
}
}
internal sealed class StartAutoDutyExecutor(GearStatsCalculator gearStatsCalculator, AutoDutyIpc autoDutyIpc, TerritoryData territoryData, IClientState clientState, IChatGui chatGui, SendNotification.Executor sendNotificationExecutor) : TaskExecutor<StartAutoDutyTask>(), IStoppableTaskExecutor, ITaskExecutor
{
protected unsafe override bool Start()
{
if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
{
throw new TaskException("Failed to get territory ID for content finder condition");
}
InventoryManager* intPtr = InventoryManager.Instance();
if (intPtr == null)
{
throw new TaskException("Inventory unavailable");
}
InventoryContainer* inventoryContainer = intPtr->GetInventoryContainer(InventoryType.EquippedItems);
if (inventoryContainer == null)
{
throw new TaskException("Equipped items unavailable");
}
short num = gearStatsCalculator.CalculateAverageItemLevel(inventoryContainer);
if (contentFinderConditionData.RequiredItemLevel > num)
{
string text = $"Could not use AutoDuty to queue for {contentFinderConditionData.Name}, required item level: {contentFinderConditionData.RequiredItemLevel}, current item level: {num}.";
if (!sendNotificationExecutor.Start(new SendNotification.Task(EInteractionType.Duty, text)))
{
chatGui.PrintError(text, "Questionable", 576);
}
return false;
}
autoDutyIpc.StartInstance(base.Task.ContentFinderConditionId, base.Task.DutyMode);
return true;
}
public override ETaskResult Update()
{
if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
{
throw new TaskException("Failed to get territory ID for content finder condition");
}
if (clientState.TerritoryType != contentFinderConditionData.TerritoryId)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public void StopNow()
{
autoDutyIpc.Stop();
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : ITask
{
public override string ToString()
{
return $"Wait(AutoDuty, left instance {ContentFinderConditionId})";
}
}
internal sealed class WaitAutoDutyExecutor(AutoDutyIpc autoDutyIpc, TerritoryData territoryData, IClientState clientState) : TaskExecutor<WaitAutoDutyTask>(), IStoppableTaskExecutor, ITaskExecutor
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
{
throw new TaskException("Failed to get territory ID for content finder condition");
}
if (clientState.TerritoryType == contentFinderConditionData.TerritoryId || !autoDutyIpc.IsStopped())
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public void StopNow()
{
autoDutyIpc.Stop();
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record OpenDutyFinderTask(uint ContentFinderConditionId) : ITask
{
public override string ToString()
{
return $"OpenDutyFinder({ContentFinderConditionId})";
}
}
internal sealed class OpenDutyFinderExecutor(GameFunctions gameFunctions, ICondition condition) : TaskExecutor<OpenDutyFinderTask>()
{
protected override bool Start()
{
if (condition[ConditionFlag.InDutyQueue])
{
return false;
}
gameFunctions.OpenDutyFinder(base.Task.ContentFinderConditionId);
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Emote
{
internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((interactionType == EInteractionType.SinglePlayerDuty || (uint)(interactionType - 28) <= 1u) ? true : false)
{
if (!step.Emote.HasValue)
{
return Array.Empty<ITask>();
}
}
else if (step.InteractionType != EInteractionType.Emote)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.Emote, "step.Emote");
Mount.UnmountTask unmountTask = new Mount.UnmountTask();
if (step.DataId.HasValue)
{
UseOnObject useOnObject = new UseOnObject(step.Emote.Value, step.DataId.Value);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, useOnObject });
}
UseOnSelf useOnSelf = new UseOnSelf(step.Emote.Value);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, useOnSelf });
}
}
internal sealed record UseOnObject(EEmote Emote, uint DataId) : ITask
{
public override string ToString()
{
return $"Emote({Emote} on {DataId})";
}
}
internal sealed class UseOnObjectExecutor(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<UseOnObject>()
{
protected override bool StartInternal()
{
chatFunctions.UseEmote(base.Task.DataId, base.Task.Emote);
return true;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed record UseOnSelf(EEmote Emote) : ITask
{
public override string ToString()
{
return $"Emote({Emote})";
}
}
internal sealed class UseOnSelfExecutor(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<UseOnSelf>()
{
protected override bool StartInternal()
{
chatFunctions.UseEmote(base.Task.Emote);
return true;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,239 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using LLib;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class EquipItem
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Questionable.Model.Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.EquipItem)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.ItemId, "step.ItemId");
return new Task(step.ItemId.Value);
}
}
internal sealed record Task(uint ItemId) : ITask
{
public override string ToString()
{
return $"Equip({ItemId})";
}
}
internal sealed class DoEquip(IDataManager dataManager, ILogger<DoEquip> logger) : TaskExecutor<Task>(), IToastAware, ITaskExecutor
{
private const int MaxAttempts = 3;
private static readonly IReadOnlyList<InventoryType> SourceInventoryTypes = new global::_003C_003Ez__ReadOnlyArray<InventoryType>(new InventoryType[16]
{
InventoryType.ArmoryMainHand,
InventoryType.ArmoryOffHand,
InventoryType.ArmoryHead,
InventoryType.ArmoryBody,
InventoryType.ArmoryHands,
InventoryType.ArmoryLegs,
InventoryType.ArmoryFeets,
InventoryType.ArmoryEar,
InventoryType.ArmoryNeck,
InventoryType.ArmoryWrist,
InventoryType.ArmoryRings,
InventoryType.ArmorySoulCrystal,
InventoryType.Inventory1,
InventoryType.Inventory2,
InventoryType.Inventory3,
InventoryType.Inventory4
});
private int _attempts;
private Item? _item;
private List<ushort> _targetSlots;
private DateTime _continueAt = DateTime.MaxValue;
protected override bool Start()
{
_item = dataManager.GetExcelSheet<Item>().GetRowOrDefault(base.Task.ItemId) ?? throw new ArgumentOutOfRangeException("ItemId");
_targetSlots = GetEquipSlot(_item) ?? throw new InvalidOperationException("Not a piece of equipment");
Equip();
_continueAt = DateTime.Now.AddSeconds(1.0);
return true;
}
public unsafe override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
{
return ETaskResult.StillRunning;
}
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return ETaskResult.StillRunning;
}
foreach (ushort targetSlot in _targetSlots)
{
InventoryItem* inventorySlot = ptr->GetInventorySlot(InventoryType.EquippedItems, targetSlot);
if (inventorySlot != null && inventorySlot->ItemId == base.Task.ItemId)
{
return ETaskResult.TaskComplete;
}
}
Equip();
_continueAt = DateTime.Now.AddSeconds(1.0);
return ETaskResult.StillRunning;
}
private unsafe void Equip()
{
_attempts++;
if (_attempts > 3)
{
throw new TaskException("Unable to equip gear.");
}
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null)
{
return;
}
InventoryContainer* inventoryContainer = inventoryManager->GetInventoryContainer(InventoryType.EquippedItems);
if (inventoryContainer == null)
{
return;
}
foreach (ushort targetSlot in _targetSlots)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(targetSlot);
if (inventorySlot != null && inventorySlot->ItemId == base.Task.ItemId)
{
logger.LogInformation("Already equipped {Item}, skipping step", _item?.Name.ToString());
return;
}
}
foreach (InventoryType sourceInventoryType in SourceInventoryTypes)
{
InventoryContainer* inventoryContainer2 = inventoryManager->GetInventoryContainer(sourceInventoryType);
if (inventoryContainer2 == null || (inventoryManager->GetItemCountInContainer(base.Task.ItemId, sourceInventoryType, isHq: true, 0) == 0 && inventoryManager->GetItemCountInContainer(base.Task.ItemId, sourceInventoryType, isHq: false, 0) == 0))
{
continue;
}
for (ushort num = 0; num < inventoryContainer2->Size; num++)
{
InventoryItem* inventorySlot2 = inventoryContainer2->GetInventorySlot(num);
if (inventorySlot2 != null && inventorySlot2->ItemId == base.Task.ItemId)
{
ushort num2 = _targetSlots.Where(delegate(ushort x)
{
InventoryItem* inventorySlot3 = inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x);
return inventorySlot3 == null || inventorySlot3->ItemId == 0;
}).Concat(_targetSlots).First();
logger.LogInformation("Equipping item from {SourceInventory}, {SourceSlot} to {TargetInventory}, {TargetSlot}", sourceInventoryType, num, InventoryType.EquippedItems, num2);
int num3 = inventoryManager->MoveItemSlot(sourceInventoryType, num, InventoryType.EquippedItems, num2, a6: true);
logger.LogInformation("MoveItemSlot result: {Result}", num3);
return;
}
}
}
throw new TaskException($"Could not equip item {base.Task.ItemId}.");
}
private static List<ushort>? GetEquipSlot(Item? item)
{
if (!item.HasValue)
{
return new List<ushort>();
}
Span<ushort> span;
switch (item.Value.EquipSlotCategory.RowId)
{
case 1u:
case 2u:
case 3u:
case 4u:
case 5u:
case 6u:
case 7u:
case 8u:
case 9u:
case 10u:
case 11u:
{
int index = 1;
List<ushort> list4 = new List<ushort>(index);
CollectionsMarshal.SetCount(list4, index);
span = CollectionsMarshal.AsSpan(list4);
int num = 0;
span[num] = (ushort)(item.Value.EquipSlotCategory.RowId - 1);
return list4;
}
case 12u:
{
int num = 2;
List<ushort> list3 = new List<ushort>(num);
CollectionsMarshal.SetCount(list3, num);
span = CollectionsMarshal.AsSpan(list3);
int index = 0;
span[index] = 11;
index++;
span[index] = 12;
return list3;
}
case 13u:
{
int index = 1;
List<ushort> list2 = new List<ushort>(index);
CollectionsMarshal.SetCount(list2, index);
span = CollectionsMarshal.AsSpan(list2);
int num = 0;
span[num] = 0;
return list2;
}
case 17u:
{
int num = 1;
List<ushort> list = new List<ushort>(num);
CollectionsMarshal.SetCount(list, num);
span = CollectionsMarshal.AsSpan(list);
int index = 0;
span[index] = 13;
return list;
}
default:
return null;
}
}
public bool OnErrorToast(SeString message)
{
string b = dataManager.GetString(709u, (LogMessage x) => x.Text);
if (GameFunctions.GameStringEquals(message.TextValue, b))
{
_attempts = 3;
}
return false;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,124 @@
using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.Interop;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class EquipRecommended
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.EquipRecommended)
{
return null;
}
return new EquipTask();
}
}
internal sealed class BeforeDutyOrInstance : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Duty && step.InteractionType != EInteractionType.SinglePlayerDuty && step.InteractionType != EInteractionType.Combat)
{
return null;
}
return new EquipTask();
}
}
internal sealed class EquipTask : ITask
{
public override string ToString()
{
return "EquipRecommended";
}
}
internal sealed class DoEquipRecommended(IClientState clientState, IChatGui chatGui, ICondition condition) : TaskExecutor<EquipTask>()
{
private bool _checkedOrTriggeredEquipmentUpdate;
private DateTime _continueAt = DateTime.MinValue;
protected unsafe override bool Start()
{
if (condition[ConditionFlag.InCombat])
{
return false;
}
RecommendEquipModule.Instance()->SetupForClassJob((byte)clientState.LocalPlayer.ClassJob.RowId);
return true;
}
public unsafe override ETaskResult Update()
{
RecommendEquipModule* ptr = RecommendEquipModule.Instance();
if (ptr->IsUpdating)
{
return ETaskResult.StillRunning;
}
if (!_checkedOrTriggeredEquipmentUpdate)
{
if (!IsAllRecommendeGearEquipped())
{
chatGui.Print("Equipping recommended gear.", "Questionable", 576);
ptr->EquipRecommendedGear();
_continueAt = DateTime.Now.AddSeconds(1.0);
}
_checkedOrTriggeredEquipmentUpdate = true;
return ETaskResult.StillRunning;
}
if (!(DateTime.Now >= _continueAt))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
private unsafe bool IsAllRecommendeGearEquipped()
{
RecommendEquipModule* intPtr = RecommendEquipModule.Instance();
InventoryContainer* inventoryContainer = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems);
bool result = true;
Span<Pointer<InventoryItem>> recommendedItems = intPtr->RecommendedItems;
for (int i = 0; i < recommendedItems.Length; i++)
{
Pointer<InventoryItem> pointer = recommendedItems[i];
InventoryItem* value = pointer.Value;
if (value == null || value->ItemId == 0)
{
continue;
}
bool flag = false;
for (int j = 0; j < inventoryContainer->Size; j++)
{
InventoryItem inventoryItem = inventoryContainer->Items[j];
if (inventoryItem.ItemId != 0 && inventoryItem.ItemId == value->ItemId)
{
flag = true;
break;
}
}
if (!flag)
{
result = false;
}
}
return result;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,337 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Interact
{
internal sealed class Factory(AutomatonIpc automatonIpc, Configuration configuration) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((interactionType == EInteractionType.SinglePlayerDuty || (uint)(interactionType - 28) <= 1u) ? true : false)
{
if ((step.InteractionType == EInteractionType.CompleteQuest && configuration.Advanced.PreventQuestCompletion) || step.Emote.HasValue || step.ChatMessage != null || step.ItemId.HasValue || !step.DataId.HasValue)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.PurchaseItem)
{
if (!step.DataId.HasValue)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.Snipe)
{
if (!automatonIpc.IsAutoSnipeEnabled)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.UnlockTaxiStand)
{
if (!step.TaxiStandId.HasValue)
{
yield break;
}
}
else if (step.InteractionType != EInteractionType.Interact)
{
yield break;
}
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0)
{
yield return new WaitAtEnd.WaitDelay();
}
uint value = step.DataId.Value;
EInteractionType interactionType2 = step.InteractionType;
int skipMarkerCheck;
if (!step.TargetTerritoryId.HasValue && !(quest.Id is SatisfactionSupplyNpcId))
{
SkipConditions skipConditions = step.SkipConditions;
if (skipConditions != null)
{
SkipStepConditions stepIf = skipConditions.StepIf;
if (stepIf != null && stepIf.Never)
{
goto IL_0247;
}
}
if (step.InteractionType != EInteractionType.PurchaseItem)
{
skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0);
goto IL_0248;
}
}
goto IL_0247;
IL_0247:
skipMarkerCheck = 1;
goto IL_0248;
IL_0248:
yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, step.PickUpItemId, step.TaxiStandId, step.SkipConditions?.StepIf, step.CompletionQuestVariablesFlags);
}
}
internal sealed record Task : ITask
{
public uint DataId { get; init; }
public Quest? Quest { get; init; }
public EInteractionType InteractionType { get; init; }
public bool SkipMarkerCheck { get; init; }
public uint? PickUpItemId { get; init; }
public byte? TaxiStandId { get; init; }
public SkipStepConditions? SkipConditions { get; init; }
public List<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
public bool HasCompletionQuestVariablesFlags { get; }
public Task(uint DataId, Quest? Quest, EInteractionType InteractionType, bool SkipMarkerCheck = false, uint? PickUpItemId = null, byte? TaxiStandId = null, SkipStepConditions? SkipConditions = null, List<QuestWorkValue?>? CompletionQuestVariablesFlags = null)
{
this.DataId = DataId;
this.Quest = Quest;
this.InteractionType = InteractionType;
this.SkipMarkerCheck = SkipMarkerCheck;
this.PickUpItemId = PickUpItemId;
this.TaxiStandId = TaxiStandId;
this.SkipConditions = SkipConditions;
this.CompletionQuestVariablesFlags = CompletionQuestVariablesFlags ?? new List<QuestWorkValue>();
HasCompletionQuestVariablesFlags = Quest != null && CompletionQuestVariablesFlags != null && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags);
base._002Ector();
}
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"Interact{(HasCompletionQuestVariablesFlags ? "*" : "")}({DataId})";
}
[CompilerGenerated]
public void Deconstruct(out uint DataId, out Quest? Quest, out EInteractionType InteractionType, out bool SkipMarkerCheck, out uint? PickUpItemId, out byte? TaxiStandId, out SkipStepConditions? SkipConditions, out List<QuestWorkValue?>? CompletionQuestVariablesFlags)
{
DataId = this.DataId;
Quest = this.Quest;
InteractionType = this.InteractionType;
SkipMarkerCheck = this.SkipMarkerCheck;
PickUpItemId = this.PickUpItemId;
TaxiStandId = this.TaxiStandId;
SkipConditions = this.SkipConditions;
CompletionQuestVariablesFlags = this.CompletionQuestVariablesFlags;
}
}
internal sealed class DoInteract(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<DoInteract> logger) : TaskExecutor<Task>(), IConditionChangeAware, ITaskExecutor
{
private enum EInteractionState
{
None,
InteractionTriggered,
InteractionConfirmed
}
private bool _needsUnmount;
private EInteractionState _interactionState;
private DateTime _continueAt = DateTime.MinValue;
private bool delayedFinalCheck;
public Quest? Quest => base.Task.Quest;
public EInteractionType InteractionType { get; set; }
protected override bool Start()
{
InteractionType = base.Task.InteractionType;
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId);
if (gameObject == null)
{
logger.LogWarning("No game object with dataId {DataId}", base.Task.DataId);
return false;
}
if (!gameObject.IsTargetable)
{
SkipStepConditions skipConditions = base.Task.SkipConditions;
if (skipConditions != null && !skipConditions.Never && skipConditions.NotTargetable)
{
logger.LogInformation("Not interacting with {DataId} because it is not targetable (but skippable)", base.Task.DataId);
return false;
}
}
if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] && gameObject.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.GatheringPoint)
{
logger.LogInformation("Preparing interaction for {DataId} by unmounting", base.Task.DataId);
_needsUnmount = true;
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1.0);
return true;
}
if (gameObject.IsTargetable && HasAnyMarker(gameObject))
{
TriggerInteraction(gameObject);
return true;
}
return true;
}
public unsafe override ETaskResult Update()
{
if (DateTime.Now <= _continueAt)
{
return ETaskResult.StillRunning;
}
if (_needsUnmount)
{
if (condition[ConditionFlag.Mounted])
{
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1.0);
return ETaskResult.StillRunning;
}
_needsUnmount = false;
}
uint? pickUpItemId = base.Task.PickUpItemId;
if (pickUpItemId.HasValue)
{
uint valueOrDefault = pickUpItemId.GetValueOrDefault();
if (InventoryManager.Instance()->GetInventoryItemCount(valueOrDefault, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0)
{
return ETaskResult.TaskComplete;
}
}
else
{
byte? taxiStandId = base.Task.TaxiStandId;
if (taxiStandId.HasValue)
{
byte valueOrDefault2 = taxiStandId.GetValueOrDefault();
if (UIState.Instance()->IsChocoboTaxiStandUnlocked(valueOrDefault2))
{
return ETaskResult.TaskComplete;
}
}
else
{
if (InteractionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
{
return ETaskResult.TaskComplete;
}
if (Quest != null && base.Task.HasCompletionQuestVariablesFlags)
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(Quest.Id);
if (questProgressInfo == null || !QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
if (base.ProgressContext != null)
{
if (base.ProgressContext.WasInterrupted())
{
return ETaskResult.StillRunning;
}
if (base.ProgressContext.WasSuccessful() || _interactionState == EInteractionState.InteractionConfirmed)
{
if (delayedFinalCheck)
{
return ETaskResult.TaskComplete;
}
_continueAt = DateTime.Now.AddSeconds(0.2);
delayedFinalCheck = true;
return ETaskResult.StillRunning;
}
}
}
}
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId);
if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
{
return ETaskResult.StillRunning;
}
TriggerInteraction(gameObject);
return ETaskResult.StillRunning;
}
private void TriggerInteraction(IGameObject gameObject)
{
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(delegate
{
if (gameFunctions.InteractWith(gameObject))
{
_interactionState = EInteractionState.InteractionTriggered;
}
else
{
_interactionState = EInteractionState.None;
}
return _interactionState != EInteractionState.None;
});
_continueAt = DateTime.Now.AddSeconds(0.5);
}
private unsafe bool HasAnyMarker(IGameObject gameObject)
{
if (base.Task.SkipMarkerCheck || gameObject.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.EventNpc)
{
return true;
}
GameObject* address = (GameObject*)gameObject.Address;
return address->NamePlateIconId != 0;
}
public void OnConditionChange(ConditionFlag flag, bool value)
{
if (base.ProgressContext == null || (!base.ProgressContext.WasInterrupted() && !base.ProgressContext.WasSuccessful()))
{
logger.LogDebug("Condition change: {Flag} = {Value}", flag, value);
bool flag2 = _interactionState == EInteractionState.InteractionTriggered;
if (flag2)
{
bool flag3 = (uint)(flag - 31) <= 1u;
flag2 = flag3;
}
if (flag2 && value)
{
logger.LogInformation("Interaction was most likely triggered");
_interactionState = EInteractionState.InteractionConfirmed;
}
}
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Jump
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Jump)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.JumpDestination, "step.JumpDestination");
if (step.JumpDestination.Type == EJumpType.SingleJump)
{
return new SingleJumpTask(step.DataId, step.JumpDestination, step.Comment);
}
return new RepeatedJumpTask(step.DataId, step.JumpDestination, step.Comment);
}
}
internal interface IJumpTask : ITask
{
uint? DataId { get; }
JumpDestination JumpDestination { get; }
string? Comment { get; }
}
internal sealed record SingleJumpTask(uint? DataId, JumpDestination JumpDestination, string? Comment) : IJumpTask, ITask
{
public override string ToString()
{
return "Jump(" + Comment + ")";
}
}
internal abstract class JumpBase<T>(MovementController movementController, IClientState clientState, IFramework framework) : TaskExecutor<T>() where T : class, IJumpTask
{
protected unsafe override bool Start()
{
float num = base.Task.JumpDestination.CalculateStopDistance();
if ((_003CclientState_003EP.LocalPlayer.Position - base.Task.JumpDestination.Position).Length() <= num)
{
return false;
}
MovementController movementController = _003CmovementController_003EP;
uint? dataId = base.Task.DataId;
int num2 = 1;
List<Vector3> list = new List<Vector3>(num2);
CollectionsMarshal.SetCount(list, num2);
Span<Vector3> span = CollectionsMarshal.AsSpan(list);
int index = 0;
span[index] = base.Task.JumpDestination.Position;
movementController.NavigateTo(EMovementType.Quest, dataId, list, fly: false, sprint: false, base.Task.JumpDestination.StopDistance ?? num);
_003Cframework_003EP.RunOnTick(delegate
{
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
}, TimeSpan.FromSeconds(base.Task.JumpDestination.DelaySeconds ?? 0.5f));
return true;
}
public override ETaskResult Update()
{
if (_003CmovementController_003EP.IsPathfinding || _003CmovementController_003EP.IsPathRunning)
{
return ETaskResult.StillRunning;
}
DateTime movementStartedAt = _003CmovementController_003EP.MovementStartedAt;
if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(1.0) >= DateTime.Now)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed class DoSingleJump : JumpBase<SingleJumpTask>
{
public DoSingleJump(MovementController movementController, IClientState clientState, IFramework framework)
: base(movementController, clientState, framework)
{
}
}
internal sealed record RepeatedJumpTask(uint? DataId, JumpDestination JumpDestination, string? Comment) : IJumpTask, ITask
{
public override string ToString()
{
return "RepeatedJump(" + Comment + ")";
}
}
internal sealed class DoRepeatedJumps : JumpBase<RepeatedJumpTask>
{
private readonly IClientState _clientState;
private DateTime _continueAt;
private int _attempts;
public DoRepeatedJumps(MovementController movementController, IClientState clientState, IFramework framework, ICondition condition, ILogger<DoRepeatedJumps> logger)
{
_003Ccondition_003EP = condition;
_003Clogger_003EP = logger;
_clientState = clientState;
_continueAt = DateTime.MinValue;
base._002Ector(movementController, clientState, framework);
}
protected override bool Start()
{
_continueAt = DateTime.Now + TimeSpan.FromSeconds(2f * (base.Task.JumpDestination.DelaySeconds ?? 0.5f));
return base.Start();
}
public unsafe override ETaskResult Update()
{
if (DateTime.Now < _continueAt || _003Ccondition_003EP[ConditionFlag.Jumping])
{
return ETaskResult.StillRunning;
}
float num = base.Task.JumpDestination.CalculateStopDistance();
if ((_clientState.LocalPlayer.Position - base.Task.JumpDestination.Position).Length() <= num || _clientState.LocalPlayer.Position.Y >= base.Task.JumpDestination.Position.Y - 0.5f)
{
return ETaskResult.TaskComplete;
}
_003Clogger_003EP.LogTrace("Y-Heights for jumps: player={A}, target={B}", _clientState.LocalPlayer.Position.Y, base.Task.JumpDestination.Position.Y - 0.5f);
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null))
{
_attempts++;
}
if (_attempts >= 50)
{
throw new TaskException("Tried to jump too many times, didn't reach the target");
}
_continueAt = DateTime.Now + TimeSpan.FromSeconds(base.Task.JumpDestination.DelaySeconds ?? 0.5f);
return ETaskResult.StillRunning;
}
}
}

View file

@ -0,0 +1,24 @@
using System;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class PurchaseItem
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.PurchaseItem)
{
return null;
}
throw new NotImplementedException();
}
}
internal sealed class PurchaseRequest
{
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Say
{
internal sealed class Factory(ExcelFunctions excelFunctions) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((uint)(interactionType - 28) <= 1u)
{
if (step.ChatMessage == null)
{
return Array.Empty<ITask>();
}
}
else if (step.InteractionType != EInteractionType.Say)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.ChatMessage, "step.ChatMessage");
string? text = excelFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key, isRegex: false).GetString();
ArgumentNullException.ThrowIfNull(text, "excelString");
Mount.UnmountTask unmountTask = new Mount.UnmountTask();
Task task = new Task(text);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, task });
}
}
internal sealed record Task(string ChatMessage) : ITask
{
public override string ToString()
{
return "Say(" + ChatMessage + ")";
}
}
internal sealed class UseChat(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<Task>()
{
protected override bool StartInternal()
{
chatFunctions.ExecuteCommand("/say " + base.Task.ChatMessage);
return true;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,268 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class SinglePlayerDuty
{
internal static class SpecialTerritories
{
public const ushort Lahabrea = 1052;
public const ushort ItsProbablyATrap = 665;
public const ushort Naadam = 688;
}
internal sealed class Factory(BossModIpc bossModIpc, TerritoryData territoryData, ICondition condition, IClientState clientState) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.SinglePlayerDuty || !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
{
yield break;
}
if (!territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, step.SinglePlayerDutyIndex, out TerritoryData.ContentFinderConditionData cfcData))
{
throw new TaskException("Failed to get content finder condition for solo instance");
}
yield return new Mount.UnmountTask();
yield return new StartSinglePlayerDuty(cfcData.ContentFinderConditionId);
yield return new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(2L));
yield return new EnableAi(cfcData.TerritoryId == 688);
if (cfcData.TerritoryId == 1052)
{
yield return new SetTarget(14643u);
yield return new WaitCondition.Task(() => condition[ConditionFlag.Unconscious] || clientState.TerritoryType != 1052, "Wait(death)");
yield return new DisableAi();
yield return new WaitCondition.Task(() => !condition[ConditionFlag.Unconscious] || clientState.TerritoryType != 1052, "Wait(resurrection)");
yield return new EnableAi();
}
else if (cfcData.TerritoryId == 665)
{
yield return new WaitCondition.Task(() => DutyActionsAvailable() || clientState.TerritoryType != 665, "Wait(Phase 2)");
yield return new EnableAi(Passive: true);
}
else if (cfcData.TerritoryId == 688)
{
yield return new WaitCondition.Task(delegate
{
if (clientState.TerritoryType != 688)
{
return true;
}
Vector3 vector = clientState.LocalPlayer?.Position ?? default(Vector3);
return (new Vector3(352.01f, -1.45f, 288.59f) - vector).Length() < 10f;
}, "Wait(moving to Ovoo)");
yield return new Mount.UnmountTask();
yield return new EnableAi();
}
yield return new WaitSinglePlayerDuty(cfcData.ContentFinderConditionId);
yield return new DisableAi();
yield return new WaitAtEnd.WaitNextStepOrSequence();
}
private unsafe bool DutyActionsAvailable()
{
ContentDirector* contentDirector = EventFramework.Instance()->GetContentDirector();
if (contentDirector != null)
{
return contentDirector->DutyActionManager.ActionsPresent;
}
return false;
}
}
internal sealed record StartSinglePlayerDuty(uint ContentFinderConditionId) : ITask
{
public override string ToString()
{
return $"Wait(BossMod, entered instance {ContentFinderConditionId})";
}
}
internal sealed class StartSinglePlayerDutyExecutor(ICondition condition) : TaskExecutor<StartSinglePlayerDuty>()
{
private DateTime _enteredAt = DateTime.MinValue;
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
if (GameMain.Instance()->CurrentContentFinderConditionId != base.Task.ContentFinderConditionId)
{
return ETaskResult.StillRunning;
}
if (!condition[ConditionFlag.BoundByDuty])
{
return ETaskResult.StillRunning;
}
if (_enteredAt == DateTime.MinValue)
{
_enteredAt = DateTime.Now;
}
if (!(DateTime.Now - _enteredAt >= TimeSpan.FromSeconds(2L)))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record EnableAi(bool Passive = false) : ITask
{
public override string ToString()
{
return "BossMod.EnableAi(" + (Passive ? "Passive" : "AutoPull") + ")";
}
}
internal sealed class EnableAiExecutor(BossModIpc bossModIpc) : TaskExecutor<EnableAi>()
{
protected override bool Start()
{
bossModIpc.EnableAi(base.Task.Passive);
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record WaitSinglePlayerDuty(uint ContentFinderConditionId) : ITask
{
public override string ToString()
{
return $"Wait(BossMod, left instance {ContentFinderConditionId})";
}
}
internal sealed class WaitSinglePlayerDutyExecutor(BossModIpc bossModIpc, MovementController movementController) : TaskExecutor<WaitSinglePlayerDuty>(), IStoppableTaskExecutor, ITaskExecutor, IDebugStateProvider
{
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
if (GameMain.Instance()->CurrentContentFinderConditionId == base.Task.ContentFinderConditionId)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public void StopNow()
{
bossModIpc.DisableAi();
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
public string? GetDebugState()
{
if (!movementController.IsNavmeshReady)
{
return $"Navmesh: {movementController.BuiltNavmeshPercent}%";
}
return null;
}
}
internal sealed record DisableAi : ITask
{
public override string ToString()
{
return "BossMod.DisableAi";
}
}
internal sealed class DisableAiExecutor(BossModIpc bossModIpc) : TaskExecutor<DisableAi>()
{
protected override bool Start()
{
bossModIpc.DisableAi();
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record SetTarget(uint DataId) : ITask
{
public override string ToString()
{
return $"SetTarget({DataId})";
}
}
internal sealed class SetTargetExecutor(ITargetManager targetManager, IObjectTable objectTable) : TaskExecutor<SetTarget>()
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
if (targetManager.Target?.DataId == base.Task.DataId)
{
return ETaskResult.TaskComplete;
}
IGameObject gameObject = objectTable.FirstOrDefault((IGameObject x) => x.DataId == base.Task.DataId);
if (gameObject == null)
{
return ETaskResult.StillRunning;
}
targetManager.Target = gameObject;
return ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,62 @@
using System;
using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class StatusOff
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.StatusOff)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.Status, "step.Status");
return new Task(step.Status.Value);
}
}
internal sealed record Task(EStatus Status) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"StatusOff({Status})";
}
}
internal sealed class DoStatusOff(GameFunctions gameFunctions) : AbstractDelayedTaskExecutor<Task>()
{
protected override bool StartInternal()
{
if (gameFunctions.HasStatus(base.Task.Status))
{
return GameFunctions.RemoveStatus(base.Task.Status);
}
return false;
}
public override ETaskResult Update()
{
if (!gameFunctions.HasStatus(base.Task.Status))
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,293 @@
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<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);
Vector3? position = (sequence.Steps.Skip(num + 1).FirstOrDefault() ?? step).Position;
return new global::_003C_003Ez__ReadOnlyArray<ITask>(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<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 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<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
bool StartingCombat { get; }
}
internal abstract class UseItemExecutorBase<T>(QuestFunctions questFunctions, ICondition condition, ILogger logger) : TaskExecutor<T>() 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<QuestWorkValue?> 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<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);
}
}
}