Automatic weekly custom delivery turn in + some gathering cleanup

pull/15/head
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,
"Z": -669.3115
},
"MinimumAngle": -65,
"MinimumAngle": -30,
"MaximumAngle": 5
}
]
@ -128,4 +128,4 @@
]
}
]
}
}

2
LLib

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

View File

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

View File

@ -25,7 +25,33 @@
},
"StopDistance": 5,
"TerritoryId": 478,
"InteractionType": "Interact"
"InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/003/CtsSfsCharacter1_00386",
"Prompt": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/003/CtsSfsCharacter1_00386",
"Prompt": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_003"
}
]
}
]
}

View File

@ -15,7 +15,33 @@
"TerritoryId": 478,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Idyllshire"
"AetheryteShortcut": "Idyllshire",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/005/CtsSfsCharacter4_00541",
"Prompt": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/005/CtsSfsCharacter4_00541",
"Prompt": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_004"
}
]
}
]
}

View File

@ -15,7 +15,33 @@
"TerritoryId": 613,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Ruby Sea - Tamamizu"
"AetheryteShortcut": "Ruby Sea - Tamamizu",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/004/CtsSfsCharacter3_00481",
"Prompt": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 613,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/004/CtsSfsCharacter3_00481",
"Prompt": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_004"
}
]
}
]
}

View File

@ -15,7 +15,33 @@
"TerritoryId": 635,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Rhalgr's Reach"
"AetheryteShortcut": "Rhalgr's Reach",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/004/CtsSfsCharacter2_00434",
"Prompt": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/004/CtsSfsCharacter2_00434",
"Prompt": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_003"
}
]
}
]
}

View File

@ -18,6 +18,32 @@
"AethernetShortcut": [
"[Ishgard] Aetheryte Plaza",
"[Ishgard] Firmament"
],
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/007/CtsSfsCharacter7_00710",
"Prompt": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/007/CtsSfsCharacter7_00710",
"Prompt": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_004"
}
]
}
]

View File

@ -18,6 +18,32 @@
"AethernetShortcut": [
"[Ishgard] Aetheryte Plaza",
"[Ishgard] Firmament"
],
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/006/CtsSfsCharacter6_00674",
"Prompt": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/006/CtsSfsCharacter6_00674",
"Prompt": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_003"
}
]
}
]

View File

@ -15,7 +15,33 @@
"TerritoryId": 820,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Eulmore"
"AetheryteShortcut": "Eulmore",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/006/CtsSfsCharacter5_00640",
"Prompt": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/006/CtsSfsCharacter5_00640",
"Prompt": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_004"
}
]
}
]
}

View File

@ -19,6 +19,32 @@
"AethernetShortcut": [
"[Old Sharlayan] Aetheryte Plaza",
"[Old Sharlayan] The Leveilleur Estate"
],
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/007/CtsSfsCharacter8_00773",
"Prompt": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/007/CtsSfsCharacter8_00773",
"Prompt": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_004"
}
]
}
]

View File

@ -16,7 +16,33 @@
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Il Mheg - Lydha Lran",
"Fly": true
"Fly": true,
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/008/CtsSfsCharacter9_00815",
"Prompt": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/008/CtsSfsCharacter9_00815",
"Prompt": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_004"
}
]
}
]
}

View File

