1
0
forked from liza/Questionable

Automatic weekly custom delivery turn in + some gathering cleanup

This commit is contained in:
Liza 2024-08-05 17:09:49 +02:00
parent 837ee7b368
commit 139250c4a4
Signed by: liza
GPG Key ID: 7199F8D727D55F67
79 changed files with 1433 additions and 636 deletions

View File

@ -79,7 +79,7 @@
"Y": 257.4255, "Y": 257.4255,
"Z": -669.3115 "Z": -669.3115
}, },
"MinimumAngle": -65, "MinimumAngle": -30,
"MaximumAngle": 5 "MaximumAngle": 5
} }
] ]

2
LLib

@ -1 +1 @@
Subproject commit 9db9f95b8cd3f36262b5b4b14f12b7331d3c7279 Subproject commit 43c3dba112c202e2d0ff1a6909020c2b83e20dc3

View File

@ -155,6 +155,10 @@ public static class RoslynShortcuts
.AsSyntaxNodeOrToken(), .AsSyntaxNodeOrToken(),
Assignment(nameof(DialogueChoice.Answer), dialogueChoice.Answer, emptyChoice.Answer) Assignment(nameof(DialogueChoice.Answer), dialogueChoice.Answer, emptyChoice.Answer)
.AsSyntaxNodeOrToken(), .AsSyntaxNodeOrToken(),
Assignment(nameof(DialogueChoice.AnswerIsRegularExpression),
dialogueChoice.AnswerIsRegularExpression,
emptyChoice.AnswerIsRegularExpression)
.AsSyntaxNodeOrToken(),
Assignment(nameof(DialogueChoice.DataId), dialogueChoice.DataId, emptyChoice.DataId) Assignment(nameof(DialogueChoice.DataId), dialogueChoice.DataId, emptyChoice.DataId)
.AsSyntaxNodeOrToken())))); .AsSyntaxNodeOrToken()))));
} }
@ -359,6 +363,9 @@ public static class RoslynShortcuts
.AsSyntaxNodeOrToken(), .AsSyntaxNodeOrToken(),
Assignment(nameof(GatheredItem.Collectability), gatheredItem.Collectability, Assignment(nameof(GatheredItem.Collectability), gatheredItem.Collectability,
emptyItem.Collectability) emptyItem.Collectability)
.AsSyntaxNodeOrToken(),
Assignment(nameof(GatheredItem.ClassJob), gatheredItem.ClassJob,
emptyItem.ClassJob)
.AsSyntaxNodeOrToken())))); .AsSyntaxNodeOrToken()))));
} }
else if (value is GatheringNodeGroup nodeGroup) else if (value is GatheringNodeGroup nodeGroup)

View File

@ -25,7 +25,33 @@
}, },
"StopDistance": 5, "StopDistance": 5,
"TerritoryId": 478, "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"
}
]
} }
] ]
} }

View File

@ -15,7 +15,33 @@
"TerritoryId": 478, "TerritoryId": 478,
"InteractionType": "Interact", "InteractionType": "Interact",
"RequiredGatheredItems": [], "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"
}
]
} }
] ]
} }

View File

@ -15,7 +15,33 @@
"TerritoryId": 613, "TerritoryId": 613,
"InteractionType": "Interact", "InteractionType": "Interact",
"RequiredGatheredItems": [], "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"
}
]
} }
] ]
} }

View File

@ -15,7 +15,33 @@
"TerritoryId": 635, "TerritoryId": 635,
"InteractionType": "Interact", "InteractionType": "Interact",
"RequiredGatheredItems": [], "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"
}
]
} }
] ]
} }

View File

@ -18,6 +18,32 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Ishgard] Aetheryte Plaza", "[Ishgard] Aetheryte Plaza",
"[Ishgard] Firmament" "[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"
}
] ]
} }
] ]

View File

@ -18,6 +18,32 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Ishgard] Aetheryte Plaza", "[Ishgard] Aetheryte Plaza",
"[Ishgard] Firmament" "[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"
}
] ]
} }
] ]

View File

@ -15,7 +15,33 @@
"TerritoryId": 820, "TerritoryId": 820,
"InteractionType": "Interact", "InteractionType": "Interact",
"RequiredGatheredItems": [], "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"
}
]
} }
] ]
} }

View File

@ -19,6 +19,32 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Old Sharlayan] Aetheryte Plaza", "[Old Sharlayan] Aetheryte Plaza",
"[Old Sharlayan] The Leveilleur Estate" "[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"
}
] ]
} }
] ]

View File

@ -16,7 +16,33 @@
"InteractionType": "Interact", "InteractionType": "Interact",
"RequiredGatheredItems": [], "RequiredGatheredItems": [],
"AetheryteShortcut": "Il Mheg - Lydha Lran", "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"
}
]
} }
] ]
} }

View File

@ -25,7 +25,33 @@
"Z": -65.14081 "Z": -65.14081
}, },
"TerritoryId": 956, "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"
}
]
} }
] ]
} }

View File

@ -13,14 +13,7 @@
"Z": -68.40625 "Z": -68.40625
}, },
"TerritoryId": 963, "TerritoryId": 963,
"InteractionType": "AcceptQuest", "InteractionType": "AcceptQuest"
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_AKTKMM103_04753_Q1_000_000",
"Answer": "TEXT_AKTKMM103_04753_A1_000_001"
}
]
} }
] ]
}, },

View File

