From 6f2ebe5a5a7439e69e9ba817df8f96edfc389f57 Mon Sep 17 00:00:00 2001
From: Liza Carvelli <liza@carvel.li>
Date: Wed, 17 Jul 2024 15:05:24 +0200
Subject: [PATCH] Run JSON schema validation in separate thread

---
 Questionable/Controller/QuestRegistry.cs      | 34 ++++----------
 Questionable/QuestionablePlugin.cs            | 41 +++++++++++++----
 Questionable/Validation/IQuestValidator.cs    |  4 ++
 Questionable/Validation/QuestValidator.cs     | 14 +++---
 .../Validators/JsonSchemaValidator.cs         | 44 +++++++++++++++++++
 5 files changed, 97 insertions(+), 40 deletions(-)
 create mode 100644 Questionable/Validation/Validators/JsonSchemaValidator.cs

diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs
index 99cb4800..c8e67986 100644
--- a/Questionable/Controller/QuestRegistry.cs
+++ b/Questionable/Controller/QuestRegistry.cs
@@ -7,15 +7,14 @@ using System.IO;
 using System.Linq;
 using System.Text.Json;
 using System.Text.Json.Nodes;
-using System.Threading.Tasks;
 using Dalamud.Plugin;
-using Json.Schema;
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
 using Questionable.Model;
 using Questionable.Model.V1;
 using Questionable.QuestPaths;
 using Questionable.Validation;
+using Questionable.Validation.Validators;
 
 namespace Questionable.Controller;
 
@@ -25,18 +24,19 @@ internal sealed class QuestRegistry
     private readonly QuestData _questData;
     private readonly QuestValidator _questValidator;
     private readonly ILogger<QuestRegistry> _logger;
-    private readonly JsonSchema _questSchema;
+    private readonly JsonSchemaValidator _jsonSchemaValidator;
 
     private readonly Dictionary<ushort, Quest> _quests = new();
 
     public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData,
-        QuestValidator questValidator, ILogger<QuestRegistry> logger)
+        QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator,
+        ILogger<QuestRegistry> logger)
     {
         _pluginInterface = pluginInterface;
         _questData = questData;
         _questValidator = questValidator;
+        _jsonSchemaValidator = jsonSchemaValidator;
         _logger = logger;
-        _questSchema = JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result;
     }
 
     public IEnumerable<Quest> AllQuests => _quests.Values;
