Decompiled source of AntiCheat Plus v1.0.0

AntiCheat.dll

Decompiled 5 hours ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AntiCheat;
using AntiCheat.Locale;
using AntiCheat.Patches;
using AntiCheat.UI;
using AntiCheat.Utils;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using GameNetcodeStuff;
using HarmonyLib;
using Netcode.Transports.Facepunch;
using Newtonsoft.Json;
using Overlay;
using Steamworks;
using Steamworks.Data;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.EventSystems;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: IgnoresAccessChecksTo("AmazingAssets.TerrainToMesh")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp-firstpass")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: IgnoresAccessChecksTo("ClientNetworkTransform")]
[assembly: IgnoresAccessChecksTo("com.olegknyazev.softmask")]
[assembly: IgnoresAccessChecksTo("DissonanceVoip")]
[assembly: IgnoresAccessChecksTo("DunGen")]
[assembly: IgnoresAccessChecksTo("DunGen.Integration.ASPP")]
[assembly: IgnoresAccessChecksTo("DunGen.Integration.UnityNav")]
[assembly: IgnoresAccessChecksTo("EasyTextEffects")]
[assembly: IgnoresAccessChecksTo("Facepunch Transport for Netcode for GameObjects")]
[assembly: IgnoresAccessChecksTo("Facepunch.Steamworks.Win64")]
[assembly: IgnoresAccessChecksTo("Unity.AI.Navigation")]
[assembly: IgnoresAccessChecksTo("Unity.Animation.Rigging")]
[assembly: IgnoresAccessChecksTo("Unity.Animation.Rigging.DocCodeExamples")]
[assembly: IgnoresAccessChecksTo("Unity.Burst")]
[assembly: IgnoresAccessChecksTo("Unity.Burst.Unsafe")]
[assembly: IgnoresAccessChecksTo("Unity.Collections")]
[assembly: IgnoresAccessChecksTo("Unity.Collections.LowLevel.ILSupport")]
[assembly: IgnoresAccessChecksTo("Unity.InputSystem")]
[assembly: IgnoresAccessChecksTo("Unity.InputSystem.ForUI")]
[assembly: IgnoresAccessChecksTo("Unity.Jobs")]
[assembly: IgnoresAccessChecksTo("Unity.Mathematics")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.Common")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.MetricTypes")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStats")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Component")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Configuration")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Implementation")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsReporting")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetworkProfiler.Runtime")]
[assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetworkSolutionInterface")]
[assembly: IgnoresAccessChecksTo("Unity.Netcode.Components")]
[assembly: IgnoresAccessChecksTo("Unity.Netcode.Runtime")]
[assembly: IgnoresAccessChecksTo("Unity.Networking.Transport")]
[assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Csg")]
[assembly: IgnoresAccessChecksTo("Unity.ProBuilder")]
[assembly: IgnoresAccessChecksTo("Unity.ProBuilder.KdTree")]
[assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Poly2Tri")]
[assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Stl")]
[assembly: IgnoresAccessChecksTo("Unity.Profiling.Core")]
[assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Core.Runtime")]
[assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Core.ShaderLibrary")]
[assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.HighDefinition.Config.Runtime")]
[assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.HighDefinition.Runtime")]
[assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Authentication")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Analytics")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Components")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Configuration")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Device")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Environments")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Environments.Internal")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Internal")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Networking")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Registration")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Scheduler")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Telemetry")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Core.Threading")]
[assembly: IgnoresAccessChecksTo("Unity.Services.QoS")]
[assembly: IgnoresAccessChecksTo("Unity.Services.Relay")]
[assembly: IgnoresAccessChecksTo("Unity.TextMeshPro")]
[assembly: IgnoresAccessChecksTo("Unity.Timeline")]
[assembly: IgnoresAccessChecksTo("Unity.VisualEffectGraph.Runtime")]
[assembly: IgnoresAccessChecksTo("Unity.XR.CoreUtils")]
[assembly: IgnoresAccessChecksTo("Unity.XR.Management")]
[assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR")]
[assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.ConformanceAutomation")]
[assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.MetaQuestSupport")]
[assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.MockRuntime")]
[assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.OculusQuestSupport")]
[assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.RuntimeDebugger")]
[assembly: IgnoresAccessChecksTo("UnityEngine.ARModule")]
[assembly: IgnoresAccessChecksTo("UnityEngine.NVIDIAModule")]
[assembly: IgnoresAccessChecksTo("UnityEngine.SpatialTracking")]
[assembly: IgnoresAccessChecksTo("UnityEngine.UI")]
[assembly: IgnoresAccessChecksTo("UnityEngine.XR.LegacyInputHelpers")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("chuxiaaaa")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyDescription("AntiCheat")]
[assembly: AssemblyFileVersion("0.8.9.0")]
[assembly: AssemblyInformationalVersion("0.8.9+c4c32a494a4e01b6094e5b49735ec59042ea2ce9")]
[assembly: AssemblyProduct("AntiCheat")]
[assembly: AssemblyTitle("AntiCheat")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.8.9.0")]
[module: UnverifiableCode]
[BepInDependency(/*Could not decode attribute arguments.*/)]
[BepInPlugin("AntiCheat", "AntiCheat", "0.8.9")]
public class AntiCheatPlugin : BaseUnityPlugin
{
	internal static ManualLogSource Log = null;

	internal static LocalizationManager localizationManager = null;

	private static readonly MethodInfo AddChatMessageMethod = AccessTools.DeclaredMethod(typeof(HUDManager), "AddChatMessage", (Type[])null, (Type[])null);

	private static readonly MethodInfo AddTextMessageClientRpcMethod = AccessTools.DeclaredMethod(typeof(HUDManager), "AddTextMessageClientRpc", (Type[])null, (Type[])null);

	private const string AlertModule = "ANTI CHEAT";

	private const double RepeatQuietSeconds = 5.0;

	private static readonly Dictionary<string, DateTime> _raisedAt = new Dictionary<string, DateTime>();

	private void Awake()
	{
		//IL_004c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0052: Expected O, but got Unknown
		Log = ((BaseUnityPlugin)this).Logger;
		localizationManager = new LocalizationManager();
		PluginConfig.Init((BaseUnityPlugin)(object)this);
		Network.PlayerNamesReceived += LateJoinSync.ApplyPlayerNames;
		Network.PlayerSlotRepairReceived += LateJoinSync.RepairPlayerSlot;
		AntiCheatMenu.Register();
		Harmony val = new Harmony("AntiCheat");
		IEnumerable<Type> enumerable = from t in Assembly.GetExecutingAssembly().GetTypes()
			where t.IsClass && t.GetCustomAttributes(typeof(HarmonyPatch), inherit: false).Any()
			select t;
		foreach (Type item in enumerable)
		{
			try
			{
				val.CreateClassProcessor(item).Patch();
			}
			catch (Exception arg)
			{
				Log.LogWarning((object)$"Failed to patch {item.Name} - that feature is off for this session: {arg}");
			}
		}
		LobbyImprovementsBans.TryIntegrate(val);
		BanList.EnsureBanCheckSubscribed();
		MethodInfo methodInfo = AccessTools.Method(typeof(StartOfRound), "Update", (Type[])null, (Type[])null);
		bool flag = methodInfo != null && val.GetPatchedMethods().Contains(methodInfo);
		Log.LogInfo((object)$"StartOfRound.Update resolved:{methodInfo != null} movementHookBound:{flag} totalPatched:{val.GetPatchedMethods().Count()}");
	}

	public static void LogInfo(string info)
	{
		MenuLog.Info(info);
		LogWithoutMenu(info);
	}

	private static void LogWithoutMenu(string info)
	{
		if ((!((Object)(object)StartOfRound.Instance != (Object)null) || ((NetworkBehaviour)StartOfRound.Instance).IsHost) && (PluginConfig.Log == null || PluginConfig.Log.Value))
		{
			Log.LogInfo((object)info);
			LogFile.Write($"[{DateTime.Now:MM-dd HH:mm:ss:ff}] {info}");
		}
	}

	public static void LogInfo(PlayerControllerB player, string rpc, params object[] param)
	{
		LogInfo(string.Format("{0}({1}) -> {2};{3}", player.playerUsername, player.playerClientId, rpc, string.Join("|", param)));
	}

	public static void Detected(string msg, string dedupeKey = null, string player = null, string cheat = null)
	{
		Raise(msg, dedupeKey, (AlertTone)2, player, cheat);
	}

	public static void Punished(string msg, string dedupeKey = null, string player = null, string cheat = null)
	{
		Raise(msg, dedupeKey, (AlertTone)1, player, cheat);
	}

	public static void Announce(string msg, string color = null)
	{
		if (!string.IsNullOrEmpty(msg))
		{
			MenuLog.Info(msg);
			LogWithoutMenu(msg);
			if (!((Object)(object)HUDManager.Instance == (Object)null))
			{
				HUDManager.Instance.AddTextToChatOnServer(Colorize(msg, color), -1);
			}
		}
	}

	public static void ForgetAlertsFor(string player)
	{
		if (!string.IsNullOrEmpty(player))
		{
			Alerts.ClearForSubject(player);
		}
	}

	private static bool SendAlertToPlayer(string msg, AlertTone tone, string player, string cheat)
	{
		//IL_0084: Unknown result type (might be due to invalid IL or missing references)
		if (string.IsNullOrEmpty(player) || !Network.IsHost || (Object)(object)StartOfRound.Instance == (Object)null || StartOfRound.Instance.allPlayerScripts == null)
		{
			return false;
		}
		PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts;
		foreach (PlayerControllerB val in allPlayerScripts)
		{
			if (!((Object)(object)val == (Object)null) && val.actualClientId != 0L && string.Equals(val.playerUsername, player, StringComparison.Ordinal))
			{
				return Network.Send(val.actualClientId, msg, tone, player, cheat);
			}
		}
		return false;
	}

	private static void Raise(string msg, string dedupeKey, AlertTone tone, string player, string cheat)
	{
		//IL_0094: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
		if (string.IsNullOrEmpty(msg))
		{
			return;
		}
		MenuLog.Alert(msg);
		LogWithoutMenu(msg);
		string key = dedupeKey ?? msg;
		DateTime utcNow = DateTime.UtcNow;
		if (!_raisedAt.TryGetValue(key, out var value) || !((utcNow - value).TotalSeconds < 5.0))
		{
			if (_raisedAt.Count > 256)
			{
				_raisedAt.Clear();
			}
			_raisedAt[key] = utcNow;
			if (!SendAlertToPlayer(msg, tone, player, cheat))
			{
				Alerts.Raise("ANTI CHEAT", msg, tone, player ?? string.Empty, cheat ?? string.Empty);
			}
		}
	}

	public static void ShowGuiTip(string body, string header = null)
	{
		if (!((Object)(object)HUDManager.Instance == (Object)null))
		{
			LogWithoutMenu("ShowGUI|" + body);
			HUDManager.Instance.DisplayTip(string.IsNullOrEmpty(header) ? localizationManager.Prefix() : header, body, false, false, "LC_Tip1");
		}
	}

	public static string Colorize(string text, string color)
	{
		if (string.IsNullOrEmpty(color) || color.Trim().Length == 0)
		{
			return text;
		}
		return "<color=" + color.Trim() + ">" + text + "</color>";
	}

	public static void ShowMessageHostOnly(string msg)
	{
		if (!((Object)(object)HUDManager.Instance == (Object)null))
		{
			LogWithoutMenu("ShowMessageHostOnly -> " + msg);
			AddChatMessageMethod.Invoke(HUDManager.Instance, new object[4] { msg, "", -1, false });
		}
	}

	public static void AddTextMessageClientRpc(string showMessage)
	{
		if (!((Object)(object)HUDManager.Instance == (Object)null))
		{
			AddTextMessageClientRpcMethod.Invoke(HUDManager.Instance, new object[1] { showMessage });
		}
	}
}
internal static class LCMPluginInfo
{
	public const string PLUGIN_GUID = "AntiCheat";

	public const string PLUGIN_NAME = "AntiCheat";

	public const string PLUGIN_VERSION = "0.8.9";
}
namespace AntiCheat
{
	[HarmonyPatch]
	[HarmonyWrapSafe]
	public static class AntiGrief
	{
		private const string HostOnlyWarning = "{0}, This Action is Restricted";

		private const string GameStartWarning = "{0}, This Action is Restricted till the Ship Lands";

		private static readonly Dictionary<ulong, int> _violationCounts = new Dictionary<ulong, int>();

		private static int _landings;

		private static bool _countedThisLanding;

		private static readonly HashSet<ulong> _landedParticipants = new HashSet<ulong>();

		private static int _lightSwitchUnlockableId = -2;

		private static int Landings()
		{
			if (!_countedThisLanding && (Object)(object)StartOfRound.Instance != (Object)null && StartOfRound.Instance.shipHasLanded)
			{
				CountLanding("Ship is on the ground");
			}
			return _landings;
		}

		private static void CountLanding(string reason)
		{
			if (_countedThisLanding)
			{
				return;
			}
			_countedThisLanding = true;
			_landings++;
			if ((Object)(object)NetworkManager.Singleton != (Object)null)
			{
				foreach (ulong connectedClientsId in NetworkManager.Singleton.ConnectedClientsIds)
				{
					_landedParticipants.Add(connectedClientsId);
				}
			}
			AntiCheatPlugin.LogInfo($"[Anti-Grief] {reason} - landing #{_landings}. Connected players are now unrestricted for GameStart actions.");
		}

		private static bool HasServedARound(ulong clientId, out string detail)
		{
			Landings();
			bool flag = _landedParticipants.Contains(clientId);
			detail = (flag ? "participated in ShipHasLanded" : "has not participated in ShipHasLanded yet");
			return flag;
		}

		[HarmonyPatch(typeof(StartOfRound), "OnShipLandedMiscEvents")]
		[HarmonyPostfix]
		public static void CountLandingOnShipLanded()
		{
			CountLanding("Ship has landed");
		}

		[HarmonyPatch(typeof(StartOfRound), "Awake")]
		[HarmonyPostfix]
		public static void ResetLatchOnNewLobby()
		{
			if (_landings != 0 || _landedParticipants.Count != 0)
			{
				AntiCheatPlugin.LogInfo("[Anti-Grief] New lobby - landings back to zero, everybody starts as a newcomer again.");
			}
			_landings = 0;
			_countedThisLanding = false;
			_landedParticipants.Clear();
			_violationCounts.Clear();
			ForgetLightSwitchId();
		}

		[HarmonyPatch(typeof(StartOfRound), "StartGame")]
		[HarmonyPostfix]
		public static void ResetViolationsOnRoundStart()
		{
			_countedThisLanding = false;
			_violationCounts.Clear();
			BanList.EnsureBanCheckSubscribed();
		}

		private static bool IsHostSender(__RpcParams rpcParams)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			return rpcParams.Server.Receive.SenderClientId == GameNetworkManager.Instance.localPlayerController.actualClientId;
		}

		private static bool RestrictionsDisabledDueToGameStart(PluginConfig.PermissionLevel level, ulong senderClientId, out string detail)
		{
			if (level != PluginConfig.PermissionLevel.GameStart)
			{
				detail = null;
				return false;
			}
			return HasServedARound(senderClientId, out detail);
		}

		private static bool IsAllowed(string ruleKey, __RpcParams rpcParams, string action, out PluginConfig.PermissionLevel level)
		{
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			GriefRule griefRule = AntiGriefRules.ByKey(ruleKey);
			if (griefRule == null)
			{
				AntiCheatPlugin.LogInfo("[Anti-Grief] No rule registered under \"" + ruleKey + "\" - " + action + " allowed.");
				level = PluginConfig.PermissionLevel.Unrestricted;
				return true;
			}
			level = griefRule.Level;
			ulong senderClientId = rpcParams.Server.Receive.SenderClientId;
			bool flag = IsHostSender(rpcParams);
			string detail = null;
			bool flag2 = !flag && RestrictionsDisabledDueToGameStart(level, senderClientId, out detail);
			bool flag3 = level == PluginConfig.PermissionLevel.Unrestricted || flag || flag2;
			bool flag4 = false;
			if (!flag3 && Profiles.AllowsGriefRule(senderClientId, griefRule.Key))
			{
				flag3 = true;
				flag4 = true;
			}
			string text = (flag4 ? " (bypass profile)" : ((detail == null) ? string.Empty : (" (" + detail + ")")));
			AntiCheatPlugin.LogInfo("[Anti-Grief] " + BanList.NameForClient(senderClientId) + " tried to " + action + " - " + (flag3 ? "allowed" : "forbidden") + text);
			return flag3;
		}

		private static bool IsNetworkRegisteredClient(ulong clientId)
		{
			if ((Object)(object)NetworkManager.Singleton == (Object)null || !NetworkManager.Singleton.IsServer)
			{
				return false;
			}
			foreach (ulong connectedClientsId in NetworkManager.Singleton.ConnectedClientsIds)
			{
				if (connectedClientsId == clientId)
				{
					return true;
				}
			}
			return false;
		}

		private static void WarnHostAboutViolation(ulong senderClientId, PluginConfig.PermissionLevel level)
		{
			string format = ((level == PluginConfig.PermissionLevel.GameStart) ? "{0}, This Action is Restricted till the Ship Lands" : "{0}, This Action is Restricted");
			AlertPlayer(senderClientId, string.Format(format, BanList.NameForClient(senderClientId)));
		}

		internal static void AlertPlayer(ulong senderClientId, string message)
		{
			try
			{
				string text = BanList.NameForClient(senderClientId);
				bool flag = Network.Send(senderClientId, message, (AlertTone)1, text, "Anti-Grief");
				AntiCheatPlugin.LogInfo(string.Format("[Anti-Grief] Warning for {0} (client {1}) sent as {2}.", text, senderClientId, flag ? "Overlay alert" : "chat fallback"));
				if (!flag && (Object)(object)HUDManager.Instance != (Object)null)
				{
					HUDManager.Instance.AddTextToChatOnServer("<color=" + PluginConfig.AntiGrief_MessageColor.Value + ">" + message + "</color>", -1);
				}
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo($"[Anti-Grief] AlertPlayer failed: {arg}");
			}
		}

		internal static void HandleViolation(ulong senderClientId, PluginConfig.PermissionLevel level)
		{
			WarnHostAboutViolation(senderClientId, level);
			_violationCounts.TryGetValue(senderClientId, out var value);
			value++;
			_violationCounts[senderClientId] = value;
			int value2 = PluginConfig.AntiGrief_MaxViolations.Value;
			if (value2 > 0 && value >= value2)
			{
				PunishPlayer(senderClientId, PluginConfig.AntiGrief_Punishment.Value);
				_violationCounts[senderClientId] = 0;
			}
		}

		internal static void PunishPlayer(ulong clientId, PunishmentType punishment, string reasonOverride = null)
		{
			try
			{
				if (punishment != PunishmentType.Disable && punishment != PunishmentType.OnlyLog)
				{
					string text = reasonOverride;
					if (string.IsNullOrEmpty(text))
					{
						text = ((punishment == PunishmentType.Ban) ? PluginConfig.AntiGrief_BanReason.Value : PluginConfig.AntiGrief_KickReason.Value);
					}
					if (string.IsNullOrEmpty(text))
					{
						text = ((punishment == PunishmentType.Ban) ? "You were banned by the Anti-Grief System" : "You were kicked by the Anti-Grief System");
					}
					AntiCheatPlugin.LogInfo(string.Format("[Anti-Grief] {0} {1} (client {2}) for exceeding the violation threshold.", (punishment == PunishmentType.Ban) ? "banned" : "kicked", BanList.NameForClient(clientId), clientId));
					PlayerControllerB val = PlayerForClient(clientId);
					if ((Object)(object)val != (Object)null)
					{
						PatchHelper.Punish(val, punishment, text, BanSource.AntiGrief);
					}
					else
					{
						NetworkManager.Singleton.DisconnectClient(clientId, text);
					}
				}
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo($"[Anti-Grief] PunishPlayer failed: {arg}");
			}
		}

		private static PlayerControllerB PlayerForClient(ulong clientId)
		{
			if ((Object)(object)StartOfRound.Instance == (Object)null || StartOfRound.Instance.allPlayerScripts == null)
			{
				return null;
			}
			PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts;
			foreach (PlayerControllerB val in allPlayerScripts)
			{
				if ((Object)(object)val != (Object)null && val.actualClientId == clientId)
				{
					return val;
				}
			}
			return null;
		}

		internal static void ForgetLightSwitchId()
		{
			_lightSwitchUnlockableId = -2;
		}

		private static bool IsLightSwitch(NetworkObject networkObject, PlaceableShipObject placeable)
		{
			if ((Object)(object)networkObject == (Object)null)
			{
				return false;
			}
			if ((Object)(object)((Component)networkObject).gameObject.GetComponentInChildren<ShipLights>(true) != (Object)null)
			{
				return true;
			}
			int num = LightSwitchUnlockableId();
			return num >= 0 && (Object)(object)placeable != (Object)null && placeable.unlockableID == num;
		}

		private static int LightSwitchUnlockableId()
		{
			if (_lightSwitchUnlockableId != -2)
			{
				return _lightSwitchUnlockableId;
			}
			_lightSwitchUnlockableId = -1;
			try
			{
				UnlockablesList val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.unlockablesList : null);
				if ((Object)(object)val == (Object)null || val.unlockables == null)
				{
					_lightSwitchUnlockableId = -2;
					return -1;
				}
				for (int i = 0; i < val.unlockables.Count; i++)
				{
					UnlockableItem val2 = val.unlockables[i];
					if (val2 != null && !string.IsNullOrEmpty(val2.unlockableName) && val2.unlockableName.Replace(" ", string.Empty).Equals("lightswitch", StringComparison.OrdinalIgnoreCase))
					{
						_lightSwitchUnlockableId = i;
						AntiCheatPlugin.LogInfo($"[Anti-Grief] Light switch is unlockable #{i} (\"{val2.unlockableName}\").");
						break;
					}
				}
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo($"[Anti-Grief] LightSwitchUnlockableId failed: {arg}");
			}
			return _lightSwitchUnlockableId;
		}

		[HarmonyPatch(typeof(Terminal), "__rpc_handler_4003509079")]
		[HarmonyPriority(600)]
		[HarmonyPrefix]
		public static bool IgnoreClientButNotHostPurchases(NetworkBehaviour target, __RpcParams rpcParams)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			if (IsAllowed("PurchaseStoreItems", rpcParams, "purchase store item", out var level))
			{
				return true;
			}
			HandleViolation(rpcParams.Server.Receive.SenderClientId, level);
			Terminal val = (Terminal)(object)((target is Terminal) ? target : null);
			if ((Object)(object)val != (Object)null)
			{
				val.SyncGroupCreditsClientRpc(val.groupCredits, val.orderedItemsFromTerminal.Count);
			}
			return false;
		}

		[HarmonyPatch(typeof(StartOfRound), "__rpc_handler_3953483456")]
		[HarmonyPriority(600)]
		[HarmonyPrefix]
		public static bool IgnoreClientButNotHostPurchaseUnlockables(NetworkBehaviour target, __RpcParams rpcParams)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			if (IsAllowed("PurchaseStoreUnlocks", rpcParams, "purchase unlockable", out var level))
			{
				return true;
			}
			HandleViolation(rpcParams.Server.Receive.SenderClientId, level);
			StartOfRound val = (StartOfRound)(object)((target is StartOfRound) ? target : null);
			if ((Object)(object)val != (Object)null)
			{
				val.BuyShipUnlockableClientRpc(Object.FindObjectOfType<Terminal>().groupCredits, -1);
			}
			return false;
		}

		[HarmonyPatch(typeof(StartOfRound), "__rpc_handler_1134466287")]
		[HarmonyPriority(600)]
		[HarmonyPrefix]
		public static bool IgnoreClientButNotHostRouteChanges(__RpcParams rpcParams)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			if (IsAllowed("RouteShipToMoon", rpcParams, "route ship to moon", out var level))
			{
				return true;
			}
			HandleViolation(rpcParams.Server.Receive.SenderClientId, level);
			return false;
		}

		[HarmonyPatch(typeof(StartOfRound), "__rpc_handler_3380566632")]
		[HarmonyPriority(600)]
		[HarmonyPrefix]
		public static bool IgnoreClientButNotHostReturnFromStorage(__RpcParams rpcParams)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			if (IsAllowed("ReturnObjectFromStorage", rpcParams, "return object from storage", out var level))
			{
				return true;
			}
			HandleViolation(rpcParams.Server.Receive.SenderClientId, level);
			return false;
		}

		[HarmonyPatch(typeof(ShipBuildModeManager), "__rpc_handler_3086821980")]
		[HarmonyPriority(600)]
		[HarmonyPrefix]
		public static bool IgnoreClientButNotHostPlaceInStorage(FastBufferReader reader, __RpcParams rpcParams)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			if (IsAllowed("PlaceObjectIntoStorage", rpcParams, "place object into storage", out var level))
			{
				return true;
			}
			HandleViolation(rpcParams.Server.Receive.SenderClientId, level);
			NetworkObjectReference val = default(NetworkObjectReference);
			((FastBufferReader)(ref reader)).ReadValueSafe<NetworkObjectReference>(ref val, default(ForNetworkSerializable));
			NetworkObject val2 = default(NetworkObject);
			if (!((NetworkObjectReference)(ref val)).TryGet(ref val2, (NetworkManager)null))
			{
				return false;
			}
			PlaceableShipObject componentInChildren = ((Component)val2).gameObject.GetComponentInChildren<PlaceableShipObject>();
			if ((Object)(object)componentInChildren != (Object)null)
			{
				Object.FindObjectOfType<StartOfRound>().ReturnUnlockableFromStorageClientRpc(componentInChildren.unlockableID);
			}
			return false;
		}

		[HarmonyPatch(typeof(ShipBuildModeManager), "__rpc_handler_861494715")]
		[HarmonyPriority(600)]
		[HarmonyPrefix]
		public static bool IgnoreClientButNotHostMoveCabinet(NetworkBehaviour target, FastBufferReader reader, __RpcParams rpcParams)
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_0124: Unknown result type (might be due to invalid IL or missing references)
			//IL_012b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0130: Unknown result type (might be due to invalid IL or missing references)
			Vector3 val = default(Vector3);
			((FastBufferReader)(ref reader)).ReadValueSafe(ref val);
			((FastBufferReader)(ref reader)).ReadValueSafe(ref val);
			NetworkObjectReference val2 = default(NetworkObjectReference);
			((FastBufferReader)(ref reader)).ReadValueSafe<NetworkObjectReference>(ref val2, default(ForNetworkSerializable));
			((FastBufferReader)(ref reader)).Seek(0);
			NetworkObject val4 = default(NetworkObject);
			PlaceableShipObject val3 = ((((NetworkObjectReference)(ref val2)).TryGet(ref val4, (NetworkManager)null) && (Object)(object)val4 != (Object)null) ? ((Component)val4).gameObject.GetComponentInChildren<PlaceableShipObject>() : null);
			bool flag = IsLightSwitch(val4, val3);
			string ruleKey = (flag ? "MoveLightSwitch" : "MoveObjects");
			if (IsAllowed(ruleKey, rpcParams, flag ? "move the light switch" : "move object", out var level))
			{
				return true;
			}
			HandleViolation(rpcParams.Server.Receive.SenderClientId, level);
			if ((Object)(object)val3 != (Object)null)
			{
				ShipBuildModeManager val5 = (ShipBuildModeManager)(object)((target is ShipBuildModeManager) ? target : null);
				if ((Object)(object)val5 != (Object)null)
				{
					Transform val6 = (((Object)(object)val3.mainMesh != (Object)null) ? ((Component)val3.mainMesh).transform : ((Component)val4).transform);
					Transform val7 = (((Object)(object)val3.placeObjectCollider != (Object)null) ? ((Component)val3.placeObjectCollider).transform : ((Component)val4).transform);
					val5.PlaceShipObjectClientRpc(val7.position, val6.eulerAngles, val2, (int)GameNetworkManager.Instance.localPlayerController.playerClientId);
				}
			}
			return false;
		}

		[HarmonyPatch(typeof(ShipLights), "__rpc_handler_1625678258")]
		[HarmonyPriority(600)]
		[HarmonyPrefix]
		public static bool IgnoreClientButNotHostSetLights(__RpcParams rpcParams)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			if (IsAllowed("ToggleShipLights", rpcParams, "toggle ship lights", out var level))
			{
				return true;
			}
			ShipLights val = Object.FindObjectOfType<ShipLights>();
			if (GameNetworkManager.Instance.localPlayerController.isPlayerDead || Vector3.Distance(((Component)GameNetworkManager.Instance.localPlayerController).transform.position, ((Component)val).transform.position) >= 20f)
			{
				return true;
			}
			HandleViolation(rpcParams.Server.Receive.SenderClientId, level);
			val.SetShipLightsClientRpc(val.areLightsOn);
			return false;
		}
	}
	internal sealed class GriefRule
	{
		public string Key;

		public string Label;

		public string Help;

		public ConfigEntry<PluginConfig.PermissionLevel> LevelEntry;

		public ConfigEntry<PluginConfig.FriendlyFirePolicy> BinaryEntry;

		public bool Binary => BinaryEntry != null;

		public PluginConfig.PermissionLevel Level => (!Binary) ? LevelEntry.Value : ((BinaryEntry.Value != PluginConfig.FriendlyFirePolicy.Allowed) ? PluginConfig.PermissionLevel.HostOnly : PluginConfig.PermissionLevel.Unrestricted);

		public void SetLevel(PluginConfig.PermissionLevel level)
		{
			if (Binary)
			{
				BinaryEntry.Value = ((level != PluginConfig.PermissionLevel.Unrestricted) ? PluginConfig.FriendlyFirePolicy.Restricted : PluginConfig.FriendlyFirePolicy.Allowed);
			}
			else
			{
				LevelEntry.Value = level;
			}
		}
	}
	internal static class AntiGriefRules
	{
		public const string FriendlyFire = "FriendlyFire";

		public const string PurchaseStoreItems = "PurchaseStoreItems";

		public const string PurchaseStoreUnlocks = "PurchaseStoreUnlocks";

		public const string RouteShipToMoon = "RouteShipToMoon";

		public const string PlaceObjectIntoStorage = "PlaceObjectIntoStorage";

		public const string ReturnObjectFromStorage = "ReturnObjectFromStorage";

		public const string MoveObjects = "MoveObjects";

		public const string MoveLightSwitch = "MoveLightSwitch";

		public const string ToggleShipLights = "ToggleShipLights";

		private static readonly List<GriefRule> Rules = new List<GriefRule>();

		private static readonly Dictionary<string, GriefRule> ByKeyLookup = new Dictionary<string, GriefRule>();

		public static IList<GriefRule> All => Rules;

		public static GriefRule ByKey(string key)
		{
			if (string.IsNullOrEmpty(key))
			{
				return null;
			}
			GriefRule value;
			return ByKeyLookup.TryGetValue(key, out value) ? value : null;
		}

		internal static void Register()
		{
			Rules.Clear();
			ByKeyLookup.Clear();
			AddBinary("FriendlyFire", "Friendly fire", PluginConfig.Restrict_FriendlyFire, "Player weapon damage. Restricted blocks it for everyone; Allowed restores vanilla.");
			AddLevel("PurchaseStoreItems", "Purchase store items", PluginConfig.Restrict_PurchaseStoreItems, "Terminal orders.");
			AddLevel("PurchaseStoreUnlocks", "Purchase unlockables", PluginConfig.Restrict_PurchaseStoreUnlocks, "Ship upgrades and furniture.");
			AddLevel("RouteShipToMoon", "Route the ship", PluginConfig.Restrict_RouteShipToMoon, "Which moon the ship flies to.");
			AddLevel("PlaceObjectIntoStorage", "Place into storage", PluginConfig.Restrict_PlaceObjectIntoStorage, "Furniture into the closet.");
			AddLevel("ReturnObjectFromStorage", "Take out of storage", PluginConfig.Restrict_ReturnObjectFromStorage, "Furniture back out.");
			AddLevel("MoveObjects", "Move ship objects", PluginConfig.Restrict_MoveObjects, "Furniture inside the ship.");
			AddLevel("MoveLightSwitch", "Move the light switch", PluginConfig.Restrict_MoveLightSwitch, "The cheapest grief there is.");
			AddLevel("ToggleShipLights", "Toggle the ship lights", PluginConfig.Restrict_ToggleShipLights, "Lights on and off.");
		}

		private static void AddLevel(string key, string label, ConfigEntry<PluginConfig.PermissionLevel> entry, string help)
		{
			GriefRule griefRule = new GriefRule
			{
				Key = key,
				Label = label,
				LevelEntry = entry,
				Help = help
			};
			Rules.Add(griefRule);
			ByKeyLookup[griefRule.Key] = griefRule;
		}

		private static void AddBinary(string key, string label, ConfigEntry<PluginConfig.FriendlyFirePolicy> entry, string help)
		{
			GriefRule griefRule = new GriefRule
			{
				Key = key,
				Label = label,
				BinaryEntry = entry,
				Help = help
			};
			Rules.Add(griefRule);
			ByKeyLookup[griefRule.Key] = griefRule;
		}
	}
	public enum BanSource
	{
		Unknown,
		AntiCheat,
		AntiGrief,
		Host
	}
	public sealed class BanRecord
	{
		public ulong SteamId;

		public BanSource Source;

		public string Name;

		public DateTime When;

		public bool HasWhen;

		public string SourceText => Source switch
		{
			BanSource.AntiCheat => "AntiCheat", 
			BanSource.AntiGrief => "Anti-Grief", 
			BanSource.Host => "Host ban", 
			_ => "-", 
		};
	}
	[HarmonyPatch]
	[HarmonyWrapSafe]
	public static class BanList
	{
		private static bool _banCheckSubscribed;

		public static HashSet<ulong> Parse()
		{
			string value = PluginConfig.BannedSteamIds.Value;
			if (string.IsNullOrEmpty(value) || value.Trim().Length == 0)
			{
				return new HashSet<ulong>();
			}
			ulong result;
			return new HashSet<ulong>(from s in value.Split(',')
				select s.Trim() into s
				where s.Length > 0
				select ulong.TryParse(s, out result) ? result : 0 into id
				where id != 0
				select id);
		}

		public static bool Contains(ulong steamId)
		{
			return steamId != 0L && Parse().Contains(steamId);
		}

		public static void Add(ulong steamId, BanSource source = BanSource.Unknown, string name = null)
		{
			if (steamId == 0)
			{
				return;
			}
			HashSet<ulong> hashSet = Parse();
			bool flag = hashSet.Add(steamId);
			Dictionary<ulong, BanRecord> dictionary = ParseRecords();
			if (!dictionary.TryGetValue(steamId, out var value))
			{
				value = (dictionary[steamId] = new BanRecord
				{
					SteamId = steamId
				});
			}
			if (source != BanSource.Unknown)
			{
				value.Source = source;
			}
			if (!string.IsNullOrEmpty(name))
			{
				value.Name = Sanitize(name);
			}
			if (flag || !value.HasWhen)
			{
				value.When = DateTime.Now;
				value.HasWhen = true;
			}
			if (flag)
			{
				PluginConfig.BannedSteamIds.Value = string.Join(",", hashSet.Select((ulong id) => id.ToString()).ToArray());
			}
			WriteRecords(dictionary, hashSet);
			Save();
		}

		public static bool Remove(ulong steamId)
		{
			HashSet<ulong> hashSet = Parse();
			if (!hashSet.Remove(steamId))
			{
				return false;
			}
			PluginConfig.BannedSteamIds.Value = string.Join(",", hashSet.Select((ulong id) => id.ToString()).ToArray());
			Dictionary<ulong, BanRecord> dictionary = ParseRecords();
			dictionary.Remove(steamId);
			WriteRecords(dictionary, hashSet);
			Save();
			StartOfRound instance = StartOfRound.Instance;
			if ((Object)(object)instance != (Object)null && instance.KickedClientIds != null)
			{
				instance.KickedClientIds.Remove(steamId);
			}
			AntiCheatPlugin.LogInfo($"[Bans] Unbanned {steamId}.");
			return true;
		}

		public static List<BanRecord> Records()
		{
			Dictionary<ulong, BanRecord> dictionary = ParseRecords();
			List<BanRecord> list = new List<BanRecord>();
			foreach (ulong item in Parse())
			{
				if (!dictionary.TryGetValue(item, out var value))
				{
					value = new BanRecord
					{
						SteamId = item
					};
				}
				list.Add(value);
			}
			list.Sort((BanRecord a, BanRecord b) => (a.HasWhen != b.HasWhen) ? ((!a.HasWhen) ? 1 : (-1)) : (a.HasWhen ? b.When.CompareTo(a.When) : a.SteamId.CompareTo(b.SteamId)));
			return list;
		}

		public static void CacheNames(Dictionary<ulong, string> resolved)
		{
			if (resolved == null || resolved.Count == 0)
			{
				return;
			}
			HashSet<ulong> hashSet = Parse();
			Dictionary<ulong, BanRecord> dictionary = ParseRecords();
			bool flag = false;
			foreach (KeyValuePair<ulong, string> item in resolved)
			{
				if (!string.IsNullOrEmpty(item.Value) && hashSet.Contains(item.Key))
				{
					if (!dictionary.TryGetValue(item.Key, out var value))
					{
						value = new BanRecord
						{
							SteamId = item.Key
						};
						dictionary[item.Key] = value;
					}
					if (!(value.Name == item.Value))
					{
						value.Name = Sanitize(item.Value);
						flag = true;
					}
				}
			}
			if (flag)
			{
				WriteRecords(dictionary, hashSet);
				Save();
			}
		}

		private static Dictionary<ulong, BanRecord> ParseRecords()
		{
			Dictionary<ulong, BanRecord> dictionary = new Dictionary<ulong, BanRecord>();
			if (PluginConfig.BanRecords == null)
			{
				return dictionary;
			}
			string value = PluginConfig.BanRecords.Value;
			if (string.IsNullOrEmpty(value))
			{
				return dictionary;
			}
			string[] array = value.Split(';');
			foreach (string text in array)
			{
				string[] array2 = text.Split('|');
				if (array2.Length >= 2 && ulong.TryParse(array2[0].Trim(), out var result) && result != 0)
				{
					BanRecord banRecord = new BanRecord
					{
						SteamId = result
					};
					if (Enum.TryParse<BanSource>(array2[1].Trim(), out var result2))
					{
						banRecord.Source = result2;
					}
					if (array2.Length >= 3)
					{
						banRecord.Name = array2[2].Trim();
					}
					if (array2.Length >= 4 && long.TryParse(array2[3].Trim(), out var result3) && result3 > 0)
					{
						banRecord.When = new DateTime(result3);
						banRecord.HasWhen = true;
					}
					dictionary[result] = banRecord;
				}
			}
			return dictionary;
		}

		private static void WriteRecords(Dictionary<ulong, BanRecord> records, HashSet<ulong> banned)
		{
			List<string> list = new List<string>();
			foreach (BanRecord value in records.Values)
			{
				if (banned.Contains(value.SteamId))
				{
					list.Add(string.Join("|", value.SteamId.ToString(), value.Source.ToString(), value.Name ?? string.Empty, value.HasWhen ? value.When.Ticks.ToString() : "0"));
				}
			}
			PluginConfig.BanRecords.Value = string.Join(";", list.ToArray());
		}

		private static string Sanitize(string name)
		{
			return name.Replace('|', ' ').Replace(';', ' ').Trim();
		}

		private static void Save()
		{
			try
			{
				((ConfigEntryBase)PluginConfig.BannedSteamIds).ConfigFile.Save();
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo($"[Bans] Failed to write the ban list to disk: {arg}");
			}
		}

		[HarmonyPatch(typeof(GameNetworkManager), "ConnectionApproval")]
		[HarmonyPriority(800)]
		[HarmonyPrefix]
		[HarmonyWrapSafe]
		public static bool RejectBannedSteamIdsAtApproval(ConnectionApprovalRequest request, ConnectionApprovalResponse response)
		{
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				string text = Encoding.UTF8.GetString(request.Payload);
				string[] array = text.Split(',');
				if (array.Length >= 2 && ulong.TryParse(array[1].Trim(), out var result) && Contains(result))
				{
					response.Approved = false;
					response.CreatePlayerObject = false;
					response.Pending = false;
					response.Reason = PluginConfig.BanReason.Value;
					AntiCheatPlugin.LogInfo($"[Bans] Rejected banned steamId {result} at connection approval.");
					return false;
				}
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo($"[Bans] RejectBannedSteamIdsAtApproval failed: {arg}");
			}
			return true;
		}

		public static void EnsureBanCheckSubscribed()
		{
			if (!_banCheckSubscribed && !((Object)(object)NetworkManager.Singleton == (Object)null))
			{
				NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnectedBanCheck;
				_banCheckSubscribed = true;
			}
		}

		[HarmonyPatch(typeof(GameNetworkManager), "Start")]
		[HarmonyPostfix]
		[HarmonyWrapSafe]
		public static void HookClientConnectedForBanCheck()
		{
			EnsureBanCheckSubscribed();
		}

		private static void OnClientConnectedBanCheck(ulong clientId)
		{
			if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsServer && clientId != NetworkManager.Singleton.LocalClientId && !((Object)(object)StartOfRound.Instance == (Object)null))
			{
				((MonoBehaviour)StartOfRound.Instance).StartCoroutine(CheckAndKickIfBannedCoroutine(clientId));
			}
		}

		private static IEnumerator CheckAndKickIfBannedCoroutine(ulong clientId)
		{
			for (int attempt = 0; attempt < 30; attempt++)
			{
				ulong steamId = SteamIdForClient(clientId);
				if (steamId != 0)
				{
					if (Contains(steamId))
					{
						AntiCheatPlugin.LogInfo($"[Bans] Rejecting banned player {steamId} on connect.");
						NetworkManager.Singleton.DisconnectClient(clientId, PluginConfig.BanReason.Value);
					}
					yield break;
				}
				yield return null;
			}
			AntiCheatPlugin.LogInfo($"[Bans] Could not resolve a Steam ID for client {clientId} within the timeout - ban check skipped for this connection.");
		}

		public static ulong SteamIdForClient(ulong clientId)
		{
			if ((Object)(object)StartOfRound.Instance == (Object)null || StartOfRound.Instance.allPlayerScripts == null)
			{
				return 0uL;
			}
			PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts;
			foreach (PlayerControllerB val in allPlayerScripts)
			{
				if ((Object)(object)val != (Object)null && val.actualClientId == clientId)
				{
					return val.playerSteamId;
				}
			}
			return 0uL;
		}

		public static string NameForClient(ulong clientId)
		{
			if ((Object)(object)StartOfRound.Instance != (Object)null && StartOfRound.Instance.allPlayerScripts != null)
			{
				PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts;
				foreach (PlayerControllerB val in allPlayerScripts)
				{
					if ((Object)(object)val != (Object)null && val.actualClientId == clientId)
					{
						return val.playerUsername;
					}
				}
			}
			return $"Client {clientId}";
		}
	}
	internal sealed class CheckEntry
	{
		public string Label;

		public string Group;

		public FeatureConfig Feature;

		public string Key => Feature.Key;
	}
	internal static class CheckCatalog
	{
		private const string Combat = "COMBAT AND DAMAGE";

		private const string Movement = "MOVEMENT";

		private const string Items = "ITEMS";

		private const string Ship = "SHIP AND TERMINAL";

		private const string World = "WORLD AND ENEMIES";

		private const string Chat = "IDENTITY AND CLIENT MODS";

		private static readonly List<CheckEntry> Entries = new List<CheckEntry>();

		private static readonly List<CheckEntry> CheatEntries = new List<CheckEntry>();

		private static readonly Dictionary<string, CheckEntry> ByKeyLookup = new Dictionary<string, CheckEntry>();

		private static readonly Dictionary<string, string> Merged = new Dictionary<string, string>
		{
			{ "DamageMagnitudeSetting", "GodModeSetting" },
			{ "KillOrderSetting", "GodModeSetting" },
			{ "EnemyHitAttributionSetting", "GodModeSetting" },
			{ "EnemyContactSetting", "GodModeSetting" },
			{ "FallDamageSetting", "GodModeSetting" },
			{ "HealthSetting", "GodModeSetting" },
			{ "RPCReportSetting", "GodModeSetting" },
			{ "ChatRealSettings", "PlayerNameSetting" },
			{ "NamelessSettings", "PlayerNameSetting" },
			{ "ItemCooldownSetting", "UseRateSetting" },
			{ "ShipLightSettings", "UseRateSetting" },
			{ "TerminalNoiseSettings", "UseRateSetting" }
		};

		public static IList<CheckEntry> All => Entries;

		public static IList<CheckEntry> Cheats => CheatEntries;

		public static string CanonicalKey(string key)
		{
			if (string.IsNullOrEmpty(key))
			{
				return key;
			}
			string value;
			return Merged.TryGetValue(key, out value) ? value : key;
		}

		public static string LabelFor(FeatureConfig feature)
		{
			if (feature == null)
			{
				return null;
			}
			CheckEntry checkEntry = ByKey(feature.Key);
			return (checkEntry != null) ? checkEntry.Label : feature.Key;
		}

		public static CheckEntry ByKey(string key)
		{
			if (string.IsNullOrEmpty(key))
			{
				return null;
			}
			CheckEntry value;
			return ByKeyLookup.TryGetValue(CanonicalKey(key), out value) ? value : null;
		}

		internal static void Register()
		{
			Entries.Clear();
			CheatEntries.Clear();
			ByKeyLookup.Clear();
			Add("COMBAT AND DAMAGE", "God mode", PluginConfig.GodMode);
			Add("COMBAT AND DAMAGE", "Impossible melee damage", PluginConfig.Shovel);
			Add("COMBAT AND DAMAGE", "Infinite shotgun ammo", PluginConfig.InfiniteAmmo);
			Add("MOVEMENT", "Excessive movement speed", PluginConfig.SpeedHack);
			Add("MOVEMENT", "Impossible position jump", PluginConfig.Teleport);
			Add("MOVEMENT", "Forced jetpack explosion", PluginConfig.Jetpack);
			Add("ITEMS", "Out-of-range item grabs", PluginConfig.GrabObject);
			Add("ITEMS", "Item deletion", PluginConfig.DespawnItem);
			Add("ITEMS", "Use-rate limits", PluginConfig.UseRate);
			Add("ITEMS", "Gift box abuse", PluginConfig.Gift);
			Add("ITEMS", "Abnormal mask spawns", PluginConfig.Mask);
			Add("SHIP AND TERMINAL", "Forced ship lever", PluginConfig.Ship);
			Add("SHIP AND TERMINAL", "Furniture placement", PluginConfig.ShipBuild);
			Add("SHIP AND TERMINAL", "Terminal use at distance", PluginConfig.RemoteTerminal);
			Add("SHIP AND TERMINAL", "Credit-free purchases", PluginConfig.FreeBuy);
			Add("SHIP AND TERMINAL", "Forced company monster", PluginConfig.Boss);
			Add("WORLD AND ENEMIES", "Forced landmine detonation", PluginConfig.Landmine);
			Add("WORLD AND ENEMIES", "Spawned spider webs", PluginConfig.SpawnWebTrap);
			Add("IDENTITY AND CLIENT MODS", "Player names", PluginConfig.PlayerName);
			Add("IDENTITY AND CLIENT MODS", "Minimap mods", PluginConfig.Map);
		}

		private static void Add(string group, string label, FeatureConfig feature)
		{
			if (feature != null)
			{
				CheckEntry checkEntry = new CheckEntry
				{
					Group = group,
					Label = label,
					Feature = feature
				};
				Entries.Add(checkEntry);
				CheatEntries.Add(checkEntry);
				ByKeyLookup[checkEntry.Key] = checkEntry;
			}
		}
	}
	public enum PunishmentType
	{
		Disable,
		OnlyLog,
		Kick,
		Ban
	}
	public class FeatureConfig
	{
		public ConfigEntry<PunishmentType> PunishmentEntry { get; }

		public string Key { get; }

		public PunishmentType Punishment => PunishmentEntry.Value;

		public bool Enable => Punishment != PunishmentType.Disable;

		public bool Value => Enable;

		public FeatureConfig(ConfigFile config, string section, string featureDesc, PunishmentType defaultPunishment = PunishmentType.OnlyLog)
		{
			Key = section;
			PunishmentEntry = config.Bind<PunishmentType>(section, "Punishment", defaultPunishment, featureDesc + "\n\n" + AntiCheatPlugin.localizationManager.Cfg_GetString("Punishment"));
		}
	}
	internal static class LateJoinSync
	{
		private const uint GenerateNewLevelRpcId = 3073943002u;

		private const uint FinishGeneratingNewLevelRpcId = 2729232387u;

		private const string Module = "[LateJoin]";

		private static readonly HashSet<ulong> Pending = new HashSet<ulong>();

		private static readonly HashSet<ulong> FinishedGenerating = new HashSet<ulong>();

		internal static void Reset()
		{
			Pending.Clear();
			FinishedGenerating.Clear();
		}

		internal static void NoteFinishedGenerating(ulong clientId)
		{
			FinishedGenerating.Add(clientId);
		}

		internal static void OnClientConnected(ulong clientId)
		{
			if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsServer && clientId != NetworkManager.Singleton.LocalClientId)
			{
				StartOfRound instance = StartOfRound.Instance;
				if (!((Object)(object)instance == (Object)null) && !instance.inShipPhase && !Pending.Contains(clientId))
				{
					Pending.Add(clientId);
					FinishedGenerating.Remove(clientId);
					((MonoBehaviour)instance).StartCoroutine(SyncJoiner(clientId));
				}
			}
		}

		private static IEnumerator SyncJoiner(ulong clientId)
		{
			AntiCheatPlugin.LogInfo(string.Format("{0} Client {1} joined mid-round; syncing them into the level.", "[LateJoin]", clientId));
			yield return (object)new WaitForSeconds(2f);
			if (!StillConnected(clientId))
			{
				Pending.Remove(clientId);
				yield break;
			}
			ClearStaleSlotState(clientId);
			SendNamesAndRepair(clientId);
			RoundManager manager = RoundManager.Instance;
			StartOfRound round = StartOfRound.Instance;
			if ((Object)(object)manager == (Object)null || (Object)(object)round == (Object)null || (Object)(object)manager.currentLevel == (Object)null)
			{
				AntiCheatPlugin.LogInfo(string.Format("{0} No round to sync client {1} into.", "[LateJoin]", clientId));
				Pending.Remove(clientId);
				yield break;
			}
			int seed = round.randomMapSeed;
			int levelId = manager.currentLevel.levelID;
			int moldIterations = manager.currentLevel.moldSpreadIterations;
			int moldStart = manager.currentLevel.moldStartPosition;
			uint generateId = TargetedClientRpc.ResolveRpcId(3073943002u, "GenerateNewLevelClientRpc");
			if (!TargetedClientRpc.Send((NetworkBehaviour)(object)manager, generateId, clientId, delegate(FastBufferWriter writer)
			{
				//IL_0001: Unknown result type (might be due to invalid IL or missing references)
				//IL_000e: Unknown result type (might be due to invalid IL or missing references)
				//IL_001b: Unknown result type (might be due to invalid IL or missing references)
				//IL_0028: Unknown result type (might be due to invalid IL or missing references)
				//IL_003d: Unknown result type (might be due to invalid IL or missing references)
				//IL_0043: Unknown result type (might be due to invalid IL or missing references)
				BytePacker.WriteValueBitPacked(writer, seed);
				BytePacker.WriteValueBitPacked(writer, levelId);
				BytePacker.WriteValueBitPacked(writer, moldIterations);
				BytePacker.WriteValueBitPacked(writer, moldStart);
				bool flag = false;
				((FastBufferWriter)(ref writer)).WriteValueSafe<bool>(ref flag, default(ForPrimitives));
			}))
			{
				AntiCheatPlugin.LogInfo(string.Format("{0} Could not send the level to client {1}. They are in the round with no interior - kick them and have them rejoin from orbit.", "[LateJoin]", clientId));
				Pending.Remove(clientId);
				yield break;
			}
			AntiCheatPlugin.LogInfo(string.Format("{0} Sent level {1} (seed {2}) to client {3}; waiting for them to finish generating.", "[LateJoin]", levelId, seed, clientId));
			float timeout = ((PluginConfig.Lobby_LateJoinSyncTimeout == null) ? 90f : Mathf.Clamp(PluginConfig.Lobby_LateJoinSyncTimeout.Value, 10f, 600f));
			float deadline = Time.realtimeSinceStartup + timeout;
			while (!FinishedGenerating.Contains(clientId) && Time.realtimeSinceStartup < deadline)
			{
				if (!StillConnected(clientId))
				{
					AntiCheatPlugin.LogInfo(string.Format("{0} Client {1} left while generating.", "[LateJoin]", clientId));
					Pending.Remove(clientId);
					yield break;
				}
				yield return (object)new WaitForSeconds(0.5f);
			}
			if (!FinishedGenerating.Contains(clientId))
			{
				AntiCheatPlugin.LogInfo(string.Format("{0} Client {1} did not report a finished dungeon within {2}s - lifting their loading screen regardless.", "[LateJoin]", clientId, timeout));
			}
			uint finishId = TargetedClientRpc.ResolveRpcId(2729232387u, "FinishGeneratingNewLevelClientRpc");
			TargetedClientRpc.Send((NetworkBehaviour)(object)manager, finishId, clientId, null);
			RecountLivingPlayers();
			Pending.Remove(clientId);
			AntiCheatPlugin.LogInfo(string.Format("{0} Client {1} is now in the round.", "[LateJoin]", clientId));
			MenuLog.Info(string.Format("{0} {1} joined mid-round.", "[LateJoin]", NameFor(clientId)));
			if (PluginConfig.Lobby_AnnounceLateJoin != null && PluginConfig.Lobby_AnnounceLateJoin.Value)
			{
				AntiCheatPlugin.AddTextMessageClientRpc(AntiCheatPlugin.Colorize($"{NameFor(clientId)} joined mid-round.", (PluginConfig.PlayerJoin_Color == null) ? string.Empty : PluginConfig.PlayerJoin_Color.Value));
			}
		}

		private static bool StillConnected(ulong clientId)
		{
			if ((Object)(object)NetworkManager.Singleton == (Object)null)
			{
				return false;
			}
			foreach (ulong connectedClientsId in NetworkManager.Singleton.ConnectedClientsIds)
			{
				if (connectedClientsId == clientId)
				{
					return true;
				}
			}
			return false;
		}

		private static PlayerControllerB PlayerFor(ulong clientId)
		{
			StartOfRound instance = StartOfRound.Instance;
			if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null)
			{
				return null;
			}
			if (instance.ClientPlayerList != null && instance.ClientPlayerList.TryGetValue(clientId, out var value) && value >= 0 && value < instance.allPlayerScripts.Length)
			{
				return instance.allPlayerScripts[value];
			}
			return null;
		}

		private static string NameFor(ulong clientId)
		{
			PlayerControllerB val = PlayerFor(clientId);
			return ((Object)(object)val == (Object)null || string.IsNullOrEmpty(val.playerUsername)) ? $"Client {clientId}" : val.playerUsername;
		}

		private static void SendNamesAndRepair(ulong clientId)
		{
			StartOfRound instance = StartOfRound.Instance;
			if (!((Object)(object)instance == (Object)null) && instance.allPlayerScripts != null && instance.ClientPlayerList != null)
			{
				string[] array = new string[instance.allPlayerScripts.Length];
				for (int i = 0; i < array.Length; i++)
				{
					PlayerControllerB val = instance.allPlayerScripts[i];
					array[i] = (((Object)(object)val == (Object)null) ? string.Empty : (val.playerUsername ?? string.Empty));
				}
				if (instance.ClientPlayerList.TryGetValue(clientId, out var value))
				{
					Network.SendPlayerNames(clientId, array);
					Network.SendPlayerSlotRepair(clientId, value);
				}
			}
		}

		internal static void ApplyPlayerNames(PlayerNames received)
		{
			StartOfRound instance = StartOfRound.Instance;
			if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null || ((PlayerNames)(ref received)).Names == null)
			{
				return;
			}
			int num = Math.Min(instance.allPlayerScripts.Length, ((PlayerNames)(ref received)).Names.Length);
			for (int i = 0; i < num; i++)
			{
				PlayerControllerB val = instance.allPlayerScripts[i];
				if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(((PlayerNames)(ref received)).Names[i]))
				{
					val.playerUsername = ((PlayerNames)(ref received)).Names[i];
				}
			}
		}

		internal static void RepairPlayerSlot(PlayerSlotRepair repair)
		{
			StartOfRound instance = StartOfRound.Instance;
			if (!((Object)(object)instance == (Object)null) && instance.allPlayerScripts != null && instance.allPlayerObjects != null && ((PlayerSlotRepair)(ref repair)).Slot >= 0 && ((PlayerSlotRepair)(ref repair)).Slot < instance.allPlayerScripts.Length && ((PlayerSlotRepair)(ref repair)).Slot < instance.allPlayerObjects.Length)
			{
				PlayerControllerB val = instance.allPlayerScripts[((PlayerSlotRepair)(ref repair)).Slot];
				if ((Object)(object)val != (Object)null && (Object)(object)instance.allPlayerObjects[((PlayerSlotRepair)(ref repair)).Slot] != (Object)null)
				{
					val.DisablePlayerModel(instance.allPlayerObjects[((PlayerSlotRepair)(ref repair)).Slot], true, false);
				}
			}
		}

		private static void ClearStaleSlotState(ulong clientId)
		{
			PlayerControllerB val = PlayerFor(clientId);
			if ((Object)(object)val == (Object)null)
			{
				return;
			}
			try
			{
				val.disconnectedMidGame = false;
				val.isPlayerDead = false;
				val.isInsideFactory = false;
				val.criticallyInjured = false;
				val.bleedingHeavily = false;
				val.health = 100;
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo(string.Format("{0} Could not clear the slot for client {1}: {2}", "[LateJoin]", clientId, arg));
			}
		}

		private static void RecountLivingPlayers()
		{
			StartOfRound instance = StartOfRound.Instance;
			if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null)
			{
				return;
			}
			int num = 0;
			for (int i = 0; i < instance.allPlayerScripts.Length; i++)
			{
				PlayerControllerB val = instance.allPlayerScripts[i];
				if ((Object)(object)val != (Object)null && val.isPlayerControlled && !val.isPlayerDead)
				{
					num++;
				}
			}
			if (instance.livingPlayers != num)
			{
				AntiCheatPlugin.LogInfo(string.Format("{0} Living player count corrected from {1} to {2} after the join.", "[LateJoin]", instance.livingPlayers, num));
				instance.livingPlayers = num;
				instance.allPlayersDead = num == 0;
			}
		}
	}
	public enum LobbyVisibility
	{
		Public,
		FriendsOnly,
		Private,
		Invisible
	}
	internal static class LobbyControl
	{
		private const string Module = "[Lobby]";

		private static bool _visibilityChosen;

		private static bool _lateJoinChosen;

		public static LobbyVisibility Visibility { get; private set; } = LobbyVisibility.FriendsOnly;

		public static bool IsOpen { get; private set; }

		public static bool LateJoin { get; private set; }

		public static bool HasLobby => (Object)(object)GameNetworkManager.Instance != (Object)null && !GameNetworkManager.Instance.disableSteam && GameNetworkManager.Instance.currentLobby.HasValue;

		public static bool IsHost => (Object)(object)NetworkManager.Singleton != (Object)null && NetworkManager.Singleton.IsServer;

		public static bool InOrbit => (Object)(object)StartOfRound.Instance != (Object)null && StartOfRound.Instance.inShipPhase;

		public static bool Reopening { get; private set; }

		private static bool CanControl(out string reason)
		{
			if (!IsHost)
			{
				reason = "Only the host can change the lobby.";
				return false;
			}
			if ((Object)(object)GameNetworkManager.Instance == (Object)null || GameNetworkManager.Instance.disableSteam)
			{
				reason = "This session is not running on Steam.";
				return false;
			}
			if (!InOrbit)
			{
				reason = "Only in orbit. Come back to orbit to change the lobby.";
				return false;
			}
			reason = null;
			return true;
		}

		public static bool CanEditVisibility(out string reason)
		{
			return CanControl(out reason);
		}

		public static bool CanEditJoining(out string reason)
		{
			if (!CanControl(out reason))
			{
				return false;
			}
			if (!HasLobby)
			{
				reason = "No Steam lobby - the game left it at take-off. Reopen one below.";
				return false;
			}
			reason = null;
			return true;
		}

		public static bool CanReopen(out string reason)
		{
			if (!CanControl(out reason))
			{
				return false;
			}
			if (HasLobby)
			{
				reason = "The lobby is already up.";
				return false;
			}
			if (Reopening)
			{
				reason = "Already opening a lobby.";
				return false;
			}
			reason = null;
			return true;
		}

		public static string Describe()
		{
			if (!HasLobby)
			{
				return "no lobby";
			}
			return string.Format("{0} / {1}", IsOpen ? "open" : "closed", VisibilityText(Visibility));
		}

		public static string VisibilityText(LobbyVisibility visibility)
		{
			return visibility switch
			{
				LobbyVisibility.Public => "public", 
				LobbyVisibility.FriendsOnly => "friends only", 
				LobbyVisibility.Private => "invite only", 
				_ => "invisible", 
			};
		}

		internal static void OnLobbyCreated(bool isPublic)
		{
			if (!_visibilityChosen)
			{
				Visibility = ((!isPublic) ? LobbyVisibility.FriendsOnly : LobbyVisibility.Public);
			}
			IsOpen = false;
			if (!_lateJoinChosen)
			{
				LateJoin = PluginConfig.Lobby_AllowLateJoin != null && PluginConfig.Lobby_AllowLateJoin.Value;
			}
			AntiCheatPlugin.LogInfo(string.Format("{0} Lobby created: {1}, late join {2}.", "[Lobby]", VisibilityText(Visibility), LateJoin ? "on" : "off"));
		}

		internal static void OnLobbyLeft()
		{
			IsOpen = false;
			LateJoinSync.Reset();
			PlayerSlots.Reset();
		}

		internal static void NoteJoinableChanged(bool joinable)
		{
			IsOpen = joinable;
		}

		public static bool SetVisibility(LobbyVisibility visibility, out string message)
		{
			if (!CanEditVisibility(out message))
			{
				return false;
			}
			if (HasLobby && !ApplyVisibility(visibility, out message))
			{
				return false;
			}
			Visibility = visibility;
			_visibilityChosen = true;
			message = (HasLobby ? $"Lobby is now {VisibilityText(visibility)}" : $"Next lobby will be {VisibilityText(visibility)}");
			MenuLog.Info(string.Format("{0} Host set the lobby {1}.", "[Lobby]", VisibilityText(visibility)));
			return true;
		}

		public static bool SetOpen(bool open, out string message)
		{
			//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)
			if (!CanEditJoining(out message))
			{
				return false;
			}
			try
			{
				Lobby value = GameNetworkManager.Instance.currentLobby.Value;
				((Lobby)(ref value)).SetJoinable(open);
			}
			catch (Exception arg)
			{
				message = "Steam refused the change - see the console";
				AntiCheatPlugin.LogInfo(string.Format("{0} SetJoinable({1}) failed: {2}", "[Lobby]", open, arg));
				return false;
			}
			IsOpen = open;
			message = (open ? "Lobby is open" : "Lobby is closed");
			MenuLog.Info(string.Format("{0} Host {1} the lobby.", "[Lobby]", open ? "opened" : "closed"));
			return true;
		}

		public static bool SetLateJoin(bool on, out string message)
		{
			if (!CanControl(out message))
			{
				return false;
			}
			LateJoin = on;
			_lateJoinChosen = true;
			message = (on ? "Late join on - the lobby stays up after take-off" : "Late join off - the lobby closes at take-off");
			MenuLog.Info(string.Format("{0} Host turned late join {1}.", "[Lobby]", on ? "on" : "off"));
			return true;
		}

		public static bool Reopen(out string message)
		{
			if (!CanReopen(out message))
			{
				return false;
			}
			Reopening = true;
			message = "Opening a new lobby...";
			AntiCheatPlugin.LogInfo(string.Format("{0} Host asked for a new lobby.", "[Lobby]"));
			CreateReplacementLobby();
			return true;
		}

		private static async void CreateReplacementLobby()
		{
			try
			{
				Lobby? created = await SteamMatchmaking.CreateLobbyAsync(4);
				if (!created.HasValue)
				{
					AntiCheatPlugin.LogInfo(string.Format("{0} Steam would not create a lobby.", "[Lobby]"));
					return;
				}
				if ((Object)(object)GameNetworkManager.Instance != (Object)null && !GameNetworkManager.Instance.currentLobby.HasValue)
				{
					AccessTools.Property(typeof(GameNetworkManager), "currentLobby").SetValue(GameNetworkManager.Instance, created);
					AntiCheatPlugin.LogInfo(string.Format("{0} Adopted the new lobby by hand.", "[Lobby]"));
				}
				ApplyVisibility(Visibility, out var _);
				Lobby lobby = GameNetworkManager.Instance.currentLobby.Value;
				((Lobby)(ref lobby)).SetJoinable(true);
				IsOpen = true;
				AntiCheatPlugin.LogInfo(string.Format("{0} New lobby is up: open, {1}.", "[Lobby]", VisibilityText(Visibility)));
				MenuLog.Info(string.Format("{0} Host opened a new lobby ({1}).", "[Lobby]", VisibilityText(Visibility)));
			}
			catch (Exception ex)
			{
				Exception ex2 = ex;
				AntiCheatPlugin.LogInfo(string.Format("{0} Could not open a new lobby: {1}", "[Lobby]", ex2));
			}
			finally
			{
				Reopening = false;
			}
		}

		internal static void ReassertAfterTakeOff()
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			if (HasLobby)
			{
				ApplyVisibility(Visibility, out var _);
				try
				{
					Lobby value = GameNetworkManager.Instance.currentLobby.Value;
					((Lobby)(ref value)).SetJoinable(IsOpen);
				}
				catch (Exception arg)
				{
					AntiCheatPlugin.LogInfo(string.Format("{0} Could not re-assert joinable at take-off: {1}", "[Lobby]", arg));
				}
				AntiCheatPlugin.LogInfo(string.Format("{0} Kept the lobby alive through take-off: {1}, {2}.", "[Lobby]", IsOpen ? "open" : "closed", VisibilityText(Visibility)));
			}
		}

		private static bool ApplyVisibility(LobbyVisibility visibility, out string message)
		{
			//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)
			message = null;
			try
			{
				Lobby value = GameNetworkManager.Instance.currentLobby.Value;
				switch (visibility)
				{
				case LobbyVisibility.Public:
					((Lobby)(ref value)).SetPublic();
					break;
				case LobbyVisibility.FriendsOnly:
					((Lobby)(ref value)).SetFriendsOnly();
					break;
				case LobbyVisibility.Private:
					((Lobby)(ref value)).SetPrivate();
					break;
				default:
					((Lobby)(ref value)).SetInvisible();
					break;
				}
				((Lobby)(ref value)).SetData("vers", GameNetworkManager.Instance.gameVersionNum.ToString());
				if (!string.IsNullOrEmpty(GameNetworkManager.Instance.steamLobbyName))
				{
					((Lobby)(ref value)).SetData("name", GameNetworkManager.Instance.steamLobbyName);
				}
				return true;
			}
			catch (Exception arg)
			{
				message = "Steam refused the change - see the console";
				AntiCheatPlugin.LogInfo(string.Format("{0} Could not set visibility {1}: {2}", "[Lobby]", visibility, arg));
				return false;
			}
		}
	}
	internal static class LobbyImprovementsBans
	{
		private static FieldInfo _kickReasonField;

		private static string _banPrefix;

		private static bool _resolved;

		private const string DefaultBanPrefix = "Banned From Lobby:";

		public static void TryIntegrate(Harmony harmony)
		{
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Expected O, but got Unknown
			if (!PluginConfig.EnableLobbyImprovementsBanIntegration.Value)
			{
				return;
			}
			try
			{
				harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(StartOfRound), "KickPlayer", (Type[])null, (Type[])null), new HarmonyMethod(typeof(LobbyImprovementsBans), "BanOnLobbyImprovementsBanPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				AntiCheatPlugin.LogInfo("[LobbyImprovements] Patched the kick method - the Ban button will now ban through the AntiCheat ban list.");
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo($"[LobbyImprovements] Setup failed: {arg}");
			}
		}

		private static bool EnsureResolved()
		{
			if (_resolved)
			{
				return _kickReasonField != null;
			}
			_resolved = true;
			Type type = AccessTools.TypeByName("LobbyImprovements.General_Patches");
			if (type == null)
			{
				AntiCheatPlugin.LogInfo("[LobbyImprovements] Type 'LobbyImprovements.General_Patches' not found - integration not active.");
				return false;
			}
			FieldInfo field = type.GetField("kickReason", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
			if (field == null || field.FieldType != typeof(string))
			{
				AntiCheatPlugin.LogInfo("[LobbyImprovements] Field 'General_Patches.kickReason' not found - integration not active.");
				return false;
			}
			_banPrefix = ReadBanPrefixConstant(type);
			_kickReasonField = field;
			AntiCheatPlugin.LogInfo("[LobbyImprovements] Resolved the kick reason - the Ban button is now wired up.");
			return true;
		}

		private static string ReadBanPrefixConstant(Type generalPatchesType)
		{
			try
			{
				FieldInfo field = generalPatchesType.GetField("banPrefixStr", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				if (field != null && field.IsLiteral)
				{
					string text = field.GetRawConstantValue() as string;
					if (!string.IsNullOrEmpty(text))
					{
						return text;
					}
				}
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo($"[LobbyImprovements] Could not read 'General_Patches.banPrefixStr', using the default wording: {arg}");
			}
			return "Banned From Lobby:";
		}

		private static bool BanOnLobbyImprovementsBanPrefix(StartOfRound __instance, int playerObjToKick)
		{
			try
			{
				if ((Object)(object)NetworkManager.Singleton == (Object)null || !NetworkManager.Singleton.IsServer)
				{
					return true;
				}
				if (!EnsureResolved())
				{
					return true;
				}
				string text = _kickReasonField.GetValue(null) as string;
				if (string.IsNullOrEmpty(text) || text.IndexOf(_banPrefix, StringComparison.OrdinalIgnoreCase) < 0)
				{
					return true;
				}
				if (playerObjToKick < 0 || playerObjToKick >= __instance.allPlayerScripts.Length)
				{
					return true;
				}
				PlayerControllerB val = __instance.allPlayerScripts[playerObjToKick];
				if ((Object)(object)val == (Object)null || (!val.isPlayerControlled && !val.isPlayerDead))
				{
					return true;
				}
				string playerUsername = val.playerUsername;
				ulong playerSteamId = val.playerSteamId;
				PatchHelper.BanPlayer(val, text, BanSource.Host);
				AntiCheatPlugin.LogInfo($"[LobbyImprovements] {playerUsername} (steamId {playerSteamId}) banned through the AntiCheat ban list instead of LobbyImprovements' session ban.");
				return false;
			}
			catch (Exception arg)
			{
				AntiCheatPlugin.LogInfo($"[LobbyImprovements] Ban redirect failed, falling back to LobbyImprovements' own ban: {arg}");
				return true;
			}
		}
	}
	internal enum SlotState
	{
		Host,
		Occupied,
		Free,
		Unavailable
	}
	internal struct SlotInfo
	{
		public int Index;

		public SlotState State;

		public string Name;
	}
	internal static class PlayerSlots
	{
		internal const int Count = 4;

		private static readonly string[] LastNames = new string[4];

		internal static void Reset()
		{
			for (int i = 0; i < LastNames.Length; i++)
			{
				LastNames[i] = null;
			}
		}

		internal static bool IsModelVisible(int index)
		{
			PlayerControllerB val = ScriptFor(index);
			return (Object)(object)val != (Object)null && (Object)(object)val.thisPlayerModel != (Object)null && ((Renderer)val.thisPlayerModel).enabled;
		}

		internal static List<SlotInfo> Describe()
		{
			List<SlotInfo> list = new List<SlotInfo>(4);
			StartOfRound instance = StartOfRound.Instance;
			for (int i = 0; i < 4; i++)
			{
				PlayerControllerB val = ScriptFor(i);
				SlotInfo item = new SlotInfo
				{
					Index = i
				};
				if ((Object)(object)val == (Object)null || (Object)(object)instance == (Object)null)
				{
					item.State = ((i != 0) ? SlotState.Free : SlotState.Host);
					item.Name = LastNames[i];
					list.Add(item);
					continue;
				}
				if (val.isPlayerControlled || val.isPlayerDead)
				{
					if (!string.IsNullOrEmpty(val.playerUsername))
					{
						LastNames[i] = val.playerUsername;
					}
					item.State = ((i != 0) ? SlotState.Occupied : SlotState.Host);
				}
				else
				{
					item.State = (IsModelVisible(i) ? SlotState.Free : SlotState.Unavailable);
				}
				item.Name = LastNames[i];
				list.Add(item);
			}
			return list;
		}

		internal static List<int> UsableFree(StartOfRound round)
		{
			return Free(round, wantVisible: true);
		}

		internal static List<int> UnavailableFree(StartOfRound round)
		{
			return Free(round, wantVisible: false);
		}

		private static List<int> Free(StartOfRound round, bool wantVisible)
		{
			List<int> list = new List<int>(4);
			if ((Object)(object)round == (Object)null || round.ClientPlayerList == null)
			{
				return list;
			}
			for (int i = 1; i < 4; i++)
			{
				if (!round.ClientPlayerList.ContainsValue(i) && IsModelVisible(i) == wantVisible)
				{
					list.Add(i);
				}
			}
			return list;
		}

		internal static string NameFor(int index)
		{
			return (index >= 0 && index < LastNames.Length) ? LastNames[index] : null;
		}

		private static PlayerControllerB ScriptFor(int index)
		{
			StartOfRound instance = StartOfRound.Instance;
			if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null || index < 0 || index >= instance.allPlayerScripts.Length)
			{
				return null;
			}
			return instance.allPlayerScripts[index];
		}
	}
	internal static class PluginConfig
	{
		public enum MessageType
		{
			GUI,
			HostChat,
			PublicChat,
			Menu
		}

		public enum PermissionLevel
		{
			Unrestricted,
			HostOnly,
			GameStart
		}

		public enum FriendlyFirePolicy
		{
			Allowed,
			Restricted
		}

		public const string DefaultAntiGriefKickReason = "You were kicked by the Anti-Grief System";

		public const string DefaultAntiGriefBanReason = "You were banned by the Anti-Grief System";

		public const string DefaultAntiCheatBanReason = "You have been banned by the Anti Cheat System for Cheating";

		public static FeatureConfig Shovel;

		public static FeatureConfig Ship;

		public static FeatureConfig ShipBuild;

		public static FeatureConfig Landmine;

		public static FeatureConfig InfiniteAmmo;

		public static FeatureConfig Mask;

		public static FeatureConfig Gift;

		public static FeatureConfig Teleport;

		public static FeatureConfig SpawnWebTrap;

		public static FeatureConfig Map;

		public static FeatureConfig FreeBuy;

		public static FeatureConfig RemoteTerminal;

		public static FeatureConfig DespawnItem;

		public static FeatureConfig PlayerName;

		public static FeatureConfig UseRate;

		public static FeatureConfig Boss;

		public static FeatureConfig GrabObject;

		public static FeatureConfig Jetpack;

		public static FeatureConfig GodMode;

		public static FeatureConfig SpeedHack;

		public static ConfigEntry<FriendlyFirePolicy> Restrict_FriendlyFire;

		public static ConfigEntry<PermissionLevel> Restrict_PurchaseStoreItems;

		public static ConfigEntry<PermissionLevel> Restrict_PurchaseStoreUnlocks;

		public static ConfigEntry<PermissionLevel> Restrict_RouteShipToMoon;

		public static ConfigEntry<PermissionLevel> Restrict_PlaceObjectIntoStorage;

		public static ConfigEntry<PermissionLevel> Restrict_ReturnObjectFromStorage;

		public static ConfigEntry<PermissionLevel> Restrict_MoveObjects;

		public static ConfigEntry<PermissionLevel> Restrict_MoveLightSwitch;

		public static ConfigEntry<PermissionLevel> Restrict_ToggleShipLights;

		public static ConfigEntry<int> Restrict_PullShipLever;

		public static ConfigEntry<int> AntiGrief_MaxViolations;

		public static ConfigEntry<PunishmentType> AntiGrief_Punishment;

		public static ConfigEntry<string> AntiGrief_KickReason;

		public static ConfigEntry<string> AntiGrief_BanReason;

		public static ConfigEntry<string> AntiGrief_MessageColor;

		public static ConfigEntry<string> Profiles_Data;

		public static ConfigEntry<string> Profiles_Assignments;

		public static ConfigEntry<string> BannedSteamIds;

		public static ConfigEntry<string> BanRecords;

		public static ConfigEntry<string> BanReason;

		public static ConfigEntry<bool> EnableLobbyImprovementsBanIntegration;

		public static ConfigEntry<bool> IgnoreClientConfig;

		public static ConfigEntry<string> Prefix;

		public static ConfigEntry<string> PlayerJoin;

		public static ConfigEntry<string> PlayerJoin_Color;

		public static ConfigEntry<MessageType> PlayerJoin_MessageType;

		public static ConfigEntry<bool> Disclaimer_Enable;

		public static ConfigEntry<bool> Disclaimer_OnJoin;

		public static ConfigEntry<string> Disclaimer_Text;

		public static ConfigEntry<string> Disclaimer_Aliases;

		public static ConfigEntry<MessageType> Disclaimer_MessageType;

		public static ConfigEntry<float> Disclaimer_JoinDelaySeconds;

		public static ConfigEntry<float> Disclaimer_CooldownSeconds;

		public static ConfigEntry<bool> Log;

		public static ConfigEntry<bool> OperationLog;

		public static ConfigEntry<int> ShipConfig2;

		public static ConfigEntry<string> ShipConfig3;

		public static ConfigEntry<int> ShipConfig4;

		public static ConfigEntry<bool> ShipSetting_OnlyOneVote;

		public static ConfigEntry<bool> ShipSetting_ChangToFreeMoon;

		public static ConfigEntry<int> UseRate_ShipLightCooldown;

		public static ConfigEntry<int> UseRate_TerminalNoiseCooldown;

		public static ConfigEntry<int> UseRate_ChatCooldown;

		public static ConfigEntry<int> GodMode_ReportDelay;

		public static ConfigEntry<bool> GodMode_WatchHitRpc;

		public static ConfigEntry<bool> GodMode_WatchKillPlayerRpc;

		public static ConfigEntry<bool> Shovel3;

		public static ConfigEntry<bool> GrabObject_SendLog;

		public static ConfigEntry<bool> GrabObject_MoreSlot;

		public static ConfigEntry<bool> GrabObject_TwoHand;

		public static ConfigEntry<bool> GrabObject_BeltBag;

		public static ConfigEntry<MessageType> OperationLog_MessageType;

		public static ConfigEntry<bool> Lobby_AllowLateJoin;

		public static ConfigEntry<float> Lobby_LateJoinSyncTimeout;

		public static ConfigEntry<bool> Lobby_AnnounceLateJoin;

		public static ConfigEntry<bool> Menu_Enable;

		public static ConfigEntry<int> Menu_MaxLogLines;

		public static ConfigEntry<int> Menu_MaxRecentPlayers;

		public static ConfigEntry<float> GodMode_InstakillGraceSeconds;

		public static ConfigEntry<int> GodMode_EnemyHitLethalThreshold;

		public static ConfigEntry<bool> GodMode_EnemyHitRequireAck;

		public static ConfigEntry<float> GodMode_EnemyHitAckSeconds;

		public static ConfigEntry<float> SpeedHack_MaxMultiplier;

		public static ConfigEntry<float> SpeedHack_MinMovingSpeed;

		public static ConfigEntry<float> SpeedHack_MinAbsoluteSpeed;

		public static ConfigEntry<int> SpeedHack_MinConsecutiveReadings;

		public static ConfigEntry<float> PositionSampleWindowSeconds;

		public static ConfigEntry<float> MaxPlausibleSpeed;

		public static ConfigEntry<float> MovementGraceSeconds;

		public static ConfigEntry<bool> MovementDiagnostics;

		public static bool ShipConfig => Ship.Enable;

		internal static void Init(BaseUnityPlugin plugin)
		{
			ConfigFile config = plugin.Config;
			LocalizationManager localizationManager = AntiCheatPlugin.localizationManager;
			IgnoreClientConfig = config.Bind<bool>("VersionSetting", "NetworkSetting", false, localizationManager.Cfg_GetString("NetworkSetting"));
			Prefix = config.Bind<string>("ServerNameSetting", "Prefix", "AC", localizationManager.Cfg_GetString("Prefix"));
			Log = config.Bind<bool>("LogSetting", "Log", true, localizationManager.Cfg_GetString("Log"));
			OperationLog = config.Bind<bool>("LogSetting", "OperationLog", true, localizationManager.Cfg_GetString("OperationLog"));
			Ship = new FeatureConfig(config, "ShipSetting", localizationManager.Cfg_GetString("ShipSetting"));
			ShipConfig2 = config.Bind<int>("ShipSetting", "StartGamePlayerCount", 8, localizationManager.Cfg_GetString("ShipConfig2"));
			ShipConfig3 = config.Bind<string>("ShipSetting", "EndGamePlayerTime", "14:00", localizationManager.Cfg_GetString("ShipConfig3"));
			ShipConfig4 = config.Bind<int>("ShipSetting", "EndGamePlayerCount", 50, localizationManager.Cfg_GetString("ShipConfig4"));
			ShipSetting_OnlyOneVote = config.Bind<bool>("ShipSetting", "OnlyOneVote", true, localizationManager.Cfg_GetString("ShipConfig6"));
			ShipSetting_ChangToFreeMoon = config.Bind<bool>("ShipSetting", "ChangToFreeMoon", false, localizationManager.Cfg_GetString("ChangToFreeMoon"));
			ShipBuild = new FeatureConfig(config, "ShipBuildSetting", localizationManager.Cfg_GetString("ShipBuild"));
			UseRate = new FeatureConfig(config, "UseRateSetting", "Detects an action repeated faster than the game allows: items and weapons used inside their own animation, the ship's light switch, the terminal's noise, and chat. The action is refused; the cooldowns themselves are the entries below.");
			DespawnItem = new FeatureConfig(config, "DespawnItemSettings", localizationManager.Cfg_GetString("DespawnItem"));
			Mask = new FeatureConfig(config, "MaskSettings", localizationManager.Cfg_GetString("Mask"));
			Gift = new FeatureConfig(config, "GiftSettings", localizationManager.Cfg_GetString("Gift"));
			InfiniteAmmo = new FeatureConfig(config, "InfiniteAmmoSettings", localizationManager.Cfg_GetString("InfiniteAmmo"));
			Teleport = new FeatureConfig(config, "InvisibilitySettings", localizationManager.Cfg_GetString("Teleport"));
			Boss = new FeatureConfig(config, "BossSetting", localizationManager.Cfg_GetString("Boss"));
			Jetpack = new FeatureConfig(config, "JetpackSetting", localizationManager.Cfg_GetString("Jetpack"));
			Landmine = new FeatureConfig(config, "LandmineSetting", localizationManager.Cfg_GetString("Landmine"));
			SpawnWebTrap = new FeatureConfig(config, "SpawnWebTrapSetting", localizationManager.Cfg_GetString("SpawnWebTrap"));
			Map = new FeatureConfig(config, "MapSetting", localizationManager.Cfg_GetString("Map"));
			FreeBuy = new FeatureConfig(config, "FreeBuySettings", localizationManager.Cfg_GetString("FreeBuy"));
			RemoteTerminal = new FeatureConfig(config, "RemoteTerminalSettings", localizationManager.Cfg_GetString("RemoteTerminal"));
			PlayerName = new FeatureConfig(config, "PlayerNameSetting", "Detects a player sending chat under somebody else's identity - a message addressed from a crewmate's player id rather than their own. The message is refused as well as reported.");
			Shovel = new FeatureConfig(config, "ShovelSettings", localizationManager.Cfg_GetString("Shovel"));
			GrabObject = new FeatureConfig(config, "GrabObjectSetting", localizationManager.Cfg_GetString("GrabObject"));
			GodMode = new FeatureConfig(config, "GodModeSetting", "Flags a player who survives what should have killed them, from every angle the host can see it. Their own damage reports (more damage taken than health left, still alive). Damage reports that never arrive at all, for an announced Hit or KillPlayer RPC. Damage added up host-side from the enemies that announce their hits (Thumper 40, Bunker Spider 90, Hoarder Bug 30). The enemies that announce nothing and decide damage on the victim's machine (Coilhead, Hygrodere, Baboon Hawk, Cadaver Bloom, both bees), simulated host-side with vanilla's own collider overlap, hit interval and damage numbers. Falls, run through vanilla's own arithmetic - 38 per second, the 30/50/80/100 ladder - with jetpacks, ladders, vehicles, lifts, quicksand and water left alone as vanilla leaves them. The instakill enemies (Eyeless Dog, Jester, Bracken, Masked, Maneater), which send no damage report at all and are only visible by the victim not dying; a kill the enemy never got to finish is never counted. And negative damage reported as healing. Damage amounts and hit timing are never judged on their own, so modded enemies and weapons do not trip it.");
			SpeedHack = new FeatureConfig(config, "SpeedHackSetting", "Flags sustained movement speed well above a player's own recent baseline, inferred from position reports over time. Only movement a player produced themselves is judged: jetpack flights, ladders, the cruiser, lifts, the ship in orbit and both the landing and the take-off are all excluded outright, and so is every sample where the player was off the ground - which their own controller reports, rather than the host guessing at it with a raycast that misses on stairs and ramps.");
			BannedSteamIds = config.Bind<string>("!Bans", "BannedSteamIds", "", "Comma-separated list of banned Steam64 IDs. Written automatically whenever a check or an Anti-Grief violation results in a Ban punishment; you can also edit it by hand. Every entry here is rejected at connection approval, so a ban survives a restart.");
			BanRecords = config.Bind<string>("!Bans", "BanRecords", "", "Written automatically - what put each banned Steam ID on the list, their name and when, for the Ban List window in the menu. One entry per ban as steamId|source|name|timestamp, separated by semicolons. BannedSteamIds above is still the list that decides who is refused; an ID there with no entry here is simply an unattributed ban and shows as \"-\".");
			BanReason = config.Bind<string>("!Bans", "BanReason", "You have been banned by the Anti Cheat System for Cheating", "Disconnect reason shown to a player banned by one of the cheat checks.");
			EnableLobbyImprovementsBanIntegration = config.Bind<bool>("!Bans", "LobbyImprovementsBanIntegration", true, "Routes LobbyImprovements' in-game Ban button through the ban list above. LobbyImprovements' own ban only lasts the session, so without this the player is back the moment the host opens a new lobby. Does nothing if LobbyImprovements is not installed.");
			Restrict_FriendlyFire = config.Bind<FriendlyFirePolicy>("AntiGriefSetting", "FriendlyFire", FriendlyFirePolicy.Restricted, "Restricted blocks player weapon damage for everyone. Allowed restores vanilla friendly fire. Creature, mine, turret, fall and ship-door damage are unchanged.");
			Restrict_PurchaseStoreItems = config.Bind<PermissionLevel>("AntiGriefSetting", "PurchaseStoreItems", PermissionLevel.GameStart, "Who can order from the terminal. Unrestricted = anybody, HostOnly = the host, GameStart = the host until a player has been through a landing.\n\nGameStart is per player: it lifts once the ship has landed since they joined, so a newcomer three rounds in is restricted exactly as the founding crew were. Reconnecting keeps the rounds already served; a new lobby starts everybody over.");
			Restrict_PurchaseStoreUnlocks = config.Bind<PermissionLevel>("AntiGriefSetting", "PurchaseStoreUnlocks", PermissionLevel.GameStart, "Restricts who can purchase store unlockables.");
			Restrict_RouteShipToMoon = config.Bind<PermissionLevel>("AntiGriefSetting", "RouteShipToMoon", PermissionLevel.GameStart, "Restricts who can route the ship to a moon.");
			Restrict_PlaceObjectIntoStorage = config.Bind<PermissionLevel>("AntiGriefSetting", "PlaceObjectIntoStorage", PermissionLevel.GameStart, "Restricts who can place objects into storage.");
			Restrict_ReturnObjectFromStorage = config.Bind<PermissionLevel>("AntiGriefSetting", "ReturnObjectFromStorage", PermissionLevel.GameStart, "Restricts who can take objects back out of storage.");
			Restrict_MoveObjects = config.Bind<PermissionLevel>("AntiGriefSetting", "MoveObjects", PermissionLevel.GameStart, "Who can move ship furniture. The light switch has its own setting below.");
			Restrict_MoveLightSwitch = config.Bind<PermissionLevel>("AntiGriefSetting", "MoveLightSwitch", PermissionLevel.GameStart, "Who can move the light switch, separately from the rest of the furniture. Hiding it is the cheap grief, so it is worth locking down even where MoveObjects is Unrestricted.");
			Restrict_ToggleShipLights = config.Bind<PermissionLevel>("AntiGriefSetting", "ToggleShipLights", PermissionLevel.GameStart, "Restricts who can toggle the ship lights.");
			Restrict_PullShipLever = config.Bind<int>("AntiGriefSetting", "PullShipLever", 0, "Earliest in-game hour players may pull the ship lever. 0 means no hour restriction.");
			AntiGrief_MaxViolations = config.Bind<int>("AntiGriefSetting", "MaxViolations", 5, "Blocked actions a player may rack up in a round before the punishment below applies. 0 turns punishment off - actions are still blocked and still warned, they just never lead to a kick or ban.");
			AntiGrief_Punishment = config.Bind<PunishmentType>("AntiGriefSetting", "Punishment", PunishmentType.Kick, "What happens to a player who reaches MaxViolations. Disable and OnlyLog both leave the player alone - actions are still blocked either way.");
			AntiGrief_KickReason = config.Bind<string>("AntiGriefSetting", "KickReason", "You were kicked by the Anti-Grief System", "Disconnect reason shown to a player kicked by the Anti-Grief system.");
			AntiGrief_BanReason = config.Bind<string>("AntiGriefSetting", "BanReason", "You were banned by the Anti-Grief System", "Disconnect reason shown to a player banned by the Anti-Grief system.");
			AntiGrief_MessageColor = config.Bind<string>("AntiGriefSetting", "MessageColor", "red", "TextMeshPro color name or hex code for chat fallback when a blocked player does not have Overlay or is not registered with its network alerts.");
			Profiles_Data = config.Bind<string>("!Profiles", "Profiles", "", "Written automatically by the Profiles window in the menu. One profile per entry as name|bypassAllChecks|antiGriefRuleKeys|checkKeys, separated by semicolons. A profile is a named set of exemptions and grants nothing until it is assigned to somebody in Assignments below.");
			Profiles_Assignments = config.Bind<string>("!Profiles", "Assignments", "", "Written automatically - which Steam ID carries which profile, as steamId=profileName separated by semicolons. Host-side only: nothing about a profile is sent to the player it applies to, and a player with no assignment here is subject to every rule and every check as normal.");
			UseRate_ShipLightCooldown = config.Bind<int>("UseRateSetting", "ShipLightCooldown", 2000, localizationManager.Cfg_GetString("Cooldown"));
			UseRate_TerminalNoiseCooldown = config.Bind<int>("UseRateSetting", "TerminalNoiseCooldown", 1000, localizationManager.Cfg_GetString("Cooldown"));
			UseRate_ChatCooldown = config.Bind<int>("UseRateSetting", "ChatCooldown", 100, localizationManager.Cfg_GetString("Cooldown"));
			GodMode_ReportDelay = config.Bind<int>("GodModeSetting", "ReportDelay", 1000, localizationManager.Cfg_GetString("RPCReport_Delay"));
			GodMode_WatchHitRpc = config.Bind<bool>("GodModeSetting", "WatchHitRpc", true, localizationManager.Cfg_GetString("RPCReport_Hit"));
			GodMode_WatchKillPlayerRpc = config.Bind<bool>("GodModeSetting", "WatchKillPlayerRpc", true, localizationManager.Cfg_GetString("RPCReport_KillPlayer"));
			OperationLog_MessageType = config.Bind<MessageType>("LogSetting", "OperationLogType", MessageType.Menu, "Where the operation log goes - who bought what, who voted, who took the apparatus. Menu = the log window in the in-game menu only, HostChat = the host's chat as it used to be, PublicChat = everyone's chat, GUI = the host's tip panel. Everything is written to AntiCheat.log either way.");
			Lobby_AllowLateJoin = config.Bind<bool>("LobbySetting", "AllowLateJoin", false, "Keeps the Steam lobby alive when the ship leaves orbit, so players can still join once the round is underway. Vanilla does not close the lobby at take-off, it leaves it for good, so this is the switch that decides whether the lobby exists at all for the rest of the session. Host-side: a joining player needs no mod. This is the starting value only - the Lobby window in the menu turns it on and off per session, in orbit.");
			Lobby_LateJoinSyncTimeout = config.Bind<float>("LobbySetting", "LateJoinSyncTimeoutSeconds", 90f, "How long to wait for a mid-round joiner to finish building the interior before lifting their loading screen anyway. They build it from the seed the host sends them, which on a large moon on a slow machine is not quick; the wait ends the moment they report it done, so this only matters when something has gone wrong.");
			Lobby_AnnounceLateJoin = config.Bind<bool>("LobbySetting", "AnnounceLateJoin", true, "Posts a chat line to everybody when a player joins mid-round. The normal join message is not shown for these, since the game itself only announces players who arrive in orbit.");
			Menu_Enable = config.Bind<bool>("MenuSetting", "Enable", true, "Whether this mod registers its windows with Overlay. Off means AntiCheat has no interface but goes on working exactly as configured - every check, every rule and every ban is host-side and none of them need a window open. The key that opens the dock, its colours and its diagnostics are Overlay's own settings, in Overlay.cfg, because they are shared by every mod that draws into it.");
			Menu_MaxLogLines = config.Bind<int>("MenuSetting", "MaxLogLines", 1000, "How many log lines the menu keeps in memory. Older lines scroll off the top of the window; AntiCheat.log on disk still has all of them.");
			Menu_MaxRecentPlayers = config.Bind<int>("MenuSetting", "MaxRecentPlayers", 50, "How many past players the Recent Players window remembers. Players currently in the lobby are never dropped.");
			PlayerJoin = config.Bind<string>("MsgSettings", "PlayerJoinShip", localizationManager.Msg_GetString("wlc_player"), "Chat message posted when a player joins, with {player} replaced by their name. Leave it empty to post nothing at all.");
			PlayerJoin_Color = config.Bind<string>("MsgSettings", "PlayerJoinShipColor", "", "TextMeshPro color name or hex code (for example: yellow, #FF8800) for the join message. Leave it empty to keep the default chat color. Applies to whichever display PlayerJoinShipType selects.");
			PlayerJoin_MessageType = config.Bind<MessageType>("MsgSettings", "PlayerJoinShipType", MessageType.PublicChat, "Where the join message is shown. PublicChat = in chat for everybody in the lobby. HostChat = in chat for the host only. GUI = the host's own tip panel (the box that shows in-game hints), host only - the game has no way to put a tip on an unmodded client's screen, so this cannot be shown to everyone.");
			Disclaimer_Enable = config.Bind<bool>("DisclaimerSetting", "Enable", true, "Posts the server's house rules to chat - what this lobby has changed, and what it has not. Host-side and sent over vanilla chat, so no client needs the mod to read it.");
			Disclaimer_OnJoin = config.Bind<bool>("DisclaimerSetting", "PostOnJoin", true, "Posts the disclaimer automatically a few seconds after a player joins. Turn this off to leave it as an on-demand command only.");
			Disclaimer_Text = config.Bind<string>("DisclaimerSetting", "Text", "<color=yellow>- Self Made Mods</color>|<color=yellow>- Dying increases quota</color>|<color=yellow>- Randomized moons</color>|<color=yellow>- Enemy AI changes</color>|<color=yellow>- Scrap rework</color>|<color=yellow>- etc...</color>|<color=red>NO usage of Control nor Brutal Company.</color>", "The disclaimer itself. One chat line per \"|\" - a config value cannot hold a real newline. TextMeshPro colour tags work: <color=yellow>text</color>, <color=red>text</color>, or a hex code. Leave the whole thing empty to post nothing.");
			Disclaimer_Aliases = config.Bind<string>("DisclaimerSetting", "Commands", "disclaimer,desclaimer,disclamer,rules,info", "Comma-separated chat commands that post the disclaimer, without the leading slash. Anybody in the lobby can type one; a leading \"/\" or \"!\" is accepted either way. Several spellings are listed by default because a command nobody can spell is a disclaimer nobody reads.");
			Disclaimer_MessageType = config.Bind<MessageType>("DisclaimerSetting", "MessageType", MessageType.PublicChat, "Where the disclaimer is shown. PublicChat = chat for everybody in the lobby, which is the only setting the players it is written for will ever see. HostChat = the host's chat only. GUI = the host's tip panel. Menu = the mod's own log window.");
			Disclaimer_JoinDelaySeconds = config.Bind<float>("DisclaimerSetting", "JoinDelaySeconds", 4f, "How long after a player joins to post it. Not zero on purpose: the joining client is still loading at the moment it connects and its chat window is not up yet to catch the lines.");
			Disclaimer_CooldownSeconds = config.Bind<float>("DisclaimerSetting", "CommandCooldownSeconds", 15f, "How long the chat command is ignored for after the disclaimer has been posted, so a lobby that all asks at once gets one copy rather than four. A join always posts it regardless of this.");
			Shovel3 = config.Bind<bool>("ShovelSettings", "EmptyHand", false, localizationManager.Cfg_GetString("Shovel2"));
			GrabObject_SendLog = config.Bind<bool>("GrabObjectSetting", "SendLog", true, localizationManager.Cfg_GetString("GrabObject_SendLog"));
			GrabObject_MoreSlot = config.Bind<bool>("GrabObjectSetting", "MoreSlot", true, localizationManager.Cfg_GetString("GrabObject_MoreSlot"));
			GrabObject_TwoHand = config.Bind<bool>("GrabObjectSetting", "TwoHand", true, localizationManager.Cfg_GetString("GrabObject_TwoHand"));
			GrabObject_BeltBag = config.Bind<bool>("GrabObjectSetting", "BeltBag", true, localizationManager.Cfg_GetString("GrabObject_BeltBag"));
			GodMode_EnemyHitRequireAck = config.Bind<bool>("GodModeSetting", "EnemyHitRequireAcknowledgement", true, "Also requires the victim's client to answer each announced enemy hit - with a damage report, a critical-injury report or a death - instead of only adding the damage up until the total should have been lethal. Catches a damage-immunity mod on the first hit rather than the fourth.");
			GodMode_EnemyHitAckSeconds = config.Bind<float>("GodModeSetting", "EnemyHitAcknowledgementSeconds", 2f, "How long the victim's client has to answer an announced enemy hit before the silence is treated as suppressed damage. Covers the round trip, so raise it if a high-ping lobby produces false reports.");
			MovementDiagnostics = config.Bind<bool>("SpeedHackSetting", "Diagnostics", true, "Prints a periodic line showing whether the movement sampling hook is running and what it sees. Purely for working out why the movement check is or is not firing - turn it off once movement detection is confirmed working.");
			MaxPlausibleSpeed = config.Bind<float>("SpeedHackSetting", "MaxPlausibleSpeed", 30f, "Any position sample implying a speed above this is treated as a discontinuity - a teleport, an entrance warp, the ship moving underneath a player, a spawn - and is thrown away rather than judged. Vanilla sprint tops out around 13u/s, so anything past this is not movement at all, cheated or otherwise.");
			MovementGraceSeconds = config.Bind<float>("SpeedHackSetting", "GraceSeconds", 5f, "How long movement checks stay quiet after something that moves players without them walking: a round starting, the ship landing or leaving, a discontinuity in a player's position, or a player dying or respawning.");
			GodMode_EnemyHitLethalThreshold = config.Bind<int>("GodModeSetting", "EnemyHitLethalThreshold", 140, "Total accumulated enemy damage, counted host-side, at which a still-living player is flagged. Deliberately above 100: vanilla gives a one-time save where a single sub-50 hit that would kill instead leaves you on 5 HP, and healing exists, so the total has to clear real health by a margin before it can only mean immunity.");
			GodMode_InstakillGraceSeconds = config.Bind<float>("GodModeSetting", "InstakillGraceSeconds", 5f, "How long to wait after an instakill enemy commits to killing a player before checking whether they actually died. Needs to comfortably outlast the kill animation and body spawn (vanilla itself gives the body 2 seconds) so a slow but legitimate death is never flagged.");
			SpeedHack_MaxMultiplier = config.Bind<float>("SpeedHackSetting", "MaxMultiplier", 2.6f, "Highest sustained speed, as a multiple of a player's own recent baseline, before it is flagged. Vanilla's own sprint multiplier tops out at 2.25x, so this has headroom built in for network jitter.");
			SpeedHack_MinAbsoluteSpeed = config.Bind<float>("SpeedHackSetting", "MinAbsoluteSpeed", 16f, "Speed, in units per second, below which the multiplier above is ignored entirely. A player who has been standing still has a baseline near zero, so simply starting to sprint clears any multiple - this stops that being a flag by requiring a speed no unmodded player reaches (vanilla sprin