@ -101,6 +101,7 @@
"type": "string", "type": "string",
"description": "What to do at the position", "description": "What to do at the position",
"enum": [ "enum": [
"None",
"Interact", "Interact",
"WalkTo", "WalkTo",
"AttuneAethernetShard", "AttuneAethernetShard",

View File

@ -7,6 +7,7 @@ public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>
{ {
private static readonly Dictionary<EInteractionType, string> Values = new() private static readonly Dictionary<EInteractionType, string> Values = new()
{ {
{ EInteractionType.None, "None" },
{ EInteractionType.Interact, "Interact" }, { EInteractionType.Interact, "Interact" },
{ EInteractionType.WalkTo, "WalkTo" }, { EInteractionType.WalkTo, "WalkTo" },
{ EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" }, { EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" },

View File

@ -16,6 +16,7 @@ public sealed class DialogueChoice
[JsonConverter(typeof(ExcelRefConverter))] [JsonConverter(typeof(ExcelRefConverter))]
public ExcelRef? Answer { get; set; } public ExcelRef? Answer { get; set; }
public bool AnswerIsRegularExpression { get; set; }
/// <summary> /// <summary>
/// If set, only applies when focusing the given target id. /// If set, only applies when focusing the given target id.

View File

@ -6,6 +6,7 @@ namespace Questionable.Model.Questing;
[JsonConverter(typeof(InteractionTypeConverter))] [JsonConverter(typeof(InteractionTypeConverter))]
public enum EInteractionType public enum EInteractionType
{ {
None,
Interact, Interact,
WalkTo, WalkTo,
AttuneAethernetShard, AttuneAethernetShard,

View File

@ -5,4 +5,9 @@ public sealed class GatheredItem
public uint ItemId { get; set; } public uint ItemId { get; set; }
public int ItemCount { get; set; } public int ItemCount { get; set; }
public ushort Collectability { 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; }
} }

View File

@ -14,6 +14,7 @@ using FFXIVClientStructs.FFXIV.Common.Math;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.CombatModules; using Questionable.Controller.CombatModules;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model.Questing; using Questionable.Model.Questing;
namespace Questionable.Controller; namespace Questionable.Controller;
@ -26,7 +27,7 @@ internal sealed class CombatController : IDisposable
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions;
private readonly ILogger<CombatController> _logger; private readonly ILogger<CombatController> _logger;
private CurrentFight? _currentFight; private CurrentFight? _currentFight;
@ -39,7 +40,7 @@ internal sealed class CombatController : IDisposable
IObjectTable objectTable, IObjectTable objectTable,
ICondition condition, ICondition condition,
IClientState clientState, IClientState clientState,
GameFunctions gameFunctions, QuestFunctions questFunctions,
ILogger<CombatController> logger) ILogger<CombatController> logger)
{ {
_combatModules = combatModules.ToList(); _combatModules = combatModules.ToList();
@ -48,7 +49,7 @@ internal sealed class CombatController : IDisposable
_objectTable = objectTable; _objectTable = objectTable;
_condition = condition; _condition = condition;
_clientState = clientState; _clientState = clientState;
_gameFunctions = gameFunctions; _questFunctions = questFunctions;
_logger = logger; _logger = logger;
_clientState.TerritoryChanged += TerritoryChanged; _clientState.TerritoryChanged += TerritoryChanged;
@ -168,9 +169,9 @@ internal sealed class CombatController : IDisposable
} }
} }
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, if (questWork != null && QuestWorkUtils.MatchesQuestWork(condition.CompletionQuestVariablesFlags,
questWork.Value)) questWork.Value))
{ {
@ -303,7 +304,7 @@ internal sealed class CombatController : IDisposable
public sealed class CombatData public sealed class CombatData
{ {
public required ElementId QuestElementId { get; init; } public required ElementId ElementId { get; init; }
public required EEnemySpawnType SpawnType { get; init; } public required EEnemySpawnType SpawnType { get; init; }
public required List<uint> KillEnemyDataIds { get; init; } public required List<uint> KillEnemyDataIds { get; init; }
public required List<ComplexCombatData> ComplexCombatDatas { get; init; } public required List<ComplexCombatData> ComplexCombatDatas { get; init; }

View File

@ -3,6 +3,7 @@ using System.Linq;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Questionable.Windows; using Questionable.Windows;
@ -23,7 +24,7 @@ internal sealed class CommandHandler : IDisposable
private readonly QuestWindow _questWindow; private readonly QuestWindow _questWindow;
private readonly QuestSelectionWindow _questSelectionWindow; private readonly QuestSelectionWindow _questSelectionWindow;
private readonly ITargetManager _targetManager; private readonly ITargetManager _targetManager;
private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions;
public CommandHandler( public CommandHandler(
ICommandManager commandManager, ICommandManager commandManager,
@ -37,7 +38,7 @@ internal sealed class CommandHandler : IDisposable
QuestWindow questWindow, QuestWindow questWindow,
QuestSelectionWindow questSelectionWindow, QuestSelectionWindow questSelectionWindow,
ITargetManager targetManager, ITargetManager targetManager,
GameFunctions gameFunctions) QuestFunctions questFunctions)
{ {
_commandManager = commandManager; _commandManager = commandManager;
_chatGui = chatGui; _chatGui = chatGui;
@ -50,7 +51,7 @@ internal sealed class CommandHandler : IDisposable
_questWindow = questWindow; _questWindow = questWindow;
_questSelectionWindow = questSelectionWindow; _questSelectionWindow = questSelectionWindow;
_targetManager = targetManager; _targetManager = targetManager;
_gameFunctions = gameFunctions; _questFunctions = questFunctions;
_commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand)
{ {
@ -149,7 +150,7 @@ internal sealed class CommandHandler : IDisposable
{ {
if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null) 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."); _chatGui.PrintError($"[Questionable] Quest {questId} is locked.");
else if (_questRegistry.TryGetQuest(questId, out Quest? quest)) else if (_questRegistry.TryGetQuest(questId, out Quest? quest))
{ {

View File

@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.GameData; using LLib.GameData;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.GameStructs; using Questionable.GameStructs;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -21,6 +22,8 @@ internal sealed class ContextMenuController : IDisposable
private readonly GatheringData _gatheringData; private readonly GatheringData _gatheringData;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly IClientState _clientState; private readonly IClientState _clientState;
@ -32,6 +35,8 @@ internal sealed class ContextMenuController : IDisposable
GatheringData gatheringData, GatheringData gatheringData,
QuestRegistry questRegistry, QuestRegistry questRegistry,
QuestData questData, QuestData questData,
GameFunctions gameFunctions,
QuestFunctions questFunctions,
IGameGui gameGui, IGameGui gameGui,
IChatGui chatGui, IChatGui chatGui,
IClientState clientState, IClientState clientState,
@ -42,6 +47,8 @@ internal sealed class ContextMenuController : IDisposable
_gatheringData = gatheringData; _gatheringData = gatheringData;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_questData = questData; _questData = questData;
_gameFunctions = gameFunctions;
_questFunctions = questFunctions;
_gameGui = gameGui; _gameGui = gameGui;
_chatGui = chatGui; _chatGui = chatGui;
_clientState = clientState; _clientState = clientState;
@ -52,7 +59,7 @@ internal sealed class ContextMenuController : IDisposable
private void MenuOpened(IMenuOpenedArgs args) private void MenuOpened(IMenuOpenedArgs args)
{ {
uint itemId = (uint) _gameGui.HoveredItem; uint itemId = (uint)_gameGui.HoveredItem;
if (itemId == 0) if (itemId == 0)
return; return;
@ -62,14 +69,25 @@ internal sealed class ContextMenuController : IDisposable
if (itemId >= 500_000) if (itemId >= 500_000)
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."); _logger.LogInformation("No gathering point found for current job.");
return; return;
} }
if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId))
{
ushort collectability = _gatheringData.GetRecommendedCollectability(itemId); ushort collectability = _gatheringData.GetRecommendedCollectability(itemId);
int quantityToGather = collectability > 0 ? 6 : int.MaxValue; int quantityToGather = collectability > 0 ? 6 : int.MaxValue;
if (collectability == 0) if (collectability == 0)
@ -85,20 +103,32 @@ internal sealed class ContextMenuController : IDisposable
} }
} }
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 args.AddMenuItem(new MenuItem
{ {
Prefix = SeIconChar.Hyadelyn, Prefix = SeIconChar.Hyadelyn,
PrefixColor = 52, PrefixColor = 52,
Name = "Gather with Questionable", Name = name,
OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability), OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability, classJob),
IsEnabled = quantityToGather > 0, 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)) if (_questRegistry.TryGetQuest(info.QuestId, out Quest? quest))
{ {
var step = quest.FindSequence(0)!.FindStep(0)!; var step = quest.FindSequence(0)!.FindStep(0)!;
@ -108,7 +138,8 @@ internal sealed class ContextMenuController : IDisposable
{ {
ItemId = itemId, ItemId = itemId,
ItemCount = quantity, ItemCount = quantity,
Collectability = collectability Collectability = collectability,
ClassJob = (uint)classJob,
} }
]; ];
_questController.SetGatheringQuest(quest); _questController.SetGatheringQuest(quest);

View File

@ -14,6 +14,8 @@ using LLib.GameUI;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Quest = Questionable.Model.Quest; using Quest = Questionable.Model.Quest;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
@ -25,6 +27,8 @@ internal sealed class GameUiController : IDisposable
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly ExcelFunctions _excelFunctions;
private readonly QuestController _questController; private readonly QuestController _questController;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData; private readonly QuestData _questData;
@ -33,13 +37,24 @@ internal sealed class GameUiController : IDisposable
private readonly ILogger<GameUiController> _logger; private readonly ILogger<GameUiController> _logger;
private readonly Regex _returnRegex; private readonly Regex _returnRegex;
public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions, public GameUiController(
QuestController questController, QuestRegistry questRegistry, QuestData questData, IGameGui gameGui, IAddonLifecycle addonLifecycle,
ITargetManager targetManager, IPluginLog pluginLog, ILogger<GameUiController> logger) 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; _addonLifecycle = addonLifecycle;
_dataManager = dataManager; _dataManager = dataManager;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_questFunctions = questFunctions;
_excelFunctions = excelFunctions;
_questController = questController; _questController = questController;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_questData = questData; _questData = questData;
@ -188,7 +203,7 @@ internal sealed class GameUiController : IDisposable
{ {
// it is possible for this to be a quest selection // it is possible for this to be a quest selection
string questName = quest.Info.Name; 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) if (questSelection >= 0)
{ {
addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection); addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
@ -210,7 +225,7 @@ internal sealed class GameUiController : IDisposable
private int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps) private int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
{ {
List<DialogueChoiceInfo> dialogueChoices = []; List<DialogueChoiceInfo> dialogueChoices = [];
var currentQuest = _questController.SimulatedQuest ?? _questController.StartedQuest; var currentQuest = _questController.SimulatedQuest ?? _questController.GatheringQuest ?? _questController.StartedQuest;
if (currentQuest != null) if (currentQuest != null)
{ {
var quest = currentQuest.Quest; var quest = currentQuest.Quest;
@ -260,9 +275,9 @@ internal sealed class GameUiController : IDisposable
var target = _targetManager.Target; var target = _targetManager.Target;
if (target != null) 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)) _questRegistry.TryGetQuest(questInfo.QuestId, out Quest? knownQuest))
{ {
var questChoices = knownQuest.FindSequence(0)?.Steps var questChoices = knownQuest.FindSequence(0)?.Steps
@ -300,8 +315,10 @@ internal sealed class GameUiController : IDisposable
continue; continue;
} }
string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt); string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
string? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer); ?.GetString();
StringOrRegex? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer,
dialogueChoice.AnswerIsRegularExpression);
if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt)) if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
{ {
@ -309,7 +326,8 @@ internal sealed class GameUiController : IDisposable
continue; 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}", _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
excelPrompt, actualPrompt); excelPrompt, actualPrompt);
@ -320,10 +338,22 @@ internal sealed class GameUiController : IDisposable
{ {
_logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}", _logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}",
answers[i], excelAnswer); answers[i], excelAnswer);
if (GameStringEquals(answers[i], excelAnswer)) if (IsMatch(answers[i], excelAnswer))
{ {
_logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'", _logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'",
i, answers[i], actualPrompt); 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 i;
} }
} }
@ -333,13 +363,24 @@ internal sealed class GameUiController : IDisposable
return null; 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) private int? HandleInstanceListChoice(string? actualPrompt)
{ {
if (!_questController.IsRunning) if (!_questController.IsRunning)
return null; return null;
string? expectedPrompt = _gameFunctions.GetDialogueTextByRowId("Addon", 2090); string? expectedPrompt = _excelFunctions.GetDialogueTextByRowId("Addon", 2090, false).GetString();
if (GameStringEquals(actualPrompt, expectedPrompt)) if (GameFunctions.GameStringEquals(actualPrompt, expectedPrompt))
{ {
_logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt); _logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt);
return 0; // any instance return 0; // any instance
@ -419,8 +460,9 @@ internal sealed class GameUiController : IDisposable
continue; continue;
} }
string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt); string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt)) ?.GetString();
if (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt))
{ {
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
excelPrompt, actualPrompt); excelPrompt, actualPrompt);
@ -506,13 +548,13 @@ internal sealed class GameUiController : IDisposable
string? excelName = entry.Name?.ToString(); string? excelName = entry.Name?.ToString();
string? excelQuestion = entry.Question?.ToString(); string? excelQuestion = entry.Question?.ToString();
if (excelQuestion != null && GameStringEquals(excelQuestion, actualPrompt)) if (excelQuestion != null && GameFunctions.GameStringEquals(excelQuestion, actualPrompt))
{ {
warpId = entry.RowId; warpId = entry.RowId;
warpText = excelQuestion; warpText = excelQuestion;
return true; return true;
} }
else if (excelName != null && GameStringEquals(excelName, actualPrompt)) else if (excelName != null && GameFunctions.GameStringEquals(excelName, actualPrompt))
{ {
warpId = entry.RowId; warpId = entry.RowId;
warpText = excelName; warpText = excelName;
@ -642,31 +684,17 @@ internal sealed class GameUiController : IDisposable
} }
} }
/// <summary> private StringOrRegex? ResolveReference(Quest quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
/// 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)
{ {
if (excelRef == null) if (excelRef == null)
return null; return null;
if (excelRef.Type == ExcelRef.EType.Key) 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) 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) else if (excelRef.Type == ExcelRef.EType.RawString)
return excelRef.AsRawString(); return new StringOrRegex(excelRef.AsRawString());
return null; return null;
} }

