using System; using System.Collections.Generic; using System.Globalization; using System.Text; namespace InteractiveVideo { /// /// Minimal JSON reader (RFC 8259) with no dependencies. Exists because JsonUtility cannot read nested /// arrays such as "polygon": [[x, y], ...]. Produces plain objects: /// object -> Dictionary<string, object>, array -> List<object>, number -> double, /// string -> string, true/false -> bool, null -> null. /// Parsing happens once at load time, so clarity beats speed here. /// public static class LightweightJson { public static object Parse(string json) { if (json == null) throw new FormatException("JSON text is null."); var p = new Parser(json); p.SkipWhitespace(); object value = p.ReadValue(); p.SkipWhitespace(); if (!p.AtEnd) throw p.Error("Unexpected trailing content"); return value; } // ---- typed accessors used by the loader ---- public static Dictionary AsObject(object o) => o as Dictionary; public static List AsArray(object o) => o as List; public static bool TryGet(Dictionary obj, string key, out object value) { value = null; return obj != null && obj.TryGetValue(key, out value) && value != null; } public static string GetString(Dictionary obj, string key, string fallback = null) => TryGet(obj, key, out var v) ? (v is string s ? s : Convert.ToString(v, CultureInfo.InvariantCulture)) : fallback; public static double GetNumber(Dictionary obj, string key, double fallback = 0) => TryGet(obj, key, out var v) && v is double d ? d : fallback; public static bool GetBool(Dictionary obj, string key, bool fallback = false) => TryGet(obj, key, out var v) && v is bool b ? b : fallback; public static double ToNumber(object o, double fallback = 0) => o is double d ? d : fallback; private struct Parser { private readonly string _s; private int _i; public Parser(string s) { _s = s; _i = 0; } public bool AtEnd => _i >= _s.Length; public FormatException Error(string message) => new FormatException($"{message} at character {_i}."); public void SkipWhitespace() { while (_i < _s.Length) { char c = _s[_i]; if (c == ' ' || c == '\t' || c == '\n' || c == '\r') _i++; else break; } } public object ReadValue() { if (AtEnd) throw Error("Unexpected end of JSON"); char c = _s[_i]; switch (c) { case '{': return ReadObject(); case '[': return ReadArray(); case '"': return ReadString(); case 't': ExpectLiteral("true"); return true; case 'f': ExpectLiteral("false"); return false; case 'n': ExpectLiteral("null"); return null; default: if (c == '-' || (c >= '0' && c <= '9')) return ReadNumber(); throw Error($"Unexpected character '{c}'"); } } private Dictionary ReadObject() { var result = new Dictionary(); _i++; // { SkipWhitespace(); if (Peek() == '}') { _i++; return result; } while (true) { SkipWhitespace(); if (Peek() != '"') throw Error("Expected string key"); string key = ReadString(); SkipWhitespace(); if (Peek() != ':') throw Error("Expected ':'"); _i++; SkipWhitespace(); result[key] = ReadValue(); SkipWhitespace(); char c = Peek(); if (c == ',') { _i++; continue; } if (c == '}') { _i++; return result; } throw Error("Expected ',' or '}'"); } } private List ReadArray() { var result = new List(); _i++; // [ SkipWhitespace(); if (Peek() == ']') { _i++; return result; } while (true) { SkipWhitespace(); result.Add(ReadValue()); SkipWhitespace(); char c = Peek(); if (c == ',') { _i++; continue; } if (c == ']') { _i++; return result; } throw Error("Expected ',' or ']'"); } } private string ReadString() { _i++; // opening quote StringBuilder sb = null; int start = _i; while (true) { if (AtEnd) throw Error("Unterminated string"); char c = _s[_i]; if (c == '"') { string plain = _s.Substring(start, _i - start); _i++; return sb == null ? plain : sb.Append(plain).ToString(); } if (c == '\\') { sb ??= new StringBuilder(); sb.Append(_s, start, _i - start); _i++; if (AtEnd) throw Error("Unterminated escape"); char e = _s[_i++]; switch (e) { case '"': sb.Append('"'); break; case '\\': sb.Append('\\'); break; case '/': sb.Append('/'); break; case 'b': sb.Append('\b'); break; case 'f': sb.Append('\f'); break; case 'n': sb.Append('\n'); break; case 'r': sb.Append('\r'); break; case 't': sb.Append('\t'); break; case 'u': if (_i + 4 > _s.Length) throw Error("Bad unicode escape"); sb.Append((char)int.Parse(_s.Substring(_i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture)); _i += 4; break; default: throw Error($"Bad escape '\\{e}'"); } start = _i; continue; } _i++; } } private object ReadNumber() { int start = _i; if (Peek() == '-') _i++; while (!AtEnd) { char c = _s[_i]; if ((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') _i++; else break; } string text = _s.Substring(start, _i - start); if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) throw Error($"Invalid number '{text}'"); return value; } private void ExpectLiteral(string literal) { if (string.CompareOrdinal(_s, _i, literal, 0, literal.Length) != 0) throw Error($"Expected '{literal}'"); _i += literal.Length; } private char Peek() => AtEnd ? '\0' : _s[_i]; } } }