Add Patch 7.2 Fantasia as pseudo-event
authorLiza Carvelli <liza@carvel.li>
Sun, 6 Apr 2025 01:46:22 +0000 (03:46 +0200)
committerLiza Carvelli <liza@carvel.li>
Sun, 6 Apr 2025 02:32:45 +0000 (04:32 +0200)
QuestPaths/7.x - Dawntrail/Unlocks/Misc/U506_Patch 7 2 Fantasia.json [new file with mode: 0644]
Questionable.IpcTest/IpcTestPlugin.cs
Questionable.Model/Questing/Converter/SkipConditionConverter.cs
Questionable.Model/Questing/EExtraSkipCondition.cs
Questionable.Model/Questing/ElementId.cs
Questionable/Configuration.cs
Questionable/Controller/Steps/Shared/ExtraConditionUtils.cs
Questionable/Data/QuestData.cs
Questionable/Functions/QuestFunctions.cs
Questionable/Model/UnlockLinkQuestInfo.cs [new file with mode: 0644]
Questionable/Windows/QuestComponents/EventInfoComponent.cs

diff --git a/QuestPaths/7.x - Dawntrail/Unlocks/Misc/U506_Patch 7 2 Fantasia.json b/QuestPaths/7.x - Dawntrail/Unlocks/Misc/U506_Patch 7 2 Fantasia.json
new file mode 100644 (file)
index 0000000..b48a33f
--- /dev/null
@@ -0,0 +1,34 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+  "Author": "liza",
+  "QuestSequence": [
+    {
+      "Sequence": 0,
+      "Steps": [
+        {
+          "DataId": 1052475,
+          "Position": {
+            "X": -22.354492,
+            "Y": 10.13581,
+            "Z": -241.41296
+          },
+          "TerritoryId": 133,
+          "InteractionType": "AcceptQuest",
+          "AetheryteShortcut": "Gridania",
+          "AethernetShortcut": [
+            "[Gridania] Aetheryte Plaza",
+            "[Gridania] Mih Khetto's Amphitheatre"
+          ],
+          "SkipConditions": {
+            "AetheryteShortcutIf": {
+              "InSameTerritory": true,
+              "InTerritory": [
+                133
+              ]
+            }
+          }
+        }
+      ]
+    }
+  ]
+}
index 6121feb..2dcc932 100644 (file)
@@ -1,4 +1,5 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Numerics;
 using Dalamud.Game.Command;
@@ -43,6 +44,15 @@ public sealed class IpcTestPlugin : IDalamudPlugin
                 .AddUiForeground(stepData?.TerritoryId.ToString() ?? "?", 61)
                 .Build());
         }
+        else if (arguments == "events")
+        {
+            var eventQuests = _pluginInterface.GetIpcSubscriber<List<string>>("Questionable.GetCurrentlyActiveEventQuests").InvokeFunc();
+            _chatGui.Print(new SeStringBuilder()
+                .AddUiForeground("[IPC]", 576)
+                .AddText(": Quests: ")
+                .AddUiForeground(string.Join(", ", eventQuests), 61)
+                .Build());
+        }
         else
             _chatGui.PrintError("Unknown subcommand");
     }
index 8833655..b49a1cf 100644 (file)
@@ -13,6 +13,5 @@ public sealed class SkipConditionConverter() : EnumConverter<EExtraSkipCondition
         { EExtraSkipCondition.RoguesGuild, "RoguesGuild"},
         { EExtraSkipCondition.NotRoguesGuild, "NotRoguesGuild"},
         { EExtraSkipCondition.DockStorehouse, "DockStorehouse"},
-        { EExtraSkipCondition.SkipFreeFantasia, "SkipFreeFantasia" },
     };
 }
index 06c0c4e..47de211 100644 (file)
@@ -21,6 +21,4 @@ public enum EExtraSkipCondition
     /// Location for NIN quests in Eastern La Noscea; located far underneath the actual zone.
     /// </summary>
     DockStorehouse,
-
-    SkipFreeFantasia,
 }
