using System.Text.Json; using System.Text.Json.Serialization; namespace SVSim.BattleNode.Protocol; /// /// The shared envelope on every encrypted msg / synchronize frame. Body is opaque /// (Dictionary<string, object?>) because shape varies per Uri — typed register-action /// models come in a later slice. /// public sealed record MsgEnvelope( NetworkBattleUri Uri, long ViewerId, string Uuid, string? Bid, int Try, EmitCategory Cat, long? PubSeq, long? PlaySeq, Dictionary Body) { private static readonly JsonSerializerOptions Options = CreateOptions(); private static readonly HashSet ReservedEnvelopeKeys = new() { "uri", "viewerId", "uuid", "bid", "try", "cat", "pubSeq", "playSeq", }; private static JsonSerializerOptions CreateOptions() { var opt = new JsonSerializerOptions { // Wire-key casing here is bare camelCase — NOT EmulatedEntrypoint's snake_case policy. DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; opt.Converters.Add(new JsonStringEnumConverter()); return opt; } public static string ToJson(MsgEnvelope env) { var doc = new Dictionary { ["uri"] = env.Uri.ToString(), ["viewerId"] = env.ViewerId, ["uuid"] = env.Uuid, ["try"] = env.Try, ["cat"] = (int)env.Cat, }; if (env.Bid is not null) doc["bid"] = env.Bid; if (env.PubSeq.HasValue) doc["pubSeq"] = env.PubSeq.Value; if (env.PlaySeq.HasValue) doc["playSeq"] = env.PlaySeq.Value; foreach (var (k, v) in env.Body) { if (ReservedEnvelopeKeys.Contains(k)) throw new ArgumentException( $"Body key '{k}' collides with a reserved envelope field. Move it to a typed field on MsgEnvelope.", nameof(env)); doc[k] = v; } return JsonSerializer.Serialize(doc, Options); } public static MsgEnvelope FromJson(string json) { using var doc = JsonDocument.Parse(json); var root = doc.RootElement; var uri = Enum.Parse(root.GetProperty("uri").GetString()!); var viewerId = root.GetProperty("viewerId").GetInt64(); var uuid = root.GetProperty("uuid").GetString()!; var bid = root.TryGetProperty("bid", out var bidEl) ? bidEl.GetString() : null; var @try = root.TryGetProperty("try", out var tryEl) ? tryEl.GetInt32() : 0; var cat = root.TryGetProperty("cat", out var catEl) ? (EmitCategory)catEl.GetInt32() : EmitCategory.Battle; var pubSeq = root.TryGetProperty("pubSeq", out var psEl) ? psEl.GetInt64() : (long?)null; var playSeq = root.TryGetProperty("playSeq", out var plsEl) ? plsEl.GetInt64() : (long?)null; var body = new Dictionary(); foreach (var prop in root.EnumerateObject()) { if (ReservedEnvelopeKeys.Contains(prop.Name)) continue; body[prop.Name] = ToObject(prop.Value); } return new MsgEnvelope(uri, viewerId, uuid, bid, @try, cat, pubSeq, playSeq, body); } private static object? ToObject(JsonElement el) => el.ValueKind switch { JsonValueKind.String => el.GetString(), JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Null => null, JsonValueKind.Array => el.EnumerateArray().Select(ToObject).ToList(), JsonValueKind.Object => el.EnumerateObject().ToDictionary(p => p.Name, p => ToObject(p.Value)), _ => el.GetRawText(), }; }