@ -25,7 +25,33 @@
"Z": -65.14081
},
"TerritoryId": 956,
"InteractionType": "Interact"
"InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/008/CtsSfsCharacter10_00842",
"Prompt": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_001",
"AnswerIsRegularExpression": true
}
]
}
]
},
{
"Sequence": 1,
"Steps": [
{
"TerritoryId": 635,
"InteractionType": "None",
"DialogueChoices": [
{
"Type": "List",
"ExcelSheet": "custom/008/CtsSfsCharacter10_00842",
"Prompt": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_000",
"Answer": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_004"
}
]
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,4 +5,9 @@ public sealed class GatheredItem
public uint ItemId { get; set; }
public int ItemCount { get; set; }
public ushort Collectability { get; set; }
/// <summary>
/// Either miner or botanist; null if it is irrelevant (prefers current class/job, then any unlocked ones).
/// </summary>
public uint? ClassJob { get; set; }
}

View File

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

View File

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

View File

@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.GameData;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Functions;
using Questionable.GameStructs;
using Questionable.Model;
using Questionable.Model.Questing;
@ -21,6 +22,8 @@ internal sealed class ContextMenuController : IDisposable
private readonly GatheringData _gatheringData;
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly IGameGui _gameGui;
private readonly IChatGui _chatGui;
private readonly IClientState _clientState;
@ -32,6 +35,8 @@ internal sealed class ContextMenuController : IDisposable
GatheringData gatheringData,
QuestRegistry questRegistry,
QuestData questData,
GameFunctions gameFunctions,
QuestFunctions questFunctions,
IGameGui gameGui,
IChatGui chatGui,
IClientState clientState,
@ -42,6 +47,8 @@ internal sealed class ContextMenuController : IDisposable
_gatheringData = gatheringData;
_questRegistry = questRegistry;
_questData = questData;
_gameFunctions = gameFunctions;
_questFunctions = questFunctions;
_gameGui = gameGui;
_chatGui = chatGui;
_clientState = clientState;
@ -52,7 +59,7 @@ internal sealed class ContextMenuController : IDisposable
private void MenuOpened(IMenuOpenedArgs args)
{
uint itemId = (uint) _gameGui.HoveredItem;
uint itemId = (uint)_gameGui.HoveredItem;
if (itemId == 0)
return;
@ -62,43 +69,66 @@ internal sealed class ContextMenuController : IDisposable
if (itemId >= 500_000)
itemId -= 500_000;
if (!_gatheringData.TryGetGatheringPointId(itemId, (EClassJob)_clientState.LocalPlayer!.ClassJob.Id, out _))
if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId))
{
AddContextMenuEntry(args, itemId, npcId, EClassJob.Miner, "Mine");
AddContextMenuEntry(args, itemId, npcId, EClassJob.Botanist, "Harvest");
}
}
private void AddContextMenuEntry(IMenuOpenedArgs args, uint itemId, uint npcId, EClassJob classJob, string verb)
{
EClassJob currentClassJob = (EClassJob)_clientState.LocalPlayer!.ClassJob.Id;
if (classJob != currentClassJob && currentClassJob is EClassJob.Miner or EClassJob.Botanist)
return;
if (!_gatheringData.TryGetGatheringPointId(itemId, classJob, out _))
{
_logger.LogInformation("No gathering point found for current job.");
return;
}
if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId))
ushort collectability = _gatheringData.GetRecommendedCollectability(itemId);
int quantityToGather = collectability > 0 ? 6 : int.MaxValue;
if (collectability == 0)
return;
unsafe
{
ushort collectability = _gatheringData.GetRecommendedCollectability(itemId);
int quantityToGather = collectability > 0 ? 6 : int.MaxValue;
if (collectability == 0)
return;
unsafe
var agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
if (agentSatisfactionSupply->IsAgentActive())
{
var agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
if (agentSatisfactionSupply->IsAgentActive())
{
quantityToGather = Math.Min(agentSatisfactionSupply->RemainingAllowances,
((AgentSatisfactionSupply2*)agentSatisfactionSupply)->TurnInsToNextRank);
}
quantityToGather = Math.Min(agentSatisfactionSupply->RemainingAllowances,
((AgentSatisfactionSupply2*)agentSatisfactionSupply)->TurnInsToNextRank);
}
args.AddMenuItem(new MenuItem
{
Prefix = SeIconChar.Hyadelyn,
PrefixColor = 52,
Name = "Gather with Questionable",
OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability),
IsEnabled = quantityToGather > 0,
});
}
string lockedReasonn = string.Empty;
if (!_questFunctions.IsClassJobUnlocked(classJob))
lockedReasonn = $"{classJob} not unlocked";
else if (quantityToGather == 0)
lockedReasonn = "No allowances";
else if (_gameFunctions.IsOccupied())
lockedReasonn = "Can't be used while interacting";
string name = $"{verb} with Questionable";
if (!string.IsNullOrEmpty(lockedReasonn))
name += $" ({lockedReasonn})";
args.AddMenuItem(new MenuItem
{
Prefix = SeIconChar.Hyadelyn,
PrefixColor = 52,
Name = name,
OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability, classJob),
IsEnabled = string.IsNullOrEmpty(lockedReasonn),
});
}
private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability)
private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability, EClassJob classJob)
{
var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId).Single(x => x is SatisfactionSupplyInfo);
var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId)
.Single(x => x is SatisfactionSupplyInfo);
if (_questRegistry.TryGetQuest(info.QuestId, out Quest? quest))
{
var step = quest.FindSequence(0)!.FindStep(0)!;
@ -108,7 +138,8 @@ internal sealed class ContextMenuController : IDisposable
{
ItemId = itemId,
ItemCount = quantity,
Collectability = collectability
Collectability = collectability,
ClassJob = (uint)classJob,
}
];
_questController.SetGatheringQuest(quest);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
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 CurrentQuestElementId { get; set; } = null!;
public ElementId NextQuestId { get; set; } = null!;
public ElementId CurrentQuestId { get; set; } = null!;
public ITask With(ElementId nextQuestElementId, ElementId currentQuestElementId)
public ITask With(ElementId nextQuestId, ElementId currentQuestId)
{
NextQuestElementId = nextQuestElementId;
CurrentQuestElementId = currentQuestElementId;
NextQuestId = nextQuestId;
CurrentQuestId = currentQuestId;
return this;
}
public bool Start()
{
if (gameFunctions.IsQuestLocked(NextQuestElementId, CurrentQuestElementId))
if (questFunctions.IsQuestLocked(NextQuestId, CurrentQuestId))
{
logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", NextQuestElementId);
logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", NextQuestId);
}
else if (questRegistry.TryGetQuest(NextQuestElementId, out Quest? quest))
else if (questRegistry.TryGetQuest(NextQuestId, out Quest? quest))
{
logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", NextQuestElementId, quest.Info.Name);
logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", NextQuestId, quest.Info.Name);
questController.SetNextQuest(quest);
}
else
{
logger.LogInformation("Next quest with id {QuestId} not found", NextQuestElementId);
logger.LogInformation("Next quest with id {QuestId} not found", NextQuestId);
questController.SetNextQuest(null);
}
@ -60,6 +61,6 @@ internal static class NextQuest
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.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
namespace Questionable.Controller.Steps.Common;

View File

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

View File

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

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
{
public ElementId QuestElementId { get; }
public ElementId ElementId { get; }
public int Sequence { get; }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model;
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 CombatController.CombatData _combatData = null!;
private IList<QuestWorkValue?> _completionQuestVariableFlags = null!;
public ITask With(ElementId questElementId, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds,
public ITask With(ElementId elementId, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds,
IList<QuestWorkValue?> completionQuestVariablesFlags, IList<ComplexCombatData> complexCombatData)
{
_isLastStep = isLastStep;
_combatData = new CombatController.CombatData
{
QuestElementId = questElementId,
ElementId = elementId,
SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(),
@ -107,9 +108,9 @@ internal static class Combat
return ETaskResult.StillRunning;
// if our quest step has any completion flags, we need to check if they are set
if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.QuestElementId is QuestId questId)
if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.ElementId is QuestId questId)
{
var questWork = gameFunctions.GetQuestEx(questId);
var questWork = questFunctions.GetQuestEx(questId);
if (questWork == null)
return ETaskResult.StillRunning;

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
@ -9,7 +10,7 @@ namespace Questionable.Controller.Steps.Interactions;
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)
{
@ -20,7 +21,7 @@ internal static class Say
ArgumentNullException.ThrowIfNull(step.ChatMessage);
string? excelString =
gameFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key);
excelFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key, false).GetString();
ArgumentNullException.ThrowIfNull(excelString);
var unmount = serviceProvider.GetRequiredService<UnmountTask>();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.Utils;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
@ -160,7 +161,7 @@ internal static class WaitAtEnd
public override string ToString() => "Wait(next step or sequence)";
}
internal sealed class WaitForCompletionFlags(GameFunctions gameFunctions) : ITask
internal sealed class WaitForCompletionFlags(QuestFunctions questFunctions) : ITask
{
public QuestId Quest { get; set; } = null!;
public QuestStep Step { get; set; } = null!;
@ -178,7 +179,7 @@ internal static class WaitAtEnd
public ETaskResult Update()
{
QuestWork? questWork = gameFunctions.GetQuestEx(Quest);
QuestWork? questWork = questFunctions.GetQuestEx(Quest);
return questWork != null &&
QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value)
? ETaskResult.TaskComplete
@ -214,13 +215,13 @@ internal static class WaitAtEnd
$"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;
}
@ -228,21 +229,21 @@ internal static class WaitAtEnd
public ETaskResult Update()
{
return gameFunctions.IsQuestAccepted(QuestElementId)
return questFunctions.IsQuestAccepted(ElementId)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
public override string ToString() => $"WaitQuestAccepted({QuestElementId})";
public override string ToString() => $"WaitQuestAccepted({ElementId})";
}
internal sealed class WaitQuestCompleted(GameFunctions gameFunctions) : ITask
internal sealed class WaitQuestCompleted(QuestFunctions questFunctions) : ITask
{
public ElementId QuestElementId { get; set; } = null!;
public ElementId ElementId { get; set; } = null!;
public ITask With(ElementId questElementId)
public ITask With(ElementId elementId)
{
QuestElementId = questElementId;
ElementId = elementId;
return this;
}
@ -250,15 +251,15 @@ internal static class WaitAtEnd
public ETaskResult Update()
{
return gameFunctions.IsQuestComplete(QuestElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
return questFunctions.IsQuestComplete(ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
}
public override string ToString() => $"WaitQuestComplete({QuestElementId})";
public override string ToString() => $"WaitQuestComplete({ElementId})";
}
internal sealed class NextStep(ElementId questElementId, int sequence) : ILastTask
internal sealed class NextStep(ElementId elementId, int sequence) : ILastTask
{
public ElementId QuestElementId { get; } = questElementId;
public ElementId ElementId { get; } = elementId;
public int Sequence { get; } = sequence;
public bool Start() => true;
@ -270,7 +271,7 @@ internal static class WaitAtEnd
internal sealed class EndAutomation : ILastTask
{
public ElementId QuestElementId => throw new InvalidOperationException();
public ElementId ElementId => throw new InvalidOperationException();
public int Sequence => throw new InvalidOperationException();
public bool Start() => true;

View File

@ -16,7 +16,7 @@ using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging;
using Questionable.Model.Questing;
namespace Questionable;
namespace Questionable.Functions;
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.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
@ -18,60 +14,51 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets2;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
using Action = Lumina.Excel.GeneratedSheets2.Action;
using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
using ContentFinderCondition = Lumina.Excel.GeneratedSheets.ContentFinderCondition;
using ContentTalk = Lumina.Excel.GeneratedSheets.ContentTalk;
using EventPathMove = Lumina.Excel.GeneratedSheets.EventPathMove;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using Quest = Questionable.Model.Quest;
using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType;
namespace Questionable;
namespace Questionable.Functions;
internal sealed unsafe class GameFunctions
{
private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet;
private readonly ReadOnlyDictionary<uint, ushort> _contentFinderConditionToContentId;
private readonly QuestFunctions _questFunctions;
private readonly IDataManager _dataManager;
private readonly IObjectTable _objectTable;
private readonly ITargetManager _targetManager;
private readonly ICondition _condition;
private readonly IClientState _clientState;
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly IGameGui _gameGui;
private readonly Configuration _configuration;
private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager,
public GameFunctions(
QuestFunctions questFunctions,
IDataManager dataManager,
IObjectTable objectTable,
ITargetManager targetManager,
ICondition condition,
IClientState clientState,
QuestRegistry questRegistry,
QuestData questData,
IGameGui gameGui,
Configuration configuration,
ILogger<GameFunctions> logger)
{
_questFunctions = questFunctions;
_dataManager = dataManager;
_objectTable = objectTable;
_targetManager = targetManager;
_condition = condition;
_clientState = clientState;
_questRegistry = questRegistry;
_questData = questData;
_gameGui = gameGui;
_configuration = configuration;
_logger = logger;
@ -89,289 +76,6 @@ internal sealed unsafe class GameFunctions
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuest()
{
var (currentQuest, sequence) = GetCurrentQuestInternal();
PlayerState* playerState = PlayerState.Instance();
if (currentQuest == null || currentQuest.Value == 0)
{
if (_clientState.TerritoryType == 181) // Starting in Limsa
return (new QuestId(107), 0);
if (_clientState.TerritoryType == 182) // Starting in Ul'dah
return (new QuestId(594), 0);
if (_clientState.TerritoryType == 183) // Starting in Gridania
return (new QuestId(39), 0);
return default;
}
else if (currentQuest.Value == 681)
{
// if we have already picked up the GC quest, just return the progress for it
if (IsQuestAccepted(currentQuest) || IsQuestComplete(currentQuest))
return (currentQuest, sequence);
// The company you keep...
return _configuration.General.GrandCompany switch
{
GrandCompany.TwinAdder => (new QuestId(680), 0),
GrandCompany.Maelstrom => (new QuestId(681), 0),
_ => default
};
}
else if (currentQuest.Value == 3856 && !playerState->IsMountUnlocked(1)) // we come in peace
{
ushort chocoboQuest = (GrandCompany)playerState->GrandCompany switch
{
GrandCompany.TwinAdder => 700,
GrandCompany.Maelstrom => 701,
_ => 0
};
if (chocoboQuest != 0 && !QuestManager.IsQuestComplete(chocoboQuest))
return (new QuestId(chocoboQuest), QuestManager.GetQuestSequence(chocoboQuest));
}
else if (currentQuest.Value == 801)
{
// skeletons in her closet, finish 'broadening horizons' to unlock the white wolf gate
QuestId broadeningHorizons = new QuestId(802);
if (IsQuestAccepted(broadeningHorizons))
return (broadeningHorizons, QuestManager.GetQuestSequence(broadeningHorizons.Value));
}
return (currentQuest, sequence);
}
public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuestInternal()
{
var questManager = QuestManager.Instance();
if (questManager != null)
{
// always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do
// side quests until the end of time.
var msqQuest = GetMainScenarioQuest(questManager);
if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
return msqQuest;
// Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item,
// do the MSQ; if a side quest is the first item do that side quest.
//
// If no quests are marked as 'priority', accepting a new quest adds it to the top of the list.
for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i)
{
ElementId currentQuest;
var trackedQuest = questManager->TrackedQuests[i];
switch (trackedQuest.QuestType)
{
default:
continue;
case 1: // normal quest
currentQuest = new QuestId(questManager->NormalQuests[trackedQuest.Index].QuestId);
break;
}
if (_questRegistry.IsKnownQuest(currentQuest))
return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
}
// if we know no quest of those currently in the to-do list, just do MSQ
return msqQuest;
}
return default;
}
private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager)
{
if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled
{
AgentInterface* questRedoHud = AgentModule.Instance()->GetAgentByInternalId(AgentId.QuestRedoHud);
if (questRedoHud != null && questRedoHud->IsAgentActive())
{
// there's surely better ways to check this, but the one in the OOB Plugin was even less reliable
if (_gameGui.TryGetAddonByName<AtkUnitBase>("QuestRedoHud", out var addon) &&
addon->AtkValuesCount == 4 &&
// 0 seems to be active,
// 1 seems to be paused,
// 2 is unknown, but it happens e.g. before the quest 'Alzadaal's Legacy'
// 3 seems to be having /ng+ open while active,
// 4 seems to be when (a) suspending the chapter, or (b) having turned in a quest
addon->AtkValues[0].UInt is 0 or 2 or 3 or 4)
{
// redoHud+44 is chapter
// redoHud+46 is quest
ushort questId = MemoryHelper.Read<ushort>((nint)questRedoHud + 46);
return (new QuestId(questId), QuestManager.GetQuestSequence(questId));
}
}
}
var scenarioTree = AgentScenarioTree.Instance();
if (scenarioTree == null)
return default;
if (scenarioTree->Data == null)
return default;
QuestId currentQuest = new QuestId(scenarioTree->Data->CurrentScenarioQuest);
if (currentQuest.Value == 0)
return default;
// if the MSQ is hidden, we generally ignore it
if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden)
return default;
// it can sometimes happen (although this isn't reliably reproducible) that the quest returned here
// is one you've just completed.
if (!IsReadyToAcceptQuest(currentQuest))
return default;
// if we're not at a high enough level to continue, we also ignore it
var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
if (currentLevel != 0 &&
_questRegistry.TryGetQuest(currentQuest, out Quest? quest)
&& quest.Info.Level > currentLevel)
return default;
return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
}
public QuestWork? GetQuestEx(QuestId questId)
{
QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId.Value);
return questWork != null ? *questWork : null;
}
public bool IsReadyToAcceptQuest(ElementId elementId)
{
if (elementId is QuestId questId)
return IsReadyToAcceptQuest(questId);
else if (elementId is SatisfactionSupplyNpcId)
return true;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsReadyToAcceptQuest(QuestId questId)
{
_questRegistry.TryGetQuest(questId, out var quest);
if (quest is { Info.IsRepeatable: true })
{
if (IsQuestAccepted(questId))
return false;
}
else
{
if (IsQuestAcceptedOrComplete(questId))
return false;
}
if (IsQuestLocked(questId))
return false;
// if we're not at a high enough level to continue, we also ignore it
var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel)
return false;
return true;
}
public bool IsQuestAcceptedOrComplete(ElementId questElementId)
{
return IsQuestComplete(questElementId) || IsQuestAccepted(questElementId);
}
public bool IsQuestAccepted(ElementId elementId)
{
if (elementId is QuestId questId)
return IsQuestAccepted(questId);
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsQuestAccepted(QuestId questId)
{
QuestManager* questManager = QuestManager.Instance();
return questManager->IsQuestAccepted(questId.Value);
}
public bool IsQuestComplete(ElementId elementId)
{
if (elementId is QuestId questId)
return IsQuestComplete(questId);
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
[SuppressMessage("Performance", "CA1822")]
public bool IsQuestComplete(QuestId questId)
{
return QuestManager.IsQuestComplete(questId.Value);
}
public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
{
if (elementId is QuestId questId)
return IsQuestLocked(questId, extraCompletedQuest);
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null)
{
var questInfo = (QuestInfo) _questData.GetQuestInfo(questId);
if (questInfo.QuestLocks.Count > 0)
{
var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests)
return true;
else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
return true;
}
if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany())
return true;
return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
}
private bool HasCompletedPreviousQuests(QuestInfo questInfo, ElementId? extraCompletedQuest)
{
if (questInfo.PreviousQuests.Count == 0)
return true;
var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All &&
questInfo.PreviousQuests.Count == completedQuests)
return true;
else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
return true;
else
return false;
}
private static bool HasCompletedPreviousInstances(QuestInfo questInfo)
{
if (questInfo.PreviousInstanceContent.Count == 0)
return true;
var completedInstances = questInfo.PreviousInstanceContent.Count(x => UIState.IsInstanceContentCompleted(x));
if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.All &&
questInfo.PreviousInstanceContent.Count == completedInstances)
return true;
else if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.AtLeastOne && completedInstances > 0)
return true;
else
return false;
}
public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
{
subIndex = 0;
@ -383,7 +87,7 @@ internal sealed unsafe class GameFunctions
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
{
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
return IsQuestComplete(new QuestId(3672));
return _questFunctions.IsQuestComplete(new QuestId(3672));
return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
}
@ -431,7 +135,7 @@ internal sealed unsafe class GameFunctions
if (_configuration.Advanced.NeverFly)
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);
if (battleChara != null && battleChara->Mount.MountId == 198) // special quest amaro, not the normal one
@ -718,61 +422,18 @@ internal sealed unsafe class GameFunctions
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)
{
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;
}
if (a == null)
return b == null;
excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}";
}
if (b == null)
return false;
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
if (excelSheet == null)
{
_logger.LogError("Unknown excel sheet '{SheetName}'", excelSheetName);
return null;
}
return excelSheet.FirstOrDefault(x => x.Key == key)?.Value?.ToDalamudString().ToString();
}
public string? GetDialogueTextByRowId(string? excelSheet, uint rowId)
{
if (excelSheet == "GimmickYesNo")
{
var questRow = _dataManager.GetExcelSheet<GimmickYesNo>()!.GetRow(rowId);
return questRow?.Unknown0?.ToString();
}
else if (excelSheet == "Warp")
{
var questRow = _dataManager.GetExcelSheet<Warp>()!.GetRow(rowId);
return questRow?.Name?.ToString();
}
else if (excelSheet is "Addon")
{
var questRow = _dataManager.GetExcelSheet<Addon>()!.GetRow(rowId);
return questRow?.Text?.ToString();
}
else if (excelSheet is "EventPathMove")
{
var questRow = _dataManager.GetExcelSheet<EventPathMove>()!.GetRow(rowId);
return questRow?.Unknown10?.ToString();
}
else if (excelSheet is "ContentTalk" or null)
{
var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
return questRow?.Text?.ToString();
}
else
throw new ArgumentOutOfRangeException(nameof(excelSheet), $"Unsupported excel sheet {excelSheet}");
return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-');
}
public bool IsOccupied()
@ -792,15 +453,28 @@ internal sealed unsafe class GameFunctions
_condition[ConditionFlag.Jumping61] || _condition[ConditionFlag.Gathering42];
}
public bool IsOccupiedWithCustomDeliveryNpc(Quest? currentQuest)
{
// not a supply quest?
if (currentQuest is not { Info: SatisfactionSupplyInfo })
return false;
if (_targetManager.Target == null || _targetManager.Target.DataId != currentQuest.Info.IssuerDataId)
return false;
if (!AgentSatisfactionSupply.Instance()->IsAgentActive())
return false;
var flags = _condition.AsReadOnlySet();
return flags.Count == 2 &&
flags.Contains(ConditionFlag.NormalConditions) &&
flags.Contains(ConditionFlag.OccupiedInQuestEvent);
}
public bool IsLoadingScreenVisible()
{
return _gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) &&
LAddon.IsAddonReady(fade) &&
fade->IsVisible;
}
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">
<PropertyGroup>
<Version>2.1</Version>
<Version>2.2</Version>
<OutputPath>dist</OutputPath>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<Platforms>x64</Platforms>

