"Y": 257.4255,
                 "Z": -669.3115
               },
-              "MinimumAngle": -65,
+              "MinimumAngle": -30,
               "MaximumAngle": 5
             }
           ]
       ]
     }
   ]
-}
+}
\ No newline at end of file
 
-Subproject commit 9db9f95b8cd3f36262b5b4b14f12b7331d3c7279
+Subproject commit 43c3dba112c202e2d0ff1a6909020c2b83e20dc3
 
                                         .AsSyntaxNodeOrToken(),
                                     Assignment(nameof(DialogueChoice.Answer), dialogueChoice.Answer, emptyChoice.Answer)
                                         .AsSyntaxNodeOrToken(),
+                                    Assignment(nameof(DialogueChoice.AnswerIsRegularExpression),
+                                            dialogueChoice.AnswerIsRegularExpression,
+                                            emptyChoice.AnswerIsRegularExpression)
+                                        .AsSyntaxNodeOrToken(),
                                     Assignment(nameof(DialogueChoice.DataId), dialogueChoice.DataId, emptyChoice.DataId)
                                         .AsSyntaxNodeOrToken()))));
             }
                                         .AsSyntaxNodeOrToken(),
                                     Assignment(nameof(GatheredItem.Collectability), gatheredItem.Collectability,
                                             emptyItem.Collectability)
+                                        .AsSyntaxNodeOrToken(),
+                                    Assignment(nameof(GatheredItem.ClassJob), gatheredItem.ClassJob,
+                                            emptyItem.ClassJob)
                                         .AsSyntaxNodeOrToken()))));
             }
             else if (value is GatheringNodeGroup nodeGroup)
 
           },
           "StopDistance": 5,
           "TerritoryId": 478,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/003/CtsSfsCharacter1_00386",
+              "Prompt": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/003/CtsSfsCharacter1_00386",
+              "Prompt": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_003"
+            }
+          ]
         }
       ]
     }
 
           "TerritoryId": 478,
           "InteractionType": "Interact",
           "RequiredGatheredItems": [],
-          "AetheryteShortcut": "Idyllshire"
+          "AetheryteShortcut": "Idyllshire",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/005/CtsSfsCharacter4_00541",
+              "Prompt": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/005/CtsSfsCharacter4_00541",
+              "Prompt": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_004"
+            }
+          ]
         }
       ]
     }
 
           "TerritoryId": 613,
           "InteractionType": "Interact",
           "RequiredGatheredItems": [],
-          "AetheryteShortcut": "Ruby Sea - Tamamizu"
+          "AetheryteShortcut": "Ruby Sea - Tamamizu",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/004/CtsSfsCharacter3_00481",
+              "Prompt": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 613,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/004/CtsSfsCharacter3_00481",
+              "Prompt": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_004"
+            }
+          ]
         }
       ]
     }
 
           "TerritoryId": 635,
           "InteractionType": "Interact",
           "RequiredGatheredItems": [],
-          "AetheryteShortcut": "Rhalgr's Reach"
+          "AetheryteShortcut": "Rhalgr's Reach",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/004/CtsSfsCharacter2_00434",
+              "Prompt": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/004/CtsSfsCharacter2_00434",
+              "Prompt": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_003"
+            }
+          ]
         }
       ]
     }
 
           "AethernetShortcut": [
             "[Ishgard] Aetheryte Plaza",
             "[Ishgard] Firmament"
+          ],
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/007/CtsSfsCharacter7_00710",
+              "Prompt": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/007/CtsSfsCharacter7_00710",
+              "Prompt": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_004"
+            }
           ]
         }
       ]
 
           "AethernetShortcut": [
             "[Ishgard] Aetheryte Plaza",
             "[Ishgard] Firmament"
+          ],
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/006/CtsSfsCharacter6_00674",
+              "Prompt": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/006/CtsSfsCharacter6_00674",
+              "Prompt": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_003"
+            }
           ]
         }
       ]
 
           "TerritoryId": 820,
           "InteractionType": "Interact",
           "RequiredGatheredItems": [],
-          "AetheryteShortcut": "Eulmore"
+          "AetheryteShortcut": "Eulmore",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/006/CtsSfsCharacter5_00640",
+              "Prompt": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/006/CtsSfsCharacter5_00640",
+              "Prompt": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_004"
+            }
+          ]
         }
       ]
     }
 
           "AethernetShortcut": [
             "[Old Sharlayan] Aetheryte Plaza",
             "[Old Sharlayan] The Leveilleur Estate"
+          ],
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/007/CtsSfsCharacter8_00773",
+              "Prompt": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/007/CtsSfsCharacter8_00773",
+              "Prompt": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_004"
+            }
           ]
         }
       ]
 
           "InteractionType": "Interact",
           "RequiredGatheredItems": [],
           "AetheryteShortcut": "Il Mheg - Lydha Lran",
-          "Fly": true
+          "Fly": true,
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/008/CtsSfsCharacter9_00815",
+              "Prompt": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/008/CtsSfsCharacter9_00815",
+              "Prompt": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_004"
+            }
+          ]
         }
       ]
     }
 
             "Z": -65.14081
           },
           "TerritoryId": 956,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/008/CtsSfsCharacter10_00842",
+              "Prompt": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_001",
+              "AnswerIsRegularExpression": true
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "TerritoryId": 635,
+          "InteractionType": "None",
+          "DialogueChoices": [
+            {
+              "Type": "List",
+              "ExcelSheet": "custom/008/CtsSfsCharacter10_00842",
+              "Prompt": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_000",
+              "Answer": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_004"
+            }
+          ]
         }
       ]
     }
 
             "Z": -68.40625
           },
           "TerritoryId": 963,
-          "InteractionType": "AcceptQuest",
-          "DialogueChoices": [
-            {
-              "Type": "List",
-              "Prompt": "TEXT_AKTKMM103_04753_Q1_000_000",
-              "Answer": "TEXT_AKTKMM103_04753_A1_000_001"
-            }
-          ]
+          "InteractionType": "AcceptQuest"
         }
       ]
     },
 
                   "type": "string",
                   "description": "What to do at the position",
                   "enum": [
+                    "None",
                     "Interact",
                     "WalkTo",
                     "AttuneAethernetShard",
 
 {
     private static readonly Dictionary<EInteractionType, string> Values = new()
     {
+        { EInteractionType.None, "None" },
         { EInteractionType.Interact, "Interact" },
         { EInteractionType.WalkTo, "WalkTo" },
         { EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" },
 
 
     [JsonConverter(typeof(ExcelRefConverter))]
     public ExcelRef? Answer { get; set; }
+    public bool AnswerIsRegularExpression { get; set; }
 
     /// <summary>
     /// If set, only applies when focusing the given target id.
 
 [JsonConverter(typeof(InteractionTypeConverter))]
 public enum EInteractionType
 {
+    None,
     Interact,
     WalkTo,
     AttuneAethernetShard,
 
     public uint ItemId { get; set; }
     public int ItemCount { get; set; }
     public ushort Collectability { get; set; }
+
+    /// <summary>
+    /// Either miner or botanist; null if it is irrelevant (prefers current class/job, then any unlocked ones).
+    /// </summary>
+    public uint? ClassJob { get; set; }
 }
 
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
-using Dalamud.Game;
-using Dalamud.Game.ClientState.Objects;
-using Dalamud.Game.ClientState.Objects.Types;
-using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.System.Framework;
-using FFXIVClientStructs.FFXIV.Client.System.Memory;
-using FFXIVClientStructs.FFXIV.Client.System.String;
-using Lumina.Excel.GeneratedSheets;
-using Microsoft.Extensions.Logging;
-using Questionable.Model.Questing;
-
-namespace Questionable;
-
-internal sealed unsafe class ChatFunctions
-{
-    private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
-
-    private readonly ReadOnlyDictionary<EEmote, string> _emoteCommands;
-
-    private readonly GameFunctions _gameFunctions;
-    private readonly ITargetManager _targetManager;
-    private readonly ILogger<ChatFunctions> _logger;
-    private readonly ProcessChatBoxDelegate _processChatBox;
-    private readonly delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString;
-
-    public ChatFunctions(ISigScanner sigScanner, IDataManager dataManager, GameFunctions gameFunctions,
-        ITargetManager targetManager, ILogger<ChatFunctions> logger)
-    {
-        _gameFunctions = gameFunctions;
-        _targetManager = targetManager;
-        _logger = logger;
-        _processChatBox =
-            Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
-        _sanitiseString =
-            (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sigScanner.ScanText(Signatures.SanitiseString);
-
-        _emoteCommands = dataManager.GetExcelSheet<Emote>()!
-            .Where(x => x.RowId > 0)
-            .Where(x => x.TextCommand != null && x.TextCommand.Value != null)
-            .Select(x => (x.RowId, Command: x.TextCommand.Value!.Command?.ToString()))
-            .Where(x => x.Command != null && x.Command.StartsWith('/'))
-            .ToDictionary(x => (EEmote)x.RowId, x => x.Command!)
-            .AsReadOnly();
-    }
-
-    /// <summary>
-    /// <para>
-    /// Send a given message to the chat box. <b>This can send chat to the server.</b>
-    /// </para>
-    /// <para>
-    /// <b>This method is unsafe.</b> This method does no checking on your input and
-    /// may send content to the server that the normal client could not. You must
-    /// verify what you're sending and handle content and length to properly use
-    /// this.
-    /// </para>
-    /// </summary>
-    /// <param name="message">Message to send</param>
-    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
-    private void SendMessageUnsafe(byte[] message)
-    {
-        var uiModule = (IntPtr)Framework.Instance()->GetUIModule();
-
-        using var payload = new ChatPayload(message);
-        var mem1 = Marshal.AllocHGlobal(400);
-        Marshal.StructureToPtr(payload, mem1, false);
-
-        _processChatBox(uiModule, mem1, IntPtr.Zero, 0);
-
-        Marshal.FreeHGlobal(mem1);
-    }
-
-    /// <summary>
-    /// <para>
-    /// Send a given message to the chat box. <b>This can send chat to the server.</b>
-    /// </para>
-    /// <para>
-    /// This method is slightly less unsafe than <see cref="SendMessageUnsafe"/>. It
-    /// will throw exceptions for certain inputs that the client can't normally send,
-    /// but it is still possible to make mistakes. Use with caution.
-    /// </para>
-    /// </summary>
-    /// <param name="message">message to send</param>
-    /// <exception cref="ArgumentException">If <paramref name="message"/> is empty, longer than 500 bytes in UTF-8, or contains invalid characters.</exception>
-    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
-    private void SendMessage(string message)
-    {
-        _logger.LogDebug("Attempting to send chat message '{Message}'", message);
-        var bytes = Encoding.UTF8.GetBytes(message);
-        if (bytes.Length == 0)
-            throw new ArgumentException("message is empty", nameof(message));
-
-        if (bytes.Length > 500)
-            throw new ArgumentException("message is longer than 500 bytes", nameof(message));
-
-        if (message.Length != SanitiseText(message).Length)
-            throw new ArgumentException("message contained invalid characters", nameof(message));
-
-        SendMessageUnsafe(bytes);
-    }
-
-    /// <summary>
-    /// <para>
-    /// Sanitises a string by removing any invalid input.
-    /// </para>
-    /// <para>
-    /// The result of this method is safe to use with
-    /// <see cref="SendMessage"/>, provided that it is not empty or too
-    /// long.
-    /// </para>
-    /// </summary>
-    /// <param name="text">text to sanitise</param>
-    /// <returns>sanitised text</returns>
-    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
-    private string SanitiseText(string text)
-    {
-        var uText = Utf8String.FromString(text);
-
-        _sanitiseString(uText, 0x27F, IntPtr.Zero);
-        var sanitised = uText->ToString();
-
-        uText->Dtor();
-        IMemorySpace.Free(uText);
-
-        return sanitised;
-    }
-
-    public void ExecuteCommand(string command)
-    {
-        if (!command.StartsWith('/'))
-            return;
-
-        SendMessage(command);
-    }
-
-    public void UseEmote(uint dataId, EEmote emote)
-    {
-        IGameObject? gameObject = _gameFunctions.FindObjectByDataId(dataId);
-        if (gameObject != null)
-        {
-            _targetManager.Target = gameObject;
-            ExecuteCommand($"{_emoteCommands[emote]} motion");
-        }
-    }
-
-    public void UseEmote(EEmote emote)
-    {
-        ExecuteCommand($"{_emoteCommands[emote]} motion");
-    }
-
-    private static class Signatures
-    {
-        internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9";
-        internal const string SanitiseString = "E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 0F B6 F0 E8 ?? ?? ?? ?? 48 8D 4D C0";
-    }
-
-    [StructLayout(LayoutKind.Explicit)]
-    [SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")]
-    private readonly struct ChatPayload : IDisposable
-    {
-        [FieldOffset(0)] private readonly IntPtr textPtr;
-
-        [FieldOffset(16)] private readonly ulong textLen;
-
-        [FieldOffset(8)] private readonly ulong unk1;
-
-        [FieldOffset(24)] private readonly ulong unk2;
-
-        internal ChatPayload(byte[] stringBytes)
-        {
-            textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
-            Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length);
-            Marshal.WriteByte(textPtr + stringBytes.Length, 0);
-
-            textLen = (ulong)(stringBytes.Length + 1);
-
-            unk1 = 64;
-            unk2 = 0;
-        }
-
-        public void Dispose()
-        {
-            Marshal.FreeHGlobal(textPtr);
-        }
-    }
-}
 
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.CombatModules;
 using Questionable.Controller.Utils;
+using Questionable.Functions;
 using Questionable.Model.Questing;
 
 namespace Questionable.Controller;
     private readonly IObjectTable _objectTable;
     private readonly ICondition _condition;
     private readonly IClientState _clientState;
-    private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly ILogger<CombatController> _logger;
 
     private CurrentFight? _currentFight;
         IObjectTable objectTable,
         ICondition condition,
         IClientState clientState,
-        GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
         ILogger<CombatController> logger)
     {
         _combatModules = combatModules.ToList();
         _objectTable = objectTable;
         _condition = condition;
         _clientState = clientState;
-        _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _logger = logger;
 
         _clientState.TerritoryChanged += TerritoryChanged;
                     }
                 }
 
-                if (QuestWorkUtils.HasCompletionFlags(condition.CompletionQuestVariablesFlags) && _currentFight.Data.QuestElementId is QuestId questId)
+                if (QuestWorkUtils.HasCompletionFlags(condition.CompletionQuestVariablesFlags) && _currentFight.Data.ElementId is QuestId questId)
                 {
-                    var questWork = _gameFunctions.GetQuestEx(questId);
+                    var questWork = _questFunctions.GetQuestEx(questId);
                     if (questWork != null && QuestWorkUtils.MatchesQuestWork(condition.CompletionQuestVariablesFlags,
                             questWork.Value))
                     {
 
     public sealed class CombatData
     {
-        public required ElementId QuestElementId { get; init; }
+        public required ElementId ElementId { get; init; }
         public required EEnemySpawnType SpawnType { get; init; }
         public required List<uint> KillEnemyDataIds { get; init; }
         public required List<ComplexCombatData> ComplexCombatDatas { get; init; }
 
 using Dalamud.Game.ClientState.Objects;
 using Dalamud.Game.Command;
 using Dalamud.Plugin.Services;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 using Questionable.Windows;
     private readonly QuestWindow _questWindow;
     private readonly QuestSelectionWindow _questSelectionWindow;
     private readonly ITargetManager _targetManager;
-    private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
 
     public CommandHandler(
         ICommandManager commandManager,
         QuestWindow questWindow,
         QuestSelectionWindow questSelectionWindow,
         ITargetManager targetManager,
-        GameFunctions gameFunctions)
+        QuestFunctions questFunctions)
     {
         _commandManager = commandManager;
         _chatGui = chatGui;
         _questWindow = questWindow;
         _questSelectionWindow = questSelectionWindow;
         _targetManager = targetManager;
-        _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
 
         _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand)
         {
     {
         if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null)
         {
-            if (_gameFunctions.IsQuestLocked(questId))
+            if (_questFunctions.IsQuestLocked(questId))
                 _chatGui.PrintError($"[Questionable] Quest {questId} is locked.");
             else if (_questRegistry.TryGetQuest(questId, out Quest? quest))
             {
 
 using LLib.GameData;
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.GameStructs;
 using Questionable.Model;
 using Questionable.Model.Questing;
     private readonly GatheringData _gatheringData;
     private readonly QuestRegistry _questRegistry;
     private readonly QuestData _questData;
+    private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly IGameGui _gameGui;
     private readonly IChatGui _chatGui;
     private readonly IClientState _clientState;
         GatheringData gatheringData,
         QuestRegistry questRegistry,
         QuestData questData,
+        GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
         IGameGui gameGui,
         IChatGui chatGui,
         IClientState clientState,
         _gatheringData = gatheringData;
         _questRegistry = questRegistry;
         _questData = questData;
+        _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _gameGui = gameGui;
         _chatGui = chatGui;
         _clientState = clientState;
 
     private void MenuOpened(IMenuOpenedArgs args)
     {
-        uint itemId = (uint) _gameGui.HoveredItem;
+        uint itemId = (uint)_gameGui.HoveredItem;
         if (itemId == 0)
             return;
 
         if (itemId >= 500_000)
             itemId -= 500_000;
 
-        if (!_gatheringData.TryGetGatheringPointId(itemId, (EClassJob)_clientState.LocalPlayer!.ClassJob.Id, out _))
+        if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId))
+        {
+            AddContextMenuEntry(args, itemId, npcId, EClassJob.Miner, "Mine");
+            AddContextMenuEntry(args, itemId, npcId, EClassJob.Botanist, "Harvest");
+        }
+    }
+
+    private void AddContextMenuEntry(IMenuOpenedArgs args, uint itemId, uint npcId, EClassJob classJob, string verb)
+    {
+        EClassJob currentClassJob = (EClassJob)_clientState.LocalPlayer!.ClassJob.Id;
+        if (classJob != currentClassJob && currentClassJob is EClassJob.Miner or EClassJob.Botanist)
+            return;
+
+        if (!_gatheringData.TryGetGatheringPointId(itemId, classJob, out _))
         {
             _logger.LogInformation("No gathering point found for current job.");
             return;
         }
 
-        if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId))
-        {
-            ushort collectability = _gatheringData.GetRecommendedCollectability(itemId);
-            int quantityToGather = collectability > 0 ? 6 : int.MaxValue;
-            if (collectability == 0)
-                return;
+        ushort collectability = _gatheringData.GetRecommendedCollectability(itemId);
+        int quantityToGather = collectability > 0 ? 6 : int.MaxValue;
+        if (collectability == 0)
+            return;
 
-            unsafe
+        unsafe
+        {
+            var agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
+            if (agentSatisfactionSupply->IsAgentActive())
             {
-                var agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
-                if (agentSatisfactionSupply->IsAgentActive())
-                {
-                    quantityToGather = Math.Min(agentSatisfactionSupply->RemainingAllowances,
-                        ((AgentSatisfactionSupply2*)agentSatisfactionSupply)->TurnInsToNextRank);
-                }
+                quantityToGather = Math.Min(agentSatisfactionSupply->RemainingAllowances,
+                    ((AgentSatisfactionSupply2*)agentSatisfactionSupply)->TurnInsToNextRank);
             }
-
-            args.AddMenuItem(new MenuItem
-            {
-                Prefix = SeIconChar.Hyadelyn,
-                PrefixColor = 52,
-                Name = "Gather with Questionable",
-                OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability),
-                IsEnabled = quantityToGather > 0,
-            });
         }
+
+        string lockedReasonn = string.Empty;
+        if (!_questFunctions.IsClassJobUnlocked(classJob))
+            lockedReasonn = $"{classJob} not unlocked";
+        else if (quantityToGather == 0)
+            lockedReasonn = "No allowances";
+        else if (_gameFunctions.IsOccupied())
+            lockedReasonn = "Can't be used while interacting";
+
+        string name = $"{verb} with Questionable";
+        if (!string.IsNullOrEmpty(lockedReasonn))
+            name += $" ({lockedReasonn})";
+
+        args.AddMenuItem(new MenuItem
+        {
+            Prefix = SeIconChar.Hyadelyn,
+            PrefixColor = 52,
+            Name = name,
+            OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability, classJob),
+            IsEnabled = string.IsNullOrEmpty(lockedReasonn),
+        });
     }
 
-    private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability)
+    private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability, EClassJob classJob)
     {
-        var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId).Single(x => x is SatisfactionSupplyInfo);
+        var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId)
+            .Single(x => x is SatisfactionSupplyInfo);
         if (_questRegistry.TryGetQuest(info.QuestId, out Quest? quest))
         {
             var step = quest.FindSequence(0)!.FindStep(0)!;
                 {
                     ItemId = itemId,
                     ItemCount = quantity,
-                    Collectability = collectability
+                    Collectability = collectability,
+                    ClassJob = (uint)classJob,
                 }
             ];
             _questController.SetGatheringQuest(quest);
 
 using Lumina.Excel.GeneratedSheets;
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
+using Questionable.Functions;
+using Questionable.Model;
 using Questionable.Model.Questing;
 using Quest = Questionable.Model.Quest;
 using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
     private readonly IAddonLifecycle _addonLifecycle;
     private readonly IDataManager _dataManager;
     private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