index 51db228..f87a7a6 100644 (file)
@@ -54,6 +54,8 @@ public abstract class ElementId : IComparable<ElementId>, IEquatable<ElementId>
     {
         if (value.StartsWith("S"))
             return new SatisfactionSupplyNpcId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
+        else if (value.StartsWith("U"))
+            return new UnlockLinkId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
         else if (value.StartsWith("A"))
         {
             value = value.Substring(1);
@@ -85,6 +87,8 @@ public abstract class ElementId : IComparable<ElementId>, IEquatable<ElementId>
             throw;
         }
     }
+
+    public abstract override string ToString();
 }
 
 public sealed class QuestId(ushort value) : ElementId(value)
@@ -105,6 +109,14 @@ public sealed class SatisfactionSupplyNpcId(ushort value) : ElementId(value)
     }
 }
 
+public sealed class UnlockLinkId(ushort value) : ElementId(value)
+{
+    public override string ToString()
+    {
+        return "U" + Value.ToString(CultureInfo.InvariantCulture);
+    }
+}
+
 public sealed class AlliedSocietyDailyId(byte alliedSociety, byte rank = 0) : ElementId((ushort)(alliedSociety * 10 + rank))
 {
     public byte AlliedSociety { get; } = alliedSociety;
index 7bf14f6..952cf2b 100644 (file)
@@ -37,9 +37,6 @@ internal sealed class Configuration : IPluginConfiguration
         public bool UseEscToCancelQuesting { get; set; } = true;
         public bool ShowIncompleteSeasonalEvents { get; set; } = true;
         public bool ConfigureTextAdvance { get; set; } = true;
-
-        // TODO Temporary setting, 7.2 adds another fantasia
-        public bool PickUpFreeFantasia { get; set; } = true;
     }
 
     internal sealed class DutyConfiguration
index 0c72f2a..1572f4e 100644 (file)
@@ -32,7 +32,7 @@ internal sealed class ExtraConditionUtils
                MatchesExtraCondition(skipCondition, position.Value, _clientState.TerritoryType);
     }
 
-    public bool MatchesExtraCondition(EExtraSkipCondition skipCondition, Vector3 position, ushort territoryType)
+    public static bool MatchesExtraCondition(EExtraSkipCondition skipCondition, Vector3 position, ushort territoryType)
     {
         return skipCondition switch
         {
@@ -42,42 +42,7 @@ internal sealed class ExtraConditionUtils
             EExtraSkipCondition.RoguesGuild => territoryType == 129 && position.Y <= -115,
             EExtraSkipCondition.NotRoguesGuild => territoryType == 129 && position.Y > -115,
             EExtraSkipCondition.DockStorehouse => territoryType == 137 && position.Y <= -20,
-            EExtraSkipCondition.SkipFreeFantasia => ShouldSkipFreeFantasia(),
             _ => throw new ArgumentOutOfRangeException(nameof(skipCondition), skipCondition, null)
         };
     }
-
-    private unsafe bool ShouldSkipFreeFantasia()
-    {
-        if (!_configuration.General.PickUpFreeFantasia)
-        {
-            _logger.LogInformation("Skipping fantasia step, as free fantasia is disabled in the configuration");
-            return true;
-        }
-
-        bool foundFestival = false;
-        for (int i = 0; i < GameMain.Instance()->ActiveFestivals.Length; ++i)
-        {
-            if (GameMain.Instance()->ActiveFestivals[i].Id == 160)
-            {
-                foundFestival = true;
-                break;
-            }
-        }
-
-        if (!foundFestival)
-        {
-            _logger.LogInformation("Skipping fantasia step, as free fantasia moogle is not available");
-            return true;
-        }
-
-        UIState* uiState = UIState.Instance();
-        if (uiState != null && uiState->IsUnlockLinkUnlocked(505))
-        {
-            _logger.LogInformation("Already picked up free fantasia");
-            return true;
-        }
-
-        return false;
-    }
 }
index c724642..c8c41ff 100644 (file)
@@ -95,6 +95,8 @@ internal sealed class QuestData
                     }
                 }));
 