View File

@ -17,6 +17,7 @@ using Questionable.Controller.Steps.Gathering;
using Questionable.Controller.Steps.Interactions;
using Questionable.Data;
using Questionable.External;
using Questionable.Functions;
using Questionable.Validation;
using Questionable.Validation.Validators;
using Questionable.Windows;
@ -47,50 +48,58 @@ public sealed class QuestionablePlugin : IDalamudPlugin
IContextMenu contextMenu)
{
ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(chatGui);
try
{
ServiceCollection serviceCollection = new();
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
.ClearProviders()
.AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..]));
serviceCollection.AddSingleton<IDalamudPlugin>(this);
serviceCollection.AddSingleton(pluginInterface);
serviceCollection.AddSingleton(clientState);
serviceCollection.AddSingleton(targetManager);
serviceCollection.AddSingleton(framework);
serviceCollection.AddSingleton(gameGui);
serviceCollection.AddSingleton(dataManager);
serviceCollection.AddSingleton(sigScanner);
serviceCollection.AddSingleton(objectTable);
serviceCollection.AddSingleton(pluginLog);
serviceCollection.AddSingleton(condition);
serviceCollection.AddSingleton(chatGui);
serviceCollection.AddSingleton(commandManager);
serviceCollection.AddSingleton(addonLifecycle);
serviceCollection.AddSingleton(keyState);
serviceCollection.AddSingleton(contextMenu);
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
ServiceCollection serviceCollection = new();
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
.ClearProviders()
.AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..]));
serviceCollection.AddSingleton<IDalamudPlugin>(this);
serviceCollection.AddSingleton(pluginInterface);
serviceCollection.AddSingleton(clientState);
serviceCollection.AddSingleton(targetManager);
serviceCollection.AddSingleton(framework);
serviceCollection.AddSingleton(gameGui);
serviceCollection.AddSingleton(dataManager);
serviceCollection.AddSingleton(sigScanner);
serviceCollection.AddSingleton(objectTable);
serviceCollection.AddSingleton(pluginLog);
serviceCollection.AddSingleton(condition);
serviceCollection.AddSingleton(chatGui);
serviceCollection.AddSingleton(commandManager);
serviceCollection.AddSingleton(addonLifecycle);
serviceCollection.AddSingleton(keyState);
serviceCollection.AddSingleton(contextMenu);
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
AddBasicFunctionsAndData(serviceCollection);
AddTaskFactories(serviceCollection);
AddControllers(serviceCollection);
AddWindows(serviceCollection);
AddQuestValidators(serviceCollection);
AddBasicFunctionsAndData(serviceCollection);
AddTaskFactories(serviceCollection);
AddControllers(serviceCollection);
AddWindows(serviceCollection);
AddQuestValidators(serviceCollection);
serviceCollection.AddSingleton<CommandHandler>();
serviceCollection.AddSingleton<DalamudInitializer>();
serviceCollection.AddSingleton<CommandHandler>();
serviceCollection.AddSingleton<DalamudInitializer>();
_serviceProvider = serviceCollection.BuildServiceProvider();
_serviceProvider.GetRequiredService<QuestRegistry>().Reload();
_serviceProvider.GetRequiredService<CommandHandler>();
_serviceProvider.GetRequiredService<ContextMenuController>();
_serviceProvider.GetRequiredService<DalamudInitializer>();
_serviceProvider = serviceCollection.BuildServiceProvider();
Initialize(_serviceProvider);
}
catch (Exception)
{
chatGui.PrintError("Unable to load plugin, check /xllog for details", "Questionable");
throw;
}
}
private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<ExcelFunctions>();
serviceCollection.AddSingleton<GameFunctions>();
serviceCollection.AddSingleton<ChatFunctions>();
serviceCollection.AddSingleton<QuestFunctions>();
serviceCollection.AddSingleton<AetherCurrentData>();
serviceCollection.AddSingleton<AetheryteData>();
serviceCollection.AddSingleton<GatheringData>();
@ -110,6 +119,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTransient<MoveToLandingLocation>();
serviceCollection.AddTransient<DoGather>();
serviceCollection.AddTransient<DoGatherCollectable>();
serviceCollection.AddTransient<SwitchClassJob>();
// task factories
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
@ -135,6 +145,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use, UseItem.UseOnPosition>();
serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>();
serviceCollection.AddTaskWithFactory<TurnInDelivery.Factory, TurnInDelivery.SatisfactionSupplyTurnIn>();
serviceCollection
.AddTaskWithFactory<SinglePlayerDuty.Factory, SinglePlayerDuty.DisableYesAlready,
SinglePlayerDuty.RestoreYesAlready>();
@ -192,10 +203,19 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<IQuestValidator, NextQuestValidator>();
serviceCollection.AddSingleton<IQuestValidator, CompletionFlagsValidator>();
serviceCollection.AddSingleton<IQuestValidator, AethernetShortcutValidator>();
serviceCollection.AddSingleton<IQuestValidator, DialogueChoiceValidator>();
serviceCollection.AddSingleton<JsonSchemaValidator>();
serviceCollection.AddSingleton<IQuestValidator>(sp => sp.GetRequiredService<JsonSchemaValidator>());
}
private static void Initialize(IServiceProvider serviceProvider)
{
serviceProvider.GetRequiredService<QuestRegistry>().Reload();
serviceProvider.GetRequiredService<CommandHandler>();
serviceProvider.GetRequiredService<ContextMenuController>();
serviceProvider.GetRequiredService<DalamudInitializer>();
}
public void Dispose()
{
_serviceProvider?.Dispose();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ internal sealed class CompletionFlagsValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.Id,
ElementId = quest.Id,
Sequence = (byte)sequence.Sequence,
Step = i,
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
{
QuestId = quest.Id,
ElementId = quest.Id,
Sequence = null,
Step = null,
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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,28 +4,29 @@ using Dalamud.Interface.Colors;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiNET;
using Questionable.Functions;
using Questionable.Model.Questing;
namespace Questionable.Windows;
internal sealed class UiUtils
{
private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly IDalamudPluginInterface _pluginInterface;
public UiUtils(GameFunctions gameFunctions, IDalamudPluginInterface pluginInterface)
public UiUtils(QuestFunctions questFunctions, IDalamudPluginInterface pluginInterface)
{
_gameFunctions = gameFunctions;
_questFunctions = questFunctions;
_pluginInterface = pluginInterface;
}
public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId questElementId)
public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId elementId)
{
if (_gameFunctions.IsQuestAccepted(questElementId))
if (_questFunctions.IsQuestAccepted(elementId))
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active");
else if (_gameFunctions.IsQuestAcceptedOrComplete(questElementId))
else if (_questFunctions.IsQuestAcceptedOrComplete(elementId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_gameFunctions.IsQuestLocked(questElementId))
else if (_questFunctions.IsQuestLocked(elementId))
return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked");
else
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available");