+    private readonly ExcelFunctions _excelFunctions;
     private readonly QuestController _questController;
     private readonly QuestRegistry _questRegistry;
     private readonly QuestData _questData;
     private readonly ILogger<GameUiController> _logger;
     private readonly Regex _returnRegex;
 
-    public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions,
-        QuestController questController, QuestRegistry questRegistry, QuestData questData, IGameGui gameGui,
-        ITargetManager targetManager, IPluginLog pluginLog, ILogger<GameUiController> logger)
+    public GameUiController(
+        IAddonLifecycle addonLifecycle,
+        IDataManager dataManager,
+        GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
+        ExcelFunctions excelFunctions,
+        QuestController questController,
+        QuestRegistry questRegistry,
+        QuestData questData,
+        IGameGui gameGui,
+        ITargetManager targetManager,
+        IPluginLog pluginLog, ILogger<GameUiController> logger)
     {
         _addonLifecycle = addonLifecycle;
         _dataManager = dataManager;
         _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
+        _excelFunctions = excelFunctions;
         _questController = questController;
         _questRegistry = questRegistry;
         _questData = questData;
     {
         // it is possible for this to be a quest selection
         string questName = quest.Info.Name;
-        int questSelection = answers.FindIndex(x => GameStringEquals(questName, x));
+        int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x));
         if (questSelection >= 0)
         {
             addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
     private int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
     {
         List<DialogueChoiceInfo> dialogueChoices = [];
-        var currentQuest = _questController.SimulatedQuest ?? _questController.StartedQuest;
+        var currentQuest = _questController.SimulatedQuest ?? _questController.GatheringQuest ?? _questController.StartedQuest;
         if (currentQuest != null)
         {
             var quest = currentQuest.Quest;
         var target = _targetManager.Target;
         if (target != null)
         {
-            foreach (var questInfo in _questData.GetAllByIssuerDataId(target.DataId))
+            foreach (var questInfo in _questData.GetAllByIssuerDataId(target.DataId).Where(x => x.QuestId is QuestId))
             {
-                if (_gameFunctions.IsReadyToAcceptQuest(questInfo.QuestId) &&
+                if (_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId) &&
                     _questRegistry.TryGetQuest(questInfo.QuestId, out Quest? knownQuest))
                 {
                     var questChoices = knownQuest.FindSequence(0)?.Steps
                 continue;
             }
 
-            string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
-            string? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer);
+            string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
+                ?.GetString();
+            StringOrRegex? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer,
+                dialogueChoice.AnswerIsRegularExpression);
 
             if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
             {
                 continue;
             }
 
-            if (actualPrompt != null && (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt)))
+            if (actualPrompt != null &&
+                (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt)))
             {
                 _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
                     excelPrompt, actualPrompt);
             {
                 _logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}",
                     answers[i], excelAnswer);
-                if (GameStringEquals(answers[i], excelAnswer))
+                if (IsMatch(answers[i], excelAnswer))
                 {
                     _logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'",
                         i, answers[i], actualPrompt);
+
+                    // ensure we only open the dialog once
+                    if (quest.Id is SatisfactionSupplyNpcId)
+                    {
+                        if (_questController.GatheringQuest == null ||
+                            _questController.GatheringQuest.Sequence == 255)
+                            return null;
+
+                        _questController.GatheringQuest.SetSequence(1);
+                        _questController.ExecuteNextStep(QuestController.EAutomationType.CurrentQuestOnly);
+                    }
+
                     return i;
                 }
             }
         return null;
     }
 
+    private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer)
+    {
+        if (actualAnswer == null && expectedAnswer == null)
+            return true;
+
+        if (actualAnswer == null || expectedAnswer == null)
+            return false;
+
+        return expectedAnswer.IsMatch(actualAnswer);
+    }
+
     private int? HandleInstanceListChoice(string? actualPrompt)
     {
         if (!_questController.IsRunning)
             return null;
 
-        string? expectedPrompt = _gameFunctions.GetDialogueTextByRowId("Addon", 2090);
-        if (GameStringEquals(actualPrompt, expectedPrompt))
+        string? expectedPrompt = _excelFunctions.GetDialogueTextByRowId("Addon", 2090, false).GetString();
+        if (GameFunctions.GameStringEquals(actualPrompt, expectedPrompt))
         {
             _logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt);
             return 0; // any instance
                 continue;
             }
 
-            string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
-            if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt))
+            string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
+                ?.GetString();
+            if (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt))
             {
                 _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
                     excelPrompt, actualPrompt);
             string? excelName = entry.Name?.ToString();
             string? excelQuestion = entry.Question?.ToString();
 
-            if (excelQuestion != null && GameStringEquals(excelQuestion, actualPrompt))
+            if (excelQuestion != null && GameFunctions.GameStringEquals(excelQuestion, actualPrompt))
             {
                 warpId = entry.RowId;
                 warpText = excelQuestion;
                 return true;
             }
-            else if (excelName != null && GameStringEquals(excelName, actualPrompt))
+            else if (excelName != null && GameFunctions.GameStringEquals(excelName, actualPrompt))
             {
                 warpId = entry.RowId;
                 warpText = excelName;
         }
     }
 
-    /// <summary>
-    /// Ensures characters like '-' are handled equally in both strings.
-    /// </summary>
-    public static bool GameStringEquals(string? a, string? b)
-    {
-        if (a == null)
-            return b == null;
-
-        if (b == null)
-            return false;
-
-        return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-');
-    }
-
-    private string? ResolveReference(Quest quest, string? excelSheet, ExcelRef? excelRef)
+    private StringOrRegex? ResolveReference(Quest quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
     {
         if (excelRef == null)
             return null;
 
         if (excelRef.Type == ExcelRef.EType.Key)
-            return _gameFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey());
+            return _excelFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey(), isRegExp);
         else if (excelRef.Type == ExcelRef.EType.RowId)
-            return _gameFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId());
+            return _excelFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId(), isRegExp);
         else if (excelRef.Type == ExcelRef.EType.RawString)
-            return excelRef.AsRawString();
+            return new StringOrRegex(excelRef.AsRawString());
 
         return null;
     }
 
 using Questionable.Controller.Steps.Interactions;
 using Questionable.Controller.Steps.Shared;
 using Questionable.External;
+using Questionable.Functions;
 using Questionable.GatheringPaths;
 using Questionable.Model.Gathering;
 
 
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.NavigationOverrides;
 using Questionable.External;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Common;
 using Questionable.Model.Common.Converter;
 
 using System.Numerics;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.UI;
