567 lines
16 KiB
C#
567 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Text.RegularExpressions;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Colors;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using Dalamud.Plugin;
|
|
using Questionable.Data;
|
|
using Questionable.Model;
|
|
using Questionable.Model.Questing;
|
|
using Questionable.Validation;
|
|
|
|
namespace Questionable.Windows.QuestComponents;
|
|
|
|
internal sealed class ValidationDetailsRenderer
|
|
{
|
|
private sealed record JsonValidationError
|
|
{
|
|
public string Path { get; init; } = string.Empty;
|
|
|
|
public List<string> Messages { get; init; } = new List<string>();
|
|
}
|
|
|
|
private readonly QuestData _questData;
|
|
|
|
private readonly IDalamudPluginInterface _pluginInterface;
|
|
|
|
private readonly Dictionary<int, ValidationIssue> _storedIssues = new Dictionary<int, ValidationIssue>();
|
|
|
|
private readonly Dictionary<int, bool> _openDetailWindows = new Dictionary<int, bool>();
|
|
|
|
private static readonly Regex JsonPropertyPathRegex = new Regex("#/([^:]*)", RegexOptions.Compiled);
|
|
|
|
private static readonly Regex UnicodeEscapeRegex = new Regex("\\\\u([0-9A-Fa-f]{4})", RegexOptions.Compiled);
|
|
|
|
private static readonly Regex JsonEscapeRegex = new Regex("\\\\(.)", RegexOptions.Compiled);
|
|
|
|
private static readonly Regex ConsecutiveQuotesRegex = new Regex("\"{2,}", RegexOptions.Compiled);
|
|
|
|
public ValidationDetailsRenderer(QuestData questData, IDalamudPluginInterface pluginInterface)
|
|
{
|
|
_questData = questData;
|
|
_pluginInterface = pluginInterface;
|
|
}
|
|
|
|
public static string CleanJsonText(string text)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return text;
|
|
}
|
|
text = UnicodeEscapeRegex.Replace(text, (Match match) => int.TryParse(match.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result) ? ((char)result).ToString() : match.Value);
|
|
text = JsonEscapeRegex.Replace(text, (Match match) => match.Groups[1].Value switch
|
|
{
|
|
"\"" => "\"",
|
|
"\\" => "\\",
|
|
"/" => "/",
|
|
"b" => "\b",
|
|
"f" => "\f",
|
|
"n" => "\n",
|
|
"r" => "\r",
|
|
"t" => "\t",
|
|
_ => match.Value,
|
|
});
|
|
if (text.Contains("\"\"", StringComparison.Ordinal))
|
|
{
|
|
text = ConsecutiveQuotesRegex.Replace(text, "\"");
|
|
text = text.Replace("Expected \"\"", "Expected \"\"", StringComparison.Ordinal);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
public void OpenDetails(ValidationIssue issue, int index)
|
|
{
|
|
_storedIssues[index] = issue;
|
|
_openDetailWindows[index] = true;
|
|
}
|
|
|
|
public void DrawDetailWindows()
|
|
{
|
|
List<int> list = new List<int>();
|
|
foreach (KeyValuePair<int, bool> item in _openDetailWindows.ToList())
|
|
{
|
|
if (item.Value && _storedIssues.TryGetValue(item.Key, out ValidationIssue value))
|
|
{
|
|
string obj = $"Validation Details##{item.Key}";
|
|
bool open = true;
|
|
ImGui.SetNextWindowSize(new Vector2(800f, 600f), ImGuiCond.FirstUseEver);
|
|
ImGui.SetNextWindowSizeConstraints(new Vector2(500f, 300f), new Vector2(1200f, 800f));
|
|
if (ImGui.Begin(obj, ref open))
|
|
{
|
|
DrawIssueDetails(value);
|
|
ImGui.End();
|
|
}
|
|
if (!open)
|
|
{
|
|
list.Add(item.Key);
|
|
}
|
|
}
|
|
}
|
|
foreach (int item2 in list)
|
|
{
|
|
_openDetailWindows.Remove(item2);
|
|
_storedIssues.Remove(item2);
|
|
}
|
|
}
|
|
|
|
private void DrawIssueDetails(ValidationIssue issue)
|
|
{
|
|
Vector4 color = ((issue.Severity == EIssueSeverity.Error) ? ImGuiColors.DalamudRed : ImGuiColors.DalamudOrange);
|
|
FontAwesomeIcon icon = ((issue.Severity == EIssueSeverity.Error) ? FontAwesomeIcon.ExclamationTriangle : FontAwesomeIcon.InfoCircle);
|
|
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
|
{
|
|
ImGui.TextUnformatted(icon.ToIconString());
|
|
}
|
|
}
|
|
ImGui.SameLine();
|
|
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
|
{
|
|
ImU8String text = new ImU8String(2, 2);
|
|
text.AppendFormatted(issue.Severity);
|
|
text.AppendLiteral(": ");
|
|
text.AppendFormatted(issue.Type);
|
|
ImGui.Text(text);
|
|
}
|
|
ImGui.Separator();
|
|
if (issue.ElementId != null)
|
|
{
|
|
IQuestInfo questInfo = _questData.GetQuestInfo(issue.ElementId);
|
|
ImU8String text = new ImU8String(10, 2);
|
|
text.AppendLiteral("Quest: ");
|
|
text.AppendFormatted(issue.ElementId);
|
|
text.AppendLiteral(" - ");
|
|
text.AppendFormatted(questInfo.Name);
|
|
ImGui.Text(text);
|
|
}
|
|
else if (issue.AlliedSociety != EAlliedSociety.None)
|
|
{
|
|
ImU8String text = new ImU8String(16, 1);
|
|
text.AppendLiteral("Allied Society: ");
|
|
text.AppendFormatted(issue.AlliedSociety);
|
|
ImGui.Text(text);
|
|
}
|
|
if (issue.Sequence.HasValue)
|
|
{
|
|
ImU8String text = new ImU8String(10, 1);
|
|
text.AppendLiteral("Sequence: ");
|
|
text.AppendFormatted(issue.Sequence);
|
|
ImGui.Text(text);
|
|
}
|
|
if (issue.Step.HasValue)
|
|
{
|
|
ImU8String text = new ImU8String(6, 1);
|
|
text.AppendLiteral("Step: ");
|
|
text.AppendFormatted(issue.Step);
|
|
ImGui.Text(text);
|
|
}
|
|
ImGui.Separator();
|
|
ImGui.Text("Description:");
|
|
string description = CleanJsonText(issue.Description ?? "(no description)");
|
|
if (issue.Type == EIssueType.QuestDisabled && issue.ElementId == null)
|
|
{
|
|
DrawDisabledTribesDetails(description);
|
|
}
|
|
else if (issue.Type == EIssueType.InvalidJsonSchema)
|
|
{
|
|
DrawEnhancedJsonSchemaDetails(description);
|
|
}
|
|
else if (issue.Type == EIssueType.InvalidJsonSyntax)
|
|
{
|
|
DrawJsonSyntaxErrorDetails(description);
|
|
}
|
|
else
|
|
{
|
|
DrawGenericDetails(description);
|
|
}
|
|
}
|
|
|
|
private static void DrawJsonSyntaxErrorDetails(string description)
|
|
{
|
|
string[] array = description.Split('\n');
|
|
for (int i = 0; i < array.Length; i++)
|
|
{
|
|
string text = array[i].Trim();
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
ImGui.Spacing();
|
|
}
|
|
else if (text.StartsWith("JSON parsing error", StringComparison.Ordinal))
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
|
{
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
else if (text.StartsWith("This usually indicates", StringComparison.Ordinal) || text.StartsWith("Please check", StringComparison.Ordinal))
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
|
|
{
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
else if (text.StartsWith("\ufffd ", StringComparison.Ordinal))
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
|
|
{
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawDisabledTribesDetails(string description)
|
|
{
|
|
string[] array = description.Split(':', 2);
|
|
if (array.Length < 2)
|
|
{
|
|
ImGui.TextWrapped(description);
|
|
return;
|
|
}
|
|
ImGui.TextWrapped(array[0]);
|
|
ImGui.Spacing();
|
|
List<string> list = (from x in array[1].Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
select x.Trim() into x
|
|
where !string.IsNullOrEmpty(x)
|
|
select x).Distinct().ToList();
|
|
if (list.Count == 0)
|
|
{
|
|
ImGui.TextWrapped("(no disabled quests listed)");
|
|
return;
|
|
}
|
|
ImGui.Text("Disabled Quests:");
|
|
ImGui.Indent();
|
|
Vector4[] array2 = new Vector4[6]
|
|
{
|
|
ImGuiColors.TankBlue,
|
|
ImGuiColors.HealerGreen,
|
|
ImGuiColors.DPSRed,
|
|
ImGuiColors.ParsedGreen,
|
|
ImGuiColors.ParsedBlue,
|
|
ImGuiColors.DalamudViolet
|
|
};
|
|
for (int num = 0; num < list.Count; num++)
|
|
{
|
|
string value = list[num];
|
|
Vector4 color = array2[num % array2.Length];
|
|
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
|
{
|
|
if (ElementId.TryFromString(value, out ElementId elementId) && elementId != null)
|
|
{
|
|
try
|
|
{
|
|
IQuestInfo questInfo = _questData.GetQuestInfo(elementId);
|
|
ImU8String text = new ImU8String(5, 2);
|
|
text.AppendLiteral("\ufffd ");
|
|
text.AppendFormatted(value);
|
|
text.AppendLiteral(" - ");
|
|
text.AppendFormatted(questInfo.Name);
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
catch
|
|
{
|
|
ImU8String text = new ImU8String(18, 1);
|
|
text.AppendLiteral("\ufffd ");
|
|
text.AppendFormatted(value);
|
|
text.AppendLiteral(" (unknown quest)");
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImU8String text = new ImU8String(2, 1);
|
|
text.AppendLiteral("\ufffd ");
|
|
text.AppendFormatted(value);
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
}
|
|
ImGui.Unindent();
|
|
}
|
|
|
|
private void DrawEnhancedJsonSchemaDetails(string description)
|
|
{
|
|
if (description.Split('\n').Length == 0)
|
|
{
|
|
ImGui.TextWrapped("No validation details available.");
|
|
return;
|
|
}
|
|
List<JsonValidationError> list = ParseJsonValidationErrors(description);
|
|
if (list.Count > 0)
|
|
{
|
|
ImGui.Text("JSON Schema Validation Errors:");
|
|
ImGui.Spacing();
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
DrawJsonValidationError(list[i], i);
|
|
if (i < list.Count - 1)
|
|
{
|
|
ImGui.Separator();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DrawSimpleJsonSchemaDetails(description);
|
|
}
|
|
}
|
|
|
|
private void DrawJsonValidationError(JsonValidationError error, int index)
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
|
|
{
|
|
ImU8String text = new ImU8String(8, 1);
|
|
text.AppendLiteral("Error #");
|
|
text.AppendFormatted(index + 1);
|
|
text.AppendLiteral(":");
|
|
ImGui.Text(text);
|
|
}
|
|
ImGui.Indent(12f);
|
|
if (!string.IsNullOrEmpty(error.Path))
|
|
{
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.Text("Location:");
|
|
ImGui.SameLine();
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
|
|
{
|
|
ImGui.TextWrapped(FormatJsonPath(error.Path));
|
|
}
|
|
}
|
|
if (error.Messages.Count > 0)
|
|
{
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.Text((error.Messages.Count == 1) ? "Issue:" : "Issues:");
|
|
foreach (string message in error.Messages)
|
|
{
|
|
ImGui.Indent(12f);
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
|
{
|
|
string text2 = CleanJsonText(message);
|
|
if (string.Equals(text2, "validation failed", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
text2 = "JSON schema validation failed - check that all properties match the expected format and values";
|
|
}
|
|
ImU8String text = new ImU8String(2, 1);
|
|
text.AppendLiteral("\ufffd ");
|
|
text.AppendFormatted(text2);
|
|
ImGui.TextWrapped(text);
|
|
ImGui.Unindent(12f);
|
|
}
|
|
}
|
|
}
|
|
List<string> validationSuggestions = GetValidationSuggestions(error);
|
|
if (validationSuggestions.Count > 0)
|
|
{
|
|
ImGui.Spacing();
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
|
|
{
|
|
ImGui.Text("Suggestions:");
|
|
foreach (string item in validationSuggestions)
|
|
{
|
|
ImGui.Indent(12f);
|
|
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
|
|
{
|
|
ImGui.Text(FontAwesomeIcon.Lightbulb.ToIconString());
|
|
}
|
|
}
|
|
ImGui.SameLine();
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
|
|
{
|
|
ImGui.TextWrapped(item);
|
|
ImGui.Unindent(12f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ImGui.Unindent(12f);
|
|
}
|
|
|
|
private static void DrawSimpleJsonSchemaDetails(string description)
|
|
{
|
|
string[] array = description.Split('\n');
|
|
foreach (string text in array)
|
|
{
|
|
if (text.StartsWith("JSON Validation failed:", StringComparison.Ordinal))
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
|
{
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
else if (text.StartsWith(" - ", StringComparison.Ordinal))
|
|
{
|
|
int num = text.IndexOf(':', 3);
|
|
if (num > 0)
|
|
{
|
|
string path = text.Substring(3, num - 3).Trim();
|
|
string text2 = CleanJsonText(text.Substring(num + 1).Trim());
|
|
if (string.Equals(text2, "validation failed", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
text2 = "Schema validation failed - check property format and values";
|
|
}
|
|
ImGui.Text("\ufffd");
|
|
ImGui.SameLine();
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
|
|
{
|
|
ImGui.Text(FormatJsonPath(path));
|
|
}
|
|
ImGui.SameLine();
|
|
ImGui.Text(":");
|
|
ImGui.SameLine();
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
|
{
|
|
ImGui.TextWrapped(text2);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
|
{
|
|
ImGui.TextWrapped(CleanJsonText(text));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextWrapped(CleanJsonText(text));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static List<JsonValidationError> ParseJsonValidationErrors(string description)
|
|
{
|
|
List<JsonValidationError> list = new List<JsonValidationError>();
|
|
string[] array = description.Split('\n');
|
|
foreach (string text in array)
|
|
{
|
|
if (!text.StartsWith(" - ", StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
int num = text.IndexOf(':', 3);
|
|
if (num > 0)
|
|
{
|
|
string path = text.Substring(3, num - 3).Trim();
|
|
List<string> messages = (from m in text.Substring(num + 1).Trim().Split(';', StringSplitOptions.RemoveEmptyEntries)
|
|
select CleanJsonText(m.Trim()) into m
|
|
where !string.IsNullOrEmpty(m)
|
|
select m).ToList();
|
|
list.Add(new JsonValidationError
|
|
{
|
|
Path = path,
|
|
Messages = messages
|
|
});
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static string FormatJsonPath(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
return "<root>";
|
|
}
|
|
Match match = JsonPropertyPathRegex.Match(path);
|
|
if (match.Success)
|
|
{
|
|
string value = match.Groups[1].Value;
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
return "<root>";
|
|
}
|
|
return value.Replace('/', '.');
|
|
}
|
|
if (!(path == "<root>"))
|
|
{
|
|
return path;
|
|
}
|
|
return "<root>";
|
|
}
|
|
|
|
private static List<string> GetValidationSuggestions(JsonValidationError error)
|
|
{
|
|
List<string> list = new List<string>();
|
|
foreach (string message in error.Messages)
|
|
{
|
|
string text = message.ToUpperInvariant();
|
|
if (text.Contains("REQUIRED", StringComparison.Ordinal))
|
|
{
|
|
list.Add("Add the missing required property to your JSON.");
|
|
}
|
|
else if (text.Contains("TYPE", StringComparison.Ordinal))
|
|
{
|
|
list.Add("Check that the property value has the correct data type (string, number, boolean, etc.).");
|
|
}
|
|
else if (text.Contains("ENUM", StringComparison.Ordinal) || text.Contains("ALLOWED VALUES", StringComparison.Ordinal))
|
|
{
|
|
list.Add("Use one of the allowed enumeration values for this property.");
|
|
}
|
|
else if (text.Contains("FORMAT", StringComparison.Ordinal))
|
|
{
|
|
list.Add("Ensure the property value follows the expected format.");
|
|
}
|
|
else if (text.Contains("MINIMUM", StringComparison.Ordinal) || text.Contains("MAXIMUM", StringComparison.Ordinal))
|
|
{
|
|
list.Add("Check that numeric values are within the allowed range.");
|
|
}
|
|
else if (text.Contains("ADDITIONAL", StringComparison.Ordinal) && text.Contains("NOT ALLOWED", StringComparison.Ordinal))
|
|
{
|
|
list.Add("Remove any extra properties that are not defined in the schema.");
|
|
}
|
|
else if (text.Contains("VALIDATION FAILED", StringComparison.Ordinal))
|
|
{
|
|
list.Add("Review the JSON structure and ensure all properties match the expected schema format.");
|
|
}
|
|
}
|
|
return list.Distinct().ToList();
|
|
}
|
|
|
|
private static void DrawGenericDetails(string description)
|
|
{
|
|
string[] array = description.Split('\n');
|
|
for (int i = 0; i < array.Length; i++)
|
|
{
|
|
string text = array[i].Trim();
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
ImGui.Spacing();
|
|
}
|
|
else if (text.StartsWith("Error:", StringComparison.Ordinal) || text.StartsWith("Invalid", StringComparison.Ordinal) || text.StartsWith("Missing", StringComparison.Ordinal))
|
|
{
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
|
{
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
else if (text.Contains(':', StringComparison.Ordinal))
|
|
{
|
|
int num = text.IndexOf(':', StringComparison.Ordinal);
|
|
string text2 = text.Substring(0, num);
|
|
string text3 = text.Substring(num + 1).TrimStart();
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
|
|
{
|
|
ImGui.Text(text2 + ":");
|
|
}
|
|
ImGui.SameLine();
|
|
ImGui.TextWrapped(text3);
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextWrapped(text);
|
|
}
|
|
}
|
|
}
|
|
}
|