Extracted docs/ (ADMIN_CONTROLS, INTEGRATION, PLAN, PROTOCOL_2, RESEARCH, SHARD_PREREQS) into the central RunicGateway/docs repo under link/, with full commit history preserved via git filter-repo. The source cites these design docs by section throughout, so every in-repo reference (C# + Rust comments, Bridge.cfg, and the READMEs) is repointed at the new docs-repo URL. README references are rendered as markdown links; a Documentation pointer section is added to the top-level README. Docs repo: https://gitea.whitlocktech.com/RunicGateway/docs
239 lines
7.8 KiB
C#
239 lines
7.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Web.Script.Serialization;
|
|
|
|
namespace Server.Custom.Bridge
|
|
{
|
|
/// <summary>
|
|
/// Outbound JSON is written by hand into a StringBuilder. It runs on the Core thread for
|
|
/// every emitted event, and the measured budget in https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/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.
|
|
/// </summary>
|
|
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 ----
|
|
|
|
/// <summary>Opens an object and writes the `t` and `kind` fields.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Serial as the canonical "0x1A2B" string the sidecar keys on.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Closes the object. The trailing newline is the frame delimiter.</summary>
|
|
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 ----
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static Dictionary<string, object> Parse(string line)
|
|
{
|
|
if (String.IsNullOrEmpty(line))
|
|
return null;
|
|
|
|
try
|
|
{
|
|
if (_parser == null)
|
|
{
|
|
_parser = new JavaScriptSerializer();
|
|
_parser.MaxJsonLength = 1 << 20;
|
|
}
|
|
|
|
return _parser.Deserialize<Dictionary<string, object>>(line);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static string GetString(Dictionary<string, object> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static List<string> GetStringList(Dictionary<string, object> o, string key)
|
|
{
|
|
var result = new List<string>();
|
|
|
|
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<string, object> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|