View File

@ -14,6 +14,7 @@ using Questionable.Controller.Steps.Gathering;
using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.External; using Questionable.External;
using Questionable.Functions;
using Questionable.GatheringPaths; using Questionable.GatheringPaths;
using Questionable.Model.Gathering; using Questionable.Model.Gathering;

View File

@ -17,6 +17,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Control;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.NavigationOverrides; using Questionable.Controller.NavigationOverrides;
using Questionable.External; using Questionable.External;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Common.Converter; using Questionable.Model.Common.Converter;

View File

@ -1,6 +1,7 @@
using System.Numerics; using System.Numerics;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
namespace Questionable.Controller; namespace Questionable.Controller;

View File

@ -5,10 +5,12 @@ using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.External; using Questionable.External;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -18,6 +20,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
{ {
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly MovementController _movementController; private readonly MovementController _movementController;
private readonly CombatController _combatController; private readonly CombatController _combatController;
private readonly GatheringController _gatheringController; private readonly GatheringController _gatheringController;
@ -46,6 +49,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
public QuestController( public QuestController(
IClientState clientState, IClientState clientState,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions,
MovementController movementController, MovementController movementController,
CombatController combatController, CombatController combatController,
GatheringController gatheringController, GatheringController gatheringController,
@ -61,6 +65,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
{ {
_clientState = clientState; _clientState = clientState;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_questFunctions = questFunctions;
_movementController = movementController; _movementController = movementController;
_combatController = combatController; _combatController = combatController;
_gatheringController = gatheringController; _gatheringController = gatheringController;
@ -78,7 +83,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
{ {
if (_simulatedQuest != null) if (_simulatedQuest != null)
return (_simulatedQuest, ECurrentQuestType.Simulated); 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); return (_nextQuest, ECurrentQuestType.Next);
else if (_gatheringQuest != null) else if (_gatheringQuest != null)
return (_gatheringQuest, ECurrentQuestType.Gathering); return (_gatheringQuest, ECurrentQuestType.Gathering);
@ -177,7 +182,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
UpdateCurrentTask(); UpdateCurrentTask();
} }
private void UpdateCurrentQuest() private unsafe void UpdateCurrentQuest()
{ {
lock (_progressLock) lock (_progressLock)
{ {
@ -188,9 +193,9 @@ internal sealed class QuestController : MiniTaskController<QuestController>
// if the quest is accepted, we no longer track it // if the quest is accepted, we no longer track it
bool canUseNextQuest; bool canUseNextQuest;
if (_nextQuest.Quest.Info.IsRepeatable) if (_nextQuest.Quest.Info.IsRepeatable)
canUseNextQuest = !_gameFunctions.IsQuestAccepted(_nextQuest.Quest.Id); canUseNextQuest = !_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id);
else else
canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id); canUseNextQuest = !_questFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id);
if (!canUseNextQuest) if (!canUseNextQuest)
{ {
@ -207,7 +212,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
currentSequence = _simulatedQuest.Sequence; currentSequence = _simulatedQuest.Sequence;
questToRun = _simulatedQuest; questToRun = _simulatedQuest;
} }
else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id)) else if (_nextQuest != null && _questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
{ {
questToRun = _nextQuest; questToRun = _nextQuest;
currentSequence = _nextQuest.Sequence; // by definition, this should always be 0 currentSequence = _nextQuest.Sequence; // by definition, this should always be 0
@ -226,11 +231,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_taskQueue.Count == 0 && _taskQueue.Count == 0 &&
_automationType == EAutomationType.Automatic) _automationType == EAutomationType.Automatic)
ExecuteNextStep(_automationType); ExecuteNextStep(_automationType);
} }
else else
{ {
(ElementId? currentQuestId, currentSequence) = _gameFunctions.GetCurrentQuest(); (ElementId? currentQuestId, currentSequence) = _questFunctions.GetCurrentQuest();
if (currentQuestId == null || currentQuestId.Value == 0) if (currentQuestId == null || currentQuestId.Value == 0)
{ {
if (_startedQuest != null) if (_startedQuest != null)
@ -276,7 +280,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
return; return;
} }
if (_gameFunctions.IsOccupied()) if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questToRun.Quest))
{ {
DebugState = "Occupied"; DebugState = "Occupied";
return; return;
@ -303,7 +307,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
if (questToRun.Sequence != currentSequence) if (questToRun.Sequence != currentSequence)
{ {
questToRun.SetSequence(currentSequence); questToRun.SetSequence(currentSequence);
Stop($"New sequence {questToRun == _startedQuest}/{_gameFunctions.GetCurrentQuestInternal()}", Stop($"New sequence {questToRun == _startedQuest}/{_questFunctions.GetCurrentQuestInternal()}",
continueIfAutomatic: true); continueIfAutomatic: true);
} }
@ -455,7 +459,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
protected override void UpdateCurrentTask() protected override void UpdateCurrentTask()
{ {
if (_gameFunctions.IsOccupied()) if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(CurrentQuest?.Quest))
return; return;
base.UpdateCurrentTask(); base.UpdateCurrentTask();
@ -469,7 +473,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
protected override void OnNextStep(ILastTask task) protected override void OnNextStep(ILastTask task)
{ {
IncreaseStepCount(task.QuestElementId, task.Sequence, true); IncreaseStepCount(task.ElementId, task.Sequence, true);
} }
public void ExecuteNextStep(EAutomationType automatic) public void ExecuteNextStep(EAutomationType automatic)
@ -484,7 +488,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
if (CurrentQuest == null || seq == null || step == null) if (CurrentQuest == null || seq == null || step == null)
{ {
if (CurrentQuestDetails?.Progress.Quest.Id is SatisfactionSupplyNpcId && if (CurrentQuestDetails?.Progress.Quest.Id is SatisfactionSupplyNpcId &&
CurrentQuestDetails?.Progress.Sequence == 0 && CurrentQuestDetails?.Progress.Sequence == 1 &&
CurrentQuestDetails?.Progress.Step == 255 && CurrentQuestDetails?.Progress.Step == 255 &&
CurrentQuestDetails?.Type == ECurrentQuestType.Gathering) CurrentQuestDetails?.Type == ECurrentQuestType.Gathering)
{ {
@ -590,7 +594,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
} }
} }
public void Skip(ElementId questQuestElementId, byte currentQuestSequence) public void Skip(ElementId elementId, byte currentQuestSequence)
{ {
lock (_progressLock) lock (_progressLock)
{ {
@ -609,13 +613,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
if (_taskQueue.Count == 0) if (_taskQueue.Count == 0)
{ {
Stop("Skip"); Stop("Skip");
IncreaseStepCount(questQuestElementId, currentQuestSequence); IncreaseStepCount(elementId, currentQuestSequence);
} }
} }
else else
{ {
Stop("SkipNx"); Stop("SkipNx");
IncreaseStepCount(questQuestElementId, currentQuestSequence); IncreaseStepCount(elementId, currentQuestSequence);
} }
} }
} }
@ -657,7 +661,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
foreach (var id in priorityQuests) foreach (var id in priorityQuests)
{ {
var questId = new QuestId(id); 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); SetNextQuest(quest);
_chatGui.Print( _chatGui.Print(

View File

@ -5,6 +5,7 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
namespace Questionable.Controller.Steps.Common; namespace Questionable.Controller.Steps.Common;

View File

@ -1,6 +1,7 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -26,32 +27,32 @@ internal static class NextQuest
} }
} }
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 NextQuestId { get; set; } = null!;
public ElementId CurrentQuestElementId { 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; NextQuestId = nextQuestId;
CurrentQuestElementId = currentQuestElementId; CurrentQuestId = currentQuestId;
return this; return this;
} }
public bool Start() 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); questController.SetNextQuest(quest);
} }
else else
{ {
logger.LogInformation("Next quest with id {QuestId} not found", NextQuestElementId); logger.LogInformation("Next quest with id {QuestId} not found", NextQuestId);
questController.SetNextQuest(null); questController.SetNextQuest(null);
} }
@ -60,6 +61,6 @@ internal static class NextQuest
public ETaskResult Update() => ETaskResult.TaskComplete; public ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => $"SetNextQuest({NextQuestElementId})"; public override string ToString() => $"SetNextQuest({NextQuestId})";
} }
} }

