Gathering leves proof-of-concept

pull/15/head
Liza 2024-08-08 01:49:14 +02:00
parent 41abceb89a
commit a7af485369
Signed by: liza
GPG Key ID: 7199F8D727D55F67
38 changed files with 1168 additions and 124 deletions

View File

@ -0,0 +1,115 @@
{
"$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
}
]
}
]
}
]
}

View File

@ -0,0 +1,39 @@
{
"$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"
}
]
}
]
}

View File

@ -121,7 +121,8 @@
"Dive",
"Instruction",
"AcceptQuest",
"CompleteQuest"
"CompleteQuest",
"InitiateLeve"
]
},
"Disabled": {
@ -326,6 +327,10 @@
"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

View File

@ -28,5 +28,6 @@ public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>
{ EInteractionType.Instruction, "Instruction" },
{ EInteractionType.AcceptQuest, "AcceptQuest" },
{ EInteractionType.CompleteQuest, "CompleteQuest" },
{ EInteractionType.InitiateLeve, "InitiateLeve" },
};
}

View File

@ -26,7 +26,12 @@ public enum EAction
MeticulousBotanist = 22188,
ScrutinyBotanist = 22189,
SharpVision1 = 235,
SharpVision2 = 237,
SharpVision3 = 295,
FieldMastery1 = 218,
FieldMastery2 = 220,
FieldMastery3 = 294,
}
public static class EActionExtensions

View File

@ -32,4 +32,7 @@ public enum EInteractionType
AcceptQuest,
CompleteQuest,
AcceptLeve,
InitiateLeve,
CompleteLeve,
}

View File

@ -3,6 +3,7 @@
public sealed class GatheredItem
{
public uint ItemId { get; set; }
public uint AlternativeItemId { get; set; }
public int ItemCount { get; set; }
public ushort Collectability { get; set; }

View File

@ -171,9 +171,8 @@ internal sealed class CombatController : IDisposable
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);

View File

@ -7,12 +7,17 @@ using Dalamud.Game.Addon.Lifecycle;
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;
@ -34,6 +39,7 @@ internal sealed class GameUiController : IDisposable
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;
@ -48,7 +54,9 @@ internal sealed class GameUiController : IDisposable
QuestData questData,
IGameGui gameGui,
ITargetManager targetManager,
IPluginLog pluginLog, ILogger<GameUiController> logger)
IFramework framework,
IPluginLog pluginLog,
ILogger<GameUiController> logger)
{
_addonLifecycle = addonLifecycle;
_dataManager = dataManager;
@ -60,6 +68,7 @@ internal sealed class GameUiController : IDisposable
_questData = questData;
_gameGui = gameGui;
_targetManager = targetManager;
_framework = framework;
_logger = logger;
_returnRegex = _dataManager.GetExcelSheet<Addon>()!.GetRow(196)!.GetRegex(addon => addon.Text, pluginLog)!;
@ -75,6 +84,8 @@ internal sealed class GameUiController : IDisposable
_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()
@ -225,7 +236,41 @@ internal sealed class GameUiController : IDisposable
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;
@ -291,10 +336,30 @@ internal sealed class GameUiController : IDisposable
}
}
}
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)
{
@ -344,7 +409,7 @@ internal sealed class GameUiController : IDisposable
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)
@ -403,6 +468,16 @@ internal sealed class GameUiController : IDisposable
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))
@ -437,6 +512,20 @@ internal sealed class GameUiController : IDisposable
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;
@ -515,22 +604,24 @@ internal sealed class GameUiController : IDisposable
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;
}
@ -684,7 +775,74 @@ internal sealed class GameUiController : IDisposable
}
}
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;
@ -701,6 +859,8 @@ internal sealed class GameUiController : IDisposable
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);
@ -714,5 +874,5 @@ internal sealed class GameUiController : IDisposable
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
}
private sealed record DialogueChoiceInfo(Quest Quest, DialogueChoice DialogueChoice);
private sealed record DialogueChoiceInfo(Quest? Quest, DialogueChoice DialogueChoice);
}

View File

@ -6,6 +6,8 @@ using Dalamud.Game.ClientState.Conditions;
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;
@ -17,6 +19,7 @@ using Questionable.External;
using Questionable.Functions;
using Questionable.GatheringPaths;
using Questionable.Model.Gathering;
using Questionable.Model.Questing;
namespace Questionable.Controller;
@ -119,6 +122,16 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
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)
@ -142,7 +155,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
_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)
@ -195,6 +208,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
public sealed record GatheringRequest(
GatheringPointId GatheringPointId,
uint ItemId,
uint AlternativeItemId,
int Quantity,
ushort Collectability = 0);

