Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimRelay v0.1.0
plugins/ValheimRelay.Core.dll
Decompiled 2 days agousing 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<string>? Received; event Action<int, string>? 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<CodeAnnouncement>? 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<long> PeerIds { get; } } public sealed class Backoff { private readonly double _baseSeconds; private readonly double _capSeconds; private readonly double _jitterFraction; private readonly Func<double> _random; private int _attempt; public int Attempt => _attempt; public Backoff(double baseSeconds = 1.0, double capSeconds = 30.0, double jitterFraction = 0.25, Func<double>? 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<double>(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<double>? 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<string>? _sendQueue; private int _generation; private bool _disposed; public TransportState State { get; private set; } public event Action? Opened; public event Action<string>? Received; public event Action<int, string>? 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<string> queue = new BlockingCollection<string>(new ConcurrentQueue<string>(), _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<string> 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<string> 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<byte> buffer2 = new ArraySegment<byte>(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<string> 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<byte>(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<string> 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<string, MarkerFrame> _owned = new Dictionary<string, MarkerFrame>(StringComparer.Ordinal); private readonly List<string> _order = new List<string>(); 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<MarkerFrame> Snapshot() { lock (_gate) { List<MarkerFrame> list = new List<MarkerFrame>(_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<string> _reliable = new Queue<string>(); 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> _seen = new List<Seen>(); 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<string, ReclaimEntry> _entries = new Dictionary<string, ReclaimEntry>(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<string, ReclaimEntry> 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<SocketEvent> _socketEvents = new ConcurrentQueue<SocketEvent>(); private readonly ConcurrentQueue<CodeAnnouncement> _announcements = new ConcurrentQueue<CodeAnnouncement>(); 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<SessionState>? StateChanged; public event Action<SessionNotice>? Notice; public event Action<PingFrame>? PingReceived; public event Action<MarkerFrame>? MarkerReceived; public RelaySession(SessionOptions options, IRelayTransport transport, IGameChannel gameChannel, IPeerView peers, IClock clock, ILog log, ReclaimStore reclaim, Func<double>? 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<RosterEntry> list = new List<RosterEntry>(); 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<RosterEntry> Players { get; } public bool IsCreator => !string.IsNullOrEmpty(Token); public WelcomeFrame(string code, string playerId, string? token, IReadOnlyList<RosterEntry> 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<string>(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<string, JsonValue> dictionary = new Dictionary<string, JsonValue>(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<JsonValue> list = new List<JsonValue>(); 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<JsonValue>? _array; private readonly Dictionary<string, JsonValue>? _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<string> Keys { get { if (_object == null) { return System.Array.Empty<string>(); } return _object.Keys; } } private JsonValue(JsonKind kind, string? str, double number, bool boolean, List<JsonValue>? array, Dictionary<string, JsonValue>? 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<JsonValue> items) { return new JsonValue(JsonKind.Array, null, 0.0, boolean: false, items, null); } public static JsonValue Object(Dictionary<string, JsonValue> fields) { return new JsonValue(JsonKind.Object, null, 0.0, boolean: false, null, fields); } public IReadOnlyList<JsonValue> AsArray() { IReadOnlyList<JsonValue> array = _array; return array ?? System.Array.Empty<JsonValue>(); } 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<byte>(); 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<string, DeadCode> _dead = new Dictionary<string, DeadCode>(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<string> list = null; foreach (KeyValuePair<string, DeadCode> item in _dead) { if (elapsed - item.Value.At >= _deadCodeTtl) { (list ?? (list = new List<string>())).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)); } } }
plugins/ValheimRelay.dll
Decompiled 2 days agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; using UnityEngine; using ValheimRelay.Core.Identity; using ValheimRelay.Core.Protocol; using ValheimRelay.Core.Qr; using ValheimRelay.Core.Session; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("ValheimRelay")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+72cd49b324620f005e0fd406d82cf7ff2dce1dd1")] [assembly: AssemblyProduct("ValheimRelay")] [assembly: AssemblyTitle("ValheimRelay")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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.Plugin { public enum PingStyle { Auto, Map, Pin } public sealed class PluginConfig { public const string DefaultRelayUrl = "wss://valheimrelay.bobmitch.com/ws"; public const string DefaultMapUrl = "https://bobmitch.com/valheim"; public ConfigEntry<bool> Enabled { get; } public ConfigEntry<string> RelayUrl { get; } public ConfigEntry<string> MapUrl { get; } public ConfigEntry<bool> AnnounceInChat { get; } public ConfigEntry<bool> ShareMyPosition { get; } public ConfigEntry<bool> ShareHealth { get; } public ConfigEntry<bool> AcceptMapMarkers { get; } public ConfigEntry<bool> ShareMyPings { get; } public ConfigEntry<float> PositionInterval { get; } public ConfigEntry<KeyCode> ToggleKey { get; } public ConfigEntry<bool> ToggleRequiresShift { get; } public ConfigEntry<PingStyle> PingStyle { get; } public bool HasMapLink => MapLink.Normalise(MapUrl.Value).Length > 0; public PluginConfig(ConfigFile file) { //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Expected O, but got Unknown if (file == null) { throw new ArgumentNullException("file"); } Enabled = file.Bind<bool>("General", "Enabled", true, "Master switch. Turn this off and the mod does nothing at all."); RelayUrl = file.Bind<string>("General", "RelayUrl", "wss://valheimrelay.bobmitch.com/ws", "Relay WebSocket URL. Leave this alone unless you run your own relay."); MapUrl = file.Bind<string>("General", "MapUrl", "https://bobmitch.com/valheim", "Web map base URL, used to build the copyable link. The code is appended as a fragment."); AnnounceInChat = file.Bind<bool>("General", "AnnounceInChat", true, "Print the session code in chat when the session starts. Local only — other players do not see it."); ShareMyPosition = file.Bind<bool>("Privacy", "ShareMyPosition", true, "Broadcast your position. Turning this off keeps you in the session and still shows you everyone else."); ShareHealth = file.Bind<bool>("Privacy", "ShareHealth", true, "Include health in position updates."); AcceptMapMarkers = file.Bind<bool>("Privacy", "AcceptMapMarkers", true, "Let the web map place pins on your in-game minimap."); ShareMyPings = file.Bind<bool>("Privacy", "ShareMyPings", true, "Send the pings you make in game to the web map. Separate from ShareMyPosition: a ping is something you chose to do, so turning off the position stream does not turn this off. Pings from other players are never forwarded by you, only your own."); PositionInterval = file.Bind<float>("Performance", "PositionInterval", 1f, new ConfigDescription("Seconds between position updates. Clamped to at least 0.5.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f), Array.Empty<object>())); ToggleKey = file.Bind<KeyCode>("UI", "ToggleKey", (KeyCode)289, "Shows and hides the relay panel. Held together with Shift unless ToggleRequiresShift is off."); ToggleRequiresShift = file.Bind<bool>("UI", "ToggleRequiresShift", true, "Require Shift to be held with ToggleKey. Turn this off for a bare keypress."); PingStyle = file.Bind<PingStyle>("UI", "PingStyle", ValheimRelay.Plugin.PingStyle.Auto, "How a ping from the web map is shown. Auto hands it to the game's own ping code, so it looks, sounds and reads exactly like a player's ping. Map draws the minimap marker only. Pin drops a short-lived pin and writes a chat line. Drop down a level if a game update breaks the one above it."); } public SessionOptions ToSessionOptions() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown SessionOptions val = new SessionOptions { RelayUrl = NormaliseRelayUrl(RelayUrl.Value), PositionInterval = TimeSpan.FromSeconds(PositionInterval.Value), SharePosition = ShareMyPosition.Value, SharePings = ShareMyPings.Value }; val.Normalise(); return val; } public static string NormaliseRelayUrl(string raw) { return RelayUrl.Normalise(raw, "wss://valheimrelay.bobmitch.com/ws"); } public string BuildShareText(string code, string? seed = null) { return MapLink.Build(MapUrl.Value, code, seed); } } public sealed class GameBridge : IPeerView { private readonly struct PingPin { public object Pin { get; } public float Expiry { get; } public PingPin(object pin, float expiry) { Pin = pin; Expiry = expiry; } } private readonly ILog _log; private readonly Func<PingStyle>? _pingStyle; private const float PingLifetimeSeconds = 6f; private const BindingFlags AnyInstanceMethod = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private readonly List<PingPin> _pingPins = new List<PingPin>(); private readonly PingEcho _pingEcho = new PingEcho((TimeSpan?)null, 2.0); private MethodInfo? _chatPing; private bool _chatPingResolved; private MethodInfo? _groundHeight; private MethodInfo? _groundHeightDirect; private MethodInfo? _generatedHeight; private bool _heightResolved; private MethodInfo? _addPing; private bool _addPingResolved; private bool _pingApiDescribed; private string? _pingPathLogged; private static readonly PinType PingPinType = ResolvePingPinType(); public static bool HasLocalPlayer => (Object)(object)Player.m_localPlayer != (Object)null; public static bool IsWorldLoaded => (Object)(object)ZNet.instance != (Object)null; public string PlayerName { get { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return "Viking"; } string playerName = localPlayer.GetPlayerName(); if (!string.IsNullOrEmpty(playerName)) { return playerName; } return "Viking"; } } public string ProfileId { get { try { Game instance = Game.instance; PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null); if (val == null) { return "unknown-profile"; } return val.GetPlayerID().ToString(CultureInfo.InvariantCulture); } catch (Exception ex) { LogExtensions.Warn(_log, "could not read the player profile id: " + ex.Message); return "unknown-profile"; } } } public bool IsHost { get { try { return (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer(); } catch (Exception) { return false; } } } public long SelfPeerId { get { try { return ((Object)(object)ZNet.instance == (Object)null) ? 0 : ZNet.GetUID(); } catch (Exception) { return 0L; } } } public IReadOnlyList<long> PeerIds { get { List<long> list = new List<long>(); try { ZNet instance = ZNet.instance; List<ZNetPeer> list2 = ((instance != null) ? instance.GetPeers() : null); if (list2 == null) { return list; } foreach (ZNetPeer item in list2) { if (item != null) { list.Add(item.m_uid); } } } catch (Exception ex) { LogExtensions.Warn(_log, "could not read the peer list: " + ex.Message); } return list; } } public static bool IsRenderingPing { get; private set; } private static TimeSpan Now => TimeSpan.FromSeconds(Time.realtimeSinceStartup); public GameBridge(ILog log, Func<PingStyle>? pingStyle = null) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown _log = log ?? throw new ArgumentNullException("log"); _pingStyle = pingStyle; } public WorldInfo ReadWorld() { //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) try { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return default(WorldInfo); } string worldName = instance.GetWorldName(); string text = instance.GetWorldUID().ToString(CultureInfo.InvariantCulture); string text2 = null; long num = 0L; WorldGenerator instance2 = WorldGenerator.instance; if (instance2?.m_world != null) { text2 = instance2.m_world.m_seedName; num = instance2.m_world.m_seed; } return new WorldInfo(worldName, text2, num, text); } catch (Exception ex) { LogExtensions.Warn(_log, "could not read world information: " + ex.Message); return default(WorldInfo); } } public bool TryReadPosition(bool includeHealth, long timestampMs, out PositionSample sample) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) sample = default(PositionSample); Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return false; } try { Transform transform = ((Component)localPlayer).transform; Vector3 position = transform.position; Quaternion rotation = transform.rotation; float y = ((Quaternion)(ref rotation)).eulerAngles.y; int num = Mathf.RoundToInt(((Character)localPlayer).GetHealth()); int num2 = Mathf.RoundToInt(((Character)localPlayer).GetMaxHealth()); bool flag = ((Character)localPlayer).IsDead() || num <= 0; sample = new PositionSample((double)position.x, (double)position.z, (double)position.y, NormaliseDegrees(y), ReadBiome(position.x, position.z), num, num2, includeHealth, flag, timestampMs); return true; } catch (Exception ex) { LogExtensions.Warn(_log, "could not read the local player: " + ex.Message); return false; } } private string? ReadBiome(float x, float z) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) try { WorldGenerator instance = WorldGenerator.instance; if (instance == null) { return null; } return ((object)instance.GetBiome(x, z, 0.02f, false)/*cast due to .constrained prefix*/).ToString(); } catch (Exception) { return null; } } private static double NormaliseDegrees(double degrees) { double num = degrees % 360.0; if (!(num < 0.0)) { return num; } return num + 360.0; } public static PinType ToPinType(string? icon) { return (PinType)(MarkerIcons.Normalise(icon) switch { "ore" => 3, "boss" => 9, "home" => 1, "death" => 4, "danger" => 2, _ => 0, }); } public object? AddPin(double x, double z, string? label, string? icon) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) return AddPinAt(new Vector3((float)x, 0f, (float)z), ToPinType(icon), label ?? string.Empty); } private object? AddPinAt(Vector3 position, PinType type, string label) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) try { Minimap instance = Minimap.instance; if ((Object)(object)instance == (Object)null) { return null; } return instance.AddPin(position, type, label, false, false, 0L, default(PlatformUserID)); } catch (Exception ex) { LogExtensions.Warn(_log, "could not add a map pin: " + ex.Message); return null; } } public void RemovePin(object? pin) { PinData val = (PinData)((pin is PinData) ? pin : null); if (val == null) { return; } try { Minimap instance = Minimap.instance; if (instance != null) { instance.RemovePin(val); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not remove a map pin: " + ex.Message); } } public void ShowPing(double x, double z, string? who) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)Minimap.instance == (Object)null) && !_pingEcho.ShouldSuppress(x, z, Now)) { DescribePingApi(); Vector3 position = default(Vector3); ((Vector3)(ref position))..ctor((float)x, GroundHeight((float)x, (float)z), (float)z); string name = (string.IsNullOrEmpty(who) ? "Ping" : who); PingStyle pingStyle = ReadPingStyle(); if ((pingStyle != PingStyle.Auto || !TryVanillaPing(position, name)) && (pingStyle == PingStyle.Pin || !TryMinimapPing(position, name))) { ShowFallbackPing(position, name); } } } catch (Exception ex) { LogExtensions.Warn(_log, "could not show a ping: " + ex.Message); } } public void NoteGamePing(double x, double z) { _pingEcho.Observe(x, z, Now); } public void ExpirePings() { if (_pingPins.Count == 0) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; for (int num = _pingPins.Count - 1; num >= 0; num--) { if (!(_pingPins[num].Expiry > realtimeSinceStartup)) { RemovePin(_pingPins[num].Pin); _pingPins.RemoveAt(num); } } } public void ClearPings() { foreach (PingPin pingPin in _pingPins) { RemovePin(pingPin.Pin); } _pingPins.Clear(); _pingEcho.Clear(); } private bool TryVanillaPing(Vector3 position, string name) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) Chat instance = Chat.instance; if ((Object)(object)instance == (Object)null) { return false; } MethodInfo methodInfo = ResolveChatPing(); if (methodInfo == null) { return false; } object[] array = BuildChatPingArgs(methodInfo, position, name); if (array == null) { return false; } IsRenderingPing = true; try { methodInfo.Invoke(instance, array); LogPingPath("the game's own chat path — " + Describe(methodInfo)); return true; } catch (Exception ex) { _chatPing = null; LogExtensions.Warn(_log, "the game's ping path failed, falling back to the minimap: " + Unwrap(ex).Message); return false; } finally { IsRenderingPing = false; } } private bool TryMinimapPing(Vector3 position, string name) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) MethodInfo methodInfo = ResolveAddPing(); if (methodInfo == null) { return false; } try { methodInfo.Invoke(Minimap.instance, BuildPingArgs(methodInfo, position, name)); LogPingPath("Minimap.AddPing — the marker only, with no sound or world text"); return true; } catch (Exception ex) { _addPing = null; LogExtensions.Warn(_log, "Minimap.AddPing failed, so pings will show as pins: " + Unwrap(ex).Message); return false; } } private PingStyle ReadPingStyle() { try { return _pingStyle?.Invoke() ?? PingStyle.Auto; } catch (Exception) { return PingStyle.Auto; } } private void LogPingPath(string path) { if (!string.Equals(_pingPathLogged, path, StringComparison.Ordinal)) { _pingPathLogged = path; LogExtensions.Info(_log, "pings are being shown through " + path); } } private void ShowFallbackPing(Vector3 position, string name) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) object obj = AddPinAt(position, PingPinType, name); if (obj != null) { _pingPins.Add(new PingPin(obj, Time.realtimeSinceStartup + 6f)); } LogPingPath("a short-lived pin and a chat line"); LocalMessage(name + " pinged " + Mathf.RoundToInt(position.x).ToString(CultureInfo.InvariantCulture) + ", " + Mathf.RoundToInt(position.z).ToString(CultureInfo.InvariantCulture)); } private float GroundHeight(float x, float z) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) ResolveHeight(); try { Vector3 val = default(Vector3); ((Vector3)(ref val))..ctor(x, 5000f, z); ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance != (Object)null && _groundHeight != null) { object[] array = Fill(_groundHeight, val); object obj = _groundHeight.Invoke(instance, array); bool flag = default(bool); int num; if (obj is bool) { flag = (bool)obj; num = 1; } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0 && array[1] is float result) { return result; } } if ((Object)(object)instance != (Object)null && _groundHeightDirect != null && _groundHeightDirect.Invoke(instance, Fill(_groundHeightDirect, val)) is float num2 && !Mathf.Approximately(num2, val.y) && Mathf.Abs(num2) > 0.001f) { return num2; } WorldGenerator instance2 = WorldGenerator.instance; if (instance2 != null && _generatedHeight != null && _generatedHeight.Invoke(instance2, Fill(_generatedHeight, x, z)) is float num3 && !float.IsNaN(num3)) { return num3; } } catch (Exception ex) { LogExtensions.Debug(_log, "could not read the ground height: " + ex.Message); } return 0f; } private void ResolveHeight() { if (_heightResolved) { return; } _heightResolved = true; try { string[] array = new string[2] { "GetGroundHeight", "GetSolidHeight" }; MethodInfo[] methods; foreach (string b in array) { methods = typeof(ZoneSystem).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, b, StringComparison.Ordinal)) { continue; } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length < 1 || parameters[0].ParameterType != typeof(Vector3)) { continue; } if (methodInfo.ReturnType == typeof(bool) && parameters.Length == 2 && parameters[1].IsOut && parameters[1].ParameterType.GetElementType() == typeof(float)) { if (_groundHeight == null) { _groundHeight = methodInfo; } } else if (methodInfo.ReturnType == typeof(float) && parameters.Length == 1 && _groundHeightDirect == null) { _groundHeightDirect = methodInfo; } } if (_groundHeight != null || _groundHeightDirect != null) { break; } } methods = typeof(WorldGenerator).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo2 in methods) { if (!string.Equals(methodInfo2.Name, "GetHeight", StringComparison.Ordinal) || methodInfo2.ReturnType != typeof(float)) { continue; } ParameterInfo[] parameters2 = methodInfo2.GetParameters(); if (parameters2.Length < 2 || parameters2[0].ParameterType != typeof(float) || parameters2[1].ParameterType != typeof(float)) { continue; } bool flag = true; for (int k = 2; k < parameters2.Length; k++) { if (!parameters2[k].IsOut && !parameters2[k].IsOptional) { flag = false; break; } } if (flag) { _generatedHeight = methodInfo2; break; } } if (_groundHeight == null && _groundHeightDirect == null && _generatedHeight == null) { LogExtensions.Info(_log, "no ground-height lookup was found on this build, so pings will sit at sea level."); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not look up the ground-height API: " + ex.Message); } } private static object[] Fill(MethodInfo method, params object[] leading) { ParameterInfo[] parameters = method.GetParameters(); object[] array = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { if (i < leading.Length) { array[i] = leading[i]; continue; } Type type = parameters[i].ParameterType; if (type.IsByRef) { type = type.GetElementType(); } array[i] = (type.IsValueType ? Activator.CreateInstance(type) : null); } return array; } private MethodInfo? ResolveChatPing() { if (_chatPingResolved) { return _chatPing; } _chatPingResolved = true; try { _chatPing = FindChatPingMethod(); if (_chatPing != null) { return _chatPing; } LogExtensions.Info(_log, "Chat.OnNewChatMessage is not usable for pings on this build, so pings will be drawn on the map only."); } catch (Exception ex) { LogExtensions.Warn(_log, "could not look up the chat ping path: " + ex.Message); } return _chatPing; } public static MethodInfo? FindChatPingMethod() { MethodInfo[] methods = typeof(Chat).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, "OnNewChatMessage", StringComparison.Ordinal)) { continue; } bool flag = false; bool flag2 = false; ParameterInfo[] parameters = methodInfo.GetParameters(); foreach (ParameterInfo parameterInfo in parameters) { if (parameterInfo.ParameterType == typeof(Vector3)) { flag = true; } else if (parameterInfo.ParameterType.IsEnum && HasEnumName(parameterInfo.ParameterType, "Ping")) { flag2 = true; } } if (flag && flag2) { return methodInfo; } } return null; } public static bool TryReadPingArgs(object[]? args, out Vector3 position, out long senderId) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) position = default(Vector3); senderId = 0L; if (args == null) { return false; } bool flag = false; bool flag2 = false; bool flag3 = false; foreach (object obj in args) { if (!(obj is Vector3 val)) { if (!(obj is long num)) { if (obj is Enum obj2 && !flag2) { flag2 = string.Equals(obj2.ToString(), "Ping", StringComparison.Ordinal); } } else if (!flag3) { senderId = num; flag3 = true; } } else if (!flag) { position = val; flag = true; } } return flag && flag2; } private object[]? BuildChatPingArgs(MethodInfo method, Vector3 position, string name) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) ParameterInfo[] parameters = method.GetParameters(); object[] array = new object[parameters.Length]; bool flag = false; ParameterInfo[] array2 = parameters; for (int i = 0; i < array2.Length; i++) { if (IsUserInfoLike(array2[i].ParameterType)) { flag = true; break; } } int num = ((!flag) ? 1 : 0); int num2 = (flag ? 1 : 2); int num3 = 0; for (int j = 0; j < parameters.Length; j++) { Type parameterType = parameters[j].ParameterType; if (parameterType == typeof(Vector3)) { array[j] = position; } else if (parameterType == typeof(long)) { array[j] = PingSenderId(name); } else if (parameterType.IsEnum) { if (!TryParseEnum(parameterType, "Ping", out object value)) { return null; } array[j] = value; } else if (parameterType == typeof(string)) { num3++; array[j] = ((num3 == num) ? name : ((num3 == num2) ? "Ping" : string.Empty)); } else if (IsUserInfoLike(parameterType)) { object obj = BuildUser(parameterType, name); if (obj == null) { return null; } array[j] = obj; } else { array[j] = (parameterType.IsValueType ? Activator.CreateInstance(parameterType) : null); } } return array; } private static object? BuildUser(Type type, string name) { object obj = null; try { MethodInfo method = type.GetMethod("GetLocalUser", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); if (method != null && type.IsAssignableFrom(method.ReturnType)) { obj = method.Invoke(null, null); } } catch (Exception) { obj = null; } if (obj == null) { try { obj = Activator.CreateInstance(type); } catch (Exception) { return null; } } if (obj == null) { return null; } SetStringMember(obj, "Name", name); SetStringMember(obj, "m_name", name); SetStringMember(obj, "Gamertag", name); SetStringMember(obj, "m_gamertag", name); return obj; } private static void SetStringMember(object target, string member, string value) { try { Type type = target.GetType(); FieldInfo field = type.GetField(member, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && field.FieldType == typeof(string) && !field.IsInitOnly) { field.SetValue(target, value); return; } PropertyInfo property = type.GetProperty(member, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.PropertyType == typeof(string) && property.CanWrite) { property.SetValue(target, value, null); } } catch (Exception) { } } private static long PingSenderId(string name) { return name.GetHashCode(); } private static bool IsUserInfoLike(Type type) { if (!type.IsPrimitive && !type.IsEnum && type != typeof(string) && type != typeof(Vector3)) { return !typeof(Object).IsAssignableFrom(type); } return false; } private static bool HasEnumName(Type type, string name) { string[] names = Enum.GetNames(type); for (int i = 0; i < names.Length; i++) { if (string.Equals(names[i], name, StringComparison.Ordinal)) { return true; } } return false; } private static bool TryParseEnum(Type type, string name, out object? value) { value = null; if (!HasEnumName(type, name)) { return false; } value = Enum.Parse(type, name); return true; } private void DescribePingApi() { if (_pingApiDescribed) { return; } _pingApiDescribed = true; try { StringBuilder stringBuilder = new StringBuilder("ping API on this game build:"); AppendMethods(stringBuilder, typeof(Chat), "OnNewChatMessage", "AddInworldText", "SendPing", "RPC_ChatMessage"); AppendMethods(stringBuilder, typeof(Minimap), "AddPing", "ShowPointOnMap"); AppendMethods(stringBuilder, typeof(ZoneSystem), "GetGroundHeight", "GetSolidHeight"); AppendMethods(stringBuilder, typeof(WorldGenerator), "GetHeight"); AppendEffectFields(stringBuilder, typeof(Chat)); AppendEffectFields(stringBuilder, typeof(Minimap)); LogExtensions.Info(_log, stringBuilder.ToString()); } catch (Exception ex) { LogExtensions.Debug(_log, "could not describe the ping API: " + ex.Message); } } private static void AppendMethods(StringBuilder report, Type type, params string[] names) { MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { foreach (string b in names) { if (string.Equals(methodInfo.Name, b, StringComparison.Ordinal)) { report.Append("\n ").Append(type.Name).Append('.') .Append(Describe(methodInfo)); break; } } } } private static void AppendEffectFields(StringBuilder report, Type type) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType.Name.IndexOf("Effect", StringComparison.OrdinalIgnoreCase) >= 0) { report.Append("\n ").Append(type.Name).Append('.') .Append(fieldInfo.Name) .Append(" : ") .Append(fieldInfo.FieldType.Name); } } } private static string Describe(MethodInfo method) { StringBuilder stringBuilder = new StringBuilder(method.Name).Append('('); ParameterInfo[] parameters = method.GetParameters(); for (int i = 0; i < parameters.Length; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(parameters[i].ParameterType.Name); if (parameters[i].IsOptional) { stringBuilder.Append('?'); } } return stringBuilder.Append(')').ToString(); } private MethodInfo? ResolveAddPing() { if (_addPingResolved) { return _addPing; } _addPingResolved = true; try { MethodInfo[] methods = typeof(Minimap).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, "AddPing", StringComparison.Ordinal)) { continue; } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length < 2 || parameters[0].ParameterType != typeof(Vector3) || parameters[1].ParameterType != typeof(string)) { continue; } bool flag = true; for (int j = 2; j < parameters.Length; j++) { if (!parameters[j].IsOptional) { flag = false; break; } } if (flag) { _addPing = methodInfo; return _addPing; } } LogExtensions.Warn(_log, "Minimap.AddPing was not found, so inbound pings will show as short-lived pins. If the game has updated, this is the signature to check."); } catch (Exception ex) { LogExtensions.Warn(_log, "could not look up Minimap.AddPing: " + ex.Message); } return _addPing; } private static object[] BuildPingArgs(MethodInfo method, Vector3 position, string name) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) ParameterInfo[] parameters = method.GetParameters(); object[] array = new object[parameters.Length]; array[0] = position; array[1] = name; for (int i = 2; i < parameters.Length; i++) { ParameterInfo parameterInfo = parameters[i]; object obj = parameterInfo.DefaultValue; if (obj == null || obj == DBNull.Value || obj is Missing) { obj = (parameterInfo.ParameterType.IsValueType ? Activator.CreateInstance(parameterInfo.ParameterType) : null); } array[i] = obj; } return array; } private static PinType ResolvePingPinType() { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) string[] array = new string[2] { "Ping", "Shout" }; for (int i = 0; i < array.Length; i++) { if (Enum.TryParse<PinType>(array[i], out PinType result) && Enum.IsDefined(typeof(PinType), result)) { return result; } } return (PinType)0; } private static Exception Unwrap(Exception ex) { return (ex as TargetInvocationException)?.InnerException ?? ex; } public void LocalMessage(string message) { try { Chat instance = Chat.instance; if (!((Object)(object)instance == (Object)null)) { ((Terminal)instance).AddString("<color=#7ec8e3>ValheimRelay</color>: " + message); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not write to chat: " + ex.Message); } } } public sealed class GameCodeChannel : IGameChannel { private const string RpcAnnounce = "ValheimRelay_Code"; private const string RpcRequest = "ValheimRelay_CodeRequest"; private const string ChatPrefix = "[vrelay]"; private readonly ILog _log; private readonly Func<bool> _chatFallbackEnabled; private bool _registered; private bool _rpcAcknowledged; private bool _useChatFallback; public bool IsReady { get { if (_registered) { return ZRoutedRpc.instance != null; } return false; } } public bool RpcWorks => _rpcAcknowledged; public event Action<CodeAnnouncement>? CodeAnnounced; public event Action? CodeRequested; public GameCodeChannel(ILog log, Func<bool>? chatFallbackEnabled = null) { _log = log ?? throw new ArgumentNullException("log"); _chatFallbackEnabled = chatFallbackEnabled ?? ((Func<bool>)(() => true)); } public void Register() { if (_registered) { return; } try { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { LogExtensions.Warn(_log, "no ZRoutedRpc yet; the code channel will register later"); return; } instance.Register<string, long>("ValheimRelay_Code", (Action<long, string, long>)OnRpcAnnounce); instance.Register("ValheimRelay_CodeRequest", (Action<long>)OnRpcRequest); _registered = true; LogExtensions.Info(_log, "code channel registered"); } catch (Exception ex) { LogExtensions.Warn(_log, "could not register the code RPC; falling back to chat: " + ex.Message); _useChatFallback = true; } } public void Reset() { _rpcAcknowledged = false; _useChatFallback = false; } public void EnableChatFallback() { if (!_useChatFallback && !_rpcAcknowledged) { _useChatFallback = true; LogExtensions.Info(_log, "no peer answered over RPC; using the chat channel for the session code"); } } public void RequestCode() { try { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ValheimRelay_CodeRequest", Array.Empty<object>()); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not ask peers for the code: " + ex.Message); } if (_useChatFallback && _chatFallbackEnabled()) { SendChat("[vrelay] ?"); } } public void AnnounceCode(string code, long epoch) { if (string.IsNullOrEmpty(code)) { return; } try { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ValheimRelay_Code", new object[2] { code, epoch }); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not announce the code over RPC: " + ex.Message); } if (_useChatFallback && _chatFallbackEnabled()) { SendChat("[vrelay] " + code + " " + epoch.ToString(CultureInfo.InvariantCulture)); } } private void OnRpcAnnounce(long sender, string code, long epoch) { _rpcAcknowledged = true; Raise(code, epoch, sender); } private void OnRpcRequest(long sender) { _rpcAcknowledged = true; this.CodeRequested?.Invoke(); } public bool TryConsumeChat(long sender, string? text) { if (string.IsNullOrEmpty(text)) { return false; } string text2 = text.Trim(); if (!text2.StartsWith("[vrelay]", StringComparison.Ordinal)) { return false; } string text3 = text2.Substring("[vrelay]".Length).Trim(); if (text3 == "?") { this.CodeRequested?.Invoke(); return true; } string[] array = text3.Split(new char[1] { ' ' }); if (array.Length >= 1 && array[0].Length > 0) { long result = 1L; if (array.Length >= 2) { long.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out result); } Raise(array[0], result, sender); } return true; } private void SendChat(string message) { try { if (!((Object)(object)Player.m_localPlayer == (Object)null)) { Chat instance = Chat.instance; if (instance != null) { instance.SendText((Type)1, message); } } } catch (Exception ex) { LogExtensions.Warn(_log, "could not send on the chat channel: " + ex.Message); } } private void Raise(string code, long epoch, long sender) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(code)) { this.CodeAnnounced?.Invoke(new CodeAnnouncement(code, epoch, sender)); } } } [BepInPlugin("com.valheimrelay.mod", "ValheimRelay", "0.1.0")] [BepInProcess("valheim.exe")] [BepInProcess("valheim_server.exe")] public sealed class ValheimRelayPlugin : BaseUnityPlugin { public const string PluginId = "com.valheimrelay.mod"; public const string PluginName = "ValheimRelay"; public const string PluginVersion = "0.1.0"; private Harmony? _harmony; public static ValheimRelayPlugin? Instance { get; private set; } public PluginConfig Settings { get; private set; } public BepInExLog Log { get; private set; } public RelayBehaviour? Behaviour { get; private set; } public string SessionStorePath => Path.Combine(Paths.ConfigPath, "ValheimRelay.session.json"); private void Awake() { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Expected O, but got Unknown Instance = this; Log = new BepInExLog(((BaseUnityPlugin)this).Logger); Settings = new PluginConfig(((BaseUnityPlugin)this).Config); if (!Settings.Enabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"ValheimRelay is disabled in config; not patching."); return; } try { _harmony = new Harmony("com.valheimrelay.mod"); _harmony.PatchAll(typeof(ValheimRelayPlugin).Assembly); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("ValheimRelay could not apply its patches and will stay dormant. This usually means the game updated. Details: " + ex)); return; } GameObject val = new GameObject("ValheimRelay"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; Behaviour = val.AddComponent<RelayBehaviour>(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"ValheimRelay 0.1.0 loaded."); } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } Instance = null; } } public sealed class BepInExLog : ILog { private readonly ManualLogSource _source; public BepInExLog(ManualLogSource source) { _source = source ?? throw new ArgumentNullException("source"); } public void Log(LogLevel level, string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected I4, but got Unknown switch ((int)level) { case 0: _source.LogDebug((object)message); break; case 2: _source.LogWarning((object)message); break; case 3: _source.LogError((object)message); break; default: _source.LogInfo((object)message); break; } } } public sealed class FileReclaimStorage : IReclaimStorage { private readonly string _path; private readonly ILog _log; public FileReclaimStorage(string path, ILog log) { _path = path ?? throw new ArgumentNullException("path"); _log = log ?? throw new ArgumentNullException("log"); } public string? Read() { if (!File.Exists(_path)) { return null; } return File.ReadAllText(_path); } public void Write(string contents) { string directoryName = Path.GetDirectoryName(_path); if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } string text = _path + ".tmp"; File.WriteAllText(text, contents); if (File.Exists(_path)) { File.Delete(_path); } File.Move(text, _path); } } public sealed class RelayBehaviour : MonoBehaviour { private readonly Dictionary<string, object> _pins = new Dictionary<string, object>(StringComparer.Ordinal); private ValheimRelayPlugin _plugin; private GameBridge _bridge; private GameCodeChannel _channel; private ClientWebSocketTransport _transport; private ReclaimStore _reclaim; private RelaySession? _session; private RelayPanel _panel; private bool _sessionRunning; private float _discoveryDeadline; private bool _fallbackConsidered; private bool _pingSenderWarned; private bool _pingSilenceWarned; private int _pingsForwarded; private int _pingsRejected; private const int PingRejectionsBeforeWarning = 3; public RelaySession? Session => _session; public string? Code { get { RelaySession? session = _session; if (session == null) { return null; } return session.Code; } } public SessionState State { get { //IL_000c: Unknown result type (might be due to invalid IL or missing references) RelaySession? session = _session; if (session == null) { return (SessionState)0; } return session.State; } } public string? ShareText { get { string code = Code; if (code != null) { return _plugin.Settings.BuildShareText(code, WorldSeed); } return null; } } private string? WorldSeed { get { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) WorldInfo val = _bridge.ReadWorld(); return ((WorldInfo)(ref val)).Seed; } } private void Awake() { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown _plugin = ValheimRelayPlugin.Instance ?? throw new InvalidOperationException("RelayBehaviour created without a plugin"); _bridge = new GameBridge((ILog)(object)_plugin.Log, () => _plugin.Settings.PingStyle.Value); _channel = new GameCodeChannel((ILog)(object)_plugin.Log, () => _plugin.Settings.AnnounceInChat.Value); _transport = new ClientWebSocketTransport((ILog)(object)_plugin.Log, 256); _reclaim = new ReclaimStore((IReclaimStorage)(object)new FileReclaimStorage(_plugin.SessionStorePath, (ILog)(object)_plugin.Log), (ILog)(object)_plugin.Log); _panel = new RelayPanel(this, _plugin.Settings); } public void OnGameStart() { _channel.Register(); } public bool TryConsumeChat(long sender, string? text) { return _channel.TryConsumeChat(sender, text); } public void OnGamePing(Vector3 position, long senderId) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) _bridge.NoteGamePing(position.x, position.z); if (!_plugin.Settings.ShareMyPings.Value || !_sessionRunning) { return; } if (!IsLocalPing(senderId)) { WarnIfNothingIsEverForwarded(senderId); return; } _pingsForwarded++; RelaySession? session = _session; if (session != null) { session.SendPing((double)position.x, (double)position.z); } } private void WarnIfNothingIsEverForwarded(long senderId) { if (!_pingSilenceWarned && _pingsForwarded <= 0 && ++_pingsRejected >= 3) { _pingSilenceWarned = true; LogExtensions.Warn((ILog)(object)_plugin.Log, "seen " + _pingsRejected + " pings in game and forwarded none of them: every one read as another player's (self=" + _bridge.SelfPeerId + ", last sender=" + senderId + "). If you are alone in this world that is a bug — pings made here are not reaching the web map, while pings FROM the map still work."); } } private bool IsLocalPing(long senderId) { long selfPeerId = _bridge.SelfPeerId; if (selfPeerId != 0L && senderId != 0L) { return senderId == selfPeerId; } if (!_pingSenderWarned) { _pingSenderWarned = true; LogExtensions.Warn((ILog)(object)_plugin.Log, "could not tell whose ping this is (self=" + selfPeerId + ", sender=" + senderId + "), so pings made in game are being forwarded without that check. If several players here run the mod, the web map may draw one ring per player for a single ping."); } return true; } public void StartSession() { //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Expected O, but got Unknown //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Expected O, but got Unknown if (!_sessionRunning && _plugin.Settings.Enabled.Value && GameBridge.IsWorldLoaded && GameBridge.HasLocalPlayer) { _channel.Register(); _channel.Reset(); SessionOptions val = _plugin.Settings.ToSessionOptions(); RelaySession? session = _session; if (session != null) { session.Dispose(); } _session = new RelaySession(val, (IRelayTransport)(object)_transport, (IGameChannel)(object)_channel, (IPeerView)(object)_bridge, (IClock)(object)new UnityClock(), (ILog)(object)_plugin.Log, _reclaim, (Func<double>)null); _session.Notice += OnNotice; _session.PingReceived += OnPingReceived; _session.MarkerReceived += OnMarkerReceived; byte[] array = default(byte[]); if (!StableUid.TryDecodeSalt(_reclaim.Salt, ref array)) { LogExtensions.Error((ILog)(object)_plugin.Log, "could not establish an identity salt; not starting a session"); _sessionRunning = false; return; } string text = StableUid.Derive(_bridge.ProfileId, array); _session.Start(new SessionIdentity(_bridge.PlayerName, text, "0.1.0", _bridge.ReadWorld())); _sessionRunning = true; _fallbackConsidered = false; _discoveryDeadline = Time.realtimeSinceStartup + (float)val.DiscoveryWindow.TotalSeconds; } } public void StopSession(string reason) { if (_sessionRunning) { _sessionRunning = false; ClearPins(); RelaySession? session = _session; if (session != null) { session.Stop(reason); } } } private void Update() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) _bridge.ExpirePings(); if (_session == null) { return; } if (Input.GetKeyDown(_plugin.Settings.ToggleKey.Value) && ToggleModifierHeld()) { _panel.Toggle(); } if (!_sessionRunning) { _session.Tick(); return; } if (!_fallbackConsidered && Time.realtimeSinceStartup >= _discoveryDeadline) { _fallbackConsidered = true; if (!_channel.RpcWorks) { _channel.EnableChatFallback(); } } SubmitPosition(); _session.Tick(); } private bool ToggleModifierHeld() { if (!_plugin.Settings.ToggleRequiresShift.Value) { return true; } if (!Input.GetKey((KeyCode)304)) { return Input.GetKey((KeyCode)303); } return true; } private void SubmitPosition() { if (_plugin.Settings.ShareMyPosition.Value && _bridge.TryReadPosition(_plugin.Settings.ShareHealth.Value, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), out var sample)) { _session.SubmitPosition(ref sample); } } private void OnGUI() { _panel.Draw(); } private void OnNotice(SessionNotice notice) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected I4, but got Unknown NoticeKind kind = notice.Kind; switch ((int)kind) { case 0: if (_plugin.Settings.AnnounceInChat.Value && notice.Code != null) { _bridge.LocalMessage(_plugin.Settings.HasMapLink ? (notice.Code + " · " + _plugin.Settings.BuildShareText(notice.Code, WorldSeed) + " (Shift+F8 copies it and shows a QR)") : ("map code " + notice.Code + " (Shift+F8 for the panel)")); } break; case 1: _bridge.LocalMessage(notice.Message); break; case 4: _bridge.LocalMessage(notice.Message); break; } LogExtensions.Info((ILog)(object)_plugin.Log, notice.Message); } private void OnPingReceived(PingFrame ping) { _bridge.ShowPing(((PingFrame)(ref ping)).X, ((PingFrame)(ref ping)).Z, ((PingFrame)(ref ping)).Name); } private void OnMarkerReceived(MarkerFrame marker) { if (!_plugin.Settings.AcceptMapMarkers.Value) { return; } if (marker.IsRemove) { if (_pins.TryGetValue(marker.Id, out object value)) { _bridge.RemovePin(value); _pins.Remove(marker.Id); } return; } if (_pins.TryGetValue(marker.Id, out object value2)) { _bridge.RemovePin(value2); _pins.Remove(marker.Id); } object obj = _bridge.AddPin(marker.X, marker.Z, marker.Label, marker.Icon); if (obj != null) { _pins[marker.Id] = obj; } } private void ClearPins() { foreach (object value in _pins.Values) { _bridge.RemovePin(value); } _pins.Clear(); _bridge.ClearPings(); } public void RetryAfterRoomFull() { RelaySession? session = _session; if (session != null) { session.Retry(); } } private void OnDestroy() { RelaySession? session = _session; if (session != null) { session.Dispose(); } _transport.Dispose(); _panel.Release(); } } public sealed class UnityClock : IClock { public TimeSpan Elapsed => TimeSpan.FromSeconds(Time.realtimeSinceStartup); public long UnixTimeMilliseconds => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } internal static class QrTexture { private const int QuietZone = 4; internal static Texture2D Create(QrCode qr, int targetPixels) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) int num = qr.Size + 8; int num2 = Mathf.Max(2, Mathf.RoundToInt((float)targetPixels / (float)num)); int num3 = num * num2; Texture2D val = new Texture2D(num3, num3, (TextureFormat)3, false) { filterMode = (FilterMode)0, wrapMode = (TextureWrapMode)1, hideFlags = (HideFlags)61 }; Color32 val2 = default(Color32); ((Color32)(ref val2))..ctor(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); Color32 val3 = default(Color32); ((Color32)(ref val3))..ctor((byte)0, (byte)0, (byte)0, byte.MaxValue); Color32[] array = (Color32[])(object)new Color32[num3 * num3]; for (int i = 0; i < array.Length; i++) { array[i] = val2; } for (int j = 0; j < qr.Size; j++) { for (int k = 0; k < qr.Size; k++) { if (!qr[k, j]) { continue; } int num4 = (k + 4) * num2; int num5 = (num - 1 - (j + 4)) * num2; for (int l = 0; l < num2; l++) { int num6 = (num5 + l) * num3; for (int m = 0; m < num2; m++) { array[num6 + num4 + m] = val3; } } } } val.SetPixels32(array); val.Apply(false, false); return val; } } public sealed class RelayPanel { private const int Width = 330; private const int BaseHeight = 190; private const int QrTargetPixels = 160; private const int QrGap = 10; private readonly RelayBehaviour _behaviour; private readonly PluginConfig _config; private bool _visible; private float _copiedAt = float.NegativeInfinity; private GUIStyle? _codeStyle; private GUIStyle? _noteStyle; private string? _code; private SessionState _state; private string? _shareText; private string? _copiedText; private bool _wasVisible; private Texture2D? _qr; private string? _qrFor; private int _qrPixels; private int Height => 190 + ((!((Object)(object)_qr == (Object)null)) ? (10 + _qrPixels) : 0); public RelayPanel(RelayBehaviour behaviour, PluginConfig config) { _behaviour = behaviour ?? throw new ArgumentNullException("behaviour"); _config = config ?? throw new ArgumentNullException("config"); } public void Toggle() { _visible = !_visible; } public void Release() { ReleaseQr(); } public void Draw() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Invalid comparison between Unknown and I4 if (!_config.Enabled.Value) { return; } EnsureStyles(); if ((int)Event.current.type == 8) { Refresh(); } DrawIndicator(); if (!_visible) { return; } int height = Height; float num = Mathf.Max(10f, Mathf.Min(20f, (float)(Screen.height - height) - 10f)); Rect val = default(Rect); ((Rect)(ref val))..ctor((float)(Screen.width - 330 - 20), num, 330f, (float)height); GUI.Box(val, "ValheimRelay"); GUILayout.BeginArea(new Rect(((Rect)(ref val)).x + 12f, ((Rect)(ref val)).y + 26f, ((Rect)(ref val)).width - 24f, ((Rect)(ref val)).height - 38f)); if (_code == null) { GUILayout.Label(DescribeState(_state), Array.Empty<GUILayoutOption>()); } else { GUILayout.Label(_code, _codeStyle, Array.Empty<GUILayoutOption>()); GUILayout.Label(DescribeState(_state), Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (GUILayout.Button((Time.realtimeSinceStartup - _copiedAt < 2f) ? "Copied to clipboard" : CopyButtonLabel(), Array.Empty<GUILayoutOption>()) && _shareText != null) { CopyToClipboard(_shareText); } GUILayout.EndHorizontal(); GUILayout.Label("Anyone with this code can watch everyone in this session move, for as long as it lasts. Share it like a link, not a name" + (((Object)(object)_qr == (Object)null) ? "." : " — and don't leave this panel up on stream."), _noteStyle, Array.Empty<GUILayoutOption>()); DrawQr(); } if ((int)_state == 6 && GUILayout.Button("Retry", Array.Empty<GUILayoutOption>())) { _behaviour.RetryAfterRoomFull(); } GUILayout.EndArea(); } private string CopyButtonLabel() { if (!_config.HasMapLink) { return "Copy code"; } return "Copy map link"; } private void Refresh() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) _code = _behaviour.Code; _state = _behaviour.State; bool flag = _visible && !_wasVisible; _wasVisible = _visible; if (!_visible) { _shareText = null; return; } _shareText = ((_code == null) ? null : _behaviour.ShareText); if (_shareText != null && (flag || _shareText != _copiedText)) { CopyToClipboard(_shareText); } RefreshQr(); } private void CopyToClipboard(string shareText) { _copiedText = shareText; _copiedAt = Time.realtimeSinceStartup; if (GUIUtility.systemCopyBuffer != shareText) { GUIUtility.systemCopyBuffer = shareText; } } private void RefreshQr() { if (_shareText == null || !_config.HasMapLink) { ReleaseQr(); } else if (!(_qrFor == _shareText)) { ReleaseQr(); _qrFor = _shareText; QrCode val = QrCode.Encode(_shareText); if (val != null) { _qr = QrTexture.Create(val, 160); _qrPixels = ((Texture)_qr).width; } } } private void ReleaseQr() { if ((Object)(object)_qr != (Object)null) { Object.Destroy((Object)(object)_qr); } _qr = null; _qrFor = null; _qrPixels = 0; } private void DrawQr() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Invalid comparison between Unknown and I4 //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_qr == (Object)null)) { GUILayout.Space(10f); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.FlexibleSpace(); Rect rect = GUILayoutUtility.GetRect((float)_qrPixels, (float)_qrPixels, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(false) }); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); if ((int)Event.current.type == 7) { ((Rect)(ref rect)).x = Mathf.Round(((Rect)(ref rect)).x); ((Rect)(ref rect)).y = Mathf.Round(((Rect)(ref rect)).y); Color color = GUI.color; GUI.color = Color.white; GUI.DrawTexture(rect, (Texture)(object)_qr); GUI.color = color; } } } private void DrawIndicator() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Invalid comparison between Unknown and I4 //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Invalid comparison between Unknown and I4 //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Invalid comparison between Unknown and I4 //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) if ((int)_state != 0 && (int)_state != 7) { bool value = _config.ShareMyPosition.Value; SessionState state = _state; Color val = (((int)state == 4) ? (value ? new Color(0.49f, 0.78f, 0.89f) : new Color(0.7f, 0.7f, 0.7f)) : (((int)state != 6) ? new Color(0.9f, 0.8f, 0.4f) : new Color(0.9f, 0.45f, 0.45f))); Color color = val; state = _state; string text = (((int)state == 4) ? (value ? "relay ●" : "relay ○ (hidden)") : (((int)state != 6) ? "relay …" : "relay ✕ full")); string text2 = text; Color color2 = GUI.color; GUI.color = color; GUI.Label(new Rect((float)(Screen.width - 130), (float)(Screen.height - 28), 120f, 20f), text2); GUI.color = color2; } } private static string DescribeState(SessionState state) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected I4, but got Unknown return (state - 1) switch { 0 => "looking for a session…", 1 => "creating a session…", 2 => "joining…", 3 => "connected", 4 => "reconnecting…", 5 => "this session is full (16 players)", 6 => "not connected", _ => "idle", }; } private void EnsureStyles() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Expected O, but got Unknown if (_codeStyle == null) { _codeStyle = new GUIStyle(GUI.skin.label) { fontSize = 26, fontStyle = (FontStyle)1 }; } if (_noteStyle == null) { _noteStyle = new GUIStyle(GUI.skin.label) { fontSize = 10, wordWrap = true }; } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "ValheimRelay"; public const string PLUGIN_NAME = "ValheimRelay"; public const string PLUGIN_VERSION = "0.1.0"; } } namespace ValheimRelay.Plugin.Patches { [HarmonyPatch] internal static class ChatMessagePatch { [HarmonyPrepare] private static bool Prepare(MethodBase? original) { if (original != null) { return true; } bool num = AccessTools.Method(typeof(Chat), "OnNewChatMessage", (Type[])null, (Type[])null) != null; if (!num) { ValheimRelayPlugin? instance = ValheimRelayPlugin.Instance; if (instance == null) { return num; } LogExtensions.Warn((ILog)(object)instance.Log, "Chat.OnNewChatMessage was not found, so the chat fallback for the session code is unavailable. The routed RPC channel still works; if the game has updated, this patch needs its signature checked."); } return num; } [HarmonyTargetMethod] private static MethodBase? TargetMethod() { return AccessTools.Method(typeof(Chat), "OnNewChatMessage", (Type[])null, (Type[])null); } private static bool Prefix(object[] __args) { RelayBehaviour behaviour = PatchHelpers.Behaviour; if ((Object)(object)behaviour == (Object)null) { return true; } try { long num = 0L; string text = null; foreach (object obj in __args) { if (!(obj is long num2)) { if (obj is string text2 && text == null && text2.Length > 0) { text = text2; } } else if (num == 0L) { num = num2; } } if (text == null) { return true; } return !behaviour.TryConsumeChat(num, text); } catch (Exception ex) { ValheimRelayPlugin? instance = ValheimRelayPlugin.Instance; if (instance != null) { LogExtensions.Warn((ILog)(object)instance.Log, "chat patch error: " + ex.Message); } return true; } } } [HarmonyPatch] [HarmonyPriority(800)] internal static class ChatPingPatch { [HarmonyPrepare] private static bool Prepare(MethodBase? original) { if (original != null) { return true; } if (GameBridge.FindChatPingMethod() != null) { return true; } ValheimRelayPlugin? instance = ValheimRelayPlugin.Instance; if (instance != null) { LogExtensions.Warn((ILog)(object)instance.Log, "no Chat.OnNewChatMessage overload on this build carries a position and a ping talker type, so pings made in game will not reach the web map. Everything else, including pings FROM the map, is unaffected."); } return false; } [HarmonyTargetMethod] private static MethodBase? TargetMethod() { return GameBridge.FindChatPingMethod(); } private static void Prefix(object[] __args) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) if (GameBridge.IsRenderingPing) { return; } RelayBehaviour behaviour = PatchHelpers.Behaviour; if ((Object)(object)behaviour == (Object)null) { return; } try { if (GameBridge.TryReadPingArgs(__args, out var position, out var senderId)) { behaviour.OnGamePing(position, senderId); } } catch (Exception ex) { ValheimRelayPlugin? instance = ValheimRelayPlugin.Instance; if (instance != null) { LogExtensions.Warn((ILog)(object)instance.Log, "ping capture error: " + ex.Message); } } } } internal static class PatchHelpers { internal static RelayBehaviour? Behaviour => ValheimRelayPlugin.Instance?.Behaviour; } [HarmonyPatch(typeof(Game), "Start")] internal static class GameStartPatch { private static void Postfix() { PatchHelpers.Behaviour?.OnGameStart(); } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class PlayerOnSpawnedPatch { private static void Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { PatchHelpers.Behaviour?.StartSession(); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] internal static class ZNetShutdownPatch { private static void Prefix() { PatchHelpers.Behaviour?.StopSession("left the world"); } } [HarmonyPatch(typeof(Game), "Logout")] internal static class GameLogoutPatch { private static void Prefix() { PatchHelpers.Behaviour?.StopSession("logged out"); } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }