using System; using System.Globalization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEngine; namespace ADOFAIRenderer.Data; /// /// Converts Unity and nullable to and from hex string (#RRGGBBAA). /// Pure C# implementation to completely avoid Unity internal native ECalls (such as Color.linear or ColorUtility.TryParseHtmlString). /// /// /// Reading accepts "#RRGGBB" (alpha 1), "#RRGGBBAA" (the '#' is optional), an object {"r","g","b","a"} with 0–1 floats /// (missing r/g/b = 0, missing a = 1), or null. Invalid or unsupported input reads as null for Color? and as /// default(Color) (all components 0) for Color; no exception is thrown for an unparsable string. /// public class ColorJsonConverter : JsonConverter { /// True for and . public override bool CanConvert(Type objectType) { return objectType == typeof(Color) || objectType == typeof(Color?); } /// Writes the color as "#RRGGBBAA" (see ), or a JSON null for a null value. public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { if (value == null) { writer.WriteNull(); return; } var color = (Color)value; writer.WriteValue(ToHex(color)); } /// Reads a color in one of the accepted forms (see the class remarks). public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { bool isNullable = Nullable.GetUnderlyingType(objectType) != null || !objectType.IsValueType; if (reader.TokenType == JsonToken.Null) { return isNullable ? null : (object)default(Color); } if (reader.TokenType == JsonToken.String) { var str = (string?)reader.Value; if (TryParseHex(str, out var parsedColor)) { return parsedColor; } return isNullable ? null : (object)default(Color); } if (reader.TokenType == JsonToken.StartObject) { var obj = JObject.Load(reader); float r = (float?)obj["r"] ?? 0f; float g = (float?)obj["g"] ?? 0f; float b = (float?)obj["b"] ?? 0f; float a = (float?)obj["a"] ?? 1f; return new Color(r, g, b, a); } return isNullable ? null : (object)default(Color); } /// /// Formats the color as "#RRGGBBAA" with uppercase hex digits. Each component is scaled to 0–255, rounded and clamped. /// The components are written as stored (no gamma conversion). /// public static string ToHex(Color color) { byte r = (byte)Math.Clamp((int)Math.Round(color.r * 255f), 0, 255); byte g = (byte)Math.Clamp((int)Math.Round(color.g * 255f), 0, 255); byte b = (byte)Math.Clamp((int)Math.Round(color.b * 255f), 0, 255); byte a = (byte)Math.Clamp((int)Math.Round(color.a * 255f), 0, 255); return $"#{r:X2}{g:X2}{b:X2}{a:X2}"; } /// /// Parses "RRGGBB" or "RRGGBBAA" (optional leading '#', surrounding white space ignored, case-insensitive). /// /// Text to parse. /// Parsed color (components / 255; alpha 1 for the 6-digit form), or default when parsing fails. /// True when the text was parsed. public static bool TryParseHex(string? hex, out Color color) { color = default; if (string.IsNullOrWhiteSpace(hex)) return false; var str = hex.Trim(); if (str.StartsWith("#")) { str = str.Substring(1); } if (str.Length == 6) { if (byte.TryParse(str.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var r) && byte.TryParse(str.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var g) && byte.TryParse(str.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b)) { color = new Color(r / 255f, g / 255f, b / 255f, 1f); return true; } } else if (str.Length == 8) { if (byte.TryParse(str.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var r) && byte.TryParse(str.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var g) && byte.TryParse(str.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b) && byte.TryParse(str.Substring(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var a)) { color = new Color(r / 255f, g / 255f, b / 255f, a / 255f); return true; } } return false; } }