muffin v7.5.5

This commit is contained in:
alydev 2026-08-17 20:25:32 +10:00
parent a8f2c1df37
commit 6266f9e6b7
110 changed files with 14907 additions and 6073 deletions

View file

@ -25,6 +25,8 @@ namespace Questionable.Controller;
internal sealed class QuestRegistry
{
internal sealed record FailedQuestLoad(ElementId QuestId, string? FilePath, string ErrorMessage, Quest.ESource Source);
private readonly IDalamudPluginInterface _pluginInterface;
private readonly QuestData _questData;
@ -49,8 +51,12 @@ internal sealed class QuestRegistry
private readonly Dictionary<ElementId, string> _questFolderNames = new Dictionary<ElementId, string>();
private readonly Dictionary<ElementId, FailedQuestLoad> _failedLoads = new Dictionary<ElementId, FailedQuestLoad>();
public IEnumerable<Quest> AllQuests => _quests.Values;
public IReadOnlyDictionary<ElementId, FailedQuestLoad> FailedLoads => _failedLoads;
public int Count => _quests.Count<KeyValuePair<ElementId, Quest>>((KeyValuePair<ElementId, Quest> x) => !x.Value.Root.Disabled);
public int ValidationIssueCount => _questValidator.IssueCount;
@ -77,6 +83,7 @@ internal sealed class QuestRegistry
{
_questValidator.Reset();
_quests.Clear();
_failedLoads.Clear();
_contentFinderConditionIds.Clear();
_lowPriorityContentFinderConditionQuests.Clear();
_questFolderNames.Clear();
@ -244,7 +251,7 @@ internal sealed class QuestRegistry
_questValidator.Validate(_quests.Values.Where((Quest x) => x.Source != Quest.ESource.Assembly).ToList());
}
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source, string directoryName)
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source, string directoryName, string? filePath = null)
{
if (source == Quest.ESource.UserDirectory)
{
@ -259,9 +266,26 @@ internal sealed class QuestRegistry
try
{
jsonNode = JsonNode.Parse(stream);
string text = FindDuplicateKey(jsonNode);
if (text != null)
{
string errorMessage = "Duplicate JSON property '" + text + "'. Remove the duplicate key.";
_questValidator.AddValidationIssue(new ValidationIssue
{
ElementId = elementId,
Sequence = null,
Step = null,
Type = EIssueType.InvalidJsonSyntax,
Severity = EIssueSeverity.Error,
Description = $"Duplicate JSON property '{text}' in file '{fileName}'. Remove the duplicate key."
});
_failedLoads[elementId] = new FailedQuestLoad(elementId, filePath, errorMessage, source);
return;
}
}
catch (JsonException ex)
{
string errorMessage2 = "JSON syntax error: " + ex.Message;
ValidationIssue issue = new ValidationIssue
{
ElementId = elementId,
@ -269,9 +293,10 @@ internal sealed class QuestRegistry
Step = null,
Type = EIssueType.InvalidJsonSyntax,
Severity = EIssueSeverity.Error,
Description = $"JSON parsing error in file '{fileName}': {ex.Message}\n\nThis usually indicates a syntax error such as:\n\ufffd Missing comma between properties\n\ufffd Unclosed quotes or brackets\n\ufffd Invalid escape sequences\n\ufffd Trailing commas where not allowed\n\nPlease check the JSON syntax around the indicated position."
Description = $"JSON parsing error in file '{fileName}': {ex.Message}\n\nThis usually indicates a syntax error such as:\n■ Missing comma between properties\n■ Unclosed quotes or brackets\n■ Invalid escape sequences\n■ Trailing commas where not allowed\n\nPlease check the JSON syntax around the indicated position."
};
_questValidator.AddValidationIssue(issue);
_failedLoads[elementId] = new FailedQuestLoad(elementId, filePath, errorMessage2, source);
return;
}
_jsonSchemaValidator.Enqueue(elementId, jsonNode);
@ -312,7 +337,26 @@ internal sealed class QuestRegistry
}
}
}
QuestRoot root = jsonNode.Deserialize<QuestRoot>();
QuestRoot root;
try
{
root = jsonNode.Deserialize<QuestRoot>();
}
catch (Exception ex2)
{
string errorMessage3 = "Failed to deserialize quest data: " + ex2.Message;
_questValidator.AddValidationIssue(new ValidationIssue
{
ElementId = elementId,
Sequence = null,
Step = null,
Type = EIssueType.InvalidJsonSchema,
Severity = EIssueSeverity.Error,
Description = "Failed to deserialize '" + fileName + "': " + ex2.Message
});
_failedLoads[elementId] = new FailedQuestLoad(elementId, filePath, errorMessage3, source);
return;
}
if (!_questData.TryGetQuestInfo(elementId, out IQuestInfo questInfo))
{
if (!(elementId is UnlockLinkId unlockLinkId))
@ -323,9 +367,9 @@ internal sealed class QuestRegistry
string name;
try
{
string text = fileName.Substring(0, fileName.Length - ".json".Length);
int num = text.IndexOf('_', StringComparison.Ordinal);
name = ((num >= 0 && num + 1 < text.Length) ? text.Substring(num + 1) : text);
string text2 = fileName.Substring(0, fileName.Length - ".json".Length);
int num = text2.IndexOf('_', StringComparison.Ordinal);
name = ((num >= 0 && num + 1 < text2.Length) ? text2.Substring(num + 1) : text2);
}
catch
{
@ -398,11 +442,17 @@ internal sealed class QuestRegistry
try
{
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
LoadQuestFromStream(fileInfo.Name, stream, source, directory.Name);
LoadQuestFromStream(fileInfo.Name, stream, source, directory.Name, fileInfo.FullName);
}
catch (Exception innerException)
catch (Exception ex)
{
throw new InvalidDataException("Unable to load file " + fileInfo.FullName, innerException);
ElementId elementId = ExtractQuestIdFromName(fileInfo.Name);
string errorMessage = "Unexpected error loading file: " + ex.Message;
_logger.LogError(ex, "Failed to load quest from '{FileName}', skipping", fileInfo.FullName);
if (elementId != null)
{
_failedLoads[elementId] = new FailedQuestLoad(elementId, fileInfo.FullName, errorMessage, source);
}
}
}
DirectoryInfo[] directories = directory.GetDirectories();
@ -412,6 +462,46 @@ internal sealed class QuestRegistry
}
}
private static string? FindDuplicateKey(JsonNode? node)
{
if (node is JsonObject jsonObject)
{
try
{
HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
foreach (var (text2, node2) in jsonObject)
{
if (!hashSet.Add(text2))
{
return text2;
}
string text3 = FindDuplicateKey(node2);
if (text3 != null)
{
return text3;
}
}
}
catch (ArgumentException ex) when (ex.Message.Contains("same key", StringComparison.OrdinalIgnoreCase))
{
Match match = Regex.Match(ex.Message, "Key: (.+)");
return match.Success ? match.Groups[1].Value.Trim() : "unknown";
}
}
else if (node is JsonArray jsonArray)
{
foreach (JsonNode item in jsonArray)
{
string text4 = FindDuplicateKey(item);
if (text4 != null)
{
return text4;
}
}
}
return null;
}
private static ElementId? ExtractQuestIdFromName(string resourceName)
{
string text = resourceName.Substring(0, resourceName.Length - ".json".Length);
@ -428,6 +518,11 @@ internal sealed class QuestRegistry
return _quests.ContainsKey(questId);
}
public bool IsFailedLoad(ElementId questId)
{
return _failedLoads.ContainsKey(questId);
}
public bool TryGetQuest(ElementId questId, [NotNullWhen(true)] out Quest? quest)
{
return _quests.TryGetValue(questId, out quest);