Decompiled source of ValheimEnforcer v0.34.0

plugins/ValheimEnforcer.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Jotunn;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using Mono.Cecil;
using Mono.Collections.Generic;
using Newtonsoft.Json.Linq;
using Splatform;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Profiling;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using ValheimEnforcer;
using ValheimEnforcer.common;
using ValheimEnforcer.modules;
using ValheimEnforcer.modules.archive;
using ValheimEnforcer.modules.audit;
using ValheimEnforcer.modules.character;
using ValheimEnforcer.modules.cheatmonitor;
using ValheimEnforcer.modules.compat;
using ValheimEnforcer.modules.compat.ExtraSlots;
using ValheimEnforcer.modules.migration;
using ValheimEnforcer.modules.mods;
using ValheimEnforcer.modules.modsync;
using ValheimEnforcer.modules.network;
using ValheimEnforcer.modules.notifications;
using ValheimEnforcer.modules.recovery;
using ValheimEnforcer.modules.worldintegrity;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("ValheimEnforcer")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ValheimEnforcer")]
[assembly: AssemblyCopyright("Copyright ©  2021")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("e3243d22-4307-4008-ba36-9f326008cde5")]
[assembly: AssemblyFileVersion("0.34.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.34.0.0")]
internal class DeltaChangeTracker : MonoBehaviour
{
	public void Update()
	{
		if (!CharacterDeltaTracker.BaselineDirty || Time.unscaledTime < CharacterDeltaTracker.DirtySince + 2f || Time.unscaledTime < CharacterDeltaTracker.LastDeltaSyncTime || CharacterManager.PlayerCharacter == null || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)ZNet.instance == (Object)null)
		{
			return;
		}
		CharacterDeltaTracker.LastDeltaSyncTime = Time.unscaledTime + (float)ValConfig.DeltaSynchronizationFrequencyInSeconds.Value;
		CharacterDeltaTracker.ClearDirty();
		StallWatch stallWatch = StallWatch.Start("Character delta flush");
		try
		{
			SyncChangesToServer();
		}
		finally
		{
			stallWatch.Stop();
		}
	}

	private static void SyncChangesToServer()
	{
		//IL_0381: Unknown result type (might be due to invalid IL or missing references)
		//IL_0388: Expected O, but got Unknown
		Logger.LogDebug("Checking for character changes to sync to server...");
		List<DataObjects.PackedItem> list = CharacterDeltaTracker.PackCurrentInventory();
		List<DataObjects.ItemDelta> list2 = CharacterDeltaTracker.BuildCharacterItemDeltas(list);
		Dictionary<string, string> dictionary = Player.m_localPlayer.m_customData ?? new Dictionary<string, string>();
		Dictionary<string, string> dictionary2 = CharacterManager.PlayerCharacter.PlayerCustomData ?? new Dictionary<string, string>();
		Dictionary<string, string> dictionary3 = new Dictionary<string, string>();
		List<string> list3 = new List<string>();
		foreach (KeyValuePair<string, string> item in dictionary)
		{
			if (CompatCustomData.IsPassthroughPlayerKey(item.Key))
			{
				continue;
			}
			if (dictionary2.ContainsKey(item.Key))
			{
				if (dictionary2[item.Key] != item.Value)
				{
					dictionary3.Add(item.Key, item.Value);
				}
			}
			else
			{
				dictionary3.Add(item.Key, item.Value);
			}
		}
		foreach (KeyValuePair<string, string> item2 in dictionary2)
		{
			if (!CompatCustomData.IsPassthroughPlayerKey(item2.Key) && !dictionary.ContainsKey(item2.Key))
			{
				list3.Add(item2.Key);
			}
		}
		string text = ForsakenPower.Capture(Player.m_localPlayer);
		bool flag = text != null && text != CharacterManager.PlayerCharacter.GuardianPower;
		List<DataObjects.PackedFood> list4 = FoodSync.Capture(Player.m_localPlayer);
		bool flag2 = FoodSync.ChangedSince(list4, CharacterManager.PlayerCharacter.Foods);
		if (list2.Count == 0 && dictionary3.Count == 0 && list3.Count == 0 && !flag && !flag2)
		{
			return;
		}
		Logger.LogDebug("Changes found, syncing deltas.");
		CharacterManager.PlayerCharacter.PlayerItems = list;
		CharacterManager.PlayerCharacter.PlayerCustomData = CompatCustomData.SnapshotForTracking(dictionary);
		Dictionary<SkillType, float> skillLevels = ((Character)Player.m_localPlayer).GetSkills().GetSkillList().ToDictionary((Skill s) => s.m_info.m_skill, (Skill s) => s.m_level);
		CharacterManager.PlayerCharacter.SkillLevels = skillLevels;
		CharacterManager.PlayerCharacter.GuardianPower = text;
		CharacterManager.PlayerCharacter.Foods = list4;
		Dictionary<string, DataObjects.PackedStatusEffect> dictionary4 = new Dictionary<string, DataObjects.PackedStatusEffect>();
		foreach (StatusEffect statusEffect in ((Character)Player.m_localPlayer).GetSEMan().GetStatusEffects())
		{
			dictionary4.Add(((Object)statusEffect).name, new DataObjects.PackedStatusEffect(statusEffect));
		}
		ZNetPeer serverPeer = ZNet.instance.GetServerPeer();
		if (serverPeer == null)
		{
			CharacterManager.PlayerCharacter.LastDisconnect = DataObjects.DisconnectionState.DirtyDisconnect;
			ValConfig.WritePlayerCharacterToSave(CharacterManager.PlayerCharacter.HostID, CharacterManager.PlayerCharacter, routine: true);
			Logger.LogDebug($"Baseline refresh written locally: {list.Count} items.");
			return;
		}
		DataObjects.DeltaSummaryUpdate deltaSummaryUpdate = new DataObjects.DeltaSummaryUpdate
		{
			Name = CharacterManager.PlayerCharacter.Name,
			HostID = CharacterManager.PlayerCharacter.HostID,
			PlayerID = (((Object)(object)Player.m_localPlayer != (Object)null) ? Player.m_localPlayer.GetPlayerID() : 0),
			DisconnectionState = DataObjects.DisconnectionState.DirtyDisconnect,
			ItemModifications = list2,
			SkillLevels = skillLevels,
			PlayerCustomDataModifications = dictionary3,
			RemovedCustomDataKeys = list3,
			ActiveCharacterEffects = dictionary4,
			GuardianPower = text,
			Foods = list4
		};
		if (DeltaWire.ServerAcceptsBinary)
		{
			ValConfig.ItemDeltaBinaryRPC.SendPackage(serverPeer.m_uid, DeltaWire.Write(deltaSummaryUpdate));
		}
		else
		{
			ZPackage val = new ZPackage();
			val.Write(DataObjects.yamlserializer.Serialize((object)deltaSummaryUpdate));
			ValConfig.ItemDeltaUpdateRPC.SendPackage(serverPeer.m_uid, val);
		}
		Logger.LogDebug($"Delta flush: {list2.Count} items, {dictionary3.Count} ({list3.Count} removed) custom data changes. Skill levels updated.");
	}
}
namespace ValheimEnforcer
{
	internal class ValConfig
	{
		private enum CharacterPayloadOutcome
		{
			Character,
			Sanitized,
			CatchUp,
			NoCharacter,
			Unreadable
		}

		private struct IncomingCharacter
		{
			internal CharacterPayloadOutcome Outcome;

			internal DataObjects.Character Character;

			internal DataObjects.Loadout GrantedLoadout;
		}

		public static ConfigFile cfg;

		public static ConfigEntry<bool> EnableDebugMode;

		public static ConfigEntry<bool> EnableTerminalColors;

		public static ConfigEntry<bool> AllowPublicDiagnosticCommands;

		public static ConfigEntry<bool> UpdateLoadedModsOnStartup;

		public static ConfigEntry<bool> AutoAddModsToRequired;

		public static ConfigEntry<bool> RemoveUnloadedModsFromRequired;

		public static ConfigEntry<bool> ModValidationExemptAdmins;

		public static ConfigEntry<string> HashEnforcement;

		public static ConfigEntry<bool> RecordHashesForLoadedMods;

		public static ConfigEntry<bool> ValidatePatchers;

		public static ConfigEntry<bool> AutoAddPatchersToAllowed;

		public static ConfigEntry<string> AttestationPolicy;

		public static ConfigEntry<bool> ResolveThunderstoreHashes;

		public static ConfigEntry<bool> OfferModDownloads;

		public static ConfigEntry<bool> AllowServerModInstalls;

		public static ConfigEntry<bool> AutoFillThunderstorePackages;

		public static ConfigEntry<int> HashComputeTimeoutSeconds;

		public static ConfigEntry<int> ThunderstoreMaxArchiveMB;

		public static ConfigEntry<bool> RemoveNontrackedItemsFromJoiningPlayers;

		public static ConfigEntry<bool> AddMissingItemsFromPlayerServerSave;

		public static ConfigEntry<bool> RestoreSkillsFromPlayerServerSave;

		public static ConfigEntry<bool> PreventExternalSkillRaises;

		public static ConfigEntry<bool> NewCharactersRemoveExtraItems;

		public static ConfigEntry<bool> NewCharacterSetSkillsToZero;

		public static ConfigEntry<bool> newCharacterClearCustomData;

		public static ConfigEntry<bool> PassthroughCompatModCustomData;

		public static ConfigEntry<string> NewCharacterStartingItems;

		public static ConfigEntry<bool> ConfiscateUnidentifiableItems;

		public static ConfigEntry<int> InitialCharacterSyncWaitSeconds;

		public static ConfigEntry<bool> PreventExternalCustomDataChanges;

		public static ConfigEntry<bool> ValidateItemCustomData;

		public static ConfigEntry<bool> ValidateItemDurability;

		public static ConfigEntry<float> ItemValidationDurabilityAllowedVariance;

		public static ConfigEntry<bool> SavePlayerStatusEffectsOnLogout;

		public static ConfigEntry<bool> ItemRemovalForDirtyReconnection;

		public static ConfigEntry<bool> ItemReturnForDirtyReconnection;

		public static ConfigEntry<bool> ServerSideJoinEnforcement;

		public static ConfigEntry<bool> CatchupOverwriteOnJoin;

		public static ConfigEntry<bool> PreventExternalForsakenPowerChanges;

		public static ConfigEntry<bool> NewCharacterClearForsakenPower;

		public static ConfigEntry<bool> NewCharacterResetMapExploration;

		public static ConfigEntry<bool> PreventExternalFoodChanges;

		public static ConfigEntry<bool> NewCharacterClearKnownRecipes;

		public static ConfigEntry<bool> NewCharacterClearKnownTexts;

		public static ConfigEntry<string> KnownTextPassthroughPrefixes;

		public static ConfigEntry<bool> EpicMMOKnownTextCompat;

		public static ConfigEntry<bool> RecordSkillReductions;

		public static ConfigEntry<bool> EnableProgressionSync;

		public static ConfigEntry<bool> SyncMapExploration;

		public static ConfigEntry<bool> SyncKnownItems;

		public static ConfigEntry<bool> SyncTrophies;

		public static ConfigEntry<bool> SyncPlayerStats;

		public static ConfigEntry<bool> SyncSpawnPoint;

		public static ConfigEntry<int> MapSyncIntervalMinutes;

		public static ConfigEntry<bool> NewCharacterClearPlayerStats;

		public static ConfigEntry<bool> EnableStarterLoadouts;