+using Questionable.Functions;
 using Questionable.Model;
 
 namespace Questionable.Controller;
 
 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;
 using Questionable.External;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
 {
     private readonly IClientState _clientState;
     private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly MovementController _movementController;
     private readonly CombatController _combatController;
     private readonly GatheringController _gatheringController;
     public QuestController(
         IClientState clientState,
         GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
         MovementController movementController,
         CombatController combatController,
         GatheringController gatheringController,
     {
         _clientState = clientState;
         _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _movementController = movementController;
         _combatController = combatController;
         _gatheringController = gatheringController;
         {
             if (_simulatedQuest != null)
                 return (_simulatedQuest, ECurrentQuestType.Simulated);
-            else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
+            else if (_nextQuest != null && _questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
                 return (_nextQuest, ECurrentQuestType.Next);
             else if (_gatheringQuest != null)
                 return (_gatheringQuest, ECurrentQuestType.Gathering);
         UpdateCurrentTask();
     }
 
-    private void UpdateCurrentQuest()
+    private unsafe void UpdateCurrentQuest()
     {
         lock (_progressLock)
         {
                 // if the quest is accepted, we no longer track it
                 bool canUseNextQuest;
                 if (_nextQuest.Quest.Info.IsRepeatable)
-                    canUseNextQuest = !_gameFunctions.IsQuestAccepted(_nextQuest.Quest.Id);
+                    canUseNextQuest = !_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id);
                 else
-                    canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id);
+                    canUseNextQuest = !_questFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id);
 
                 if (!canUseNextQuest)
                 {
                 currentSequence = _simulatedQuest.Sequence;
                 questToRun = _simulatedQuest;
             }
-            else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
+            else if (_nextQuest != null && _questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
             {
                 questToRun = _nextQuest;
                 currentSequence = _nextQuest.Sequence; // by definition, this should always be 0
                     _taskQueue.Count == 0 &&
                     _automationType == EAutomationType.Automatic)
                     ExecuteNextStep(_automationType);
-
             }
             else
             {
-                (ElementId? currentQuestId, currentSequence) = _gameFunctions.GetCurrentQuest();
+                (ElementId? currentQuestId, currentSequence) = _questFunctions.GetCurrentQuest();
                 if (currentQuestId == null || currentQuestId.Value == 0)
                 {
                     if (_startedQuest != null)
                 return;
             }
 
-            if (_gameFunctions.IsOccupied())
+            if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questToRun.Quest))
             {
                 DebugState = "Occupied";
                 return;
             if (questToRun.Sequence != currentSequence)
             {
                 questToRun.SetSequence(currentSequence);
-                Stop($"New sequence {questToRun == _startedQuest}/{_gameFunctions.GetCurrentQuestInternal()}",
+                Stop($"New sequence {questToRun == _startedQuest}/{_questFunctions.GetCurrentQuestInternal()}",
                     continueIfAutomatic: true);
             }
 
 
     protected override void UpdateCurrentTask()
     {
-        if (_gameFunctions.IsOccupied())
+        if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(CurrentQuest?.Quest))
             return;
 
         base.UpdateCurrentTask();
 
     protected override void OnNextStep(ILastTask task)
     {
-        IncreaseStepCount(task.QuestElementId, task.Sequence, true);
+        IncreaseStepCount(task.ElementId, task.Sequence, true);
     }
 
     public void ExecuteNextStep(EAutomationType automatic)
         if (CurrentQuest == null || seq == null || step == null)
         {
             if (CurrentQuestDetails?.Progress.Quest.Id is SatisfactionSupplyNpcId &&
-                CurrentQuestDetails?.Progress.Sequence == 0 &&
+                CurrentQuestDetails?.Progress.Sequence == 1 &&
                 CurrentQuestDetails?.Progress.Step == 255 &&
                 CurrentQuestDetails?.Type == ECurrentQuestType.Gathering)
             {
         }
     }
 
-    public void Skip(ElementId questQuestElementId, byte currentQuestSequence)
+    public void Skip(ElementId elementId, byte currentQuestSequence)
     {
         lock (_progressLock)
         {
                 if (_taskQueue.Count == 0)
                 {
                     Stop("Skip");
-                    IncreaseStepCount(questQuestElementId, currentQuestSequence);
+                    IncreaseStepCount(elementId, currentQuestSequence);
                 }
             }
             else
             {
                 Stop("SkipNx");
-                IncreaseStepCount(questQuestElementId, currentQuestSequence);
+                IncreaseStepCount(elementId, currentQuestSequence);
             }
         }
     }
         foreach (var id in priorityQuests)
         {
             var questId = new QuestId(id);
-            if (_gameFunctions.IsReadyToAcceptQuest(questId) && _questRegistry.TryGetQuest(questId, out var quest))
+            if (_questFunctions.IsReadyToAcceptQuest(questId) && _questRegistry.TryGetQuest(questId, out var quest))
             {
                 SetNextQuest(quest);
                 _chatGui.Print(
 
 using FFXIVClientStructs.FFXIV.Client.Game;
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
+using Questionable.Functions;
 
 namespace Questionable.Controller.Steps.Common;
 
 
 using System;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
         }
     }
 
-    internal sealed class SetQuest(QuestRegistry questRegistry, QuestController questController, GameFunctions gameFunctions, ILogger<SetQuest> logger) : ITask
+    internal sealed class SetQuest(QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger<SetQuest> logger) : ITask
     {
-        public ElementId NextQuestElementId { get; set; } = null!;
-        public ElementId CurrentQuestElementId { get; set; } = null!;
+        public ElementId NextQuestId { get; set; } = null!;
+        public ElementId CurrentQuestId { get; set; } = null!;
 
-        public ITask With(ElementId nextQuestElementId, ElementId currentQuestElementId)
+        public ITask With(ElementId nextQuestId, ElementId currentQuestId)
         {
-            NextQuestElementId = nextQuestElementId;
-            CurrentQuestElementId = currentQuestElementId;
+            NextQuestId = nextQuestId;
+            CurrentQuestId = currentQuestId;
             return this;
         }
 
         public bool Start()
         {
-            if (gameFunctions.IsQuestLocked(NextQuestElementId, CurrentQuestElementId))
+            if (questFunctions.IsQuestLocked(NextQuestId, CurrentQuestId))
             {
-                logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", NextQuestElementId);
+                logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", NextQuestId);
             }
-            else if (questRegistry.TryGetQuest(NextQuestElementId, out Quest? quest))
+            else if (questRegistry.TryGetQuest(NextQuestId, out Quest? quest))
             {
-                logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", NextQuestElementId, quest.Info.Name);
+                logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", NextQuestId, quest.Info.Name);
                 questController.SetNextQuest(quest);
             }
             else
             {
-                logger.LogInformation("Next quest with id {QuestId} not found", NextQuestElementId);
+                logger.LogInformation("Next quest with id {QuestId} not found", NextQuestId);
                 questController.SetNextQuest(null);
             }
 
 
         public ETaskResult Update() => ETaskResult.TaskComplete;
 
-        public override string ToString() => $"SetNextQuest({NextQuestElementId})";
+        public override string ToString() => $"SetNextQuest({NextQuestId})";
     }
 }
 
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin.Services;
 using Microsoft.Extensions.Logging;
+using Questionable.Functions;
 
 namespace Questionable.Controller.Steps.Common;
 
 
 using LLib.GameData;
 using LLib.GameUI;
 using Microsoft.Extensions.Logging;
+using Questionable.Functions;
 using Questionable.Model.Gathering;
 using Questionable.Model.Questing;
 
 
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Shared;
+using Questionable.Functions;
 using Questionable.Model.Gathering;
 
 namespace Questionable.Controller.Steps.Gathering;
 
--- /dev/null
+using System;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.Questing;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.Steps.Gathering;
+
+internal static class TurnInDelivery
+{
+    internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+    {
+        public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+        {
+            if (quest.Id is not SatisfactionSupplyNpcId || sequence.Sequence != 1)
+                return null;
+
+            return serviceProvider.GetRequiredService<SatisfactionSupplyTurnIn>();
+        }
+    }
+
+    internal sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : ITask
+    {
+        private ushort? _remainingAllowances;
+
+        public bool Start() => true;
+
+        public unsafe ETaskResult Update()
+        {
+            AgentSatisfactionSupply* agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
+            if (agentSatisfactionSupply == null || !agentSatisfactionSupply->IsAgentActive())
+                return _remainingAllowances == null ? ETaskResult.StillRunning : ETaskResult.TaskComplete;
+
+            var addonId = agentSatisfactionSupply->GetAddonId();
+            if (addonId == 0)
+                return _remainingAllowances == null ? ETaskResult.StillRunning : ETaskResult.TaskComplete;
+
+            AtkUnitBase* addon = LAddon.GetAddonById(addonId);
+            if (addon == null || !LAddon.IsAddonReady(addon))
+                return ETaskResult.StillRunning;
+
+            ushort remainingAllowances = agentSatisfactionSupply->RemainingAllowances;
+            if (remainingAllowances == 0)
+            {
+                logger.LogInformation("No remaining weekly allowances");
+                addon->FireCallbackInt(0);
+                return ETaskResult.TaskComplete;
+            }
+
+            if (InventoryManager.Instance()->GetInventoryItemCount(agentSatisfactionSupply->Items[1].Id,
+                    minCollectability: (short)agentSatisfactionSupply->Items[1].Collectability1) == 0)
+            {
+                logger.LogInformation("Inventory has no {ItemId}", agentSatisfactionSupply->Items[1].Id);
+                addon->FireCallbackInt(0);
+                return ETaskResult.TaskComplete;
+            }
+
+            // we should at least wait until we have less allowances
+            if (_remainingAllowances == remainingAllowances)
+                return ETaskResult.StillRunning;
+
+            // try turning it in...
+            logger.LogInformation("Attempting turn-in (remaining allowances: {RemainingAllowances})",
+                remainingAllowances);
+            _remainingAllowances = remainingAllowances;
+
+            var pickGatheringItem = stackalloc AtkValue[]
+            {
+                new() { Type = ValueType.Int, Int = 1 },
+                new() { Type = ValueType.Int, Int = 1 }
+            };
+            addon->FireCallback(2, pickGatheringItem);
+            return ETaskResult.StillRunning;
+        }
+
+        public override string ToString() => "WeeklyDeliveryTurnIn";
+    }
+}
 
 
 internal interface ILastTask : ITask
 {
-    public ElementId QuestElementId { get; }
+    public ElementId ElementId { get; }
     public int Sequence { get; }
 }
 
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
 
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
 
 using System;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Common;
 using Questionable.Model.Questing;
 
 using System;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Common;
 using Questionable.Model.Questing;
 
 using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Steps.Shared;
 using Questionable.Controller.Utils;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
         }
     }
 
-    internal sealed class HandleCombat(CombatController combatController, GameFunctions gameFunctions) : ITask
+    internal sealed class HandleCombat(CombatController combatController, QuestFunctions questFunctions) : ITask
     {
         private bool _isLastStep;
         private CombatController.CombatData _combatData = null!;
         private IList<QuestWorkValue?> _completionQuestVariableFlags = null!;
 
-        public ITask With(ElementId questElementId, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds,
+        public ITask With(ElementId elementId, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds,
             IList<QuestWorkValue?> completionQuestVariablesFlags, IList<ComplexCombatData> complexCombatData)
         {
             _isLastStep = isLastStep;
             _combatData = new CombatController.CombatData
             {
-                QuestElementId = questElementId,
+                ElementId = elementId,
                 SpawnType = enemySpawnType,
                 KillEnemyDataIds = killEnemyDataIds.ToList(),
                 ComplexCombatDatas = complexCombatData.ToList(),
                 return ETaskResult.StillRunning;
 
             // if our quest step has any completion flags, we need to check if they are set
-            if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.QuestElementId is QuestId questId)
+            if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.ElementId is QuestId questId)
             {
-                var questWork = gameFunctions.GetQuestEx(questId);
+                var questWork = questFunctions.GetQuestEx(questId);
                 if (questWork == null)
                     return ETaskResult.StillRunning;
 
 
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin.Services;
 using Microsoft.Extensions.DependencyInjection;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
 
 using System.Collections.Generic;
 using Microsoft.Extensions.DependencyInjection;
 using Questionable.Controller.Steps.Common;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
 
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Shared;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
 
 using System.Collections.Generic;
 using Microsoft.Extensions.DependencyInjection;
 using Questionable.Controller.Steps.Common;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
 
 internal static class Say
 {
-    internal sealed class Factory(IServiceProvider serviceProvider, GameFunctions gameFunctions) : ITaskFactory
+    internal sealed class Factory(IServiceProvider serviceProvider, ExcelFunctions excelFunctions) : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
             ArgumentNullException.ThrowIfNull(step.ChatMessage);
 
             string? excelString =
-                gameFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key);
+                excelFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key, false).GetString();
             ArgumentNullException.ThrowIfNull(excelString);
 
             var unmount = serviceProvider.GetRequiredService<UnmountTask>();
 
 using Questionable.Controller.Steps.Shared;
 using Questionable.Controller.Utils;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Common;
 using Questionable.Model.Questing;
             yield return serviceProvider.GetRequiredService<AetheryteShortcut.UseAetheryteShortcut>()
                 .With(null, EAetheryteLocation.Limsa, territoryId);
             yield return serviceProvider.GetRequiredService<AethernetShortcut.UseAethernetShortcut>()
-                .With(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist, null);
+                .With(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
             yield return serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>();
             yield return serviceProvider.GetRequiredService<Move.MoveInternal>()
                 .With(territoryId, destination, dataId: npcId, sprint: false);
         }
     }
 
-    internal abstract class UseItemBase(GameFunctions gameFunctions, ICondition condition, ILogger logger) : ITask
+    internal abstract class UseItemBase(QuestFunctions questFunctions, ICondition condition, ILogger logger) : ITask
     {
         private bool _usedItem;
         private DateTime _continueAt;
         {
             if (QuestId is QuestId questId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
             {
-                QuestWork? questWork = gameFunctions.GetQuestEx(questId);
+                QuestWork? questWork = questFunctions.GetQuestEx(questId);
                 if (questWork != null &&
                     QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork.Value))
                     return ETaskResult.TaskComplete;
     }
 
 
-    internal sealed class UseOnGround(GameFunctions gameFunctions, ICondition condition, ILogger<UseOnGround> logger)
-        : UseItemBase(gameFunctions, condition, logger)
+    internal sealed class UseOnGround(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<UseOnGround> logger)
+        : UseItemBase(questFunctions, condition, logger)
     {
-        private readonly GameFunctions _gameFunctions = gameFunctions;
-
         public uint DataId { get; set; }
 
         public ITask With(ElementId? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
             return this;
         }
 
-        protected override bool UseItem() => _gameFunctions.UseItemOnGround(DataId, ItemId);
+        protected override bool UseItem() => gameFunctions.UseItemOnGround(DataId, ItemId);
 
         public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
     }
 
     internal sealed class UseOnPosition(
         GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
         ICondition condition,
         ILogger<UseOnPosition> logger)
-        : UseItemBase(gameFunctions, condition, logger)
+        : UseItemBase(questFunctions, condition, logger)
     {
-        private readonly GameFunctions _gameFunctions = gameFunctions;
-
         public Vector3 Position { get; set; }
 
         public ITask With(ElementId? questId, Vector3 position, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
             return this;
         }
 
-        protected override bool UseItem() => _gameFunctions.UseItemOnPosition(Position, ItemId);
+        protected override bool UseItem() => gameFunctions.UseItemOnPosition(Position, ItemId);
 
         public override string ToString() =>
             $"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})";
     }
 
-    internal sealed class UseOnObject(GameFunctions gameFunctions, ICondition condition, ILogger<UseOnObject> logger)
-        : UseItemBase(gameFunctions, condition, logger)
+    internal sealed class UseOnObject(QuestFunctions questFunctions, GameFunctions gameFunctions, ICondition condition, ILogger<UseOnObject> logger)
+        : UseItemBase(questFunctions, condition, logger)
     {
-        private readonly GameFunctions _gameFunctions = gameFunctions;
-
         public uint DataId { get; set; }
 
         public ITask With(ElementId? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags,
             return this;
         }
 
-        protected override bool UseItem() => _gameFunctions.UseItem(DataId, ItemId);
+        protected override bool UseItem() => gameFunctions.UseItem(DataId, ItemId);
 
         public override string ToString() => $"UseItem({ItemId} on {DataId})";
     }
 
-    internal sealed class Use(GameFunctions gameFunctions, ICondition condition, ILogger<Use> logger)
-        : UseItemBase(gameFunctions, condition, logger)
+    internal sealed class Use(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<Use> logger)
+        : UseItemBase(questFunctions, condition, logger)
     {
-        private readonly GameFunctions _gameFunctions = gameFunctions;
-
         public ITask With(ElementId? questId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
         {
             QuestId = questId;
             return this;
         }
 
-        protected override bool UseItem() => _gameFunctions.UseItem(ItemId);
+        protected override bool UseItem() => gameFunctions.UseItem(ItemId);
 
         public override string ToString() => $"UseItem({ItemId})";
     }
 
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
 using Questionable.External;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Common;
 using Questionable.Model.Common.Converter;
 
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Common;
 using Questionable.Model.Questing;
 
         {
             foreach (var requiredGatheredItems in step.RequiredGatheredItems)
             {
-                if (!gatheringData.TryGetGatheringPointId(requiredGatheredItems.ItemId,
-                        (EClassJob)clientState.LocalPlayer!.ClassJob.Id, out var gatheringPointId))
+                EClassJob currentClassJob = (EClassJob)clientState.LocalPlayer!.ClassJob.Id;
+                EClassJob classJob = currentClassJob;
+                if (requiredGatheredItems.ClassJob != null)
+                    classJob = (EClassJob)requiredGatheredItems.ClassJob.Value;
+
+                if (!gatheringData.TryGetGatheringPointId(requiredGatheredItems.ItemId, classJob,
+                        out var gatheringPointId))
                     throw new TaskException($"No gathering point found for item {requiredGatheredItems.ItemId}");
 
                 if (!AssemblyGatheringLocationLoader.GetLocations()
                         .TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot))
                     throw new TaskException($"No path found for gathering point {gatheringPointId}");
 
+                if (classJob != currentClassJob)
+                {
+                    yield return serviceProvider.GetRequiredService<SwitchClassJob>()
+                        .With(classJob);
+                }
+
                 if (HasRequiredItems(requiredGatheredItems))
                     continue;
 
             InventoryManager* inventoryManager = InventoryManager.Instance();
             return inventoryManager != null &&
                    inventoryManager->GetInventoryItemCount(requiredGatheredItems.ItemId,
-                       minCollectability: (short)requiredGatheredItems.Collectability) >= requiredGatheredItems.ItemCount;
+                       minCollectability: (short)requiredGatheredItems.Collectability) >=
+                   requiredGatheredItems.ItemCount;
         }
 
         public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
 
 using Questionable.Controller.NavigationOverrides;
 using Questionable.Controller.Steps.Common;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
 
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Utils;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Common;
 using Questionable.Model.Questing;
     internal sealed class CheckSkip(
         ILogger<CheckSkip> logger,
         GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
         IClientState clientState) : ITask
     {
         public QuestStep Step { get; set; } = null!;
         public SkipStepConditions SkipConditions { get; set; } = null!;
-        public ElementId QuestElementId { get; set; } = null!;
+        public ElementId ElementId { get; set; } = null!;
 
-        public ITask With(QuestStep step, SkipStepConditions skipConditions, ElementId questElementId)
+        public ITask With(QuestStep step, SkipStepConditions skipConditions, ElementId elementId)
         {
             Step = step;
             SkipConditions = skipConditions;
-            QuestElementId = questElementId;
+            ElementId = elementId;
             return this;
         }
 
             }
 
             if (SkipConditions.QuestsCompleted.Count > 0 &&
-                SkipConditions.QuestsCompleted.All(gameFunctions.IsQuestComplete))
+                SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
             {
                 logger.LogInformation("Skipping step, all prequisite quests are complete");
                 return true;
             }
 
             if (SkipConditions.QuestsAccepted.Count > 0 &&
-                SkipConditions.QuestsAccepted.All(gameFunctions.IsQuestAccepted))
+                SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
             {
                 logger.LogInformation("Skipping step, all prequisite quests are accepted");
                 return true;
                 return true;
             }
 
-            if (QuestElementId is QuestId questId)
+            if (ElementId is QuestId questId)
             {
-                QuestWork? questWork = gameFunctions.GetQuestEx(questId);
+                QuestWork? questWork = questFunctions.GetQuestEx(questId);
                 if (QuestWorkUtils.HasCompletionFlags(Step.CompletionQuestVariablesFlags) && questWork != null)
                 {
                     if (QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value))
                 }
             }
 
-            if (Step.PickUpQuestId != null && gameFunctions.IsQuestAcceptedOrComplete(Step.PickUpQuestId))
+            if (Step.PickUpQuestId != null && questFunctions.IsQuestAcceptedOrComplete(Step.PickUpQuestId))
             {
                 logger.LogInformation("Skipping step, as we have already picked up the relevant quest");
                 return true;
             }
 
-            if (Step.TurnInQuestId != null && gameFunctions.IsQuestComplete(Step.TurnInQuestId))
+            if (Step.TurnInQuestId != null && questFunctions.IsQuestComplete(Step.TurnInQuestId))
             {
                 logger.LogInformation("Skipping step, as we have already completed the relevant quest");
                 return true;
 
--- /dev/null
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+using LLib.GameData;
+using Questionable.Controller.Steps.Common;
+
+namespace Questionable.Controller.Steps.Shared;
+
+internal sealed class SwitchClassJob(IClientState clientState) : AbstractDelayedTask
+{
+    private EClassJob _classJob;
+
+    public ITask With(EClassJob classJob)
+    {
+        _classJob = classJob;
+        return this;
+    }
+
+    protected override unsafe bool StartInternal()
+    {
+        if (clientState.LocalPlayer!.ClassJob.Id == (uint)_classJob)
+            return false;
+
+        var gearsetModule = RaptureGearsetModule.Instance();
+        if (gearsetModule != null)
+        {
+            for (int i = 0; i < 100; ++i)
+            {
+                var gearset = gearsetModule->GetGearset(i);
+                if (gearset->ClassJob == (byte)_classJob)
+                {
+                    gearsetModule->EquipGearset(gearset->Id, gearset->BannerIndex);
+                    return true;
+                }
+            }
+        }
+
+        throw new TaskException($"No gearset found for {_classJob}");
+    }
+
+    protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
+
+    public override string ToString() => $"SwitchJob({_classJob})";
+}
 
 using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Utils;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
         public override string ToString() => "Wait(next step or sequence)";
     }
 
-    internal sealed class WaitForCompletionFlags(GameFunctions gameFunctions) : ITask
+    internal sealed class WaitForCompletionFlags(QuestFunctions questFunctions) : ITask
     {
         public QuestId Quest { get; set; } = null!;
         public QuestStep Step { get; set; } = null!;
 
         public ETaskResult Update()
         {
-            QuestWork? questWork = gameFunctions.GetQuestEx(Quest);
+            QuestWork? questWork = questFunctions.GetQuestEx(Quest);
             return questWork != null &&
                    QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value)
                 ? ETaskResult.TaskComplete
             $"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})";
     }
 
-    internal sealed class WaitQuestAccepted(GameFunctions gameFunctions) : ITask
+    internal sealed class WaitQuestAccepted(QuestFunctions questFunctions) : ITask
     {
-        public ElementId QuestElementId { get; set; } = null!;
+        public ElementId ElementId { get; set; } = null!;
 
-        public ITask With(ElementId questElementId)
+        public ITask With(ElementId elementId)
         {
-            QuestElementId = questElementId;
+            ElementId = elementId;
             return this;
         }
 
 
         public ETaskResult Update()
         {
-            return gameFunctions.IsQuestAccepted(QuestElementId)
+            return questFunctions.IsQuestAccepted(ElementId)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
 
-        public override string ToString() => $"WaitQuestAccepted({QuestElementId})";
+        public override string ToString() => $"WaitQuestAccepted({ElementId})";
     }
 
-    internal sealed class WaitQuestCompleted(GameFunctions gameFunctions) : ITask
+    internal sealed class WaitQuestCompleted(QuestFunctions questFunctions) : ITask
     {
-        public ElementId QuestElementId { get; set; } = null!;
+        public ElementId ElementId { get; set; } = null!;
 
-        public ITask With(ElementId questElementId)
+        public ITask With(ElementId elementId)
         {
-            QuestElementId = questElementId;
+            ElementId = elementId;
             return this;
         }
 
 
         public ETaskResult Update()
         {
-            return gameFunctions.IsQuestComplete(QuestElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+            return questFunctions.IsQuestComplete(ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
         }
 
-        public override string ToString() => $"WaitQuestComplete({QuestElementId})";
+        public override string ToString() => $"WaitQuestComplete({ElementId})";
     }
 
-    internal sealed class NextStep(ElementId questElementId, int sequence) : ILastTask
+    internal sealed class NextStep(ElementId elementId, int sequence) : ILastTask
     {
-        public ElementId QuestElementId { get; } = questElementId;
+        public ElementId ElementId { get; } = elementId;
         public int Sequence { get; } = sequence;
 
         public bool Start() => true;
 
     internal sealed class EndAutomation : ILastTask
     {
-        public ElementId QuestElementId => throw new InvalidOperationException();
+        public ElementId ElementId => throw new InvalidOperationException();
         public int Sequence => throw new InvalidOperationException();
 
         public bool Start() => true;
 
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using Dalamud.Game;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.Game.ClientState.Objects.Types;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.System.Framework;
+using FFXIVClientStructs.FFXIV.Client.System.Memory;
+using FFXIVClientStructs.FFXIV.Client.System.String;
+using Lumina.Excel.GeneratedSheets;
+using Microsoft.Extensions.Logging;
+using Questionable.Model.Questing;
+
+namespace Questionable.Functions;
+
+internal sealed unsafe class ChatFunctions
+{
+    private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
+
+    private readonly ReadOnlyDictionary<EEmote, string> _emoteCommands;
+
+    private readonly GameFunctions _gameFunctions;
+    private readonly ITargetManager _targetManager;
+    private readonly ILogger<ChatFunctions> _logger;
+    private readonly ProcessChatBoxDelegate _processChatBox;
+    private readonly delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString;
+
+    public ChatFunctions(ISigScanner sigScanner, IDataManager dataManager, GameFunctions gameFunctions,
+        ITargetManager targetManager, ILogger<ChatFunctions> logger)
+    {
+        _gameFunctions = gameFunctions;
+        _targetManager = targetManager;
+        _logger = logger;
+        _processChatBox =
+            Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
+        _sanitiseString =
+            (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sigScanner.ScanText(Signatures.SanitiseString);
+
+        _emoteCommands = dataManager.GetExcelSheet<Emote>()!
+            .Where(x => x.RowId > 0)
+            .Where(x => x.TextCommand != null && x.TextCommand.Value != null)
+            .Select(x => (x.RowId, Command: x.TextCommand.Value!.Command?.ToString()))
+            .Where(x => x.Command != null && x.Command.StartsWith('/'))
+            .ToDictionary(x => (EEmote)x.RowId, x => x.Command!)
+            .AsReadOnly();
+    }
+
+    /// <summary>
+    /// <para>
+    /// Send a given message to the chat box. <b>This can send chat to the server.</b>
+    /// </para>
+    /// <para>
+    /// <b>This method is unsafe.</b> This method does no checking on your input and
+    /// may send content to the server that the normal client could not. You must
+    /// verify what you're sending and handle content and length to properly use
+    /// this.
+    /// </para>
+    /// </summary>
+    /// <param name="message">Message to send</param>
+    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
+    private void SendMessageUnsafe(byte[] message)
+    {
+        var uiModule = (IntPtr)Framework.Instance()->GetUIModule();
+
+        using var payload = new ChatPayload(message);
+        var mem1 = Marshal.AllocHGlobal(400);
+        Marshal.StructureToPtr(payload, mem1, false);
+
+        _processChatBox(uiModule, mem1, IntPtr.Zero, 0);
+
+        Marshal.FreeHGlobal(mem1);
+    }
+
+    /// <summary>
+    /// <para>
+    /// Send a given message to the chat box. <b>This can send chat to the server.</b>
+    /// </para>
+    /// <para>
+    /// This method is slightly less unsafe than <see cref="SendMessageUnsafe"/>. It
+    /// will throw exceptions for certain inputs that the client can't normally send,
+    /// but it is still possible to make mistakes. Use with caution.
+    /// </para>
+    /// </summary>
+    /// <param name="message">message to send</param>
+    /// <exception cref="ArgumentException">If <paramref name="message"/> is empty, longer than 500 bytes in UTF-8, or contains invalid characters.</exception>
+    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
+    private void SendMessage(string message)
+    {
+        _logger.LogDebug("Attempting to send chat message '{Message}'", message);
+        var bytes = Encoding.UTF8.GetBytes(message);
+        if (bytes.Length == 0)
+            throw new ArgumentException("message is empty", nameof(message));
+
+        if (bytes.Length > 500)
+            throw new ArgumentException("message is longer than 500 bytes", nameof(message));
+
+        if (message.Length != SanitiseText(message).Length)
+            throw new ArgumentException("message contained invalid characters", nameof(message));
+
+        SendMessageUnsafe(bytes);
+    }
+
+    /// <summary>
+    /// <para>
+    /// Sanitises a string by removing any invalid input.
+    /// </para>
+    /// <para>
+    /// The result of this method is safe to use with
+    /// <see cref="SendMessage"/>, provided that it is not empty or too
+    /// long.
+    /// </para>
+    /// </summary>
+    /// <param name="text">text to sanitise</param>
+    /// <returns>sanitised text</returns>
+    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
+    private string SanitiseText(string text)
+    {
+        var uText = Utf8String.FromString(text);
+
+        _sanitiseString(uText, 0x27F, IntPtr.Zero);
+        var sanitised = uText->ToString();
+
+        uText->Dtor();
+        IMemorySpace.Free(uText);
+
+        return sanitised;
+    }
+
+    public void ExecuteCommand(string command)
+    {
+        if (!command.StartsWith('/'))
+            return;
+
+        SendMessage(command);
+    }
+
+    public void UseEmote(uint dataId, EEmote emote)
+    {
+        IGameObject? gameObject = _gameFunctions.FindObjectByDataId(dataId);
+        if (gameObject != null)
+        {
+            _targetManager.Target = gameObject;
+            ExecuteCommand($"{_emoteCommands[emote]} motion");
+        }
+    }
+
+    public void UseEmote(EEmote emote)
+    {
+        ExecuteCommand($"{_emoteCommands[emote]} motion");
+    }
+
+    private static class Signatures
+    {
+        internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9";
+        internal const string SanitiseString = "E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 0F B6 F0 E8 ?? ?? ?? ?? 48 8D 4D C0";
+    }
+
+    [StructLayout(LayoutKind.Explicit)]
+    [SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")]
+    private readonly struct ChatPayload : IDisposable
+    {
+        [FieldOffset(0)] private readonly IntPtr textPtr;
+
+        [FieldOffset(16)] private readonly ulong textLen;
+
+        [FieldOffset(8)] private readonly ulong unk1;
+
+        [FieldOffset(24)] private readonly ulong unk2;
+
+        internal ChatPayload(byte[] stringBytes)
+        {
+            textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
+            Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length);
+            Marshal.WriteByte(textPtr + stringBytes.Length, 0);
+
+            textLen = (ulong)(stringBytes.Length + 1);
+
+            unk1 = 64;
+            unk2 = 0;
+        }
+
+        public void Dispose()
+        {
+            Marshal.FreeHGlobal(textPtr);
+        }
+    }
+}
 
--- /dev/null
+using System;
+using System.Linq;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+using LLib;
+using Lumina.Excel.CustomSheets;
+using Lumina.Excel.GeneratedSheets;
+using Lumina.Text;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Quest = Questionable.Model.Quest;
+using GimmickYesNo = Lumina.Excel.GeneratedSheets2.GimmickYesNo;
+
+namespace Questionable.Functions;
+
+internal sealed class ExcelFunctions
+{
+    private readonly IDataManager _dataManager;
+    private readonly ILogger<ExcelFunctions> _logger;
+
+    public ExcelFunctions(IDataManager dataManager, ILogger<ExcelFunctions> logger)
+    {
+        _dataManager = dataManager;
+        _logger = logger;
+    }
+
+    public StringOrRegex GetDialogueText(Quest currentQuest, string? excelSheetName, string key, bool isRegex)
+    {
+        var seString = GetRawDialogueText(currentQuest, excelSheetName, key);
+        if (isRegex)
+            return new StringOrRegex(seString.ToRegex());
+        else
+            return new StringOrRegex(seString?.ToDalamudString().ToString());
+    }
+
+    public SeString? GetRawDialogueText(Quest currentQuest, string? excelSheetName, string key)
+    {
+        if (excelSheetName == null)
+        {
+            var questRow =
+                _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.Id.Value +
+                    0x10000);
+            if (questRow == null)
+            {
+                _logger.LogError("Could not find quest row for {QuestId}", currentQuest.Id);
+                return null;
+            }
+
+            excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}";
+        }
+
+        var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
+        if (excelSheet == null)
+        {
+            _logger.LogError("Unknown excel sheet '{SheetName}'", excelSheetName);
+            return null;
+        }
+
+        return excelSheet.FirstOrDefault(x => x.Key == key)?.Value;
+    }
+
+    public StringOrRegex GetDialogueTextByRowId(string? excelSheet, uint rowId, bool isRegex)
+    {
+        var seString = GetRawDialogueTextByRowId(excelSheet, rowId);
+        if (isRegex)
+            return new StringOrRegex(seString.ToRegex());
+        else
+            return new StringOrRegex(seString?.ToDalamudString().ToString());
+    }
+
+    public SeString? GetRawDialogueTextByRowId(string? excelSheet, uint rowId)
+    {
+        if (excelSheet == "GimmickYesNo")
+        {
+            var questRow = _dataManager.GetExcelSheet<GimmickYesNo>()!.GetRow(rowId);
+            return questRow?.Unknown0;
+        }
+        else if (excelSheet == "Warp")
+        {
+            var questRow = _dataManager.GetExcelSheet<Warp>()!.GetRow(rowId);
+            return questRow?.Name;
+        }
+        else if (excelSheet is "Addon")
+        {
+            var questRow = _dataManager.GetExcelSheet<Addon>()!.GetRow(rowId);
+            return questRow?.Text;
+        }
+        else if (excelSheet is "EventPathMove")
+        {
+            var questRow = _dataManager.GetExcelSheet<EventPathMove>()!.GetRow(rowId);
+            return questRow?.Unknown10;
+        }
+        else if (excelSheet is "ContentTalk" or null)
+        {
+            var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
+            return questRow?.Text;
+        }
+        else
+            throw new ArgumentOutOfRangeException(nameof(excelSheet), $"Unsupported excel sheet {excelSheet}");
+    }
+}
 
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Numerics;
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.Game.ClientState.Objects.Types;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Control;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
+using Action = Lumina.Excel.GeneratedSheets2.Action;
+using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
+using ContentFinderCondition = Lumina.Excel.GeneratedSheets.ContentFinderCondition;
+using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
+using Quest = Questionable.Model.Quest;
+using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType;
+
+namespace Questionable.Functions;
+
+internal sealed unsafe class GameFunctions
+{
+    private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet;
+    private readonly ReadOnlyDictionary<uint, ushort> _contentFinderConditionToContentId;
+
+    private readonly QuestFunctions _questFunctions;
+    private readonly IDataManager _dataManager;
+    private readonly IObjectTable _objectTable;
+    private readonly ITargetManager _targetManager;
+    private readonly ICondition _condition;
+    private readonly IClientState _clientState;
+    private readonly IGameGui _gameGui;
+    private readonly Configuration _configuration;
+    private readonly ILogger<GameFunctions> _logger;
+
+    public GameFunctions(
+        QuestFunctions questFunctions,
+        IDataManager dataManager,
+        IObjectTable objectTable,
+        ITargetManager targetManager,
+        ICondition condition,
+        IClientState clientState,
+        IGameGui gameGui,
+        Configuration configuration,
+        ILogger<GameFunctions> logger)
+    {
+        _questFunctions = questFunctions;
+        _dataManager = dataManager;
+        _objectTable = objectTable;
+        _targetManager = targetManager;
+        _condition = condition;
+        _clientState = clientState;
+        _gameGui = gameGui;
+        _configuration = configuration;
+        _logger = logger;
+
+        _territoryToAetherCurrentCompFlgSet = dataManager.GetExcelSheet<TerritoryType>()!
+            .Where(x => x.RowId > 0)
+            .Where(x => x.Unknown32 > 0)
+            .ToDictionary(x => (ushort)x.RowId, x => x.Unknown32)
+            .AsReadOnly();
+        _contentFinderConditionToContentId = dataManager.GetExcelSheet<ContentFinderCondition>()!
+            .Where(x => x.RowId > 0 && x.Content > 0)
+            .ToDictionary(x => x.RowId, x => x.Content)
+            .AsReadOnly();
+    }
+
+    public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
+
+    public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
+    {
+        subIndex = 0;
+
+        var uiState = UIState.Instance();
+        return uiState != null && uiState->IsAetheryteUnlocked(aetheryteId);
+    }
+
+    public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
+    {
+        if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
+            return _questFunctions.IsQuestComplete(new QuestId(3672));
+        return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
+    }
+
+    public bool CanTeleport(EAetheryteLocation aetheryteLocation)
+    {
+        if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId &&
+            ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
+            return true;
+
+        return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
+    }
+
+    public bool TeleportAetheryte(uint aetheryteId)
+    {
+        _logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId);
+        if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
+        {
+            if (aetheryteId == PlayerState.Instance()->HomeAetheryteId &&
+                ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
+            {
+                ReturnRequestedAt = DateTime.Now;
+                if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8))
+                {
+                    _logger.LogInformation("Using 'return' for home aetheryte");
+                    return true;
+                }
+            }
+
+            if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0)
+            {
+                // fallback if return isn't available or (more likely) on a different aetheryte
+                _logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId);
+                return Telepo.Instance()->Teleport(aetheryteId, subIndex);
+            }
+        }
+
+        return false;
+    }
+
+    public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
+        => TeleportAetheryte((uint)aetheryteLocation);
+
+    public bool IsFlyingUnlocked(ushort territoryId)
+    {
+        if (_configuration.Advanced.NeverFly)
+            return false;
+
+        if (_questFunctions.IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted])
+        {
+            BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0);
+            if (battleChara != null && battleChara->Mount.MountId == 198) // special quest amaro, not the normal one
+                return true;
+        }
+
+        var playerState = PlayerState.Instance();
+        return playerState != null &&
+               _territoryToAetherCurrentCompFlgSet.TryGetValue(territoryId, out byte aetherCurrentCompFlgSet) &&
+               playerState->IsAetherCurrentZoneComplete(aetherCurrentCompFlgSet);
+    }
+
+    public bool IsFlyingUnlockedInCurrentZone() => IsFlyingUnlocked(_clientState.TerritoryType);
+
+    public bool IsAetherCurrentUnlocked(uint aetherCurrentId)
+    {
+        var playerState = PlayerState.Instance();
+        return playerState != null &&
+               playerState->IsAetherCurrentUnlocked(aetherCurrentId);
+    }
+
+    public IGameObject? FindObjectByDataId(uint dataId, ObjectKind? kind = null, bool targetable = false)
+    {
+        foreach (var gameObject in _objectTable)
+        {
+            if (targetable && !gameObject.IsTargetable)
+                continue;
+
+            if (gameObject.ObjectKind is ObjectKind.Player or ObjectKind.Companion or ObjectKind.MountType
+                or ObjectKind.Retainer or ObjectKind.Housing)
+                continue;
+
+            if (gameObject.DataId == dataId && (kind == null || kind.Value == gameObject.ObjectKind))
+            {
+                return gameObject;
+            }
+        }
+
+        _logger.LogWarning("Could not find GameObject with dataId {DataId}", dataId);
+        return null;
+    }
+
+    public bool InteractWith(uint dataId, ObjectKind? kind = null)
+    {
+        IGameObject? gameObject = FindObjectByDataId(dataId, kind);
+        if (gameObject != null)
+            return InteractWith(gameObject);
+
+        _logger.LogDebug("Game object is null");
+        return false;
+    }
+
+    public bool InteractWith(IGameObject gameObject)
+    {
+        _logger.LogInformation("Setting target with {DataId} to {ObjectId}", gameObject.DataId, gameObject.EntityId);
+        _targetManager.Target = null;
+        _targetManager.Target = gameObject;
+
+        if (gameObject.ObjectKind == ObjectKind.GatheringPoint)
+        {
+            TargetSystem.Instance()->OpenObjectInteraction((GameObject*)gameObject.Address);
+            _logger.LogInformation("Interact result: (none) for GatheringPoint");
+            return true;
+        }
+        else
+        {
+            long result = (long)TargetSystem.Instance()->InteractWithObject((GameObject*)gameObject.Address, false);
+
+            _logger.LogInformation("Interact result: {Result}", result);
+            return result != 7 && result > 0;
+        }
+    }
+
+    public bool UseItem(uint itemId)
+    {
+        long result = AgentInventoryContext.Instance()->UseItem(itemId);
+        _logger.LogInformation("UseItem result: {Result}", result);
+
+        return result == 0;
+    }
+
+    public bool UseItem(uint dataId, uint itemId)
+    {
+        IGameObject? gameObject = FindObjectByDataId(dataId);
+        if (gameObject != null)
+        {
+            _targetManager.Target = gameObject;
+            long result = AgentInventoryContext.Instance()->UseItem(itemId);
+
+            _logger.LogInformation("UseItem result on {DataId}: {Result}", dataId, result);
+
+            // TODO is 1 a generally accepted result?
+            return result == 0 || (itemId == 2002450 && result == 1);
+        }
+
+        return false;
+    }
+
+    public bool UseItemOnGround(uint dataId, uint itemId)
+    {
+        IGameObject? gameObject = FindObjectByDataId(dataId);
+        if (gameObject != null)
+        {
+            Vector3 position = gameObject.Position;
+            return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, location: &position);
+        }
+
+        return false;
+    }
+
+    public bool UseItemOnPosition(Vector3 position, uint itemId)
+    {
+        return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, location: &position);
+    }
+
+    public bool UseAction(EAction action)
+    {
+        if (ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0)
+        {
+            bool result = ActionManager.Instance()->UseAction(ActionType.Action, (uint)action);
+            _logger.LogInformation("UseAction {Action} result: {Result}", action, result);
+
+            return result;
+        }
+
+        return false;
+    }
+
+    public bool UseAction(IGameObject gameObject, EAction action)
+    {
+        var actionRow = _dataManager.GetExcelSheet<Action>()!.GetRow((uint)action)!;
+        if (!ActionManager.CanUseActionOnTarget((uint)action, (GameObject*)gameObject.Address))
+        {
+            _logger.LogWarning("Can not use action {Action} on target {Target}", action, gameObject);
+            return false;
+        }
+
+        _targetManager.Target = gameObject;
+        if (ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action, gameObject.GameObjectId) == 0)
+        {
+            bool result;
+            if (actionRow.TargetArea)
+            {
+                Vector3 position = gameObject.Position;
+                result = ActionManager.Instance()->UseActionLocation(ActionType.Action, (uint)action,
+                    location: &position);
+                _logger.LogInformation("UseAction {Action} on target area {Target} result: {Result}", action,
+                    gameObject,
+                    result);
+            }
+            else
+            {
+                result = ActionManager.Instance()->UseAction(ActionType.Action, (uint)action, gameObject.GameObjectId);
+                _logger.LogInformation("UseAction {Action} on target {Target} result: {Result}", action, gameObject,
+                    result);
+            }
+
+            return result;
+        }
+
+        return false;
+    }
+
+    public bool IsObjectAtPosition(uint dataId, Vector3 position, float distance)
+    {
+        IGameObject? gameObject = FindObjectByDataId(dataId);
+        return gameObject != null && (gameObject.Position - position).Length() < distance;
+    }
+
+    public bool HasStatusPreventingMount()
+    {
+        if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone())
+            return true;
+
+        // company chocobo is locked
+        var playerState = PlayerState.Instance();
+        if (playerState != null && !playerState->IsMountUnlocked(1))
+            return true;
+
+        var localPlayer = _clientState.LocalPlayer;
+        if (localPlayer == null)
+            return false;
+
+        var battleChara = (BattleChara*)localPlayer.Address;
+        StatusManager* statusManager = battleChara->GetStatusManager();
+        if (statusManager->HasStatus(1151))
+            return true;
+
+        return HasCharacterStatusPreventingMountOrSprint();
+    }
+
+    public bool HasStatusPreventingSprint() => HasCharacterStatusPreventingMountOrSprint();
+
+    private bool HasCharacterStatusPreventingMountOrSprint()
+    {
+        var localPlayer = _clientState.LocalPlayer;
+        if (localPlayer == null)
+            return false;
+
+        var battleChara = (BattleChara*)localPlayer.Address;
+        StatusManager* statusManager = battleChara->GetStatusManager();
+        return statusManager->HasStatus(565) ||
+               statusManager->HasStatus(404) ||
+               statusManager->HasStatus(416) ||
+               statusManager->HasStatus(2729) ||
+               statusManager->HasStatus(2730);
+    }
+
+    public bool Mount()
+    {
+        if (_condition[ConditionFlag.Mounted])
+            return true;
+
+        var playerState = PlayerState.Instance();
+        if (playerState != null && _configuration.General.MountId != 0 &&
+            playerState->IsMountUnlocked(_configuration.General.MountId))
+        {
+            if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, _configuration.General.MountId) == 0)
+            {
+                _logger.LogDebug("Attempting to use preferred mount...");
+                if (ActionManager.Instance()->UseAction(ActionType.Mount, _configuration.General.MountId))
+                {
+                    _logger.LogInformation("Using preferred mount");
+                    return true;
+                }
+
+                return false;
+            }
+        }
+        else
+        {
+            if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
+            {
+                _logger.LogDebug("Attempting to use mount roulette...");
+                if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9))
+                {
+                    _logger.LogInformation("Using mount roulette");
+                    return true;
+                }
+
+                return false;
+            }
+        }
+
+        return false;
+    }
+
+    public bool Unmount()
+    {
+        if (!_condition[ConditionFlag.Mounted])
+            return true;
+
+        if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
+        {
+            _logger.LogDebug("Attempting to unmount...");
+            if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23))
+            {
+                _logger.LogInformation("Unmounted");
+                return true;
+            }
+
+            return false;
+        }
+        else
+        {
+            _logger.LogWarning("Can't unmount right now?");
+            return false;
+        }
+    }
+
+    public void OpenDutyFinder(uint contentFinderConditionId)
+    {
+        if (_contentFinderConditionToContentId.TryGetValue(contentFinderConditionId, out ushort contentId))
+        {
+            if (UIState.IsInstanceContentUnlocked(contentId))
+                AgentContentsFinder.Instance()->OpenRegularDuty(contentFinderConditionId);
+            else
+                _logger.LogError(
+                    "Trying to access a locked duty (cf: {ContentFinderId}, content: {ContentId})",
+                    contentFinderConditionId, contentId);
+        }
+        else
+            _logger.LogError("Could not find content for content finder condition (cf: {ContentFinderId})",
+                contentFinderConditionId);
+    }
+
+    /// <summary>
+    /// Ensures characters like '-' are handled equally in both strings.
+    /// </summary>
+    public static bool GameStringEquals(string? a, string? b)
+    {
+        if (a == null)
+            return b == null;
+
+        if (b == null)
+            return false;
+
+        return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-');
+    }
+
+    public bool IsOccupied()
+    {
+        if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
+            return true;
+
+        if (IsLoadingScreenVisible())
+            return true;
+
+        return _condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
+               _condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
+               _condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
+               _condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
+               _condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57] ||
+               _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] ||
+               _condition[ConditionFlag.Jumping61] || _condition[ConditionFlag.Gathering42];
+    }
+
+    public bool IsOccupiedWithCustomDeliveryNpc(Quest? currentQuest)
+    {
+        // not a supply quest?
+        if (currentQuest is not { Info: SatisfactionSupplyInfo })
+            return false;
+
+        if (_targetManager.Target == null || _targetManager.Target.DataId != currentQuest.Info.IssuerDataId)
+            return false;
+
+        if (!AgentSatisfactionSupply.Instance()->IsAgentActive())
+            return false;
+
+        var flags = _condition.AsReadOnlySet();
+        return flags.Count == 2 &&
+               flags.Contains(ConditionFlag.NormalConditions) &&
+               flags.Contains(ConditionFlag.OccupiedInQuestEvent);
+    }
+
+    public bool IsLoadingScreenVisible()
+    {
+        return _gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) &&
+               LAddon.IsAddonReady(fade) &&
+               fade->IsVisible;
+    }
+}
 
