Add basic quest validation

mnk-heavensward
Liza 2024-07-15 03:05:37 +02:00
parent 320ce14aed
commit a0e675cbdc
Signed by: liza
GPG Key ID: 7199F8D727D55F67
6 changed files with 147 additions and 36 deletions

View File

@ -65,16 +65,6 @@
"TerritoryId": 1187, "TerritoryId": 1187,
"InteractionType": "CompleteQuest", "InteractionType": "CompleteQuest",
"AetheryteShortcut": "Urqopacha - Wachunpelo" "AetheryteShortcut": "Urqopacha - Wachunpelo"
},
{
"DataId": 1050684,
"Position": {
"X": 391.37854,
"Y": -156.07434,
"Z": -388.50995
},
"TerritoryId": 1187,
"InteractionType": "CompleteQuest"
} }
] ]
} }

View File

@ -50,7 +50,7 @@
}, },
"StopDistance": 0.5, "StopDistance": 0.5,
"TerritoryId": 1187, "TerritoryId": 1187,
"InteractionType": "AcceptQuest", "InteractionType": "CompleteQuest",
"AetheryteShortcut": "Urqopacha - Wachunpelo", "AetheryteShortcut": "Urqopacha - Wachunpelo",
"Fly": true "Fly": true
} }

View File

@ -13,7 +13,7 @@
"Z": -52.99463 "Z": -52.99463
}, },
"TerritoryId": 1186, "TerritoryId": 1186,
"InteractionType": "Interact", "InteractionType": "AcceptQuest",
"Comment": "Quest is completed instantly" "Comment": "Quest is completed instantly"
} }
] ]

View File

@ -13,7 +13,7 @@
"Z": -38.132385 "Z": -38.132385
}, },
"TerritoryId": 1186, "TerritoryId": 1186,
"InteractionType": "Interact", "InteractionType": "AcceptQuest",
"Comment": "Quest is completed instantly" "Comment": "Quest is completed instantly"
} }
] ]

View File

@ -1,11 +1,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using Questionable.Model.V1; using Questionable.Model.V1;
#if RELEASE
namespace Questionable.QuestPaths; namespace Questionable.QuestPaths;
public static partial class AssemblyQuestLoader 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 #endif
}

View File

@ -1,13 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.IO; using System.IO;
using System.Numerics;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Utils;
using Questionable.Data; using Questionable.Data;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.V1; using Questionable.Model.V1;
@ -18,15 +22,17 @@ internal sealed class QuestRegistry
{ {
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly IChatGui _chatGui;
private readonly ILogger<QuestRegistry> _logger; private readonly ILogger<QuestRegistry> _logger;
private readonly Dictionary<ushort, Quest> _quests = new(); private readonly Dictionary<ushort, Quest> _quests = new();
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui,
ILogger<QuestRegistry> logger) ILogger<QuestRegistry> logger)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_questData = questData; _questData = questData;
_chatGui = chatGui;
_logger = logger; _logger = logger;
} }
@ -36,7 +42,26 @@ internal sealed class QuestRegistry
{ {
_quests.Clear(); _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"); _logger.LogInformation("Loading quests from assembly");
foreach ((ushort questId, QuestRoot questRoot) in QuestPaths.AssemblyQuestLoader.GetQuests()) foreach ((ushort questId, QuestRoot questRoot) in QuestPaths.AssemblyQuestLoader.GetQuests())
@ -49,7 +74,11 @@ internal sealed class QuestRegistry
}; };
_quests[questId] = quest; _quests[questId] = quest;
} }
#else }
[Conditional("DEBUG")]
private void LoadQuestsFromProjectDirectory()
{
DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent; DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent;
if (solutionDirectory != null) if (solutionDirectory != null)
{ {
@ -75,27 +104,116 @@ internal sealed class QuestRegistry
} }
} }
} }
#endif }
try [Conditional("DEBUG")]
private void ValidateQuests()
{
Task.Run(() =>
{ {
LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests"))); try
} {
catch (Exception e) int foundProblems = 0;
{ foreach (var quest in _quests.Values)
_logger.LogError(e, "Failed to load all quests from user directory (some may have been successfully loaded)"); {
} 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 var totalSequenceCount = quest.Root.QuestSequence.Count;
foreach (var quest in _quests.Values) var distinctSequenceCount = quest.Root.QuestSequence.Select(x => x.Sequence).Distinct().Count();
{ if (totalSequenceCount != distinctSequenceCount)
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 duplicate sequence numbers: {QuestId} / {QuestName}", quest.QuestId,
_logger.LogWarning("Quest has missing steps: {QuestId} / {QuestName} → {Count}", quest.QuestId, quest.Info.Name, missingSteps); quest.Info.Name);
} ++foundProblems;
#endif }
_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.");
}
});
} }