Gathering leves proof-of-concept
authorLiza Carvelli <liza@carvel.li>
Wed, 7 Aug 2024 23:49:14 +0000 (01:49 +0200)
committerLiza Carvelli <liza@carvel.li>
Wed, 7 Aug 2024 23:49:14 +0000 (01:49 +0200)
38 files changed:
GatheringPaths/7.x - Dawntrail/Yak T'el/970.md [new file with mode: 0644]
GatheringPaths/7.x - Dawntrail/Yak T'el/970__MIN.json [new file with mode: 0644]
QuestPaths/7.x - Dawntrail/Leves/MIN/L1794_Vestiges of War.json [new file with mode: 0644]
QuestPaths/quest-v1.json
Questionable.Model/Questing/Converter/InteractionTypeConverter.cs
Questionable.Model/Questing/EAction.cs
Questionable.Model/Questing/EInteractionType.cs
Questionable.Model/Questing/GatheredItem.cs
Questionable/Controller/CombatController.cs
Questionable/Controller/GameUiController.cs
Questionable/Controller/GatheringController.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/QuestRegistry.cs
Questionable/Controller/Steps/Gathering/DoGather.cs
Questionable/Controller/Steps/Interactions/Combat.cs
Questionable/Controller/Steps/Interactions/Interact.cs
Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs
Questionable/Controller/Steps/Interactions/UseItem.cs
Questionable/Controller/Steps/Leves/InitiateLeve.cs [new file with mode: 0644]
Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs
Questionable/Controller/Steps/Shared/WaitAtEnd.cs
Questionable/Controller/Utils/QuestWorkUtils.cs
Questionable/Data/LeveData.cs [new file with mode: 0644]
Questionable/Data/QuestData.cs
Questionable/Functions/ExcelFunctions.cs
Questionable/Functions/GameFunctions.cs
Questionable/Functions/QuestFunctions.cs
Questionable/Model/IQuestInfo.cs
Questionable/Model/LeveInfo.cs [new file with mode: 0644]
Questionable/Model/QuestInfo.cs
Questionable/Model/QuestInfoUtils.cs [new file with mode: 0644]
Questionable/Model/QuestProgressInfo.cs [new file with mode: 0644]
Questionable/Model/SatisfactionSupplyInfo.cs
Questionable/QuestionablePlugin.cs
Questionable/Windows/QuestComponents/ActiveQuestComponent.cs
Questionable/Windows/QuestComponents/CreationUtilsComponent.cs
Questionable/Windows/QuestSelectionWindow.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 (file)
index 0000000..e69de29
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 (file)
index 0000000..a6abea0
--- /dev/null
@@ -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 (file)
index 0000000..ecbe236
--- /dev/null
@@ -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"
+        }
+      ]
+    }
+  ]
+}
index f0029f01f971e2a5d962451aa086720522a2dc91..313239f43f40907cfe19c681878ac2842daeff9e 100644 (file)
                     "Dive",
                     "Instruction",
                     "AcceptQuest",
-                    "CompleteQuest"
+                    "CompleteQuest",
+                    "InitiateLeve"
                   ]
                 },
                 "Disabled": {
                       "ItemId": {
                         "type": "number"
                       },
+                      "AlternativeItemId": {
+                        "description": "For leves that allow you to gather two items with different chance percentage, this is the preferred item if the gathering chance is 100% (after buffs)",
+                        "type": "number"
+                      },
                       "ItemCount": {
                         "type": "number",
                         "exclusiveMinimum": 0
index 4e375415189d1b5ccde902d2d2777bfc58c905a5..2343676dc867b0276eb42e41af48c69cd5859ad0 100644 (file)
@@ -28,5 +28,6 @@ public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>
         { EInteractionType.Instruction, "Instruction" },
         { EInteractionType.AcceptQuest, "AcceptQuest" },
         { EInteractionType.CompleteQuest, "CompleteQuest" },
+        { EInteractionType.InitiateLeve, "InitiateLeve" },
     };
 }
index c078e4a3f7bba279257cf2bf32c851d05566a12c..f6afab6bc6ba7ef6940809f0fddb90b679aea6f7 100644 (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
index e080f07cf5d648fb76e346ab4f791de708b94c50..144861b752c76b7d5ceb385250bbd9d5ba5b8398 100644 (file)
@@ -32,4 +32,7 @@ public enum EInteractionType
 
     AcceptQuest,
     CompleteQuest,
+    AcceptLeve,
+    InitiateLeve,
+    CompleteLeve,
 }
index 8b915954d12784ef815dba572ba62d2c351bd120..5f98aa591b46203df3b101af7608309fcca8cd29 100644 (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; }
 
index 057604d5263b68366e04921f19576b50454ac7ba..533cc816786cb78b1a7040ec285cb661467aea62 100644 (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);
index 8cd3f046abaadff17e2201a100718a3b0015a5e7..397b6abcc37b98c32f6296eaf9bf9565e528a1ad 100644 (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);
 }
