diff --git a/LLib/LLib.Shop.Model/GrandCompanyItem.cs b/LLib/LLib.Shop.Model/GrandCompanyItem.cs new file mode 100644 index 0000000..24ee7e6 --- /dev/null +++ b/LLib/LLib.Shop.Model/GrandCompanyItem.cs @@ -0,0 +1,12 @@ +namespace LLib.Shop.Model; + +public sealed class GrandCompanyItem +{ + public required int Index { get; init; } + + public required uint ItemId { get; init; } + + public required uint IconId { get; init; } + + public required uint SealCost { get; init; } +} diff --git a/LLib/LLib.Shop/GrandCompanyShop.cs b/LLib/LLib.Shop/GrandCompanyShop.cs new file mode 100644 index 0000000..a4cea2c --- /dev/null +++ b/LLib/LLib.Shop/GrandCompanyShop.cs @@ -0,0 +1,288 @@ +using System; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using LLib.GameUI; +using LLib.Shop.Model; + +namespace LLib.Shop; + +public sealed class GrandCompanyShop : IDisposable +{ + private sealed class GrandCompanyPurchaseRequest + { + public required uint ItemId { get; init; } + + public required int RankTabIndex { get; init; } + + public required int CategoryTabIndex { get; init; } + } + + private const string AddonName = "GrandCompanyExchange"; + + private const string SelectYesNoAddonName = "SelectYesno"; + + private readonly IPluginLog _pluginLog; + + private readonly IGameGui _gameGui; + + private readonly IAddonLifecycle _addonLifecycle; + + private int _navigationStep; + + private DateTime _lastActionTime = DateTime.MinValue; + + private GrandCompanyPurchaseRequest? _pendingPurchase; + + public bool IsOpen { get; private set; } + + public bool IsPurchaseInProgress => _pendingPurchase != null; + + public bool IsAwaitingConfirmation { get; private set; } + + public event EventHandler? PurchaseCompleted; + + public event EventHandler? ShopOpened; + + public event EventHandler? ShopClosed; + + public GrandCompanyShop(IPluginLog pluginLog, IGameGui gameGui, IAddonLifecycle addonLifecycle) + { + _pluginLog = pluginLog; + _gameGui = gameGui; + _addonLifecycle = addonLifecycle; + _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GrandCompanyExchange", OnShopPostSetup); + _addonLifecycle.RegisterListener(AddonEvent.PreFinalize, "GrandCompanyExchange", OnShopPreFinalize); + _addonLifecycle.RegisterListener(AddonEvent.PostUpdate, "SelectYesno", OnSelectYesNoPostUpdate); + } + + private void OnShopPostSetup(AddonEvent type, AddonArgs args) + { + IsOpen = true; + _navigationStep = 0; + _pluginLog.Debug("[GCShop] Shop opened"); + this.ShopOpened?.Invoke(this, EventArgs.Empty); + } + + private void OnShopPreFinalize(AddonEvent type, AddonArgs args) + { + IsOpen = false; + _pendingPurchase = null; + IsAwaitingConfirmation = false; + _pluginLog.Debug("[GCShop] Shop closed"); + this.ShopClosed?.Invoke(this, EventArgs.Empty); + } + + private unsafe void OnSelectYesNoPostUpdate(AddonEvent type, AddonArgs args) + { + if (_pendingPurchase != null && IsAwaitingConfirmation) + { + AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address; + if (LAddon.IsAddonReady(address)) + { + _pluginLog.Information("[GCShop] Confirming purchase dialog"); + ClickYes(address); + address->Close(fireCallback: true); + IsAwaitingConfirmation = false; + } + } + } + + public bool StartPurchase(uint itemId, int rankTabIndex, int categoryTabIndex) + { + if (!IsOpen || _pendingPurchase != null) + { + return false; + } + _pendingPurchase = new GrandCompanyPurchaseRequest + { + ItemId = itemId, + RankTabIndex = rankTabIndex, + CategoryTabIndex = categoryTabIndex + }; + _navigationStep = 0; + _lastActionTime = DateTime.MinValue; + _pluginLog.Information($"[GCShop] Starting purchase of item {itemId}"); + return true; + } + + public void CancelPurchase() + { + _pendingPurchase = null; + IsAwaitingConfirmation = false; + _navigationStep = 0; + } + + public unsafe bool ProcessPurchase() + { + if (_pendingPurchase == null || !IsOpen) + { + return false; + } + if ((DateTime.Now - _lastActionTime).TotalMilliseconds < 600.0) + { + return false; + } + if (InventoryManager.Instance()->GetInventoryItemCount(_pendingPurchase.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0) + { + _pluginLog.Information($"[GCShop] Item {_pendingPurchase.ItemId} already in inventory"); + uint itemId = _pendingPurchase.ItemId; + _pendingPurchase = null; + this.PurchaseCompleted?.Invoke(this, new PurchaseCompletedEventArgs(itemId, success: true)); + return true; + } + if (!_gameGui.TryGetAddonByName("GrandCompanyExchange", out var addonPtr) || !LAddon.IsAddonReady(addonPtr)) + { + return false; + } + switch (_navigationStep) + { + case 0: + FireCallback(addonPtr, 1, _pendingPurchase.RankTabIndex); + _navigationStep++; + _lastActionTime = DateTime.Now; + break; + case 1: + FireCallback(addonPtr, 2, _pendingPurchase.CategoryTabIndex); + _navigationStep++; + _lastActionTime = DateTime.Now; + break; + case 2: + { + GrandCompanyItem grandCompanyItem = FindItemInShop(addonPtr, _pendingPurchase.ItemId); + if (grandCompanyItem != null) + { + _pluginLog.Information($"[GCShop] Found item {grandCompanyItem.ItemId} at index {grandCompanyItem.Index}, purchasing..."); + FirePurchaseCallback(addonPtr, grandCompanyItem); + IsAwaitingConfirmation = true; + _navigationStep++; + _lastActionTime = DateTime.Now; + } + else + { + _pluginLog.Warning($"[GCShop] Item {_pendingPurchase.ItemId} not found in shop"); + _lastActionTime = DateTime.Now; + } + break; + } + case 3: + if (!IsAwaitingConfirmation && InventoryManager.Instance()->GetInventoryItemCount(_pendingPurchase.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0) + { + _pluginLog.Information($"[GCShop] Purchase of {_pendingPurchase.ItemId} completed"); + uint itemId2 = _pendingPurchase.ItemId; + _pendingPurchase = null; + _navigationStep = 0; + this.PurchaseCompleted?.Invoke(this, new PurchaseCompletedEventArgs(itemId2, success: true)); + return true; + } + break; + } + return false; + } + + public unsafe void CloseShop() + { + if (IsOpen && _gameGui.TryGetAddonByName("GrandCompanyExchange", out var addonPtr)) + { + addonPtr->Close(fireCallback: true); + } + } + + public unsafe static int GetCompanySeals() + { + return (int)InventoryManager.Instance()->GetCompanySeals(PlayerState.Instance()->GrandCompany); + } + + private unsafe static GrandCompanyItem? FindItemInShop(AtkUnitBase* addon, uint targetItemId) + { + int atkValueInt = GetAtkValueInt(addon, 1); + if (atkValueInt <= 0) + { + return null; + } + for (int i = 0; i < atkValueInt; i++) + { + int atkValueInt2 = GetAtkValueInt(addon, 317 + i); + if (atkValueInt2 == (int)targetItemId) + { + return new GrandCompanyItem + { + Index = i, + ItemId = (uint)atkValueInt2, + IconId = (uint)GetAtkValueInt(addon, 167 + i), + SealCost = (uint)GetAtkValueInt(addon, 67 + i) + }; + } + } + return null; + } + + private unsafe void FireCallback(AtkUnitBase* addon, params int[] args) + { + AtkValue* ptr = stackalloc AtkValue[args.Length]; + for (int i = 0; i < args.Length; i++) + { + ptr[i].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr[i].Int = args[i]; + } + addon->FireCallback((uint)args.Length, ptr); + } + + private unsafe void FirePurchaseCallback(AtkUnitBase* addon, GrandCompanyItem item) + { + AtkValue* ptr = stackalloc AtkValue[9]; + ptr->Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr->Int = 0; + ptr[1].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr[1].Int = item.Index; + ptr[2].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr[2].Int = 1; + ptr[3].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr[3].Int = 0; + ptr[4].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr[4].Int = 0; + ptr[5].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr[5].Int = 0; + ptr[6].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt; + ptr[6].UInt = item.ItemId; + ptr[7].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt; + ptr[7].UInt = item.IconId; + ptr[8].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt; + ptr[8].UInt = item.SealCost; + addon->FireCallback(9u, ptr); + } + + private unsafe static void ClickYes(AtkUnitBase* addon) + { + AtkValue* ptr = stackalloc AtkValue[1]; + ptr->Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr->Int = 0; + addon->FireCallback(1u, ptr); + } + + private unsafe static int GetAtkValueInt(AtkUnitBase* addon, int index) + { + if (addon == null || addon->AtkValues == null || index < 0 || index >= addon->AtkValuesCount) + { + return -1; + } + AtkValue atkValue = addon->AtkValues[index]; + return atkValue.Type switch + { + FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int => atkValue.Int, + FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt => (int)atkValue.UInt, + FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Bool => (atkValue.Byte != 0) ? 1 : 0, + _ => -1, + }; + } + + public void Dispose() + { + _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GrandCompanyExchange", OnShopPostSetup); + _addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "GrandCompanyExchange", OnShopPreFinalize); + _addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "SelectYesno", OnSelectYesNoPostUpdate); + } +} diff --git a/LLib/LLib.Shop/PurchaseCompletedEventArgs.cs b/LLib/LLib.Shop/PurchaseCompletedEventArgs.cs new file mode 100644 index 0000000..fe57d42 --- /dev/null +++ b/LLib/LLib.Shop/PurchaseCompletedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace LLib.Shop; + +public sealed class PurchaseCompletedEventArgs : EventArgs +{ + public uint ItemId { get; } + + public bool Success { get; } + + public PurchaseCompletedEventArgs(uint itemId, bool success) + { + ItemId = itemId; + Success = success; + } +} diff --git a/QuestPaths/Questionable.QuestPaths.QuestSchema b/QuestPaths/Questionable.QuestPaths.QuestSchema index 9cb9d6e..dc4d130 100644 --- a/QuestPaths/Questionable.QuestPaths.QuestSchema +++ b/QuestPaths/Questionable.QuestPaths.QuestSchema @@ -584,6 +584,26 @@ "properties": { "PickUpItemId": { "type": "number" + }, + "GCPurchase": { + "type": "object", + "description": "Configuration for purchasing an item from the Grand Company Exchange shop", + "properties": { + "ItemId": { + "type": "number", + "description": "The item ID to purchase from the GC shop" + }, + "RankTab": { + "type": "integer", + "description": "The rank tab index (0-based)" + }, + "CategoryTab": { + "type": "integer", + "description": "The category tab index (0-based)" + } + }, + "required": ["ItemId"], + "additionalProperties": false } }, "required": [ diff --git a/QuestPaths/Questionable.QuestPaths/AssemblyQuestLoader.cs b/QuestPaths/Questionable.QuestPaths/AssemblyQuestLoader.cs index 06d02ee..6c57ea3 100644 --- a/QuestPaths/Questionable.QuestPaths/AssemblyQuestLoader.cs +++ b/QuestPaths/Questionable.QuestPaths/AssemblyQuestLoader.cs @@ -54759,9 +54759,22 @@ public static class AssemblyQuestLoader CollectionsMarshal.SetCount(list4, index2); Span span4 = CollectionsMarshal.AsSpan(list4); num2 = 0; - span4[num2] = new QuestStep(EInteractionType.Instruction, 1002394u, new Vector3(-69.5354f, -0.50095016f, -9.99469f), 132) + span4[num2] = new QuestStep(EInteractionType.Interact, 1002393u, new Vector3(-67.94849f, -0.5009766f, -10.818665f), 132) { - Comment = "Buy Serpent Chocobo Issuance" + StopDistance = 3f, + Comment = "Buy Serpent Chocobo Issuance", + SkipConditions = new SkipConditions + { + StepIf = new SkipStepConditions + { + Never = true + } + }, + GCPurchase = new GCPurchaseInfo + { + ItemId = 6018u, + CategoryTab = 1 + } }; num2++; span4[num2] = new QuestStep(EInteractionType.Interact, 1000136u, new Vector3(32.303345f, -0.050583772f, 70.29822f), 132); @@ -54841,14 +54854,29 @@ public static class AssemblyQuestLoader { Sequence = 1 }; - index2 = 3; + index2 = 4; List list10 = new List(index2); CollectionsMarshal.SetCount(list10, index2); Span span10 = CollectionsMarshal.AsSpan(list10); num2 = 0; - span10[num2] = new QuestStep(EInteractionType.Instruction, 1002387u, new Vector3(95.750244f, 40.250286f, 76.67651f), 128) + span10[num2] = new QuestStep(EInteractionType.WalkTo, null, new Vector3(93.22066f, 40.275368f, 76.687836f), 128); + num2++; + span10[num2] = new QuestStep(EInteractionType.Interact, 1002387u, new Vector3(95.750244f, 40.250286f, 76.67651f), 128) { - Comment = "Buy Storm Chocobo Issuance" + StopDistance = 3f, + Comment = "Buy Storm Chocobo Issuance", + SkipConditions = new SkipConditions + { + StepIf = new SkipStepConditions + { + Never = true + } + }, + GCPurchase = new GCPurchaseInfo + { + ItemId = 6017u, + CategoryTab = 1 + } }; num2++; ref QuestStep reference7 = ref span10[num2]; @@ -54949,9 +54977,22 @@ public static class AssemblyQuestLoader CollectionsMarshal.SetCount(list17, index2); Span span17 = CollectionsMarshal.AsSpan(list17); num2 = 0; - span17[num2] = new QuestStep(EInteractionType.Instruction, 1002390u, new Vector3(-143.45007f, 4.5499268f, -108.537415f), 130) + span17[num2] = new QuestStep(EInteractionType.Interact, 1002390u, new Vector3(-143.45007f, 4.5499268f, -108.537415f), 130) { - Comment = "Buy Flame Chocobo Issuance" + StopDistance = 3f, + Comment = "Buy Flame Chocobo Issuance", + SkipConditions = new SkipConditions + { + StepIf = new SkipStepConditions + { + Never = true + } + }, + GCPurchase = new GCPurchaseInfo + { + ItemId = 6019u, + CategoryTab = 1 + } }; num2++; span17[num2] = new QuestStep(EInteractionType.Interact, 1001978u, new Vector3(55.344482f, 4.124078f, -143.9079f), 130); @@ -474393,6 +474434,7 @@ public static class AssemblyQuestLoader { DutyOptions = new DutyOptions { + Enabled = true, ContentFinderConditionId = 1064u } }; diff --git a/Questionable.Model/Questionable.Model.Questing/GCPurchaseInfo.cs b/Questionable.Model/Questionable.Model.Questing/GCPurchaseInfo.cs new file mode 100644 index 0000000..44de4af --- /dev/null +++ b/Questionable.Model/Questionable.Model.Questing/GCPurchaseInfo.cs @@ -0,0 +1,10 @@ +namespace Questionable.Model.Questing; + +public sealed class GCPurchaseInfo +{ + public uint ItemId { get; set; } + + public int RankTab { get; set; } + + public int CategoryTab { get; set; } +} diff --git a/Questionable.Model/Questionable.Model.Questing/QuestStep.cs b/Questionable.Model/Questionable.Model.Questing/QuestStep.cs index 0cdabca..c666775 100644 --- a/Questionable.Model/Questionable.Model.Questing/QuestStep.cs +++ b/Questionable.Model/Questionable.Model.Questing/QuestStep.cs @@ -118,6 +118,8 @@ public sealed class QuestStep public PurchaseMenu? PurchaseMenu { get; set; } + public GCPurchaseInfo? GCPurchase { get; set; } + [JsonConverter(typeof(ElementIdConverter))] public ElementId? PickUpQuestId { get; set; } diff --git a/Questionable/Questionable.Controller.DebugCommands/SequencesCommandHandler.cs b/Questionable/Questionable.Controller.DebugCommands/SequencesCommandHandler.cs index 330300c..3e3c9f6 100644 --- a/Questionable/Questionable.Controller.DebugCommands/SequencesCommandHandler.cs +++ b/Questionable/Questionable.Controller.DebugCommands/SequencesCommandHandler.cs @@ -1,3 +1,4 @@ +using Questionable.Model.Questing; using Questionable.Windows; namespace Questionable.Controller.DebugCommands; @@ -15,6 +16,13 @@ internal sealed class SequencesCommandHandler : IDebugCommandHandler public void Execute(string[] arguments) { - _questSequenceWindow.ToggleOrUncollapse(); + if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId elementId) && elementId != null) + { + _questSequenceWindow.OpenForQuest(elementId); + } + else + { + _questSequenceWindow.ToggleOrUncollapse(); + } } } diff --git a/Questionable/Questionable.Controller.GameUi/AutoSnipeHandler.cs b/Questionable/Questionable.Controller.GameUi/AutoSnipeHandler.cs new file mode 100644 index 0000000..47d8a1a --- /dev/null +++ b/Questionable/Questionable.Controller.GameUi/AutoSnipeHandler.cs @@ -0,0 +1,63 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Event; +using FFXIVClientStructs.FFXIV.Common.Lua; +using Microsoft.Extensions.Logging; + +namespace Questionable.Controller.GameUi; + +internal sealed class AutoSnipeHandler : IDisposable +{ + private unsafe delegate ulong EnqueueSnipeTaskDelegate(EventSceneModuleImplBase* scene, lua_State* state); + + private readonly QuestController _questController; + + private readonly Configuration _configuration; + + private readonly ILogger _logger; + + private readonly Hook? _enqueueSnipeTaskHook; + + public unsafe AutoSnipeHandler(QuestController questController, Configuration configuration, IGameInteropProvider gameInteropProvider, ILogger logger) + { + _questController = questController; + _configuration = configuration; + _logger = logger; + try + { + _enqueueSnipeTaskHook = gameInteropProvider.HookFromSignature("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 50 48 8B F9 48 8D 4C 24 ??", EnqueueSnipeTask); + _enqueueSnipeTaskHook.Enable(); + _logger.LogInformation("AutoSnipeHandler enabled"); + } + catch (Exception exception) + { + _logger.LogError(exception, "Failed to initialize AutoSnipeHandler"); + _enqueueSnipeTaskHook = null; + } + } + + private unsafe ulong EnqueueSnipeTask(EventSceneModuleImplBase* scene, lua_State* state) + { + if (_configuration.General.AutoSnipe && _questController.IsRunning) + { + _logger.LogDebug("Auto-completing snipe task"); + TValue* top = state->top; + top->tt = 3; + top->value.n = 1.0; + state->top++; + return 1uL; + } + return _enqueueSnipeTaskHook.Original(scene, state); + } + + public void Dispose() + { + if (_enqueueSnipeTaskHook != null) + { + _enqueueSnipeTaskHook.Disable(); + _enqueueSnipeTaskHook.Dispose(); + _logger.LogInformation("AutoSnipeHandler disabled"); + } + } +} diff --git a/Questionable/Questionable.Controller.GameUi/ChocoboNameHandler.cs b/Questionable/Questionable.Controller.GameUi/ChocoboNameHandler.cs new file mode 100644 index 0000000..621d8c0 --- /dev/null +++ b/Questionable/Questionable.Controller.GameUi/ChocoboNameHandler.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; +using LLib.GameUI; +using Microsoft.Extensions.Logging; +using Questionable.Model.Questing; + +namespace Questionable.Controller.GameUi; + +internal sealed class ChocoboNameHandler : IDisposable +{ + private static readonly HashSet ChocoboQuestIds = new HashSet + { + new QuestId(700), + new QuestId(701), + new QuestId(702) + }; + + private readonly QuestController _questController; + + private readonly Configuration _configuration; + + private readonly IAddonLifecycle _addonLifecycle; + + private readonly ILogger _logger; + + private bool _awaitingConfirmation; + + public ChocoboNameHandler(QuestController questController, Configuration configuration, IAddonLifecycle addonLifecycle, ILogger logger) + { + _questController = questController; + _configuration = configuration; + _addonLifecycle = addonLifecycle; + _logger = logger; + _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "InputString", OnInputStringSetup); + _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", OnSelectYesNoSetup); + } + + public void Dispose() + { + _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", OnSelectYesNoSetup); + _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "InputString", OnInputStringSetup); + } + + private bool IsChocoboQuestActive() + { + QuestController.QuestProgress currentQuest = _questController.CurrentQuest; + if (currentQuest != null) + { + return ChocoboQuestIds.Contains(currentQuest.Quest.Id); + } + return false; + } + + private unsafe void OnInputStringSetup(AddonEvent type, AddonArgs args) + { + if (_questController.IsRunning && IsChocoboQuestActive()) + { + AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address; + if (LAddon.IsAddonReady(address)) + { + string validatedChocoboName = GetValidatedChocoboName(_configuration.Advanced.ChocoboName); + _logger.LogInformation("Entering chocobo name: {Name}", validatedChocoboName); + FireInputStringCallback(address, validatedChocoboName); + _awaitingConfirmation = true; + } + } + } + + private unsafe void OnSelectYesNoSetup(AddonEvent type, AddonArgs args) + { + if (_questController.IsRunning && _awaitingConfirmation) + { + AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address; + if (LAddon.IsAddonReady(address)) + { + _logger.LogInformation("Confirming chocobo name"); + ClickYes(address); + _awaitingConfirmation = false; + } + } + } + + private static string GetValidatedChocoboName(string? configuredName) + { + if (string.IsNullOrWhiteSpace(configuredName)) + { + return "Kweh"; + } + string text = configuredName.Trim(); + if (text.Length < 2 || text.Length > 20) + { + return "Kweh"; + } + string text2 = text.Substring(0, 1).ToUpperInvariant(); + string text3; + if (text.Length <= 1) + { + text3 = string.Empty; + } + else + { + string text4 = text; + text3 = text4.Substring(1, text4.Length - 1).ToLowerInvariant(); + } + return text2 + text3; + } + + private unsafe void FireInputStringCallback(AtkUnitBase* addon, string text) + { + AtkComponentNode* componentNodeById = addon->GetComponentNodeById(9u); + if (componentNodeById == null) + { + _logger.LogWarning("Could not find text input node in InputString addon"); + return; + } + AtkComponentTextInput* asAtkComponentTextInput = componentNodeById->GetAsAtkComponentTextInput(); + if (asAtkComponentTextInput == null) + { + _logger.LogWarning("Node is not a text input component"); + return; + } + asAtkComponentTextInput->SetText(text); + AtkValue* ptr = stackalloc AtkValue[2]; + ptr->Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr->Int = 0; + byte[] bytes = Encoding.UTF8.GetBytes(text); + nint num = Marshal.AllocHGlobal(bytes.Length + 1); + Marshal.Copy(bytes, 0, num, bytes.Length); + Marshal.WriteByte(num + bytes.Length, 0); + ptr[1].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String; + ptr[1].String = (byte*)num; + addon->FireCallback(2u, ptr); + Marshal.FreeHGlobal(num); + } + + private unsafe static void ClickYes(AtkUnitBase* addon) + { + AtkValue* ptr = stackalloc AtkValue[1]; + ptr->Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int; + ptr->Int = 0; + addon->FireCallback(1u, ptr); + } +} diff --git a/Questionable/Questionable.Controller.GameUi/GCShopHandler.cs b/Questionable/Questionable.Controller.GameUi/GCShopHandler.cs new file mode 100644 index 0000000..e88a530 --- /dev/null +++ b/Questionable/Questionable.Controller.GameUi/GCShopHandler.cs @@ -0,0 +1,152 @@ +using System; +using Dalamud.Plugin.Services; +using LLib.Shop; +using Microsoft.Extensions.Logging; +using Questionable.Model.Questing; + +namespace Questionable.Controller.GameUi; + +internal sealed class GCShopHandler : IDisposable +{ + private enum GCShopState + { + Inactive, + Purchasing, + Completed + } + + private readonly QuestController _questController; + + private readonly GrandCompanyShop _gcShop; + + private readonly IObjectTable _objectTable; + + private readonly IFramework _framework; + + private readonly ILogger _logger; + + private GCShopState _state; + + private GCPurchaseInfo? _currentPurchase; + + private DateTime _lastActionTime = DateTime.MinValue; + + private bool _purchaseCompletedThisStep; + + public bool IsActive => _state != GCShopState.Inactive; + + public GCShopHandler(QuestController questController, GrandCompanyShop gcShop, IObjectTable objectTable, IFramework framework, ILogger logger) + { + _questController = questController; + _gcShop = gcShop; + _objectTable = objectTable; + _framework = framework; + _logger = logger; + _gcShop.PurchaseCompleted += OnPurchaseCompleted; + _framework.Update += OnFrameworkUpdate; + } + + public void Dispose() + { + _framework.Update -= OnFrameworkUpdate; + _gcShop.PurchaseCompleted -= OnPurchaseCompleted; + } + + private void OnFrameworkUpdate(IFramework framework) + { + if (_objectTable.LocalPlayer == null) + { + return; + } + if (!_questController.IsRunning) + { + if (_state != GCShopState.Inactive) + { + Reset(); + } + return; + } + CheckForGCPurchase(); + if (_state == GCShopState.Purchasing) + { + HandlePurchasing(); + } + else if (_state == GCShopState.Completed) + { + _logger.LogInformation("GC shop purchase completed!"); + _state = GCShopState.Inactive; + _lastActionTime = DateTime.MinValue; + } + } + + private void CheckForGCPurchase() + { + QuestController.QuestProgress currentQuest = _questController.CurrentQuest; + if (currentQuest == null) + { + if (_currentPurchase != null) + { + Reset(); + } + return; + } + GCPurchaseInfo gCPurchaseInfo = (currentQuest.Quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step))?.GCPurchase; + if (gCPurchaseInfo == null) + { + if (_currentPurchase != null) + { + _currentPurchase = null; + _purchaseCompletedThisStep = false; + } + if (_gcShop.IsOpen) + { + _logger.LogDebug("GC shop is open but current step has no GCPurchase. Quest={QuestId}, Seq={Sequence}, Step={Step}", currentQuest.Quest.Id, currentQuest.Sequence, currentQuest.Step); + } + } + else + { + if (_currentPurchase != gCPurchaseInfo) + { + _currentPurchase = gCPurchaseInfo; + _purchaseCompletedThisStep = false; + } + if (_gcShop.IsOpen && _state == GCShopState.Inactive && !_purchaseCompletedThisStep) + { + _logger.LogInformation("GC shop is open. Starting purchase of item {ItemId}.", gCPurchaseInfo.ItemId); + _state = GCShopState.Purchasing; + } + } + } + + private void HandlePurchasing() + { + if (_gcShop.IsOpen && _currentPurchase != null && !((DateTime.Now - _lastActionTime).TotalMilliseconds < 500.0)) + { + if (!_gcShop.IsPurchaseInProgress) + { + _gcShop.StartPurchase(_currentPurchase.ItemId, _currentPurchase.RankTab, _currentPurchase.CategoryTab); + _lastActionTime = DateTime.Now; + } + _gcShop.ProcessPurchase(); + } + } + + private void OnPurchaseCompleted(object? sender, PurchaseCompletedEventArgs e) + { + if (_state == GCShopState.Purchasing && _currentPurchase != null && _questController.IsRunning && e.Success && e.ItemId == _currentPurchase.ItemId) + { + _logger.LogInformation("GC purchase of item {ItemId} completed successfully", e.ItemId); + _gcShop.CloseShop(); + _purchaseCompletedThisStep = true; + _state = GCShopState.Completed; + } + } + + private void Reset() + { + _state = GCShopState.Inactive; + _currentPurchase = null; + _lastActionTime = DateTime.MinValue; + _purchaseCompletedThisStep = false; + } +} diff --git a/Questionable/Questionable.Controller.GameUi/QteController.cs b/Questionable/Questionable.Controller.GameUi/QteController.cs new file mode 100644 index 0000000..6bc45b0 --- /dev/null +++ b/Questionable/Questionable.Controller.GameUi/QteController.cs @@ -0,0 +1,113 @@ +using System; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; +using FFXIVClientStructs.Interop; +using LLib.GameUI; +using Microsoft.Extensions.Logging; +using Questionable.Controller.Utils; + +namespace Questionable.Controller.GameUi; + +internal sealed class QteController : IDisposable +{ + private readonly Configuration _configuration; + + private readonly IGameGui _gameGui; + + private readonly IGameConfig _gameConfig; + + private readonly IFramework _framework; + + private readonly ILogger _logger; + + private long _throttler = Environment.TickCount64; + + private readonly Random _random = new Random(); + + private bool _hasDirectChat; + + private bool _directChatWasEnabled; + + public QteController(Configuration configuration, IGameGui gameGui, IGameConfig gameConfig, IFramework framework, ILogger logger) + { + _configuration = configuration; + _gameGui = gameGui; + _gameConfig = gameConfig; + _framework = framework; + _logger = logger; + _framework.Update += OnUpdate; + } + + private unsafe void OnUpdate(IFramework framework) + { + AtkUnitBase* addonPtr; + if (!_configuration.General.AutoSolveQte) + { + EnableDirectChatIfNeeded(); + } + else if (_gameGui.TryGetAddonByName("QTE", out addonPtr) && LAddon.IsAddonReady(addonPtr) && addonPtr->IsVisible) + { + DisableDirectChatIfNeeded(); + if (Environment.TickCount64 >= _throttler) + { + if (IsChatLogFocused()) + { + WindowsKeypress.SendKeypress(WindowsKeypress.LimitedKeys.Escape); + } + WindowsKeypress.SendKeypress(WindowsKeypress.LimitedKeys.A); + _throttler = Environment.TickCount64 + _random.Next(25, 50); + } + } + else + { + EnableDirectChatIfNeeded(); + } + } + + private unsafe bool IsChatLogFocused() + { + AtkStage* ptr = AtkStage.Instance(); + if (ptr == null) + { + return false; + } + Span> entries = ptr->RaptureAtkUnitManager->AtkUnitManager.FocusedUnitsList.Entries; + for (int i = 0; i < entries.Length; i++) + { + Pointer pointer = entries[i]; + if (pointer.Value != null && pointer.Value->NameString == "ChatLog") + { + return true; + } + } + return false; + } + + private void EnableDirectChatIfNeeded() + { + if (_hasDirectChat) + { + _gameConfig.UiControl.Set("DirectChat", _directChatWasEnabled); + _hasDirectChat = false; + } + } + + private void DisableDirectChatIfNeeded() + { + if (!_hasDirectChat && _gameConfig.UiControl.TryGetBool("DirectChat", out var value)) + { + _directChatWasEnabled = value; + if (value) + { + _gameConfig.UiControl.Set("DirectChat", value: false); + } + _hasDirectChat = true; + } + } + + public void Dispose() + { + _framework.Update -= OnUpdate; + EnableDirectChatIfNeeded(); + } +} diff --git a/Questionable/Questionable.Controller.Steps.Common/SendNotification.cs b/Questionable/Questionable.Controller.Steps.Common/SendNotification.cs index 52748df..363fc0f 100644 --- a/Questionable/Questionable.Controller.Steps.Common/SendNotification.cs +++ b/Questionable/Questionable.Controller.Steps.Common/SendNotification.cs @@ -10,14 +10,14 @@ namespace Questionable.Controller.Steps.Common; internal static class SendNotification { - internal sealed class Factory(AutomatonIpc automatonIpc, AutoDutyIpc autoDutyIpc, BossModIpc bossModIpc, TerritoryData territoryData) : SimpleTaskFactory() + internal sealed class Factory(Configuration configuration, AutoDutyIpc autoDutyIpc, BossModIpc bossModIpc, TerritoryData territoryData) : SimpleTaskFactory() { public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) { switch (step.InteractionType) { case EInteractionType.Snipe: - if (!automatonIpc.IsAutoSnipeEnabled) + if (!configuration.General.AutoSnipe) { return new Task(step.InteractionType, step.Comment); } diff --git a/Questionable/Questionable.Controller.Steps.Interactions/Dive.cs b/Questionable/Questionable.Controller.Steps.Interactions/Dive.cs index 0b6840b..12aec3f 100644 --- a/Questionable/Questionable.Controller.Steps.Interactions/Dive.cs +++ b/Questionable/Questionable.Controller.Steps.Interactions/Dive.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Input; using FFXIVClientStructs.FFXIV.Client.UI; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; +using Questionable.Controller.Utils; using Questionable.Model; using Questionable.Model.Questing; @@ -67,7 +68,7 @@ internal static class Dive return ETaskResult.StillRunning; } logger.LogDebug("{Action} key {KeyCode:X2}", (result.Item1 == 256) ? "Pressing" : "Releasing", result.Item2); - NativeMethods.SendMessage((nint)Device.Instance()->hWnd, result.Item1, result.Item2, IntPtr.Zero); + WindowsKeypress.SendMessage((nint)Device.Instance()->hWnd, result.Item1, result.Item2); if (result.Item1 == 256) { _pressedKeys.Add(result.Item2); @@ -86,7 +87,7 @@ internal static class Dive foreach (nint pressedKey in _pressedKeys) { logger.LogDebug("Releasing stuck key {KeyCode:X2} on stop", pressedKey); - NativeMethods.SendMessage((nint)Device.Instance()->hWnd, 257u, pressedKey, IntPtr.Zero); + WindowsKeypress.SendMessage((nint)Device.Instance()->hWnd, 257u, pressedKey); } _pressedKeys.Clear(); _keysToPress.Clear(); @@ -160,17 +161,6 @@ internal static class Dive } } - private static class NativeMethods - { - public const uint WM_KEYUP = 257u; - - public const uint WM_KEYDOWN = 256u; - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - public static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, nint lParam); - } - private static List? GetKeysToPress(SeVirtualKey key, KeyModifierFlag modifier) { List list = new List(); diff --git a/Questionable/Questionable.Controller.Steps.Interactions/Duty.cs b/Questionable/Questionable.Controller.Steps.Interactions/Duty.cs index a04b0b5..afa27f0 100644 --- a/Questionable/Questionable.Controller.Steps.Interactions/Duty.cs +++ b/Questionable/Questionable.Controller.Steps.Interactions/Duty.cs @@ -135,25 +135,28 @@ internal static class Duty { throw new TaskException("Failed to get territory ID for content finder condition"); } - InventoryManager* intPtr = InventoryManager.Instance(); - if (intPtr == null) + if (base.Task.DutyMode != AutoDutyIpc.DutyMode.UnsyncRegular) { - throw new TaskException("Inventory unavailable"); - } - InventoryContainer* inventoryContainer = intPtr->GetInventoryContainer(InventoryType.EquippedItems); - if (inventoryContainer == null) - { - throw new TaskException("Equipped items unavailable"); - } - short num = gearStatsCalculator.CalculateAverageItemLevel(inventoryContainer); - if (contentFinderConditionData.RequiredItemLevel > num) - { - string text = $"Could not use AutoDuty to queue for {contentFinderConditionData.Name}, required item level: {contentFinderConditionData.RequiredItemLevel}, current item level: {num}."; - if (!sendNotificationExecutor.Start(new SendNotification.Task(EInteractionType.Duty, text))) + InventoryManager* intPtr = InventoryManager.Instance(); + if (intPtr == null) { - chatGui.PrintError(text, "Questionable", 576); + throw new TaskException("Inventory unavailable"); + } + InventoryContainer* inventoryContainer = intPtr->GetInventoryContainer(InventoryType.EquippedItems); + if (inventoryContainer == null) + { + throw new TaskException("Equipped items unavailable"); + } + short num = gearStatsCalculator.CalculateAverageItemLevel(inventoryContainer); + if (contentFinderConditionData.RequiredItemLevel > num) + { + string text = $"Could not use AutoDuty to queue for {contentFinderConditionData.Name}, required item level: {contentFinderConditionData.RequiredItemLevel}, current item level: {num}."; + if (!sendNotificationExecutor.Start(new SendNotification.Task(EInteractionType.Duty, text))) + { + chatGui.PrintError(text, "Questionable", 576); + } + return false; } - return false; } autoDutyIpc.StartInstance(base.Task.ContentFinderConditionId, base.Task.DutyMode); return true; diff --git a/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs b/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs index defd9ac..30aa0f6 100644 --- a/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs +++ b/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs @@ -11,7 +11,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; -using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; @@ -20,7 +19,7 @@ namespace Questionable.Controller.Steps.Interactions; internal static class Interact { - internal sealed class Factory(AutomatonIpc automatonIpc, Configuration configuration) : ITaskFactory + internal sealed class Factory(Configuration configuration) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { @@ -41,7 +40,7 @@ internal static class Interact } else if (step.InteractionType == EInteractionType.Snipe) { - if (!automatonIpc.IsAutoSnipeEnabled) + if (!configuration.General.AutoSnipe) { yield break; } @@ -73,21 +72,21 @@ internal static class Interact SkipStepConditions stepIf = skipConditions.StepIf; if (stepIf != null && stepIf.Never) { - goto IL_0247; + goto IL_024f; } } if (step.InteractionType != EInteractionType.PurchaseItem) { skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0); - goto IL_0248; + goto IL_0250; } } - goto IL_0247; - IL_0247: + goto IL_024f; + IL_024f: skipMarkerCheck = 1; - goto IL_0248; - IL_0248: - yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, step.PickUpItemId, step.TaxiStandId, step.SkipConditions?.StepIf, step.CompletionQuestVariablesFlags); + goto IL_0250; + IL_0250: + yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, step.PickUpItemId ?? step.GCPurchase?.ItemId, step.TaxiStandId, step.SkipConditions?.StepIf, step.CompletionQuestVariablesFlags); } } diff --git a/Questionable/Questionable.Controller.Utils/WindowsKeypress.cs b/Questionable/Questionable.Controller.Utils/WindowsKeypress.cs new file mode 100644 index 0000000..0b3ffc7 --- /dev/null +++ b/Questionable/Questionable.Controller.Utils/WindowsKeypress.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; + +namespace Questionable.Controller.Utils; + +internal static class WindowsKeypress +{ + public enum LimitedKeys + { + A = 65, + Escape = 27 + } + + private static class NativeMethods + { + [DllImport("user32.dll", CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + public static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, nint lParam); + } + + public const uint WM_KEYUP = 257u; + + public const uint WM_KEYDOWN = 256u; + + public unsafe static void SendKeypress(LimitedKeys key) + { + void* hWnd = Device.Instance()->hWnd; + NativeMethods.SendMessage((nint)hWnd, 256u, (nint)key, IntPtr.Zero); + NativeMethods.SendMessage((nint)hWnd, 257u, (nint)key, IntPtr.Zero); + } + + public static void SendMessage(nint hWnd, uint msg, nint key) + { + NativeMethods.SendMessage(hWnd, msg, key, IntPtr.Zero); + } +} diff --git a/Questionable/Questionable.Controller/QuestController.cs b/Questionable/Questionable.Controller/QuestController.cs index 103ade6..cfa5e89 100644 --- a/Questionable/Questionable.Controller/QuestController.cs +++ b/Questionable/Questionable.Controller/QuestController.cs @@ -9,6 +9,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Gui.Toast; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.Logging; @@ -118,6 +119,8 @@ internal sealed class QuestController : MiniTaskController private readonly AutoDutyIpc _autoDutyIpc; + private readonly IDalamudPluginInterface _pluginInterface; + private readonly ILogger _logger; private readonly object _progressLock = new object(); @@ -241,7 +244,7 @@ internal sealed class QuestController : MiniTaskController public event AutomationTypeChangedEventHandler? AutomationTypeChanged; - public QuestController(IClientState clientState, IObjectTable objectTable, GameFunctions gameFunctions, QuestFunctions questFunctions, MovementController movementController, CombatController combatController, GatheringController gatheringController, ILogger logger, QuestRegistry questRegistry, JournalData journalData, IKeyState keyState, IChatGui chatGui, ICondition condition, IToastGui toastGui, Configuration configuration, TaskCreator taskCreator, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent, AutoDutyIpc autoDutyIpc) + public QuestController(IClientState clientState, IObjectTable objectTable, GameFunctions gameFunctions, QuestFunctions questFunctions, MovementController movementController, CombatController combatController, GatheringController gatheringController, ILogger logger, QuestRegistry questRegistry, JournalData journalData, IKeyState keyState, IChatGui chatGui, ICondition condition, IToastGui toastGui, Configuration configuration, TaskCreator taskCreator, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent, AutoDutyIpc autoDutyIpc, IDalamudPluginInterface pluginInterface) : base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger) { _clientState = clientState; @@ -261,6 +264,7 @@ internal sealed class QuestController : MiniTaskController _taskCreator = taskCreator; _singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent; _autoDutyIpc = autoDutyIpc; + _pluginInterface = pluginInterface; _logger = logger; _toastGui.ErrorToast += base.OnErrorToast; _toastGui.Toast += OnNormalToast; @@ -273,7 +277,7 @@ internal sealed class QuestController : MiniTaskController { lock (_progressLock) { - _logger.LogInformation("Reload, resetting curent quest progress"); + _logger.LogInformation("Reload, resetting current quest progress"); ResetInternalState(); ResetAutoRefreshState(); _questRegistry.Reload(); @@ -699,7 +703,6 @@ internal sealed class QuestController : MiniTaskController { if (_startedQuest != null && !_taskQueue.AllTasksComplete) { - _logger.LogTrace("Not switching from quest {CurrentQuestId} to {NewQuestId} because tasks are still running", _startedQuest.Quest.Id, elementId); questProgress = _startedQuest; b = _startedQuest.Sequence; } @@ -1415,6 +1418,7 @@ internal sealed class QuestController : MiniTaskController _logger.LogWarning("Could not find quest {QuestId} during import", questElement); } _logger.LogInformation("Imported {Count} priority quests", ManualPriorityQuests.Count); + SavePriorityQuests(); } public string ExportQuestPriority() @@ -1425,6 +1429,7 @@ internal sealed class QuestController : MiniTaskController public void ClearQuestPriority() { ManualPriorityQuests.Clear(); + SavePriorityQuests(); } public bool AddQuestPriority(ElementId elementId) @@ -1432,6 +1437,7 @@ internal sealed class QuestController : MiniTaskController if (_questRegistry.TryGetQuest(elementId, out Quest quest) && !ManualPriorityQuests.Contains(quest)) { ManualPriorityQuests.Add(quest); + SavePriorityQuests(); return true; } return false; @@ -1439,25 +1445,25 @@ internal sealed class QuestController : MiniTaskController public int AddAllAvailableQuests() { - List list = (from q in _questRegistry.AllQuests.Where(delegate(Quest quest) + List list = (from q in _questRegistry.AllQuests.Where(delegate(Quest quest2) { - if (quest.Root.Disabled || _questFunctions.IsQuestRemoved(quest.Id) || _questFunctions.IsQuestComplete(quest.Id) || _questFunctions.IsQuestAccepted(quest.Id) || quest.Info.IsRepeatable) + if (quest2.Root.Disabled || _questFunctions.IsQuestRemoved(quest2.Id) || _questFunctions.IsQuestComplete(quest2.Id) || _questFunctions.IsQuestAccepted(quest2.Id) || quest2.Info.IsRepeatable) { return false; } - if (quest.Info.AlliedSociety != EAlliedSociety.None) + if (quest2.Info.AlliedSociety != EAlliedSociety.None) { - _logger.LogDebug("Excluding allied society quest {QuestId} from bulk add", quest.Id); + _logger.LogDebug("Excluding allied society quest {QuestId} from bulk add", quest2.Id); return false; } - if (quest.Info is QuestInfo questInfo && _journalData.MoogleDeliveryGenreId.HasValue && questInfo.JournalGenre == _journalData.MoogleDeliveryGenreId.Value) + if (quest2.Info is QuestInfo questInfo && _journalData.MoogleDeliveryGenreId.HasValue && questInfo.JournalGenre == _journalData.MoogleDeliveryGenreId.Value) { - _logger.LogDebug("Excluding moogle delivery quest {QuestId} from bulk add", quest.Id); + _logger.LogDebug("Excluding moogle delivery quest {QuestId} from bulk add", quest2.Id); return false; } - if (!_questFunctions.IsReadyToAcceptQuest(quest.Id)) + if (!_questFunctions.IsReadyToAcceptQuest(quest2.Id)) { - _logger.LogTrace("Quest {QuestId} not ready to accept", quest.Id); + _logger.LogTrace("Quest {QuestId} not ready to accept", quest2.Id); return false; } return true; @@ -1467,11 +1473,16 @@ internal sealed class QuestController : MiniTaskController int num = 0; foreach (ElementId item in list) { - if (AddQuestPriority(item)) + if (_questRegistry.TryGetQuest(item, out Quest quest) && !ManualPriorityQuests.Contains(quest)) { + ManualPriorityQuests.Add(quest); num++; } } + if (num > 0) + { + SavePriorityQuests(); + } return num; } @@ -1482,6 +1493,7 @@ internal sealed class QuestController : MiniTaskController if (_questRegistry.TryGetQuest(elementId, out Quest quest) && !ManualPriorityQuests.Contains(quest)) { ManualPriorityQuests.Insert(index, quest); + SavePriorityQuests(); } return true; } @@ -1493,14 +1505,39 @@ internal sealed class QuestController : MiniTaskController } } - public bool WasLastTaskUpdateWithin(TimeSpan timeSpan) + public void LoadPriorityQuests() { - _logger.LogInformation("Last update: {Update}", _lastTaskUpdate); - if (!IsRunning) + if (!_configuration.General.PersistPriorityQuestsBetweenSessions) { - return DateTime.Now <= _lastTaskUpdate.Add(timeSpan); + _logger.LogDebug("Priority quest persistence is disabled, skipping load"); + return; } - return true; + ManualPriorityQuests.Clear(); + foreach (ElementId priorityQuest in _configuration.General.PriorityQuests) + { + if (_questRegistry.TryGetQuest(priorityQuest, out Quest quest)) + { + ManualPriorityQuests.Add(quest); + continue; + } + _logger.LogWarning("Could not find quest {QuestId} during load from config", priorityQuest); + } + if (ManualPriorityQuests.Count > 0) + { + _logger.LogInformation("Loaded {Count} priority quests from configuration", ManualPriorityQuests.Count); + } + } + + public void SavePriorityQuests() + { + if (!_configuration.General.PersistPriorityQuestsBetweenSessions) + { + _logger.LogDebug("Priority quest persistence is disabled, skipping save"); + return; + } + _configuration.General.PriorityQuests = ManualPriorityQuests.Select((Quest q) => q.Id).ToList(); + _pluginInterface.SavePluginConfig(_configuration); + _logger.LogDebug("Saved {Count} priority quests to configuration", ManualPriorityQuests.Count); } private void OnConditionChange(ConditionFlag flag, bool value) @@ -1530,6 +1567,11 @@ internal sealed class QuestController : MiniTaskController { _logger.LogInformation("Clearing priority quests on logout"); ManualPriorityQuests.Clear(); + if (_configuration.General.PersistPriorityQuestsBetweenSessions) + { + _configuration.General.PriorityQuests.Clear(); + _pluginInterface.SavePluginConfig(_configuration); + } } } diff --git a/Questionable/Questionable.Data/ChangelogData.cs b/Questionable/Questionable.Data/ChangelogData.cs index 3df06df..e76d250 100644 --- a/Questionable/Questionable.Data/ChangelogData.cs +++ b/Questionable/Questionable.Data/ChangelogData.cs @@ -11,417 +11,433 @@ internal static class ChangelogData static ChangelogData() { - int num = 54; + int num = 55; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); int num2 = 0; ref ChangelogEntry reference = ref span[num2]; - DateOnly releaseDate = new DateOnly(2025, 12, 31); - int num3 = 3; + DateOnly releaseDate = new DateOnly(2026, 1, 18); + int num3 = 2; List list2 = new List(num3); CollectionsMarshal.SetCount(list2, num3); Span span2 = CollectionsMarshal.AsSpan(list2); int num4 = 0; ref ChangeEntry reference2 = ref span2[num4]; - int num5 = 1; + int num5 = 5; List list3 = new List(num5); CollectionsMarshal.SetCount(list3, num5); Span span3 = CollectionsMarshal.AsSpan(list3); - int index = 0; - span3[index] = "Add Heavensturn (2026) quests"; - reference2 = new ChangeEntry(EChangeCategory.QuestUpdates, "New Quest Paths", list3); + int num6 = 0; + span3[num6] = "Added quest blacklisting"; + num6++; + span3[num6] = "Added MSQ Priority config"; + num6++; + span3[num6] = "Added Quest priority persistence config"; + num6++; + span3[num6] = "Added Duties tab to Journal"; + num6++; + span3[num6] = "Added GC shop handling and chocobo naming for chocobo quests"; + reference2 = new ChangeEntry(EChangeCategory.Added, "Major Features", list3); num4++; ref ChangeEntry reference3 = ref span2[num4]; - index = 1; - List list4 = new List(index); - CollectionsMarshal.SetCount(list4, index); + num6 = 5; + List list4 = new List(num6); + CollectionsMarshal.SetCount(list4, num6); Span span4 = CollectionsMarshal.AsSpan(list4); num5 = 0; - span4[num5] = "Added missing quest sequences"; + span4[num5] = "Removed PandorasBox dependency and added QTE handling"; + num5++; + span4[num5] = "Removed CBT dependency and added Snipe handling"; + num5++; + span4[num5] = "Added drag reordering to Stop condition quests"; + num5++; + span4[num5] = "Ignore item level requirements if using Unsync config"; + num5++; + span4[num5] = "Setting a Stop quest to Off no longer removes it from the list"; reference3 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list4); - num4++; - ref ChangeEntry reference4 = ref span2[num4]; - num5 = 1; - List list5 = new List(num5); - CollectionsMarshal.SetCount(list5, num5); - Span span5 = CollectionsMarshal.AsSpan(list5); - index = 0; - span5[index] = "Fixed leveling mode not restarting properly"; - reference4 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list5); - reference = new ChangelogEntry("7.4.9", releaseDate, list2); + reference = new ChangelogEntry("7.4.10", releaseDate, list2); num2++; - ref ChangelogEntry reference5 = ref span[num2]; - DateOnly releaseDate2 = new DateOnly(2025, 12, 29); - num4 = 1; - List list6 = new List(num4); - CollectionsMarshal.SetCount(list6, num4); - Span span6 = CollectionsMarshal.AsSpan(list6); + ref ChangelogEntry reference4 = ref span[num2]; + DateOnly releaseDate2 = new DateOnly(2025, 12, 31); + num4 = 3; + List list5 = new List(num4); + CollectionsMarshal.SetCount(list5, num4); + Span span5 = CollectionsMarshal.AsSpan(list5); num3 = 0; - ref ChangeEntry reference6 = ref span6[num3]; - index = 2; - List list7 = new List(index); - CollectionsMarshal.SetCount(list7, index); + ref ChangeEntry reference5 = ref span5[num3]; + num5 = 1; + List list6 = new List(num5); + CollectionsMarshal.SetCount(list6, num5); + Span span6 = CollectionsMarshal.AsSpan(list6); + num6 = 0; + span6[num6] = "Add Heavensturn (2026) quests"; + reference5 = new ChangeEntry(EChangeCategory.QuestUpdates, "New Quest Paths", list6); + num3++; + ref ChangeEntry reference6 = ref span5[num3]; + num6 = 1; + List list7 = new List(num6); + CollectionsMarshal.SetCount(list7, num6); Span span7 = CollectionsMarshal.AsSpan(list7); num5 = 0; - span7[num5] = "Fixed infinite teleport loop when multiple quests compete for priority"; - num5++; - span7[num5] = "Fixed leveling mode enabling for quest duties"; - reference6 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list7); - reference5 = new ChangelogEntry("7.4.8", releaseDate2, list6); - num2++; - ref ChangelogEntry reference7 = ref span[num2]; - DateOnly releaseDate3 = new DateOnly(2025, 12, 28); - num3 = 4; - List list8 = new List(num3); - CollectionsMarshal.SetCount(list8, num3); - Span span8 = CollectionsMarshal.AsSpan(list8); - num4 = 0; - ref ChangeEntry reference8 = ref span8[num4]; + span7[num5] = "Added missing quest sequences"; + reference6 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list7); + num3++; + ref ChangeEntry reference7 = ref span5[num3]; num5 = 1; - List list9 = new List(num5); - CollectionsMarshal.SetCount(list9, num5); - Span span9 = CollectionsMarshal.AsSpan(list9); - index = 0; - span9[index] = "Added leveling mode when underleveled for MSQ"; - reference8 = new ChangeEntry(EChangeCategory.Added, "Major Features", list9); - num4++; - ref ChangeEntry reference9 = ref span8[num4]; - index = 3; - List list10 = new List(index); - CollectionsMarshal.SetCount(list10, index); + List list8 = new List(num5); + CollectionsMarshal.SetCount(list8, num5); + Span span8 = CollectionsMarshal.AsSpan(list8); + num6 = 0; + span8[num6] = "Fixed leveling mode not restarting properly"; + reference7 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list8); + reference4 = new ChangelogEntry("7.4.9", releaseDate2, list5); + num2++; + ref ChangelogEntry reference8 = ref span[num2]; + DateOnly releaseDate3 = new DateOnly(2025, 12, 29); + num3 = 1; + List list9 = new List(num3); + CollectionsMarshal.SetCount(list9, num3); + Span span9 = CollectionsMarshal.AsSpan(list9); + num4 = 0; + ref ChangeEntry reference9 = ref span9[num4]; + num6 = 2; + List list10 = new List(num6); + CollectionsMarshal.SetCount(list10, num6); Span span10 = CollectionsMarshal.AsSpan(list10); num5 = 0; - span10[num5] = "Added missing dungeons to Duties"; + span10[num5] = "Fixed infinite teleport loop when multiple quests compete for priority"; num5++; - span10[num5] = "Added Normal Raids and Alliance Raids to duties tab"; - num5++; - span10[num5] = "Added Pause/Stop modes to stop conditions"; - reference9 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list10); - num4++; - ref ChangeEntry reference10 = ref span8[num4]; - num5 = 1; - List list11 = new List(num5); - CollectionsMarshal.SetCount(list11, num5); - Span span11 = CollectionsMarshal.AsSpan(list11); - index = 0; - span11[index] = "Added leveling mode IPC: IsLevelingModeEnabled, SetLevelingModeEnabled, GetMsqLevelLockInfo, StartLevelingMode, StopLevelingMode"; - reference10 = new ChangeEntry(EChangeCategory.Added, "IPC Changes", list11); - num4++; - ref ChangeEntry reference11 = ref span8[num4]; - index = 1; - List list12 = new List(index); - CollectionsMarshal.SetCount(list12, index); - Span span12 = CollectionsMarshal.AsSpan(list12); - num5 = 0; - span12[num5] = "Fixed UI appearing in duties when debuff or interrupts occur"; - reference11 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list12); - reference7 = new ChangelogEntry("7.4.7", releaseDate3, list8); + span10[num5] = "Fixed leveling mode enabling for quest duties"; + reference9 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list10); + reference8 = new ChangelogEntry("7.4.8", releaseDate3, list9); num2++; - ref ChangelogEntry reference12 = ref span[num2]; - DateOnly releaseDate4 = new DateOnly(2025, 12, 23); - num4 = 2; - List list13 = new List(num4); - CollectionsMarshal.SetCount(list13, num4); - Span span13 = CollectionsMarshal.AsSpan(list13); + ref ChangelogEntry reference10 = ref span[num2]; + DateOnly releaseDate4 = new DateOnly(2025, 12, 28); + num4 = 4; + List list11 = new List(num4); + CollectionsMarshal.SetCount(list11, num4); + Span span11 = CollectionsMarshal.AsSpan(list11); num3 = 0; - ref ChangeEntry reference13 = ref span13[num3]; + ref ChangeEntry reference11 = ref span11[num3]; + num5 = 1; + List list12 = new List(num5); + CollectionsMarshal.SetCount(list12, num5); + Span span12 = CollectionsMarshal.AsSpan(list12); + num6 = 0; + span12[num6] = "Added leveling mode when underleveled for MSQ"; + reference11 = new ChangeEntry(EChangeCategory.Added, "Major Features", list12); + num3++; + ref ChangeEntry reference12 = ref span11[num3]; + num6 = 3; + List list13 = new List(num6); + CollectionsMarshal.SetCount(list13, num6); + Span span13 = CollectionsMarshal.AsSpan(list13); + num5 = 0; + span13[num5] = "Added missing dungeons to Duties"; + num5++; + span13[num5] = "Added Normal Raids and Alliance Raids to duties tab"; + num5++; + span13[num5] = "Added Pause/Stop modes to stop conditions"; + reference12 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list13); + num3++; + ref ChangeEntry reference13 = ref span11[num3]; num5 = 1; List list14 = new List(num5); CollectionsMarshal.SetCount(list14, num5); Span span14 = CollectionsMarshal.AsSpan(list14); - index = 0; - span14[index] = "Added Cinema Mode to not skip cutscenes"; - reference13 = new ChangeEntry(EChangeCategory.Added, "Major Features", list14); + num6 = 0; + span14[num6] = "Added leveling mode IPC: IsLevelingModeEnabled, SetLevelingModeEnabled, GetMsqLevelLockInfo, StartLevelingMode, StopLevelingMode"; + reference13 = new ChangeEntry(EChangeCategory.Added, "IPC Changes", list14); num3++; - ref ChangeEntry reference14 = ref span13[num3]; - index = 2; - List list15 = new List(index); - CollectionsMarshal.SetCount(list15, index); + ref ChangeEntry reference14 = ref span11[num3]; + num6 = 1; + List list15 = new List(num6); + CollectionsMarshal.SetCount(list15, num6); Span span15 = CollectionsMarshal.AsSpan(list15); num5 = 0; - span15[num5] = "Added handling for Unsync (Party) to Party Watchdog and configuration to disable if in party"; - num5++; - span15[num5] = "Stop conditions now act as a true stop"; - reference14 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list15); - reference12 = new ChangelogEntry("7.4.6", releaseDate4, list13); + span15[num5] = "Fixed UI appearing in duties when debuff or interrupts occur"; + reference14 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list15); + reference10 = new ChangelogEntry("7.4.7", releaseDate4, list11); num2++; ref ChangelogEntry reference15 = ref span[num2]; - DateOnly releaseDate5 = new DateOnly(2025, 12, 22); - num3 = 1; + DateOnly releaseDate5 = new DateOnly(2025, 12, 23); + num3 = 2; List list16 = new List(num3); CollectionsMarshal.SetCount(list16, num3); Span span16 = CollectionsMarshal.AsSpan(list16); num4 = 0; ref ChangeEntry reference16 = ref span16[num4]; - num5 = 2; + num5 = 1; List list17 = new List(num5); CollectionsMarshal.SetCount(list17, num5); Span span17 = CollectionsMarshal.AsSpan(list17); - index = 0; - span17[index] = "Dive adjustments"; - index++; - span17[index] = "Logging message adjustments"; - reference16 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list17); - reference15 = new ChangelogEntry("7.4.5", releaseDate5, list16); - num2++; - ref ChangelogEntry reference17 = ref span[num2]; - DateOnly releaseDate6 = new DateOnly(2025, 12, 21); - num4 = 2; - List list18 = new List(num4); - CollectionsMarshal.SetCount(list18, num4); - Span span18 = CollectionsMarshal.AsSpan(list18); - num3 = 0; - ref ChangeEntry reference18 = ref span18[num3]; - index = 1; - List list19 = new List(index); - CollectionsMarshal.SetCount(list19, index); - Span span19 = CollectionsMarshal.AsSpan(list19); + num6 = 0; + span17[num6] = "Added Cinema Mode to not skip cutscenes"; + reference16 = new ChangeEntry(EChangeCategory.Added, "Major Features", list17); + num4++; + ref ChangeEntry reference17 = ref span16[num4]; + num6 = 2; + List list18 = new List(num6); + CollectionsMarshal.SetCount(list18, num6); + Span span18 = CollectionsMarshal.AsSpan(list18); num5 = 0; - span19[num5] = "Changelog only shows once per update now"; - reference18 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list19); - num3++; - ref ChangeEntry reference19 = ref span18[num3]; - num5 = 1; + span18[num5] = "Added handling for Unsync (Party) to Party Watchdog and configuration to disable if in party"; + num5++; + span18[num5] = "Stop conditions now act as a true stop"; + reference17 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list18); + reference15 = new ChangelogEntry("7.4.6", releaseDate5, list16); + num2++; + ref ChangelogEntry reference18 = ref span[num2]; + DateOnly releaseDate6 = new DateOnly(2025, 12, 22); + num4 = 1; + List list19 = new List(num4); + CollectionsMarshal.SetCount(list19, num4); + Span span19 = CollectionsMarshal.AsSpan(list19); + num3 = 0; + ref ChangeEntry reference19 = ref span19[num3]; + num5 = 2; List list20 = new List(num5); CollectionsMarshal.SetCount(list20, num5); Span span20 = CollectionsMarshal.AsSpan(list20); - index = 0; - span20[index] = "Fixed gathering paths loading"; - reference19 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list20); - reference17 = new ChangelogEntry("7.4.4", releaseDate6, list18); + num6 = 0; + span20[num6] = "Dive adjustments"; + num6++; + span20[num6] = "Logging message adjustments"; + reference19 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list20); + reference18 = new ChangelogEntry("7.4.5", releaseDate6, list19); num2++; ref ChangelogEntry reference20 = ref span[num2]; DateOnly releaseDate7 = new DateOnly(2025, 12, 21); - num3 = 1; + num3 = 2; List list21 = new List(num3); CollectionsMarshal.SetCount(list21, num3); Span span21 = CollectionsMarshal.AsSpan(list21); num4 = 0; ref ChangeEntry reference21 = ref span21[num4]; - index = 1; - List list22 = new List(index); - CollectionsMarshal.SetCount(list22, index); + num6 = 1; + List list22 = new List(num6); + CollectionsMarshal.SetCount(list22, num6); Span span22 = CollectionsMarshal.AsSpan(list22); num5 = 0; - span22[num5] = "Fixed changelog version checks"; - reference21 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list22); - reference20 = new ChangelogEntry("7.4.3", releaseDate7, list21); - num2++; - ref ChangelogEntry reference22 = ref span[num2]; - DateOnly releaseDate8 = new DateOnly(2025, 12, 20); - num4 = 2; - List list23 = new List(num4); - CollectionsMarshal.SetCount(list23, num4); - Span span23 = CollectionsMarshal.AsSpan(list23); - num3 = 0; - ref ChangeEntry reference23 = ref span23[num3]; + span22[num5] = "Changelog only shows once per update now"; + reference21 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list22); + num4++; + ref ChangeEntry reference22 = ref span21[num4]; num5 = 1; - List list24 = new List(num5); - CollectionsMarshal.SetCount(list24, num5); - Span span24 = CollectionsMarshal.AsSpan(list24); - index = 0; - span24[index] = "Add 7.4 Starlight Celebration (2025) quest"; - reference23 = new ChangeEntry(EChangeCategory.QuestUpdates, "New Quest Paths", list24); - num3++; - ref ChangeEntry reference24 = ref span23[num3]; - index = 1; - List list25 = new List(index); - CollectionsMarshal.SetCount(list25, index); + List list23 = new List(num5); + CollectionsMarshal.SetCount(list23, num5); + Span span23 = CollectionsMarshal.AsSpan(list23); + num6 = 0; + span23[num6] = "Fixed gathering paths loading"; + reference22 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list23); + reference20 = new ChangelogEntry("7.4.4", releaseDate7, list21); + num2++; + ref ChangelogEntry reference23 = ref span[num2]; + DateOnly releaseDate8 = new DateOnly(2025, 12, 21); + num4 = 1; + List list24 = new List(num4); + CollectionsMarshal.SetCount(list24, num4); + Span span24 = CollectionsMarshal.AsSpan(list24); + num3 = 0; + ref ChangeEntry reference24 = ref span24[num3]; + num6 = 1; + List list25 = new List(num6); + CollectionsMarshal.SetCount(list25, num6); Span span25 = CollectionsMarshal.AsSpan(list25); num5 = 0; - span25[num5] = "Fixed 7.4 MSQ"; + span25[num5] = "Fixed changelog version checks"; reference24 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list25); - reference22 = new ChangelogEntry("7.4.2", releaseDate8, list23); + reference23 = new ChangelogEntry("7.4.3", releaseDate8, list24); num2++; ref ChangelogEntry reference25 = ref span[num2]; - DateOnly releaseDate9 = new DateOnly(2025, 12, 19); - num3 = 1; + DateOnly releaseDate9 = new DateOnly(2025, 12, 20); + num3 = 2; List list26 = new List(num3); CollectionsMarshal.SetCount(list26, num3); Span span26 = CollectionsMarshal.AsSpan(list26); num4 = 0; ref ChangeEntry reference26 = ref span26[num4]; - num5 = 2; + num5 = 1; List list27 = new List(num5); CollectionsMarshal.SetCount(list27, num5); Span span27 = CollectionsMarshal.AsSpan(list27); - index = 0; - span27[index] = "Add 7.4 MSQ"; - index++; - span27[index] = "Add 7.4 Arcadion Raid quests"; + num6 = 0; + span27[num6] = "Add 7.4 Starlight Celebration (2025) quest"; reference26 = new ChangeEntry(EChangeCategory.QuestUpdates, "New Quest Paths", list27); - reference25 = new ChangelogEntry("7.4.1", releaseDate9, list26); - num2++; - ref ChangelogEntry reference27 = ref span[num2]; - DateOnly releaseDate10 = new DateOnly(2025, 12, 17); - num4 = 1; - List list28 = new List(num4); - CollectionsMarshal.SetCount(list28, num4); - Span span28 = CollectionsMarshal.AsSpan(list28); - num3 = 0; - span28[num3] = new ChangeEntry(EChangeCategory.Changed, "Api 14 update"); - reference27 = new ChangelogEntry("7.4.0", releaseDate10, list28); + num4++; + ref ChangeEntry reference27 = ref span26[num4]; + num6 = 1; + List list28 = new List(num6); + CollectionsMarshal.SetCount(list28, num6); + Span span28 = CollectionsMarshal.AsSpan(list28); + num5 = 0; + span28[num5] = "Fixed 7.4 MSQ"; + reference27 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list28); + reference25 = new ChangelogEntry("7.4.2", releaseDate9, list26); num2++; ref ChangelogEntry reference28 = ref span[num2]; - DateOnly releaseDate11 = new DateOnly(2025, 12, 6); - num3 = 2; - List list29 = new List(num3); - CollectionsMarshal.SetCount(list29, num3); + DateOnly releaseDate10 = new DateOnly(2025, 12, 19); + num4 = 1; + List list29 = new List(num4); + CollectionsMarshal.SetCount(list29, num4); Span span29 = CollectionsMarshal.AsSpan(list29); - num4 = 0; - ref ChangeEntry reference29 = ref span29[num4]; - index = 4; - List list30 = new List(index); - CollectionsMarshal.SetCount(list30, index); + num3 = 0; + ref ChangeEntry reference29 = ref span29[num3]; + num5 = 2; + List list30 = new List(num5); + CollectionsMarshal.SetCount(list30, num5); Span span30 = CollectionsMarshal.AsSpan(list30); - num5 = 0; - span30[num5] = "Added reloading and rebuilding to movement system"; - num5++; - span30[num5] = "Improved interrupts and refresh states to allow continuation of questing"; - num5++; - span30[num5] = "Added player input detection to stop automation when manually moving character"; - num5++; - span30[num5] = "Added various missing quest sequences"; - reference29 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list30); - num4++; - ref ChangeEntry reference30 = ref span29[num4]; - num5 = 1; - List list31 = new List(num5); - CollectionsMarshal.SetCount(list31, num5); - Span span31 = CollectionsMarshal.AsSpan(list31); - index = 0; - span31[index] = "Fixed reset task state to prevent stuck interactions after interruption"; - reference30 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list31); - reference28 = new ChangelogEntry("7.38.9", releaseDate11, list29); + num6 = 0; + span30[num6] = "Add 7.4 MSQ"; + num6++; + span30[num6] = "Add 7.4 Arcadion Raid quests"; + reference29 = new ChangeEntry(EChangeCategory.QuestUpdates, "New Quest Paths", list30); + reference28 = new ChangelogEntry("7.4.1", releaseDate10, list29); + num2++; + ref ChangelogEntry reference30 = ref span[num2]; + DateOnly releaseDate11 = new DateOnly(2025, 12, 17); + num3 = 1; + List list31 = new List(num3); + CollectionsMarshal.SetCount(list31, num3); + Span span31 = CollectionsMarshal.AsSpan(list31); + num4 = 0; + span31[num4] = new ChangeEntry(EChangeCategory.Changed, "Api 14 update"); + reference30 = new ChangelogEntry("7.4.0", releaseDate11, list31); num2++; ref ChangelogEntry reference31 = ref span[num2]; - DateOnly releaseDate12 = new DateOnly(2025, 11, 29); + DateOnly releaseDate12 = new DateOnly(2025, 12, 6); num4 = 2; List list32 = new List(num4); CollectionsMarshal.SetCount(list32, num4); Span span32 = CollectionsMarshal.AsSpan(list32); num3 = 0; ref ChangeEntry reference32 = ref span32[num3]; - index = 3; - List list33 = new List(index); - CollectionsMarshal.SetCount(list33, index); + num6 = 4; + List list33 = new List(num6); + CollectionsMarshal.SetCount(list33, num6); Span span33 = CollectionsMarshal.AsSpan(list33); num5 = 0; - span33[num5] = "Movement update with automatic retrying if character can't reach target position"; + span33[num5] = "Added reloading and rebuilding to movement system"; num5++; - span33[num5] = "Added Hunt mob data"; + span33[num5] = "Improved interrupts and refresh states to allow continuation of questing"; num5++; - span33[num5] = "Refactored commands"; + span33[num5] = "Added player input detection to stop automation when manually moving character"; + num5++; + span33[num5] = "Added various missing quest sequences"; reference32 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list33); num3++; ref ChangeEntry reference33 = ref span32[num3]; - num5 = 3; + num5 = 1; List list34 = new List(num5); CollectionsMarshal.SetCount(list34, num5); Span span34 = CollectionsMarshal.AsSpan(list34); - index = 0; - span34[index] = "Fixed quest (Way of the Archer)"; - index++; - span34[index] = "Fixed quest (Spirithold Broken)"; - index++; - span34[index] = "Fixed quest (It's Probably Not Pirates)"; - reference33 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list34); - reference31 = new ChangelogEntry("7.38.8", releaseDate12, list32); + num6 = 0; + span34[num6] = "Fixed reset task state to prevent stuck interactions after interruption"; + reference33 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list34); + reference31 = new ChangelogEntry("7.38.9", releaseDate12, list32); num2++; ref ChangelogEntry reference34 = ref span[num2]; - DateOnly releaseDate13 = new DateOnly(2025, 11, 25); + DateOnly releaseDate13 = new DateOnly(2025, 11, 29); num3 = 2; List list35 = new List(num3); CollectionsMarshal.SetCount(list35, num3); Span span35 = CollectionsMarshal.AsSpan(list35); num4 = 0; ref ChangeEntry reference35 = ref span35[num4]; - index = 2; - List list36 = new List(index); - CollectionsMarshal.SetCount(list36, index); + num6 = 3; + List list36 = new List(num6); + CollectionsMarshal.SetCount(list36, num6); Span span36 = CollectionsMarshal.AsSpan(list36); num5 = 0; - span36[num5] = "Added individual sequence stop condition for each quest"; + span36[num5] = "Movement update with automatic retrying if character can't reach target position"; num5++; - span36[num5] = "Added Trials to Duties tab in config"; - reference35 = new ChangeEntry(EChangeCategory.Added, "Major features", list36); + span36[num5] = "Added Hunt mob data"; + num5++; + span36[num5] = "Refactored commands"; + reference35 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list36); num4++; ref ChangeEntry reference36 = ref span35[num4]; - num5 = 1; + num5 = 3; List list37 = new List(num5); CollectionsMarshal.SetCount(list37, num5); Span span37 = CollectionsMarshal.AsSpan(list37); - index = 0; - span37[index] = "Added IPC for stop conditions: GetQuestSequenceStopCondition, SetQuestSequenceStopCondition, RemoveQuestSequenceStopCondition, GetAllQuestSequenceStopConditions"; - reference36 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list37); - reference34 = new ChangelogEntry("7.38.7", releaseDate13, list35); + num6 = 0; + span37[num6] = "Fixed quest (Way of the Archer)"; + num6++; + span37[num6] = "Fixed quest (Spirithold Broken)"; + num6++; + span37[num6] = "Fixed quest (It's Probably Not Pirates)"; + reference36 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list37); + reference34 = new ChangelogEntry("7.38.8", releaseDate13, list35); num2++; ref ChangelogEntry reference37 = ref span[num2]; DateOnly releaseDate14 = new DateOnly(2025, 11, 25); - num4 = 3; + num4 = 2; List list38 = new List(num4); CollectionsMarshal.SetCount(list38, num4); Span span38 = CollectionsMarshal.AsSpan(list38); num3 = 0; ref ChangeEntry reference38 = ref span38[num3]; - index = 2; - List list39 = new List(index); - CollectionsMarshal.SetCount(list39, index); + num6 = 2; + List list39 = new List(num6); + CollectionsMarshal.SetCount(list39, num6); Span span39 = CollectionsMarshal.AsSpan(list39); num5 = 0; - span39[num5] = "Updated Allied Society journal text"; + span39[num5] = "Added individual sequence stop condition for each quest"; num5++; - span39[num5] = "Improved Allied Society rank handling"; - reference38 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list39); + span39[num5] = "Added Trials to Duties tab in config"; + reference38 = new ChangeEntry(EChangeCategory.Added, "Major features", list39); num3++; ref ChangeEntry reference39 = ref span38[num3]; num5 = 1; List list40 = new List(num5); CollectionsMarshal.SetCount(list40, num5); Span span40 = CollectionsMarshal.AsSpan(list40); - index = 0; - span40[index] = "Added IPC for Allied Society: AddAlliedSocietyOptimalQuests, GetAlliedSocietyOptimalQuests"; + num6 = 0; + span40[num6] = "Added IPC for stop conditions: GetQuestSequenceStopCondition, SetQuestSequenceStopCondition, RemoveQuestSequenceStopCondition, GetAllQuestSequenceStopConditions"; reference39 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list40); - num3++; - ref ChangeEntry reference40 = ref span38[num3]; - index = 1; - List list41 = new List(index); - CollectionsMarshal.SetCount(list41, index); - Span span41 = CollectionsMarshal.AsSpan(list41); - num5 = 0; - span41[num5] = "Fixed quest (We Come in Peace)"; - reference40 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list41); - reference37 = new ChangelogEntry("7.38.6", releaseDate14, list38); + reference37 = new ChangelogEntry("7.38.7", releaseDate14, list38); num2++; - ref ChangelogEntry reference41 = ref span[num2]; - DateOnly releaseDate15 = new DateOnly(2025, 11, 24); - num3 = 2; - List list42 = new List(num3); - CollectionsMarshal.SetCount(list42, num3); - Span span42 = CollectionsMarshal.AsSpan(list42); + ref ChangelogEntry reference40 = ref span[num2]; + DateOnly releaseDate15 = new DateOnly(2025, 11, 25); + num3 = 3; + List list41 = new List(num3); + CollectionsMarshal.SetCount(list41, num3); + Span span41 = CollectionsMarshal.AsSpan(list41); num4 = 0; - ref ChangeEntry reference42 = ref span42[num4]; + ref ChangeEntry reference41 = ref span41[num4]; + num6 = 2; + List list42 = new List(num6); + CollectionsMarshal.SetCount(list42, num6); + Span span42 = CollectionsMarshal.AsSpan(list42); + num5 = 0; + span42[num5] = "Updated Allied Society journal text"; + num5++; + span42[num5] = "Improved Allied Society rank handling"; + reference41 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list42); + num4++; + ref ChangeEntry reference42 = ref span41[num4]; num5 = 1; List list43 = new List(num5); CollectionsMarshal.SetCount(list43, num5); Span span43 = CollectionsMarshal.AsSpan(list43); - index = 0; - span43[index] = "Added Allied Society daily allowance tracker with bulk quest adding buttons"; - reference42 = new ChangeEntry(EChangeCategory.Added, "Major features", list43); + num6 = 0; + span43[num6] = "Added IPC for Allied Society: AddAlliedSocietyOptimalQuests, GetAlliedSocietyOptimalQuests"; + reference42 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list43); num4++; - ref ChangeEntry reference43 = ref span42[num4]; - index = 1; - List list44 = new List(index); - CollectionsMarshal.SetCount(list44, index); + ref ChangeEntry reference43 = ref span41[num4]; + num6 = 1; + List list44 = new List(num6); + CollectionsMarshal.SetCount(list44, num6); Span span44 = CollectionsMarshal.AsSpan(list44); num5 = 0; - span44[num5] = "Added IPC for Allied Society: GetRemainingAllowances, GetTimeUntilReset, GetAvailableQuestIds, GetAllAvailableQuestCounts, IsMaxRank, GetCurrentRank, GetSocietiesWithAvailableQuests"; - reference43 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list44); - reference41 = new ChangelogEntry("7.38.5", releaseDate15, list42); + span44[num5] = "Fixed quest (We Come in Peace)"; + reference43 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list44); + reference40 = new ChangelogEntry("7.38.6", releaseDate15, list41); num2++; ref ChangelogEntry reference44 = ref span[num2]; - DateOnly releaseDate16 = new DateOnly(2025, 11, 23); + DateOnly releaseDate16 = new DateOnly(2025, 11, 24); num4 = 2; List list45 = new List(num4); CollectionsMarshal.SetCount(list45, num4); @@ -432,110 +448,102 @@ internal static class ChangelogData List list46 = new List(num5); CollectionsMarshal.SetCount(list46, num5); Span span46 = CollectionsMarshal.AsSpan(list46); - index = 0; - span46[index] = "Explicitly declare support for BMR singleplayer duty (The Rematch)"; - reference45 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list46); + num6 = 0; + span46[num6] = "Added Allied Society daily allowance tracker with bulk quest adding buttons"; + reference45 = new ChangeEntry(EChangeCategory.Added, "Major features", list46); num3++; ref ChangeEntry reference46 = ref span45[num3]; - index = 8; - List list47 = new List(index); - CollectionsMarshal.SetCount(list47, index); + num6 = 1; + List list47 = new List(num6); + CollectionsMarshal.SetCount(list47, num6); Span span47 = CollectionsMarshal.AsSpan(list47); num5 = 0; - span47[num5] = "Fixed quest (Microbrewing) to not get stuck near ramp"; - num5++; - span47[num5] = "Fixed quest (The Illuminated Land) where pathing would kill the player due to fall damage"; - num5++; - span47[num5] = "Fixed quest (It's Probably Not Pirates) improper pathing and removed unneeded step"; - num5++; - span47[num5] = "Fixed quest (The Black Wolf's Ultimatum) not exiting landing area"; - num5++; - span47[num5] = "Fixed quest (Magiteknical Failure) from not interacting with NPC due to being mounted"; - num5++; - span47[num5] = "Fixed quest (We Come in Peace) shortcut navigation"; - num5++; - span47[num5] = "Fixed quest (Poisoned Hearts) where incorrect pathing caused the player to die"; - num5++; - span47[num5] = "Fixed quests (Savage Snares) and (An Apple a Day) not detecting kills"; - reference46 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list47); - reference44 = new ChangelogEntry("7.38.4", releaseDate16, list45); + span47[num5] = "Added IPC for Allied Society: GetRemainingAllowances, GetTimeUntilReset, GetAvailableQuestIds, GetAllAvailableQuestCounts, IsMaxRank, GetCurrentRank, GetSocietiesWithAvailableQuests"; + reference46 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list47); + reference44 = new ChangelogEntry("7.38.5", releaseDate16, list45); num2++; ref ChangelogEntry reference47 = ref span[num2]; DateOnly releaseDate17 = new DateOnly(2025, 11, 23); - num3 = 3; + num3 = 2; List list48 = new List(num3); CollectionsMarshal.SetCount(list48, num3); Span span48 = CollectionsMarshal.AsSpan(list48); num4 = 0; ref ChangeEntry reference48 = ref span48[num4]; - num5 = 2; + num5 = 1; List list49 = new List(num5); CollectionsMarshal.SetCount(list49, num5); Span span49 = CollectionsMarshal.AsSpan(list49); - index = 0; - span49[index] = "Added RequireHq to crafting InteractionType"; - index++; - span49[index] = "Mark GC quests as Locked if rank not achieved"; + num6 = 0; + span49[num6] = "Explicitly declare support for BMR singleplayer duty (The Rematch)"; reference48 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list49); num4++; ref ChangeEntry reference49 = ref span48[num4]; - index = 2; - List list50 = new List(index); - CollectionsMarshal.SetCount(list50, index); + num6 = 8; + List list50 = new List(num6); + CollectionsMarshal.SetCount(list50, num6); Span span50 = CollectionsMarshal.AsSpan(list50); num5 = 0; - span50[num5] = "Added IPC for stop conditions: GetStopConditionsEnabled, SetStopConditionsEnabled, GetStopQuestList, AddStopQuest, RemoveStopQuest, ClearStopQuests, GetLevelStopCondition, SetLevelStopCondition, GetSequenceStopCondition, SetSequenceStopCondition"; + span50[num5] = "Fixed quest (Microbrewing) to not get stuck near ramp"; num5++; - span50[num5] = "Added IPC for priority quests: GetPriorityQuests, RemovePriorityQuest, ReorderPriorityQuest, GetAvailablePresets, GetPresetQuests, AddPresetToPriority, IsPresetAvailable, IsQuestInPriority, GetQuestPriorityIndex, HasAvailablePriorityQuests"; - reference49 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list50); - num4++; - ref ChangeEntry reference50 = ref span48[num4]; - num5 = 3; - List list51 = new List(num5); - CollectionsMarshal.SetCount(list51, num5); - Span span51 = CollectionsMarshal.AsSpan(list51); - index = 0; - span51[index] = "Fixed line breaks not working in dialog strings"; - index++; - span51[index] = "Fixed quest (Labor of Love)"; - index++; - span51[index] = "Fixed quest (Sea of Sorrow)"; - reference50 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list51); - reference47 = new ChangelogEntry("7.38.3", releaseDate17, list48); + span50[num5] = "Fixed quest (The Illuminated Land) where pathing would kill the player due to fall damage"; + num5++; + span50[num5] = "Fixed quest (It's Probably Not Pirates) improper pathing and removed unneeded step"; + num5++; + span50[num5] = "Fixed quest (The Black Wolf's Ultimatum) not exiting landing area"; + num5++; + span50[num5] = "Fixed quest (Magiteknical Failure) from not interacting with NPC due to being mounted"; + num5++; + span50[num5] = "Fixed quest (We Come in Peace) shortcut navigation"; + num5++; + span50[num5] = "Fixed quest (Poisoned Hearts) where incorrect pathing caused the player to die"; + num5++; + span50[num5] = "Fixed quests (Savage Snares) and (An Apple a Day) not detecting kills"; + reference49 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list50); + reference47 = new ChangelogEntry("7.38.4", releaseDate17, list48); num2++; - ref ChangelogEntry reference51 = ref span[num2]; - DateOnly releaseDate18 = new DateOnly(2025, 11, 18); + ref ChangelogEntry reference50 = ref span[num2]; + DateOnly releaseDate18 = new DateOnly(2025, 11, 23); num4 = 3; - List list52 = new List(num4); - CollectionsMarshal.SetCount(list52, num4); - Span span52 = CollectionsMarshal.AsSpan(list52); + List list51 = new List(num4); + CollectionsMarshal.SetCount(list51, num4); + Span span51 = CollectionsMarshal.AsSpan(list51); num3 = 0; - ref ChangeEntry reference52 = ref span52[num3]; - index = 2; - List list53 = new List(index); - CollectionsMarshal.SetCount(list53, index); + ref ChangeEntry reference51 = ref span51[num3]; + num5 = 2; + List list52 = new List(num5); + CollectionsMarshal.SetCount(list52, num5); + Span span52 = CollectionsMarshal.AsSpan(list52); + num6 = 0; + span52[num6] = "Added RequireHq to crafting InteractionType"; + num6++; + span52[num6] = "Mark GC quests as Locked if rank not achieved"; + reference51 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list52); + num3++; + ref ChangeEntry reference52 = ref span51[num3]; + num6 = 2; + List list53 = new List(num6); + CollectionsMarshal.SetCount(list53, num6); Span span53 = CollectionsMarshal.AsSpan(list53); num5 = 0; - span53[num5] = "Auto Duty unsync options for each duty (Duty Support, Unsync Solo, Unsync Party)"; + span53[num5] = "Added IPC for stop conditions: GetStopConditionsEnabled, SetStopConditionsEnabled, GetStopQuestList, AddStopQuest, RemoveStopQuest, ClearStopQuests, GetLevelStopCondition, SetLevelStopCondition, GetSequenceStopCondition, SetSequenceStopCondition"; num5++; - span53[num5] = "Added Auto Duty unsync options to quest schema and updated quests using old unsync method"; - reference52 = new ChangeEntry(EChangeCategory.Added, "Major features", list53); + span53[num5] = "Added IPC for priority quests: GetPriorityQuests, RemovePriorityQuest, ReorderPriorityQuest, GetAvailablePresets, GetPresetQuests, AddPresetToPriority, IsPresetAvailable, IsQuestInPriority, GetQuestPriorityIndex, HasAvailablePriorityQuests"; + reference52 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list53); num3++; - ref ChangeEntry reference53 = ref span52[num3]; + ref ChangeEntry reference53 = ref span51[num3]; num5 = 3; List list54 = new List(num5); CollectionsMarshal.SetCount(list54, num5); Span span54 = CollectionsMarshal.AsSpan(list54); - index = 0; - span54[index] = "Added IPC for duty sync handling: GetDefaultDutyMode, SetDefaultDutyMode"; - index++; - span54[index] = "Added IPC for duty mode overrides: GetDutyModeOverride, SetDutyModeOverride"; - index++; - span54[index] = "Added IPC for clearing overrides: ClearDutyModeOverride, ClearAllDutyModeOverrides"; - reference53 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list54); - num3++; - span52[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest (Constant Cravings)"); - reference51 = new ChangelogEntry("7.38.2", releaseDate18, list52); + num6 = 0; + span54[num6] = "Fixed line breaks not working in dialog strings"; + num6++; + span54[num6] = "Fixed quest (Labor of Love)"; + num6++; + span54[num6] = "Fixed quest (Sea of Sorrow)"; + reference53 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list54); + reference50 = new ChangelogEntry("7.38.3", releaseDate18, list51); num2++; ref ChangelogEntry reference54 = ref span[num2]; DateOnly releaseDate19 = new DateOnly(2025, 11, 18); @@ -545,606 +553,640 @@ internal static class ChangelogData Span span55 = CollectionsMarshal.AsSpan(list55); num4 = 0; ref ChangeEntry reference55 = ref span55[num4]; - index = 1; - List list56 = new List(index); - CollectionsMarshal.SetCount(list56, index); + num6 = 2; + List list56 = new List(num6); + CollectionsMarshal.SetCount(list56, num6); Span span56 = CollectionsMarshal.AsSpan(list56); num5 = 0; - span56[num5] = "Added new fields to quest schema"; - reference55 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list56); + span56[num5] = "Auto Duty unsync options for each duty (Duty Support, Unsync Solo, Unsync Party)"; + num5++; + span56[num5] = "Added Auto Duty unsync options to quest schema and updated quests using old unsync method"; + reference55 = new ChangeEntry(EChangeCategory.Added, "Major features", list56); num4++; ref ChangeEntry reference56 = ref span55[num4]; num5 = 3; List list57 = new List(num5); CollectionsMarshal.SetCount(list57, num5); Span span57 = CollectionsMarshal.AsSpan(list57); - index = 0; - span57[index] = "A Faerie Tale Come True"; - index++; - span57[index] = "Constant Cravings"; - index++; - span57[index] = "A Bridge Too Full"; - reference56 = new ChangeEntry(EChangeCategory.QuestUpdates, "Added new quest paths", list57); + num6 = 0; + span57[num6] = "Added IPC for duty sync handling: GetDefaultDutyMode, SetDefaultDutyMode"; + num6++; + span57[num6] = "Added IPC for duty mode overrides: GetDutyModeOverride, SetDutyModeOverride"; + num6++; + span57[num6] = "Added IPC for clearing overrides: ClearDutyModeOverride, ClearAllDutyModeOverrides"; + reference56 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list57); num4++; - ref ChangeEntry reference57 = ref span55[num4]; - index = 3; - List list58 = new List(index); - CollectionsMarshal.SetCount(list58, index); - Span span58 = CollectionsMarshal.AsSpan(list58); - num5 = 0; - span58[num5] = "Fixed various quest schemas"; - num5++; - span58[num5] = "Fixed changelog bullet point encoding"; - num5++; - span58[num5] = "Fixed item use to wait until item is used before next action"; - reference57 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list58); - reference54 = new ChangelogEntry("7.38.1", releaseDate19, list55); + span55[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest (Constant Cravings)"); + reference54 = new ChangelogEntry("7.38.2", releaseDate19, list55); num2++; - ref ChangelogEntry reference58 = ref span[num2]; - DateOnly releaseDate20 = new DateOnly(2025, 11, 17); - num4 = 5; - List list59 = new List(num4); - CollectionsMarshal.SetCount(list59, num4); - Span span59 = CollectionsMarshal.AsSpan(list59); + ref ChangelogEntry reference57 = ref span[num2]; + DateOnly releaseDate20 = new DateOnly(2025, 11, 18); + num4 = 3; + List list58 = new List(num4); + CollectionsMarshal.SetCount(list58, num4); + Span span58 = CollectionsMarshal.AsSpan(list58); num3 = 0; - ref ChangeEntry reference59 = ref span59[num3]; - num5 = 2; + ref ChangeEntry reference58 = ref span58[num3]; + num6 = 1; + List list59 = new List(num6); + CollectionsMarshal.SetCount(list59, num6); + Span span59 = CollectionsMarshal.AsSpan(list59); + num5 = 0; + span59[num5] = "Added new fields to quest schema"; + reference58 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list59); + num3++; + ref ChangeEntry reference59 = ref span58[num3]; + num5 = 3; List list60 = new List(num5); CollectionsMarshal.SetCount(list60, num5); Span span60 = CollectionsMarshal.AsSpan(list60); - index = 0; - span60[index] = "Quest sequence window to show expected sequences in each quest (with quest searching)"; - index++; - span60[index] = "Changelog"; - reference59 = new ChangeEntry(EChangeCategory.Added, "Major features", list60); + num6 = 0; + span60[num6] = "A Faerie Tale Come True"; + num6++; + span60[num6] = "Constant Cravings"; + num6++; + span60[num6] = "A Bridge Too Full"; + reference59 = new ChangeEntry(EChangeCategory.QuestUpdates, "Added new quest paths", list60); num3++; - ref ChangeEntry reference60 = ref span59[num3]; - index = 2; - List list61 = new List(index); - CollectionsMarshal.SetCount(list61, index); + ref ChangeEntry reference60 = ref span58[num3]; + num6 = 3; + List list61 = new List(num6); + CollectionsMarshal.SetCount(list61, num6); Span span61 = CollectionsMarshal.AsSpan(list61); num5 = 0; - span61[num5] = "Updated quest schemas"; + span61[num5] = "Fixed various quest schemas"; num5++; - span61[num5] = "Added search bar to preferred mounts and capitalization to mirror game mount names"; - reference60 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list61); - num3++; - ref ChangeEntry reference61 = ref span59[num3]; + span61[num5] = "Fixed changelog bullet point encoding"; + num5++; + span61[num5] = "Fixed item use to wait until item is used before next action"; + reference60 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list61); + reference57 = new ChangelogEntry("7.38.1", releaseDate20, list58); + num2++; + ref ChangelogEntry reference61 = ref span[num2]; + DateOnly releaseDate21 = new DateOnly(2025, 11, 17); + num3 = 5; + List list62 = new List(num3); + CollectionsMarshal.SetCount(list62, num3); + Span span62 = CollectionsMarshal.AsSpan(list62); + num4 = 0; + ref ChangeEntry reference62 = ref span62[num4]; + num5 = 2; + List list63 = new List(num5); + CollectionsMarshal.SetCount(list63, num5); + Span span63 = CollectionsMarshal.AsSpan(list63); + num6 = 0; + span63[num6] = "Quest sequence window to show expected sequences in each quest (with quest searching)"; + num6++; + span63[num6] = "Changelog"; + reference62 = new ChangeEntry(EChangeCategory.Added, "Major features", list63); + num4++; + ref ChangeEntry reference63 = ref span62[num4]; + num6 = 2; + List list64 = new List(num6); + CollectionsMarshal.SetCount(list64, num6); + Span span64 = CollectionsMarshal.AsSpan(list64); + num5 = 0; + span64[num5] = "Updated quest schemas"; + num5++; + span64[num5] = "Added search bar to preferred mounts and capitalization to mirror game mount names"; + reference63 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list64); + num4++; + ref ChangeEntry reference64 = ref span62[num4]; num5 = 3; - List list62 = new List(num5); - CollectionsMarshal.SetCount(list62, num5); - Span span62 = CollectionsMarshal.AsSpan(list62); - index = 0; - span62[index] = "Renamed IsQuestCompleted → IsQuestComplete"; - index++; - span62[index] = "Renamed IsQuestAvailable → IsReadyToAcceptQuest"; - index++; - span62[index] = "Added GetCurrentTask IPC"; - reference61 = new ChangeEntry(EChangeCategory.Changed, "IPC changes", list62); - num3++; - span59[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added all Hildibrand quests"); - num3++; - span59[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed credits/cutscenes playback"); - reference58 = new ChangelogEntry("7.38.0", releaseDate20, list59); - num2++; - ref ChangelogEntry reference62 = ref span[num2]; - DateOnly releaseDate21 = new DateOnly(2025, 11, 8); - num3 = 1; - List list63 = new List(num3); - CollectionsMarshal.SetCount(list63, num3); - Span span63 = CollectionsMarshal.AsSpan(list63); - num4 = 0; - span63[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Fall Guys quest (Just Crowning Around)"); - reference62 = new ChangelogEntry("6.38", releaseDate21, list63); - num2++; - ref ChangelogEntry reference63 = ref span[num2]; - DateOnly releaseDate22 = new DateOnly(2025, 11, 8); - num4 = 1; - List list64 = new List(num4); - CollectionsMarshal.SetCount(list64, num4); - Span span64 = CollectionsMarshal.AsSpan(list64); - num3 = 0; - span64[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Cosmic Exploration and various unlock quests"); - reference63 = new ChangelogEntry("6.37", releaseDate22, list64); - num2++; - ref ChangelogEntry reference64 = ref span[num2]; - DateOnly releaseDate23 = new DateOnly(2025, 11, 2); - num3 = 1; - List list65 = new List(num3); - CollectionsMarshal.SetCount(list65, num3); - Span span65 = CollectionsMarshal.AsSpan(list65); - num4 = 0; - span65[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy Rank 6 quest (With High Spirits)"); - reference64 = new ChangelogEntry("6.36", releaseDate23, list65); + List list65 = new List(num5); + CollectionsMarshal.SetCount(list65, num5); + Span span65 = CollectionsMarshal.AsSpan(list65); + num6 = 0; + span65[num6] = "Renamed IsQuestCompleted → IsQuestComplete"; + num6++; + span65[num6] = "Renamed IsQuestAvailable → IsReadyToAcceptQuest"; + num6++; + span65[num6] = "Added GetCurrentTask IPC"; + reference64 = new ChangeEntry(EChangeCategory.Changed, "IPC changes", list65); + num4++; + span62[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added all Hildibrand quests"); + num4++; + span62[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed credits/cutscenes playback"); + reference61 = new ChangelogEntry("7.38.0", releaseDate21, list62); num2++; ref ChangelogEntry reference65 = ref span[num2]; - DateOnly releaseDate24 = new DateOnly(2025, 10, 28); + DateOnly releaseDate22 = new DateOnly(2025, 11, 8); num4 = 1; List list66 = new List(num4); CollectionsMarshal.SetCount(list66, num4); Span span66 = CollectionsMarshal.AsSpan(list66); num3 = 0; - span66[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed level 3 MSQ handling if character started on non-XP buff world"); - reference65 = new ChangelogEntry("6.35", releaseDate24, list66); + span66[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Fall Guys quest (Just Crowning Around)"); + reference65 = new ChangelogEntry("6.38", releaseDate22, list66); num2++; ref ChangelogEntry reference66 = ref span[num2]; - DateOnly releaseDate25 = new DateOnly(2025, 10, 23); - num3 = 2; + DateOnly releaseDate23 = new DateOnly(2025, 11, 8); + num3 = 1; List list67 = new List(num3); CollectionsMarshal.SetCount(list67, num3); Span span67 = CollectionsMarshal.AsSpan(list67); num4 = 0; - span67[num4] = new ChangeEntry(EChangeCategory.Added, "Added clear priority quests on logout and on completion config settings"); - num4++; - span67[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed priority quest importing to respect import order"); - reference66 = new ChangelogEntry("6.34", releaseDate25, list67); + span67[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Cosmic Exploration and various unlock quests"); + reference66 = new ChangelogEntry("6.37", releaseDate23, list67); num2++; ref ChangelogEntry reference67 = ref span[num2]; - DateOnly releaseDate26 = new DateOnly(2025, 10, 23); + DateOnly releaseDate24 = new DateOnly(2025, 11, 2); num4 = 1; List list68 = new List(num4); CollectionsMarshal.SetCount(list68, num4); Span span68 = CollectionsMarshal.AsSpan(list68); num3 = 0; - span68[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed RSR combat module"); - reference67 = new ChangelogEntry("6.33", releaseDate26, list68); + span68[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy Rank 6 quest (With High Spirits)"); + reference67 = new ChangelogEntry("6.36", releaseDate24, list68); num2++; ref ChangelogEntry reference68 = ref span[num2]; - DateOnly releaseDate27 = new DateOnly(2025, 10, 23); + DateOnly releaseDate25 = new DateOnly(2025, 10, 28); num3 = 1; List list69 = new List(num3); CollectionsMarshal.SetCount(list69, num3); Span span69 = CollectionsMarshal.AsSpan(list69); num4 = 0; - span69[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy Rank 5 quest (Forged in Corn)"); - reference68 = new ChangelogEntry("6.32", releaseDate27, list69); + span69[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed level 3 MSQ handling if character started on non-XP buff world"); + reference68 = new ChangelogEntry("6.35", releaseDate25, list69); num2++; ref ChangelogEntry reference69 = ref span[num2]; - DateOnly releaseDate28 = new DateOnly(2025, 10, 21); - num4 = 1; + DateOnly releaseDate26 = new DateOnly(2025, 10, 23); + num4 = 2; List list70 = new List(num4); CollectionsMarshal.SetCount(list70, num4); Span span70 = CollectionsMarshal.AsSpan(list70); num3 = 0; - span70[num3] = new ChangeEntry(EChangeCategory.Changed, "Added checks for moogle and allied society quests when using add all available quests"); - reference69 = new ChangelogEntry("6.31", releaseDate28, list70); + span70[num3] = new ChangeEntry(EChangeCategory.Added, "Added clear priority quests on logout and on completion config settings"); + num3++; + span70[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed priority quest importing to respect import order"); + reference69 = new ChangelogEntry("6.34", releaseDate26, list70); num2++; ref ChangelogEntry reference70 = ref span[num2]; - DateOnly releaseDate29 = new DateOnly(2025, 10, 21); + DateOnly releaseDate27 = new DateOnly(2025, 10, 23); num3 = 1; List list71 = new List(num3); CollectionsMarshal.SetCount(list71, num3); Span span71 = CollectionsMarshal.AsSpan(list71); num4 = 0; - span71[num4] = new ChangeEntry(EChangeCategory.Added, "Added button to journal that allows adding all available quests to priority"); - reference70 = new ChangelogEntry("6.30", releaseDate29, list71); + span71[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed RSR combat module"); + reference70 = new ChangelogEntry("6.33", releaseDate27, list71); num2++; ref ChangelogEntry reference71 = ref span[num2]; - DateOnly releaseDate30 = new DateOnly(2025, 10, 20); - num4 = 2; + DateOnly releaseDate28 = new DateOnly(2025, 10, 23); + num4 = 1; List list72 = new List(num4); CollectionsMarshal.SetCount(list72, num4); Span span72 = CollectionsMarshal.AsSpan(list72); num3 = 0; - ref ChangeEntry reference72 = ref span72[num3]; - index = 2; - List list73 = new List(index); - CollectionsMarshal.SetCount(list73, index); - Span span73 = CollectionsMarshal.AsSpan(list73); - num5 = 0; - span73[num5] = "Added item count to combat handling rework"; - num5++; - span73[num5] = "Updated Pandora conflicting features"; - reference72 = new ChangeEntry(EChangeCategory.Changed, "Combat handling improvements", list73); - num3++; - span72[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest to purchase Gysahl Greens if not in inventory"); - reference71 = new ChangelogEntry("6.29", releaseDate30, list72); + span72[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy Rank 5 quest (Forged in Corn)"); + reference71 = new ChangelogEntry("6.32", releaseDate28, list72); + num2++; + ref ChangelogEntry reference72 = ref span[num2]; + DateOnly releaseDate29 = new DateOnly(2025, 10, 21); + num3 = 1; + List list73 = new List(num3); + CollectionsMarshal.SetCount(list73, num3); + Span span73 = CollectionsMarshal.AsSpan(list73); + num4 = 0; + span73[num4] = new ChangeEntry(EChangeCategory.Changed, "Added checks for moogle and allied society quests when using add all available quests"); + reference72 = new ChangelogEntry("6.31", releaseDate29, list73); num2++; ref ChangelogEntry reference73 = ref span[num2]; - DateOnly releaseDate31 = new DateOnly(2025, 10, 19); - num3 = 1; - List list74 = new List(num3); - CollectionsMarshal.SetCount(list74, num3); + DateOnly releaseDate30 = new DateOnly(2025, 10, 21); + num4 = 1; + List list74 = new List(num4); + CollectionsMarshal.SetCount(list74, num4); Span span74 = CollectionsMarshal.AsSpan(list74); - num4 = 0; - span74[num4] = new ChangeEntry(EChangeCategory.Changed, "Reworked kill count combat handling - combat and enemy kills are now processed instantly"); - reference73 = new ChangelogEntry("6.28", releaseDate31, list74); + num3 = 0; + span74[num3] = new ChangeEntry(EChangeCategory.Added, "Added button to journal that allows adding all available quests to priority"); + reference73 = new ChangelogEntry("6.30", releaseDate30, list74); num2++; ref ChangelogEntry reference74 = ref span[num2]; - DateOnly releaseDate32 = new DateOnly(2025, 10, 18); - num4 = 2; - List list75 = new List(num4); - CollectionsMarshal.SetCount(list75, num4); + DateOnly releaseDate31 = new DateOnly(2025, 10, 20); + num3 = 2; + List list75 = new List(num3); + CollectionsMarshal.SetCount(list75, num3); Span span75 = CollectionsMarshal.AsSpan(list75); - num3 = 0; - span75[num3] = new ChangeEntry(EChangeCategory.Changed, "Improved Aether Current checking logic"); - num3++; - span75[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Chocobo Taxi Stand CheckSkip error and Patch 7.3 Fantasia unlock quest date/time"); - reference74 = new ChangelogEntry("6.27", releaseDate32, list75); - num2++; - ref ChangelogEntry reference75 = ref span[num2]; - DateOnly releaseDate33 = new DateOnly(2025, 10, 18); - num3 = 1; - List list76 = new List(num3); - CollectionsMarshal.SetCount(list76, num3); - Span span76 = CollectionsMarshal.AsSpan(list76); num4 = 0; - span76[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 4 quests"); - reference75 = new ChangelogEntry("6.26", releaseDate33, list76); + ref ChangeEntry reference75 = ref span75[num4]; + num6 = 2; + List list76 = new List(num6); + CollectionsMarshal.SetCount(list76, num6); + Span span76 = CollectionsMarshal.AsSpan(list76); + num5 = 0; + span76[num5] = "Added item count to combat handling rework"; + num5++; + span76[num5] = "Updated Pandora conflicting features"; + reference75 = new ChangeEntry(EChangeCategory.Changed, "Combat handling improvements", list76); + num4++; + span75[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest to purchase Gysahl Greens if not in inventory"); + reference74 = new ChangelogEntry("6.29", releaseDate31, list75); num2++; ref ChangelogEntry reference76 = ref span[num2]; - DateOnly releaseDate34 = new DateOnly(2025, 10, 17); + DateOnly releaseDate32 = new DateOnly(2025, 10, 19); num4 = 1; List list77 = new List(num4); CollectionsMarshal.SetCount(list77, num4); Span span77 = CollectionsMarshal.AsSpan(list77); num3 = 0; - span77[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added All Saints' Wake 2025 quests and 7.35 Yok Huy rank 4 quests"); - reference76 = new ChangelogEntry("6.25", releaseDate34, list77); + span77[num3] = new ChangeEntry(EChangeCategory.Changed, "Reworked kill count combat handling - combat and enemy kills are now processed instantly"); + reference76 = new ChangelogEntry("6.28", releaseDate32, list77); num2++; ref ChangelogEntry reference77 = ref span[num2]; - DateOnly releaseDate35 = new DateOnly(2025, 10, 16); - num3 = 1; + DateOnly releaseDate33 = new DateOnly(2025, 10, 18); + num3 = 2; List list78 = new List(num3); CollectionsMarshal.SetCount(list78, num3); Span span78 = CollectionsMarshal.AsSpan(list78); num4 = 0; - span78[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 4 quests and Deep Dungeon quest"); - reference77 = new ChangelogEntry("6.24", releaseDate35, list78); + span78[num4] = new ChangeEntry(EChangeCategory.Changed, "Improved Aether Current checking logic"); + num4++; + span78[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Chocobo Taxi Stand CheckSkip error and Patch 7.3 Fantasia unlock quest date/time"); + reference77 = new ChangelogEntry("6.27", releaseDate33, list78); num2++; ref ChangelogEntry reference78 = ref span[num2]; - DateOnly releaseDate36 = new DateOnly(2025, 10, 13); + DateOnly releaseDate34 = new DateOnly(2025, 10, 18); num4 = 1; List list79 = new List(num4); CollectionsMarshal.SetCount(list79, num4); Span span79 = CollectionsMarshal.AsSpan(list79); num3 = 0; - span79[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quest (Larder Logistics)"); - reference78 = new ChangelogEntry("6.23", releaseDate36, list79); + span79[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 4 quests"); + reference78 = new ChangelogEntry("6.26", releaseDate34, list79); num2++; ref ChangelogEntry reference79 = ref span[num2]; - DateOnly releaseDate37 = new DateOnly(2025, 10, 12); - num3 = 3; + DateOnly releaseDate35 = new DateOnly(2025, 10, 17); + num3 = 1; List list80 = new List(num3); CollectionsMarshal.SetCount(list80, num3); Span span80 = CollectionsMarshal.AsSpan(list80); num4 = 0; - span80[num4] = new ChangeEntry(EChangeCategory.Changed, "Prevent disabled or Locked quests from being started as 'Start as next quest'"); - num4++; - span80[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quests"); - num4++; - span80[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Yok Huy quest and journal quest chain priority issues"); - reference79 = new ChangelogEntry("6.22", releaseDate37, list80); + span80[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added All Saints' Wake 2025 quests and 7.35 Yok Huy rank 4 quests"); + reference79 = new ChangelogEntry("6.25", releaseDate35, list80); num2++; ref ChangelogEntry reference80 = ref span[num2]; - DateOnly releaseDate38 = new DateOnly(2025, 10, 12); - num4 = 2; + DateOnly releaseDate36 = new DateOnly(2025, 10, 16); + num4 = 1; List list81 = new List(num4); CollectionsMarshal.SetCount(list81, num4); Span span81 = CollectionsMarshal.AsSpan(list81); num3 = 0; - span81[num3] = new ChangeEntry(EChangeCategory.Added, "Added expansion abbreviation to journal window"); - num3++; - span81[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quests"); - reference80 = new ChangelogEntry("6.21", releaseDate38, list81); + span81[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 4 quests and Deep Dungeon quest"); + reference80 = new ChangelogEntry("6.24", releaseDate36, list81); num2++; ref ChangelogEntry reference81 = ref span[num2]; - DateOnly releaseDate39 = new DateOnly(2025, 10, 10); - num3 = 2; + DateOnly releaseDate37 = new DateOnly(2025, 10, 13); + num3 = 1; List list82 = new List(num3); CollectionsMarshal.SetCount(list82, num3); Span span82 = CollectionsMarshal.AsSpan(list82); num4 = 0; - span82[num4] = new ChangeEntry(EChangeCategory.Changed, "Allow completed repeatable quests to be used with 'Add quest and requirements to priority' feature"); - num4++; - span82[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 1 quest (A Work of Cart)"); - reference81 = new ChangelogEntry("6.20", releaseDate39, list82); + span82[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quest (Larder Logistics)"); + reference81 = new ChangelogEntry("6.23", releaseDate37, list82); num2++; ref ChangelogEntry reference82 = ref span[num2]; - DateOnly releaseDate40 = new DateOnly(2025, 10, 9); + DateOnly releaseDate38 = new DateOnly(2025, 10, 12); num4 = 3; List list83 = new List(num4); CollectionsMarshal.SetCount(list83, num4); Span span83 = CollectionsMarshal.AsSpan(list83); num3 = 0; - span83[num3] = new ChangeEntry(EChangeCategory.Added, "Added config to batch Allied Society quest turn-ins"); + span83[num3] = new ChangeEntry(EChangeCategory.Changed, "Prevent disabled or Locked quests from being started as 'Start as next quest'"); num3++; - span83[num3] = new ChangeEntry(EChangeCategory.Changed, "Repeatable quests now show correct availability state in journal"); + span83[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quests"); num3++; - span83[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 2 quests"); - reference82 = new ChangelogEntry("6.19", releaseDate40, list83); + span83[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Yok Huy quest and journal quest chain priority issues"); + reference82 = new ChangelogEntry("6.22", releaseDate38, list83); num2++; ref ChangelogEntry reference83 = ref span[num2]; - DateOnly releaseDate41 = new DateOnly(2025, 10, 9); + DateOnly releaseDate39 = new DateOnly(2025, 10, 12); num3 = 2; List list84 = new List(num3); CollectionsMarshal.SetCount(list84, num3); Span span84 = CollectionsMarshal.AsSpan(list84); num4 = 0; - span84[num4] = new ChangeEntry(EChangeCategory.Changed, "Show once completed quests with improved state display"); + span84[num4] = new ChangeEntry(EChangeCategory.Added, "Added expansion abbreviation to journal window"); num4++; - span84[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy daily quest and improvements to various Yok Huy quests"); - reference83 = new ChangelogEntry("6.18", releaseDate41, list84); + span84[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quests"); + reference83 = new ChangelogEntry("6.21", releaseDate39, list84); num2++; ref ChangelogEntry reference84 = ref span[num2]; - DateOnly releaseDate42 = new DateOnly(2025, 10, 8); - num4 = 1; + DateOnly releaseDate40 = new DateOnly(2025, 10, 10); + num4 = 2; List list85 = new List(num4); CollectionsMarshal.SetCount(list85, num4); Span span85 = CollectionsMarshal.AsSpan(list85); num3 = 0; - span85[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 1 and rank 2 quests"); - reference84 = new ChangelogEntry("6.17", releaseDate42, list85); + span85[num3] = new ChangeEntry(EChangeCategory.Changed, "Allow completed repeatable quests to be used with 'Add quest and requirements to priority' feature"); + num3++; + span85[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 1 quest (A Work of Cart)"); + reference84 = new ChangelogEntry("6.20", releaseDate40, list85); num2++; ref ChangelogEntry reference85 = ref span[num2]; - DateOnly releaseDate43 = new DateOnly(2025, 10, 8); - num3 = 1; + DateOnly releaseDate41 = new DateOnly(2025, 10, 9); + num3 = 3; List list86 = new List(num3); CollectionsMarshal.SetCount(list86, num3); Span span86 = CollectionsMarshal.AsSpan(list86); num4 = 0; - span86[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Deep Dungeon quest (Faerie Tale)"); - reference85 = new ChangelogEntry("6.16", releaseDate43, list86); + span86[num4] = new ChangeEntry(EChangeCategory.Added, "Added config to batch Allied Society quest turn-ins"); + num4++; + span86[num4] = new ChangeEntry(EChangeCategory.Changed, "Repeatable quests now show correct availability state in journal"); + num4++; + span86[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 2 quests"); + reference85 = new ChangelogEntry("6.19", releaseDate41, list86); num2++; ref ChangelogEntry reference86 = ref span[num2]; - DateOnly releaseDate44 = new DateOnly(2025, 10, 8); + DateOnly releaseDate42 = new DateOnly(2025, 10, 9); num4 = 2; List list87 = new List(num4); CollectionsMarshal.SetCount(list87, num4); Span span87 = CollectionsMarshal.AsSpan(list87); num3 = 0; - span87[num3] = new ChangeEntry(EChangeCategory.Changed, "Dalamud cleanup"); + span87[num3] = new ChangeEntry(EChangeCategory.Changed, "Show once completed quests with improved state display"); num3++; - span87[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest level requirement check log spam"); - reference86 = new ChangelogEntry("6.15", releaseDate44, list87); + span87[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy daily quest and improvements to various Yok Huy quests"); + reference86 = new ChangelogEntry("6.18", releaseDate42, list87); num2++; ref ChangelogEntry reference87 = ref span[num2]; - DateOnly releaseDate45 = new DateOnly(2025, 10, 8); + DateOnly releaseDate43 = new DateOnly(2025, 10, 8); num3 = 1; List list88 = new List(num3); CollectionsMarshal.SetCount(list88, num3); Span span88 = CollectionsMarshal.AsSpan(list88); num4 = 0; - span88[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed abandoned quest check logic if quest were MSQ"); - reference87 = new ChangelogEntry("6.14", releaseDate45, list88); + span88[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 1 and rank 2 quests"); + reference87 = new ChangelogEntry("6.17", releaseDate43, list88); num2++; ref ChangelogEntry reference88 = ref span[num2]; - DateOnly releaseDate46 = new DateOnly(2025, 10, 8); - num4 = 2; + DateOnly releaseDate44 = new DateOnly(2025, 10, 8); + num4 = 1; List list89 = new List(num4); CollectionsMarshal.SetCount(list89, num4); Span span89 = CollectionsMarshal.AsSpan(list89); num3 = 0; - ref ChangeEntry reference89 = ref span89[num3]; - num5 = 3; - List list90 = new List(num5); - CollectionsMarshal.SetCount(list90, num5); - Span span90 = CollectionsMarshal.AsSpan(list90); - index = 0; - span90[index] = "Context menu option to add required quests and their chain to priority list"; - index++; - span90[index] = "AetheryteShortcut to multiple quests"; - index++; - span90[index] = "Artisan as a recommended plugin/dependency"; - reference89 = new ChangeEntry(EChangeCategory.Added, "Quest improvements", list90); - num3++; - span89[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed abandoned quest check and priority list issues"); - reference88 = new ChangelogEntry("6.13", releaseDate46, list89); + span89[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Deep Dungeon quest (Faerie Tale)"); + reference88 = new ChangelogEntry("6.16", releaseDate44, list89); + num2++; + ref ChangelogEntry reference89 = ref span[num2]; + DateOnly releaseDate45 = new DateOnly(2025, 10, 8); + num3 = 2; + List list90 = new List(num3); + CollectionsMarshal.SetCount(list90, num3); + Span span90 = CollectionsMarshal.AsSpan(list90); + num4 = 0; + span90[num4] = new ChangeEntry(EChangeCategory.Changed, "Dalamud cleanup"); + num4++; + span90[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest level requirement check log spam"); + reference89 = new ChangelogEntry("6.15", releaseDate45, list90); num2++; ref ChangelogEntry reference90 = ref span[num2]; - DateOnly releaseDate47 = new DateOnly(2025, 10, 7); - num3 = 4; - List list91 = new List(num3); - CollectionsMarshal.SetCount(list91, num3); + DateOnly releaseDate46 = new DateOnly(2025, 10, 8); + num4 = 1; + List list91 = new List(num4); + CollectionsMarshal.SetCount(list91, num4); Span span91 = CollectionsMarshal.AsSpan(list91); + num3 = 0; + span91[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed abandoned quest check logic if quest were MSQ"); + reference90 = new ChangelogEntry("6.14", releaseDate46, list91); + num2++; + ref ChangelogEntry reference91 = ref span[num2]; + DateOnly releaseDate47 = new DateOnly(2025, 10, 8); + num3 = 2; + List list92 = new List(num3); + CollectionsMarshal.SetCount(list92, num3); + Span span92 = CollectionsMarshal.AsSpan(list92); num4 = 0; - ref ChangeEntry reference91 = ref span91[num4]; - index = 4; - List list92 = new List(index); - CollectionsMarshal.SetCount(list92, index); - Span span92 = CollectionsMarshal.AsSpan(list92); - num5 = 0; - span92[num5] = "FATE combat handling with auto level syncing"; - num5++; - span92[num5] = "Start accepted quests from journal with 'Start as next quest'"; - num5++; - span92[num5] = "Update quest tracking when quests are hidden or prioritised in game"; - num5++; - span92[num5] = "QuestMap as a recommended plugin/dependency"; - reference91 = new ChangeEntry(EChangeCategory.Added, "FATE and quest tracking", list92); - num4++; - ref ChangeEntry reference92 = ref span91[num4]; + ref ChangeEntry reference92 = ref span92[num4]; num5 = 3; List list93 = new List(num5); CollectionsMarshal.SetCount(list93, num5); Span span93 = CollectionsMarshal.AsSpan(list93); - index = 0; - span93[index] = "Always prioritise next quest during teleportation/zone transitions"; - index++; - span93[index] = "Improved accepted quest logic with abandoned quest detection"; - index++; - span93[index] = "Show quests without quest paths as Locked"; - reference92 = new ChangeEntry(EChangeCategory.Changed, "Quest prioritisation improvements", list93); + num6 = 0; + span93[num6] = "Context menu option to add required quests and their chain to priority list"; + num6++; + span93[num6] = "AetheryteShortcut to multiple quests"; + num6++; + span93[num6] = "Artisan as a recommended plugin/dependency"; + reference92 = new ChangeEntry(EChangeCategory.Added, "Quest improvements", list93); num4++; - span91[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Deep Dungeon, Hildibrand, Yok Huy, Monster Hunter Wilds Collab, and Doman Enclave quests"); - num4++; - span91[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed accepted/active quest display and Hildibrand quest issues"); - reference90 = new ChangelogEntry("6.12", releaseDate47, list91); + span92[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed abandoned quest check and priority list issues"); + reference91 = new ChangelogEntry("6.13", releaseDate47, list92); num2++; ref ChangelogEntry reference93 = ref span[num2]; - DateOnly releaseDate48 = new DateOnly(2025, 10, 3); - num4 = 1; + DateOnly releaseDate48 = new DateOnly(2025, 10, 7); + num4 = 4; List list94 = new List(num4); CollectionsMarshal.SetCount(list94, num4); Span span94 = CollectionsMarshal.AsSpan(list94); num3 = 0; - span94[num3] = new ChangeEntry(EChangeCategory.Changed, "Added remaining checks for quest priority to prevent infinite teleport looping"); - reference93 = new ChangelogEntry("6.11", releaseDate48, list94); - num2++; - ref ChangelogEntry reference94 = ref span[num2]; - DateOnly releaseDate49 = new DateOnly(2025, 10, 2); - num3 = 1; - List list95 = new List(num3); - CollectionsMarshal.SetCount(list95, num3); - Span span95 = CollectionsMarshal.AsSpan(list95); - num4 = 0; - ref ChangeEntry reference95 = ref span95[num4]; - index = 2; - List list96 = new List(index); - CollectionsMarshal.SetCount(list96, index); - Span span96 = CollectionsMarshal.AsSpan(list96); + ref ChangeEntry reference94 = ref span94[num3]; + num6 = 4; + List list95 = new List(num6); + CollectionsMarshal.SetCount(list95, num6); + Span span95 = CollectionsMarshal.AsSpan(list95); num5 = 0; - span96[num5] = "Don't show quests as available if player doesn't meet level requirements"; + span95[num5] = "FATE combat handling with auto level syncing"; num5++; - span96[num5] = "Updated 'required for MSQ' text in Crystal Tower quest preset window"; - reference95 = new ChangeEntry(EChangeCategory.Changed, "Quest window improvements", list96); - reference94 = new ChangelogEntry("6.10", releaseDate49, list95); + span95[num5] = "Start accepted quests from journal with 'Start as next quest'"; + num5++; + span95[num5] = "Update quest tracking when quests are hidden or prioritised in game"; + num5++; + span95[num5] = "QuestMap as a recommended plugin/dependency"; + reference94 = new ChangeEntry(EChangeCategory.Added, "FATE and quest tracking", list95); + num3++; + ref ChangeEntry reference95 = ref span94[num3]; + num5 = 3; + List list96 = new List(num5); + CollectionsMarshal.SetCount(list96, num5); + Span span96 = CollectionsMarshal.AsSpan(list96); + num6 = 0; + span96[num6] = "Always prioritise next quest during teleportation/zone transitions"; + num6++; + span96[num6] = "Improved accepted quest logic with abandoned quest detection"; + num6++; + span96[num6] = "Show quests without quest paths as Locked"; + reference95 = new ChangeEntry(EChangeCategory.Changed, "Quest prioritisation improvements", list96); + num3++; + span94[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Deep Dungeon, Hildibrand, Yok Huy, Monster Hunter Wilds Collab, and Doman Enclave quests"); + num3++; + span94[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed accepted/active quest display and Hildibrand quest issues"); + reference93 = new ChangelogEntry("6.12", releaseDate48, list94); num2++; ref ChangelogEntry reference96 = ref span[num2]; - DateOnly releaseDate50 = new DateOnly(2025, 9, 21); - num4 = 5; - List list97 = new List(num4); - CollectionsMarshal.SetCount(list97, num4); + DateOnly releaseDate49 = new DateOnly(2025, 10, 3); + num3 = 1; + List list97 = new List(num3); + CollectionsMarshal.SetCount(list97, num3); Span span97 = CollectionsMarshal.AsSpan(list97); + num4 = 0; + span97[num4] = new ChangeEntry(EChangeCategory.Changed, "Added remaining checks for quest priority to prevent infinite teleport looping"); + reference96 = new ChangelogEntry("6.11", releaseDate49, list97); + num2++; + ref ChangelogEntry reference97 = ref span[num2]; + DateOnly releaseDate50 = new DateOnly(2025, 10, 2); + num4 = 1; + List list98 = new List(num4); + CollectionsMarshal.SetCount(list98, num4); + Span span98 = CollectionsMarshal.AsSpan(list98); num3 = 0; - ref ChangeEntry reference97 = ref span97[num3]; - num5 = 4; - List list98 = new List(num5); - CollectionsMarshal.SetCount(list98, num5); - Span span98 = CollectionsMarshal.AsSpan(list98); - index = 0; - span98[index] = "Reworked event quest handling - automatically displays when events are active"; - index++; - span98[index] = "Reworked journal system with improved filtering and display"; - index++; - span98[index] = "Reworked Priority Quests tab (Manual Priority and Quest Presets)"; - index++; - span98[index] = "Quest path viewer site (https://wigglymuffin.github.io/FFXIV-Tools/)"; - reference97 = new ChangeEntry(EChangeCategory.Added, "Major system reworks", list98); - num3++; - ref ChangeEntry reference98 = ref span97[num3]; - index = 4; - List list99 = new List(index); - CollectionsMarshal.SetCount(list99, index); + ref ChangeEntry reference98 = ref span98[num3]; + num6 = 2; + List list99 = new List(num6); + CollectionsMarshal.SetCount(list99, num6); Span span99 = CollectionsMarshal.AsSpan(list99); num5 = 0; - span99[num5] = "Questionable.IsQuestCompleted"; + span99[num5] = "Don't show quests as available if player doesn't meet level requirements"; num5++; - span99[num5] = "Questionable.IsQuestAvailable"; - num5++; - span99[num5] = "Questionable.IsQuestAccepted"; - num5++; - span99[num5] = "Questionable.IsQuestUnobtainable"; - reference98 = new ChangeEntry(EChangeCategory.Added, "New IPC commands", list99); - num3++; - ref ChangeEntry reference99 = ref span97[num3]; - num5 = 5; - List list100 = new List(num5); - CollectionsMarshal.SetCount(list100, num5); - Span span100 = CollectionsMarshal.AsSpan(list100); - index = 0; - span100[index] = "Improved JSON quest validation with specific error reasons"; - index++; - span100[index] = "Added stop at sequence stop condition"; - index++; - span100[index] = "Improved Pandora plugin conflict detection"; - index++; - span100[index] = "Improved DialogueChoices regex matching"; - index++; - span100[index] = "Improved refresh checker for all quest states"; - reference99 = new ChangeEntry(EChangeCategory.Changed, "Various improvements", list100); - num3++; - span97[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.31 Occult Crescent quests"); - num3++; - span97[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed cutscene crashes, Single Player Duty triggers, and various quest issues"); - reference96 = new ChangelogEntry("6.9", releaseDate50, list97); + span99[num5] = "Updated 'required for MSQ' text in Crystal Tower quest preset window"; + reference98 = new ChangeEntry(EChangeCategory.Changed, "Quest window improvements", list99); + reference97 = new ChangelogEntry("6.10", releaseDate50, list98); num2++; - ref ChangelogEntry reference100 = ref span[num2]; - DateOnly releaseDate51 = new DateOnly(2025, 9, 2); - num3 = 4; - List list101 = new List(num3); - CollectionsMarshal.SetCount(list101, num3); - Span span101 = CollectionsMarshal.AsSpan(list101); + ref ChangelogEntry reference99 = ref span[num2]; + DateOnly releaseDate51 = new DateOnly(2025, 9, 21); + num3 = 5; + List list100 = new List(num3); + CollectionsMarshal.SetCount(list100, num3); + Span span100 = CollectionsMarshal.AsSpan(list100); num4 = 0; - ref ChangeEntry reference101 = ref span101[num4]; - index = 4; - List list102 = new List(index); - CollectionsMarshal.SetCount(list102, index); + ref ChangeEntry reference100 = ref span100[num4]; + num5 = 4; + List list101 = new List(num5); + CollectionsMarshal.SetCount(list101, num5); + Span span101 = CollectionsMarshal.AsSpan(list101); + num6 = 0; + span101[num6] = "Reworked event quest handling - automatically displays when events are active"; + num6++; + span101[num6] = "Reworked journal system with improved filtering and display"; + num6++; + span101[num6] = "Reworked Priority Quests tab (Manual Priority and Quest Presets)"; + num6++; + span101[num6] = "Quest path viewer site (https://wigglymuffin.github.io/FFXIV-Tools/)"; + reference100 = new ChangeEntry(EChangeCategory.Added, "Major system reworks", list101); + num4++; + ref ChangeEntry reference101 = ref span100[num4]; + num6 = 4; + List list102 = new List(num6); + CollectionsMarshal.SetCount(list102, num6); Span span102 = CollectionsMarshal.AsSpan(list102); num5 = 0; - span102[num5] = "Help commands and priority quest command"; + span102[num5] = "Questionable.IsQuestCompleted"; num5++; - span102[num5] = "Prevent 'CompleteQuest' step setting"; + span102[num5] = "Questionable.IsQuestAvailable"; num5++; - span102[num5] = "Duty counts and controls in 'Quest Battles' tab"; + span102[num5] = "Questionable.IsQuestAccepted"; num5++; - span102[num5] = "'Refresh quest timer' setting (WIP)"; - reference101 = new ChangeEntry(EChangeCategory.Added, "Command and UI improvements", list102); + span102[num5] = "Questionable.IsQuestUnobtainable"; + reference101 = new ChangeEntry(EChangeCategory.Added, "New IPC commands", list102); num4++; - span101[num4] = new ChangeEntry(EChangeCategory.Changed, "Improved 'Clear All' buttons to require CTRL being held"); + ref ChangeEntry reference102 = ref span100[num4]; + num5 = 5; + List list103 = new List(num5); + CollectionsMarshal.SetCount(list103, num5); + Span span103 = CollectionsMarshal.AsSpan(list103); + num6 = 0; + span103[num6] = "Improved JSON quest validation with specific error reasons"; + num6++; + span103[num6] = "Added stop at sequence stop condition"; + num6++; + span103[num6] = "Improved Pandora plugin conflict detection"; + num6++; + span103[num6] = "Improved DialogueChoices regex matching"; + num6++; + span103[num6] = "Improved refresh checker for all quest states"; + reference102 = new ChangeEntry(EChangeCategory.Changed, "Various improvements", list103); num4++; - span101[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Zodiac quests and 7.31 Cosmic/Occult Crescent quests"); + span100[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.31 Occult Crescent quests"); num4++; - span101[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Fishing for Friendship and Cosmic Exploration quests"); - reference100 = new ChangelogEntry("6.8", releaseDate51, list101); + span100[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed cutscene crashes, Single Player Duty triggers, and various quest issues"); + reference99 = new ChangelogEntry("6.9", releaseDate51, list100); num2++; - ref ChangelogEntry reference102 = ref span[num2]; - DateOnly releaseDate52 = new DateOnly(2025, 8, 27); + ref ChangelogEntry reference103 = ref span[num2]; + DateOnly releaseDate52 = new DateOnly(2025, 9, 2); num4 = 4; - List list103 = new List(num4); - CollectionsMarshal.SetCount(list103, num4); - Span span103 = CollectionsMarshal.AsSpan(list103); + List list104 = new List(num4); + CollectionsMarshal.SetCount(list104, num4); + Span span104 = CollectionsMarshal.AsSpan(list104); num3 = 0; - ref ChangeEntry reference103 = ref span103[num3]; - num5 = 2; - List list104 = new List(num5); - CollectionsMarshal.SetCount(list104, num5); - Span span104 = CollectionsMarshal.AsSpan(list104); - index = 0; - span104[index] = "Icon to 'Clear All' button in stop conditions"; - index++; - span104[index] = "Duty counts and 'Enable All' button in 'Duties' tab"; - reference103 = new ChangeEntry(EChangeCategory.Added, "UI improvements", list104); - num3++; - span103[num3] = new ChangeEntry(EChangeCategory.Changed, "Renamed 'Clear' button to 'Clear All' in priority window"); - num3++; - span103[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Rising 2025 Event Quests"); - num3++; - span103[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed clipboard assigning blacklist to whitelist in 'Duties' tab"); - reference102 = new ChangelogEntry("6.7", releaseDate52, list103); - num2++; - ref ChangelogEntry reference104 = ref span[num2]; - DateOnly releaseDate53 = new DateOnly(2025, 8, 25); - num3 = 2; - List list105 = new List(num3); - CollectionsMarshal.SetCount(list105, num3); - Span span105 = CollectionsMarshal.AsSpan(list105); - num4 = 0; - ref ChangeEntry reference105 = ref span105[num4]; - index = 2; - List list106 = new List(index); - CollectionsMarshal.SetCount(list106, index); - Span span106 = CollectionsMarshal.AsSpan(list106); + ref ChangeEntry reference104 = ref span104[num3]; + num6 = 4; + List list105 = new List(num6); + CollectionsMarshal.SetCount(list105, num6); + Span span105 = CollectionsMarshal.AsSpan(list105); num5 = 0; - span106[num5] = "Missing emotes to schema and emote handler"; + span105[num5] = "Help commands and priority quest command"; num5++; - span106[num5] = "Improved stop conditions with 'Clear All' button"; - reference105 = new ChangeEntry(EChangeCategory.Added, "Emote support and stop conditions", list106); - num4++; - span105[num4] = new ChangeEntry(EChangeCategory.Changed, "Stop at level functionality"); - reference104 = new ChangelogEntry("6.6", releaseDate53, list105); + span105[num5] = "Prevent 'CompleteQuest' step setting"; + num5++; + span105[num5] = "Duty counts and controls in 'Quest Battles' tab"; + num5++; + span105[num5] = "'Refresh quest timer' setting (WIP)"; + reference104 = new ChangeEntry(EChangeCategory.Added, "Command and UI improvements", list105); + num3++; + span104[num3] = new ChangeEntry(EChangeCategory.Changed, "Improved 'Clear All' buttons to require CTRL being held"); + num3++; + span104[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Zodiac quests and 7.31 Cosmic/Occult Crescent quests"); + num3++; + span104[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Fishing for Friendship and Cosmic Exploration quests"); + reference103 = new ChangelogEntry("6.8", releaseDate52, list104); num2++; - ref ChangelogEntry reference106 = ref span[num2]; + ref ChangelogEntry reference105 = ref span[num2]; + DateOnly releaseDate53 = new DateOnly(2025, 8, 27); + num3 = 4; + List list106 = new List(num3); + CollectionsMarshal.SetCount(list106, num3); + Span span106 = CollectionsMarshal.AsSpan(list106); + num4 = 0; + ref ChangeEntry reference106 = ref span106[num4]; + num5 = 2; + List list107 = new List(num5); + CollectionsMarshal.SetCount(list107, num5); + Span span107 = CollectionsMarshal.AsSpan(list107); + num6 = 0; + span107[num6] = "Icon to 'Clear All' button in stop conditions"; + num6++; + span107[num6] = "Duty counts and 'Enable All' button in 'Duties' tab"; + reference106 = new ChangeEntry(EChangeCategory.Added, "UI improvements", list107); + num4++; + span106[num4] = new ChangeEntry(EChangeCategory.Changed, "Renamed 'Clear' button to 'Clear All' in priority window"); + num4++; + span106[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Rising 2025 Event Quests"); + num4++; + span106[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed clipboard assigning blacklist to whitelist in 'Duties' tab"); + reference105 = new ChangelogEntry("6.7", releaseDate53, list106); + num2++; + ref ChangelogEntry reference107 = ref span[num2]; DateOnly releaseDate54 = new DateOnly(2025, 8, 25); num4 = 2; - List list107 = new List(num4); - CollectionsMarshal.SetCount(list107, num4); - Span span107 = CollectionsMarshal.AsSpan(list107); + List list108 = new List(num4); + CollectionsMarshal.SetCount(list108, num4); + Span span108 = CollectionsMarshal.AsSpan(list108); num3 = 0; - span107[num3] = new ChangeEntry(EChangeCategory.Fixed, "Potential fix to single/solo duties softlocking"); + ref ChangeEntry reference108 = ref span108[num3]; + num6 = 2; + List list109 = new List(num6); + CollectionsMarshal.SetCount(list109, num6); + Span span109 = CollectionsMarshal.AsSpan(list109); + num5 = 0; + span109[num5] = "Missing emotes to schema and emote handler"; + num5++; + span109[num5] = "Improved stop conditions with 'Clear All' button"; + reference108 = new ChangeEntry(EChangeCategory.Added, "Emote support and stop conditions", list109); num3++; - span107[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added San d'Oria: The Second Walk and various side quests"); - reference106 = new ChangelogEntry("6.5", releaseDate54, list107); + span108[num3] = new ChangeEntry(EChangeCategory.Changed, "Stop at level functionality"); + reference107 = new ChangelogEntry("6.6", releaseDate54, list108); + num2++; + ref ChangelogEntry reference109 = ref span[num2]; + DateOnly releaseDate55 = new DateOnly(2025, 8, 25); + num3 = 2; + List list110 = new List(num3); + CollectionsMarshal.SetCount(list110, num3); + Span span110 = CollectionsMarshal.AsSpan(list110); + num4 = 0; + span110[num4] = new ChangeEntry(EChangeCategory.Fixed, "Potential fix to single/solo duties softlocking"); + num4++; + span110[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added San d'Oria: The Second Walk and various side quests"); + reference109 = new ChangelogEntry("6.5", releaseDate55, list110); Changelogs = list; } } diff --git a/Questionable/Questionable.Data/DutyInfo.cs b/Questionable/Questionable.Data/DutyInfo.cs new file mode 100644 index 0000000..b37e13e --- /dev/null +++ b/Questionable/Questionable.Data/DutyInfo.cs @@ -0,0 +1,22 @@ +using System.Collections.Immutable; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using Questionable.Model.Questing; + +namespace Questionable.Data; + +public sealed record DutyInfo(uint ContentFinderConditionId, uint ContentId, string Name, uint ContentTypeId, string ContentTypeName, ushort Level, ushort ItemLevel, ImmutableList UnlockQuests, bool IsHighEndDuty, EDutyCategory DutyCategory) +{ + public bool IsUnlocked => UIState.IsInstanceContentUnlocked(ContentId); + + public bool? IsCompleted + { + get + { + if (DutyCategory != EDutyCategory.DeepDungeon) + { + return UIState.IsInstanceContentCompleted((ushort)ContentId); + } + return null; + } + } +} diff --git a/Questionable/Questionable.Data/DutyUnlockData.cs b/Questionable/Questionable.Data/DutyUnlockData.cs new file mode 100644 index 0000000..5a1c67d --- /dev/null +++ b/Questionable/Questionable.Data/DutyUnlockData.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Dalamud.Game; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using Lumina.Excel; +using Lumina.Excel.Sheets; +using Microsoft.Extensions.Logging; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Data; + +internal sealed class DutyUnlockData +{ + private static class ContentTypeIds + { + public const uint Dungeon = 2u; + + public const uint Guildhest = 3u; + + public const uint Trial = 4u; + + public const uint Raid = 5u; + + public const uint PvP = 6u; + + public const uint QuestBattle = 7u; + + public const uint DeepDungeon = 21u; + + public const uint AllianceRaid = 26u; + + public const uint Ultimate = 28u; + + public const uint VCDungeonFinder = 29u; + + public const uint Criterion = 30u; + + public const uint CriterionSavage = 31u; + } + + private static class ContentMemberTypeIds + { + public const byte LightParty = 2; + + public const byte FullParty = 3; + + public const byte Alliance = 4; + } + + private readonly ImmutableDictionary _duties; + + private readonly ImmutableDictionary> _questToDuties; + + private readonly ImmutableDictionary> _dutyToQuests; + + private readonly ILogger _logger; + + private static readonly HashSet VariantDungeonCfcIds = new HashSet { 868u, 945u, 961u }; + + public DutyUnlockData(IDataManager dataManager, ILogger logger) + { + _logger = logger; + ExcelSheet excelSheet = dataManager.GetExcelSheet(); + ExcelSheet excelSheet2 = dataManager.GetExcelSheet(); + ImmutableDictionary immutableDictionary = (from c in excelSheet + where c.RowId != 0 && c.Content.RowId != 0 && c.ContentLinkType == 1 + group c by c.Content.RowId).ToImmutableDictionary((IGrouping g) => g.Key, (IGrouping g) => g.First().RowId); + Dictionary> dictionary = new Dictionary>(); + Dictionary> dictionary2 = new Dictionary>(); + foreach (QuestEx item in excelSheet2.Where((QuestEx q) => q.RowId != 0)) + { + uint rowId = item.InstanceContentUnlock.RowId; + if (rowId != 0 && immutableDictionary.TryGetValue(rowId, out var value)) + { + QuestId questId = QuestId.FromRowId(item.RowId); + if (!dictionary.ContainsKey(questId)) + { + dictionary[questId] = new List(); + } + dictionary[questId].Add(value); + if (!dictionary2.ContainsKey(value)) + { + dictionary2[value] = new List(); + } + dictionary2[value].Add(questId); + } + } + _questToDuties = dictionary.ToImmutableDictionary((KeyValuePair> x) => x.Key, (KeyValuePair> x) => x.Value.ToImmutableList()); + _dutyToQuests = dictionary2.ToImmutableDictionary((KeyValuePair> x) => x.Key, (KeyValuePair> x) => x.Value.ToImmutableList()); + Dictionary dictionary3 = new Dictionary(); + foreach (ContentFinderCondition item2 in excelSheet.Where((ContentFinderCondition x) => x.RowId != 0 && x.Content.RowId != 0)) + { + if (item2.ContentLinkType != 1) + { + continue; + } + ContentType? valueNullable = item2.ContentType.ValueNullable; + if (!valueNullable.HasValue) + { + continue; + } + uint rowId2 = valueNullable.Value.RowId; + if (rowId2 - 6 > 1) + { + string text = FixName(item2.Name.ToDalamudString().ToString(), dataManager.Language); + if (!string.IsNullOrWhiteSpace(text) && !IsExcludedContent(text, item2.RowId)) + { + List value2; + ImmutableList unlockQuests = (dictionary2.TryGetValue(item2.RowId, out value2) ? value2.ToImmutableList() : ImmutableList.Empty); + uint rowId3 = item2.ContentMemberType.RowId; + EDutyCategory dutyCategory = DetermineDutyCategory(rowId2, text, item2.HighEndDuty, item2.RowId, (byte)rowId3); + DutyInfo value3 = new DutyInfo(item2.RowId, item2.Content.RowId, text, rowId2, valueNullable.Value.Name.ToDalamudString().ToString(), item2.ClassJobLevelRequired, item2.ItemLevelRequired, unlockQuests, item2.HighEndDuty, dutyCategory); + dictionary3[item2.RowId] = value3; + } + } + } + _duties = dictionary3.ToImmutableDictionary(); + _logger.LogInformation("Loaded {DutyCount} duties with {QuestMappings} quest unlock mappings", _duties.Count, _questToDuties.Values.Sum((ImmutableList x) => x.Count)); + } + + private static bool IsExcludedContent(string name, uint cfcId) + { + if (cfcId - 69 <= 2) + { + return true; + } + return false; + } + + private static EDutyCategory DetermineDutyCategory(uint contentTypeId, string name, bool isHighEndDuty, uint cfcId, byte contentMemberType) + { + if (VariantDungeonCfcIds.Contains(cfcId)) + { + return EDutyCategory.VariantDungeon; + } + switch (contentTypeId) + { + case 2u: + if (name.EndsWith("(Hard)", StringComparison.OrdinalIgnoreCase)) + { + return EDutyCategory.HardDungeon; + } + return EDutyCategory.Dungeon; + case 3u: + return EDutyCategory.Guildhest; + case 4u: + if (name.EndsWith("(Unreal)", StringComparison.OrdinalIgnoreCase)) + { + return EDutyCategory.UnrealTrial; + } + if (name.EndsWith("(Extreme)", StringComparison.OrdinalIgnoreCase) || name.StartsWith("The Minstrel's Ballad:", StringComparison.OrdinalIgnoreCase) || isHighEndDuty) + { + return EDutyCategory.ExtremeTrial; + } + if (name.EndsWith("(Hard)", StringComparison.OrdinalIgnoreCase)) + { + return EDutyCategory.HardTrial; + } + return EDutyCategory.Trial; + case 5u: + if (contentMemberType == 4) + { + if (isHighEndDuty) + { + return EDutyCategory.ChaoticAllianceRaid; + } + return EDutyCategory.AllianceRaid; + } + if (name.EndsWith("(Savage)", StringComparison.OrdinalIgnoreCase) || isHighEndDuty) + { + return EDutyCategory.SavageRaid; + } + return EDutyCategory.NormalRaid; + case 21u: + return EDutyCategory.DeepDungeon; + case 26u: + if (isHighEndDuty) + { + return EDutyCategory.ChaoticAllianceRaid; + } + return EDutyCategory.AllianceRaid; + case 28u: + return EDutyCategory.Ultimate; + case 29u: + if (name.EndsWith("(Savage)", StringComparison.OrdinalIgnoreCase) || isHighEndDuty) + { + return EDutyCategory.CriterionSavage; + } + return EDutyCategory.CriterionDungeon; + case 30u: + if (name.EndsWith("(Savage)", StringComparison.OrdinalIgnoreCase) || isHighEndDuty) + { + return EDutyCategory.CriterionSavage; + } + return EDutyCategory.CriterionDungeon; + case 31u: + return EDutyCategory.CriterionSavage; + default: + if (name.EndsWith("(Savage)", StringComparison.OrdinalIgnoreCase)) + { + return EDutyCategory.SavageRaid; + } + if (name.EndsWith("(Extreme)", StringComparison.OrdinalIgnoreCase) || name.StartsWith("The Minstrel's Ballad:", StringComparison.OrdinalIgnoreCase)) + { + return EDutyCategory.ExtremeTrial; + } + return EDutyCategory.Other; + } + } + + private static string FixName(string name, ClientLanguage language) + { + if (string.IsNullOrEmpty(name) || language != ClientLanguage.English) + { + return name; + } + return string.Concat(name[0].ToString().ToUpper(CultureInfo.InvariantCulture).AsSpan(), name.AsSpan(1)); + } + + public IEnumerable GetAllDuties() + { + return _duties.Values; + } + + public IEnumerable GetDutiesByContentType(uint contentTypeId) + { + return _duties.Values.Where((DutyInfo d) => d.ContentTypeId == contentTypeId); + } + + public IEnumerable GetDutiesByCategory(EDutyCategory category) + { + return _duties.Values.Where((DutyInfo d) => d.DutyCategory == category); + } + + public IEnumerable GetDungeons() + { + return GetDutiesByCategory(EDutyCategory.Dungeon); + } + + public IEnumerable GetHardDungeons() + { + return GetDutiesByCategory(EDutyCategory.HardDungeon); + } + + public IEnumerable GetGuildhests() + { + return GetDutiesByCategory(EDutyCategory.Guildhest); + } + + public IEnumerable GetTrials() + { + return GetDutiesByCategory(EDutyCategory.Trial); + } + + public IEnumerable GetHardTrials() + { + return GetDutiesByCategory(EDutyCategory.HardTrial); + } + + public IEnumerable GetExtremeTrials() + { + return GetDutiesByCategory(EDutyCategory.ExtremeTrial); + } + + public IEnumerable GetUnrealTrials() + { + return GetDutiesByCategory(EDutyCategory.UnrealTrial); + } + + public IEnumerable GetNormalRaids() + { + return GetDutiesByCategory(EDutyCategory.NormalRaid); + } + + public IEnumerable GetSavageRaids() + { + return GetDutiesByCategory(EDutyCategory.SavageRaid); + } + + public IEnumerable GetUltimateRaids() + { + return GetDutiesByCategory(EDutyCategory.Ultimate); + } + + public IEnumerable GetAllianceRaids() + { + return GetDutiesByCategory(EDutyCategory.AllianceRaid); + } + + public IEnumerable GetChaoticAllianceRaids() + { + return GetDutiesByCategory(EDutyCategory.ChaoticAllianceRaid); + } + + public IEnumerable GetDeepDungeons() + { + return GetDutiesByCategory(EDutyCategory.DeepDungeon); + } + + public IEnumerable GetVariantDungeons() + { + return GetDutiesByCategory(EDutyCategory.VariantDungeon); + } + + public IEnumerable GetCriterionDungeons() + { + return GetDutiesByCategory(EDutyCategory.CriterionDungeon); + } + + public IEnumerable GetCriterionSavageDungeons() + { + return GetDutiesByCategory(EDutyCategory.CriterionSavage); + } + + public IEnumerable GetDutiesUnlockedByQuest(QuestId questId) + { + if (!_questToDuties.TryGetValue(questId, out ImmutableList value)) + { + yield break; + } + foreach (uint item in value) + { + if (_duties.TryGetValue(item, out DutyInfo value2)) + { + yield return value2; + } + } + } + + public IEnumerable GetUnlockQuestsForDuty(uint cfcId) + { + if (_dutyToQuests.TryGetValue(cfcId, out ImmutableList value)) + { + return value; + } + return Array.Empty(); + } + + public bool IsDutyUnlocked(uint cfcId) + { + if (!_duties.TryGetValue(cfcId, out DutyInfo value)) + { + return false; + } + return UIState.IsInstanceContentUnlocked(value.ContentId); + } + + public static bool IsContentUnlocked(uint contentId) + { + return UIState.IsInstanceContentUnlocked(contentId); + } + + public bool TryGetDuty(uint cfcId, [NotNullWhen(true)] out DutyInfo? duty) + { + return _duties.TryGetValue(cfcId, out duty); + } + + public DutyInfo? GetDuty(uint cfcId) + { + return _duties.GetValueOrDefault(cfcId); + } + + public IEnumerable GetUnlockedDuties() + { + return _duties.Values.Where((DutyInfo d) => UIState.IsInstanceContentUnlocked(d.ContentId)); + } + + public IEnumerable GetLockedDuties() + { + return _duties.Values.Where((DutyInfo d) => !UIState.IsInstanceContentUnlocked(d.ContentId)); + } + + public IEnumerable GetDutiesWithUnlockQuests() + { + return _duties.Values.Where((DutyInfo d) => d.UnlockQuests.Count > 0); + } +} diff --git a/Questionable/Questionable.Data/EDutyCategory.cs b/Questionable/Questionable.Data/EDutyCategory.cs new file mode 100644 index 0000000..b1a6911 --- /dev/null +++ b/Questionable/Questionable.Data/EDutyCategory.cs @@ -0,0 +1,22 @@ +namespace Questionable.Data; + +public enum EDutyCategory +{ + Dungeon, + HardDungeon, + Guildhest, + Trial, + HardTrial, + ExtremeTrial, + UnrealTrial, + NormalRaid, + SavageRaid, + AllianceRaid, + ChaoticAllianceRaid, + Ultimate, + DeepDungeon, + VariantDungeon, + CriterionDungeon, + CriterionSavage, + Other +} diff --git a/Questionable/Questionable.Data/QuestData.cs b/Questionable/Questionable.Data/QuestData.cs index 764abcf..c406f63 100644 --- a/Questionable/Questionable.Data/QuestData.cs +++ b/Questionable/Questionable.Data/QuestData.cs @@ -111,7 +111,7 @@ internal sealed class QuestData list3.Add(0); list3.AddRange((from QuestInfo y in quests.Where((IQuestInfo y) => (uint)y.AlliedSociety == (byte)x.RowId && y.IsRepeatable) select (byte)y.AlliedSocietyRank).Distinct()); - return new _003C_003Ez__ReadOnlyList(list3).Select((byte rank) => new AlliedSocietyDailyInfo(x, rank, classJobUtils)); + return new global::_003C_003Ez__ReadOnlyList(list3).Select((byte rank) => new AlliedSocietyDailyInfo(x, rank, classJobUtils)); } return new global::_003C_003Ez__ReadOnlySingleElementList(new AlliedSocietyDailyInfo(x, 0, classJobUtils)); })); diff --git a/Questionable/Questionable.External/AutomatonIpc.cs b/Questionable/Questionable.External/AutomatonIpc.cs index de6e379..982a24c 100644 --- a/Questionable/Questionable.External/AutomatonIpc.cs +++ b/Questionable/Questionable.External/AutomatonIpc.cs @@ -1,42 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; +using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; +using Questionable.Controller; +using Questionable.Data; namespace Questionable.External; -internal sealed class AutomatonIpc +internal sealed class AutomatonIpc : IDisposable { + private static readonly ImmutableHashSet ConflictingTweaks = new HashSet + { + "DateWithDestiny", "AutoFollow", "AutoPillion", "AutoInvite", "AutoBusy", "AutoEquipXPBoosts", "FateToolKit", "AutoMerge", "AutoQueue", "EnhancedDutyStartEnd", + "EnhancedLoginLogout", "GettingTooAttached", "RetrieveMateria" + }.ToImmutableHashSet(); + + private const string AutoSnipeTweak = "AutoSnipeQuests"; + + private readonly Configuration _configuration; + + private readonly QuestController _questController; + + private readonly TerritoryData _territoryData; + + private readonly IClientState _clientState; + private readonly ILogger _logger; private readonly ICallGateSubscriber _isTweakEnabled; - private bool _loggedIpcError; + private readonly ICallGateSubscriber _setTweakState; - public bool IsAutoSnipeEnabled + private HashSet? _pausedTweaks; + + public AutomatonIpc(Configuration configuration, IDalamudPluginInterface pluginInterface, QuestController questController, TerritoryData territoryData, IClientState clientState, ILogger logger) { - get + _configuration = configuration; + _questController = questController; + _territoryData = territoryData; + _clientState = clientState; + _logger = logger; + _isTweakEnabled = pluginInterface.GetIpcSubscriber("Automaton.IsTweakEnabled"); + _setTweakState = pluginInterface.GetIpcSubscriber("Automaton.SetTweakState"); + _questController.AutomationTypeChanged += OnAutomationTypeChanged; + } + + private void OnAutomationTypeChanged(object sender, QuestController.EAutomationType automationType) + { + if (automationType != QuestController.EAutomationType.Manual && !_territoryData.IsDutyInstance(_clientState.TerritoryType)) { - try - { - return _isTweakEnabled.InvokeFunc("AutoSnipeQuests"); - } - catch (IpcError exception) - { - if (!_loggedIpcError) - { - _loggedIpcError = true; - _logger.LogWarning(exception, "Could not query automaton for tweak status, probably not installed"); - } - return false; - } + Task.Run((Action)DisableConflictingTweaks); + } + else + { + Task.Run((Action)RestoreConflictingTweaks); } } - public AutomatonIpc(IDalamudPluginInterface pluginInterface, ILogger logger) + private void DisableConflictingTweaks() { - _logger = logger; - _isTweakEnabled = pluginInterface.GetIpcSubscriber("Automaton.IsTweakEnabled"); - logger.LogInformation("Automaton auto-snipe enabled: {IsTweakEnabled}", IsAutoSnipeEnabled); + if (_pausedTweaks != null) + { + return; + } + _pausedTweaks = new HashSet(); + foreach (string conflictingTweak in ConflictingTweaks) + { + TryDisableTweak(conflictingTweak); + } + if (_configuration.General.AutoSnipe) + { + TryDisableTweak("AutoSnipeQuests"); + } + } + + private void TryDisableTweak(string tweak) + { + try + { + if (_isTweakEnabled.InvokeFunc(tweak)) + { + _setTweakState.InvokeAction(tweak, arg2: false); + _pausedTweaks.Add(tweak); + _logger.LogInformation("Paused Automaton tweak: {Tweak}", tweak); + } + } + catch (IpcError) + { + } + } + + private void RestoreConflictingTweaks() + { + if (_pausedTweaks == null) + { + return; + } + foreach (string pausedTweak in _pausedTweaks) + { + try + { + _setTweakState.InvokeAction(pausedTweak, arg2: true); + _logger.LogInformation("Restored Automaton tweak: {Tweak}", pausedTweak); + } + catch (IpcError) + { + } + } + _pausedTweaks = null; + } + + public void Dispose() + { + _questController.AutomationTypeChanged -= OnAutomationTypeChanged; + RestoreConflictingTweaks(); } } diff --git a/Questionable/Questionable.External/PandorasBoxIpc.cs b/Questionable/Questionable.External/PandorasBoxIpc.cs index bd17117..9a8e546 100644 --- a/Questionable/Questionable.External/PandorasBoxIpc.cs +++ b/Questionable/Questionable.External/PandorasBoxIpc.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Threading.Tasks; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; @@ -20,7 +21,9 @@ internal sealed class PandorasBoxIpc : IDisposable "Pandora Quick Gather", "Switch Gatherers Automatically" }.ToImmutableHashSet(); - private readonly IFramework _framework; + private const string AutoActiveTimeManeuverFeature = "Auto Active Time Maneuver"; + + private readonly Configuration _configuration; private readonly QuestController _questController; @@ -34,58 +37,35 @@ internal sealed class PandorasBoxIpc : IDisposable private readonly ICallGateSubscriber _setFeatureEnabled; - private bool _loggedIpcError; - private HashSet? _pausedFeatures; - public bool IsAutoActiveTimeManeuverEnabled + public PandorasBoxIpc(Configuration configuration, IDalamudPluginInterface pluginInterface, QuestController questController, TerritoryData territoryData, IClientState clientState, ILogger logger) { - get - { - try - { - return _getFeatureEnabled.InvokeFunc("Auto Active Time Maneuver") == true; - } - catch (IpcError exception) - { - if (!_loggedIpcError) - { - _loggedIpcError = true; - _logger.LogWarning(exception, "Could not query pandora's box for feature status, probably not installed"); - } - return false; - } - } - } - - public PandorasBoxIpc(IDalamudPluginInterface pluginInterface, IFramework framework, QuestController questController, TerritoryData territoryData, IClientState clientState, ILogger logger) - { - _framework = framework; + _configuration = configuration; _questController = questController; _territoryData = territoryData; _clientState = clientState; _logger = logger; _getFeatureEnabled = pluginInterface.GetIpcSubscriber("PandorasBox.GetFeatureEnabled"); _setFeatureEnabled = pluginInterface.GetIpcSubscriber("PandorasBox.SetFeatureEnabled"); - logger.LogInformation("Pandora's Box auto active time maneuver enabled: {IsAtmEnabled}", IsAutoActiveTimeManeuverEnabled); - _framework.Update += OnUpdate; + _questController.AutomationTypeChanged += OnAutomationTypeChanged; } - private void OnUpdate(IFramework framework) + private void OnAutomationTypeChanged(object sender, QuestController.EAutomationType automationType) { - if ((_questController.IsRunning || _questController.AutomationType != QuestController.EAutomationType.Manual) && !_territoryData.IsDutyInstance(_clientState.TerritoryType)) + if (automationType != QuestController.EAutomationType.Manual && !_territoryData.IsDutyInstance(_clientState.TerritoryType)) { - DisableConflictingFeatures(); + Task.Run((Action)DisableConflictingFeatures); } else { - RestoreConflictingFeatures(); + Task.Run((Action)RestoreConflictingFeatures); } } public void Dispose() { - _framework.Update -= OnUpdate; + _questController.AutomationTypeChanged -= OnAutomationTypeChanged; RestoreConflictingFeatures(); } @@ -98,20 +78,28 @@ internal sealed class PandorasBoxIpc : IDisposable _pausedFeatures = new HashSet(); foreach (string conflictingFeature in ConflictingFeatures) { - try + TryDisableFeature(conflictingFeature); + } + if (_configuration.General.AutoSolveQte) + { + TryDisableFeature("Auto Active Time Maneuver"); + } + } + + private void TryDisableFeature(string feature) + { + try + { + if (_getFeatureEnabled.InvokeFunc(feature) == true) { - if (_getFeatureEnabled.InvokeFunc(conflictingFeature) == true) - { - _setFeatureEnabled.InvokeAction(conflictingFeature, arg2: false); - _pausedFeatures.Add(conflictingFeature); - _logger.LogInformation("Paused Pandora's Box feature: {Feature}", conflictingFeature); - } - } - catch (IpcError exception) - { - _logger.LogWarning(exception, "Failed to pause Pandora's Box feature: {Feature}", conflictingFeature); + _setFeatureEnabled.InvokeAction(feature, arg2: false); + _pausedFeatures.Add(feature); + _logger.LogInformation("Paused Pandora's Box feature: {Feature}", feature); } } + catch (IpcError) + { + } } private void RestoreConflictingFeatures() @@ -127,9 +115,8 @@ internal sealed class PandorasBoxIpc : IDisposable _setFeatureEnabled.InvokeAction(pausedFeature, arg2: true); _logger.LogInformation("Restored Pandora's Box feature: {Feature}", pausedFeature); } - catch (IpcError exception) + catch (IpcError) { - _logger.LogWarning(exception, "Failed to restore Pandora's Box feature: {Feature}", pausedFeature); } } _pausedFeatures = null; diff --git a/Questionable/Questionable.External/QuestionableIpc.cs b/Questionable/Questionable.External/QuestionableIpc.cs index 605dc92..3ff98d4 100644 --- a/Questionable/Questionable.External/QuestionableIpc.cs +++ b/Questionable/Questionable.External/QuestionableIpc.cs @@ -196,6 +196,20 @@ internal sealed class QuestionableIpc : IDisposable private const string IpcStopLevelingMode = "Questionable.StopLevelingMode"; + private const string IpcGetBlacklistedQuests = "Questionable.GetBlacklistedQuests"; + + private const string IpcAddBlacklistedQuest = "Questionable.AddBlacklistedQuest"; + + private const string IpcRemoveBlacklistedQuest = "Questionable.RemoveBlacklistedQuest"; + + private const string IpcClearBlacklistedQuests = "Questionable.ClearBlacklistedQuests"; + + private const string IpcIsQuestBlacklisted = "Questionable.IsQuestBlacklisted"; + + private const string IpcGetMsqPriorityMode = "Questionable.GetMsqPriorityMode"; + + private const string IpcSetMsqPriorityMode = "Questionable.SetMsqPriorityMode"; + private readonly QuestController _questController; private readonly QuestRegistry _questRegistry; @@ -350,6 +364,20 @@ internal sealed class QuestionableIpc : IDisposable private readonly ICallGateProvider _stopLevelingMode; + private readonly ICallGateProvider> _getBlacklistedQuests; + + private readonly ICallGateProvider _addBlacklistedQuest; + + private readonly ICallGateProvider _removeBlacklistedQuest; + + private readonly ICallGateProvider _clearBlacklistedQuests; + + private readonly ICallGateProvider _isQuestBlacklisted; + + private readonly ICallGateProvider _getMsqPriorityMode; + + private readonly ICallGateProvider _setMsqPriorityMode; + public QuestionableIpc(QuestController questController, EventInfoComponent eventInfoComponent, QuestRegistry questRegistry, QuestFunctions questFunctions, QuestData questData, ManualPriorityComponent manualPriorityComponent, PresetBuilderComponent presetBuilderComponent, Configuration configuration, IDalamudPluginInterface pluginInterface, IServiceProvider serviceProvider, AutoDutyIpc autoDutyIpc) { QuestionableIpc questionableIpc = this; @@ -542,6 +570,20 @@ internal sealed class QuestionableIpc : IDisposable _startLevelingMode.RegisterFunc(StartLevelingMode); _stopLevelingMode = pluginInterface.GetIpcProvider("Questionable.StopLevelingMode"); _stopLevelingMode.RegisterFunc(StopLevelingMode); + _getBlacklistedQuests = pluginInterface.GetIpcProvider>("Questionable.GetBlacklistedQuests"); + _getBlacklistedQuests.RegisterFunc(GetBlacklistedQuests); + _addBlacklistedQuest = pluginInterface.GetIpcProvider("Questionable.AddBlacklistedQuest"); + _addBlacklistedQuest.RegisterFunc(AddBlacklistedQuest); + _removeBlacklistedQuest = pluginInterface.GetIpcProvider("Questionable.RemoveBlacklistedQuest"); + _removeBlacklistedQuest.RegisterFunc(RemoveBlacklistedQuest); + _clearBlacklistedQuests = pluginInterface.GetIpcProvider("Questionable.ClearBlacklistedQuests"); + _clearBlacklistedQuests.RegisterFunc(ClearBlacklistedQuests); + _isQuestBlacklisted = pluginInterface.GetIpcProvider("Questionable.IsQuestBlacklisted"); + _isQuestBlacklisted.RegisterFunc(IsQuestBlacklistedIpc); + _getMsqPriorityMode = pluginInterface.GetIpcProvider("Questionable.GetMsqPriorityMode"); + _getMsqPriorityMode.RegisterFunc(GetMsqPriorityMode); + _setMsqPriorityMode = pluginInterface.GetIpcProvider("Questionable.SetMsqPriorityMode"); + _setMsqPriorityMode.RegisterFunc(SetMsqPriorityMode); } private bool StartQuest(string questId, bool single) @@ -1387,6 +1429,67 @@ internal sealed class QuestionableIpc : IDisposable } } + private List GetBlacklistedQuests() + { + return _configuration.General.BlacklistedQuests.Select((ElementId q) => q.ToString()).ToList(); + } + + private bool AddBlacklistedQuest(string questId) + { + if (ElementId.TryFromString(questId, out ElementId elementId) && elementId != null && _configuration.General.BlacklistedQuests.Add(elementId)) + { + _pluginInterface.SavePluginConfig(_configuration); + return true; + } + return false; + } + + private bool RemoveBlacklistedQuest(string questId) + { + if (ElementId.TryFromString(questId, out ElementId elementId) && elementId != null) + { + bool num = _configuration.General.BlacklistedQuests.Remove(elementId); + if (num) + { + _pluginInterface.SavePluginConfig(_configuration); + } + return num; + } + return false; + } + + private bool ClearBlacklistedQuests() + { + _configuration.General.BlacklistedQuests.Clear(); + _pluginInterface.SavePluginConfig(_configuration); + return true; + } + + private bool IsQuestBlacklistedIpc(string questId) + { + if (ElementId.TryFromString(questId, out ElementId elementId) && elementId != null) + { + return _questFunctions.IsQuestBlacklisted(elementId); + } + return false; + } + + private int GetMsqPriorityMode() + { + return (int)_configuration.General.MsqPriority; + } + + private bool SetMsqPriorityMode(int mode) + { + if (!Enum.IsDefined(typeof(Configuration.EMsqPriorityMode), mode)) + { + return false; + } + _configuration.General.MsqPriority = (Configuration.EMsqPriorityMode)mode; + _pluginInterface.SavePluginConfig(_configuration); + return true; + } + public void Dispose() { _exportQuestPriority.UnregisterFunc(); @@ -1456,5 +1559,12 @@ internal sealed class QuestionableIpc : IDisposable _getMsqLevelLockInfo.UnregisterFunc(); _setLevelingModeEnabled.UnregisterFunc(); _isLevelingModeEnabled.UnregisterFunc(); + _isQuestBlacklisted.UnregisterFunc(); + _clearBlacklistedQuests.UnregisterFunc(); + _removeBlacklistedQuest.UnregisterFunc(); + _addBlacklistedQuest.UnregisterFunc(); + _getBlacklistedQuests.UnregisterFunc(); + _setMsqPriorityMode.UnregisterFunc(); + _getMsqPriorityMode.UnregisterFunc(); } } diff --git a/Questionable/Questionable.Functions/GameSignatures.cs b/Questionable/Questionable.Functions/GameSignatures.cs index 19a9aa4..e875bdd 100644 --- a/Questionable/Questionable.Functions/GameSignatures.cs +++ b/Questionable/Questionable.Functions/GameSignatures.cs @@ -2,4 +2,5 @@ namespace Questionable.Functions; internal static class GameSignatures { + internal const string EnqueueSnipeTask = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 50 48 8B F9 48 8D 4C 24 ??"; } diff --git a/Questionable/Questionable.Functions/QuestFunctions.cs b/Questionable/Questionable.Functions/QuestFunctions.cs index 5057ca8..27955df 100644 --- a/Questionable/Questionable.Functions/QuestFunctions.cs +++ b/Questionable/Questionable.Functions/QuestFunctions.cs @@ -179,7 +179,6 @@ internal sealed class QuestFunctions Questionable.Model.Quest quest; bool flag = _questRegistry.TryGetQuest(questReference.CurrentQuest, out quest); bool flag2 = IsQuestAccepted(questReference.CurrentQuest); - _logger.LogTrace("MSQ check: QuestId={QuestId}, InRegistry={InRegistry}, Level={QuestLevel}, CurrentLevel={CurrentLevel}, IsAccepted={IsAccepted}", questReference.CurrentQuest, flag, ((int?)quest?.Info.Level) ?? (-1), currentLevel, flag2); EClassJob valueOrDefault = ((EClassJob?)_objectTable.LocalPlayer?.ClassJob.RowId).GetValueOrDefault(); if (valueOrDefault != EClassJob.Adventurer) { @@ -193,7 +192,7 @@ internal sealed class QuestFunctions return new QuestReference(questInfo.QuestId, QuestManager.GetQuestSequence(questInfo.QuestId.Value), questReference.State); } } - if (flag && quest.Info.Level > currentLevel && !flag2) + if (flag && quest.Info.Level > currentLevel && !flag2 && _configuration.General.MsqPriority != Configuration.EMsqPriorityMode.Manual) { if (_lastLoggedLevelLockedMsq != questReference.CurrentQuest) { @@ -208,10 +207,10 @@ internal sealed class QuestFunctions List lockedQuests = _questData.GetLockedClassQuests(); List list = (from x in classJobQuests.Where(delegate(QuestInfo x) { - bool num2 = x.Level <= currentLevel && x.Level <= 5; - bool flag3 = !IsQuestComplete(x.QuestId); - bool flag4 = !lockedQuests.Contains(x.QuestId); - return num2 && flag3 && flag4; + bool num3 = x.Level <= currentLevel && x.Level <= 5; + bool flag4 = !IsQuestComplete(x.QuestId); + bool flag5 = !lockedQuests.Contains(x.QuestId); + return num3 && flag4 && flag5; }) orderby x.Level select x).ToList(); @@ -276,24 +275,33 @@ internal sealed class QuestFunctions _loggedNoClassQuestsAvailable = false; _loggedAdventurerClass = false; } - if (questReference.CurrentQuest != null && !IsQuestAccepted(questReference.CurrentQuest)) + bool num = questReference.CurrentQuest != null && !IsQuestAccepted(questReference.CurrentQuest); + bool flag3 = true; + if (num) { - if (allowNewMsq) + switch (_configuration.General.MsqPriority) { - return questReference; + case Configuration.EMsqPriorityMode.Always: + if (allowNewMsq) + { + return questReference; + } + break; + case Configuration.EMsqPriorityMode.Manual: + flag3 = false; + break; } - questReference = QuestReference.NoQuest(questReference.State); } List<(ElementId, byte)> list3 = new List<(ElementId, byte)>(); - for (int num = ptr->TrackedQuests.Length - 1; num >= 0; num--) + for (int num2 = ptr->TrackedQuests.Length - 1; num2 >= 0; num2--) { - TrackingWork trackingWork = ptr->TrackedQuests[num]; + TrackingWork trackingWork = ptr->TrackedQuests[num2]; switch (trackingWork.QuestType) { case 1: { ElementId elementId2 = new QuestId(ptr->NormalQuests[trackingWork.Index].QuestId); - if (_questRegistry.IsKnownQuest(elementId2)) + if (_questRegistry.IsKnownQuest(elementId2) && !IsQuestBlacklisted(elementId2)) { list3.Add((elementId2, QuestManager.GetQuestSequence(elementId2.Value))); } @@ -356,7 +364,7 @@ internal sealed class QuestFunctions { return new QuestReference(elementId3, QuestManager.GetQuestSequence(elementId3.Value), questReference.State); } - if (questReference.CurrentQuest != null) + if (flag3 && questReference.CurrentQuest != null) { return questReference; } @@ -687,6 +695,10 @@ internal sealed class QuestFunctions public unsafe bool IsReadyToAcceptQuest(ElementId questId, bool ignoreLevel = false) { + if (IsQuestBlacklisted(questId)) + { + return false; + } _questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest); if (quest != null) { @@ -712,15 +724,15 @@ internal sealed class QuestFunctions { return false; } - goto IL_0077; + goto IL_0082; } } if (IsQuestAcceptedOrComplete(questId)) { return false; } - goto IL_0077; - IL_0077: + goto IL_0082; + IL_0082: if (IsQuestLocked(questId)) { return false; @@ -752,6 +764,11 @@ internal sealed class QuestFunctions return true; } + public bool IsQuestBlacklisted(ElementId questId) + { + return _configuration.General.BlacklistedQuests.Contains(questId); + } + public bool IsQuestAcceptedOrComplete(ElementId elementId) { if (!IsQuestComplete(elementId)) @@ -1069,18 +1086,17 @@ internal sealed class QuestFunctions List list = quest?.Root?.QuestSequence; if (list != null && list.Count > 0) { - goto IL_0228; + goto IL_01fd; } } if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value)) { _questData.ApplySeasonalOverride(questId, isSeasonal: true, null); - _logger.LogDebug("Quest {QuestId} unobtainable: journal genre {Genre} is 'event (seasonal)' and no quest path", questId, questInfo2.JournalGenre); } return true; } - goto IL_0228; - IL_0228: + goto IL_01fd; + IL_01fd: if (questInfo2.QuestLocks.Count > 0) { int num = questInfo2.QuestLocks.Count((QuestId x) => IsQuestComplete(x) || x.Equals(extraCompletedQuest)); diff --git a/Questionable/Questionable.Model/AlliedSocietyDailyInfo.cs b/Questionable/Questionable.Model/AlliedSocietyDailyInfo.cs index 9eb661c..09a6f49 100644 --- a/Questionable/Questionable.Model/AlliedSocietyDailyInfo.cs +++ b/Questionable/Questionable.Model/AlliedSocietyDailyInfo.cs @@ -95,7 +95,7 @@ internal sealed class AlliedSocietyDailyInfo : IQuestInfo List list4 = new List(); list4.AddRange(classJobUtils.AsIndividualJobs(EExtendedClassJob.DoW, null)); list4.AddRange(classJobUtils.AsIndividualJobs(EExtendedClassJob.DoM, null)); - readOnlyList = new _003C_003Ez__ReadOnlyList(list4); + readOnlyList = new global::_003C_003Ez__ReadOnlyList(list4); } ClassJobs = readOnlyList; Expansion = (EExpansionVersion)beastTribe.Expansion.RowId; diff --git a/Questionable/Questionable.Windows.ConfigComponents/BlacklistConfigComponent.cs b/Questionable/Questionable.Windows.ConfigComponents/BlacklistConfigComponent.cs new file mode 100644 index 0000000..287c2da --- /dev/null +++ b/Questionable/Questionable.Windows.ConfigComponents/BlacklistConfigComponent.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; +using Questionable.Controller; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Questing; +using Questionable.Windows.QuestComponents; +using Questionable.Windows.Utils; + +namespace Questionable.Windows.ConfigComponents; + +internal sealed class BlacklistConfigComponent : ConfigComponent +{ + private readonly IDalamudPluginInterface _pluginInterface; + + private readonly QuestSelector _questSelector; + + private readonly QuestRegistry _questRegistry; + + private readonly QuestFunctions _questFunctions; + + private readonly QuestTooltipComponent _questTooltipComponent; + + private readonly UiUtils _uiUtils; + + public BlacklistConfigComponent(IDalamudPluginInterface pluginInterface, QuestSelector questSelector, QuestFunctions questFunctions, QuestRegistry questRegistry, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, Configuration configuration) + : base(pluginInterface, configuration) + { + BlacklistConfigComponent blacklistConfigComponent = this; + _pluginInterface = pluginInterface; + _questSelector = questSelector; + _questRegistry = questRegistry; + _questFunctions = questFunctions; + _questTooltipComponent = questTooltipComponent; + _uiUtils = uiUtils; + _questSelector.SuggestionPredicate = (Quest quest) => !configuration.General.BlacklistedQuests.Contains(quest.Id); + _questSelector.DefaultPredicate = (Quest quest) => !quest.Root.Disabled && questFunctions.IsQuestAccepted(quest.Id); + _questSelector.QuestSelected = delegate(Quest quest) + { + configuration.General.BlacklistedQuests.Add(quest.Id); + blacklistConfigComponent.Save(); + }; + } + + public override void DrawTab() + { + using ImRaii.IEndObject endObject = ImRaii.TabItem("Blacklist###QuestBlacklist"); + if (!endObject) + { + return; + } + ImGui.TextWrapped("Blacklisted quests will never be automatically picked up or executed by Questionable."); + ImGui.TextWrapped("Use this to permanently skip specific quests you don't want to do."); + ImGui.Separator(); + DrawCurrentlyAcceptedQuests(); + HashSet blacklistedQuests = base.Configuration.General.BlacklistedQuests; + ImU8String text = new ImU8String(22, 1); + text.AppendLiteral("Blacklisted Quests ("); + text.AppendFormatted(blacklistedQuests.Count); + text.AppendLiteral("):"); + ImGui.Text(text); + using (ImRaii.IEndObject endObject2 = ImRaii.Child("BlacklistedQuestsList", new Vector2(-1f, 200f), border: true)) + { + if (endObject2) + { + if (blacklistedQuests.Count > 0) + { + ElementId elementId = null; + int num = 0; + foreach (ElementId item in blacklistedQuests.OrderBy((ElementId x) => x.ToString())) + { + if (!_questRegistry.TryGetQuest(item, out Quest quest)) + { + ImU8String id = new ImU8String(5, 1); + id.AppendLiteral("Quest"); + id.AppendFormatted(item); + using (ImRaii.PushId(id)) + { + ImGui.AlignTextToFramePadding(); + Vector4 col = new Vector4(0.7f, 0.7f, 0.7f, 1f); + ImU8String text2 = new ImU8String(16, 1); + text2.AppendLiteral("Unknown Quest ("); + text2.AppendFormatted(item); + text2.AppendLiteral(")"); + ImGui.TextColored(in col, text2); + ImGui.SameLine(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 2f); + if (ImGuiComponents.IconButton($"##Remove{num}", FontAwesomeIcon.Times)) + { + elementId = item; + } + } + num++; + continue; + } + ImU8String id2 = new ImU8String(5, 1); + id2.AppendLiteral("Quest"); + id2.AppendFormatted(item); + using (ImRaii.PushId(id2)) + { + (Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(item); + bool flag; + using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(in questStyle.Item1, questStyle.Item2.ToIconString()); + flag = ImGui.IsItemHovered(); + } + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(quest.Info.Name); + flag |= ImGui.IsItemHovered(); + if (flag) + { + _questTooltipComponent.Draw(quest.Info); + } + ImGui.SameLine(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 2f); + if (ImGuiComponents.IconButton($"##Remove{num}", FontAwesomeIcon.Times)) + { + elementId = item; + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Remove from blacklist"); + } + } + num++; + } + if (elementId != null) + { + base.Configuration.General.BlacklistedQuests.Remove(elementId); + Save(); + } + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.7f, 0.7f, 0.7f, 1f)); + ImGui.TextWrapped("No quests blacklisted. Use the dropdown above or add from currently accepted quests."); + ImGui.PopStyleColor(); + } + } + } + _questSelector.DrawSelection(); + if (blacklistedQuests.Count <= 0) + { + return; + } + using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) + { + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Trash, "Clear All")) + { + base.Configuration.General.BlacklistedQuests.Clear(); + Save(); + } + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + ImGui.SetTooltip("Hold CTRL to enable this button."); + } + } + + private void DrawCurrentlyAcceptedQuests() + { + List currentlyAcceptedQuests = GetCurrentlyAcceptedQuests(); + ImGui.Text("Currently Accepted Quests:"); + using (ImRaii.IEndObject endObject = ImRaii.Child("AcceptedQuestsList", new Vector2(-1f, 100f), border: true)) + { + if (endObject) + { + if (currentlyAcceptedQuests.Count > 0) + { + foreach (Quest item in currentlyAcceptedQuests) + { + ImU8String id = new ImU8String(13, 1); + id.AppendLiteral("AcceptedQuest"); + id.AppendFormatted(item.Id); + using (ImRaii.PushId(id)) + { + (Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(item.Id); + bool flag = false; + bool flag2 = base.Configuration.General.BlacklistedQuests.Contains(item.Id); + using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(flag2 ? new Vector4(0.5f, 0.5f, 0.5f, 1f) : questStyle.Item1, questStyle.Item2.ToIconString()); + flag = ImGui.IsItemHovered(); + } + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(flag2 ? new Vector4(0.7f, 0.7f, 0.7f, 1f) : new Vector4(1f, 1f, 1f, 1f), item.Info.Name); + if (flag | ImGui.IsItemHovered()) + { + _questTooltipComponent.Draw(item.Info); + } + ImGui.SameLine(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(FontAwesomeIcon.Ban.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 2f); + using (ImRaii.Disabled(flag2)) + { + if (ImGuiComponents.IconButton($"##Blacklist{item.Id}", FontAwesomeIcon.Ban)) + { + base.Configuration.General.BlacklistedQuests.Add(item.Id); + Save(); + } + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + ImGui.SetTooltip(flag2 ? "Quest already blacklisted" : "Add this quest to blacklist"); + } + } + } + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.7f, 0.7f, 0.7f, 1f)); + ImGui.TextWrapped("No quests currently accepted"); + ImGui.PopStyleColor(); + } + } + } + ImGui.Spacing(); + } + + private List GetCurrentlyAcceptedQuests() + { + List list = new List(); + try + { + foreach (Quest allQuest in _questRegistry.AllQuests) + { + if (_questFunctions.IsQuestAccepted(allQuest.Id)) + { + list.Add(allQuest); + } + } + list.Sort((Quest a, Quest b) => string.Compare(a.Info.Name, b.Info.Name, StringComparison.OrdinalIgnoreCase)); + } + catch (Exception) + { + list.Clear(); + } + return list; + } +} diff --git a/Questionable/Questionable.Windows.ConfigComponents/DebugConfigComponent.cs b/Questionable/Questionable.Windows.ConfigComponents/DebugConfigComponent.cs index bab710a..ce8b997 100644 --- a/Questionable/Questionable.Windows.ConfigComponents/DebugConfigComponent.cs +++ b/Questionable/Questionable.Windows.ConfigComponents/DebugConfigComponent.cs @@ -22,6 +22,8 @@ internal sealed class DebugConfigComponent : ConfigComponent } ImGui.TextColored(ImGuiColors.DalamudRed, "Enabling any option here may cause unexpected behavior. Use at your own risk."); ImGui.Separator(); + DrawChocoboSettings(); + ImGui.Separator(); bool v = base.Configuration.Advanced.DebugOverlay; if (ImGui.Checkbox("Enable debug overlay", ref v)) { @@ -120,4 +122,55 @@ internal sealed class DebugConfigComponent : ConfigComponent ImGuiComponents.HelpMarker("When enabled, Questionable will not attempt to turn-in and complete quests. This will do everything automatically except the final turn-in step."); } } + + private void DrawChocoboSettings() + { + ImGui.Text("Chocobo Settings"); + using (ImRaii.PushIndent()) + { + ImGui.AlignTextToFramePadding(); + ImGui.Text("Chocobo Name:"); + ImGui.SameLine(); + string buf = base.Configuration.Advanced.ChocoboName; + ImGui.SetNextItemWidth(200f); + if (ImGui.InputText("##ChocoboName", ref buf, 20)) + { + string chocoboName = FormatChocoboName(buf); + base.Configuration.Advanced.ChocoboName = chocoboName; + Save(); + } + if (!string.IsNullOrEmpty(buf) && buf.Length < 2) + { + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.DalamudRed, "Name must be at least 2 characters"); + } + ImGui.SameLine(); + ImGuiComponents.HelpMarker("The name to give your chocobo during the 'My Little Chocobo' quest.\nMust be 2-20 characters. First letter will be capitalized.\nIf left empty, defaults to 'Kweh'."); + } + } + + private static string FormatChocoboName(string name) + { + if (string.IsNullOrEmpty(name)) + { + return string.Empty; + } + name = name.Trim(); + if (name.Length == 0) + { + return string.Empty; + } + string text = name.Substring(0, 1).ToUpperInvariant(); + string text2; + if (name.Length <= 1) + { + text2 = string.Empty; + } + else + { + string text3 = name; + text2 = text3.Substring(1, text3.Length - 1).ToLowerInvariant(); + } + return text + text2; + } } diff --git a/Questionable/Questionable.Windows.ConfigComponents/GeneralConfigComponent.cs b/Questionable/Questionable.Windows.ConfigComponents/GeneralConfigComponent.cs index 674af38..1994dff 100644 --- a/Questionable/Questionable.Windows.ConfigComponents/GeneralConfigComponent.cs +++ b/Questionable/Questionable.Windows.ConfigComponents/GeneralConfigComponent.cs @@ -35,6 +35,8 @@ internal sealed class GeneralConfigComponent : ConfigComponent private readonly string[] _grandCompanyNames = new string[4] { "None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames" }; + private readonly string[] _msqPriorityNames = new string[3] { "Always (MSQ first)", "After Tracked (MSQ last)", "Manual (never auto-accept MSQ)" }; + private readonly EClassJob[] _classJobIds; private readonly string[] _classJobNames; @@ -159,6 +161,28 @@ internal sealed class GeneralConfigComponent : ConfigComponent base.Configuration.General.GrandCompany = (FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany)currentItem2; Save(); } + int currentItem3 = (int)base.Configuration.General.MsqPriority; + if (ImGui.Combo((ImU8String)"MSQ Priority", ref currentItem3, (ReadOnlySpan)_msqPriorityNames, _msqPriorityNames.Length)) + { + base.Configuration.General.MsqPriority = (Configuration.EMsqPriorityMode)currentItem3; + Save(); + } + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString()); + } + if (ImGui.IsItemHovered()) + { + using (ImRaii.Tooltip()) + { + ImGui.Text("Controls when the Main Scenario Quest (MSQ) is automatically accepted:"); + ImGui.Spacing(); + ImGui.BulletText("Always: Auto-accept MSQ immediately"); + ImGui.BulletText("After Tracked: Complete accepted quests first, then pick up MSQ"); + ImGui.BulletText("Manual: Never auto-accept MSQ, only do manually accepted quests"); + } + } int num3 = Array.IndexOf(_classJobIds, base.Configuration.General.CombatJob); if (num3 == -1) { @@ -253,10 +277,48 @@ internal sealed class GeneralConfigComponent : ConfigComponent ImGui.Text("Questing"); using (ImRaii.PushIndent()) { - bool v7 = base.Configuration.General.CinemaMode; - if (ImGui.Checkbox("Cinema Mode (watch cutscenes)", ref v7)) + bool v7 = base.Configuration.General.AutoSolveQte; + if (ImGui.Checkbox("Automatically solve Quick Time Events (QTEs)", ref v7)) { - base.Configuration.General.CinemaMode = v7; + base.Configuration.General.AutoSolveQte = v7; + Save(); + } + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString()); + } + if (ImGui.IsItemHovered()) + { + using (ImRaii.Tooltip()) + { + ImGui.Text("Automatically mashes the button during Active Time Maneuver (ATM)"); + ImGui.Text("prompts that appear in certain duties and quest battles."); + } + } + bool v8 = base.Configuration.General.AutoSnipe; + if (ImGui.Checkbox("Automatically complete snipe quests", ref v8)) + { + base.Configuration.General.AutoSnipe = v8; + Save(); + } + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString()); + } + if (ImGui.IsItemHovered()) + { + using (ImRaii.Tooltip()) + { + ImGui.Text("Automatically completes sniping minigames introduced in Stormblood."); + ImGui.Text("When enabled, snipe targets are instantly hit without manual aiming."); + } + } + bool v9 = base.Configuration.General.CinemaMode; + if (ImGui.Checkbox("Cinema Mode (watch cutscenes)", ref v9)) + { + base.Configuration.General.CinemaMode = v9; Save(); } ImGui.SameLine(); @@ -275,16 +337,16 @@ internal sealed class GeneralConfigComponent : ConfigComponent ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 1f), "Recommended for first-time story playthroughs."); } } - bool v8 = base.Configuration.General.ConfigureTextAdvance; - if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings", ref v8)) + bool v10 = base.Configuration.General.ConfigureTextAdvance; + if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings", ref v10)) { - base.Configuration.General.ConfigureTextAdvance = v8; + base.Configuration.General.ConfigureTextAdvance = v10; Save(); } - bool v9 = base.Configuration.General.SkipLowPriorityDuties; - if (ImGui.Checkbox("Unlock certain optional dungeons and raids (instead of waiting for completion)", ref v9)) + bool v11 = base.Configuration.General.SkipLowPriorityDuties; + if (ImGui.Checkbox("Unlock certain optional dungeons and raids (instead of waiting for completion)", ref v11)) { - base.Configuration.General.SkipLowPriorityDuties = v9; + base.Configuration.General.SkipLowPriorityDuties = v11; Save(); } ImGui.SameLine(); @@ -312,10 +374,10 @@ internal sealed class GeneralConfigComponent : ConfigComponent } } ImGui.Spacing(); - bool v10 = base.Configuration.General.AutoStepRefreshEnabled; - if (ImGui.Checkbox("Automatically refresh quest steps when stuck", ref v10)) + bool v12 = base.Configuration.General.AutoStepRefreshEnabled; + if (ImGui.Checkbox("Automatically refresh quest steps when stuck", ref v12)) { - base.Configuration.General.AutoStepRefreshEnabled = v10; + base.Configuration.General.AutoStepRefreshEnabled = v12; Save(); } ImGui.SameLine(); @@ -331,20 +393,20 @@ internal sealed class GeneralConfigComponent : ConfigComponent ImGui.Text("This helps resume automated quest completion when interruptions occur."); } } - using (ImRaii.Disabled(!v10)) + using (ImRaii.Disabled(!v12)) { ImGui.Indent(); - int v11 = base.Configuration.General.AutoStepRefreshDelaySeconds; + int v13 = base.Configuration.General.AutoStepRefreshDelaySeconds; ImGui.SetNextItemWidth(150f); - if (ImGui.SliderInt("Refresh delay (seconds)", ref v11, 10, 180)) + if (ImGui.SliderInt("Refresh delay (seconds)", ref v13, 10, 180)) { - base.Configuration.General.AutoStepRefreshDelaySeconds = v11; + base.Configuration.General.AutoStepRefreshDelaySeconds = v13; Save(); } Vector4 col = new Vector4(0.7f, 0.7f, 0.7f, 1f); ImU8String text4 = new ImU8String(77, 1); text4.AppendLiteral("Quest steps will refresh automatically after "); - text4.AppendFormatted(v11); + text4.AppendFormatted(v13); text4.AppendLiteral(" seconds if no progress is made."); ImGui.TextColored(in col, text4); ImGui.Unindent(); @@ -352,16 +414,48 @@ internal sealed class GeneralConfigComponent : ConfigComponent ImGui.Spacing(); ImGui.Separator(); ImGui.Text("Priority Quest Management"); - bool v12 = base.Configuration.General.ClearPriorityQuestsOnLogout; - if (ImGui.Checkbox("Clear priority quests on character logout", ref v12)) + bool v14 = base.Configuration.General.PersistPriorityQuestsBetweenSessions; + if (ImGui.Checkbox("Save priority quests between sessions", ref v14)) { - base.Configuration.General.ClearPriorityQuestsOnLogout = v12; + base.Configuration.General.PersistPriorityQuestsBetweenSessions = v14; Save(); } - bool v13 = base.Configuration.General.ClearPriorityQuestsOnCompletion; - if (ImGui.Checkbox("Remove priority quests when completed", ref v13)) + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) { - base.Configuration.General.ClearPriorityQuestsOnCompletion = v13; + ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString()); + } + if (ImGui.IsItemHovered()) + { + using (ImRaii.Tooltip()) + { + ImGui.Text("When enabled, your priority quest list will be saved and restored"); + ImGui.Text("when the plugin reloads or the game restarts."); + } + } + bool v15 = base.Configuration.General.ClearPriorityQuestsOnLogout; + if (ImGui.Checkbox("Clear priority quests on character logout", ref v15)) + { + base.Configuration.General.ClearPriorityQuestsOnLogout = v15; + Save(); + } + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString()); + } + if (ImGui.IsItemHovered()) + { + using (ImRaii.Tooltip()) + { + ImGui.Text("Clears the priority queue when your character logs out."); + ImGui.Text("This also clears the saved list if persistence is enabled."); + } + } + bool v16 = base.Configuration.General.ClearPriorityQuestsOnCompletion; + if (ImGui.Checkbox("Remove priority quests when completed", ref v16)) + { + base.Configuration.General.ClearPriorityQuestsOnCompletion = v16; Save(); } ImGui.SameLine(); diff --git a/Questionable/Questionable.Windows.ConfigComponents/PluginConfigComponent.cs b/Questionable/Questionable.Windows.ConfigComponents/PluginConfigComponent.cs index dae6ceb..a77aec5 100644 --- a/Questionable/Questionable.Windows.ConfigComponents/PluginConfigComponent.cs +++ b/Questionable/Questionable.Windows.ConfigComponents/PluginConfigComponent.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; @@ -13,7 +12,6 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; using Questionable.Controller; -using Questionable.External; namespace Questionable.Windows.ConfigComponents; @@ -58,7 +56,7 @@ internal sealed class PluginConfigComponent : ConfigComponent private readonly ICommandManager _commandManager; - public PluginConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration, CombatController combatController, UiUtils uiUtils, ICommandManager commandManager, AutomatonIpc automatonIpc, PandorasBoxIpc pandorasBoxIpc) + public PluginConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration, CombatController combatController, UiUtils uiUtils, ICommandManager commandManager) : base(pluginInterface, configuration) { _configuration = configuration; @@ -66,34 +64,12 @@ internal sealed class PluginConfigComponent : ConfigComponent _pluginInterface = pluginInterface; _uiUtils = uiUtils; _commandManager = commandManager; - PluginInfo[] obj = new PluginInfo[5] + _recommendedPlugins = new global::_003C_003Ez__ReadOnlyArray(new PluginInfo[3] { new PluginInfo("Artisan", "Artisan", "Handles automatic crafting for quests that require\ncrafted items.", new Uri("https://github.com/PunishXIV/Artisan"), new Uri("https://puni.sh/api/plugins")), new PluginInfo("AutoDuty", "AutoDuty", "Automatically handles dungeon and trial completion during\nMain Scenario Quest progression.", new Uri("https://github.com/erdelf/AutoDuty"), new Uri("https://puni.sh/api/repository/erdelf")), - null, - null, - null - }; - Uri websiteUri = new Uri("https://github.com/Jaksuhn/Automaton"); - Uri dalamudRepositoryUri = new Uri("https://puni.sh/api/repository/croizat"); - int num = 1; - List list = new List(num); - CollectionsMarshal.SetCount(list, num); - Span span = CollectionsMarshal.AsSpan(list); - int index = 0; - span[index] = new PluginDetailInfo("'Sniper no sniping' enabled", "Automatically completes sniping tasks introduced in Stormblood", () => automatonIpc.IsAutoSnipeEnabled); - obj[2] = new PluginInfo("CBT (formerly known as Automaton)", "Automaton", "Automaton is a collection of automation-related tweaks.", websiteUri, dalamudRepositoryUri, "/cbt", list); - Uri websiteUri2 = new Uri("https://github.com/PunishXIV/PandorasBox"); - Uri dalamudRepositoryUri2 = new Uri("https://puni.sh/api/plugins"); - index = 1; - List list2 = new List(index); - CollectionsMarshal.SetCount(list2, index); - Span span2 = CollectionsMarshal.AsSpan(list2); - num = 0; - span2[num] = new PluginDetailInfo("'Auto Active Time Maneuver' enabled", "Automatically completes active time maneuvers in\nsingle player instances, trials and raids\"", () => pandorasBoxIpc.IsAutoActiveTimeManeuverEnabled); - obj[3] = new PluginInfo("Pandora's Box", "PandorasBox", "Pandora's Box is a collection of tweaks.", websiteUri2, dalamudRepositoryUri2, "/pandora", list2); - obj[4] = new PluginInfo("QuestMap", "QuestMap", "Displays quest objectives and markers on the map for\nbetter navigation and tracking.", new Uri("https://github.com/rreminy/QuestMap"), null); - _recommendedPlugins = new global::_003C_003Ez__ReadOnlyArray(obj); + new PluginInfo("QuestMap", "QuestMap", "Displays quest objectives and markers on the map for\nbetter navigation and tracking.", new Uri("https://github.com/rreminy/QuestMap"), null) + }); } public override void DrawTab() diff --git a/Questionable/Questionable.Windows.ConfigComponents/StopConditionComponent.cs b/Questionable/Questionable.Windows.ConfigComponents/StopConditionComponent.cs index a37963d..cf173f4 100644 --- a/Questionable/Questionable.Windows.ConfigComponents/StopConditionComponent.cs +++ b/Questionable/Questionable.Windows.ConfigComponents/StopConditionComponent.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; @@ -39,6 +40,8 @@ internal sealed class StopConditionComponent : ConfigComponent private readonly QuestController _questController; + private ElementId? _draggedItem; + public StopConditionComponent(IDalamudPluginInterface pluginInterface, QuestSelector questSelector, QuestFunctions questFunctions, QuestRegistry questRegistry, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IObjectTable objectTable, IClientState clientState, QuestController questController, Configuration configuration) : base(pluginInterface, configuration) { @@ -167,112 +170,172 @@ internal sealed class StopConditionComponent : ConfigComponent ImGui.SetTooltip("Hold CTRL to enable this button."); } ImGui.Separator(); + ImGui.Text("(Drag arrow buttons to reorder)"); } - Quest quest = null; - for (int i = 0; i < questsToStopAfter.Count; i++) + DrawStopQuestList(questsToStopAfter); + } + } + + private void DrawStopQuestList(List questsToStopAfter) + { + Quest quest = null; + Quest quest2 = null; + int index = 0; + float x = ImGui.GetContentRegionAvail().X; + List<(Vector2, Vector2)> list = new List<(Vector2, Vector2)>(); + for (int i = 0; i < questsToStopAfter.Count; i++) + { + Vector2 item = ImGui.GetCursorScreenPos() + new Vector2(0f, (0f - ImGui.GetStyle().ItemSpacing.Y) / 2f); + ElementId elementId = questsToStopAfter[i]; + if (!_questRegistry.TryGetQuest(elementId, out Quest quest3)) { - ElementId elementId = questsToStopAfter[i]; - if (!_questRegistry.TryGetQuest(elementId, out Quest quest2)) + continue; + } + ImU8String id = new ImU8String(5, 1); + id.AppendLiteral("Quest"); + id.AppendFormatted(elementId); + using (ImRaii.PushId(id)) + { + ImGui.AlignTextToFramePadding(); + ImU8String text = new ImU8String(1, 1); + text.AppendFormatted(i + 1); + text.AppendLiteral("."); + ImGui.Text(text); + ImGui.SameLine(); + (Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(elementId); + bool flag; + using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { - continue; - } - ImU8String id = new ImU8String(5, 1); - id.AppendLiteral("Quest"); - id.AppendFormatted(elementId); - using (ImRaii.PushId(id)) - { - (Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(elementId); - bool flag; - using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) - { - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(in questStyle.Item1, questStyle.Item2.ToIconString()); - flag = ImGui.IsItemHovered(); - } - ImGui.SameLine(); ImGui.AlignTextToFramePadding(); - ImGui.Text(quest2.Info.Name); - flag |= ImGui.IsItemHovered(); - if (flag) + ImGui.TextColored(in questStyle.Item1, questStyle.Item2.ToIconString()); + flag = ImGui.IsItemHovered(); + } + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text(quest3.Info.Name); + flag |= ImGui.IsItemHovered(); + if (flag) + { + _questTooltipComponent.Draw(quest3.Info); + } + string text2 = elementId.ToString(); + int currentItem = (int)base.Configuration.Stop.QuestStopModes.GetValueOrDefault(text2, Questionable.Configuration.EStopConditionMode.Stop); + ImGui.SameLine(); + ImGui.SetNextItemWidth(70f); + ImU8String label = new ImU8String(6, 1); + label.AppendLiteral("##Mode"); + label.AppendFormatted(text2); + if (ImGui.Combo(label, ref currentItem, (ReadOnlySpan)StopModeNames, StopModeNames.Length)) + { + base.Configuration.Stop.QuestStopModes[text2] = (Configuration.EStopConditionMode)currentItem; + Save(); + } + ImGui.SameLine(); + base.Configuration.Stop.QuestSequences.TryGetValue(text2, out var value); + bool v = value.HasValue; + ImU8String label2 = new ImU8String(8, 1); + label2.AppendLiteral("##UseSeq"); + label2.AppendFormatted(text2); + if (ImGui.Checkbox(label2, ref v)) + { + if (v) { - _questTooltipComponent.Draw(quest2.Info); + base.Configuration.Stop.QuestSequences[text2] = 1; } - string text3 = elementId.ToString(); - int currentItem3 = (int)base.Configuration.Stop.QuestStopModes.GetValueOrDefault(text3, Questionable.Configuration.EStopConditionMode.Stop); - ImGui.SameLine(); - ImGui.SetNextItemWidth(70f); - ImU8String label = new ImU8String(6, 1); - label.AppendLiteral("##Mode"); - label.AppendFormatted(text3); - if (ImGui.Combo(label, ref currentItem3, (ReadOnlySpan)StopModeNames, StopModeNames.Length)) + else { - if (currentItem3 == 0) - { - quest = quest2; - } - else - { - base.Configuration.Stop.QuestStopModes[text3] = (Configuration.EStopConditionMode)currentItem3; - Save(); - } + base.Configuration.Stop.QuestSequences.Remove(text2); } + Save(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Stop at specific sequence (unchecked = stop on quest completion)"); + } + using (ImRaii.Disabled(!v)) + { ImGui.SameLine(); - base.Configuration.Stop.QuestSequences.TryGetValue(text3, out var value); - bool v2 = value.HasValue; - ImU8String label2 = new ImU8String(8, 1); - label2.AppendLiteral("##UseSeq"); - label2.AppendFormatted(text3); - if (ImGui.Checkbox(label2, ref v2)) + ImGui.Text("Seq:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(85f); + int data = value ?? 1; + ImU8String label3 = new ImU8String(10, 1); + label3.AppendLiteral("##SeqValue"); + label3.AppendFormatted(text2); + if (ImGui.InputInt(label3, ref data, 1, 1) && v) { - if (v2) - { - base.Configuration.Stop.QuestSequences[text3] = 1; - } - else - { - base.Configuration.Stop.QuestSequences.Remove(text3); - } + base.Configuration.Stop.QuestSequences[text2] = Math.Max(0, Math.Min(255, data)); Save(); } - if (ImGui.IsItemHovered()) + } + if (questsToStopAfter.Count > 1) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) { - ImGui.SetTooltip("Stop at specific sequence (unchecked = stop on quest completion)"); + ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 4f - ImGui.GetStyle().ItemSpacing.X); } - using (ImRaii.Disabled(!v2)) + if (_draggedItem == elementId) { - ImGui.SameLine(); - ImGui.Text("Seq:"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(85f); - int data3 = value ?? 1; - ImU8String label3 = new ImU8String(10, 1); - label3.AppendLiteral("##SeqValue"); - label3.AppendFormatted(text3); - if (ImGui.InputInt(label3, ref data3, 1, 1) && v2) - { - base.Configuration.Stop.QuestSequences[text3] = Math.Max(0, Math.Min(255, data3)); - Save(); - } + ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown, ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive))); } + else + { + ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown); + } + if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + { + _draggedItem = elementId; + } + ImGui.SameLine(); + } + else + { using (ImRaii.PushFont(UiBuilder.IconFont)) { ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 2f); } - if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times)) - { - quest = quest2; - } + } + if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times)) + { + quest = quest3; } } - if (quest != null) + Vector2 item2 = new Vector2(item.X + x, ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2f); + list.Add((item, item2)); + } + if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + { + _draggedItem = null; + } + else if (_draggedItem != null) + { + int num = questsToStopAfter.FindIndex((ElementId elementId2) => elementId2 == _draggedItem); + if (num >= 0 && num < list.Count) { - string key2 = quest.Id.ToString(); - base.Configuration.Stop.QuestsToStopAfter.Remove(quest.Id); - base.Configuration.Stop.QuestSequences.Remove(key2); - base.Configuration.Stop.QuestStopModes.Remove(key2); - Save(); + var (pMin, pMax) = list[num]; + ImGui.GetWindowDrawList().AddRect(pMin, pMax, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f, ImDrawFlags.RoundCornersAll); + int num2 = list.FindIndex(((Vector2 TopLeft, Vector2 BottomRight) tuple2) => ImGui.IsMouseHoveringRect(tuple2.TopLeft, tuple2.BottomRight, clip: true)); + if (num2 >= 0 && num != num2) + { + quest2 = (_questRegistry.TryGetQuest(_draggedItem, out Quest quest4) ? quest4 : null); + index = num2; + } } } + if (quest != null) + { + string key = quest.Id.ToString(); + base.Configuration.Stop.QuestsToStopAfter.Remove(quest.Id); + base.Configuration.Stop.QuestSequences.Remove(key); + base.Configuration.Stop.QuestStopModes.Remove(key); + Save(); + } + if (quest2 != null) + { + questsToStopAfter.Remove(quest2.Id); + questsToStopAfter.Insert(index, quest2.Id); + Save(); + } } private void DrawCurrentlyAcceptedQuests() diff --git a/Questionable/Questionable.Windows.JournalComponents/DutyJournalComponent.cs b/Questionable/Questionable.Windows.JournalComponents/DutyJournalComponent.cs new file mode 100644 index 0000000..b21e4e1 --- /dev/null +++ b/Questionable/Questionable.Windows.JournalComponents/DutyJournalComponent.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; +using Microsoft.Extensions.Logging; +using Questionable.Data; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Windows.JournalComponents; + +internal sealed class DutyJournalComponent +{ + private sealed record DutyCategory(string Name, List Duties); + + private sealed record DutyCounts(int Unlocked, int Completed, int CompletionTrackable, int Total) + { + public DutyCounts() + : this(0, 0, 0, 0) + { + } + } + + private readonly DutyUnlockData _dutyUnlockData; + + private readonly QuestData _questData; + + private readonly QuestFunctions _questFunctions; + + private readonly UiUtils _uiUtils; + + private readonly IDalamudPluginInterface _pluginInterface; + + private readonly ILogger _logger; + + private List _filteredCategories = new List(); + + private Dictionary _categoryCounts = new Dictionary(); + + private string _searchText = string.Empty; + + private bool _showLockedOnly; + + private bool _showUnlockedOnly; + + public DutyJournalComponent(DutyUnlockData dutyUnlockData, QuestData questData, QuestFunctions questFunctions, UiUtils uiUtils, IDalamudPluginInterface pluginInterface, ILogger logger) + { + _dutyUnlockData = dutyUnlockData; + _questData = questData; + _questFunctions = questFunctions; + _uiUtils = uiUtils; + _pluginInterface = pluginInterface; + _logger = logger; + } + + public void DrawDuties() + { + using ImRaii.IEndObject endObject = ImRaii.TabItem("Duties"); + if (!endObject) + { + return; + } + if (ImGui.CollapsingHeader("Explanation", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.Text("The list below shows all duties and their unlock status."); + ImGui.BulletText("'Unlocked' shows duties you have access to."); + ImGui.BulletText("'Completed' shows duties you have finished at least once."); + ImGui.BulletText("Click on a duty to see which quest unlocks it."); + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + } + DrawFilterControls(); + if (_filteredCategories.Count > 0) + { + using (ImRaii.IEndObject endObject2 = ImRaii.Table("Duties", 3, ImGuiTableFlags.NoSavedSettings)) + { + if (!endObject2) + { + return; + } + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide); + ImGui.TableSetupColumn("Unlocked", ImGuiTableColumnFlags.WidthFixed, 120f * ImGui.GetIO().FontGlobalScale); + ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120f * ImGui.GetIO().FontGlobalScale); + ImGui.TableHeadersRow(); + foreach (DutyCategory filteredCategory in _filteredCategories) + { + DrawCategory(filteredCategory); + } + return; + } + } + ImGui.Text("No duties match your search."); + } + + private void DrawFilterControls() + { + if (ImGui.Checkbox("Locked Only", ref _showLockedOnly)) + { + if (_showLockedOnly) + { + _showUnlockedOnly = false; + } + UpdateFilter(); + } + ImGui.SameLine(); + if (ImGui.Checkbox("Unlocked Only", ref _showUnlockedOnly)) + { + if (_showUnlockedOnly) + { + _showLockedOnly = false; + } + UpdateFilter(); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint(string.Empty, "Search duties...", ref _searchText, 256)) + { + UpdateFilter(); + } + } + + private void DrawCategory(DutyCategory category) + { + DutyCounts valueOrDefault = _categoryCounts.GetValueOrDefault(category.Name, new DutyCounts()); + if (valueOrDefault.Total == 0) + { + return; + } + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + bool num = ImGui.TreeNodeEx(category.Name, ImGuiTreeNodeFlags.SpanFullWidth); + ImGui.TableNextColumn(); + DrawCount(valueOrDefault.Unlocked, valueOrDefault.Total); + ImGui.TableNextColumn(); + DrawCount(valueOrDefault.Completed, valueOrDefault.CompletionTrackable); + if (!num) + { + return; + } + foreach (DutyInfo duty in category.Duties) + { + DrawDuty(duty); + } + ImGui.TreePop(); + } + + private void DrawDuty(DutyInfo duty) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + string text = $"Lv{duty.Level} {duty.Name}"; + if (duty.ItemLevel > 0) + { + text += $" (i{duty.ItemLevel})"; + } + ImGui.TreeNodeEx(text, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.SpanFullWidth); + if (ImGui.IsItemHovered()) + { + DrawDutyTooltip(duty); + } + ImGui.TableNextColumn(); + DrawDutyStatus(duty.IsUnlocked); + ImGui.TableNextColumn(); + DrawDutyStatus(duty.IsCompleted); + } + + private void DrawDutyTooltip(DutyInfo duty) + { + using ImRaii.IEndObject endObject = ImRaii.Tooltip(); + if (!endObject) + { + return; + } + ImGui.TextColored(ImGuiColors.DalamudWhite, duty.Name); + Vector4 col = ImGuiColors.DalamudGrey; + ImU8String text = new ImU8String(12, 1); + text.AppendLiteral("Content ID: "); + text.AppendFormatted(duty.ContentFinderConditionId); + ImGui.TextColored(in col, text); + ImGui.Separator(); + ImU8String text2 = new ImU8String(16, 1); + text2.AppendLiteral("Level Required: "); + text2.AppendFormatted(duty.Level); + ImGui.Text(text2); + if (duty.ItemLevel > 0) + { + ImU8String text3 = new ImU8String(21, 1); + text3.AppendLiteral("Item Level Required: "); + text3.AppendFormatted(duty.ItemLevel); + ImGui.Text(text3); + } + ImU8String text4 = new ImU8String(6, 1); + text4.AppendLiteral("Type: "); + text4.AppendFormatted(duty.ContentTypeName); + ImGui.Text(text4); + if (duty.IsHighEndDuty) + { + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.DalamudOrange, "(High-End)"); + } + ImGui.Separator(); + if (duty.IsUnlocked) + { + ImGui.TextColored(ImGuiColors.ParsedGreen, "Status: Unlocked"); + } + else + { + ImGui.TextColored(ImGuiColors.DalamudRed, "Status: Locked"); + } + if (duty.UnlockQuests.Count > 0) + { + ImGui.Separator(); + ImGui.Text("Unlock Quest(s):"); + { + foreach (QuestId unlockQuest in duty.UnlockQuests) + { + if (_questData.TryGetQuestInfo(unlockQuest, out IQuestInfo questInfo)) + { + var (color, icon, _) = _uiUtils.GetQuestStyle(unlockQuest); + _uiUtils.ChecklistItem($"{questInfo.Name} ({unlockQuest})", color, icon); + } + else + { + _uiUtils.ChecklistItem($"Unknown Quest ({unlockQuest})", ImGuiColors.DalamudGrey, FontAwesomeIcon.Question); + } + } + return; + } + } + ImGui.Separator(); + ImGui.TextColored(ImGuiColors.DalamudGrey, "No unlock quest data available."); + } + + private void DrawDutyStatus(bool? status) + { + float num; + using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + { + num = ImGui.GetColumnWidth() / 2f - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X; + } + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num); + if (!status.HasValue) + { + _uiUtils.ChecklistItem(string.Empty, ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus); + } + else if (status.Value) + { + _uiUtils.ChecklistItem(string.Empty, ImGuiColors.ParsedGreen, FontAwesomeIcon.Check); + } + else + { + _uiUtils.ChecklistItem(string.Empty, ImGuiColors.DalamudRed, FontAwesomeIcon.Times); + } + } + + private static void DrawCount(int count, int total) + { + string text = 9999.ToString(CultureInfo.CurrentCulture); + ImGui.PushFont(UiBuilder.MonoFont); + if (total == 0) + { + Vector4 col = ImGuiColors.DalamudGrey; + ImU8String text2 = new ImU8String(3, 2); + text2.AppendFormatted("-".PadLeft(text.Length)); + text2.AppendLiteral(" / "); + text2.AppendFormatted("-".PadLeft(text.Length)); + ImGui.TextColored(in col, text2); + } + else + { + string text3 = count.ToString(CultureInfo.CurrentCulture).PadLeft(text.Length) + " / " + total.ToString(CultureInfo.CurrentCulture).PadLeft(text.Length); + if (count == total) + { + ImGui.TextColored(ImGuiColors.ParsedGreen, text3); + } + else + { + ImGui.TextUnformatted(text3); + } + } + ImGui.PopFont(); + } + + public void UpdateFilter() + { + List list = new List(); + AddCategory(list, "Dungeons", _dutyUnlockData.GetDungeons()); + AddCategory(list, "Hard Dungeons", _dutyUnlockData.GetHardDungeons()); + AddCategory(list, "Guildhests", _dutyUnlockData.GetGuildhests()); + AddCategory(list, "Trials", _dutyUnlockData.GetTrials()); + AddCategory(list, "Hard Trials", _dutyUnlockData.GetHardTrials()); + AddCategory(list, "Extreme Trials", _dutyUnlockData.GetExtremeTrials()); + AddCategory(list, "Unreal Trials", _dutyUnlockData.GetUnrealTrials()); + AddCategory(list, "Normal Raids", _dutyUnlockData.GetNormalRaids()); + AddCategory(list, "Savage Raids", _dutyUnlockData.GetSavageRaids()); + AddCategory(list, "Alliance Raids", _dutyUnlockData.GetAllianceRaids()); + AddCategory(list, "Chaotic Alliance Raids", _dutyUnlockData.GetChaoticAllianceRaids()); + AddCategory(list, "Ultimate Raids", _dutyUnlockData.GetUltimateRaids()); + AddCategory(list, "Deep Dungeons", _dutyUnlockData.GetDeepDungeons()); + AddCategory(list, "Variant Dungeons", _dutyUnlockData.GetVariantDungeons()); + AddCategory(list, "Criterion Dungeons", _dutyUnlockData.GetCriterionDungeons()); + AddCategory(list, "Criterion Savage", _dutyUnlockData.GetCriterionSavageDungeons()); + _filteredCategories = list.Where((DutyCategory c) => c.Duties.Count > 0).ToList(); + RefreshCounts(); + } + + private void AddCategory(List categories, string name, IEnumerable duties) + { + List list = (from d in duties + where MatchesFilter(d) + orderby d.Level, d.ItemLevel, d.ContentFinderConditionId + select d).ToList(); + if (list.Count > 0) + { + categories.Add(new DutyCategory(name, list)); + } + } + + private bool MatchesFilter(DutyInfo duty) + { + if (!string.IsNullOrEmpty(_searchText) && !duty.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + if (_showLockedOnly && duty.IsUnlocked) + { + return false; + } + if (_showUnlockedOnly && !duty.IsUnlocked) + { + return false; + } + return true; + } + + public void RefreshCounts() + { + _categoryCounts.Clear(); + foreach (DutyCategory filteredCategory in _filteredCategories) + { + int unlocked = filteredCategory.Duties.Count((DutyInfo d) => d.IsUnlocked); + int completed = filteredCategory.Duties.Count((DutyInfo d) => d.IsCompleted == true); + int completionTrackable = filteredCategory.Duties.Count((DutyInfo d) => d.IsCompleted.HasValue); + int count = filteredCategory.Duties.Count; + _categoryCounts[filteredCategory.Name] = new DutyCounts(unlocked, completed, completionTrackable, count); + } + } + + public void ClearCounts(int type, int code) + { + foreach (string item in _categoryCounts.Keys.ToList()) + { + _categoryCounts[item] = _categoryCounts[item]with + { + Unlocked = 0, + Completed = 0, + CompletionTrackable = 0 + }; + } + } +} diff --git a/Questionable/Questionable.Windows.JournalComponents/QuestJournalComponent.cs b/Questionable/Questionable.Windows.JournalComponents/QuestJournalComponent.cs index ae247b6..c921dc9 100644 --- a/Questionable/Questionable.Windows.JournalComponents/QuestJournalComponent.cs +++ b/Questionable/Questionable.Windows.JournalComponents/QuestJournalComponent.cs @@ -388,13 +388,18 @@ internal sealed class QuestJournalComponent { bool num = IsQuestExpired(questInfo); bool flag = _questFunctions.IsQuestUnobtainable(questInfo.QuestId); - bool flag2 = _questFunctions.IsQuestLocked(questInfo.QuestId); - bool flag3 = _questFunctions.IsReadyToAcceptQuest(questInfo.QuestId); + bool flag2 = _questFunctions.IsQuestBlacklisted(questInfo.QuestId); + bool flag3 = _questFunctions.IsQuestLocked(questInfo.QuestId); + bool flag4 = _questFunctions.IsReadyToAcceptQuest(questInfo.QuestId); if (num || flag) { _uiUtils.ChecklistItem("Unobtainable", ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus); } - else if (flag2 || !flag3 || !_questRegistry.IsKnownQuest(questInfo.QuestId)) + else if (flag2) + { + _uiUtils.ChecklistItem("Blacklisted", ImGuiColors.DalamudGrey3, FontAwesomeIcon.Ban); + } + else if (flag3 || !flag4 || !_questRegistry.IsKnownQuest(questInfo.QuestId)) { _uiUtils.ChecklistItem("Locked", ImGuiColors.DalamudRed, FontAwesomeIcon.Times); } diff --git a/Questionable/Questionable.Windows.QuestComponents/ActiveQuestComponent.cs b/Questionable/Questionable.Windows.QuestComponents/ActiveQuestComponent.cs index d42a064..62f0770 100644 --- a/Questionable/Questionable.Windows.QuestComponents/ActiveQuestComponent.cs +++ b/Questionable/Questionable.Windows.QuestComponents/ActiveQuestComponent.cs @@ -57,7 +57,7 @@ internal sealed class ActiveQuestComponent public event EventHandler? Reload; [GeneratedRegex("\\s\\s+", RegexOptions.IgnoreCase, "en-US")] - [GeneratedCode("System.Text.RegularExpressions.Generator", "10.0.13.7005")] + [GeneratedCode("System.Text.RegularExpressions.Generator", "10.0.13.11305")] private static Regex MultipleWhitespaceRegex() { return _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0.Instance; diff --git a/Questionable/Questionable.Windows.QuestComponents/ManualPriorityComponent.cs b/Questionable/Questionable.Windows.QuestComponents/ManualPriorityComponent.cs index 982fa25..6f7bf1c 100644 --- a/Questionable/Questionable.Windows.QuestComponents/ManualPriorityComponent.cs +++ b/Questionable/Questionable.Windows.QuestComponents/ManualPriorityComponent.cs @@ -51,6 +51,7 @@ internal sealed class ManualPriorityComponent _questSelector.QuestSelected = delegate(Quest quest) { manualPriorityComponent._questController.ManualPriorityQuests.Add(quest); + manualPriorityComponent._questController.SavePriorityQuests(); }; } @@ -92,9 +93,9 @@ internal sealed class ManualPriorityComponent ExportToClipboard(); } ImGui.SameLine(); - if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove finished Quests")) + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove finished Quests") && _questController.ManualPriorityQuests.RemoveAll((Quest q) => _questFunctions.IsQuestComplete(q.Id)) > 0) { - _questController.ManualPriorityQuests.RemoveAll((Quest q) => _questFunctions.IsQuestComplete(q.Id)); + _questController.SavePriorityQuests(); } ImGui.SameLine(); using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) @@ -212,11 +213,13 @@ internal sealed class ManualPriorityComponent if (quest != null) { manualPriorityQuests.Remove(quest); + _questController.SavePriorityQuests(); } if (quest2 != null) { manualPriorityQuests.Remove(quest2); manualPriorityQuests.Insert(index, quest2); + _questController.SavePriorityQuests(); } } diff --git a/Questionable/Questionable.Windows.QuestComponents/QuestSequenceComponent.cs b/Questionable/Questionable.Windows.QuestComponents/QuestSequenceComponent.cs index 7b61031..b28c9a8 100644 --- a/Questionable/Questionable.Windows.QuestComponents/QuestSequenceComponent.cs +++ b/Questionable/Questionable.Windows.QuestComponents/QuestSequenceComponent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; @@ -35,6 +36,8 @@ internal sealed class QuestSequenceComponent private int _hoveredSequenceId = -1; + public bool HasLookedUpQuest => _lookedUpQuestId != null; + public QuestSequenceComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, IDataManager dataManager, ILogger logger) { _questData = questData; @@ -96,6 +99,26 @@ internal sealed class QuestSequenceComponent { DrawEmptyState(FontAwesomeIcon.QuestionCircle, "No Active Quest", "Accept a quest to view its sequence information.", new Vector4(0.6f, 0.5f, 0.8f, 0.7f)); } + if (!(tuple.Item1 is QuestId questId)) + { + return; + } + ImGui.Spacing(); + if (ImGui.Button("Copy filename")) + { + if (_questData.TryGetQuestInfo(questId, out IQuestInfo questInfo)) + { + ImGui.SetClipboardText(GetSequenceFilename(questId.Value, questInfo.Name)); + } + else + { + ImGui.SetClipboardText(GetSequenceFilename(questId.Value, questId.Value.ToString(CultureInfo.InvariantCulture))); + } + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Copies filename as QuestID_QuestNameWithSpaces.json"); + } } public void DrawQuestLookupTab() @@ -230,6 +253,25 @@ internal sealed class QuestSequenceComponent text.AppendFormatted(questId.Value); text.AppendLiteral(")"); ImGui.TextColored(in col, text); + ImGui.SameLine(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 8f); + using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0.18f, 0.16f, 0.22f, 0.8f))) + { + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, new Vector4(0.28f, 0.24f, 0.32f, 0.95f))) + { + using (ImRaii.PushColor(ImGuiCol.ButtonActive, new Vector4(0.35f, 0.3f, 0.45f, 1f))) + { + if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) + { + ImGui.SetClipboardText(GetSequenceFilename(questId.Value, questInfo.Name)); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Copies filename as QuestID_QuestNameWithSpaces.json"); + } + } + } + } if (currentSequence.HasValue) { ImGui.SameLine(); @@ -592,4 +634,15 @@ internal sealed class QuestSequenceComponent } ImGui.SetCursorScreenPos(cursorScreenPos + new Vector2(0f, vector.Y)); } + + private static string GetSequenceFilename(int questId, string questName) + { + string value = (questName ?? string.Empty).Trim(); + return $"{questId}_{value}.json"; + } + + public void SetLookedUpQuest(ElementId? id) + { + _lookedUpQuestId = id; + } } diff --git a/Questionable/Questionable.Windows/ConfigWindow.cs b/Questionable/Questionable.Windows/ConfigWindow.cs index c9e4a26..ef7f0e0 100644 --- a/Questionable/Questionable.Windows/ConfigWindow.cs +++ b/Questionable/Questionable.Windows/ConfigWindow.cs @@ -23,6 +23,8 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig private readonly StopConditionComponent _stopConditionComponent; + private readonly BlacklistConfigComponent _blacklistConfigComponent; + private readonly NotificationConfigComponent _notificationConfigComponent; private readonly DebugConfigComponent _debugConfigComponent; @@ -31,7 +33,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig public WindowConfig WindowConfig => _configuration.ConfigWindowConfig; - public ConfigWindow(IDalamudPluginInterface pluginInterface, GeneralConfigComponent generalConfigComponent, PluginConfigComponent pluginConfigComponent, DutyConfigComponent dutyConfigComponent, SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent, StopConditionComponent stopConditionComponent, NotificationConfigComponent notificationConfigComponent, DebugConfigComponent debugConfigComponent, Configuration configuration, ChangelogWindow changelogWindow) + public ConfigWindow(IDalamudPluginInterface pluginInterface, GeneralConfigComponent generalConfigComponent, PluginConfigComponent pluginConfigComponent, DutyConfigComponent dutyConfigComponent, SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent, StopConditionComponent stopConditionComponent, BlacklistConfigComponent blacklistConfigComponent, NotificationConfigComponent notificationConfigComponent, DebugConfigComponent debugConfigComponent, Configuration configuration, ChangelogWindow changelogWindow) : base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize) { _pluginInterface = pluginInterface; @@ -40,6 +42,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig _dutyConfigComponent = dutyConfigComponent; _singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent; _stopConditionComponent = stopConditionComponent; + _blacklistConfigComponent = blacklistConfigComponent; _notificationConfigComponent = notificationConfigComponent; _debugConfigComponent = debugConfigComponent; _configuration = configuration; @@ -70,6 +73,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig _dutyConfigComponent.DrawTab(); _singlePlayerDutyConfigComponent.DrawTab(); _stopConditionComponent.DrawTab(); + _blacklistConfigComponent.DrawTab(); _notificationConfigComponent.DrawTab(); _debugConfigComponent.DrawTab(); } diff --git a/Questionable/Questionable.Windows/JournalProgressWindow.cs b/Questionable/Questionable.Windows/JournalProgressWindow.cs index 386aefc..2614037 100644 --- a/Questionable/Questionable.Windows/JournalProgressWindow.cs +++ b/Questionable/Questionable.Windows/JournalProgressWindow.cs @@ -19,21 +19,26 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable private readonly GatheringJournalComponent _gatheringJournalComponent; + private readonly DutyJournalComponent _dutyJournalComponent; + private readonly QuestRegistry _questRegistry; private readonly IClientState _clientState; - public JournalProgressWindow(QuestJournalComponent questJournalComponent, QuestRewardComponent questRewardComponent, AlliedSocietyJournalComponent alliedSocietyJournalComponent, GatheringJournalComponent gatheringJournalComponent, QuestRegistry questRegistry, IClientState clientState) + public JournalProgressWindow(QuestJournalComponent questJournalComponent, QuestRewardComponent questRewardComponent, AlliedSocietyJournalComponent alliedSocietyJournalComponent, GatheringJournalComponent gatheringJournalComponent, DutyJournalComponent dutyJournalComponent, QuestRegistry questRegistry, IClientState clientState) : base("Journal Progress###QuestionableJournalProgress") { _questJournalComponent = questJournalComponent; _alliedSocietyJournalComponent = alliedSocietyJournalComponent; _questRewardComponent = questRewardComponent; _gatheringJournalComponent = gatheringJournalComponent; + _dutyJournalComponent = dutyJournalComponent; _questRegistry = questRegistry; _clientState = clientState; _clientState.Login += _questJournalComponent.RefreshCounts; + _clientState.Logout += _dutyJournalComponent.ClearCounts; _clientState.Login += _gatheringJournalComponent.RefreshCounts; + _clientState.Login += _dutyJournalComponent.RefreshCounts; _clientState.Logout += _questJournalComponent.ClearCounts; _clientState.Logout += _gatheringJournalComponent.ClearCounts; _questRegistry.Reloaded += OnQuestsReloaded; @@ -46,6 +51,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable private void OnQuestsReloaded(object? sender, EventArgs e) { _questJournalComponent.RefreshCounts(); + _dutyJournalComponent.RefreshCounts(); _gatheringJournalComponent.RefreshCounts(); } @@ -53,6 +59,8 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable { _questJournalComponent.UpdateFilter(); _questJournalComponent.RefreshCounts(); + _dutyJournalComponent.UpdateFilter(); + _dutyJournalComponent.RefreshCounts(); _gatheringJournalComponent.UpdateFilter(); _gatheringJournalComponent.RefreshCounts(); } @@ -63,6 +71,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable if (!(!endObject)) { _questJournalComponent.DrawQuests(); + _dutyJournalComponent.DrawDuties(); _alliedSocietyJournalComponent.DrawAlliedSocietyQuests(); _questRewardComponent.DrawItemRewards(); _gatheringJournalComponent.DrawGatheringItems(); @@ -74,7 +83,9 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable _questRegistry.Reloaded -= OnQuestsReloaded; _clientState.Logout -= _gatheringJournalComponent.ClearCounts; _clientState.Logout -= _questJournalComponent.ClearCounts; + _clientState.Login -= _dutyJournalComponent.RefreshCounts; _clientState.Login -= _gatheringJournalComponent.RefreshCounts; + _clientState.Logout -= _dutyJournalComponent.ClearCounts; _clientState.Login -= _questJournalComponent.RefreshCounts; } } diff --git a/Questionable/Questionable.Windows/QuestSequenceWindow.cs b/Questionable/Questionable.Windows/QuestSequenceWindow.cs index 060e289..7c2e568 100644 --- a/Questionable/Questionable.Windows/QuestSequenceWindow.cs +++ b/Questionable/Questionable.Windows/QuestSequenceWindow.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using LLib.ImGui; +using Questionable.Model.Questing; using Questionable.Windows.QuestComponents; namespace Questionable.Windows; @@ -16,6 +17,8 @@ internal sealed class QuestSequenceWindow : LWindow, IPersistableWindowConfig private readonly QuestSequenceComponent _questSequenceComponent; + private bool _selectLookupTabNextFrame; + public WindowConfig WindowConfig => _configuration.QuestSequenceWindowConfig; public QuestSequenceWindow(IDalamudPluginInterface pluginInterface, Configuration configuration, QuestSequenceComponent questSequenceComponent) @@ -38,6 +41,13 @@ internal sealed class QuestSequenceWindow : LWindow, IPersistableWindowConfig _pluginInterface.SavePluginConfig(_configuration); } + public void OpenForQuest(ElementId questId) + { + _questSequenceComponent.SetLookedUpQuest(questId); + _selectLookupTabNextFrame = true; + base.IsOpenAndUncollapsed = true; + } + public override void DrawContent() { bool isOpen = base.IsOpen; @@ -51,31 +61,29 @@ internal sealed class QuestSequenceWindow : LWindow, IPersistableWindowConfig { using (ImRaii.PushColor(ImGuiCol.TabActive, new Vector4(0.28f, 0.24f, 0.35f, 1f))) { - using (ImRaii.PushColor(ImGuiCol.TabUnfocused, new Vector4(0.13f, 0.11f, 0.18f, 0.6f))) + using ImRaii.IEndObject endObject = ImRaii.TabBar("QuestSequenceTabs", ImGuiTabBarFlags.None); + if (!endObject) { - using (ImRaii.PushColor(ImGuiCol.TabUnfocusedActive, new Vector4(0.25f, 0.22f, 0.3f, 0.95f))) + return; + } + using (ImRaii.IEndObject endObject2 = ImRaii.TabItem("Current Quest", ImGuiTabItemFlags.None)) + { + if (endObject2) { - using ImRaii.IEndObject endObject = ImRaii.TabBar("QuestSequenceTabs", ImGuiTabBarFlags.None); - if (!endObject) - { - return; - } - using (ImRaii.IEndObject endObject2 = ImRaii.TabItem("Current Quest")) - { - if (endObject2) - { - ImGui.Spacing(); - _questSequenceComponent.DrawCurrentQuestTab(); - } - } - using ImRaii.IEndObject endObject3 = ImRaii.TabItem("Quest Lookup"); - if (endObject3) - { - ImGui.Spacing(); - _questSequenceComponent.DrawQuestLookupTab(); - } + ImGui.Spacing(); + _questSequenceComponent.DrawCurrentQuestTab(); } } + ImGuiTabItemFlags flags = (_selectLookupTabNextFrame ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None); + using (ImRaii.IEndObject endObject3 = ImRaii.TabItem("Quest Lookup", flags)) + { + if (endObject3) + { + ImGui.Spacing(); + _questSequenceComponent.DrawQuestLookupTab(); + } + } + _selectLookupTabNextFrame = false; } } } diff --git a/Questionable/Questionable.Windows/UiUtils.cs b/Questionable/Questionable.Windows/UiUtils.cs index a1bb103..10061ac 100644 --- a/Questionable/Questionable.Windows/UiUtils.cs +++ b/Questionable/Questionable.Windows/UiUtils.cs @@ -33,6 +33,10 @@ internal sealed class UiUtils } if (elementId is QuestId questId && _questFunctions.IsDailyAlliedSocietyQuestAndAvailableToday(questId)) { + if (_questFunctions.IsQuestBlacklisted(questId)) + { + return (Color: ImGuiColors.DalamudGrey3, Icon: FontAwesomeIcon.Ban, Status: "Blacklisted"); + } if (!_questFunctions.IsReadyToAcceptQuest(questId)) { return (Color: ImGuiColors.ParsedGreen, Icon: FontAwesomeIcon.Check, Status: "Complete"); @@ -51,6 +55,10 @@ internal sealed class UiUtils { return (Color: ImGuiColors.DalamudGrey, Icon: FontAwesomeIcon.Minus, Status: "Unobtainable"); } + if (_questFunctions.IsQuestBlacklisted(elementId)) + { + return (Color: ImGuiColors.DalamudGrey3, Icon: FontAwesomeIcon.Ban, Status: "Blacklisted"); + } if (_questFunctions.IsQuestLocked(elementId) || !_questFunctions.IsReadyToAcceptQuest(elementId) || !_questRegistry.IsKnownQuest(elementId)) { return (Color: ImGuiColors.DalamudRed, Icon: FontAwesomeIcon.Times, Status: "Locked"); diff --git a/Questionable/Questionable/Configuration.cs b/Questionable/Questionable/Configuration.cs index 14390e8..0090f04 100644 --- a/Questionable/Questionable/Configuration.cs +++ b/Questionable/Questionable/Configuration.cs @@ -22,6 +22,8 @@ internal sealed class Configuration : IPluginConfiguration public EClassJob CombatJob { get; set; } + public EMsqPriorityMode MsqPriority { get; set; } + public bool HideInAllInstances { get; set; } = true; public bool UseEscToCancelQuesting { get; set; } = true; @@ -44,9 +46,21 @@ internal sealed class Configuration : IPluginConfiguration public bool ClearPriorityQuestsOnCompletion { get; set; } + public bool PersistPriorityQuestsBetweenSessions { get; set; } + public bool ShowChangelogOnUpdate { get; set; } = true; public bool StopOnPlayerInput { get; set; } + + public bool AutoSolveQte { get; set; } = true; + + public bool AutoSnipe { get; set; } = true; + + [JsonProperty(ItemConverterType = typeof(ElementIdNConverter))] + public HashSet BlacklistedQuests { get; set; } = new HashSet(); + + [JsonProperty(ItemConverterType = typeof(ElementIdNConverter))] + public List PriorityQuests { get; set; } = new List(); } internal sealed class StopConfiguration @@ -129,6 +143,8 @@ internal sealed class Configuration : IPluginConfiguration public bool SkipCrystalTowerRaids { get; set; } public bool PreventQuestCompletion { get; set; } + + public string ChocoboName { get; set; } = string.Empty; } internal enum ECombatModule @@ -139,6 +155,13 @@ internal sealed class Configuration : IPluginConfiguration RotationSolverReborn } + internal enum EMsqPriorityMode + { + Always, + AfterTracked, + Manual + } + internal enum EStopConditionMode { Off, diff --git a/Questionable/Questionable/QuestionablePlugin.cs b/Questionable/Questionable/QuestionablePlugin.cs index 0cae44d..d95b5a8 100644 --- a/Questionable/Questionable/QuestionablePlugin.cs +++ b/Questionable/Questionable/QuestionablePlugin.cs @@ -5,6 +5,7 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using LLib; using LLib.Gear; +using LLib.Shop; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Controller; @@ -36,7 +37,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable { private readonly ServiceProvider? _serviceProvider; - public QuestionablePlugin(IDalamudPluginInterface pluginInterface, IClientState clientState, ITargetManager targetManager, IFramework framework, IGameGui gameGui, IDataManager dataManager, ISigScanner sigScanner, IObjectTable objectTable, IPluginLog pluginLog, ICondition condition, IChatGui chatGui, ICommandManager commandManager, IAddonLifecycle addonLifecycle, IKeyState keyState, IContextMenu contextMenu, IToastGui toastGui, IGameInteropProvider gameInteropProvider, IAetheryteList aetheryteList) + public QuestionablePlugin(IDalamudPluginInterface pluginInterface, IClientState clientState, ITargetManager targetManager, IFramework framework, IGameGui gameGui, IDataManager dataManager, ISigScanner sigScanner, IObjectTable objectTable, IPluginLog pluginLog, ICondition condition, IChatGui chatGui, ICommandManager commandManager, IAddonLifecycle addonLifecycle, IKeyState keyState, IContextMenu contextMenu, IToastGui toastGui, IGameInteropProvider gameInteropProvider, IAetheryteList aetheryteList, IGameConfig gameConfig) { ArgumentNullException.ThrowIfNull(pluginInterface, "pluginInterface"); ArgumentNullException.ThrowIfNull(chatGui, "chatGui"); @@ -74,6 +75,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable serviceCollection.AddSingleton(toastGui); serviceCollection.AddSingleton(gameInteropProvider); serviceCollection.AddSingleton(aetheryteList); + serviceCollection.AddSingleton(gameConfig); serviceCollection.AddSingleton(new WindowSystem("Questionable")); serviceCollection.AddSingleton(((Configuration)pluginInterface.GetPluginConfig()) ?? new Configuration()); AddBasicFunctionsAndData(serviceCollection); @@ -106,6 +108,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -115,10 +118,10 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } @@ -216,10 +219,15 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -247,6 +255,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -261,6 +270,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -290,13 +300,20 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable serviceProvider.GetRequiredService().Reload(); serviceProvider.GetRequiredService().Reload(); serviceProvider.GetRequiredService().Reload(); + serviceProvider.GetRequiredService().LoadPriorityQuests(); serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); ChangelogWindow requiredService = serviceProvider.GetRequiredService(); diff --git a/Questionable/System.Text.RegularExpressions.Generated/-RegexGenerator_g-FBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0.cs b/Questionable/System.Text.RegularExpressions.Generated/-RegexGenerator_g-FBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0.cs index e2b40e7..6d16aec 100644 --- a/Questionable/System.Text.RegularExpressions.Generated/-RegexGenerator_g-FBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0.cs +++ b/Questionable/System.Text.RegularExpressions.Generated/-RegexGenerator_g-FBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; namespace System.Text.RegularExpressions.Generated; -[GeneratedCode("System.Text.RegularExpressions.Generator", "10.0.13.7005")] +[GeneratedCode("System.Text.RegularExpressions.Generator", "10.0.13.11305")] [SkipLocalsInit] internal sealed class _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0 : Regex { diff --git a/Questionable/System.Text.RegularExpressions.Generated/-RegexGenerator_g-FBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities.cs b/Questionable/System.Text.RegularExpressions.Generated/-RegexGenerator_g-FBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities.cs index ba18887..f3d94c7 100644 --- a/Questionable/System.Text.RegularExpressions.Generated/-RegexGenerator_g-FBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities.cs +++ b/Questionable/System.Text.RegularExpressions.Generated/-RegexGenerator_g-FBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities.cs @@ -3,7 +3,7 @@ using System.CodeDom.Compiler; namespace System.Text.RegularExpressions.Generated; -[GeneratedCode("System.Text.RegularExpressions.Generator", "10.0.13.7005")] +[GeneratedCode("System.Text.RegularExpressions.Generator", "10.0.13.11305")] internal static class _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities { internal static readonly TimeSpan s_defaultTimeout = ((AppContext.GetData("REGEX_DEFAULT_MATCH_TIMEOUT") is TimeSpan timeSpan) ? timeSpan : Regex.InfiniteMatchTimeout);