--- /dev/null
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Dalamud.Memory;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameData;
+using LLib.GameUI;
+using Lumina.Excel.GeneratedSheets;
+using Questionable.Controller;
+using Questionable.Data;
+using Questionable.Model;
+using Questionable.Model.Questing;
+using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
+using Quest = Questionable.Model.Quest;
+
+namespace Questionable.Functions;
+
+internal sealed unsafe class QuestFunctions
+{
+    private readonly QuestRegistry _questRegistry;
+    private readonly QuestData _questData;
+    private readonly Configuration _configuration;
+    private readonly IDataManager _dataManager;
+    private readonly IClientState _clientState;
+    private readonly IGameGui _gameGui;
+
+    public QuestFunctions(QuestRegistry questRegistry, QuestData questData, Configuration configuration, IDataManager dataManager, IClientState clientState, IGameGui gameGui)
+    {
+        _questRegistry = questRegistry;
+        _questData = questData;
+        _configuration = configuration;
+        _dataManager = dataManager;
+        _clientState = clientState;
+        _gameGui = gameGui;
+    }
+
+    public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuest()
+    {
+        var (currentQuest, sequence) = GetCurrentQuestInternal();
+        PlayerState* playerState = PlayerState.Instance();
+
+        if (currentQuest == null || currentQuest.Value == 0)
+        {
+            if (_clientState.TerritoryType == 181) // Starting in Limsa
+                return (new QuestId(107), 0);
+            if (_clientState.TerritoryType == 182) // Starting in Ul'dah
+                return (new QuestId(594), 0);
+            if (_clientState.TerritoryType == 183) // Starting in Gridania
+                return (new QuestId(39), 0);
+            return default;
+        }
+        else if (currentQuest.Value == 681)
+        {
+            // if we have already picked up the GC quest, just return the progress for it
+            if (IsQuestAccepted(currentQuest) || IsQuestComplete(currentQuest))
+                return (currentQuest, sequence);
+
+            // The company you keep...
+            return _configuration.General.GrandCompany switch
+            {
+                GrandCompany.TwinAdder => (new QuestId(680), 0),
+                GrandCompany.Maelstrom => (new QuestId(681), 0),
+                _ => default
+            };
+        }
+        else if (currentQuest.Value == 3856 && !playerState->IsMountUnlocked(1)) // we come in peace
+        {
+            ushort chocoboQuest = (GrandCompany)playerState->GrandCompany switch
+            {
+                GrandCompany.TwinAdder => 700,
+                GrandCompany.Maelstrom => 701,
+                _ => 0
+            };
+
+            if (chocoboQuest != 0 && !QuestManager.IsQuestComplete(chocoboQuest))
+                return (new QuestId(chocoboQuest), QuestManager.GetQuestSequence(chocoboQuest));
+        }
+        else if (currentQuest.Value == 801)
+        {
+            // skeletons in her closet, finish 'broadening horizons' to unlock the white wolf gate
+            QuestId broadeningHorizons = new QuestId(802);
+            if (IsQuestAccepted(broadeningHorizons))
+                return (broadeningHorizons, QuestManager.GetQuestSequence(broadeningHorizons.Value));
+        }
+
+        return (currentQuest, sequence);
+    }
+
+    public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuestInternal()
+    {
+        var questManager = QuestManager.Instance();
+        if (questManager != null)
+        {
+            // always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do
+            // side quests until the end of time.
+            var msqQuest = GetMainScenarioQuest(questManager);
+            if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
+                return msqQuest;
+
+            // Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item,
+            // do the MSQ; if a side quest is the first item do that side quest.
+            //
+            // If no quests are marked as 'priority', accepting a new quest adds it to the top of the list.
+            for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i)
+            {
+                ElementId currentQuest;
+                var trackedQuest = questManager->TrackedQuests[i];
+                switch (trackedQuest.QuestType)
+                {
+                    default:
+                        continue;
+
+                    case 1: // normal quest
+                        currentQuest = new QuestId(questManager->NormalQuests[trackedQuest.Index].QuestId);
+                        break;
+                }
+
+                if (_questRegistry.IsKnownQuest(currentQuest))
+                    return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
+            }
+
+            // if we know no quest of those currently in the to-do list, just do MSQ
+            return msqQuest;
+        }
+
+        return default;
+    }
+
+    private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager)
+    {
+        if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled
+        {
+            AgentInterface* questRedoHud = AgentModule.Instance()->GetAgentByInternalId(AgentId.QuestRedoHud);
+            if (questRedoHud != null && questRedoHud->IsAgentActive())
+            {
+                // there's surely better ways to check this, but the one in the OOB Plugin was even less reliable
+                if (_gameGui.TryGetAddonByName<AtkUnitBase>("QuestRedoHud", out var addon) &&
+                    addon->AtkValuesCount == 4 &&
+                    // 0 seems to be active,
+                    // 1 seems to be paused,
+                    // 2 is unknown, but it happens e.g. before the quest 'Alzadaal's Legacy'
+                    // 3 seems to be having /ng+ open while active,
+                    // 4 seems to be when (a) suspending the chapter, or (b) having turned in a quest
+                    addon->AtkValues[0].UInt is 0 or 2 or 3 or 4)
+                {
+                    // redoHud+44 is chapter
+                    // redoHud+46 is quest
+                    ushort questId = MemoryHelper.Read<ushort>((nint)questRedoHud + 46);
+                    return (new QuestId(questId), QuestManager.GetQuestSequence(questId));
+                }
+            }
+        }
+
+        var scenarioTree = AgentScenarioTree.Instance();
+        if (scenarioTree == null)
+            return default;
+
+        if (scenarioTree->Data == null)
+            return default;
+
+        QuestId currentQuest = new QuestId(scenarioTree->Data->CurrentScenarioQuest);
+        if (currentQuest.Value == 0)
+            return default;
+
+        // if the MSQ is hidden, we generally ignore it
+        if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden)
+            return default;
+
+        // it can sometimes happen (although this isn't reliably reproducible) that the quest returned here
+        // is one you've just completed.
+        if (!IsReadyToAcceptQuest(currentQuest))
+            return default;
+
+        // if we're not at a high enough level to continue, we also ignore it
+        var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
+        if (currentLevel != 0 &&
+            _questRegistry.TryGetQuest(currentQuest, out Quest? quest)
+            && quest.Info.Level > currentLevel)
+            return default;
+
+        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)
+    {
+        if (elementId is QuestId questId)
+            return IsReadyToAcceptQuest(questId);
+        else if (elementId is SatisfactionSupplyNpcId)
+            return true;
+        else
+            throw new ArgumentOutOfRangeException(nameof(elementId));
+    }
+
+    public bool IsReadyToAcceptQuest(QuestId questId)
+    {
+        _questRegistry.TryGetQuest(questId, out var quest);
+        if (quest is { Info.IsRepeatable: true })
+        {
+            if (IsQuestAccepted(questId))
+                return false;
+        }
+        else
+        {
+            if (IsQuestAcceptedOrComplete(questId))
+                return false;
+        }
+
+        if (IsQuestLocked(questId))
+            return false;
+
+        // if we're not at a high enough level to continue, we also ignore it
+        var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
+        if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel)
+            return false;
+
+        return true;
+    }
+
+    public bool IsQuestAcceptedOrComplete(ElementId elementId)
+    {
+        return IsQuestComplete(elementId) || IsQuestAccepted(elementId);
+    }
+
+    public bool IsQuestAccepted(ElementId elementId)
+    {
+        if (elementId is QuestId questId)
+            return IsQuestAccepted(questId);
+        else if (elementId is SatisfactionSupplyNpcId)
+            return false;
+        else
+            throw new ArgumentOutOfRangeException(nameof(elementId));
+    }
+
+    public bool IsQuestAccepted(QuestId questId)
+    {
+        QuestManager* questManager = QuestManager.Instance();
+        return questManager->IsQuestAccepted(questId.Value);
+    }
+
+    public bool IsQuestComplete(ElementId elementId)
+    {
+        if (elementId is QuestId questId)
+            return IsQuestComplete(questId);
+        else if (elementId is SatisfactionSupplyNpcId)
+            return false;
+        else
+            throw new ArgumentOutOfRangeException(nameof(elementId));
+    }
+
+    [SuppressMessage("Performance", "CA1822")]
+    public bool IsQuestComplete(QuestId questId)
+    {
+        return QuestManager.IsQuestComplete(questId.Value);
+    }
+
+    public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
+    {
+        if (elementId is QuestId questId)
+            return IsQuestLocked(questId, extraCompletedQuest);
+        else if (elementId is SatisfactionSupplyNpcId)
+            return false;
+        else
+            throw new ArgumentOutOfRangeException(nameof(elementId));
+    }
+
+    public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null)
+    {
+        var questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
+        if (questInfo.QuestLocks.Count > 0)
+        {
+            var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
+            if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests)
+                return true;
+            else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
+                return true;
+        }
+
+        if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany())
+            return true;
+
+        return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
+    }
+
+    private bool HasCompletedPreviousQuests(QuestInfo questInfo, ElementId? extraCompletedQuest)
+    {
+        if (questInfo.PreviousQuests.Count == 0)
+            return true;
+
+        var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
+        if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All &&
+            questInfo.PreviousQuests.Count == completedQuests)
+            return true;
+        else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
+            return true;
+        else
+            return false;
+    }
+
+    private static bool HasCompletedPreviousInstances(QuestInfo questInfo)
+    {
+        if (questInfo.PreviousInstanceContent.Count == 0)
+            return true;
+
+        var completedInstances = questInfo.PreviousInstanceContent.Count(x => UIState.IsInstanceContentCompleted(x));
+        if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.All &&
+            questInfo.PreviousInstanceContent.Count == completedInstances)
+            return true;
+        else if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.AtLeastOne && completedInstances > 0)
+            return true;
+        else
+            return false;
+    }
+
+    public bool IsClassJobUnlocked(EClassJob classJob)
+    {
+        var classJobRow = _dataManager.GetExcelSheet<ClassJob>()!.GetRow((uint)classJob)!;
+        var questId = (ushort)classJobRow.UnlockQuest.Row;
+        if (questId != 0)
+            return IsQuestComplete(new QuestId(questId));
+
+        PlayerState* playerState = PlayerState.Instance();
+        return playerState != null && playerState->ClassJobLevels[classJobRow.ExpArrayIndex] > 0;
+    }
+
+    public bool IsJobUnlocked(EClassJob classJob)
+    {
+        var classJobRow = _dataManager.GetExcelSheet<ClassJob>()!.GetRow((uint)classJob)!;
+        return IsClassJobUnlocked((EClassJob)classJobRow.ClassJobParent.Row);
+    }
+
+    public GrandCompany GetGrandCompany()
+    {
+        return (GrandCompany)PlayerState.Instance()->GrandCompany;
+    }
+}
 
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Numerics;
-using Dalamud.Game.ClientState.Conditions;
-using Dalamud.Game.ClientState.Objects;
-using Dalamud.Game.ClientState.Objects.Types;
-using Dalamud.Memory;
-using Dalamud.Plugin.Services;
-using Dalamud.Utility;
-using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
-using FFXIVClientStructs.FFXIV.Client.Game;
-using FFXIVClientStructs.FFXIV.Client.Game.Control;
-using FFXIVClientStructs.FFXIV.Client.Game.Object;
-using FFXIVClientStructs.FFXIV.Client.Game.UI;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
-using FFXIVClientStructs.FFXIV.Component.GUI;
-using LLib.GameUI;
-using Lumina.Excel.CustomSheets;
-using Lumina.Excel.GeneratedSheets2;
-using Microsoft.Extensions.Logging;
-using Questionable.Controller;
-using Questionable.Data;
-using Questionable.Model;
-using Questionable.Model.Common;
-using Questionable.Model.Questing;
-using Action = Lumina.Excel.GeneratedSheets2.Action;
-using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
-using ContentFinderCondition = Lumina.Excel.GeneratedSheets.ContentFinderCondition;
-using ContentTalk = Lumina.Excel.GeneratedSheets.ContentTalk;
-using EventPathMove = Lumina.Excel.GeneratedSheets.EventPathMove;
-using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
-using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
-using Quest = Questionable.Model.Quest;
-using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType;
-
-namespace Questionable;
-
-internal sealed unsafe class GameFunctions
-{
-    private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet;
-    private readonly ReadOnlyDictionary<uint, ushort> _contentFinderConditionToContentId;
-
-    private readonly IDataManager _dataManager;
-    private readonly IObjectTable _objectTable;
-    private readonly ITargetManager _targetManager;
-    private readonly ICondition _condition;
-    private readonly IClientState _clientState;
-    private readonly QuestRegistry _questRegistry;
-    private readonly QuestData _questData;
-    private readonly IGameGui _gameGui;
-    private readonly Configuration _configuration;
-    private readonly ILogger<GameFunctions> _logger;
-
-    public GameFunctions(IDataManager dataManager,
-        IObjectTable objectTable,
-        ITargetManager targetManager,
-        ICondition condition,
-        IClientState clientState,
-        QuestRegistry questRegistry,
-        QuestData questData,
-        IGameGui gameGui,
-        Configuration configuration,
-        ILogger<GameFunctions> logger)
-    {
-        _dataManager = dataManager;
-        _objectTable = objectTable;
-        _targetManager = targetManager;
-        _condition = condition;
-        _clientState = clientState;
-        _questRegistry = questRegistry;
-        _questData = questData;
-        _gameGui = gameGui;
-        _configuration = configuration;
-        _logger = logger;
-
-        _territoryToAetherCurrentCompFlgSet = dataManager.GetExcelSheet<TerritoryType>()!
-            .Where(x => x.RowId > 0)
-            .Where(x => x.Unknown32 > 0)
-            .ToDictionary(x => (ushort)x.RowId, x => x.Unknown32)
-            .AsReadOnly();
-        _contentFinderConditionToContentId = dataManager.GetExcelSheet<ContentFinderCondition>()!
-            .Where(x => x.RowId > 0 && x.Content > 0)
-            .ToDictionary(x => x.RowId, x => x.Content)
-            .AsReadOnly();
-    }
-
-    public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
-
-    public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuest()
-    {
-        var (currentQuest, sequence) = GetCurrentQuestInternal();
-        PlayerState* playerState = PlayerState.Instance();
-
-        if (currentQuest == null || currentQuest.Value == 0)
-        {
-            if (_clientState.TerritoryType == 181) // Starting in Limsa
-                return (new QuestId(107), 0);
-            if (_clientState.TerritoryType == 182) // Starting in Ul'dah
-                return (new QuestId(594), 0);
-            if (_clientState.TerritoryType == 183) // Starting in Gridania
-                return (new QuestId(39), 0);
-            return default;
-        }
-        else if (currentQuest.Value == 681)
-        {
-            // if we have already picked up the GC quest, just return the progress for it
-            if (IsQuestAccepted(currentQuest) || IsQuestComplete(currentQuest))
-                return (currentQuest, sequence);
-
-            // The company you keep...
-            return _configuration.General.GrandCompany switch
-            {
-                GrandCompany.TwinAdder => (new QuestId(680), 0),
-                GrandCompany.Maelstrom => (new QuestId(681), 0),
-                _ => default
-            };
-        }
-        else if (currentQuest.Value == 3856 && !playerState->IsMountUnlocked(1)) // we come in peace
-        {
-            ushort chocoboQuest = (GrandCompany)playerState->GrandCompany switch
-            {
-                GrandCompany.TwinAdder => 700,
-                GrandCompany.Maelstrom => 701,
-                _ => 0
-            };
-
-            if (chocoboQuest != 0 && !QuestManager.IsQuestComplete(chocoboQuest))
-                return (new QuestId(chocoboQuest), QuestManager.GetQuestSequence(chocoboQuest));
-        }
-        else if (currentQuest.Value == 801)
-        {
-            // skeletons in her closet, finish 'broadening horizons' to unlock the white wolf gate
-            QuestId broadeningHorizons = new QuestId(802);
-            if (IsQuestAccepted(broadeningHorizons))
-                return (broadeningHorizons, QuestManager.GetQuestSequence(broadeningHorizons.Value));
-        }
-
-        return (currentQuest, sequence);
-    }
-
-    public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuestInternal()
-    {
-        var questManager = QuestManager.Instance();
-        if (questManager != null)
-        {
-            // always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do
-            // side quests until the end of time.
-            var msqQuest = GetMainScenarioQuest(questManager);
-            if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
-                return msqQuest;
-
-            // Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item,
-            // do the MSQ; if a side quest is the first item do that side quest.
-            //
-            // If no quests are marked as 'priority', accepting a new quest adds it to the top of the list.
-            for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i)
-            {
-                ElementId currentQuest;
-                var trackedQuest = questManager->TrackedQuests[i];
-                switch (trackedQuest.QuestType)
-                {
-                    default:
-                        continue;
-
-                    case 1: // normal quest
-                        currentQuest = new QuestId(questManager->NormalQuests[trackedQuest.Index].QuestId);
-                        break;
-                }
-
-                if (_questRegistry.IsKnownQuest(currentQuest))
-                    return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
-            }
-
-            // if we know no quest of those currently in the to-do list, just do MSQ
-            return msqQuest;
-        }
-
-        return default;
-    }
-
-    private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager)
-    {
-        if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled
-        {
-            AgentInterface* questRedoHud = AgentModule.Instance()->GetAgentByInternalId(AgentId.QuestRedoHud);
-            if (questRedoHud != null && questRedoHud->IsAgentActive())
-            {
-                // there's surely better ways to check this, but the one in the OOB Plugin was even less reliable
-                if (_gameGui.TryGetAddonByName<AtkUnitBase>("QuestRedoHud", out var addon) &&
-                    addon->AtkValuesCount == 4 &&
-                    // 0 seems to be active,
-                    // 1 seems to be paused,
-                    // 2 is unknown, but it happens e.g. before the quest 'Alzadaal's Legacy'
-                    // 3 seems to be having /ng+ open while active,
-                    // 4 seems to be when (a) suspending the chapter, or (b) having turned in a quest
-                    addon->AtkValues[0].UInt is 0 or 2 or 3 or 4)
-                {
-                    // redoHud+44 is chapter
-                    // redoHud+46 is quest
-                    ushort questId = MemoryHelper.Read<ushort>((nint)questRedoHud + 46);
-                    return (new QuestId(questId), QuestManager.GetQuestSequence(questId));
-                }
-            }
-        }
-
-        var scenarioTree = AgentScenarioTree.Instance();
-        if (scenarioTree == null)
-            return default;
-
-        if (scenarioTree->Data == null)
-            return default;
-
-        QuestId currentQuest = new QuestId(scenarioTree->Data->CurrentScenarioQuest);
-        if (currentQuest.Value == 0)
-            return default;
-
-        // if the MSQ is hidden, we generally ignore it
-        if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden)
-            return default;
-
-        // it can sometimes happen (although this isn't reliably reproducible) that the quest returned here
-        // is one you've just completed.
-        if (!IsReadyToAcceptQuest(currentQuest))
-            return default;
-
-        // if we're not at a high enough level to continue, we also ignore it
-        var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
-        if (currentLevel != 0 &&
-            _questRegistry.TryGetQuest(currentQuest, out Quest? quest)
-            && quest.Info.Level > currentLevel)
-            return default;
-
-        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)
-    {
-        if (elementId is QuestId questId)
-            return IsReadyToAcceptQuest(questId);
-        else if (elementId is SatisfactionSupplyNpcId)
-            return true;
-        else
-            throw new ArgumentOutOfRangeException(nameof(elementId));
-    }
-
-    public bool IsReadyToAcceptQuest(QuestId questId)
-    {
-        _questRegistry.TryGetQuest(questId, out var quest);
-        if (quest is { Info.IsRepeatable: true })
-        {
-            if (IsQuestAccepted(questId))
-                return false;
-        }
-        else
-        {
-            if (IsQuestAcceptedOrComplete(questId))
-                return false;
-        }
-
-        if (IsQuestLocked(questId))
-            return false;
-
-        // if we're not at a high enough level to continue, we also ignore it
-        var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
-        if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel)
-            return false;
-
-        return true;
-    }
-
-    public bool IsQuestAcceptedOrComplete(ElementId questElementId)
-    {
-        return IsQuestComplete(questElementId) || IsQuestAccepted(questElementId);
-    }
-
-    public bool IsQuestAccepted(ElementId elementId)
-    {
-        if (elementId is QuestId questId)
-            return IsQuestAccepted(questId);
-        else if (elementId is SatisfactionSupplyNpcId)
-            return false;
-        else
-            throw new ArgumentOutOfRangeException(nameof(elementId));
-    }
-
-    public bool IsQuestAccepted(QuestId questId)
-    {
-        QuestManager* questManager = QuestManager.Instance();
-        return questManager->IsQuestAccepted(questId.Value);
-    }
-
-    public bool IsQuestComplete(ElementId elementId)
-    {
-        if (elementId is QuestId questId)
-            return IsQuestComplete(questId);
-        else if (elementId is SatisfactionSupplyNpcId)
-            return false;
-        else
-            throw new ArgumentOutOfRangeException(nameof(elementId));
-    }
-
-    [SuppressMessage("Performance", "CA1822")]
-    public bool IsQuestComplete(QuestId questId)
-    {
-        return QuestManager.IsQuestComplete(questId.Value);
-    }
-
-    public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
-    {
-        if (elementId is QuestId questId)
-            return IsQuestLocked(questId, extraCompletedQuest);
-        else if (elementId is SatisfactionSupplyNpcId)
-            return false;
-        else
-            throw new ArgumentOutOfRangeException(nameof(elementId));
-    }
-
-    public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null)
-    {
-        var questInfo = (QuestInfo) _questData.GetQuestInfo(questId);
-        if (questInfo.QuestLocks.Count > 0)
-        {
-            var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
-            if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests)
-                return true;
-            else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
-                return true;
-        }
-
-        if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany())
-            return true;
-
-        return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
-    }
-
-    private bool HasCompletedPreviousQuests(QuestInfo questInfo, ElementId? extraCompletedQuest)
-    {
-        if (questInfo.PreviousQuests.Count == 0)
-            return true;
-
-        var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
-        if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All &&
-            questInfo.PreviousQuests.Count == completedQuests)
-            return true;
-        else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
-            return true;
-        else
-            return false;
-    }
-
-    private static bool HasCompletedPreviousInstances(QuestInfo questInfo)
-    {
-        if (questInfo.PreviousInstanceContent.Count == 0)
-            return true;
-
-        var completedInstances = questInfo.PreviousInstanceContent.Count(x => UIState.IsInstanceContentCompleted(x));
-        if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.All &&
-            questInfo.PreviousInstanceContent.Count == completedInstances)
-            return true;
-        else if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.AtLeastOne && completedInstances > 0)
-            return true;
-        else
-            return false;
-    }
-
-    public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
-    {
-        subIndex = 0;
-
-        var uiState = UIState.Instance();
-        return uiState != null && uiState->IsAetheryteUnlocked(aetheryteId);
-    }
-
-    public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
-    {
-        if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
-            return IsQuestComplete(new QuestId(3672));
-        return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
-    }
-
-    public bool CanTeleport(EAetheryteLocation aetheryteLocation)
-    {
-        if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId &&
-            ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
-            return true;
-
-        return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
-    }
-
-    public bool TeleportAetheryte(uint aetheryteId)
-    {
-        _logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId);
-        if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
-        {
-            if (aetheryteId == PlayerState.Instance()->HomeAetheryteId &&
-                ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
-            {
-                ReturnRequestedAt = DateTime.Now;
-                if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8))
-                {
-                    _logger.LogInformation("Using 'return' for home aetheryte");
-                    return true;
-                }
-            }
-
-            if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0)
-            {
-                // fallback if return isn't available or (more likely) on a different aetheryte
-                _logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId);
-                return Telepo.Instance()->Teleport(aetheryteId, subIndex);
-            }
-        }
-
-        return false;
-    }
-
-    public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
-        => TeleportAetheryte((uint)aetheryteLocation);
-
-    public bool IsFlyingUnlocked(ushort territoryId)
-    {
-        if (_configuration.Advanced.NeverFly)
-            return false;
-
-        if (IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted])
-        {
-            BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0);
-            if (battleChara != null && battleChara->Mount.MountId == 198) // special quest amaro, not the normal one
-                return true;
-        }
-
-        var playerState = PlayerState.Instance();
-        return playerState != null &&
-               _territoryToAetherCurrentCompFlgSet.TryGetValue(territoryId, out byte aetherCurrentCompFlgSet) &&
-               playerState->IsAetherCurrentZoneComplete(aetherCurrentCompFlgSet);
-    }
-
-    public bool IsFlyingUnlockedInCurrentZone() => IsFlyingUnlocked(_clientState.TerritoryType);
-
-    public bool IsAetherCurrentUnlocked(uint aetherCurrentId)
-    {
-        var playerState = PlayerState.Instance();
-        return playerState != null &&
-               playerState->IsAetherCurrentUnlocked(aetherCurrentId);
-    }
-
-    public IGameObject? FindObjectByDataId(uint dataId, ObjectKind? kind = null, bool targetable = false)
-    {
-        foreach (var gameObject in _objectTable)
-        {
-            if (targetable && !gameObject.IsTargetable)
-                continue;
-
-            if (gameObject.ObjectKind is ObjectKind.Player or ObjectKind.Companion or ObjectKind.MountType
-                or ObjectKind.Retainer or ObjectKind.Housing)
-                continue;
-
-            if (gameObject.DataId == dataId && (kind == null || kind.Value == gameObject.ObjectKind))
-            {
-                return gameObject;
-            }
-        }
-
-        _logger.LogWarning("Could not find GameObject with dataId {DataId}", dataId);
-        return null;
-    }
-
-    public bool InteractWith(uint dataId, ObjectKind? kind = null)
-    {
-        IGameObject? gameObject = FindObjectByDataId(dataId, kind);
-        if (gameObject != null)
-            return InteractWith(gameObject);
-
-        _logger.LogDebug("Game object is null");
-        return false;
-    }
-
-    public bool InteractWith(IGameObject gameObject)
-    {
-        _logger.LogInformation("Setting target with {DataId} to {ObjectId}", gameObject.DataId, gameObject.EntityId);
-        _targetManager.Target = null;
-        _targetManager.Target = gameObject;
-
-        if (gameObject.ObjectKind == ObjectKind.GatheringPoint)
-        {
-            TargetSystem.Instance()->OpenObjectInteraction((GameObject*)gameObject.Address);
-            _logger.LogInformation("Interact result: (none) for GatheringPoint");
-            return true;
-        }
-        else
-        {
-            long result = (long)TargetSystem.Instance()->InteractWithObject((GameObject*)gameObject.Address, false);
-
-            _logger.LogInformation("Interact result: {Result}", result);
-            return result != 7 && result > 0;
-        }
-    }
-
-    public bool UseItem(uint itemId)
-    {
-        long result = AgentInventoryContext.Instance()->UseItem(itemId);
-        _logger.LogInformation("UseItem result: {Result}", result);
-
-        return result == 0;
-    }
-
-    public bool UseItem(uint dataId, uint itemId)
-    {
-        IGameObject? gameObject = FindObjectByDataId(dataId);
-        if (gameObject != null)
-        {
-            _targetManager.Target = gameObject;
-            long result = AgentInventoryContext.Instance()->UseItem(itemId);
-
-            _logger.LogInformation("UseItem result on {DataId}: {Result}", dataId, result);
-
-            // TODO is 1 a generally accepted result?
-            return result == 0 || (itemId == 2002450 && result == 1);
-        }
-
-        return false;
-    }
-
-    public bool UseItemOnGround(uint dataId, uint itemId)
-    {
-        IGameObject? gameObject = FindObjectByDataId(dataId);
-        if (gameObject != null)
-        {
-            Vector3 position = gameObject.Position;
-            return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, location: &position);
-        }
-
-        return false;
-    }
-
-    public bool UseItemOnPosition(Vector3 position, uint itemId)
-    {
-        return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, location: &position);
-    }
-
-    public bool UseAction(EAction action)
-    {
-        if (ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0)
-        {
-            bool result = ActionManager.Instance()->UseAction(ActionType.Action, (uint)action);
-            _logger.LogInformation("UseAction {Action} result: {Result}", action, result);
-
-            return result;
-        }
-
-        return false;
-    }
-
-    public bool UseAction(IGameObject gameObject, EAction action)
-    {
-        var actionRow = _dataManager.GetExcelSheet<Action>()!.GetRow((uint)action)!;
-        if (!ActionManager.CanUseActionOnTarget((uint)action, (GameObject*)gameObject.Address))
-        {
-            _logger.LogWarning("Can not use action {Action} on target {Target}", action, gameObject);
-            return false;
-        }
-
-        _targetManager.Target = gameObject;
-        if (ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action, gameObject.GameObjectId) == 0)
-        {
-            bool result;
-            if (actionRow.TargetArea)
-            {
-                Vector3 position = gameObject.Position;
-                result = ActionManager.Instance()->UseActionLocation(ActionType.Action, (uint)action,
-                    location: &position);
-                _logger.LogInformation("UseAction {Action} on target area {Target} result: {Result}", action,
-                    gameObject,
-                    result);
-            }
-            else
-            {
-                result = ActionManager.Instance()->UseAction(ActionType.Action, (uint)action, gameObject.GameObjectId);
-                _logger.LogInformation("UseAction {Action} on target {Target} result: {Result}", action, gameObject,
-                    result);
-            }
-
-            return result;
-        }
-
-        return false;
-    }
-
-    public bool IsObjectAtPosition(uint dataId, Vector3 position, float distance)
-    {
-        IGameObject? gameObject = FindObjectByDataId(dataId);
-        return gameObject != null && (gameObject.Position - position).Length() < distance;
-    }
-
-    public bool HasStatusPreventingMount()
-    {
-        if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone())
-            return true;
-
-        // company chocobo is locked
-        var playerState = PlayerState.Instance();
-        if (playerState != null && !playerState->IsMountUnlocked(1))
-            return true;
-
-        var localPlayer = _clientState.LocalPlayer;
-        if (localPlayer == null)
-            return false;
-
-        var battleChara = (BattleChara*)localPlayer.Address;
-        StatusManager* statusManager = battleChara->GetStatusManager();
-        if (statusManager->HasStatus(1151))
-            return true;
-
-        return HasCharacterStatusPreventingMountOrSprint();
-    }
-
-    public bool HasStatusPreventingSprint() => HasCharacterStatusPreventingMountOrSprint();
-
-    private bool HasCharacterStatusPreventingMountOrSprint()
-    {
-        var localPlayer = _clientState.LocalPlayer;
-        if (localPlayer == null)
-            return false;
-
-        var battleChara = (BattleChara*)localPlayer.Address;
-        StatusManager* statusManager = battleChara->GetStatusManager();
-        return statusManager->HasStatus(565) ||
-               statusManager->HasStatus(404) ||
-               statusManager->HasStatus(416) ||
-               statusManager->HasStatus(2729) ||
-               statusManager->HasStatus(2730);
-    }
-
-    public bool Mount()
-    {
-        if (_condition[ConditionFlag.Mounted])
-            return true;
-
-        var playerState = PlayerState.Instance();
-        if (playerState != null && _configuration.General.MountId != 0 &&
-            playerState->IsMountUnlocked(_configuration.General.MountId))
-        {
-            if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, _configuration.General.MountId) == 0)
-            {
-                _logger.LogDebug("Attempting to use preferred mount...");
-                if (ActionManager.Instance()->UseAction(ActionType.Mount, _configuration.General.MountId))
-                {
-                    _logger.LogInformation("Using preferred mount");
-                    return true;
-                }
-
-                return false;
-            }
-        }
-        else
-        {
-            if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
-            {
-                _logger.LogDebug("Attempting to use mount roulette...");
-                if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9))
-                {
-                    _logger.LogInformation("Using mount roulette");
-                    return true;
-                }
-
-                return false;
-            }
-        }
-
-        return false;
-    }
-
-    public bool Unmount()
-    {
-        if (!_condition[ConditionFlag.Mounted])
-            return true;
-
-        if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
-        {
-            _logger.LogDebug("Attempting to unmount...");
-            if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23))
-            {
-                _logger.LogInformation("Unmounted");
-                return true;
-            }
-
-            return false;
-        }
-        else
-        {
-            _logger.LogWarning("Can't unmount right now?");
-            return false;
-        }
-    }
-
-    public void OpenDutyFinder(uint contentFinderConditionId)
-    {
-        if (_contentFinderConditionToContentId.TryGetValue(contentFinderConditionId, out ushort contentId))
-        {
-            if (UIState.IsInstanceContentUnlocked(contentId))
-                AgentContentsFinder.Instance()->OpenRegularDuty(contentFinderConditionId);
-            else
-                _logger.LogError(
-                    "Trying to access a locked duty (cf: {ContentFinderId}, content: {ContentId})",
-                    contentFinderConditionId, contentId);
-        }
-        else
-            _logger.LogError("Could not find content for content finder condition (cf: {ContentFinderId})",
-                contentFinderConditionId);
-    }
-
-    public string? GetDialogueText(Quest currentQuest, string? excelSheetName, string key)
-    {
-        if (excelSheetName == null)
-        {
-            var questRow =
-                _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.Id.Value +
-                    0x10000);
-            if (questRow == null)
-            {
-                _logger.LogError("Could not find quest row for {QuestId}", currentQuest.Id);
-                return null;
-            }
-
-            excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}";
-        }
-
-        var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
-        if (excelSheet == null)
-        {
-            _logger.LogError("Unknown excel sheet '{SheetName}'", excelSheetName);
-            return null;
-        }
-
-        return excelSheet.FirstOrDefault(x => x.Key == key)?.Value?.ToDalamudString().ToString();
-    }
-
-    public string? GetDialogueTextByRowId(string? excelSheet, uint rowId)
-    {
-        if (excelSheet == "GimmickYesNo")
-        {
-            var questRow = _dataManager.GetExcelSheet<GimmickYesNo>()!.GetRow(rowId);
-            return questRow?.Unknown0?.ToString();
-        }
-        else if (excelSheet == "Warp")
-        {
-            var questRow = _dataManager.GetExcelSheet<Warp>()!.GetRow(rowId);
-            return questRow?.Name?.ToString();
-        }
-        else if (excelSheet is "Addon")
-        {
-            var questRow = _dataManager.GetExcelSheet<Addon>()!.GetRow(rowId);
-            return questRow?.Text?.ToString();
-        }
-        else if (excelSheet is "EventPathMove")
-        {
-            var questRow = _dataManager.GetExcelSheet<EventPathMove>()!.GetRow(rowId);
-            return questRow?.Unknown10?.ToString();
-        }
-        else if (excelSheet is "ContentTalk" or null)
-        {
-            var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
-            return questRow?.Text?.ToString();
-        }
-        else
-            throw new ArgumentOutOfRangeException(nameof(excelSheet), $"Unsupported excel sheet {excelSheet}");
-    }
-
-    public bool IsOccupied()
-    {
-        if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
-            return true;
-
-        if (IsLoadingScreenVisible())
-            return true;
-
-        return _condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
-               _condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
-               _condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
-               _condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
-               _condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57] ||
-               _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] ||
-               _condition[ConditionFlag.Jumping61] || _condition[ConditionFlag.Gathering42];
-    }
-
-    public bool IsLoadingScreenVisible()
-    {
-        return _gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) &&
-               LAddon.IsAddonReady(fade) &&
-               fade->IsVisible;
-    }
-
-    public GrandCompany GetGrandCompany()
-    {
-        return (GrandCompany)PlayerState.Instance()->GrandCompany;
-    }
-}
 