View File

@ -2,6 +2,7 @@
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Functions;
namespace Questionable.Controller.Steps.Common; namespace Questionable.Controller.Steps.Common;

View File

@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameData; using LLib.GameData;
using LLib.GameUI; using LLib.GameUI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model.Gathering; using Questionable.Model.Gathering;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -8,6 +8,7 @@ using GatheringPathRenderer;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Functions;
using Questionable.Model.Gathering; using Questionable.Model.Gathering;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;

View File

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

View File

@ -4,6 +4,6 @@ namespace Questionable.Controller.Steps;
internal interface ILastTask : ITask internal interface ILastTask : ITask
{ {
public ElementId QuestElementId { get; } public ElementId ElementId { get; }
public int Sequence { get; } public int Sequence { get; }
} }

View File

@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -3,6 +3,7 @@ using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -1,6 +1,7 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -1,6 +1,7 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -78,19 +79,19 @@ internal static class Combat
} }
} }
internal sealed class HandleCombat(CombatController combatController, GameFunctions gameFunctions) : ITask internal sealed class HandleCombat(CombatController combatController, QuestFunctions questFunctions) : ITask
{ {
private bool _isLastStep; private bool _isLastStep;
private CombatController.CombatData _combatData = null!; private CombatController.CombatData _combatData = null!;
private IList<QuestWorkValue?> _completionQuestVariableFlags = 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) IList<QuestWorkValue?> completionQuestVariablesFlags, IList<ComplexCombatData> complexCombatData)
{ {
_isLastStep = isLastStep; _isLastStep = isLastStep;
_combatData = new CombatController.CombatData _combatData = new CombatController.CombatData
{ {
QuestElementId = questElementId, ElementId = elementId,
SpawnType = enemySpawnType, SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(), KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(), ComplexCombatDatas = complexCombatData.ToList(),
@ -107,9 +108,9 @@ internal static class Combat
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
// if our quest step has any completion flags, we need to check if they are set // 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) if (questWork == null)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;

View File

@ -2,6 +2,7 @@
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -7,6 +7,7 @@ using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -9,7 +10,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Say 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) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -20,7 +21,7 @@ internal static class Say
ArgumentNullException.ThrowIfNull(step.ChatMessage); ArgumentNullException.ThrowIfNull(step.ChatMessage);
string? excelString = 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); ArgumentNullException.ThrowIfNull(excelString);
var unmount = serviceProvider.GetRequiredService<UnmountTask>(); var unmount = serviceProvider.GetRequiredService<UnmountTask>();

View File

@ -12,6 +12,7 @@ using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -103,7 +104,7 @@ internal static class UseItem
yield return serviceProvider.GetRequiredService<AetheryteShortcut.UseAetheryteShortcut>() yield return serviceProvider.GetRequiredService<AetheryteShortcut.UseAetheryteShortcut>()
.With(null, EAetheryteLocation.Limsa, territoryId); .With(null, EAetheryteLocation.Limsa, territoryId);
yield return serviceProvider.GetRequiredService<AethernetShortcut.UseAethernetShortcut>() 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<WaitAtEnd.WaitDelay>();
yield return serviceProvider.GetRequiredService<Move.MoveInternal>() yield return serviceProvider.GetRequiredService<Move.MoveInternal>()
.With(territoryId, destination, dataId: npcId, sprint: false); .With(territoryId, destination, dataId: npcId, sprint: false);
@ -112,7 +113,7 @@ internal static class UseItem
} }
} }
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 bool _usedItem;
private DateTime _continueAt; private DateTime _continueAt;
@ -144,7 +145,7 @@ internal static class UseItem
{ {
if (QuestId is QuestId questId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags)) if (QuestId is QuestId questId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
{ {
QuestWork? questWork = gameFunctions.GetQuestEx(questId); QuestWork? questWork = questFunctions.GetQuestEx(questId);
if (questWork != null && if (questWork != null &&
QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork.Value)) QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork.Value))
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
@ -196,11 +197,9 @@ internal static class UseItem
} }
internal sealed class UseOnGround(GameFunctions gameFunctions, ICondition condition, ILogger<UseOnGround> logger) internal sealed class UseOnGround(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<UseOnGround> logger)
: UseItemBase(gameFunctions, condition, logger) : UseItemBase(questFunctions, condition, logger)
{ {
private readonly GameFunctions _gameFunctions = gameFunctions;
public uint DataId { get; set; } public uint DataId { get; set; }
public ITask With(ElementId? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags) public ITask With(ElementId? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
@ -212,19 +211,18 @@ internal static class UseItem
return this; 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})"; public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
} }
internal sealed class UseOnPosition( internal sealed class UseOnPosition(
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger<UseOnPosition> logger) ILogger<UseOnPosition> logger)
: UseItemBase(gameFunctions, condition, logger) : UseItemBase(questFunctions, condition, logger)
{ {
private readonly GameFunctions _gameFunctions = gameFunctions;
public Vector3 Position { get; set; } public Vector3 Position { get; set; }
public ITask With(ElementId? questId, Vector3 position, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags) public ITask With(ElementId? questId, Vector3 position, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
@ -236,17 +234,15 @@ internal static class UseItem
return this; return this;
} }
protected override bool UseItem() => _gameFunctions.UseItemOnPosition(Position, ItemId); protected override bool UseItem() => gameFunctions.UseItemOnPosition(Position, ItemId);
public override string ToString() => public override string ToString() =>
$"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})"; $"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})";
} }
internal sealed class UseOnObject(GameFunctions gameFunctions, ICondition condition, ILogger<UseOnObject> logger) internal sealed class UseOnObject(QuestFunctions questFunctions, GameFunctions gameFunctions, ICondition condition, ILogger<UseOnObject> logger)
: UseItemBase(gameFunctions, condition, logger) : UseItemBase(questFunctions, condition, logger)
{ {
private readonly GameFunctions _gameFunctions = gameFunctions;
public uint DataId { get; set; } public uint DataId { get; set; }
public ITask With(ElementId? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags, public ITask With(ElementId? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags,
@ -260,16 +256,14 @@ internal static class UseItem
return this; 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})"; public override string ToString() => $"UseItem({ItemId} on {DataId})";
} }
internal sealed class Use(GameFunctions gameFunctions, ICondition condition, ILogger<Use> logger) internal sealed class Use(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<Use> logger)
: UseItemBase(gameFunctions, condition, logger) : UseItemBase(questFunctions, condition, logger)
{ {
private readonly GameFunctions _gameFunctions = gameFunctions;
public ITask With(ElementId? questId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags) public ITask With(ElementId? questId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
{ {
QuestId = questId; QuestId = questId;
@ -278,7 +272,7 @@ internal static class UseItem
return this; return this;
} }
protected override bool UseItem() => _gameFunctions.UseItem(ItemId); protected override bool UseItem() => gameFunctions.UseItem(ItemId);
public override string ToString() => $"UseItem({ItemId})"; public override string ToString() => $"UseItem({ItemId})";
} }

View File

@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Data; using Questionable.Data;
using Questionable.External; using Questionable.External;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Common.Converter; using Questionable.Model.Common.Converter;

View File

@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -28,14 +28,25 @@ internal static class GatheringRequiredItems
{ {
foreach (var requiredGatheredItems in step.RequiredGatheredItems) foreach (var requiredGatheredItems in step.RequiredGatheredItems)
{ {
if (!gatheringData.TryGetGatheringPointId(requiredGatheredItems.ItemId, EClassJob currentClassJob = (EClassJob)clientState.LocalPlayer!.ClassJob.Id;
(EClassJob)clientState.LocalPlayer!.ClassJob.Id, out var gatheringPointId)) 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}"); throw new TaskException($"No gathering point found for item {requiredGatheredItems.ItemId}");
if (!AssemblyGatheringLocationLoader.GetLocations() if (!AssemblyGatheringLocationLoader.GetLocations()
.TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot)) .TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot))
throw new TaskException($"No path found for gathering point {gatheringPointId}"); throw new TaskException($"No path found for gathering point {gatheringPointId}");
if (classJob != currentClassJob)
{
yield return serviceProvider.GetRequiredService<SwitchClassJob>()
.With(classJob);
}
if (HasRequiredItems(requiredGatheredItems)) if (HasRequiredItems(requiredGatheredItems))
continue; continue;
@ -71,7 +82,8 @@ internal static class GatheringRequiredItems
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
return inventoryManager != null && return inventoryManager != null &&
inventoryManager->GetInventoryItemCount(requiredGatheredItems.ItemId, 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) public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)

View File

@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
using Questionable.Controller.NavigationOverrides; using Questionable.Controller.NavigationOverrides;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;

View File

@ -10,6 +10,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -41,17 +42,18 @@ internal static class SkipCondition
internal sealed class CheckSkip( internal sealed class CheckSkip(
ILogger<CheckSkip> logger, ILogger<CheckSkip> logger,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions,
IClientState clientState) : ITask IClientState clientState) : ITask
{ {
public QuestStep Step { get; set; } = null!; public QuestStep Step { get; set; } = null!;
public SkipStepConditions SkipConditions { 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; Step = step;
SkipConditions = skipConditions; SkipConditions = skipConditions;
QuestElementId = questElementId; ElementId = elementId;
return this; return this;
} }
@ -95,14 +97,14 @@ internal static class SkipCondition
} }
if (SkipConditions.QuestsCompleted.Count > 0 && if (SkipConditions.QuestsCompleted.Count > 0 &&
SkipConditions.QuestsCompleted.All(gameFunctions.IsQuestComplete)) SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
{ {
logger.LogInformation("Skipping step, all prequisite quests are complete"); logger.LogInformation("Skipping step, all prequisite quests are complete");
return true; return true;
} }
if (SkipConditions.QuestsAccepted.Count > 0 && if (SkipConditions.QuestsAccepted.Count > 0 &&
SkipConditions.QuestsAccepted.All(gameFunctions.IsQuestAccepted)) SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
{ {
logger.LogInformation("Skipping step, all prequisite quests are accepted"); logger.LogInformation("Skipping step, all prequisite quests are accepted");
return true; return true;
@ -156,9 +158,9 @@ internal static class SkipCondition
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.HasCompletionFlags(Step.CompletionQuestVariablesFlags) && questWork != null)
{ {
if (QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value)) if (QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value))
@ -198,13 +200,13 @@ internal static class SkipCondition
} }
} }
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"); logger.LogInformation("Skipping step, as we have already picked up the relevant quest");
return true; 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"); logger.LogInformation("Skipping step, as we have already completed the relevant quest");
return true; return true;

