Add basic quest validation
authorLiza Carvelli <liza@carvel.li>
Mon, 15 Jul 2024 01:05:37 +0000 (03:05 +0200)
committerLiza Carvelli <liza@carvel.li>
Mon, 15 Jul 2024 01:05:37 +0000 (03:05 +0200)
QuestPaths/7.x - Dawntrail/Aether Currents/Urqopacha/5039_Traveler to the Rescue.json
QuestPaths/7.x - Dawntrail/Side Quests/Urqopacha/5038_In Pursuit of Mezcal.json
QuestPaths/7.x - Dawntrail/Unlocks/Instant/5007_Sights of the West and Beyond.json
QuestPaths/7.x - Dawntrail/Unlocks/Instant/5008_Dawn of a New Deal.json
QuestPaths/AssemblyQuestLoader.cs
Questionable/Controller/QuestRegistry.cs

index c9da7b591cce9de8a7c9a4b4c8428d9fee9d4b74..9dd6fa0c471245108d3412a8702bfb10fe35264e 100644 (file)
           "TerritoryId": 1187,
           "InteractionType": "CompleteQuest",
           "AetheryteShortcut": "Urqopacha - Wachunpelo"
-        },
-        {
-          "DataId": 1050684,
-          "Position": {
-            "X": 391.37854,
-            "Y": -156.07434,
-            "Z": -388.50995
-          },
-          "TerritoryId": 1187,
-          "InteractionType": "CompleteQuest"
         }
       ]
     }
index e463241df61745e6d1eaccdba6a24c75088d3614..44f4d8a73dca44042d590a14d39372ea77418439 100644 (file)
@@ -50,7 +50,7 @@
           },
           "StopDistance": 0.5,
           "TerritoryId": 1187,
-          "InteractionType": "AcceptQuest",
+          "InteractionType": "CompleteQuest",
           "AetheryteShortcut": "Urqopacha - Wachunpelo",
           "Fly": true
         }
index cb2725b355f7e7b69cc9a98194f3e41559c838a7..fd2ab340b0f98d26d88565947cc17e091e3f8597 100644 (file)
@@ -13,7 +13,7 @@
             "Z": -52.99463
           },
           "TerritoryId": 1186,
-          "InteractionType": "Interact",
+          "InteractionType": "AcceptQuest",
           "Comment": "Quest is completed instantly"
         }
       ]
index 720a825ff9d9f0a76a09f4b7419d2c3e070460bc..4cc2f982fdd355229bd2c20a9c0ed0a9ff766794 100644 (file)
@@ -13,7 +13,7 @@
             "Z": -38.132385
           },
           "TerritoryId": 1186,
-          "InteractionType": "Interact",
+          "InteractionType": "AcceptQuest",
           "Comment": "Quest is completed instantly"
         }
       ]
index 4abfb4c7c20da404d315eb64d7d655240d2826d0..b0b8efc01e679ceefe1abff2ae3412d0dc71a873 100644 (file)
@@ -1,11 +1,14 @@
 using System.Collections.Generic;
 using Questionable.Model.V1;
 
-#if RELEASE
 namespace Questionable.QuestPaths;
 
 public static partial class AssemblyQuestLoader
 {
-    public static IReadOnlyDictionary<ushort, QuestRoot> GetQuests() => Quests;
-}
+    public static IReadOnlyDictionary<ushort, QuestRoot> GetQuests() =>
+#if RELEASE
+        Quests;
+#else
+        new Dictionary<ushort, QuestRoot>();
 #endif
+}
index b7450c534394cac766483859ab8cbdb4ea8ffe91..f8a412539c39aa0072d83c012e30289f5571202d 100644 (file)
@@ -1,13 +1,17 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Linq;
 using System.IO;
+using System.Numerics;
 using System.Text.Json;
+using System.Threading.Tasks;
 using Dalamud.Plugin;
 using Dalamud.Plugin.Services;
 using Microsoft.Extensions.Logging;
+using Questionable.Controller.Utils;
 using Questionable.Data;
 using Questionable.Model;
 using Questionable.Model.V1;
@@ -18,15 +22,17 @@ internal sealed class QuestRegistry
 {
     private readonly IDalamudPluginInterface _pluginInterface;
     private readonly QuestData _questData;
+    private readonly IChatGui _chatGui;
     private readonly ILogger<QuestRegistry> _logger;
 
     private readonly Dictionary<ushort, Quest> _quests = new();
 
-    public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData,
+    public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui,
         ILogger<QuestRegistry> logger)
     {
         _pluginInterface = pluginInterface;
         _questData = questData;
+        _chatGui = chatGui;
         _logger = logger;
     }
 
@@ -36,7 +42,26 @@ internal sealed class QuestRegistry
     {
         _quests.Clear();
 
-#if RELEASE
+        LoadQuestsFromAssembly();
+        LoadQuestsFromProjectDirectory();
+
+        try
+        {
+            LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")));
+        }
+        catch (Exception e)
+        {
+            _logger.LogError(e,
+                "Failed to load all quests from user directory (some may have been successfully loaded)");
+        }
+
+        ValidateQuests();
+        _logger.LogInformation("Loaded {Count} quests", _quests.Count);
+    }
+
+    [Conditional("RELEASE")]
+    private void LoadQuestsFromAssembly()
+    {
         _logger.LogInformation("Loading quests from assembly");
 
         foreach ((ushort questId, QuestRoot questRoot) in QuestPaths.AssemblyQuestLoader.GetQuests())
@@ -49,7 +74,11 @@ internal sealed class QuestRegistry
             };
             _quests[questId] = quest;
         }