--- /dev/null
+using System;
+using System.Text.RegularExpressions;
+using Questionable.Functions;
+
+namespace Questionable.Model;
+
+internal sealed class StringOrRegex
+{
+    private readonly Regex? _regex;
+    private readonly string? _stringValue;
+
+    public StringOrRegex(Regex? regex)
+    {
+        ArgumentNullException.ThrowIfNull(regex);
+        _regex = regex;
+        _stringValue = null;
+    }
+
+    public StringOrRegex(string? str)
+    {
+        ArgumentNullException.ThrowIfNull(str);
+        _regex = null;
+        _stringValue = str;
+    }
+
+    public bool IsMatch(string other)
+    {
+        if (_regex != null)
+            return _regex.IsMatch(other);
+        else
+            return GameFunctions.GameStringEquals(_stringValue, other);
+    }
+
+    public string? GetString()
+    {
+        if (_stringValue == null)
+            throw new InvalidOperationException();
+
+        return _stringValue;
+    }
+
+    public override string? ToString() => _regex?.ToString() ?? _stringValue;
+}
 
 <Project Sdk="Dalamud.NET.Sdk/10.0.0">
     <PropertyGroup>
-        <Version>2.1</Version>
+        <Version>2.2</Version>
         <OutputPath>dist</OutputPath>
         <PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
         <Platforms>x64</Platforms>
 
 using Questionable.Controller.Steps.Interactions;
 using Questionable.Data;
 using Questionable.External;