		public static ConfigEntry<string> DefaultStarterLoadout;

		public static ConfigEntry<bool> EnableSaveArchives;

		public static ConfigEntry<int> SaveArchiveIntervalMinutes;

		public static ConfigEntry<int> SaveArchiveKeepCount;

		public static ConfigEntry<int> SaveArchiveMaxTotalMB;

		public static ConfigEntry<bool> SaveArchiveIncludeCharacters;

		public static ConfigEntry<bool> SaveArchiveIncludeConfig;

		public static ConfigEntry<string> SaveArchiveCompression;

		public static ConfigEntry<string> SaveArchivePath;

		public static ConfigEntry<bool> EnableCrashRecovery;

		public static ConfigEntry<bool> CrashRecoveryAutoRestore;

		public static ConfigEntry<int> CrashRecoveryWindowMinutes;

		public static ConfigEntry<int> CrashRecoveryPushIntervalMinutes;

		public static ConfigEntry<int> CrashRecoveryKeepBlobs;

		public static ConfigEntry<bool> EnforceCharacterLimit;

		public static ConfigEntry<int> MaxCharactersPerAccount;

		public static ConfigEntry<string> CharacterLimitExemptAccounts;

		public static ConfigEntry<bool> CharacterLimitExemptAdmins;

		public static ConfigEntry<bool> ImportServerCharacters;

		public static ConfigEntry<string> ServerCharactersImportPath;

		public static ConfigEntry<bool> InternalStorageMode;

		public static ConfigEntry<int> ConfigPollIntervalSeconds;

		public static ConfigEntry<int> DeltaSynchronizationFrequencyInSeconds;

		public static ConfigEntry<int> FullSyncPullIntervalMinutes;

		public static ConfigEntry<int> FullSyncMaxConcurrentPlayers;

		public static ConfigEntry<bool> EnforceRoutedRpcSender;

		public static ConfigEntry<int> StallWarningThresholdMs;

		public static ConfigEntry<int> CharacterCacheIdleMinutes;

		public static ConfigEntry<int> MemoryReportIntervalMinutes;

		public static ConfigEntry<int> CharacterWriteIntervalSeconds;

		public static ConfigEntry<bool> FullSyncSpreadAcrossInterval;

		public static ConfigEntry<bool> FastCharacterWriter;

		public static ConfigEntry<bool> BackgroundCharacterWrites;

		public static ConfigEntry<bool> BinaryDeltaUpdates;

		public static ConfigEntry<bool> CaptureCustomDataOnRespawn;

		public static ConfigEntry<bool> EnableCheatDetection;

		public static ConfigEntry<bool> DetectCheatEngine;

		public static ConfigEntry<bool> DetectValheimTooler;

		public static ConfigEntry<bool> DetectInjectedCheatAssemblies;

		public static ConfigEntry<bool> DetectProxyLoaders;

		public static ConfigEntry<bool> DetectCheatTools;

		public static ConfigEntry<bool> DetectGenericTrainers;

		public static ConfigEntry<bool> ScanLoadedModules;

		public static ConfigEntry<bool> ScanWindowTitles;

		public static ConfigEntry<bool> ScanElevatedProcesses;

		public static ConfigEntry<string> AdditionalCheatProcesses;

		public static ConfigEntry<string> IgnoredCheatProcesses;

		public static ConfigEntry<string> AllowedProxyLoaderHashes;

		public static ConfigEntry<string> CheatDetectionAction;

		public static ConfigEntry<int> CheatScanIntervalSeconds;

		public static ConfigEntry<string> BannedProxyLoaderHashes;

		public static ConfigEntry<string> BannedAssemblyNamespaces;

		public static ConfigEntry<string> BannedAssemblyMvids;

		public static ConfigEntry<bool> VerifyDoorstopTarget;

		public static ConfigEntry<bool> DetectUnlistedGameFolderDlls;

		public static ConfigEntry<bool> ReportUnaccountedCode;

		public static ConfigEntry<bool> DetectAntiCheatTampering;

		public static ConfigEntry<bool> DetectCheatToolArtifacts;

		public static ConfigEntry<bool> CheatScanHeartbeat;

		public static ConfigEntry<int> CheatHeartbeatIntervalMinutes;

		public static ConfigEntry<int> CheatHeartbeatTimeoutSeconds;

		public static ConfigEntry<int> CheatHeartbeatMissesAllowed;

		public static ConfigEntry<bool> EnableStructureValidation;

		public static ConfigEntry<bool> DetectNonBuildableStructures;

		public static ConfigEntry<bool> BlockSpawnObjectRPC;

		public static ConfigEntry<bool> DetectExcessiveStructureHealth;

		public static ConfigEntry<float> StructureHealthAllowedMultiplier;

		public static ConfigEntry<string> StructureValidationAction;

		public static ConfigEntry<bool> RemoveDetectedStructures;

		public static ConfigEntry<bool> StructureValidationExemptAdmins;

		public static ConfigEntry<string> IgnoredStructurePrefabs;

		public static ConfigEntry<bool> DetectItemOrigins;

		public static ConfigEntry<bool> DetectUncraftedEquipment;

		public static ConfigEntry<bool> DetectUnknownCrafterIds;

		public static ConfigEntry<bool> ItemOriginExemptAdmins;

		public static ConfigEntry<string> IgnoredItemOriginPrefabs;

		public static ConfigEntry<bool> DetectInventoryGrid;

		public static ConfigEntry<bool> InventoryGridExemptAdmins;

		public static ConfigEntry<int> MaxInventoryWidth;

		public static ConfigEntry<int> MaxInventoryHeight;

		public static ConfigEntry<bool> EnableRpcGuards;

		public static ConfigEntry<bool> GuardChatSenderName;

		public static ConfigEntry<bool> GuardPlayerTeleportRpc;

		public static ConfigEntry<bool> GuardZdoDestruction;

		public static ConfigEntry<int> MaxZdoDestroysPerPacket;

		public static ConfigEntry<float> ZdoDestroyProximityMetres;

		public static ConfigEntry<bool> GuardDamageRpc;

		public static ConfigEntry<float> MaxAllowedHitDamage;

		public static ConfigEntry<bool> GuardPvpDamage;

		public static ConfigEntry<bool> GuardGlobalKeys;

		public static ConfigEntry<bool> BlockClientGlobalKeyRemoval;

		public static ConfigEntry<string> AllowedClientGlobalKeys;

		public static ConfigEntry<bool> RpcGuardExemptAdmins;

		public static ConfigEntry<string> RpcGuardAction;

		public static ConfigEntry<bool> ReportClientContradictions;

		public static ConfigEntry<int> ContradictionThreshold;

		public static ConfigEntry<string> ContradictionAction;

		public static ConfigEntry<bool> EnableAuditLog;

		public static ConfigEntry<int> AuditRetentionDays;

		public static ConfigEntry<int> AuditFlushIntervalSeconds;

		public static ConfigEntry<int> AuditDamageWindowSeconds;

		public static ConfigEntry<float> AuditHighDamageThreshold;

		public static ConfigEntry<float> AuditHighDamagePerWindow;

		public static ConfigEntry<int> AuditContainerTrackingLimit;

		public static ConfigEntry<int> AuditMaxDownloadDays;

		public static ConfigEntry<bool> AuditExemptAdmins;

		public static ConfigEntry<string> DiscordWebhookUrl;

		public static ConfigEntry<string> DiscordWebhookUrlPlayerActivity;

		public static ConfigEntry<string> DiscordWebhookUrlServerStatus;

		public static ConfigEntry<string> DiscordWebhookUrlModeration;

		public static ConfigEntry<string> DiscordWebhookUrlModMismatch;

		public static ConfigEntry<string> DiscordServerLabel;

		public static ConfigEntry<string> DiscordProxyUrl;

		public static ConfigEntry<bool> DiscordNotifyServerStartup;

		public static ConfigEntry<bool> DiscordNotifyServerShutdown;

		public static ConfigEntry<bool> DiscordNotifyWorldSaved;

		public static ConfigEntry<bool> DiscordNotifyPlayerJoined;

		public static ConfigEntry<bool> DiscordNotifyPlayerLeft;

		public static ConfigEntry<bool> DiscordNotifyWrongMods;

		public static ConfigEntry<bool> DiscordNotifyCheaterBanned;

		public static ConfigEntry<bool> DiscordNotifyCharacterRejected;

		public static ConfigEntry<bool> DiscordNotifyStructureFlagged;

		public static ConfigEntry<bool> DiscordNotifyClientContradiction;

		public static ConfigEntry<bool> DiscordNotifyItemOrigin;

		public static ConfigEntry<bool> DiscordNotifyCheatSuspected;

		internal const string ModsFileName = "Mods.yaml";

		internal const string ServerActiveModsFileName = "ServerActiveMods.yaml";

		internal const string ValheimEnforcer = "ValheimEnforcer";

		internal const string CharacterFolder = "Characters";

		internal const string KnownCheatersFileName = "KnownCheaters.yaml";

		internal const string NotificationsFileName = "Notifications.yaml";

		internal static string ModsConfigFilePath = Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Mods.yaml");

		internal static string ServerActiveModsFilePath = Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "ServerActiveMods.yaml");

		internal static string CharacterFilePath = Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters");

