using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Net.WebSockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using ValheimRelay.Core.Election; using ValheimRelay.Core.Identity; using ValheimRelay.Core.Json; using ValheimRelay.Core.Protocol; using ValheimRelay.Core.Session; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("ValheimRelay.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+72cd49b324620f005e0fd406d82cf7ff2dce1dd1")] [assembly: AssemblyProduct("ValheimRelay.Core")] [assembly: AssemblyTitle("ValheimRelay.Core")] [assembly: InternalsVisibleTo("ValheimRelay.Core.Tests")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimRelay.Core.Session { public interface IClock { TimeSpan Elapsed { get; } long UnixTimeMilliseconds { get; } } public enum LogLevel { Debug, Info, Warning, Error } public interface ILog { void Log(LogLevel level, string message); } public static class LogExtensions { public static void Debug(this ILog log, string message) { log.Log(LogLevel.Debug, message); } public static void Info(this ILog log, string message) { log.Log(LogLevel.Info, message); } public static void Warn(this ILog log, string message) { log.Log(LogLevel.Warning, message); } public static void Error(this ILog log, string message) { log.Log(LogLevel.Error, message); } } public enum TransportState { Closed, Connecting, Open } public interface IRelayTransport { TransportState State { get; } event Action? Opened; event Action? Received; event Action? Closed; void Connect(string relayUrl, string? code, string? token); bool Send(string frame); void Close(int code, string reason); } public interface IGameChannel { bool IsReady { get; } event Action? CodeAnnounced; event Action? CodeRequested; void RequestCode(); void AnnounceCode(string code, long epoch); } public readonly struct CodeAnnouncement { public string Code { get; } public long Epoch { get; } public long SenderPeerId { get; } public CodeAnnouncement(string code, long epoch, long senderPeerId) { Code = code; Epoch = epoch; SenderPeerId = senderPeerId; } } public interface IPeerView { bool IsHost { get; } long SelfPeerId { get; } IReadOnlyList PeerIds { get; } } public sealed class Backoff { private readonly double _baseSeconds; private readonly double _capSeconds; private readonly double _jitterFraction; private readonly Func _random; private int _attempt; public int Attempt => _attempt; public Backoff(double baseSeconds = 1.0, double capSeconds = 30.0, double jitterFraction = 0.25, Func? random = null) { if (baseSeconds <= 0.0) { throw new ArgumentOutOfRangeException("baseSeconds"); } if (capSeconds < baseSeconds) { throw new ArgumentOutOfRangeException("capSeconds"); } if (jitterFraction < 0.0 || jitterFraction > 1.0) { throw new ArgumentOutOfRangeException("jitterFraction"); } _baseSeconds = baseSeconds; _capSeconds = capSeconds; _jitterFraction = jitterFraction; _random = random ?? new Func(SharedRandom.NextDouble); } public void Reset() { _attempt = 0; } public TimeSpan Next() { double num = _baseSeconds * Math.Pow(2.0, _attempt); if (num > _capSeconds || double.IsInfinity(num)) { num = _capSeconds; } if (_attempt < 30) { _attempt++; } double num2 = num * _jitterFraction; double num3 = num - num2 + _random() * num2 * 2.0; if (num3 < 0.0) { num3 = 0.0; } return TimeSpan.FromSeconds(num3); } public static Backoff ForRelayFull(Func? random = null) { return new Backoff(5.0, 120.0, 0.5, random); } } internal static class SharedRandom { [ThreadStatic] private static Random? _random; public static double NextDouble() { if (_random == null) { _random = new Random(Environment.TickCount ^ (Thread.CurrentThread.ManagedThreadId * 7919)); } return _random.NextDouble(); } } public sealed class ClientWebSocketTransport : IRelayTransport, IDisposable { private readonly ILog _log; private readonly int _sendQueueCapacity; private readonly object _gate = new object(); private ClientWebSocket? _socket; private CancellationTokenSource? _cancellation; private BlockingCollection? _sendQueue; private int _generation; private bool _disposed; public TransportState State { get; private set; } public event Action? Opened; public event Action? Received; public event Action? Closed; public ClientWebSocketTransport(ILog log, int sendQueueCapacity = 256) { _log = log ?? throw new ArgumentNullException("log"); _sendQueueCapacity = sendQueueCapacity; } public void Connect(string relayUrl, string? code, string? token) { if (_disposed) { throw new ObjectDisposedException("ClientWebSocketTransport"); } AbandonCurrent(); Uri uri = BuildUri(relayUrl, code, token); ClientWebSocket socket = new ClientWebSocket(); CancellationTokenSource cancellation = new CancellationTokenSource(); BlockingCollection queue = new BlockingCollection(new ConcurrentQueue(), _sendQueueCapacity); int generation; lock (_gate) { _socket = socket; _cancellation = cancellation; _sendQueue = queue; generation = ++_generation; State = TransportState.Connecting; } Task.Run(() => RunAsync(socket, queue, cancellation, uri, generation)); } internal static Uri BuildUri(string relayUrl, string? code, string? token) { if (string.IsNullOrEmpty(relayUrl)) { throw new ArgumentException("relay URL required", "relayUrl"); } UriBuilder uriBuilder = new UriBuilder(relayUrl); if (uriBuilder.Scheme == Uri.UriSchemeHttp) { uriBuilder.Scheme = "ws"; } else if (uriBuilder.Scheme == Uri.UriSchemeHttps) { uriBuilder.Scheme = "wss"; } StringBuilder query = new StringBuilder(uriBuilder.Query.TrimStart(new char[1] { '?' })); Append("role", "mod"); if (!string.IsNullOrEmpty(code)) { Append("code", code); } if (!string.IsNullOrEmpty(token)) { Append("token", token); } uriBuilder.Query = query.ToString(); return uriBuilder.Uri; void Append(string name, string value) { if (query.Length > 0) { query.Append('&'); } query.Append(name).Append('=').Append(Uri.EscapeDataString(value)); } } public bool Send(string frame) { BlockingCollection sendQueue = _sendQueue; if (sendQueue == null || State != TransportState.Open) { return false; } try { return sendQueue.TryAdd(frame); } catch (ObjectDisposedException) { return false; } catch (InvalidOperationException) { return false; } } public void Close(int code, string reason) { AbandonCurrent(); SetClosed(code, reason); } private async Task RunAsync(ClientWebSocket socket, BlockingCollection queue, CancellationTokenSource cancellation, Uri uri, int generation) { int closeCode = 1000; string closeReason = string.Empty; try { await socket.ConnectAsync(uri, cancellation.Token).ConfigureAwait(continueOnCapturedContext: false); if (!IsCurrent(generation)) { return; } State = TransportState.Open; this.Opened?.Invoke(); Task sender = Task.Run(() => SendLoopAsync(socket, queue, cancellation.Token)); await ReceiveLoopAsync(socket, cancellation, generation).ConfigureAwait(continueOnCapturedContext: false); cancellation.Cancel(); await sender.ConfigureAwait(continueOnCapturedContext: false); if (socket.CloseStatus.HasValue) { closeCode = (int)socket.CloseStatus.Value; closeReason = socket.CloseStatusDescription ?? string.Empty; } } catch (OperationCanceledException) { return; } catch (WebSocketException ex2) { closeCode = 1006; closeReason = ex2.Message; } catch (Exception ex3) { closeCode = 1006; closeReason = ex3.Message; _log.Warn("relay transport error: " + ex3.Message); } finally { queue.CompleteAdding(); socket.Dispose(); } if (IsCurrent(generation)) { SetClosed(closeCode, closeReason); } } private async Task ReceiveLoopAsync(ClientWebSocket socket, CancellationTokenSource cancellation, int generation) { byte[] buffer = new byte[8192]; StringBuilder assembled = new StringBuilder(); while (socket.State == WebSocketState.Open && !cancellation.IsCancellationRequested) { ArraySegment buffer2 = new ArraySegment(buffer); WebSocketReceiveResult webSocketReceiveResult = await socket.ReceiveAsync(buffer2, cancellation.Token).ConfigureAwait(continueOnCapturedContext: false); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Close) { await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(continueOnCapturedContext: false); break; } if (webSocketReceiveResult.MessageType != WebSocketMessageType.Text) { continue; } assembled.Append(Encoding.UTF8.GetString(buffer, 0, webSocketReceiveResult.Count)); if (assembled.Length > 16384) { _log.Warn("inbound frame exceeded the size cap; dropping the connection"); await socket.CloseOutputAsync(WebSocketCloseStatus.MessageTooBig, "frame too large", CancellationToken.None).ConfigureAwait(continueOnCapturedContext: false); break; } if (webSocketReceiveResult.EndOfMessage) { string obj = assembled.ToString(); assembled.Length = 0; if (IsCurrent(generation)) { this.Received?.Invoke(obj); } } } } private static async Task SendLoopAsync(ClientWebSocket socket, BlockingCollection queue, CancellationToken token) { try { foreach (string item in queue.GetConsumingEnumerable(token)) { if (socket.State != WebSocketState.Open) { return; } byte[] bytes = Encoding.UTF8.GetBytes(item); await socket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, endOfMessage: true, token).ConfigureAwait(continueOnCapturedContext: false); } } catch (OperationCanceledException) { } catch (WebSocketException) { } catch (ObjectDisposedException) { } } private bool IsCurrent(int generation) { lock (_gate) { return _generation == generation; } } private void AbandonCurrent() { ClientWebSocket socket; CancellationTokenSource cancellation; BlockingCollection sendQueue; lock (_gate) { socket = _socket; cancellation = _cancellation; sendQueue = _sendQueue; _socket = null; _cancellation = null; _sendQueue = null; _generation++; } try { cancellation?.Cancel(); } catch (ObjectDisposedException) { } try { sendQueue?.CompleteAdding(); } catch (ObjectDisposedException) { } if (socket != null && socket.State == WebSocketState.Open) { try { socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } catch (Exception) { } } } private void SetClosed(int code, string reason) { if (State != TransportState.Closed) { State = TransportState.Closed; this.Closed?.Invoke(code, reason); } } public void Dispose() { if (!_disposed) { _disposed = true; AbandonCurrent(); State = TransportState.Closed; } } } public static class MapLink { public const string Default = "https://bobmitch.com/valheim"; public static string Normalise(string? raw) { string text = (raw ?? string.Empty).Trim(); if (text.Length == 0) { return string.Empty; } if (StartsWith(text, "wss://")) { text = "https://" + text.Substring("wss://".Length); } else if (StartsWith(text, "ws://")) { text = "http://" + text.Substring("ws://".Length); } else if (!StartsWith(text, "http://") && !StartsWith(text, "https://")) { text = "https://" + text; } if (!Uri.TryCreate(text, UriKind.Absolute, out Uri result)) { return string.Empty; } if (string.IsNullOrEmpty(result.Host)) { return string.Empty; } return result.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped); } public static string Build(string? mapUrl, string code, string? seed = null) { if (string.IsNullOrEmpty(code)) { return string.Empty; } string text = Normalise(mapUrl); if (text.Length == 0) { return code; } int num = text.IndexOf('?'); string text2 = ((num >= 0) ? text.Substring(0, num) : text); string text3 = ((num >= 0) ? text.Substring(num + 1) : string.Empty); text2 = text2.TrimEnd(new char[1] { '/' }); if (!HasPath(text)) { text2 += "/"; } if (!string.IsNullOrEmpty(seed)) { if (text3.Length > 0) { text3 += "&"; } text3 = text3 + "seed=" + Uri.EscapeDataString(seed); } return ((text3.Length > 0) ? (text2 + "?" + text3) : text2) + "#" + Uri.EscapeDataString(code); } private static bool HasPath(string url) { if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result)) { return false; } return result.AbsolutePath.Trim(new char[1] { '/' }).Length > 0; } private static bool StartsWith(string value, string prefix) { return value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); } } public sealed class MarkerStore { public const int MaxOwnedMarkers = 64; private readonly object _gate = new object(); private readonly Dictionary _owned = new Dictionary(StringComparer.Ordinal); private readonly List _order = new List(); public int Count { get { lock (_gate) { return _owned.Count; } } } public bool Add(MarkerFrame marker) { if (marker == null) { throw new ArgumentNullException("marker"); } if (!marker.IsAdd) { throw new ArgumentException("expected an add", "marker"); } lock (_gate) { if (_owned.ContainsKey(marker.Id)) { _owned[marker.Id] = marker; return true; } if (_owned.Count >= 64) { return false; } _owned[marker.Id] = marker; _order.Add(marker.Id); return true; } } public bool Remove(string id) { if (string.IsNullOrEmpty(id)) { return false; } lock (_gate) { if (!_owned.Remove(id)) { return false; } _order.Remove(id); return true; } } public IReadOnlyList Snapshot() { lock (_gate) { List list = new List(_order.Count); foreach (string item in _order) { if (_owned.TryGetValue(item, out MarkerFrame value)) { list.Add(value); } } return list; } } public void Clear() { lock (_gate) { _owned.Clear(); _order.Clear(); } } public static string NewId(string ownerUid, int sequence) { return ownerUid + ":m" + sequence.ToString(CultureInfo.InvariantCulture); } } public sealed class OutboundQueue { private readonly object _gate = new object(); private readonly Queue _reliable = new Queue(); private readonly int _reliableCapacity; private string? _latestPosition; private string? _peeked; private bool _peekedFromReliable; public int DroppedReliable { get; private set; } public int SupersededPositions { get; private set; } public int Count { get { lock (_gate) { return _reliable.Count + ((_latestPosition != null) ? 1 : 0); } } } public OutboundQueue(int reliableCapacity = 64) { if (reliableCapacity < 1) { throw new ArgumentOutOfRangeException("reliableCapacity"); } _reliableCapacity = reliableCapacity; } public bool EnqueueReliable(string frame) { if (frame == null) { throw new ArgumentNullException("frame"); } lock (_gate) { if (_reliable.Count >= _reliableCapacity) { DroppedReliable++; return false; } _reliable.Enqueue(frame); return true; } } public void SetPosition(string frame) { if (frame == null) { throw new ArgumentNullException("frame"); } lock (_gate) { if (_latestPosition != null) { SupersededPositions++; } _latestPosition = frame; } } public bool TryPeek(out string frame) { lock (_gate) { if (_reliable.Count > 0) { _peeked = _reliable.Peek(); _peekedFromReliable = true; frame = _peeked; return true; } if (_latestPosition != null) { _peeked = _latestPosition; _peekedFromReliable = false; frame = _peeked; return true; } _peeked = null; } frame = string.Empty; return false; } public void CommitPeek() { lock (_gate) { if (_peeked == null) { return; } if (_peekedFromReliable) { if (_reliable.Count > 0 && (object)_reliable.Peek() == _peeked) { _reliable.Dequeue(); } } else if ((object)_latestPosition == _peeked) { _latestPosition = null; } _peeked = null; } } public bool TryDequeue(out string frame) { if (!TryPeek(out frame)) { return false; } CommitPeek(); return true; } public void Clear() { lock (_gate) { _reliable.Clear(); _latestPosition = null; _peeked = null; } } } public sealed class PingEcho { private readonly struct Seen { public double X { get; } public double Z { get; } public TimeSpan At { get; } public Seen(double x, double z, TimeSpan at) { X = x; Z = z; At = at; } } public static readonly TimeSpan DefaultWindow = TimeSpan.FromSeconds(8.0); public const double DefaultMatchRadius = 2.0; private const int MaxTracked = 32; private readonly List _seen = new List(); private readonly TimeSpan _window; private readonly double _radiusSquared; public int Tracked => _seen.Count; public PingEcho(TimeSpan? window = null, double matchRadius = 2.0) { _window = window ?? DefaultWindow; _radiusSquared = matchRadius * matchRadius; } public void Observe(double x, double z, TimeSpan now) { Prune(now); if (_seen.Count >= 32) { _seen.RemoveAt(0); } _seen.Add(new Seen(x, z, now)); } public bool ShouldSuppress(double x, double z, TimeSpan now) { Prune(now); for (int num = _seen.Count - 1; num >= 0; num--) { double num2 = _seen[num].X - x; double num3 = _seen[num].Z - z; if (!(num2 * num2 + num3 * num3 > _radiusSquared)) { _seen.RemoveAt(num); return true; } } return false; } public void Clear() { _seen.Clear(); } private void Prune(TimeSpan now) { for (int num = _seen.Count - 1; num >= 0; num--) { TimeSpan timeSpan = now - _seen[num].At; if (timeSpan >= _window || timeSpan < TimeSpan.Zero) { _seen.RemoveAt(num); } } } } public sealed class PositionThrottle { private readonly SessionOptions _options; private PositionSample _last; private TimeSpan _lastSentAt; private bool _hasSent; public PositionThrottle(SessionOptions options) { _options = options ?? throw new ArgumentNullException("options"); } public void Reset() { _hasSent = false; _lastSentAt = TimeSpan.Zero; } public bool ShouldSend(in PositionSample sample, TimeSpan now) { if (!_hasSent) { return true; } if (sample.Dead != _last.Dead) { return true; } if (sample.IncludeHealth && _last.IncludeHealth && sample.Health != _last.Health) { return true; } if (!string.Equals(sample.Biome, _last.Biome, StringComparison.Ordinal)) { return true; } if (now - _lastSentAt >= _options.PositionKeepalive) { return true; } double positionMinMetres = _options.PositionMinMetres; if (sample.HorizontalDistanceSquaredTo(in _last) >= positionMinMetres * positionMinMetres) { return true; } return Math.Abs(AngleDelta(sample.RotationDegrees, _last.RotationDegrees)) >= _options.PositionMinRotationDegrees; } public void MarkSent(in PositionSample sample, TimeSpan now) { _last = sample; _lastSentAt = now; _hasSent = true; } internal static double AngleDelta(double a, double b) { double num = (a - b) % 360.0; if (num > 180.0) { num -= 360.0; } if (num < -180.0) { num += 360.0; } return num; } } public sealed class ReclaimEntry { public string Code { get; } public string Token { get; } public long Epoch { get; } public long SavedAtUnixMs { get; } public ReclaimEntry(string code, string token, long epoch, long savedAtUnixMs) { Code = code; Token = token; Epoch = epoch; SavedAtUnixMs = savedAtUnixMs; } } public interface IReclaimStorage { string? Read(); void Write(string contents); } public sealed class ReclaimStore { private const int CurrentVersion = 1; private readonly IReclaimStorage _storage; private readonly ILog _log; private readonly Dictionary _entries = new Dictionary(StringComparer.Ordinal); private string? _salt; private bool _loaded; public string Salt { get { EnsureLoaded(); if (_salt == null || !StableUid.TryDecodeSalt(_salt, out byte[] _)) { if (_salt != null) { _log.Warn("the stored identity salt was unusable; generating a new one"); } _salt = StableUid.EncodeSalt(StableUid.NewSalt()); Save(); } return _salt; } } public ReclaimStore(IReclaimStorage storage, ILog log) { _storage = storage ?? throw new ArgumentNullException("storage"); _log = log ?? throw new ArgumentNullException("log"); } public ReclaimEntry? Get(string worldUid) { if (string.IsNullOrEmpty(worldUid)) { return null; } EnsureLoaded(); if (!_entries.TryGetValue(worldUid, out ReclaimEntry value)) { return null; } return value; } public void Put(string worldUid, ReclaimEntry entry) { if (!string.IsNullOrEmpty(worldUid)) { EnsureLoaded(); _entries[worldUid] = entry ?? throw new ArgumentNullException("entry"); Save(); } } public void Forget(string worldUid) { if (!string.IsNullOrEmpty(worldUid)) { EnsureLoaded(); if (_entries.Remove(worldUid)) { Save(); } } } private void EnsureLoaded() { if (_loaded) { return; } _loaded = true; string text; try { text = _storage.Read(); } catch (Exception ex) { _log.Warn("could not read reclaim store: " + ex.Message); return; } if (string.IsNullOrEmpty(text)) { return; } if (!JsonParser.TryParse(text, out JsonValue value) || value.Kind != JsonKind.Object) { _log.Warn("reclaim store is not valid JSON; starting fresh"); return; } _salt = value["salt"].AsString(); JsonValue jsonValue = value["worlds"]; if (jsonValue.Kind != JsonKind.Object) { return; } foreach (string key in jsonValue.Keys) { JsonValue jsonValue2 = jsonValue[key]; if (jsonValue2.Kind == JsonKind.Object) { string text2 = jsonValue2["code"].AsString(); string text3 = jsonValue2["token"].AsString(); if (!string.IsNullOrEmpty(text2) && !string.IsNullOrEmpty(text3)) { _entries[key] = new ReclaimEntry(text2, text3, jsonValue2["epoch"].AsLong(0L), jsonValue2["savedAt"].AsLong(0L)); } } } } private void Save() { JsonWriter jsonWriter = new JsonWriter(); jsonWriter.BeginObject().Prop("version", 1L); if (_salt != null) { jsonWriter.Prop("salt", _salt); } jsonWriter.Name("worlds").BeginObject(); foreach (KeyValuePair entry in _entries) { jsonWriter.Name(entry.Key).BeginObject().Prop("code", entry.Value.Code) .Prop("token", entry.Value.Token) .Prop("epoch", entry.Value.Epoch) .Prop("savedAt", entry.Value.SavedAtUnixMs) .EndObject(); } jsonWriter.EndObject(); try { _storage.Write(jsonWriter.EndObject().ToString()); } catch (Exception ex) { _log.Warn("could not write reclaim store: " + ex.Message); } } } public sealed class SessionIdentity { public string PlayerName { get; } public string Uid { get; } public string ModVersion { get; } public WorldInfo World { get; } public SessionIdentity(string playerName, string uid, string modVersion, WorldInfo world) { PlayerName = playerName; Uid = uid; ModVersion = modVersion; World = world; } } public sealed class RelaySession : IDisposable { private enum SocketEventKind { Opened, Received, Closed } private readonly struct SocketEvent { public SocketEventKind Kind { get; } public string Text { get; } public int Code { get; } public SocketEvent(SocketEventKind kind, string text, int code) { Kind = kind; Text = text; Code = code; } } private readonly SessionOptions _options; private readonly IRelayTransport _transport; private readonly IGameChannel _gameChannel; private readonly IPeerView _peers; private readonly IClock _clock; private readonly ILog _log; private readonly ReclaimStore _reclaim; private readonly CodeArbiter _arbiter; private readonly OutboundQueue _outbound; private readonly PositionThrottle _throttle; private readonly MarkerStore _markers = new MarkerStore(); private readonly Backoff _backoff; private readonly Backoff _relayFullBackoff; private readonly ConcurrentQueue _socketEvents = new ConcurrentQueue(); private readonly ConcurrentQueue _announcements = new ConcurrentQueue(); private int _codeRequests; private SessionIdentity? _identity; private SessionState _state; private bool _disposed; private TimeSpan _stateEnteredAt; private TimeSpan _retryAt; private TimeSpan _lastDiscoveryAskAt; private TimeSpan _lastAnnounceAt; private TimeSpan _lastHelloAt; private TimeSpan _lastPositionAt; private TimeSpan _connectionOpenedAt; private TimeSpan _lastStateReplayAt; private bool _stateReplayPending; private bool _healthyResetDone; private int _deliberateCloses; private string? _pendingCode; private string? _pendingToken; private long _pendingEpoch; private string? _activeCode; private long _activeEpoch; private string? _codeShownToPlayer; private bool _isCreator; private int _markerSequence; public SessionState State => _state; public string? Code => _activeCode; public bool IsCreator => _isCreator; public int PeerCount { get; private set; } public OutboundQueue Outbound => _outbound; public MarkerStore Markers => _markers; public event Action? StateChanged; public event Action? Notice; public event Action? PingReceived; public event Action? MarkerReceived; public RelaySession(SessionOptions options, IRelayTransport transport, IGameChannel gameChannel, IPeerView peers, IClock clock, ILog log, ReclaimStore reclaim, Func? random = null) { _options = options ?? throw new ArgumentNullException("options"); _transport = transport ?? throw new ArgumentNullException("transport"); _gameChannel = gameChannel ?? throw new ArgumentNullException("gameChannel"); _peers = peers ?? throw new ArgumentNullException("peers"); _clock = clock ?? throw new ArgumentNullException("clock"); _log = log ?? throw new ArgumentNullException("log"); _reclaim = reclaim ?? throw new ArgumentNullException("reclaim"); _options.Normalise(); _arbiter = new CodeArbiter(clock); _outbound = new OutboundQueue(_options.OutboundReliableCapacity); _throttle = new PositionThrottle(_options); _backoff = new Backoff(1.0, 30.0, 0.25, random); _relayFullBackoff = Backoff.ForRelayFull(random); _transport.Opened += OnTransportOpened; _transport.Received += OnTransportReceived; _transport.Closed += OnTransportClosed; _gameChannel.CodeAnnounced += OnCodeAnnounced; _gameChannel.CodeRequested += OnCodeRequested; } public void Start(SessionIdentity identity) { _identity = identity ?? throw new ArgumentNullException("identity"); _markers.Clear(); _markerSequence = 0; _codeShownToPlayer = null; _deliberateCloses = 0; _arbiter.ClearCurrent(); _healthyResetDone = false; _backoff.Reset(); _relayFullBackoff.Reset(); EnterDiscovering(); } public void Stop(string reason = "left the world") { if (_state == SessionState.Stopped || _state == SessionState.Idle) { _state = SessionState.Stopped; return; } CloseTransport(1000, reason, expectClose: false); _outbound.Clear(); _markers.Clear(); _activeCode = null; _codeShownToPlayer = null; _isCreator = false; SetState(SessionState.Stopped); Raise(new SessionNotice(NoticeKind.Stopped, "session ended: " + reason)); } public void Retry() { if (_state == SessionState.Blocked) { _backoff.Reset(); EnterDiscovering(); } } public void Dispose() { if (!_disposed) { _disposed = true; _transport.Opened -= OnTransportOpened; _transport.Received -= OnTransportReceived; _transport.Closed -= OnTransportClosed; _gameChannel.CodeAnnounced -= OnCodeAnnounced; _gameChannel.CodeRequested -= OnCodeRequested; } } public void Tick() { DrainSocketEvents(); DrainAnnouncements(); DrainCodeRequests(); switch (_state) { case SessionState.Discovering: TickDiscovering(); break; case SessionState.Active: TickActive(); break; case SessionState.Creating: case SessionState.Joining: TickConnecting(); break; case SessionState.Reconnecting: TickReconnecting(); break; } PumpOutbound(); } private void TickDiscovering() { TimeSpan elapsed = _clock.Elapsed; if (elapsed - _lastDiscoveryAskAt >= _options.DiscoveryRetryInterval) { _lastDiscoveryAskAt = elapsed; if (_gameChannel.IsReady) { _gameChannel.RequestCode(); } } if (elapsed - _stateEnteredAt < _options.DiscoveryWindow) { return; } string text = _identity?.World.Uid; ReclaimEntry reclaimEntry = ((text != null) ? _reclaim.Get(text) : null); if (!CreatorElection.IsElectedCreator(_peers)) { return; } TimeSpan timeSpan = CreatorElection.CreationStagger(_peers, _options.CreationStaggerSpread); if (!(elapsed - _stateEnteredAt < _options.DiscoveryWindow + timeSpan)) { if (reclaimEntry != null) { BeginConnect(reclaimEntry.Code, reclaimEntry.Token, reclaimEntry.Epoch, SessionState.Joining); } else { BeginConnect(null, null, _arbiter.NextEpoch(), SessionState.Creating); } } } private void TickActive() { TimeSpan elapsed = _clock.Elapsed; if (!_healthyResetDone && elapsed - _connectionOpenedAt >= _options.HealthyConnectionThreshold) { _backoff.Reset(); _relayFullBackoff.Reset(); _healthyResetDone = true; } if (elapsed - _lastHelloAt >= _options.HelloInterval) { SendHello(); } if (_isCreator && _activeCode != null && elapsed - _lastAnnounceAt >= _options.CodeAnnounceInterval) { AnnounceCode(); } if (_stateReplayPending && elapsed - _lastStateReplayAt >= _options.RequestStateCooldown) { ReplayState(); } } private void TickConnecting() { if (!(_clock.Elapsed - _stateEnteredAt < _options.ConnectTimeout)) { _log.Warn("no welcome within " + _options.ConnectTimeout.TotalSeconds + "s; retrying"); CloseTransport(1000, "connect timed out"); ScheduleRetry(_backoff.Next()); Raise(new SessionNotice(NoticeKind.Reconnecting, "reconnecting to the relay")); } } private void TickReconnecting() { if (!(_clock.Elapsed < _retryAt)) { BeginConnect(_pendingCode, _pendingToken, _pendingEpoch, (_pendingCode == null) ? SessionState.Creating : SessionState.Joining); } } private void PumpOutbound() { if (_transport.State == TransportState.Open) { string frame; while (_outbound.TryPeek(out frame) && _transport.Send(frame)) { _outbound.CommitPeek(); } } } public void SubmitPosition(in PositionSample sample) { if (_state == SessionState.Active && _options.SharePosition) { TimeSpan elapsed = _clock.Elapsed; if (!(elapsed - _lastPositionAt < _options.PositionInterval) && _throttle.ShouldSend(in sample, elapsed)) { _lastPositionAt = elapsed; _throttle.MarkSent(in sample, elapsed); _outbound.SetPosition(FrameCodec.WritePosition(in sample)); } } } public void SendPing(double x, double z) { if (_state == SessionState.Active && _options.SharePings) { PingFrame ping = new PingFrame(x, z, _identity?.PlayerName, _clock.UnixTimeMilliseconds); EnqueueReliable(FrameCodec.WritePing(in ping)); } } public string? AddMarker(double x, double z, string? label, string? icon) { if (_state != SessionState.Active || _identity == null) { return null; } string text = MarkerStore.NewId(_identity.Uid, ++_markerSequence); MarkerFrame marker = new MarkerFrame("add", text, x, z, label, MarkerIcons.Normalise(icon), _clock.UnixTimeMilliseconds); if (!_markers.Add(marker)) { _markerSequence--; _log.Warn("marker limit reached (" + 64 + "); not adding"); return null; } EnqueueReliable(FrameCodec.WriteMarker(marker)); return text; } public bool RemoveMarker(string id) { if (_state != SessionState.Active) { return false; } if (!_markers.Remove(id)) { return false; } MarkerFrame marker = new MarkerFrame("remove", id, 0.0, 0.0, null, null, _clock.UnixTimeMilliseconds); EnqueueReliable(FrameCodec.WriteMarker(marker)); return true; } private void EnqueueReliable(string frame) { if (!FrameCodec.FitsInFrame(frame)) { _log.Warn("refusing oversized frame (" + FrameCodec.MeasureBytes(frame) + " bytes)"); } else if (!_outbound.EnqueueReliable(frame)) { _log.Warn("outbound queue full; dropped a frame"); } } private void SendHello() { if (_identity != null) { _lastHelloAt = _clock.Elapsed; HelloFrame hello = new HelloFrame(_identity.PlayerName, _identity.Uid, _identity.ModVersion, _identity.World, _options.SharePosition); EnqueueReliable(FrameCodec.WriteHello(hello)); } } private void ReplayState() { _stateReplayPending = false; _lastStateReplayAt = _clock.Elapsed; SendHello(); foreach (MarkerFrame item in _markers.Snapshot()) { EnqueueReliable(FrameCodec.WriteMarker(item)); } _throttle.Reset(); _lastPositionAt = TimeSpan.Zero; } private void AnnounceCode() { if (_activeCode != null && _gameChannel.IsReady) { _lastAnnounceAt = _clock.Elapsed; _gameChannel.AnnounceCode(_activeCode, _activeEpoch); } } private void DrainSocketEvents() { SocketEvent result; while (_socketEvents.TryDequeue(out result)) { switch (result.Kind) { case SocketEventKind.Opened: HandleOpened(); break; case SocketEventKind.Received: HandleFrame(result.Text); break; case SocketEventKind.Closed: HandleClosed(result.Code, result.Text); break; } } } private void HandleOpened() { _connectionOpenedAt = _clock.Elapsed; _healthyResetDone = false; } private void HandleFrame(string text) { JsonValue jsonValue = FrameCodec.ParseFrame(text); if (jsonValue == null) { _log.Debug("ignoring unparseable frame"); return; } string text2 = FrameCodec.TypeOf(jsonValue); switch (text2) { default: _ = text2 == "player_left"; break; case "welcome": HandleWelcome(jsonValue); break; case "request_state": HandleRequestState(); break; case "ping": { PingFrame? pingFrame = FrameCodec.ReadPing(jsonValue); if (pingFrame.HasValue) { PingFrame valueOrDefault = pingFrame.GetValueOrDefault(); this.PingReceived?.Invoke(valueOrDefault); } break; } case "marker": { MarkerFrame markerFrame = FrameCodec.ReadMarker(jsonValue); if (markerFrame != null) { this.MarkerReceived?.Invoke(markerFrame); } break; } case "player_joined": break; } } private void HandleWelcome(JsonValue frame) { WelcomeFrame welcomeFrame = FrameCodec.ReadWelcome(frame); if (welcomeFrame == null) { _log.Warn("malformed welcome; dropping the connection"); CloseTransport(1002, "bad welcome", expectClose: false); return; } _activeCode = welcomeFrame.Code; _activeEpoch = _pendingEpoch; _isCreator = welcomeFrame.IsCreator; PeerCount = welcomeFrame.Players.Count; _arbiter.SetCurrent(welcomeFrame.Code, _activeEpoch); _pendingCode = welcomeFrame.Code; _pendingToken = welcomeFrame.Token; if (welcomeFrame.IsCreator) { string text = _identity?.World.Uid; if (text != null) { _reclaim.Put(text, new ReclaimEntry(welcomeFrame.Code, welcomeFrame.Token, _activeEpoch, _clock.UnixTimeMilliseconds)); } } SetState(SessionState.Active); _throttle.Reset(); _lastPositionAt = TimeSpan.Zero; SendHello(); if (_isCreator) { AnnounceCode(); } if (_codeShownToPlayer == null) { _codeShownToPlayer = welcomeFrame.Code; Raise(new SessionNotice(NoticeKind.SessionStarted, "map code " + welcomeFrame.Code, welcomeFrame.Code)); } else if (!string.Equals(_codeShownToPlayer, welcomeFrame.Code, StringComparison.Ordinal)) { _codeShownToPlayer = welcomeFrame.Code; Raise(new SessionNotice(NoticeKind.CodeChanged, "the map code changed to " + welcomeFrame.Code + " — re-enter it in the web map", welcomeFrame.Code)); } } private void HandleRequestState() { if (_clock.Elapsed - _lastStateReplayAt >= _options.RequestStateCooldown) { ReplayState(); } else { _stateReplayPending = true; } } private void HandleClosed(int closeCode, string reason) { _outbound.Clear(); if (_state == SessionState.Stopped) { return; } if (_deliberateCloses > 0) { _deliberateCloses--; return; } _log.Info("relay connection closed: " + CloseCodes.Describe(closeCode)); switch (closeCode) { case 4003: ForgetReclaim("reclaim token rejected"); _activeCode = null; _isCreator = false; EnterDiscovering(); break; case 4004: HandleUnknownCode(); break; case 4008: _activeCode = null; _isCreator = false; SetState(SessionState.Blocked); Raise(new SessionNotice(NoticeKind.RoomFull, "the session is full (16 players). Retry from the relay panel.")); break; case 4013: ScheduleRetry(_relayFullBackoff.Next()); Raise(new SessionNotice(NoticeKind.RelayBusy, "the relay is busy; retrying shortly")); break; default: ScheduleRetry(_backoff.Next()); Raise(new SessionNotice(NoticeKind.Reconnecting, "reconnecting to the relay")); break; } } private void HandleUnknownCode() { string text = _pendingCode ?? _activeCode; if (text != null) { _arbiter.MarkDead(text, _pendingEpoch); } _activeCode = null; if (_isCreator || _pendingToken != null) { ForgetReclaim("code expired"); _isCreator = false; BeginConnect(null, null, _arbiter.NextEpoch(), SessionState.Creating); } else { _isCreator = false; EnterDiscovering(); Raise(new SessionNotice(NoticeKind.CodeChanged, "the session ended; finding or creating a new one")); } } private void ForgetReclaim(string why) { string text = _identity?.World.Uid; if (text != null) { _log.Info("discarding stored session for this world: " + why); _reclaim.Forget(text); } } private void DrainAnnouncements() { CodeAnnouncement result; while (_announcements.TryDequeue(out result)) { if (_state != SessionState.Stopped && _state != SessionState.Blocked) { switch (_arbiter.Consider(in result)) { case CodeDecision.Adopt: AdoptCode(in result); break; case CodeDecision.Defend: AnnounceCode(); break; } } } } private void AdoptCode(in CodeAnnouncement announcement) { if (!string.Equals(_activeCode, announcement.Code, StringComparison.OrdinalIgnoreCase) && (!string.Equals(_pendingCode, announcement.Code, StringComparison.OrdinalIgnoreCase) || (_state != SessionState.Joining && _state != SessionState.Reconnecting))) { if (_isCreator) { ForgetReclaim("lost the code tiebreak to " + announcement.Code); _isCreator = false; } _log.Info("adopting session code " + announcement.Code); _arbiter.SetCurrent(announcement.Code, announcement.Epoch); CloseTransport(1000, "migrating to " + announcement.Code); _outbound.Clear(); BeginConnect(announcement.Code, null, announcement.Epoch, SessionState.Joining); } } private void DrainCodeRequests() { if (Interlocked.Exchange(ref _codeRequests, 0) != 0 && _state == SessionState.Active && _isCreator && _activeCode != null) { AnnounceCode(); } } private void EnterDiscovering() { _activeCode = null; _isCreator = false; _pendingCode = null; _pendingToken = null; _lastDiscoveryAskAt = TimeSpan.Zero; _arbiter.ClearCurrent(); SetState(SessionState.Discovering); if (_gameChannel.IsReady) { _gameChannel.RequestCode(); } _lastDiscoveryAskAt = _clock.Elapsed; } private void BeginConnect(string? code, string? token, long epoch, SessionState state) { _pendingCode = code; _pendingToken = token; _pendingEpoch = epoch; SetState(state); _transport.Connect(_options.RelayUrl, code, token); } private void ScheduleRetry(TimeSpan delay) { _retryAt = _clock.Elapsed + delay; SetState(SessionState.Reconnecting); } private void CloseTransport(int code, string reason, bool expectClose = true) { if (_transport.State != TransportState.Closed) { if (expectClose) { _deliberateCloses++; } _transport.Close(code, reason); } } private void SetState(SessionState state) { if (_state != state) { _state = state; _stateEnteredAt = _clock.Elapsed; this.StateChanged?.Invoke(state); } } private void Raise(SessionNotice notice) { this.Notice?.Invoke(notice); } private void OnTransportOpened() { _socketEvents.Enqueue(new SocketEvent(SocketEventKind.Opened, string.Empty, 0)); } private void OnTransportReceived(string text) { _socketEvents.Enqueue(new SocketEvent(SocketEventKind.Received, text, 0)); } private void OnTransportClosed(int code, string reason) { _socketEvents.Enqueue(new SocketEvent(SocketEventKind.Closed, reason ?? string.Empty, code)); } private void OnCodeAnnounced(CodeAnnouncement announcement) { _announcements.Enqueue(announcement); } private void OnCodeRequested() { Interlocked.Increment(ref _codeRequests); } } public static class RelayUrl { public const string PathSuffix = "/ws"; public const string Default = "wss://valheimrelay.bobmitch.com/ws"; public const string LocalDevelopment = "ws://localhost:8080/ws"; public static string Normalise(string? raw, string? fallback = null) { if (fallback == null) { fallback = "wss://valheimrelay.bobmitch.com/ws"; } string text = (raw ?? string.Empty).Trim(); if (text.Length == 0) { return fallback; } if (StartsWith(text, "https://")) { text = "wss://" + text.Substring("https://".Length); } else if (StartsWith(text, "http://")) { text = "ws://" + text.Substring("http://".Length); } else if (!StartsWith(text, "ws://") && !StartsWith(text, "wss://")) { text = "wss://" + text; } if (!Uri.TryCreate(text, UriKind.Absolute, out Uri result)) { return fallback; } if (string.IsNullOrEmpty(result.Host)) { return fallback; } UriBuilder uriBuilder = new UriBuilder(result); string text2 = uriBuilder.Path.TrimEnd(new char[1] { '/' }); if (!EndsWith(text2, "/ws")) { text2 += "/ws"; } uriBuilder.Path = text2; string components = uriBuilder.Uri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped); if (!string.IsNullOrEmpty(components)) { return components; } return fallback; } public static bool IsInsecure(string url) { if (StartsWith(url ?? string.Empty, "ws://")) { if (!url.Contains("localhost") && !url.Contains("127.0.0.1")) { return !url.Contains("[::1]"); } return false; } return false; } private static bool StartsWith(string value, string prefix) { return value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); } private static bool EndsWith(string value, string suffix) { return value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase); } } public enum SessionState { Idle, Discovering, Creating, Joining, Active, Reconnecting, Blocked, Stopped } public sealed class SessionOptions { public string RelayUrl { get; set; } = "wss://valheimrelay.bobmitch.com/ws"; public TimeSpan DiscoveryWindow { get; set; } = TimeSpan.FromSeconds(5.0); public TimeSpan CreationStaggerSpread { get; set; } = TimeSpan.FromSeconds(3.0); public TimeSpan DiscoveryRetryInterval { get; set; } = TimeSpan.FromSeconds(10.0); public TimeSpan CodeAnnounceInterval { get; set; } = TimeSpan.FromSeconds(30.0); public TimeSpan HelloInterval { get; set; } = TimeSpan.FromSeconds(60.0); public TimeSpan PositionInterval { get; set; } = TimeSpan.FromSeconds(1.0); public TimeSpan RequestStateCooldown { get; set; } = TimeSpan.FromSeconds(5.0); public TimeSpan HealthyConnectionThreshold { get; set; } = TimeSpan.FromSeconds(60.0); public double PositionMinMetres { get; set; } = 1.0; public double PositionMinRotationDegrees { get; set; } = 5.0; public TimeSpan PositionKeepalive { get; set; } = TimeSpan.FromSeconds(10.0); public bool SharePosition { get; set; } = true; public bool SharePings { get; set; } = true; public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(20.0); public int OutboundReliableCapacity { get; set; } = 96; public SessionOptions Clone() { return (SessionOptions)MemberwiseClone(); } public void Normalise() { if (PositionInterval < TimeSpan.FromSeconds(0.5)) { PositionInterval = TimeSpan.FromSeconds(0.5); } if (DiscoveryWindow < TimeSpan.FromSeconds(1.0)) { DiscoveryWindow = TimeSpan.FromSeconds(1.0); } if (RequestStateCooldown < TimeSpan.FromSeconds(1.0)) { RequestStateCooldown = TimeSpan.FromSeconds(1.0); } if (HelloInterval < TimeSpan.FromSeconds(10.0)) { HelloInterval = TimeSpan.FromSeconds(10.0); } if (PositionKeepalive < PositionInterval) { PositionKeepalive = PositionInterval; } if (ConnectTimeout < TimeSpan.FromSeconds(5.0)) { ConnectTimeout = TimeSpan.FromSeconds(5.0); } int num = 72; if (OutboundReliableCapacity < num) { OutboundReliableCapacity = num; } } } public enum NoticeKind { SessionStarted, CodeChanged, Disconnected, Reconnecting, RoomFull, RelayBusy, Stopped } public sealed class SessionNotice { public NoticeKind Kind { get; } public string Message { get; } public string? Code { get; } public SessionNotice(NoticeKind kind, string message, string? code = null) { Kind = kind; Message = message; Code = code; } } } namespace ValheimRelay.Core.Qr { public sealed class QrCode { private readonly bool[] _modules; public int Version { get; } public int Mask { get; } public int Size { get; } public bool this[int x, int y] => _modules[y * Size + x]; private QrCode(int version, int mask, bool[] modules) { Version = version; Mask = mask; Size = QrVersions.Size(version); _modules = modules; } public static QrCode? Encode(string? text) { return Encode(text, -1); } internal static QrCode? Encode(string? text, int forcedMask) { if (string.IsNullOrEmpty(text)) { return null; } byte[] bytes = Encoding.UTF8.GetBytes(text); int num = QrVersions.SmallestFor(bytes.Length); if (num == 0) { return null; } byte[] codewords = BuildCodewords(bytes, num); QrMatrix qrMatrix = new QrMatrix(num); qrMatrix.PlaceData(codewords); int chosenMask; bool[] modules = qrMatrix.Finish(forcedMask, out chosenMask); return new QrCode(num, chosenMask, modules); } private static byte[] BuildCodewords(byte[] bytes, int version) { BlockPlan plan = QrVersions.Plan(version); byte[] array = EncodeData(bytes, plan); byte[] generator = ReedSolomon.Generator(plan.ErrorCodewords); byte[][] array2 = new byte[plan.Blocks][]; byte[][] array3 = new byte[plan.Blocks][]; int num = 0; for (int i = 0; i < plan.Blocks; i++) { int num2 = ((i < plan.Group1Blocks) ? plan.Group1Data : plan.Group2Data); byte[] array4 = new byte[num2]; Array.Copy(array, num, array4, 0, num2); num += num2; array2[i] = array4; array3[i] = ReedSolomon.Remainder(array4, 0, num2, generator); } byte[] array5 = new byte[array.Length + plan.Blocks * plan.ErrorCodewords]; int num3 = 0; int num4 = ((plan.Group2Blocks > 0) ? plan.Group2Data : plan.Group1Data); for (int j = 0; j < num4; j++) { byte[][] array6 = array2; foreach (byte[] array7 in array6) { if (j < array7.Length) { array5[num3++] = array7[j]; } } } for (int l = 0; l < plan.ErrorCodewords; l++) { byte[][] array6 = array3; foreach (byte[] array8 in array6) { array5[num3++] = array8[l]; } } return array5; } private static byte[] EncodeData(byte[] bytes, BlockPlan plan) { byte[] array = new byte[plan.DataCodewords]; int num = array.Length * 8; int bit = 0; Append(array, ref bit, 4, 4); Append(array, ref bit, bytes.Length, 8); foreach (byte value in bytes) { Append(array, ref bit, value, 8); } bit += Math.Min(4, num - bit); bit = (bit + 7) / 8 * 8; for (int j = bit / 8; j < array.Length; j++) { array[j] = (byte)(((j - bit / 8) % 2 == 0) ? 236 : 17); } return array; } private static void Append(byte[] data, ref int bit, int value, int count) { for (int num = count - 1; num >= 0; num--) { if (((value >> num) & 1) != 0) { data[bit >> 3] |= (byte)(1 << 7 - (bit & 7)); } bit++; } } } internal sealed class QrMatrix { private readonly bool[] _function; private readonly bool[] _modules; internal int Version { get; } internal int Size { get; } internal QrMatrix(int version) { Version = version; Size = QrVersions.Size(version); _modules = new bool[Size * Size]; _function = new bool[Size * Size]; DrawFinder(0, 0); DrawFinder(Size - 7, 0); DrawFinder(0, Size - 7); DrawTiming(); DrawAlignmentPatterns(); DrawFormat(0); DrawVersion(); } private void Set(int x, int y, bool dark, bool function) { if (x >= 0 && x < Size && y >= 0 && y < Size) { _modules[y * Size + x] = dark; if (function) { _function[y * Size + x] = true; } } } private void DrawFinder(int left, int top) { for (int i = -1; i <= 7; i++) { for (int j = -1; j <= 7; j++) { bool flag = j >= 0 && j <= 6 && i >= 0 && i <= 6; bool flag2 = j == 0 || j == 6 || i == 0 || i == 6; bool flag3 = j >= 2 && j <= 4 && i >= 2 && i <= 4; Set(left + j, top + i, flag && (flag2 || flag3), function: true); } } } private void DrawTiming() { for (int i = 8; i < Size - 8; i++) { bool dark = i % 2 == 0; Set(i, 6, dark, function: true); Set(6, i, dark, function: true); } } private void DrawAlignmentPatterns() { int[] array = QrVersions.Alignment(Version); if (array.Length == 0) { return; } int num = array.Length - 1; for (int i = 0; i <= num; i++) { for (int j = 0; j <= num; j++) { if ((i != 0 || j != 0) && (i != 0 || j != num) && (i != num || j != 0)) { DrawAlignment(array[j], array[i]); } } } } private void DrawAlignment(int cx, int cy) { for (int i = -2; i <= 2; i++) { for (int j = -2; j <= 2; j++) { bool dark = Math.Max(Math.Abs(j), Math.Abs(i)) != 1; Set(cx + j, cy + i, dark, function: true); } } } private void DrawFormat(int bits) { for (int i = 0; i < 15; i++) { bool dark = ((bits >> i) & 1) != 0; if (i <= 5) { Set(8, i, dark, function: true); } else { switch (i) { case 6: Set(8, 7, dark, function: true); break; case 7: Set(8, 8, dark, function: true); break; case 8: Set(7, 8, dark, function: true); break; default: Set(14 - i, 8, dark, function: true); break; } } if (i <= 7) { Set(Size - 1 - i, 8, dark, function: true); } else { Set(8, Size - 15 + i, dark, function: true); } } Set(8, Size - 8, dark: true, function: true); } private void DrawVersion() { if (Version >= 7) { int num = VersionBits(Version); for (int i = 0; i < 18; i++) { bool dark = ((num >> i) & 1) != 0; int num2 = i / 3; int num3 = Size - 11 + i % 3; Set(num2, num3, dark, function: true); Set(num3, num2, dark, function: true); } } } internal void PlaceData(byte[] codewords) { int num = 0; int num2 = codewords.Length * 8; bool flag = true; for (int num3 = Size - 1; num3 >= 1; num3 -= 2) { if (num3 == 6) { num3 = 5; } for (int i = 0; i < Size; i++) { int num4 = (flag ? (Size - 1 - i) : i); for (int j = 0; j < 2; j++) { int num5 = num3 - j; if (!_function[num4 * Size + num5]) { bool flag2 = false; if (num < num2) { flag2 = ((codewords[num >> 3] >> 7 - (num & 7)) & 1) != 0; num++; } _modules[num4 * Size + num5] = flag2; } } } flag = !flag; } } internal bool[] Finish(int forcedMask, out int chosenMask) { bool[] array = new bool[_modules.Length]; int num = int.MaxValue; chosenMask = 0; bool[] array2 = new bool[_modules.Length]; for (int i = 0; i < 8; i++) { if (forcedMask < 0 || i == forcedMask) { Array.Copy(_modules, array2, _modules.Length); ApplyMask(array2, i); WriteFormat(array2, i); int num2 = Penalty(array2); if (num2 < num) { num = num2; chosenMask = i; Array.Copy(array2, array, array2.Length); } } } return array; } private void ApplyMask(bool[] modules, int mask) { for (int i = 0; i < Size; i++) { for (int j = 0; j < Size; j++) { int num = i * Size + j; if (!_function[num] && Masked(mask, j, i)) { modules[num] = !modules[num]; } } } } private static bool Masked(int mask, int x, int y) { return mask switch { 0 => (x + y) % 2 == 0, 1 => y % 2 == 0, 2 => x % 3 == 0, 3 => (x + y) % 3 == 0, 4 => (y / 2 + x / 3) % 2 == 0, 5 => x * y % 2 + x * y % 3 == 0, 6 => (x * y % 2 + x * y % 3) % 2 == 0, 7 => ((x + y) % 2 + x * y % 3) % 2 == 0, _ => false, }; } private void WriteFormat(bool[] modules, int mask) { int num = FormatBits(0, mask); for (int i = 0; i < 15; i++) { bool flag = ((num >> i) & 1) != 0; if (i <= 5) { modules[i * Size + 8] = flag; } else { switch (i) { case 6: modules[7 * Size + 8] = flag; break; case 7: modules[8 * Size + 8] = flag; break; case 8: modules[8 * Size + 7] = flag; break; default: modules[8 * Size + 14 - i] = flag; break; } } if (i <= 7) { modules[8 * Size + Size - 1 - i] = flag; } else { modules[(Size - 15 + i) * Size + 8] = flag; } } } internal static int FormatBits(int eccBits, int mask) { int num = (eccBits << 3) | mask; int num2 = num; for (int i = 0; i < 10; i++) { num2 = (num2 << 1) ^ ((num2 >> 9) * 1335); } return (((num << 10) | num2) ^ 0x5412) & 0x7FFF; } internal static int VersionBits(int version) { int num = version; for (int i = 0; i < 12; i++) { num = (num << 1) ^ ((num >> 11) * 7973); } return (version << 12) | num; } private int Penalty(bool[] modules) { int num = 0; int num2 = 0; for (int i = 0; i < Size; i++) { num += LinePenalty(modules, i, horizontal: true); num += LinePenalty(modules, i, horizontal: false); } for (int j = 0; j < Size - 1; j++) { for (int k = 0; k < Size - 1; k++) { bool flag = modules[j * Size + k]; if (flag == modules[j * Size + k + 1] && flag == modules[(j + 1) * Size + k] && flag == modules[(j + 1) * Size + k + 1]) { num += 3; } } } for (int l = 0; l < modules.Length; l++) { if (modules[l]) { num2++; } } int num3 = modules.Length; return num + Math.Abs(num2 * 2 - num3) * 10 / num3 * 10; } private int LinePenalty(bool[] modules, int line, bool horizontal) { int num = 0; bool flag = false; int num2 = 0; int num3 = 0; for (int i = 0; i < Size; i++) { bool flag2 = (horizontal ? modules[line * Size + i] : modules[i * Size + line]); if (i > 0 && flag2 == flag) { num2++; if (num2 == 5) { num += 3; } else if (num2 > 5) { num++; } } else { flag = flag2; num2 = 1; } num3 = (int)(((uint)(num3 << 1) | (flag2 ? 1u : 0u)) & 0x7FF); if (i >= 10 && (num3 == 1488 || num3 == 93)) { num += 40; } } return num; } } internal readonly struct BlockPlan { internal int ErrorCodewords { get; } internal int Group1Blocks { get; } internal int Group1Data { get; } internal int Group2Blocks { get; } internal int Group2Data { get; } internal int Blocks => Group1Blocks + Group2Blocks; internal int DataCodewords => Group1Blocks * Group1Data + Group2Blocks * Group2Data; internal BlockPlan(int errorCodewords, int group1Blocks, int group1Data, int group2Blocks, int group2Data) { ErrorCodewords = errorCodewords; Group1Blocks = group1Blocks; Group1Data = group1Data; Group2Blocks = group2Blocks; Group2Data = group2Data; } } internal static class QrVersions { internal const int MinVersion = 1; internal const int MaxVersion = 9; internal const int EccLevelBits = 0; private static readonly BlockPlan[] Plans = new BlockPlan[9] { new BlockPlan(10, 1, 16, 0, 0), new BlockPlan(16, 1, 28, 0, 0), new BlockPlan(26, 1, 44, 0, 0), new BlockPlan(18, 2, 32, 0, 0), new BlockPlan(24, 2, 43, 0, 0), new BlockPlan(16, 4, 27, 0, 0), new BlockPlan(18, 4, 31, 0, 0), new BlockPlan(22, 2, 38, 2, 39), new BlockPlan(22, 3, 36, 2, 37) }; private static readonly int[][] AlignmentCentres = new int[9][] { new int[0], new int[2] { 6, 18 }, new int[2] { 6, 22 }, new int[2] { 6, 26 }, new int[2] { 6, 30 }, new int[2] { 6, 34 }, new int[3] { 6, 22, 38 }, new int[3] { 6, 24, 42 }, new int[3] { 6, 26, 46 } }; internal static BlockPlan Plan(int version) { return Plans[version - 1]; } internal static int[] Alignment(int version) { return AlignmentCentres[version - 1]; } internal static int Size(int version) { return 4 * version + 17; } internal static int SmallestFor(int byteCount) { int num = 12 + 8 * byteCount; for (int i = 1; i <= 9; i++) { if (Plan(i).DataCodewords * 8 >= num) { return i; } } return 0; } } internal static class ReedSolomon { private const int Primitive = 285; private static readonly byte[] Exp; private static readonly byte[] Log; static ReedSolomon() { Exp = new byte[512]; Log = new byte[256]; int num = 1; for (int i = 0; i < 255; i++) { Exp[i] = (byte)num; Log[num] = (byte)i; num <<= 1; if ((num & 0x100) != 0) { num ^= 0x11D; } } for (int j = 255; j < 512; j++) { Exp[j] = Exp[j - 255]; } } internal static byte Multiply(byte a, byte b) { if (a != 0 && b != 0) { return Exp[Log[a] + Log[b]]; } return 0; } internal static byte[] Generator(int degree) { byte[] array = new byte[1] { 1 }; for (int i = 0; i < degree; i++) { byte b = Exp[i]; byte[] array2 = new byte[array.Length + 1]; for (int j = 0; j < array.Length; j++) { array2[j] ^= array[j]; array2[j + 1] ^= Multiply(array[j], b); } array = array2; } return array; } internal static byte[] Remainder(byte[] data, int offset, int count, byte[] generator) { int num = generator.Length - 1; byte[] array = new byte[num]; for (int i = 0; i < count; i++) { byte b = (byte)(data[offset + i] ^ array[0]); Array.Copy(array, 1, array, 0, num - 1); array[num - 1] = 0; if (b != 0) { for (int j = 0; j < num; j++) { array[j] ^= Multiply(generator[j + 1], b); } } } return array; } } } namespace ValheimRelay.Core.Protocol { public static class CloseCodes { public const int TokenMismatch = 4003; public const int UnknownCode = 4004; public const int RoomFull = 4008; public const int RelayFull = 4013; public static bool RequiresSpecialHandling(int code) { if (code != 4008) { return code == 4013; } return true; } public static string Describe(int code) { return code switch { 4003 => "reclaim token rejected", 4004 => "unknown or expired code", 4008 => "room is full", 4013 => "relay is at its room limit", 1000 => "normal closure", 1001 => "endpoint going away", 1006 => "connection lost", _ => "close code " + code.ToString(CultureInfo.InvariantCulture), }; } } public static class FrameCodec { public const int MaxFrameBytes = 8192; public static int MeasureBytes(string frame) { return Encoding.UTF8.GetByteCount(frame); } public static bool FitsInFrame(string frame) { return MeasureBytes(frame) <= 8192; } public static string WriteHello(HelloFrame hello) { JsonWriter jsonWriter = new JsonWriter(); jsonWriter.BeginObject().Prop("type", "hello").Prop("v", 1L) .Prop("name", hello.Name) .Prop("uid", hello.Uid) .Prop("mod", hello.ModVersion); if (!hello.SharingPosition) { jsonWriter.Prop("share", value: false); } if (!hello.World.IsEmpty) { jsonWriter.Name("world").BeginObject().Prop("name", hello.World.Name) .Prop("seed", hello.World.Seed) .Prop("seedInt", hello.World.SeedInt) .Prop("uid", hello.World.Uid) .EndObject(); } return jsonWriter.EndObject().ToString(); } public static string WritePosition(in PositionSample p) { JsonWriter jsonWriter = new JsonWriter(); jsonWriter.BeginObject().Prop("type", "position").Prop("v", 1L) .Prop("x", p.X) .Prop("z", p.Z) .Prop("y", p.Y, 1) .Prop("rot", p.RotationDegrees, 1); if (!string.IsNullOrEmpty(p.Biome)) { jsonWriter.Prop("biome", p.Biome); } if (p.IncludeHealth) { jsonWriter.Prop("hp", p.Health).Prop("maxHp", p.MaxHealth); } if (p.Dead) { jsonWriter.Prop("dead", value: true); } return jsonWriter.Prop("t", p.TimestampMs).EndObject().ToString(); } public static string WritePing(in PingFrame ping) { JsonWriter jsonWriter = new JsonWriter(); jsonWriter.BeginObject().Prop("type", "ping").Prop("v", 1L) .Prop("x", ping.X) .Prop("z", ping.Z); if (!string.IsNullOrEmpty(ping.Name)) { jsonWriter.Prop("name", ping.Name); } return jsonWriter.Prop("t", ping.TimestampMs).EndObject().ToString(); } public static string WriteMarker(MarkerFrame marker) { JsonWriter jsonWriter = new JsonWriter(); jsonWriter.BeginObject().Prop("type", "marker").Prop("v", 1L) .Prop("op", marker.Op) .Prop("id", marker.Id); if (!marker.IsRemove) { jsonWriter.Prop("x", marker.X).Prop("z", marker.Z); if (!string.IsNullOrEmpty(marker.Label)) { jsonWriter.Prop("label", marker.Label); } jsonWriter.Prop("icon", MarkerIcons.Normalise(marker.Icon)); } return jsonWriter.Prop("t", marker.TimestampMs).EndObject().ToString(); } public static JsonValue? ParseFrame(string text) { if (string.IsNullOrEmpty(text)) { return null; } if (!JsonParser.TryParse(text, out JsonValue value)) { return null; } if (value.Kind != JsonKind.Object) { return null; } if (value["type"].AsString() == null) { return null; } return value; } public static string? TypeOf(JsonValue frame) { return frame["type"].AsString(); } public static WelcomeFrame? ReadWelcome(JsonValue frame) { string text = frame["code"].AsString(); string text2 = frame["playerId"].AsString(); if (text == null || text2 == null) { return null; } List list = new List(); foreach (JsonValue item in frame["players"].AsArray()) { string text3 = item["playerId"].AsString(); if (text3 != null) { list.Add(new RosterEntry(text3, item["name"].AsString(), item["uid"].AsString())); } } string text4 = frame["token"].AsString(); if (string.IsNullOrEmpty(text4)) { text4 = null; } return new WelcomeFrame(text, text2, text4, list); } public static PingFrame? ReadPing(JsonValue frame) { if (frame["x"].Kind != JsonKind.Number || frame["z"].Kind != JsonKind.Number) { return null; } return new PingFrame(frame["x"].AsDouble(), frame["z"].AsDouble(), frame["name"].AsString(), frame["t"].AsLong(0L)); } public static MarkerFrame? ReadMarker(JsonValue frame) { string text = frame["id"].AsString(); if (string.IsNullOrEmpty(text)) { return null; } string text2 = frame["op"].AsString("add"); if (!string.Equals(text2, "add", StringComparison.Ordinal) && !string.Equals(text2, "remove", StringComparison.Ordinal)) { return null; } if (string.Equals(text2, "add", StringComparison.Ordinal) && (frame["x"].Kind != JsonKind.Number || frame["z"].Kind != JsonKind.Number)) { return null; } return new MarkerFrame(text2, text, frame["x"].AsDouble(), frame["z"].AsDouble(), frame["label"].AsString(), MarkerIcons.Normalise(frame["icon"].AsString()), frame["t"].AsLong(0L)); } public static string? ReadPlayerId(JsonValue frame) { return frame["playerId"].AsString(); } } public readonly struct WorldInfo { public string? Name { get; } public string? Seed { get; } public long SeedInt { get; } public string? Uid { get; } public bool IsEmpty { get { if (string.IsNullOrEmpty(Name)) { return string.IsNullOrEmpty(Uid); } return false; } } public WorldInfo(string? name, string? seed, long seedInt, string? uid) { Name = name; Seed = seed; SeedInt = seedInt; Uid = uid; } } public sealed class HelloFrame { public string Name { get; } public string Uid { get; } public string ModVersion { get; } public WorldInfo World { get; } public bool SharingPosition { get; } public HelloFrame(string name, string uid, string modVersion, WorldInfo world, bool sharingPosition = true) { Name = name ?? string.Empty; Uid = uid ?? string.Empty; ModVersion = modVersion ?? string.Empty; World = world; SharingPosition = sharingPosition; } } public readonly struct PositionSample { public double X { get; } public double Z { get; } public double Y { get; } public double RotationDegrees { get; } public string? Biome { get; } public int Health { get; } public int MaxHealth { get; } public bool IncludeHealth { get; } public bool Dead { get; } public long TimestampMs { get; } public PositionSample(double x, double z, double y, double rotationDegrees, string? biome, int health, int maxHealth, bool includeHealth, bool dead, long timestampMs) { X = x; Z = z; Y = y; RotationDegrees = rotationDegrees; Biome = biome; Health = health; MaxHealth = maxHealth; IncludeHealth = includeHealth; Dead = dead; TimestampMs = timestampMs; } public double HorizontalDistanceSquaredTo(in PositionSample other) { double num = X - other.X; double num2 = Z - other.Z; return num * num + num2 * num2; } } public readonly struct PingFrame { public double X { get; } public double Z { get; } public string? Name { get; } public long TimestampMs { get; } public PingFrame(double x, double z, string? name, long timestampMs) { X = x; Z = z; Name = name; TimestampMs = timestampMs; } } public sealed class MarkerFrame { public string Op { get; } public string Id { get; } public double X { get; } public double Z { get; } public string? Label { get; } public string? Icon { get; } public long TimestampMs { get; } public bool IsAdd => string.Equals(Op, "add", StringComparison.Ordinal); public bool IsRemove => string.Equals(Op, "remove", StringComparison.Ordinal); public MarkerFrame(string op, string id, double x, double z, string? label, string? icon, long timestampMs) { Op = op; Id = id; X = x; Z = z; Label = label; Icon = icon; TimestampMs = timestampMs; } } public readonly struct RosterEntry { public string PlayerId { get; } public string? Name { get; } public string? Uid { get; } public RosterEntry(string playerId, string? name, string? uid) { PlayerId = playerId; Name = name; Uid = uid; } } public sealed class WelcomeFrame { public string Code { get; } public string PlayerId { get; } public string? Token { get; } public IReadOnlyList Players { get; } public bool IsCreator => !string.IsNullOrEmpty(Token); public WelcomeFrame(string code, string playerId, string? token, IReadOnlyList players) { Code = code; PlayerId = playerId; Token = token; Players = players; } } public static class FrameTypes { public const string Welcome = "welcome"; public const string PlayerJoined = "player_joined"; public const string PlayerLeft = "player_left"; public const string Hello = "hello"; public const string Position = "position"; public const string Ping = "ping"; public const string Marker = "marker"; public const string RequestState = "request_state"; } public static class ProtocolVersion { public const int Current = 1; } public static class MarkerIcons { public const string Dot = "dot"; public const string Ore = "ore"; public const string Boss = "boss"; public const string Home = "home"; public const string Death = "death"; public const string Danger = "danger"; private static readonly string[] Known = new string[6] { "dot", "ore", "boss", "home", "death", "danger" }; public static string Normalise(string? icon) { if (string.IsNullOrEmpty(icon)) { return "dot"; } string[] known = Known; foreach (string text in known) { if (string.Equals(text, icon, StringComparison.OrdinalIgnoreCase)) { return text; } } return "dot"; } public static bool IsKnown(string? icon) { if (!string.IsNullOrEmpty(icon)) { return Array.IndexOf(Known, icon) >= 0; } return false; } } public static class MarkerOps { public const string Add = "add"; public const string Remove = "remove"; } } namespace ValheimRelay.Core.Json { public sealed class JsonParseException : Exception { public int Position { get; } public JsonParseException(string message, int position) : base(message + " at offset " + position.ToString(CultureInfo.InvariantCulture)) { Position = position; } } public static class JsonParser { public const int MaxDepth = 24; public static JsonValue Parse(string text) { if (text == null) { throw new ArgumentNullException("text"); } int i = 0; JsonValue result = ParseValue(text, ref i, 0); SkipWhitespace(text, ref i); if (i != text.Length) { throw new JsonParseException("trailing content", i); } return result; } public static bool TryParse(string text, out JsonValue value) { try { value = Parse(text); return true; } catch (JsonParseException) { value = JsonValue.Null; return false; } catch (ArgumentNullException) { value = JsonValue.Null; return false; } } private static JsonValue ParseValue(string s, ref int i, int depth) { if (depth > 24) { throw new JsonParseException("nesting too deep", i); } SkipWhitespace(s, ref i); if (i >= s.Length) { throw new JsonParseException("unexpected end of input", i); } switch (s[i]) { case '{': return ParseObject(s, ref i, depth); case '[': return ParseArray(s, ref i, depth); case '"': return JsonValue.String(ParseString(s, ref i)); case 't': Expect(s, ref i, "true"); return JsonValue.Bool(value: true); case 'f': Expect(s, ref i, "false"); return JsonValue.Bool(value: false); case 'n': Expect(s, ref i, "null"); return JsonValue.Null; default: return JsonValue.Number(ParseNumber(s, ref i)); } } private static JsonValue ParseObject(string s, ref int i, int depth) { i++; Dictionary dictionary = new Dictionary(StringComparer.Ordinal); SkipWhitespace(s, ref i); if (i < s.Length && s[i] == '}') { i++; return JsonValue.Object(dictionary); } while (true) { SkipWhitespace(s, ref i); if (i >= s.Length || s[i] != '"') { throw new JsonParseException("expected object key", i); } string key = ParseString(s, ref i); SkipWhitespace(s, ref i); if (i >= s.Length || s[i] != ':') { throw new JsonParseException("expected ':'", i); } i++; dictionary[key] = ParseValue(s, ref i, depth + 1); SkipWhitespace(s, ref i); if (i >= s.Length) { throw new JsonParseException("unterminated object", i); } if (s[i] != ',') { break; } i++; } if (s[i] == '}') { i++; return JsonValue.Object(dictionary); } throw new JsonParseException("expected ',' or '}'", i); } private static JsonValue ParseArray(string s, ref int i, int depth) { i++; List list = new List(); SkipWhitespace(s, ref i); if (i < s.Length && s[i] == ']') { i++; return JsonValue.Array(list); } while (true) { list.Add(ParseValue(s, ref i, depth + 1)); SkipWhitespace(s, ref i); if (i >= s.Length) { throw new JsonParseException("unterminated array", i); } if (s[i] != ',') { break; } i++; } if (s[i] == ']') { i++; return JsonValue.Array(list); } throw new JsonParseException("expected ',' or ']'", i); } private static string ParseString(string s, ref int i) { i++; StringBuilder stringBuilder = new StringBuilder(); while (i < s.Length) { char c = s[i]; switch (c) { case '"': i++; return stringBuilder.ToString(); default: stringBuilder.Append(c); i++; break; case '\\': { i++; if (i >= s.Length) { throw new JsonParseException("unterminated escape", i); } char c2 = s[i++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': { if (i + 4 > s.Length) { throw new JsonParseException("truncated \\u escape", i); } if (!int.TryParse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { throw new JsonParseException("bad \\u escape", i); } i += 4; stringBuilder.Append((char)result); break; } default: throw new JsonParseException("unknown escape '" + c2 + "'", i); } break; } } } throw new JsonParseException("unterminated string", i); } private static double ParseNumber(string s, ref int i) { int num = i; if (i < s.Length && (s[i] == '-' || s[i] == '+')) { i++; } while (i < s.Length && (char.IsDigit(s[i]) || s[i] == '.' || s[i] == 'e' || s[i] == 'E' || ((s[i] == '-' || s[i] == '+') && (s[i - 1] == 'e' || s[i - 1] == 'E')))) { i++; } string text = s.Substring(num, i - num); if (text.Length == 0 || !double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw new JsonParseException("invalid number '" + text + "'", num); } return result; } private static void Expect(string s, ref int i, string literal) { if (i + literal.Length > s.Length || string.CompareOrdinal(s, i, literal, 0, literal.Length) != 0) { throw new JsonParseException("expected '" + literal + "'", i); } i += literal.Length; } private static void SkipWhitespace(string s, ref int i) { while (i < s.Length) { char c = s[i]; if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { i++; continue; } break; } } } public enum JsonKind { Null, Bool, Number, String, Array, Object } public sealed class JsonValue { public static readonly JsonValue Null = new JsonValue(JsonKind.Null, null, 0.0, boolean: false, null, null); private readonly string? _string; private readonly double _number; private readonly bool _bool; private readonly List? _array; private readonly Dictionary? _object; public JsonKind Kind { get; } public bool IsNull => Kind == JsonKind.Null; public JsonValue this[string name] { get { if (_object != null && _object.TryGetValue(name, out JsonValue value)) { return value; } return Null; } } public IEnumerable Keys { get { if (_object == null) { return System.Array.Empty(); } return _object.Keys; } } private JsonValue(JsonKind kind, string? str, double number, bool boolean, List? array, Dictionary? obj) { Kind = kind; _string = str; _number = number; _bool = boolean; _array = array; _object = obj; } public static JsonValue String(string value) { return new JsonValue(JsonKind.String, value, 0.0, boolean: false, null, null); } public static JsonValue Number(double value) { return new JsonValue(JsonKind.Number, null, value, boolean: false, null, null); } public static JsonValue Bool(bool value) { return new JsonValue(JsonKind.Bool, null, 0.0, value, null, null); } public static JsonValue Array(List items) { return new JsonValue(JsonKind.Array, null, 0.0, boolean: false, items, null); } public static JsonValue Object(Dictionary fields) { return new JsonValue(JsonKind.Object, null, 0.0, boolean: false, null, fields); } public IReadOnlyList AsArray() { IReadOnlyList array = _array; return array ?? System.Array.Empty(); } public bool Has(string name) { if (_object != null) { return _object.ContainsKey(name); } return false; } public string? AsString(string? fallback = null) { if (Kind != JsonKind.String) { return fallback; } return _string; } public double AsDouble(double fallback = 0.0) { if (Kind != JsonKind.Number) { return fallback; } return _number; } public long AsLong(long fallback = 0L) { if (Kind != JsonKind.Number) { return fallback; } if (double.IsNaN(_number) || double.IsInfinity(_number)) { return fallback; } if (_number >= 9.223372036854776E+18) { return long.MaxValue; } if (_number <= -9.223372036854776E+18) { return long.MinValue; } return (long)_number; } public int AsInt(int fallback = 0) { long num = AsLong(fallback); if (num > int.MaxValue) { return int.MaxValue; } if (num < int.MinValue) { return int.MinValue; } return (int)num; } public bool AsBool(bool fallback = false) { if (Kind != JsonKind.Bool) { return fallback; } return _bool; } public override string ToString() { switch (Kind) { case JsonKind.Null: return "null"; case JsonKind.Bool: return _bool ? "true" : "false"; case JsonKind.Number: { double number = _number; return number.ToString(CultureInfo.InvariantCulture); } case JsonKind.String: return _string ?? string.Empty; case JsonKind.Array: return "[" + AsArray().Count + " items]"; default: return "{object}"; } } } public sealed class JsonWriter { private readonly StringBuilder _sb; private bool _needComma; public JsonWriter(StringBuilder? sb = null) { _sb = sb ?? new StringBuilder(256); } public JsonWriter BeginObject() { Separate(); _sb.Append('{'); _needComma = false; return this; } public JsonWriter EndObject() { _sb.Append('}'); _needComma = true; return this; } public JsonWriter BeginArray() { Separate(); _sb.Append('['); _needComma = false; return this; } public JsonWriter EndArray() { _sb.Append(']'); _needComma = true; return this; } public JsonWriter Name(string name) { Separate(); WriteQuoted(name); _sb.Append(':'); _needComma = false; return this; } public JsonWriter Value(string? value) { Separate(); if (value == null) { _sb.Append("null"); } else { WriteQuoted(value); } _needComma = true; return this; } public JsonWriter Value(bool value) { Separate(); _sb.Append(value ? "true" : "false"); _needComma = true; return this; } public JsonWriter Value(long value) { Separate(); _sb.Append(value.ToString(CultureInfo.InvariantCulture)); _needComma = true; return this; } public JsonWriter Value(int value) { return Value((long)value); } public JsonWriter Value(double value, int decimals = 2) { Separate(); if (double.IsNaN(value) || double.IsInfinity(value)) { _sb.Append("null"); } else { string text = Math.Round(value, decimals, MidpointRounding.AwayFromZero).ToString("0.##########", CultureInfo.InvariantCulture); if (text == "-0") { text = "0"; } _sb.Append(text); } _needComma = true; return this; } public JsonWriter Prop(string name, string? value) { if (value == null) { return this; } return Name(name).Value(value); } public JsonWriter Prop(string name, long value) { return Name(name).Value(value); } public JsonWriter Prop(string name, bool value) { return Name(name).Value(value); } public JsonWriter Prop(string name, double value, int decimals = 2) { return Name(name).Value(value, decimals); } public JsonWriter PropIf(string name, bool condition, double value, int decimals = 2) { if (!condition) { return this; } return Prop(name, value, decimals); } public JsonWriter PropIf(string name, bool condition, long value) { if (!condition) { return this; } return Prop(name, value); } private void Separate() { if (_needComma) { _sb.Append(','); } } private void WriteQuoted(string value) { _sb.Append('"'); foreach (char c in value) { switch (c) { case '"': _sb.Append("\\\""); continue; case '\\': _sb.Append("\\\\"); continue; case '\b': _sb.Append("\\b"); continue; case '\f': _sb.Append("\\f"); continue; case '\n': _sb.Append("\\n"); continue; case '\r': _sb.Append("\\r"); continue; case '\t': _sb.Append("\\t"); continue; } if (c < ' ' || c == '\u2028' || c == '\u2029') { StringBuilder stringBuilder = _sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { _sb.Append(c); } } _sb.Append('"'); } public override string ToString() { return _sb.ToString(); } } } namespace ValheimRelay.Core.Identity { public static class StableUid { public const string Prefix = "vh_"; public const int DigestChars = 16; public static byte[] NewSalt() { byte[] array = new byte[32]; using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create(); randomNumberGenerator.GetBytes(array); return array; } public static string Derive(string profileId, byte[] salt) { if (profileId == null) { throw new ArgumentNullException("profileId"); } if (salt == null) { throw new ArgumentNullException("salt"); } if (salt.Length == 0) { throw new ArgumentException("salt must not be empty", "salt"); } using HMACSHA256 hMACSHA = new HMACSHA256(salt); byte[] array = hMACSHA.ComputeHash(Encoding.UTF8.GetBytes(profileId)); StringBuilder stringBuilder = new StringBuilder("vh_".Length + 16); stringBuilder.Append("vh_"); for (int i = 0; i < 8; i++) { stringBuilder.Append(array[i].ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } public static string EncodeSalt(byte[] salt) { return Convert.ToBase64String(salt); } public static bool TryDecodeSalt(string? encoded, out byte[] salt) { salt = Array.Empty(); if (string.IsNullOrEmpty(encoded)) { return false; } try { byte[] array = Convert.FromBase64String(encoded); if (array.Length < 16) { return false; } salt = array; return true; } catch (FormatException) { return false; } } } } namespace ValheimRelay.Core.Election { public enum CodeDecision { Ignore, Adopt, Defend } public sealed class CodeArbiter { private readonly struct DeadCode { public long Epoch { get; } public TimeSpan At { get; } public DeadCode(long epoch, TimeSpan at) { Epoch = epoch; At = at; } } private readonly IClock _clock; private readonly TimeSpan _deadCodeTtl; private readonly Dictionary _dead = new Dictionary(StringComparer.OrdinalIgnoreCase); public string? CurrentCode { get; private set; } public long CurrentEpoch { get; private set; } public long HighestSeenEpoch { get; private set; } public CodeArbiter(IClock clock, TimeSpan? deadCodeTtl = null) { _clock = clock ?? throw new ArgumentNullException("clock"); _deadCodeTtl = deadCodeTtl ?? TimeSpan.FromMinutes(10.0); } public void SetCurrent(string code, long epoch) { if (string.IsNullOrEmpty(code)) { throw new ArgumentException("code required", "code"); } CurrentCode = code; CurrentEpoch = epoch; if (epoch > HighestSeenEpoch) { HighestSeenEpoch = epoch; } } public void ClearCurrent() { CurrentCode = null; CurrentEpoch = 0L; } public long NextEpoch() { return HighestSeenEpoch + 1; } public void MarkDead(string code, long epoch) { if (!string.IsNullOrEmpty(code)) { PruneDead(); if (_dead.TryGetValue(code, out var value) && value.Epoch >= epoch) { _dead[code] = new DeadCode(value.Epoch, _clock.Elapsed); } else { _dead[code] = new DeadCode(epoch, _clock.Elapsed); } if (string.Equals(CurrentCode, code, StringComparison.OrdinalIgnoreCase) && CurrentEpoch <= epoch) { ClearCurrent(); } } } public bool IsKnownDead(string code, long epoch) { PruneDead(); if (_dead.TryGetValue(code, out var value)) { return value.Epoch >= epoch; } return false; } public CodeDecision Consider(in CodeAnnouncement announcement) { string code = announcement.Code; if (string.IsNullOrEmpty(code)) { return CodeDecision.Ignore; } if (announcement.Epoch > HighestSeenEpoch) { HighestSeenEpoch = announcement.Epoch; } if (IsKnownDead(code, announcement.Epoch)) { return CodeDecision.Ignore; } if (CurrentCode == null) { return CodeDecision.Adopt; } if (string.Equals(CurrentCode, code, StringComparison.OrdinalIgnoreCase)) { if (announcement.Epoch > CurrentEpoch) { CurrentEpoch = announcement.Epoch; } return CodeDecision.Ignore; } if (announcement.Epoch > CurrentEpoch) { return CodeDecision.Adopt; } if (announcement.Epoch < CurrentEpoch) { return CodeDecision.Defend; } int num = string.CompareOrdinal(code.ToUpperInvariant(), CurrentCode.ToUpperInvariant()); if (num < 0) { return CodeDecision.Adopt; } if (num > 0) { return CodeDecision.Defend; } return CodeDecision.Ignore; } private void PruneDead() { if (_dead.Count == 0) { return; } TimeSpan elapsed = _clock.Elapsed; List list = null; foreach (KeyValuePair item in _dead) { if (elapsed - item.Value.At >= _deadCodeTtl) { (list ?? (list = new List())).Add(item.Key); } } if (list == null) { return; } foreach (string item2 in list) { _dead.Remove(item2); } } } public static class CreatorElection { public static bool IsElectedCreator(IPeerView peers) { if (peers == null) { throw new ArgumentNullException("peers"); } if (peers.IsHost) { return true; } long selfPeerId = peers.SelfPeerId; foreach (long peerId in peers.PeerIds) { if (peerId != selfPeerId && peerId < selfPeerId) { return false; } } return true; } public static int CreatorRank(IPeerView peers) { if (peers == null) { throw new ArgumentNullException("peers"); } if (peers.IsHost) { return 0; } long selfPeerId = peers.SelfPeerId; int num = 0; foreach (long peerId in peers.PeerIds) { if (peerId != selfPeerId && peerId < selfPeerId) { num++; } } return num; } public static TimeSpan CreationStagger(IPeerView peers, TimeSpan spread) { if (peers == null) { throw new ArgumentNullException("peers"); } if (peers.IsHost || spread <= TimeSpan.Zero) { return TimeSpan.Zero; } double num = (double)(Mix(peers.SelfPeerId) % 10000) / 10000.0; return TimeSpan.FromTicks((long)((double)spread.Ticks * num)); } private static ulong Mix(long value) { long num = value + -7046029254386353131L; long num2 = (num ^ (num >>> 30)) * -4658895280553007687L; long num3 = (num2 ^ (num2 >>> 27)) * -7723592293110705685L; return (ulong)(num3 ^ (num3 >>> 31)); } } }