@@ -46,7 +46,7 @@ internal sealed class QuestRegistry
 
     public void Reload()
     {
-        _questValidator.ClearIssues();
+        _questValidator.Reset();
         _quests.Clear();
 
         LoadQuestsFromAssembly();
@@ -130,26 +130,8 @@ internal sealed class QuestRegistry
         if (questId == null)
             return;
 
-        var questNode = JsonNode.Parse(stream);
-        Task.Run(() =>
-        {
-            var evaluationResult = _questSchema.Evaluate(questNode, new EvaluationOptions
-            {
-                Culture = CultureInfo.InvariantCulture,
-                OutputFormat = OutputFormat.List
-            });
-            if (!evaluationResult.IsValid)
-            {
-                _questValidator.AddIssue(new ValidationIssue
-                {
-                    QuestId = questId.Value,
-                    Sequence = null,
-                    Step = null,
-                    Severity = EIssueSeverity.Error,
-                    Description = "JSON Validation failed"
-                });
-            }
-        });
+        var questNode = JsonNode.Parse(stream)!;
+        _jsonSchemaValidator.Enqueue(questId.Value, questNode);
 
         Quest quest = new Quest
         {
diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs
index 58f580e3..eca5656a 100644
--- a/Questionable/QuestionablePlugin.cs
+++ b/Questionable/QuestionablePlugin.cs
@@ -67,6 +67,23 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
         serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
 
+        AddBasicFunctionsAndData(serviceCollection);
+        AddTaskFactories(serviceCollection);
+        AddControllers(serviceCollection);
+        AddWindows(serviceCollection);
+        AddQuestValidators(serviceCollection);
+
+        serviceCollection.AddSingleton<CommandHandler>();
+        serviceCollection.AddSingleton<DalamudInitializer>();
+
+        _serviceProvider = serviceCollection.BuildServiceProvider();
+        _serviceProvider.GetRequiredService<QuestRegistry>().Reload();
+        _serviceProvider.GetRequiredService<CommandHandler>();
+        _serviceProvider.GetRequiredService<DalamudInitializer>();
+    }
+
+    private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
+    {
         serviceCollection.AddSingleton<GameFunctions>();
         serviceCollection.AddSingleton<ChatFunctions>();
         serviceCollection.AddSingleton<AetherCurrentData>();
@@ -76,12 +93,15 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<NavmeshIpc>();
         serviceCollection.AddSingleton<LifestreamIpc>();
         serviceCollection.AddSingleton<YesAlreadyIpc>();
+    }
 
+    private static void AddTaskFactories(ServiceCollection serviceCollection)
+    {
         // individual tasks
         serviceCollection.AddTransient<MountTask>();
         serviceCollection.AddTransient<UnmountTask>();
 
-        // tasks with factories
+        // task factories
         serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
         serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
         serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckTask>();
@@ -115,7 +135,10 @@ public sealed class QuestionablePlugin : IDalamudPlugin
                 WaitAtEnd.WaitObjectAtPosition>();
         serviceCollection.AddTransient<WaitAtEnd.WaitQuestAccepted>();
         serviceCollection.AddTransient<WaitAtEnd.WaitQuestCompleted>();
+    }
 
+    private static void AddControllers(ServiceCollection serviceCollection)
+    {
         serviceCollection.AddSingleton<MovementController>();
         serviceCollection.AddSingleton<MovementOverrideController>();
         serviceCollection.AddSingleton<QuestRegistry>();
@@ -125,27 +148,27 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<CombatController>();
 
         serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
+    }
 
+    private static void AddWindows(ServiceCollection serviceCollection)
+    {
         serviceCollection.AddSingleton<QuestWindow>();
         serviceCollection.AddSingleton<ConfigWindow>();
         serviceCollection.AddSingleton<DebugOverlay>();
         serviceCollection.AddSingleton<QuestSelectionWindow>();
         serviceCollection.AddSingleton<QuestValidationWindow>();
+    }
 
+    private static void AddQuestValidators(ServiceCollection serviceCollection)
+    {
         serviceCollection.AddSingleton<QuestValidator>();
         serviceCollection.AddSingleton<IQuestValidator, QuestDisabledValidator>();
         serviceCollection.AddSingleton<IQuestValidator, BasicSequenceValidator>();
         serviceCollection.AddSingleton<IQuestValidator, UniqueStartStopValidator>();
         serviceCollection.AddSingleton<IQuestValidator, NextQuestValidator>();
         serviceCollection.AddSingleton<IQuestValidator, CompletionFlagsValidator>();
-
-        serviceCollection.AddSingleton<CommandHandler>();
-        serviceCollection.AddSingleton<DalamudInitializer>();
-
-        _serviceProvider = serviceCollection.BuildServiceProvider();
-        _serviceProvider.GetRequiredService<QuestRegistry>().Reload();
-        _serviceProvider.GetRequiredService<CommandHandler>();
-        _serviceProvider.GetRequiredService<DalamudInitializer>();
+        serviceCollection.AddSingleton<JsonSchemaValidator>();
+        serviceCollection.AddSingleton<IQuestValidator>(sp => sp.GetRequiredService<JsonSchemaValidator>());
     }
 
     public void Dispose()
diff --git a/Questionable/Validation/IQuestValidator.cs b/Questionable/Validation/IQuestValidator.cs
index 02a5f182..893b5d92 100644
--- a/Questionable/Validation/IQuestValidator.cs
+++ b/Questionable/Validation/IQuestValidator.cs
@@ -6,4 +6,8 @@ namespace Questionable.Validation;
 internal interface IQuestValidator
 {
     IEnumerable<ValidationIssue> Validate(Quest quest);
+
+    void Reset()
+    {
+    }
 }
diff --git a/Questionable/Validation/QuestValidator.cs b/Questionable/Validation/QuestValidator.cs
index 1e6d6201..b5b7bd4f 100644
--- a/Questionable/Validation/QuestValidator.cs
+++ b/Questionable/Validation/QuestValidator.cs
@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 using Questionable.Model;
@@ -26,11 +27,16 @@ internal sealed class QuestValidator
     public int IssueCount => _validationIssues.Count;
     public int ErrorCount => _validationIssues.Count(x => x.Severity == EIssueSeverity.Error);
 
-    public void ClearIssues() => _validationIssues.Clear();
+    public void Reset()
+    {
+        foreach (var validator in _validators)
+            validator.Reset();
+        _validationIssues.Clear();
+    }
 
     public void Validate(IEnumerable<Quest> quests)
     {
-        Task.Run(() =>
+        Task.Factory.StartNew(() =>
         {
             foreach (var quest in quests)
             {
@@ -52,8 +58,6 @@ internal sealed class QuestValidator
                 .ThenBy(x => x.Step)
                 .ThenBy(x => x.Description)
                 .ToList();
-        });
+        }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
     }
-
-    public void AddIssue(ValidationIssue issue) => _validationIssues.Add(issue);
 }
diff --git a/Questionable/Validation/Validators/JsonSchemaValidator.cs b/Questionable/Validation/Validators/JsonSchemaValidator.cs
new file mode 100644
index 00000000..04abc012
--- /dev/null
+++ b/Questionable/Validation/Validators/JsonSchemaValidator.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.Json.Nodes;
+using Json.Schema;
+using Questionable.Model;
+using Questionable.QuestPaths;
+
+namespace Questionable.Validation.Validators;
+
+internal sealed class JsonSchemaValidator : IQuestValidator
+{
+    private readonly Dictionary<ushort, JsonNode> _questNodes = new();
+    private JsonSchema? _questSchema;
+
+    public IEnumerable<ValidationIssue> Validate(Quest quest)
+    {
+        _questSchema ??= JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result;
+
+        if (_questNodes.TryGetValue(quest.QuestId, out JsonNode? questNode))
+        {
+            var evaluationResult = _questSchema.Evaluate(questNode, new EvaluationOptions
+            {
+                Culture = CultureInfo.InvariantCulture,
+                OutputFormat = OutputFormat.List
+            });
+            if (!evaluationResult.IsValid)
+            {
+                yield return new ValidationIssue
+                {
+                    QuestId = quest.QuestId,
+                    Sequence = null,
+                    Step = null,
+                    Severity = EIssueSeverity.Error,
+                    Description = "JSON Validation failed"
+                };
+            }
+        }
+
+    }
+
+    public void Enqueue(ushort questId, JsonNode questNode) => _questNodes[questId] = questNode;
+
+    public void Reset() => _questNodes.Clear();
+}