+using Questionable.Functions;
 using Questionable.Validation;
 using Questionable.Validation.Validators;
 using Questionable.Windows;
         IContextMenu contextMenu)
     {
         ArgumentNullException.ThrowIfNull(pluginInterface);
-
-        ServiceCollection serviceCollection = new();
-        serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
-            .ClearProviders()
-            .AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..]));
-        serviceCollection.AddSingleton<IDalamudPlugin>(this);
-        serviceCollection.AddSingleton(pluginInterface);
-        serviceCollection.AddSingleton(clientState);
-        serviceCollection.AddSingleton(targetManager);
-        serviceCollection.AddSingleton(framework);
-        serviceCollection.AddSingleton(gameGui);
-        serviceCollection.AddSingleton(dataManager);
-        serviceCollection.AddSingleton(sigScanner);
-        serviceCollection.AddSingleton(objectTable);
-        serviceCollection.AddSingleton(pluginLog);
-        serviceCollection.AddSingleton(condition);
-        serviceCollection.AddSingleton(chatGui);
-        serviceCollection.AddSingleton(commandManager);
-        serviceCollection.AddSingleton(addonLifecycle);
-        serviceCollection.AddSingleton(keyState);
-        serviceCollection.AddSingleton(contextMenu);
-        serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
-        serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
-
-        AddBasicFunctionsAndData(serviceCollection);
-        AddTaskFactories(serviceCollection);
-        AddControllers(serviceCollection);
-        AddWindows(serviceCollection);
-        AddQuestValidators(serviceCollection);
-
-        serviceCollection.AddSingleton<CommandHandler>();
-        serviceCollection.AddSingleton<DalamudInitializer>();
-
-        _serviceProvider = serviceCollection.BuildServiceProvider();
-        _serviceProvider.GetRequiredService<QuestRegistry>().Reload();
-        _serviceProvider.GetRequiredService<CommandHandler>();
-        _serviceProvider.GetRequiredService<ContextMenuController>();
-        _serviceProvider.GetRequiredService<DalamudInitializer>();
+        ArgumentNullException.ThrowIfNull(chatGui);
+        try
+        {
+            ServiceCollection serviceCollection = new();
+            serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
+                .ClearProviders()
+                .AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..]));
+            serviceCollection.AddSingleton<IDalamudPlugin>(this);
+            serviceCollection.AddSingleton(pluginInterface);
+            serviceCollection.AddSingleton(clientState);
+            serviceCollection.AddSingleton(targetManager);
+            serviceCollection.AddSingleton(framework);
+            serviceCollection.AddSingleton(gameGui);
+            serviceCollection.AddSingleton(dataManager);
+            serviceCollection.AddSingleton(sigScanner);
+            serviceCollection.AddSingleton(objectTable);
+            serviceCollection.AddSingleton(pluginLog);
+            serviceCollection.AddSingleton(condition);
+            serviceCollection.AddSingleton(chatGui);
+            serviceCollection.AddSingleton(commandManager);
+            serviceCollection.AddSingleton(addonLifecycle);
+            serviceCollection.AddSingleton(keyState);
+            serviceCollection.AddSingleton(contextMenu);
+            serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
+            serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
+
+            AddBasicFunctionsAndData(serviceCollection);
+            AddTaskFactories(serviceCollection);
+            AddControllers(serviceCollection);
+            AddWindows(serviceCollection);
+            AddQuestValidators(serviceCollection);
+
+            serviceCollection.AddSingleton<CommandHandler>();
+            serviceCollection.AddSingleton<DalamudInitializer>();
+
+            _serviceProvider = serviceCollection.BuildServiceProvider();
+            Initialize(_serviceProvider);
+        }
+        catch (Exception)
+        {
+            chatGui.PrintError("Unable to load plugin, check /xllog for details", "Questionable");
+            throw;
+        }
     }
 
     private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
     {
+        serviceCollection.AddSingleton<ExcelFunctions>();
         serviceCollection.AddSingleton<GameFunctions>();
         serviceCollection.AddSingleton<ChatFunctions>();
+        serviceCollection.AddSingleton<QuestFunctions>();
+
         serviceCollection.AddSingleton<AetherCurrentData>();
         serviceCollection.AddSingleton<AetheryteData>();
         serviceCollection.AddSingleton<GatheringData>();
         serviceCollection.AddTransient<MoveToLandingLocation>();
         serviceCollection.AddTransient<DoGather>();
         serviceCollection.AddTransient<DoGatherCollectable>();
+        serviceCollection.AddTransient<SwitchClassJob>();
 
         // task factories
         serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
         serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
         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.AddSingleton<IQuestValidator, NextQuestValidator>();
         serviceCollection.AddSingleton<IQuestValidator, CompletionFlagsValidator>();
         serviceCollection.AddSingleton<IQuestValidator, AethernetShortcutValidator>();
+        serviceCollection.AddSingleton<IQuestValidator, DialogueChoiceValidator>();
         serviceCollection.AddSingleton<JsonSchemaValidator>();
         serviceCollection.AddSingleton<IQuestValidator>(sp => sp.GetRequiredService<JsonSchemaValidator>());
     }
 
