diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index 6843b84c..ed3a1c29 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -172,7 +172,7 @@ internal sealed class QuestData private void AddPreviousQuest(QuestId questToUpdate, QuestId requiredQuestId) { QuestInfo quest = (QuestInfo)_quests[questToUpdate]; - quest.AddPreviousQuest(new QuestInfo.PreviousQuestInfo(requiredQuestId)); + quest.AddPreviousQuest(new PreviousQuestInfo(requiredQuestId)); } private void AddGcFollowUpQuests() @@ -181,7 +181,7 @@ internal sealed class QuestData foreach (QuestId questId in questIds) { QuestInfo quest = (QuestInfo)_quests[questId]; - quest.AddQuestLocks(QuestInfo.QuestJoin.AtLeastOne, questIds.Where(x => x != questId).ToArray()); + quest.AddQuestLocks(EQuestJoin.AtLeastOne, questIds.Where(x => x != questId).ToArray()); } } diff --git a/Questionable/Functions/QuestFunctions.cs b/Questionable/Functions/QuestFunctions.cs index 46f52f8f..b0a92395 100644 --- a/Questionable/Functions/QuestFunctions.cs +++ b/Questionable/Functions/QuestFunctions.cs @@ -435,13 +435,13 @@ internal sealed unsafe class QuestFunctions return IsQuestLocked(questId, extraCompletedQuest); else if (elementId is LeveId leveId) return IsQuestLocked(leveId); - else if (elementId is SatisfactionSupplyNpcId) - return false; + else if (elementId is SatisfactionSupplyNpcId satisfactionSupplyNpcId) + return IsQuestLocked(satisfactionSupplyNpcId); else throw new ArgumentOutOfRangeException(nameof(elementId)); } - public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null) + private bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null) { if (IsQuestUnobtainable(questId, extraCompletedQuest)) return true; @@ -453,7 +453,7 @@ internal sealed unsafe class QuestFunctions return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo); } - public bool IsQuestLocked(LeveId leveId) + private bool IsQuestLocked(LeveId leveId) { if (IsQuestUnobtainable(leveId)) return true; @@ -467,6 +467,12 @@ internal sealed unsafe class QuestFunctions return !IsQuestAccepted(leveId) && QuestManager.Instance()->NumLeveAllowances == 0; } + private bool IsQuestLocked(SatisfactionSupplyNpcId satisfactionSupplyNpcId) + { + SatisfactionSupplyInfo questInfo = (SatisfactionSupplyInfo)_questData.GetQuestInfo(satisfactionSupplyNpcId); + return !HasCompletedPreviousQuests(questInfo, null); + } + public bool IsQuestUnobtainable(ElementId elementId, ElementId? extraCompletedQuest = null) { if (elementId is QuestId questId) @@ -486,9 +492,9 @@ internal sealed unsafe class QuestFunctions if (questInfo.QuestLocks.Count > 0) { var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest)); - if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests) + if (questInfo.QuestLockJoin == EQuestJoin.All && questInfo.QuestLocks.Count == completedQuests) return true; - else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0) + else if (questInfo.QuestLockJoin == EQuestJoin.AtLeastOne && completedQuests > 0) return true; } @@ -543,23 +549,23 @@ internal sealed unsafe class QuestFunctions return false; } - private bool HasCompletedPreviousQuests(QuestInfo questInfo, ElementId? extraCompletedQuest) + private bool HasCompletedPreviousQuests(IQuestInfo questInfo, ElementId? extraCompletedQuest) { if (questInfo.PreviousQuests.Count == 0) return true; var completedQuests = questInfo.PreviousQuests.Count(x => HasEnoughProgressOnPreviousQuest(x) || x.QuestId.Equals(extraCompletedQuest)); - if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All && + if (questInfo.PreviousQuestJoin == EQuestJoin.All && questInfo.PreviousQuests.Count == completedQuests) return true; - else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0) + else if (questInfo.PreviousQuestJoin == EQuestJoin.AtLeastOne && completedQuests > 0) return true; else return false; } - private bool HasEnoughProgressOnPreviousQuest(QuestInfo.PreviousQuestInfo previousQuestInfo) + private bool HasEnoughProgressOnPreviousQuest(PreviousQuestInfo previousQuestInfo) { if (IsQuestComplete(previousQuestInfo.QuestId)) return true; @@ -579,10 +585,10 @@ internal sealed unsafe class QuestFunctions return true; var completedInstances = questInfo.PreviousInstanceContent.Count(x => UIState.IsInstanceContentCompleted(x)); - if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.All && + if (questInfo.PreviousInstanceContentJoin == EQuestJoin.All && questInfo.PreviousInstanceContent.Count == completedInstances) return true; - else if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.AtLeastOne && completedInstances > 0) + else if (questInfo.PreviousInstanceContentJoin == EQuestJoin.AtLeastOne && completedInstances > 0) return true; else return false; diff --git a/Questionable/Model/EQuestJoin.cs b/Questionable/Model/EQuestJoin.cs new file mode 100644 index 00000000..61abf423 --- /dev/null +++ b/Questionable/Model/EQuestJoin.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Questionable.Model; + +[SuppressMessage("Design", "CA1028", Justification = "Game type")] +[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] +internal enum EQuestJoin : byte +{ + None = 0, + All = 1, + AtLeastOne = 2, +} diff --git a/Questionable/Model/IQuestInfo.cs b/Questionable/Model/IQuestInfo.cs index 5bc8d5f9..d2c0e386 100644 --- a/Questionable/Model/IQuestInfo.cs +++ b/Questionable/Model/IQuestInfo.cs @@ -1,17 +1,20 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using Dalamud.Game.Text; using LLib.GameData; using Questionable.Model.Questing; namespace Questionable.Model; -public interface IQuestInfo +internal interface IQuestInfo { public ElementId QuestId { get; } public string Name { get; } public uint IssuerDataId { get; } public bool IsRepeatable { get; } + public ImmutableList PreviousQuests { get; } + public EQuestJoin PreviousQuestJoin { get; } public ushort Level { get; } public EAlliedSociety AlliedSociety { get; } public uint? JournalGenre { get; } diff --git a/Questionable/Model/LeveInfo.cs b/Questionable/Model/LeveInfo.cs index d50ac82e..14878f71 100644 --- a/Questionable/Model/LeveInfo.cs +++ b/Questionable/Model/LeveInfo.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using LLib.GameData; using Lumina.Excel.GeneratedSheets2; using Questionable.Model.Questing; @@ -22,6 +23,8 @@ internal sealed class LeveInfo : IQuestInfo public ElementId QuestId { get; } public string Name { get; } public uint IssuerDataId { get; } + public ImmutableList PreviousQuests { get; } = []; + public EQuestJoin PreviousQuestJoin => EQuestJoin.All; public bool IsRepeatable => true; public ushort Level { get; } public EAlliedSociety AlliedSociety => EAlliedSociety.None; diff --git a/Questionable/Model/PreviousQuestInfo.cs b/Questionable/Model/PreviousQuestInfo.cs new file mode 100644 index 00000000..302a32e8 --- /dev/null +++ b/Questionable/Model/PreviousQuestInfo.cs @@ -0,0 +1,5 @@ +using Questionable.Model.Questing; + +namespace Questionable.Model; + +internal sealed record PreviousQuestInfo(QuestId QuestId, byte Sequence = 0); diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index 05603009..6149209a 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -47,18 +47,18 @@ internal sealed class QuestInfo : IQuestInfo } .Where(x => x.QuestId.Value != 0) .ToImmutableList(); - PreviousQuestJoin = (QuestJoin)quest.PreviousQuestJoin; + PreviousQuestJoin = (EQuestJoin)quest.PreviousQuestJoin; QuestLocks = quest.QuestLock .Select(x => new QuestId((ushort)(x.Row & 0xFFFFF))) .Where(x => x.Value != 0) .ToImmutableList(); - QuestLockJoin = (QuestJoin)quest.QuestLockJoin; + QuestLockJoin = (EQuestJoin)quest.QuestLockJoin; JournalGenre = quest.JournalGenre?.Row; SortKey = quest.SortKey; IsMainScenarioQuest = quest.JournalGenre?.Value?.JournalCategory?.Value?.JournalSection?.Row is 0 or 1; CompletesInstantly = quest.TodoParams[0].ToDoCompleteSeq == 0; PreviousInstanceContent = quest.InstanceContent.Select(x => (ushort)x.Row).Where(x => x != 0).ToList(); - PreviousInstanceContentJoin = (QuestJoin)quest.InstanceContentJoin; + PreviousInstanceContentJoin = (EQuestJoin)quest.InstanceContentJoin; GrandCompany = (GrandCompany)quest.GrandCompany.Row; AlliedSociety = (EAlliedSociety)quest.BeastTribe.Row; ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.Value!); @@ -75,11 +75,11 @@ internal sealed class QuestInfo : IQuestInfo public uint IssuerDataId { get; } public bool IsRepeatable { get; } public ImmutableList PreviousQuests { get; private set; } - public QuestJoin PreviousQuestJoin { get; } + public EQuestJoin PreviousQuestJoin { get; } public ImmutableList QuestLocks { get; private set; } - public QuestJoin QuestLockJoin { get; private set; } + public EQuestJoin QuestLockJoin { get; private set; } public List PreviousInstanceContent { get; } - public QuestJoin PreviousInstanceContentJoin { get; } + public EQuestJoin PreviousInstanceContentJoin { get; } public uint? JournalGenre { get; } public ushort SortKey { get; } public bool IsMainScenarioQuest { get; } @@ -92,20 +92,12 @@ internal sealed class QuestInfo : IQuestInfo public byte StartingCity { get; set; } public EExpansionVersion Expansion { get; } - [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] - public enum QuestJoin : byte - { - None = 0, - All = 1, - AtLeastOne = 2, - } - public void AddPreviousQuest(PreviousQuestInfo questId) { PreviousQuests = [..PreviousQuests, questId]; } - public void AddQuestLocks(QuestJoin questJoin, params QuestId[] questId) + public void AddQuestLocks(EQuestJoin questJoin, params QuestId[] questId) { if (QuestLocks.Count > 0 && QuestLockJoin != questJoin) throw new InvalidOperationException(); @@ -113,6 +105,4 @@ internal sealed class QuestInfo : IQuestInfo QuestLockJoin = questJoin; QuestLocks = [..QuestLocks, ..questId]; } - - public sealed record PreviousQuestInfo(QuestId QuestId, byte Sequence = 0); } diff --git a/Questionable/Model/SatisfactionSupplyInfo.cs b/Questionable/Model/SatisfactionSupplyInfo.cs index 9b3a7a6d..41f6d5e2 100644 --- a/Questionable/Model/SatisfactionSupplyInfo.cs +++ b/Questionable/Model/SatisfactionSupplyInfo.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using LLib.GameData; using Lumina.Excel.GeneratedSheets; using Questionable.Model.Questing; @@ -15,12 +16,15 @@ internal sealed class SatisfactionSupplyInfo : IQuestInfo Level = npc.LevelUnlock; SortKey = QuestId.Value; Expansion = (EExpansionVersion)npc.QuestRequired.Value!.Expansion.Row; + PreviousQuests = [new PreviousQuestInfo(new QuestId((ushort)(npc.QuestRequired.Row & 0xFFFF)))]; } public ElementId QuestId { get; } public string Name { get; } public uint IssuerDataId { get; } public bool IsRepeatable => true; + public ImmutableList PreviousQuests { get; } + public EQuestJoin PreviousQuestJoin => EQuestJoin.All; public ushort Level { get; } public EAlliedSociety AlliedSociety => EAlliedSociety.None; public uint? JournalGenre => null; diff --git a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs index 4fac3f95..702541b6 100644 --- a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs +++ b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs @@ -8,7 +8,6 @@ using Questionable.Controller; using Questionable.Data; using Questionable.Functions; using Questionable.Model; -using Questionable.Model.Questing; namespace Questionable.Windows.QuestComponents; @@ -37,40 +36,34 @@ internal sealed class QuestTooltipComponent _configuration = configuration; } - public void Draw(IQuestInfo quest) - { - if (quest is QuestInfo questInfo) - Draw(questInfo); - } - - public void Draw(QuestInfo quest) + public void Draw(IQuestInfo questInfo) { using var tooltip = ImRaii.Tooltip(); if (tooltip) { - ImGui.Text($"{SeIconChar.LevelEn.ToIconString()}{quest.Level}"); + ImGui.Text($"{SeIconChar.LevelEn.ToIconString()}{questInfo.Level}"); ImGui.SameLine(); - var (color, _, tooltipText) = _uiUtils.GetQuestStyle(quest.QuestId); + var (color, _, tooltipText) = _uiUtils.GetQuestStyle(questInfo.QuestId); ImGui.TextColored(color, tooltipText); - if (quest.IsRepeatable) + if (questInfo.IsRepeatable) { ImGui.SameLine(); ImGui.TextUnformatted("Repeatable"); } - if (quest.CompletesInstantly) + if (questInfo is QuestInfo { CompletesInstantly: true }) { ImGui.SameLine(); ImGui.TextUnformatted("Instant"); } - if (_questRegistry.TryGetQuest(quest.QuestId, out Quest? knownQuest)) + if (_questRegistry.TryGetQuest(questInfo.QuestId, out Quest? quest)) { - if (knownQuest.Root.Author.Count == 1) - ImGui.Text($"Author: {knownQuest.Root.Author[0]}"); + if (quest.Root.Author.Count == 1) + ImGui.Text($"Author: {quest.Root.Author[0]}"); else - ImGui.Text($"Authors: {string.Join(", ", knownQuest.Root.Author)}"); + ImGui.Text($"Authors: {string.Join(", ", quest.Root.Author)}"); } else { @@ -78,35 +71,35 @@ internal sealed class QuestTooltipComponent ImGui.TextUnformatted("NoQuestPath"); } - DrawQuestUnlocks(quest, 0); + DrawQuestUnlocks(questInfo, 0); } } - private void DrawQuestUnlocks(QuestInfo quest, int counter) + private void DrawQuestUnlocks(IQuestInfo questInfo, int counter) { if (counter >= 10) return; - if (counter != 0 && quest.IsMainScenarioQuest) + if (counter != 0 && questInfo.IsMainScenarioQuest) return; if (counter > 0) ImGui.Indent(); - if (quest.PreviousQuests.Count > 0) + if (questInfo.PreviousQuests.Count > 0) { if (counter == 0) ImGui.Separator(); - if (quest.PreviousQuests.Count > 1) + if (questInfo.PreviousQuests.Count > 1) { - if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.All) + if (questInfo.PreviousQuestJoin == EQuestJoin.All) ImGui.Text("Requires all:"); - else if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne) + else if (questInfo.PreviousQuestJoin == EQuestJoin.AtLeastOne) ImGui.Text("Requires one:"); } - foreach (var q in quest.PreviousQuests) + foreach (var q in questInfo.PreviousQuests) { if (_questData.TryGetQuestInfo(q.QuestId, out var qInfo)) { @@ -114,7 +107,9 @@ internal sealed class QuestTooltipComponent if (!_questRegistry.IsKnownQuest(qInfo.QuestId)) iconColor = ImGuiColors.DalamudGrey; - _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo, _questFunctions.IsQuestComplete(q.QuestId) ? byte.MinValue : q.Sequence), iconColor, icon); + _uiUtils.ChecklistItem( + FormatQuestUnlockName(qInfo, + _questFunctions.IsQuestComplete(q.QuestId) ? byte.MinValue : q.Sequence), iconColor, icon); if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check)) DrawQuestUnlocks(qstInfo, counter + 1); @@ -127,64 +122,67 @@ internal sealed class QuestTooltipComponent } } - if (counter == 0 && quest.QuestLocks.Count > 0) + if (questInfo is QuestInfo actualQuestInfo) { - ImGui.Separator(); - if (quest.QuestLocks.Count > 1) + if (counter == 0 && actualQuestInfo.QuestLocks.Count > 0) { - if (quest.QuestLockJoin == QuestInfo.QuestJoin.All) - ImGui.Text("Blocked by (if all completed):"); - else if (quest.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne) - ImGui.Text("Blocked by (if at least completed):"); + ImGui.Separator(); + if (actualQuestInfo.QuestLocks.Count > 1) + { + if (actualQuestInfo.QuestLockJoin == EQuestJoin.All) + ImGui.Text("Blocked by (if all completed):"); + else if (actualQuestInfo.QuestLockJoin == EQuestJoin.AtLeastOne) + ImGui.Text("Blocked by (if at least completed):"); + } + else + ImGui.Text("Blocked by (if completed):"); + + foreach (var q in actualQuestInfo.QuestLocks) + { + var qInfo = _questData.GetQuestInfo(q); + var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q); + if (!_questRegistry.IsKnownQuest(qInfo.QuestId)) + iconColor = ImGuiColors.DalamudGrey; + + _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon); + } } - else - ImGui.Text("Blocked by (if completed):"); - foreach (var q in quest.QuestLocks) + if (counter == 0 && actualQuestInfo.PreviousInstanceContent.Count > 0) { - var qInfo = _questData.GetQuestInfo(q); - var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q); - if (!_questRegistry.IsKnownQuest(qInfo.QuestId)) - iconColor = ImGuiColors.DalamudGrey; + ImGui.Separator(); + if (actualQuestInfo.PreviousInstanceContent.Count > 1) + { + if (questInfo.PreviousQuestJoin == EQuestJoin.All) + ImGui.Text("Requires all:"); + else if (questInfo.PreviousQuestJoin == EQuestJoin.AtLeastOne) + ImGui.Text("Requires one:"); + } + else + ImGui.Text("Requires:"); - _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon); + foreach (var instanceId in actualQuestInfo.PreviousInstanceContent) + { + string instanceName = _territoryData.GetInstanceName(instanceId) ?? "?"; + var (iconColor, icon) = UiUtils.GetInstanceStyle(instanceId); + _uiUtils.ChecklistItem(instanceName, iconColor, icon); + } } - } - if (counter == 0 && quest.PreviousInstanceContent.Count > 0) - { - ImGui.Separator(); - if (quest.PreviousInstanceContent.Count > 1) + if (counter == 0 && actualQuestInfo.GrandCompany != GrandCompany.None) { - if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.All) - ImGui.Text("Requires all:"); - else if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne) - ImGui.Text("Requires one:"); + ImGui.Separator(); + string gcName = actualQuestInfo.GrandCompany switch + { + GrandCompany.Maelstrom => "Maelstrom", + GrandCompany.TwinAdder => "Twin Adder", + GrandCompany.ImmortalFlames => "Immortal Flames", + _ => "None", + }; + + GrandCompany currentGrandCompany = ~_questFunctions.GetGrandCompany(); + _uiUtils.ChecklistItem($"Grand Company: {gcName}", actualQuestInfo.GrandCompany == currentGrandCompany); } - else - ImGui.Text("Requires:"); - - foreach (var instanceId in quest.PreviousInstanceContent) - { - string instanceName = _territoryData.GetInstanceName(instanceId) ?? "?"; - var (iconColor, icon) = UiUtils.GetInstanceStyle(instanceId); - _uiUtils.ChecklistItem(instanceName, iconColor, icon); - } - } - - if (counter == 0 && quest.GrandCompany != GrandCompany.None) - { - ImGui.Separator(); - string gcName = quest.GrandCompany switch - { - GrandCompany.Maelstrom => "Maelstrom", - GrandCompany.TwinAdder => "Twin Adder", - GrandCompany.ImmortalFlames => "Immortal Flames", - _ => "None", - }; - - GrandCompany currentGrandCompany = ~_questFunctions.GetGrandCompany(); - _uiUtils.ChecklistItem($"Grand Company: {gcName}", quest.GrandCompany == currentGrandCompany); } if (counter > 0)