Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimEnforcer v0.19.0
plugins/ValheimEnforcer.dll
Decompiled 19 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; 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 TMPro; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; using ValheimEnforcer; using ValheimEnforcer.common; using ValheimEnforcer.modules; using ValheimEnforcer.modules.character; using ValheimEnforcer.modules.cheatmonitor; using ValheimEnforcer.modules.commands; using ValheimEnforcer.modules.compat; using ValheimEnforcer.modules.compat.ExtraSlots; using ValheimEnforcer.modules.migration; using ValheimEnforcer.modules.mods; using ValheimEnforcer.modules.notifications; 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.19.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.19.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)) { CharacterDeltaTracker.LastDeltaSyncTime = Time.unscaledTime + (float)ValConfig.DeltaSynchronizationFrequencyInSeconds.Value; CharacterDeltaTracker.ClearDirty(); SyncChangesToServer(); } } private static void SyncChangesToServer() { //IL_0334: Unknown result type (might be due to invalid IL or missing references) //IL_033b: Expected O, but got Unknown Logger.LogDebug("Checking for character changes to sync to server..."); List<DataObjects.ItemDelta> list = CharacterDeltaTracker.BuildCharacterItemDeltas(); Dictionary<string, string> customData = Player.m_localPlayer.m_customData; Dictionary<string, string> dictionary = new Dictionary<string, string>(); List<string> list2 = new List<string>(); foreach (KeyValuePair<string, string> item in customData) { if (CharacterManager.PlayerCharacter.PlayerCustomData.ContainsKey(item.Key)) { if (CharacterManager.PlayerCharacter.PlayerCustomData[item.Key] != item.Value) { dictionary.Add(item.Key, item.Value); } } else { dictionary.Add(item.Key, item.Value); } } foreach (KeyValuePair<string, string> playerCustomDatum in CharacterManager.PlayerCharacter.PlayerCustomData) { if (!customData.ContainsKey(playerCustomDatum.Key)) { list2.Add(playerCustomDatum.Key); } } if (list.Count == 0 && dictionary.Count == 0 && list2.Count == 0) { return; } Logger.LogDebug("Changes found, syncing deltas."); List<DataObjects.PackedItem> list3 = new List<DataObjects.PackedItem>(); foreach (ItemData allItem in ((Humanoid)Player.m_localPlayer).GetInventory().GetAllItems()) { list3.Add(CharacterDeltaTracker.BuildPackedItem(allItem)); } CharacterManager.PlayerCharacter.PlayerItems = list3; CharacterManager.PlayerCharacter.PlayerCustomData = customData; CharacterManager.PlayerCharacter.SkillLevels = ((Character)Player.m_localPlayer).GetSkills().GetSkillList().ToDictionary((Skill s) => s.m_info.m_skill, (Skill s) => s.m_level); Dictionary<string, DataObjects.PackedStatusEffect> dictionary2 = new Dictionary<string, DataObjects.PackedStatusEffect>(); foreach (StatusEffect statusEffect in ((Character)Player.m_localPlayer).GetSEMan().GetStatusEffects()) { dictionary2.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: {list3.Count} items."); return; } DataObjects.DeltaSummaryUpdate deltaSummaryUpdate = new DataObjects.DeltaSummaryUpdate { Name = CharacterManager.PlayerCharacter.Name, HostID = CharacterManager.PlayerCharacter.HostID, DisconnectionState = DataObjects.DisconnectionState.DirtyDisconnect, ItemModifications = list, SkillLevels = ((Character)Player.m_localPlayer).GetSkills().GetSkillList().ToDictionary((Skill s) => s.m_info.m_skill, (Skill s) => s.m_level), PlayerCustomDataModifications = dictionary, RemovedCustomDataKeys = list2, ActiveCharacterEffects = dictionary2 }; ZPackage val = new ZPackage(); val.Write(DataObjects.yamlserializer.Serialize((object)deltaSummaryUpdate)); ValConfig.ItemDeltaUpdateRPC.SendPackage(serverPeer.m_uid, val); Logger.LogDebug($"Delta flush: {list.Count} items, {dictionary.Count} ({list2.Count} removed) custom data changes. Skill levels updated."); } } namespace ValheimEnforcer { internal class ValConfig { public static ConfigFile cfg; public static ConfigEntry<bool> EnableDebugMode; public static ConfigEntry<bool> UpdateLoadedModsOnStartup; public static ConfigEntry<bool> AutoAddModsToRequired; public static ConfigEntry<string> HashEnforcement; public static ConfigEntry<bool> RecordHashesForLoadedMods; public static ConfigEntry<bool> ResolveThunderstoreHashes; public static ConfigEntry<int> HashComputeTimeoutSeconds; public static ConfigEntry<int> ThunderstoreMaxArchiveMB; public static ConfigEntry<bool> RemoveNontrackedItemsFromJoiningPlayers; public static ConfigEntry<bool> AddMissingItemsFromPlayerServerSave; public static ConfigEntry<bool> PreventExternalSkillRaises; public static ConfigEntry<bool> NewCharactersRemoveExtraItems; public static ConfigEntry<bool> NewCharacterSetSkillsToZero; public static ConfigEntry<bool> newCharacterClearCustomData; 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> 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> EnableCheatDetection; public static ConfigEntry<bool> DetectCheatEngine; public static ConfigEntry<bool> DetectValheimTooler; public static ConfigEntry<bool> DetectCheatTools; public static ConfigEntry<bool> DetectGenericTrainers; public static ConfigEntry<bool> ScanLoadedModules; public static ConfigEntry<bool> ScanWindowTitles; public static ConfigEntry<string> AdditionalCheatProcesses; public static ConfigEntry<string> IgnoredCheatProcesses; public static ConfigEntry<string> CheatDetectionAction; public static ConfigEntry<int> CheatScanIntervalSeconds; 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<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; internal const string ModsFileName = "Mods.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 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 ReturnConfiscatedItemsRPC; internal static CustomRPC CheatDetectionRPC; internal static CustomRPC ItemDeltaUpdateRPC; internal static CustomRPC ListPlayerRPC; internal static CustomRPC ClearConfiscatedRPC; internal static CustomRPC FullSyncRequestRPC; internal static CustomRPC ImportServerCharactersRPC; internal static CustomRPC TestNotificationRPC; private static DateTime lastTestNotification = DateTime.MinValue; private static readonly TimeSpan TestNotificationCooldown = TimeSpan.FromSeconds(3.0); private const double DriftResyncCooldownSeconds = 60.0; private static readonly ConcurrentDictionary<string, DateTime> lastDriftResync = new ConcurrentDictionary<string, DateTime>(); 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 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(OnServerReturnConfiscatedReceive), new CoroutineHandler(OnClientReceiveConfiscatedItems)); CheatDetectionRPC = NetworkManager.Instance.AddRPC("VENFORCE_CHEAT", new CoroutineHandler(OnServerReceiveCheatReport), new CoroutineHandler(OnClientReceiveCheatReport)); ItemDeltaUpdateRPC = NetworkManager.Instance.AddRPC("VENFORCE_ITEMDELTA", new CoroutineHandler(OnServerRecieveDeltaItemUpdate), new CoroutineHandler(OnClientReceiveDeltaItemUpdate)); ListPlayerRPC = NetworkManager.Instance.AddRPC("VENFORCE_LIST_PLAYER", new CoroutineHandler(OnServerReceiveListPlayer), new CoroutineHandler(OnClientReceiveListPlayer)); ClearConfiscatedRPC = NetworkManager.Instance.AddRPC("VENFORCE_CLEAR_CONFISCATED", new CoroutineHandler(OnServerRecieveClearConfiscated), new CoroutineHandler(OnClientReceiveClearConfiscated)); FullSyncRequestRPC = NetworkManager.Instance.AddRPC("VENFORCE_FULLSYNC_REQ", new CoroutineHandler(OnServerReceiveFullSyncRequest), new CoroutineHandler(OnClientReceiveFullSyncRequest)); ImportServerCharactersRPC = NetworkManager.Instance.AddRPC("VENFORCE_IMPORT_SC", new CoroutineHandler(OnServerReceiveImportRequest), new CoroutineHandler(OnClientReceiveImportReport)); TestNotificationRPC = NetworkManager.Instance.AddRPC("VENFORCE_TEST_NOTIFY", new CoroutineHandler(OnServerReceiveTestNotification), new CoroutineHandler(OnClientReceiveTestNotificationReport)); SynchronizationManager.Instance.AddInitialSynchronization(CharacterSaveRPC, (Func<ZNetPeer, ZPackage>)SendSavedCharacter); LoadYamlConfigs(new Dictionary<string, Action<string>> { { ModsConfigFilePath, CreateModsFile }, { KnownCheatersFilePath, CreateKnownCheatersFile }, { NotificationsFilePath, CreateNotificationsFile } }); KnownCheaterTracker.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(); 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."); 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" })); 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: false, "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. Only thunderstore.io and its CDN are ever contacted; arbitrary download URLs are deliberately not supported. Off by default because it makes outbound network requests."); 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"); 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."); 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; on a dirty reconnect restoration is skipped by default (to avoid duping items consumed in the unsaved window) unless this is enabled."); 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); 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); 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."); DetectValheimTooler = BindServerConfig("Anti-Cheat", "DetectValheimTooler", value: true, "Detect ValheimTooler by the namespace of the types it loads (rename-proof), including assemblies injected mid-session. A confirmed detection is always auto-banned regardless of ActionOnDetection. High confidence, very low cost."); 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. 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."); 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."); CheatDetectionAction = BindServerConfig("Anti-Cheat", "ActionOnDetection", "Kick", "Server-side action taken when a cheat tool is reported. Note that dedicated game-cheating tools (injectors, ValheimTooler, ValHack, Valheim Mod Menu) are always auto-banned regardless of this setting, and low-confidence sightings (generic window classes) are always logged only, regardless of this setting.", 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. ValheimTooler assembly detection is event-driven and not affected by this interval.", advanced: false, 5, 300); DiscordWebhookUrl = BindLocalConfig("Discord", "WebhookUrl", "", "Discord webhook URL the server posts notifications to. This is a server-only secret and is never synced to clients. Leave empty to disable. Note: player names are sent to Discord when enabled. Every category falls back to this URL unless it has one of its own, so a server that wants everything in one channel only needs this setting."); DiscordWebhookUrlPlayerActivity = BindLocalConfig("Discord", "WebhookUrlPlayerActivity", "", "Webhook URL for player joins and leaves. Leave empty to use WebhookUrl. Set this to keep routine join/leave traffic out of the channel you actually watch - it is by far the noisiest category on a busy server."); DiscordWebhookUrlServerStatus = BindLocalConfig("Discord", "WebhookUrlServerStatus", "", "Webhook URL for server startup, shutdown and world-save messages. Leave empty to use WebhookUrl."); DiscordWebhookUrlModeration = BindLocalConfig("Discord", "WebhookUrlModeration", "", "Webhook URL for cheat bans and character-limit rejections. Leave empty to use WebhookUrl. This is the one worth pointing at a private moderator channel: the messages name the account behind a ban."); DiscordWebhookUrlModMismatch = BindLocalConfig("Discord", "WebhookUrlModMismatch", "", "Webhook URL for connections refused over a mod mismatch. Leave empty to use WebhookUrl. Often worth a support channel of its own, since the message lists exactly which mods the player needs to fix."); DiscordServerLabel = BindLocalConfig("Discord", "ServerLabel", "", "Name for this server in notification messages, available to templates as the {server} placeholder. Empty by default, and no built-in template uses it - set it only if several servers post into the same channel and you need to tell them apart. Deliberately a setting rather than the server's advertised name, so it also works on a player-hosted world."); DiscordNotifyServerStartup = BindLocalConfig("Discord", "NotifyServerStartup", value: true, "Post a message when the server comes online."); DiscordNotifyServerShutdown = BindLocalConfig("Discord", "NotifyServerShutdown", value: true, "Post a message when the server shuts down."); DiscordNotifyWorldSaved = BindLocalConfig("Discord", "NotifyWorldSaved", value: false, "Post a message every time the world is saved, covering both the periodic autosave and a manual 'save' from the console. Off by default because the autosave fires roughly every twenty minutes, all day, whether or not anyone is playing - on most servers that buries everything else in the channel. Worth turning on temporarily when you are chasing a save problem, or permanently if it has its own channel via WebhookUrlServerStatus."); DiscordNotifyPlayerJoined = BindLocalConfig("Discord", "NotifyPlayerJoined", value: true, "Post a message when a player joins."); DiscordNotifyPlayerLeft = BindLocalConfig("Discord", "NotifyPlayerLeft", value: true, "Post a message when a player leaves, including whether their saved data is up to date."); DiscordNotifyWrongMods = BindLocalConfig("Discord", "NotifyWrongMods", value: true, "Post a message when a player is rejected for a mod mismatch, listing the offending mods."); DiscordNotifyCheaterBanned = BindLocalConfig("Discord", "NotifyCheaterBanned", value: true, "Post a message when a player is banned for cheat usage, including the detected cheat(s)."); DiscordNotifyCharacterRejected = BindLocalConfig("Discord", "NotifyCharacterRejected", value: true, "Post a message when a connection is refused by EnforceCharacterLimit, naming the character that was turned away."); } internal static void WritePlayerCharacterToSave(string id, DataObjects.Character character, bool routine = false) { if (InternalStorageMode.Value) { if (routine) { Logger.LogDebug("Saving character with internal storage mode."); } else { Logger.LogInfo("Saving character with internal storage mode."); } InternalDataStore.SaveAccountCharacter(character); } Directory.CreateDirectory(Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters")); string text = Path.Combine(Directory.CreateDirectory(Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters", id)).FullName, character.Name + ".yaml"); if (routine) { Logger.LogDebug("Writing to " + text); } else { Logger.LogInfo("Writing to " + text); } try { File.WriteAllText(text, DataObjects.yamlserializer.Serialize((object)character)); } catch (Exception ex) { Logger.LogWarning("Failed to write character data to disk at " + text + ": " + ex.Message); } } internal static DataObjects.Character LoadCharacterFromSave(string id, string name) { if (InternalStorageMode.Value) { Logger.LogInfo("Loading character from internal storage system."); DataObjects.Character accountCharacter = InternalDataStore.GetAccountCharacter(id, name); if (accountCharacter == null) { Logger.LogDebug("No character file found for player with " + id + "-" + name + " is this character new?"); } return accountCharacter; } string path = Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters", id, name + ".yaml"); if (!File.Exists(path)) { Logger.LogDebug("No character file found for player with " + id + "-" + name + " is this character new?"); return null; } string text = File.ReadAllText(path); return DataObjects.yamldeserializer.Deserialize<DataObjects.Character>(text); } public static string GetSecondaryConfigDirectoryPath() { string text = Path.Combine(Paths.ConfigPath, "ValheimEnforcer"); if (!Directory.Exists(text)) { Directory.CreateDirectory(text); } return text; } internal void LoadYamlConfigs(Dictionary<string, Action<string>> configFilesToFind) { string[] files = Directory.GetFiles(GetSecondaryConfigDirectoryPath()); List<string> list = new List<string>(); List<string> list2 = configFilesToFind.Keys.ToList(); string[] array = files; foreach (string text in array) { if (list2.Contains(text)) { list.Add(text); Logger.LogDebug("Found config: " + text); } } foreach (KeyValuePair<string, Action<string>> item in configFilesToFind) { if (!list.Contains(item.Key)) { configFilesToFind[item.Key](item.Key); list.Add(item.Key); } } foreach (string item2 in list) { string fileName = Path.GetFileName(item2); Logger.LogDebug("Setting filewatcher for " + fileName); SetupFileWatcher(item2); } } private void SetupFileWatcher(string fullPath) { ConfigFileWatcher.Register(fullPath, UpdateConfigFileOnChange); } private static void UpdateConfigFileOnChange(string filepath) { if (!SynchronizationManager.Instance.PlayerIsAdmin) { Logger.LogInfo("Player is not an admin, and not allowed to change local configuration. Ignoring."); } else { if (!File.Exists(filepath)) { return; } string text = File.ReadAllText(filepath); FileInfo fileInfo = new FileInfo(filepath); Logger.LogDebug("Filewatch changes from: (" + fileInfo.Name + ") " + fileInfo.FullName); switch (fileInfo.Name) { case "Mods.yaml": Logger.LogDebug("Triggering Mod Settings update."); ModManager.UpdateModSettingConfigs(text); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { ThunderstoreResolver.RequestPass("Mods.yaml changed"); } break; case "KnownCheaters.yaml": Logger.LogDebug("Triggering KnownCheaters list update."); KnownCheaterTracker.LoadFromText(text); break; case "Notifications.yaml": Logger.LogDebug("Triggering notification template update."); NotificationTemplates.LoadFromText(text); break; } } } private static void CreateModsFile(string filepath) { Logger.LogDebug("Mods config missing, recreating."); using StreamWriter streamWriter = new StreamWriter(filepath); streamWriter.WriteLine(string.Join(Environment.NewLine, ModManager.ModsFileHeaderLines)); streamWriter.WriteLine(); streamWriter.WriteLine(ModManager.GetDefaultConfig()); } private static void CreateNotificationsFile(string filepath) { Logger.LogDebug("Notification templates file missing, recreating."); File.WriteAllText(filepath, NotificationTemplates.GetDefaultConfig()); } private static void CreateKnownCheatersFile(string filepath) { Logger.LogDebug("KnownCheaters file missing, recreating."); using StreamWriter streamWriter = new StreamWriter(filepath); string value = "#################################################\n# Valheim Enforcer - Known Cheaters (server side)\n# Auto-populated when cheaters are banned. Entries: { id, reason }\n#################################################\n"; streamWriter.WriteLine(value); } internal static ZPackage SendSavedCharacter(ZNetPeer peer) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Expected O, but got Unknown //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Expected O, but got Unknown string endPointString = peer.m_socket.GetEndPointString(); Logger.LogInfo("Sending saved character data to player " + peer.m_playerName + " with ID: " + endPointString); ZPackage val = new ZPackage(); if (InternalStorageMode.Value) { Logger.LogInfo("Using internal storage mode to send character data."); DataObjects.Character accountCharacter = InternalDataStore.GetAccountCharacter(endPointString, peer.m_playerName); if (accountCharacter == null) { Logger.LogInfo("No character data found for player " + peer.m_playerName + " with ID: " + endPointString + ", no character data will be sent."); return new ZPackage(); } return SendCharacterToClientAsZpackage(accountCharacter); } string text = Path.Combine(Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters", endPointString ?? ""), peer.m_playerName + ".yaml"); bool flag = File.Exists(text); DateTime dateTime = (flag ? File.GetLastWriteTimeUtc(text) : DateTime.MinValue); string yamlIfCurrent = CharacterStore.GetYamlIfCurrent(endPointString, peer.m_playerName, dateTime); if (yamlIfCurrent != null) { val.Write(StripConfiscatedItemsFromYaml(yamlIfCurrent)); return val; } if (!flag) { Logger.LogInfo("path: " + text + " does not exist, no character data will be sent."); return new ZPackage(); } string yaml = File.ReadAllText(text); CharacterStore.Seed(endPointString, peer.m_playerName, yaml, dateTime); val.Write(StripConfiscatedItemsFromYaml(yaml)); return val; } public static IEnumerator OnServerRecieveCharacter(long sender, ZPackage package) { string yaml = package.ReadString(); PersistReceivedCharacterYaml(sender, yaml); yield break; } internal static void PersistReceivedCharacterYaml(long sender, string yaml) { if (InternalStorageMode.Value) { try { DataObjects.Character character = DataObjects.yamldeserializer.Deserialize<DataObjects.Character>(yaml); Logger.LogInfo($"Recieved Player data update for {sender} - {character.Name}|{character.HostID}"); DataObjects.Character accountCharacter = InternalDataStore.GetAccountCharacter(character.HostID, character.Name); List<DataObjects.PackedItem> confiscatedItems = character.ConfiscatedItems; character.ConfiscatedItems = accountCharacter?.ConfiscatedItems ?? new List<DataObjects.PackedItem>(); int num = character.MergeConfiscatedItems(confiscatedItems); if (num > 0) { Logger.LogInfo($"Recorded {num} newly confiscated item(s) for {character.Name}."); } WritePlayerCharacterToSave(character.HostID, character); return; } catch (Exception ex) { Logger.LogWarning($"Failed to deserialize character data from {sender}: {ex.Message}"); return; } } CharacterStore.SubmitFullSave(yaml); } public static IEnumerator OnServerRecieveClearConfiscated(long sender, ZPackage package) { DataObjects.RPCServerUpdateData rPCServerUpdateData = DataObjects.yamldeserializer.Deserialize<DataObjects.RPCServerUpdateData>(package.ReadString()); ZNetPeer peerByPlatformID = GetPeerByPlatformID(rPCServerUpdateData.PlatformID); if (peerByPlatformID == null) { Logger.LogWarning("Could not find peer with PlatformID " + rPCServerUpdateData.PlatformID + " to clear confiscated items."); yield break; } CommandHelpers.ClearSpecifiedPlayerConfiscatedItems(rPCServerUpdateData.PlatformID, rPCServerUpdateData.PlayerName, rPCServerUpdateData.ItemPrefabFilter); ClearConfiscatedRPC.SendPackage(peerByPlatformID.m_uid, package); } public static IEnumerator OnClientReceiveClearConfiscated(long sender, ZPackage package) { DataObjects.RPCServerUpdateData rPCServerUpdateData = DataObjects.yamldeserializer.Deserialize<DataObjects.RPCServerUpdateData>(package.ReadString()); CommandHelpers.ClearSpecifiedPlayerConfiscatedItems(rPCServerUpdateData.PlatformID, rPCServerUpdateData.PlayerName, rPCServerUpdateData.ItemPrefabFilter); ClearInMemoryConfiscatedItems(rPCServerUpdateData.ItemPrefabFilter); yield break; } private static void ClearInMemoryConfiscatedItems(string prefabFilter) { DataObjects.Character playerCharacter = CharacterManager.PlayerCharacter; if (playerCharacter?.ConfiscatedItems == null || playerCharacter.ConfiscatedItems.Count == 0) { return; } int count = playerCharacter.ConfiscatedItems.Count; if (string.Compare(prefabFilter, "all", ignoreCase: true) == 0) { playerCharacter.ConfiscatedItems.Clear(); } else { List<string> targets = (from s in prefabFilter.Split(new char[1] { ',' }) select s.Trim()).ToList(); playerCharacter.ConfiscatedItems.RemoveAll((DataObjects.PackedItem i) => i != null && targets.Contains(i.prefabName)); } Logger.LogDebug($"Cleared {count - playerCharacter.ConfiscatedItems.Count} tracked confiscated item(s) locally."); } public static IEnumerator OnClientReceiveCharacter(long sender, ZPackage package) { DataObjects.Character playerCharacter = DataObjects.yamldeserializer.Deserialize<DataObjects.Character>(package.ReadString()); Logger.LogDebug("Recieved Player character data from server."); CharacterManager.SetPlayerCharacter(playerCharacter); yield break; } public static IEnumerator OnServerReturnConfiscatedReceive(long sender, ZPackage package) { DataObjects.RPCServerUpdateData rPCServerUpdateData = DataObjects.yamldeserializer.Deserialize<DataObjects.RPCServerUpdateData>(package.ReadString()); List<DataObjects.PackedItem> list = CommandHelpers.LoadCharacterAndFindItemsToReturn(rPCServerUpdateData.PlatformID, rPCServerUpdateData.PlayerName, rPCServerUpdateData.ItemPrefabFilter); DataObjects.Character character = LoadCharacterFromSave(rPCServerUpdateData.PlatformID, rPCServerUpdateData.PlayerName); ZNetPeer peerByPlatformID = GetPeerByPlatformID(rPCServerUpdateData.PlatformID); if (peerByPlatformID == null) { Logger.LogInfo("Player " + rPCServerUpdateData.PlayerName + " is not currently connected. Moving items to player inventory save so they are restored on next login."); foreach (DataObjects.PackedItem item in list) { character.PlayerItems.Add(item); } WritePlayerCharacterToSave(rPCServerUpdateData.PlatformID, character); if (InternalStorageMode.Value) { Logger.LogInfo("Also updating character data in internal storage."); InternalDataStore.SaveAccountCharacter(character); } yield break; } Logger.LogInfo($"Sending {list.Count} confiscated item(s) to player {rPCServerUpdateData.PlayerName}."); WritePlayerCharacterToSave(rPCServerUpdateData.PlatformID, character); CharacterStore.Invalidate(rPCServerUpdateData.PlatformID, rPCServerUpdateData.PlayerName); if (InternalStorageMode.Value) { Logger.LogInfo("Also updating character data in internal storage."); InternalDataStore.SaveAccountCharacter(character); } ZPackage val = new ZPackage(); val.Write(DataObjects.yamlserializer.Serialize((object)list)); ReturnConfiscatedItemsRPC.SendPackage(peerByPlatformID.m_uid, val); CharacterSaveRPC.SendPackage(peerByPlatformID.m_uid, SendCharacterToClientAsZpackage(character)); } public static IEnumerator OnServerReceiveCheatReport(long sender, ZPackage package) { string text = package.ReadString(); DataObjects.CheatSummaryReport cheatSummaryReport; try { cheatSummaryReport = DataObjects.yamldeserializer.Deserialize<DataObjects.CheatSummaryReport>(text); } catch (Exception ex) { Logger.LogWarning($"Failed to deserialize cheat report from {sender}: {ex.Message}"); yield break; } ZNetPeer peer = ZNet.instance.GetPeer(sender); string playerName = cheatSummaryReport.PlayerName; if (peer == null) { Logger.LogWarning("Received cheat report for " + playerName + " but could not find corresponding peer. No action will be taken."); yield break; } string hostName = peer.m_socket.GetHostName(); string endPointString = peer.m_socket.GetEndPointString(); Logger.LogWarning($"Cheat detection from {playerName} ({endPointString}): valheim-tooler: {cheatSummaryReport.ValheimToolerStatus} tools: {DescribeDetectedTools(cheatSummaryReport)}"); if (cheatSummaryReport.ValheimToolerStatus) { Logger.LogWarning("Banning " + playerName + " for ValheimTooler usage."); BanCheater(peer, playerName, cheatSummaryReport); yield break; } List<DataObjects.CheatToolDetection> list = new List<DataObjects.CheatToolDetection>(); if (cheatSummaryReport.DetectedTools != null) { foreach (DataObjects.CheatToolDetection detectedTool in cheatSummaryReport.DetectedTools) { if (!detectedTool.Weak) { list.Add(detectedTool); } } } foreach (DataObjects.CheatToolDetection item in list) { if (CheatToolCatalog.IsAutoBan(item.Tool)) { Logger.LogWarning("Banning " + playerName + " for " + item.Tool + " usage."); BanCheater(peer, playerName, cheatSummaryReport); yield break; } } if (list.Count == 0) { Logger.LogWarning("Low-confidence sighting from " + playerName + " (" + endPointString + "), logged without action: " + DescribeDetectedTools(cheatSummaryReport)); yield break; } switch (CheatDetectionAction.Value ?? "Log") { case "Kick": Logger.LogWarning("Kicking " + playerName + " for cheat usage."); ZNet.instance.Kick(hostName); break; case "Ban": Logger.LogWarning("Banning " + playerName + " for cheat usage."); BanCheater(peer, playerName, cheatSummaryReport); break; case "Log": break; } } private static void BanCheater(ZNetPeer peer, string playerName, DataObjects.CheatSummaryReport summary) { string hostName = peer.m_socket.GetHostName(); string text = BuildCheatReason(summary); KnownCheaterTracker.AddCheater(hostName, text); ZNet.instance.Ban(hostName); if (DiscordNotifyCheaterBanned.Value) { DiscordNotifier.Notify(NotificationEvent.CheaterBanned, new Dictionary<string, string> { { "player", playerName }, { "playerId", hostName }, { "reason", text }, { "detections", DescribeDetectedTools(summary) }, { "action", "Ban" } }); } } private static string BuildCheatReason(DataObjects.CheatSummaryReport summary) { List<string> list = new List<string>(); if (summary.ValheimToolerStatus) { list.Add("ValheimTooler"); } if (summary.DetectedTools != null) { foreach (DataObjects.CheatToolDetection detectedTool in summary.DetectedTools) { list.Add(detectedTool.Tool + " (" + detectedTool.Vector + ": " + detectedTool.Detail + ")" + (detectedTool.Weak ? " (weak)" : "")); } } string text = ((list.Count > 0) ? string.Join(", ", list) : "cheat detected"); return "Cheat detection: " + text; } private static string DescribeDetectedTools(DataObjects.CheatSummaryReport summary) { if (summary.DetectedTools == null || summary.DetectedTools.Count == 0) { return "none"; } return string.Join(", ", summary.DetectedTools.Select((DataObjects.CheatToolDetection d) => d.Tool + " [" + d.Vector + ": " + d.Detail + "]" + (d.Weak ? " (weak)" : ""))); } public static IEnumerator OnClientReceiveCheatReport(long sender, ZPackage package) { yield break; } public static IEnumerator OnClientReceiveImportReport(long sender, ZPackage package) { string[] array = package.ReadString().Split(new char[1] { '\n' }); for (int i = 0; i < array.Length; i++) { Logger.LogInfo(array[i].TrimEnd(Array.Empty<char>())); } yield break; } public static IEnumerator OnServerReceiveImportRequest(long sender, ZPackage package) { ZNet instance = ZNet.instance; ZNetPeer obj = ((instance != null) ? instance.GetPeer(sender) : null); object obj2; if (obj == null) { obj2 = null; } else { ISocket socket = obj.m_socket; obj2 = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj2; if (string.IsNullOrEmpty(text) || !ZNet.instance.IsAdmin(text)) { Logger.LogWarning("Ignoring a ServerCharacters import request from non-admin " + (text ?? sender.ToString()) + "."); yield break; } string obj3 = package.ReadString() ?? ""; bool force = obj3.IndexOf("force", StringComparison.OrdinalIgnoreCase) >= 0; bool dryRun = obj3.IndexOf("import", StringComparison.OrdinalIgnoreCase) < 0; string text2; try { text2 = ServerCharactersImport.Run(dryRun, force).Summary(); } catch (Exception ex) { text2 = "ServerCharacters import failed: " + ex.Message; Logger.LogError($"ServerCharacters import failed: {ex}"); } Logger.LogInfo(text2); ZPackage val = new ZPackage(); val.Write(text2); ImportServerCharactersRPC.SendPackage(sender, val); } public static IEnumerator OnClientReceiveTestNotificationReport(long sender, ZPackage package) { string[] array = package.ReadString().Split(new char[1] { '\n' }); for (int i = 0; i < array.Length; i++) { Logger.LogInfo(array[i].TrimEnd(Array.Empty<char>())); } yield break; } public static IEnumerator OnServerReceiveTestNotification(long sender, ZPackage package) { ZNet instance = ZNet.instance; ZNetPeer obj = ((instance != null) ? instance.GetPeer(sender) : null); object obj2; if (obj == null) { obj2 = null; } else { ISocket socket = obj.m_socket; obj2 = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj2; if (string.IsNullOrEmpty(text) || !ZNet.instance.IsAdmin(text)) { Logger.LogWarning("Ignoring a test notification request from non-admin " + (text ?? sender.ToString()) + "."); yield break; } string text2 = package.ReadString() ?? ""; string text3; if (!Enum.TryParse<NotificationEvent>(text2, ignoreCase: true, out var result) || !Enum.IsDefined(typeof(NotificationEvent), result)) { text3 = "Unknown notification event '" + text2 + "'. One of: " + string.Join(", ", Enum.GetNames(typeof(NotificationEvent))); } else if (DateTime.UtcNow - lastTestNotification < TestNotificationCooldown) { text3 = "A test notification was just sent - wait a moment before sending another."; } else if (!DiscordNotifier.IsValidWebhookUrl(DiscordNotifier.ResolveUrl(NotificationTemplates.CategoryOf(result)))) { text3 = $"No usable webhook URL for the {NotificationTemplates.CategoryOf(result)} category. Set Discord.WebhookUrl on the server, or the URL for that category."; } else { lastTestNotification = DateTime.UtcNow; DiscordNotifier.Notify(result, NotificationTemplates.SampleTokens()); text3 = $"Posted a sample {result} notification to the {NotificationTemplates.CategoryOf(result)} webhook."; Logger.LogInfo(text3 + " Requested by admin " + text + "."); } ZPackage val = new ZPackage(); val.Write(text3); TestNotificationRPC.SendPackage(sender, val); } public static IEnumerator OnClientReceiveListPlayer(long sender, ZPackage package) { foreach (KeyValuePair<string, List<string>> item in DataObjects.yamldeserializer.Deserialize<Dictionary<string, List<string>>>(package.ReadString())) { Logger.LogInfo("AccountID: " + item.Key); foreach (string item2 in item.Value) { Logger.LogInfo(" Character: " + item2); } } yield break; } public static IEnumerator OnServerReceiveListPlayer(long sender, ZPackage package) { Dictionary<string, List<string>> dictionary = new Dictionary<string, List<string>>(); if (InternalStorageMode.Value) { dictionary = InternalDataStore.GetAccountRegistry(); ListPlayerRPC.SendPackage(sender, new ZPackage(DataObjects.yamlserializer.Serialize((object)dictionary))); yield break; } foreach (string item in Directory.GetFiles(Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters")).ToList()) { List<string> list = Directory.GetFiles(item).ToList(); string key = item.Split(new char[1] { '/' }).Last(); List<string> list2 = new List<string>(); foreach (string item2 in list) { list2.Add(item2.Split(new char[1] { '/' }).Last()); } dictionary.Add(key, list2); } ListPlayerRPC.SendPackage(sender, new ZPackage(DataObjects.yamlserializer.Serialize((object)dictionary))); } public static IEnumerator OnClientReceiveConfiscatedItems(long sender, ZPackage package) { List<DataObjects.PackedItem> list = DataObjects.yamldeserializer.Deserialize<List<DataObjects.PackedItem>>(package.ReadString()); Logger.LogInfo($"Received {list.Count} confiscated item(s) returned from server."); foreach (DataObjects.PackedItem item in list) { Logger.LogInfo($"Adding returned confiscated item: {item.prefabName} x{item.m_stack}"); item.AddToInventory(Player.m_localPlayer, use_position: false); } yield break; } internal static IEnumerator OnServerRecieveDeltaItemUpdate(long sender, ZPackage package) { string text = package.ReadString(); DataObjects.DeltaSummaryUpdate deltaSummaryUpdate; try { deltaSummaryUpdate = DataObjects.yamldeserializer.Deserialize<DataObjects.DeltaSummaryUpdate>(text); } catch (Exception ex) { Logger.LogWarning($"Failed to deserialize delta update from {sender}: {ex.Message}"); yield break; } if (string.IsNullOrEmpty(deltaSummaryUpdate.Name) || string.IsNullOrEmpty(deltaSummaryUpdate.HostID)) { Logger.LogWarning($"Malformed delta update from {sender}: missing CharacterName or HostName."); } else if (InternalStorageMode.Value) { Logger.LogInfo("Loading character for delta update with internal storage mode."); DataObjects.Character accountCharacter = InternalDataStore.GetAccountCharacter(deltaSummaryUpdate.HostID, deltaSummaryUpdate.Name); if (accountCharacter == null) { RequestFullSync(sender, deltaSummaryUpdate); yield break; } Logger.LogInfo($"Received delta update from {deltaSummaryUpdate.Name} ({deltaSummaryUpdate.HostID}): {deltaSummaryUpdate.ItemModifications?.Count ?? 0} item delta(s)."); if (UpdatePlayerSaveWithDeltaData(deltaSummaryUpdate, accountCharacter)) { RequestFullSyncForDrift(sender, deltaSummaryUpdate.HostID, deltaSummaryUpdate.Name); } } else if (!CharacterStore.IsCached(deltaSummaryUpdate.HostID, deltaSummaryUpdate.Name) && !File.Exists(Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters", deltaSummaryUpdate.HostID, deltaSummaryUpdate.Name + ".yaml"))) { RequestFullSync(sender, deltaSummaryUpdate); } else { Logger.LogInfo($"Received delta update from {deltaSummaryUpdate.Name} ({deltaSummaryUpdate.HostID}): {deltaSummaryUpdate.ItemModifications?.Count ?? 0} item delta(s)."); CharacterStore.SubmitDelta(deltaSummaryUpdate, sender); } } private static void RequestFullSync(long sender, DataObjects.DeltaSummaryUpdate deltaUpdate) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown Logger.LogInfo("No saved data for " + deltaUpdate.Name + " (" + deltaUpdate.HostID + "); requesting a full character sync from the client. This delta is dropped and will be superseded by the full save."); ZPackage val = new ZPackage(); val.Write(deltaUpdate.Name); FullSyncRequestRPC.SendPackage(sender, val); } internal static void RequestFullSyncForDrift(long sender, string hostId, string name) { //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Expected O, but got Unknown string key = CharacterStore.KeyFor(hostId, name); DateTime utcNow = DateTime.UtcNow; if (lastDriftResync.TryGetValue(key, out var value) && (utcNow - value).TotalSeconds < 60.0) { Logger.LogDebug("Drift resync for " + name + " already requested recently; skipping."); return; } lastDriftResync[key] = utcNow; if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.GetPeer(sender) == null) { Logger.LogDebug($"Not requesting a drift resync for {name}: peer {sender} is no longer connected."); return; } Logger.LogInfo("Requesting a full character sync from " + name + " (" + hostId + ") to repair drifted server state."); ZPackage val = new ZPackage(); val.Write(name); FullSyncRequestRPC.SendPackage(sender, val); } public static IEnumerator OnClientReceiveDeltaItemUpdate(long sender, ZPackage package) { yield break; } public static IEnumerator OnServerReceiveFullSyncRequest(long sender, ZPackage package) { yield break; } public static IEnumerator OnClientReceiveFullSyncRequest(long sender, ZPackage package) { if ((Object)(object)Player.m_localPlayer == (Object)null) { Logger.LogWarning("Server requested a full character sync but the local player is null; cannot respond."); yield break; } Logger.LogInfo("Server requested a full character sync. Sending full character save."); CharacterManager.SavePlayerCharacter(Player.m_localPlayer); } internal static bool MergeDelta(DataObjects.DeltaSummaryUpdate deltaSummary, DataObjects.Character character) { bool result = false; foreach (DataObjects.ItemDelta itemModification in deltaSummary.ItemModifications) { switch (itemModification.Op) { case DataObjects.ItemDeltaChangeType.Added: character.PlayerItems.Add(itemModification.Item); Logger.LogDebug($"Delta: added {itemModification.Item.prefabName} x{itemModification.Item.m_stack}."); break; case DataObjects.ItemDeltaChangeType.Removed: if (!character.RemoveFromPlayerItems(itemModification.Item)) { result = true; Logger.LogWarning($"Delta removal for {character.Name} found no match for {itemModification.Item?.prefabName} x{itemModification.Item?.m_stack}; our copy has drifted from the client's baseline."); } break; } } Logger.LogDebug($"Applied {deltaSummary.ItemModifications.Count} item delta(s) for {character.Name}."); foreach (string removedCustomDataKey in deltaSummary.RemovedCustomDataKeys) { character.PlayerCustomData.Remove(removedCustomDataKey); } foreach (KeyValuePair<string, string> playerCustomDataModification in deltaSummary.PlayerCustomDataModifications) { character.PlayerCustomData[playerCustomDataModification.Key] = playerCustomDataModification.Value; } Logger.LogDebug("Updated custom data for " + character.Name + "."); character.SkillLevels = deltaSummary.SkillLevels; character.ActiveCharacterEffects = deltaSummary.ActiveCharacterEffects; character.LastDisconnect = deltaSummary.DisconnectionState; return result; } internal static bool UpdatePlayerSaveWithDeltaData(DataObjects.DeltaSummaryUpdate deltaSummary, DataObjects.Character character) { bool result = MergeDelta(deltaSummary, character); if (InternalStorageMode.Value) { Logger.LogInfo("Saving character with internal storage mode."); InternalDataStore.SaveAccountCharacter(character); } string text = Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters", deltaSummary.HostID); Directory.CreateDirectory(text); File.WriteAllText(Path.Combine(text, deltaSummary.Name + ".yaml"), DataObjects.yamlserializer.Serialize((object)character)); Logger.LogInfo("Saved delta update for " + character.Name + "."); return result; } internal static ZPackage SendCharacterAsZpackage(DataObjects.Character chara) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Expected O, but got Unknown string text = DataObjects.yamlserializer.Serialize((object)chara); ZPackage val = new ZPackage(); val.Write(text); return val; } internal static ZPackage SendCharacterToClientAsZpackage(DataObjects.Character chara) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Expected O, but got Unknown if (chara == null) { return new ZPackage(); } List<DataObjects.PackedItem> confiscatedItems = chara.ConfiscatedItems; try { chara.ConfiscatedItems = null; return SendCharacterAsZpackage(chara); } finally { chara.ConfiscatedItems = confiscatedItems; } } internal static string StripConfiscatedItemsFromYaml(string yaml) { if (string.IsNullOrEmpty(yaml)) { return yaml; } try { DataObjects.Character character = DataObjects.yamldeserializer.Deserialize<DataObjects.Character>(yaml); if (character == null) { return yaml; } character.ConfiscatedItems = null; return DataObjects.yamlserializer.Serialize((object)character); } catch (Exception ex) { Logger.LogWarning("Could not strip confiscated items from a character payload, sending it as-is: " + ex.Message); return yaml; } } public static ZNetPeer GetPeerByPlatformID(string platformID) { foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer.IsReady() && peer.m_socket.GetHostName() == platformID) { return peer; } } return null; } internal static void SetupMainFileWatcher() { ConfigFileWatcher.Register(cfg.ConfigFilePath, OnMainConfigFileChanged); } private static void OnMainConfigFileChanged(string _) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { Logger.LogInfo("Configuration file has been changed, reloading settings."); cfg.Reload(); } } public static ConfigEntry<string> BindLocalConfig(string catagory, string key, string value, string description, bool advanced = false) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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 return cfg.Bind<string>(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = false, IsAdvanced = advanced } })); } public static ConfigEntry<bool> BindLocalConfig(string catagory, string key, bool value, string description, bool advanced = false) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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 return cfg.Bind<bool>(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = false, IsAdvanced = advanced } })); } public static ConfigEntry<List<string>> BindServerConfig(string catagory, string key, List<string> value, string description, bool advanced = false) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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 return cfg.Bind<List<string>>(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<float[]> BindServerConfig(string catagory, string key, float[] value, string description, bool advanced = false, float valmin = 0f, float valmax = 150f) { //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_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return cfg.Bind<float[]>(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<float>(valmin, valmax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<bool> BindServerConfig(string catagory, string key, bool value, string description, AcceptableValueBase acceptableValues = null, bool advanced = false) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: 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_002d: Expected O, but got Unknown //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown return cfg.Bind<bool>(catagory, key, value, new ConfigDescription(description, acceptableValues, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<int> BindServerConfig(string catagory, string key, int value, string description, bool advanced = false, int valmin = 0, int valmax = 150) { //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_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return cfg.Bind<int>(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<int>(valmin, valmax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<float> BindServerConfig(string catagory, string key, float value, string description, bool advanced = false, float valmin = 0f, float valmax = 150f) { //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_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return cfg.Bind<float>(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<float>(valmin, valmax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<string> BindServerConfig(string catagory, string key, string value, string description, AcceptableValueList<string> acceptableValues = null, bool advanced = false) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: 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_002d: Expected O, but got Unknown //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown return cfg.Bind<string>(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)acceptableValues, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } } internal class Logger { public static LogLevel Level = (LogLevel)16; public static void EnableDebugLogging(object sender, EventArgs e) { CheckEnableDebugLogging(); } public static void CheckEnableDebugLogging() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (ValConfig.EnableDebugMode.Value) { Level = (LogLevel)32; } else { Level = (LogLevel)16; } } public static void SetDebugLogging(bool state) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) if (state) { Level = (LogLevel)32; } else { Level = (LogLevel)16; } } public static void LogDebug(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)Level >= 32) { ValheimEnforcer.Log.LogInfo((object)message); } } public static void LogInfo(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)Level >= 16) { ValheimEnforcer.Log.LogInfo((object)message); } } public static void LogWarning(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 if ((int)Level >= 4) { ValheimEnforcer.Log.LogWarning((object)message); } } public static void LogError(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 if ((int)Level >= 2) { ValheimEnforcer.Log.LogError((object)message); } } } [BepInPlugin("MidnightsFX.ValheimEnforcer", "ValheimEnforcer", "0.19.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInIncompatibility("org.bepinex.plugins.servercharacters")] internal class ValheimEnforcer : BaseUnityPlugin { public const string PluginGUID = "MidnightsFX.ValheimEnforcer"; public const string PluginName = "ValheimEnforcer"; public const string PluginVersion = "0.19.0"; internal static ManualLogSource Log; internal ValConfig cfg; public static CustomLocalization Localization = LocalizationManager.Instance.GetLocalization(); public static AssetBundle EmbeddedResourceBundle; public void Awake() { Log = ((BaseUnityPlugin)this).Logger; cfg = new ValConfig(((BaseUnityPlugin)this).Config); EmbeddedResourceBundle = AssetUtils.LoadAssetBundleFromResources("ValheimEnforcer.assets.vebundle", typeof(ValheimEnforcer).Assembly); PrefabManager.OnPrefabsRegistered += ModManager.SetModsActive; ZoneManager.OnLocationsRegistered += InternalDataStore.InstanciateOrLinkMetadataRegistry; PrefabManager.OnVanillaPrefabsAvailable += ModManager.SetModsActive; GUIManager.OnCustomGUIAvailable += ModManager.AddErrorMessageDetailsForMenu; InternalDataStore.RegisterMetadataHolder(); TerminalCommands.AddCommands(); MinimapManager.OnVanillaMapDataLoaded += CheatDetector.Initialize; MinimapManager.OnVanillaMapDataLoaded += CharacterDeltaTracker.Initialize; ModCompatability.CheckModCompat(); Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), (string)null); } } } namespace ValheimEnforcer.modules { internal static class InternalDataStore { private static ZDO MetadataRegistry; internal static void SaveAccountCharacter(DataObjects.Character character) { UpdateAccountRegistry(character.HostID, character.Name); string text = MetadataRegistry.GetString(character.HostID, (string)null); if (text != null) { DataObjects.CharacterSaveData characterSaveData = DataObjects.yamldeserializer.Deserialize<DataObjects.CharacterSaveData>(text); if (characterSaveData.SavedCharacters.ContainsKey(character.Name)) { characterSaveData.SavedCharacters[character.Name] = character; } else { characterSaveData.SavedCharacters.Add(character.Name, character); } string text2 = DataObjects.yamlserializer.Serialize((object)characterSaveData); MetadataRegistry.Set(character.HostID, text2); } else { DataObjects.CharacterSaveData characterSaveData2 = new DataObjects.CharacterSaveData { SavedCharacters = new Dictionary<string, DataObjects.Character> { { character.Name, character } } }; string text3 = DataObjects.yamlserializer.Serialize((object)characterSaveData2); MetadataRegistry.Set(character.HostID, text3); } } internal static DataObjects.Character GetAccountCharacter(string accountID, string characterName) { InstanciateOrLinkMetadataRegistry(); string text = MetadataRegistry.GetString(accountID, (string)null); if (text != null) { Logger.LogDebug("Character data found " + accountID + "-" + characterName + "."); DataObjects.CharacterSaveData characterSaveData = DataObjects.yamldeserializer.Deserialize<DataObjects.CharacterSaveData>(text); if (characterSaveData.SavedCharacters.ContainsKey(characterName)) { return characterSaveData.SavedCharacters[characterName]; } } return null; } internal static DataObjects.CharacterSaveData GetAccountData(string accountID) { InstanciateOrLinkMetadataRegistry(); string text = MetadataRegistry.GetString(accountID, (string)null); if (text != null) { return DataObjects.yamldeserializer.Deserialize<DataObjects.CharacterSaveData>(text); } return null; } internal static void RegisterMetadataHolder() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected O, but got Unknown CustomPrefab val = new CustomPrefab(ValheimEnforcer.EmbeddedResourceBundle.LoadAsset<GameObject>("VE_METADATA"), false); PrefabManager.Instance.AddPrefab(val); } internal static void InstanciateOrLinkMetadataRegistry() { //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) if (!ValConfig.InternalStorageMode.Value || MetadataRegistry != null) { return; } long sessionID = ZDOMan.GetSessionID(); string text = default(string); if (ZoneSystem.instance.GetGlobalKey(DataObjects.CustomDataKey ?? "", ref text)) { string[] array = text.Split(new char[1] { ' ' }); if (array.Length == 2 && long.TryParse(array[0], out var result) && uint.TryParse(array[1], out var result2)) { ZDOID val = default(ZDOID); ((ZDOID)(ref val))..ctor(result, result2); ZDO zDO = ZDOMan.instance.GetZDO(val); if (zDO != null) { zDO.SetOwner(sessionID); MetadataRegistry = zDO; Logger.LogInfo($"Linked existing Metadata Registry. SessionID:{sessionID} ZDO:{zDO.m_uid}"); return; } Logger.LogWarning($"Metadata Registry global key {DataObjects.CustomDataKey}={text} present but ZDO {val} could not be found; creating a new registry."); } } ZDO val2 = ZDOMan.instance.CreateNewZDO(Vector3.zero, 0); val2.Persistent = true; val2.SetOwner(sessionID); MetadataRegistry = val2; ZoneSystem.instance.SetGlobalKey($"{DataObjects.CustomDataKey} {((ZDOID)(ref MetadataRegistry.m_uid)).UserID} {((ZDOID)(ref MetadataRegistry.m_uid)).ID}"); Logger.LogInfo($"Hooking up Metadata Registry. SessionID:{sessionID} ZDO:{val2.m_uid}"); Logger.LogInfo($"Setting globalkey: {DataObjects.CustomDataKey} {((ZDOID)(ref MetadataRegistry.m_uid)).UserID} {((ZDOID)(ref MetadataRegistry.m_uid)).ID}"); } internal static void UpdateAccountRegistry(string accountID, string chara = null) { InstanciateOrLinkMetadataRegistry(); string text = MetadataRegistry.GetString("VE_ACCOUNTS", (string)null); if (text != null) { Dictionary<string, List<string>> dictionary = DataObjects.yamldeserializer.Deserialize<Dictionary<string, List<string>>>(text); if (!dictionary.ContainsKey(accountID)) { if (chara != null) { dictionary[accountID] = new List<string> { chara }; } else { dictionary[accountID] = new List<string>(); } string text2 = DataObjects.yamlserializer.Serialize((object)dictionary); MetadataRegistry.Set("VE_ACCOUNTS", text2); } } else { List<string> list = new List<string>(); if (chara != null) { list.Add(chara); } Dictionary<string, List<string>> dictionary2 = new Dictionary<string, List<string>> { { accountID, list } }; string text3 = DataObjects.yamlserializer.Serialize((object)dictionary2); MetadataRegistry.Set("VE_ACCOUNTS", text3); } } internal static Dictionary<string, List<string>> GetAccountRegistry() { InstanciateOrLinkMetadataRegistry(); string text = MetadataRegistry.GetString("VE_ACCOUNTS", (string)null); if (text != null) { return DataObjects.yamldeserializer.Deserialize<Dictionary<string, List<string>>>(text); } return new Dictionary<string, List<string>>(); } } internal static class ModManager { internal class ModMismatchDetail { internal List<string> MissingMods = new List<string>(); internal List<string> ExtraMods = new List<string>(); internal List<string> VersionMismatches = new List<string>(); internal List<string> AdminOnlyMods = new List<string>(); internal List<string> HashMismatches = new List<string>(); internal List<string> UnverifiedMods = new List<string>(); internal static string Join(List<string> entries) { if (entries != null && entries.Count != 0) { return string.Join(", ", entries); } return ""; } } internal static class ValidateMods { [HarmonyPatch(typeof(ZNet), "OnNewConnection")] public static class ZNet_OnNewConnection_Patch { [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(ZNet __instance, ZNetPeer peer) { Logger.LogDebug("New Connection, register VE Mod Sync RPC."); peer.m_rpc.Register<ZPackage>("RPC_ReceiveModVersionData", (Action<ZRpc, ZPackage>)RPC_ReceiveModVersionData); } } } [HarmonyPatch(typeof(ZNet), "RPC_ClientHandshake")] public static class ZNet_RPC_ClientHandshake_Patch { [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(ZNet __instance, ZRpc rpc) { if (ZNetExtension.IsClientInstance(__instance)) { if (ModSettings == null) { Logger.LogWarning("Mod settings are not initialized yet; sending no mod data. The server will see an empty mod list and is likely to reject this connection."); return; } PluginHasher.WaitForPass(2000); PluginHasher.ApplyTo(ModSettings.ActiveMods); Logger.LogDebug("Client sending mod version data to server"); rpc.Invoke("RPC_ReceiveModVersionData", new object[1] { ModSettings.ActiveModsToZPackage() }); } } } [HarmonyPatch(typeof(ZNet), "RPC_ServerHandshake")] public static class ZNet_RPC_ServerHandshake_Patch { [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(ZNet __instance, ZRpc rpc) { if (__instance.IsServer()) { if (ModSettings == null) { Logger.LogWarning("Mod settings are not initialized yet; not sending the server mod list to this client."); return; } Logger.LogDebug("Server sending mod version data to client"); rpc.Invoke("RPC_ReceiveModVersionData", new object[1] { ModSettings.ToZPackage() }); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] public static class ZNet_RPC_PeerInfo_ModRejection { [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(ZNet __instance, ZRpc rpc) { if (!__instance.IsServer()) { return true; } ISocket socket = rpc.GetSocket(); string text = ((socket != null) ? socket.GetHostName() : null); if (string.IsNullOrEmpty(text) || !RejectedHosts.Contains(text)) { return true; } Logger.LogWarning("Refusing peer info from " + text + ": rejected earlier for a mod validation failure."); rpc.Invoke("Error", new object[1] { 3 }); return false; } } [HarmonyPatch(typeof(ZNet), "Disconnect")] public static class ZNet_Disconnect_ClearRejection { [HarmonyPrefix] private static void Prefix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer() && peer != null) { ISocket socket = peer.m_socket; string text = ((socket != null) ? socket.GetHostName() : null); if (!string.IsNullOrEmpty(text) && RejectedHosts.Remove(text)) { Logger.LogDebug("Cleared mod rejection for " + text + "; a corrected client may reconnect."); } } } } public class JotunnDetailDisconnectExpansion : MonoBehaviour { private GameObject ContentView; private Text HeaderText; private Text FooterText; private static string HeaderMessage = ""; private static string FooterMessage = ""; private bool textset; public void UpdateErrorText(string header, string footer) { Logger.LogDebug("Set Error results " + header + " " + footer); HeaderMessage = header; FooterMessage = footer; textset = false; } public void Update() { if ((Object)(object)GUIManager.CustomGUIFront == (Object)null) { return; } Transform val = GUIManager.CustomGUIFront.transform.Find("CompatibilityWindow(Clone)/Scroll View/Viewport/Content"); if ((Object)(object)val == (Object)null) { textset = false; } else if (!textset) { ((Component)GUIManager.CustomGUIFront.transform.Find("CompatibilityWindow(Clone)/Scroll View")).GetComponent<ScrollRect>().scrollSensitivity = 1000f; ContentView = ((Component)val).gameObject; Transform val2 = ContentView.transform.Find("Failed Connection Text"); if ((Object)(object)val2 != (Object)null) { HeaderText = ((Component)val2).GetComponent<Text>(); } else { Logger.LogDebug("Could not find HeaderText"); } Transform val3 = ContentView.transform.Find("Error Messages Text"); if ((Object)(object)val3 != (Object)null) { FooterText = ((Component)val3).GetComponent<Text>(); } else { Logger.LogDebug("Could not find FooterText"); } if ((Object)(object)HeaderText != (Object)null && !string.IsNullOrEmpty(HeaderMessage)) { HeaderText.text = "<color=#FFA13C>Failed Connection:</color>\n" + HeaderMessage; } if ((Object)(object)FooterText != (Object)null && !string.IsNullOrEmpty(FooterMessage)) { FooterText.text = "<color=#FFA13C>Further Steps:</color>\n" + FooterMessage; } Logger.LogDebug("Set error results. H:" + HeaderMessage + " F:" + FooterMessage); textset = true; } } } internal static Dictionary<string, BaseUnityPlugin> ActiveMods = new Dictionary<string, BaseUnityPlugin>(); internal static readonly string[] ModsFileHeaderLines = new string[20] { "#################################################", "# Valheim Enforcer - Mod List", "#", "# Regenerated on startup, and re-read within ConfigPollIntervalSeconds of being edited.", "# Comments are kept: a note on its own line stays with the entry below it. A comment sharing", "# a line with a value is not kept, because that line gets rewritten.", "#", "# Every entry is keyed by its BepInEx plugin GUID.", "#", "# activeMods What this machine actually loaded. Rebuilt every start - editing it does nothing.", "# requiredMods Clients must have these. Mods the server loads land here by themselves.", "# optionalMods Clients may have these, and may connect without them.", "# adminOnlyMods Only admins may connect with these; everyone else is rejected.", "# serverOnlyMods Server side only. Not demanded of clients - but a client that installs one", "# is rejected for it, so this is not the list for client-side mods.", "#", "# Per entry: enforceVersion: true requires an exact version match (defaults to false).", "# File verification uses acceptedHashes / hashSource / thunderstorePackage / hashEnforcement.", "# The README covers all of it, including how to pin a mod the server does not run itself.", "#################################################" }; private static readonly HashSet<string> RejectedHosts = new HashSet<string>(); internal static DataObjects.Mods ModSettings { get; set; } internal static JotunnDetailDisconnectExpansion DetailsUpdater { get; set; } private static string ResolvePeerName(ZRpc rpc) { ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetPeer(rpc) : null); if (!string.IsNullOrEmpty(val?.m_playerName)) { return val.m_playerName; } return null; } internal static void SetModsActive() { ActiveMods.Clear(); ActiveMods = BepInExUtils.GetPlugins(true); PluginHasher.BeginPass(ActiveMods); ModSettings = new DataObjects.Mods(); Logger.LogDebug($"Detected {ActiveMods.Keys.Count} mods."); LoadConfig(File.ReadAllText(ValConfig.ModsConfigFilePath)); PluginHasher.WaitForPass(ValConfig.HashComputeTimeoutSeconds.Value * 1000); RebuildActiveMods(); foreach (KeyValuePair<string, BaseUnityPlugin> activeMod in ActiveMods) { Logger.LogDebug($"Found active mod: {activeMod.Key} v{activeMod.Value.Info.Metadata.Version}"); string text = activeMod.Value.Info.Metadata.Version.ToString(); string hash = PluginHasher.Get(activeMod.Key)?.Hash; if (ModSettings.RequiredMods.ContainsKey(activeMod.Key)) { UpdateModVersionIfChanged(ModSettings.RequiredMods, activeMod.Key, text); RecordLocalHashIfAllowed(ModSettings.RequiredMods, activeMod.Key, hash, text); } else if (ModSettings.AdminOnlyMods.ContainsKey(activeMod.Key)) { UpdateModVersionIfChanged(ModSettings.AdminOnlyMods, activeMod.Key, text); RecordLocalHashIfAllowed(ModSettings.AdminOnlyMods, activeMod.Key, hash, text); } else if (ModSettings.OptionalMods.ContainsKey(activeMod.Key)) { UpdateModVersionIfChanged(ModSettings.OptionalMods, activeMod.Key, text); RecordLocalHashIfAllowed(ModSettings.OptionalMods, activeMod.Key, hash, text); } else if (ModSettings.ServerOnlyMods.ContainsKey(activeMod.Key)) { UpdateModVersionIfChanged(ModSettings.ServerOnlyMods, activeMod.Key, text); } else if (ValConfig.AutoAddModsToRequired.Value) { Logger.LogDebug("Automatically adding " + activeMod.Key + " as a required mod."); ModSettings.RequiredMods.Add(activeMod.Key, new DataObjects.Mod { EnforceVersion = false, Version = text, PluginID = activeMod.Value.Info.Metadata.GUID, Name = activeMod.Value.Info.Metadata.Name }); RecordLocalHashIfAllowed(ModSettings.RequiredMods, activeMod.Key, hash, text); } } if (ValConfig.UpdateLoadedModsOnStartup.Value) { Logger.LogDebug("Updated Mods.yaml."); PersistModSettings(); } } internal static void PersistModSettings() { if (ModSettings == null) { return; } try { string yaml = DataObjects.yamlserializer.Serialize((object)ModSettings); File.WriteAllText(ValConfig.ModsConfigFilePath, WithPreservedComments(yaml)); ConfigFileWatcher.NoteSelfWrite(ValConfig.ModsConfigFilePath); } catch (Exception ex) { Logger.LogWarning("Could not write " + ValConfig.ModsConfigFilePath + ": " + ex.Message); } } private static string WithPreservedComments(string yaml) { try { YamlComments.Captured captured = YamlComments.Capture(File.Exists(ValConfig.ModsConfigFilePath) ? File.ReadAllText(ValConfig.ModsConfigFilePath) : null); string text = YamlComments.Reapply(yaml, captured); if (captured.HasLeadingBlock) { return text; } string text2 = YamlComments.DetectNewline(yaml); return string.Join(text2, ModsFileHeaderLines) + text2 + text2 + text; } catch (Exception ex) { Logger.LogWarning("Could not preserve the comments in " + ValConfig.ModsConfigFilePath + ": " + ex.Message + ". Writing it without them."); return yaml; } } private static void RecordLocalHashIfAllowed(Dictionary<string, DataObjects.Mod> modList, string key, string hash, string version) { if (ValConfig.RecordHashesForLoadedMods.Value && !string.IsNullOrEmpty(hash)) { DataObjects.Mod mod = modList[key]; if ((string.IsNullOrEmpty(mod.HashSource) || string.Equals(mod.HashSource, "Local", StringComparison.OrdinalIgnoreCase)) && (!mod.AcceptsHash(hash) || mod.AcceptedHashes.Count != 1)) { Logger.LogInfo("Recording local file hash for " + key + " (" + version + ")."); mod.AcceptedHashes = new List<string> { hash }; mod.HashSource = "Local"; mod.HashedFrom = "local:" + version; } } } private static void UpdateModVersionIfChanged(Dictionary<string, DataObjects.Mod> modList, string key, string currentVersion) { if (modList[key].Version != currentVersion) { Logger.LogInfo("Updating version for " + key + ": " + modList[key].Version + " -> " + currentVersion); modList[key].Version = currentVersion; } } private static void RebuildActiveMods() { if (ModSettings == null) { ModSettings = new DataObjects.Mods(); } if (ModSettings.ActiveMods == null) { ModSettings.ActiveMods = new Dictionary<string, DataObjects.Mod>(); } ModSettings.ActiveMods.Clear(); foreach (KeyValuePair<string, BaseUnityPlugin> activeMod in ActiveMods) { DataObjects.Mod mod = new DataObjects.Mod { EnforceVersion = true, Version = activeMod.Value.Info.Metadata.Version.ToString(), PluginID = activeMod.Value.Info.Metadata.GUID, Name = activeMod.Value.Info.Metadata.Name }; PluginHasher.Apply(activeMod.Key, mod); ModSettings.ActiveMods[activeMod.Key] = mod; } } internal static void UpdateModSettingConfigs(string yamlstring) { try { DataObjects.Mods mods = DataObjects.yamldeserializer.Deserialize<DataObjects.Mods>(yamlstring); if (mods == null) { Logger.LogWarning("Mod configuration file was empty, keeping the current settings."); return; } ModSettings = mods; RebuildActiveMods(); } catch (Exception ex) { Logger.LogWarning("Failed to deserialize mod configurations: " + ex.Message); } } internal static bool ValidateModlist(DataObjects.Mods CheckingMods, DataObjects.Mods AuthoratativeMods, bool isAdmin, bool adminStatusKnown, out string summay, out string details, out ModMismatchDetail detail) { summay = ""; details = ""; detail = new ModMismatchDetail(); List<string> list = new List<string>(); List<string> list2 = new List<string>(); List<string> list3 = new List<string>(); List<string> list4 = new List<string>(); List<string> list5 = new List<string>(); List<string> list6 = new List<string>(); List<string> list7 = new List<string>(); List<string> list8 = AuthoratativeMods.RequiredMods.Keys.Distinct().ToList(); bool flag = false; Logger.LogDebug($"Validating modlist of {CheckingMods.ActiveMods.Count} mods isAdmin? {isAdmin}"); foreach (KeyValuePair<string, DataObjects.Mod> activeMod in CheckingMods.ActiveMods) { list8.Remove(activeMod.Key); DataObjects.Mod mod = null; bool requiredOrAdmin = false; bool flag2 = false; if (AuthoratativeMods.RequiredMods.ContainsKey(activeMod.Key)) { mod = AuthoratativeMods.RequiredMods[activeMod.Key]; requiredOrAdmin = true; flag2 = mod.EnforceVersion; } else if (AuthoratativeMods.AdminOnlyMods.ContainsKey(activeMod.Key)) { mod = AuthoratativeMods.AdminOnlyMods[activeMod.Key]; requiredOrAdmin = true; if (!adminStatusKnown) { list4.Add(activeMod.Key); } else if (isAdmin) { flag2 = mod.EnforceVersion; } else { list3.Add(activeMod.Key); } } else if (AuthoratativeMods.OptionalMods.ContainsKey(activeMod.Key)) { mod = AuthoratativeMods.OptionalMods[activeMod.Key]; flag2 = mod.EnforceVersion; } if (mod == null) { list.Add(activeMod.Key); continue; } bool flag3 = mod.Version != activeMod.Value.Version; if (flag2 && flag3) { list2.Add(DescribeVersions(activeMod.Key, mod.Version, activeMod.Value.Version)); continue; } switch (HashPolicy.Evaluate(mod, activeMod.Value, requiredOrAdmin)) { case HashVerdict.Mismatch: if (flag3 && !string.IsNullOrEmpty(mod.Version)) { list2.Add(DescribeVersions(activeMod.Key, mod.Version, activeMod.Value.Version)); flag = true; } else { list5.Add(activeMod.Key); } break; case HashVerdict.Unverifiable: list6.Add(activeMod.Key + " (" + (activeMod.Value.HashStatus ?? "no hash reported") + ")"); break; case HashVerdict.NotRecorded: list7.Add(activeMod.Key); break; } } detail.MissingMods = list8; detail.ExtraMods = list; detail.VersionMismatches = list2; detail.AdminOnlyMods = list3; detail.HashMismatches = list5; detail.UnverifiedMods = new List<string>(list6); detail.UnverifiedMods.AddRange(list7); if (list2.Count > 0) { string text = "\nMod versions that do not match the server: " + string.Join(", ", list2); summay += text; Logger.LogWarning(text); } if (list8.Count > 0) { string text2 = "\nMissing required mods: " + string.Join(", ", list8); summay += text2; Logger.LogWarning(text2); } if (list.Count > 0) { string text3 = "\nNon-allowed mods found: " + string.Join(", ", list); summay += text3; Logger.LogWarning(text3); } if (list3.Count > 0) { string text4 = "\nAdmin-only mods not permitted for non-admins: " + string.Join(", ", list3); summay += text4; Logger.LogWarning(text4); } if (list4.Count > 0) { string text5 = "\nThis server restricts some mods to admins; if you are not an admin you will be disconnected: " + string.Join(", ", list4); summay += text5; Logger.LogInfo(text5); } if (list5.Count > 0) { string text6 = "\nModified mod files detected: " + string.Join(", ", list5); summay += text6; Logger.LogWarning(text6); } if (list6.Count > 0) { string text7 = "\nMod files that could not be verified: " + string.Join(", ", list6); summay += text7; Logger.LogWarning(text7); } if (list7.Count > 0) { string text8 = "\nThe server has no recorded file hash for: " + string.Join(", ", list7); summay += text8; Logger.LogWarning(text8); } if (list2.Count > 0 || list8.Count > 0 || list.Count > 0 || list3.Count > 0 || list4.Count > 0 || list5.Count > 0 || list6.Count > 0 || list7.Count > 0) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("\n<b>ValheimEnforcer - Mod Validation Failed</b>"); if (list2.Count > 0) { stringBuilder.AppendLine("\n<b>Version Mismatches:</b>"); AppendBullets(stringBuilder, lis