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;
}