View File

@ -0,0 +1,44 @@
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})";
}

View File

@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -160,7 +161,7 @@ internal static class WaitAtEnd
public override string ToString() => "Wait(next step or sequence)"; 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 QuestId Quest { get; set; } = null!;
public QuestStep Step { get; set; } = null!; public QuestStep Step { get; set; } = null!;
@ -178,7 +179,7 @@ internal static class WaitAtEnd
public ETaskResult Update() public ETaskResult Update()
{ {
QuestWork? questWork = gameFunctions.GetQuestEx(Quest); QuestWork? questWork = questFunctions.GetQuestEx(Quest);
return questWork != null && return questWork != null &&
QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value) QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
@ -214,13 +215,13 @@ internal static class WaitAtEnd
$"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})"; $"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; return this;
} }
@ -228,21 +229,21 @@ internal static class WaitAtEnd
public ETaskResult Update() public ETaskResult Update()
{ {
return gameFunctions.IsQuestAccepted(QuestElementId) return questFunctions.IsQuestAccepted(ElementId)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : 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; return this;
} }
@ -250,15 +251,15 @@ internal static class WaitAtEnd
public ETaskResult Update() 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 int Sequence { get; } = sequence;
public bool Start() => true; public bool Start() => true;
@ -270,7 +271,7 @@ internal static class WaitAtEnd
internal sealed class EndAutomation : ILastTask internal sealed class EndAutomation : ILastTask
{ {
public ElementId QuestElementId => throw new InvalidOperationException(); public ElementId ElementId => throw new InvalidOperationException();
public int Sequence => throw new InvalidOperationException(); public int Sequence => throw new InvalidOperationException();
public bool Start() => true; public bool Start() => true;

View File

@ -16,7 +16,7 @@ using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Model.Questing; using Questionable.Model.Questing;
namespace Questionable; namespace Questionable.Functions;
internal sealed unsafe class ChatFunctions internal sealed unsafe class ChatFunctions
{ {

View File

@ -0,0 +1,101 @@
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}");
}
}

View File

@ -1,16 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Memory;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
@ -18,60 +14,51 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI; using LLib.GameUI;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets2;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Action = Lumina.Excel.GeneratedSheets2.Action; using Action = Lumina.Excel.GeneratedSheets2.Action;
using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara; using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
using ContentFinderCondition = Lumina.Excel.GeneratedSheets.ContentFinderCondition; 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 ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using Quest = Questionable.Model.Quest; using Quest = Questionable.Model.Quest;
using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType; using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType;
namespace Questionable; namespace Questionable.Functions;
internal sealed unsafe class GameFunctions internal sealed unsafe class GameFunctions
{ {
private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet; private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet;
private readonly ReadOnlyDictionary<uint, ushort> _contentFinderConditionToContentId; private readonly ReadOnlyDictionary<uint, ushort> _contentFinderConditionToContentId;
private readonly QuestFunctions _questFunctions;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly ITargetManager _targetManager; private readonly ITargetManager _targetManager;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ILogger<GameFunctions> _logger; private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager, public GameFunctions(
QuestFunctions questFunctions,
IDataManager dataManager,
IObjectTable objectTable, IObjectTable objectTable,
ITargetManager targetManager, ITargetManager targetManager,
ICondition condition, ICondition condition,
IClientState clientState, IClientState clientState,
QuestRegistry questRegistry,
QuestData questData,
IGameGui gameGui, IGameGui gameGui,
Configuration configuration, Configuration configuration,
ILogger<GameFunctions> logger) ILogger<GameFunctions> logger)
{ {
_questFunctions = questFunctions;
_dataManager = dataManager; _dataManager = dataManager;
_objectTable = objectTable; _objectTable = objectTable;
_targetManager = targetManager; _targetManager = targetManager;
_condition = condition; _condition = condition;
_clientState = clientState; _clientState = clientState;
_questRegistry = questRegistry;
_questData = questData;
_gameGui = gameGui; _gameGui = gameGui;
_configuration = configuration; _configuration = configuration;
_logger = logger; _logger = logger;
@ -89,289 +76,6 @@ internal sealed unsafe class GameFunctions
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue; 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) public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
{ {
subIndex = 0; subIndex = 0;
@ -383,7 +87,7 @@ internal sealed unsafe class GameFunctions
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation) public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
{ {
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament) if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
return IsQuestComplete(new QuestId(3672)); return _questFunctions.IsQuestComplete(new QuestId(3672));
return IsAetheryteUnlocked((uint)aetheryteLocation, out _); return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
} }
@ -431,7 +135,7 @@ internal sealed unsafe class GameFunctions
if (_configuration.Advanced.NeverFly) if (_configuration.Advanced.NeverFly)
return false; return false;
if (IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted]) if (_questFunctions.IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted])
{ {
BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0); BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0);
if (battleChara != null && battleChara->Mount.MountId == 198) // special quest amaro, not the normal one if (battleChara != null && battleChara->Mount.MountId == 198) // special quest amaro, not the normal one
@ -718,61 +422,18 @@ internal sealed unsafe class GameFunctions
contentFinderConditionId); contentFinderConditionId);
} }
public string? GetDialogueText(Quest currentQuest, string? excelSheetName, string key) /// <summary>
/// Ensures characters like '-' are handled equally in both strings.
/// </summary>
public static bool GameStringEquals(string? a, string? b)
{ {
if (excelSheetName == null) if (a == null)
{ return b == 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}"; if (b == null)
} return false;
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName); return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-');
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() public bool IsOccupied()
@ -792,15 +453,28 @@ internal sealed unsafe class GameFunctions
_condition[ConditionFlag.Jumping61] || _condition[ConditionFlag.Gathering42]; _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() public bool IsLoadingScreenVisible()
{ {
return _gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) && return _gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) &&
LAddon.IsAddonReady(fade) && LAddon.IsAddonReady(fade) &&
fade->IsVisible; fade->IsVisible;
} }
public GrandCompany GetGrandCompany()
{
return (GrandCompany)PlayerState.Instance()->GrandCompany;
}
} }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<Project Sdk="Dalamud.NET.Sdk/10.0.0"> <Project Sdk="Dalamud.NET.Sdk/10.0.0">
<PropertyGroup> <PropertyGroup>
<Version>2.1</Version> <Version>2.2</Version>
<OutputPath>dist</OutputPath> <OutputPath>dist</OutputPath>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap> <PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>