-#else
+    }
+
+    [Conditional("DEBUG")]
+    private void LoadQuestsFromProjectDirectory()
+    {
         DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent;
         if (solutionDirectory != null)
         {
@@ -75,27 +104,116 @@ internal sealed class QuestRegistry
                 }
             }
         }
-#endif
+    }
 
-        try
-        {
-            LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")));
-        }
-        catch (Exception e)
+    [Conditional("DEBUG")]
+    private void ValidateQuests()
+    {
+        Task.Run(() =>
         {
-            _logger.LogError(e, "Failed to load all quests from user directory (some may have been successfully loaded)");
-        }
+            try
+            {
+                int foundProblems = 0;
+                foreach (var quest in _quests.Values)
+                {
+                    int missingSteps = quest.Root.QuestSequence.Where(x => x.Sequence < 255).Max(x => x.Sequence) -
+                        quest.Root.QuestSequence.Count(x => x.Sequence < 255) + 1;
+                    if (missingSteps != 0)
+                    {
+                        _logger.LogWarning("Quest has missing steps: {QuestId} / {QuestName} → {Count}", quest.QuestId,
+                            quest.Info.Name, missingSteps);
+                        ++foundProblems;
+                    }
 
-#if !RELEASE
-        foreach (var quest in _quests.Values)
-        {
-            int missingSteps = quest.Root.QuestSequence.Where(x => x.Sequence < 255).Max(x => x.Sequence) - quest.Root.QuestSequence.Count(x => x.Sequence < 255) + 1;
-            if (missingSteps != 0)
-                _logger.LogWarning("Quest has missing steps: {QuestId} / {QuestName} → {Count}", quest.QuestId, quest.Info.Name, missingSteps);
-        }
-#endif
+                    var totalSequenceCount = quest.Root.QuestSequence.Count;
+                    var distinctSequenceCount = quest.Root.QuestSequence.Select(x => x.Sequence).Distinct().Count();
+                    if (totalSequenceCount != distinctSequenceCount)
+                    {
+                        _logger.LogWarning("Quest has duplicate sequence numbers: {QuestId} / {QuestName}", quest.QuestId,
+                            quest.Info.Name);
+                        ++foundProblems;
+                    }
 
-        _logger.LogInformation("Loaded {Count} quests", _quests.Count);
+                    foreach (var sequence in quest.Root.QuestSequence)
+                    {
+                        if (sequence.Sequence == 0 &&
+                            sequence.Steps.LastOrDefault()?.InteractionType != EInteractionType.AcceptQuest)
+                        {
+                            _logger.LogWarning(
+                                "Quest likely has AcceptQuest configured wrong: {QuestId} / {QuestName} → {Sequence} / {Step}",
+                                quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.Count - 1);
+                            ++foundProblems;
+                        }
+                        else if (sequence.Sequence == 255 &&
+                                 sequence.Steps.LastOrDefault()?.InteractionType != EInteractionType.CompleteQuest)
+                        {
+                            _logger.LogWarning(
+                                "Quest likely has CompleteQuest configured wrong: {QuestId} / {QuestName} → {Sequence} / {Step}",
+                                quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.Count - 1);
+                            ++foundProblems;
+                        }
+
+
+                        var acceptQuestSteps = sequence.Steps
+                            .Where(x => x is { InteractionType: EInteractionType.AcceptQuest, PickupQuestId: null })
+                            .Where(x => sequence.Sequence != 0 || x != sequence.Steps.Last());
+                        foreach (var step in acceptQuestSteps)
+                        {
+                            _logger.LogWarning(
+                                "Quest has unexpected AcceptQuest steps: {QuestId} / {QuestName} → {Sequence} / {Step}",
+                                quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.IndexOf(step));
+                            ++foundProblems;
+                        }
+
+                        var completeQuestSteps = sequence.Steps
+                            .Where(x => x is { InteractionType: EInteractionType.CompleteQuest, TurnInQuestId: null })
+                            .Where(x => sequence.Sequence != 255 || x != sequence.Steps.Last());
+                        foreach (var step in completeQuestSteps)
+                        {
+                            _logger.LogWarning(
+                                "Quest has unexpected CompleteQuest steps: {QuestId} / {QuestName} → {Sequence} / {Step}",
+                                quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.IndexOf(step));
+                            ++foundProblems;
+                        }
+
+                        var completionFlags = sequence.Steps.Select(x => x.CompletionQuestVariablesFlags)
+                            .Where(QuestWorkUtils.HasCompletionFlags)
+                            .GroupBy(x =>
+                            {
+                                return Enumerable.Range(0, 6).Select(y =>
+                                    {
+                                        short? value = x[y];
+                                        if (value == null || value.Value < 0)
+                                            return (long)0;
+                                        return (long)BitOperations.RotateLeft((ulong)value.Value, 8 * y);
+                                    })
+                                    .Sum();
+                            })
+                            .Where(x => x.Key != 0)
+                            .Where(x => x.Count() > 1);
+                        foreach (var duplicate in completionFlags)
+                        {
+                            _logger.LogWarning(
+                                "Quest step has duplicate completion flags: {QuestId} / {QuestName} → {Sequence} → {Flags}",
+                                quest.QuestId, quest.Info.Name, sequence.Sequence, string.Join(", ", duplicate.First()));
+                            ++foundProblems;
+                        }
+                    }
+                }
+
+                if (foundProblems > 0)
+                {
+                    _chatGui.Print(
+                        $"[Questionable] Quest validation has found {foundProblems} problems. Check the log for details.");
+                }
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, "Unable to validate quests");
+                _chatGui.PrintError(
+                    $"[Questionable] Unable to validate quests. Check the log for details.");
+            }
+        });
     }