From 32f60fa772f288c9e03bc1633dd0ad92cf585315 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 8 Jan 2023 00:38:50 +0100 Subject: [PATCH] Add quest filter for unobtainable quests (different starting city, different gc, removed from the game) --- QuestMap/Configuration.cs | 1 + QuestMap/Filters.cs | 362 ++++++++++++++++++++++++++++++++++++++ QuestMap/Plugin.cs | 3 +- QuestMap/PluginUi.cs | 12 +- QuestMap/QuestMap.yaml | 6 +- 5 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 QuestMap/Filters.cs diff --git a/QuestMap/Configuration.cs b/QuestMap/Configuration.cs index 7516330..28ab57e 100644 --- a/QuestMap/Configuration.cs +++ b/QuestMap/Configuration.cs @@ -7,6 +7,7 @@ namespace QuestMap { public int Version { get; set; } = 1; public bool ShowCompleted; + public bool ShowUnobtainable; public bool ShowSeasonal; public bool ShowArrowheads; public bool CondenseMsq; diff --git a/QuestMap/Filters.cs b/QuestMap/Filters.cs new file mode 100644 index 0000000..3f3e72e --- /dev/null +++ b/QuestMap/Filters.cs @@ -0,0 +1,362 @@ +using Lumina.Excel.GeneratedSheets; +using System.Collections.Generic; +using System.Linq; + +namespace QuestMap +{ + internal class Filters + { + private Plugin Plugin; + + private List FilterConditions { get; } = new List { + // starting city questlines + new OneQuestOf(65_575, 65_643, 66_130), // Coming to [starting city] + new OneQuestOf(65_621, 65_644, 65_645, 65_659, 65_660, 66_104, 66_105, 66_106), // Close to Home (one per starting class) + #region Gridania + new ConditionalQuests( + 65_575, // Coming to Gridania + + // MSQ + 65_564, // To the Bannock + 65_737, // Passing Muster + 65_981, // Chasing Shadows + 65_664, // Eggs over Queasy (old) + 69_390, // Eggs over Queasy + 65_711, // Surveying the Damage + 65_734, // Butcher of Greentear (removed) + 65_661, // A Soldier's Breakfast (old) + 69_391, // A Soldier's Breakfast + 65_665, // Spirithold Broken + 65_712, // On the Bentbranch + 65_912, // You Shall Not Trespass + 65_913, // Don't Look Down + 65_915, // In the Grim Darkness of the Forest + 65_916, // Threat Level Elevated + 65_917, // Migrant Marauders + 65_920, // A Hearer Is Often Late + 65_923, // Salvaging the Scene + 65_697, // Leia's Legacy + 65_982, // Dread is in the Air + 65_983, // To Guard a Guardian + 65_984, // Festive Endeavors + 65_985, // Renewing the Covenant + 66_043, // The Gridanian Envoy + 66_210, // Call of the Sea, used in both Gridania & Limsa + + // side quests + 65_756, // Leves of Bentbranch + 65_596, // Simply the Hest + 65_537, // A Good Adventurer is Hard to Find + 65_573, // Population Control + 65_568, // A Hard Nut to Crack + 65_570, // Derision of Labor + 65_732, // An Eft for Effort + 65_708, // More than a Flesh Wound + 65_663, // Parasite Cleave + 65_666, // Soil Despoilers + 65_914 // Look, but Won't Touch + + // This may not be a complete list, my main char is from Gridania so this doesn't matter much to me + ), + new ReplacedQuest(65_664, 69_390), // Eggs over Queasy + new ReplacedQuest(65_661, 69_391), // A Soldier's Breakfast + new UnobtainableQuest(66_964), // He's Got a Ticket to Ride + new UnobtainableQuest(65_734), // Butcher of Greentear + new UnobtainableQuest(65_910), // Feeding Time + new UnobtainableQuest(65_692), // Ruffled Feathers + new UnobtainableQuest(65_918), // Skeletons in My Deepcroft + new UnobtainableQuest(65_695), // Lights Out + #endregion Gridania + + #region Limsa + new ConditionalQuests( + 65_643, // Coming to Limsa Lominsa + + // MSQ + 65_998, // On to Summerford + 65_999, // Dressed to Call + 66_079, // Lurkers in the Grotto + 66_001, // Washed Up + 66_002, // Double Dealing + 66_003, // Loam Maintenance + 66_004, // Plowshares to Swords + 66_005, // Just Deserts + 65_933, // Sky-high + 65_938, // Thanks a Million + 65_939, // Relighting the Torch + 65_942, // On the Drydocks + 65_948, // Without a Doubt + 65_951, // Righting the Shipwright + 65_949, // Do Angry Pirates Dream + 65_950, // Victory in Peril + 66_225, // Men of the Blue Tattoos + 66_080, // Feint and Strike + 66_226, // High Society + 66_081, // A Mizzenmast Repast + 66_082, // The Lominsan Envoy + 66_210, // Call of the Sea, used in both Gridania & Limsa + + // side quests + 66_229, // Leves of Swiftperch + 65_595, // Simply the Hest + 65_647, // Making a Name + 65_648, // Suspiciously Sober + 65_658, // Glory Days + 66_199, // Fool Me Once + 66_008, // The Time Is Ripe + 66_009, // Orange Crush + 66_000, // Further Afield + 66_006, // Yarzon, Yarzoff + 66_001, // Washed Up, + 66_002, // Double Dealing + 66_010, // A Pound of Cure + 66_023, // Meet, Greet, and Deceit + 65_936, // Keeping it Fresh + 65_937, // Baiting Buzzards + 65_940, // Farmer of Fortune + 65_941, // Need to Breed + 66_011, // Cry Me a Liver + 66_012, // Wool's Errand + 66_013, // Flour Power + 66_014, // Kobolds and Ingredients + 66_015, // Bait and Swat + 66_022 // Kobolds Ahoy + + ), + new UnobtainableQuest(65_934), // Courier for a Day + #endregion Limsa + + #region Ul'dah + new ConditionalQuests( + 66_130, // Coming to Ul'dah + + // MSQ + 66_131, // We Must Rebuild + 66_207, // Nothing to See Here + 66_086, // Underneath the Sultantree + 65_839, // Step Nine + 65_842, // Prudence at this Junction (old) + 69_388, // Prudence at this Junction + 65_843, // Out of House and Home + 65_856, // Way Down in the Hole + 66_159, // Takin' What They're Givin' + 65_864, // Supply & Demand + 66_039, // Give It to Me Raw + 65_865, // The Perfect Swarm + 65_866, // The Letter to Lost Hope + 65_867, // Heir Today, Gone Tomorrow (old) + 69_389, // Heir Today, Gone Tomorrow + 65_868, // Passing the Blade + 65_869, // Following Footfalls + 65_870, // Storms on the Horizon + 65_872, // Oh Captain, My Captain + 66_164, // Secrets and Lies + 66_087, // Duty, Honor, Country + 66_177, // A Matter of Tradition + 66_088, // A Royal Reception + 66_064, // The Ul'dahn Envoy + 66_209, // Call of the Sea, used only in Ul'dah + + // side quests + 66_223, // Leves of Horizon + 65_594, // Simply the Hest + 65_857, // The Slaying of the Shrews + 65_858, // Listen to Your Fish + 65_840, // The Tasting of the Shrews + 65_862, // Sometimes the Old Ways Are Best + 65_844, // In the Court of the Rat King + 65_877, // The Scent of a Mongrel + 65_878, // Lominsan Rubbish + 65_924, // Bloodied and Bowed + 65_925, // Fantastic Voyage + 65_926, // Catch Your Breath + 66_040, // No Tool Gets Left Behind + 66_041, // Angling Ankle-Biters + 66_042, // This Fishwife's Life + 66_066, // Alternative Medicine + 66_067, // Need Not Apply in Person + 66_109 // Fringe Benefits + ), + new UnobtainableQuest(65_860), // Disorderly Conduct + new UnobtainableQuest(65_841), // Until a Quieter Time + new UnobtainableQuest(65_863), // Spriggan Cleaning + new UnobtainableQuest(65_871), // Compulsory Catering + new UnobtainableQuest(66_965), // Lend Me Your Ears Already + new ReplacedQuest(65_842, 69_388), // Prudence at this Junction + new ReplacedQuest(65_867, 69_389), // Heir Today, Gone Tomorrow + #endregion Ul'dah + + // Grand Company specific quests - can only complete them for one GC, even if you switch later + // Shadows Uncast & Gilding the Bilbious can be done once per GC, so they're not exclusive + new OneQuestOf(66_216, 66_217, 66_218), // The Company You Keep + new OneQuestOf(66_219, 66_220, 66_221), // The Company You Keep follow-up quest + new OneQuestOf(66_236, 66_237, 66_238), // My Little Chocobo + new OneQuestOf(66_640, 66_641, 66_642), // A Pup No Longer + new OneQuestOf(67_001, 67_002, 67_003), // Call of the Wild + new OneQuestOf(67_063, 67_064, 67_065), // Like Civilized Men and Women + new OneQuestOf(67_099, 67_100, 67_101), // Let the Hunt Begin + new OneQuestOf(67_925, 67_926, 67_927), // Squadron and Commander + + // class quests: depending on whether or not it's your starting class, you get a different quest + // Format: ([quest id if this was your starting class], [so you want to be a lancer], [way of the lancer, if picked up later], [your first spear]) + new StartingJobQuest(65_557, 65_717, 65_667, 65_755), // ARC + new StartingJobQuest(65_558, 65_718, 65_669, 65_747), // CNJ // seems odd that the replaced if for CNJ & LNC aren't the same order as the original ids + new StartingJobQuest(65_559, 65_716, 65_668, 65_754), // LNC + new StartingJobQuest(65_789, 65_713, 65_821, 65_822), // GLA + new StartingJobQuest(65_847, 65_715, 65_846, 65_848), // MRD + new StartingJobQuest(65_881, 65_719, 65_880, 65_882), // THM + new StartingJobQuest(65_989, 65_987, 65_988, 65_990), // ACN + new StartingJobQuest(66_069, 65_714, 66_068, 66_089), // PGL + + // duplicate quests - unsure what happened to those, as the quest ids are very close; might be dependent on your starting job/city but isn't a thing for all classes + new OneQuestOf(65_571, 65_679), // LNC: Courage of Stone + new OneQuestOf(65_850, 65_851), // MRD: Wake of Destruction + new OneQuestOf(65_884, 65_885), // THM: The Threat of Paucity + new OneQuestOf(65_992, 65_993), // ACN: Tactical Planning + new OneQuestOf(65_797, 65_824), // GLA: Ul'dah's Most Wanted, large id difference + new OneQuestOf(66_091, 66_234), // PGL: Burning Up the Quarter Malm + + // quest choices + new OneQuestOf(66_968, 66_969, 66_970), // An Ill-conceived Venture + new OneQuestOf(69_256, 69_257), // NieR: Tails, You Lose -- Heads, I Win + new OneQuestOf(69_336, 69_337), // Qitari: First Stela + new OneQuestOf(69_338, 69_339), // Qitari: Second Stela + new OneQuestOf(69_340, 69_341), // Qitari: Third Stela + new IslandSanctuaryQuest(), + + // Ishgard Restoration, I think this was something to do with interacting with the board when the relvant stage was finished + // These aren't in the journal, and they don't have any wiki pages either. As of 6.28, none of them seems obtainable. + new UnobtainableQuest(69_296), // The Mendicant's Court + new UnobtainableQuest(69_377), // The New Nest + new UnobtainableQuest(69_508), // Featherfall + new UnobtainableQuest(69_578), // The Risensong Quarter + + // updated quests + new ReplacedQuest(65_603, 65_670), // BRD: School of Hard Nooks -> Training with Leih + new ReplacedQuest(65_627, 65_683), // WHM: Trial by Wind + new ReplacedQuest(66_957, 68_553), // Glamour unlock + new ReplacedQuest(66_958, 68_554), // Glamour prism crafting + new ReplacedQuest(66_097, 67_823), // ARR Relics: The Vital Title + new UnobtainableQuest(67_819), // A Seat at the Feast + }; + + public Filters(Plugin plugin) + { + Plugin = plugin; + } + + public bool IsObtainable(Quest quest) + { + if (Plugin.Common.Functions.Journal.IsQuestCompleted(quest)) + return true; + + if (FilterConditions.Any(f => f.IsUnobtainable(Plugin, quest.RowId))) + return false; + + return true; + } + + internal interface IFiltered + { + bool IsUnobtainable(Plugin plugin, uint quest); + } + + internal class OneQuestOf : IFiltered + { + public List QuestIds { get; } + + public OneQuestOf(params uint[] questIds) + { + QuestIds = questIds.ToList(); + } + + public bool IsUnobtainable(Plugin plugin, uint quest) + { + return QuestIds.Contains(quest) && QuestIds.Any(plugin.Common.Functions.Journal.IsQuestCompleted); + } + } + + internal class ReplacedQuest : IFiltered + { + public uint OriginalQuestId { get; } + public uint NewQuestId { get; } + + public ReplacedQuest(uint originalQuestId, uint newQuestId) + { + OriginalQuestId = originalQuestId; + NewQuestId = newQuestId; + } + + public bool IsUnobtainable(Plugin plugin, uint quest) + { + return quest == OriginalQuestId || (quest == NewQuestId && plugin.Common.Functions.Journal.IsQuestCompleted(OriginalQuestId)); + } + } + + internal class ConditionalQuests : IFiltered + { + public uint RequiredQuest { get; } + public List FollowUpQuests { get; } + + public ConditionalQuests(uint requiredQuest, params uint[] followUpQuestIds) + { + RequiredQuest = requiredQuest; + FollowUpQuests = followUpQuestIds.ToList(); + } + + public bool IsUnobtainable(Plugin plugin, uint quest) + { + if (plugin.Common.Functions.Journal.IsQuestCompleted(RequiredQuest)) + return false; + + return FollowUpQuests.Contains(quest); + } + } + + internal class StartingJobQuest : IFiltered + { + public uint StartingClassQuestId; + public List LaterClassQuestIds; + + public StartingJobQuest(uint wayOfTheMsqQuestId, uint soYouWantToBeAQuestId, uint wayOfTheSideQuestId, uint myFirstWeaponQuestId) + { + StartingClassQuestId = wayOfTheMsqQuestId; + LaterClassQuestIds = new List { soYouWantToBeAQuestId, wayOfTheSideQuestId, myFirstWeaponQuestId }; + } + + public bool IsUnobtainable(Plugin plugin, uint quest) + { + if (plugin.Common.Functions.Journal.IsQuestCompleted(StartingClassQuestId)) + return LaterClassQuestIds.Contains(quest); + else + return quest == StartingClassQuestId; + } + } + + internal class UnobtainableQuest : IFiltered + { + public uint QuestId { get; } + + public UnobtainableQuest(uint questId) + { + this.QuestId = questId; + } + + public bool IsUnobtainable(Plugin plugin, uint quest) + { + return quest == QuestId; + } + } + + internal class IslandSanctuaryQuest : IFiltered + { + // Seeing the Cieldalaes doesn't seem to be available after unlocking the Island Sanctuary + // - Seeking the Cieldalaes is unlockable after the Praetorium + // - Seeking Sanctuary is unlockable after 6.0 + public bool IsUnobtainable(Plugin plugin, uint quest) + { + return quest == 70_180 && plugin.Common.Functions.Journal.IsQuestCompleted(70_179); + } + } + } +} diff --git a/QuestMap/Plugin.cs b/QuestMap/Plugin.cs index 7f6de49..ee5fd87 100644 --- a/QuestMap/Plugin.cs +++ b/QuestMap/Plugin.cs @@ -33,7 +33,8 @@ namespace QuestMap { internal PluginUi Ui { get; } private Commands Commands { get; } - public Plugin() { + public Plugin(DalamudPluginInterface pi) { + pi.Inject(this); // not sure why [PluginInterface] above aren't initialized by dalamud, but they're null this.Common = new XivCommonBase(); this.Config = this.Interface.GetPluginConfig() as Configuration ?? new Configuration(); diff --git a/QuestMap/PluginUi.cs b/QuestMap/PluginUi.cs index 7fa210e..82946fd 100644 --- a/QuestMap/PluginUi.cs +++ b/QuestMap/PluginUi.cs @@ -30,9 +30,11 @@ namespace QuestMap { internal static readonly Vector4 NormalQuest = new(0.54f, 0.45f, 0.36f, 1); internal static readonly Vector4 MsqQuest = new(0.29f, 0.35f, 0.44f, 1); internal static readonly Vector4 BlueQuest = new(0.024F, 0.016f, 0.72f, 1); + internal static readonly Vector4 UnobtainableQuest = new(0.54f, 0, 0, 0.5f); } private Plugin Plugin { get; } + private Filters Filters { get; } private string _filter = string.Empty; private Quest? Quest { get; set; } @@ -56,6 +58,7 @@ namespace QuestMap { internal PluginUi(Plugin plugin, ChannelReader graphChannel) { this.Plugin = plugin; + this.Filters = new Filters(plugin); this.GraphChannel = graphChannel; this.Refilter(); @@ -96,6 +99,9 @@ namespace QuestMap { return false; } + if (!this.Plugin.Config.ShowUnobtainable && !this.Filters.IsObtainable(quest)) + return false; + if (this.Plugin.Config.EmoteVis == Visibility.Only && !this.Plugin.Quests.EmoteRewards.ContainsKey(quest.RowId)) { return false; } @@ -209,6 +215,7 @@ namespace QuestMap { if (ImGui.BeginMenu("Quest list")) { anyChanged |= ImGui.MenuItem("Show completed quests", null, ref this.Plugin.Config.ShowCompleted); + anyChanged |= ImGui.MenuItem("Show unobtainable quests", null, ref this.Plugin.Config.ShowUnobtainable); anyChanged |= ImGui.MenuItem("Show seasonal quests", null, ref this.Plugin.Config.ShowSeasonal); ImGui.EndMenu(); @@ -289,7 +296,7 @@ namespace QuestMap { var (quest, indent, drawItem) = this.FilteredQuests[row]; void DrawSelectable(string name, Quest quest) { - var completed = this.Plugin.Common.Functions.Journal.IsQuestCompleted(quest); + var completed = this.Plugin.Common.Functions.Journal.IsQuestCompleted(quest) || !this.Filters.IsObtainable(quest); if (completed) { Vector4 disabled; unsafe { @@ -840,6 +847,9 @@ namespace QuestMap { if (completed) { colour.W = .5f; textColour = (uint) ((0x80 << 24) | (textColour & 0xFFFFFF)); + } else if (!this.Filters.IsObtainable(quest)) { + colour = Colours.UnobtainableQuest; + textColour = (uint)((0x80 << 24) | (textColour & 0xFFFFFF)); } var end = canvasBottomRight - this.GetBottomRight(node); diff --git a/QuestMap/QuestMap.yaml b/QuestMap/QuestMap.yaml index 6a4440d..bc53f81 100644 --- a/QuestMap/QuestMap.yaml +++ b/QuestMap/QuestMap.yaml @@ -1,5 +1,5 @@ -name: Quest Map -author: ascclemens +name: Filtered Quest Map +author: Liza Carvelli, ascclemens punchline: Explore quests and their rewards. description: |- Explore quests and their rewards. @@ -11,4 +11,4 @@ description: |- Icons: treasure map by Anthony Ledoux from the Noun Project and locked book by Anthony Ledoux from the Noun Project -repo_url: https://git.annaclemens.io/ascclemens/QuestMap +repo_url: https://github.com/carvelli/FilteredQuestMap