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 list = CharacterDeltaTracker.BuildCharacterItemDeltas(); Dictionary customData = Player.m_localPlayer.m_customData; Dictionary dictionary = new Dictionary(); List list2 = new List(); foreach (KeyValuePair 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 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 list3 = new List(); 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 dictionary2 = new Dictionary(); 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 EnableDebugMode; public static ConfigEntry UpdateLoadedModsOnStartup; public static ConfigEntry AutoAddModsToRequired; public static ConfigEntry HashEnforcement; public static ConfigEntry RecordHashesForLoadedMods; public static ConfigEntry ResolveThunderstoreHashes; public static ConfigEntry HashComputeTimeoutSeconds; public static ConfigEntry ThunderstoreMaxArchiveMB; public static ConfigEntry RemoveNontrackedItemsFromJoiningPlayers; public static ConfigEntry AddMissingItemsFromPlayerServerSave; public static ConfigEntry PreventExternalSkillRaises; public static ConfigEntry NewCharactersRemoveExtraItems; public static ConfigEntry NewCharacterSetSkillsToZero; public static ConfigEntry newCharacterClearCustomData; public static ConfigEntry PreventExternalCustomDataChanges; public static ConfigEntry ValidateItemCustomData; public static ConfigEntry ValidateItemDurability; public static ConfigEntry ItemValidationDurabilityAllowedVariance; public static ConfigEntry SavePlayerStatusEffectsOnLogout; public static ConfigEntry ItemRemovalForDirtyReconnection; public static ConfigEntry ItemReturnForDirtyReconnection; public static ConfigEntry EnforceCharacterLimit; public static ConfigEntry MaxCharactersPerAccount; public static ConfigEntry CharacterLimitExemptAccounts; public static ConfigEntry CharacterLimitExemptAdmins; public static ConfigEntry ImportServerCharacters; public static ConfigEntry ServerCharactersImportPath; public static ConfigEntry InternalStorageMode; public static ConfigEntry ConfigPollIntervalSeconds; public static ConfigEntry DeltaSynchronizationFrequencyInSeconds; public static ConfigEntry FullSyncPullIntervalMinutes; public static ConfigEntry FullSyncMaxConcurrentPlayers; public static ConfigEntry EnableCheatDetection; public static ConfigEntry DetectCheatEngine; public static ConfigEntry DetectValheimTooler; public static ConfigEntry DetectCheatTools; public static ConfigEntry DetectGenericTrainers; public static ConfigEntry ScanLoadedModules; public static ConfigEntry ScanWindowTitles; public static ConfigEntry AdditionalCheatProcesses; public static ConfigEntry IgnoredCheatProcesses; public static ConfigEntry CheatDetectionAction; public static ConfigEntry CheatScanIntervalSeconds; public static ConfigEntry DiscordWebhookUrl; public static ConfigEntry DiscordWebhookUrlPlayerActivity; public static ConfigEntry DiscordWebhookUrlServerStatus; public static ConfigEntry DiscordWebhookUrlModeration; public static ConfigEntry DiscordWebhookUrlModMismatch; public static ConfigEntry DiscordServerLabel; public static ConfigEntry DiscordNotifyServerStartup; public static ConfigEntry DiscordNotifyServerShutdown; public static ConfigEntry DiscordNotifyWorldSaved; public static ConfigEntry DiscordNotifyPlayerJoined; public static ConfigEntry DiscordNotifyPlayerLeft; public static ConfigEntry DiscordNotifyWrongMods; public static ConfigEntry DiscordNotifyCheaterBanned; public static ConfigEntry 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 lastDriftResync = new ConcurrentDictionary(); 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)SendSavedCharacter); LoadYamlConfigs(new Dictionary> { { 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("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(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//.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(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(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> configFilesToFind) { string[] files = Directory.GetFiles(GetSecondaryConfigDirectoryPath()); List list = new List(); List 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> 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(yaml); Logger.LogInfo($"Recieved Player data update for {sender} - {character.Name}|{character.HostID}"); DataObjects.Character accountCharacter = InternalDataStore.GetAccountCharacter(character.HostID, character.Name); List confiscatedItems = character.ConfiscatedItems; character.ConfiscatedItems = accountCharacter?.ConfiscatedItems ?? new List(); 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(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(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 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(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(package.ReadString()); List 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(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 list = new List(); 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 { { "player", playerName }, { "playerId", hostName }, { "reason", text }, { "detections", DescribeDetectedTools(summary) }, { "action", "Ban" } }); } } private static string BuildCheatReason(DataObjects.CheatSummaryReport summary) { List list = new List(); 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())); } 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())); } 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(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> item in DataObjects.yamldeserializer.Deserialize>>(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> dictionary = new Dictionary>(); 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 list = Directory.GetFiles(item).ToList(); string key = item.Split(new char[1] { '/' }).Last(); List list2 = new List(); 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 list = DataObjects.yamldeserializer.Deserialize>(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(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 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 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(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 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(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = false, IsAdvanced = advanced } })); } public static ConfigEntry 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(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = false, IsAdvanced = advanced } })); } public static ConfigEntry> BindServerConfig(string catagory, string key, List 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>(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry 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(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange(valmin, valmax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry 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(catagory, key, value, new ConfigDescription(description, acceptableValues, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry 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(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange(valmin, valmax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry 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(catagory, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange(valmin, valmax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry BindServerConfig(string catagory, string key, string value, string description, AcceptableValueList 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(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(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 { { 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(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(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("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> dictionary = DataObjects.yamldeserializer.Deserialize>>(text); if (!dictionary.ContainsKey(accountID)) { if (chara != null) { dictionary[accountID] = new List { chara }; } else { dictionary[accountID] = new List(); } string text2 = DataObjects.yamlserializer.Serialize((object)dictionary); MetadataRegistry.Set("VE_ACCOUNTS", text2); } } else { List list = new List(); if (chara != null) { list.Add(chara); } Dictionary> dictionary2 = new Dictionary> { { accountID, list } }; string text3 = DataObjects.yamlserializer.Serialize((object)dictionary2); MetadataRegistry.Set("VE_ACCOUNTS", text3); } } internal static Dictionary> GetAccountRegistry() { InstanciateOrLinkMetadataRegistry(); string text = MetadataRegistry.GetString("VE_ACCOUNTS", (string)null); if (text != null) { return DataObjects.yamldeserializer.Deserialize>>(text); } return new Dictionary>(); } } internal static class ModManager { internal class ModMismatchDetail { internal List MissingMods = new List(); internal List ExtraMods = new List(); internal List VersionMismatches = new List(); internal List AdminOnlyMods = new List(); internal List HashMismatches = new List(); internal List UnverifiedMods = new List(); internal static string Join(List 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("RPC_ReceiveModVersionData", (Action)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().scrollSensitivity = 1000f; ContentView = ((Component)val).gameObject; Transform val2 = ContentView.transform.Find("Failed Connection Text"); if ((Object)(object)val2 != (Object)null) { HeaderText = ((Component)val2).GetComponent(); } 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(); } else { Logger.LogDebug("Could not find FooterText"); } if ((Object)(object)HeaderText != (Object)null && !string.IsNullOrEmpty(HeaderMessage)) { HeaderText.text = "Failed Connection:\n" + HeaderMessage; } if ((Object)(object)FooterText != (Object)null && !string.IsNullOrEmpty(FooterMessage)) { FooterText.text = "Further Steps:\n" + FooterMessage; } Logger.LogDebug("Set error results. H:" + HeaderMessage + " F:" + FooterMessage); textset = true; } } } internal static Dictionary ActiveMods = new Dictionary(); 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 RejectedHosts = new HashSet(); 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 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 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 { hash }; mod.HashSource = "Local"; mod.HashedFrom = "local:" + version; } } } private static void UpdateModVersionIfChanged(Dictionary 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(); } ModSettings.ActiveMods.Clear(); foreach (KeyValuePair 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(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 list = new List(); List list2 = new List(); List list3 = new List(); List list4 = new List(); List list5 = new List(); List list6 = new List(); List list7 = new List(); List list8 = AuthoratativeMods.RequiredMods.Keys.Distinct().ToList(); bool flag = false; Logger.LogDebug($"Validating modlist of {CheckingMods.ActiveMods.Count} mods isAdmin? {isAdmin}"); foreach (KeyValuePair 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(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("\nValheimEnforcer - Mod Validation Failed"); if (list2.Count > 0) { stringBuilder.AppendLine("\nVersion Mismatches:"); AppendBullets(stringBuilder, list2); stringBuilder.AppendLine(" Install the version listed for each of these - not a newer one."); if (flag) { stringBuilder.AppendLine(" This server verifies mod files, so only the exact build it has on record is accepted."); } } if (list8.Count > 0) { stringBuilder.AppendLine("\nMissing Required Mods:"); foreach (string item in list8) { stringBuilder.AppendLine(" • " + item); } } if (list.Count > 0) { stringBuilder.AppendLine("\nNon-Allowed Mods:"); foreach (string item2 in list) { stringBuilder.AppendLine(" • " + item2); } } if (list3.Count > 0) { stringBuilder.AppendLine("\nAdmin-Only Mods (not permitted):"); foreach (string item3 in list3) { stringBuilder.AppendLine(" • " + item3); } } if (list4.Count > 0) { stringBuilder.AppendLine("\nAdmin-Only Mods (require admin):"); foreach (string item4 in list4) { stringBuilder.AppendLine(" • " + item4); } } if (list5.Count > 0) { stringBuilder.AppendLine("\nModified Mod Files:"); AppendBullets(stringBuilder, list5); stringBuilder.AppendLine(" Reinstall these from their original download - a recompiled or edited DLL will not match."); } if (list6.Count > 0) { stringBuilder.AppendLine("\nUnverifiable Mod Files:"); AppendBullets(stringBuilder, list6); stringBuilder.AppendLine(" These could not be checked against a file on disk. Plugins loaded from memory cannot be verified."); } if (list7.Count > 0) { stringBuilder.AppendLine("\nMods The Server Has Not Pinned (server misconfiguration):"); AppendBullets(stringBuilder, list7); stringBuilder.AppendLine(" Ask the server admin to record a hash for these, or to lower HashEnforcement."); } string text9 = stringBuilder.ToString(); details = text9; return false; } Logger.LogInfo("Client mod list validated successfully."); return true; } private static string DescribeVersions(string key, string expected, string reported) { if (string.IsNullOrEmpty(expected)) { return key; } return key + " (needs " + expected + ", has " + (string.IsNullOrEmpty(reported) ? "unknown" : reported) + ")"; } private static void AppendBullets(StringBuilder builder, List entries, int limit = 25) { for (int i = 0; i < entries.Count && i < limit; i++) { builder.AppendLine(" • " + entries[i]); } if (entries.Count > limit) { builder.AppendLine($" ... and {entries.Count - limit} more"); } } internal static void LoadConfig(string yaml) { try { ModSettings = DataObjects.yamldeserializer.Deserialize(yaml) ?? new DataObjects.Mods(); } catch (Exception ex) { Logger.LogError("Could not parse " + ValConfig.ModsConfigFilePath + ": " + ex.Message + ". Continuing with empty mod settings; fix the file and restart, or delete it to have it regenerated."); ModSettings = new DataObjects.Mods(); } } internal static string GetDefaultConfig() { if (ModSettings != null) { return DataObjects.yamlserializer.Serialize((object)ModSettings); } return DataObjects.yamlserializer.Serialize((object)new DataObjects.Mods()); } private static void RPC_ReceiveModVersionData(ZRpc sender, ZPackage data) { Logger.LogDebug("Received mod version data from " + sender.m_socket.GetEndPointString()); string endPointString = sender.m_socket.GetEndPointString(); if (!ZNet.instance.IsServer()) { DataObjects.Mods mods = new DataObjects.Mods().FromZPackage(data); Logger.LogDebug($"Client received server mod data: Required: {mods.RequiredMods.Count}, Optional: {mods.OptionalMods.Count}, AdminOnly: {mods.AdminOnlyMods.Count} mods"); string summay; string details; ModMismatchDetail detail; bool num = ValidateModlist(ModSettings, mods, isAdmin: false, adminStatusKnown: false, out summay, out details, out detail); DetailsUpdater?.UpdateErrorText(summay, details); if (!num) { Logger.LogWarning("Mod compatibility check failed for client."); } return; } DataObjects.Mods mods2 = new DataObjects.Mods().FromZPackage(data); bool flag = ZNet.instance.IsAdmin(sender.m_socket.GetHostName()); Logger.LogDebug($"Server received server mod data from {endPointString} Admin?{flag}: Required: {mods2.RequiredMods.Count}, Optional: {mods2.OptionalMods.Count}, AdminOnly: {mods2.AdminOnlyMods.Count} mods"); if (!ValidateModlist(mods2, ModSettings, flag, adminStatusKnown: true, out var summay2, out var _, out var detail2)) { Logger.LogWarning("Mod compatibility check failed for client at " + endPointString + "\n" + summay2); if (ValConfig.DiscordNotifyWrongMods.Value) { string value = ResolvePeerName(sender) ?? endPointString; Dictionary obj = new Dictionary { { "player", value } }; ISocket socket = sender.m_socket; obj.Add("playerId", ((socket != null) ? socket.GetHostName() : null) ?? ""); obj.Add("summary", summay2.Trim()); obj.Add("missingMods", ModMismatchDetail.Join(detail2.MissingMods)); obj.Add("extraMods", ModMismatchDetail.Join(detail2.ExtraMods)); obj.Add("versionMismatches", ModMismatchDetail.Join(detail2.VersionMismatches)); obj.Add("adminOnlyMods", ModMismatchDetail.Join(detail2.AdminOnlyMods)); obj.Add("hashMismatches", ModMismatchDetail.Join(detail2.HashMismatches)); obj.Add("unverifiedMods", ModMismatchDetail.Join(detail2.UnverifiedMods)); DiscordNotifier.Notify(NotificationEvent.ModMismatch, obj); } RejectPeer(sender); } } private static void RejectPeer(ZRpc sender) { ISocket socket = sender.GetSocket(); string text = ((socket != null) ? socket.GetHostName() : null); if (!string.IsNullOrEmpty(text)) { RejectedHosts.Add(text); } sender.Invoke("Error", new object[1] { 3 }); ISocket socket2 = sender.GetSocket(); if (socket2 != null) { socket2.Flush(); } } internal static void AddErrorMessageDetailsForMenu() { //IL_0000: 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) Scene activeScene = SceneManager.GetActiveScene(); if (((Scene)(ref activeScene)).name.Equals("start")) { DetailsUpdater = GUIManager.CustomGUIFront.AddComponent(); } } } } namespace ValheimEnforcer.modules.notifications { internal static class DiscordNotifier { private static readonly HttpClient http = new HttpClient { Timeout = TimeSpan.FromSeconds(10.0) }; internal static void Initialize() { bool flag = false; NotificationCategory[] array = (NotificationCategory[])Enum.GetValues(typeof(NotificationCategory)); foreach (NotificationCategory notificationCategory in array) { string text = ResolveUrl(notificationCategory); if (!string.IsNullOrWhiteSpace(text)) { if (!IsValidWebhookUrl(text)) { Logger.LogWarning($"Discord notifications: the webhook URL for {notificationCategory} is invalid, so that category is disabled. Expected https://discord.com/api/webhooks/..."); continue; } flag = true; bool flag2 = !string.IsNullOrWhiteSpace(CategoryUrl(notificationCategory)); Logger.LogDebug(string.Format("Discord notifications: {0} -> {1}.", notificationCategory, flag2 ? "its own webhook" : "the shared WebhookUrl")); } } if (flag) { Logger.LogInfo("Discord notifications enabled."); } else { Logger.LogInfo("Discord notifications: no webhook URL configured, disabled."); } } internal static bool IsValidWebhookUrl(string url) { if (string.IsNullOrWhiteSpace(url)) { return false; } if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result)) { return false; } if (result.Scheme != "https") { return false; } if (result.Host == "discord.com" || result.Host == "discordapp.com" || result.Host == "ptb.discord.com" || result.Host == "canary.discord.com") { return result.AbsolutePath.StartsWith("/api/webhooks/", StringComparison.Ordinal); } return false; } private static string CategoryUrl(NotificationCategory category) { return category switch { NotificationCategory.PlayerActivity => ValConfig.DiscordWebhookUrlPlayerActivity.Value, NotificationCategory.ServerStatus => ValConfig.DiscordWebhookUrlServerStatus.Value, NotificationCategory.Moderation => ValConfig.DiscordWebhookUrlModeration.Value, NotificationCategory.ModMismatch => ValConfig.DiscordWebhookUrlModMismatch.Value, _ => null, }; } internal static string ResolveUrl(NotificationCategory category) { string text = CategoryUrl(category); if (string.IsNullOrWhiteSpace(text)) { text = ValConfig.DiscordWebhookUrl.Value; } return text; } private static bool IsActive(NotificationCategory category, out string url) { url = ResolveUrl(category); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { return IsValidWebhookUrl(url); } return false; } internal static void Notify(NotificationEvent evt, IDictionary tokens = null) { NotificationCategory category = NotificationTemplates.CategoryOf(evt); if (IsActive(category, out var _)) { SendAsync(category, Build(evt, tokens)); } } internal static void NotifySync(NotificationEvent evt, IDictionary tokens = null) { NotificationCategory category = NotificationTemplates.CategoryOf(evt); if (IsActive(category, out var _)) { SendSync(category, Build(evt, tokens)); } } private static string Build(NotificationEvent evt, IDictionary tokens) { try { Dictionary dictionary = CommonTokens(); if (tokens != null) { foreach (KeyValuePair token in tokens) { dictionary[token.Key] = token.Value; } } return NotificationTemplates.Render(evt, dictionary); } catch (Exception ex) { Logger.LogWarning($"Discord notifications: could not build the {evt} message: {ex.Message}"); return null; } } private static Dictionary CommonTokens() { Dictionary dictionary = new Dictionary(StringComparer.Ordinal) { { "server", ValConfig.DiscordServerLabel.Value ?? "" }, { "timestamp", DateTime.UtcNow.ToString("o") }, { "colorGreen", 5763719.ToString() }, { "colorAmber", 16705372.ToString() }, { "colorRed", 15548997.ToString() }, { "colorGrey", 9807270.ToString() }, { "world", "" }, { "onlinePlayers", "" } }; try { if ((Object)(object)ZNet.instance != (Object)null) { dictionary["world"] = ZNet.instance.GetWorldName() ?? ""; dictionary["onlinePlayers"] = ZNet.instance.GetNrOfPlayers().ToString(); } } catch (Exception ex) { Logger.LogDebug("Discord notifications: could not read the world state for placeholders: " + ex.Message); } return dictionary; } internal static void SendAsync(NotificationCategory category, string body) { if (!string.IsNullOrWhiteSpace(body) && IsActive(category, out var url)) { Task.Run(() => Post(url, body)); } } internal static void SendSync(NotificationCategory category, string body) { if (string.IsNullOrWhiteSpace(body) || !IsActive(category, out var url)) { return; } try { Post(url, body).Wait(TimeSpan.FromSeconds(8.0)); } catch (Exception ex) { Logger.LogWarning("Discord notifications: synchronous send failed: " + ex.Message); } } private static async Task Post(string url, string body) { try { StringContent content = new StringContent(body, Encoding.UTF8, "application/json"); try { HttpResponseMessage val = await http.PostAsync(url, (HttpContent)(object)content).ConfigureAwait(continueOnCapturedContext: false); if (!val.IsSuccessStatusCode) { Logger.LogWarning($"Discord notifications: webhook returned HTTP {(int)val.StatusCode}. Check the template for this event against Discord's webhook reference."); } } finally { ((IDisposable)content)?.Dispose(); } } catch (Exception ex) { Logger.LogWarning("Discord notifications: send failed: " + ex.Message); } } } internal static class NotificationPatches { [HarmonyPatch(typeof(ZNet), "Start")] public static class ZNet_Start_Patch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { if (__instance.IsServer()) { AnnouncedPeers.Clear(); DiscordNotifier.Initialize(); if (ValConfig.DiscordNotifyServerStartup.Value) { DiscordNotifier.Notify(NotificationEvent.ServerStartup); } } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] public static class ZNet_Shutdown_Patch { [HarmonyPrefix] private static void Prefix(ZNet __instance) { if (__instance.IsServer() && ValConfig.DiscordNotifyServerShutdown.Value) { DiscordNotifier.NotifySync(NotificationEvent.ServerShutdown); } } } [HarmonyPatch(typeof(ZNet), "SaveWorld")] public static class ZNet_SaveWorld_Patch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { if (__instance.IsServer() && ValConfig.DiscordNotifyWorldSaved.Value) { DiscordNotifier.Notify(NotificationEvent.WorldSaved); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] public static class ZNet_RPC_PeerInfo_Patch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZRpc rpc) { if (__instance.IsServer()) { ZNetPeer peer = __instance.GetPeer(rpc); if (peer != null && !string.IsNullOrEmpty(peer.m_playerName) && AnnouncedPeers.Add(peer.m_uid) && ValConfig.DiscordNotifyPlayerJoined.Value) { string text = ResolveHostId(peer); DiscordNotifier.Notify(NotificationEvent.PlayerJoined, new Dictionary { { "player", peer.m_playerName }, { "playerId", text }, { "isAdmin", IsAdmin(__instance, text) ? "yes" : "no" } }); } } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] public static class ZNet_Disconnect_Patch { [HarmonyPrefix] private static void Prefix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer() && peer != null && AnnouncedPeers.Remove(peer.m_uid) && ValConfig.DiscordNotifyPlayerLeft.Value) { DataObjects.DisconnectionState num = ResolveSavedDataState(peer); int value = ValConfig.DeltaSynchronizationFrequencyInSeconds.Value; bool flag = num == DataObjects.DisconnectionState.Clean; string value2 = (flag ? "Clean logout" : "Disconnected"); string value3 = (flag ? "✅ Player Data up to date." : $"⚠\ufe0f Stale — Data outdated by {value}s"); DiscordNotifier.Notify(NotificationEvent.PlayerLeft, new Dictionary { { "player", peer.m_playerName }, { "playerId", ResolveHostId(peer) }, { "disconnect", value2 }, { "savedData", value3 }, { "deltaWindow", value.ToString() }, { "statusColor", flag ? "Green" : "Amber" } }); } } } private static readonly HashSet AnnouncedPeers = new HashSet(); private static string ResolveHostId(ZNetPeer peer) { try { ISocket socket = peer.m_socket; string text = ((socket != null) ? socket.GetHostName() : null); if (string.IsNullOrEmpty(text)) { return ""; } if (text.Contains(":")) { text = text.Split(new char[1] { ':' })[0]; } return text; } catch (Exception ex) { Logger.LogDebug("Discord notifications: could not read the host id for " + peer.m_playerName + ": " + ex.Message); return ""; } } private static bool IsAdmin(ZNet znet, string hostId) { if (string.IsNullOrEmpty(hostId)) { return false; } try { return znet.IsAdmin(hostId); } catch (Exception ex) { Logger.LogDebug("Discord notifications: could not read admin status for " + hostId + ": " + ex.Message); return false; } } private static DataObjects.DisconnectionState ResolveSavedDataState(ZNetPeer peer) { try { string text = ResolveHostId(peer); DataObjects.Character character = ValConfig.LoadCharacterFromSave(text, peer.m_playerName); if (character == null) { Logger.LogDebug("Discord notifications: no saved character for " + peer.m_playerName + " (" + text + "); reporting saved data as stale."); return DataObjects.DisconnectionState.DirtyDisconnect; } return character.LastDisconnect; } catch (Exception ex) { Logger.LogDebug("Discord notifications: failed to resolve saved-data state for " + peer.m_playerName + ": " + ex.Message); return DataObjects.DisconnectionState.DirtyDisconnect; } } } internal enum NotificationEvent { ServerStartup, ServerShutdown, WorldSaved, PlayerJoined, PlayerLeft, CheaterBanned, CharacterRejected, ModMismatch } internal enum NotificationCategory { PlayerActivity, ServerStatus, Moderation, ModMismatch } internal static class NotificationTemplates { internal const string EmbeddedResourceName = "ValheimEnforcer.assets.Notifications.yaml"; private static readonly ISerializer serializer = ((BuilderSkeleton)new SerializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).DisableAliases().Build(); private const int TokenLimit = 1000; private const int LongTokenLimit = 3900; private static readonly HashSet LongTokens = new HashSet(StringComparer.Ordinal) { "summary" }; private static DataObjects.NotificationTemplateSet templates; private static DataObjects.NotificationTemplateSet defaults; private static string[] headerLines; private static readonly NotificationEvent[] AllEvents = (NotificationEvent[])Enum.GetValues(typeof(NotificationEvent)); private const int MaxPlaceholderName = 64; internal static string[] FileHeaderLines { get { if (headerLines == null) { LoadEmbedded(); } return headerLines; } } internal static DataObjects.NotificationTemplateSet Defaults() { if (defaults == null) { LoadEmbedded(); } return defaults; } internal static string GetDefaultConfig() { return ReadEmbedded() ?? ""; } private static string ReadEmbedded() { try { using Stream stream = typeof(ValheimEnforcer).Assembly.GetManifestResourceStream("ValheimEnforcer.assets.Notifications.yaml"); if (stream == null) { Logger.LogWarning("Embedded notification templates 'ValheimEnforcer.assets.Notifications.yaml' were not found."); return null; } using StreamReader streamReader = new StreamReader(stream); return streamReader.ReadToEnd(); } catch (Exception ex) { Logger.LogWarning("Could not read the embedded notification templates: " + ex.Message); return null; } } private static void LoadEmbedded() { defaults = new DataObjects.NotificationTemplateSet(); headerLines = new string[0]; string text = ReadEmbedded(); if (text == null) { return; } List list = new List(); string[] array = text.Replace("\r\n", "\n").Split(new char[1] { '\n' }); foreach (string text2 in array) { if (!text2.StartsWith("#", StringComparison.Ordinal)) { break; } list.Add(text2); } headerLines = list.ToArray(); try { DataObjects.NotificationTemplateSet notificationTemplateSet = DataObjects.yamldeserializer.Deserialize(text); if (notificationTemplateSet != null) { defaults = notificationTemplateSet; } } catch (Exception ex) { Logger.LogError("The embedded notification templates could not be parsed: " + ex.Message + ". Notifications will not post until this build is fixed."); } } internal static NotificationCategory CategoryOf(NotificationEvent evt) { switch (evt) { case NotificationEvent.PlayerJoined: case NotificationEvent.PlayerLeft: return NotificationCategory.PlayerActivity; case NotificationEvent.CheaterBanned: case NotificationEvent.CharacterRejected: return NotificationCategory.Moderation; case NotificationEvent.ModMismatch: return NotificationCategory.ModMismatch; default: return NotificationCategory.ServerStatus; } } internal static string Get(NotificationEvent evt) { if (templates == null) { templates = Defaults(); } return GetFrom(templates, evt); } private static string GetFrom(DataObjects.NotificationTemplateSet set, NotificationEvent evt) { if (set == null) { return null; } return evt switch { NotificationEvent.ServerStartup => set.ServerStartup, NotificationEvent.ServerShutdown => set.ServerShutdown, NotificationEvent.WorldSaved => set.WorldSaved, NotificationEvent.PlayerJoined => set.PlayerJoined, NotificationEvent.PlayerLeft => set.PlayerLeft, NotificationEvent.CheaterBanned => set.CheaterBanned, NotificationEvent.CharacterRejected => set.CharacterRejected, NotificationEvent.ModMismatch => set.ModMismatch, _ => null, }; } private static void Set(DataObjects.NotificationTemplateSet set, NotificationEvent evt, string template) { switch (evt) { case NotificationEvent.ServerStartup: set.ServerStartup = template; break; case NotificationEvent.ServerShutdown: set.ServerShutdown = template; break; case NotificationEvent.WorldSaved: set.WorldSaved = template; break; case NotificationEvent.PlayerJoined: set.PlayerJoined = template; break; case NotificationEvent.PlayerLeft: set.PlayerLeft = template; break; case NotificationEvent.CheaterBanned: set.CheaterBanned = template; break; case NotificationEvent.CharacterRejected: set.CharacterRejected = template; break; case NotificationEvent.ModMismatch: set.ModMismatch = template; break; } } internal static void Initialize() { string notificationsFilePath = ValConfig.NotificationsFilePath; if (!File.Exists(notificationsFilePath)) { Logger.LogDebug("Notifications.yaml not present, using the built-in notification templates."); templates = Defaults(); return; } try { if (LoadFromText(File.ReadAllText(notificationsFilePath))) { Logger.LogInfo("Notifications.yaml was missing one or more templates; filling them in from the defaults."); Persist(); } } catch (Exception ex) { Logger.LogWarning("Could not read " + notificationsFilePath + ": " + ex.Message + ". Using the built-in notification templates."); templates = Defaults(); } } internal static bool LoadFromText(string yaml) { DataObjects.NotificationTemplateSet notificationTemplateSet; try { notificationTemplateSet = DataObjects.yamldeserializer.Deserialize(yaml ?? ""); } catch (Exception ex) { Logger.LogWarning("Could not parse Notifications.yaml: " + ex.Message + ". Keeping the templates already loaded."); return false; } if (notificationTemplateSet == null) { notificationTemplateSet = new DataObjects.NotificationTemplateSet(); } DataObjects.NotificationTemplateSet set = Defaults(); bool flag = false; bool flag2 = false; NotificationEvent[] allEvents = AllEvents; foreach (NotificationEvent evt in allEvents) { string text = GetFrom(notificationTemplateSet, evt); string problem; if (string.IsNullOrWhiteSpace(text)) { Set(notificationTemplateSet, evt, GetFrom(set, evt)); flag = true; } else if (!IsUsable(evt, text, out problem)) { Logger.LogWarning("Notification template '" + CamelName(evt) + "' is not valid JSON: " + problem + ". Using the built-in default for it until this is fixed - your version of it is left in the file untouched."); Set(notificationTemplateSet, evt, GetFrom(set, evt)); flag2 = true; } } templates = notificationTemplateSet; Logger.LogDebug("Notification templates loaded."); if (flag2 && flag) { Logger.LogInfo("Not adding the missing notification templates to the file while another one is broken, so nothing overwrites the template being fixed."); } if (flag) { return !flag2; } return false; } internal static bool IsUsable(NotificationEvent evt, string template, out string problem) { string text = Substitute(template, SampleTokens()); if (string.IsNullOrWhiteSpace(text)) { problem = "it renders to nothing"; return false; } return JsonWellFormed.Validate(text, out problem); } internal static string CamelName(NotificationEvent evt) { string text = evt.ToString(); return char.ToLowerInvariant(text[0]) + text.Substring(1); } internal static Dictionary SampleTokens() { return new Dictionary(StringComparer.Ordinal) { { "server", "Test Server" }, { "world", "TestWorld" }, { "onlinePlayers", "3" }, { "timestamp", DateTime.UtcNow.ToString("o") }, { "colorGreen", 5763719.ToString() }, { "colorAmber", 16705372.ToString() }, { "colorRed", 15548997.ToString() }, { "colorGrey", 9807270.ToString() }, { "statusColor", 5763719.ToString() }, { "player", "TestViking" }, { "character", "TestViking" }, { "playerId", "76561190000000000" }, { "isAdmin", "no" }, { "disconnect", "Clean logout" }, { "savedData", "Player Data up to date." }, { "deltaWindow", "15" }, { "reason", "Cheat detection: sample entry, no ban was issued" }, { "detections", "SampleTool [process: sample.exe]" }, { "action", "Test" }, { "maxCharacters", "1" }, { "summary", "This is a sample mod mismatch. Nobody was actually rejected." }, { "missingMods", "com.example.SampleMod" }, { "extraMods", "com.example.NotAllowed" }, { "versionMismatches", "com.example.WrongVersion (needs 1.4.2, has 1.3.0)" }, { "adminOnlyMods", "com.example.AdminTool" }, { "hashMismatches", "com.example.Recompiled" }, { "unverifiedMods", "com.example.Unpinned" } }; } internal static void Persist() { string notificationsFilePath = ValConfig.NotificationsFilePath; try { string yaml = serializer.Serialize((object)templates); File.WriteAllText(notificationsFilePath, WithPreservedComments(yaml)); ConfigFileWatcher.NoteSelfWrite(notificationsFilePath); } catch (Exception ex) { Logger.LogWarning("Could not write " + notificationsFilePath + ": " + ex.Message); } } internal static string WithPreservedComments(string yaml) { string notificationsFilePath = ValConfig.NotificationsFilePath; try { YamlComments.Captured captured = YamlComments.Capture(File.Exists(notificationsFilePath) ? File.ReadAllText(notificationsFilePath) : null); string text = YamlComments.Reapply(yaml, captured); if (captured.HasLeadingBlock) { return text; } string text2 = YamlComments.DetectNewline(yaml); return string.Join(text2, FileHeaderLines) + text2 + text2 + text; } catch (Exception ex) { Logger.LogWarning("Could not preserve the comments in " + notificationsFilePath + ": " + ex.Message + ". Writing it without them."); return yaml; } } internal static string Render(NotificationEvent evt, IDictionary tokens) { string text = Get(evt); if (string.IsNullOrWhiteSpace(text)) { text = GetFrom(Defaults(), evt); } if (string.IsNullOrWhiteSpace(text)) { return null; } string text2 = Substitute(text, tokens); if (string.IsNullOrWhiteSpace(text2)) { Logger.LogDebug($"Notification template for {evt} renders to nothing; skipping the post."); return null; } return text2; } internal static string Substitute(string text, IDictionary tokens) { if (string.IsNullOrEmpty(text) || text.IndexOf('{') < 0) { return text ?? ""; } StringBuilder stringBuilder = new StringBuilder(text.Length + 64); int num = 0; while (num < text.Length) { char c = text[num]; if (c != '{') { stringBuilder.Append(c); num++; continue; } int num2 = FindPlaceholderEnd(text, num); if (num2 < 0) { stringBuilder.Append(c); num++; continue; } string text2 = text.Substring(num + 1, num2 - num - 1); if (tokens != null && tokens.TryGetValue(text2, out var value)) { stringBuilder.Append(Prepare(text2, value)); } else { Logger.LogDebug("Notification template placeholder '{" + text2 + "}' is not available for this event; leaving it as written."); stringBuilder.Append(text, num, num2 - num + 1); } num = num2 + 1; } return stringBuilder.ToString(); } private static int FindPlaceholderEnd(string text, int start) { int num = Math.Min(text.Length, start + 64 + 2); for (int i = start + 1; i < num; i++) { char c = text[i]; switch (c) { case '}': if (i <= start + 1) { return -1; } return i; default: if ((c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '_') { return -1; } break; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': break; } } return -1; } private static string Prepare(string name, string value) { if (string.IsNullOrEmpty(value)) { return ""; } int num = (LongTokens.Contains(name) ? 3900 : 1000); if (value.Length > num) { Logger.LogDebug($"Notification placeholder '{{{name}}}' was {value.Length} characters and has been truncated to {num}."); value = value.Substring(0, num); } return EscapeJson(value); } internal static string EscapeJson(string value) { if (string.IsNullOrEmpty(value)) { return ""; } StringBuilder stringBuilder = new StringBuilder(value.Length + 8); foreach (char c in value) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } } } namespace ValheimEnforcer.modules.mods { internal enum HashVerdict { Pass, Mismatch, Unverifiable, NotRecorded } internal static class HashPolicy { internal const string Off = "Off"; internal const string WhenKnown = "WhenKnown"; internal const string Strict = "Strict"; internal const string SourceLocal = "Local"; internal const string SourceManual = "Manual"; internal const string SourceThunderstore = "Thunderstore"; private static readonly HashSet warnedOverrides = new HashSet(); internal static string EffectiveMode(DataObjects.Mod authoritative) { string text = ValConfig.HashEnforcement.Value ?? "WhenKnown"; string text2 = authoritative?.HashEnforcement; if (string.IsNullOrWhiteSpace(text2)) { return text; } if (string.Equals(text2, "Off", StringComparison.OrdinalIgnoreCase)) { return "Off"; } if (string.Equals(text2, "WhenKnown", StringComparison.OrdinalIgnoreCase)) { return "WhenKnown"; } if (string.Equals(text2, "Strict", StringComparison.OrdinalIgnoreCase)) { return "Strict"; } WarnBadOverrideOnce(authoritative, text2, text); return text; } private static void WarnBadOverrideOnce(DataObjects.Mod authoritative, string over, string global) { string item = authoritative?.PluginID + "|" + over; lock (warnedOverrides) { if (!warnedOverrides.Add(item)) { return; } } Logger.LogWarning("Mods.yaml entry '" + authoritative?.PluginID + "' has hashEnforcement '" + over + "', which is not one of Off/WhenKnown/Strict. Falling back to the server setting (" + global + "). Fix the value."); } internal static HashVerdict Evaluate(DataObjects.Mod authoritative, DataObjects.Mod reported, bool requiredOrAdmin) { if (authoritative == null || reported == null) { return HashVerdict.Pass; } string text = EffectiveMode(authoritative); if (text == "Off") { return HashVerdict.Pass; } if (!authoritative.HasRecordedHash()) { if (text == "Strict" && requiredOrAdmin) { if (!string.IsNullOrEmpty(authoritative.ThunderstorePackage) && !ThunderstoreResolver.ResolutionSettled) { Logger.LogInfo("Thunderstore hash resolution has not settled yet; deferring Strict enforcement for " + authoritative.PluginID + " on this connection."); return HashVerdict.Pass; } return HashVerdict.NotRecorded; } return HashVerdict.Pass; } if (string.IsNullOrEmpty(reported.Hash)) { return HashVerdict.Unverifiable; } if (!authoritative.AcceptsHash(reported.Hash)) { return HashVerdict.Mismatch; } return HashVerdict.Pass; } } internal static class PluginHasher { internal sealed class PluginFingerprint { public string Hash; public string Status; } private sealed class CacheEntry { public long Length; public DateTime MTimeUtc; public string Hash; } internal const string StatusDynamic = "dynamic"; internal const string StatusMissing = "missing"; internal const string StatusUnreadable = "unreadable"; internal const string StatusTimedOut = "timeout"; private static readonly ConcurrentDictionary fileCache = new ConcurrentDictionary(); private static readonly ConcurrentDictionary results = new ConcurrentDictionary(); private static readonly HashSet warnedDynamic = new HashSet(); private static Task pass; private static readonly object passLock = new object(); internal static void BeginPass(Dictionary plugins) { if (plugins == null || plugins.Count == 0) { return; } lock (passLock) { if (pass != null && !pass.IsCompleted) { return; } List> work = plugins.ToList(); int workers = Math.Max(1, Math.Min(4, Environment.ProcessorCount)); pass = Task.Run(delegate { Stopwatch stopwatch = Stopwatch.StartNew(); try { Parallel.ForEach(work, new ParallelOptions { MaxDegreeOfParallelism = workers }, delegate(KeyValuePair plugin) { results[plugin.Key] = Fingerprint(plugin.Key, plugin.Value); }); Logger.LogInfo($"Hashed {work.Count} plugin file(s) in {stopwatch.ElapsedMilliseconds}ms."); } catch (Exception ex) { Logger.LogWarning("Plugin hashing pass failed: " + ex.Message); } }); } } private static PluginFingerprint Fingerprint(string guid, BaseUnityPlugin plugin) { string text = ResolveLocation(plugin); if (string.IsNullOrEmpty(text)) { WarnDynamicOnce(guid); return new PluginFingerprint { Status = "dynamic" }; } string status; string hash = HashFile(text, out status); return new PluginFingerprint { Hash = hash, Status = status }; } private static void WarnDynamicOnce(string guid) { lock (warnedDynamic) { if (!warnedDynamic.Add(guid)) { return; } } Logger.LogWarning("Plugin " + guid + " has no file on disk (it was loaded from memory), so it cannot be file-verified. A server that verifies this mod will reject this client."); } internal static bool WaitForPass(int timeoutMs) { Task task; lock (passLock) { task = pass; } if (task == null) { return true; } if (task.Wait(timeoutMs)) { return true; } Logger.LogWarning($"Plugin hashing did not finish within {timeoutMs}ms; the remaining plugins are reported as unverifiable."); return false; } internal static void ApplyTo(Dictionary mods) { if (mods == null) { return; } foreach (KeyValuePair mod in mods) { Apply(mod.Key, mod.Value); } } internal static void Apply(string guid, DataObjects.Mod mod) { if (mod != null) { if (results.TryGetValue(guid, out var value)) { mod.Hash = value.Hash; mod.HashStatus = value.Status; } else { mod.Hash = null; mod.HashStatus = "timeout"; } } } internal static PluginFingerprint Get(string guid) { if (!results.TryGetValue(guid, out var value)) { return null; } return value; } internal static string ResolveLocation(BaseUnityPlugin plugin) { try { object obj; if (plugin == null) { obj = null; } else { PluginInfo info = plugin.Info; obj = ((info != null) ? info.Location : null); } string text = (string)obj; if (!string.IsNullOrEmpty(text)) { return text; } return ((object)plugin)?.GetType()?.Assembly?.Location; } catch (Exception ex) { Logger.LogDebug("Could not resolve a file location for a plugin: " + ex.Message); return null; } } internal static string HashFile(string path, out string status) { status = null; try { FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists) { status = "missing"; return null; } if (fileCache.TryGetValue(path, out var value) && value.Length == fileInfo.Length && value.MTimeUtc == fileInfo.LastWriteTimeUtc) { return value.Hash; } string text; using (FileStream inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 65536)) { using SHA256 sHA = SHA256.Create(); text = ToHex(sHA.ComputeHash(inputStream)); } fileCache[path] = new CacheEntry { Length = fileInfo.Length, MTimeUtc = fileInfo.LastWriteTimeUtc, Hash = text }; return text; } catch (Exception ex) { Logger.LogWarning("Could not hash plugin file " + path + ": " + ex.Message); status = "unreadable"; return null; } } internal static string ToHex(byte[] bytes) { if (bytes == null) { return null; } StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); foreach (byte b in bytes) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } } internal static class ThunderstoreResolver { private sealed class Attempt { public int Failures; public DateTime NextAttemptUtc; } private sealed class ResolvedEntry { public string PluginID; public List Hashes; public string HashedFrom; } internal class ResolverBehaviour : MonoBehaviour { private DateTime modsFileStamp; public void Update() { DrainResults(); if (!passQueued || running) { return; } passQueued = false; if (!ValConfig.ResolveThunderstoreHashes.Value) { firstPassDone = true; return; } List> work = CollectWork(); if (work.Count == 0) { firstPassDone = true; return; } try { modsFileStamp = File.GetLastWriteTimeUtc(ValConfig.ModsConfigFilePath); } catch (Exception) { modsFileStamp = DateTime.MinValue; } running = true; CancellationToken token = cancellation?.Token ?? CancellationToken.None; Logger.LogInfo($"Resolving Thunderstore hashes for {work.Count} package(s)."); Task.Run(delegate { try { RunPass(work, token); } catch (Exception ex2) { Logger.LogWarning("Thunderstore resolve pass failed: " + ex2.Message); } finally { running = false; firstPassDone = true; } }); } private void DrainResults() { if (resolved.IsEmpty) { return; } bool flag = false; ResolvedEntry result; while (resolved.TryDequeue(out result)) { if (ApplyResolved(result)) { flag = true; } } if (!flag) { return; } DateTime lastWriteTimeUtc; try { lastWriteTimeUtc = File.GetLastWriteTimeUtc(ValConfig.ModsConfigFilePath); } catch (Exception) { lastWriteTimeUtc = modsFileStamp; } if (lastWriteTimeUtc != modsFileStamp) { Logger.LogInfo("Mods.yaml changed while hashes were being resolved; not overwriting it. Re-resolving against the new file."); RequestPass("mods file changed during resolve"); return; } ModManager.PersistModSettings(); try { modsFileStamp = File.GetLastWriteTimeUtc(ValConfig.ModsConfigFilePath); } catch (Exception) { } } private static bool ApplyResolved(ResolvedEntry entry) { DataObjects.Mods modSettings = ModManager.ModSettings; if (modSettings == null || entry?.Hashes == null || entry.Hashes.Count == 0) { return false; } bool result = false; Dictionary[] array = new Dictionary[3] { modSettings.RequiredMods, modSettings.OptionalMods, modSettings.AdminOnlyMods }; foreach (Dictionary dictionary in array) { if (dictionary != null && dictionary.TryGetValue(entry.PluginID, out var value)) { if (string.Equals(value.HashSource, "Manual", StringComparison.OrdinalIgnoreCase)) { Logger.LogDebug("Keeping the manually pinned hash for " + entry.PluginID + "; not applying the resolved one."); continue; } value.AcceptedHashes = new List(entry.Hashes); value.HashSource = "Thunderstore"; value.HashedFrom = entry.HashedFrom; result = true; Logger.LogInfo($"Recorded {entry.Hashes.Count} hash(es) for {entry.PluginID} from {entry.HashedFrom}."); } } return result; } } [HarmonyPatch(typeof(ZNet), "Start")] public static class ZNet_Start_Patch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { if (__instance.IsServer()) { Initialize(); RequestPass("server start"); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] public static class ZNet_Shutdown_Patch { [HarmonyPostfix] private static void Postfix() { Teardown(); } } private static volatile bool firstPassDone = false; private static readonly Regex NamePart = new Regex("^[A-Za-z0-9_]+$", RegexOptions.Compiled); private static readonly Regex VersionPart = new Regex("^\\d{1,6}\\.\\d{1,6}\\.\\d{1,6}$", RegexOptions.Compiled); private static readonly string[] AllowedHosts = new string[4] { "thunderstore.io", "www.thunderstore.io", "gcdn.thunderstore.io", "hcdn-1.hcdn.thunderstore.io" }; private const int MaxRedirects = 5; private const int MaxZipEntries = 2000; private const long MaxEntryBytes = 67108864L; private const long MaxTotalUncompressedBytes = 419430400L; private static readonly HttpClient http = new HttpClient((HttpMessageHandler)new HttpClientHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None }) { Timeout = TimeSpan.FromSeconds(90.0) }; private static readonly Dictionary attempts = new Dictionary(); private static readonly ConcurrentQueue resolved = new ConcurrentQueue(); private static ResolverBehaviour host; private static CancellationTokenSource cancellation; private static volatile bool running; private static volatile bool passQueued; internal static bool ResolutionSettled { get { if (!firstPassDone && !((Object)(object)ZNet.instance == (Object)null)) { return !ZNet.instance.IsServer(); } return true; } } internal static void Initialize() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown //IL_0028: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)host != (Object)null)) { cancellation = new CancellationTokenSource(); GameObject val = new GameObject("VE_ThunderstoreResolver"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; host = val.AddComponent(); Logger.LogDebug("Thunderstore hash resolver initialized."); } } internal static void Teardown() { try { cancellation?.Cancel(); } catch (Exception) { } cancellation?.Dispose(); cancellation = null; if ((Object)(object)host != (Object)null) { Object.Destroy((Object)(object)((Component)host).gameObject); host = null; } ResolvedEntry result; while (resolved.TryDequeue(out result)) { } running = false; passQueued = false; firstPassDone = false; } internal static void RequestPass(string reason) { if (!ValConfig.ResolveThunderstoreHashes.Value) { firstPassDone = true; } else if (!((Object)(object)host == (Object)null)) { Logger.LogDebug("Thunderstore hash resolve requested (" + reason + ")."); passQueued = true; } } private static List> CollectWork() { List> list = new List>(); DataObjects.Mods modSettings = ModManager.ModSettings; if (modSettings == null) { return list; } Dictionary[] array = new Dictionary[3] { modSettings.RequiredMods, modSettings.OptionalMods, modSettings.AdminOnlyMods }; foreach (Dictionary dictionary in array) { if (dictionary == null) { continue; } foreach (KeyValuePair item in dictionary) { DataObjects.Mod value = item.Value; if (value != null && !string.IsNullOrWhiteSpace(value.ThunderstorePackage) && TryResolveSpec(item.Key, value, out var spec) && (!value.HasRecordedHash() || !string.Equals(value.HashedFrom, spec, StringComparison.OrdinalIgnoreCase)) && ShouldAttempt(spec) && !list.Any((KeyValuePair w) => w.Value == spec)) { list.Add(new KeyValuePair(item.Key, spec)); } } } return list; } private static bool TryResolveSpec(string key, DataObjects.Mod mod, out string spec) { spec = null; if (!TryParseSpec(mod.ThunderstorePackage, out var owner, out var name, out var version)) { Logger.LogWarning("Mods.yaml entry '" + key + "' has an unusable thunderstorePackage '" + mod.ThunderstorePackage + "'. Expected Owner-ModName or Owner-ModName-Version, with no punctuation beyond the separating dashes."); return false; } if (version == null) { if (mod.Version == null || !VersionPart.IsMatch(mod.Version)) { Logger.LogWarning("Mods.yaml entry '" + key + "' has thunderstorePackage '" + mod.ThunderstorePackage + "' with no version, and its version field '" + mod.Version + "' is not a Thunderstore version. Pin it explicitly, e.g. '" + owner + "-" + name + "-1.0.0'."); return false; } version = mod.Version; } spec = owner + "-" + name + "-" + version; return true; } internal static bool TryParseSpec(string spec, out string owner, out string name, out string version) { owner = (name = (version = null)); if (string.IsNullOrWhiteSpace(spec)) { return false; } string[] array = spec.Trim().Split(new char[1] { '-' }); if (array.Length != 2 && array.Length != 3) { return false; } if (!NamePart.IsMatch(array[0]) || !NamePart.IsMatch(array[1])) { return false; } if (array.Length == 3 && !VersionPart.IsMatch(array[2])) { return false; } owner = array[0]; name = array[1]; version = ((array.Length == 3) ? array[2] : null); return true; } internal static bool IsAllowedUrl(string url) { if (string.IsNullOrWhiteSpace(url)) { return false; } if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result)) { return false; } if (result.Scheme != "https") { return false; } string[] allowedHosts = AllowedHosts; foreach (string b in allowedHosts) { if (string.Equals(result.Host, b, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static string DownloadUrlFor(string owner, string name, string version) { return "https://thunderstore.io/package/download/" + owner + "/" + name + "/" + version + "/"; } private static bool ShouldAttempt(string spec) { lock (attempts) { if (!attempts.TryGetValue(spec, out var value)) { return true; } if (value.NextAttemptUtc == DateTime.MaxValue) { return false; } return DateTime.UtcNow >= value.NextAttemptUtc; } } private static void NoteFailure(string spec, string reason) { DateTime dateTime; lock (attempts) { if (!attempts.TryGetValue(spec, out var value)) { value = new Attempt(); attempts[spec] = value; } value.Failures++; dateTime = (value.NextAttemptUtc = value.Failures switch { 1 => DateTime.UtcNow.AddMinutes(1.0), 2 => DateTime.UtcNow.AddMinutes(5.0), 3 => DateTime.UtcNow.AddMinutes(30.0), _ => DateTime.MaxValue, }); } string text = ((dateTime == DateTime.MaxValue) ? "no further attempts until Mods.yaml changes or the server restarts" : $"retrying after {dateTime:HH:mm:ss}Z"); Logger.LogWarning("Could not resolve Thunderstore package '" + spec + "': " + reason + " (" + text + ")."); } private static void NoteSuccess(string spec) { lock (attempts) { attempts.Remove(spec); } } private static void RunPass(List> work, CancellationToken token) { long capBytes = (long)ValConfig.ThunderstoreMaxArchiveMB.Value * 1024L * 1024; foreach (KeyValuePair item in work) { if (token.IsCancellationRequested) { break; } string key = item.Key; string value = item.Value; try { TryParseSpec(value, out var owner, out var name, out var version); using MemoryStream memoryStream = DownloadCapped(DownloadUrlFor(owner, name, version), capBytes, token); if (memoryStream == null) { continue; } int dllCount; Dictionary> dictionary = HashArchiveDlls(memoryStream, value, key, out dllCount); if (dllCount == 0) { NoteFailure(value, "the archive contains no DLLs"); continue; } foreach (KeyValuePair> item2 in dictionary) { resolved.Enqueue(new ResolvedEntry { PluginID = item2.Key, Hashes = item2.Value, HashedFrom = value }); } NoteSuccess(value); Logger.LogInfo($"Resolved {value}: {dllCount} DLL(s), hashes recorded for {dictionary.Count} plugin id(s)."); } catch (OperationCanceledException) { break; } catch (Exception ex2) { NoteFailure(value, ex2.Message); } } } private static MemoryStream DownloadCapped(string url, long capBytes, CancellationToken token) { string text = url; for (int i = 0; i <= 5; i++) { if (!IsAllowedUrl(text)) { NoteFailure(url, "refused to follow '" + text + "': not a permitted Thunderstore host"); return null; } HttpResponseMessage result = http.GetAsync(text, (HttpCompletionOption)1, token).GetAwaiter().GetResult(); try { if (result.StatusCode >= HttpStatusCode.MultipleChoices && result.StatusCode < HttpStatusCode.BadRequest) { Uri location = result.Headers.Location; if (location == null) { NoteFailure(url, $"HTTP {(int)result.StatusCode} with no redirect target"); return null; } text = (location.IsAbsoluteUri ? location.ToString() : new Uri(new Uri(text), location).ToString()); continue; } if (!result.IsSuccessStatusCode) { NoteFailure(url, $"HTTP {(int)result.StatusCode}"); return null; } long? contentLength = result.Content.Headers.ContentLength; if (contentLength.HasValue && contentLength.Value > capBytes) { NoteFailure(url, $"archive is {contentLength.Value / 1048576}MB, over the {capBytes / 1048576}MB limit"); return null; } MemoryStream memoryStream = new MemoryStream((int)(contentLength.HasValue ? Math.Min(contentLength.Value, capBytes) : 1048576)); try { using (Stream stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) { byte[] array = new byte[65536]; long num = 0L; int num2; while ((num2 = stream.Read(array, 0, array.Length)) > 0) { token.ThrowIfCancellationRequested(); num += num2; if (num > capBytes) { NoteFailure(url, $"archive exceeded the {capBytes / 1048576}MB limit while downloading"); memoryStream.Dispose(); return null; } memoryStream.Write(array, 0, num2); } } memoryStream.Position = 0L; return memoryStream; } catch { memoryStream.Dispose(); throw; } } finally { ((IDisposable)result)?.Dispose(); } } NoteFailure(url, $"more than {5} redirects"); return null; } private static Dictionary> HashArchiveDlls(MemoryStream archiveBytes, string spec, string declaringGuid, out int dllCount) { Dictionary> dictionary = new Dictionary>(); List list = new List(); dllCount = 0; long num = 0L; using (ZipArchive zipArchive = new ZipArchive(archiveBytes, ZipArchiveMode.Read, leaveOpen: true)) { if (zipArchive.Entries.Count > 2000) { Logger.LogWarning($"{spec}: archive has {zipArchive.Entries.Count} entries, over the {2000} limit. Skipping."); return dictionary; } foreach (ZipArchiveEntry entry in zipArchive.Entries) { if (entry.FullName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { Logger.LogWarning(spec + ": nested archive '" + entry.FullName + "' ignored; nested archives are not opened."); } else { if (!entry.FullName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) { continue; } if (entry.Length > 67108864) { Logger.LogWarning($"{spec}: '{entry.FullName}' is larger than the {64L}MB per-file limit. Skipping it."); continue; } using MemoryStream memoryStream = new MemoryStream(); using (Stream stream = entry.Open()) { byte[] array = new byte[65536]; int num2; while ((num2 = stream.Read(array, 0, array.Length)) > 0) { num += num2; if (num > 419430400 || memoryStream.Length + num2 > 67108864) { Logger.LogWarning(spec + ": uncompressed contents exceeded the safety limit while reading '" + entry.FullName + "'. Skipping the rest of the archive."); dllCount = 0; return dictionary; } memoryStream.Write(array, 0, num2); } } dllCount++; memoryStream.Position = 0L; string item; using (SHA256 sHA = SHA256.Create()) { item = PluginHasher.ToHex(sHA.ComputeHash(memoryStream)); } List list2 = ReadPluginGuids(memoryStream); if (list2 == null) { list.Add(item); continue; } foreach (string item2 in list2) { if (!string.IsNullOrEmpty(item2)) { if (!dictionary.TryGetValue(item2, out var value)) { value = (dictionary[item2] = new List()); } if (!value.Contains(item)) { value.Add(item); } } } } } } if (list.Count > 0 && !dictionary.ContainsKey(declaringGuid)) { Logger.LogInfo($"{spec}: could not read plugin metadata from {list.Count} DLL(s); accepting all of them for {declaringGuid}."); dictionary[declaringGuid] = list; } return dictionary; } private static List ReadPluginGuids(MemoryStream dll) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) try { dll.Position = 0L; AssemblyDefinition val = AssemblyDefinition.ReadAssembly((Stream)dll); try { List list = new List(); Enumerator enumerator = val.MainModule.Types.GetEnumerator(); try { while (enumerator.MoveNext()) { Enumerator enumerator2 = enumerator.Current.CustomAttributes.GetEnumerator(); try { while (enumerator2.MoveNext()) { CustomAttribute current = enumerator2.Current; if (!(((MemberReference)current.AttributeType).FullName != "BepInEx.BepInPlugin") && current.ConstructorArguments.Count > 0) { CustomAttributeArgument val2 = current.ConstructorArguments[0]; list.Add(((CustomAttributeArgument)(ref val2)).Value as string); } } } finally { ((IDisposable)enumerator2/*cast due to .constrained prefix*/).Dispose(); } } } finally { ((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose(); } return (list.Count > 0) ? list : null; } finally { ((IDisposable)val)?.Dispose(); } } catch (Exception ex) { Logger.LogDebug("Could not read plugin metadata from an archive DLL: " + ex.Message); return null; } } } } namespace ValheimEnforcer.modules.migration { internal sealed class FchItem { internal string PrefabName; internal int Stack; internal float Durability; internal Vector2i GridPos; internal bool Equipped; internal int Quality; internal int Variant; internal long CrafterID; internal string CrafterName = ""; internal Dictionary CustomData; internal int WorldLevel; } internal sealed class FchProfile { internal string PlayerName; internal long PlayerID; internal List Items = new List(); internal Dictionary SkillLevels = new Dictionary(); internal Dictionary CustomData = new Dictionary(); } internal static class FchReader { private const int ProfileVersionMin = 27; private const int ProfileVersionMax = 43; private const int PlayerDataVersionMax = 29; private const int ItemDataVersionMax = 106; private const int SkillsVersionMax = 2; private const int MaxPlausibleHashLength = 1024; internal static bool TryRead(string path, out FchProfile profile, out string error) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown profile = null; error = null; try { if (!TryReadEnvelope(path, out var payload, out error)) { return false; } profile = ReadProfile(new ZPackage(payload)); return true; } catch (EndOfStreamException) { error = "file ends mid-record (truncated or not a player profile)"; return false; } catch (InvalidDataException ex2) { error = ex2.Message; return false; } catch (Exception ex3) { error = ex3.GetType().Name + ": " + ex3.Message; return false; } } private static bool TryReadEnvelope(string path, out byte[] payload, out string error) { payload = null; error = null; using FileStream fileStream = File.OpenRead(path); using BinaryReader binaryReader = new BinaryReader(fileStream); long length = fileStream.Length; if (length < 8) { error = "file is too small to be a player profile"; return false; } int num = binaryReader.ReadInt32(); if (num <= 0 || num > length) { error = $"declared payload length {num} is not plausible for a {length} byte file"; return false; } byte[] array = binaryReader.ReadBytes(num); if (array.Length != num) { error = "file is truncated inside the profile payload"; return false; } int num2 = binaryReader.ReadInt32(); if (num2 <= 0 || num2 > 1024) { error = $"declared checksum length {num2} is not plausible"; return false; } byte[] array2 = binaryReader.ReadBytes(num2); if (array2.Length != num2) { error = "file is truncated inside the checksum"; return false; } using (SHA512 sHA = SHA512.Create()) { if (!sHA.ComputeHash(array).SequenceEqual(array2)) { error = "checksum does not match the payload; the file is corrupt"; return false; } } payload = array; return true; } private static FchProfile ReadProfile(ZPackage pkg) { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Expected O, but got Unknown int num = pkg.ReadInt(); if (num < 27) { throw new InvalidDataException($"player profile version {num} predates the oldest version Valheim itself loads ({27})"); } if (num > 43) { throw new InvalidDataException($"player profile version {num} is newer than this build understands ({43}); the mod needs rebuilding against the current game"); } if (num >= 38) { int num2 = pkg.ReadInt(); for (int i = 0; i < num2; i++) { pkg.ReadSingle(); } } else if (num >= 28) { for (int j = 0; j < 4; j++) { pkg.ReadInt(); } } if (num >= 40) { pkg.ReadBool(); } int num3 = pkg.ReadInt(); for (int k = 0; k < num3; k++) { pkg.ReadLong(); pkg.ReadBool(); pkg.ReadVector3(); pkg.ReadBool(); pkg.ReadVector3(); if (num >= 30) { pkg.ReadBool(); pkg.ReadVector3(); } pkg.ReadVector3(); if (num >= 29 && pkg.ReadBool()) { pkg.ReadByteArray(); } } FchProfile fchProfile = new FchProfile { PlayerName = pkg.ReadString(), PlayerID = pkg.ReadLong() }; pkg.ReadString(); if (num >= 38) { pkg.ReadBool(); pkg.ReadLong(); SkipStringFloatMap(pkg); SkipStringFloatMap(pkg); SkipStringFloatMap(pkg); if (num >= 42) { SkipStringFloatMap(pkg); SkipStringFloatMap(pkg); SkipStringFloatMap(pkg); } } if (!pkg.ReadBool()) { throw new InvalidDataException("profile contains no character data"); } ReadPlayerData(new ZPackage(pkg.ReadByteArray()), fchProfile); return fchProfile; } private static void ReadPlayerData(ZPackage pkg, FchProfile profile) { //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) int num = pkg.ReadInt(); if (num > 29) { throw new InvalidDataException($"character data version {num} is newer than this build understands ({29}); the mod needs rebuilding against the current game"); } if (num >= 7) { pkg.ReadSingle(); } pkg.ReadSingle(); if (num >= 10) { pkg.ReadSingle(); } if (num >= 8 && num < 28) { pkg.ReadBool(); } if (num >= 20) { pkg.ReadSingle(); } if (num >= 23) { pkg.ReadString(); } if (num >= 24) { pkg.ReadSingle(); } if (num == 2) { pkg.ReadZDOID(); } ReadInventory(pkg, profile); SkipStringList(pkg); if (num < 15) { SkipStringList(pkg); } else { int num2 = pkg.ReadInt(); for (int i = 0; i < num2; i++) { pkg.ReadString(); pkg.ReadInt(); } } SkipStringList(pkg); if (num < 19 || num >= 21) { SkipStringList(pkg); } if (num >= 6) { SkipStringList(pkg); } if (num >= 9) { SkipStringList(pkg); } if (num >= 18) { int num3 = pkg.ReadInt(); for (int j = 0; j < num3; j++) { pkg.ReadInt(); } } if (num >= 22) { int num4 = pkg.ReadInt(); for (int k = 0; k < num4; k++) { pkg.ReadString(); pkg.ReadString(); } } if (num >= 4) { pkg.ReadString(); pkg.ReadString(); } if (num >= 5) { pkg.ReadVector3(); pkg.ReadVector3(); } if (num >= 11) { pkg.ReadInt(); } if (num >= 12) { SkipFoods(pkg, num); } if (num >= 17) { ReadSkills(pkg, profile); } if (num >= 26) { int num5 = pkg.ReadInt(); for (int l = 0; l < num5; l++) { string key = pkg.ReadString(); profile.CustomData[key] = pkg.ReadString(); } } } private static void ReadInventory(ZPackage pkg, FchProfile profile) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) int num = pkg.ReadInt(); if (num > 106) { throw new InvalidDataException($"inventory version {num} is newer than this build understands ({106}); the mod needs rebuilding against the current game"); } int num2 = pkg.ReadInt(); for (int i = 0; i < num2; i++) { FchItem fchItem = new FchItem { PrefabName = pkg.ReadString(), Stack = pkg.ReadInt(), Durability = pkg.ReadSingle(), GridPos = pkg.ReadVector2i(), Equipped = pkg.ReadBool() }; fchItem.Quality = ((num < 101) ? 1 : pkg.ReadInt()); fchItem.Variant = ((num >= 102) ? pkg.ReadInt() : 0); if (num >= 103) { fchItem.CrafterID = pkg.ReadLong(); fchItem.CrafterName = pkg.ReadString(); } if (num >= 104) { int num3 = pkg.ReadInt(); for (int j = 0; j < num3; j++) { string key = pkg.ReadString(); string value = pkg.ReadString(); if (fchItem.CustomData == null) { fchItem.CustomData = new Dictionary(); } fchItem.CustomData[key] = value; } } fchItem.WorldLevel = ((num >= 105) ? pkg.ReadInt() : 0); if (num >= 106) { pkg.ReadBool(); } if (!string.IsNullOrEmpty(fchItem.PrefabName)) { profile.Items.Add(fchItem); } } } private static void ReadSkills(ZPackage pkg, FchProfile profile) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) int num = pkg.ReadInt(); if (num > 2) { throw new InvalidDataException($"skills version {num} is newer than this build understands ({2}); the mod needs rebuilding against the current game"); } int num2 = pkg.ReadInt(); for (int i = 0; i < num2; i++) { SkillType key = (SkillType)pkg.ReadInt(); float value = pkg.ReadSingle(); if (num >= 2) { pkg.ReadSingle(); } profile.SkillLevels[key] = value; } } private static void SkipFoods(ZPackage pkg, int version) { int num = pkg.ReadInt(); for (int i = 0; i < num; i++) { if (version >= 14) { pkg.ReadString(); if (version >= 25) { pkg.ReadSingle(); continue; } pkg.ReadSingle(); if (version >= 16) { pkg.ReadSingle(); } } else { pkg.ReadString(); for (int j = 0; j < 6; j++) { pkg.ReadSingle(); } if (version >= 13) { pkg.ReadSingle(); } } } } private static void SkipStringList(ZPackage pkg) { int num = pkg.ReadInt(); for (int i = 0; i < num; i++) { pkg.ReadString(); } } private static void SkipStringFloatMap(ZPackage pkg) { int num = pkg.ReadInt(); for (int i = 0; i < num; i++) { pkg.ReadString(); pkg.ReadSingle(); } } } [HarmonyPatch(typeof(ZNet), "Start")] internal static class ZNet_Start_ServerCharactersImport { [HarmonyPostfix] private static void Postfix(ZNet __instance) { if ((Object)(object)__instance == (Object)null || !__instance.IsServer() || ValConfig.ImportServerCharacters == null || !ValConfig.ImportServerCharacters.Value) { return; } try { Logger.LogInfo(ServerCharactersImport.Run(dryRun: false, force: false).Summary()); } catch (Exception arg) { Logger.LogError($"ServerCharacters import failed: {arg}"); } } } internal sealed class ImportReport { internal string SourceDirectory; internal bool DryRun; internal int Scanned; internal int Imported; internal int SkippedExisting; internal int SkippedUnreadable; internal int Failed; internal readonly List Details = new List(); internal string Summary() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(DryRun ? ("ServerCharacters import (DRY RUN - nothing was written) from " + SourceDirectory) : ("ServerCharacters import from " + SourceDirectory)); stringBuilder.AppendLine($" Candidate files: {Scanned}"); stringBuilder.AppendLine(DryRun ? $" Would import: {Imported}" : $" Imported: {Imported}"); stringBuilder.AppendLine($" Already present: {SkippedExisting}"); stringBuilder.AppendLine($" Unreadable: {SkippedUnreadable}"); stringBuilder.AppendLine($" Failed to write: {Failed}"); foreach (string detail in Details) { stringBuilder.AppendLine(" - " + detail); } return stringBuilder.ToString().TrimEnd(Array.Empty()); } } internal static class ServerCharactersImport { private static readonly Regex ServerCharacterFile = new Regex("^(?[A-Za-z0-9]+)_(?[^_]+)_(?.+)\\.fch$", RegexOptions.IgnoreCase); internal static ImportReport Run(bool dryRun, bool force) { ImportReport importReport = new ImportReport { DryRun = dryRun }; string text = ResolveSourceDirectory(); importReport.SourceDirectory = text ?? "(unresolved)"; if (string.IsNullOrEmpty(text)) { importReport.Details.Add("Could not determine where ServerCharacters keeps its files. Set ServerCharactersImportPath."); return importReport; } if (!Directory.Exists(text)) { importReport.Details.Add("Source directory does not exist: " + text); return importReport; } string[] files; try { files = Directory.GetFiles(text, "*.fch", SearchOption.TopDirectoryOnly); } catch (Exception ex) { importReport.Details.Add("Could not list " + text + ": " + ex.Message); return importReport; } if (!dryRun && ValConfig.InternalStorageMode.Value) { try { InternalDataStore.InstanciateOrLinkMetadataRegistry(); } catch (Exception ex2) { importReport.Details.Add("Could not open the in-world character registry (" + ex2.Message + "); nothing was imported."); return importReport; } } string[] array = files; foreach (string path in array) { string fileName = Path.GetFileName(path); Match match = ServerCharacterFile.Match(fileName); if (!match.Success) { Logger.LogDebug("Import: skipping " + fileName + ", not a ServerCharacters file name."); continue; } if (fileName.IndexOf("_backup_", StringComparison.OrdinalIgnoreCase) >= 0) { Logger.LogDebug("Import: skipping " + fileName + ", it is a backup copy."); continue; } importReport.Scanned++; ImportOne(path, fileName, match, dryRun, force, importReport); } return importReport; } private static void ImportOne(string path, string fileName, Match match, bool dryRun, bool force, ImportReport report) { if (!FchReader.TryRead(path, out var profile, out var error)) { report.SkippedUnreadable++; report.Details.Add(fileName + ": " + error); Logger.LogWarning("Import: could not read " + fileName + " - " + error); return; } string text = PlatformIds.Normalize(match.Groups["platform"].Value + "_" + match.Groups["id"].Value); string playerName = profile.PlayerName; if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(playerName)) { report.SkippedUnreadable++; report.Details.Add(fileName + ": could not determine an account id and character name"); return; } if (!string.Equals(match.Groups["name"].Value, playerName, StringComparison.OrdinalIgnoreCase)) { Logger.LogDebug("Import: " + fileName + " is named for '" + match.Groups["name"].Value + "' but the profile inside is '" + playerName + "'; using the profile's name."); } if (!force && ValConfig.LoadCharacterFromSave(text, playerName) != null) { report.SkippedExisting++; report.Details.Add(playerName + " (" + text + "): already has a character save, left alone"); return; } DataObjects.Character character = BuildCharacter(profile, text, playerName); if (dryRun) { report.Imported++; report.Details.Add($"{playerName} ({text}): would import {character.PlayerItems.Count} item(s), {character.SkillLevels.Count} skill(s)"); return; } try { ValConfig.WritePlayerCharacterToSave(text, character); CharacterStore.Invalidate(text, playerName); report.Imported++; report.Details.Add($"{playerName} ({text}): imported {character.PlayerItems.Count} item(s), {character.SkillLevels.Count} skill(s)"); Logger.LogInfo("Imported ServerCharacters save for " + playerName + " (" + text + ")."); } catch (Exception ex) { report.Failed++; report.Details.Add(playerName + " (" + text + "): write failed - " + ex.Message); Logger.LogWarning("Import: failed to write character save for " + playerName + " (" + text + "): " + ex.Message); } } private static DataObjects.Character BuildCharacter(FchProfile profile, string accountId, string characterName) { //IL_0105: Unknown result type (might be due to invalid IL or missing references) DataObjects.Character character = new DataObjects.Character { Name = characterName, HostID = accountId, LastDisconnect = DataObjects.DisconnectionState.Clean, SkillLevels = new Dictionary(profile.SkillLevels), PlayerCustomData = new Dictionary(profile.CustomData), PlayerItems = new List(), ConfiscatedItems = new List() }; foreach (FchItem item in profile.Items) { character.PlayerItems.Add(new DataObjects.PackedItem { prefabName = item.PrefabName, m_stack = item.Stack, m_durability = item.Durability, m_quality = ((item.Quality == 0) ? 1 : item.Quality), m_variant = item.Variant, m_worldlevel = item.WorldLevel, m_crafterID = item.CrafterID, m_crafterName = (item.CrafterName ?? ""), m_customdata = item.CustomData, m_equipped = item.Equipped, m_gridpos = item.GridPos }); } return character; } internal static string ResolveSourceDirectory() { string value = ValConfig.ServerCharactersImportPath.Value; if (!string.IsNullOrWhiteSpace(value)) { return value.Trim(); } try { return PlayerProfile.GetCharacterFolderPath((FileSource)1); } catch (Exception ex) { Logger.LogWarning("Import: could not resolve the game's character folder (" + ex.Message + "). Set ServerCharactersImportPath."); return null; } } } } namespace ValheimEnforcer.modules.compat { internal class ModCompatability { public static bool IsExtraSlotsEnabled; public static bool IsTrialsOfToilEnabled; internal static void CheckModCompat() { try { Dictionary plugins = BepInExUtils.GetPlugins(false); if (plugins != null) { if (Enumerable.Contains(plugins.Keys, "shudnal.ExtraSlots")) { IsExtraSlotsEnabled = API.IsReady(); } if (Enumerable.Contains(plugins.Keys, "maxfoxgaming.environmentalawareness")) { IsTrialsOfToilEnabled = true; } } } catch { Logger.LogWarning("Unable to check mod compatibility. Ensure that Bepinex can load."); } } } } namespace ValheimEnforcer.modules.compat.ExtraSlots { internal static class Extensions { internal static ExtraSlot ToExtraSlot(this object slot) { return new ExtraSlot { _id = () => (string)AccessTools.Property(API._typeSlot, "ID").GetValue(slot), _name = () => (string)AccessTools.Property(API._typeSlot, "Name").GetValue(slot), _gridPosition = () => (Vector2i)AccessTools.Property(API._typeSlot, "GridPosition").GetValue(slot), _item = () => (ItemData)AccessTools.Property(API._typeSlot, "Item").GetValue(slot), _itemFits = (ItemData item) => (bool)AccessTools.Method(API._typeSlot, "ItemFits", (Type[])null, (Type[])null).Invoke(slot, new object[1] { item }), _isActive = () => (bool)AccessTools.Property(API._typeSlot, "IsActive").GetValue(slot), _isFree = () => (bool)AccessTools.Property(API._typeSlot, "IsFree").GetValue(slot), _isHotkeySlot = () => (bool)AccessTools.Property(API._typeSlot, "IsHotkeySlot").GetValue(slot), _isEquipmentSlot = () => (bool)AccessTools.Property(API._typeSlot, "IsEquipmentSlot").GetValue(slot), _isQuickSlot = () => (bool)AccessTools.Property(API._typeSlot, "IsQuickSlot").GetValue(slot), _isMiscSlot = () => (bool)AccessTools.Property(API._typeSlot, "IsMiscSlot").GetValue(slot), _isAmmoSlot = () => (bool)AccessTools.Property(API._typeSlot, "IsAmmoSlot").GetValue(slot), _isFoodSlot = () => (bool)AccessTools.Property(API._typeSlot, "IsFoodSlot").GetValue(slot), _isCustomSlot = () => (bool)AccessTools.Property(API._typeSlot, "IsCustomSlot").GetValue(slot), _isEmptySlot = () => (bool)AccessTools.Property(API._typeSlot, "IsEmptySlot").GetValue(slot) }; } } public class ExtraSlot { internal Func _id; internal Func _name; internal Func _gridPosition; internal Func _item; internal Func _itemFits; internal Func _isActive; internal Func _isFree; internal Func _isHotkeySlot; internal Func _isEquipmentSlot; internal Func _isQuickSlot; internal Func _isMiscSlot; internal Func _isAmmoSlot; internal Func _isFoodSlot; internal Func _isCustomSlot; internal Func _isEmptySlot; public static readonly Vector2i emptyPosition = new Vector2i(-1, -1); public string ID { get { if (_id == null) { return ""; } return _id(); } } public string Name { get { if (_name == null) { return ""; } return _name(); } } public Vector2i GridPosition { get { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) if (_gridPosition == null) { return emptyPosition; } return _gridPosition(); } } public ItemData Item { get { if (_item == null) { return null; } return _item(); } } public bool IsActive { get { if (_isActive != null) { return _isActive(); } return false; } } public bool IsFree { get { if (_isFree != null) { return _isFree(); } return false; } } public bool IsHotkeySlot { get { if (_isHotkeySlot != null) { return _isHotkeySlot(); } return false; } } public bool IsEquipmentSlot { get { if (_isEquipmentSlot != null) { return _isEquipmentSlot(); } return false; } } public bool IsQuickSlot { get { if (_isQuickSlot != null) { return _isQuickSlot(); } return false; } } public bool IsMiscSlot { get { if (_isMiscSlot != null) { return _isMiscSlot(); } return false; } } public bool IsAmmoSlot { get { if (_isAmmoSlot != null) { return _isAmmoSlot(); } return false; } } public bool IsFoodSlot { get { if (_isFoodSlot != null) { return _isFoodSlot(); } return false; } } public bool IsCustomSlot { get { if (_isCustomSlot != null) { return _isCustomSlot(); } return false; } } public bool IsEmptySlot { get { if (_isEmptySlot != null) { return _isEmptySlot(); } return false; } } public bool ItemFits(ItemData item) { if (_itemFits != null) { return _itemFits(item); } return false; } } public static class API { private static bool _isNotReady; private static readonly List _emptyItemList = new List(); private static readonly List _emptySlotList = new List(); internal static Type _typeAPI; internal static Type _typeSlot; public static bool IsReady() { if (_isNotReady) { return false; } if (_typeAPI != null && _typeSlot != null) { return true; } _isNotReady = !Chainloader.PluginInfos.ContainsKey("shudnal.ExtraSlots"); if (_isNotReady) { return false; } if (_typeAPI == null || _typeSlot == null) { Assembly assembly = Assembly.GetAssembly(((object)Chainloader.PluginInfos["shudnal.ExtraSlots"].Instance).GetType()); if (assembly == null) { _isNotReady = true; return false; } _typeAPI = assembly.GetType("ExtraSlots.API"); _typeSlot = assembly.GetType("ExtraSlots.Slots+Slot"); } if (_typeAPI != null) { return _typeSlot != null; } return false; } public static List GetExtraSlots() { if (IsReady()) { return ((IEnumerable)AccessTools.Method(_typeAPI, "GetExtraSlots", (Type[])null, (Type[])null).Invoke(_typeAPI, null)).Select((object slot) => slot.ToExtraSlot()).ToList(); } return _emptySlotList; } public static List GetEquipmentSlots() { if (IsReady()) { return ((IEnumerable)AccessTools.Method(_typeAPI, "GetEquipmentSlots", (Type[])null, (Type[])null).Invoke(_typeAPI, null)).Select((object slot) => slot.ToExtraSlot()).ToList(); } return _emptySlotList; } public static List GetQuickSlots() { if (IsReady()) { return ((IEnumerable)AccessTools.Method(_typeAPI, "GetQuickSlots", (Type[])null, (Type[])null).Invoke(_typeAPI, null)).Select((object slot) => slot.ToExtraSlot()).ToList(); } return _emptySlotList; } public static List GetFoodSlots() { if (IsReady()) { return ((IEnumerable)AccessTools.Method(_typeAPI, "GetFoodSlots", (Type[])null, (Type[])null).Invoke(_typeAPI, null)).Select((object slot) => slot.ToExtraSlot()).ToList(); } return _emptySlotList; } public static List GetAmmoSlots() { if (IsReady()) { return ((IEnumerable)AccessTools.Method(_typeAPI, "GetAmmoSlots", (Type[])null, (Type[])null).Invoke(_typeAPI, null)).Select((object slot) => slot.ToExtraSlot()).ToList(); } return _emptySlotList; } public static List GetMiscSlots() { if (IsReady()) { return ((IEnumerable)AccessTools.Method(_typeAPI, "GetMiscSlots", (Type[])null, (Type[])null).Invoke(_typeAPI, null)).Select((object slot) => slot.ToExtraSlot()).ToList(); } return _emptySlotList; } public static ExtraSlot FindSlot(string slotID) { if (!IsReady()) { return null; } return AccessTools.Method(_typeAPI, "FindSlot", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[1] { slotID }).ToExtraSlot(); } public static List GetAllExtraSlotsItems() { if (IsReady()) { return (List)AccessTools.Method(_typeAPI, "GetAllExtraSlotsItems", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return _emptyItemList; } public static List GetEquipmentSlotsItems() { if (IsReady()) { return (List)AccessTools.Method(_typeAPI, "GetEquipmentSlotsItems", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return _emptyItemList; } public static List GetQuickSlotsItems() { if (IsReady()) { return (List)AccessTools.Method(_typeAPI, "GetQuickSlotsItems", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return _emptyItemList; } public static List GetFoodSlotsItems() { if (IsReady()) { return (List)AccessTools.Method(_typeAPI, "GetFoodSlotsItems", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return _emptyItemList; } public static List GetAmmoSlotsItems() { if (IsReady()) { return (List)AccessTools.Method(_typeAPI, "GetAmmoSlotsItems", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return _emptyItemList; } public static List GetMiscSlotsItems() { if (IsReady()) { return (List)AccessTools.Method(_typeAPI, "GetMiscSlotsItems", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return _emptyItemList; } public static int GetExtraRows() { if (IsReady()) { return (int)AccessTools.Method(_typeAPI, "GetExtraRows", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return -1; } public static int GetInventoryHeightFull() { if (IsReady()) { return (int)AccessTools.Method(_typeAPI, "GetInventoryHeightFull", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return -1; } public static int GetInventoryHeightPlayer() { if (IsReady()) { return (int)AccessTools.Method(_typeAPI, "GetInventoryHeightPlayer", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } return -1; } public static bool IsGridPositionASlot(Vector2i gridPos) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "IsGridPositionASlot", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[1] { gridPos }); } public static bool IsItemInSlot(ItemData item) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "IsItemInSlot", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[1] { item }); } public static bool IsItemInEquipmentSlot(ItemData item) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "IsItemInEquipmentSlot", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[1] { item }); } public static bool IsAnyGlobalKeyActive(string requiredKeys) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "requiredKeys", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[1] { requiredKeys }); } public static bool IsItemTypeKnown(ItemType itemType) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "IsItemTypeKnown", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[1] { itemType }); } public static bool IsAnyMaterialDiscovered(string itemNames) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "IsAnyMaterialDiscovered", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[1] { itemNames }); } public static bool AddSlot(string slotID, Func getName, Func itemIsValid, Func isActive) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "AddSlot", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[5] { slotID, -1, getName, itemIsValid, isActive }); } public static bool AddSlotWithIndex(string slotID, int slotIndex, Func getName, Func itemIsValid, Func isActive) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "AddSlotWithIndex", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[5] { slotID, slotIndex, getName, itemIsValid, isActive }); } public static bool AddSlotBefore(string slotID, Func getName, Func itemIsValid, Func isActive, params string[] slotIDs) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "AddSlotBefore", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[5] { slotID, getName, itemIsValid, isActive, slotIDs }); } public static bool AddSlotAfter(string slotID, Func getName, Func itemIsValid, Func isActive, params string[] slotIDs) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "AddSlotAfter", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[5] { slotID, getName, itemIsValid, isActive, slotIDs }); } public static bool RemoveSlot(string slotID) { if (!IsReady()) { return false; } return (bool)AccessTools.Method(_typeAPI, "RemoveSlot", (Type[])null, (Type[])null).Invoke(_typeAPI, new object[1] { slotID }); } public static void UpdateSlots() { if (IsReady()) { AccessTools.Method(_typeAPI, "UpdateSlots", (Type[])null, (Type[])null).Invoke(_typeAPI, null); } } } } namespace ValheimEnforcer.modules.commands { internal static class CommandHelpers { public static void ClearSpecifiedPlayerConfiscatedItems(string account, string username, string prefab) { DataObjects.Character character = ValConfig.LoadCharacterFromSave(account, username); Logger.LogInfo($"Found {character.ConfiscatedItems.Count} confiscated items."); if (string.Compare(prefab, "all", ignoreCase: true) == 0) { character.ConfiscatedItems.Clear(); ValConfig.WritePlayerCharacterToSave(account, character); CharacterStore.Invalidate(account, username); Logger.LogInfo("Cleared all confiscated items."); return; } List targetItems = prefab.Split(new char[1] { ',' }).ToList(); character.ConfiscatedItems = character.ConfiscatedItems.Where((DataObjects.PackedItem x) => !targetItems.Contains(x.prefabName)).ToList(); Logger.LogInfo("Removed confiscated item with prefab " + string.Join(",", targetItems) + "."); } public static List LoadCharacterAndFindItemsToReturn(string account, string username, string prefabfilter) { DataObjects.Character character = ValConfig.LoadCharacterFromSave(account, username); List result = new List(); if (character == null) { Logger.LogInfo("Character was not found for the specified account."); return result; } if (character.ConfiscatedItems.Count == 0) { Logger.LogInfo("Player does not have any confiscated items."); return result; } if (string.Compare(prefabfilter, "all", ignoreCase: true) == 0) { result = new List(character.ConfiscatedItems); character.ConfiscatedItems.Clear(); } else { List targetPrefabs = (from s in prefabfilter.Split(new char[1] { ',' }) select s.Trim()).ToList(); result = character.ConfiscatedItems.Where((DataObjects.PackedItem i) => targetPrefabs.Contains(i.prefabName)).ToList(); character.ConfiscatedItems.RemoveAll((DataObjects.PackedItem i) => targetPrefabs.Contains(i.prefabName)); } if (result.Count == 0) { Logger.LogInfo("No matching confiscated items found for the specified filter."); return result; } return result; } } internal static class TerminalCommands { internal class TestNotification : ConsoleCommand { public override string Name => "Enforcer-Test-Notification"; public override bool IsCheat => true; public override string Help => "Posts one Discord notification using sample data, to preview a template from Notifications.yaml. Ignores the Notify* on/off settings but still needs a webhook URL. Server admins only. Format: Enforcer-Test-Notification , or 'list' for the event names."; public override List CommandOptionList() { return Enum.GetNames(typeof(NotificationEvent)).ToList(); } public override void Run(string[] args) { //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown NotificationEvent result; if (args.Length < 1) { Logger.LogInfo("An event is required. One of: " + string.Join(", ", Enum.GetNames(typeof(NotificationEvent)))); } else if (string.Equals(args[0], "list", StringComparison.OrdinalIgnoreCase)) { Logger.LogInfo("Notification events: " + string.Join(", ", Enum.GetNames(typeof(NotificationEvent)))); } else if (!Enum.TryParse(args[0], ignoreCase: true, out result) || !Enum.IsDefined(typeof(NotificationEvent), result)) { Logger.LogInfo("Unknown event '" + args[0] + "'. One of: " + string.Join(", ", Enum.GetNames(typeof(NotificationEvent)))); } else if ((Object)(object)ZNet.instance == (Object)null) { Logger.LogInfo("Not in a game. Join the server first."); } else if (!ZNet.instance.IsServer()) { ZPackage val = new ZPackage(); val.Write(result.ToString()); ValConfig.TestNotificationRPC.SendPackage(ZRoutedRpc.instance.GetServerPeerID(), val); Logger.LogInfo($"Asking the server to post a sample {result} notification..."); } else if (!DiscordNotifier.IsValidWebhookUrl(DiscordNotifier.ResolveUrl(NotificationTemplates.CategoryOf(result)))) { Logger.LogInfo($"No usable webhook URL for the {NotificationTemplates.CategoryOf(result)} category. Set Discord.WebhookUrl, or the URL for that category."); } else { DiscordNotifier.Notify(result, NotificationTemplates.SampleTokens()); Logger.LogInfo($"Posted a sample {result} notification to the {NotificationTemplates.CategoryOf(result)} webhook."); } } } internal class ImportServerCharacters : ConsoleCommand { public override string Name => "Enforcer-Import-ServerCharacters"; public override bool IsCheat => true; public override string Help => "Imports character saves left behind by the ServerCharacters mod. Run 'dryrun' first to see what would happen. Characters that already have a save here are skipped unless 'force' is given. Format: Enforcer-Import-ServerCharacters dryrun|import [force]"; public override void Run(string[] args) { //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Expected O, but got Unknown if (args.Length < 1) { Logger.LogInfo("A mode is required. Format: Enforcer-Import-ServerCharacters dryrun|import [force]"); return; } bool dryRun; if (string.Equals(args[0], "dryrun", StringComparison.OrdinalIgnoreCase)) { dryRun = true; } else { if (!string.Equals(args[0], "import", StringComparison.OrdinalIgnoreCase)) { Logger.LogInfo("Unknown mode '" + args[0] + "'. Use 'dryrun' or 'import'."); return; } dryRun = false; } bool flag = args.Length > 1 && string.Equals(args[1], "force", StringComparison.OrdinalIgnoreCase); if ((Object)(object)ZNet.instance == (Object)null) { Logger.LogInfo("Not in a game. Start or join the server first."); } else if (!ZNet.instance.IsServer()) { ZPackage val = new ZPackage(); val.Write(flag ? (args[0].ToLowerInvariant() + " force") : args[0].ToLowerInvariant()); ValConfig.ImportServerCharactersRPC.SendPackage(ZRoutedRpc.instance.GetServerPeerID(), val); Logger.LogInfo("Requesting the ServerCharacters import from the server..."); } else { Logger.LogInfo(ServerCharactersImport.Run(dryRun, flag).Summary()); } } } internal class ListPlayers : ConsoleCommand { public override string Name => "Enforcer-List-Players"; public override bool IsCheat => true; public override string Help => "Enforcer-List-Players - Provides a full list of all accounts and Player names stored."; public override void Run(string[] args) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown if (ZNet.instance.IsCurrentServerDedicated()) { ValConfig.ListPlayerRPC.SendPackage(ZRoutedRpc.instance.GetServerPeerID(), new ZPackage()); Logger.LogInfo("Requesting player list from server..."); return; } if (ValConfig.InternalStorageMode.Value) { foreach (KeyValuePair> item in InternalDataStore.GetAccountRegistry()) { Logger.LogInfo("Account:" + item.Key); foreach (string item2 in item.Value) { Logger.LogInfo(" " + item2); } } return; } foreach (string item3 in Directory.GetFiles(Path.Combine(Paths.ConfigPath, "ValheimEnforcer", "Characters")).ToList()) { List list = Directory.GetFiles(item3).ToList(); Logger.LogInfo("Account:" + item3.Split(new char[1] { '/' }).Last()); foreach (string item4 in list) { Logger.LogInfo(" " + item4.Split(new char[1] { '/' }).Last()); } } } } internal class ListPlayerConfiscatedItems : ConsoleCommand { public override string Name => "Enforcer-List-Confiscated"; public override bool IsCheat => true; public override string Help => "Gets a list of confiscated items, specific to a player/character. Format: enforcer-list-confiscated 99999999 TerryTheTerrible"; public override void Run(string[] args) { if (args.Length != 2) { Logger.LogInfo("Account ID and playername are required. Ensure your command follows the format: enforcer-list-confiscated 99999999 TerryTheTerrible"); return; } string id = args[0]; string name = args[1]; DataObjects.Character character = ValConfig.LoadCharacterFromSave(id, name); if (character.ConfiscatedItems.Count == 0) { Logger.LogInfo("Player does not have any confiscated items."); return; } Logger.LogInfo($"Found {character.ConfiscatedItems.Count} confiscated items."); foreach (DataObjects.PackedItem confiscatedItem in character.ConfiscatedItems) { Logger.LogInfo($" {confiscatedItem.prefabName} x {confiscatedItem.m_stack}"); } } } internal class ClearPlayerConfiscatedItems : ConsoleCommand { public override string Name => "Enforcer-Clear-Confiscated"; public override bool IsCheat => true; public override string Help => "Clears any confiscated items listed for the specified player Format: enforcer-retrieve-confiscated 99999999 TerryTheTerrible all"; public override void Run(string[] args) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Expected O, but got Unknown if (args.Length != 3) { Logger.LogInfo("Account ID and playername are required. Ensure your command follows the format: enforcer-retrieve-confiscated 99999999 TerryTheTerrible all"); return; } string text = args[0]; string text2 = args[1]; string text3 = args[2]; if (ZNet.instance.IsCurrentServerDedicated()) { ZPackage val = new ZPackage(); DataObjects.RPCServerUpdateData rPCServerUpdateData = new DataObjects.RPCServerUpdateData(); rPCServerUpdateData.ItemPrefabFilter = text3; rPCServerUpdateData.PlatformID = text; rPCServerUpdateData.PlayerName = text2; val.Write(DataObjects.yamlserializer.Serialize((object)rPCServerUpdateData)); ValConfig.ClearConfiscatedRPC.SendPackage(ZRoutedRpc.instance.GetServerPeerID(), val); Logger.LogInfo("Sending command to clear confiscated items on server..."); } else { CommandHelpers.ClearSpecifiedPlayerConfiscatedItems(text, text2, text3); } } } internal class RestorePlayerConfiscatedItems : ConsoleCommand { public override string Name => "Enforcer-Admin-Take-Confiscated"; public override bool IsCheat => true; public override string Help => "Gives you player confiscated items, use either item prefab or 'all'. Format: enforcer-admin-take-confiscated 99999999 TerryTheTerrible all"; public override void Run(string[] args) { if (args.Length != 3) { Logger.LogInfo("Account ID and playername are required. Ensure your command follows the format: enforcer-admin-take-confiscated 99999999 TerryTheTerrible all"); return; } string id = args[0]; string name = args[1]; string text = args[2]; DataObjects.Character character = ValConfig.LoadCharacterFromSave(id, name); if (character == null) { Logger.LogInfo("Character was not found for the specified account."); return; } if (character.ConfiscatedItems.Count == 0) { Logger.LogInfo("Player does not have any confiscated items."); return; } Logger.LogInfo($"Found {character.ConfiscatedItems.Count} confiscated items."); if (string.Compare(text, "all", ignoreCase: true) == 0) { Logger.LogInfo("Providing all confiscated items."); foreach (DataObjects.PackedItem confiscatedItem in character.ConfiscatedItems) { confiscatedItem.AddToInventory(Player.m_localPlayer, use_position: false); } character.ConfiscatedItems.Clear(); return; } foreach (DataObjects.PackedItem confiscatedItem2 in character.ConfiscatedItems) { _ = confiscatedItem2; List list = text.Split(new char[1] { ',' }).ToList(); foreach (DataObjects.PackedItem confiscatedItem3 in character.ConfiscatedItems) { if (list.Contains(confiscatedItem3.prefabName)) { Logger.LogInfo("Providing " + confiscatedItem3.prefabName); confiscatedItem3.AddToInventory(Player.m_localPlayer, use_position: false); } } foreach (string target in list) { character.ConfiscatedItems.RemoveAll((DataObjects.PackedItem i) => i.prefabName == target); } } ValConfig.WritePlayerCharacterToSave(character.HostID, character); } } internal class ReturnPlayerConfiscatedItems : ConsoleCommand { public override string Name => "Enforcer-Return-Confiscated"; public override bool IsCheat => true; public override string Help => "Sends confiscated items to a connected player via RPC. Use 'all' or comma-separated prefab names. Format: Enforcer-Return-Confiscated 99999999 TerryTheTerrible all"; public override void Run(string[] args) { //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Expected O, but got Unknown if (args.Length != 3) { Logger.LogInfo("Account ID, player name, and item filter are required. Ensure your command follows the format: Enforcer-Return-Confiscated 99999999 TerryTheTerrible all"); return; } string text = args[0]; string text2 = args[1]; string text3 = args[2]; if ((Object)(object)Player.m_localPlayer != (Object)null && Player.m_localPlayer.GetPlayerName() == text2 && !ZNet.instance.IsCurrentServerDedicated()) { Logger.LogInfo("Local player is the target, returning player items."); List list = CommandHelpers.LoadCharacterAndFindItemsToReturn(text, text2, text3); DataObjects.Character character = ValConfig.LoadCharacterFromSave(text, text2); foreach (DataObjects.PackedItem item in list) { Logger.LogInfo("Providing " + item.prefabName); item.AddToInventory(Player.m_localPlayer, use_position: false); } ValConfig.WritePlayerCharacterToSave(text, character); } else { ZPackage val = new ZPackage(); DataObjects.RPCServerUpdateData rPCServerUpdateData = new DataObjects.RPCServerUpdateData(); rPCServerUpdateData.PlatformID = text; rPCServerUpdateData.ItemPrefabFilter = text3; rPCServerUpdateData.PlayerName = text2; val.Write(DataObjects.yamlserializer.Serialize((object)rPCServerUpdateData)); ValConfig.ReturnConfiscatedItemsRPC.SendPackage(ZRoutedRpc.instance.GetServerPeerID(), val); } } } internal static void AddCommands() { CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new ListPlayers()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new ClearPlayerConfiscatedItems()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new ReturnPlayerConfiscatedItems()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new ImportServerCharacters()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new TestNotification()); } } } namespace ValheimEnforcer.modules.cheatmonitor { internal static class CheatDetector { private static class NativeWin32 { public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("kernel32.dll")] public static extern bool IsDebuggerPresent(); [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] public static extern bool CheckRemoteDebuggerPresent(IntPtr hProcess, ref bool isDebuggerPresent); } internal class CheatDetectorBehaviour : MonoBehaviour { private float nextScan; private readonly HashSet inspected = new HashSet(); private readonly ConcurrentQueue pending = new ConcurrentQueue(); private bool toolerDetected; private string toolerDetail; private bool reported; private readonly HashSet reportedTools = new HashSet(); private int scanPhase; private void OnEnable() { AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoaded; } private void OnDisable() { AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoaded; } private void Start() { ((MonoBehaviour)this).StartCoroutine(InitialAssemblySweep()); } private void OnAssemblyLoaded(object sender, AssemblyLoadEventArgs args) { if (args?.LoadedAssembly != null) { pending.Enqueue(args.LoadedAssembly); } } private void Update() { bool value = ValConfig.EnableCheatDetection.Value; DrainPending(value && ValConfig.DetectValheimTooler.Value); if (value) { if (toolerDetected && !reported) { TryReportTooler(); } if (!(Time.unscaledTime < nextScan)) { nextScan = Time.unscaledTime + (float)Mathf.Max(5, ValConfig.CheatScanIntervalSeconds.Value); RunPeriodicScan(); } } } private void DrainPending(bool inspect) { Assembly result; while (pending.TryDequeue(out result)) { if (inspect) { InspectAssembly(result); } } } private IEnumerator InitialAssemblySweep() { int processed = 0; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly asm in assemblies) { if (ValConfig.EnableCheatDetection.Value && ValConfig.DetectValheimTooler.Value) { InspectAssembly(asm); } int num = processed + 1; processed = num; if (num % 15 == 0) { yield return null; } } } private void InspectAssembly(Assembly asm) { if (!(asm == null)) { string fullName = asm.FullName; if ((fullName == null || inspected.Add(fullName)) && !toolerDetected && AssemblyHostsTooler(asm, out var detail)) { toolerDetected = true; toolerDetail = detail; Logger.LogWarning("ValheimTooler detected (" + detail + ")."); } } } private void TryReportTooler() { if (CharacterManager.PlayerCharacter != null) { reported = true; Logger.LogWarning("Reporting ValheimTooler detection to server for ban (" + toolerDetail + ")."); ReportCheatScanSummary(new DataObjects.CheatSummaryReport { PlayerName = CharacterManager.PlayerCharacter.Name, PlatformID = CharacterManager.PlayerCharacter.HostID, ValheimToolerStatus = true }); } } private void RunPeriodicScan() { if (ValConfig.DetectValheimTooler.Value && !toolerDetected) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly asm in assemblies) { InspectAssembly(asm); if (toolerDetected) { break; } } } if (CharacterManager.PlayerCharacter != null) { List list = CheatToolCatalog.Enabled(); if (list.Count != 0 || ValConfig.DetectGenericTrainers.Value) { ReportNewDetections((scanPhase++ % 3) switch { 0 => ScanProcesses(list), 1 => ValConfig.ScanLoadedModules.Value ? ScanLoadedModules(list) : new List(), _ => ValConfig.ScanWindowTitles.Value ? ScanWindows(list) : new List(), }); } } } private void ReportNewDetections(List detections) { List list = null; foreach (DataObjects.CheatToolDetection detection in detections) { if ((!detection.Weak || !reportedTools.Contains(detection.Tool)) && reportedTools.Add(detection.Weak ? (detection.Tool + "|weak") : detection.Tool)) { if (list == null) { list = new List(); } list.Add(detection); if (detection.Weak) { Logger.LogWarning("Possible cheat tool, low confidence (server will log only): " + detection.Tool + " (" + detection.Vector + ": " + detection.Detail + ")."); } else { Logger.LogWarning("Cheat tool detected: " + detection.Tool + " (" + detection.Vector + ": " + detection.Detail + ")."); } } } if (list != null) { ReportCheatScanSummary(new DataObjects.CheatSummaryReport { PlayerName = CharacterManager.PlayerCharacter.Name, PlatformID = CharacterManager.PlayerCharacter.HostID, DetectedTools = list }); } } } private const string ToolerNamespace = "ValheimTooler"; private const string ToolerNamespacePrefix = "ValheimTooler."; private const int MaxWindowMatches = 8; internal static void Initialize() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown //IL_0037: Unknown result type (might be due to invalid IL or missing references) if ((!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsDedicated()) && ValConfig.EnableCheatDetection.Value) { GameObject val = new GameObject("VE_CheatDetector"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; val.AddComponent(); Logger.LogDebug("CheatDetector initialized."); } } internal static bool AssemblyHostsTooler(Assembly asm, out string detail) { detail = null; if (asm == null || asm.IsDynamic) { return false; } try { Type[] types; try { types = asm.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types; } Type[] array = types; foreach (Type type in array) { if (!(type == null)) { string text = type.Namespace; if (text != null && (text == "ValheimTooler" || text.StartsWith("ValheimTooler.", StringComparison.Ordinal))) { detail = "type:" + type.FullName + " asm:" + asm.GetName().Name; return true; } } } } catch (Exception ex2) { Logger.LogDebug("CheatDetector.AssemblyHostsTooler failed for " + asm.FullName + ": " + ex2.Message); } return false; } internal static List ScanProcesses(List signatures) { List list = new List(); Process[] array = null; try { array = Process.GetProcesses(); bool value = ValConfig.DetectGenericTrainers.Value; Process[] array2 = array; foreach (Process process in array2) { string text; try { text = process.ProcessName ?? ""; } catch { continue; } if (text.Length == 0 || CheatToolCatalog.IsIgnored(text)) { continue; } foreach (CheatToolSignature signature in signatures) { if (CheatToolCatalog.Matches(text, signature.ProcessNames, signature.ProcessMatch)) { Add(list, signature.Tool, "process", text); } } if (value && CheatToolCatalog.IsGenericTrainerName(text)) { Add(list, "Generic trainer", "process", text); } } } catch (Exception ex) { Logger.LogDebug("CheatDetector.ScanProcesses failed: " + ex.Message); } finally { if (array != null) { Process[] array2 = array; foreach (Process process2 in array2) { try { process2.Dispose(); } catch { } } } } return list; } internal static List ScanLoadedModules(List signatures) { List list = new List(); try { using Process process = Process.GetCurrentProcess(); foreach (ProcessModule module in process.Modules) { string text; try { text = module.ModuleName ?? ""; } catch { continue; } if (text.Length == 0 || CheatToolCatalog.IsIgnored(text)) { continue; } foreach (CheatToolSignature signature in signatures) { if (CheatToolCatalog.Matches(text, signature.ModuleNames, MatchMode.Prefix)) { Add(list, signature.Tool, "module", text); } } } } catch (Exception ex) { Logger.LogDebug("CheatDetector.ScanLoadedModules failed: " + ex.Message); } return list; } internal static List ScanWindows(List signatures) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Invalid comparison between Unknown and I4 List found = new List(); if ((int)Application.platform != 2 && (int)Application.platform != 7) { return found; } List windowed = signatures.Where((CheatToolSignature s) => s.WindowClasses.Length != 0 || s.WeakWindowClasses.Length != 0 || s.WindowTitles.Length != 0).ToList(); if (windowed.Count == 0) { return found; } try { NativeWin32.EnumWindows(delegate(IntPtr hWnd, IntPtr _) { StringBuilder stringBuilder = new StringBuilder(256); NativeWin32.GetClassName(hWnd, stringBuilder, stringBuilder.Capacity); StringBuilder stringBuilder2 = new StringBuilder(256); NativeWin32.GetWindowTextW(hWnd, stringBuilder2, stringBuilder2.Capacity); string text = stringBuilder.ToString(); string text2 = stringBuilder2.ToString(); if (CheatToolCatalog.IsIgnored(text) || CheatToolCatalog.IsIgnored(text2)) { return true; } foreach (CheatToolSignature item in windowed) { WindowMatch windowMatch = CheatToolCatalog.MatchWindow(text, text2, item); if (windowMatch != WindowMatch.None) { Add(found, item.Tool, "window", "class=" + text + "|title=" + text2, windowMatch == WindowMatch.Weak); } } return found.Count < 8; }, IntPtr.Zero); } catch (Exception ex) { Logger.LogDebug("CheatDetector.ScanWindows failed: " + ex.Message); } return found; } private static void Add(List found, string tool, string vector, string detail, bool weak = false) { foreach (DataObjects.CheatToolDetection item in found) { if (item.Tool == tool) { if (item.Weak && !weak) { item.Weak = false; item.Vector = vector; item.Detail = detail; } return; } } found.Add(new DataObjects.CheatToolDetection { Tool = tool, Vector = vector, Detail = detail, Weak = weak }); } internal static void ReportCheatScanSummary(DataObjects.CheatSummaryReport report) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown try { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.GetServerPeer() != null && ValConfig.CheatDetectionRPC != null) { string text = DataObjects.yamlserializer.Serialize((object)report); ZPackage val = new ZPackage(); val.Write(text); ValConfig.CheatDetectionRPC.SendPackage(ZNet.instance.GetServerPeer().m_uid, val); } } catch (Exception ex) { Logger.LogDebug("CheatDetector.ReportCheatScanSummary failed: " + ex.Message); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] internal static class ZNet_RPC_PeerInfo_BanCheck { [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) || !KnownCheaterTracker.IsListed(text)) { return true; } Logger.LogWarning("Rejecting known cheater " + text + ": " + KnownCheaterTracker.GetReason(text)); rpc.Invoke("Error", new object[1] { 8 }); return false; } } internal enum MatchMode { Exact, Prefix, Contains } internal enum WindowMatch { None, Weak, Strong } internal sealed class CheatToolSignature { public string Tool; public string[] ProcessNames = Empty; public MatchMode ProcessMatch; public string[] ModuleNames = Empty; public string[] WindowClasses = Empty; public string[] WeakWindowClasses = Empty; public string[] WindowTitles = Empty; public MatchMode WindowTitleMatch = MatchMode.Prefix; public bool AutoBan; private static readonly string[] Empty = new string[0]; } internal static class CheatToolCatalog { internal const string AdditionalToolLabel = "Admin-listed tool"; internal const string GenericTrainerLabel = "Generic trainer"; private static readonly Regex GenericTrainerPattern = new Regex("\\btrainer\\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly CheatToolSignature[] AutoBanTools = new CheatToolSignature[6] { new CheatToolSignature { Tool = "ValheimTooler", ProcessNames = new string[1] { "valheimtoolerlauncher" }, ProcessMatch = MatchMode.Contains, ModuleNames = new string[1] { "valheimtooler" }, AutoBan = true }, new CheatToolSignature { Tool = "ValHack", ModuleNames = new string[1] { "valhack" }, AutoBan = true }, new CheatToolSignature { Tool = "Valheim Mod Menu", ProcessNames = new string[1] { "valheimmodmenuloader" }, ProcessMatch = MatchMode.Contains, AutoBan = true }, new CheatToolSignature { Tool = "SharpMonoInjector", ProcessNames = new string[2] { "smi", "smi_gui" }, ProcessMatch = MatchMode.Exact, AutoBan = true }, new CheatToolSignature { Tool = "Xenos Injector", ProcessNames = new string[2] { "xenos", "xenos64" }, ProcessMatch = MatchMode.Exact, AutoBan = true }, new CheatToolSignature { Tool = "Extreme Injector", ProcessNames = new string[1] { "extreme injector" }, ProcessMatch = MatchMode.Prefix, AutoBan = true } }; private static readonly CheatToolSignature[] GeneralTools = new CheatToolSignature[8] { new CheatToolSignature { Tool = "WeMod/Wand", ProcessNames = new string[1] { "wemod" }, ProcessMatch = MatchMode.Prefix, ModuleNames = new string[2] { "trainerlib_x64", "celib_x64" }, WindowTitles = new string[1] { "WeMod" }, WindowTitleMatch = MatchMode.Exact }, new CheatToolSignature { Tool = "WeMod/Wand", ProcessNames = new string[2] { "wand", "infinity" }, ProcessMatch = MatchMode.Exact, WindowTitles = new string[1] { "Wand" }, WindowTitleMatch = MatchMode.Exact }, new CheatToolSignature { Tool = "CheatEngine", ProcessNames = new string[3] { "cheatengine", "cheat engine", "magic-engine" }, ProcessMatch = MatchMode.Prefix, ModuleNames = new string[4] { "speedhack-", "dbk32", "dbk64", "vehdebug" }, WeakWindowClasses = new string[2] { "TfrmMain", "TfrmMemView" }, WindowTitles = new string[1] { "Cheat Engine" } }, new CheatToolSignature { Tool = "ArtMoney", ProcessNames = new string[1] { "artmoney" }, ProcessMatch = MatchMode.Contains, WindowTitles = new string[1] { "ArtMoney" } }, new CheatToolSignature { Tool = "PLITCH", ProcessNames = new string[1] { "plitch" }, ProcessMatch = MatchMode.Prefix, WindowTitles = new string[1] { "PLITCH" } }, new CheatToolSignature { Tool = "Speed Gear", ProcessNames = new string[2] { "speedgear", "speederxp" }, ProcessMatch = MatchMode.Prefix }, new CheatToolSignature { Tool = "Squalr", ProcessNames = new string[1] { "squalr" }, ProcessMatch = MatchMode.Exact }, new CheatToolSignature { Tool = "WPE Pro", ProcessNames = new string[2] { "wpe pro", "wpe" }, ProcessMatch = MatchMode.Prefix, WindowTitles = new string[1] { "WPE PRO" } } }; private static string ignoreListRaw; private static List ignoreListParsed = new List(); private static readonly string[] ContentHostWindowClasses = new string[8] { "Chrome_WidgetWin_", "Mozilla", "ApplicationFrameWindow", "IEFrame", "CabinetWClass", "ExploreWClass", "ConsoleWindowClass", "CASCADIA_HOSTING_WINDOW_CLASS" }; internal static List Enabled() { List list = new List(); if (ValConfig.DetectCheatTools.Value) { list.AddRange(AutoBanTools); CheatToolSignature[] generalTools = GeneralTools; foreach (CheatToolSignature cheatToolSignature in generalTools) { if (!(cheatToolSignature.Tool == "CheatEngine") || ValConfig.DetectCheatEngine.Value) { list.Add(cheatToolSignature); } } } foreach (string item in SplitList(ValConfig.AdditionalCheatProcesses.Value)) { list.Add(new CheatToolSignature { Tool = "Admin-listed tool (" + item + ")", ProcessNames = new string[1] { item }, ProcessMatch = MatchMode.Exact }); } return list; } internal static List SplitList(string value) { List list = new List(); if (string.IsNullOrEmpty(value)) { return list; } string[] array = value.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { list.Add(text); } } return list; } internal static bool IsAutoBan(string toolLabel) { if (string.IsNullOrEmpty(toolLabel)) { return false; } CheatToolSignature[] autoBanTools = AutoBanTools; for (int i = 0; i < autoBanTools.Length; i++) { if (string.Equals(autoBanTools[i].Tool, toolLabel, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } internal static bool IsGenericTrainerName(string processName) { if (string.IsNullOrEmpty(processName)) { return false; } return GenericTrainerPattern.IsMatch(processName); } internal static bool IsIgnored(string name) { if (string.IsNullOrEmpty(name)) { return false; } foreach (string item in IgnoreList()) { if (name.IndexOf(item, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } private static List IgnoreList() { string text = ValConfig.IgnoredCheatProcesses.Value ?? ""; if (text != ignoreListRaw) { ignoreListParsed = SplitList(text); ignoreListRaw = text; } return ignoreListParsed; } internal static bool IsContentHostWindow(string windowClass) { return Matches(windowClass, ContentHostWindowClasses, MatchMode.Prefix); } internal static WindowMatch MatchWindow(string windowClass, string windowTitle, CheatToolSignature sig) { if (Matches(windowClass, sig.WindowClasses, MatchMode.Prefix)) { return WindowMatch.Strong; } if (!IsContentHostWindow(windowClass) && Matches(windowTitle, sig.WindowTitles, sig.WindowTitleMatch)) { return WindowMatch.Strong; } if (Matches(windowClass, sig.WeakWindowClasses, MatchMode.Exact)) { return WindowMatch.Weak; } return WindowMatch.None; } internal static bool Matches(string candidate, string[] needles, MatchMode mode) { if (string.IsNullOrEmpty(candidate) || needles == null) { return false; } foreach (string text in needles) { if (string.IsNullOrEmpty(text)) { continue; } switch (mode) { case MatchMode.Exact: if (string.Equals(candidate, text, StringComparison.OrdinalIgnoreCase)) { return true; } break; case MatchMode.Prefix: if (candidate.StartsWith(text, StringComparison.OrdinalIgnoreCase)) { return true; } break; case MatchMode.Contains: if (candidate.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } break; } } return false; } } internal static class KnownCheaterTracker { private const string EmbeddedResourceName = "ValheimEnforcer.assets.KnownCheaters.yaml"; private static readonly Dictionary Cheaters = new Dictionary(); internal static void Initialize() { Cheaters.Clear(); foreach (DataObjects.KnownCheaterEntry item in ReadEmbeddedSeed()) { Upsert(item); } string knownCheatersFilePath = ValConfig.KnownCheatersFilePath; if (File.Exists(knownCheatersFilePath)) { try { foreach (DataObjects.KnownCheaterEntry item2 in Parse(File.ReadAllText(knownCheatersFilePath))) { Upsert(item2); } } catch (Exception ex) { Logger.LogWarning("Failed to read KnownCheaters file at " + knownCheatersFilePath + ": " + ex.Message); } } SaveToDisk(); Logger.LogDebug($"KnownCheaterTracker initialized with {Cheaters.Count} entr(ies)."); } internal static void LoadFromText(string yaml) { Cheaters.Clear(); foreach (DataObjects.KnownCheaterEntry item in ReadEmbeddedSeed()) { Upsert(item); } try { foreach (DataObjects.KnownCheaterEntry item2 in Parse(yaml)) { Upsert(item2); } } catch (Exception ex) { Logger.LogWarning("Failed to parse KnownCheaters update: " + ex.Message); } Logger.LogInfo($"KnownCheaters list reloaded ({Cheaters.Count} entries)."); } internal static void AddCheater(string id, string reason) { if (!string.IsNullOrEmpty(id) && !Cheaters.ContainsKey(id)) { Cheaters[id] = reason ?? ""; Logger.LogInfo("Added " + id + " to the known cheaters list (" + reason + ")."); SaveToDisk(); } } internal static bool IsListed(string hostId) { return FindMatchKey(hostId) != null; } internal static string GetReason(string hostId) { string text = FindMatchKey(hostId); if (text == null) { return null; } return Cheaters[text]; } private static string FindMatchKey(string hostId) { if (string.IsNullOrEmpty(hostId)) { return null; } if (Cheaters.ContainsKey(hostId)) { return hostId; } foreach (string key in Cheaters.Keys) { if (PlatformIds.Matches(key, hostId)) { return key; } } return null; } private static void Upsert(DataObjects.KnownCheaterEntry entry) { if (entry != null && !string.IsNullOrEmpty(entry.Id)) { Cheaters[entry.Id] = entry.Reason ?? ""; } } private static List Parse(string yaml) { if (string.IsNullOrWhiteSpace(yaml)) { return new List(); } return DataObjects.yamldeserializer.Deserialize>(yaml) ?? new List(); } private static List ReadEmbeddedSeed() { try { using Stream stream = typeof(ValheimEnforcer).Assembly.GetManifestResourceStream("ValheimEnforcer.assets.KnownCheaters.yaml"); if (stream == null) { Logger.LogWarning("Embedded KnownCheaters seed resource 'ValheimEnforcer.assets.KnownCheaters.yaml' was not found."); return new List(); } using StreamReader streamReader = new StreamReader(stream); return Parse(streamReader.ReadToEnd()); } catch (Exception ex) { Logger.LogWarning("Failed to read embedded KnownCheaters seed: " + ex.Message); return new List(); } } private static void SaveToDisk() { List list = Cheaters.Select((KeyValuePair kvp) => new DataObjects.KnownCheaterEntry { Id = kvp.Key, Reason = kvp.Value }).ToList(); try { ValConfig.GetSecondaryConfigDirectoryPath(); File.WriteAllText(ValConfig.KnownCheatersFilePath, DataObjects.yamlserializer.Serialize((object)list)); } catch (Exception ex) { Logger.LogWarning("Failed to write KnownCheaters file at " + ValConfig.KnownCheatersFilePath + ": " + ex.Message); } } } } namespace ValheimEnforcer.modules.character { internal static class AccountCharacterLimit { private static string exemptRaw; private static List exemptParsed = new List(); internal static bool Enabled { get { if (ValConfig.EnforceCharacterLimit != null) { return ValConfig.EnforceCharacterLimit.Value; } return false; } } internal static string EvaluateJoin(string hostId, string playerName) { if (!Enabled) { return null; } if (string.IsNullOrEmpty(hostId) || string.IsNullOrEmpty(playerName)) { return null; } if (IsExempt(hostId)) { Logger.LogDebug("Character limit: " + hostId + " is exempt, allowing '" + playerName + "'."); return null; } List knownCharacterNames = GetKnownCharacterNames(hostId); if (knownCharacterNames == null) { return null; } if (knownCharacterNames.Any((string name) => string.Equals(name, playerName, StringComparison.OrdinalIgnoreCase))) { Logger.LogDebug("Character limit: '" + playerName + "' is a known character for " + hostId + "."); return null; } int value = ValConfig.MaxCharactersPerAccount.Value; if (knownCharacterNames.Count < value) { Logger.LogDebug($"Character limit: {hostId} has {knownCharacterNames.Count}/{value} character(s), allowing new character '{playerName}'."); return null; } return BuildRejectionMessage(knownCharacterNames, value); } internal static bool IsExempt(string hostId) { foreach (string item in ExemptIds()) { if (PlatformIds.Matches(item, hostId)) { return true; } } if (ValConfig.CharacterLimitExemptAdmins.Value && (Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsAdmin(hostId); } return false; } internal static List GetKnownCharacterNames(string accountId) { try { if (ValConfig.InternalStorageMode.Value) { List list = new List(); foreach (KeyValuePair> item in InternalDataStore.GetAccountRegistry()) { if (PlatformIds.Matches(item.Key, accountId) && item.Value != null) { list.AddRange(item.Value); } } return Dedupe(list); } string characterFilePath = ValConfig.CharacterFilePath; if (!Directory.Exists(characterFilePath)) { return new List(); } List list2 = new List(); string[] directories = Directory.GetDirectories(characterFilePath); foreach (string path in directories) { if (PlatformIds.Matches(Path.GetFileName(path), accountId)) { string[] files = Directory.GetFiles(path, "*.yaml"); foreach (string path2 in files) { list2.Add(Path.GetFileNameWithoutExtension(path2)); } } } return Dedupe(list2); } catch (Exception ex) { Logger.LogWarning("Character limit: could not read stored characters for " + accountId + " (" + ex.Message + "). Allowing the connection."); return null; } } private static string BuildRejectionMessage(List known, int limit) { if (limit == 1 && known.Count == 1) { return "This server allows one character per account.\nYou are already playing here as '" + known[0] + "' - rejoin with that character.\nAsk a server admin if you need your character reset."; } return $"This server allows {limit} character(s) per account.\n" + "You already have: " + string.Join(", ", known.ToArray()) + ".\nRejoin with one of those, or ask a server admin if you need one reset."; } private static List ExemptIds() { string text = ValConfig.CharacterLimitExemptAccounts.Value ?? ""; if (text != exemptRaw) { exemptParsed = (from entry in text.Split(new char[1] { ',' }) select entry.Trim() into entry where entry.Length > 0 select entry).ToList(); exemptRaw = text; } return exemptParsed; } private static List Dedupe(List names) { return names.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } } internal static class CharacterDeltaTracker { internal static float LastDeltaSyncTime; internal static DeltaChangeTracker DeltaTracker; internal const float SettleSeconds = 2f; private static Inventory watched; internal static bool BaselineDirty { get; private set; } internal static float DirtySince { get; private set; } internal static void Initialize() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown //IL_0037: Unknown result type (might be due to invalid IL or missing references) if ((!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsDedicated()) && !((Object)(object)DeltaTracker != (Object)null)) { GameObject val = new GameObject("VE_ItemDeltaTracker"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; DeltaTracker = val.AddComponent(); Logger.LogDebug("ItemDeltaTracker initialized."); } } internal static void WatchInventory(Player player) { if (!((Object)(object)player == (Object)null)) { Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory != null && inventory != watched) { StopWatching(); watched = inventory; Inventory obj = watched; obj.m_onChanged = (Action)Delegate.Combine(obj.m_onChanged, new Action(MarkBaselineDirty)); Logger.LogDebug("Watching local player inventory for changes."); } } } internal static void StopWatching() { if (watched != null) { Inventory obj = watched; obj.m_onChanged = (Action)Delegate.Remove(obj.m_onChanged, new Action(MarkBaselineDirty)); } watched = null; BaselineDirty = false; } private static void MarkBaselineDirty() { BaselineDirty = true; DirtySince = Time.unscaledTime; } internal static void ClearDirty() { BaselineDirty = false; } internal static DataObjects.PackedItem BuildPackedItem(ItemData item) { //IL_00b8: Unknown result type (might be due to invalid IL or missing references) return new DataObjects.PackedItem { prefabName = ((Object)item.m_dropPrefab).name, m_stack = item.m_stack, m_durability = Mathf.Clamp(item.m_durability, 0f, item.m_shared.m_maxDurability + item.m_shared.m_durabilityPerLevel * (float)Mathf.Max(item.m_quality, 1)), m_quality = item.m_quality, m_variant = item.m_variant, m_worldlevel = item.m_worldLevel, m_crafterID = item.m_crafterID, m_crafterName = item.m_crafterName, m_customdata = DataObjects.PackedItem.CopyCustomData(item.m_customData), m_equipped = item.m_equipped, m_gridpos = item.m_gridPos }; } internal static List BuildCharacterItemDeltas() { List list = new List(); if (CharacterManager.PlayerCharacter == null) { return list; } List list2 = new List(); foreach (ItemData allItem in ((Humanoid)Player.m_localPlayer).GetInventory().GetAllItems()) { list2.Add(BuildPackedItem(allItem)); } foreach (DataObjects.PackedItem playerItem in CharacterManager.PlayerCharacter.PlayerItems) { if (playerItem != null) { int num = list2.IndexOf(playerItem); if (num >= 0) { list2.RemoveAt(num); continue; } list.Add(new DataObjects.ItemDelta { Item = playerItem, Op = DataObjects.ItemDeltaChangeType.Removed }); } } foreach (DataObjects.PackedItem item in list2) { list.Add(new DataObjects.ItemDelta { Item = item, Op = DataObjects.ItemDeltaChangeType.Added }); } return list; } } internal static class CharacterLimitPatches { [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] internal static class ZNet_RPC_PeerInfo_CharacterLimit { [HarmonyPrefix] private static bool Prefix(ZNet __instance, ZRpc rpc, ZPackage pkg) { if (!__instance.IsServer() || !AccountCharacterLimit.Enabled) { return true; } ISocket socket = rpc.GetSocket(); string text = ((socket != null) ? socket.GetHostName() : null); if (string.IsNullOrEmpty(text)) { return true; } if (!TryPeekPlayerName(pkg, out var playerName)) { return true; } string text2 = AccountCharacterLimit.EvaluateJoin(text, playerName); if (text2 == null) { return true; } Logger.LogWarning("Refusing '" + playerName + "' from " + text + ": account character limit reached."); NotifyRejection(playerName, text, text2); rpc.Invoke("VE_CHARLIMIT_MSG", new object[1] { text2 }); rpc.Invoke("Error", new object[1] { 12 }); ISocket socket2 = rpc.GetSocket(); if (socket2 != null) { socket2.Flush(); } return false; } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] internal static class ZNet_OnNewConnection_RegisterCharacterLimitReason { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (!((Object)(object)__instance == (Object)null) && !__instance.IsServer() && peer != null) { PendingRejectReason = null; peer.m_rpc.Register("VE_CHARLIMIT_MSG", (Action)RPC_CharacterLimitReason); } } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError", new Type[] { typeof(ConnectionStatus) })] internal static class FejdStartup_ShowConnectError_CharacterLimitReason { [HarmonyPostfix] private static void Postfix(FejdStartup __instance) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001c: 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_0021: Invalid comparison between Unknown and I4 //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Invalid comparison between Unknown and I4 if (string.IsNullOrEmpty(PendingRejectReason) || (Object)(object)__instance == (Object)null) { return; } ConnectionStatus connectionStatus = ZNet.GetConnectionStatus(); if ((int)connectionStatus == 0 || (int)connectionStatus == 1 || (int)connectionStatus == 2) { return; } string pendingRejectReason = PendingRejectReason; PendingRejectReason = null; GameObject connectionFailedPanel = __instance.m_connectionFailedPanel; TMP_Text connectionFailedError = __instance.m_connectionFailedError; if ((Object)(object)connectionFailedError == (Object)null) { Logger.LogDebug("Character limit: no connection failed label to write the rejection reason to."); return; } if ((Object)(object)connectionFailedPanel != (Object)null) { connectionFailedPanel.SetActive(true); } connectionFailedError.text = pendingRejectReason; } } internal const string RPC_NAME = "VE_CHARLIMIT_MSG"; private static string PendingRejectReason; private static bool TryPeekPlayerName(ZPackage pkg, out string playerName) { //IL_0025: 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_0045: Unknown result type (might be due to invalid IL or missing references) playerName = null; if (pkg == null) { return false; } int pos = pkg.GetPos(); try { pkg.ReadLong(); GameVersion val = default(GameVersion); if (!GameVersion.TryParseGameVersion(pkg.ReadString(), ref val) || val < Version.FirstVersionWithNetworkVersion) { return false; } if (pkg.ReadUInt() != 36) { return false; } pkg.ReadVector3(); playerName = pkg.ReadString(); return !string.IsNullOrEmpty(playerName); } catch (Exception ex) { Logger.LogWarning("Character limit: could not read the character name from the peer info package (" + ex.Message + "). Allowing the connection."); playerName = null; return false; } finally { pkg.SetPos(pos); } } private static void NotifyRejection(string playerName, string hostId, string reason) { if (ValConfig.DiscordNotifyCharacterRejected.Value) { DiscordNotifier.Notify(NotificationEvent.CharacterRejected, new Dictionary { { "character", playerName }, { "playerId", hostId }, { "reason", reason ?? "" }, { "maxCharacters", ValConfig.MaxCharactersPerAccount.Value.ToString() } }); } } private static void RPC_CharacterLimitReason(ZRpc rpc, string reason) { if (!string.IsNullOrEmpty(reason)) { Logger.LogInfo("Server refused this character: " + reason); PendingRejectReason = reason; ModManager.DetailsUpdater?.UpdateErrorText(reason, ""); } } } internal static class CharacterManager { internal static DataObjects.Character PlayerCharacter = null; internal static bool LogoutInProgress = false; internal static bool JoinValidationComplete = false; internal static List staringAllowedPrefabs = new List { "ArmorRagsChest", "ArmorRagsLegs", "Torch" }; internal static void SetPlayerCharacter(DataObjects.Character character) { if (character != null) { Logger.LogDebug("Set character from Saved server data"); PlayerCharacter = character; } } internal static string GetPlayerID(Player player) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) List playerList = ZNet.instance.GetPlayerList(); string text = ""; foreach (PlayerInfo item in playerList) { Logger.LogDebug($"Checking player {item.m_characterID} with ID {item.m_userInfo.m_id.m_userID} against local player {((Character)player).m_nview.GetZDO().m_uid}"); if (item.m_characterID == ((Character)player).m_nview.GetZDO().m_uid) { text = item.m_userInfo.m_id.m_userID; break; } } if (text.Length < 1) { string playerName = player.GetPlayerName(); foreach (PlayerInfo item2 in playerList) { if (item2.m_name == playerName) { text = item2.m_userInfo.m_id.m_userID; Logger.LogDebug("Matched player " + playerName + " by name to ID " + text); break; } } } if (text.Length < 1) { Logger.LogWarning("Failed to find matching player ID for local player " + player.GetPlayerName() + ". Defaulting to ZDO UID as player ID."); text = ((object)Unsafe.As(ref ((Character)player).m_nview.GetZDO().m_uid)/*cast due to .constrained prefix*/).ToString(); } if (text.Contains(":")) { Logger.LogDebug("Player ID contained invalid character : removing."); text = text.Split(new char[1] { ':' })[0]; } return text; } internal static void SavePlayerCharacter(Player __instance) { //IL_0009: 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 ((Object)(object)__instance == (Object)null) { return; } Scene activeScene = SceneManager.GetActiveScene(); if (!((Scene)(ref activeScene)).name.Equals("main")) { return; } DataObjects.DisconnectionState lastDisconnect = ((!LogoutInProgress) ? DataObjects.DisconnectionState.DirtyDisconnect : DataObjects.DisconnectionState.Clean); string text = ""; string text2 = ""; DataObjects.Character character = null; if (PlayerCharacter != null) { character = PlayerCharacter; text = PlayerCharacter.HostID; text2 = PlayerCharacter.Name; } else { text = GetPlayerID(__instance); text2 = __instance.GetPlayerName(); } Logger.LogDebug("Saving character for player " + text2 + " with id " + text); if (PlayerCharacter == null) { character = ValConfig.LoadCharacterFromSave(text, text2); } if (character == null) { Logger.LogWarning("Attempted to save character for player " + text2 + " with ID " + text + " but no existing character data was found. Creating new character data."); character = new DataObjects.Character { Name = text2, HostID = text, SkillLevels = ((Character)__instance).GetSkills().GetSkillList().ToDictionary((Skill skill) => skill.m_info.m_skill, (Skill skill) => skill.m_level), ConfiscatedItems = null, LastDisconnect = lastDisconnect }; foreach (ItemData item in ((Humanoid)__instance).GetInventory().GetAllItems().ToList()) { character.AddItemToPlayerItems(item); } if (ValConfig.PreventExternalCustomDataChanges.Value) { character.PlayerCustomData = __instance.m_customData; } if (ValConfig.SavePlayerStatusEffectsOnLogout.Value) { character.ActiveCharacterEffects.Clear(); foreach (StatusEffect statusEffect in ((Character)__instance).GetSEMan().GetStatusEffects()) { Logger.LogDebug("Saving active status effect: " + ((Object)statusEffect).name); if (character.ActiveCharacterEffects.ContainsKey(((Object)statusEffect).name)) { character.ActiveCharacterEffects[((Object)statusEffect).name] = new DataObjects.PackedStatusEffect(statusEffect); } else { character.ActiveCharacterEffects.Add(((Object)statusEffect).name, new DataObjects.PackedStatusEffect(statusEffect)); } } } } else { Logger.LogDebug("Existing character data found for player " + text2 + " with ID " + text + ". Updating character data with current player information."); character.LastDisconnect = lastDisconnect; character.SkillLevels = ((Character)__instance).GetSkills().GetSkillList().ToDictionary((Skill skill) => skill.m_info.m_skill, (Skill skill) => skill.m_level); Logger.LogDebug("Updated player skills for " + text2 + " with ID " + text + "."); if (ValConfig.PreventExternalCustomDataChanges.Value) { character.PlayerCustomData = __instance.m_customData; Logger.LogDebug("Updated player custom data."); } character.PlayerItems.Clear(); foreach (ItemData item2 in ((Humanoid)__instance).GetInventory().GetAllItems().ToList()) { character.AddItemToPlayerItems(item2); } Logger.LogDebug("Updated player Items for " + text2 + " with ID " + text + "."); if (ValConfig.SavePlayerStatusEffectsOnLogout.Value) { character.ActiveCharacterEffects.Clear(); foreach (StatusEffect statusEffect2 in ((Character)__instance).GetSEMan().GetStatusEffects()) { Logger.LogDebug("Saving active status effect: " + ((Object)statusEffect2).name); if (character.ActiveCharacterEffects.ContainsKey(((Object)statusEffect2).name)) { character.ActiveCharacterEffects[((Object)statusEffect2).name] = new DataObjects.PackedStatusEffect(statusEffect2); } else { character.ActiveCharacterEffects.Add(((Object)statusEffect2).name, new DataObjects.PackedStatusEffect(statusEffect2)); } } Logger.LogDebug("Updated player active status effects."); } } if (character == null) { Logger.LogWarning("Savable character was null, not sending network updates."); return; } ValConfig.WritePlayerCharacterToSave(text, character); ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetServerPeer() : null); if (val != null) { if (LogoutInProgress) { Logger.LogDebug("Sending final character data to server (synchronous, logout)."); FinalSaveRpc.SendFinalSaveSync(val, character); } else { Logger.LogDebug("Sending updated character data to server."); ValConfig.CharacterSaveRPC.SendPackage(val.m_uid, ValConfig.SendCharacterAsZpackage(character)); } } else if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { Logger.LogDebug("No server peer; local write is authoritative."); } else { Logger.LogWarning("Server Disconnected, can't sync player data. This may result in desync issues."); } } internal static void LoadAndValidatePlayer(Player player) { //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) LogoutInProgress = false; string text; string text2; if (PlayerCharacter != null) { text = PlayerCharacter.HostID; text2 = PlayerCharacter.Name; } else { text = GetPlayerID(player); text2 = player.GetPlayerName(); } Logger.LogInfo("Player " + text2 + " with ID " + text + " validating character data."); DataObjects.Character savableChar = PlayerCharacter; if (savableChar == null) { Logger.LogInfo("No existing character data found for player " + text2 + " with ID " + text + ". Attempting to load from local save."); savableChar = ValConfig.LoadCharacterFromSave(text, text2); if (savableChar == null) { savableChar = new DataObjects.Character { Name = player.GetPlayerName(), HostID = text, SkillLevels = ((Character)player).GetSkills().GetSkillList().ToDictionary((Skill skill) => skill.m_info.m_skill, (Skill skill) => skill.m_level) }; if (ValConfig.NewCharacterSetSkillsToZero.Value) { Logger.LogInfo("New character save for player " + text2 + " with ID " + text + ", skills set to zero."); foreach (SkillType item2 in savableChar.SkillLevels.Keys.ToList()) { savableChar.SkillLevels[item2] = 0f; } } if (ValConfig.NewCharactersRemoveExtraItems.Value) { Logger.LogInfo("New character save for player " + text2 + " with ID " + text + ", checking to removing non-starter items."); List removeItems = new List(); ((Humanoid)player).m_inventory.GetAllItems().ForEach(delegate(ItemData val2) { if (!staringAllowedPrefabs.Contains(((Object)val2.m_dropPrefab).name)) { Logger.LogInfo($"Removing non-starter item {((Object)val2.m_dropPrefab).name}x{val2.m_stack} from new player {savableChar.Name}"); savableChar.AddConfiscatedItem(val2, "New character, non-starter item"); removeItems.Add(val2); } if (val2.m_quality > 1) { Logger.LogInfo($"Removing high quality item {((Object)val2.m_dropPrefab).name}x{val2.m_stack} with quality {val2.m_quality} from new player {savableChar.Name}"); savableChar.AddConfiscatedItem(val2, $"Item quality did not match saved item {val2.m_quality}"); removeItems.Add(val2); } }); foreach (ItemData item3 in removeItems) { ((Humanoid)player).UnequipItem(item3, true); ((Humanoid)player).GetInventory().RemoveItem(item3); } } foreach (ItemData item4 in ((Humanoid)player).GetInventory().GetAllItems().ToList()) { savableChar.AddItemToPlayerItems(item4); if (ValConfig.ValidateItemCustomData.Value) { item4.m_customData.Clear(); } } } } bool flag = savableChar.LastDisconnect == DataObjects.DisconnectionState.DirtyDisconnect && ValConfig.ItemRemovalForDirtyReconnection.Value; if (ValConfig.RemoveNontrackedItemsFromJoiningPlayers.Value && !flag) { ConfiscateUntrackedItems(player, savableChar); } bool flag2 = savableChar.LastDisconnect == DataObjects.DisconnectionState.DirtyDisconnect && !ValConfig.ItemReturnForDirtyReconnection.Value; if (ValConfig.AddMissingItemsFromPlayerServerSave.Value && !flag2) { Logger.LogDebug("Checking to restore player items."); List> list = new List>(); foreach (ItemData allItem in ((Humanoid)player).m_inventory.GetAllItems()) { list.Add(new Tuple(((Object)allItem.m_dropPrefab).name, allItem.m_stack)); } foreach (DataObjects.PackedItem playerItem in savableChar.PlayerItems) { Tuple item = new Tuple(playerItem.prefabName, playerItem.m_stack); if (!list.Contains(item)) { Logger.LogInfo($"Adding missing item to players inventory: {playerItem.prefabName}x{playerItem.m_stack}"); playerItem.AddToInventory(player, use_position: false); } } } Logger.LogDebug("Validated player items."); if (ValConfig.PreventExternalSkillRaises.Value) { ((Character)player).GetSkills().GetSkillList().ForEach(delegate(Skill skill) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) if (savableChar.SkillLevels.TryGetValue(skill.m_info.m_skill, out var value) && skill.m_level > value) { Logger.LogInfo($"Removing external skill gains for {skill.m_info.m_skill} from {value} to {skill.m_level} from player {savableChar.Name}"); skill.m_level = value; } }); } Logger.LogDebug("Validated player skills."); if (ValConfig.SavePlayerStatusEffectsOnLogout.Value && savableChar.ActiveCharacterEffects != null && savableChar.ActiveCharacterEffects.Count > 0) { SEMan sEMan = ((Character)player).GetSEMan(); foreach (KeyValuePair activeCharacterEffect in savableChar.ActiveCharacterEffects) { Logger.LogDebug("Applying status effect: " + activeCharacterEffect.Key); StatusEffect val = activeCharacterEffect.Value.ToStatusEffect(); if (!((Object)(object)val == (Object)null)) { sEMan.AddStatusEffect(val, false, 0, 0f); } } savableChar.ActiveCharacterEffects.Clear(); Logger.LogDebug("Validated saved status effects."); } PlayerCharacter = savableChar; PersistAndPushCharacter(text, savableChar); JoinValidationComplete = true; } private static void ConfiscateUntrackedItems(Player player, DataObjects.Character savableChar) { foreach (KeyValuePair item in ValidateItems(((Humanoid)player).m_inventory.GetAllItems(), savableChar)) { if (!item.Value.Validated) { Logger.LogInfo($"Removing item {((Object)item.Key.m_dropPrefab).name}x{item.Key.m_stack} from player {savableChar.Name}. Validation message: {item.Value.ValidationMessage}"); savableChar.AddConfiscatedItem(item.Key, item.Value.ValidationMessage); ((Humanoid)player).UnequipItem(item.Key, true); ((Humanoid)player).GetInventory().RemoveItem(item.Key); } } } internal static void PersistAndPushCharacter(string playerID, DataObjects.Character character) { if (character != null) { ValConfig.WritePlayerCharacterToSave(playerID, character); ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetServerPeer() : null); if (val != null) { ValConfig.CharacterSaveRPC.SendPackage(val.m_uid, ValConfig.SendCharacterAsZpackage(character)); } } } internal static void RebaselineFromLiveInventory(Player player) { if ((Object)(object)player == (Object)null) { return; } LogoutInProgress = false; DataObjects.Character playerCharacter = PlayerCharacter; if (playerCharacter == null) { Logger.LogWarning("Respawn with no tracked character, falling back to full join validation."); LoadAndValidatePlayer(player); return; } Logger.LogInfo("Player " + playerCharacter.Name + " respawned, re-baselining tracked state from their live inventory."); playerCharacter.LastDisconnect = DataObjects.DisconnectionState.DirtyDisconnect; playerCharacter.PlayerItems.Clear(); foreach (ItemData item in ((Humanoid)player).GetInventory().GetAllItems().ToList()) { playerCharacter.AddItemToPlayerItems(item); } playerCharacter.SkillLevels = ((Character)player).GetSkills().GetSkillList().ToDictionary((Skill skill) => skill.m_info.m_skill, (Skill skill) => skill.m_level); playerCharacter.ActiveCharacterEffects.Clear(); if (ValConfig.PreventExternalCustomDataChanges.Value) { playerCharacter.PlayerCustomData = player.m_customData; } PlayerCharacter = playerCharacter; PersistAndPushCharacter(playerCharacter.HostID, playerCharacter); } internal static void ClearTrackedItemsForDeath(Player player) { if (!((Object)(object)player == (Object)null) && PlayerCharacter != null) { Logger.LogInfo("Player " + PlayerCharacter.Name + " died, clearing tracked items pending re-enumeration."); PlayerCharacter.LastDisconnect = DataObjects.DisconnectionState.DirtyDisconnect; PlayerCharacter.PlayerItems.Clear(); PlayerCharacter.ActiveCharacterEffects.Clear(); PlayerCharacter.SkillLevels = ((Character)player).GetSkills().GetSkillList().ToDictionary((Skill skill) => skill.m_info.m_skill, (Skill skill) => skill.m_level); PersistAndPushCharacter(PlayerCharacter.HostID, PlayerCharacter); } } internal static Dictionary ValidateItems(List playerItems, DataObjects.Character savedChar) { Dictionary dictionary = new Dictionary(); Logger.LogInfo($"Player Items: {playerItems.Count} | SavedCharacter Items: {savedChar.PlayerItems.Count}"); foreach (ItemData playerItem in playerItems) { Logger.LogDebug("Checking player item: " + ((Object)playerItem.m_dropPrefab).name); DataObjects.ValidationSummary validationSummary = new DataObjects.ValidationSummary(); dictionary.Add(playerItem, new DataObjects.ItemValidatorResult { CharacterItemRef = playerItem }); string text = ""; foreach (DataObjects.PackedItem playerItem2 in savedChar.PlayerItems) { if (!(playerItem2.prefabName == ((Object)playerItem.m_dropPrefab).name) || playerItem2.m_stack != playerItem.m_stack) { continue; } validationSummary.NameAndStackMatch = true; int num = playerItem2.m_quality; if (num == 0) { num = 1; } if (num == playerItem.m_quality) { validationSummary.QualityMatch = true; text += $"{num} != {playerItem.m_quality} "; } if (ValConfig.ValidateItemDurability.Value && playerItem.m_durability <= playerItem2.m_durability - ValConfig.ItemValidationDurabilityAllowedVariance.Value && playerItem.m_durability >= playerItem2.m_durability + ValConfig.ItemValidationDurabilityAllowedVariance.Value) { validationSummary.DurabilityMatch = false; text += $"Durability mismatch. Expected {playerItem2.m_durability} got {playerItem.m_durability} "; Logger.LogDebug($"Item {((Object)playerItem.m_dropPrefab).name} durability mismatch. Expected {playerItem2.m_durability} got {playerItem.m_durability} | {playerItem.m_durability} >= {playerItem2.m_durability - ValConfig.ItemValidationDurabilityAllowedVariance.Value} && {playerItem.m_durability} <= {playerItem2.m_durability + ValConfig.ItemValidationDurabilityAllowedVariance.Value}"); } else { validationSummary.DurabilityMatch = true; } validationSummary.CustomDataMatch = true; if (ValConfig.ValidateItemCustomData.Value) { foreach (KeyValuePair customDatum in playerItem.m_customData) { if (playerItem2.m_customdata.ContainsKey(customDatum.Key) && playerItem2.m_customdata[customDatum.Key] != customDatum.Value) { validationSummary.CustomDataMatch = false; text = text + "Custom data mismatch on key " + customDatum.Key + ". Expected " + playerItem2.m_customdata[customDatum.Key] + " got " + customDatum.Value + " "; Logger.LogDebug("Item " + ((Object)playerItem.m_dropPrefab).name + " custom data mismatch on key " + customDatum.Key + ". Expected " + playerItem2.m_customdata[customDatum.Key] + " got " + customDatum.Value); } } } if (validationSummary.IsValid()) { Logger.LogDebug("Item " + ((Object)playerItem.m_dropPrefab).name + " passed validation checks against saved character data."); dictionary[playerItem].SavedItemRef = playerItem2; dictionary[playerItem].Validated = true; break; } } dictionary[playerItem].ValidationResult = validationSummary; if (!validationSummary.IsValid()) { dictionary[playerItem].ValidationMessage = "Item " + ((Object)playerItem.m_dropPrefab).name + " failed validation checks against saved character data. " + $"Stack Match: {validationSummary.NameAndStackMatch}, " + $"Quality Match: {validationSummary.QualityMatch}, " + $"Custom Data Match: {validationSummary.CustomDataMatch}, " + $"Durability Match: {validationSummary.DurabilityMatch} | " + text; } } return dictionary; } } internal static class CharacterPatches { [HarmonyPatch(typeof(Game), "SpawnPlayer")] public static class LoadAndValidatePlayerPatch { [HarmonyPostfix] [HarmonyPriority(800)] private static void PlayerSpawn(Game __instance) { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null)) { if (!CharacterManager.JoinValidationComplete) { CharacterManager.LoadAndValidatePlayer(localPlayer); } else { CharacterManager.RebaselineFromLiveInventory(localPlayer); } CharacterDeltaTracker.WatchInventory(localPlayer); } } } [HarmonyPatch(typeof(Game), "Logout")] public static class ClearPlayerCharacterOnLogout { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix() { if (CharacterManager.PlayerCharacter != null) { Logger.LogDebug("Clearing selected save profile for " + CharacterManager.PlayerCharacter.Name + " on logout."); CharacterManager.PlayerCharacter = null; } CharacterManager.LogoutInProgress = false; CharacterManager.JoinValidationComplete = false; CharacterDeltaTracker.StopWatching(); } } [HarmonyPatch(typeof(Player))] public static class LoadPlayerCustomData { [HarmonyPostfix] [HarmonyPriority(800)] [HarmonyPatch("Load")] private static void Postfix(Player __instance) { DataObjects.Character character = null; string id; string name; if (CharacterManager.PlayerCharacter != null) { character = CharacterManager.PlayerCharacter; id = CharacterManager.PlayerCharacter.HostID; name = CharacterManager.PlayerCharacter.Name; } else { id = CharacterManager.GetPlayerID(__instance); name = __instance.GetPlayerName(); } if (CharacterManager.PlayerCharacter == null) { character = ValConfig.LoadCharacterFromSave(id, name); } if (character == null) { if (ValConfig.PreventExternalCustomDataChanges.Value && ValConfig.newCharacterClearCustomData.Value) { __instance.m_customData.Clear(); } } else if (ValConfig.PreventExternalCustomDataChanges.Value) { __instance.m_customData = character.PlayerCustomData; Logger.LogDebug("Set player custom data."); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] public static class FlushCharacterStoreOnShutdown { [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { CharacterStore.Shutdown(); } } } [HarmonyPatch(typeof(Game), "Shutdown")] public static class SaveSyncForShutdown { [HarmonyPrefix] [HarmonyPriority(800)] private static void PlayerSave(Game __instance, bool saveWorld) { if (!__instance.m_shuttingDown && saveWorld && !((Object)(object)Player.m_localPlayer == (Object)null)) { CharacterManager.LogoutInProgress = true; CharacterManager.SavePlayerCharacter(Player.m_localPlayer); } } } [HarmonyPatch(typeof(Player), "OnDeath")] public static class ClearTrackedItemsOnDeath { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Player __instance) { if (!((Object)(object)__instance == (Object)null) && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !((Object)(object)((Character)__instance).m_nview == (Object)null) && ((Character)__instance).m_nview.IsOwner()) { CharacterManager.ClearTrackedItemsForDeath(__instance); } } } } internal static class CharacterStore { private sealed class Entry { public DataObjects.Character Character; public string Yaml; public DateTime SourceMtime; } private abstract class Message { } private sealed class FullSaveMessage : Message { public string RawYaml; } private sealed class DeltaMessage : Message { public DataObjects.DeltaSummaryUpdate Delta; public long Sender; } internal sealed class DriftResync { public long Sender; public string HostID; public string Name; } private static readonly ConcurrentDictionary cache = new ConcurrentDictionary(); private static readonly ConcurrentQueue messages = new ConcurrentQueue(); private static readonly ConcurrentQueue driftResyncs = new ConcurrentQueue(); private static readonly AutoResetEvent signal = new AutoResetEvent(initialState: false); private static readonly object startLock = new object(); private static Thread worker; private static volatile bool running; private static volatile bool workerBusy; internal static string KeyFor(string id, string name) { return id + "/" + name; } private static void EnsureWorker() { if (running) { return; } lock (startLock) { if (!running) { running = true; worker = new Thread(WorkerLoop) { IsBackground = true, Name = "VE-CharacterStore" }; worker.Start(); } } } internal static void SubmitFullSave(string rawYaml) { EnsureWorker(); messages.Enqueue(new FullSaveMessage { RawYaml = rawYaml }); signal.Set(); } internal static void SubmitDelta(DataObjects.DeltaSummaryUpdate delta, long sender) { EnsureWorker(); messages.Enqueue(new DeltaMessage { Delta = delta, Sender = sender }); signal.Set(); } internal static DriftResync TryDequeueDriftResync() { if (!driftResyncs.TryDequeue(out var result)) { return null; } return result; } internal static void ClearDriftResyncs() { DriftResync result; while (driftResyncs.TryDequeue(out result)) { } } internal static void Invalidate(string id, string name) { cache.TryRemove(KeyFor(id, name), out var _); } internal static bool IsCached(string id, string name) { return cache.ContainsKey(KeyFor(id, name)); } internal static string GetYaml(string id, string name) { if (!cache.TryGetValue(KeyFor(id, name), out var value)) { return null; } return value.Yaml; } internal static string GetYamlIfCurrent(string id, string name, DateTime diskMtime) { string text = KeyFor(id, name); if (!cache.TryGetValue(text, out var value)) { return null; } if (diskMtime > value.SourceMtime) { Logger.LogInfo("On-disk save for " + text + " is newer than cache; reloading from disk."); return null; } return value.Yaml; } internal static void Seed(string id, string name, string yaml, DateTime sourceMtime) { if (!string.IsNullOrEmpty(yaml)) { cache[KeyFor(id, name)] = new Entry { Character = null, Yaml = yaml, SourceMtime = sourceMtime }; } } internal static void Flush(TimeSpan timeout) { if (!running) { return; } signal.Set(); DateTime dateTime = DateTime.UtcNow + timeout; while (DateTime.UtcNow < dateTime) { if (messages.IsEmpty && !workerBusy) { return; } Thread.Sleep(15); } Logger.LogWarning("CharacterStore flush timed out; some pending saves may not have been written."); } internal static void Shutdown() { if (running) { Flush(TimeSpan.FromSeconds(10.0)); running = false; signal.Set(); worker?.Join(TimeSpan.FromSeconds(5.0)); } } private static void WorkerLoop() { while (running) { signal.WaitOne(1000); DrainOnce(); } DrainOnce(); } private static void DrainOnce() { workerBusy = true; try { HashSet hashSet = new HashSet(); Message result; while (messages.TryDequeue(out result)) { try { string text = Apply(result); if (text != null) { hashSet.Add(text); } } catch (Exception ex) { Logger.LogWarning("CharacterStore failed to apply an update: " + ex.Message); } } foreach (string item in hashSet) { if (cache.TryGetValue(item, out var value) && value.Character != null && value.Yaml != null) { try { value.SourceMtime = WriteToDisk(value.Character, value.Yaml); } catch (Exception ex2) { Logger.LogWarning("CharacterStore failed to write " + item + " to disk: " + ex2.Message); } } } } finally { workerBusy = false; } } private static string Apply(Message msg) { if (!(msg is FullSaveMessage fullSaveMessage)) { if (msg is DeltaMessage { Delta: var delta } deltaMessage) { string text = KeyFor(delta.HostID, delta.Name); DataObjects.Character orLoad = GetOrLoad(text, delta.HostID, delta.Name); if (orLoad == null) { Logger.LogWarning("CharacterStore dropped a delta for " + delta.Name + " (" + delta.HostID + "): no existing save to apply onto."); return null; } if (ValConfig.MergeDelta(delta, orLoad)) { driftResyncs.Enqueue(new DriftResync { Sender = deltaMessage.Sender, HostID = delta.HostID, Name = delta.Name }); } cache[text] = new Entry { Character = orLoad, Yaml = DataObjects.yamlserializer.Serialize((object)orLoad) }; Logger.LogInfo("Saved delta update for " + orLoad.Name + "."); return text; } return null; } DataObjects.Character character = DataObjects.yamldeserializer.Deserialize(fullSaveMessage.RawYaml); if (character == null || string.IsNullOrEmpty(character.HostID) || string.IsNullOrEmpty(character.Name)) { Logger.LogWarning("CharacterStore received a full save with no HostID/Name; dropping."); return null; } string text2 = KeyFor(character.HostID, character.Name); List confiscatedItems = character.ConfiscatedItems; character.ConfiscatedItems = GetOrLoad(text2, character.HostID, character.Name)?.ConfiscatedItems ?? new List(); int num = character.MergeConfiscatedItems(confiscatedItems); if (num > 0) { Logger.LogInfo($"Recorded {num} newly confiscated item(s) for {character.Name}."); } cache[text2] = new Entry { Character = character, Yaml = DataObjects.yamlserializer.Serialize((object)character) }; Logger.LogInfo("Recieved Player data update - " + character.Name + "|" + character.HostID); return text2; } private static DataObjects.Character GetOrLoad(string key, string id, string name) { if (cache.TryGetValue(key, out var value)) { if (value.Character != null) { return value.Character; } if (!string.IsNullOrEmpty(value.Yaml)) { try { value.Character = DataObjects.yamldeserializer.Deserialize(value.Yaml); return value.Character; } catch (Exception ex) { Logger.LogWarning("CharacterStore failed to parse seeded save for " + key + ": " + ex.Message + ". Falling back to disk."); } } } string path = Path.Combine(ValConfig.CharacterFilePath, id, name + ".yaml"); if (!File.Exists(path)) { return null; } try { string text = File.ReadAllText(path); DataObjects.Character character = DataObjects.yamldeserializer.Deserialize(text); cache[key] = new Entry { Character = character, Yaml = text, SourceMtime = File.GetLastWriteTimeUtc(path) }; return character; } catch (Exception ex2) { Logger.LogWarning("CharacterStore failed to load existing save for " + key + ": " + ex2.Message + ". Update dropped."); return null; } } private static DateTime WriteToDisk(DataObjects.Character c, string yaml) { Directory.CreateDirectory(ValConfig.CharacterFilePath); string text = Path.Combine(ValConfig.CharacterFilePath, c.HostID); Directory.CreateDirectory(text); string text2 = Path.Combine(text, c.Name + ".yaml"); File.WriteAllText(text2, yaml); Logger.LogInfo("Writing to " + text2); return File.GetLastWriteTimeUtc(text2); } } internal static class FinalSaveRpc { [HarmonyPatch(typeof(ZNet), "OnNewConnection")] public static class ZNet_OnNewConnection_RegisterFinalSave { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (!((Object)(object)__instance == (Object)null) && __instance.IsServer() && peer != null) { peer.m_rpc.Register("VE_FINAL_CHAR_SAVE", (Action)RPC_FinalCharSave); } } } internal const string RPC_NAME = "VE_FINAL_CHAR_SAVE"; private static void RPC_FinalCharSave(ZRpc rpc, ZPackage pkg) { ZNet instance = ZNet.instance; long valueOrDefault = ((instance == null) ? ((long?)null) : instance.GetPeer(rpc)?.m_uid).GetValueOrDefault(); string yaml; try { yaml = Decompress(pkg.ReadByteArray()); } catch (Exception ex) { Logger.LogWarning($"Failed to decompress final character save from {valueOrDefault}: {ex.Message}"); return; } Logger.LogDebug($"Received synchronous final character save from {valueOrDefault}."); ValConfig.PersistReceivedCharacterYaml(valueOrDefault, yaml); } internal static void SendFinalSaveSync(ZNetPeer serverPeer, DataObjects.Character character) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Expected O, but got Unknown if (serverPeer != null && character != null) { ZPackage val = new ZPackage(); val.Write(Compress(DataObjects.yamlserializer.Serialize((object)character))); serverPeer.m_rpc.Invoke("VE_FINAL_CHAR_SAVE", new object[1] { val }); ISocket socket = serverPeer.m_socket; if (socket != null) { socket.Flush(); } Logger.LogDebug($"Sent synchronous final character save for {character.Name} ({val.Size()} bytes) and flushed the socket."); } } private static byte[] Compress(string text) { byte[] bytes = Encoding.UTF8.GetBytes(text); using MemoryStream memoryStream = new MemoryStream(); using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Compress)) { gZipStream.Write(bytes, 0, bytes.Length); } return memoryStream.ToArray(); } private static string Decompress(byte[] data) { using MemoryStream stream = new MemoryStream(data); using GZipStream gZipStream = new GZipStream(stream, CompressionMode.Decompress); using MemoryStream memoryStream = new MemoryStream(); gZipStream.CopyTo(memoryStream); return Encoding.UTF8.GetString(memoryStream.ToArray()); } } internal static class FullSyncScheduler { [HarmonyPatch(typeof(ZNet), "Start")] public static class ZNet_Start_Patch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { Initialize(); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] public static class ZNet_Shutdown_Patch { [HarmonyPostfix] private static void Postfix() { Teardown(); } } internal const float WaveStaggerSeconds = 3f; private static GameObject host; internal static void Initialize() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown if (!((Object)(object)host != (Object)null)) { host = new GameObject("VE_FullSyncScheduler"); Object.DontDestroyOnLoad((Object)(object)host); ((Object)host).hideFlags = (HideFlags)61; host.AddComponent(); Logger.LogDebug("FullSyncScheduler initialized."); } } internal static void Teardown() { if (!((Object)(object)host == (Object)null)) { Object.Destroy((Object)(object)host); host = null; CharacterStore.ClearDriftResyncs(); } } } internal class FullSyncSchedulerBehaviour : MonoBehaviour { private float nextCycle; private bool cycleRunning; private void Start() { nextCycle = Time.unscaledTime + IntervalSeconds(); } private void Update() { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { DrainDriftResyncs(); if (!cycleRunning && !(Time.unscaledTime < nextCycle)) { ((MonoBehaviour)this).StartCoroutine(RunPullCycle()); } } } private static void DrainDriftResyncs() { CharacterStore.DriftResync driftResync; while ((driftResync = CharacterStore.TryDequeueDriftResync()) != null) { ValConfig.RequestFullSyncForDrift(driftResync.Sender, driftResync.HostID, driftResync.Name); } } private static float IntervalSeconds() { return (float)Mathf.Max(1, ValConfig.FullSyncPullIntervalMinutes.Value) * 60f; } private IEnumerator RunPullCycle() { cycleRunning = true; try { List peers = ReadyClientPeers(); if (peers.Count == 0) { yield break; } int batch = Mathf.Clamp(ValConfig.FullSyncMaxConcurrentPlayers.Value, 1, peers.Count); Logger.LogDebug($"FullSyncScheduler: requesting full character saves from {peers.Count} player(s) in waves of {batch}."); for (int i = 0; i < peers.Count; i += batch) { if ((Object)(object)ZNet.instance == (Object)null) { break; } if (!ZNet.instance.IsServer()) { break; } int num = Mathf.Min(i + batch, peers.Count); for (int j = i; j < num; j++) { RequestFullSync(peers[j]); } if (num < peers.Count) { yield return (object)new WaitForSecondsRealtime(3f); } } } finally { FullSyncSchedulerBehaviour fullSyncSchedulerBehaviour = this; fullSyncSchedulerBehaviour.cycleRunning = false; fullSyncSchedulerBehaviour.nextCycle = Time.unscaledTime + IntervalSeconds(); } } private static List ReadyClientPeers() { List list = new List(); foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer != null && peer.IsReady()) { list.Add(peer); } } return list; } private static void RequestFullSync(ZNetPeer peer) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown ValConfig.FullSyncRequestRPC.SendPackage(peer.m_uid, new ZPackage()); Logger.LogDebug("FullSyncScheduler: requested full character save from " + peer.m_playerName + "."); } } } namespace ValheimEnforcer.common { internal static class ConfigFileWatcher { private class WatchEntry { public Action Callback; public DateTime LastWriteUTC { get; set; } public long FileLength { get; set; } public void Update(DateTime lastwrite, long len) { LastWriteUTC = lastwrite; FileLength = len; } } internal class ConfigFileWatcherBehaviour : MonoBehaviour { private float nextPollTime; public void Update() { if (!(Time.unscaledTime < nextPollTime)) { nextPollTime = Time.unscaledTime + (float)ValConfig.ConfigPollIntervalSeconds.Value; Poll(); } } private static void Poll() { if (WatchedFiles.Count == 0) { return; } foreach (string key in WatchedFiles.Keys) { if (!File.Exists(key)) { continue; } FileInfo fileInfo = new FileInfo(key); DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; long length = fileInfo.Length; WatchEntry watchEntry = WatchedFiles[key]; if (lastWriteTimeUtc == watchEntry.LastWriteUTC && length == watchEntry.FileLength) { continue; } WatchedFiles[key].LastWriteUTC = lastWriteTimeUtc; WatchedFiles[key].FileLength = length; try { if (watchEntry.Callback != null) { watchEntry.Callback(key); } } catch (Exception ex) { Logger.LogWarning("ConfigFileWatcher callback for " + key + " threw: " + ex.Message); } } } } private static Dictionary WatchedFiles = new Dictionary(); private static ConfigFileWatcherBehaviour watchProcess; internal static void Initialize() { //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_001e: Expected O, but got Unknown //IL_001e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)watchProcess != (Object)null)) { GameObject val = new GameObject("VE_ConfigFileWatcher"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; watchProcess = val.AddComponent(); Logger.LogDebug("ConfigFileWatcher initialized."); } } internal static void Register(string fullPath, Action onChanged) { if (File.Exists(fullPath)) { FileInfo fileInfo = new FileInfo(fullPath); DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; long length = fileInfo.Length; WatchedFiles.Add(fullPath, new WatchEntry { LastWriteUTC = lastWriteTimeUtc, FileLength = length, Callback = onChanged }); } else { WatchedFiles.Add(fullPath, new WatchEntry { LastWriteUTC = DateTime.MinValue, FileLength = 0L, Callback = onChanged }); } Logger.LogDebug("ConfigFileWatcher watching " + fullPath); } internal static void NoteSelfWrite(string fullPath) { if (!WatchedFiles.TryGetValue(fullPath, out var value)) { return; } try { FileInfo fileInfo = new FileInfo(fullPath); if (fileInfo.Exists) { value.Update(fileInfo.LastWriteTimeUtc, fileInfo.Length); } } catch (Exception ex) { Logger.LogDebug("Could not record our own write to " + fullPath + ": " + ex.Message); } } } internal static class DataObjects { public enum ItemDeltaChangeType { Added, Removed } public enum DisconnectionState { Clean, DirtyDisconnect } public class RPCServerUpdateData { public string PlatformID { get; set; } public string PlayerName { get; set; } public string ItemPrefabFilter { get; set; } = "All"; } public class Mod { public string PluginID { get; set; } public string Version { get; set; } public string Name { get; set; } [DefaultValue(false)] public bool EnforceVersion { get; set; } [DefaultValue("Minor")] public string VersionStrictness { get; set; } = "Minor"; [DefaultValue(null)] public string Hash { get; set; } [DefaultValue(null)] public List AcceptedHashes { get; set; } [DefaultValue(null)] public string HashSource { get; set; } [DefaultValue(null)] public string HashedFrom { get; set; } [DefaultValue(null)] public string ThunderstorePackage { get; set; } [DefaultValue(null)] public string HashEnforcement { get; set; } [DefaultValue(null)] public string HashStatus { get; set; } public bool AcceptsHash(string candidate) { if (AcceptedHashes == null || AcceptedHashes.Count == 0) { return false; } if (string.IsNullOrEmpty(candidate)) { return false; } foreach (string acceptedHash in AcceptedHashes) { if (string.Equals(acceptedHash, candidate, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public bool HasRecordedHash() { if (AcceptedHashes != null) { return AcceptedHashes.Count > 0; } return false; } } public class Mods { public Dictionary ActiveMods { get; set; } = new Dictionary(); public Dictionary RequiredMods { get; set; } = new Dictionary(); public Dictionary OptionalMods { get; set; } = new Dictionary(); public Dictionary AdminOnlyMods { get; set; } = new Dictionary(); public Dictionary ServerOnlyMods { get; set; } = new Dictionary(); public ZPackage ToZPackage() { //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 = yamlserializer.Serialize((object)this); ZPackage val = new ZPackage(); val.Write(text); return val; } public ZPackage ActiveModsToZPackage() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown Mods mods = new Mods { ActiveMods = ActiveMods }; ZPackage val = new ZPackage(); val.Write(yamlserializer.Serialize((object)mods)); return val; } public Mods FromZPackage(ZPackage incoming) { Mods mods = yamldeserializer.Deserialize(incoming.ReadString()); ActiveMods = mods.ActiveMods; RequiredMods = mods.RequiredMods; OptionalMods = mods.OptionalMods; AdminOnlyMods = mods.AdminOnlyMods; ServerOnlyMods = mods.ServerOnlyMods; return mods; } } public class KnownCheaterEntry { public string Id { get; set; } public string Reason { get; set; } } public class CheatToolDetection { public string Tool { get; set; } public string Vector { get; set; } public string Detail { get; set; } [DefaultValue(false)] public bool Weak { get; set; } } public class CheatSummaryReport { public string PlayerName { get; set; } public string PlatformID { get; set; } public List DetectedTools { get; set; } public bool ValheimToolerStatus { get; set; } public bool cheatsDetected() { if (!ValheimToolerStatus) { if (DetectedTools != null) { return DetectedTools.Count > 0; } return false; } return true; } } public class ItemValidatorResult { public PackedItem SavedItemRef { get; set; } public ItemData CharacterItemRef { get; set; } [DefaultValue(false)] public bool Validated { get; set; } public string ValidationMessage { get; set; } public ValidationSummary ValidationResult { get; set; } } public class ValidationSummary { [DefaultValue(false)] public bool NameAndStackMatch { get; set; } [DefaultValue(false)] public bool QualityMatch { get; set; } [DefaultValue(false)] public bool CustomDataMatch { get; set; } [DefaultValue(false)] public bool DurabilityMatch { get; set; } public bool IsValid() { if (NameAndStackMatch && QualityMatch && CustomDataMatch) { return DurabilityMatch; } return false; } } [Serializable] public class PackedStatusEffect { public float TimeRemaining { get; set; } public float Time { get; set; } public int NameHash { get; set; } [DefaultValue(0f)] public float DamageLeft { get; set; } [DefaultValue(0f)] public float DamagePerHit { get; set; } [DefaultValue(0f)] public float FireDamageLeft { get; set; } [DefaultValue(0f)] public float FireDamagePerHit { get; set; } [DefaultValue(0f)] public float SpiritDamageLeft { get; set; } [DefaultValue(0f)] public float SpiritDamagePerHit { get; set; } public PackedStatusEffect() { } public PackedStatusEffect(StatusEffect status) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown NameHash = status.NameHash(); TimeRemaining = status.m_ttl; Time = status.m_time; if (NameHash == Poison) { SE_Poison val = (SE_Poison)status; DamageLeft = val.m_damageLeft; DamagePerHit = val.m_damagePerHit; } else if (NameHash == Burning || NameHash == Spirit) { SE_Burning val2 = (SE_Burning)status; FireDamageLeft = val2.m_fireDamageLeft; FireDamagePerHit = val2.m_fireDamagePerHit; SpiritDamageLeft = val2.m_spiritDamageLeft; SpiritDamagePerHit = val2.m_spiritDamagePerHit; } } public StatusEffect ToStatusEffect() { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Expected O, but got Unknown //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Expected O, but got Unknown StatusEffect statusEffect = ObjectDB.instance.GetStatusEffect(NameHash); if ((Object)(object)statusEffect == (Object)null) { Logger.LogWarning($"Tried to get a status effect which does not exist ID:{NameHash}"); return null; } StatusEffect val = statusEffect.Clone(); if (NameHash == Poison) { SE_Poison val2 = (SE_Poison)val; ((StatusEffect)val2).m_ttl = TimeRemaining; ((StatusEffect)val2).m_time = Time; val2.m_damageLeft = DamageLeft; val2.m_damagePerHit = DamagePerHit; return (StatusEffect)val2; } if (NameHash == Burning || NameHash == Spirit) { SE_Burning val3 = (SE_Burning)val; ((StatusEffect)val3).m_ttl = TimeRemaining; ((StatusEffect)val3).m_time = Time; val3.m_fireDamageLeft = FireDamageLeft; val3.m_fireDamagePerHit = FireDamagePerHit; val3.m_spiritDamageLeft = SpiritDamageLeft; val3.m_spiritDamagePerHit = SpiritDamagePerHit; return (StatusEffect)val3; } return val; } } [Serializable] public class PackedItem : IEquatable { public string prefabName { get; set; } public int m_stack { get; set; } public float m_durability { get; set; } public int m_quality { get; set; } [DefaultValue(0)] public int m_variant { get; set; } [DefaultValue(0)] public int m_worldlevel { get; set; } [DefaultValue(0L)] public long m_crafterID { get; set; } [DefaultValue("")] public string m_crafterName { get; set; } public Dictionary m_customdata { get; set; } [DefaultValue(false)] public bool m_equipped { get; set; } public Vector2i m_gridpos { get; set; } public string confiscatedReason { get; set; } public DateTime confiscatedTime { get; set; } [DefaultValue(null)] public string confiscationId { get; set; } internal static Dictionary CopyCustomData(Dictionary source) { if (source != null) { return new Dictionary(source); } return null; } private static int NormalizedQuality(int quality) { if (quality != 0) { return quality; } return 1; } private static bool CustomDataEquals(Dictionary a, Dictionary b) { int num = a?.Count ?? 0; int num2 = b?.Count ?? 0; if (num != num2) { return false; } if (num == 0) { return true; } foreach (KeyValuePair item in a) { if (!b.TryGetValue(item.Key, out var value)) { return false; } if (item.Value != value) { return false; } } return true; } private static int CustomDataHash(Dictionary data) { if (data == null || data.Count == 0) { return 0; } int num = 0; foreach (KeyValuePair datum in data) { num ^= ((datum.Key?.GetHashCode() ?? 0) * 31) ^ (datum.Value?.GetHashCode() ?? 0); } return num; } public bool Equals(PackedItem other) { if (this == other) { return true; } if (other == null) { return false; } if (prefabName == other.prefabName && m_stack == other.m_stack && NormalizedQuality(m_quality) == NormalizedQuality(other.m_quality) && m_variant == other.m_variant && m_worldlevel == other.m_worldlevel && m_crafterID == other.m_crafterID && m_crafterName == other.m_crafterName) { return CustomDataEquals(m_customdata, other.m_customdata); } return false; } public override bool Equals(object obj) { return Equals(obj as PackedItem); } public override int GetHashCode() { return (((((((17 * 31 + (prefabName?.GetHashCode() ?? 0)) * 31 + m_stack) * 31 + NormalizedQuality(m_quality)) * 31 + m_variant) * 31 + m_worldlevel) * 31 + m_crafterID.GetHashCode()) * 31 + (m_crafterName?.GetHashCode() ?? 0)) * 31 + CustomDataHash(m_customdata); } public void AddToInventory(Player player, bool use_position) { //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_0187: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_0212: Unknown result type (might be due to invalid IL or missing references) //IL_0222: Unknown result type (might be due to invalid IL or missing references) Inventory inventory = ((Humanoid)player).GetInventory(); ZNetView.m_forceDisableInit = true; GameObject prefab = PrefabManager.Instance.GetPrefab(prefabName); if ((Object)(object)prefab == (Object)null) { Logger.LogError($"Could not find prefab with name {prefabName} for item with crafter name {m_crafterName} and crafter ID {m_crafterID}. This item will not be added to the inventory."); ZNetView.m_forceDisableInit = false; return; } GameObject obj = Object.Instantiate(prefab); ZNetView.m_forceDisableInit = false; ItemDrop component = obj.GetComponent(); component.m_itemData.m_stack = m_stack; component.m_itemData.m_durability = m_durability; if (m_quality == 0) { component.m_itemData.m_quality = 1; } else { component.m_itemData.m_quality = m_quality; } component.m_itemData.m_variant = m_variant; component.m_itemData.m_worldLevel = m_worldlevel; component.m_itemData.m_crafterID = m_crafterID; if (m_crafterName == null) { component.m_itemData.m_crafterName = ""; } else { component.m_itemData.m_crafterName = m_crafterName; } component.m_itemData.m_customData = CopyCustomData(m_customdata) ?? new Dictionary(); component.m_itemData.m_pickedUp = true; bool flag = false; if (use_position || (ModCompatability.IsExtraSlotsEnabled && API.IsGridPositionASlot(m_gridpos))) { component.m_itemData.m_gridPos = m_gridpos; flag = inventory.AddItem(component.m_itemData, component.m_itemData.m_stack, m_gridpos.x, m_gridpos.y); if (!flag) { Logger.LogDebug($"Saved grid position {m_gridpos} for {prefabName} is occupied or out of range, falling back to the first free slot."); } } if (!flag && inventory.CanAddItem(component.m_itemData, -1)) { flag = inventory.AddItem(component.m_itemData); } if (!flag) { Logger.LogDebug("Dropping item " + prefabName + " at player position because it cannot be added to the inventory."); ItemDrop.DropItem(component.m_itemData, component.m_itemData.m_stack, ((Component)player).gameObject.transform.position, ((Component)player).gameObject.transform.rotation); } else if (m_equipped) { ((Humanoid)player).EquipItem(component.m_itemData, true); } Object.Destroy((Object)(object)obj); } } public class ItemDelta { public PackedItem Item { get; set; } public ItemDeltaChangeType Op { get; set; } } public class DeltaSummaryUpdate { public string Name { get; set; } public string HostID { get; set; } public DisconnectionState DisconnectionState { get; set; } = DisconnectionState.DirtyDisconnect; public List ItemModifications { get; set; } = new List(); public Dictionary PlayerCustomDataModifications { get; set; } = new Dictionary(); public List RemovedCustomDataKeys { get; set; } = new List(); public Dictionary SkillLevels { get; set; } = new Dictionary(); public Dictionary ActiveCharacterEffects { get; set; } = new Dictionary(); } public class CharacterSaveData { public Dictionary SavedCharacters = new Dictionary(); } public class AccountEntries { public Dictionary> AccountCharacterEntries = new Dictionary>(); } public class Character { public string Name { get; set; } public string HostID { get; set; } public DisconnectionState LastDisconnect { get; set; } public Dictionary SkillLevels { get; set; } = new Dictionary(); public Dictionary PlayerCustomData { get; set; } = new Dictionary(); public Dictionary ActiveCharacterEffects { get; set; } = new Dictionary(); public List PlayerItems { get; set; } = new List(); public List ConfiscatedItems { get; set; } = new List(); public bool RemoveFromPlayerItems(PackedItem packedItem) { bool flag = false; if (packedItem == null) { return false; } if (PlayerItems != null && PlayerItems.Contains(packedItem)) { flag = PlayerItems.Remove(packedItem); } if (flag) { return true; } if (PlayerItems != null) { foreach (PackedItem playerItem in PlayerItems) { if (packedItem.prefabName == playerItem.prefabName && packedItem.m_stack == playerItem.m_stack && packedItem.m_variant == playerItem.m_variant && packedItem.m_worldlevel == playerItem.m_worldlevel && packedItem.m_crafterID == playerItem.m_crafterID && packedItem.m_crafterName == playerItem.m_crafterName) { flag = PlayerItems.Remove(playerItem); if (flag) { Logger.LogDebug("Removed item " + playerItem.prefabName + " from player items based on a fuzzy match."); break; } } } } return flag; } public void AddItemToPlayerItems(ItemData item) { //IL_00f6: Unknown result type (might be due to invalid IL or missing references) if (PlayerItems == null) { PlayerItems = new List(); } Logger.LogDebug($"Adding saved item {((Object)item.m_dropPrefab).name} with quality - {item.m_quality}"); PlayerItems.Add(new PackedItem { prefabName = ((Object)item.m_dropPrefab).name, m_stack = item.m_stack, m_durability = Mathf.Clamp(item.m_durability, 0f, item.m_shared.m_maxDurability + item.m_shared.m_durabilityPerLevel * (float)Mathf.Max(item.m_quality, 1)), m_quality = item.m_quality, m_variant = item.m_variant, m_worldlevel = item.m_worldLevel, m_crafterID = item.m_crafterID, m_crafterName = item.m_crafterName, m_customdata = PackedItem.CopyCustomData(item.m_customData), m_equipped = item.m_equipped, m_gridpos = item.m_gridPos }); } public void AddConfiscatedItem(ItemData item, string reason = "") { //IL_009c: Unknown result type (might be due to invalid IL or missing references) if (ConfiscatedItems == null) { ConfiscatedItems = new List(); } PackedItem packedItem = new PackedItem { prefabName = ((Object)item.m_dropPrefab).name, m_stack = item.m_stack, m_durability = item.m_durability, m_quality = item.m_quality, m_variant = item.m_variant, m_worldlevel = item.m_worldLevel, m_crafterID = item.m_crafterID, m_crafterName = item.m_crafterName, m_customdata = PackedItem.CopyCustomData(item.m_customData), m_equipped = item.m_equipped, m_gridpos = item.m_gridPos }; if (!string.IsNullOrEmpty(reason)) { packedItem.confiscatedReason = reason; } packedItem.confiscatedTime = DateTime.UtcNow; packedItem.confiscationId = Guid.NewGuid().ToString("N"); ConfiscatedItems.Add(packedItem); } public int MergeConfiscatedItems(List incoming) { if (incoming == null || incoming.Count == 0) { return 0; } if (ConfiscatedItems == null) { ConfiscatedItems = new List(); } HashSet hashSet = new HashSet(); foreach (PackedItem confiscatedItem in ConfiscatedItems) { if (confiscatedItem != null && !string.IsNullOrEmpty(confiscatedItem.confiscationId)) { hashSet.Add(confiscatedItem.confiscationId); } } int num = 0; foreach (PackedItem item in incoming) { if (item != null && !string.IsNullOrEmpty(item.confiscationId) && hashSet.Add(item.confiscationId)) { ConfiscatedItems.Add(item); num++; } } return num; } } internal class NotificationTemplateSet { [YamlMember(/*Could not decode attribute arguments.*/)] public string ServerStartup { get; set; } [YamlMember(/*Could not decode attribute arguments.*/)] public string ServerShutdown { get; set; } [YamlMember(/*Could not decode attribute arguments.*/)] public string WorldSaved { get; set; } [YamlMember(/*Could not decode attribute arguments.*/)] public string PlayerJoined { get; set; } [YamlMember(/*Could not decode attribute arguments.*/)] public string PlayerLeft { get; set; } [YamlMember(/*Could not decode attribute arguments.*/)] public string CheaterBanned { get; set; } [YamlMember(/*Could not decode attribute arguments.*/)] public string CharacterRejected { get; set; } [YamlMember(/*Could not decode attribute arguments.*/)] public string ModMismatch { get; set; } } public static IDeserializer yamldeserializer = ((BuilderSkeleton)new DeserializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build(); public static ISerializer yamlserializer = ((BuilderSkeleton)new SerializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).ConfigureDefaultValuesHandling((DefaultValuesHandling)2).DisableAliases() .Build(); public static readonly string CustomDataKey = "VE_CUSTOM_DATA"; private static readonly int Poison = StringExtensionMethods.GetStableHashCode("Poison"); private static readonly int Burning = StringExtensionMethods.GetStableHashCode("Burning"); private static readonly int Spirit = StringExtensionMethods.GetStableHashCode("Spirit"); internal const int Green = 5763719; internal const int Grey = 9807270; internal const int Amber = 16705372; internal const int Red = 15548997; } internal static class JsonWellFormed { private sealed class JsonError : Exception { internal readonly int Index; internal readonly string Reason; internal JsonError(int index, string reason) : base(reason) { Index = index; Reason = reason; } } private const int MaxDepth = 64; internal static bool Validate(string text, out string error) { error = null; if (string.IsNullOrWhiteSpace(text)) { error = "the document is empty"; return false; } int i = 0; try { SkipWhitespace(text, ref i); ParseValue(text, ref i, 0); SkipWhitespace(text, ref i); if (i < text.Length) { throw new JsonError(i, $"unexpected '{text[i]}' after the end of the document"); } return true; } catch (JsonError jsonError) { error = $"{jsonError.Reason} (line {LineOf(text, jsonError.Index)}, column {ColumnOf(text, jsonError.Index)})"; return false; } } private static void SkipWhitespace(string s, ref int i) { while (i < s.Length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r' || s[i] == '\n')) { i++; } } private static void ParseValue(string s, ref int i, int depth) { if (depth > 64) { throw new JsonError(i, "nested too deeply"); } if (i >= s.Length) { throw new JsonError(i, "the document ends where a value was expected"); } switch (s[i]) { case '{': ParseObject(s, ref i, depth); return; case '[': ParseArray(s, ref i, depth); return; case '"': ParseString(s, ref i); return; case '\'': throw new JsonError(i, "single quotes are not valid JSON - use double quotes"); case 't': Expect(s, ref i, "true"); return; case 'f': Expect(s, ref i, "false"); return; case 'n': Expect(s, ref i, "null"); return; } if (s[i] == '-' || (s[i] >= '0' && s[i] <= '9')) { ParseNumber(s, ref i); return; } throw new JsonError(i, $"'{s[i]}' does not start a valid JSON value"); } private static void ParseObject(string s, ref int i, int depth) { i++; SkipWhitespace(s, ref i); if (i < s.Length && s[i] == '}') { i++; return; } while (true) { SkipWhitespace(s, ref i); if (i >= s.Length) { throw new JsonError(i, "the document ends inside an object - a '}' is missing"); } if (s[i] == '}') { throw new JsonError(i, "trailing comma before '}' - JSON does not allow one"); } if (s[i] != '"') { throw new JsonError(i, "an object key must be a double-quoted string"); } ParseString(s, ref i); SkipWhitespace(s, ref i); if (i >= s.Length || s[i] != ':') { throw new JsonError(i, "expected ':' after an object key"); } i++; SkipWhitespace(s, ref i); ParseValue(s, ref i, depth + 1); SkipWhitespace(s, ref i); if (i >= s.Length) { throw new JsonError(i, "the document ends inside an object - a '}' is missing"); } if (s[i] != ',') { break; } i++; } if (s[i] == '}') { i++; return; } throw new JsonError(i, $"expected ',' or '}}' in an object but found '{s[i]}'"); } private static void ParseArray(string s, ref int i, int depth) { i++; SkipWhitespace(s, ref i); if (i < s.Length && s[i] == ']') { i++; return; } while (true) { SkipWhitespace(s, ref i); if (i >= s.Length) { throw new JsonError(i, "the document ends inside an array - a ']' is missing"); } if (s[i] == ']') { throw new JsonError(i, "trailing comma before ']' - JSON does not allow one"); } ParseValue(s, ref i, depth + 1); SkipWhitespace(s, ref i); if (i >= s.Length) { throw new JsonError(i, "the document ends inside an array - a ']' is missing"); } if (s[i] != ',') { break; } i++; } if (s[i] == ']') { i++; return; } throw new JsonError(i, $"expected ',' or ']' in an array but found '{s[i]}'"); } private static void ParseString(string s, ref int i) { int index = i; i++; while (true) { if (i >= s.Length) { throw new JsonError(index, "a string is never closed - a '\"' is missing"); } char c = s[i]; if (c == '"') { i++; return; } if (c == '\\') { i++; if (i >= s.Length) { throw new JsonError(i, "the document ends inside a string escape"); } char c2 = s[i]; switch (c2) { case '"': case '/': case '\\': case 'b': case 'f': case 'n': case 'r': case 't': i++; break; case 'u': { if (i + 4 >= s.Length) { throw new JsonError(i, "a \\u escape needs four hex digits"); } for (int j = 1; j <= 4; j++) { char c3 = s[i + j]; if ((c3 < '0' || c3 > '9') && (c3 < 'a' || c3 > 'f') && (c3 < 'A' || c3 > 'F')) { throw new JsonError(i + j, "a \\u escape needs four hex digits"); } } i += 5; break; } default: throw new JsonError(i, $"'\\{c2}' is not a valid JSON escape"); } } else { if (c < ' ') { break; } i++; } } throw new JsonError(i, "a string contains a raw control character - a '\"' is probably missing"); } private static void ParseNumber(string s, ref int i) { int index = i; if (i < s.Length && s[i] == '-') { i++; } if (i >= s.Length || s[i] < '0' || s[i] > '9') { throw new JsonError(index, "a number is missing its digits"); } if (s[i] == '0') { i++; } else { while (i < s.Length && s[i] >= '0' && s[i] <= '9') { i++; } } if (i < s.Length && s[i] == '.') { i++; if (i >= s.Length || s[i] < '0' || s[i] > '9') { throw new JsonError(i, "a number has no digits after its decimal point"); } while (i < s.Length && s[i] >= '0' && s[i] <= '9') { i++; } } if (i < s.Length && (s[i] == 'e' || s[i] == 'E')) { i++; if (i < s.Length && (s[i] == '+' || s[i] == '-')) { i++; } if (i >= s.Length || s[i] < '0' || s[i] > '9') { throw new JsonError(i, "a number has no digits in its exponent"); } while (i < s.Length && s[i] >= '0' && s[i] <= '9') { i++; } } } private static void Expect(string s, ref int i, string literal) { if (i + literal.Length > s.Length || string.CompareOrdinal(s, i, literal, 0, literal.Length) != 0) { throw new JsonError(i, "expected '" + literal + "'"); } i += literal.Length; } private static int LineOf(string s, int index) { int num = 1; for (int i = 0; i < index && i < s.Length; i++) { if (s[i] == '\n') { num++; } } return num; } private static int ColumnOf(string s, int index) { int num = 1; int num2 = index - 1; while (num2 >= 0 && num2 < s.Length && s[num2] != '\n') { num++; num2--; } return num; } } internal static class PlatformIds { internal static string Normalize(string id) { if (string.IsNullOrEmpty(id)) { return id; } int num = id.LastIndexOf('_'); if (num < 0 || num >= id.Length - 1) { return id; } return id.Substring(num + 1); } internal static bool Matches(string left, string right) { if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right)) { return false; } if (left == right) { return true; } return Normalize(left) == Normalize(right); } } internal static class YamlComments { internal sealed class Captured { internal readonly Dictionary> Blocks = new Dictionary>(); internal readonly List Order = new List(); internal readonly List Trailing = new List(); internal bool HasLeadingBlock { get; set; } internal bool IsEmpty { get { if (Blocks.Count == 0) { return Trailing.Count == 0; } return false; } } internal void Add(string path, List block) { if (!Blocks.ContainsKey(path)) { Blocks.Add(path, block); Order.Add(path); } } } private sealed class PathTracker { private readonly List indents = new List(); private readonly List keys = new List(); private string sequenceParent; private int sequenceIndex; internal string Next(string line) { int num = CountIndent(line); string text = line.Substring(num); if (text.Length == 0 || text[0] == '#') { return null; } if (text[0] == '-' && (text.Length == 1 || text[1] == ' ')) { string text2 = Join(); if (text2 != sequenceParent) { sequenceParent = text2; sequenceIndex = 0; } return $"{text2}[{sequenceIndex++}]"; } string text3 = ParseKey(text); if (text3 == null) { return null; } while (indents.Count > 0 && indents[indents.Count - 1] >= num) { indents.RemoveAt(indents.Count - 1); keys.RemoveAt(keys.Count - 1); } indents.Add(num); keys.Add(text3); sequenceParent = null; sequenceIndex = 0; return Join(); } private string Join() { return string.Join("/", keys.ToArray()); } } internal const string OrphanNotice = "# --- The comments below were attached to entries that are no longer in this file ---"; internal static Captured Capture(string existingText) { Captured captured = new Captured(); if (string.IsNullOrEmpty(existingText)) { return captured; } PathTracker pathTracker = new PathTracker(); List list = new List(); bool flag = false; foreach (string item in SplitLines(existingText)) { if (IsComment(item)) { list.Add(item); continue; } if (IsBlank(item)) { if (list.Count > 0) { list.Add(item); } continue; } string text = pathTracker.Next(item); if (text == null) { continue; } if (list.Count > 0) { List list2 = TrimLeadingBlanks(list); if (list2.Count > 0) { captured.Add(text, list2); if (!flag) { captured.HasLeadingBlock = true; } } list.Clear(); } flag = true; } captured.Trailing.AddRange(TrimTrailingBlanks(TrimLeadingBlanks(list))); return captured; } internal static string Reapply(string newText, Captured captured) { if (captured == null || captured.IsEmpty || string.IsNullOrEmpty(newText)) { return newText; } string text = DetectNewline(newText); List list = SplitLines(newText); bool flag = list.Count > 0 && list[list.Count - 1].Length == 0; if (flag) { list.RemoveAt(list.Count - 1); } PathTracker pathTracker = new PathTracker(); HashSet hashSet = new HashSet(); StringBuilder stringBuilder = new StringBuilder(); foreach (string item in list) { if (!IsComment(item) && !IsBlank(item)) { string text2 = pathTracker.Next(item); if (text2 != null && !hashSet.Contains(text2) && captured.Blocks.TryGetValue(text2, out var value)) { hashSet.Add(text2); foreach (string item2 in value) { stringBuilder.Append(item2).Append(text); } } } stringBuilder.Append(item).Append(text); } foreach (string item3 in captured.Trailing) { stringBuilder.Append(item3).Append(text); } AppendOrphans(stringBuilder, captured, hashSet, text); string text3 = stringBuilder.ToString(); if (!flag && text3.EndsWith(text, StringComparison.Ordinal)) { text3 = text3.Substring(0, text3.Length - text.Length); } return text3; } private static void AppendOrphans(StringBuilder output, Captured captured, HashSet placed, string newline) { List list = new List(); foreach (string item in captured.Order) { if (!placed.Contains(item)) { list.Add(item); } } if (list.Count == 0) { return; } Logger.LogInfo(string.Format("Keeping comments for {0} entry/entries no longer present in the file: {1}", list.Count, string.Join(", ", list.ToArray()))); if (!captured.Trailing.Contains("# --- The comments below were attached to entries that are no longer in this file ---")) { output.Append("# --- The comments below were attached to entries that are no longer in this file ---").Append(newline); } foreach (string item2 in list) { foreach (string item3 in captured.Blocks[item2]) { output.Append(item3).Append(newline); } } } private static string ParseKey(string body) { char c = body[0]; if (c == '"' || c == '\'') { int num = body.IndexOf(c, 1); if (num < 0 || num + 1 >= body.Length || body[num + 1] != ':') { return null; } return body.Substring(1, num - 1); } for (int i = 0; i < body.Length; i++) { if (body[i] == ':' && (i + 1 == body.Length || body[i + 1] == ' ')) { string text = body.Substring(0, i).TrimEnd(Array.Empty()); if (text.Length != 0) { return text; } return null; } } return null; } internal static string DetectNewline(string text) { if (string.IsNullOrEmpty(text) || text.IndexOf("\r\n", StringComparison.Ordinal) < 0) { return "\n"; } return "\r\n"; } private static int CountIndent(string line) { int i; for (i = 0; i < line.Length && (line[i] == ' ' || line[i] == '\t'); i++) { } return i; } private static bool IsComment(string line) { int num = CountIndent(line); if (num < line.Length) { return line[num] == '#'; } return false; } private static bool IsBlank(string line) { return CountIndent(line) >= line.Length; } private static List SplitLines(string text) { List list = new List(); string[] array = text.Split(new char[1] { '\n' }); foreach (string text2 in array) { list.Add(text2.TrimEnd(new char[1] { '\r' })); } return list; } private static List TrimLeadingBlanks(List block) { int i; for (i = 0; i < block.Count && IsBlank(block[i]); i++) { } return block.GetRange(i, block.Count - i); } private static List TrimTrailingBlanks(List block) { int num = block.Count; while (num > 0 && IsBlank(block[num - 1])) { num--; } return block.GetRange(0, num); } } }