"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");