From a7af4853699840d1a1178e2aa569389b87ed5c68 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 8 Aug 2024 01:49:14 +0200 Subject: [PATCH] Gathering leves proof-of-concept --- .../7.x - Dawntrail/Yak T'el/970.md | 0 .../7.x - Dawntrail/Yak T'el/970__MIN.json | 115 +++++++++++ .../Leves/MIN/L1794_Vestiges of War.json | 39 ++++ QuestPaths/quest-v1.json | 7 +- .../Converter/InteractionTypeConverter.cs | 1 + Questionable.Model/Questing/EAction.cs | 7 +- .../Questing/EInteractionType.cs | 3 + Questionable.Model/Questing/GatheredItem.cs | 1 + Questionable/Controller/CombatController.cs | 5 +- Questionable/Controller/GameUiController.cs | 178 +++++++++++++++++- .../Controller/GatheringController.cs | 16 +- Questionable/Controller/QuestController.cs | 51 ++++- Questionable/Controller/QuestRegistry.cs | 20 +- .../Controller/Steps/Gathering/DoGather.cs | 172 +++++++++++++++-- .../Controller/Steps/Interactions/Combat.cs | 6 +- .../Controller/Steps/Interactions/Interact.cs | 11 +- .../Steps/Interactions/SinglePlayerDuty.cs | 2 +- .../Controller/Steps/Interactions/UseItem.cs | 6 +- .../Controller/Steps/Leves/InitiateLeve.cs | 122 ++++++++++++ .../Steps/Shared/GatheringRequiredItems.cs | 3 +- .../Controller/Steps/Shared/SkipCondition.cs | 11 +- .../Controller/Steps/Shared/WaitAtEnd.cs | 4 +- .../Controller/Utils/QuestWorkUtils.cs | 13 +- Questionable/Data/LeveData.cs | 133 +++++++++++++ Questionable/Data/QuestData.cs | 6 +- Questionable/Functions/ExcelFunctions.cs | 7 +- Questionable/Functions/GameFunctions.cs | 23 +++ Questionable/Functions/QuestFunctions.cs | 69 +++++-- Questionable/Model/IQuestInfo.cs | 3 + Questionable/Model/LeveInfo.cs | 27 +++ Questionable/Model/QuestInfo.cs | 7 +- Questionable/Model/QuestInfoUtils.cs | 70 +++++++ Questionable/Model/QuestProgressInfo.cs | 57 ++++++ Questionable/Model/SatisfactionSupplyInfo.cs | 9 +- Questionable/QuestionablePlugin.cs | 9 +- .../QuestComponents/ActiveQuestComponent.cs | 46 ++--- .../QuestComponents/CreationUtilsComponent.cs | 30 ++- Questionable/Windows/QuestSelectionWindow.cs | 3 +- 38 files changed, 1168 insertions(+), 124 deletions(-) create mode 100644 GatheringPaths/7.x - Dawntrail/Yak T'el/970.md create mode 100644 GatheringPaths/7.x - Dawntrail/Yak T'el/970__MIN.json create mode 100644 QuestPaths/7.x - Dawntrail/Leves/MIN/L1794_Vestiges of War.json create mode 100644 Questionable/Controller/Steps/Leves/InitiateLeve.cs create mode 100644 Questionable/Data/LeveData.cs create mode 100644 Questionable/Model/LeveInfo.cs create mode 100644 Questionable/Model/QuestInfoUtils.cs create mode 100644 Questionable/Model/QuestProgressInfo.cs diff --git a/GatheringPaths/7.x - Dawntrail/Yak T'el/970.md b/GatheringPaths/7.x - Dawntrail/Yak T'el/970.md new file mode 100644 index 00000000..e69de29b diff --git a/GatheringPaths/7.x - Dawntrail/Yak T'el/970__MIN.json b/GatheringPaths/7.x - Dawntrail/Yak T'el/970__MIN.json new file mode 100644 index 00000000..a6abea06 --- /dev/null +++ b/GatheringPaths/7.x - Dawntrail/Yak T'el/970__MIN.json @@ -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 + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/QuestPaths/7.x - Dawntrail/Leves/MIN/L1794_Vestiges of War.json b/QuestPaths/7.x - Dawntrail/Leves/MIN/L1794_Vestiges of War.json new file mode 100644 index 00000000..ecbe236a --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Leves/MIN/L1794_Vestiges of War.json @@ -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" + } + ] + } + ] +} diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index f0029f01..313239f4 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -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 diff --git a/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs b/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs index 4e375415..2343676d 100644 --- a/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs +++ b/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs @@ -28,5 +28,6 @@ public sealed class InteractionTypeConverter() : EnumConverter { EInteractionType.Instruction, "Instruction" }, { EInteractionType.AcceptQuest, "AcceptQuest" }, { EInteractionType.CompleteQuest, "CompleteQuest" }, + { EInteractionType.InitiateLeve, "InitiateLeve" }, }; } diff --git a/Questionable.Model/Questing/EAction.cs b/Questionable.Model/Questing/EAction.cs index c078e4a3..f6afab6b 100644 --- a/Questionable.Model/Questing/EAction.cs +++ b/Questionable.Model/Questing/EAction.cs @@ -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 diff --git a/Questionable.Model/Questing/EInteractionType.cs b/Questionable.Model/Questing/EInteractionType.cs index e080f07c..144861b7 100644 --- a/Questionable.Model/Questing/EInteractionType.cs +++ b/Questionable.Model/Questing/EInteractionType.cs @@ -32,4 +32,7 @@ public enum EInteractionType AcceptQuest, CompleteQuest, + AcceptLeve, + InitiateLeve, + CompleteLeve, } diff --git a/Questionable.Model/Questing/GatheredItem.cs b/Questionable.Model/Questing/GatheredItem.cs index 8b915954..5f98aa59 100644 --- a/Questionable.Model/Questing/GatheredItem.cs +++ b/Questionable.Model/Questing/GatheredItem.cs @@ -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; } diff --git a/Questionable/Controller/CombatController.cs b/Questionable/Controller/CombatController.cs index 057604d5..533cc816 100644 --- a/Questionable/Controller/CombatController.cs +++ b/Questionable/Controller/CombatController.cs @@ -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); diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs index 8cd3f046..397b6abc 100644 --- a/Questionable/Controller/GameUiController.cs +++ b/Questionable/Controller/GameUiController.cs @@ -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 _logger; private readonly Regex _returnRegex; @@ -48,7 +54,9 @@ internal sealed class GameUiController : IDisposable QuestData questData, IGameGui gameGui, ITargetManager targetManager, - IPluginLog pluginLog, ILogger logger) + IFramework framework, + IPluginLog pluginLog, + ILogger 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()!.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 answers, bool checkAllSteps) { List dialogueChoices = []; - var currentQuest = _questController.SimulatedQuest ?? _questController.GatheringQuest ?? _questController.StartedQuest; + + // levequest choices have some vague sort of priority + if (_questController.HasCurrentTaskMatching(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); } diff --git a/Questionable/Controller/GatheringController.cs b/Questionable/Controller/GatheringController.cs index 52f251a8..18cb157f 100644 --- a/Questionable/Controller/GatheringController.cs +++ b/Questionable/Controller/GatheringController.cs @@ -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 : MiniTaskControllerDirectorTodo.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() .With(_currentRequest.Root.TerritoryId, MountTask.EMountIf.Always)); if (currentNode.Locations.Count > 1) @@ -142,7 +155,7 @@ internal sealed unsafe class GatheringController : MiniTaskController() .With(_currentRequest.Root.TerritoryId, currentNode)); _taskQueue.Enqueue(_serviceProvider.GetRequiredService() - .With(currentNode.DataId, true)); + .With(currentNode.DataId, null, EInteractionType.None, true)); _taskQueue.Enqueue(_serviceProvider.GetRequiredService() .With(_currentRequest.Data, currentNode)); if (_currentRequest.Data.Collectability > 0) @@ -195,6 +208,7 @@ internal sealed unsafe class GatheringController : MiniTaskController private QuestProgress? _nextQuest; private QuestProgress? _simulatedQuest; private QuestProgress? _gatheringQuest; + private QuestProgress? _pendingQuest; private EAutomationType _automationType; /// @@ -101,6 +102,11 @@ internal sealed class QuestController : MiniTaskController public QuestProgress? NextQuest => _nextQuest; public QuestProgress? GatheringQuest => _gatheringQuest; + /// + /// Used when accepting leves, as there's a small delay + /// + public QuestProgress? PendingQuest => _pendingQuest; + public string? DebugState { get; private set; } public void Reload() @@ -112,6 +118,7 @@ internal sealed class QuestController : MiniTaskController _startedQuest = null; _nextQuest = null; _gatheringQuest = null; + _pendingQuest = null; _simulatedQuest = null; _safeAnimationEnd = DateTime.MinValue; @@ -188,6 +195,20 @@ internal sealed class QuestController : MiniTaskController { 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 { _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 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 _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 return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})"; } - public bool HasCurrentTaskMatching() => - _currentTask is T; + public bool HasCurrentTaskMatching([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; diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 4457b3ab..713f5d7b 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -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 _logger; - private readonly ICallGateProvider _reloadDataIpc; + private readonly LeveData _leveData; + private readonly ICallGateProvider _reloadDataIpc; private readonly Dictionary _quests = new(); public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator, - ILogger logger) + ILogger logger, LeveData leveData) { _pluginInterface = pluginInterface; _questData = questData; _questValidator = questValidator; _jsonSchemaValidator = jsonSchemaValidator; _logger = logger; + _leveData = leveData; _reloadDataIpc = _pluginInterface.GetIpcProvider("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()!; + var questInfo = _questData.GetQuestInfo(questId); + if (questInfo is LeveInfo leveInfo) + _leveData.AddQuestSteps(leveInfo, questRoot); Quest quest = new Quest { Id = questId, - Root = questNode.Deserialize()!, - Info = _questData.GetQuestInfo(questId), + Root = questRoot, + Info = questInfo, ReadOnly = false, }; _quests[quest.Id] = quest; diff --git a/Questionable/Controller/Steps/Gathering/DoGather.cs b/Questionable/Controller/Steps/Gathering/DoGather.cs index 9bdf27a2..e947212c 100644 --- a/Questionable/Controller/Steps/Gathering/DoGather.cs +++ b/Questionable/Controller/Steps/Gathering/DoGather.cs @@ -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 logger) : ITask { + private const uint StatusGatheringRateUp = 218; + private GatheringController.GatheringRequest _currentRequest = null!; private GatheringNode _currentNode = null!; private bool _wasGathering; - private List? _slots; - + private SlotInfo? _slotToGather; + private Queue? _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 ReadSlots(AtkUnitBase* atkUnitBase) + private unsafe List ReadSlots(AddonGathering* addonGathering) { - var atkValues = atkUnitBase->AtkValues; + var atkValues = addonGathering->AtkValues; List slots = new List(); 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 GetNextActions(NodeCondition nodeCondition, List slots) + { + uint gp = clientState.LocalPlayer!.CurrentGp; + Queue 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); } diff --git a/Questionable/Controller/Steps/Interactions/Combat.cs b/Questionable/Controller/Steps/Interactions/Combat.cs index 51120464..e3bfc258 100644 --- a/Questionable/Controller/Steps/Interactions/Combat.cs +++ b/Questionable/Controller/Steps/Interactions/Combat.cs @@ -37,7 +37,7 @@ internal static class Combat ArgumentNullException.ThrowIfNull(step.DataId); yield return serviceProvider.GetRequiredService() - .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; diff --git a/Questionable/Controller/Steps/Interactions/Interact.cs b/Questionable/Controller/Steps/Interactions/Interact.cs index 06674e92..85e56c44 100644 --- a/Questionable/Controller/Steps/Interactions/Interact.cs +++ b/Questionable/Controller/Steps/Interactions/Interact.cs @@ -19,7 +19,8 @@ internal static class Interact { public IEnumerable 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(); yield return serviceProvider.GetRequiredService() - .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; } diff --git a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs index 648627c1..0e1dbf8c 100644 --- a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs +++ b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs @@ -22,7 +22,7 @@ internal static class SinglePlayerDuty [ serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService() - .With(step.DataId.Value, true), + .With(step.DataId.Value, quest, EInteractionType.None, true), serviceProvider.GetRequiredService() ]; } diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index dca8260f..0a8f953a 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -109,7 +109,7 @@ internal static class UseItem yield return serviceProvider.GetRequiredService() .With(territoryId, destination, dataId: npcId, sprint: false); yield return serviceProvider.GetRequiredService() - .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; } diff --git a/Questionable/Controller/Steps/Leves/InitiateLeve.cs b/Questionable/Controller/Steps/Leves/InitiateLeve.cs new file mode 100644 index 00000000..794120cf --- /dev/null +++ b/Questionable/Controller/Steps/Leves/InitiateLeve.cs @@ -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 CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.InteractionType != EInteractionType.InitiateLeve) + yield break; + + yield return serviceProvider.GetRequiredService().With(quest.Id); + yield return serviceProvider.GetRequiredService().With(quest.Id); + yield return serviceProvider.GetRequiredService(); + 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; + } + } +} diff --git a/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs index 18afd5a5..7817f49c 100644 --- a/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs +++ b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs @@ -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() diff --git a/Questionable/Controller/Steps/Shared/SkipCondition.cs b/Questionable/Controller/Steps/Shared/SkipCondition.cs index 731d5d38..3bab5526 100644 --- a/Questionable/Controller/Steps/Shared/SkipCondition.cs +++ b/Questionable/Controller/Steps/Shared/SkipCondition.cs @@ -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; diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index badab7dc..97e53f7d 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -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; } diff --git a/Questionable/Controller/Utils/QuestWorkUtils.cs b/Questionable/Controller/Utils/QuestWorkUtils.cs index 59235918..4af3c902 100644 --- a/Questionable/Controller/Utils/QuestWorkUtils.cs +++ b/Questionable/Controller/Utils/QuestWorkUtils.cs @@ -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 completionQuestVariablesFlags, QuestWork questWork) + public static bool MatchesQuestWork(IList 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?> requiredQuestVariables, - QuestWork questWork, ILogger logger) + QuestProgressInfo questWork, ILogger logger) { if (requiredQuestVariables.Count != 6 || requiredQuestVariables.All(x => x == null || x.Count == 0)) { diff --git a/Questionable/Data/LeveData.cs b/Questionable/Data/LeveData.cs new file mode 100644 index 00000000..463f7c43 --- /dev/null +++ b/Questionable/Data/LeveData.cs @@ -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 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; + } + } +} diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index a57f8531..7ea55ca9 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -24,7 +24,11 @@ internal sealed class QuestData .Select(x => new QuestInfo(x)), ..dataManager.GetExcelSheet()! .Where(x => x.RowId > 0) - .Select(x => new SatisfactionSupplyInfo(x)) + .Select(x => new SatisfactionSupplyInfo(x)), + ..dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0) + .Where(x => x.LevelLevemete.Row != 0) + .Select(x => new LeveInfo(x)), ]; _quests = quests.ToDictionary(x => x.QuestId, x => x); } diff --git a/Questionable/Functions/ExcelFunctions.cs b/Questionable/Functions/ExcelFunctions.cs index cdf50650..cb0cf63e 100644 --- a/Questionable/Functions/ExcelFunctions.cs +++ b/Questionable/Functions/ExcelFunctions.cs @@ -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()!.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(excelSheetName); if (excelSheet == null) { diff --git a/Questionable/Functions/GameFunctions.cs b/Questionable/Functions/GameFunctions.cs index 2427c81d..73f2a195 100644 --- a/Questionable/Functions/GameFunctions.cs +++ b/Questionable/Functions/GameFunctions.cs @@ -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 } diff --git a/Questionable/Functions/QuestFunctions.cs b/Questionable/Functions/QuestFunctions.cs index 42c9fb95..7c907f83 100644 --- a/Questionable/Functions/QuestFunctions.cs +++ b/Questionable/Functions/QuestFunctions.cs @@ -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) diff --git a/Questionable/Model/IQuestInfo.cs b/Questionable/Model/IQuestInfo.cs index 6822cce9..1410de30 100644 --- a/Questionable/Model/IQuestInfo.cs +++ b/Questionable/Model/IQuestInfo.cs @@ -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 ClassJobs { get; } public string SimplifiedName => Name .Replace(".", "", StringComparison.Ordinal) diff --git a/Questionable/Model/LeveInfo.cs b/Questionable/Model/LeveInfo.cs new file mode 100644 index 00000000..2be46ecb --- /dev/null +++ b/Questionable/Model/LeveInfo.cs @@ -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 ClassJobs { get; } +} diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index 526b973a..6737aab0 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -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 ClassJobs { get; } [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] public enum QuestJoin : byte diff --git a/Questionable/Model/QuestInfoUtils.cs b/Questionable/Model/QuestInfoUtils.cs new file mode 100644 index 00000000..68845884 --- /dev/null +++ b/Questionable/Model/QuestInfoUtils.cs @@ -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> CachedClassJobs = new(); + + internal static IReadOnlyList AsList(ClassJobCategory classJobCategory) + { + if (CachedClassJobs.TryGetValue(classJobCategory.RowId, out IReadOnlyList? classJobs)) + return classJobs; + + classJobs = new Dictionary + { + { 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; + } +} diff --git a/Questionable/Model/QuestProgressInfo.cs b/Questionable/Model/QuestProgressInfo.cs new file mode 100644 index 00000000..9d918e2f --- /dev/null +++ b/Questionable/Model/QuestProgressInfo.cs @@ -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 Variables { get; } + public bool IsHidden { get; } + + public override string ToString() => _asString; +} diff --git a/Questionable/Model/SatisfactionSupplyInfo.cs b/Questionable/Model/SatisfactionSupplyInfo.cs index b5d11bfe..810437c6 100644 --- a/Questionable/Model/SatisfactionSupplyInfo.cs +++ b/Questionable/Model/SatisfactionSupplyInfo.cs @@ -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; + + /// + /// We don't have collectables implemented for any other class. + /// + public IReadOnlyList ClassJobs { get; } = [EClassJob.Miner, EClassJob.Botanist]; } diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 18033c6d..3c0c7842 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -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(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -143,12 +145,17 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); - serviceCollection.AddTaskWithFactory(); + serviceCollection + .AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); serviceCollection .AddTaskWithFactory(); + serviceCollection + .AddTaskWithFactory(); serviceCollection .AddTaskWithFactory(); + && _questController.HasCurrentTaskMatching(out _); ImGui.BeginDisabled(lastStep); if (colored) diff --git a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs index dfcae518..61bb0563 100644 --- a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs +++ b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs @@ -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(); diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index ce6cb0fd..1f7c4705 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -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)))