using System; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Web.Script.Serialization; namespace Server.Custom.Bridge { /// /// Outbound JSON is written by hand into a StringBuilder. It runs on the Core thread for /// every emitted event, and the measured budget in docs/PLAN.md assumes this cost, not a /// reflection serializer's. /// /// Inbound JSON is parsed with JavaScriptSerializer. Commands arrive at human rates, so /// correctness beats speed there, and parsing happens on the reader thread anyway. /// public static class BridgeJson { private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); [ThreadStatic] private static JavaScriptSerializer _parser; public static long NowMs() { return (long)(DateTime.UtcNow - Epoch).TotalMilliseconds; } // ---- outbound ---- /// Opens an object and writes the `t` and `kind` fields. public static StringBuilder Begin(string kind) { var sb = new StringBuilder(256); sb.Append("{\"t\":").Append(NowMs()); sb.Append(",\"kind\":\"").Append(kind).Append('"'); return sb; } public static StringBuilder Str(this StringBuilder sb, string name, string value) { sb.Append(",\"").Append(name).Append("\":"); if (value == null) sb.Append("null"); else Escape(sb, value); return sb; } public static StringBuilder Num(this StringBuilder sb, string name, long value) { sb.Append(",\"").Append(name).Append("\":").Append(value); return sb; } public static StringBuilder Num(this StringBuilder sb, string name, double value) { sb.Append(",\"").Append(name).Append("\":") .Append(value.ToString("R", CultureInfo.InvariantCulture)); return sb; } public static StringBuilder Bool(this StringBuilder sb, string name, bool value) { sb.Append(",\"").Append(name).Append("\":").Append(value ? "true" : "false"); return sb; } /// Serial as the canonical "0x1A2B" string the sidecar keys on. public static StringBuilder Ser(this StringBuilder sb, string name, Serial serial) { sb.Append(",\"").Append(name).Append("\":\"0x") .Append(serial.Value.ToString("X")).Append('"'); return sb; } /// Closes the object. The trailing newline is the frame delimiter. public static string End(this StringBuilder sb) { sb.Append('}'); return sb.ToString(); } public static void Escape(StringBuilder sb, string value) { sb.Append('"'); for (int i = 0; i < value.Length; i++) { char c = value[i]; switch (c) { case '"': sb.Append("\\\""); break; case '\\': sb.Append("\\\\"); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; case '\b': sb.Append("\\b"); break; case '\f': sb.Append("\\f"); break; default: if (c < ' ') sb.Append("\\u").Append(((int)c).ToString("x4")); else sb.Append(c); break; } } sb.Append('"'); } // ---- inbound ---- /// /// Parses one line into a dictionary. Returns null on malformed input rather than /// throwing: a bad line from the sidecar must never reach a game code path. /// public static Dictionary Parse(string line) { if (String.IsNullOrEmpty(line)) return null; try { if (_parser == null) { _parser = new JavaScriptSerializer(); _parser.MaxJsonLength = 1 << 20; } return _parser.Deserialize>(line); } catch { return null; } } public static string GetString(Dictionary o, string key) { object v; if (o == null || !o.TryGetValue(key, out v) || v == null) return null; return v as string ?? Convert.ToString(v, CultureInfo.InvariantCulture); } public static int GetInt(Dictionary o, string key, int fallback) { object v; if (o == null || !o.TryGetValue(key, out v) || v == null) return fallback; try { return Convert.ToInt32(v, CultureInfo.InvariantCulture); } catch { return fallback; } } } }