View File

@ -1,11 +1,11 @@
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;
@ -37,6 +37,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
private QuestProgress? _nextQuest;
private QuestProgress? _simulatedQuest;
private QuestProgress? _gatheringQuest;
private QuestProgress? _pendingQuest;
private EAutomationType _automationType;
/// <summary>
@ -101,6 +102,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>
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()
@ -112,6 +118,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_startedQuest = null;
_nextQuest = null;
_gatheringQuest = null;
_pendingQuest = null;
_simulatedQuest = null;
_safeAnimationEnd = DateTime.MinValue;
@ -188,6 +195,20 @@ internal sealed class QuestController : MiniTaskController<QuestController>
{
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
@ -201,6 +222,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>
{
_logger.LogInformation("Next quest {QuestId} accepted or completed",
_nextQuest.Quest.Id);
// if (_nextQuest.Quest.Id is LeveId)
// _startedQuest = _nextQuest;
_nextQuest = null;
}
}
@ -315,7 +340,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
var sequence = q.FindSequence(questToRun.Sequence);
if (sequence == null)
{
DebugState = "Sequence not found";
DebugState = $"Sequence {sequence} not found";
Stop("Unknown sequence");
return;
}
@ -457,6 +482,12 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_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))
@ -555,8 +586,20 @@ internal sealed class QuestController : MiniTaskController<QuestController>
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;

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
@ -26,19 +25,21 @@ internal sealed class QuestRegistry
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");
}
@ -89,11 +90,14 @@ internal sealed class QuestRegistry
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;
@ -143,11 +147,15 @@ internal sealed class QuestRegistry
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;

View File

@ -1,11 +1,18 @@
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;
@ -13,13 +20,17 @@ internal sealed class DoGather(
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)
{
@ -45,17 +56,44 @@ internal sealed class DoGather(
_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);
}
}
}
}
}
@ -65,9 +103,9 @@ internal sealed class DoGather(
: 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)
{
@ -76,14 +114,122 @@ internal sealed class DoGather(
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);
}

View File

@ -37,7 +37,7 @@ internal static class Combat
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;
}
@ -110,11 +110,11 @@ internal static class Combat
// 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;

View File

@ -19,7 +19,8 @@ internal static class Interact
{
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;
@ -34,7 +35,7 @@ internal static class Interact
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);
}
@ -50,11 +51,15 @@ internal static class Interact
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;
}

View File

@ -22,7 +22,7 @@ internal static class SinglePlayerDuty
[
serviceProvider.GetRequiredService<DisableYesAlready>(),
serviceProvider.GetRequiredService<Interact.DoInteract>()
.With(step.DataId.Value, true),
.With(step.DataId.Value, quest, EInteractionType.None, true),
serviceProvider.GetRequiredService<RestoreYesAlready>()
];
}

View File

@ -109,7 +109,7 @@ internal static class UseItem
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);
}
}
@ -145,9 +145,9 @@ internal static class UseItem
{
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;
}

View File

@ -0,0 +1,122 @@
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;
}
}
}

View File

@ -105,7 +105,8 @@ internal static class GatheringRequiredItems
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()

View File

@ -158,12 +158,12 @@ internal static class SkipCondition
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;
@ -172,7 +172,7 @@ internal static class SkipCondition
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;
@ -181,8 +181,7 @@ internal static class SkipCondition
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;

View File

@ -179,9 +179,9 @@ internal static class WaitAtEnd
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;
}

View File

@ -4,6 +4,7 @@ using System.Linq;
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;
@ -15,12 +16,12 @@ internal static class QuestWorkUtils
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)
@ -28,8 +29,8 @@ internal static class QuestWorkUtils
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;
@ -60,7 +61,7 @@ internal static class QuestWorkUtils
}
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))
{

View File

@ -0,0 +1,133 @@
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;
}
}
}

View File

@ -24,7 +24,11 @@ internal sealed class QuestData
.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);
}

View File

