Decompiled source of Multibonk v0.2.7

Mods/Multibonk.dll

Decompiled a day ago
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