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;
}
///
/// Writes a nested actor object: serial, name, account (when there is one), the linked
/// webId (when the account is linked), and the player flag. A `null` mobile writes null.
/// The richer counterpart to BridgeEvents' internal writer, used by the Part B streams so a
/// guild leader / joiner / governor can be attributed to a site user without a lookup.
///
public static StringBuilder Actor(this StringBuilder sb, string name, Mobile m)
{
sb.Append(",\"").Append(name).Append("\":");
if (m == null)
{
sb.Append("null");
return sb;
}
sb.Append("{\"serial\":\"0x").Append(m.Serial.Value.ToString("X")).Append('"');
sb.Append(",\"name\":");
Escape(sb, m.Name ?? "");
var acct = m.Account as Accounting.Account;
if (acct != null)
{
sb.Append(",\"acct\":");
Escape(sb, acct.Username);
var webId = BridgeAccountLink.WebIdFor(acct);
if (webId != null)
{
sb.Append(",\"webId\":");
Escape(sb, webId);
}
}
sb.Append(",\"player\":").Append(m.Player ? "true" : "false");
sb.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);
}
///
/// Extracts a JSON array of strings. JavaScriptSerializer materializes JSON arrays as
/// object[] (or ArrayList) when the target is object, so handle both and stringify each
/// element. Returns an empty list for a missing or non-array value, never null.
///
public static List GetStringList(Dictionary o, string key)
{
var result = new List();
object v;
if (o == null || !o.TryGetValue(key, out v) || v == null)
return result;
var enumerable = v as System.Collections.IEnumerable;
if (enumerable == null || v is string)
return result;
foreach (var item in enumerable)
{
if (item == null)
continue;
result.Add(item as string ?? Convert.ToString(item, CultureInfo.InvariantCulture));
}
return result;
}
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;
}
}
}
}