Decompiled source of Multibonk v0.2.7
Mods/Multibonk.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using HarmonyLib; using Il2Cpp; using Il2CppAssets.Scripts.Actors; using Il2CppAssets.Scripts.Actors.Enemies; using Il2CppAssets.Scripts.Actors.Player; using Il2CppAssets.Scripts.Game.Combat; using Il2CppAssets.Scripts.Game.Combat.ConstantAttacks; using Il2CppAssets.Scripts.Game.Other; using Il2CppAssets.Scripts.Game.Spawning.New; using Il2CppAssets.Scripts.Game.Spawning.New.Timelines; using Il2CppAssets.Scripts.Inventory.Stats; using Il2CppAssets.Scripts.Inventory__Items__Pickups; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Chests; using Il2CppAssets.Scripts.Inventory__Items__Pickups.GoldAndMoney; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Interactables; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Items; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Items.ItemImplementations; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Pickups; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Stats; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Weapons; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Weapons.Attacks; using Il2CppAssets.Scripts.Inventory__Items__Pickups.Weapons.Projectiles; using Il2CppAssets.Scripts.Managers; using Il2CppAssets.Scripts.Menu.Shop; using Il2CppAssets.Scripts.Objects.Pooling; using Il2CppAssets.Scripts.Player.Movement; using Il2CppAssets.Scripts.Saves___Serialization.Progression.Achievements; using Il2CppAssets.Scripts.Saves___Serialization.Progression.Stats; using Il2CppAssets.Scripts.UI.HUD; using Il2CppAssets.Scripts.UI.InGame.FullMap; using Il2CppAssets.Scripts.UI.InGame.Levelup; using Il2CppAssets.Scripts.UI.InGame.Rewards; using Il2CppAssets.Scripts.Utility; using Il2CppAssets.Scripts._Data; using Il2CppAssets.Scripts._Data.MapsAndStages; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppInventory__Items__Pickups; using Il2CppInventory__Items__Pickups.Xp_and_Levels; using Il2CppSystem; using Il2CppSystem.Collections.Generic; using Il2CppSystem.Reflection; using Il2CppTMPro; using Il2CppUtility; using MelonLoader; using Microsoft.CodeAnalysis; using Multibonk; using Multibonk.Modules; using Multibonk.Modules.BossRewards; using Multibonk.Modules.CombatVisuals; using Multibonk.Modules.Control; using Multibonk.Modules.DedicatedHost; using Multibonk.Modules.Diagnostics; using Multibonk.Modules.Enemies; using Multibonk.Modules.FinalFight; using Multibonk.Modules.Interactables; using Multibonk.Modules.MapSync; using Multibonk.Modules.Pickups; using Multibonk.Modules.PlayerState; using Multibonk.Modules.Players; using Multibonk.Modules.Purchases; using Multibonk.Modules.RewardChoices; using Multibonk.Modules.RunState; using Multibonk.Modules.Session; using Multibonk.Modules.SharedRewards; using Multibonk.Modules.TimeSync; using Multibonk.Net; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: MelonInfo(typeof(Mod), "Multibonk", "0.2.5", "AvinadavCoh", null)] [assembly: MelonGame("Ved", "Megabonk")] [assembly: HarmonyDontPatchAll] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("Multibonk")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("0.2.5.0")] [assembly: AssemblyInformationalVersion("0.2.5")] [assembly: AssemblyProduct("Multibonk")] [assembly: AssemblyTitle("Multibonk")] [assembly: AssemblyVersion("0.2.5.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 Multibonk { public static class Log { private static string _lastStatus = "No sync activity yet."; public static string LastStatus => Volatile.Read(in _lastStatus); private static void Remember(string message) { Volatile.Write(ref _lastStatus, (message == null) ? "" : ((message.Length <= 180) ? message : (message.Substring(0, 177) + "..."))); } public static void Info(string message) { Remember(message); MelonLogger.Msg(message); } public static void Warn(string message) { Remember(message); MelonLogger.Warning(message); } public static void Error(string message) { Remember(message); MelonLogger.Error(message); } } public sealed class Mod : MelonMod { private const string DevJoinHost = "127.0.0.1"; private const int DevJoinPort = 25565; private static readonly int DevStartGameSeed = new Random().Next(int.MinValue, int.MaxValue); private static readonly TimeSpan DevJoinTimeout = TimeSpan.FromSeconds(10.0); private Task _joinTask; private CancellationTokenSource _joinCancellation; private bool _startHostAfterJoinStops; private bool _isQuitting; private string _joinAddress = "127.0.0.1"; private string _connectionMessage = "Choose Host or enter the server address to Join."; private bool _panelOpen = true; private bool _wasGameplayReady; private bool _ownsCursor; private bool _savedCursorVisible; private CursorLockMode _savedCursorLock; private Vector2 _rosterScroll; private int _lastAddressInputFrame = -1; private bool _tracedUpdate; private bool _tracedGui; private static int DevHostPort => HostConfiguration.Current.Port; internal static string ConnectionMessage => Instance?._connectionMessage ?? "Offline"; internal static Mod Instance { get; private set; } internal static void RequestDisconnect() { Instance?.Disconnect(); } internal static void RequestJoin(string ip) { if (ModuleHost.IsInstalled) { Instance?.HandleJoinTrigger(ip); } } private void Disconnect() { _startHostAfterJoinStops = false; CancelPendingJoin(); Multibonk.Net.Net.StopAll(); _connectionMessage = "Disconnected."; } internal static void RequestHost() { if (ModuleHost.IsInstalled) { Instance?.HandleHostTrigger(); } } internal static void RequestJoin() { if (ModuleHost.IsInstalled) { Instance?.HandleJoinTrigger(); } } internal static void RequestStartGame() { if (ModuleHost.IsInstalled) { Instance?.HandleStartGameTrigger(); } } public override void OnInitializeMelon() { Instance = this; ((MelonBase)this).LoggerInstance.Msg("Multibonk (rebuild) initializing…"); ModuleHost.Install(); Multibonk.Net.Net.OnSessionEnded += delegate { _connectionMessage = "Disconnected. Choose Host or Join to reconnect."; }; PrintDevTriggerLegend(); ((MelonBase)this).LoggerInstance.Msg("Multibonk initialized."); if (Array.IndexOf(Environment.GetCommandLineArgs(), "--multibonk-no-patches") >= 0) { ((MelonBase)this).LoggerInstance.Msg("[CrashTrace] Diagnostic: skipping hooks"); return; } Type[] types = typeof(Mod).Assembly.GetTypes(); foreach (Type type in types) { if (Attribute.IsDefined(type, typeof(HarmonyPatch))) { ((MelonBase)this).LoggerInstance.Msg("[PatchTrace] BEGIN " + type.FullName); ((MelonBase)this).HarmonyInstance.CreateClassProcessor(type).Patch(); ((MelonBase)this).LoggerInstance.Msg("[PatchTrace] END " + type.FullName); } } } public override void OnLateInitializeMelon() { ((MelonBase)this).LoggerInstance.Msg("[CrashTrace] LateInitialize"); } public override void OnUpdate() { if (ModuleHost.IsInstalled) { if (!_tracedUpdate) { ((MelonBase)this).LoggerInstance.Msg("[CrashTrace] OnUpdate drain"); } MainThread.Drain(); if (!_tracedUpdate) { ((MelonBase)this).LoggerInstance.Msg("[CrashTrace] OnUpdate receive"); } Multibonk.Net.Net.PumpReceive(); if (!_tracedUpdate) { ((MelonBase)this).LoggerInstance.Msg("[CrashTrace] OnUpdate modules"); } ModuleHost.TickAll(); if (!_tracedUpdate) { ((MelonBase)this).LoggerInstance.Msg("[CrashTrace] OnUpdate triggers"); } CheckDevTriggers(); _tracedUpdate = true; } } public override void OnGUI() { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: 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_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected O, but got Unknown //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) if (ModuleHost.IsInstalled) { if (!_tracedGui) { ((MelonBase)this).LoggerInstance.Msg("[CrashTrace] OnGUI panel"); } DrawConnectionPanel(); if (!_tracedGui) { ((MelonBase)this).LoggerInstance.Msg("[CrashTrace] OnGUI panel returned"); } _tracedGui = true; string text; try { text = ModuleHost.RewardChoices.OverlayText() ?? ModuleHost.RunState.LevelUpOverlayText(); } catch { return; } if (!string.IsNullOrEmpty(text)) { GUIStyle val = new GUIStyle(GUI.skin.box) { fontSize = 20, alignment = (TextAnchor)4, fontStyle = (FontStyle)1 }; val.normal.textColor = Color.white; float num = 560f; float num2 = 54f; GUI.Box(new Rect(((float)Screen.width - num) / 2f, (float)Screen.height * 0.12f, num, num2), text, val); } } } public override void OnApplicationQuit() { _isQuitting = true; CancelPendingJoin(); Multibonk.Net.Net.StopAll(); } private void DrawConnectionPanel() { //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_05dd: 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_0101: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Expected O, but got Unknown //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_0214: Unknown result type (might be due to invalid IL or missing references) //IL_024e: Unknown result type (might be due to invalid IL or missing references) //IL_02a5: Unknown result type (might be due to invalid IL or missing references) //IL_02f6: Unknown result type (might be due to invalid IL or missing references) //IL_037e: Unknown result type (might be due to invalid IL or missing references) //IL_03c7: Unknown result type (might be due to invalid IL or missing references) //IL_0401: Unknown result type (might be due to invalid IL or missing references) //IL_0407: Unknown result type (might be due to invalid IL or missing references) //IL_0430: Unknown result type (might be due to invalid IL or missing references) //IL_0435: Unknown result type (might be due to invalid IL or missing references) //IL_043a: Unknown result type (might be due to invalid IL or missing references) //IL_05c7: Unknown result type (might be due to invalid IL or missing references) //IL_04b1: Unknown result type (might be due to invalid IL or missing references) Color color = GUI.color; bool enabled = GUI.enabled; try { bool inSession = Multibonk.Net.Net.InSession; bool flag = _joinCancellation != null; string text = (flag ? "Connecting..." : ((!inSession) ? "Offline" : (Multibonk.Net.Net.IsHost ? $"Hosting :{DevHostPort}" : ("Connected to " + _joinAddress)))); float num = Mathf.Min(370f, (float)Screen.width - 24f); float num2 = (float)Screen.width - num - 12f; if (GUI.Button(new Rect(num2, 12f, num, 30f), "MULTIBONK | " + text + " [F5]")) { _panelOpen = !_panelOpen; } if (!_panelOpen) { return; } GUI.Box(new Rect(num2, 44f, num, 346f), ""); GUIStyle val = new GUIStyle(GUI.skin.label) { fontSize = 14, wordWrap = true, richText = false }; GUI.Label(new Rect(num2 + 12f, 52f, num - 24f, 24f), "Server address (host:port)", val); GUI.enabled = !inSession && !flag; Rect val2 = default(Rect); ((Rect)(ref val2))..ctor(num2 + 12f, 78f, num - 24f, 28f); GUI.Box(val2, ""); if (GUI.enabled) { CaptureAddressInput(); } string text2 = ((GUI.enabled && Mathf.RoundToInt(Time.unscaledTime * 2f) % 2 == 0) ? "_" : ""); GUI.Label(new Rect(((Rect)(ref val2)).x + 6f, ((Rect)(ref val2)).y + 4f, ((Rect)(ref val2)).width - 12f, ((Rect)(ref val2)).height - 4f), _joinAddress + text2, val); if (GUI.Button(new Rect(num2 + 12f, 114f, (num - 32f) / 2f, 30f), "Host")) { SafeUiAction(HandleHostTrigger); } if (GUI.Button(new Rect(num2 + 16f + (num - 24f) / 2f, 114f, (num - 32f) / 2f, 30f), "Join")) { SafeUiAction(delegate { HandleJoinTrigger(_joinAddress); }); } GUI.enabled = inSession || flag; if (GUI.Button(new Rect(num2 + 12f, 152f, (num - 32f) / 2f, 30f), flag ? "Cancel" : "Disconnect")) { Disconnect(); } int enabled2; if (inSession && Multibonk.Net.Net.IsHost) { if (ModuleHost.Session.LocalGameplayReady) { GameManager instance = GameManager.Instance; enabled2 = ((instance != null && instance.isGameOver) ? 1 : 0); } else { enabled2 = 1; } } else { enabled2 = 0; } GUI.enabled = (byte)enabled2 != 0; if (GUI.Button(new Rect(num2 + 16f + (num - 24f) / 2f, 152f, (num - 32f) / 2f, 30f), "Start Game")) { SafeUiAction(HandleStartGameTrigger); } GUI.enabled = true; GUI.Label(new Rect(num2 + 12f, 190f, num - 24f, 42f), _connectionMessage, val); List<LobbyPlayer> list = ModuleHost.Session.Players(); _rosterScroll = GUI.BeginScrollView(new Rect(num2 + 12f, 238f, num - 24f, 102f), _rosterScroll, new Rect(0f, 0f, num - 46f, (float)Mathf.Max(100, list.Count * 38))); for (int num3 = 0; num3 < list.Count; num3++) { LobbyPlayer lobbyPlayer = list[num3]; string value = (lobbyPlayer.IsDead ? "Downed" : ((lobbyPlayer.IsLocal ? ModuleHost.Session.LocalGameplayReady : ModuleHost.Session.IsPlayerGameLoaded(lobbyPlayer.Uuid)) ? "In game" : "In lobby")); GUI.Label(new Rect(0f, (float)(num3 * 38), num - 48f, 38f), $"{lobbyPlayer.Name}{(lobbyPlayer.IsLocal ? " (you)" : "")} | {lobbyPlayer.Character} | {value}\nHP {lobbyPlayer.CurrentHealth:0}/{lobbyPlayer.MaxHealth:0} | Level {lobbyPlayer.Level}", val); } GUI.EndScrollView(); GUI.Label(new Rect(num2 + 12f, 348f, num - 24f, 36f), Log.LastStatus, val); } finally { GUI.color = color; GUI.enabled = enabled; } } private void CaptureAddressInput() { int frameCount = Time.frameCount; if (_lastAddressInputFrame == frameCount) { return; } _lastAddressInputFrame = frameCount; string inputString = Input.inputString; if (string.IsNullOrEmpty(inputString)) { return; } string text = inputString; for (int i = 0; i < text.Length; i++) { char c = text[i]; switch (c) { case '\b': if (_joinAddress.Length > 0) { _joinAddress = _joinAddress.Substring(0, _joinAddress.Length - 1); } break; default: if (c != '\r') { if (_joinAddress.Length < 253 && (char.IsLetterOrDigit(c) || c == '.' || c == ':' || c == '-')) { _joinAddress += c; } break; } goto case '\n'; case '\n': SafeUiAction(delegate { HandleJoinTrigger(_joinAddress); }); break; } } } private void SafeUiAction(Action action) { try { action(); } catch (Exception ex) { _connectionMessage = ex.Message; Log.Error("[Connection] " + ex.Message); } } private void CheckDevTriggers() { //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) try { bool localGameplayReady = ModuleHost.Session.LocalGameplayReady; if (localGameplayReady && !_wasGameplayReady) { _panelOpen = false; } _wasGameplayReady = localGameplayReady; if (Input.GetKeyDown((KeyCode)286)) { _panelOpen = !_panelOpen; } if (localGameplayReady && _panelOpen) { if (!_ownsCursor) { _savedCursorLock = Cursor.lockState; _savedCursorVisible = Cursor.visible; _ownsCursor = true; } Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; } else if (_ownsCursor) { Cursor.lockState = _savedCursorLock; Cursor.visible = _savedCursorVisible; _ownsCursor = false; } if (Input.GetKeyDown((KeyCode)287)) { HandleHostTrigger(); } else if (Input.GetKeyDown((KeyCode)288)) { HandleJoinTrigger(); } else if (Input.GetKeyDown((KeyCode)289)) { HandleStartGameTrigger(); } } catch (Exception value) { Log.Error($"[DevTrigger] threw: {value}"); } } private void HandleHostTrigger() { if (_joinTask != null && !_joinTask.IsCompleted) { _startHostAfterJoinStops = true; CancelPendingJoin(); Log.Info("[DevTrigger] Cancelling the in-progress join before hosting..."); } else { StartHost(); } } private void HandleJoinTrigger(string ip = null) { if (_joinTask != null && !_joinTask.IsCompleted) { Log.Warn("[DevTrigger] A join attempt is already in progress - ignoring."); } else { BeginJoin(ip ?? _joinAddress); } } private void HandleStartGameTrigger() { Log.Info($"[DevTrigger] Sending START_GAME (seed={DevStartGameSeed}) to all clients..."); ModuleHost.Session.SendStartGameToAll(DevStartGameSeed); } private void BeginJoin(string ip) { ip = ip?.Trim(); if (!ServerAddress.TryParse(ip, out var host, out var port)) { throw new ArgumentException("Enter a valid server address, optionally followed by :port."); } _joinAddress = ip; _connectionMessage = $"Connecting to {host}:{port}..."; CancelPendingJoin(); _startHostAfterJoinStops = false; CancellationTokenSource cancellation = new CancellationTokenSource(); _joinCancellation = cancellation; Log.Info($"[DevTrigger] F7 - joining {host}:{port} (timeout {DevJoinTimeout.TotalSeconds:0}s)..."); _joinTask = Task.Run(delegate { Exception failure = null; try { Multibonk.Net.Net.JoinHost(host, port, DevJoinTimeout, cancellation.Token); } catch (Exception ex) { failure = ex; } MainThread.Enqueue(delegate { CompleteJoin(cancellation, failure); }); }); } private void CompleteJoin(CancellationTokenSource cancellation, Exception failure) { if (_joinCancellation != cancellation) { cancellation.Dispose(); return; } _joinTask = null; _joinCancellation = null; _connectionMessage = ((failure == null) ? ("Connected to " + _joinAddress + ".") : (cancellation.IsCancellationRequested ? "Connection cancelled." : ("Join failed: " + failure.Message))); if (failure == null) { Log.Info($"[DevTrigger] Connected to {_joinAddress}:{25565}."); } else if (failure is OperationCanceledException && cancellation.IsCancellationRequested) { Log.Info("[DevTrigger] Join attempt cancelled."); } else { Log.Error("[DevTrigger] Join failed: " + failure.Message); } cancellation.Dispose(); if (_startHostAfterJoinStops && !_isQuitting) { _startHostAfterJoinStops = false; StartHost(); } } private void StartHost() { Log.Info($"[DevTrigger] F6 - hosting a session on port {DevHostPort}..."); Multibonk.Net.Net.StartHost(DevHostPort); _connectionMessage = $"Hosting on port {DevHostPort}. Players can join using your IP address and port."; Log.Info($"[DevTrigger] Now hosting on port {DevHostPort}. Run the test kit with: dotnet run -- join --host {"127.0.0.1"} --port {DevHostPort}"); } private void CancelPendingJoin() { CancellationTokenSource joinCancellation = _joinCancellation; if (joinCancellation == null) { return; } try { joinCancellation.Cancel(); } catch (ObjectDisposedException) { } } private void PrintDevTriggerLegend() { Log.Info("[DevTrigger] Keybind legend (scaffolding until a real UI exists):"); Log.Info($"[DevTrigger] F6 = host a session on port {DevHostPort}"); Log.Info($"[DevTrigger] F7 = join {"127.0.0.1"}:{25565}"); Log.Info($"[DevTrigger] F8 = (host only) send START_GAME to all clients, seed={DevStartGameSeed}"); } } } namespace Multibonk.Net { public sealed class Connection { private const int HeaderLength = 2; internal const int MaxPayloadSize = 16384; private const int MaxQueuedOutgoingPackets = 256; private static readonly TimeSpan SendTimeout = TimeSpan.FromSeconds(10.0); private static int _nextId; private readonly TcpClient _client; private NetworkStream _stream; private readonly byte[] _headerBuf = new byte[2]; private readonly byte[] _bodyBuf = new byte[16384]; private readonly ConcurrentQueue<byte[]> _outgoing = new ConcurrentQueue<byte[]>(); private readonly SemaphoreSlim _outgoingSignal = new SemaphoreSlim(0); private readonly object _outgoingGate = new object(); private int _outgoingCount; private int _closed; private int _startState; private Task _readTask; private Task _sendTask; public int Id { get; } public ushort PlayerUuid { get; set; } public bool IsConnected => Volatile.Read(in _closed) == 0 && Volatile.Read(in _startState) == 1 && (_client?.Connected ?? false); public bool IsClosed => Volatile.Read(in _closed) != 0; public int PendingSendCount => Volatile.Read(in _outgoingCount); public event Action<Connection, byte[]> OnMessage; public event Action<Connection> OnClosed; public Connection(TcpClient client) { _client = client ?? throw new ArgumentNullException("client"); Id = Interlocked.Increment(ref _nextId); } public void Start() { if (Interlocked.CompareExchange(ref _startState, 1, 0) != 0) { throw new InvalidOperationException($"Connection {Id} has already been started."); } if (Volatile.Read(in _closed) != 0) { throw new ObjectDisposedException("Connection", $"Connection {Id} is already closed."); } try { _client.NoDelay = true; _client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, optionValue: true); _stream = _client.GetStream(); } catch (Exception ex) { Log.Error($"Connection {Id}: failed to start ({ex.Message})"); Close(); throw new InvalidOperationException($"Connection {Id} failed to start.", ex); } _readTask = Task.Run((Func<Task?>)ReadLoop); _sendTask = Task.Run((Func<Task?>)SendLoop); } public bool Send(IPacket packet) { if (packet == null) { throw new ArgumentNullException("packet"); } if (!IsConnected) { return false; } NetWriter netWriter = new NetWriter(); netWriter.WriteByte(packet.Id); packet.Write(netWriter); byte[] array = netWriter.ToArray(); if (array.Length > 16384) { Log.Error($"Connection {Id}: dropped packet id {packet.Id}, payload {array.Length} bytes exceeds max {16384}."); return false; } lock (_outgoingGate) { if (!IsConnected) { return false; } if (_outgoingCount >= 256) { Log.Warn($"Connection {Id}: outgoing queue is full; rejected packet id {packet.Id}."); return false; } _outgoing.Enqueue(array); _outgoingCount++; _outgoingSignal.Release(); return true; } } private async Task ReadLoop() { try { while (Volatile.Read(in _closed) == 0) { await ReadExact(_headerBuf, 0, 2).ConfigureAwait(continueOnCapturedContext: false); short len = BitConverter.ToInt16(_headerBuf, 0); if (len <= 0 || len > 16384) { throw new IOException($"Invalid packet length {len}"); } await ReadExact(_bodyBuf, 0, len).ConfigureAwait(continueOnCapturedContext: false); byte[] payload = new byte[len]; Array.Copy(_bodyBuf, 0, payload, 0, len); try { this.OnMessage?.Invoke(this, payload); } catch (Exception ex) { Log.Error($"Connection {Id}: OnMessage handler threw: {ex}"); throw new IOException("Incoming message handler rejected the packet.", ex); } } } catch (Exception ex2) { Exception e = ex2; Log.Warn($"Connection {Id}: read loop ended ({e.Message})"); } finally { Close(); } } private async Task ReadExact(byte[] buffer, int offset, int count) { int n; for (int total = 0; total < count; total += n) { n = await _stream.ReadAsync(buffer, offset + total, count - total).ConfigureAwait(continueOnCapturedContext: false); if (n == 0) { throw new EndOfStreamException("Connection closed by remote host (received 0 bytes)."); } } } private async Task SendLoop() { try { while (Volatile.Read(in _closed) == 0) { await _outgoingSignal.WaitAsync().ConfigureAwait(continueOnCapturedContext: false); byte[] payload; while (TryDequeueOutgoing(out payload) && Volatile.Read(in _closed) == 0) { await InternalSend(payload).ConfigureAwait(continueOnCapturedContext: false); } } } catch (Exception ex) { Exception e = ex; Log.Warn($"Connection {Id}: send loop ended ({e.Message})"); } finally { Close(); } } private bool TryDequeueOutgoing(out byte[] payload) { lock (_outgoingGate) { if (!_outgoing.TryDequeue(out payload)) { return false; } _outgoingCount--; return true; } } private async Task InternalSend(byte[] payload) { byte[] frame = new byte[payload.Length + 2]; Array.Copy(BitConverter.GetBytes((short)payload.Length), 0, frame, 0, 2); Array.Copy(payload, 0, frame, 2, payload.Length); await _stream.WriteAsync(frame, 0, frame.Length).WaitAsync(SendTimeout).ConfigureAwait(continueOnCapturedContext: false); } public void Close() { if (Interlocked.Exchange(ref _closed, 1) != 0) { return; } lock (_outgoingGate) { byte[] result; while (_outgoing.TryDequeue(out result)) { } _outgoingCount = 0; } try { _outgoingSignal.Release(); } catch { } try { _stream?.Close(); } catch { } try { _client?.Close(); } catch { } try { this.OnClosed?.Invoke(this); } catch (Exception value) { Log.Error($"Connection {Id}: OnClosed handler threw: {value}"); } } } public sealed class HostConfiguration { public static HostConfiguration Current { get; } = Parse(Environment.GetCommandLineArgs()); public bool Dedicated { get; private set; } public int MaxClients { get; private set; } = 8; public int Port { get; private set; } = 25565; public static HostConfiguration Parse(string[] args) { HostConfiguration hostConfiguration = new HostConfiguration(); for (int i = 0; i < args.Length; i++) { string text = args[i]; if (text == "--multibonk-dedicated") { hostConfiguration.Dedicated = true; } else if (text == "--multibonk-max-clients" || text == "--multibonk-port") { if (++i >= args.Length || !int.TryParse(args[i], out var result)) { throw new ArgumentException(text + " requires an integer."); } int num = ((text == "--multibonk-port") ? 65535 : 8); if (result < 1 || result > num) { throw new ArgumentOutOfRangeException(text); } if (text == "--multibonk-port") { hostConfiguration.Port = result; } else { hostConfiguration.MaxClients = result; } } } return hostConfiguration; } } public sealed class DedicatedLaunchGate { public bool Issued { get; private set; } public bool TryIssue(bool hasSelectedParticipant) { if (Issued || !hasSelectedParticipant) { return false; } Issued = true; return true; } public void Reset() { Issued = false; } } public interface IPacket { byte Id { get; } void Write(NetWriter w); } public static class MainThread { private static readonly ConcurrentQueue<Action> Queue = new ConcurrentQueue<Action>(); public static void Enqueue(Action action) { if (action != null) { Queue.Enqueue(action); } } public static void Drain() { int count = Queue.Count; for (int i = 0; i < count; i++) { if (!Queue.TryDequeue(out var result)) { break; } try { result(); } catch (Exception value) { Log.Error($"MainThread: queued action threw: {value}"); } } } } public static class Net { private sealed class SessionContext { public readonly long Epoch; public readonly bool IsHost; public readonly CancellationTokenSource Cancellation = new CancellationTokenSource(); public NetServer Server; public NetClient Client; public SessionContext(long epoch, bool isHost) { Epoch = epoch; IsHost = isHost; } } private readonly struct IncomingItem { public readonly SessionContext Context; public readonly Connection From; public readonly byte[] Payload; public IncomingItem(SessionContext context, Connection from, byte[] payload) { Context = context; From = from; Payload = payload; } } public const int IncomingPacketBudgetPerPump = 64; public const int MaxQueuedIncomingPackets = 1024; private static readonly object StateGate = new object(); private static readonly object IncomingGate = new object(); private static readonly PacketRegistry Registry = new PacketRegistry(); private static readonly ConcurrentQueue<IncomingItem> Incoming = new ConcurrentQueue<IncomingItem>(); private static NetServer _server; private static NetClient _client; private static SessionContext _activeContext; private static long _nextEpoch; private static int _sessionActive; private static int _incomingCount; public static bool IsHost { get { lock (StateGate) { return _server != null; } } } public static bool InSession { get { lock (StateGate) { if (_server != null) { return _server.IsRunning; } return _client != null && _client.Connection != null && _client.Connection.IsConnected; } } } public static PacketRegistry Handlers => Registry; public static event Action<Connection> OnClientConnected; public static event Action<Connection> OnClientDisconnected; public static event Action<bool> OnSessionStarted; public static event Action OnSessionEnded; public static void StartHost(int port) { StopAll(); NetServer netServer = new NetServer(); SessionContext context = new SessionContext(Interlocked.Increment(ref _nextEpoch), isHost: true) { Server = netServer }; netServer.OnConnectionPreparing += delegate(Connection conn) { conn.OnMessage += delegate(Connection from, byte[] payload) { EnqueueIncoming(context, from, payload); }; }; netServer.OnClientConnected += delegate(Connection conn) { MainThread.Enqueue(delegate { if (IsCurrent(context)) { Net.OnClientConnected?.Invoke(conn); } }); }; netServer.OnClientDisconnected += delegate(Connection conn) { MainThread.Enqueue(delegate { if (IsCurrent(context)) { Net.OnClientDisconnected?.Invoke(conn); } }); }; ActivateAttempt(context); try { netServer.Start(port); lock (StateGate) { if (_activeContext != context) { throw new OperationCanceledException("Host startup was superseded by another session."); } _server = netServer; Volatile.Write(ref _sessionActive, 1); } QueueSessionStarted(context, isHost: true); } catch { FailAttempt(context); netServer.Stop(); throw; } } public static void JoinHost(string host, int port) { JoinHost(host, port, NetClient.DefaultConnectTimeout, CancellationToken.None); } public static void JoinHost(string host, int port, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); StopAll(); cancellationToken.ThrowIfCancellationRequested(); NetClient netClient = new NetClient(); SessionContext context = new SessionContext(Interlocked.Increment(ref _nextEpoch), isHost: false) { Client = netClient }; netClient.OnConnectionPreparing += delegate(Connection conn) { conn.OnMessage += delegate(Connection from, byte[] payload) { EnqueueIncoming(context, from, payload); }; }; netClient.OnDisconnected += delegate(Connection conn) { MainThread.Enqueue(delegate { if (!IsCurrent(context)) { return; } try { Net.OnClientDisconnected?.Invoke(conn); } finally { EndUnexpectedClientSession(context); } }); }; ActivateAttempt(context); try { using (CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, context.Cancellation.Token)) { netClient.ConnectAsync(host, port, timeout, cancellationTokenSource.Token).GetAwaiter().GetResult(); } Connection connection; lock (StateGate) { if (_activeContext != context) { throw new OperationCanceledException("Join was superseded by another session."); } connection = netClient.Connection; if (connection == null || !connection.IsConnected) { throw new InvalidOperationException("Host connection closed before session startup completed."); } _client = netClient; Volatile.Write(ref _sessionActive, 1); } QueueSessionStarted(context, isHost: false); MainThread.Enqueue(delegate { if (IsCurrent(context) && Volatile.Read(in _sessionActive) != 0) { Net.OnClientConnected?.Invoke(connection); } }); } catch { FailAttempt(context); netClient.Disconnect(); throw; } } public static void StopAll() { SessionContext activeContext; NetServer netServer; NetClient netClient; bool flag; lock (StateGate) { activeContext = _activeContext; _activeContext = null; netServer = _server ?? activeContext?.Server; netClient = _client ?? activeContext?.Client; _server = null; _client = null; flag = Interlocked.Exchange(ref _sessionActive, 0) != 0; } if (activeContext != null) { try { activeContext.Cancellation.Cancel(); } catch (ObjectDisposedException) { } } netServer?.Stop(); netClient?.Disconnect(); ClearIncoming(); if (flag) { MainThread.Enqueue(delegate { Net.OnSessionEnded?.Invoke(); }); } } private static void ActivateAttempt(SessionContext context) { lock (StateGate) { if (_activeContext != null) { throw new InvalidOperationException("A networking session is already active."); } _activeContext = context; } } private static void FailAttempt(SessionContext context) { lock (StateGate) { if (_activeContext != context) { return; } _activeContext = null; if (_server == context.Server) { _server = null; } if (_client == context.Client) { _client = null; } Volatile.Write(ref _sessionActive, 0); } try { context.Cancellation.Cancel(); } catch (ObjectDisposedException) { } } private static void QueueSessionStarted(SessionContext context, bool isHost) { MainThread.Enqueue(delegate { if (IsCurrent(context) && Volatile.Read(in _sessionActive) != 0) { Net.OnSessionStarted?.Invoke(isHost); } }); } private static void EndUnexpectedClientSession(SessionContext context) { bool flag; lock (StateGate) { if (_activeContext != context) { return; } _activeContext = null; _client = null; flag = Interlocked.Exchange(ref _sessionActive, 0) != 0; } try { context.Cancellation.Cancel(); } catch (ObjectDisposedException) { } ClearIncoming(); if (flag) { Net.OnSessionEnded?.Invoke(); } } private static bool IsCurrent(SessionContext context) { lock (StateGate) { return _activeContext == context; } } private static void EnqueueIncoming(SessionContext context, Connection from, byte[] payload) { bool flag = false; lock (IncomingGate) { if (!IsCurrent(context)) { return; } if (_incomingCount >= 1024) { flag = true; } else { Incoming.Enqueue(new IncomingItem(context, from, payload)); _incomingCount++; } } if (flag) { Log.Warn($"Connection {from.Id}: incoming queue limit exceeded; closing flood source."); from.Close(); } } private static void ClearIncoming() { lock (IncomingGate) { IncomingItem result; while (Incoming.TryDequeue(out result)) { } _incomingCount = 0; } } private static bool TryDequeueIncoming(out IncomingItem item) { lock (IncomingGate) { if (!Incoming.TryDequeue(out item)) { return false; } _incomingCount--; return true; } } public static void PumpReceive() { for (int i = 0; i < 64; i++) { if (!TryDequeueIncoming(out var item)) { break; } if (!IsCurrent(item.Context) || item.From.IsClosed) { continue; } try { NetReader netReader = new NetReader(item.Payload); byte id = netReader.ReadByte(); if (item.Context.IsHost) { Registry.DispatchIncomingHost(id, netReader, item.From); } else { Registry.DispatchIncomingClient(id, netReader, item.From); } } catch (Exception value) { Log.Error($"Connection {item.From.Id}: malformed packet rejected: {value}"); item.From.Close(); } } } public static void Broadcast(IPacket packet) { _server?.Broadcast(packet); } public static void BroadcastExcept(Connection except, IPacket packet) { _server?.BroadcastExcept(except, packet); } public static bool SendToHost(IPacket packet) { return _client?.Send(packet) ?? false; } public static bool Send(Connection conn, IPacket packet) { return conn?.Send(packet) ?? false; } } public sealed class NetClient { public static readonly TimeSpan DefaultConnectTimeout = TimeSpan.FromSeconds(10.0); private readonly object _stateGate = new object(); private Connection _connection; private CancellationTokenSource _pendingConnectCancellation; public Connection Connection { get { lock (_stateGate) { return _connection; } } private set { lock (_stateGate) { _connection = value; } } } public event Action<Connection> OnConnectionPreparing; public event Action<Connection> OnConnected; public event Action<Connection> OnDisconnected; public Task ConnectAsync(string host, int port) { return ConnectAsync(host, port, DefaultConnectTimeout, CancellationToken.None); } public async Task ConnectAsync(string host, int port, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrWhiteSpace(host)) { throw new ArgumentException("Host is required.", "host"); } if (timeout <= TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan) { throw new ArgumentOutOfRangeException("timeout", "Timeout must be positive or infinite."); } CancellationTokenSource pendingCancellation = new CancellationTokenSource(); lock (_stateGate) { if (_connection != null || _pendingConnectCancellation != null) { throw new InvalidOperationException("NetClient is already connected or connecting."); } _pendingConnectCancellation = pendingCancellation; } TcpClient tcp = new TcpClient(); Connection conn = null; try { using (CancellationTokenSource linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, pendingCancellation.Token)) { Task connectTask = tcp.ConnectAsync(host, port); try { await connectTask.WaitAsync(timeout, linkedCancellation.Token).ConfigureAwait(continueOnCapturedContext: false); } catch { try { tcp.Close(); } catch { } ObserveFault(connectTask); throw; } } conn = new Connection(tcp); conn.OnClosed += HandleClosed; this.OnConnectionPreparing?.Invoke(conn); conn.Start(); lock (_stateGate) { if (_pendingConnectCancellation != pendingCancellation || pendingCancellation.IsCancellationRequested) { conn.Close(); throw new OperationCanceledException("Connection attempt was canceled."); } if (!conn.IsConnected) { conn.Close(); throw new IOException("Connection closed before it could be published."); } _connection = conn; _pendingConnectCancellation = null; try { this.OnConnected?.Invoke(conn); } catch (Exception value) { Log.Error($"NetClient: OnConnected handler threw: {value}"); } } } catch { if (conn != null) { conn.Close(); } else { try { tcp.Close(); } catch { } } lock (_stateGate) { if (_connection == conn) { _connection = null; } } throw; } finally { lock (_stateGate) { if (_pendingConnectCancellation == pendingCancellation) { _pendingConnectCancellation = null; } } pendingCancellation.Dispose(); } } private void HandleClosed(Connection conn) { lock (_stateGate) { if (_connection != conn) { return; } _connection = null; } try { this.OnDisconnected?.Invoke(conn); } catch (Exception value) { Log.Error($"NetClient: OnDisconnected handler threw: {value}"); } } public bool Send(IPacket packet) { return Connection?.Send(packet) ?? false; } public void Disconnect() { Connection connection; CancellationTokenSource pendingConnectCancellation; lock (_stateGate) { connection = _connection; pendingConnectCancellation = _pendingConnectCancellation; } try { pendingConnectCancellation?.Cancel(); } catch (ObjectDisposedException) { } connection?.Close(); } private static void ObserveFault(Task task) { task.ContinueWith(ObserveFaultedTask, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } private static void ObserveFaultedTask(Task task) { if (task.Exception != null) { } } } public sealed class NetReader { private readonly byte[] _buffer; private int _offset; public int Remaining => _buffer.Length - _offset; public NetReader(byte[] data) { _buffer = data ?? throw new ArgumentNullException("data"); _offset = 0; } public byte ReadByte() { EnsureAvailable(1); return _buffer[_offset++]; } public bool ReadBool() { return ReadByte() != 0; } public short ReadShort() { EnsureAvailable(2); short result = BitConverter.ToInt16(_buffer, _offset); _offset += 2; return result; } public ushort ReadUShort() { EnsureAvailable(2); ushort result = BitConverter.ToUInt16(_buffer, _offset); _offset += 2; return result; } public int ReadInt() { EnsureAvailable(4); int result = BitConverter.ToInt32(_buffer, _offset); _offset += 4; return result; } public long ReadLong() { EnsureAvailable(8); long result = BitConverter.ToInt64(_buffer, _offset); _offset += 8; return result; } public float ReadFloat() { EnsureAvailable(4); float result = BitConverter.ToSingle(_buffer, _offset); _offset += 4; return result; } public double ReadDouble() { EnsureAvailable(8); double result = BitConverter.ToDouble(_buffer, _offset); _offset += 8; return result; } public string ReadString() { short num = ReadShort(); EnsureAvailable(num); string result = Encoding.UTF8.GetString(_buffer, _offset, num); _offset += num; return result; } public byte[] ReadBytes(int count) { EnsureAvailable(count); byte[] array = new byte[count]; Array.Copy(_buffer, _offset, array, 0, count); _offset += count; return array; } private void EnsureAvailable(int length) { if (length < 0 || length > _buffer.Length - _offset) { throw new IndexOutOfRangeException($"Not enough bytes to read {length} bytes from packet."); } } } public sealed class NetServer { private sealed class AcceptLoopRunner { private readonly NetServer _owner; private readonly TcpListener _listener; private readonly long _generation; public AcceptLoopRunner(NetServer owner, TcpListener listener, long generation) { _owner = owner; _listener = listener; _generation = generation; } public Task Run() { return _owner.AcceptLoop(_listener, _generation); } } private readonly object _stateGate = new object(); private TcpListener _listener; private readonly ConcurrentDictionary<int, Connection> _connections = new ConcurrentDictionary<int, Connection>(); private readonly ConcurrentDictionary<int, Connection> _preparingConnections = new ConcurrentDictionary<int, Connection>(); private bool _running; private long _generation; private Task _acceptTask; public int MaxConnections { get; } = HostConfiguration.Current.MaxClients; public bool IsRunning { get { lock (_stateGate) { return _running; } } } public IEnumerable<Connection> Connections => _connections.Values; public event Action<Connection> OnConnectionPreparing; public event Action<Connection> OnClientConnected; public event Action<Connection> OnClientDisconnected; public void Start(int port) { TcpListener tcpListener = new TcpListener(IPAddress.Any, port); lock (_stateGate) { if (_running) { throw new InvalidOperationException("NetServer is already running."); } try { tcpListener.Start(); } catch { try { tcpListener.Stop(); } catch { } throw; } long generation = ++_generation; _listener = tcpListener; _running = true; AcceptLoopRunner acceptLoopRunner = new AcceptLoopRunner(this, tcpListener, generation); _acceptTask = Task.Run((Func<Task?>)acceptLoopRunner.Run); } Log.Info($"NetServer listening on port {port}."); } public void Stop() { TcpListener listener; lock (_stateGate) { if (!_running && _listener == null) { return; } _running = false; _generation++; listener = _listener; _listener = null; } try { listener?.Stop(); } catch { } foreach (Connection value in _connections.Values) { value.Close(); } foreach (Connection value2 in _preparingConnections.Values) { value2.Close(); } _connections.Clear(); _preparingConnections.Clear(); } private async Task AcceptLoop(TcpListener listener, long generation) { try { while (IsCurrentListener(listener, generation)) { TcpClient client; try { client = await listener.AcceptTcpClientAsync().ConfigureAwait(continueOnCapturedContext: false); } catch (Exception ex) { if (IsCurrentListener(listener, generation)) { Log.Warn("NetServer: accept loop ended (" + ex.Message + ")"); } break; } if (!IsCurrentListener(listener, generation)) { try { client.Close(); break; } catch { break; } } if (_connections.Count + _preparingConnections.Count >= MaxConnections) { Log.Warn($"NetServer: rejected connection because the {MaxConnections}-client limit is full."); try { client.Close(); } catch { } continue; } Connection conn = new Connection(client); conn.OnClosed += HandleConnectionClosed; bool preparing; lock (_stateGate) { preparing = IsCurrentListenerUnsafe(listener, generation); if (preparing) { _preparingConnections[conn.Id] = conn; } } if (!preparing) { conn.Close(); break; } try { this.OnConnectionPreparing?.Invoke(conn); conn.Start(); } catch (Exception ex2) { Log.Warn("NetServer: rejected connection during startup (" + ex2.Message + ")"); conn.Close(); continue; } bool accepted = false; lock (_stateGate) { _preparingConnections.TryRemove(conn.Id, out var _); if (IsCurrentListenerUnsafe(listener, generation) && conn.IsConnected) { _connections[conn.Id] = conn; accepted = true; try { this.OnClientConnected?.Invoke(conn); } catch (Exception ex3) { Exception e = ex3; Log.Error($"NetServer: OnClientConnected handler threw: {e}"); } } } if (!accepted) { conn.Close(); } } } finally { lock (_stateGate) { if (IsCurrentListenerUnsafe(listener, generation)) { _acceptTask = null; } } } } private bool IsCurrentListener(TcpListener listener, long generation) { lock (_stateGate) { return IsCurrentListenerUnsafe(listener, generation); } } private bool IsCurrentListenerUnsafe(TcpListener listener, long generation) { return _running && _generation == generation && _listener == listener; } private void HandleConnectionClosed(Connection conn) { lock (_stateGate) { _preparingConnections.TryRemove(conn.Id, out var value); if (!_connections.TryRemove(conn.Id, out value)) { return; } try { this.OnClientDisconnected?.Invoke(conn); } catch (Exception value2) { Log.Error($"NetServer: OnClientDisconnected handler threw: {value2}"); } } } public void Broadcast(IPacket packet) { foreach (Connection value in _connections.Values) { value.Send(packet); } } public void BroadcastExcept(Connection except, IPacket packet) { foreach (Connection value in _connections.Values) { if (value != except) { value.Send(packet); } } } } public sealed class NetWriter { private readonly List<byte> _buffer = new List<byte>(); public int Length => _buffer.Count; public void WriteByte(byte value) { _buffer.Add(value); } public void WriteBool(bool value) { _buffer.Add((byte)(value ? 1 : 0)); } public void WriteShort(short value) { _buffer.AddRange(BitConverter.GetBytes(value)); } public void WriteUShort(ushort value) { _buffer.AddRange(BitConverter.GetBytes(value)); } public void WriteInt(int value) { _buffer.AddRange(BitConverter.GetBytes(value)); } public void WriteLong(long value) { _buffer.AddRange(BitConverter.GetBytes(value)); } public void WriteFloat(float value) { _buffer.AddRange(BitConverter.GetBytes(value)); } public void WriteDouble(double value) { _buffer.AddRange(BitConverter.GetBytes(value)); } public void WriteString(string value) { if (value == null) { value = string.Empty; } byte[] bytes = Encoding.UTF8.GetBytes(value); if (bytes.Length > 32767) { throw new ArgumentException("String too long to write"); } WriteShort((short)bytes.Length); _buffer.AddRange(bytes); } public void WriteBytes(byte[] data) { if (data == null) { throw new ArgumentNullException("data"); } _buffer.AddRange(data); } public byte[] ToArray() { return _buffer.ToArray(); } public void Clear() { _buffer.Clear(); } } public sealed class PacketRegistry { public delegate void Handler(NetReader reader, Connection from); private readonly object _gate = new object(); private readonly Dictionary<byte, Handler> _incomingHostHandlers = new Dictionary<byte, Handler>(); private readonly Dictionary<byte, Handler> _incomingClientHandlers = new Dictionary<byte, Handler>(); private readonly HashSet<byte> _warnedMissingHostIds = new HashSet<byte>(); private readonly HashSet<byte> _warnedMissingClientIds = new HashSet<byte>(); private Func<byte, bool> _incomingClientGate; private bool _disabled; public void Disable() { lock (_gate) { _disabled = true; _incomingHostHandlers.Clear(); _incomingClientHandlers.Clear(); } } public void SetIncomingClientGate(Func<byte, bool> gate) { lock (_gate) { _incomingClientGate = gate; } } public void Register(byte id, Handler handler) { if (handler == null) { throw new ArgumentNullException("handler"); } lock (_gate) { CheckAvailable(_incomingHostHandlers, "host", id); CheckAvailable(_incomingClientHandlers, "client", id); RegisterIncomingHost(id, handler); RegisterIncomingClient(id, handler); } } private void CheckAvailable(Dictionary<byte, Handler> table, string role, byte id) { if (_disabled) { throw new InvalidOperationException("PacketRegistry: composition failed; registry disabled."); } if (table.ContainsKey(id)) { throw new InvalidOperationException($"PacketRegistry: duplicate incoming-{role} handler for id {id}."); } } public void RegisterIncomingHost(byte id, Handler handler) { Register(_incomingHostHandlers, _warnedMissingHostIds, "host", id, handler); } public void RegisterIncomingClient(byte id, Handler handler) { Register(_incomingClientHandlers, _warnedMissingClientIds, "client", id, handler); } private void Register(Dictionary<byte, Handler> table, HashSet<byte> warnedMissingIds, string role, byte id, Handler handler) { if (handler == null) { throw new ArgumentNullException("handler"); } lock (_gate) { CheckAvailable(table, role, id); table[id] = handler; warnedMissingIds.Remove(id); } } public void DispatchIncomingHost(byte id, NetReader reader, Connection from) { Dispatch(_incomingHostHandlers, _warnedMissingHostIds, "host", id, reader, from); } public void DispatchIncomingClient(byte id, NetReader reader, Connection from) { Func<byte, bool> incomingClientGate; lock (_gate) { incomingClientGate = _incomingClientGate; } if (incomingClientGate == null || incomingClientGate(id)) { Dispatch(_incomingClientHandlers, _warnedMissingClientIds, "client", id, reader, from); } } private void Dispatch(Dictionary<byte, Handler> table, HashSet<byte> warnedMissingIds, string role, byte id, NetReader reader, Connection from) { Handler value; lock (_gate) { if (_disabled) { return; } if (!table.TryGetValue(id, out value)) { if (warnedMissingIds.Add(id)) { Log.Warn($"PacketRegistry: no incoming-{role} handler for packet id {id}, skipping further repeats silently."); } return; } } try { value(reader, from); } catch (Exception value2) { Log.Error($"PacketRegistry: incoming-{role} handler for id {id} rejected malformed packet: {value2}"); from?.Close(); } } } internal sealed class PositionTimeline { internal readonly struct Sample { public readonly float Time; public readonly float X; public readonly float Y; public readonly float Z; public Sample(float time, float x, float y, float z) { Time = time; X = x; Y = y; Z = z; } } private readonly List<Sample> _samples = new List<Sample>(32); private readonly float _snapDistanceSquared; internal int Count => _samples.Count; internal PositionTimeline(float snapDistance) { _snapDistanceSquared = snapDistance * snapDistance; } internal void Add(float time, float x, float y, float z) { if (!Finite(time) || !Finite(x) || !Finite(y) || !Finite(z)) { return; } if (_samples.Count > 0) { Sample sample = _samples[_samples.Count - 1]; if (time < sample.Time) { return; } float num = x - sample.X; float num2 = y - sample.Y; float num3 = z - sample.Z; if (num * num + num2 * num2 + num3 * num3 > _snapDistanceSquared) { _samples.Clear(); } else if (time == sample.Time) { _samples.RemoveAt(_samples.Count - 1); } } if (_samples.Count == 32) { _samples.RemoveAt(0); } _samples.Add(new Sample(time, x, y, z)); } internal bool TrySample(float time, out Sample result) { result = default(Sample); if (_samples.Count == 0 || !Finite(time)) { return false; } while (_samples.Count > 2 && _samples[1].Time <= time) { _samples.RemoveAt(0); } Sample sample = _samples[0]; if (time <= sample.Time) { result = sample; return true; } Sample sample2 = _samples[_samples.Count - 1]; if (time >= sample2.Time) { result = sample2; return true; } for (int i = 1; i < _samples.Count; i++) { Sample sample3 = _samples[i]; if (!(sample3.Time < time)) { Sample sample4 = _samples[i - 1]; float num = (time - sample4.Time) / (sample3.Time - sample4.Time); result = new Sample(time, sample4.X + (sample3.X - sample4.X) * num, sample4.Y + (sample3.Y - sample4.Y) * num, sample4.Z + (sample3.Z - sample4.Z) * num); return true; } } return false; } private static bool Finite(float x) { return !float.IsNaN(x) && !float.IsInfinity(x); } } public static class ProtocolVersion { public const int Current = 111; } internal static class ServerAddress { internal static bool TryParse(string value, out string host, out int port) { host = null; port = 25565; value = value?.Trim(); if (string.IsNullOrEmpty(value)) { return false; } if (Uri.CheckHostName(value) != UriHostNameType.Unknown) { host = value; return true; } if (!Uri.TryCreate("tcp://" + value, UriKind.Absolute, out Uri result) || result.UserInfo.Length != 0 || result.AbsolutePath != "/" || result.Query.Length != 0 || result.Fragment.Length != 0 || result.Port < 1 || result.Port > 65535 || result.HostNameType == UriHostNameType.Unknown || result.Host.Length == 0) { return false; } host = result.DnsSafeHost; port = result.Port; return true; } } } namespace Multibonk.Modules { public interface IModule { void Install(PacketRegistry registry); void OnSessionStart(bool isHost); void OnSessionEnd(); void Tick() { } } public static class ModuleHost { private static readonly List<IModule> Modules = new List<IModule> { Session, DedicatedHost, PlayerSync, TimeSync, EnemySync, PickupSync, PlayerState, MapSync, InteractableSync, Purchases, RunState, RewardChoices, BossRewards, SharedRewards, FinalFight, CombatVisuals, StateDigest, GameControl }; private static bool _installed; private static bool _tracedTick; public static SessionModule Session { get; } = new SessionModule(); public static PlayerSyncModule PlayerSync { get; } = new PlayerSyncModule(); public static TimeSyncModule TimeSync { get; } = new TimeSyncModule(); public static EnemySyncModule EnemySync { get; } = new EnemySyncModule(); public static PickupSyncModule PickupSync { get; } = new PickupSyncModule(); public static PlayerStateSyncModule PlayerState { get; } = new PlayerStateSyncModule(); public static MapSyncModule MapSync { get; } = new MapSyncModule(); public static InteractableSyncModule InteractableSync { get; } = new InteractableSyncModule(); public static PurchaseModule Purchases { get; } = new PurchaseModule(); public static RunStateSyncModule RunState { get; } = new RunStateSyncModule(); public static RewardChoiceSyncModule RewardChoices { get; } = new RewardChoiceSyncModule(); public static BossRewardSyncModule BossRewards { get; } = new BossRewardSyncModule(); public static SharedRewardSyncModule SharedRewards { get; } = new SharedRewardSyncModule(); public static FinalFightSyncModule FinalFight { get; } = new FinalFightSyncModule(); public static CombatVisualSyncModule CombatVisuals { get; } = new CombatVisualSyncModule(); public static StateDigestModule StateDigest { get; } = new StateDigestModule(); public static GameControlModule GameControl { get; } = new GameControlModule(); public static DedicatedHostModule DedicatedHost { get; } = new DedicatedHostModule(); public static bool IsInstalled => _installed; public static void Install() { if (_installed) { return; } Multibonk.Net.Net.Handlers.SetIncomingClientGate((byte id) => id <= 2 || id == 38 || Session.LocalGameplayReady); ModuleInstallation.InstallAll(Modules, Multibonk.Net.Net.Handlers); Multibonk.Net.Net.OnSessionStarted += delegate(bool isHost) { foreach (IModule module in Modules) { try { module.OnSessionStart(isHost); } catch (Exception value) { Log.Error($"ModuleHost: {module.GetType().Name}.OnSessionStart threw: {value}"); } } }; Multibonk.Net.Net.OnSessionEnded += delegate { foreach (IModule module2 in Modules) { try { module2.OnSessionEnd(); } catch (Exception value) { Log.Error($"ModuleHost: {module2.GetType().Name}.OnSessionEnd threw: {value}"); } } }; _installed = true; Log.Info($"ModuleHost: installed {Modules.Count} module(s)."); } public static void TickAll() { if (!_installed) { return; } foreach (IModule module in Modules) { try { if (!_tracedTick) { Log.Info("[CrashTrace] Tick " + module.GetType().Name); } module.Tick(); } catch (Exception value) { Log.Error($"ModuleHost: {module.GetType().Name}.Tick threw: {value}"); } } _tracedTick = true; } } internal static class ModuleInstallation { internal static void InstallAll(IEnumerable<IModule> modules, PacketRegistry registry) { foreach (IModule module in modules) { try { module.Install(registry); } catch (Exception innerException) { registry.Disable(); throw new InvalidOperationException("ModuleHost: " + module.GetType().Name + ".Install failed; mod composition aborted.", innerException); } } } } } namespace Multibonk.Modules.TimeSync { public enum ServerSentPacketId : byte { PAUSE_GAME = 3, UNPAUSE_GAME = 4, TIME_SYNC = 27, TIME_SCALE = 35 } internal sealed class PauseGamePacket : IPacket { public byte Id => 3; public void Write(NetWriter writer) { } } internal sealed class UnpauseGamePacket : IPacket { public byte Id => 4; public void Write(NetWriter writer) { } } internal sealed class TimeScalePacket : IPacket { private readonly float _scale; private readonly float _duration; public byte Id => 35; public TimeScalePacket(float scale, float duration) { _scale = scale; _duration = duration; } public void Write(NetWriter writer) { writer.WriteFloat(_scale); writer.WriteFloat(_duration); } } internal sealed class TimeSyncPacket : IPacket { private readonly float _stageTime; private readonly float _runTime; private readonly bool _paused; public byte Id => 27; public TimeSyncPacket(float stageTime, float runTime, bool paused) { _stageTime = stageTime; _runTime = runTime; _paused = paused; } public void Write(NetWriter writer) { writer.WriteFloat(_stageTime); writer.WriteFloat(_runTime); writer.WriteBool(_paused); } } public sealed class TimeSyncModule : IModule { private const float BroadcastIntervalSeconds = 2f; private const float DriftToleranceSeconds = 0.3f; private const float MaxAcceptedTimerSeconds = 604800f; private float _nextBroadcastTime; private bool _hasObservedHostPause; private bool _lastHostPaused; private bool _networkPauseActive; private float _lastSharedScale = 1f; private float _lastSharedAt; internal static bool ApplyingNetworkTimeScale { get; private set; } public void Install(PacketRegistry registry) { registry.RegisterIncomingClient(3, HandlePause); registry.RegisterIncomingClient(4, HandleUnpause); registry.RegisterIncomingClient(27, HandleTimeSync); registry.RegisterIncomingClient(35, HandleTimeScale); registry.RegisterIncomingHost(35, HandleTimeScale); ModuleHost.Session.PlayerGameLoaded += delegate(LobbyPlayer player) { if (Multibonk.Net.Net.IsHost && player?.Connection != null && IsValidTimer(MyTime.stageTimer) && IsValidTimer(MyTime.runTimer)) { Multibonk.Net.Net.Send(player.Connection, new TimeSyncPacket(MyTime.stageTimer, MyTime.runTimer, MyTime.paused)); } }; Log.Info("[TimeSync] Module installed - clock, pause and time-scale handlers ready."); } internal void ObserveLocalTimeScale(float scale, float duration) { if (ApplyingNetworkTimeScale || !Multibonk.Net.Net.InSession || !ModuleHost.Session.LocalGameplayReady || float.IsNaN(scale) || float.IsInfinity(scale) || scale < 0f || scale > 100f) { return; } if (float.IsNaN(duration) || float.IsInfinity(duration)) { duration = 0f; } duration = Mathf.Clamp(duration, 0f, 120f); float unscaledTime = Time.unscaledTime; if (!Mathf.Approximately(scale, _lastSharedScale) || !(unscaledTime - _lastSharedAt < 0.25f)) { _lastSharedScale = scale; _lastSharedAt = unscaledTime; TimeScalePacket packet = new TimeScalePacket(scale, duration); if (Multibonk.Net.Net.IsHost) { Multibonk.Net.Net.Broadcast(packet); } else { Multibonk.Net.Net.SendToHost(packet); } } } private void HandleTimeScale(NetReader reader, Connection from) { float num = reader.ReadFloat(); float num2 = reader.ReadFloat(); if (float.IsNaN(num) || float.IsInfinity(num) || num < 0f || num > 100f) { return; } if (float.IsNaN(num2) || float.IsInfinity(num2) || num2 < 0f) { num2 = 0f; } if (Multibonk.Net.Net.IsHost && from.PlayerUuid != 0) { Multibonk.Net.Net.BroadcastExcept(from, new TimeScalePacket(num, num2)); } try { ApplyingNetworkTimeScale = true; if (num2 > 0f) { MyTime.SetTimeScale(num, num2); } else if (num >= 0.999f) { MyTime.ResetTimeScale(); } else { MyTime.SetTimeScale(num, 120f); } } catch (Exception ex) { Log.Warn($"[TimeSync] Failed to apply network time-scale {num}: {ex.Message}"); } finally { ApplyingNetworkTimeScale = false; } } public void OnSessionStart(bool isHost) { _nextBroadcastTime = 0f; _hasObservedHostPause = false; _lastHostPaused = false; _networkPauseActive = false; _lastSharedScale = 1f; _lastSharedAt = 0f; } public void OnSessionEnd() { if (_networkPauseActive) { try { if (MyTime.paused) { MyTime.Unpause(); } } catch (Exception ex) { Log.Warn("[TimeSync] Could not release network pause during disconnect: " + ex.Message); } } _networkPauseActive = false; _hasObservedHostPause = false; } public void Tick() { if (!Multibonk.Net.Net.InSession || !ModuleHost.Session.LocalGameplayReady || !Multibonk.Net.Net.IsHost) { return; } try { bool paused = MyTime.paused; if (!_hasObservedHostPause || paused != _lastHostPaused) { _hasObservedHostPause = true; _lastHostPaused = paused; IPacket packet2; if (!paused) { IPacket packet = new UnpauseGamePacket(); packet2 = packet; } else { IPacket packet = new PauseGamePacket(); packet2 = packet; } Multibonk.Net.Net.Broadcast(packet2); } float unscaledTime = Time.unscaledTime; if (!(unscaledTime < _nextBroadcastTime)) { _nextBroadcastTime = unscaledTime + 2f; float stageTimer = MyTime.stageTimer; float runTimer = MyTime.runTimer; if (!IsValidTimer(stageTimer) || !IsValidTimer(runTimer)) { Log.Warn("[TimeSync] Host clock is invalid; skipped TIME_SYNC broadcast."); } else { Multibonk.Net.Net.Broadcast(new TimeSyncPacket(stageTimer, runTimer, paused)); } } } catch (Exception value) { Log.Error($"[TimeSync] Host tick failed: {value}"); } } private void HandlePause(NetReader reader, Connection from) { ApplyNetworkPause(); } private void HandleUnpause(NetReader reader, Connection from) { ApplyNetworkUnpause(); } private void HandleTimeSync(NetReader reader, Connection from) { float num = reader.ReadFloat(); float num2 = reader.ReadFloat(); bool flag = reader.ReadBool(); if (!IsValidTimer(num) || !IsValidTimer(num2)) { Log.Warn($"[TimeSync] Rejected invalid host clock stage={num}, run={num2}."); return; } if (flag) { ApplyNetworkPause(); } else { ApplyNetworkUnpause(); } if (!(Math.Abs(MyTime.stageTimer - num) <= 0.3f) || !(Math.Abs(MyTime.runTimer - num2) <= 0.3f)) { MyTime.stageTimer = num; MyTime.runTimer = num2; } } private void ApplyNetworkPause() { try { if (!MyTime.paused) { MyTime.Pause(); _networkPauseActive = true; Log.Info("[TimeSync] Applied host pause."); } } catch (Exception value) { Log.Error($"[TimeSync] Failed to apply host pause: {value}"); } } private void ApplyNetworkUnpause() { try { if (_networkPauseActive && MyTime.paused && !ModuleHost.RunState.HoldsLevelUpPause && !ModuleHost.RewardChoices.HoldsPause) { MyTime.Unpause(); Log.Info("[TimeSync] Released host pause."); } _networkPauseActive = false; } catch (Exception value) { Log.Error($"[TimeSync] Failed to release host pause: {value}"); } } private static bool IsValidTimer(float value) { return !float.IsNaN(value) && !float.IsInfinity(value) && value >= 0f && value <= 604800f; } } [HarmonyPatch(typeof(MyTime), "SetTimeScale")] internal static class TimeScaleSetPatch { private static void Postfix(float newTimeScale, float duration) { ModuleHost.TimeSync.ObserveLocalTimeScale(newTimeScale, duration); } } [HarmonyPatch(typeof(MyTime), "ResetTimeScale")] internal static class TimeScaleResetPatch { private static void Postfix() { ModuleHost.TimeSync.ObserveLocalTimeScale(1f, 0f); } } } namespace Multibonk.Modules.SharedRewards { public enum ServerSentPacketId : byte { SHARED_REWARD = 58 } public enum ClientSentPacketId : byte { SHARED_REWARD_REQUEST = 59 } internal readonly struct SharedRewardWorld { public readonly int Seed; public readonly int Stage; public readonly int Map; public readonly bool Final; public SharedRewardWorld(int seed, int stage, int map, bool final) { Seed = seed; Stage = stage; Map = map; Final = final; } public bool Matches(SharedRewardWorld other) { return Seed == other.Seed && Stage == other.Stage && Map == other.Map && Final == other.Final; } public void Write(NetWriter writer) { writer.WriteInt(Seed); writer.WriteInt(Stage); writer.WriteInt(Map); writer.WriteBool(Final); } public static SharedRewardWorld Read(NetReader reader) { return new SharedRewardWorld(reader.ReadInt(), reader.ReadInt(), reader.ReadInt(), reader.ReadBool()); } } internal sealed class SharedRewardPacket : IPacket { public readonly SharedRewardWorld World; public readonly long EventId; public readonly int Encounter; public byte Id => 58; public SharedRewardPacket(SharedRewardWorld world, long id, int encounter) { World = world; EventId = id; Encounter = encounter; } public void Write(NetWriter writer) { World.Write(writer); writer.WriteLong(EventId); writer.WriteInt(Encounter); } public static SharedRewardPacket Read(NetReader reader) { SharedRewardWorld world = SharedRewardWorld.Read(reader); long num = reader.ReadLong(); int encounter = reader.ReadInt(); if (num <= 0 || !SharedRewardSyncModule.IsSharedEncounter(encounter) || reader.Remaining != 0) { throw new ArgumentException("Invalid shared reward event."); } return new SharedRewardPacket(world, num, encounter); } } internal sealed class SharedRewardRequestPacket : IPacket { private readonly SharedRewardWorld _world; private readonly long _sequence; private readonly int _encounter; public byte Id => 59; public SharedRewardRequestPacket(SharedRewardWorld world, long sequence, int encounter) { _world = world; _sequence = sequence; _encounter = encounter; } public void Write(NetWriter writer) { _world.Write(writer); writer.WriteLong(_sequence); writer.WriteInt(_encounter); } } public sealed class SharedRewardSyncModule : IModule { private readonly Dictionary<int, long> _sourceSequences = new Dictionary<int, long>(); private readonly Queue<EEncounter> _pending = new Queue<EEncounter>(); private SharedRewardWorld _world; private EncounterWindows _windows; private int _scene = int.MinValue; private bool _haveWorld; private bool _applying; private long _localSequence; private long _nextHostEvent; private long _lastHostEvent; internal static SharedRewardSyncModule Instance { get; private set; } private bool Playing { get { int result; if (Multibonk.Net.Net.InSession && ModuleHost.Session.LocalGameplayReady && ModuleHost.RunState.LevelUpAllowed) { GameManager instance = GameManager.Instance; result = ((instance == null || !instance.isGameOver) ? 1 : 0); } else { result = 0; } return (byte)result != 0; } } public void Install(PacketRegistry registry) { Instance = this; registry.RegisterIncomingHost(59, HandleRequest); registry.RegisterIncomingClient(58, HandleSharedReward); } public void OnSessionStart(bool isHost) { ResetSession(); } public void OnSessionEnd() { ResetSession(); } private bool Eligible(ushort uuid) { LobbyPlayer player = ModuleHost.Session.GetPlayer(uuid); int result; if (player != null && player.IsParticipant && !player.IsDead) { if (!player.IsLocal) { Connection connection = player.Connection; result = ((connection != null && connection.IsConnected && ModuleHost.Session.IsPlayerGameLoaded(uuid)) ? 1 : 0); } else { result = 1; } } else { result = 0; } return (byte)result != 0; } private static SharedRewardWorld CurrentWorld() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) return new SharedRewardWorld(ModuleHost.Session.RunSeed, MapController.GetStageIndex(), (!((Object)(object)MapController.currentMap != (Object)null)) ? (-1) : ((int)MapController.currentMap.eMap), MapController.IsFinalBossStage()); } private void EnsureWorld() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) SharedRewardWorld sharedRewardWorld = CurrentWorld(); Scene activeScene = SceneManager.GetActiveScene(); int handle = ((Scene)(ref activeScene)).handle; if (_haveWorld && (!_world.Matches(sharedRewardWorld) || _scene != handle)) { _pending.Clear(); _windows = null; } _world = sharedRewardWorld; _scene = handle; _haveWorld = true; } internal static bool IsSharedEncounter(int encounter) { switch (encounter) { case 1: case 2: case 3: case 4: case 5: case 6: case 8: case 10: case 11: return true; default: return false; } } internal void NativeEncounterQueued(EEncounter encounter) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected I4, but got Unknown //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected I4, but got Unknown //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected I4, but got Unknown if (!_applying && Playing && Eligible(ModuleHost.Session.LocalUuid) && IsSharedEncounter((int)encounter)) { EnsureWorld(); if (Multibonk.Net.Net.IsHost) { Fanout((int)encounter, ModuleHost.Session.LocalUuid); } else { Multibonk.Net.Net.SendToHost(new SharedRewardRequestPacket(_world, checked(++_localSequence), (int)encounter)); } } } private void HandleRequest(NetReader reader, Connection from) { SharedRewardWorld other = SharedRewardWorld.Read(reader); long num = reader.ReadLong(); int encounter = reader.ReadInt(); if (num <= 0 || !IsSharedEncounter(encounter) || reader.Remaining != 0) { throw new ArgumentException("Invalid shared reward request."); } if (Multibonk.Net.Net.IsHost && Playing && from != null && Eligible(from.PlayerUuid) && ModuleHost.Session.GetPlayer(from.PlayerUuid)?.Connection == from) { EnsureWorld(); if (_world.Matches(other) && (!_sourceSequences.TryGetValue(from.Id, out var value) || num > value)) { _sourceSequences[from.Id] = num; Fanout(encounter, from.PlayerUuid); } } } private void Fanout(int encounter, ushort origin) { SharedRewardPacket sharedRewardPacket = new SharedRewardPacket(_world, checked(++_nextHostEvent), encounter); foreach (LobbyPlayer item in ModuleHost.Session.Players()) { if (item.Uuid != origin && Eligible(item.Uuid)) { if (item.IsLocal) { QueueLocal((EEncounter)encounter); } else { Multibonk.Net.Net.Send(item.Connection, sharedRewardPacket); } } } Log.Info($"[SharedRewards] Native {encounter} from player={origin} shared as event={sharedRewardPacket.EventId}."); } private void HandleSharedReward(NetReader reader, Connection from) { SharedRewardPacket sharedRewardPacket = SharedRewardPacket.Read(reader); if (!Multibonk.Net.Net.IsHost && Playing && Eligible(ModuleHost.Session.LocalUuid)) { EnsureWorld(); if (_world.Matches(sharedRewardPacket.World) && sharedRewardPacket.EventId > _lastHostEvent) { _lastHostEvent = sharedRewardPacket.EventId; QueueLocal((EEncounter)sharedRewardPacket.Encounter); } } } private void QueueLocal(EEncounter encounter) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) if (_pending.Count >= 256) { throw new InvalidOperationException("Shared reward queue exceeded its safety bound."); } _pending.Enqueue(encounter); FlushNativeQueue(); } private EncounterWindows FindWindows() { if ((Object)(object)_windows == (Object)null) { _windows = ((IEnumerable<EncounterWindows>)Object.FindObjectsOfType<EncounterWindows>()).FirstOrDefault(); } return _windows; } private void FlushNativeQueue() { //IL_001e: 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) //IL_002d: Unknown result type (might be due to invalid IL or missing references) EncounterWindows val = FindWindows(); if ((Object)(object)val == (Object)null) { return; } while (_pending.Count > 0) { EEncounter val2 = _pending.Dequeue(); _applying = true; try { val.AddEncounter(val2); } finally { _applying = false; } } } internal bool BeforeInstantChest(EChest chestType) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: 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_0034: Expected I4, but got Unknown //IL_005a: Unknown result type (might be due to invalid IL or missing references) if (!Playing || !Eligible(ModuleHost.Session.LocalUuid)) { return true; } EEncounter val = ChestUtility.ChestTypeToEncounter(chestType); if (!IsSharedEncounter((int)val)) { return true; } EncounterWindows val2 = FindWindows(); if ((Object)(object)val2 == (Object)null) { return true; } val2.AddEncounter(val); return false; } public void Tick() { if (!Playing) { _pending.Clear(); _windows = null; return; } EnsureWorld(); if (Eligible(ModuleHost.Session.LocalUuid)) { FlushNativeQueue(); } else { _pending.Clear(); } } public object Describe() { return new { pending = _pending.Count, localSequence = _localSequence, hostEvent = _nextHostEvent, lastHostEvent = _lastHostEvent }; } private void ResetSession() { _sourceSequences.Clear(); _pending.Clear(); _windows = null; _scene = int.MinValue; _haveWorld = (_applying = false); _localSequence = (_nextHostEvent = (_lastHostEvent = 0L)); } } [HarmonyPatch(typeof(EncounterWindows), "AddEncounter")] internal static class SharedNativeEncounterPatch { private static void Postfix(EEncounter __0) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) SharedRewardSyncModule.Instance?.NativeEncounterQueued(__0); } } [HarmonyPatch(typeof(ChestUtility), "OpenChestNoAnimation")] internal static class SharedInstantChestPatch { private static bool Prefix(EChest __0) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) return SharedRewardSyncModule.Instance?.BeforeInstantChest(__0) ?? true; } } } namespace Multibonk.Modules.Session { [HarmonyPatch(typeof(CharacterInfoUI), "OnCharacterSelected", new Type[] { typeof(MyButtonCharacter) })] internal static class CharacterSelectionPatch { [HarmonyPostfix] private static void Postfix(MyButtonCharacter btn) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)btn == (Object)null) && !((Object)(object)btn.characterData == (Object)null)) { ModuleHost.PlayerSync.CacheCharacterData(btn.characterData); ModuleHost.Session.ObserveLocalCharacterSelection(btn.characterData); Log.Info($"[Session] Observed local game character selection: {btn.characterData.eCharacter}."); } } catch (Exception value) { Log.Error($"[Session] Character-selection postfix threw: {value}"); } } } public sealed class LobbyPlayer { public ushort Uuid { get; set; } public string Name { get; set; } public string Character { get; set; } public Connection Connection { get; set; } public bool IsLocal { get; set; } public bool IsParticipant { get; set; } = true; public int Level { get; set; } = 1; public float CurrentHealth { get; set; } public float MaxHealth { get; set; } public bool IsDead { get; set; } } public sealed class LobbyPlayerRegistry { private readonly object _lock = new object(); private readonly Dictionary<ushort, LobbyPlayer> _players = new Dictionary<ushort, LobbyPlayer>(); public void Add(LobbyPlayer player) { lock (_lock) { _players[player.Uuid] = player; } } public LobbyPlayer Get(ushort uuid) { lock (_lock) { LobbyPlayer value; return _players.TryGetValue(uuid, out value) ? value : null; } } public LobbyPlayer GetByConnection(Connection conn) { lock (_lock) { return _players.Values.FirstOrDefault((LobbyPlayer p) => p.Connection == conn); } } public LobbyPlayer RemoveByConnection(Connection conn) { lock (_lock) { LobbyPlayer lobbyPlayer = _players.Values.FirstOrDefault((LobbyPlayer p) => p.Connection == conn); if (lobbyPlayer != null) { _players.Remove(lobbyPlayer.Uuid); } return lobbyPlayer; } } public List<LobbyPlayer> All() { lock (_lock) { return _players.Values.ToList(); } } public void Clear() { lock (_lock) { _players.Clear(); } } } [HarmonyPatch(typeof(MapController), "LoadNextStage", new Type[] { })] internal static class NextStageLifecyclePatch { private static void Prefix() { ModuleHost.Session.BeginStageLoad(); } } [HarmonyPatch(typeof(MapController), "LoadFinalStage", new Type[] { })] internal static class FinalStageLifecyclePatch { private static void Prefix() { ModuleHost.Session.BeginStageLoad(); } } [HarmonyPatch(typeof(GameManager), "StartPlaying", new Type[] { })] internal static class StagePlayingLifecyclePatch { private static void Postfix() { ModuleHost.Session.ObserveLocalStagePlaying(); } } [HarmonyPatch(typeof(MapSelectionUi), "StartMap", new Type[] { })] internal static class LocalMapStartPatch { [HarmonyPrefix] private static bool Prefix() { try { if (!ModuleHost.Session.LocalRunStartAllowed()) { Log.Info("[Session] Blocked a local StartMap - waiting for START_GAME from the server."); return false; } ModuleHost.Session.ObserveLocalMapStart(); } catch (Exception value) { Log.Error($"[Session] Local map-start prefix threw: {value}"); } return true; } } public enum ServerSentPacketId : byte { LOBBY_PLAYER_LIST_PACKET = 0, PLAYER_SELECTED_CHARACTER = 1, START_GAME = 2, SPAWN_PLAYER_PACKET = 6, SPAWN_LOCAL_PLAYER_AT = 34 } public enum ClientSentPacketId : byte { JOIN_LOBBY_PACKET, CHARACTER_SELECTION, GAME_LOADED_PACKET } public sealed class JoinLobbyPacket : IPacket { private readonly int _modVersion; private readonly string _playerName; public byte Id => 0; public JoinLobbyPacket(int modVersion, string playerName) { _modVersion = modVersion; _playerName = playerName; } public void Write(NetWriter w) { w.WriteInt(_modVersion); w.WriteString(_playerName); } } public sealed class SelectCharacterPacket : IPacket { private readonly string _characterName; public byte Id => 1; public SelectCharacterPacket(string characterName) { _characterName = characterName; } public void Write(NetWriter w) { w.WriteString(_characterName); } } public sealed class GameLoadedPacket : IPacket { private readonly float _px; private readonly float _py; private readonly float _pz; private readonly float _rx; private readonly float _ry; private readonly float _rz; private readonly float _rw; private readonly int _seed; private readonly int _stage; private readonly bool _finalStage; public byte Id => 2; public GameLoadedPacket(float px, float py, float pz, float rx, float ry, float rz, float rw, int seed, int stage, bool finalStage) { _px = px; _py = py; _pz = pz; _rx = rx; _ry = ry; _rz = rz; _rw = rw; _seed = seed; _stage = stage; _finalStage = finalStage; } public void Write(NetWriter w) { w.WriteFloat(_px); w.WriteFloat(_py); w.WriteFloat(_pz); w.WriteFloat(_rx); w.WriteFloat(_ry); w.WriteFloat(_rz); w.WriteFloat(_rw); w.WriteInt(_seed); w.WriteInt(_stage); w.WriteBool(_finalStage); } } public sealed class LobbyPlayerListPacket : IPacket { private readonly IReadOnlyList<LobbyPlayer> _players; public byte Id => 0; public LobbyPlayerListPacket(IReadOnlyList<LobbyPlayer> players) { _players = players; } public void Write(NetWriter w) { w.WriteByte((byte)_players.Count); foreach (LobbyPlayer player in _players) { w.WriteUShort(player.Uuid); w.WriteString(player.Name ?? string.Empty); w.WriteString(player.Character ?? string.Empty); w.WriteBool(player.IsParticipant); } } } public sealed class PlayerSelectedCharacterPacket : IPacket { private readonly ushort _playerId; private readonly string _characterName; public byte Id => 1; public PlayerSelectedCharacterPacket(ushort playerId, string characterName) { _playerId = playerId; _characterName = characterName; } public void Write(NetWriter w) { w.WriteUShort(_playerId); w.WriteString(_characterName); } } public sealed class StartGamePacket : IPacket { private readonly int _seed; public byte Id => 2; public StartGamePacket(int seed) { _seed = seed; } public void Write(NetWriter w) { w.WriteInt(_seed); } } public sealed class SpawnPlayerPacket : IPacket { private readonly byte _characterByte; private readonly ushort _playerId; private readonly float _px; private readonly float _py; private readonly float _pz; private readonly float _rx; private readonly float _ry; private readonly float _rz; private readonly float _rw; public byte Id => 6; public SpawnPlayerPacket(byte characterByte, ushort playerId, float px, float py, float pz, float rx, float ry, float rz, float rw) { _characterByte = characterByte; _playerId = playerId; _px = px; _py = py; _pz = pz; _rx = rx; _ry = ry; _rz = rz; _rw = rw; } public void Write(NetWriter w) { w.WriteByte(_characterByte); w.WriteUShort(_playerId); w.WriteFloat(_px); w.WriteFloat(_py); w.WriteFloat(_pz); w.WriteFloat(_rx); w.WriteFloat(_ry); w.WriteFloat(_rz); w.WriteFloat(_rw); } } public sealed class SpawnLocalPlayerAtPacket : IPacket { private readonly float _x; private readonly float _y; private readonly float _z; public byte Id => 34; public SpawnLocalPlayerAtPacket(Vector3 pos) { //IL_0009: 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_0021: Unknown result type (might be due to invalid IL or missing references) _x = pos.x; _y = pos.y; _z = pos.z; } public void Write(NetWriter w) { w.WriteFloat(_x); w.WriteFloat(_y); w.WriteFloat(_z); } } public readonly struct RunWorldStamp { public readonly int Seed; public readonly int Stage; public readonly bool FinalStage; public RunWorldStamp(int seed, int stage, bool finalStage) { Seed = seed; Stage = stage; FinalStage = finalStage; } public bool Matches(int seed, int stage, bool finalStage) { return Seed == seed && Stage == stage && FinalStage == finalStage; } } public sealed class RunJoinContext : IPacket { public int Seed; public int Map; public int Tier; public int Stage; public int Music; public bool FinalStage; public string Challenge = string.Empty; public byte Id => 38; public void Write(NetWriter writer) { writer.WriteInt(Seed); writer.WriteInt(Map); writer.WriteInt(Tier); writer.WriteInt(Stage); writer.WriteBool(FinalStage); writer.WriteInt(Music); writer.WriteString(Challenge); } public static RunJoinContext Read(NetReader reader) { RunJoinContext runJoinContext = new RunJoinContext { Seed = reader.ReadInt(), Map = reader.ReadInt(), Tier = reader.ReadInt(), Stage = reader.ReadInt(), FinalStage = reader.ReadBool(), Music = reader.ReadInt(), Challenge = reader.ReadString() }; if (runJoinContext.Stage < 0 || runJoinContext.Stage > 64 || runJoinContext.Tier < 0 || runJoinContext.Tier > 64 || runJoinContext.Music < -1 || runJoinContext.Music > 10000 || runJoinContext.Challenge.Length > 128 || reader.Remaining != 0) { throw new ArgumentException("Invalid run join context."); } return runJoinContext; } public bool SameWorld(RunJoinContext other) { return other != null && Seed == other.Seed && Map == other.Map && Tier == other.Tier && Stage == other.Stage && FinalStage == other.FinalStage && Music == other.Music && Challenge == other.Challenge; } } public sealed class SessionModule : IModule { private sealed class PlayerSpawnState { public Vector3 Position { get; } public Quaternion Rotation { get; } public RunWorldStamp? World { get; } public PlayerSpawnState(Vector3 position, Quaternion rotation, RunWorldStamp? world = null) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: 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) Position = position; Rotation = rotation; World = world; } } private readonly struct SpawnDeliveryKey : IEquatable<SpawnDeliveryKey> { public ushort RecipientUuid { get; } public ushort SubjectUuid { get; } public SpawnDeliveryKey(ushort recipientUuid, ushort subjectUuid) { RecipientUuid = recipientUuid; SubjectUuid = subjectUuid; } public bool Equals(SpawnDeliveryKey other) { return RecipientUuid == other.RecipientUuid && SubjectUuid == other.SubjectUuid; } public override bool Equals(object obj) { return obj is SpawnDeliveryKey other && Equals(other); } public override int GetHashCode() { return (RecipientUuid * 397) ^ SubjectUuid; } } private sealed class PendingSpawnDelivery { private int _failures; public PlayerSpawnState State { get; } public float NextAttemptTime { get; private set; } public PendingSpawnDelivery(PlayerSpawnState state, float nextAttemptTime) { State = state; NextAttemptTime = nextAttemptTime; } public void RecordFailure(float now) { _failures++; NextAttemptTime = now + Mathf.Min(0.25f * Mathf.Pow(2f, (float)_failures), 2f); } } private const byte IdJoinLobbyOrPlayerList = 0; private const byte IdCharacterSelectOrSelected = 1; private const byte IdGameLoadedOrStartGame = 2; private const byte IdSpawnPlayer = 6; private const int ModVersion = 111; private const int MaxLobbyTextUtf8Bytes = 48; private readonly LobbyPlayerRegistry _registry = new LobbyPlayerRegistry(); private readonly HashSet<ushort> _gameLoadedPlayers = new HashSet<ushort>(); private readonly HashSet<ushort> _clientsAwaitingHostSpawn = new HashSet<ushort>(); private readonly Dictionary<ushort, PlayerSpawnState> _spawnStates = new Dictionary<ushort, PlayerSpawnState>(); private readonly Dictionary<ushort, PlayerSpawnState> _gameLoadsAwaitingCharacter = new Dictionary<ushort, PlayerSpawnState>(); private readonly Dictionary<SpawnDeliveryKey, PendingSpawnDelivery> _pendingSpawnDeliveries = new Dictionary<SpawnDeliveryKey, PendingSpawnDelivery>(); private ushort _nextUuid = 1; private ushort _localUuid; private bool _subscribedToNet; private bool _waitingForLocalPlayerAfterStart; private bool _sentGameLoadedForCurrentStart; private float _nextLocalRunStartAttempt; private Vector3? _pendingLocalSpawnPos; private float _pendingLocalSpawnDeadline; private bool _hostStartIssued; private bool _hostLaunchPending; private int _runSeed; private bool _hasRunSeed; private string _retainedLocalCharacter; private string _lastSubmittedLocalCharacter; private RunJoinContext _joinContext; private bool _joinMapStarted; private int _joinWaitingScene = int.MinValue; private int _joinWaitingStage; private bool _joinWaitingFinal; private bool _joinGenerationComplete; private bool _joinInitialLoad; private int _readyScene = int.MinValue; private float _joinDeadline; private readonly Dictionary<ushort, RunJoinContext> _authorizedJoins = new Dictionary<ushort, RunJoinContext>(); private readonly HashSet<ushort> _lateJoinCandidates = new HashSet<ushort>(); private bool _stageLoading; private bool _stageGenerationComplete; private int _stageLoadScene; private int _stageLoadIndex; private bool _stageLoadFinal; public ushort LocalUuid => _localUuid; public bool LocalGameplayReady => Multibonk.Net.Net.InSession && ((!Multibonk.Net.Net.IsHost) ? _sentGameLoadedForCurrentStart : (_hostStartIssued && !_hostLaunchPending)); public bool HasRunSeed => _hasRunSeed; public int RunSeed => _runSeed; public event Action<LobbyPlayer> PlayerJoined; public event Action<LobbyPlayer> PlayerLeft; public event Action<LobbyPlayer> PlayerGameLoaded; public LobbyPlayer GetPlayer(ushort uuid) { return _registry.Get(uuid); } public LobbyPlayer GetPlayer(Connection connection) { return _registry.GetByConnection(connection); } public List<LobbyPlayer> Players() { return _registry.All(); } public bool IsPlayerGameLoaded(ushort uuid) { return _gameLoadedPlayers.Contains(uuid); } public void Install(PacketRegistry registry) { registry.RegisterIncomingHost(0, HandleJoinLobby); registry.RegisterIncomingClient(0, HandleLobbyPlayerList); registry.RegisterIncomingHost(1, HandleCharacterSelection); registry.RegisterIncomingClient(1, HandlePlayerSelectedCharacter); registry.RegisterIncomingHost(2, HandleGameLoaded); registry.RegisterIncomingClient(2, HandleStartGame); registry.RegisterIncomingClient(38, HandleRunJoinContext); registry.RegisterIncomingClient(34, HandleSpawnLocalPlayerAt); registry.RegisterIncomingClient(6, HandleSpawnPlayer); if (!_subscribedToNet) { _subscribedToNet = true; Multibonk.Net.Net.OnClientConnected += HandleNetConnected; Multibonk.Net.Net.OnClientDisconnected += HandleNetDisconnected; } Log.Info("[Session] Module installed - lobby/handshake packet handlers registered."); } public void OnSessionStart(bool isHost) { _registry.Clear(); _nextUuid = 1; _localUuid = 0; _lastSubmittedLocalCharacter = null; _waitingForLocalPlayerAfterStart = false; _sentGameLoadedForCurrentStart = false; _gameLoadedPlayers.Clear(); _clientsAwaitingHostSpawn.Clear(); _spawnStates.Clear(); _gameLoadsAwaitingCharacter.Clear(); _pendingSpawnDeliveries.Clear(); _hostStartIssued = false; _runSeed = 0; _hasRunSeed = false; ResetJoinState(); if (isHost) { LobbyPlayer lobbyPlayer = new LobbyPlayer { Uuid = AllocateUuid(), Name = LocalPlayerName(), Character = GetLocalCharacterName(), Connection = null, IsLocal = true, IsParticipant = !DedicatedHostModule.Enabled }; _registry.Add(lobbyPlayer); _localUuid = lobbyPlayer.Uuid; Log.Info($"[Session] Session started as HOST. Local player uuid={_localUuid} name=\"{lobbyPlayer.Name}\"."); } else { Log.Info("[Session] Session started as CLIENT. Waiting for the connection to finish before sending JOIN_LOBBY_PACKET."); } } public void OnSessionEnd() { Log.Info("[Session] Session ended - clearing lobby roster."); _registry.Clear(); _localUuid = 0; _nextUuid = 1; _lastSubmittedLocalCharacter = null; _waitingForLocalPlayerAfterStart = false; _sentGameLoadedForCurrentStart = false; _gameLoadedPlayers.Clear(); _clientsAwaitingHostSpawn.Clear(); _spawnStates.Clear(); _gameLoadsAwaitingCharacter.Clear(); _pendingSpawnDeliveries.Clear(); _hostStartIssued = false; _runSeed = 0; _hasRunSeed = false; ResetJoinState(); } public void Tick() { //IL_027c: Unknown result type (might be due to invalid IL or missing references) //IL_0281: Unknown result type (might be due to invalid IL or missing references) //IL_02cf: Unknown result type (might be due to invalid IL or missing references) //IL_02d4: Unknown result type (might be due to invalid IL or missing references) //IL_02db: Unknown result type (might be due t