@ -24,7 +24,7 @@ internal sealed class ExcelFunctions
_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)
@ -33,9 +33,9 @@ internal sealed class ExcelFunctions
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 +
@ -49,6 +49,7 @@ internal sealed class ExcelFunctions
excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}";
}
ArgumentNullException.ThrowIfNull(excelSheetName);
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
if (excelSheet == null)
{

View File

@ -344,6 +344,17 @@ internal sealed unsafe class GameFunctions
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])
@ -503,4 +514,16 @@ internal sealed unsafe class GameFunctions
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
}

View File

@ -29,7 +29,8 @@ internal sealed unsafe class QuestFunctions
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;
@ -117,6 +118,14 @@ internal sealed unsafe class QuestFunctions
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;
}
@ -189,23 +198,23 @@ internal sealed unsafe class QuestFunctions
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 })
@ -239,6 +248,8 @@ internal sealed unsafe class QuestFunctions
{
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
@ -251,10 +262,24 @@ internal sealed unsafe class QuestFunctions
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
@ -267,10 +292,17 @@ internal sealed unsafe class QuestFunctions
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
@ -295,6 +327,17 @@ internal sealed unsafe class QuestFunctions
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)

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.Text;
using LLib.GameData;
using Questionable.Model.Questing;
namespace Questionable.Model;
@ -13,6 +15,7 @@ public interface IQuestInfo
public ushort Level { get; }
public EBeastTribe BeastTribe { get; }
public bool IsMainScenarioQuest { get; }
public IReadOnlyList<EClassJob> ClassJobs { get; }
public string SimplifiedName => Name
.Replace(".", "", StringComparison.Ordinal)

View File

@ -0,0 +1,27 @@
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; }
}

View File

@ -1,10 +1,9 @@
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;
@ -53,6 +52,7 @@ internal sealed class QuestInfo : IQuestInfo
PreviousInstanceContentJoin = (QuestJoin)quest.InstanceContentJoin;
GrandCompany = (GrandCompany)quest.GrandCompany.Row;
BeastTribe = (EBeastTribe)quest.BeastTribe.Row;
ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.Value!);
}
@ -73,6 +73,7 @@ internal sealed class QuestInfo : IQuestInfo
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

View File

@ -0,0 +1,70 @@
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;
}
}

View File

@ -0,0 +1,57 @@
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;
}

View File

@ -1,4 +1,6 @@
using Lumina.Excel.GeneratedSheets;
using System.Collections.Generic;
using LLib.GameData;
using Lumina.Excel.GeneratedSheets;
using Questionable.Model.Questing;
namespace Questionable.Model;
@ -20,4 +22,9 @@ internal sealed class SatisfactionSupplyInfo : IQuestInfo
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];
}

View File

@ -15,6 +15,7 @@ using Questionable.Controller.Steps.Shared;
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;
@ -103,6 +104,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<AetherCurrentData>();
serviceCollection.AddSingleton<AetheryteData>();
serviceCollection.AddSingleton<GatheringData>();
serviceCollection.AddSingleton<LeveData>();
serviceCollection.AddSingleton<JournalData>();
serviceCollection.AddSingleton<QuestData>();
serviceCollection.AddSingleton<TerritoryData>();
@ -143,12 +145,17 @@ public sealed class QuestionablePlugin : IDalamudPlugin
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,

View File

@ -14,6 +14,7 @@ using ImGuiNET;
using Questionable.Controller;
using Questionable.Controller.Steps.Shared;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents;
@ -156,18 +157,15 @@ internal sealed class ActiveQuestComponent
}
}
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
@ -175,34 +173,12 @@ internal sealed class ActiveQuestComponent
}
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())
@ -213,7 +189,7 @@ internal sealed class ActiveQuestComponent
ImGui.PopFont();
}
}
else
else if (currentQuest.Quest.Id is QuestId)
{
using var disabled = ImRaii.Disabled();
@ -227,13 +203,13 @@ internal sealed class ActiveQuestComponent
}
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);
@ -261,7 +237,7 @@ internal sealed class ActiveQuestComponent
bool colored = currentStep != null
&& !lastStep
&& currentStep.InteractionType == EInteractionType.Instruction
&& _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
&& _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>(out _);
ImGui.BeginDisabled(lastStep);
if (colored)

View File

@ -9,8 +9,11 @@ using Dalamud.Interface;
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;
@ -93,16 +96,37 @@ internal sealed class CreationUtilsComponent
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();

View File

@ -223,7 +223,8 @@ internal sealed class QuestSelectionWindow : LWindow
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)))