qstbak/Questionable/Questionable.Windows.QuestComponents/ValidationDetailsRenderer.cs
2025-10-09 07:53:51 +10:00

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);
}
}
}
}