+        quests.Add(new UnlockLinkQuestInfo(new UnlockLinkId(506), "Patch 7.2 Fantasia", 1052475));
+
         _quests = quests.ToDictionary(x => x.QuestId, x => x);
 
         // workaround because the game doesn't require completion of the CT questline through normal means
index 78055d6..fcb107b 100644 (file)
@@ -533,6 +533,8 @@ internal sealed unsafe class QuestFunctions
             return false;
         else if (elementId is AlliedSocietyDailyId)
             return false;
+        else if (elementId is UnlockLinkId)
+            return false;
         else
             throw new ArgumentOutOfRangeException(nameof(elementId));
     }
@@ -551,6 +553,8 @@ internal sealed unsafe class QuestFunctions
             return false;
         else if (elementId is AlliedSocietyDailyId)
             return false;
+        else if (elementId is UnlockLinkId unlockLinkId)
+            return IsQuestComplete(unlockLinkId);
         else
             throw new ArgumentOutOfRangeException(nameof(elementId));
     }
@@ -561,6 +565,11 @@ internal sealed unsafe class QuestFunctions
         return QuestManager.IsQuestComplete(questId.Value);
     }
 
+    public bool IsQuestComplete(UnlockLinkId unlockLinkId)
+    {
+        return UIState.Instance()->IsUnlockLinkUnlocked(unlockLinkId.Value);
+    }
+
     public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
     {
         if (elementId is QuestId questId)
@@ -569,6 +578,8 @@ internal sealed unsafe class QuestFunctions
             return IsQuestLocked(satisfactionSupplyNpcId);
         else if (elementId is AlliedSocietyDailyId alliedSocietyDailyId)
             return IsQuestLocked(alliedSocietyDailyId);
+        else if (elementId is UnlockLinkId unlockLinkId)
+            return IsQuestLocked(unlockLinkId);
         else
             throw new ArgumentOutOfRangeException(nameof(elementId));
     }
@@ -613,6 +624,11 @@ internal sealed unsafe class QuestFunctions
         return currentRank == 0 || currentRank < alliedSocietyDailyId.Rank;
     }
 
+    private static bool IsQuestLocked(UnlockLinkId unlockLinkId)
+    {
+        return IsQuestUnobtainable(unlockLinkId);
+    }
+
     public bool IsDailyAlliedSocietyQuest(QuestId questId)
     {
         var questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
@@ -632,6 +648,8 @@ internal sealed unsafe class QuestFunctions
     {
         if (elementId is QuestId questId)
             return IsQuestUnobtainable(questId, extraCompletedQuest);
+        else if (elementId is UnlockLinkId unlockLinkId)
+            return IsQuestUnobtainable(unlockLinkId);
         else
             return false;
     }
@@ -696,6 +714,29 @@ internal sealed unsafe class QuestFunctions
         return false;
     }
 
