Decompiled source of koumodgp v0.1.9

koumodgp.dll

Decompiled 2 days ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Mono.Cecil;
using Mono.Collections.Generic;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("0.0.0.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 koumodgp
{
	internal static class Admin
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static ConsoleEvent <>9__2_0;

			internal void <Register>b__2_0(ConsoleEventArgs args)
			{
				Terminal context = args.Context;
				if (context != null)
				{
					context.AddString(Execute(args.Args));
				}
			}
		}

		private static bool _registered;

		private static string _cfgDir;

		internal static void Register(string configDir)
		{
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Expected O, but got Unknown
			if (_registered)
			{
				return;
			}
			_cfgDir = configDir;
			try
			{
				object obj = <>c.<>9__2_0;
				if (obj == null)
				{
					ConsoleEvent val = delegate(ConsoleEventArgs args)
					{
						Terminal context = args.Context;
						if (context != null)
						{
							context.AddString(Execute(args.Args));
						}
					};
					<>c.<>9__2_0 = val;
					obj = (object)val;
				}
				new ConsoleCommand("kougp", "koumodgp admin: status|list|reload|allow|sig|kick|ban", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, true);
				_registered = true;
				Plugin.Log.LogInfo((object)"[koumodgp] admin command 'kougp' registered (server console).");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] could not register command: " + ex.Message));
			}
		}

		internal static void SetConfigDir(string configDir)
		{
			_cfgDir = configDir;
		}

		internal static string Execute(string[] a)
		{
			StringBuilder stringBuilder = new StringBuilder();
			switch ((a.Length > 1) ? a[1].ToLowerInvariant() : "status")
			{
			case "status":
				Status(stringBuilder);
				break;
			case "list":
				ListCmd(stringBuilder);
				break;
			case "reload":
				Config.LoadLists();
				PushToAll();
				stringBuilder.AppendLine("config + lists reloaded, pushed to clients.");
				Discord.PostLifecycle("koumodgp: config reloaded by admin.");
				break;
			case "allow":
				Allow(stringBuilder, a);
				break;
			case "sig":
				Sig(stringBuilder, a);
				break;
			case "kick":
				KickBan(stringBuilder, a, ban: false);
				break;
			case "ban":
				KickBan(stringBuilder, a, ban: true);
				break;
			default:
				stringBuilder.AppendLine("usage: kougp status|list|reload|allow add/remove <guid> [ver]|sig add/remove <text>|kick <name>|ban <name>");
				break;
			}
			string text = stringBuilder.ToString().TrimEnd(Array.Empty<char>());
			Plugin.Log.LogInfo((object)("[koumodgp] " + text));
			return text;
		}

		private static void Status(StringBuilder sb)
		{
			List<Session> list = Sessions.Snapshot();
			sb.AppendLine($"koumodgp: {list.Count} tracked peers, {Config.AllowedMods.Count} required, {Config.OptionalMods.Count} optional, {Config.Signatures.Count} signatures.");
			foreach (Session item in list)
			{
				sb.AppendLine($"  {item.PlayerName} ({item.SteamId}) verified={item.Verified} lastHB={Time.time - item.LastHeartbeat:F0}s ago");
			}
		}

		private static void ListCmd(StringBuilder sb)
		{
			sb.AppendLine("Required mods:");
			foreach (KeyValuePair<string, string> allowedMod in Config.AllowedMods)
			{
				sb.AppendLine("  " + allowedMod.Key + " = " + allowedMod.Value);
			}
			sb.AppendLine("Optional mods:");
			foreach (KeyValuePair<string, string> optionalMod in Config.OptionalMods)
			{
				sb.AppendLine("  " + optionalMod.Key + " = " + optionalMod.Value);
			}
			sb.AppendLine("Signatures:");
			foreach (string signature in Config.Signatures)
			{
				sb.AppendLine("  " + signature);
			}
		}

		private static void Allow(StringBuilder sb, string[] a)
		{
			if (a.Length < 4)
			{
				sb.AppendLine("usage: kougp allow add <guid> [version] | remove <guid>");
				return;
			}
			string text = a[2].ToLowerInvariant();
			string guid = a[3];
			string text2 = ((a.Length > 4) ? a[4] : "*");
			string path = Path.Combine(_cfgDir, "koumodgp", "allowed_mods.txt");
			if (text == "add")
			{
				File.AppendAllText(path, guid + "=" + text2 + Environment.NewLine);
				sb.AppendLine("added " + guid + "=" + text2);
			}
			else if (text == "remove")
			{
				string[] contents = (from l in File.ReadAllLines(path)
					where !l.Trim().StartsWith(guid + "=")
					select l).ToArray();
				File.WriteAllLines(path, contents);
				sb.AppendLine("removed " + guid);
			}
			Config.LoadLists();
			PushToAll();
			Discord.PostLifecycle("koumodgp: allow list edited by admin (" + text + " " + guid + ").");
		}

		private static void Sig(StringBuilder sb, string[] a)
		{
			if (a.Length < 4)
			{
				sb.AppendLine("usage: kougp sig add <text> | remove <text>");
				return;
			}
			string text = a[2].ToLowerInvariant();
			string text2 = string.Join(" ", a.Skip(3)).ToLowerInvariant();
			string path = Path.Combine(_cfgDir, "koumodgp", "signatures.txt");
			if (text == "add")
			{
				File.AppendAllText(path, text2 + Environment.NewLine);
				sb.AppendLine("added signature '" + text2 + "'");
			}
			else if (text == "remove")
			{
				string[] contents = (from l in File.ReadAllLines(path)
					where l.Trim().ToLowerInvariant() != text2
					select l).ToArray();
				File.WriteAllLines(path, contents);
				sb.AppendLine("removed signature '" + text2 + "'");
			}
			Config.LoadLists();
			PushToAll();
			Discord.PostLifecycle("koumodgp: signature list edited by admin (" + text + " " + text2 + ").");
		}

		private static void KickBan(StringBuilder sb, string[] a, bool ban)
		{
			if (a.Length < 3)
			{
				sb.AppendLine("usage: kougp " + (ban ? "ban" : "kick") + " <player name>");
				return;
			}
			string text = string.Join(" ", a.Skip(2));
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null)
			{
				sb.AppendLine("no ZNet.");
			}
			else if (ban)
			{
				instance.Ban(text);
				sb.AppendLine("banned " + text + " (bannedlist.txt).");
				Discord.PostLifecycle("koumodgp: admin banned " + text + ".");
			}
			else
			{
				instance.Kick(text);
				sb.AppendLine("kicked " + text + ".");
				Discord.PostLifecycle("koumodgp: admin kicked " + text + ".");
			}
		}

		private static void PushToAll()
		{
			foreach (Session item in Sessions.Snapshot())
			{
				ServerModule.PushConfig(item.PeerUid, item.SessionKey);
			}
		}
	}
	internal static class ClientModule
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static ConsoleEvent <>9__6_0;

			internal void <RegisterInGameCommand>b__6_0(ConsoleEventArgs args)
			{
				Rpc.SendToServer(Protocol.MsgType.AdminCmd, new <>c__DisplayClass6_0
				{
					cmd = ((args.Args.Length > 1) ? string.Join(" ", args.Args.Skip(1)) : "status")
				}.<RegisterInGameCommand>b__1, SessionKey);
				Terminal context = args.Context;
				if (context != null)
				{
					context.AddString("koumodgp: sent to server…");
				}
			}
		}

		[CompilerGenerated]
		private sealed class <>c__DisplayClass6_0
		{
			public string cmd;

			internal void <RegisterInGameCommand>b__1(ZPackage pkg)
			{
				pkg.Write(cmd);
			}
		}

		internal static volatile byte[] SessionKey;

		private static int _counter;

		private static float _lastBeat;

		private const float BeatInterval = 10f;

		private static bool _cmdRegistered;

		internal static void Init()
		{
			SessionKey = null;
			_counter = 0;
			_lastBeat = 0f;
			RegisterInGameCommand();
			Plugin.Log.LogInfo((object)"[koumodgp] client module active — awaiting challenge.");
		}

		private static void RegisterInGameCommand()
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Expected O, but got Unknown
			if (_cmdRegistered)
			{
				return;
			}
			try
			{
				object obj = <>c.<>9__6_0;
				if (obj == null)
				{
					ConsoleEvent val = delegate(ConsoleEventArgs args)
					{
						string cmd = ((args.Args.Length > 1) ? string.Join(" ", args.Args.Skip(1)) : "status");
						Rpc.SendToServer(Protocol.MsgType.AdminCmd, delegate(ZPackage pkg)
						{
							pkg.Write(cmd);
						}, SessionKey);
						Terminal context = args.Context;
						if (context != null)
						{
							context.AddString("koumodgp: sent to server…");
						}
					};
					<>c.<>9__6_0 = val;
					obj = (object)val;
				}
				new ConsoleCommand("kougp", "koumodgp admin (sent to server)", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, true);
				_cmdRegistered = true;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] client command register failed: " + ex.Message));
			}
		}

		internal static void OnNonce(ZPackage inner)
		{
			try
			{
				byte[] nonce = inner.ReadByteArray();
				byte[] salt = inner.ReadByteArray();
				string identity = inner.ReadString();
				SessionKey = Crypto.DeriveSessionKey(nonce, salt, identity);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] bad nonce: " + ex.Message));
				return;
			}
			List<Verdict.Finding> findings = new List<Verdict.Finding>();
			RunScans(findings);
			byte[] selfHash = SelfHash.Compute();
			Dictionary<string, string> plugins = CollectPlugins();
			Rpc.SendToServer(Protocol.MsgType.Manifest, delegate(ZPackage pkg)
			{
				pkg.Write(selfHash);
				pkg.Write(plugins.Count);
				foreach (KeyValuePair<string, string> item in plugins)
				{
					pkg.Write(item.Key);
					pkg.Write(item.Value);
				}
				WriteFindings(pkg, findings);
			}, SessionKey);
			_lastBeat = Time.time;
			Plugin.Log.LogInfo((object)string.Format("[{0}] manifest sent ({1} mods, {2} findings).", "koumodgp", plugins.Count, findings.Count));
		}

		internal static void OnConfigPush(ZPackage inner)
		{
			try
			{
				int num = inner.ReadInt();
				string[] array = new string[num];
				for (int i = 0; i < num; i++)
				{
					array[i] = inner.ReadString();
				}
				ScanProcess.Signatures = array;
				Plugin.Log.LogInfo((object)string.Format("[{0}] received {1} signatures.", "koumodgp", num));
			}
			catch
			{
			}
		}

		internal static void Tick()
		{
			if (SessionKey != null && !(Time.time - _lastBeat < 10f))
			{
				_lastBeat = Time.time;
				List<Verdict.Finding> findings = new List<Verdict.Finding>();
				RunScans(findings);
				int counter = ++_counter;
				Rpc.SendToServer(Protocol.MsgType.Heartbeat, delegate(ZPackage pkg)
				{
					pkg.Write(counter);
					WriteFindings(pkg, findings);
				}, SessionKey);
			}
		}

		private static void RunScans(List<Verdict.Finding> findings)
		{
			ScanAssembly.Collect(findings);
			ScanState.Collect(findings);
			ScanProcess.Collect(findings);
		}

		private static Dictionary<string, string> CollectPlugins()
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			try
			{
				foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
				{
					PluginInfo value = pluginInfo.Value;
					BepInPlugin val = ((value != null) ? value.Metadata : null);
					if (val != null)
					{
						dictionary[val.GUID] = val.Version?.ToString() ?? "0";
					}
				}
			}
			catch
			{
			}
			return dictionary;
		}

		private static void WriteFindings(ZPackage pkg, List<Verdict.Finding> findings)
		{
			pkg.Write(findings.Count);
			foreach (Verdict.Finding finding in findings)
			{
				pkg.Write((byte)finding.Cat);
				pkg.Write(finding.Detail ?? "");
			}
		}
	}
	internal static class Config
	{
		internal enum Action
		{
			Log,
			Warn,
			Flag,
			Kick,
			Ban
		}

		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<int> HandshakeTimeoutSeconds;

		internal static ConfigEntry<int> HeartbeatIntervalSeconds;

		internal static ConfigEntry<int> MaxMissedHeartbeats;

		internal static ConfigEntry<bool> ExemptAdmins;

		internal static ConfigEntry<Action> ActMissingCompanion;

		internal static ConfigEntry<Action> ActModMismatch;

		internal static ConfigEntry<Action> ActInjectedAssembly;

		internal static ConfigEntry<Action> ActForeignHarmony;

		internal static ConfigEntry<Action> ActStateAnomaly;

		internal static ConfigEntry<Action> ActProcessMatch;

		internal static ConfigEntry<Action> ActHeartbeatFail;

		internal static ConfigEntry<string> DiscordWebhookUrl;

		internal static ConfigEntry<bool> DiscordPostLifecycle;

		internal static readonly Dictionary<string, string> AllowedMods = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

		internal static readonly Dictionary<string, string> OptionalMods = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

		internal static readonly List<string> Signatures = new List<string>();

		private static string _cfgDir;

		internal static void Init(ConfigFile cfg, string configDir)
		{
			_cfgDir = configDir;
			Enabled = cfg.Bind<bool>("General", "Enabled", true, "Master switch.");
			HandshakeTimeoutSeconds = cfg.Bind<int>("General", "HandshakeTimeoutSeconds", 20, "Seconds a joining client has to complete the handshake before being kicked.");
			HeartbeatIntervalSeconds = cfg.Bind<int>("General", "HeartbeatIntervalSeconds", 15, "How often clients send a signed heartbeat.");
			MaxMissedHeartbeats = cfg.Bind<int>("General", "MaxMissedHeartbeats", 2, "Consecutive missed heartbeats tolerated before kick.");
			ExemptAdmins = cfg.Bind<bool>("General", "ExemptAdmins", true, "Admins (adminlist.txt) are fully exempt and silent — never checked or logged.");
			ActMissingCompanion = cfg.Bind<Action>("Policy", "MissingCompanion", Action.Kick, "Client never completed the handshake (vanilla or stripped companion).");
			ActModMismatch = cfg.Bind<Action>("Policy", "ModMismatch", Action.Kick, "Client mod list does not match the allowed list.");
			ActInjectedAssembly = cfg.Bind<Action>("Policy", "InjectedAssembly", Action.Kick, "A managed assembly not loaded by Chainloader was found (injected tool).");
			ActForeignHarmony = cfg.Bind<Action>("Policy", "ForeignHarmony", Action.Kick, "A Harmony patch from a non-whitelisted owner id was found.");
			ActStateAnomaly = cfg.Bind<Action>("Policy", "StateAnomaly", Action.Kick, "A monitored game field held an impossible value (trainer edit).");
			ActProcessMatch = cfg.Bind<Action>("Policy", "ProcessMatch", Action.Kick, "A known cheat-program signature (process/window) matched. Kick on its own, even if open for another game.");
			ActHeartbeatFail = cfg.Bind<Action>("Policy", "HeartbeatFail", Action.Kick, "Heartbeats stopped, went out of order, or failed their signature.");
			DiscordWebhookUrl = cfg.Bind<string>("Logging", "DiscordWebhookUrl", "", "Discord webhook URL. Empty = disabled. Detection posts are generic ('External violation detected').");
			DiscordPostLifecycle = cfg.Bind<bool>("Logging", "DiscordPostLifecycleEvents", true, "Also post server start/stop, config reloads and manual admin actions.");
			LoadLists();
		}

		internal static void LoadLists()
		{
			AllowedMods.Clear();
			OptionalMods.Clear();
			Signatures.Clear();
			string path = Path.Combine(_cfgDir, "koumodgp", "allowed_mods.txt");
			string path2 = Path.Combine(_cfgDir, "koumodgp", "signatures.txt");
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			if (!File.Exists(path))
			{
				File.WriteAllText(path, "# koumodgp allowed mods — one per line: guid=version  (or guid=* for any version)\n# Required on every client (missing => kick). Prefix with '?' to make it OPTIONAL\n# (allowed if present, version checked, but not required): ?guid=version\n# example:\n# com.modbykou.koumodgp=0.1.9\n# ?some.clientonly.mod=*\n");
			}
			if (!File.Exists(path2))
			{
				File.WriteAllText(path2, "# koumodgp signatures — process names or window-title fragments, one per line.\n# example:\n# wemod\n# cheat engine\n");
			}
			string[] array = File.ReadAllLines(path);
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (text.Length == 0 || text.StartsWith("#"))
				{
					continue;
				}
				bool flag = text.StartsWith("?");
				if (flag)
				{
					text = text.Substring(1).Trim();
				}
				int num = text.IndexOf('=');
				if (num <= 0)
				{
					continue;
				}
				string text2 = text.Substring(0, num).Trim();
				string value = text.Substring(num + 1).Trim();
				if (text2.Length != 0)
				{
					if (flag)
					{
						OptionalMods[text2] = value;
					}
					else
					{
						AllowedMods[text2] = value;
					}
				}
			}
			array = File.ReadAllLines(path2);
			for (int i = 0; i < array.Length; i++)
			{
				string text3 = array[i].Trim();
				if (text3.Length != 0 && !text3.StartsWith("#"))
				{
					Signatures.Add(text3.ToLowerInvariant());
				}
			}
			LoadModFolder(Path.Combine(_cfgDir, "koumodgp", "required"), AllowedMods);
			LoadModFolder(Path.Combine(_cfgDir, "koumodgp", "allowed"), OptionalMods);
			Plugin.Log.LogInfo((object)string.Format("[{0}] lists loaded: {1} required, {2} optional, {3} signatures.", "koumodgp", AllowedMods.Count, OptionalMods.Count, Signatures.Count));
		}

		private static void LoadModFolder(string folder, Dictionary<string, string> target)
		{
			try
			{
				Directory.CreateDirectory(folder);
				string path = Path.Combine(folder, "_README.txt");
				if (!File.Exists(path))
				{
					File.WriteAllText(path, "Paste mod .dll files here. koumodgp reads each mod's id and version\nautomatically — nothing else to configure.\n");
				}
				string[] files = Directory.GetFiles(folder, "*.dll", SearchOption.AllDirectories);
				for (int i = 0; i < files.Length; i++)
				{
					var (text, text2) = ReadPluginInfo(files[i]);
					if (!string.IsNullOrEmpty(text))
					{
						target[text] = text2 ?? "*";
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] mod folder '" + folder + "' read error: " + ex.Message));
			}
		}

		private static (string guid, string version) ReadPluginInfo(string dllPath)
		{
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: 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_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				AssemblyDefinition val = AssemblyDefinition.ReadAssembly(dllPath);
				try
				{
					Enumerator<TypeDefinition> enumerator = val.MainModule.Types.GetEnumerator();
					try
					{
						while (enumerator.MoveNext())
						{
							Enumerator<CustomAttribute> enumerator2 = enumerator.Current.CustomAttributes.GetEnumerator();
							try
							{
								while (enumerator2.MoveNext())
								{
									CustomAttribute current = enumerator2.Current;
									if (!(((MemberReference)current.AttributeType).Name != "BepInPlugin") && current.ConstructorArguments.Count >= 3)
									{
										CustomAttributeArgument val2 = current.ConstructorArguments[0];
										string item = ((CustomAttributeArgument)(ref val2)).Value as string;
										val2 = current.ConstructorArguments[2];
										string item2 = ((CustomAttributeArgument)(ref val2)).Value as string;
										return (guid: item, version: item2);
									}
								}
							}
							finally
							{
								((IDisposable)enumerator2/*cast due to .constrained prefix*/).Dispose();
							}
						}
					}
					finally
					{
						((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose();
					}
				}
				finally
				{
					((IDisposable)val)?.Dispose();
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] could not read '" + Path.GetFileName(dllPath) + "': " + ex.Message));
			}
			return (guid: null, version: null);
		}
	}
	internal static class Crypto
	{
		private static readonly byte[] SharedSecret = Encoding.UTF8.GetBytes("kou-gp-shared-v1-8f2a1c9e7b40d5a3");

		internal const int TagLen = 32;

		internal static byte[] Hmac(byte[] data, byte[] key = null)
		{
			using HMACSHA256 hMACSHA = new HMACSHA256(key ?? SharedSecret);
			return hMACSHA.ComputeHash(data);
		}

		internal static byte[] DeriveSessionKey(byte[] nonce, byte[] salt, string identity)
		{
			byte[] bytes = Encoding.UTF8.GetBytes(identity ?? "");
			byte[] array = new byte[((nonce != null) ? nonce.Length : 0) + ((salt != null) ? salt.Length : 0) + bytes.Length];
			int num = 0;
			if (nonce != null)
			{
				Buffer.BlockCopy(nonce, 0, array, num, nonce.Length);
				num += nonce.Length;
			}
			if (salt != null)
			{
				Buffer.BlockCopy(salt, 0, array, num, salt.Length);
				num += salt.Length;
			}
			Buffer.BlockCopy(bytes, 0, array, num, bytes.Length);
			return Hmac(array);
		}

		internal static byte[] RandomBytes(int n)
		{
			byte[] array = new byte[n];
			using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create();
			randomNumberGenerator.GetBytes(array);
			return array;
		}

		internal static bool FixedTimeEquals(byte[] a, byte[] b)
		{
			if (a == null || b == null || a.Length != b.Length)
			{
				return false;
			}
			int num = 0;
			for (int i = 0; i < a.Length; i++)
			{
				num |= a[i] ^ b[i];
			}
			return num == 0;
		}
	}
	internal static class Discord
	{
		private static readonly BlockingCollection<string> _queue = new BlockingCollection<string>();

		private static Thread _worker;

		private static volatile bool _running;

		internal static void Init()
		{
			if (_running)
			{
				return;
			}
			_running = true;
			_worker = new Thread(Drain)
			{
				IsBackground = true,
				Name = "koumodgp-webhook"
			};
			_worker.Start();
			try
			{
				ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
			}
			catch
			{
			}
		}

		internal static void Shutdown()
		{
			_running = false;
			try
			{
				_queue.CompleteAdding();
			}
			catch
			{
			}
		}

		internal static void PostDetection(string utc, string playerName, string steamId, string type, string detail)
		{
			string text = "Player: `" + Escape(playerName) + "`\nID: `" + Escape(steamId) + "`\nTime: " + utc + " UTC";
			if (type == "ModMismatch")
			{
				Post("**Mod check failed**\n" + text + "\nIssue: " + Escape(detail));
			}
			else
			{
				Post("**Player using external/internal cheat**\n" + text);
			}
		}

		internal static void PostLifecycle(string message)
		{
			if (Config.DiscordPostLifecycle != null && Config.DiscordPostLifecycle.Value)
			{
				Post(message);
			}
		}

		private static void Post(string content)
		{
			if (string.IsNullOrEmpty(CleanUrl()))
			{
				return;
			}
			if (!_running)
			{
				Init();
			}
			try
			{
				_queue.Add(content);
			}
			catch
			{
			}
		}

		private static string CleanUrl()
		{
			string text = Config.DiscordWebhookUrl?.Value;
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			text = text.Trim().Trim('"', '\'', '<', '>');
			if (!text.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !text.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
			{
				return null;
			}
			return text;
		}

		private static void Drain()
		{
			foreach (string item in _queue.GetConsumingEnumerable())
			{
				string text = CleanUrl();
				if (text == null)
				{
					continue;
				}
				for (int i = 0; i < 2; i++)
				{
					if (TrySend(text, item))
					{
						break;
					}
					Thread.Sleep(1000);
				}
			}
		}

		private static bool TrySend(string url, string content)
		{
			try
			{
				Uri uri = new Uri(url);
				string s = "{\"content\":\"" + JsonEscape(content) + "\"}";
				byte[] bytes = Encoding.UTF8.GetBytes(s);
				using TcpClient tcpClient = new TcpClient();
				tcpClient.Connect(uri.Host, (uri.Port == -1) ? 443 : uri.Port);
				using SslStream sslStream = new SslStream(tcpClient.GetStream(), leaveInnerStreamOpen: false, (object obj, X509Certificate c, X509Chain ch, SslPolicyErrors e) => true);
				sslStream.AuthenticateAsClient(uri.Host, null, SslProtocols.Tls12, checkCertificateRevocation: false);
				StringBuilder stringBuilder = new StringBuilder();
				stringBuilder.Append("POST ").Append(uri.PathAndQuery).Append(" HTTP/1.1\r\n");
				stringBuilder.Append("Host: ").Append(uri.Host).Append("\r\n");
				stringBuilder.Append("User-Agent: koumodgp\r\n");
				stringBuilder.Append("Content-Type: application/json\r\n");
				stringBuilder.Append("Content-Length: ").Append(bytes.Length).Append("\r\n");
				stringBuilder.Append("Connection: close\r\n\r\n");
				byte[] bytes2 = Encoding.ASCII.GetBytes(stringBuilder.ToString());
				sslStream.Write(bytes2, 0, bytes2.Length);
				sslStream.Write(bytes, 0, bytes.Length);
				sslStream.Flush();
				using StreamReader streamReader = new StreamReader(sslStream);
				string text = streamReader.ReadLine();
				int num;
				if (text != null)
				{
					if (!text.Contains(" 200"))
					{
						num = (text.Contains(" 204") ? 1 : 0);
						if (num == 0)
						{
							goto IL_0190;
						}
					}
					else
					{
						num = 1;
					}
					goto IL_01a6;
				}
				num = 0;
				goto IL_0190;
				IL_0190:
				Plugin.Log.LogWarning((object)("[koumodgp] webhook status: " + text));
				goto IL_01a6;
				IL_01a6:
				return (byte)num != 0;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] webhook post failed: " + ex.Message));
				return false;
			}
		}

		private static string Escape(string s)
		{
			return (s ?? "").Replace("`", "'");
		}

		private static string JsonEscape(string s)
		{
			return (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n")
				.Replace("\r", "");
		}
	}
	[BepInPlugin("com.modbykou.koumodgp", "koumodgp", "0.1.9")]
	public class Plugin : BaseUnityPlugin
	{
		internal enum Role
		{
			Unknown,
			Server,
			Client
		}

		[HarmonyPatch(typeof(ZNet), "Awake")]
		private static class ZNet_Awake_Patch
		{
			private static void Postfix(ZNet __instance)
			{
				bool num = __instance.IsServer();
				CurrentRole = (num ? Role.Server : Role.Client);
				Rpc.Reset();
				Rpc.Register();
				Log.LogInfo((object)(string.Format("[{0}] role resolved: {1} ", "koumodgp", CurrentRole) + $"(dedicated={__instance.IsDedicated()})."));
				if (num)
				{
					ServerModule.Init();
					Admin.Register(Paths.ConfigPath);
				}
				else
				{
					ClientModule.Init();
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
		private static class ZNet_RPC_PeerInfo_Patch
		{
			private static void Postfix(ZNet __instance, ZRpc rpc)
			{
				if (CurrentRole != Role.Server || (Object)(object)__instance == (Object)null || rpc == null)
				{
					return;
				}
				ZNetPeer val = null;
				foreach (ZNetPeer peer in __instance.GetPeers())
				{
					if (peer != null && peer.m_rpc == rpc)
					{
						val = peer;
						break;
					}
				}
				if (val != null && val.m_uid != 0L)
				{
					ServerModule.OnPeerJoined(__instance, val);
				}
			}
		}

		internal static ManualLogSource Log;

		private Harmony _harmony;

		internal static Role CurrentRole;

		private void Awake()
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			string configPath = Paths.ConfigPath;
			Config.Init(((BaseUnityPlugin)this).Config, configPath);
			ViolationLog.Init(configPath);
			Discord.Init();
			_harmony = new Harmony("com.modbykou.koumodgp");
			_harmony.PatchAll(typeof(Plugin).Assembly);
			Log.LogInfo((object)string.Format("[{0}] v{1} loaded (wire {2}) — awaiting role.", "koumodgp", "0.1.9", 1));
		}

		private void OnDestroy()
		{
			Discord.Shutdown();
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}

		private void Update()
		{
			if (CurrentRole == Role.Server)
			{
				ServerModule.Tick(ZNet.instance);
			}
			else if (CurrentRole == Role.Client)
			{
				ClientModule.Tick();
			}
		}
	}
	public static class Protocol
	{
		public enum MsgType : byte
		{
			Ping,
			Nonce,
			Manifest,
			Heartbeat,
			ConfigPush,
			Verdict,
			AdminCmd,
			AdminReply
		}

		public enum FindingCat : byte
		{
			InjectedAssembly,
			ForeignHarmony,
			StateAnomaly,
			ProcessMatch
		}

		public const string PluginGuid = "com.modbykou.koumodgp";

		public const string PluginName = "koumodgp";

		public const string PluginVersion = "0.1.9";

		public const string RpcClientToServer = "kou_gpsync_a";

		public const string RpcServerToClient = "kou_gpsync_b";

		public const int WireVersion = 1;
	}
	internal static class Rpc
	{
		private static bool s_registered;

		private static readonly MethodInfo _getServerPeerID = AccessTools.Method(typeof(ZRoutedRpc), "GetServerPeerID", (Type[])null, (Type[])null);

		internal static void Register()
		{
			if (!s_registered && ZRoutedRpc.instance != null)
			{
				ZRoutedRpc.instance.Register<ZPackage>("kou_gpsync_a", (Action<long, ZPackage>)OnClientToServer);
				ZRoutedRpc.instance.Register<ZPackage>("kou_gpsync_b", (Action<long, ZPackage>)OnServerToClient);
				s_registered = true;
				Plugin.Log.LogInfo((object)"[koumodgp] transport registered (kou_gpsync_a, kou_gpsync_b).");
			}
		}

		internal static void Reset()
		{
			s_registered = false;
		}

		internal static void SendToServer(Protocol.MsgType type, Action<ZPackage> writeBody, byte[] key = null)
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null && !(_getServerPeerID == null))
			{
				long num = (long)_getServerPeerID.Invoke(instance, null);
				ZPackage val = Build(type, writeBody, key);
				instance.InvokeRoutedRPC(num, "kou_gpsync_a", new object[1] { val });
			}
		}

		internal static void SendToClient(long peerUid, Protocol.MsgType type, Action<ZPackage> writeBody, byte[] key = null)
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null && peerUid != 0L)
			{
				ZPackage val = Build(type, writeBody, key);
				instance.InvokeRoutedRPC(peerUid, "kou_gpsync_b", new object[1] { val });
			}
		}

		private static ZPackage Build(Protocol.MsgType type, Action<ZPackage> writeBody, byte[] key)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			//IL_0025: 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)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)1);
			val.Write((byte)type);
			writeBody?.Invoke(val);
			byte[] array = val.GetArray();
			ZPackage val2 = new ZPackage();
			val2.Write(array);
			val2.Write(Crypto.Hmac(array, key));
			return val2;
		}

		private static ZPackage Open(ZPackage pkg, out Protocol.MsgType type, byte[] key)
		{
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Expected O, but got Unknown
			type = Protocol.MsgType.Ping;
			if (pkg == null)
			{
				return null;
			}
			byte[] array;
			byte[] a;
			try
			{
				array = pkg.ReadByteArray();
				a = pkg.ReadByteArray();
			}
			catch
			{
				return null;
			}
			if (!Crypto.FixedTimeEquals(a, Crypto.Hmac(array, key)))
			{
				Plugin.Log.LogWarning((object)"[koumodgp] dropped packet with bad tag.");
				return null;
			}
			ZPackage val = new ZPackage(array);
			try
			{
				int num = val.ReadByte();
				if (num != 1)
				{
					Plugin.Log.LogWarning((object)string.Format("[{0}] wire mismatch (got {1}, want {2}) — ignored.", "koumodgp", num, 1));
					return null;
				}
				type = (Protocol.MsgType)val.ReadByte();
				return val;
			}
			catch
			{
				return null;
			}
		}

		private static void OnClientToServer(long sender, ZPackage pkg)
		{
			if (Plugin.CurrentRole != Plugin.Role.Server)
			{
				return;
			}
			byte[] key = Sessions.KeyFor(sender);
			Protocol.MsgType type;
			ZPackage val = Open(pkg, out type, key);
			if (val != null)
			{
				switch (type)
				{
				case Protocol.MsgType.Ping:
					Plugin.Log.LogInfo((object)string.Format("[{0}] server: ping reply from peer {1}. Round-trip OK.", "koumodgp", sender));
					break;
				case Protocol.MsgType.Manifest:
					ServerModule.OnManifest(sender, val);
					break;
				case Protocol.MsgType.Heartbeat:
					ServerModule.OnHeartbeat(sender, val);
					break;
				case Protocol.MsgType.AdminCmd:
					HandleAdminCmd(sender, val);
					break;
				case Protocol.MsgType.Nonce:
				case Protocol.MsgType.ConfigPush:
				case Protocol.MsgType.Verdict:
					break;
				}
			}
		}

		private static void HandleAdminCmd(long sender, ZPackage inner)
		{
			string text;
			try
			{
				text = inner.ReadString();
			}
			catch
			{
				return;
			}
			ZNet instance = ZNet.instance;
			ZNetPeer val = ((instance != null) ? instance.GetPeer(sender) : null);
			string text2 = null;
			try
			{
				object obj2;
				if (val == null)
				{
					obj2 = null;
				}
				else
				{
					ISocket socket = val.m_socket;
					obj2 = ((socket != null) ? socket.GetHostName() : null);
				}
				text2 = (string)obj2;
			}
			catch
			{
			}
			if ((Object)(object)instance == (Object)null || string.IsNullOrEmpty(text2) || !instance.IsAdmin(text2))
			{
				Plugin.Log.LogWarning((object)string.Format("[{0}] rejected admin command from non-admin {1}.", "koumodgp", sender));
				return;
			}
			string[] a = ("kougp " + text).Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			string reply = Admin.Execute(a);
			byte[] key = Sessions.KeyFor(sender);
			SendToClient(sender, Protocol.MsgType.AdminReply, delegate(ZPackage pkg)
			{
				pkg.Write(reply ?? "");
			}, key);
		}

		private static void OnServerToClient(long sender, ZPackage pkg)
		{
			if (Plugin.CurrentRole != Plugin.Role.Client)
			{
				return;
			}
			byte[] sessionKey = ClientModule.SessionKey;
			Protocol.MsgType type;
			ZPackage val = Open(pkg, out type, sessionKey);
			if (val == null)
			{
				return;
			}
			switch (type)
			{
			case Protocol.MsgType.Ping:
				Plugin.Log.LogInfo((object)"[koumodgp] client: ping from server. Replying.");
				SendToServer(Protocol.MsgType.Ping, null, ClientModule.SessionKey);
				break;
			case Protocol.MsgType.Nonce:
				ClientModule.OnNonce(val);
				break;
			case Protocol.MsgType.ConfigPush:
				ClientModule.OnConfigPush(val);
				break;
			case Protocol.MsgType.AdminReply:
				try
				{
					Console instance = Console.instance;
					if (instance != null)
					{
						((Terminal)instance).AddString(val.ReadString());
					}
					break;
				}
				catch
				{
					break;
				}
			}
		}
	}
	internal static class ScanAssembly
	{
		private static readonly string[] GoodPrefixes = new string[34]
		{
			"mscorlib", "System", "netstandard", "Microsoft.", "UnityEngine", "Unity.", "BepInEx", "0Harmony", "HarmonyX", "MonoMod",
			"Mono.", "assembly_valheim", "assembly_utils", "assembly_googleanalytics", "assembly_postprocessing", "assembly_sunshafts", "assembly_guiutils", "gui_framework", "SoftReferenceableAssets", "Splatform",
			"PlayFab", "com.rlabrecque.steamworks.net", "steamworks", "ConnectorSteamworks", "Newtonsoft.Json", "YamlDotNet", "ServerSync", "Jotunn", "SoftReference", "koumodgp",
			"Anonymously Hosted", "MMHOOK", "DOTween", "Splatform"
		};

		private static HashSet<string> PluginAssemblyNames()
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			try
			{
				foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
				{
					PluginInfo value = pluginInfo.Value;
					BaseUnityPlugin val = ((value != null) ? value.Instance : null);
					if ((Object)(object)val != (Object)null)
					{
						hashSet.Add(((object)val).GetType().Assembly.GetName().Name);
					}
				}
			}
			catch
			{
			}
			return hashSet;
		}

		internal static void Collect(List<Verdict.Finding> outList)
		{
			CollectForeignHarmony(outList);
			CollectInjectedAssemblies(outList);
		}

		private static void CollectForeignHarmony(List<Verdict.Finding> outList)
		{
			try
			{
				HashSet<string> plugins = AllowedAssemblies();
				HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				foreach (MethodBase allPatchedMethod in Harmony.GetAllPatchedMethods())
				{
					Patches patchInfo = Harmony.GetPatchInfo(allPatchedMethod);
					if (patchInfo == null)
					{
						continue;
					}
					foreach (Patch item in AllPatches(patchInfo))
					{
						string text = PatchAssembly(item);
						if (text != null && !IsAllowed(text, plugins) && hashSet.Add(text))
						{
							outList.Add(new Verdict.Finding
							{
								Cat = Protocol.FindingCat.ForeignHarmony,
								Detail = "patch from '" + text + "'"
							});
						}
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] harmony scan error: " + ex.Message));
			}
		}

		private static IEnumerable<Patch> AllPatches(Patches info)
		{
			foreach (Patch prefix in info.Prefixes)
			{
				yield return prefix;
			}
			foreach (Patch postfix in info.Postfixes)
			{
				yield return postfix;
			}
			foreach (Patch transpiler in info.Transpilers)
			{
				yield return transpiler;
			}
			foreach (Patch finalizer in info.Finalizers)
			{
				yield return finalizer;
			}
		}

		private static string PatchAssembly(Patch p)
		{
			try
			{
				return p.PatchMethod?.DeclaringType?.Assembly?.GetName().Name;
			}
			catch
			{
				return null;
			}
		}

		private static HashSet<string> AllowedAssemblies()
		{
			HashSet<string> hashSet = PluginAssemblyNames();
			hashSet.Add("0Harmony");
			hashSet.Add("HarmonyX");
			hashSet.Add("MonoMod.RuntimeDetour");
			return hashSet;
		}

		private static bool IsAllowed(string name, HashSet<string> plugins)
		{
			if (plugins.Contains(name))
			{
				return true;
			}
			return GoodPrefixes.Any((string pre) => name.StartsWith(pre, StringComparison.OrdinalIgnoreCase));
		}

		private static void CollectInjectedAssemblies(List<Verdict.Finding> outList)
		{
			try
			{
				HashSet<string> plugins = PluginAssemblyNames();
				Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
				foreach (Assembly assembly in assemblies)
				{
					if (assembly.IsDynamic)
					{
						continue;
					}
					string name = assembly.GetName().Name;
					if (!string.IsNullOrEmpty(name) && !IsGac(assembly) && !IsAllowed(name, plugins))
					{
						string value;
						try
						{
							value = assembly.Location;
						}
						catch
						{
							value = null;
						}
						if (string.IsNullOrEmpty(value))
						{
							outList.Add(new Verdict.Finding
							{
								Cat = Protocol.FindingCat.InjectedAssembly,
								Detail = "in-memory assembly '" + name + "'"
							});
						}
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] assembly scan error: " + ex.Message));
			}
		}

		private static bool IsGac(Assembly a)
		{
			try
			{
				return a.GlobalAssemblyCache;
			}
			catch
			{
				return false;
			}
		}
	}
	internal static class ScanProcess
	{
		private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

		internal static volatile string[] Signatures = new string[0];

		internal static void Collect(List<Verdict.Finding> outList)
		{
			string[] signatures = Signatures;
			if (signatures == null || signatures.Length == 0)
			{
				return;
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			CollectProcessNames(signatures, hashSet);
			CollectWindowTitles(signatures, hashSet);
			foreach (string item in hashSet)
			{
				outList.Add(new Verdict.Finding
				{
					Cat = Protocol.FindingCat.ProcessMatch,
					Detail = "sig '" + item + "'"
				});
			}
		}

		private static void CollectProcessNames(string[] sigs, HashSet<string> matched)
		{
			try
			{
				Process[] processes = Process.GetProcesses();
				foreach (Process process in processes)
				{
					string text;
					try
					{
						text = process.ProcessName?.ToLowerInvariant() ?? "";
					}
					catch
					{
						continue;
					}
					finally
					{
						try
						{
							process.Dispose();
						}
						catch
						{
						}
					}
					foreach (string text2 in sigs)
					{
						if (text.Length > 0 && text.Contains(text2))
						{
							matched.Add(text2);
						}
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] process scan error: " + ex.Message));
			}
		}

		[DllImport("user32.dll")]
		private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

		[DllImport("user32.dll", CharSet = CharSet.Unicode)]
		private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);

		[DllImport("user32.dll")]
		private static extern bool IsWindowVisible(IntPtr hWnd);

		[DllImport("user32.dll")]
		private static extern int GetWindowTextLength(IntPtr hWnd);

		private static void CollectWindowTitles(string[] sigs, HashSet<string> matched)
		{
			try
			{
				EnumWindows(delegate(IntPtr h, IntPtr l)
				{
					if (!IsWindowVisible(h))
					{
						return true;
					}
					int windowTextLength = GetWindowTextLength(h);
					if (windowTextLength <= 0)
					{
						return true;
					}
					StringBuilder stringBuilder = new StringBuilder(windowTextLength + 1);
					GetWindowText(h, stringBuilder, stringBuilder.Capacity);
					string text = stringBuilder.ToString().ToLowerInvariant();
					if (text.Length == 0)
					{
						return true;
					}
					string[] array = sigs;
					foreach (string text2 in array)
					{
						if (text.Contains(text2))
						{
							matched.Add(text2);
						}
					}
					return true;
				}, IntPtr.Zero);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] window scan error: " + ex.Message));
			}
		}
	}
	internal static class ScanState
	{
		private static readonly FieldInfo fGod = AccessTools.Field(typeof(Player), "m_godMode");

		private static readonly FieldInfo fGhost = AccessTools.Field(typeof(Player), "m_ghostMode");

		private static readonly FieldInfo fDebugFly = AccessTools.Field(typeof(Player), "m_debugFly");

		private static readonly FieldInfo fDebug = AccessTools.Field(typeof(Player), "m_debugMode");

		private static readonly FieldInfo fNoCost = AccessTools.Field(typeof(Player), "m_noPlacementCost");

		private static readonly FieldInfo fFlying = AccessTools.Field(typeof(Character), "m_flying");

		internal static void Collect(List<Verdict.Finding> outList)
		{
			Player localPlayer = Player.m_localPlayer;
			if (!((Object)(object)localPlayer == (Object)null))
			{
				Check(outList, fGod, localPlayer, "god mode");
				Check(outList, fGhost, localPlayer, "ghost mode");
				Check(outList, fDebugFly, localPlayer, "debug fly");
				Check(outList, fDebug, localPlayer, "debug mode");
				Check(outList, fNoCost, localPlayer, "no placement cost");
				Check(outList, fFlying, localPlayer, "flying");
			}
		}

		private static void Check(List<Verdict.Finding> outList, FieldInfo fi, object inst, string label)
		{
			if (fi == null)
			{
				return;
			}
			try
			{
				object value = fi.GetValue(inst);
				bool flag = default(bool);
				int num;
				if (value is bool)
				{
					flag = (bool)value;
					num = 1;
				}
				else
				{
					num = 0;
				}
				if (((uint)num & (flag ? 1u : 0u)) != 0)
				{
					outList.Add(new Verdict.Finding
					{
						Cat = Protocol.FindingCat.StateAnomaly,
						Detail = label
					});
				}
			}
			catch
			{
			}
		}
	}
	internal static class SelfHash
	{
		private static byte[] _cached;

		internal static byte[] Compute()
		{
			if (_cached != null)
			{
				return _cached;
			}
			try
			{
				string location = Assembly.GetExecutingAssembly().Location;
				byte[] buffer = ((!string.IsNullOrEmpty(location) && File.Exists(location)) ? File.ReadAllBytes(location) : new byte[0]);
				using SHA256 sHA = SHA256.Create();
				_cached = sHA.ComputeHash(buffer);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] self-hash failed: " + ex.Message));
				_cached = new byte[0];
			}
			return _cached;
		}
	}
	internal static class ServerModule
	{
		private static readonly MethodInfo _internalKick = AccessTools.Method(typeof(ZNet), "InternalKick", new Type[1] { typeof(ZNetPeer) }, (Type[])null);

		internal static void Init()
		{
			Plugin.Log.LogInfo((object)string.Format("[{0}] server module active. Allowed mods: {1}.", "koumodgp", Config.AllowedMods.Count));
			Discord.PostLifecycle("koumodgp server online (v0.1.9).");
		}

		internal static void OnPeerJoined(ZNet net, ZNetPeer peer)
		{
			if (!Config.Enabled.Value || peer == null || peer.m_uid == 0L)
			{
				return;
			}
			string text = SafeHostName(peer);
			if (Config.ExemptAdmins.Value && !string.IsNullOrEmpty(text) && net.IsAdmin(text))
			{
				Plugin.Log.LogInfo((object)string.Format("[{0}] peer {1} is admin — exempt, silent.", "koumodgp", peer.m_uid));
				return;
			}
			Session s = Sessions.Create(peer.m_uid);
			s.HostName = text;
			s.SteamId = SafeSteamId(peer);
			s.PlayerName = peer.m_playerName ?? "";
			s.Nonce = Crypto.RandomBytes(16);
			s.Salt = Crypto.RandomBytes(16);
			s.JoinTime = Time.time;
			s.LastHeartbeat = Time.time;
			string identity = text ?? peer.m_uid.ToString();
			s.SessionKey = Crypto.DeriveSessionKey(s.Nonce, s.Salt, identity);
			Rpc.SendToClient(peer.m_uid, Protocol.MsgType.Nonce, delegate(ZPackage pkg)
			{
				pkg.Write(s.Nonce);
				pkg.Write(s.Salt);
				pkg.Write(identity);
			});
			Plugin.Log.LogInfo((object)string.Format("[{0}] peer {1} joined — challenge sent, awaiting manifest.", "koumodgp", peer.m_uid));
		}

		internal static void OnManifest(long uid, ZPackage inner)
		{
			Session session = Sessions.Get(uid);
			if (session == null)
			{
				return;
			}
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			List<Verdict.Finding> list = new List<Verdict.Finding>();
			try
			{
				inner.ReadByteArray();
				int num = inner.ReadInt();
				for (int i = 0; i < num; i++)
				{
					string key = inner.ReadString();
					string value = inner.ReadString();
					dictionary[key] = value;
				}
				ReadFindings(inner, list);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)string.Format("[{0}] malformed manifest from {1}: {2}", "koumodgp", uid, ex.Message));
				return;
			}
			session.Verified = true;
			session.LastHeartbeat = Time.time;
			Verdict.Result result = Verdict.Evaluate(dictionary, list);
			if (result.Action == Config.Action.Log || result.Type == null)
			{
				Plugin.Log.LogInfo((object)string.Format("[{0}] peer {1} ({2}) verified clean.", "koumodgp", uid, session.PlayerName));
				PushConfig(uid, session.SessionKey);
			}
			else
			{
				Apply(session, result);
			}
		}

		internal static void OnHeartbeat(long uid, ZPackage inner)
		{
			Session session = Sessions.Get(uid);
			if (session == null)
			{
				return;
			}
			List<Verdict.Finding> list = new List<Verdict.Finding>();
			int num;
			try
			{
				num = inner.ReadInt();
				ReadFindings(inner, list);
			}
			catch
			{
				return;
			}
			if (num <= session.LastCounter)
			{
				Apply(session, new Verdict.Result
				{
					Type = "HeartbeatFail",
					Action = Config.ActHeartbeatFail.Value,
					Confidence = "single",
					Detail = $"counter regressed ({num} <= {session.LastCounter})"
				});
				return;
			}
			session.LastCounter = num;
			session.LastHeartbeat = Time.time;
			session.MissedHeartbeats = 0;
			Verdict.Result result = Verdict.Evaluate(null, list);
			if (result.Type != null && result.Action != Config.Action.Log)
			{
				Apply(session, result);
			}
		}

		internal static void Tick(ZNet net)
		{
			if ((Object)(object)net == (Object)null)
			{
				return;
			}
			float time = Time.time;
			int value = Config.HandshakeTimeoutSeconds.Value;
			float num = Config.HeartbeatIntervalSeconds.Value * (Config.MaxMissedHeartbeats.Value + 1);
			HashSet<long> hashSet = new HashSet<long>();
			foreach (ZNetPeer peer in net.GetPeers())
			{
				if (peer != null)
				{
					hashSet.Add(peer.m_uid);
				}
			}
			foreach (Session item in Sessions.Snapshot())
			{
				if (!hashSet.Contains(item.PeerUid))
				{
					Sessions.Remove(item.PeerUid);
				}
				else if (!item.Verified && time - item.JoinTime > (float)value)
				{
					Apply(item, new Verdict.Result
					{
						Type = "MissingCompanion",
						Action = Config.ActMissingCompanion.Value,
						Confidence = "single",
						Detail = "no valid manifest before timeout"
					});
				}
				else if (item.Verified && time - item.LastHeartbeat > num)
				{
					Apply(item, new Verdict.Result
					{
						Type = "HeartbeatFail",
						Action = Config.ActHeartbeatFail.Value,
						Confidence = "single",
						Detail = $"no heartbeat for {time - item.LastHeartbeat:F0}s"
					});
				}
			}
		}

		internal static void PushConfig(long uid, byte[] key)
		{
			Rpc.SendToClient(uid, Protocol.MsgType.ConfigPush, delegate(ZPackage pkg)
			{
				pkg.Write(Config.Signatures.Count);
				foreach (string signature in Config.Signatures)
				{
					pkg.Write(signature);
				}
			}, key);
		}

		internal static void Apply(Session s, Verdict.Result v)
		{
			string utc = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
			ViolationLog.Write(utc, s.SteamId, s.PlayerName, v.Type, v.Confidence, v.Action.ToString(), v.Detail);
			switch (v.Action)
			{
			case Config.Action.Log:
				break;
			case Config.Action.Warn:
			case Config.Action.Flag:
				Plugin.Log.LogWarning((object)("[koumodgp] flag " + s.PlayerName + " (" + s.SteamId + "): " + v.Type + " [" + v.Confidence + "]."));
				break;
			case Config.Action.Kick:
			case Config.Action.Ban:
				Discord.PostDetection(utc, s.PlayerName, s.SteamId, v.Type, v.Detail);
				Plugin.Log.LogWarning((object)("[koumodgp] kicking " + s.PlayerName + " (" + s.SteamId + "): " + v.Type + "."));
				KickPeer(s.PeerUid);
				Sessions.Remove(s.PeerUid);
				break;
			}
		}

		private static void KickPeer(long uid)
		{
			ZNet instance = ZNet.instance;
			if (!((Object)(object)instance == (Object)null))
			{
				ZNetPeer peer = instance.GetPeer(uid);
				if (peer != null && _internalKick != null)
				{
					_internalKick.Invoke(instance, new object[1] { peer });
				}
			}
		}

		private static void ReadFindings(ZPackage inner, List<Verdict.Finding> outList)
		{
			int num = inner.ReadInt();
			for (int i = 0; i < num; i++)
			{
				Protocol.FindingCat cat = (Protocol.FindingCat)inner.ReadByte();
				string detail = inner.ReadString();
				outList.Add(new Verdict.Finding
				{
					Cat = cat,
					Detail = detail
				});
			}
		}

		private static string SafeHostName(ZNetPeer peer)
		{
			try
			{
				object result;
				if (peer == null)
				{
					result = null;
				}
				else
				{
					ISocket socket = peer.m_socket;
					result = ((socket != null) ? socket.GetHostName() : null);
				}
				return (string)result;
			}
			catch
			{
				return null;
			}
		}

		private static string SafeSteamId(ZNetPeer peer)
		{
			return SafeHostName(peer) ?? peer?.m_uid.ToString() ?? "unknown";
		}
	}
	internal class Session
	{
		internal long PeerUid;

		internal string HostName;

		internal string SteamId;

		internal string PlayerName;

		internal byte[] Nonce;

		internal byte[] Salt;

		internal byte[] SessionKey;

		internal float JoinTime;

		internal float LastHeartbeat;

		internal int LastCounter = -1;

		internal int MissedHeartbeats;

		internal bool Verified;
	}
	internal static class Sessions
	{
		private static readonly Dictionary<long, Session> _byUid = new Dictionary<long, Session>();

		private static readonly object _gate = new object();

		internal static int Count
		{
			get
			{
				lock (_gate)
				{
					return _byUid.Count;
				}
			}
		}

		internal static Session Create(long uid)
		{
			lock (_gate)
			{
				Session session = new Session
				{
					PeerUid = uid
				};
				_byUid[uid] = session;
				return session;
			}
		}

		internal static Session Get(long uid)
		{
			lock (_gate)
			{
				_byUid.TryGetValue(uid, out var value);
				return value;
			}
		}

		internal static void Remove(long uid)
		{
			lock (_gate)
			{
				_byUid.Remove(uid);
			}
		}

		internal static byte[] KeyFor(long uid)
		{
			lock (_gate)
			{
				Session value;
				return _byUid.TryGetValue(uid, out value) ? value.SessionKey : null;
			}
		}

		internal static List<Session> Snapshot()
		{
			lock (_gate)
			{
				return new List<Session>(_byUid.Values);
			}
		}
	}
	internal static class Verdict
	{
		internal struct Finding
		{
			internal Protocol.FindingCat Cat;

			internal string Detail;
		}

		internal class Result
		{
			internal string Type;

			internal Config.Action Action;

			internal string Confidence = "single";

			internal string Detail = "";
		}

		internal static Result Evaluate(Dictionary<string, string> plugins, List<Finding> findings)
		{
			if (plugins != null)
			{
				Result result = CheckMods(plugins);
				if (result != null)
				{
					return result;
				}
			}
			bool flag = false;
			bool flag2 = false;
			bool flag3 = false;
			bool flag4 = false;
			string detail = "";
			string detail2 = "";
			string detail3 = "";
			string detail4 = "";
			foreach (Finding finding in findings)
			{
				switch (finding.Cat)
				{
				case Protocol.FindingCat.InjectedAssembly:
					flag = true;
					detail = finding.Detail;
					break;
				case Protocol.FindingCat.ForeignHarmony:
					flag2 = true;
					detail2 = finding.Detail;
					break;
				case Protocol.FindingCat.StateAnomaly:
					flag3 = true;
					detail3 = finding.Detail;
					break;
				case Protocol.FindingCat.ProcessMatch:
					flag4 = true;
					detail4 = finding.Detail;
					break;
				}
			}
			if (flag2)
			{
				return new Result
				{
					Type = "ForeignHarmony",
					Action = Config.ActForeignHarmony.Value,
					Confidence = "single",
					Detail = detail2
				};
			}
			if (flag3)
			{
				return new Result
				{
					Type = "StateAnomaly",
					Action = Config.ActStateAnomaly.Value,
					Confidence = "single",
					Detail = detail3
				};
			}
			if (flag4)
			{
				return new Result
				{
					Type = "ProcessMatch",
					Action = Config.ActProcessMatch.Value,
					Confidence = "single",
					Detail = detail4
				};
			}
			if (flag)
			{
				return new Result
				{
					Type = "InjectedAssembly",
					Action = Config.ActInjectedAssembly.Value,
					Confidence = "single",
					Detail = detail
				};
			}
			return new Result();
		}

		private static Result CheckMods(Dictionary<string, string> reported)
		{
			Dictionary<string, string> allowedMods = Config.AllowedMods;
			foreach (KeyValuePair<string, string> item in allowedMods)
			{
				if (!reported.TryGetValue(item.Key, out var value))
				{
					return Fail("missing required mod " + item.Key);
				}
				if (item.Value != "*" && !string.Equals(item.Value, value, StringComparison.OrdinalIgnoreCase))
				{
					return Fail("version mismatch " + item.Key + ": has " + value + ", requires " + item.Value);
				}
			}
			Dictionary<string, string> optionalMods = Config.OptionalMods;
			foreach (KeyValuePair<string, string> item2 in reported)
			{
				if (!allowedMods.ContainsKey(item2.Key))
				{
					if (!optionalMods.TryGetValue(item2.Key, out var value2))
					{
						return Fail("disallowed mod " + item2.Key + " (" + item2.Value + ")");
					}
					if (value2 != "*" && !string.Equals(value2, item2.Value, StringComparison.OrdinalIgnoreCase))
					{
						return Fail("version mismatch " + item2.Key + ": has " + item2.Value + ", allows " + value2);
					}
				}
			}
			return null;
		}

		private static Result Fail(string detail)
		{
			return new Result
			{
				Type = "ModMismatch",
				Action = Config.ActModMismatch.Value,
				Confidence = "single",
				Detail = detail
			};
		}
	}
	internal static class ViolationLog
	{
		private static string _path;

		private static readonly object _gate = new object();

		internal static void Init(string configDir)
		{
			_path = Path.Combine(configDir, "koumodgp", "violations.log");
			Directory.CreateDirectory(Path.GetDirectoryName(_path));
		}

		internal static void Write(string utc, string steamId, string name, string type, string confidence, string action, string detail)
		{
			if (_path == null)
			{
				return;
			}
			string text = utc + "\t" + steamId + "\t" + name + "\t" + type + "\t" + confidence + "\t" + action + "\t" + detail;
			try
			{
				lock (_gate)
				{
					File.AppendAllText(_path, text + Environment.NewLine);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[koumodgp] violation log write failed: " + ex.Message));
			}
		}
	}
}