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 Messages { get; init; } = new List(); } private readonly QuestData _questData; private readonly IDalamudPluginInterface _pluginInterface; private readonly Dictionary _storedIssues = new Dictionary(); private readonly Dictionary _openDetailWindows = new Dictionary(); 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 list = new List(); foreach (KeyValuePair 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 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 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 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 ParseJsonValidationErrors(string description) { List list = new List(); 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 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 ""; } Match match = JsonPropertyPathRegex.Match(path); if (match.Success) { string value = match.Groups[1].Value; if (string.IsNullOrEmpty(value)) { return ""; } return value.Replace('/', '.'); } if (!(path == "")) { return path; } return ""; } private static List GetValidationSuggestions(JsonValidationError error) { List list = new List(); 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); } } } }