+    /// <summary>
+    /// All unlock links (presumably) have unique conditions, be that quests or otherwise.
+    /// </summary>
+    private static bool IsQuestUnobtainable(UnlockLinkId unlockLinkId)
+    {
+        if (unlockLinkId.Value == 506)
+            return !IsFestivalActive(160, 2);
+        else
+            return true;
+    }
+
+    private static bool IsFestivalActive(ushort id, ushort? phase = null)
+    {
+        for (int i = 0; i < GameMain.Instance()->ActiveFestivals.Length; ++i)
+        {
+            var festival = GameMain.Instance()->ActiveFestivals[i];
+            if (festival.Id == id)
+                return phase == null || festival.Phase == phase;
+        }
+
+        return false;
+    }
+
     public bool IsQuestRemoved(ElementId elementId)
     {
         if (elementId is QuestId questId)
diff --git a/Questionable/Model/UnlockLinkQuestInfo.cs b/Questionable/Model/UnlockLinkQuestInfo.cs
new file mode 100644 (file)
index 0000000..6e08f79
--- /dev/null
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using LLib.GameData;
+using Questionable.Model.Questing;
+
+namespace Questionable.Model;
+
+internal sealed class UnlockLinkQuestInfo : IQuestInfo
+{
+    public UnlockLinkQuestInfo(UnlockLinkId unlockLinkId, string name, uint issuerDataId)
+    {
+        QuestId = unlockLinkId;
+        Name = name;
+        IssuerDataId = issuerDataId;
+    }
+
+    public ElementId QuestId { get; }
+    public string Name { get; }
+    public uint IssuerDataId { get; }
+    public bool IsRepeatable => false;
+    public ImmutableList<PreviousQuestInfo> PreviousQuests => [];
+    public EQuestJoin PreviousQuestJoin => EQuestJoin.All;
+    public ushort Level => 1;
+    public EAlliedSociety AlliedSociety => EAlliedSociety.None;
+    public uint? JournalGenre => null;
+    public ushort SortKey => 0;
+    public bool IsMainScenarioQuest => false;
+    public IReadOnlyList<EClassJob> ClassJobs => [];
+    public EExpansionVersion Expansion => EExpansionVersion.ARealmReborn;
+}
index c10658a..b86a795 100644 (file)
@@ -22,6 +22,7 @@ internal sealed class EventInfoComponent
     [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
     private readonly List<EventQuest> _eventQuests =
     [
+        new EventQuest("Limited Time Items", [new UnlockLinkId(506)], DateTime.MaxValue),
     ];
 
     private readonly QuestData _questData;
@@ -66,12 +67,17 @@ internal sealed class EventInfoComponent
 
     private void DrawEventQuest(EventQuest eventQuest)
     {
-        string time = (eventQuest.EndsAtUtc - DateTime.UtcNow).Humanize(
-            precision: 1,
-            culture: CultureInfo.InvariantCulture,
-            minUnit: TimeUnit.Minute,
-            maxUnit: TimeUnit.Day);
-        ImGui.Text($"{eventQuest.Name} ({time})");
+        if (eventQuest.EndsAtUtc != DateTime.MaxValue)
+        {
+            string time = (eventQuest.EndsAtUtc - DateTime.UtcNow).Humanize(
+                precision: 1,
+                culture: CultureInfo.InvariantCulture,
+                minUnit: TimeUnit.Minute,
+                maxUnit: TimeUnit.Day);
+            ImGui.Text($"{eventQuest.Name} ({time})");
+        }
+        else
+            ImGui.Text(eventQuest.Name);
 
         float width;
         using (var _ = _pluginInterface.UiBuilder.IconFontHandle.Push())
@@ -80,7 +86,7 @@ internal sealed class EventInfoComponent
         using (var _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
             width -= ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
 
-        List<QuestId> startableQuests = eventQuest.QuestIds.Where(x =>
+        List<ElementId> startableQuests = eventQuest.QuestIds.Where(x =>
                 _questRegistry.IsKnownQuest(x) &&
                 _questFunctions.IsReadyToAcceptQuest(x) &&
                 x != _questController.StartedQuest?.Quest.Id &&
@@ -132,15 +138,19 @@ internal sealed class EventInfoComponent
         if (eventQuest.EndsAtUtc <= DateTime.UtcNow)
             return false;
 
-        return !eventQuest.QuestIds.All(x => _questFunctions.IsQuestComplete(x));
+        return eventQuest.QuestIds.Any(ShouldShowQuest);
     }
 
-    public IEnumerable<QuestId> GetCurrentlyActiveEventQuests()
+    public IEnumerable<ElementId> GetCurrentlyActiveEventQuests()
     {
         return _eventQuests
             .Where(x => x.EndsAtUtc >= DateTime.UtcNow)
-            .SelectMany(x => x.QuestIds);
+            .SelectMany(x => x.QuestIds)
+            .Where(ShouldShowQuest);
     }
 
-    private sealed record EventQuest(string Name, List<QuestId> QuestIds, DateTime EndsAtUtc);
+    private bool ShouldShowQuest(ElementId elementId) => !_questFunctions.IsQuestComplete(elementId) &&
+                                                         !_questFunctions.IsQuestUnobtainable(elementId);
+
+    private sealed record EventQuest(string Name, List<ElementId> QuestIds, DateTime EndsAtUtc);
 }