+    private static void Initialize(IServiceProvider serviceProvider)
+    {
+        serviceProvider.GetRequiredService<QuestRegistry>().Reload();
+        serviceProvider.GetRequiredService<CommandHandler>();
+        serviceProvider.GetRequiredService<ContextMenuController>();
+        serviceProvider.GetRequiredService<DalamudInitializer>();
+    }
+
     public void Dispose()
     {
         _serviceProvider?.Dispose();
 
     UnexpectedAcceptQuestStep,
     UnexpectedCompleteQuestStep,
     InvalidAethernetShortcut,
+    InvalidExcelRef,
 }
 
                                 : LogLevel.Information;
                             _logger.Log(level,
                                 "Validation failed: {QuestId} ({QuestName}) / {QuestSequence} / {QuestStep} - {Description}",
-                                issue.QuestId, quest.Info.Name, issue.Sequence, issue.Step, issue.Description);
+                                issue.ElementId, quest.Info.Name, issue.Sequence, issue.Step, issue.Description);
                             if (issue.Type == EIssueType.QuestDisabled && quest.Info.BeastTribe != EBeastTribe.None)
                             {
                                 disabledTribeQuests.TryAdd(quest.Info.BeastTribe, 0);
 
                 var disabledQuests = issues
                     .Where(x => x.Type == EIssueType.QuestDisabled)
-                    .Select(x => x.QuestId)
+                    .Select(x => x.ElementId)
                     .ToList();
 
                 _validationIssues = issues
-                    .Where(x => !disabledQuests.Contains(x.QuestId) || x.Type == EIssueType.QuestDisabled)
-                    .OrderBy(x => x.QuestId)
+                    .Where(x => !disabledQuests.Contains(x.ElementId) || x.Type == EIssueType.QuestDisabled)
+                    .OrderBy(x => x.ElementId)
                     .ThenBy(x => x.Sequence)
                     .ThenBy(x => x.Step)
                     .ThenBy(x => x.Description)
             .OrderBy(x => x.Key)
             .Select(x => new ValidationIssue
             {
-                QuestId = null,
+                ElementId = null,
                 Sequence = null,
                 Step = null,
                 BeastTribe = x.Key,
 
 
 internal sealed record ValidationIssue
 {
-    public required ElementId? QuestId { get; init; }
+    public required ElementId? ElementId { get; init; }
     public required byte? Sequence { get; init; }
     public required int? Step { get; init; }
     public EBeastTribe BeastTribe { get; init; } = EBeastTribe.None;
 
             .Cast<ValidationIssue>();
     }
 
-    private ValidationIssue? Validate(ElementId questElementId, int sequenceNo, int stepId, AethernetShortcut? aethernetShortcut)
+    private ValidationIssue? Validate(ElementId elementId, int sequenceNo, int stepId, AethernetShortcut? aethernetShortcut)
     {
         if (aethernetShortcut == null)
             return null;
         {
             return new ValidationIssue
             {
-                QuestId = questElementId,
+                ElementId = elementId,
                 Sequence = (byte)sequenceNo,
                 Step = stepId,
                 Type = EIssueType.InvalidAethernetShortcut,
 
         {
             yield return new ValidationIssue
             {
-                QuestId = quest.Id,
+                ElementId = quest.Id,
                 Sequence = 0,
                 Step = null,
                 Type = EIssueType.MissingSequence0,
 
                 yield return new ValidationIssue
                 {
-                    QuestId = quest.Id,
+                    ElementId = quest.Id,
                     Sequence = (byte)sequence.Sequence,
                     Step = null,
                     Type = EIssueType.InstantQuestWithMultipleSteps,
         {
             return new ValidationIssue
             {
-                QuestId = quest.Id,
+                ElementId = quest.Id,
                 Sequence = (byte)sequenceNo,
                 Step = null,
                 Type = EIssueType.MissingSequence,
         {
             return new ValidationIssue
             {
-                QuestId = quest.Id,
+                ElementId = quest.Id,
                 Sequence = (byte)sequenceNo,
                 Step = null,
                 Type = EIssueType.DuplicateSequence,
 
                 {
                     yield return new ValidationIssue
                     {
-                        QuestId = quest.Id,
+                        ElementId = quest.Id,
                         Sequence = (byte)sequence.Sequence,
                         Step = i,
                         Type = EIssueType.DuplicateCompletionFlags,
 
--- /dev/null
+using System.Collections.Generic;
+using Questionable.Functions;
+using Questionable.Model;
+using Questionable.Model.Questing;
+
+namespace Questionable.Validation.Validators;
+
+internal sealed class DialogueChoiceValidator : IQuestValidator
+{
+    private readonly ExcelFunctions _excelFunctions;
+
+    public DialogueChoiceValidator(ExcelFunctions excelFunctions)
+    {
+        _excelFunctions = excelFunctions;
+    }
+
+    public IEnumerable<ValidationIssue> Validate(Quest quest)
+    {
+        foreach (var x in quest.AllSteps())
+        {
+            if (x.Step.DialogueChoices.Count == 0)
+                continue;
+
+            foreach (var dialogueChoice in x.Step.DialogueChoices)
+            {
+                ExcelRef? prompt = dialogueChoice.Prompt;
+                if (prompt != null)
+                {
+                    ValidationIssue? promptIssue = Validate(quest, x.Sequence, x.StepId, dialogueChoice.ExcelSheet,
+                        prompt, "Prompt");
+                    if (promptIssue != null)
+                        yield return promptIssue;
+                }
+
+                ExcelRef? answer = dialogueChoice.Answer;
+                if (answer != null)
+                {
+                    ValidationIssue? answerIssue = Validate(quest, x.Sequence, x.StepId, dialogueChoice.ExcelSheet,
+                        answer, "Answer");
+                    if (answerIssue != null)
+                        yield return answerIssue;
+                }
+            }
+        }
+    }
+
+    private ValidationIssue? Validate(Quest quest, QuestSequence sequence, int stepId, string? excelSheet,
+        ExcelRef excelRef, string label)
+    {
+        if (excelRef.Type == ExcelRef.EType.Key)
+        {
+            if (_excelFunctions.GetRawDialogueText(quest, excelSheet, excelRef.AsKey()) == null)
+            {
+                return new ValidationIssue
+                {
+                    ElementId = quest.Id,
+                    Sequence = (byte)sequence.Sequence,
+                    Step = stepId,
+                    Type = EIssueType.InvalidExcelRef,
+                    Severity = EIssueSeverity.Error,
+                    Description = $"{label} invalid: {excelSheet} → {excelRef.AsKey()}",
+                };
+            }
+        }
+        else if (excelRef.Type == ExcelRef.EType.RowId)
+        {
+            if (_excelFunctions.GetRawDialogueTextByRowId(excelSheet, excelRef.AsRowId()) == null)
+            {
+                return new ValidationIssue
+                {
+                    ElementId = quest.Id,
+                    Sequence = (byte)sequence.Sequence,
+                    Step = stepId,
+                    Type = EIssueType.InvalidExcelRef,
+                    Severity = EIssueSeverity.Error,
+                    Description = $"{label} invalid: {excelSheet} → {excelRef.AsRowId()}",
+                };
+            }
+        }
+
+        return null;
+    }
+}
 
             {
                 yield return new ValidationIssue
                 {
-                    QuestId = quest.Id,
+                    ElementId = quest.Id,
                     Sequence = null,
                     Step = null,
                     Type = EIssueType.InvalidJsonSchema,
         }
     }
 
-    public void Enqueue(ElementId questElementId, JsonNode questNode) => _questNodes[questElementId] = questNode;
+    public void Enqueue(ElementId elementId, JsonNode questNode) => _questNodes[elementId] = questNode;
 
     public void Reset() => _questNodes.Clear();
 }
 
         {
             yield return new ValidationIssue
             {
-                QuestId = quest.Id,
+                ElementId = quest.Id,
                 Sequence = (byte)invalidNextQuest.Sequence.Sequence,
                 Step = invalidNextQuest.StepId,
                 Type = EIssueType.InvalidNextQuestId,
 
         {
             yield return new ValidationIssue
             {
-                QuestId = quest.Id,
+                ElementId = quest.Id,
                 Sequence = null,
                 Step = null,
                 Type = EIssueType.QuestDisabled,
 
             {
                 yield return new ValidationIssue
                 {
-                    QuestId = quest.Id,
+                    ElementId = quest.Id,
                     Sequence = (byte)accept.Sequence.Sequence,
                     Step = accept.StepId,
                     Type = EIssueType.UnexpectedAcceptQuestStep,
         {
             yield return new ValidationIssue
             {
-                QuestId = quest.Id,
+                ElementId = quest.Id,
                 Sequence = 0,
                 Step = null,
                 Type = EIssueType.MissingQuestAccept,
             {
                 yield return new ValidationIssue
                 {
-                    QuestId = quest.Id,
+                    ElementId = quest.Id,
                     Sequence = (byte)complete.Sequence.Sequence,
                     Step = complete.StepId,
                     Type = EIssueType.UnexpectedCompleteQuestStep,
         {
             yield return new ValidationIssue
             {
-                QuestId = quest.Id,
+                ElementId = quest.Id,
                 Sequence = 255,
                 Step = null,
                 Type = EIssueType.MissingQuestComplete,
 
 using LLib.ImGui;
 using Questionable.Controller;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Windows.QuestComponents;
 
 {
     private readonly JournalData _journalData;
     private readonly QuestRegistry _questRegistry;
-    private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly UiUtils _uiUtils;
     private readonly QuestTooltipComponent _questTooltipComponent;
     private readonly IDalamudPluginInterface _pluginInterface;
 
     public JournalProgressWindow(JournalData journalData,
         QuestRegistry questRegistry,
-        GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
         UiUtils uiUtils,
         QuestTooltipComponent questTooltipComponent,
         IDalamudPluginInterface pluginInterface,
     {
         _journalData = journalData;
         _questRegistry = questRegistry;
-        _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _uiUtils = uiUtils;
         _questTooltipComponent = questTooltipComponent;
         _pluginInterface = pluginInterface;
         {
             int available = genre.Quests.Count(x =>
                 _questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled);
-            int completed = genre.Quests.Count(x => _gameFunctions.IsQuestComplete(x.QuestId));
+            int completed = genre.Quests.Count(x => _questFunctions.IsQuestComplete(x.QuestId));
             _genreCounts[genre] = (available, completed);
         }
 
 
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using FFXIVClientStructs.FFXIV.Common.Math;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model.Questing;
 
 namespace Questionable.Windows.QuestComponents;
     private static readonly QuestId[] RequiredAllianceRaidQuests =
         [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)];
 
-    private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly QuestData _questData;
     private readonly TerritoryData _territoryData;
     private readonly UiUtils _uiUtils;
 
-    public ARealmRebornComponent(GameFunctions gameFunctions, QuestData questData, TerritoryData territoryData,
+    public ARealmRebornComponent(QuestFunctions questFunctions, QuestData questData, TerritoryData territoryData,
         UiUtils uiUtils)
     {
-        _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _questData = questData;
         _territoryData = territoryData;
         _uiUtils = uiUtils;
     }
 
-    public bool ShouldDraw => !_gameFunctions.IsQuestAcceptedOrComplete(ATimeForEveryPurpose) &&
-                              _gameFunctions.IsQuestComplete(TheUltimateWeapon);
+    public bool ShouldDraw => !_questFunctions.IsQuestAcceptedOrComplete(ATimeForEveryPurpose) &&
+                              _questFunctions.IsQuestComplete(TheUltimateWeapon);
 
     public void Draw()
     {
-        if (!_gameFunctions.IsQuestAcceptedOrComplete(GoodIntentions))
+        if (!_questFunctions.IsQuestAcceptedOrComplete(GoodIntentions))
             DrawPrimals();
 
         DrawAllianceRaids();
 
     private void DrawAllianceRaids()
     {
-        bool complete = _gameFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last());
+        bool complete = _questFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last());
         bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete);
         if (complete || !hover)
             return;
 
 using ImGuiNET;
 using Questionable.Controller;
 using Questionable.Controller.Steps.Shared;
+using Questionable.Functions;
 using Questionable.Model.Questing;
 
 namespace Questionable.Windows.QuestComponents;
     private readonly CombatController _combatController;
     private readonly GatheringController _gatheringController;
     private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly ICommandManager _commandManager;
     private readonly IDalamudPluginInterface _pluginInterface;
     private readonly Configuration _configuration;
         CombatController combatController,
         GatheringController gatheringController,
         GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
         ICommandManager commandManager,
         IDalamudPluginInterface pluginInterface,
         Configuration configuration,
         _combatController = combatController;
         _gatheringController = gatheringController;
         _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _commandManager = commandManager;
         _pluginInterface = pluginInterface;
         _configuration = configuration;
             ImGui.TextUnformatted(
                 $"Simulated Quest: {Shorten(currentQuest.Quest.Info.Name)} / {currentQuest.Sequence} / {currentQuest.Step}");
         }
+        else if (currentQuestType == QuestController.ECurrentQuestType.Gathering)
+        {
+            using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGold);
+            ImGui.TextUnformatted(
+                $"Gathering: {Shorten(currentQuest.Quest.Info.Name)} / {currentQuest.Sequence} / {currentQuest.Step}");
+        }
         else
         {
             var startedQuest = _questController.StartedQuest;
         if (currentQuest.Quest.Id is not QuestId questId)
             return null;
 
-        var questWork = _gameFunctions.GetQuestEx(questId);
+        var questWork = _questFunctions.GetQuestEx(questId);
         if (questWork != null)
         {
             Vector4 color;
 
 using Microsoft.Extensions.Logging;
 using Questionable.Controller;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Common;
 using Questionable.Model.Questing;
 {
     private readonly MovementController _movementController;
     private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly TerritoryData _territoryData;
     private readonly QuestData _questData;
     private readonly QuestSelectionWindow _questSelectionWindow;
     private readonly IGameGui _gameGui;
     private readonly ILogger<CreationUtilsComponent> _logger;
 
-    public CreationUtilsComponent(MovementController movementController, GameFunctions gameFunctions,
-        TerritoryData territoryData, QuestData questData, QuestSelectionWindow questSelectionWindow,
-        IClientState clientState, ITargetManager targetManager, ICondition condition, IGameGui gameGui,
+    public CreationUtilsComponent(
+        MovementController movementController,
+        GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
+        TerritoryData territoryData,
+        QuestData questData,
+        QuestSelectionWindow questSelectionWindow,
+        IClientState clientState,
+        ITargetManager targetManager,
+        ICondition condition,
+        IGameGui gameGui,
         ILogger<CreationUtilsComponent> logger)
     {
         _movementController = movementController;
         _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _territoryData = territoryData;
         _questData = questData;
         _questSelectionWindow = questSelectionWindow;
             ImGui.Text(SeIconChar.BotanistSprout.ToIconString());
         }
 
-        var q = _gameFunctions.GetCurrentQuest();
+        var q = _questFunctions.GetCurrentQuest();
         ImGui.Text($"Current Quest: {q.CurrentQuest} → {q.Sequence}");
 
 #if false
 
 using ImGuiNET;
 using Questionable.Controller;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 
     private readonly QuestRegistry _questRegistry;
     private readonly QuestData _questData;
     private readonly TerritoryData _territoryData;
-    private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly UiUtils _uiUtils;
 
     public QuestTooltipComponent(
         QuestRegistry questRegistry,
         QuestData questData,
         TerritoryData territoryData,
-        GameFunctions gameFunctions,
+        QuestFunctions questFunctions,
         UiUtils uiUtils)
     {
         _questRegistry = questRegistry;
         _questData = questData;
         _territoryData = territoryData;
-        _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _uiUtils = uiUtils;
     }
 
                 _ => "None",
             };
 
-            GrandCompany currentGrandCompany = _gameFunctions.GetGrandCompany();
+            GrandCompany currentGrandCompany = ~_questFunctions.GetGrandCompany();
             _uiUtils.ChecklistItem($"Grand Company: {gcName}", quest.GrandCompany == currentGrandCompany);
         }
 
 
 using ImGuiNET;
 using Questionable.Controller;
 using Questionable.External;
+using Questionable.Functions;
 
 namespace Questionable.Windows.QuestComponents;
 
 
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using FFXIVClientStructs.FFXIV.Client.UI;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
 using ImGuiNET;
 using LLib.GameUI;
 using LLib.ImGui;
 using Questionable.Controller;
 using Questionable.Data;
+using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 using Questionable.Windows.QuestComponents;
     private readonly QuestData _questData;
     private readonly IGameGui _gameGui;
     private readonly IChatGui _chatGui;
-    private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly QuestController _questController;
     private readonly QuestRegistry _questRegistry;
     private readonly IDalamudPluginInterface _pluginInterface;
     private List<IQuestInfo> _offeredQuests = [];
     private bool _onlyAvailableQuests = true;
 
-    public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions,
-        QuestController questController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface,
-        TerritoryData territoryData, IClientState clientState, UiUtils uiUtils,
+    public QuestSelectionWindow(
+        QuestData questData,
+        IGameGui gameGui,
+        IChatGui chatGui,
+        QuestFunctions questFunctions,
+        QuestController questController,
+        QuestRegistry questRegistry,
+        IDalamudPluginInterface pluginInterface,
+        TerritoryData territoryData,
+        IClientState clientState,
+        UiUtils uiUtils,
         QuestTooltipComponent questTooltipComponent)
         : base($"Quest Selection{WindowId}")
     {
         _questData = questData;
         _gameGui = gameGui;
         _chatGui = chatGui;
-        _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _questController = questController;
         _questRegistry = questRegistry;
         _pluginInterface = pluginInterface;
             {
                 var answers = GameUiController.GetChoices(addonSelectIconString);
                 _offeredQuests = _quests
-                    .Where(x => answers.Any(y => GameUiController.GameStringEquals(x.Name, y)))
+                    .Where(x => answers.Any(y => GameFunctions.GameStringEquals(x.Name, y)))
                     .ToList();
             }
             else
 
                 if (knownQuest != null &&
                     knownQuest.FindSequence(0)?.LastStep()?.InteractionType == EInteractionType.AcceptQuest &&
-                    !_gameFunctions.IsQuestAccepted(quest.QuestId) &&
-                    !_gameFunctions.IsQuestLocked(quest.QuestId) &&
-                    (quest.IsRepeatable || !_gameFunctions.IsQuestAcceptedOrComplete(quest.QuestId)))
+                    !_questFunctions.IsQuestAccepted(quest.QuestId) &&
+                    !_questFunctions.IsQuestLocked(quest.QuestId) &&
+                    (quest.IsRepeatable || !_questFunctions.IsQuestAcceptedOrComplete(quest.QuestId)))
                 {
                     ImGui.BeginDisabled(_questController.NextQuest != null || _questController.SimulatedQuest != null);
 
 
             ImGui.TableNextRow();
 
             if (ImGui.TableNextColumn())
-                ImGui.TextUnformatted(validationIssue.QuestId?.ToString() ?? string.Empty);
+                ImGui.TextUnformatted(validationIssue.ElementId?.ToString() ?? string.Empty);
 
             if (ImGui.TableNextColumn())
-                ImGui.TextUnformatted(validationIssue.QuestId != null
-                    ? _questData.GetQuestInfo(validationIssue.QuestId).Name
+                ImGui.TextUnformatted(validationIssue.ElementId != null
+                    ? _questData.GetQuestInfo(validationIssue.ElementId).Name
                     : validationIssue.BeastTribe.ToString());
 
             if (ImGui.TableNextColumn())
 
 
     public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
 
+    public override void PreOpenCheck()
+    {
+        IsOpen |= _questController.IsRunning;
+    }
+
     public override bool DrawConditions()
     {
         if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null || _clientState.IsPvPExcludingDen)
 
 using Dalamud.Plugin;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using ImGuiNET;
+using Questionable.Functions;
 using Questionable.Model.Questing;
 
 namespace Questionable.Windows;
 
 internal sealed class UiUtils
 {
-    private readonly GameFunctions _gameFunctions;
+    private readonly QuestFunctions _questFunctions;
     private readonly IDalamudPluginInterface _pluginInterface;
 
-    public UiUtils(GameFunctions gameFunctions, IDalamudPluginInterface pluginInterface)
+    public UiUtils(QuestFunctions questFunctions, IDalamudPluginInterface pluginInterface)
     {
-        _gameFunctions = gameFunctions;
+        _questFunctions = questFunctions;
         _pluginInterface = pluginInterface;
     }
 
-    public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId questElementId)
+    public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId elementId)
     {
-        if (_gameFunctions.IsQuestAccepted(questElementId))
+        if (_questFunctions.IsQuestAccepted(elementId))
             return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active");
-        else if (_gameFunctions.IsQuestAcceptedOrComplete(questElementId))
+        else if (_questFunctions.IsQuestAcceptedOrComplete(elementId))
             return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
-        else if (_gameFunctions.IsQuestLocked(questElementId))
+        else if (_questFunctions.IsQuestLocked(elementId))
             return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked");
         else
             return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available");