using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Robust.Shared.Utility; namespace Robust.Shared.Network; // Why did this dinky class grow to this LOC... /// /// Stores structured information about why a connection was denied or disconnected. /// /// /// /// The core networking layer (Lidgren) allows passing plain strings for disconnect reasons. /// We can beam a structured format (like JSON) over this, /// but Lidgren also generates messages internally (such as on timeout). /// This class is responsible for bridging the two to produce consistent results. /// /// /// Disconnect messages are just a simple key/value format. /// Valid value types are , , , and . /// /// public sealed class NetDisconnectMessage { private const string LidgrenDisconnectedPrefix = "Disconnected: "; /// /// The reason given if none was included in the structured message. /// internal const string DefaultReason = "unknown reason"; /// /// The default redial flag given if none was included in the structured message. /// internal const bool DefaultRedialFlag = false; /// /// The key of the value. /// public const string ReasonKey = "reason"; /// /// The key of the value. /// public const string RedialKey = "redial"; internal readonly Dictionary Values; internal NetDisconnectMessage(Dictionary values) { Values = values; } internal NetDisconnectMessage( string reason = DefaultReason, bool redialFlag = DefaultRedialFlag) { Values = new Dictionary { { ReasonKey, reason }, { RedialKey, redialFlag } }; } /// /// The human-readable reason for why the disconnection happened. /// /// public string Reason => StringOf(ReasonKey, DefaultReason); /// /// Whether the client should "redial" to reconnect to the server. /// /// /// Redial means the client gets restarted by the launcher, to enable an update to occur. /// This is generally set if the disconnection reason is some sort of version mismatch. /// /// public bool RedialFlag => BoolOf(RedialKey, DefaultRedialFlag); /// /// Decode from a disconnect message. /// /// /// /// If structured JSON can be extracted, it is used. /// Otherwise, or if the format is invalid, the entire input is returned as disconnect reason. /// /// Invalid JSON values (e.g. arrays) are discarded. /// /// The disconnect reason from Lidgren's disconnect message. internal static NetDisconnectMessage Decode(string text) { var start = text.AsMemory().TrimStart(); // Lidgren generates this prefix internally. if (start.Span.StartsWith(LidgrenDisconnectedPrefix)) start = start[LidgrenDisconnectedPrefix.Length..]; // If it starts with { it's probably a JSON object. if (start.Span.StartsWith("{")) { try { using var node = JsonDocument.Parse(start); DebugTools.Assert(node.RootElement.ValueKind == JsonValueKind.Object); return JsonToReason(node.RootElement); } catch (Exception) { // Discard the exception } } // Something went wrong. That probably means it's not a structured reason. // Or worst case scenario, some poor end-user has to look at half-broken JSON. return new NetDisconnectMessage(new Dictionary { { ReasonKey, text } }); } /// /// Encode to a textual string, that can be embedded into a disconnect message. /// internal string Encode() { return JsonSerializer.Serialize(Values); } private static NetDisconnectMessage JsonToReason(JsonElement obj) { DebugTools.Assert(obj.ValueKind == JsonValueKind.Object); var dict = new Dictionary(); foreach (var property in obj.EnumerateObject()) { object value; switch (property.Value.ValueKind) { case JsonValueKind.String: value = property.Value.GetString()!; break; case JsonValueKind.Number: if (property.Value.TryGetInt32(out var valueInt)) value = valueInt; else value = property.Value.GetSingle(); break; case JsonValueKind.True: case JsonValueKind.False: value = property.Value.GetBoolean(); break; default: // Discard invalid values intentionally. continue; } dict[property.Name] = value; } return new NetDisconnectMessage(dict); } /// /// Get a value by its key. /// /// The key of the value to look up. /// /// Null if no such value exists, otherwise an object of one of the valid types (int, float, string, bool). /// public object? ValueOf(string key) { return Values.GetValueOrDefault(key); } /// /// Get a value by its key. /// /// The key of the value to look up. /// Default value to return if the value does not exist or is the wrong type. /// /// The value with the given key, /// or if no such value exists or it's a different type. /// [return: NotNullIfNotNull(nameof(defaultValue))] public string? StringOf(string key, string? defaultValue = null) { if (ValueOf(key) is not string valueString) return defaultValue; return valueString; } /// /// Get a value by its key. /// /// The key of the value to look up. /// /// The value with the given key, or if no such value exists or it's a different type. /// public bool? BoolOf(string key) => ValueOf(key) as bool?; /// /// Get a value by its key. /// /// The key of the value to look up. /// Default value to return if the value does not exist or is the wrong type. /// /// The value with the given key, /// or if no such value exists or it's a different type. /// public bool BoolOf(string key, bool defaultValue) => BoolOf(key) ?? defaultValue; /// /// Get a value by its key. /// /// The key of the value to look up. /// /// The value with the given key, or if no such value exists or it's a different type. /// public int? Int32Of(string key) => ValueOf(key) as int?; /// /// Get an value by its key. /// /// The key of the value to look up. /// Default value to return if the value does not exist or is the wrong type. /// /// The value with the given key, /// or if no such value exists or it's a different type. /// public int Int32Of(string key, int defaultValue) => Int32Of(key) ?? defaultValue; /// /// Get a value by its key. /// /// The key of the value to look up. /// /// The value with the given key, or if no such value exists or it's a different type. /// public float? SingleOf(string key) { var value = ValueOf(key); return value as float? ?? value as int?; } /// /// Get a value by its key. /// /// The key of the value to look up. /// Default value to return if the value does not exist or is the wrong type. /// /// The value with the given key, /// or if no such value exists or it's a different type. /// public float SingleOf(string key, float defaultValue) => SingleOf(key) ?? defaultValue; }