index 52f251a8856bb55abae459638f56dc63c576e4be..18cb157f1e433b199908b70c5328f674639a1a23 100644 (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);
 
index 3cf9def6388195744261cb30f20c9c769627ac24..cb1cb91db9a00cf2f7dd34a5c6e564d562034225 100644 (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;
 
index 4457b3abf2564e8c1881c87a2efff70fbf652148..713f5d7bc891e71bdfdd9d427b6f92f10d021912 100644 (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;
index 9bdf27a222d531ac202957b99ede2a021a895a1b..e947212cf956a2b38f008919f82f6896f0963b4d 100644 (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);
 }
index 5112046405f7a27b5df80bba142f1c63a4c46cc8..e3bfc2588fbf5da5bf4b0d9b89cee33a0a88510e 100644 (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;
index 06674e929b07d07321cfae04f345495a13857dda..85e56c4449e1a26d00db6edbb92fff3ac4d4d725 100644 (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;
         }
index 648627c1ad22f560f27bde241255f9ef84183f23..0e1dbf8ce60a0a50467b142a0acca85c4929adee 100644 (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>()
             ];
         }
index dca8260f01f9ac29b6af64724d6a82558a415804..0a8f953a467e7706f5840b365993693c3e1188a0 100644 (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;
             }
 
diff --git a/Questionable/Controller/Steps/Leves/InitiateLeve.cs b/Questionable/Controller/Steps/Leves/InitiateLeve.cs
new file mode 100644 (file)
index 0000000..794120c
--- /dev/null
@@ -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;
+        }
+    }
+}
index 18afd5a521ac569cbdeed4c0060ab28c051c1d0c..7817f49ca8b478b2ca3771a72e3a804c02824f0a 100644 (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()
index 731d5d3856c75dcfd4902663a993fd69f12463ed..3bab5526d6eba736ac991d841a90fb7a08512227 100644 (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;
index badab7dc7b60285d6ec4b749e2716c1c25d1c0c8..97e53f7d317574f45cd7aa5cfe3d2cd22e84dbdd 100644 (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;
         }
index 5923591810bf724192b09ca754873da6ea49bd4d..4af3c90294820f40e1a8e2ad7d6964821ea10712 100644 (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))
         {
diff --git a/Questionable/Data/LeveData.cs b/Questionable/Data/LeveData.cs
new file mode 100644 (file)
index 0000000..463f7c4
--- /dev/null
@@ -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;
+        }
+    }
+}
index a57f8531a78b26e2efde8f1f92e1dd63868d742e..7ea55ca978eb38bffcb294a86f6ff5121f9f96cd 100644 (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);
     }
index cdf506502fc8c111f48f8b4ab40852ac2beb34cf..cb0cf63ea46ab013767c6593f8d11d5aea9a2015 100644 (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)
         {
index 2427c81dcd9d9f2edc9fc4b46152157d61cb2391..73f2a195424f74a5563ab0d5282e5847b5f3bc00 100644 (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
 }
index 42c9fb959001e20b1e748b084989ef7e2318ff84..7c907f8368d231f3650ff6f3b7ca4fcb564d3d25 100644 (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)
index 6822cce9b7ed59f468bce648a573aff20d413042..1410de30f9008c1b1c4123c3282fe4c4f31dc9a2 100644 (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)
diff --git a/Questionable/Model/LeveInfo.cs b/Questionable/Model/LeveInfo.cs
new file mode 100644 (file)
index 0000000..2be46ec
--- /dev/null
@@ -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; }
+}
index 526b973a2cded94f12d8203d3d0a37dabdb447ef..6737aab0d6468cc2bcfadcf3452da8934399c601 100644 (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
diff --git a/Questionable/Model/QuestInfoUtils.cs b/Questionable/Model/QuestInfoUtils.cs
new file mode 100644 (file)
index 0000000..6884588
--- /dev/null
@@ -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;
+    }
+}
diff --git a/Questionable/Model/QuestProgressInfo.cs b/Questionable/Model/QuestProgressInfo.cs
new file mode 100644 (file)
index 0000000..9d918e2
--- /dev/null
@@ -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;
+}
index b5d11bfed3ce76580d04c281d88bd994fa4f1b70..810437c63b192f531e04319a64fb2a181c66dd36 100644 (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];
 }
index 18033c6d181d323b43d38e33c4a2891914d51c73..3c0c78429e3a3e168d7bcb0293ee5089937b67fa 100644 (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,
index e1ef508bdb185cf5dbe19294ced217ac5a59fee8..bd5802ae9c8a7a73ff73cdca8dbe7cbc4202c254 100644 (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)
index dfcae518965c29263d50b85df73aee12ffd7c822..61bb05633c8122697a451ab77bf19c4163c26b48 100644 (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();
index ce6cb0fdc7bbca6377905ff934d4cd5a3a935ffb..1f7c47057e283244193ea2e5c51b61b92d1f6a95 100644 (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)))