Decompiled source of Sakura Direct Connect v0.1.3

BepInEx/plugins/SakuraDirectConnect/SakuraDirectConnect.dll

Decompiled 3 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using RepoSteamTransport;
using RepoSteamTransport.Transport;
using Steamworks;
using Steamworks.Data;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("SakuraDirectConnect")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.3.0")]
[assembly: AssemblyInformationalVersion("0.1.3")]
[assembly: AssemblyProduct("SakuraDirectConnect")]
[assembly: AssemblyTitle("SakuraDirectConnect")]
[assembly: AssemblyVersion("0.1.3.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace SakuraDirectConnect
{
	internal enum ResolutionStatus
	{
		Empty,
		Pending,
		Ready,
		Failed
	}
	internal readonly struct HostResolution
	{
		internal readonly ResolutionStatus Status;

		internal readonly string Ip;

		internal readonly string Detail;

		internal HostResolution(ResolutionStatus status, string ip = "", string detail = "")
		{
			Status = status;
			Ip = ip;
			Detail = detail;
		}
	}
	internal sealed class HostResolver
	{
		private readonly object _gate = new object();

		private readonly Func<string, Task<IPAddress[]>> _lookup;

		private readonly Func<long> _now;

		private string _key = "";

		private string _pendingKey = "";

		private string _ready = "";

		private string _error = "";

		private Task<IPAddress[]>? _pending;

		private long _started;

		private long _retryAt;

		private bool _timedOut;

		internal HostResolver()
			: this((string name) => Task.Run(() => Dns.GetHostAddressesAsync(name)), CreateClock())
		{
		}

		internal HostResolver(Func<string, Task<IPAddress[]>> lookup, Func<long> now)
		{
			_lookup = lookup;
			_now = now;
		}

		private static Func<long> CreateClock()
		{
			Stopwatch clock = Stopwatch.StartNew();
			return () => clock.ElapsedMilliseconds;
		}

		internal static bool TryDomain(string? raw, out string domain)
		{
			domain = "";
			if (string.IsNullOrWhiteSpace(raw))
			{
				return false;
			}
			string text = raw.Trim();
			if (text.EndsWith(".", StringComparison.Ordinal))
			{
				text = text.Substring(0, text.Length - 1);
			}
			if (text.Length == 0 || text.IndexOfAny(new char[8] { ':', '/', '\\', '@', '?', '#', '|', ' ' }) >= 0)
			{
				return false;
			}
			bool flag = true;
			string text2 = text;
			foreach (char c in text2)
			{
				if ((c < '0' || c > '9') && c != '.')
				{
					flag = false;
					break;
				}
			}
			if (flag || text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
			{
				return false;
			}
			try
			{
				text = new IdnMapping().GetAscii(text).ToLowerInvariant();
			}
			catch (ArgumentException)
			{
				return false;
			}
			if (text.Length > 253)
			{
				return false;
			}
			string[] array = text.Split('.');
			if (array.Length < 2)
			{
				return false;
			}
			string[] array2 = array;
			foreach (string text3 in array2)
			{
				if (text3.Length == 0 || text3.Length > 63 || text3[0] == '-' || text3[text3.Length - 1] == '-')
				{
					return false;
				}
				text2 = text3;
				foreach (char c2 in text2)
				{
					if ((c2 < 'a' || c2 > 'z') && (c2 < '0' || c2 > '9') && c2 != '-')
					{
						return false;
					}
				}
			}
			domain = text;
			return true;
		}

		internal HostResolution Poll(string? raw, ulong lobbyId)
		{
			lock (_gate)
			{
				string text = (raw ?? "").Trim();
				string text2 = lobbyId.ToString(CultureInfo.InvariantCulture) + "|" + text.ToLowerInvariant();
				if (_key != text2)
				{
					_key = text2;
					_ready = (_error = "");
					_retryAt = 0L;
				}
				if (text.Length == 0)
				{
					return new HostResolution(ResolutionStatus.Empty);
				}
				if (Routing.TryHostIPv4(text, out IPAddress address))
				{
					return new HostResolution(ResolutionStatus.Ready, address.ToString());
				}
				if (!TryDomain(text, out string domain))
				{
					return new HostResolution(ResolutionStatus.Failed, "", "Use an IPv4 or domain only, without scheme, port or path.");
				}
				if (_ready.Length > 0)
				{
					return new HostResolution(ResolutionStatus.Ready, _ready);
				}
				if (_pending != null)
				{
					if (!_pending.IsCompleted)
					{
						if (_now() - _started >= 5000)
						{
							_timedOut = true;
							return new HostResolution(ResolutionStatus.Failed, "", "DNS lookup timed out; original transport retained while the OS finishes the lookup.");
						}
						return new HostResolution(ResolutionStatus.Pending);
					}
					Task<IPAddress[]> pending = _pending;
					_pending = null;
					bool num = _pendingKey == _key && !_timedOut;
					Exception exception = pending.Exception;
					if (num)
					{
						if (pending.Status == TaskStatus.RanToCompletion)
						{
							IPAddress[] result = pending.Result;
							foreach (IPAddress iPAddress in result)
							{
								if (Routing.IsUsable(iPAddress))
								{
									_ready = iPAddress.ToString();
									break;
								}
							}
						}
						if (_ready.Length > 0)
						{
							return new HostResolution(ResolutionStatus.Ready, _ready);
						}
						_error = ((exception == null) ? "DNS returned no usable IPv4 address (an A record is required)." : ("DNS lookup failed: " + exception.GetBaseException().Message));
						_retryAt = _now() + 15000;
					}
					else if (_pendingKey == _key)
					{
						_error = "Discarded a DNS result after timeout; retrying in 15 seconds.";
						_retryAt = _now() + 15000;
					}
				}
				if (_now() < _retryAt)
				{
					return new HostResolution(ResolutionStatus.Failed, "", _error);
				}
				try
				{
					_pendingKey = _key;
					_started = _now();
					_timedOut = false;
					_pending = _lookup(domain);
					return new HostResolution(ResolutionStatus.Pending);
				}
				catch (Exception ex)
				{
					_error = "DNS lookup failed: " + ex.Message;
					_retryAt = _now() + 15000;
					return new HostResolution(ResolutionStatus.Failed, "", _error);
				}
			}
		}
	}
	[BepInPlugin("rorodo.sakura.directconnect", "Sakura Direct Connect", "0.1.3")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		internal sealed class HostState
		{
			internal readonly string HostIp;

			internal readonly LocalNetwork[] Networks;

			internal readonly long Expires;

			internal HostState(string ip, LocalNetwork[] networks, long expires)
			{
				HostIp = ip;
				Networks = networks;
				Expires = expires;
			}
		}

		public const string Guid = "rorodo.sakura.directconnect";

		internal static Plugin? Instance;

		private static readonly Stopwatch Clock = Stopwatch.StartNew();

		private ConfigEntry<bool> _enabled;

		private ConfigEntry<string> _hostIp;

		private HostResolver _resolver = new HostResolver();

		private ConfigEntry<bool> _preferLan;

		private Harmony? _harmony;

		private RoomLifecycle? _lifecycle;

		private float _nextTick;

		private float _nextNetworks;

		private string _lastWarning = "";

		private string _lastRoute = "";

		private ulong _routeLobby;

		private ulong _routeOwner;

		private string _routeAdvertised = "";

		private string _routeSelected = "";

		private long _routeExpires;

		private volatile HostState? _hostState;

		private LocalNetwork[] _networks = Array.Empty<LocalNetwork>();

		private bool Active
		{
			get
			{
				if (_harmony != null && _enabled.Value)
				{
					return ModConfig.Enabled?.Value ?? false;
				}
				return false;
			}
		}

		private void Awake()
		{
			//IL_0100: Unknown result type (might be due to invalid IL or missing references)
			//IL_010a: Expected O, but got Unknown
			//IL_0184: Unknown result type (might be due to invalid IL or missing references)
			//IL_0192: Expected O, but got Unknown
			//IL_01aa: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b8: Expected O, but got Unknown
			//IL_01d1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01de: Expected O, but got Unknown
			Instance = this;
			_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable this extension. Original REPO Steam P2P is still required.");
			_hostIp = ((BaseUnityPlugin)this).Config.Bind<string>("Host", "HostIPv4", "", "HOST ONLY: reachable IPv4 (public, LAN, VPN) OR domain, e.g. repo.example.com. No scheme, port or path. Legacy key name retained. DNS is resolved asynchronously to IPv4 once per lobby/config; restart the lobby after DNS changes. Cloudflare records must be DNS-only for ordinary direct UDP. Leave empty for the original transport.");
			_preferLan = ((BaseUnityPlugin)this).Config.Bind<bool>("Client", "PreferLAN", true, "Try the original host-ID LAN discovery before using the advertised host IP. Guests need no IP settings.");
			try
			{
				MethodInfo methodInfo = RequireMethod(typeof(ModConfig), "ResolveTargetAddress", typeof(string), Type.EmptyTypes);
				MethodInfo methodInfo2 = RequireMethod(typeof(SdrFabric), "ResolveAdvertiseIp", typeof(string), new Type[1] { typeof(IPAddress) });
				MethodInfo methodInfo3 = RequireMethod(typeof(ModConfig), "TryGetLobby", typeof(bool), new Type[1] { typeof(Lobby).MakeByRefType() });
				_harmony = new Harmony("rorodo.sakura.directconnect");
				Assembly assembly = typeof(ModConfig).Assembly;
				Type type = assembly.GetType("RepoSteamTransport.Transport.VirtualServer", throwOnError: true);
				Type type2 = assembly.GetType("RepoSteamTransport.Transport.EnetCodec", throwOnError: true);
				Type lobbyManager = FindType("SteamLobbyManager");
				_lifecycle = new RoomLifecycle(type, type2, delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)message);
				});
				_lifecycle.Install(_harmony, type, lobbyManager);
				_harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Plugin), "TargetPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				_harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(Plugin), "AdvertisePrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				_harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "LobbyPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Sakura Direct Connect 0.1.3 loaded; host-exit room cleanup enabled. Original DLL unchanged. Tested API: Makfly REPO Steam P2P package 1.1.3.");
				if (!string.IsNullOrWhiteSpace(ModConfig.HostAddress?.Value))
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)"Original HostAddress is nonempty. Lobby auto-routing takes precedence in Steam lobbies; outside a lobby the original manual setting is preserved.");
				}
			}
			catch (Exception ex)
			{
				_hostState = null;
				Harmony? harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
				_harmony = null;
				((BaseUnityPlugin)this).Logger.LogError((object)("Extension disabled: incompatible original MOD API or patch failure. " + ex));
			}
		}

		private static MethodInfo RequireMethod(Type type, string name, Type returns, Type[] args)
		{
			MethodInfo method = type.GetMethod(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, args, null);
			if (method == null || method.ReturnType != returns)
			{
				throw new MissingMethodException(type.FullName, name);
			}
			return method;
		}

		private void Update()
		{
			if (_harmony == null || Time.unscaledTime < _nextTick)
			{
				return;
			}
			_nextTick = Time.unscaledTime + 0.5f;
			try
			{
				if (Time.unscaledTime >= _nextNetworks)
				{
					_nextNetworks = Time.unscaledTime + 10f;
					_networks = ReadNetworks();
				}
				Lobby val = default(Lobby);
				if (!ModConfig.TryGetLobby(ref val))
				{
					ResetSession();
				}
			}
			catch (Exception ex)
			{
				_hostState = null;
				WarnOnce("Lobby update failed: " + ex.Message);
			}
		}

		private void ResetSession()
		{
			_hostState = null;
			_routeLobby = (_routeOwner = 0uL);
			_routeAdvertised = (_routeSelected = (_lastRoute = ""));
			_routeExpires = 0L;
		}

		private static LocalNetwork[] ReadNetworks()
		{
			List<LocalNetwork> list = new List<LocalNetwork>();
			NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
			foreach (NetworkInterface networkInterface in allNetworkInterfaces)
			{
				if (networkInterface.OperationalStatus != OperationalStatus.Up || networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback)
				{
					continue;
				}
				foreach (UnicastIPAddressInformation unicastAddress in networkInterface.GetIPProperties().UnicastAddresses)
				{
					if (unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork && Routing.IsUsable(unicastAddress.Address) && unicastAddress.IPv4Mask != null)
					{
						list.Add(new LocalNetwork(unicastAddress.Address, unicastAddress.IPv4Mask));
					}
				}
			}
			return list.ToArray();
		}

		private void ObserveLobby(Lobby lobby)
		{
			//IL_0010: 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_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			if (!SteamClient.IsValid)
			{
				ResetSession();
				return;
			}
			ulong num = SteamId.op_Implicit(((Lobby)(ref lobby)).Owner.Id);
			ulong num2 = SteamId.op_Implicit(SteamClient.SteamId);
			if (num == 0L || num != num2)
			{
				_hostState = null;
				return;
			}
			_lifecycle?.ObserveHost(SteamId.op_Implicit(((Lobby)(ref lobby)).Id));
			string text = "";
			HostResolution hostResolution = (Active ? _resolver.Poll(_hostIp.Value, _lifecycle?.HostEpoch ?? SteamId.op_Implicit(((Lobby)(ref lobby)).Id)) : new HostResolution(ResolutionStatus.Empty));
			if (hostResolution.Status == ResolutionStatus.Ready)
			{
				string ip = hostResolution.Ip;
				_hostState = new HostState(ip, _networks, Clock.ElapsedMilliseconds + 5000);
				text = Routing.Encode(num, ip);
			}
			else
			{
				_hostState = null;
				if (hostResolution.Status == ResolutionStatus.Failed)
				{
					WarnOnce(hostResolution.Detail);
				}
			}
			string data = ((Lobby)(ref lobby)).GetData("sakura_direct_v1");
			if (!(data == text) && (!string.IsNullOrEmpty(data) || text.Length != 0))
			{
				if ((text.Length == 0) ? ((Lobby)(ref lobby)).DeleteData("sakura_direct_v1") : ((Lobby)(ref lobby)).SetData("sakura_direct_v1", text))
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)((text.Length == 0) ? "Removed this lobby's direct-IP announcement." : ("Published this host's direct IP: " + _hostState.HostIp + " (UDP 7777/7778/7779).")));
				}
				else
				{
					WarnOnce("Steam lobby metadata write failed; will retry.");
				}
			}
		}

		private static void LobbyPostfix(bool __result, Lobby __0)
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			Plugin instance = Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			try
			{
				if (__result)
				{
					instance.ObserveLobby(__0);
				}
				else
				{
					instance.ResetSession();
				}
			}
			catch (Exception ex)
			{
				instance._hostState = null;
				instance.WarnOnce("Lobby metadata unavailable: " + ex.Message);
			}
		}

		private static bool TargetPrefix(ref string __result)
		{
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			Plugin instance = Instance;
			if ((Object)(object)instance == (Object)null || !instance.Active)
			{
				return true;
			}
			try
			{
				Lobby val = default(Lobby);
				if (!SteamClient.IsValid || !ModConfig.TryGetLobby(ref val))
				{
					return true;
				}
				ulong num = SteamId.op_Implicit(((Lobby)(ref val)).Owner.Id);
				ulong num2 = SteamId.op_Implicit(SteamClient.SteamId);
				ulong num3 = SteamId.op_Implicit(((Lobby)(ref val)).Id);
				if (num == 0L || num == num2)
				{
					return true;
				}
				if (!Routing.TryAnnouncement(((Lobby)(ref val)).GetData("sakura_direct_v1"), num, num2, out string ip))
				{
					__result = num.ToString(CultureInfo.InvariantCulture);
					instance.LogRoute("Current lobby has no valid direct-IP announcement; using its owner and the original LAN/ZeroTier/Steam ladder.");
					return false;
				}
				if (instance._routeLobby != num3 || instance._routeOwner != num || instance._routeAdvertised != ip || Clock.ElapsedMilliseconds >= instance._routeExpires)
				{
					string routeSelected = ip;
					if (instance._preferLan.Value)
					{
						try
						{
							if (Routing.TryIPv4(LanDiscovery.FindHost(num, 1200), out IPAddress address) && Routing.IsUsable(address))
							{
								routeSelected = address.ToString();
							}
						}
						catch (Exception ex)
						{
							instance.WarnOnce("LAN discovery failed; using announced host IP: " + ex.Message);
						}
					}
					instance._routeLobby = num3;
					instance._routeOwner = num;
					instance._routeAdvertised = ip;
					instance._routeSelected = routeSelected;
					instance._routeExpires = Clock.ElapsedMilliseconds + 10000;
				}
				__result = instance._routeSelected;
				instance.LogRoute("Current lobby direct route: " + __result + " (owner " + num + ").");
				return false;
			}
			catch (Exception ex2)
			{
				instance.WarnOnce("Auto-route unavailable; original transport retained: " + ex2.Message);
				return true;
			}
		}

		private static bool AdvertisePrefix(IPAddress __0, ref string __result)
		{
			Plugin? instance = Instance;
			HostState hostState = ((instance != null) ? instance._hostState : null);
			if (hostState == null || Clock.ElapsedMilliseconds >= hostState.Expires || __0 == null)
			{
				return true;
			}
			__result = Routing.Advertise(__0, hostState.HostIp, hostState.Networks);
			return false;
		}

		private void LogRoute(string message)
		{
			if (_lastRoute != message)
			{
				_lastRoute = message;
				((BaseUnityPlugin)this).Logger.LogInfo((object)message);
			}
		}

		private void WarnOnce(string message)
		{
			if (_lastWarning != message)
			{
				_lastWarning = message;
				((BaseUnityPlugin)this).Logger.LogWarning((object)message);
			}
		}

		private void OnDestroy()
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			_hostState = null;
			try
			{
				Lobby val = default(Lobby);
				if (SteamClient.IsValid && ModConfig.TryGetLobby(ref val) && SteamId.op_Implicit(((Lobby)(ref val)).Owner.Id) == SteamId.op_Implicit(SteamClient.SteamId))
				{
					((Lobby)(ref val)).DeleteData("sakura_direct_v1");
				}
			}
			catch
			{
			}
			_lifecycle?.EndHost();
			_hostState = null;
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			_harmony = null;
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
			if (RoomLifecycle.Current == _lifecycle)
			{
				RoomLifecycle.Current = null;
			}
		}

		private static Type FindType(string name)
		{
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			for (int i = 0; i < assemblies.Length; i++)
			{
				Type type = assemblies[i].GetType(name);
				if (type != null)
				{
					return type;
				}
			}
			throw new TypeLoadException(name);
		}

		private static void BeforeLeaveLobby()
		{
			//IL_0023: 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_0032: Unknown result type (might be due to invalid IL or missing references)
			Plugin instance = Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			try
			{
				Lobby val = default(Lobby);
				if (SteamClient.IsValid && ModConfig.TryGetLobby(ref val) && SteamId.op_Implicit(((Lobby)(ref val)).Owner.Id) == SteamId.op_Implicit(SteamClient.SteamId))
				{
					((Lobby)(ref val)).SetJoinable(false);
					((Lobby)(ref val)).DeleteData("sakura_direct_v1");
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)instance).Logger.LogError((object)("Host exit cleanup failed: " + ex));
			}
			finally
			{
				instance._lifecycle?.EndHost();
				instance.ResetSession();
			}
		}
	}
	internal sealed class RoomLifecycle
	{
		internal static RoomLifecycle? Current;

		private readonly object gate = new object();

		private readonly object serverLock;

		private readonly IDictionary sessions;

		private readonly IDictionary rooms;

		private readonly FieldInfo peerId;

		private readonly FieldInfo challenge;

		private readonly FieldInfo send;

		private readonly FieldInfo sessionRoom;

		private readonly FieldInfo sessionActor;

		private readonly MethodInfo disconnect;

		private readonly MethodInfo datagram;

		private readonly Action<string> log;

		private readonly Dictionary<string, int> retired = new Dictionary<string, int>();

		private ulong lobbyId;

		private bool closed;

		internal ulong HostEpoch { get; private set; }

		internal RoomLifecycle(Type server, Type codec, Action<string> logger)
		{
			log = logger;
			serverLock = Field(server, "Lock").GetValue(null);
			sessions = (IDictionary)Field(server, "Sessions").GetValue(null);
			rooms = (IDictionary)Field(server, "Rooms").GetValue(null);
			Type t = server.GetNestedType("ClientSession", BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingMemberException("ClientSession");
			peerId = Field(t, "PeerId");
			challenge = Field(t, "Challenge");
			send = Field(t, "Send");
			sessionRoom = Field(t, "Room");
			sessionActor = Field(t, "Actor");
			disconnect = Method(codec, "BuildDisconnect", Type.EmptyTypes);
			datagram = Method(codec, "BuildDatagram", new Type[4]
			{
				typeof(int),
				typeof(int),
				typeof(int),
				typeof(List<byte[]>)
			});
		}

		internal static FieldInfo Field(Type t, string name)
		{
			return t.GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException(t.FullName, name);
		}

		internal static MethodInfo Method(Type t, string name, Type[] args)
		{
			return t.GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, args, null) ?? throw new MissingMethodException(t.FullName, name);
		}

		internal void Install(Harmony harmony, Type server, Type lobbyManager)
		{
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a2: Expected O, but got Unknown
			//IL_00a2: Expected O, but got Unknown
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d7: Expected O, but got Unknown
			//IL_00d7: Expected O, but got Unknown
			//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f8: Expected O, but got Unknown
			MethodInfo methodInfo = Method(server, "HandleClientDatagram", new Type[4]
			{
				typeof(string),
				typeof(byte[]),
				typeof(Action<byte[]>),
				typeof(string)
			});
			MethodInfo methodInfo2 = Method(server, "PumpSessions", Type.EmptyTypes);
			MethodInfo methodInfo3 = Method(lobbyManager, "LeaveLobby", Type.EmptyTypes);
			Current = this;
			harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(RoomLifecycle), "DatagramPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(RoomLifecycle), "Unlock", (Type[])null), (HarmonyMethod)null);
			harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(RoomLifecycle), "LockPump", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(RoomLifecycle), "Unlock", (Type[])null), (HarmonyMethod)null);
			harmony.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeof(Plugin), "BeforeLeaveLobby", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		}

		internal void ObserveHost(ulong id)
		{
			lock (gate)
			{
				if (id == 0L || id == lobbyId)
				{
					return;
				}
				lock (serverLock)
				{
					object obj = sessions["loopback:7778"];
					if (lobbyId != 0L && !closed && obj != null && sessionRoom.GetValue(obj) != null && sessionActor.GetValue(obj) != null)
					{
						log("Lobby replacement preserved: " + lobbyId + " -> " + id + "; existing game/voice rooms and sessions unchanged.");
						lobbyId = id;
						return;
					}
				}
				Clear("starting a new host lobby", preserveMaster: true);
				lobbyId = id;
				HostEpoch = id;
				closed = false;
			}
		}

		internal void EndHost()
		{
			lock (gate)
			{
				if (lobbyId != 0L)
				{
					closed = true;
					lobbyId = 0uL;
					HostEpoch = 0uL;
					Clear("real host exit cleanup; disconnecting all remaining players");
				}
			}
		}

		private void Clear(string reason, bool preserveMaster = false)
		{
			lock (serverLock)
			{
				int count = sessions.Count;
				int count2 = rooms.Count;
				foreach (DictionaryEntry session in sessions)
				{
					string text = (string)session.Key;
					object value = session.Value;
					int num = (int)challenge.GetValue(value);
					if (text.StartsWith("loopback:", StringComparison.Ordinal))
					{
						continue;
					}
					retired[text] = num;
					try
					{
						byte[] item = (byte[])disconnect.Invoke(null, null);
						byte[] obj = (byte[])datagram.Invoke(null, new object[4]
						{
							(int)peerId.GetValue(value),
							Environment.TickCount,
							num,
							new List<byte[]> { item }
						});
						Action<byte[]> action = (Action<byte[]>)send.GetValue(value);
						for (int i = 0; i < 3; i++)
						{
							action?.Invoke(obj);
						}
					}
					catch (Exception ex)
					{
						log("Disconnect notice failed for " + text + ": " + ex.GetBaseException().Message);
					}
				}
				object obj2 = (preserveMaster ? sessions["loopback:7777"] : null);
				sessions.Clear();
				if (obj2 != null)
				{
					sessions["loopback:7777"] = obj2;
				}
				rooms.Clear();
				log("Room cleanup: " + reason + "; removed " + count2 + " rooms and " + count + " sessions.");
			}
		}

		internal bool Accept(string key, byte[] packet)
		{
			if (key.StartsWith("loopback:", StringComparison.Ordinal))
			{
				if (closed)
				{
					return key == "loopback:7777";
				}
				return true;
			}
			if (closed)
			{
				return false;
			}
			if (packet == null || packet.Length < 12)
			{
				return false;
			}
			int num = (packet[8] << 24) | (packet[9] << 16) | (packet[10] << 8) | packet[11];
			if (key.StartsWith("udp:", StringComparison.Ordinal) && packet.Length == 24 && packet[3] == 1 && packet[12] == 5 && num == 28036591)
			{
				return true;
			}
			if (retired.TryGetValue(key, out var value) && value == num)
			{
				return false;
			}
			if (!sessions.Contains(key) && (packet.Length < 24 || packet[3] == 0 || packet[12] != 2))
			{
				return false;
			}
			return true;
		}

		private static bool DatagramPrefix(string __0, byte[] __1, ref object? __state)
		{
			RoomLifecycle current = Current;
			if (current == null)
			{
				return true;
			}
			Monitor.Enter(current.gate);
			__state = current.gate;
			lock (current.serverLock)
			{
				return current.Accept(__0, __1);
			}
		}

		private static void LockPump(ref object? __state)
		{
			RoomLifecycle current = Current;
			if (current != null)
			{
				Monitor.Enter(current.gate);
				__state = current.gate;
			}
		}

		private static Exception? Unlock(Exception? __exception, object? __state)
		{
			if (__state != null)
			{
				Monitor.Exit(__state);
			}
			return __exception;
		}
	}
	internal readonly struct LocalNetwork
	{
		internal readonly IPAddress Address;

		internal readonly IPAddress Mask;

		internal LocalNetwork(IPAddress address, IPAddress mask)
		{
			Address = address;
			Mask = mask;
		}
	}
	internal static class Routing
	{
		internal const string LobbyKey = "sakura_direct_v1";

		internal static bool TryIPv4(string? value, out IPAddress address)
		{
			address = IPAddress.None;
			if (string.IsNullOrWhiteSpace(value))
			{
				return false;
			}
			string[] array = value.Trim().Split('.');
			if (array.Length != 4)
			{
				return false;
			}
			byte[] array2 = new byte[4];
			for (int i = 0; i < 4; i++)
			{
				if (array[i].Length == 0 || array[i].Length > 3)
				{
					return false;
				}
				string text = array[i];
				foreach (char c in text)
				{
					if (c < '0' || c > '9')
					{
						return false;
					}
				}
				if (!byte.TryParse(array[i], NumberStyles.None, CultureInfo.InvariantCulture, out array2[i]))
				{
					return false;
				}
			}
			address = new IPAddress(array2);
			return true;
		}

		internal static bool IsUsable(IPAddress address)
		{
			if (address.AddressFamily != AddressFamily.InterNetwork)
			{
				return false;
			}
			byte[] addressBytes = address.GetAddressBytes();
			if (addressBytes[0] != 0 && addressBytes[0] != 127 && addressBytes[0] < 224)
			{
				if (addressBytes[0] == 169)
				{
					return addressBytes[1] != 254;
				}
				return true;
			}
			return false;
		}

		internal static bool IsPrivate(IPAddress address)
		{
			if (address.AddressFamily != AddressFamily.InterNetwork)
			{
				return false;
			}
			byte[] addressBytes = address.GetAddressBytes();
			if (addressBytes[0] != 10 && (addressBytes[0] != 172 || addressBytes[1] < 16 || addressBytes[1] > 31))
			{
				if (addressBytes[0] == 192)
				{
					return addressBytes[1] == 168;
				}
				return false;
			}
			return true;
		}

		internal static bool TryHostIPv4(string? value, out IPAddress address)
		{
			if (TryIPv4(value, out address))
			{
				return IsUsable(address);
			}
			return false;
		}

		internal static string Encode(ulong owner, string ip)
		{
			return "1|" + owner.ToString(CultureInfo.InvariantCulture) + "|" + ip;
		}

		internal static bool TryAnnouncement(string? value, ulong actualOwner, ulong self, out string ip)
		{
			ip = "";
			if (actualOwner == 0L || actualOwner == self || string.IsNullOrEmpty(value) || value.Length > 80)
			{
				return false;
			}
			string[] array = value.Split('|');
			if (array.Length != 3 || array[0] != "1" || !ulong.TryParse(array[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result != actualOwner || !TryHostIPv4(array[2], out IPAddress address))
			{
				return false;
			}
			ip = address.ToString();
			return true;
		}

		internal static bool SameSubnet(IPAddress a, IPAddress b, IPAddress mask)
		{
			if (a.AddressFamily != AddressFamily.InterNetwork || b.AddressFamily != AddressFamily.InterNetwork || mask.AddressFamily != AddressFamily.InterNetwork)
			{
				return false;
			}
			byte[] addressBytes = a.GetAddressBytes();
			byte[] addressBytes2 = b.GetAddressBytes();
			byte[] addressBytes3 = mask.GetAddressBytes();
			int num = (addressBytes3[0] << 24) | (addressBytes3[1] << 16) | (addressBytes3[2] << 8) | addressBytes3[3];
			uint num2 = (uint)(~num);
			if (num == 0 || (num2 & (num2 + 1)) != 0)
			{
				return false;
			}
			for (int i = 0; i < 4; i++)
			{
				if ((addressBytes[i] & addressBytes3[i]) != (addressBytes2[i] & addressBytes3[i]))
				{
					return false;
				}
			}
			return true;
		}

		internal static string Advertise(IPAddress remote, string hostIp, LocalNetwork[] networks)
		{
			if (remote.ToString() == hostIp)
			{
				return hostIp;
			}
			for (int i = 0; i < networks.Length; i++)
			{
				LocalNetwork localNetwork = networks[i];
				if (IsUsable(localNetwork.Address) && SameSubnet(remote, localNetwork.Address, localNetwork.Mask))
				{
					return localNetwork.Address.ToString();
				}
			}
			return hostIp;
		}
	}
}