--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
+ "Author": "liza",
+ "TerritoryId": 1189,
+ "Groups": [
+ {
+ "Nodes": [
+ {
+ "DataId": 34721,
+ "Locations": [
+ {
+ "Position": {
+ "X": 663.934,
+ "Y": 25.09505,
+ "Z": -87.81284
+ },
+ "MinimumAngle": -30,
+ "MaximumAngle": 45
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Nodes": [
+ {
+ "DataId": 34722,
+ "Locations": [
+ {
+ "Position": {
+ "X": 652.5192,
+ "Y": 21.87234,
+ "Z": -111.9597
+ },
+ "MinimumAngle": 195,
+ "MaximumAngle": 310
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Nodes": [
+ {
+ "DataId": 34723,
+ "Locations": [
+ {
+ "Position": {
+ "X": 605.4673,
+ "Y": 22.40212,
+ "Z": -91.82993
+ },
+ "MinimumAngle": 220,
+ "MaximumAngle": 330
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Nodes": [
+ {
+ "DataId": 34724,
+ "Locations": [
+ {
+ "Position": {
+ "X": 547.7242,
+ "Y": 17.74087,
+ "Z": -106.2755
+ },
+ "MinimumAngle": 45,
+ "MaximumAngle": 180
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Nodes": [
+ {
+ "DataId": 34725,
+ "Locations": [
+ {
+ "Position": {
+ "X": 534.3469,
+ "Y": 18.59627,
+ "Z": -78.46846
+ },
+ "MinimumAngle": -20,
+ "MaximumAngle": 55
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Nodes": [
+ {
+ "DataId": 34726,
+ "Locations": [
+ {
+ "Position": {
+ "X": 485.1973,
+ "Y": 17.44523,
+ "Z": -79.501
+ },
+ "MinimumAngle": -100,
+ "MaximumAngle": 35
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 1,
+ "Steps": [
+ {
+ "Position": {
+ "X": 664.32874,
+ "Y": 24.373428,
+ "Z": -85.7219
+ },
+ "TerritoryId": 1189,
+ "InteractionType": "InitiateLeve",
+ "AetheryteShortcut": "Yak T'el - Mamook",
+ "Fly": true,
+ "SkipConditions": {
+ "AetheryteShortcutIf": {
+ "InSameTerritory": true
+ }
+ }
+ },
+ {
+ "TerritoryId": 1189,
+ "InteractionType": "None",
+ "RequiredGatheredItems": [
+ {
+ "ItemId": 2003552,
+ "AlternativeItemId": 2003553,
+ "ItemCount": 999
+ }
+ ],
+ "$.0": "41635 → 970"
+ }
+ ]
+ }
+ ]
+}
"Dive",
"Instruction",
"AcceptQuest",
- "CompleteQuest"
+ "CompleteQuest",
+ "InitiateLeve"
]
},
"Disabled": {
"ItemId": {
"type": "number"
},
+ "AlternativeItemId": {
+ "description": "For leves that allow you to gather two items with different chance percentage, this is the preferred item if the gathering chance is 100% (after buffs)",
+ "type": "number"
+ },
"ItemCount": {
"type": "number",
"exclusiveMinimum": 0
{ EInteractionType.Instruction, "Instruction" },
{ EInteractionType.AcceptQuest, "AcceptQuest" },
{ EInteractionType.CompleteQuest, "CompleteQuest" },
+ { EInteractionType.InitiateLeve, "InitiateLeve" },
};
}
MeticulousBotanist = 22188,
ScrutinyBotanist = 22189,
-
+ SharpVision1 = 235,
+ SharpVision2 = 237,
+ SharpVision3 = 295,
+ FieldMastery1 = 218,
+ FieldMastery2 = 220,
+ FieldMastery3 = 294,
}
public static class EActionExtensions
AcceptQuest,
CompleteQuest,
+ AcceptLeve,
+ InitiateLeve,
+ CompleteLeve,
}
public sealed class GatheredItem
{
public uint ItemId { get; set; }
+ public uint AlternativeItemId { get; set; }
public int ItemCount { get; set; }
public ushort Collectability { get; set; }
if (QuestWorkUtils.HasCompletionFlags(condition.CompletionQuestVariablesFlags) && _currentFight.Data.ElementId is QuestId questId)
{
- var questWork = _questFunctions.GetQuestEx(questId);
- if (questWork != null && QuestWorkUtils.MatchesQuestWork(condition.CompletionQuestVariablesFlags,
- questWork.Value))
+ var questWork = _questFunctions.GetQuestProgressInfo(questId);
+ if (questWork != null && QuestWorkUtils.MatchesQuestWork(condition.CompletionQuestVariablesFlags, questWork))
{
_logger.LogInformation("Complex combat condition fulfilled: QuestWork matches");
_currentFight.Data.CompletedComplexDatas.Add(i);
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game.Event;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib;
using LLib.GameUI;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps.Interactions;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
private readonly QuestData _questData;
private readonly IGameGui _gameGui;
private readonly ITargetManager _targetManager;
+ private readonly IFramework _framework;
private readonly ILogger<GameUiController> _logger;
private readonly Regex _returnRegex;
QuestData questData,
IGameGui gameGui,
ITargetManager targetManager,
- IPluginLog pluginLog, ILogger<GameUiController> logger)
+ IFramework framework,
+ IPluginLog pluginLog,
+ ILogger<GameUiController> logger)
{
_addonLifecycle = addonLifecycle;
_dataManager = dataManager;
_questData = questData;
_gameGui = gameGui;
_targetManager = targetManager;
+ _framework = framework;
_logger = logger;
_returnRegex = _dataManager.GetExcelSheet<Addon>()!.GetRow(196)!.GetRegex(addon => addon.Text, pluginLog)!;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
}
internal unsafe void HandleCurrentDialogueChoices()
private int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
{
List<DialogueChoiceInfo> dialogueChoices = [];
- var currentQuest = _questController.SimulatedQuest ?? _questController.GatheringQuest ?? _questController.StartedQuest;
+
+ // levequest choices have some vague sort of priority
+ if (_questController.HasCurrentTaskMatching<Interact.DoInteract>(out var interact) &&
+ interact.Quest != null &&
+ interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
+ {
+ if (interact.InteractionType == EInteractionType.AcceptLeve)
+ {
+ dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
+ new DialogueChoice
+ {
+ Type = EDialogChoiceType.List,
+ ExcelSheet = "leve/GuildleveAssignment",
+ Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+ Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_01"),
+ }));
+ interact.InteractionType = EInteractionType.None;
+ }
+ else if (interact.InteractionType == EInteractionType.CompleteLeve)
+ {
+ dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
+ new DialogueChoice
+ {
+ Type = EDialogChoiceType.List,
+ ExcelSheet = "leve/GuildleveAssignment",
+ Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+ Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_REWARD"),
+ }));
+ interact.InteractionType = EInteractionType.None;
+ }
+ }
+
+ var currentQuest = _questController.SimulatedQuest ??
+ _questController.GatheringQuest ??
+ _questController.StartedQuest;
if (currentQuest != null)
{
var quest = currentQuest.Quest;
}
}
}
+
+ if (_questController.NextQuest == null)
+ {
+ // make sure to always close the leve dialogue
+ if (_questData.GetAllByIssuerDataId(target.DataId).Any(x => x.QuestId is LeveId))
+ {
+ _logger.LogInformation("Adding close leve dialogue as option");
+ dialogueChoices.Add(new DialogueChoiceInfo(null,
+ new DialogueChoice
+ {
+ Type = EDialogChoiceType.List,
+ ExcelSheet = "leve/GuildleveAssignment",
+ Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+ Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_07"),
+ }));
+ }
+ }
}
if (dialogueChoices.Count == 0)
+ {
+ _logger.LogDebug("No dialogue choices to check");
return null;
+ }
foreach (var (quest, dialogueChoice) in dialogueChoices)
{
i, answers[i], actualPrompt);
// ensure we only open the dialog once
- if (quest.Id is SatisfactionSupplyNpcId)
+ if (quest?.Id is SatisfactionSupplyNpcId)
{
if (_questController.GatheringQuest == null ||
_questController.GatheringQuest.Sequence == 255)
return;
_logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
+ var director = UIState.Instance()->DirectorTodo.Director;
+ if (director != null && director->EventHandlerInfo != null &&
+ director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
+ director->Sequence == 254)
+ {
+ // just close the dialogue for 'do you want to return to next settlement', should prolly be different for
+ // ARR territories
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(1);
+ return;
+ }
var currentQuest = _questController.StartedQuest;
if (currentQuest != null && CheckQuestYesNo(addonSelectYesno, currentQuest, actualPrompt, checkAllSteps))
return true;
}
+ if (currentQuest.Quest.Id is LeveId)
+ {
+ var dialogueChoice = new DialogueChoice
+ {
+ Type = EDialogChoiceType.YesNo,
+ ExcelSheet = "Addon",
+ Prompt = new ExcelRef(608),
+ Yes = true
+ };
+
+ if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt))
+ return true;
+ }
+
if (HandleTravelYesNo(addonSelectYesno, currentQuest, actualPrompt))
return true;
QuestStep? step = sequence.FindStep(currentQuest.Step);
if (step != null)
- _logger.LogTrace("Current step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId,
+ _logger.LogTrace("FindTargetTerritoryFromQuestStep (current): {CurrentTerritory}, {TargetTerritory}",
+ step.TerritoryId,
step.TargetTerritoryId);
if (step == null || step.TargetTerritoryId == null)
{
- _logger.LogTrace("TravelYesNo: Checking previous step...");
+ _logger.LogTrace("FindTargetTerritoryFromQuestStep: Checking previous step...");
step = sequence.FindStep(currentQuest.Step == 255 ? (sequence.Steps.Count - 1) : (currentQuest.Step - 1));
if (step != null)
- _logger.LogTrace("Previous step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId,
+ _logger.LogTrace("FindTargetTerritoryFromQuestStep (previous): {CurrentTerritory}, {TargetTerritory}",
+ step.TerritoryId,
step.TargetTerritoryId);
}
if (step == null || step.TargetTerritoryId == null)
{
- _logger.LogTrace("TravelYesNo: Not found");
+ _logger.LogTrace("FindTargetTerritoryFromQuestStep: Not found");
return null;
}
}
}
- private StringOrRegex? ResolveReference(Quest quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
+ private unsafe void JournalResultPostSetup(AddonEvent type, AddonArgs args)
+ {
+ if (_questController.IsRunning)
+ {
+ _logger.LogInformation("Checking for quest name of journal result");
+ AddonJournalResult* addon = (AddonJournalResult*)args.Addon;
+
+ string questName = addon->AtkTextNode250->NodeText.ToString();
+ if (_questController.CurrentQuest != null &&
+ GameFunctions.GameStringEquals(_questController.CurrentQuest.Quest.Info.Name, questName))
+ addon->FireCallbackInt(0);
+ else
+ addon->FireCallbackInt(1);
+ }
+ }
+
+ private unsafe void GuildLevePostSetup(AddonEvent type, AddonArgs args)
+ {
+ var target = _targetManager.Target;
+ if (target == null)
+ return;
+
+ if (_questController is { IsRunning: true, NextQuest: { Quest.Id: LeveId } nextQuest } &&
+ _questFunctions.IsReadyToAcceptQuest(nextQuest.Quest.Id))
+ {
+ var addon = (AddonGuildLeve*)args.Addon;
+ /*
+ var atkValues = addon->AtkValues;
+
+ var availableLeves = _questData.GetAllByIssuerDataId(target.DataId);
+ List<(int, IQuestInfo)> offeredLeves = [];
+ for (int i = 0; i <= 20; ++i) // 3 leves per group, 1 label for group
+ {
+ string? leveName = atkValues[626 + i * 2].ReadAtkString();
+ if (leveName == null)
+ continue;
+
+ var questInfo = availableLeves.FirstOrDefault(x => GameFunctions.GameStringEquals(x.Name, leveName));
+ if (questInfo == null)
+ continue;
+
+ offeredLeves.Add((i, questInfo));
+
+ }
+
+ foreach (var (i, questInfo) in offeredLeves)
+ _logger.LogInformation("Leve {Index} = {Id}, {Name}", i, questInfo.QuestId, questInfo.Name);
+ */
+
+ _framework.RunOnTick(() =>
+ {
+ _questController.SetPendingQuest(nextQuest);
+ _questController.SetNextQuest(null);
+
+ var agent = UIModule.Instance()->GetAgentModule()->GetAgentByInternalId(AgentId.LeveQuest);
+ var returnValue = stackalloc AtkValue[1];
+ var selectQuest = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 3 },
+ new() { Type = ValueType.UInt, UInt = nextQuest.Quest.Id.Value }
+ };
+ agent->ReceiveEvent(returnValue, selectQuest, 2, 0);
+ addon->Close(true);
+ }, TimeSpan.FromMilliseconds(100));
+ }
+ }
+
+ private StringOrRegex? ResolveReference(Quest? quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
{
if (excelRef == null)
return null;
public void Dispose()
{
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
}
- private sealed record DialogueChoiceInfo(Quest Quest, DialogueChoice DialogueChoice);
+ private sealed record DialogueChoiceInfo(Quest? Quest, DialogueChoice DialogueChoice);
}
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Event;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
using Questionable.Functions;
using Questionable.GatheringPaths;
using Questionable.Model.Gathering;
+using Questionable.Model.Questing;
namespace Questionable.Controller;
var currentNode = _currentRequest.Nodes[_currentRequest.CurrentIndex++ % _currentRequest.Nodes.Count];
+ var director = UIState.Instance()->DirectorTodo.Director;
+ if (director != null && director->EventHandlerInfo != null &&
+ director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector)
+ {
+ if (director->Sequence == 254)
+ return;
+
+ _taskQueue.Enqueue(new WaitAtEnd.WaitDelay());
+ }
+
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<MountTask>()
.With(_currentRequest.Root.TerritoryId, MountTask.EMountIf.Always));
if (currentNode.Locations.Count > 1)
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<MoveToLandingLocation>()
.With(_currentRequest.Root.TerritoryId, currentNode));
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<Interact.DoInteract>()
- .With(currentNode.DataId, true));
+ .With(currentNode.DataId, null, EInteractionType.None, true));
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<DoGather>()
.With(_currentRequest.Data, currentNode));
if (_currentRequest.Data.Collectability > 0)
public sealed record GatheringRequest(
GatheringPointId GatheringPointId,
uint ItemId,
+ uint AlternativeItemId,
int Quantity,
ushort Collectability = 0);
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Shared;
private QuestProgress? _nextQuest;
private QuestProgress? _simulatedQuest;
private QuestProgress? _gatheringQuest;
+ private QuestProgress? _pendingQuest;
private EAutomationType _automationType;
/// <summary>
public QuestProgress? NextQuest => _nextQuest;
public QuestProgress? GatheringQuest => _gatheringQuest;
+ /// <summary>
+ /// Used when accepting leves, as there's a small delay
+ /// </summary>
+ public QuestProgress? PendingQuest => _pendingQuest;
+
public string? DebugState { get; private set; }
public void Reload()
_startedQuest = null;
_nextQuest = null;
_gatheringQuest = null;
+ _pendingQuest = null;
_simulatedQuest = null;
_safeAnimationEnd = DateTime.MinValue;
{
DebugState = null;
+ if (_pendingQuest != null)
+ {
+ if (!_questFunctions.IsQuestAccepted(_pendingQuest.Quest.Id))
+ {
+ DebugState = $"Waiting for Leve {_pendingQuest.Quest.Id}";
+ return;
+ }
+ else
+ {
+ _startedQuest = _pendingQuest;
+ _pendingQuest = null;
+ Stop("Pending quest accepted", continueIfAutomatic: true);
+ }
+ }
if (_simulatedQuest == null && _nextQuest != null)
{
// if the quest is accepted, we no longer track it
{
_logger.LogInformation("Next quest {QuestId} accepted or completed",
_nextQuest.Quest.Id);
+
+ // if (_nextQuest.Quest.Id is LeveId)
+ // _startedQuest = _nextQuest;
+
_nextQuest = null;
}
}
var sequence = q.FindSequence(questToRun.Sequence);
if (sequence == null)
{
- DebugState = "Sequence not found";
+ DebugState = $"Sequence {sequence} not found";
Stop("Unknown sequence");
return;
}
_gatheringQuest = null;
}
+ public void SetPendingQuest(QuestProgress? quest)
+ {
+ _logger.LogInformation("PendingQuest: {QuestId}", quest?.Quest.Id);
+ _pendingQuest = quest;
+ }
+
protected override void UpdateCurrentTask()
{
if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(CurrentQuest?.Quest))
return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
}
- public bool HasCurrentTaskMatching<T>() =>
- _currentTask is T;
+ public bool HasCurrentTaskMatching<T>([NotNullWhen(true)] out T? task)
+ where T : class, ITask
+ {
+ if (_currentTask is T t)
+ {
+ task = t;
+ return true;
+ }
+ else
+ {
+ task = null;
+ return false;
+ }
+ }
public bool IsRunning => _currentTask != null || _taskQueue.Count > 0;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
private readonly QuestValidator _questValidator;
private readonly JsonSchemaValidator _jsonSchemaValidator;
private readonly ILogger<QuestRegistry> _logger;
- private readonly ICallGateProvider<object> _reloadDataIpc;
+ private readonly LeveData _leveData;
+ private readonly ICallGateProvider<object> _reloadDataIpc;
private readonly Dictionary<ElementId, Quest> _quests = new();
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData,
QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator,
- ILogger<QuestRegistry> logger)
+ ILogger<QuestRegistry> logger, LeveData leveData)
{
_pluginInterface = pluginInterface;
_questData = questData;
_questValidator = questValidator;
_jsonSchemaValidator = jsonSchemaValidator;
_logger = logger;
+ _leveData = leveData;
_reloadDataIpc = _pluginInterface.GetIpcProvider<object>("Questionable.ReloadData");
}
foreach ((ElementId questId, QuestRoot questRoot) in AssemblyQuestLoader.GetQuests())
{
+ var questInfo = _questData.GetQuestInfo(questId);
+ if (questInfo is LeveInfo leveInfo)
+ _leveData.AddQuestSteps(leveInfo, questRoot);
Quest quest = new()
{
Id = questId,
Root = questRoot,
- Info = _questData.GetQuestInfo(questId),
+ Info = questInfo,
ReadOnly = true,
};
_quests[quest.Id] = quest;
var questNode = JsonNode.Parse(stream)!;
_jsonSchemaValidator.Enqueue(questId, questNode);
+ var questRoot = questNode.Deserialize<QuestRoot>()!;
+ var questInfo = _questData.GetQuestInfo(questId);
+ if (questInfo is LeveInfo leveInfo)
+ _leveData.AddQuestSteps(leveInfo, questRoot);
Quest quest = new Quest
{
Id = questId,
- Root = questNode.Deserialize<QuestRoot>()!,
- Info = _questData.GetQuestInfo(questId),
+ Root = questRoot,
+ Info = questInfo,
ReadOnly = false,
};
_quests[quest.Id] = quest;
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Memory;
using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameData;
using LLib.GameUI;
+using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model.Gathering;
+using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering;
GatheringController gatheringController,
GameFunctions gameFunctions,
IGameGui gameGui,
- ICondition condition) : ITask
+ IClientState clientState,
+ ICondition condition,
+ ILogger<DoGather> logger) : ITask
{
+ private const uint StatusGatheringRateUp = 218;
+
private GatheringController.GatheringRequest _currentRequest = null!;
private GatheringNode _currentNode = null!;
private bool _wasGathering;
- private List<SlotInfo>? _slots;
-
+ private SlotInfo? _slotToGather;
+ private Queue<EAction>? _actionQueue;
public ITask With(GatheringController.GatheringRequest currentRequest, GatheringNode currentNode)
{
_wasGathering = true;
- if (gameGui.TryGetAddonByName("Gathering", out AtkUnitBase* atkUnitBase))
+ if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering))
{
if (gatheringController.HasRequestedItems())
{
- atkUnitBase->FireCallbackInt(-1);
+ addonGathering->FireCallbackInt(-1);
}
else
{
- _slots ??= ReadSlots(atkUnitBase);
- var slot = _slots.Single(x => x.ItemId == _currentRequest.ItemId);
- atkUnitBase->FireCallbackInt(slot.Index);
+ var slots = ReadSlots(addonGathering);
+ if (_currentRequest.Collectability > 0)
+ {
+ var slot = slots.Single(x => x.ItemId == _currentRequest.ItemId);
+ addonGathering->FireCallbackInt(slot.Index);
+ }
+ else
+ {
+ NodeCondition nodeCondition = new NodeCondition(
+ addonGathering->AtkValues[110].UInt,
+ addonGathering->AtkValues[111].UInt);
+
+ if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
+ {
+ if (gameFunctions.UseAction(nextAction))
+ {
+ logger.LogInformation("Used action {Action} on node", nextAction);
+ _actionQueue.Dequeue();
+ }
+
+ return ETaskResult.StillRunning;
+ }
+
+ _actionQueue = GetNextActions(nodeCondition, slots);
+ if (_actionQueue.Count == 0)
+ {
+ var slot = _slotToGather ?? slots.Single(x => x.ItemId == _currentRequest.ItemId);
+ addonGathering->FireCallbackInt(slot.Index);
+ }
+ }
}
}
}
: ETaskResult.StillRunning;
}
- private unsafe List<SlotInfo> ReadSlots(AtkUnitBase* atkUnitBase)
+ private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering)
{
- var atkValues = atkUnitBase->AtkValues;
+ var atkValues = addonGathering->AtkValues;
List<SlotInfo> slots = new List<SlotInfo>();
for (int i = 0; i < 8; ++i)
{
if (itemId == 0)
continue;
- var slot = new SlotInfo(i, itemId);
+ AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value;
+
+ AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode();
+ if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance))
+ gatheringChance = 0;
+
+ AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode();
+ if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance))
+ boonChance = 0;
+
+ AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode();
+ AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode();
+ if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity))
+ quantity = 1;
+
+ var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity);
slots.Add(slot);
}
return slots;
}
+ private Queue<EAction> GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots)
+ {
+ uint gp = clientState.LocalPlayer!.CurrentGp;
+ Queue<EAction> actions = new();
+
+ if (!gameFunctions.HasStatus(StatusGatheringRateUp))
+ {
+ // do we have an alternative item? only happens for 'evaluation' leve quests
+ if (_currentRequest.AlternativeItemId != 0)
+ {
+ var alternativeSlot = slots.Single(x => x.ItemId == _currentRequest.AlternativeItemId);
+
+ if (alternativeSlot.GatheringChance == 100)
+ {
+ _slotToGather = alternativeSlot;
+ return actions;
+ }
+
+ if (alternativeSlot.GatheringChance > 0)
+ {
+ if (alternativeSlot.GatheringChance >= 95 &&
+ CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
+ {
+ _slotToGather = alternativeSlot;
+ actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
+ return actions;
+ }
+
+ if (alternativeSlot.GatheringChance >= 85 &&
+ CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
+ {
+ _slotToGather = alternativeSlot;
+ actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
+ return actions;
+ }
+
+ if (alternativeSlot.GatheringChance >= 50 &&
+ CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
+ {
+ _slotToGather = alternativeSlot;
+ actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
+ return actions;
+ }
+ }
+ }
+
+ var slot = slots.Single(x => x.ItemId == _currentRequest.ItemId);
+ if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
+ {
+ if (slot.GatheringChance >= 95 &&
+ CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
+ {
+ actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
+ return actions;
+ }
+
+ if (slot.GatheringChance >= 85 &&
+ CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
+ {
+ actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
+ return actions;
+ }
+
+ if (slot.GatheringChance >= 50 &&
+ CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
+ {
+ actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
+ return actions;
+ }
+ }
+ }
+
+ return actions;
+ }
+
+ private EAction PickAction(EAction minerAction, EAction botanistAction)
+ {
+ if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
+ return minerAction;
+ else
+ return botanistAction;
+ }
+
+ private unsafe bool CanUseAction(EAction minerAction, EAction botanistAction)
+ {
+ EAction action = PickAction(minerAction, botanistAction);
+ return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
+ }
+
public override string ToString() => "DoGather";
- private sealed record SlotInfo(int Index, uint ItemId);
+ private sealed record SlotInfo(int Index, uint ItemId, int GatheringChance, int BoonChance, int Quantity);
+
+ private sealed record NodeCondition(
+ uint CurrentIntegrity,
+ uint MaxIntegrity);
}
ArgumentNullException.ThrowIfNull(step.DataId);
yield return serviceProvider.GetRequiredService<Interact.DoInteract>()
- .With(step.DataId.Value, true);
+ .With(step.DataId.Value, quest, EInteractionType.None, true);
yield return CreateTask(quest, sequence, step);
break;
}
// if our quest step has any completion flags, we need to check if they are set
if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.ElementId is QuestId questId)
{
- var questWork = questFunctions.GetQuestEx(questId);
+ var questWork = questFunctions.GetQuestProgressInfo(questId);
if (questWork == null)
return ETaskResult.StillRunning;
- if (QuestWorkUtils.MatchesQuestWork(_completionQuestVariableFlags, questWork.Value))
+ if (QuestWorkUtils.MatchesQuestWork(_completionQuestVariableFlags, questWork))
return ETaskResult.TaskComplete;
else
return ETaskResult.StillRunning;
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
- if (step.InteractionType is EInteractionType.AcceptQuest or EInteractionType.CompleteQuest)
+ if (step.InteractionType is EInteractionType.AcceptQuest or EInteractionType.CompleteQuest
+ or EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
{
if (step.Emote != null)
yield break;
yield return serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>();
yield return serviceProvider.GetRequiredService<DoInteract>()
- .With(step.DataId.Value,
+ .With(step.DataId.Value, quest, step.InteractionType,
step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId);
}
private DateTime _continueAt = DateTime.MinValue;
private uint DataId { get; set; }
+ public Quest? Quest { get; private set; }
+ public EInteractionType InteractionType { get; set; }
private bool SkipMarkerCheck { get; set; }
- public ITask With(uint dataId, bool skipMarkerCheck)
+ public DoInteract With(uint dataId, Quest? quest, EInteractionType interactionType, bool skipMarkerCheck)
{
DataId = dataId;
+ Quest = quest;
+ InteractionType = interactionType;
SkipMarkerCheck = skipMarkerCheck;
return this;
}
[
serviceProvider.GetRequiredService<DisableYesAlready>(),
serviceProvider.GetRequiredService<Interact.DoInteract>()
- .With(step.DataId.Value, true),
+ .With(step.DataId.Value, quest, EInteractionType.None, true),
serviceProvider.GetRequiredService<RestoreYesAlready>()
];
}
yield return serviceProvider.GetRequiredService<Move.MoveInternal>()
.With(territoryId, destination, dataId: npcId, sprint: false);
yield return serviceProvider.GetRequiredService<Interact.DoInteract>()
- .With(npcId, true);
+ .With(npcId, null, EInteractionType.None, true);
}
}
{
if (QuestId is QuestId questId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
{
- QuestWork? questWork = questFunctions.GetQuestEx(questId);
+ QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(questId);
if (questWork != null &&
- QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork.Value))
+ QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork))
return ETaskResult.TaskComplete;
}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Controller.Steps.Common;
+using Questionable.Model;
+using Questionable.Model.Questing;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.Steps.Leves;
+
+internal static class InitiateLeve
+{
+ internal sealed class Factory(IServiceProvider serviceProvider, ICondition condition) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.InitiateLeve)
+ yield break;
+
+ yield return serviceProvider.GetRequiredService<OpenJournal>().With(quest.Id);
+ yield return serviceProvider.GetRequiredService<Initiate>().With(quest.Id);
+ yield return serviceProvider.GetRequiredService<SelectDifficulty>();
+ yield return new WaitConditionTask(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)");
+ }
+
+ public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new NotImplementedException();
+ }
+
+ internal sealed unsafe class OpenJournal : ITask
+ {
+ private ElementId _elementId = null!;
+ private uint _questType;
+
+ public ITask With(ElementId elementId)
+ {
+ _elementId = elementId;
+ _questType = _elementId is LeveId ? 2u : 1u;
+ return this;
+ }
+
+ public bool Start()
+ {
+ AgentQuestJournal.Instance()->OpenForQuest(_elementId.Value, _questType);
+ return true;
+ }
+
+ public ETaskResult Update()
+ {
+ AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance();
+ if (!agentQuestJournal->IsAgentActive())
+ return ETaskResult.StillRunning;
+
+ return agentQuestJournal->SelectedQuestId == _elementId.Value &&
+ agentQuestJournal->SelectedQuestType == _questType
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+ }
+
+ public override string ToString() => $"OpenJournal({_elementId})";
+ }
+
+ internal sealed unsafe class Initiate(IGameGui gameGui) : ITask
+ {
+ private ElementId _elementId = null!;
+
+ public ITask With(ElementId elementId)
+ {
+ _elementId = elementId;
+ return this;
+ }
+
+ public bool Start() => true;
+
+ public ETaskResult Update()
+ {
+ if (gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail))
+ {
+ var pickQuest = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 4 },
+ new() { Type = ValueType.UInt, Int = _elementId.Value }
+ };
+ addonJournalDetail->FireCallback(2, pickQuest);
+ return ETaskResult.TaskComplete;
+ }
+
+ return ETaskResult.StillRunning;
+ }
+
+ public override string ToString() => $"InitiateLeve({_elementId})";
+ }
+
+ internal sealed unsafe class SelectDifficulty(IGameGui gameGui) : ITask
+ {
+ public bool Start() => true;
+
+ public ETaskResult Update()
+ {
+ if (gameGui.TryGetAddonByName("GuildLeveDifficulty", out AtkUnitBase* addon))
+ {
+ // atkvalues: 1 → default difficulty, 2 → min, 3 → max
+
+
+ var pickDifficulty = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 0 },
+ new() { Type = ValueType.Int, Int = addon->AtkValues[1].Int }
+ };
+ addon->FireCallback(2, pickDifficulty, true);
+ return ETaskResult.TaskComplete;
+ }
+
+ return ETaskResult.StillRunning;
+ }
+ }
+}
public bool Start()
{
return gatheringController.Start(new GatheringController.GatheringRequest(_gatheringPointId,
- _gatheredItem.ItemId, _gatheredItem.ItemCount, _gatheredItem.Collectability));
+ _gatheredItem.ItemId, _gatheredItem.AlternativeItemId, _gatheredItem.ItemCount,
+ _gatheredItem.Collectability));
}
public ETaskResult Update()
return true;
}
- if (ElementId is QuestId questId)
+ if (ElementId is QuestId || ElementId is LeveId)
{
- QuestWork? questWork = questFunctions.GetQuestEx(questId);
+ QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(ElementId);
if (QuestWorkUtils.HasCompletionFlags(Step.CompletionQuestVariablesFlags) && questWork != null)
{
- if (QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value))
+ if (QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork))
{
logger.LogInformation("Skipping step, as quest variables match (step is complete)");
return true;
if (Step is { SkipConditions.StepIf: { } conditions } && questWork != null)
{
- if (QuestWorkUtils.MatchesQuestWork(conditions.CompletionQuestVariablesFlags, questWork.Value))
+ if (QuestWorkUtils.MatchesQuestWork(conditions.CompletionQuestVariablesFlags, questWork))
{
logger.LogInformation("Skipping step, as quest variables match (step can be skipped)");
return true;
if (Step is { RequiredQuestVariables: { } requiredQuestVariables } && questWork != null)
{
- if (!QuestWorkUtils.MatchesRequiredQuestWorkConfig(requiredQuestVariables, questWork.Value,
- logger))
+ if (!QuestWorkUtils.MatchesRequiredQuestWorkConfig(requiredQuestVariables, questWork, logger))
{
logger.LogInformation("Skipping step, as required variables do not match");
return true;
public ETaskResult Update()
{
- QuestWork? questWork = questFunctions.GetQuestEx(Quest);
+ QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(Quest);
return questWork != null &&
- QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value)
+ QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Shared;
+using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Utils;
return completionQuestVariablesFlags.Count == 6 && completionQuestVariablesFlags.Any(x => x != null && (x.High != 0 || x.Low != 0));
}
- public static bool MatchesQuestWork(IList<QuestWorkValue?> completionQuestVariablesFlags, QuestWork questWork)
+ public static bool MatchesQuestWork(IList<QuestWorkValue?> completionQuestVariablesFlags, QuestProgressInfo questProgressInfo)
{
- if (!HasCompletionFlags(completionQuestVariablesFlags))
+ if (!HasCompletionFlags(completionQuestVariablesFlags) || questProgressInfo.Variables.Count != 6)
return false;
- for (int i = 0; i < 6; ++i)
+ for (int i = 0; i < questProgressInfo.Variables.Count; ++i)
{
QuestWorkValue? check = completionQuestVariablesFlags[i];
if (check == null)
EQuestWorkMode mode = check.Mode;
- byte actualHigh = (byte)(questWork.Variables[i] >> 4);
- byte actualLow = (byte)(questWork.Variables[i] & 0xF);
+ byte actualHigh = (byte)(questProgressInfo.Variables[i] >> 4);
+ byte actualLow = (byte)(questProgressInfo.Variables[i] & 0xF);
byte? checkHigh = check.High;
byte? checkLow = check.Low;
}
public static bool MatchesRequiredQuestWorkConfig(List<List<QuestWorkValue>?> requiredQuestVariables,
- QuestWork questWork, ILogger<SkipCondition.CheckSkip> logger)
+ QuestProgressInfo questWork, ILogger<SkipCondition.CheckSkip> logger)
{
if (requiredQuestVariables.Count != 6 || requiredQuestVariables.All(x => x == null || x.Count == 0))
{
--- /dev/null
+using System.Collections.Generic;
+using System.Linq;
+using FFXIVClientStructs.FFXIV.Common.Math;
+using LLib.GameData;
+using Questionable.Model;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
+
+namespace Questionable.Data;
+
+internal sealed class LeveData
+{
+ private static readonly List<LeveStepData> Leves =
+ [
+ new(EAetheryteLocation.Tuliyollal, 1048390, new(15.243713f, -14.000001f, 85.83191f)),
+ ];
+
+ private readonly AetheryteData _aetheryteData;
+
+ public LeveData(AetheryteData aetheryteData)
+ {
+ _aetheryteData = aetheryteData;
+ }
+
+ public void AddQuestSteps(LeveInfo leveInfo, QuestRoot questRoot)
+ {
+ LeveStepData leveStepData = Leves.Single(x => x.IssuerDataId == leveInfo.IssuerDataId);
+
+ QuestSequence? startSequence = questRoot.QuestSequence.FirstOrDefault(x => x.Sequence == 0);
+ if (startSequence == null)
+ {
+ questRoot.QuestSequence.Add(new QuestSequence
+ {
+ Sequence = 0,
+ Steps =
+ [
+ new QuestStep
+ {
+ DataId = leveStepData.IssuerDataId,
+ Position = leveStepData.IssuerPosition,
+ TerritoryId = _aetheryteData.TerritoryIds[leveStepData.AetheryteLocation],
+ InteractionType = EInteractionType.AcceptLeve,
+ AetheryteShortcut = leveStepData.AetheryteLocation,
+ SkipConditions = new()
+ {
+ AetheryteShortcutIf = new()
+ {
+ InSameTerritory = true,
+ }
+ }
+ }
+ ]
+ });
+ }
+
+ QuestSequence? endSequence = questRoot.QuestSequence.FirstOrDefault(x => x.Sequence == 255);
+ if (endSequence == null)
+ {
+ questRoot.QuestSequence.Add(new QuestSequence
+ {
+ Sequence = 255,
+ Steps =
+ [
+ new QuestStep
+ {
+ DataId = leveStepData.GetTurnInDataId(leveInfo),
+ Position = leveStepData.GetTurnInPosition(leveInfo),
+ TerritoryId = _aetheryteData.TerritoryIds[leveStepData.AetheryteLocation],
+ InteractionType = EInteractionType.CompleteLeve,
+ AetheryteShortcut = leveStepData.AetheryteLocation,
+ SkipConditions = new()
+ {
+ AetheryteShortcutIf = new()
+ {
+ InSameTerritory = true,
+ }
+ }
+ }
+ ]
+ });
+ }
+ }
+
+ private sealed class LeveStepData
+ {
+ private readonly uint? _turnInDataId;
+ private readonly Vector3? _turnInPosition;
+ private readonly uint? _gathererTurnInDataId;
+ private readonly Vector3? _gathererTurnInPosition;
+ private readonly uint? _crafterTurnInDataId;
+ private readonly Vector3? _crafterTurnInPosition;
+
+ public LeveStepData(EAetheryteLocation aetheryteLocation, uint issuerDataId, Vector3 issuerPosition,
+ uint? turnInDataId = null, Vector3? turnInPosition = null,
+ uint? gathererTurnInDataId = null, Vector3? gathererTurnInPosition = null,
+ uint? crafterTurnInDataId = null, Vector3? crafterTurnInPosition = null)
+ {
+ _turnInDataId = turnInDataId;
+ _turnInPosition = turnInPosition;
+ _gathererTurnInDataId = gathererTurnInDataId;
+ _gathererTurnInPosition = gathererTurnInPosition;
+ _crafterTurnInDataId = crafterTurnInDataId;
+ _crafterTurnInPosition = crafterTurnInPosition;
+ AetheryteLocation = aetheryteLocation;
+ IssuerDataId = issuerDataId;
+ IssuerPosition = issuerPosition;
+ }
+
+ public EAetheryteLocation AetheryteLocation { get; }
+ public uint IssuerDataId { get; }
+ public Vector3 IssuerPosition { get; }
+
+ public uint GetTurnInDataId(LeveInfo leveInfo)
+ {
+ if (leveInfo.ClassJobs.Any(x => x.IsGatherer()))
+ return _gathererTurnInDataId ?? _turnInDataId ?? IssuerDataId;
+ else if (leveInfo.ClassJobs.Any(x => x.IsCrafter()))
+ return _crafterTurnInDataId ?? _turnInDataId ?? IssuerDataId;
+ else
+ return _turnInDataId ?? IssuerDataId;
+ }
+
+ public Vector3 GetTurnInPosition(LeveInfo leveInfo)
+ {
+ if (leveInfo.ClassJobs.Any(x => x.IsGatherer()))
+ return _gathererTurnInPosition ?? _turnInPosition ?? IssuerPosition;
+ else if (leveInfo.ClassJobs.Any(x => x.IsCrafter()))
+ return _crafterTurnInPosition ?? _turnInPosition ?? IssuerPosition;
+ else
+ return _turnInPosition ?? IssuerPosition;
+ }
+ }
+}
.Select(x => new QuestInfo(x)),
..dataManager.GetExcelSheet<SatisfactionNpc>()!
.Where(x => x.RowId > 0)
- .Select(x => new SatisfactionSupplyInfo(x))
+ .Select(x => new SatisfactionSupplyInfo(x)),
+ ..dataManager.GetExcelSheet<Leve>()!
+ .Where(x => x.RowId > 0)
+ .Where(x => x.LevelLevemete.Row != 0)
+ .Select(x => new LeveInfo(x)),
];
_quests = quests.ToDictionary(x => x.QuestId, x => x);
}
_logger = logger;
}
- public StringOrRegex GetDialogueText(Quest currentQuest, string? excelSheetName, string key, bool isRegex)
+ public StringOrRegex GetDialogueText(Quest? currentQuest, string? excelSheetName, string key, bool isRegex)
{
var seString = GetRawDialogueText(currentQuest, excelSheetName, key);
if (isRegex)
return new StringOrRegex(seString?.ToDalamudString().ToString());
}
- public SeString? GetRawDialogueText(Quest currentQuest, string? excelSheetName, string key)
+ public SeString? GetRawDialogueText(Quest? currentQuest, string? excelSheetName, string key)
{
- if (excelSheetName == null)
+ if (currentQuest != null && excelSheetName == null)
{
var questRow =
_dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.Id.Value +
excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}";
}
+ ArgumentNullException.ThrowIfNull(excelSheetName);
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
if (excelSheet == null)
{
statusManager->HasStatus(2730);
}
+ public bool HasStatus(uint statusId)
+ {
+ var localPlayer = _clientState.LocalPlayer;
+ if (localPlayer == null)
+ return false;
+
+ var battleChara = (BattleChara*)localPlayer.Address;
+ StatusManager* statusManager = battleChara->GetStatusManager();
+ return statusManager->HasStatus(statusId);
+ }
+
public bool Mount()
{
if (_condition[ConditionFlag.Mounted])
return slots;
}
+
+#if false
+ private byte ExecuteCommand(int id, int a, int b, int c, int d)
+ {
+ // Initiate Leve: 804 1794 [1] 0 0 // with [1] = extra difficulty levels
+ // 705 2 1794 0 0
+ // 801 0 0 0 0
+ // Abandon: 805 1794 0 0 0
+ // Retry button: 803 1794 0 0 0
+ return 0;
+ }
+#endif
}
private readonly IClientState _clientState;
private readonly IGameGui _gameGui;
- public QuestFunctions(QuestRegistry questRegistry, QuestData questData, Configuration configuration, IDataManager dataManager, IClientState clientState, IGameGui gameGui)
+ public QuestFunctions(QuestRegistry questRegistry, QuestData questData, Configuration configuration,
+ IDataManager dataManager, IClientState clientState, IGameGui gameGui)
{
_questRegistry = questRegistry;
_questData = questData;
case 1: // normal quest
currentQuest = new QuestId(questManager->NormalQuests[trackedQuest.Index].QuestId);
+ if (_questRegistry.IsKnownQuest(currentQuest))
+ return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
+ break;
+
+ case 2: // leve
+ currentQuest = new LeveId(questManager->LeveQuests[trackedQuest.Index].LeveId);
+ if (_questRegistry.IsKnownQuest(currentQuest))
+ return (currentQuest, questManager->GetLeveQuestById(currentQuest.Value)->Sequence);
break;
}
return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
}
- public QuestWork? GetQuestEx(QuestId questId)
- {
- QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId.Value);
- return questWork != null ? *questWork : null;
- }
-
- public bool IsReadyToAcceptQuest(ElementId elementId)
+ public QuestProgressInfo? GetQuestProgressInfo(ElementId elementId)
{
if (elementId is QuestId questId)
- return IsReadyToAcceptQuest(questId);
- else if (elementId is SatisfactionSupplyNpcId)
- return true;
+ {
+ QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId.Value);
+ return questWork != null ? new QuestProgressInfo(*questWork) : null;
+ }
+ else if (elementId is LeveId leveId)
+ {
+ LeveWork* leveWork = QuestManager.Instance()->GetLeveQuestById(leveId.Value);
+ return leveWork != null ? new QuestProgressInfo(*leveWork) : null;
+ }
else
- throw new ArgumentOutOfRangeException(nameof(elementId));
+ return null;
}
- public bool IsReadyToAcceptQuest(QuestId questId)
+ public bool IsReadyToAcceptQuest(ElementId questId)
{
_questRegistry.TryGetQuest(questId, out var quest);
if (quest is { Info.IsRepeatable: true })
{
if (elementId is QuestId questId)
return IsQuestAccepted(questId);
+ else if (elementId is LeveId leveId)
+ return IsQuestAccepted(leveId);
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
return questManager->IsQuestAccepted(questId.Value);
}
+ public bool IsQuestAccepted(LeveId leveId)
+ {
+ QuestManager* questManager = QuestManager.Instance();
+ foreach (var leveQuest in questManager->LeveQuests)
+ {
+ if (leveQuest.LeveId == leveId.Value)
+ return true;
+ }
+
+ return false;
+ }
+
public bool IsQuestComplete(ElementId elementId)
{
if (elementId is QuestId questId)
return IsQuestComplete(questId);
+ else if (elementId is LeveId leveId)
+ return IsQuestComplete(leveId);
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
return QuestManager.IsQuestComplete(questId.Value);
}
+ public bool IsQuestComplete(LeveId leveId)
+ {
+ return QuestManager.Instance()->IsLevequestComplete(leveId.Value);
+ }
+
public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
{
if (elementId is QuestId questId)
return IsQuestLocked(questId, extraCompletedQuest);
+ else if (elementId is LeveId leveId)
+ return IsQuestLocked(leveId);
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
}
+ public bool IsQuestLocked(LeveId leveId)
+ {
+ // this only checks for the current class
+ IQuestInfo questInfo = _questData.GetQuestInfo(leveId);
+ if (!questInfo.ClassJobs.Contains((EClassJob)_clientState.LocalPlayer!.ClassJob.Id) ||
+ questInfo.Level > _clientState.LocalPlayer.Level)
+ return true;
+
+ return !IsQuestAccepted(leveId) && QuestManager.Instance()->NumLeveAllowances == 0;
+ }
+
private bool HasCompletedPreviousQuests(QuestInfo questInfo, ElementId? extraCompletedQuest)
{
if (questInfo.PreviousQuests.Count == 0)
using System;
+using System.Collections.Generic;
using Dalamud.Game.Text;
+using LLib.GameData;
using Questionable.Model.Questing;
namespace Questionable.Model;
public ushort Level { get; }
public EBeastTribe BeastTribe { get; }
public bool IsMainScenarioQuest { get; }
+ public IReadOnlyList<EClassJob> ClassJobs { get; }
public string SimplifiedName => Name
.Replace(".", "", StringComparison.Ordinal)
--- /dev/null
+using System.Collections.Generic;
+using LLib.GameData;
+using Lumina.Excel.GeneratedSheets;
+using Questionable.Model.Questing;
+
+namespace Questionable.Model;
+
+internal sealed class LeveInfo : IQuestInfo
+{
+ public LeveInfo(Leve leve)
+ {
+ QuestId = new LeveId((ushort)leve.RowId);
+ Name = leve.Name;
+ Level = leve.ClassJobLevel;
+ IssuerDataId = leve.LevelLevemete.Value!.Object;
+ ClassJobs = QuestInfoUtils.AsList(leve.ClassJobCategory.Value!);
+ }
+
+ public ElementId QuestId { get; }
+ public string Name { get; }
+ public uint IssuerDataId { get; }
+ public bool IsRepeatable => true;
+ public ushort Level { get; }
+ public EBeastTribe BeastTribe => EBeastTribe.None;
+ public bool IsMainScenarioQuest => false;
+ public IReadOnlyList<EClassJob> ClassJobs { get; }
+}
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
-using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using JetBrains.Annotations;
+using LLib.GameData;
using Questionable.Model.Questing;
using ExcelQuest = Lumina.Excel.GeneratedSheets.Quest;
PreviousInstanceContentJoin = (QuestJoin)quest.InstanceContentJoin;
GrandCompany = (GrandCompany)quest.GrandCompany.Row;
BeastTribe = (EBeastTribe)quest.BeastTribe.Row;
+ ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.Value!);
}
public bool CompletesInstantly { get; }
public GrandCompany GrandCompany { get; }
public EBeastTribe BeastTribe { get; }
+ public IReadOnlyList<EClassJob> ClassJobs { get; }
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)]
public enum QuestJoin : byte
--- /dev/null
+using System.Collections.Generic;
+using System.Linq;
+using LLib.GameData;
+using Lumina.Excel.GeneratedSheets;
+
+namespace Questionable.Model;
+
+internal static class QuestInfoUtils
+{
+ private static readonly Dictionary<uint, IReadOnlyList<EClassJob>> CachedClassJobs = new();
+
+ internal static IReadOnlyList<EClassJob> AsList(ClassJobCategory classJobCategory)
+ {
+ if (CachedClassJobs.TryGetValue(classJobCategory.RowId, out IReadOnlyList<EClassJob>? classJobs))
+ return classJobs;
+
+ classJobs = new Dictionary<EClassJob, bool>
+ {
+ { EClassJob.Adventurer, classJobCategory.ADV },
+ { EClassJob.Gladiator, classJobCategory.GLA },
+ { EClassJob.Pugilist, classJobCategory.PGL },
+ { EClassJob.Marauder, classJobCategory.MRD },
+ { EClassJob.Lancer, classJobCategory.LNC },
+ { EClassJob.Archer, classJobCategory.ARC },
+ { EClassJob.Conjurer, classJobCategory.CNJ },
+ { EClassJob.Thaumaturge, classJobCategory.THM },
+ { EClassJob.Carpenter, classJobCategory.CRP },
+ { EClassJob.Blacksmith, classJobCategory.BSM },
+ { EClassJob.Armorer, classJobCategory.ARM },
+ { EClassJob.Goldsmith, classJobCategory.GSM },
+ { EClassJob.Leatherworker, classJobCategory.LTW },
+ { EClassJob.Weaver, classJobCategory.WVR },
+ { EClassJob.Alchemist, classJobCategory.ALC },
+ { EClassJob.Culinarian, classJobCategory.CUL },
+ { EClassJob.Miner, classJobCategory.MIN },
+ { EClassJob.Botanist, classJobCategory.BTN },
+ { EClassJob.Fisher, classJobCategory.FSH },
+ { EClassJob.Paladin, classJobCategory.PLD },
+ { EClassJob.Monk, classJobCategory.MNK },
+ { EClassJob.Warrior, classJobCategory.WAR },
+ { EClassJob.Dragoon, classJobCategory.DRG },
+ { EClassJob.Bard, classJobCategory.BRD },
+ { EClassJob.WhiteMage, classJobCategory.WHM },
+ { EClassJob.BlackMage, classJobCategory.BLM },
+ { EClassJob.Arcanist, classJobCategory.ACN },
+ { EClassJob.Summoner, classJobCategory.SMN },
+ { EClassJob.Scholar, classJobCategory.SCH },
+ { EClassJob.Rogue, classJobCategory.ROG },
+ { EClassJob.Ninja, classJobCategory.NIN },
+ { EClassJob.Machinist, classJobCategory.MCH },
+ { EClassJob.DarkKnight, classJobCategory.DRK },
+ { EClassJob.Astrologian, classJobCategory.AST },
+ { EClassJob.Samurai, classJobCategory.SAM },
+ { EClassJob.RedMage, classJobCategory.RDM },
+ { EClassJob.BlueMage, classJobCategory.BLU },
+ { EClassJob.Gunbreaker, classJobCategory.GNB },
+ { EClassJob.Dancer, classJobCategory.DNC },
+ { EClassJob.Reaper, classJobCategory.RPR },
+ { EClassJob.Sage, classJobCategory.SGE },
+ { EClassJob.Viper, classJobCategory.VPR },
+ { EClassJob.Pictomancer, classJobCategory.PCT }
+ }
+ .Where(y => y.Value)
+ .Select(y => y.Key)
+ .ToList()
+ .AsReadOnly();
+ CachedClassJobs[classJobCategory.RowId] = classJobs;
+ return classJobs;
+ }
+}
--- /dev/null
+using System.Collections.Generic;
+using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
+using LLib.GameData;
+using Questionable.Model.Questing;
+
+namespace Questionable.Model;
+
+internal sealed class QuestProgressInfo
+{
+ private readonly string _asString;
+
+ public QuestProgressInfo(QuestWork questWork)
+ {
+ Id = new QuestId(questWork.QuestId);
+ Sequence = questWork.Sequence;
+ Flags = questWork.Flags;
+ Variables = [..questWork.Variables.ToArray()];
+ IsHidden = questWork.IsHidden;
+
+ var qw = questWork.Variables;
+ string vars = "";
+ for (int i = 0; i < qw.Length; ++i)
+ {
+ vars += qw[i] + " ";
+ if (i % 2 == 1)
+ vars += " ";
+ }
+
+ // For combat quests, a sequence to kill 3 enemies works a bit like this:
+ // Trigger enemies → 0
+ // Kill first enemy → 1
+ // Kill second enemy → 2
+ // Last enemy → increase sequence, reset variable to 0
+ // The order in which enemies are killed doesn't seem to matter.
+ // If multiple waves spawn, this continues to count up (e.g. 1 enemy from wave 1, 2 enemies from wave 2, 1 from wave 3) would count to 3 then 0
+ _asString = $"QW: {vars.Trim()}";
+ }
+
+ public QuestProgressInfo(LeveWork leveWork)
+ {
+ Id = new LeveId(leveWork.LeveId);
+ Sequence = leveWork.Sequence;
+ Flags = leveWork.Flags;
+ Variables = [0, 0, 0, 0, 0, 0];
+ IsHidden = leveWork.IsHidden;
+
+ _asString = $"Seed: {leveWork.LeveSeed}, Flags: {Flags:X}, Class: {(EClassJob)leveWork.ClearClass}";
+ }
+
+ public ElementId Id { get; }
+ public byte Sequence { get; }
+ public ushort Flags { get; init; }
+ public List<byte> Variables { get; }
+ public bool IsHidden { get; }
+
+ public override string ToString() => _asString;
+}
-using Lumina.Excel.GeneratedSheets;
+using System.Collections.Generic;
+using LLib.GameData;
+using Lumina.Excel.GeneratedSheets;
using Questionable.Model.Questing;
namespace Questionable.Model;
public ushort Level { get; }
public EBeastTribe BeastTribe => EBeastTribe.None;
public bool IsMainScenarioQuest => false;
+
+ /// <summary>
+ /// We don't have collectables implemented for any other class.
+ /// </summary>
+ public IReadOnlyList<EClassJob> ClassJobs { get; } = [EClassJob.Miner, EClassJob.Botanist];
}
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Gathering;
using Questionable.Controller.Steps.Interactions;
+using Questionable.Controller.Steps.Leves;
using Questionable.Data;
using Questionable.External;
using Questionable.Functions;
serviceCollection.AddSingleton<AetherCurrentData>();
serviceCollection.AddSingleton<AetheryteData>();
serviceCollection.AddSingleton<GatheringData>();
+ serviceCollection.AddSingleton<LeveData>();
serviceCollection.AddSingleton<JournalData>();
serviceCollection.AddSingleton<QuestData>();
serviceCollection.AddSingleton<TerritoryData>();
serviceCollection.AddTaskWithFactory<Jump.Factory, Jump.SingleJump, Jump.RepeatedJumps>();
serviceCollection.AddTaskWithFactory<Dive.Factory, Dive.DoDive>();
serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
- serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use, UseItem.UseOnPosition>();
+ serviceCollection
+ .AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use,
+ UseItem.UseOnPosition>();
serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>();
serviceCollection.AddTaskWithFactory<TurnInDelivery.Factory, TurnInDelivery.SatisfactionSupplyTurnIn>();
serviceCollection
.AddTaskWithFactory<SinglePlayerDuty.Factory, SinglePlayerDuty.DisableYesAlready,
SinglePlayerDuty.RestoreYesAlready>();
+ serviceCollection
+ .AddTaskWithFactory<InitiateLeve.Factory, InitiateLeve.OpenJournal, InitiateLeve.Initiate,
+ InitiateLeve.SelectDifficulty>();
serviceCollection
.AddTaskWithFactory<WaitAtEnd.Factory,
using Questionable.Controller;
using Questionable.Controller.Steps.Shared;
using Questionable.Functions;
+using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents;
}
}
- private QuestWork? DrawQuestWork(QuestController.QuestProgress currentQuest)
+ private QuestProgressInfo? DrawQuestWork(QuestController.QuestProgress currentQuest)
{
- if (currentQuest.Quest.Id is not QuestId questId)
- return null;
-
- var questWork = _questFunctions.GetQuestEx(questId);
+ var questWork = _questFunctions.GetQuestProgressInfo(currentQuest.Quest.Id);
if (questWork != null)
{
Vector4 color;
unsafe
{
- var ptr =ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled);
+ var ptr = ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled);
if (ptr != null)
color = *ptr;
else
}
using var styleColor = ImRaii.PushColor(ImGuiCol.Text, color);
-
- var qw = questWork.Value;
- string vars = "";
- for (int i = 0; i < 6; ++i)
- {
- vars += qw.Variables[i] + " ";
- if (i % 2 == 1)
- vars += " ";
- }
-
- // For combat quests, a sequence to kill 3 enemies works a bit like this:
- // Trigger enemies → 0
- // Kill first enemy → 1
- // Kill second enemy → 2
- // Last enemy → increase sequence, reset variable to 0
- // The order in which enemies are killed doesn't seem to matter.
- // If multiple waves spawn, this continues to count up (e.g. 1 enemy from wave 1, 2 enemies from wave 2, 1 from wave 3) would count to 3 then 0
- ImGui.Text($"QW: {vars.Trim()}");
+ ImGui.Text($"{questWork}");
if (ImGui.IsItemClicked())
{
- string copy = "";
- for (int i = 0; i < 6; ++i)
- copy += qw.Variables[i] + " ";
-
- copy = copy.Trim();
- ImGui.SetClipboardText(copy);
- _chatGui.Print($"Copied '{copy}' to clipboard");
+ ImGui.SetClipboardText(questWork.ToString());
+ _chatGui.Print($"Copied '{questWork}' to clipboard");
}
if (ImGui.IsItemHovered())
ImGui.PopFont();
}
}
- else
+ else if (currentQuest.Quest.Id is QuestId)
{
using var disabled = ImRaii.Disabled();
}
private void DrawQuestButtons(QuestController.QuestProgress currentQuest, QuestStep? currentStep,
- QuestWork? questWork)
+ QuestProgressInfo? questProgressInfo)
{
ImGui.BeginDisabled(_questController.IsRunning);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{
// if we haven't accepted this quest, mark it as next quest so that we can optionally use aetherytes to travel
- if (questWork == null)
+ if (questProgressInfo == null)
_questController.SetNextQuest(currentQuest.Quest);
_questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
bool colored = currentStep != null
&& !lastStep
&& currentStep.InteractionType == EInteractionType.Instruction
- && _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
+ && _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>(out _);
ImGui.BeginDisabled(lastStep);
if (colored)
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
+using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiNET;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
break;
case 1:
- _questRegistry.TryGetQuest(questManager->NormalQuests[trackedQuest.Index].QuestId,
- out var quest);
+ //_questRegistry.TryGetQuest(questManager->NormalQuests[trackedQuest.Index].QuestId,
+ // out var quest);
ImGui.Text(
- $"Tracked quest: {questManager->NormalQuests[trackedQuest.Index].QuestId}, {trackedQuest.Index}: {quest?.Info.Name}");
+ $"Quest: {questManager->NormalQuests[trackedQuest.Index].QuestId}, {trackedQuest.Index}");
+ break;
+
+ case 2:
+ ImGui.Text($"Leve: {questManager->LeveQuests[trackedQuest.Index].LeveId}, {trackedQuest.Index}");
break;
}
}
}
#endif
+#if false
+ var director = UIState.Instance()->DirectorTodo.Director;
+ if (director != null)
+ {
+ ImGui.Text($"Director: {director->ContentId}");
+ ImGui.Text($"Seq: {director->Sequence}");
+ ImGui.Text($"Ico: {director->IconId}");
+ if (director->EventHandlerInfo != null)
+ {
+ ImGui.Text($" EHI: {director->EventHandlerInfo->EventId.ContentId}");
+ ImGui.Text($" EHI: {director->EventHandlerInfo->EventId.Id}");
+ ImGui.Text($" EHI: {director->EventHandlerInfo->EventId.EntryId}");
+ ImGui.Text($" EHI: {director->EventHandlerInfo->Flags}");
+ }
+ }
+#endif
+
if (_targetManager.Target != null)
{
ImGui.Separator();
ImGui.SameLine();
if (knownQuest != null &&
- knownQuest.FindSequence(0)?.LastStep()?.InteractionType == EInteractionType.AcceptQuest &&
+ knownQuest.FindSequence(0)?.LastStep()?.InteractionType is EInteractionType.AcceptQuest
+ or EInteractionType.AcceptLeve &&
!_questFunctions.IsQuestAccepted(quest.QuestId) &&
!_questFunctions.IsQuestLocked(quest.QuestId) &&
(quest.IsRepeatable || !_questFunctions.IsQuestAcceptedOrComplete(quest.QuestId)))