View File

@ -17,6 +17,7 @@ using Questionable.Controller.Steps.Gathering;
using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Interactions;
using Questionable.Data; using Questionable.Data;
using Questionable.External; using Questionable.External;
using Questionable.Functions;
using Questionable.Validation; using Questionable.Validation;
using Questionable.Validation.Validators; using Questionable.Validation.Validators;
using Questionable.Windows; using Questionable.Windows;
@ -47,7 +48,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin
IContextMenu contextMenu) IContextMenu contextMenu)
{ {
ArgumentNullException.ThrowIfNull(pluginInterface); ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(chatGui);
try
{
ServiceCollection serviceCollection = new(); ServiceCollection serviceCollection = new();
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace) serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
.ClearProviders() .ClearProviders()
@ -81,16 +84,22 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<DalamudInitializer>(); serviceCollection.AddSingleton<DalamudInitializer>();
_serviceProvider = serviceCollection.BuildServiceProvider(); _serviceProvider = serviceCollection.BuildServiceProvider();
_serviceProvider.GetRequiredService<QuestRegistry>().Reload(); Initialize(_serviceProvider);
_serviceProvider.GetRequiredService<CommandHandler>(); }
_serviceProvider.GetRequiredService<ContextMenuController>(); catch (Exception)
_serviceProvider.GetRequiredService<DalamudInitializer>(); {
chatGui.PrintError("Unable to load plugin, check /xllog for details", "Questionable");
throw;
}
} }
private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection) private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
{ {
serviceCollection.AddSingleton<ExcelFunctions>();
serviceCollection.AddSingleton<GameFunctions>(); serviceCollection.AddSingleton<GameFunctions>();
serviceCollection.AddSingleton<ChatFunctions>(); serviceCollection.AddSingleton<ChatFunctions>();
serviceCollection.AddSingleton<QuestFunctions>();
serviceCollection.AddSingleton<AetherCurrentData>(); serviceCollection.AddSingleton<AetherCurrentData>();
serviceCollection.AddSingleton<AetheryteData>(); serviceCollection.AddSingleton<AetheryteData>();
serviceCollection.AddSingleton<GatheringData>(); serviceCollection.AddSingleton<GatheringData>();
@ -110,6 +119,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTransient<MoveToLandingLocation>(); serviceCollection.AddTransient<MoveToLandingLocation>();
serviceCollection.AddTransient<DoGather>(); serviceCollection.AddTransient<DoGather>();
serviceCollection.AddTransient<DoGatherCollectable>(); serviceCollection.AddTransient<DoGatherCollectable>();
serviceCollection.AddTransient<SwitchClassJob>();
// task factories // task factories
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>(); serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
@ -135,6 +145,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>(); serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use, UseItem.UseOnPosition>(); serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use, UseItem.UseOnPosition>();
serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>(); serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>();
serviceCollection.AddTaskWithFactory<TurnInDelivery.Factory, TurnInDelivery.SatisfactionSupplyTurnIn>();
serviceCollection serviceCollection
.AddTaskWithFactory<SinglePlayerDuty.Factory, SinglePlayerDuty.DisableYesAlready, .AddTaskWithFactory<SinglePlayerDuty.Factory, SinglePlayerDuty.DisableYesAlready,
SinglePlayerDuty.RestoreYesAlready>(); SinglePlayerDuty.RestoreYesAlready>();
@ -192,10 +203,19 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<IQuestValidator, NextQuestValidator>(); serviceCollection.AddSingleton<IQuestValidator, NextQuestValidator>();
serviceCollection.AddSingleton<IQuestValidator, CompletionFlagsValidator>(); serviceCollection.AddSingleton<IQuestValidator, CompletionFlagsValidator>();
serviceCollection.AddSingleton<IQuestValidator, AethernetShortcutValidator>(); serviceCollection.AddSingleton<IQuestValidator, AethernetShortcutValidator>();
serviceCollection.AddSingleton<IQuestValidator, DialogueChoiceValidator>();
serviceCollection.AddSingleton<JsonSchemaValidator>(); serviceCollection.AddSingleton<JsonSchemaValidator>();
serviceCollection.AddSingleton<IQuestValidator>(sp => sp.GetRequiredService<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() public void Dispose()
{ {
_serviceProvider?.Dispose(); _serviceProvider?.Dispose();

View File

@ -16,4 +16,5 @@ public enum EIssueType
UnexpectedAcceptQuestStep, UnexpectedAcceptQuestStep,
UnexpectedCompleteQuestStep, UnexpectedCompleteQuestStep,
InvalidAethernetShortcut, InvalidAethernetShortcut,
InvalidExcelRef,
} }

View File

@ -56,7 +56,7 @@ internal sealed class QuestValidator
: LogLevel.Information; : LogLevel.Information;
_logger.Log(level, _logger.Log(level,
"Validation failed: {QuestId} ({QuestName}) / {QuestSequence} / {QuestStep} - {Description}", "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) if (issue.Type == EIssueType.QuestDisabled && quest.Info.BeastTribe != EBeastTribe.None)
{ {
disabledTribeQuests.TryAdd(quest.Info.BeastTribe, 0); disabledTribeQuests.TryAdd(quest.Info.BeastTribe, 0);
@ -70,12 +70,12 @@ internal sealed class QuestValidator
var disabledQuests = issues var disabledQuests = issues
.Where(x => x.Type == EIssueType.QuestDisabled) .Where(x => x.Type == EIssueType.QuestDisabled)
.Select(x => x.QuestId) .Select(x => x.ElementId)
.ToList(); .ToList();
_validationIssues = issues _validationIssues = issues
.Where(x => !disabledQuests.Contains(x.QuestId) || x.Type == EIssueType.QuestDisabled) .Where(x => !disabledQuests.Contains(x.ElementId) || x.Type == EIssueType.QuestDisabled)
.OrderBy(x => x.QuestId) .OrderBy(x => x.ElementId)
.ThenBy(x => x.Sequence) .ThenBy(x => x.Sequence)
.ThenBy(x => x.Step) .ThenBy(x => x.Step)
.ThenBy(x => x.Description) .ThenBy(x => x.Description)
@ -95,7 +95,7 @@ internal sealed class QuestValidator
.OrderBy(x => x.Key) .OrderBy(x => x.Key)
.Select(x => new ValidationIssue .Select(x => new ValidationIssue
{ {
QuestId = null, ElementId = null,
Sequence = null, Sequence = null,
Step = null, Step = null,
BeastTribe = x.Key, BeastTribe = x.Key,

View File

@ -5,7 +5,7 @@ namespace Questionable.Validation;
internal sealed record ValidationIssue internal sealed record ValidationIssue
{ {
public required ElementId? QuestId { get; init; } public required ElementId? ElementId { get; init; }
public required byte? Sequence { get; init; } public required byte? Sequence { get; init; }
public required int? Step { get; init; } public required int? Step { get; init; }
public EBeastTribe BeastTribe { get; init; } = EBeastTribe.None; public EBeastTribe BeastTribe { get; init; } = EBeastTribe.None;

View File

@ -24,7 +24,7 @@ internal sealed class AethernetShortcutValidator : IQuestValidator
.Cast<ValidationIssue>(); .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) if (aethernetShortcut == null)
return null; return null;
@ -35,7 +35,7 @@ internal sealed class AethernetShortcutValidator : IQuestValidator
{ {
return new ValidationIssue return new ValidationIssue
{ {
QuestId = questElementId, ElementId = elementId,
Sequence = (byte)sequenceNo, Sequence = (byte)sequenceNo,
Step = stepId, Step = stepId,
Type = EIssueType.InvalidAethernetShortcut, Type = EIssueType.InvalidAethernetShortcut,

View File

@ -18,7 +18,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = 0, Sequence = 0,
Step = null, Step = null,
Type = EIssueType.MissingSequence0, Type = EIssueType.MissingSequence0,
@ -37,7 +37,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = (byte)sequence.Sequence, Sequence = (byte)sequence.Sequence,
Step = null, Step = null,
Type = EIssueType.InstantQuestWithMultipleSteps, Type = EIssueType.InstantQuestWithMultipleSteps,
@ -73,7 +73,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
{ {
return new ValidationIssue return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = (byte)sequenceNo, Sequence = (byte)sequenceNo,
Step = null, Step = null,
Type = EIssueType.MissingSequence, Type = EIssueType.MissingSequence,
@ -85,7 +85,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
{ {
return new ValidationIssue return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = (byte)sequenceNo, Sequence = (byte)sequenceNo,
Step = null, Step = null,
Type = EIssueType.DuplicateSequence, Type = EIssueType.DuplicateSequence,

View File

@ -45,7 +45,7 @@ internal sealed class CompletionFlagsValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = (byte)sequence.Sequence, Sequence = (byte)sequence.Sequence,
Step = i, Step = i,
Type = EIssueType.DuplicateCompletionFlags, Type = EIssueType.DuplicateCompletionFlags,

View File

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

View File

@ -36,7 +36,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = null, Sequence = null,
Step = null, Step = null,
Type = EIssueType.InvalidJsonSchema, Type = EIssueType.InvalidJsonSchema,
@ -47,7 +47,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator
} }
} }
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(); public void Reset() => _questNodes.Clear();
} }

View File

@ -12,7 +12,7 @@ internal sealed class NextQuestValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = (byte)invalidNextQuest.Sequence.Sequence, Sequence = (byte)invalidNextQuest.Sequence.Sequence,
Step = invalidNextQuest.StepId, Step = invalidNextQuest.StepId,
Type = EIssueType.InvalidNextQuestId, Type = EIssueType.InvalidNextQuestId,

View File

@ -11,7 +11,7 @@ internal sealed class QuestDisabledValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = null, Sequence = null,
Step = null, Step = null,
Type = EIssueType.QuestDisabled, Type = EIssueType.QuestDisabled,

View File

@ -21,7 +21,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = (byte)accept.Sequence.Sequence, Sequence = (byte)accept.Sequence.Sequence,
Step = accept.StepId, Step = accept.StepId,
Type = EIssueType.UnexpectedAcceptQuestStep, Type = EIssueType.UnexpectedAcceptQuestStep,
@ -35,7 +35,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = 0, Sequence = 0,
Step = null, Step = null,
Type = EIssueType.MissingQuestAccept, Type = EIssueType.MissingQuestAccept,
@ -53,7 +53,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = (byte)complete.Sequence.Sequence, Sequence = (byte)complete.Sequence.Sequence,
Step = complete.StepId, Step = complete.StepId,
Type = EIssueType.UnexpectedCompleteQuestStep, Type = EIssueType.UnexpectedCompleteQuestStep,
@ -67,7 +67,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.Id, ElementId = quest.Id,
Sequence = 255, Sequence = 255,
Step = null, Step = null,
Type = EIssueType.MissingQuestComplete, Type = EIssueType.MissingQuestComplete,

View File

@ -12,6 +12,7 @@ using ImGuiNET;
using LLib.ImGui; using LLib.ImGui;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Windows.QuestComponents; using Questionable.Windows.QuestComponents;
@ -21,7 +22,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
{ {
private readonly JournalData _journalData; private readonly JournalData _journalData;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions;
private readonly UiUtils _uiUtils; private readonly UiUtils _uiUtils;
private readonly QuestTooltipComponent _questTooltipComponent; private readonly QuestTooltipComponent _questTooltipComponent;
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
@ -37,7 +38,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
public JournalProgressWindow(JournalData journalData, public JournalProgressWindow(JournalData journalData,
QuestRegistry questRegistry, QuestRegistry questRegistry,
GameFunctions gameFunctions, QuestFunctions questFunctions,
UiUtils uiUtils, UiUtils uiUtils,
QuestTooltipComponent questTooltipComponent, QuestTooltipComponent questTooltipComponent,
IDalamudPluginInterface pluginInterface, IDalamudPluginInterface pluginInterface,
@ -47,7 +48,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
{ {
_journalData = journalData; _journalData = journalData;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_gameFunctions = gameFunctions; _questFunctions = questFunctions;
_uiUtils = uiUtils; _uiUtils = uiUtils;
_questTooltipComponent = questTooltipComponent; _questTooltipComponent = questTooltipComponent;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
@ -327,7 +328,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
{ {
int available = genre.Quests.Count(x => int available = genre.Quests.Count(x =>
_questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled); _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); _genreCounts[genre] = (available, completed);
} }

View File

@ -4,6 +4,7 @@ using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Common.Math; using FFXIVClientStructs.FFXIV.Common.Math;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model.Questing; using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents; namespace Questionable.Windows.QuestComponents;
@ -18,26 +19,26 @@ internal sealed class ARealmRebornComponent
private static readonly QuestId[] RequiredAllianceRaidQuests = private static readonly QuestId[] RequiredAllianceRaidQuests =
[new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)]; [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 QuestData _questData;
private readonly TerritoryData _territoryData; private readonly TerritoryData _territoryData;
private readonly UiUtils _uiUtils; private readonly UiUtils _uiUtils;
public ARealmRebornComponent(GameFunctions gameFunctions, QuestData questData, TerritoryData territoryData, public ARealmRebornComponent(QuestFunctions questFunctions, QuestData questData, TerritoryData territoryData,
UiUtils uiUtils) UiUtils uiUtils)
{ {
_gameFunctions = gameFunctions; _questFunctions = questFunctions;
_questData = questData; _questData = questData;
_territoryData = territoryData; _territoryData = territoryData;
_uiUtils = uiUtils; _uiUtils = uiUtils;
} }
public bool ShouldDraw => !_gameFunctions.IsQuestAcceptedOrComplete(ATimeForEveryPurpose) && public bool ShouldDraw => !_questFunctions.IsQuestAcceptedOrComplete(ATimeForEveryPurpose) &&
_gameFunctions.IsQuestComplete(TheUltimateWeapon); _questFunctions.IsQuestComplete(TheUltimateWeapon);
public void Draw() public void Draw()
{ {
if (!_gameFunctions.IsQuestAcceptedOrComplete(GoodIntentions)) if (!_questFunctions.IsQuestAcceptedOrComplete(GoodIntentions))
DrawPrimals(); DrawPrimals();
DrawAllianceRaids(); DrawAllianceRaids();
@ -63,7 +64,7 @@ internal sealed class ARealmRebornComponent
private void DrawAllianceRaids() private void DrawAllianceRaids()
{ {
bool complete = _gameFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last()); bool complete = _questFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last());
bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete); bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete);
if (complete || !hover) if (complete || !hover)
return; return;

View File

@ -13,6 +13,7 @@ using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using ImGuiNET; using ImGuiNET;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Functions;
using Questionable.Model.Questing; using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents; namespace Questionable.Windows.QuestComponents;
@ -24,6 +25,7 @@ internal sealed class ActiveQuestComponent
private readonly CombatController _combatController; private readonly CombatController _combatController;
private readonly GatheringController _gatheringController; private readonly GatheringController _gatheringController;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration; private readonly Configuration _configuration;
@ -36,6 +38,7 @@ internal sealed class ActiveQuestComponent
CombatController combatController, CombatController combatController,
GatheringController gatheringController, GatheringController gatheringController,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions,
ICommandManager commandManager, ICommandManager commandManager,
IDalamudPluginInterface pluginInterface, IDalamudPluginInterface pluginInterface,
Configuration configuration, Configuration configuration,
@ -47,6 +50,7 @@ internal sealed class ActiveQuestComponent
_combatController = combatController; _combatController = combatController;
_gatheringController = gatheringController; _gatheringController = gatheringController;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_questFunctions = questFunctions;
_commandManager = commandManager; _commandManager = commandManager;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_configuration = configuration; _configuration = configuration;
@ -116,6 +120,12 @@ internal sealed class ActiveQuestComponent
ImGui.TextUnformatted( ImGui.TextUnformatted(
$"Simulated Quest: {Shorten(currentQuest.Quest.Info.Name)} / {currentQuest.Sequence} / {currentQuest.Step}"); $"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 else
{ {
var startedQuest = _questController.StartedQuest; var startedQuest = _questController.StartedQuest;
@ -154,7 +164,7 @@ internal sealed class ActiveQuestComponent
if (currentQuest.Quest.Id is not QuestId questId) if (currentQuest.Quest.Id is not QuestId questId)
return null; return null;
var questWork = _gameFunctions.GetQuestEx(questId); var questWork = _questFunctions.GetQuestEx(questId);
if (questWork != null) if (questWork != null)
{ {
Vector4 color; Vector4 color;

View File

@ -15,6 +15,7 @@ using ImGuiNET;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common; using Questionable.Model.Common;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -26,6 +27,7 @@ internal sealed class CreationUtilsComponent
{ {
private readonly MovementController _movementController; private readonly MovementController _movementController;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly TerritoryData _territoryData; private readonly TerritoryData _territoryData;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly QuestSelectionWindow _questSelectionWindow; private readonly QuestSelectionWindow _questSelectionWindow;
@ -35,13 +37,22 @@ internal sealed class CreationUtilsComponent
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly ILogger<CreationUtilsComponent> _logger; private readonly ILogger<CreationUtilsComponent> _logger;
public CreationUtilsComponent(MovementController movementController, GameFunctions gameFunctions, public CreationUtilsComponent(
TerritoryData territoryData, QuestData questData, QuestSelectionWindow questSelectionWindow, MovementController movementController,
IClientState clientState, ITargetManager targetManager, ICondition condition, IGameGui gameGui, GameFunctions gameFunctions,
QuestFunctions questFunctions,
TerritoryData territoryData,
QuestData questData,
QuestSelectionWindow questSelectionWindow,
IClientState clientState,
ITargetManager targetManager,
ICondition condition,
IGameGui gameGui,
ILogger<CreationUtilsComponent> logger) ILogger<CreationUtilsComponent> logger)
{ {
_movementController = movementController; _movementController = movementController;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_questFunctions = questFunctions;
_territoryData = territoryData; _territoryData = territoryData;
_questData = questData; _questData = questData;
_questSelectionWindow = questSelectionWindow; _questSelectionWindow = questSelectionWindow;
@ -65,7 +76,7 @@ internal sealed class CreationUtilsComponent
ImGui.Text(SeIconChar.BotanistSprout.ToIconString()); ImGui.Text(SeIconChar.BotanistSprout.ToIconString());
} }
var q = _gameFunctions.GetCurrentQuest(); var q = _questFunctions.GetCurrentQuest();
ImGui.Text($"Current Quest: {q.CurrentQuest} → {q.Sequence}"); ImGui.Text($"Current Quest: {q.CurrentQuest} → {q.Sequence}");
#if false #if false

View File

@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET; using ImGuiNET;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -15,20 +16,20 @@ internal sealed class QuestTooltipComponent
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly TerritoryData _territoryData; private readonly TerritoryData _territoryData;
private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions;
private readonly UiUtils _uiUtils; private readonly UiUtils _uiUtils;
public QuestTooltipComponent( public QuestTooltipComponent(
QuestRegistry questRegistry, QuestRegistry questRegistry,
QuestData questData, QuestData questData,
TerritoryData territoryData, TerritoryData territoryData,
GameFunctions gameFunctions, QuestFunctions questFunctions,
UiUtils uiUtils) UiUtils uiUtils)
{ {
_questRegistry = questRegistry; _questRegistry = questRegistry;
_questData = questData; _questData = questData;
_territoryData = territoryData; _territoryData = territoryData;
_gameFunctions = gameFunctions; _questFunctions = questFunctions;
_uiUtils = uiUtils; _uiUtils = uiUtils;
} }
@ -161,7 +162,7 @@ internal sealed class QuestTooltipComponent
_ => "None", _ => "None",
}; };
GrandCompany currentGrandCompany = _gameFunctions.GetGrandCompany(); GrandCompany currentGrandCompany = ~_questFunctions.GetGrandCompany();
_uiUtils.ChecklistItem($"Grand Company: {gcName}", quest.GrandCompany == currentGrandCompany); _uiUtils.ChecklistItem($"Grand Company: {gcName}", quest.GrandCompany == currentGrandCompany);
} }

View File

@ -12,6 +12,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET; using ImGuiNET;
using Questionable.Controller; using Questionable.Controller;
using Questionable.External; using Questionable.External;
using Questionable.Functions;
namespace Questionable.Windows.QuestComponents; namespace Questionable.Windows.QuestComponents;

View File

@ -12,12 +12,12 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET; using ImGuiNET;
using LLib.GameUI; using LLib.GameUI;
using LLib.ImGui; using LLib.ImGui;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Questionable.Windows.QuestComponents; using Questionable.Windows.QuestComponents;
@ -30,7 +30,7 @@ internal sealed class QuestSelectionWindow : LWindow
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions;
private readonly QuestController _questController; private readonly QuestController _questController;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
@ -43,16 +43,24 @@ internal sealed class QuestSelectionWindow : LWindow
private List<IQuestInfo> _offeredQuests = []; private List<IQuestInfo> _offeredQuests = [];
private bool _onlyAvailableQuests = true; private bool _onlyAvailableQuests = true;
public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions, public QuestSelectionWindow(
QuestController questController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface, QuestData questData,
TerritoryData territoryData, IClientState clientState, UiUtils uiUtils, IGameGui gameGui,
IChatGui chatGui,
QuestFunctions questFunctions,
QuestController questController,
QuestRegistry questRegistry,
IDalamudPluginInterface pluginInterface,
TerritoryData territoryData,
IClientState clientState,
UiUtils uiUtils,
QuestTooltipComponent questTooltipComponent) QuestTooltipComponent questTooltipComponent)
: base($"Quest Selection{WindowId}") : base($"Quest Selection{WindowId}")
{ {
_questData = questData; _questData = questData;
_gameGui = gameGui; _gameGui = gameGui;
_chatGui = chatGui; _chatGui = chatGui;
_gameFunctions = gameFunctions; _questFunctions = questFunctions;
_questController = questController; _questController = questController;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
@ -82,7 +90,7 @@ internal sealed class QuestSelectionWindow : LWindow
{ {
var answers = GameUiController.GetChoices(addonSelectIconString); var answers = GameUiController.GetChoices(addonSelectIconString);
_offeredQuests = _quests _offeredQuests = _quests
.Where(x => answers.Any(y => GameUiController.GameStringEquals(x.Name, y))) .Where(x => answers.Any(y => GameFunctions.GameStringEquals(x.Name, y)))
.ToList(); .ToList();
} }
else else
@ -216,9 +224,9 @@ internal sealed class QuestSelectionWindow : LWindow
if (knownQuest != null && if (knownQuest != null &&
knownQuest.FindSequence(0)?.LastStep()?.InteractionType == EInteractionType.AcceptQuest && knownQuest.FindSequence(0)?.LastStep()?.InteractionType == EInteractionType.AcceptQuest &&
!_gameFunctions.IsQuestAccepted(quest.QuestId) && !_questFunctions.IsQuestAccepted(quest.QuestId) &&
!_gameFunctions.IsQuestLocked(quest.QuestId) && !_questFunctions.IsQuestLocked(quest.QuestId) &&
(quest.IsRepeatable || !_gameFunctions.IsQuestAcceptedOrComplete(quest.QuestId))) (quest.IsRepeatable || !_questFunctions.IsQuestAcceptedOrComplete(quest.QuestId)))
{ {
ImGui.BeginDisabled(_questController.NextQuest != null || _questController.SimulatedQuest != null); ImGui.BeginDisabled(_questController.NextQuest != null || _questController.SimulatedQuest != null);

View File

@ -56,11 +56,11 @@ internal sealed class QuestValidationWindow : LWindow
ImGui.TableNextRow(); ImGui.TableNextRow();
if (ImGui.TableNextColumn()) if (ImGui.TableNextColumn())
ImGui.TextUnformatted(validationIssue.QuestId?.ToString() ?? string.Empty); ImGui.TextUnformatted(validationIssue.ElementId?.ToString() ?? string.Empty);
if (ImGui.TableNextColumn()) if (ImGui.TableNextColumn())
ImGui.TextUnformatted(validationIssue.QuestId != null ImGui.TextUnformatted(validationIssue.ElementId != null
? _questData.GetQuestInfo(validationIssue.QuestId).Name ? _questData.GetQuestInfo(validationIssue.ElementId).Name
: validationIssue.BeastTribe.ToString()); : validationIssue.BeastTribe.ToString());
if (ImGui.TableNextColumn()) if (ImGui.TableNextColumn())

View File

@ -63,6 +63,11 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration); public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
public override void PreOpenCheck()
{
IsOpen |= _questController.IsRunning;
}
public override bool DrawConditions() public override bool DrawConditions()
{ {
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null || _clientState.IsPvPExcludingDen) if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null || _clientState.IsPvPExcludingDen)

View File

@ -4,28 +4,29 @@ using Dalamud.Interface.Colors;
using Dalamud.Plugin; using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiNET; using ImGuiNET;
using Questionable.Functions;
using Questionable.Model.Questing; using Questionable.Model.Questing;
namespace Questionable.Windows; namespace Questionable.Windows;
internal sealed class UiUtils internal sealed class UiUtils
{ {
private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions;
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
public UiUtils(GameFunctions gameFunctions, IDalamudPluginInterface pluginInterface) public UiUtils(QuestFunctions questFunctions, IDalamudPluginInterface pluginInterface)
{ {
_gameFunctions = gameFunctions; _questFunctions = questFunctions;
_pluginInterface = pluginInterface; _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"); return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active");
else if (_gameFunctions.IsQuestAcceptedOrComplete(questElementId)) else if (_questFunctions.IsQuestAcceptedOrComplete(elementId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete"); return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_gameFunctions.IsQuestLocked(questElementId)) else if (_questFunctions.IsQuestLocked(elementId))
return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked"); return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked");
else else
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available"); return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available");