		internal static string KnownCheatersFilePath = Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "KnownCheaters.yaml");

		internal static string NotificationsFilePath = Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Notifications.yaml");

		internal static CustomRPC CharacterSaveRPC;

		internal static CustomRPC CheatDetectionRPC;

		internal static CustomRPC CheatHeartbeatRPC;

		internal static CustomRPC ItemDeltaUpdateRPC;

		internal static CustomRPC ItemDeltaBinaryRPC;

		internal static CustomRPC FullSyncRequestRPC;

		internal static CustomRPC ReturnConfiscatedItemsRPC;

		internal static CustomRPC ClearConfiscatedRPC;

		internal static CustomRPC SkillRestoreRPC;

		internal static CustomRPC ClientCommandRequestRPC;

		internal static CustomRPC CommandOutputRPC;

		internal static CustomRPC MapSyncRPC;

		internal static CustomRPC MapRequestRPC;

		internal static CustomRPC RecoveryRPC;

		internal static CustomRPC RecoveryOfferRPC;

		internal static CustomRPC AuditRequestRPC;

		internal static CustomRPC AuditDataRPC;

		internal static readonly TimeSpan LoginFlushBound = TimeSpan.FromMilliseconds(750.0);

		internal const int MaxCharacterPayloadBytes = 8388608;

		internal const int MaxMapPayloadBytes = 4194304;

		internal const int MaxMapRequestBytes = 1024;

		internal const int MaxDeltaPayloadBytes = 1048576;

		internal const int MaxCheatReportBytes = 65536;

		internal const int MaxModListBytes = 524288;

		internal const int MaxDeclaredModEntries = 4096;

		internal const int MaxCommandArgs = 32;

		internal const int MaxAuditRequestBytes = 4096;

		internal const int MaxAuditDownloadBytes = 16777216;

		internal const string MapPayloadMap = "MAP";

		internal const string MapPayloadNone = "NONE";

		internal const string RecoveryPayloadBlob = "BLOB";

		internal const string RecoveryPayloadWant = "WANT";

		private const int MaxCheatDetections = 32;

		private const int MaxCheatFieldLength = 256;

		private const int MaxUnaccountedEntries = 16;

		private static readonly HashSet<string> loggedUnaccounted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		private const int MaxLoggedUnaccounted = 4096;

		private const int MaxCommandOutputLines = 256;

		private const double DriftResyncCooldownSeconds = 60.0;

		private static readonly ConcurrentDictionary<string, DateTime> lastDriftResync = new ConcurrentDictionary<string, DateTime>();

		internal const string CharPayloadCharacter = "CHAR";

		internal const string CharPayloadNone = "NONE";

		internal const string CharPayloadSanitized = "SANITIZED";

		internal const string CharPayloadCatchUp = "CATCHUP";

		internal static int DriftResyncTrackedCount => lastDriftResync.Count;

		public ValConfig(ConfigFile cf)
		{
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Expected O, but got Unknown
			//IL_005e: Expected O, but got Unknown
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_008a: Expected O, but got Unknown
			//IL_008a: Expected O, but got Unknown
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Expected O, but got Unknown
			//IL_00b6: Expected O, but got Unknown
			//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Expected O, but got Unknown
			//IL_00e2: Expected O, but got Unknown
			//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
			//IL_0104: Unknown result type (might be due to invalid IL or missing references)
			//IL_010e: Expected O, but got Unknown
			//IL_010e: Expected O, but got Unknown
			//IL_0124: 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)
			//IL_013a: Expected O, but got Unknown
			//IL_013a: Expected O, but got Unknown
			//IL_0150: Unknown result type (might be due to invalid IL or missing references)
			//IL_015c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0166: Expected O, but got Unknown
			//IL_0166: Expected O, but got Unknown
			//IL_017c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0188: Unknown result type (might be due to invalid IL or missing references)
			//IL_0192: Expected O, but got Unknown
			//IL_0192: Expected O, but got Unknown
			//IL_01a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01be: Expected O, but got Unknown
			//IL_01be: Expected O, but got Unknown
			//IL_01d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ea: Expected O, but got Unknown
			//IL_01ea: Expected O, but got Unknown
			//IL_0200: Unknown result type (might be due to invalid IL or missing references)
			//IL_020c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0216: Expected O, but got Unknown
			//IL_0216: Expected O, but got Unknown
			//IL_022c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0238: Unknown result type (might be due to invalid IL or missing references)
			//IL_0242: Expected O, but got Unknown
			//IL_0242: Expected O, but got Unknown
			//IL_0258: Unknown result type (might be due to invalid IL or missing references)
			//IL_0264: Unknown result type (might be due to invalid IL or missing references)
			//IL_026e: Expected O, but got Unknown
			//IL_026e: Expected O, but got Unknown
			//IL_0284: Unknown result type (might be due to invalid IL or missing references)
			//IL_0290: Unknown result type (might be due to invalid IL or missing references)
			//IL_029a: Expected O, but got Unknown
			//IL_029a: Expected O, but got Unknown
			//IL_02b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_02bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c6: Expected O, but got Unknown
			//IL_02c6: Expected O, but got Unknown
			//IL_02dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_02e8: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f2: Expected O, but got Unknown
			//IL_02f2: Expected O, but got Unknown
			//IL_0308: Unknown result type (might be due to invalid IL or missing references)
			//IL_0314: Unknown result type (might be due to invalid IL or missing references)
			//IL_031e: Expected O, but got Unknown
			//IL_031e: Expected O, but got Unknown
			cfg = cf;
			cfg.SaveOnConfigSet = true;
			CreateConfigValues(cf);
			Logger.SetDebugLogging(EnableDebugMode.Value);
			ConfigFileWatcher.Initialize();
			SetupMainFileWatcher();
			CharacterSaveRPC = NetworkManager.Instance.AddRPC("VENFORCE_CHAR", new CoroutineHandler(OnServerRecieveCharacter), new CoroutineHandler(OnClientReceiveCharacter));
			ReturnConfiscatedItemsRPC = NetworkManager.Instance.AddRPC("VENFORCE_RETURN_CONFISCATED", new CoroutineHandler(NoServerHandler), new CoroutineHandler(OnClientReceiveConfiscatedItems));
			CheatDetectionRPC = NetworkManager.Instance.AddRPC("VENFORCE_CHEAT", new CoroutineHandler(OnServerReceiveCheatReport), new CoroutineHandler(OnClientReceiveCheatReport));
			CheatHeartbeatRPC = NetworkManager.Instance.AddRPC("VENFORCE_CHEAT_HB", new CoroutineHandler(CheatHeartbeat.OnServerReceive), new CoroutineHandler(CheatHeartbeat.OnClientReceive));
			ItemDeltaUpdateRPC = NetworkManager.Instance.AddRPC("VENFORCE_ITEMDELTA", new CoroutineHandler(OnServerRecieveDeltaItemUpdate), new CoroutineHandler(OnClientReceiveDeltaItemUpdate));
			ItemDeltaBinaryRPC = NetworkManager.Instance.AddRPC("VENFORCE_ITEMDELTA_BIN", new CoroutineHandler(OnServerReceiveBinaryDelta), new CoroutineHandler(NoClientHandler));
			ClearConfiscatedRPC = NetworkManager.Instance.AddRPC("VENFORCE_CLEAR_CONFISCATED", new CoroutineHandler(NoServerHandler), new CoroutineHandler(OnClientReceiveClearConfiscated));
			SkillRestoreRPC = NetworkManager.Instance.AddRPC("VENFORCE_SKILL_RESTORE", new CoroutineHandler(NoServerHandler), new CoroutineHandler(OnClientReceiveSkillRestore));
			FullSyncRequestRPC = NetworkManager.Instance.AddRPC("VENFORCE_FULLSYNC_REQ", new CoroutineHandler(OnServerReceiveFullSyncRequest), new CoroutineHandler(OnClientReceiveFullSyncRequest));
			ClientCommandRequestRPC = NetworkManager.Instance.AddRPC("VENFORCE_CMD_REQ", new CoroutineHandler(OnServerReceiveCommandRequest), new CoroutineHandler(NoClientHandler));
			CommandOutputRPC = NetworkManager.Instance.AddRPC("VENFORCE_CMD_OUT", new CoroutineHandler(NoServerHandler), new CoroutineHandler(OnClientReceiveCommandOutput));
			MapSyncRPC = NetworkManager.Instance.AddRPC("VENFORCE_MAP", new CoroutineHandler(OnServerReceiveMap), new CoroutineHandler(OnClientReceiveMap));
			MapRequestRPC = NetworkManager.Instance.AddRPC("VENFORCE_MAP_REQ", new CoroutineHandler(OnServerReceiveMapRequest), new CoroutineHandler(NoClientHandler));
			RecoveryRPC = NetworkManager.Instance.AddRPC("VENFORCE_RECOVERY", new CoroutineHandler(NoServerHandler), new CoroutineHandler(OnClientReceiveRecovery));
			RecoveryOfferRPC = NetworkManager.Instance.AddRPC("VENFORCE_RECOVERY_OFFER", new CoroutineHandler(OnServerReceiveRecoveryOffer), new CoroutineHandler(NoClientHandler));
			AuditRequestRPC = NetworkManager.Instance.AddRPC("VENFORCE_AUDIT_REQ", new CoroutineHandler(OnServerReceiveAuditRequest), new CoroutineHandler(NoClientHandler));
			AuditDataRPC = NetworkManager.Instance.AddRPC("VENFORCE_AUDIT_OUT", new CoroutineHandler(NoServerHandler), new CoroutineHandler(OnClientReceiveAuditData));
			SynchronizationManager.Instance.AddInitialSynchronization(CharacterSaveRPC, (Func<ZNetPeer, ZPackage>)SendSavedCharacter);
			ModManager.DeleteActiveModsFile();
			LoadYamlConfigs(new Dictionary<string, Action<string>>
			{
				{ ModsConfigFilePath, CreateModsFile },
				{ KnownCheatersFilePath, CreateKnownCheatersFile },
				{ NotificationsFilePath, CreateNotificationsFile },
				{
					StarterLoadouts.FilePath,
					CreateLoadoutsFile
				}
			});
			StarterLoadouts.Initialize();
			KnownCheaterTracker.Initialize();
			KnownPlayerIds.Initialize();
			NotificationTemplates.Initialize();
		}

		private void CreateConfigValues(ConfigFile Config)
		{
			//IL_001a: 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_002c: Expected O, but got Unknown
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Expected O, but got Unknown
			EnableDebugMode = Config.Bind<bool>("Client config", "EnableDebugMode", false, new ConfigDescription("Enables Debug logging.", (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes
			{
				IsAdvanced = true
			} }));
			EnableDebugMode.SettingChanged += Logger.EnableDebugLogging;
			Logger.CheckEnableDebugLogging();
			EnableTerminalColors = BindLocalConfig("Client config", "EnableTerminalColors", value: true, "Colour the output of this mod's console commands by severity - green for a result, blue for detail lines, amber for a warning, red for a failure. Turn it off if your console theme makes the colours hard to read, or if you are copying output somewhere that would show the markup.");
			AllowServerModInstalls = BindLocalConfig("Client config", "AllowServerModInstalls", value: true, "Lets a server that turns you away for your mods offer to fix them. When a server with OfferModDownloads switched on refuses your connection, the connection error window gets a 'Fix mods' button. It shows you exactly what would change - which Thunderstore packages would be installed or updated, and which of your mods would be turned off - and nothing happens until you agree. Every package comes from thunderstore.io at the exact version the server pinned, and its files are checked against the hashes the server recorded before anything is installed; mods are turned off by renaming them to .old, never deleted, and anything replaced is moved to BepInEx/cache/ValheimEnforcer/modsync/replaced. Applying the changes closes Valheim, which then starts again and rejoins the server; a small helper program that ships inside this mod makes the changes once the game has closed. Windows, Linux (including Proton) and macOS. On by default: this only ever offers, and nothing happens without your say-so.");
			AllowPublicDiagnosticCommands = BindServerConfig("Advanced", "AllowPublicDiagnosticCommands", value: true, "If enabled (the default), the two commands that tell somebody nothing but what they could already work out - 'enforcer-help', which lists the command names, and 'enforcer-whoami', which reports what this server makes of the connection the caller is sitting on - run for any player rather than admins only. This exists because the person who most needs to ask 'why am I not an admin here?' is by definition the person the admin check is refusing, and with the answer locked behind that same check an operator whose id is misspelled in adminlist.txt has no way to find out from in-game. Neither command reads or changes anything belonging to the server or to another player, and enforcer-whoami never names another admin. Turn it off if you would rather players could not see the command list at all; every other command stays admin-only regardless.", null, advanced: true);
			UpdateLoadedModsOnStartup = BindServerConfig("Mods", "UpdateLoadedModsOnStartup", value: true, "Whether or not the mod configuration file will update its loaded mods once they are detected.");
			AutoAddModsToRequired = BindServerConfig("Mods", "AutoAddModsToRequired", value: true, "If true, automatically adds mods not found in the optional, admin, or server-only mod lists.");
			RemoveUnloadedModsFromRequired = BindServerConfig("Mods", "RemoveUnloadedModsFromRequired", value: true, "If enabled, any mod in requiredMods that this server does not have loaded is removed from the list at startup, so a mod taken off the server stops being demanded of every client without anyone editing Mods.yaml. Only requiredMods is touched: optionalMods, adminOnlyMods and serverOnlyMods routinely hold mods the server never runs, and are left alone. This also removes a mod you required by hand that the server does not run itself, including one pinned with a thunderstorePackage - put 'keepWhenUnloaded: true' on those entries and they are left alone without having to turn this off for every other mod. Needs UpdateLoadedModsOnStartup for the removal to reach disk. On by default.");
			ModValidationExemptAdmins = BindServerConfig("Mods", "ModValidationExemptAdmins", value: false, "If enabled, anyone on the server's adminlist may connect with any mods at all - missing required mods, mods this server does not allow, mismatched versions, modified mod files, unlisted BepInEx patchers, and a missing or wrong attestation. It also covers an admin running no ValheimEnforcer at all, who is otherwise refused for never sending a mod list. The check still runs and its result is still written to the server log, so you can see what your admin was carrying; only the rejection is skipped, and the Discord mod-mismatch notification is not sent for them. This exists so you can test a mod against the live server without editing Mods.yaml first. OFF by default, and be deliberate about turning it on: it makes a place on adminlist.txt the only thing between an account and the entire mod gate, so every admin id in that file needs to be somebody you would trust with an arbitrary client. Admin status is read from the server's own admin list against the connection the request arrived on, so no client can claim it.");
			HashEnforcement = BindServerConfig("Mods", "HashEnforcement", "WhenKnown", "Controls SHA256 file verification of client plugin DLLs during the connect handshake, which catches a mod somebody recompiled with different numbers in it even though its version string is unchanged. 'Off' never checks. 'WhenKnown' (the default) enforces only the mods this server has a recorded hash for, so verification is opt-in per mod and enabling it breaks nothing. 'Strict' additionally rejects any client carrying a Required or AdminOnly mod the server has NO recorded hash for - a deliberately loud signal that the mod list is not fully pinned. Individual mods override this with a 'hashEnforcement' field in Mods.yaml. Note this raises the bar from 'edit one file and rebuild' to 'reverse engineer and patch the enforcer'; it is not a wall.", new AcceptableValueList<string>(new string[3] { "Off", "WhenKnown", "Strict" }));
			AttestationPolicy = BindServerConfig("Mods", "AttestationPolicy", "Report", "Make each client prove its mod report was generated for THIS connection. The server hands every connection a random value during the handshake and the client returns a digest over that value and the exact mod and patcher list it is sending; the server recomputes it and compares. Without this, a client reports the same list and the same file hashes every session forever, so a client patched to skip the work can replay one captured answer indefinitely. Be clear on what a pass means: it proves the report was produced now, by code that actually ran - it does NOT prove the report is true, because a client that keeps the original DLLs and hashes those still passes. What it costs an attacker is the difference between returning a constant and keeping a working hashing path alive. Off does nothing at all. Report (the default) logs a failure and lets the player in. Require rejects them - and note that a player on an older ValheimEnforcer sends no attestation at all and is therefore rejected too, so only set Require once your pack has rolled out.", new AcceptableValueList<string>(new string[3] { "Off", "Report", "Require" }));
			ValidatePatchers = BindServerConfig("Mods", "ValidatePatchers", value: false, "Hold clients to a list of allowed BepInEx patchers, the same way they are held to a list of allowed mods. Patchers are the DLLs in BepInEx/patchers - not plugins. BepInEx loads them before any plugin exists and hands each one the game assemblies to rewrite on the way in, which is a more powerful position than any plugin has, and until now nothing here looked at that folder at all. The list is an allowlist, not a required list: a client with no patchers always passes, and only a patcher the server has not allowed is refused. Off by default. Patchers are enumerated and logged either way, so leave this off for a while first and read the log to see what your players actually carry.");
			AutoAddPatchersToAllowed = BindServerConfig("Mods", "AutoAddPatchersToAllowed", value: true, "Adds the patchers this server itself has to the allowedPatchers list in Mods.yaml at startup, with their file hashes, so a server running patchers of its own does not have to write them out by hand. Mirrors AutoAddModsToRequired. Hashes are only ever added, never replaced, so an entry an admin pinned by hand survives. Turn it off to curate the list yourself - patchers the server runs are then refused for clients like any other.");
			RecordHashesForLoadedMods = BindServerConfig("Mods", "RecordHashesForLoadedMods", value: true, "If enabled, the SHA256 of every plugin DLL loaded on this machine is recorded into Mods.yaml at startup, so the mods the server itself runs get pinned with no manual work. Hashes an admin pinned by hand, or that came from a thunderstorePackage, are never overwritten. Requires UpdateLoadedModsOnStartup for the result to reach disk.");
			ResolveThunderstoreHashes = BindServerConfig("Mods", "ResolveThunderstoreHashes", value: true, "If enabled, the server downloads any mod in Mods.yaml carrying a 'thunderstorePackage' field (format Owner-ModName or Owner-ModName-Version, the same format a Thunderstore manifest uses), hashes the DLLs inside the archive in memory, records them, and discards the download. This is how you pin a client-only mod the server never loads itself, and what lets OfferModDownloads offer one. Each package is downloaded once and re-downloaded only when its pinned version changes. Only thunderstore.io and its CDN are ever contacted; arbitrary download URLs are deliberately not supported. On by default; turn it off if this server should make no outbound requests, along with AutoFillThunderstorePackages.");
			OfferModDownloads = BindServerConfig("Mods", "OfferModDownloads", value: true, "If enabled, a player turned away for their mods is offered a fix. The server's mod list already goes to every connecting client; with this on it also tells the client it may install from it, and a player who has AllowServerModInstalls switched on in their own config gets a 'Fix mods' button in the connection error window. Only entries that can be pinned exactly are offered: a 'thunderstorePackage' with a version (or one ResolveThunderstoreHashes has resolved) AND recorded file hashes, so the client downloads exactly the build the server checks for and refuses anything that does not match. Mods with no package or no hash are listed as things the player has to fix by hand. Unlisted mods the player carries are offered to be turned off. Nothing is installed without the player confirming it, and only from thunderstore.io. On by default; AutoFillThunderstorePackages fills in the packages for the mods this server runs.");
			AutoFillThunderstorePackages = BindServerConfig("Mods", "AutoFillThunderstorePackages", value: true, "If enabled, a mod in Mods.yaml with no 'thunderstorePackage' gets one filled in, so it can be offered to players (OfferModDownloads) and resolved (ResolveThunderstoreHashes) without writing each one by hand. The server first reads the manifest.json of the package folder the mod is installed in - the BepInEx/plugins/Owner-Name folder r2modman and Gale create - which needs no network. Failing that it searches thunderstore.io by the mod's name and only accepts a result after downloading that exact version and finding the plugin in it; for a mod the server runs itself, the file has to be the very one the server runs. Filled-in entries are marked 'packageSource' and kept current when the mod is updated; a thunderstorePackage you wrote yourself is never touched, and deleting packageSource makes a filled-in one yours. Searches go to thunderstore.io only, once per mod that lacks a manifest, and the result is remembered in Mods.yaml. On by default.");
			RemoveNontrackedItemsFromJoiningPlayers = BindServerConfig("Player Sync", "RemoveNontrackedItemsFromJoiningPlayers", value: true, "If enabled, any items that are not tracked by the server will be removed from joining player's inventories.");
			AddMissingItemsFromPlayerServerSave = BindServerConfig("Player Sync", "AddMissingItemsFromPlayerServerSave", value: true, "If enabled, any items the player does not have that are listed on the server will be given to the player when joining");
			RestoreSkillsFromPlayerServerSave = BindServerConfig("Player Sync", "RestoreSkillsFromPlayerServerSave", value: true, "If enabled, a returning character whose skills are below the levels the server holds for them has them raised back to those levels when they join - the skill counterpart of AddMissingItemsFromPlayerServerSave. This is what brings a character back after its local save was deleted and the character recreated under the same name: the server still holds their progress, and without this the recreated character's blank skills would be uploaded as the new record on their first save. Never lowers a skill; PreventExternalSkillRaises covers the other direction. Skipped on a dirty reconnect unless ItemReturnForDirtyReconnection is on, for the same reason item restoration is: the stored copy can be a delta window stale, and the one way it can hold a higher skill than the player has is a death the server never heard about. On by default.");
			PreventExternalSkillRaises = BindServerConfig("Player Sync", "PreventExternalSkillRaises", value: true, "If enabled, player skill gains outside of the server are removed when connecting.");
			NewCharactersRemoveExtraItems = BindServerConfig("Player Sync", "NewCharactersRemoveExtraItems", value: false, "If enabled, new characters that have no existing character file will have all items removed except for starting items.");
			NewCharacterSetSkillsToZero = BindServerConfig("Player Sync", "NewCharacterSetSkillsToZero", value: false, "If enabled, new characters will have their skills set to zero. Prevents players from raising skills before connecting.");
			PreventExternalCustomDataChanges = BindServerConfig("Player Sync", "PreventExternalCustomDataChanges", value: true, "If enabled, tracks player custom data. Warning: custom data can be large and can impact how other mods function.");
			newCharacterClearCustomData = BindServerConfig("Player Sync", "newCharacterClearCustomData", value: true, "If enabled, new characters will have their custom data cleared.");
			PassthroughCompatModCustomData = BindServerConfig("Player Sync", "PassthroughCompatModCustomData", value: true, "Leaves inventory-describing custom data owned by slot mods (ExtraSlots and its CustomSlots addon, EquipmentAndQuickSlots including its 2.x legacy keys, InventorySlots) to those mods instead of tracking and enforcing it. These mods keep a serialized backup of every slot item in player custom data and restore those items whenever a character loads with empty slots; when the enforcer tracked that backup and re-applied its (stale) copy on every spawn, each death resurrected and duplicated the player's slot gear. With this enabled the affected keys are never stored in server saves, never streamed as deltas, and never overwrite the live player's own value, and the mods' per-item slot-memory keys are ignored when deciding whether an item matches the save. Disable only to restore the old behaviour.", null, advanced: true);
			PassthroughCompatModCustomData.SettingChanged += delegate
			{
				CompatCustomData.RefreshEnabled();
			};
			CompatCustomData.RefreshEnabled();
			KnownTextPassthroughPrefixes = BindServerConfig("Player Sync", "KnownTextPassthroughPrefixes", "EpicMMOSystem", "Comma separated key prefixes in a character's known texts that are left to the mod that owns them instead of being tracked and enforced. Valheim's known texts are the compendium - the runestone and lore entries a character has read - but nothing namespaces the keys, so some mods keep per-player progression there. EpicMMO is the shipped case: its level, experience and attribute points are one entry each, all prefixed EpicMMOSystem. Progression is only captured on full saves, never on deltas, so enforcing such a key against the server's copy would roll back everything earned since the last one. A matched key is never stored in a server save and never overwrites the live player's own value. Matching is by prefix and case sensitive. Leave this empty to track known texts in full, and note that it does not exempt anything from NewCharacterClearKnownTexts.", null, advanced: true);
			KnownTextPassthroughPrefixes.SettingChanged += delegate
			{
				CompatKnownTexts.RefreshPrefixes();
			};
			CompatKnownTexts.RefreshPrefixes();
			EpicMMOKnownTextCompat = BindServerConfig("Player Sync", "EpicMMOKnownTextCompat", value: true, "Only does anything when EpicMMO is installed, and only for a character joining this server for the first time under NewCharacterClearKnownTexts. EpicMMO does not read its level out of the known texts when something asks for it - it reads them once in a Game.SpawnPlayer patch and publishes what it found to the player's ZDO and custom data, which is where its own monster scaling reads a player's level from. That patch runs before the enforcer's join validation, so the reset left the character at level 1 while the world went on scaling monsters to the level they walked in with for the rest of the session. With this on, the reset is done ahead of every spawn patch when the server has already confirmed the character is new, so EpicMMO reads the reset state itself; when the join had to wait for that answer and there was nothing left to get in front of, EpicMMO is asked to read again afterwards instead. Turn it off to leave EpicMMO entirely alone, at the cost of the stale level above. Nothing here changes what is reset - only when, and who is told about it.", null, advanced: true);
			NewCharacterStartingItems = BindServerConfig("Player Sync", "NewCharacterStartingItems", "ArmorRagsChest,ArmorRagsLegs,Torch", "Comma separated prefab names a brand new character is allowed to arrive holding when NewCharactersRemoveExtraItems is enabled. Anything else in their inventory on their first join is confiscated, as is any item above quality 1. Names are matched exactly (case insensitively), not as substrings, so 'Torch' does not also permit 'TorchMist'. Change this if your modpack starts players with different gear; leave it empty to allow no starting items at all.");
			ConfiscateUnidentifiableItems = BindServerConfig("Player Sync", "ConfiscateUnidentifiableItems", value: false, "Controls what happens to an inventory item whose ItemDrop prefab does not resolve on the client - usually a modded item, or an entry another mod created directly. These cannot be tracked, matched or handed back, so by default they are left alone and logged. Enable to confiscate them instead; note that a confiscated item with no prefab name can never be returned with the confiscation commands.", null, advanced: true);
			InitialCharacterSyncWaitSeconds = BindServerConfig("Player Sync", "InitialCharacterSyncWaitSeconds", 10, "How long a joining client waits for the server's answer about its stored character before giving up and treating the character as new. The answer normally arrives during the connection handshake, well before the world finishes loading, so this only matters if that is delayed. Set to 0 to never wait. Either way the character is treated as NEW when no answer arrives - the local save file on the joining machine is never used as the baseline for a server.", advanced: true, 0, 60);
			ValidateItemCustomData = BindServerConfig("Player Sync", "ValidateItemCustomData", value: true, "If enabled, custom data on items will be validated.");
			ValidateItemDurability = BindServerConfig("Player Sync", "ValidateItemDurability", value: true, "If enabled, item durability will be validated");
			ItemValidationDurabilityAllowedVariance = BindServerConfig("Player Sync", "ItemValidationDurabilityAllowedVariance", 10f, "Allowed variance for item durability validation.", advanced: true, 0f, 100f);
			SavePlayerStatusEffectsOnLogout = BindServerConfig("Player Sync", "SavePlayerStatusEffectsOnLogout", value: true, "Whether or not to save active character effects on logout and reapply on login");
			ItemRemovalForDirtyReconnection = BindServerConfig("Player Sync", "ItemRemovalForDirtyReconnection", value: false, "Leniency for dirty reconnects (crash/timeout, where the server save may be up to one delta window stale). RemoveNontrackedItemsFromJoiningPlayers always runs otherwise; if this is enabled, untracked items are NOT confiscated when the player's last disconnect was dirty, so crash victims keep items gained in the unsaved window.");
			ItemReturnForDirtyReconnection = BindServerConfig("Player Sync", "ItemReturnForDirtyReconnection", value: false, "Leniency for dirty reconnects. AddMissingItemsFromPlayerServerSave always restores missing tracked items on a clean join, and RestoreSkillsFromPlayerServerSave raises lowered skills the same way; on a dirty reconnect both are skipped by default (to avoid duping items consumed, or handing back skill lost to a death, in the unsaved window) unless this is enabled.");
			ServerSideJoinEnforcement = BindServerConfig("Player Sync", "ServerSideJoinEnforcement", value: true, "If enabled, the server re-applies the join rules (item confiscation, skill clamping, custom-data, Forsaken Power and food reset) to the first full character save a RETURNING player uploads each session, instead of trusting the client to have done it. This is the returning-character counterpart to the first-save enforcement the server already runs for brand new characters: the client runs the same checks, but the client is what you are defending against, so this is the copy a modified client cannot skip. Honours RemoveNontrackedItemsFromJoiningPlayers, PreventExternalSkillRaises, PreventExternalCustomDataChanges, PreventExternalForsakenPowerChanges, PreventExternalFoodChanges and the dirty-reconnect leniency settings, so turning those off turns off the matching server-side check too. Inert if none of them are on.");
			CatchupOverwriteOnJoin = BindServerConfig("Player Sync", "CatchupOverwriteOnJoin", value: false, "For switching this mod back on after the server has run without it for a while. The characters it already holds a save for still count as returning players, but those saves stopped being updated when the mod was turned off, so a normal join would enforce the stale copy - confiscating what a player gained in the meantime, lowering their skills and putting back their old progression, Forsaken Power, food, custom data and map. While this is enabled, a returning character is instead adopted exactly as the joining client holds it, and the stored save is replaced by it on that join; nothing is confiscated, lowered or restored, on the client or by ServerSideJoinEnforcement. The confiscated item and skill reduction records are kept, a pending admin skill restore still lands, and a character the server has no save for is still held to the new-character rules. Enforcement against returning players is effectively suspended while this is on, so turn it off again once everyone has rejoined. The replaced saves are not kept - enable EnableSaveArchives first if you want a copy of them. Off by default.");
			PreventExternalForsakenPowerChanges = BindServerConfig("Player Sync", "PreventExternalForsakenPowerChanges", value: false, "If enabled, each character's save records the Forsaken Power they have selected, and it is put back when they join - so a power picked up in a solo world or on another server, one this server may never have unlocked at its boss stones, cannot be brought in. Selecting a power at a boss stone while playing here is saved as normal. A character whose save was written before this was enabled has no power recorded yet: they keep the one they arrive with on their next join and are tracked from then on, so switching this on strips nobody. Also checked server side when ServerSideJoinEnforcement is on. Pair with NewCharacterClearForsakenPower, which covers a character's first join. Off by default.");
			NewCharacterClearForsakenPower = BindServerConfig("Player Sync", "NewCharacterClearForsakenPower", value: false, "If enabled, a character joining this server for the first time has their Forsaken Power cleared, so one selected in a solo world or on another server does not come with them. They can select one at a boss stone here as normal. Pair with PreventExternalForsakenPowerChanges, which stops a returning character bringing a different one in later. Off by default.");
			NewCharacterResetMapExploration = BindServerConfig("Player Sync", "NewCharacterResetMapExploration", value: false, "If enabled, a character joining this server for the first time has their map of this world wiped: explored areas, anything a cartography table revealed, and their saved pins. Valheim keeps a separate map for every world, so arriving with this one already uncovered means having played a copy of this world somewhere else - or being a character whose save an admin deleted to reset them, who now gets a fresh map along with everything else. The wipe only happens once the server has confirmed it holds no save for the character; if that answer has not arrived by the time the join is validated the map is left alone, because unlike an item a wiped map cannot be given back. The map lives only in the player's own character file and never reaches the server, so this is carried out by the client and cannot be checked server side. Off by default.");
			PreventExternalFoodChanges = BindServerConfig("Player Sync", "PreventExternalFoodChanges", value: false, "If enabled, each character's save records the foods they have eaten and how long each has left, and those exact foods are put back when they join. Vanilla keeps eaten food in the player's own character file, so without this a player can log off, eat food this server has not reached yet in a solo world, and come straight back with it - or just top their food back up for free. A returning character gets back what they left with, whatever they ate or let run out in between; a character joining for the first time has all their food cleared, and can eat as normal once here. A character whose save was written before this was enabled has no foods recorded yet: they keep what they arrive with on their next join and are tracked from then on, so switching this on strips nobody. The record is only as fresh as the character's last save, so after a crash or dropped connection a player can get back the burn time used since then - at most FullSyncPullIntervalMinutes' worth. Also checked server side when ServerSideJoinEnforcement is on. Off by default.");
			EnableProgressionSync = BindServerConfig("Player Sync", "EnableProgressionSync", value: false, "Master switch for storing a character's map, known recipes, trophies, statistics and spawn point on the server the way their items and skills already are. Everything in this group lives in the player's own character file, which means a player can edit it offline and a corrupted local save loses it for good; with this on the server keeps its own copy, hands it back on join, and its copy is the one that counts. Nothing is stored until at least one of the Sync* settings below is also on, and no extra data crosses the wire or reaches the disk while this is off. Note what this cannot do: the server can restore a statistic and refuse a value higher than the one it holds, but it has no way to prove any individual increment was earned honestly, so treat the statistics as progress that survives a lost save rather than as a cheat-proof record. Steam and Xbox achievement unlocks are held by the platform, not in the save, and are not touched at all. Off by default; every part of the feature is inert until this is on.");
			SyncMapExploration = BindServerConfig("Player Sync", "SyncMapExploration", value: false, "If enabled, the server stores each character's map of this world - explored ground, what a cartography table revealed, and their saved pins - and pushes its copy back when they join. A map uncovered in a copy of this world elsewhere is replaced by what this server has seen, and a player whose local character file is lost gets their exploration back. The map is stored beside the character save as an opaque .map file rather than inside it, because a fully explored map is 8.4 megabytes before compression; a real one costs tens of kilobytes to a megabyte of disk per character per world. The client only rebuilds and uploads it when something has actually been explored since the last upload, at most once every MapSyncIntervalMinutes and once more at logout, because building it is not free on the player's machine. Requires EnableProgressionSync. Not stored in InternalStorageMode - a megabyte of opaque bytes per character does not belong in the world file - so the two together leave the map untracked and say so in the log. Off by default.");
			SyncKnownItems = BindServerConfig("Player Sync", "SyncKnownItems", value: false, "If enabled, the server stores what each character knows - recipes, build pieces, materials, crafting stations, discovered biomes, runestone texts and permanent unlocks - and restores its copy on join. This is the opposite end of NewCharacterClearKnownRecipes: instead of wiping what a character arrived knowing because there is nothing to check it against, the server now has something to check it against, and anything learned somewhere it did not see is replaced. Vanilla rediscovers a recipe the moment the player holds its materials and has seen its crafting station, so the materials and stations are what is really being held and the recipe list is rebuilt from them on arrival. Requires EnableProgressionSync. Off by default.");
			SyncTrophies = BindServerConfig("Player Sync", "SyncTrophies", value: false, "If enabled, the server stores the trophy list vanilla keeps in each character file - every trophy that character has ever picked up, which is what the compendium reads - and restores it on join. Kept separate from SyncKnownItems because vanilla files trophies by prefab name while everything else is a localization token, and because an admin may reasonably want one without the other. Requires EnableProgressionSync. Off by default.");
			SyncPlayerStats = BindServerConfig("Player Sync", "SyncPlayerStats", value: false, "If enabled, the server stores each character's statistics - the counters behind the in-game Statistics panel and the post-1.0 achievement system: kills, deaths, distance travelled, items crafted, pieces placed and the rest - and restores them on join. A counter the client reports above the value the server holds was raised somewhere this server did not see, and is put back, the same way PreventExternalSkillRaises handles skills. Be clear about the limit: the server can hold a ceiling and restore a lost save, but it cannot verify that any single increment was honest. Achievement unlocks themselves are held by Steam or Xbox rather than in the save and are not affected. Requires EnableProgressionSync. Off by default.");
			SyncSpawnPoint = BindServerConfig("Player Sync", "SyncSpawnPoint", value: false, "If enabled, the server stores where each character respawns on this world - the bed they have claimed, and the point the game falls back to when that bed is gone - and restores it on join, so a lost or corrupted local character file no longer means waking up at the start stone with a base on the other side of the map. Only the spawn point is stored; where a character logged out and where they died are deliberately not, because putting a player back at a stale logout point would relocate somebody who has since walked away. Requires EnableProgressionSync. Off by default.");
			MapSyncIntervalMinutes = BindServerConfig("Advanced", "MapSyncIntervalMinutes", 15, "How often, in minutes, a client may upload its map. Building the upload means rebuilding and compressing an 8.4 megabyte package on the player's own machine, so this is deliberately coarse; the client also skips the upload entirely when nothing has been explored since the last one, and sends once more at logout regardless. Only used when SyncMapExploration is on.", advanced: true, 1, 240);
			NewCharacterClearPlayerStats = BindServerConfig("Player Sync", "NewCharacterClearPlayerStats", value: false, "If enabled, a character joining this server for the first time starts with their statistics at zero rather than carrying in the totals from wherever they were played before. The same reasoning as NewCharacterSetSkillsToZero, and worth knowing before turning it on: these counters are per character and not per world, so this discards a record of everything that character has ever done anywhere, and it cannot be undone. Only used when SyncPlayerStats is on. Off by default.");
			EnableSaveArchives = BindServerConfig("Backups", "EnableSaveArchives", value: false, "Master switch for keeping rolling, compressed archives of this server's world save together with its character saves. The game already rotates backups of its own - two of them, uncompressed, of the world only - so what this adds is a longer history, compression, and the character store in the same archive. That last part is the reason it exists: restoring one of the game's backups puts the world back and leaves every character exactly as they were, which is its own kind of rollback. Be aware of the cost before switching it on. A real world file reaches 200 megabytes, the game's own rotation already keeps around a gigabyte beside it, and each archive here is another copy of that on top - compressed, but still measured in hundreds of megabytes. Archives are written after a world save finishes, never during one, so an archive is always of a complete save. Off by default; every part of the feature is inert until this is on.");
			SaveArchiveIntervalMinutes = BindServerConfig("Backups", "SaveArchiveIntervalMinutes", 120, "The shortest gap, in minutes, between two archives. An archive is only ever taken just after a world save completes, so the real cadence is this rounded up to the next save; a value below the server's own save interval simply means every save. enforcer-archive-now ignores this for one archive.", advanced: false, 5, 1440);
			SaveArchiveKeepCount = BindServerConfig("Backups", "SaveArchiveKeepCount", 5, "How many archives to keep. The oldest beyond this are deleted right after a new one is written, and lowering it takes effect at the next archive rather than one file at a time - this is a setting about disk space, so it has to give the disk back when you ask.", advanced: false, 1, 100);
			SaveArchiveMaxTotalMB = BindServerConfig("Backups", "SaveArchiveMaxTotalMB", 0, "A ceiling, in megabytes, on what all archives together may occupy. Oldest first are deleted until the total fits. 0 means no ceiling, and SaveArchiveKeepCount alone decides. The newest archive is never deleted to satisfy this, so setting it below the size of a single archive keeps one rather than none.", advanced: false, 0, 1000000);
			SaveArchiveIncludeCharacters = BindServerConfig("Backups", "SaveArchiveIncludeCharacters", value: true, "Whether to include the Characters folder - every player's items, skills, confiscation record and, if it is on, their synced progression. On by default, because a world restored without it is a world where everybody's character is from a different moment in time.");
			SaveArchiveIncludeConfig = BindServerConfig("Backups", "SaveArchiveIncludeConfig", value: true, "Whether to include this mod's own configuration: the .cfg and the YAML files beside it (Mods.yaml, Notifications.yaml, KnownCheaters.yaml, PlayerIds.yaml). Small, and it is what makes an archive enough to rebuild a server from. Other mods' configuration is deliberately not included - it is not this mod's to copy around.");
			SaveArchiveCompression = BindServerConfig("Backups", "SaveArchiveCompression", "Fastest", "How hard to compress. 'Fastest' is the default and the right answer for a world file, which is already dense; 'Optimal' buys a little more at a large cost in CPU time on a file this size; 'NoCompression' just packs the files together. The work happens on a background thread either way, so this trades disk against a background core rather than against frame time.", new AcceptableValueList<string>(new string[3] { "Fastest", "Optimal", "NoCompression" }));
			SaveArchivePath = BindServerConfig("Backups", "SaveArchivePath", "", "Where archives are written. Empty means BepInEx/config/ValheimEnforcer/Archives. Point it at another drive if you would rather not keep a second copy of the world on the same one that holds the first.");
			EnableCrashRecovery = BindServerConfig("Crash Recovery", "EnableCrashRecovery", value: false, "Master switch for emergency crash recovery. Every few minutes the server hands each connected player a sealed snapshot of their own character - encrypted and signed with a key that never leaves the server, so it is opaque to whoever is holding it and useless anywhere else. If this server then crashes and comes back from an older save, it asks for those snapshots back and adopts the ones that verify and are genuinely newer than what it holds. Understand the trade before switching it on. The world and the character store are separate saves and roll back independently, so restoring a character to a state newer than the world can re-introduce items whose source in the world - the chest they came out of, the vein they were mined from - has itself rolled back. That is item duplication, and it is inherent to putting a character back rather than a fault in how it is done. It is also why restoration only ever happens inside a narrow window after an unclean shutdown. Note what this is not: the client cannot read its own snapshot, so this is no help against a lost or corrupted LOCAL character file - EnableProgressionSync is what covers that direction. Off by default; every part of the feature is inert until this is on.");
			CrashRecoveryAutoRestore = BindServerConfig("Crash Recovery", "CrashRecoveryAutoRestore", value: true, "Whether a verified, newer snapshot is adopted on its own while the recovery window is open, or whether the window only reports what could be restored. On by default, because the window is already narrow and only opens after an unclean shutdown - but it is a separate switch from EnableCrashRecovery so an admin can have the snapshots kept and distributed without the server ever acting on one unasked. With this off, nothing a client hands back is ever applied.");
			CrashRecoveryWindowMinutes = BindServerConfig("Crash Recovery", "CrashRecoveryWindowMinutes", 30, "How long after an unclean shutdown the server keeps accepting snapshots. The window opens only when the previous run left no clean-shutdown marker, and closes on its own after this many minutes; an admin can open one by hand with enforcer-recovery-open for a rollback the server could not have noticed, such as a restore from a backup.", advanced: false, 1, 720);
			CrashRecoveryPushIntervalMinutes = BindServerConfig("Crash Recovery", "CrashRecoveryPushIntervalMinutes", 10, "How often each connected player is sent a fresh snapshot. This is the bound on how much can be lost: a crash costs at most whatever happened since a player's last snapshot. It is also the bandwidth knob - a snapshot is a compressed character save, tens to hundreds of kilobytes, sent per player per interval - so a busy server should think about this number rather than only about the smallest one.", advanced: false, 1, 120);
			CrashRecoveryKeepBlobs = BindServerConfig("Crash Recovery", "CrashRecoveryKeepBlobs", 3, "How many characters' snapshots a client keeps per world. One file per character, each replaced by a newer one rather than accumulating, so this bounds how many different characters are remembered rather than how many copies of one.", advanced: false, 1, 20);
			EnableStarterLoadouts = BindServerConfig("Player Sync", "EnableStarterLoadouts", value: false, "Master switch for starter loadouts: a kit of items, skill levels, known materials and a spawn point handed to a character joining this server for the first time. The kits themselves are written in Loadouts.yaml beside this file, and DefaultStarterLoadout says which one new characters get. A loadout is granted AFTER the new-character rules have stripped whatever the character arrived with, so its items do not also need listing in NewCharacterStartingItems, and it may hand over an upgraded item even though the allowlist refuses one that turns up on its own. Skills are only ever raised, never lowered. Off by default.");
			DefaultStarterLoadout = BindServerConfig("Player Sync", "DefaultStarterLoadout", "", "The name of the loadout in Loadouts.yaml that a character joining for the first time gets. Empty means none, which leaves the feature doing nothing even with EnableStarterLoadouts on. A name that is not in the file is reported in the log and treated as none, rather than as a reason to refuse the join.");
			RecordSkillReductions = BindServerConfig("Player Sync", "RecordSkillReductions", value: true, "If enabled (the default), every time this mod lowers a character's skill - a returning character clamped back to the level the server holds for them under PreventExternalSkillRaises, or a first-time character's skills set to zero under NewCharacterSetSkillsToZero - the skill, the level it was lowered from and to, when, and why are written into that character's save, and enforcer-skills-list shows them. That record is what lets an admin undo one: enforcer-skills-restore puts each skill back to the highest level it was recorded being lowered from, straight away if the player is online and on their next join if not, and enforcer-skills-clear forgets a record without restoring anything. Only reductions this mod makes are recorded - the skill loss on death is the game's own, and a reported value the game could never produce (above 100, negative) is corrected without a record because there is nothing valid to put it back to. Turn this off to stop recording; records already made are kept until they are restored or cleared.");
			NewCharacterClearKnownRecipes = BindServerConfig("Player Sync", "NewCharacterClearKnownRecipes", value: true, "If enabled, a character joining this server for the first time forgets every recipe and build piece they discovered somewhere else. Valheim discovers a recipe as soon as the player knows its materials and crafting station, so the materials, crafting stations and trophies they know are cleared too - the same reset as the game's resetknownitems command - and discovery starts again from what they are carrying once the other new-character rules have run. Like NewCharacterResetMapExploration this only happens once the server has confirmed it holds no save for the character, because forgotten recipes cannot be given back, and it is carried out by the client because recipes never reach the server. Never applies in singleplayer or to a listen host's own character. Note that on a server which already has players, anyone joining for the first time since Valheim Enforcer was installed has no save yet and counts as new. On by default.");
			NewCharacterClearKnownTexts = BindServerConfig("Player Sync", "NewCharacterClearKnownTexts", value: true, "If enabled, a character joining this server for the first time forgets its known texts - the runestone and lore entries of the in-game compendium, read somewhere this server never saw. Vanilla's own reset does not cover these: Player.ResetCharacterKnownItems, which NewCharacterClearKnownRecipes runs, clears recipes, materials, crafting stations and trophies and leaves known texts untouched. That matters beyond the compendium, because some mods keep per-player progression in the same dictionary - EpicMMO stores a character's level, experience and attribute points there, one entry each - so with this off a first-time joiner whose items, skills, recipes and custom data were all reset still arrives at the level they reached elsewhere. KnownTextPassthroughPrefixes does not exempt anything from this; it governs what is enforced between joins, not what a character may bring in on its first one. Like the recipe and map resets this cannot be undone, so it only happens once the server has confirmed it holds no save for the character, and it is carried out by the client because known texts never reach the server. Never applies in singleplayer or to a listen host's own character. On by default.");
			EnforceCharacterLimit = BindServerConfig("Player Sync", "EnforceCharacterLimit", value: false, "Master switch for the one-character-per-account rule. When enabled, an account may only join with a character the server already has a save for, up to MaxCharactersPerAccount; any other character is refused at the connect handshake and told which character to use instead. Characters that already have a save are always allowed, so turning this on never locks out an existing player - it only stops new characters being added. Freeing a slot means deleting that character's save file (BepInEx/config/ValheimEnforcer/Characters/<accountId>/<Name>.yaml), which is what a character reset already involves. Off by default.");
			MaxCharactersPerAccount = BindServerConfig("Player Sync", "MaxCharactersPerAccount", 1, "How many characters one account may have on this server when EnforceCharacterLimit is enabled. Accounts that already have more than this keep every character they have; the limit only blocks adding another.", advanced: false, 1, 20);
			CharacterLimitExemptAccounts = BindServerConfig("Player Sync", "CharacterLimitExemptAccounts", "", "Comma-separated list of account ids allowed to connect with any number of characters, regardless of EnforceCharacterLimit. Independent of admin status - an id listed here does not need to be an admin, and an admin is not exempt unless listed (or CharacterLimitExemptAdmins is enabled). Both the platform-prefixed form (Steam_76561198012345678) and the bare id (76561198012345678) are accepted. Note this setting is synced to connected clients, so the ids in it are visible to players.");
			CharacterLimitExemptAdmins = BindServerConfig("Player Sync", "CharacterLimitExemptAdmins", value: false, "If enabled, anyone on the server's adminlist is exempt from the character limit without needing an entry in CharacterLimitExemptAccounts. Off by default so the two permissions stay separate.");
			ImportServerCharacters = BindLocalConfig("Migration", "ImportServerCharacters", value: false, "If enabled, the server imports character saves from the ServerCharacters mod once at startup, so players migrating from it keep their inventory and skills instead of having everything confiscated on their first join. Characters that already have a save here are left alone, so the pass is safe to leave on. IMPORTANT: uninstall ServerCharacters first - the two mods are declared incompatible and BepInEx will refuse to load ValheimEnforcer while both are present. The files ServerCharacters leaves behind in the character folder are what gets read; nothing is moved or deleted. Off by default.");
			ServerCharactersImportPath = BindLocalConfig("Migration", "ServerCharactersImportPath", "", "Where to look for ServerCharacters' character files. Leave empty to use the game's own local character folder, which is where ServerCharacters puts them and which follows Valheim's -savedir argument automatically. Only set this if you moved the files somewhere else.");
			InternalStorageMode = BindServerConfig("Advanced", "InternalStorageMode", value: false, "If enabled, player character data will be stored within your world. Enables full portability of the world without having to synchronize configurations.", null, advanced: true);
			StallWarningThresholdMs = BindServerConfig("Advanced", "StallWarningThresholdMs", 100, "How long (in milliseconds) one of this mod's operations may take before it is reported as a frame hitch in the log. Covers the periodic cheat scans, the character delta flush and character saves. Purely diagnostic - it changes nothing about what the mod does, it only decides when a slow operation is worth a warning. Lower it while chasing a stutter report; raise it, or set 0 to disable the warning entirely, if a slow disk makes it noisy.", advanced: true, 0, 10000);
			ConfigPollIntervalSeconds = BindServerConfig("Advanced", "ConfigPollIntervalSeconds", 30, "How frequently (in seconds) the mod polls config files on disk for changes.", advanced: true, 1, 300);
			DeltaSynchronizationFrequencyInSeconds = BindServerConfig("Advanced", "CharacterDeltaTracker", 15, "Minimum time (in seconds) between incremental inventory/skill/custom-data updates. Updates are only produced when the player's inventory actually changes, so an idle player sends nothing; this is a rate limit rather than a polling interval.", advanced: true, 5, 300);
			FullSyncPullIntervalMinutes = BindServerConfig("Advanced", "FullSyncPullIntervalMinutes", 25, "How often (in minutes) the server asks connected players to upload a full character save. Full saves are a periodic reconciliation layered on top of the incremental delta updates (CharacterDeltaTracker); they are no longer tied to the world/profile autosave.", advanced: true, 1, 1440);
			HashComputeTimeoutSeconds = BindServerConfig("Advanced", "HashComputeTimeoutSeconds", 30, "Maximum time spent hashing local plugin DLLs at startup before giving up and reporting the remainder as unverifiable. Hashing runs on background threads and usually takes well under a second; this is a safety valve for a stalled disk, not a tuning knob.", advanced: true, 5, 300);
			ThunderstoreMaxArchiveMB = BindServerConfig("Advanced", "ThunderstoreMaxArchiveMB", 128, "Largest Thunderstore archive, in megabytes, the server will download when resolving mod hashes. Archives are held in memory while their DLLs are hashed, so this is also the peak transient allocation; packages are resolved one at a time so it is never multiplied. Larger archives are skipped and logged.", advanced: true, 1, 512);
			FullSyncMaxConcurrentPlayers = BindServerConfig("Advanced", "FullSyncMaxConcurrentPlayers", 5, "Maximum number of players the server asks to upload a full character save at the same time. Larger player counts are staggered into successive waves of this size to avoid a bandwidth spike. 10 is safe on a healthy server; lower it on constrained upload/VPS hosts.", advanced: true, 1, 50);
			EnforceRoutedRpcSender = BindServerConfig("Advanced", "EnforceRoutedRpcSender", value: true, "If enabled (the default), the server verifies the sender id on every routed network message against the connection it actually arrived on, and corrects it if they disagree. Valheim's routed RPC carries a sender id that the sending client writes and the server never checks, so without this a modified client can impersonate any other connected player - running admin commands, getting someone else banned, or overwriting another player's character save. This affects only forged packets; an honest client is never touched. Leave it on unless another mod is misbehaving because of it, in which case report the mod - turning this off re-opens sender spoofing for this mod AND every other routed RPC on the server.", null, advanced: true);
			CharacterCacheIdleMinutes = BindServerConfig("Advanced", "CharacterCacheIdleMinutes", 30, "How long, in minutes, the server keeps a character's save in memory after that character was last touched - a save, an incremental update, a death record, or a login. Saves on disk are always the authority, so an entry dropped from memory costs one file read the next time that player's data changes; this exists so a busy server's memory reflects who is playing now rather than everyone who has joined since the last restart. Keep it above FullSyncPullIntervalMinutes so players who are online but idle are not reloaded after every periodic pull. 0 keeps every character in memory until restart, which is what earlier versions did. Not used with InternalStorageMode.", advanced: true, 0, 1440);
			CharacterCacheIdleMinutes.SettingChanged += delegate
			{
				CharacterStore.SetIdleEvictionMinutes(CharacterCacheIdleMinutes.Value);
			};
			CharacterStore.SetIdleEvictionMinutes(CharacterCacheIdleMinutes.Value);
			MemoryReportIntervalMinutes = BindServerConfig("Advanced", "MemoryReportIntervalMinutes", 0, "When above 0, writes the summary enforcer-memory prints to the server log every this-many minutes: process working set, managed heap, what this mod is holding (cached characters, audit buffers, per-player tables) and the world's object counts. Purely diagnostic. Off by default.", advanced: true, 0, 1440);
			CharacterWriteIntervalSeconds = BindServerConfig("Advanced", "CharacterWriteIntervalSeconds", 0, "When above 0, a character changed by an incremental update is written to disk at most once every this-many seconds, instead of after every update. The copy in memory is what the server works from either way, so nothing a connected player does is affected; what changes is how far the FILE can trail it. Full saves, deaths, a player leaving, a player joining and a server shutdown all still write straight away, so this only ever delays the routine updates in between. The cost is the obvious one: a server that dies without shutting down loses up to this many seconds of those updates for whoever was mid-session, on top of the rate limit they already ride (CharacterDeltaTracker). Worth turning on for a busy server, where rewriting a whole character for every player every few seconds is the largest single source of memory churn this mod has; 30 to 60 is a sensible value. 0, the default, writes after every update as earlier versions did. Not used with InternalStorageMode.", advanced: true, 0, 600);
			CharacterWriteIntervalSeconds.SettingChanged += delegate
			{
				CharacterStore.SetWriteIntervalSeconds(CharacterWriteIntervalSeconds.Value);
			};
			CharacterStore.SetWriteIntervalSeconds(CharacterWriteIntervalSeconds.Value);
			FullSyncSpreadAcrossInterval = BindServerConfig("Advanced", "FullSyncSpreadAcrossInterval", value: true, "If enabled (the default), the periodic full character saves are asked for one player at a time, evenly spaced across FullSyncPullIntervalMinutes, rather than from everybody at once in waves at the end of it. Each player is still asked exactly as often. The difference is to the server: a full save is the most expensive thing a player sends, and a hundred of them inside a minute is a burst of work and memory every interval where one every few seconds is nothing at all. Turn it off to go back to waves of FullSyncMaxConcurrentPlayers, which that setting then controls again.", null, advanced: true);
			FastCharacterWriter = BindServerConfig("Advanced", "FastCharacterWriter", value: true, "If enabled (the default), character saves are written by a purpose-built writer instead of the general YAML serializer. The file is the same file - same keys, same layout, read back by the same reader, and interchangeable with saves written the other way - but producing it costs a small fraction of the memory, which matters because a character is rewritten every time a player's inventory changes. The writer checks itself against the serializer once at startup and hands everything back to the serializer for the session if the two disagree about anything, saying so in the log. Turn this off only if you suspect it of a problem; saves keep working exactly as they did before it existed.", null, advanced: true);
			FastCharacterWriter.SettingChanged += delegate
			{
				CharacterYaml.SetEnabled(FastCharacterWriter.Value);
			};
			CharacterYaml.SetEnabled(FastCharacterWriter.Value);
			BackgroundCharacterWrites = BindServerConfig("Advanced", "BackgroundCharacterWrites", value: true, "If enabled (the default), character saves are written to disk on a background thread. The game takes a quick copy of the character and carries on, and the file is written a moment later - the same file, published the same way. Before this, every save this machine made for itself was written in the middle of a frame: on a client that is the join, every death and every respawn, each a visible stutter with a large character. Saves of the same character that pile up are written once, anything that reads a character back waits for its pending write first, and logging out, a server shutting down and quitting all wait for every pending write before going on. Saves clients send to a server were already written in the background and still are, regardless of this setting. Turn this off only if you suspect it of a problem; saves are then written on the spot, exactly as before. The in-world copy InternalStorageMode keeps is written on the spot either way, because the world's data can only be changed from the game's main thread.", null, advanced: true);
			BinaryDeltaUpdates = BindServerConfig("Advanced", "BinaryDeltaUpdates", value: true, "If enabled (the default), the server tells connecting clients it accepts incremental character updates in a compact binary form, and clients that understand that send it instead of YAML. Same content either way; the binary form costs the server a few kilobytes to read where the YAML one cost around a hundred and fifty, once per player every few seconds, on the main thread. Clients running a build from before this existed keep sending YAML and are handled as they always were, and turning this off makes every client do the same from its next connection.", null, advanced: true);
			CaptureCustomDataOnRespawn = BindServerConfig("Advanced", "CaptureCustomDataOnRespawn", value: true, "If enabled (the default), a player's custom data is recorded at the save the game takes just before every respawn, so what other mods wrote there survives the respawn. Many mods only write their player data into the character at the moment the game saves it - Travel Totems keeps its unlocked totems that way, EpicLoot its bounties and adventure progress - and the game's own save before a respawn is one this mod never used to see. The respawn then put back the copy this mod last recorded, which could be up to half an hour old, and the other mod read that back in: totems unlocked since were forgotten on every death. Only used with PreventExternalCustomDataChanges, and only after a player's join has been checked, so what a character arrives with is enforced exactly as before. Turn this off to go back to the old behaviour.", null, advanced: true);
			EnableCheatDetection = BindServerConfig("Anti-Cheat", "EnableCheatDetection", value: true, "Master switch for client-side cheat scanning. When enabled the client checks running processes, the DLLs loaded into the game, and open window titles against a catalog of known cheat tools. Only matched entries are reported to the server - the player's full process list is never transmitted.");
			DetectInjectedCheatAssemblies = BindServerConfig("Anti-Cheat", "DetectInjectedCheatAssemblies", value: true, "Detect injected cheat menus by the namespace of the types they load into the game, including assemblies that arrive mid-session. This is the vector that sees a cheat which is not a BepInEx plugin: it has no file in BepInEx/plugins to hash, it is not in the mod list the client declares at join, and a menu that draws inside the game has neither a process nor a window to find it by - but its own types are loaded in this process, and their namespace survives renaming the file. Currently covers ValheimTooler and Valheaven (a.k.a. ValheimAdminMenu). Both are auto-banned on a confirmed detection regardless of ActionOnDetection. High confidence, very low cost: an assembly is inspected once and the result cached for the session.");
			DetectValheimTooler = BindServerConfig("Anti-Cheat", "DetectValheimTooler", value: true, "Include ValheimTooler in the injected-assembly scan. Split out from the rest because it predates the catalog and servers may already have turned it off. Requires DetectInjectedCheatAssemblies.");
			DetectProxyLoaders = BindServerConfig("Anti-Cheat", "DetectProxyLoaders", value: true, "Detect proxy-DLL loaders: a copy of a Windows system DLL sitting beside valheim.exe instead of in System32. Windows resolves a DLL from the game folder first, so dropping one there gets it loaded into the game ahead of the real one, which then does as it likes and forwards the genuine exports on. This is how Valheaven ships (as version.dll) and it is the standard way to load a cheat that is not a mod. The check is not the name - every name involved is a real Windows DLL the game legitimately loads - it is where the file was loaded FROM, so it survives renaming and rebuilding. Every flagged file is hashed, and the hash decides first: a build known to be a cheat is reported under that tool's own name and cannot be allowlisted away. BepInEx's own doorstop is a winhttp.dll proxy and is recognised as such. The graphics names (dxgi, d3d9/10/11/12, ddraw, opengl32) are reported as low confidence and never enforced on their own, because ReShade, Special K and ENB all install exactly this way. Everything else is an injector nobody has identified and follows ActionOnDetection, legitimate ones included - nothing is trusted here that an admin has not vouched for. If you recognise one, put its hash in AllowedProxyLoaderHashes rather than its name in IgnoredCheatProcesses. Requires ScanLoadedModules.");
			DetectCheatTools = BindServerConfig("Anti-Cheat", "DetectCheatTools", value: true, "Scan for the built-in catalog of known cheat tools: WeMod/Wand, ArtMoney, PLITCH, Speed Gear, Squalr, WPE Pro, and the injectors/loaders used to deliver Valheim cheats (SharpMonoInjector, Xenos, Extreme Injector, ValheimTooler launcher, ValHack, Valheim Mod Menu). Tools with no legitimate purpose are auto-banned; the rest follow ActionOnDetection.");
			DetectCheatEngine = BindServerConfig("Anti-Cheat", "DetectCheatEngine", value: true, "Include Cheat Engine in the catalog scan (process names, window titles, and injected speedhack/DBK modules). Its TfrmMain/TfrmMemView window classes are generic Delphi names shared by legitimate software, so a class-only sighting is logged but never kicked or banned. Note: Cheat Engine has legitimate uses — prefer Log action over Kick/Ban. Requires DetectCheatTools.");
			DetectGenericTrainers = BindServerConfig("Anti-Cheat", "DetectGenericTrainers", value: true, "Flag any running process whose executable name contains the word 'trainer' (e.g. 'Valheim Trainer.exe', 'Hitman 3 Trainer - FLiNG.exe'). Catches FLiNG, MrAntiFun and Cheat Happens trainers without listing each one. Follows ActionOnDetection.");
			ScanLoadedModules = BindServerConfig("Anti-Cheat", "ScanLoadedModules", value: true, "Scan the native DLLs loaded into the game process itself. This is the only way to see a cheat that has already injected and then closed its launcher, and it survives renaming the tool's executable. Also carries the proxy-loader check (DetectProxyLoaders). Cheap - the module list is local to our own process.");
			ScanWindowTitles = BindServerConfig("Anti-Cheat", "ScanWindowTitles", value: true, "Scan open window classes and titles. Catches tools that have been renamed to evade the process-name check, most notably Cheat Engine. Generic framework window classes (e.g. Delphi's TfrmMain) are treated as low confidence: the server logs the sighting but takes no action on it alone.");
			ScanElevatedProcesses = BindServerConfig("Anti-Cheat", "ScanElevatedProcesses", value: true, "Let the process scan see programs run as administrator and background services. Valheim's runtime silently leaves those out of its own process list - roughly a third of what runs on a typical desktop - so without this any cheat tool started elevated, WeMod/Wand included, is invisible to the process check. Names are read from a Windows process snapshot without opening any process, and as before only matched names are reported to the server. Turn off only to go back to the old process list if this causes a problem. Windows only.", null, advanced: true);
			AdditionalCheatProcesses = BindServerConfig("Anti-Cheat", "AdditionalCheatProcesses", "", "Comma-separated list of extra process names to treat as cheat tools, without the '.exe' suffix, matched exactly and case-insensitively. Empty by default. Suggested opt-in values for strict servers: x64dbg, x32dbg, x96dbg, ProcessHacker, SystemInformer, HxD, ReClass.NET, ollydbg, Scylla_x64, frida, Fiddler, Charles. WARNING: every one of those is a standard developer tool with heavy legitimate use by modders and streamers, which is why none of them ship enabled. Deliberately excluded from the built-in catalog and NOT recommended here: Aurora (collides with Aurora RGB lighting software), Process Lasso (a CPU priority optimiser, not a speedhack), AutoHotkey (compiled scripts take arbitrary names, so the check is worthless, and it is widely used for accessibility and key remapping), and MSI Afterburner/RivaTuner/OBS (their overlay DLLs look injector-shaped).");
			IgnoredCheatProcesses = BindServerConfig("Anti-Cheat", "IgnoredCheatProcesses", "", "Comma-separated allowlist of process, module or window names to never flag, matched as a case-insensitive substring. Applied last, so it overrides the built-in catalog and AdditionalCheatProcesses. Use this to keep playing when a legitimate program trips a signature. An entry containing a path separator ('\\' or '/') is also matched against the folder a proxy DLL was loaded from, so a whole directory can be exempted - entries without one keep matching names only, so an existing entry like 'steam' cannot become an exemption for the entire game folder. One thing this no longer overrides: a proxy DLL whose build is known to be a cheat is matched before any allowlist, and DetectProxyLoaders is the only way past that.");
			AllowedProxyLoaderHashes = BindServerConfig("Anti-Cheat", "AllowedProxyLoaderHashes", "", "Comma-separated SHA256 hashes of proxy DLLs to treat as legitimate, matched against the hash of the file itself, case-insensitively. Empty by default, and this is the right escape hatch to reach for when DetectProxyLoaders flags something it should not. A proxy DLL is judged by where it was loaded from, and the game folder is also where every mod manager puts its own loader - so past the names, a legitimate loader is not distinguishable from a cheat by name or by path, and the file's own content is all that is left. The hash of the flagged file is printed in the server log line for the detection; copy it from there. Two things this deliberately does not do. It does not exempt a build known to be a cheat: those are matched first and cannot be allowlisted. And it does not exempt a name - a hash is one specific file, so the check keeps working against every other DLL that later turns up under the same name, which is the whole difference between this and putting the name in IgnoredCheatProcesses. Set it on the server; connected clients pick it up on their next module scan, but a player already kicked has to rejoin.");
			CheatDetectionAction = BindServerConfig("Anti-Cheat", "ActionOnDetection", "Kick", "Server-side action taken when a cheat tool is reported. Note that dedicated game-cheating tools (injectors, ValheimTooler, Valheaven, ValHack, Valheim Mod Menu) are always auto-banned regardless of this setting, and low-confidence sightings (generic window classes, graphics proxy DLLs) are always logged only, regardless of this setting. An unidentified proxy loader follows this setting: it says a loader is installed, not which one. See AllowedProxyLoaderHashes if one of those turns out to be legitimate.", new AcceptableValueList<string>(new string[3] { "Log", "Kick", "Ban" }));
			CheatScanIntervalSeconds = BindServerConfig("Anti-Cheat", "ScanIntervalSeconds", 30, "Seconds between periodic client scan ticks. The process, module and window scans are staggered across successive ticks so their cost never lands on the same frame, so each individual scan runs every three intervals. Injected-assembly detection is event-driven and not affected by this interval.", advanced: false, 5, 300);
			BannedProxyLoaderHashes = BindLocalConfig("Anti-Cheat", "BannedProxyLoaderHashes", "", "Comma-separated SHA256 hashes of proxy DLLs to ban on sight, in addition to the builds this mod already knows. Matched on the server against the hashes a client reports for a DLL loaded from the game folder - both the whole file and its code - so a build you have identified is banned from the next report on, without waiting for a release. Server-only: never sent to clients. The hash to paste is in the server log line for the detection. Same format as AllowedProxyLoaderHashes; a hash in both lists is banned.");
			BannedAssemblyNamespaces = BindLocalConfig("Anti-Cheat", "BannedAssemblyNamespaces", "", "Comma-separated namespaces to ban on sight when a client reports unaccounted code carrying them (see ReportUnaccountedCode). Matched exactly or as a dotted prefix, so 'Foo' covers 'Foo.UI' and not 'FooBar'. Server-only: never sent to clients.");
			BannedAssemblyMvids = BindLocalConfig("Anti-Cheat", "BannedAssemblyMvids", "", "Comma-separated module version ids (MVIDs, the GUID printed in the server log for an unaccounted assembly) to ban on sight. An MVID names one exact build of an assembly, whatever it is called. Server-only: never sent to clients.");
			VerifyDoorstopTarget = BindServerConfig("Anti-Cheat", "VerifyDoorstopTarget", value: true, "Only treat the winhttp.dll in the game folder as BepInEx's own loader when the assembly it started is the BepInEx preloader this game is actually running. Anything else is reported as a low-confidence sighting: logged, never acted on. Turn off to go back to recognising it by the files installed beside it. Requires DetectProxyLoaders.", null, advanced: true);
			DetectUnlistedGameFolderDlls = BindServerConfig("Anti-Cheat", "DetectUnlistedGameFolderDlls", value: true, "Report any DLL loaded into the game from the game folder itself that the game does not ship there, whatever its name. The game folder is where Windows looks first, so a DLL placed there is loaded ahead of the real one. Reported as a low-confidence sighting with the file's hashes: logged, never acted on, unless the hash is a known cheat build or listed in BannedProxyLoaderHashes. Requires ScanLoadedModules.");
			ReportUnaccountedCode = BindServerConfig("Anti-Cheat", "ReportUnaccountedCode", value: true, "Have clients report managed code loaded into the game that no installed mod, patcher, BepInEx or the game itself accounts for - for example an assembly loaded straight from memory. Each report names the assembly, its build id (MVID), up to eight namespaces, its file name without the folder, what it patches and whether it draws on screen. The server logs each one once per session and bans only on a match against the cheat catalog, BannedAssemblyNamespaces or BannedAssemblyMvids.");
			DetectAntiCheatTampering = BindServerConfig("Anti-Cheat", "DetectAntiCheatTampering", value: true, "Have the client check that nothing has patched the cheat detector's own methods or detached it from the game's assembly loading. A finding is a medium-confidence sighting: written to the server log and posted to the moderation channel (Discord.NotifyCheatSuspected),