"Z": 365.7129
},
"TerritoryId": 958,
- "InteractionType": "WaitForManualProgress",
+ "InteractionType": "SinglePlayerDuty",
"Comment": "Follow Alphinaud and Alisaie",
"DialogueChoices": [
{
null,
null,
null,
- 128
+ -128
]
},
{
"Z": -503.2578
},
"TerritoryId": 958,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 128
+ ]
},
{
"DataId": 2012094,
"TerritoryId": 958,
"InteractionType": "Interact",
"Comment": "Map",
- "Mount": true
+ "Mount": true,
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 64
+ ]
},
{
"DataId": 2012005,
},
"TerritoryId": 958,
"InteractionType": "Interact",
- "Comment": "Warmachine Wreckage"
+ "Comment": "Warmachine Wreckage",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 32
+ ]
},
{
"DataId": 2012096,
},
"TerritoryId": 958,
"InteractionType": "Interact",
- "Comment": "Children's Slide"
+ "Comment": "Children's Slide",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 16
+ ]
}
]
},
{
"Sequence": 1,
"Steps": [
+ {
+ "Position": {
+ "X": -148.48793,
+ "Y": -10.30035,
+ "Z": -247.25652
+ },
+ "TerritoryId": 956,
+ "InteractionType": "WalkTo",
+ "Comment": "Avoids Combat"
+ },
{
"DataId": 2011987,
"Position": {
"Mount": true,
"DisableNavmesh": true
},
+ {
+ "Position": {
+ "X": -480.30975,
+ "Y": -22.946651,
+ "Z": -145.08534
+ },
+ "TerritoryId": 956,
+ "InteractionType": "WalkTo",
+ "Comment": "Avoids Combat (typically)"
+ },
{
"DataId": 2011988,
"Position": {
if (_keyState[VirtualKey.ESCAPE])
{
- Stop();
+ if (_currentTask != null || _taskQueue.Count > 0)
+ Stop("ESC pressed");
_movementController.Stop();
}
{
_logger.LogInformation("No current quest, resetting data");
CurrentQuest = null;
- Stop();
+ Stop("Resetting current quest");
}
}
else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
{
_logger.LogInformation("New quest: {QuestName}", quest.Name);
CurrentQuest = new QuestProgress(quest, currentSequence, 0);
+ Stop("Different Quest");
}
else if (CurrentQuest != null)
{
_logger.LogInformation("No active quest anymore? Not sure what happened...");
CurrentQuest = null;
+ Stop("No active Quest");
}
- Stop();
return;
}
{
DebugState = "No quest active";
Comment = null;
- Stop();
+ Stop("No quest active");
return;
}
if (CurrentQuest.Sequence != currentSequence)
{
CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 };
-
- bool automatic = _automatic;
- Stop();
- if (automatic)
- ExecuteNextStep(true);
+ Stop("New sequence", continueIfAutomatic: true);
}
var q = CurrentQuest.Quest;
{
DebugState = "Sequence not found";
Comment = null;
- Stop();
+ Stop("Unknown sequence");
return;
}
{
DebugState = "Step completed";
Comment = null;
- Stop();
+ if (_currentTask != null || _taskQueue.Count > 0)
+ Stop("Step complete", continueIfAutomatic: true);
return;
}
{
DebugState = "Step not found";
Comment = null;
- Stop();
+ Stop("Unknown step");
return;
}
*/
}
- public void Stop()
+ private void ClearTasksInternal()
{
_currentTask = null;
if (_taskQueue.Count > 0)
_taskQueue.Clear();
+ }
+
+ public void Stop(string label, bool continueIfAutomatic = false)
+ {
+ using var scope = _logger.BeginScope(label);
+
+ ClearTasksInternal();
// reset task queue
- _automatic = false;
+ if (continueIfAutomatic && _automatic)
+ {
+ if (CurrentQuest?.Step is >= 0 and < 255)
+ ExecuteNextStep(true);
+ }
+ else
+ {
+ _logger.LogInformation("Stopping automatic questing");
+ _automatic = false;
+ }
}
private void UpdateCurrentTask()
catch (Exception e)
{
_logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString());
- Stop();
+ Stop("Task failed to start");
return;
}
}
catch (Exception e)
{
_logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString());
- Stop();
+ Stop("Task failed to update");
return;
}
return;
case ETaskResult.SkipRemainingTasksForStep:
- _logger.LogInformation("Result: {Result}, skipping remaining tasks for step", result);
+ _logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
+ _currentTask, result);
_currentTask = null;
while (_taskQueue.TryDequeue(out ITask? nextTask))
return;
case ETaskResult.TaskComplete:
- _logger.LogInformation("Result: {Result}, remaining tasks: {RemainingTaskCount}", result,
- _taskQueue.Count);
+ _logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}",
+ _currentTask, result, _taskQueue.Count);
_currentTask = null;
// handled in next update
return;
case ETaskResult.NextStep:
- _logger.LogInformation("Result: {Result}", result);
+ _logger.LogInformation("{Task} → {Result}", _currentTask, result);
IncreaseStepCount(true);
return;
case ETaskResult.End:
- _logger.LogInformation("Result: {Result}", result);
- Stop();
+ _logger.LogInformation("{Task} → {Result}", _currentTask, result);
+ Stop("Task end");
return;
}
}
public void ExecuteNextStep(bool automatic)
{
- Stop();
+ ClearTasksInternal();
_automatic = automatic;
(QuestSequence? seq, QuestStep? step) = GetNextStep();
return;
}
+ _logger.LogInformation("Tasks for {QuestId}, {Sequence}, {Step}: {Tasks}",
+ CurrentQuest.Quest.QuestId, seq.Sequence, seq.Steps.IndexOf(step),
+ string.Join(", ", newTasks.Select(x => x.ToString())));
foreach (var task in newTasks)
_taskQueue.Enqueue(task);
}
return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
}
+ public bool HasCurrentTaskMatching<T>() =>
+ _currentTask is T;
+
public sealed record QuestProgress(
Quest Quest,
byte Sequence,
{
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
- if (step.SkipIf.Count == 0)
+ if (step.SkipIf.Contains(ESkipCondition.Never))
return null;
- if (step.SkipIf.Contains(ESkipCondition.Never))
+ if (step.SkipIf.Count == 0 && step.CompletionQuestVariablesFlags.Count == 0)
return null;
return serviceProvider.GetRequiredService<CheckTask>()
using System.Globalization;
using System.Linq;
using System.Numerics;
+using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.BaseTasks;
internal static class WaitAtEnd
{
- internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ internal sealed class Factory(IServiceProvider serviceProvider, IClientState clientState) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
new NextStep()
];
+ case EInteractionType.Interact when step.TargetTerritoryId != null:
+ ITask waitInteraction;
+ if (step.TerritoryId != step.TargetTerritoryId)
+ {
+ // interaction moves to a different territory
+ waitInteraction = new WaitConditionTask(() => clientState.TerritoryType == step.TerritoryId,
+ $"Wait(tp to territory: {step.TerritoryId})");
+ }
+ else
+ {
+ Vector3 lastPosition = step.Position ?? clientState.LocalPlayer?.Position ?? Vector3.Zero;
+ waitInteraction = new WaitConditionTask(() =>
+ {
+ Vector3? currentPosition = clientState.LocalPlayer?.Position;
+ if (currentPosition == null)
+ return false;
+
+ // interaction moved to elsewhere in the zone
+ return (lastPosition - currentPosition.Value).Length() > 20;
+ }, $"Wait(tp away from {lastPosition.ToString("G", CultureInfo.InvariantCulture)})");
+ }
+
+ return
+ [
+ waitInteraction,
+ serviceProvider.GetRequiredService<WaitDelay>(),
+ new NextStep()
+ ];
+
+ case EInteractionType.Interact:
default:
return [serviceProvider.GetRequiredService<WaitDelay>(), new NextStep()];
}
public ETaskResult Update() => ETaskResult.NextStep;
- public override string ToString() => "Next Step";
+ public override string ToString() => "NextStep";
}
internal sealed class EndAutomation : ILastTask
public ETaskResult Update() => ETaskResult.End;
- public override string ToString() => "End automation";
+ public override string ToString() => "EndAutomation";
}
}
}
if (gameFunctions.HasStatusPreventingSprintOrMount())
+ {
+ logger.LogInformation("Can't mount due to status preventing sprint or mount");
return false;
+ }
logger.LogInformation("Step wants a mount, trying to mount in territory {Id}...", _territoryId);
if (!condition[ConditionFlag.InCombat])
{
if (!_mountTriggered)
{
+ if (gameFunctions.HasStatusPreventingSprintOrMount())
+ {
+ logger.LogInformation("Can't mount due to status preventing sprint or mount");
+ return ETaskResult.TaskComplete;
+ }
+
_mountTriggered = gameFunctions.Mount();
return ETaskResult.StillRunning;
}
ArgumentNullException.ThrowIfNull(step.DataId);
var task = serviceProvider.GetRequiredService<Interact.DoInteract>()
- .With(step.DataId.Value);
+ .With(step.DataId.Value, true);
return [unmount, task];
}
else if (step.EnemySpawnType == EEnemySpawnType.AfterItemUse)
ArgumentNullException.ThrowIfNull(step.DataId);
- return serviceProvider.GetRequiredService<DoInteract>().With(step.DataId.Value);
+ return serviceProvider.GetRequiredService<DoInteract>()
+ .With(step.DataId.Value, step.TargetTerritoryId != null);
}
}
internal sealed class DoInteract(GameFunctions gameFunctions, ICondition condition, ILogger<DoInteract> logger)
: ITask
{
+ private bool _needsUnmount;
private bool _interacted;
private DateTime _continueAt = DateTime.MinValue;
private uint DataId { get; set; }
+ private bool SkipMarkerCheck { get; set; }
- public ITask With(uint dataId)
+ public ITask With(uint dataId, bool skipMarkerCheck)
{
DataId = dataId;
+ SkipMarkerCheck = skipMarkerCheck;
return this;
}
// this is only relevant for followers on quests
if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted])
{
+ _needsUnmount = true;
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
if (DateTime.Now <= _continueAt)
return ETaskResult.StillRunning;
+ if (_needsUnmount)
+ {
+ if (condition[ConditionFlag.Mounted])
+ {
+ gameFunctions.Unmount();
+ _continueAt = DateTime.Now.AddSeconds(0.5);
+ return ETaskResult.StillRunning;
+ }
+ else
+ _needsUnmount = false;
+ }
+
if (!_interacted)
{
GameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
private unsafe bool HasAnyMarker(GameObject gameObject)
{
- if (gameObject.ObjectKind != ObjectKind.EventNpc)
+ if (SkipMarkerCheck || gameObject.ObjectKind != ObjectKind.EventNpc)
return true;
var gameObjectStruct = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address;
}
catch (MovementController.PathfindingFailedException)
{
- _questController.Stop();
+ _questController.Stop("Pathfinding failed");
}
}
public bool Unmount()
{
if (!_condition[ConditionFlag.Mounted])
- return false;
+ return true;
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
{
_logger.LogInformation("Unmounting...");
- ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
+ return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
}
else
+ {
_logger.LogWarning("Can't unmount right now?");
-
- return true;
+ return false;
+ }
}
public void OpenDutyFinder(uint contentFinderConditionId)
using System.Numerics;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
+using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using LLib.ImGui;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
+using Questionable.Controller.Steps.BaseFactory;
using Questionable.Model;
using Questionable.Model.V1;
ImGui.Separator();
DrawQuickAccessButtons();
+ DrawRemainingTasks();
}
private unsafe void DrawQuest()
if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
{
_movementController.Stop();
- _questController.Stop();
+ _questController.Stop("Manual");
}
+ QuestStep? currentStep = currentQuest.Quest
+ .FindSequence(currentQuest.Sequence)
+ ?.FindStep(currentQuest.Step);
+ bool colored = currentStep != null && currentStep.InteractionType == EInteractionType.Instruction
+ && _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
+
+ if (colored)
+ ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip"))
{
- _questController.Stop();
+ _movementController.Stop();
+ _questController.Stop("Manual");
_questController.IncreaseStepCount();
}
+ if (colored)
+ ImGui.PopStyleColor();
}
else
ImGui.Text("No active quest");
if (ImGui.Button("Stop Nav"))
{
_movementController.Stop();
- _questController.Stop();
+ _questController.Stop("Manual");
}
ImGui.EndDisabled();
_framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
TimeSpan.FromMilliseconds(200));
}
+ }
+ private void DrawRemainingTasks()
+ {
var remainingTasks = _questController.GetRemainingTaskNames();
if (remainingTasks.Count > 0)
{