Please disclose if your mod was created primarily using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Valheim PvP Tweaks v1.1.1
ValheimPvPTweaks.dll
Decompiled a year ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using DiscordTools; using HarmonyLib; using JetBrains.Annotations; using Jotunn.Entities; using Jotunn.Managers; using Microsoft.CodeAnalysis; using ServerSync; using TMPro; using UnityEngine; [assembly: AssemblyDescription("")] [assembly: Guid("a765106f-9a9b-42ab-b53d-e0f7dad369a9")] [assembly: AssemblyFileVersion("1.1.0")] [assembly: AssemblyCopyright("Copyright © 2023")] [assembly: AssemblyProduct("ValheimPvPTweaks")] [assembly: AssemblyCompany("")] [assembly: AssemblyConfiguration("")] [assembly: ComVisible(false)] [assembly: AssemblyTitle("ValheimPvPTweaks")] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTrademark("")] [assembly: CompilationRelaxations(8)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.1.0.0")] [module: UnverifiableCode] internal static class CommonExtensions { public static bool ContainsIgnoreCase(this string self, string another) { return self.IndexOf(another, StringComparison.OrdinalIgnoreCase) >= 0; } public static void IncreaseCounter(this Dictionary<string, int> dictionary, string key, int increaseCount = 1) { dictionary.TryGetValue(key, out var value); value += increaseCount; dictionary[key] = value; } public static void ForeachSafe<T>(this IList<T> list, Action<T> action) { for (int i = 0; i < list.Count; i++) { action?.Invoke(list[i]); } } public static string MinutesFormat(this TimeSpan timeSpan) { return timeSpan.ToString("mm\\:ss"); } public static string HoursFormat(this TimeSpan timeSpan) { return timeSpan.ToString("hh\\:mm\\:ss"); } public static T GetRandom<T>(this IEnumerable<T> collection) { if (!collection.Any()) { return default(T); } int num = collection.Count(); int index = Random.Range(0, num); return collection.ElementAt(index); } public static IEnumerable<Type> GetTypesWithAttribute<T>(this Assembly assembly) where T : Attribute { Type[] types; try { types = assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types; } return types.Where((Type t) => t != null && t.GetCustomAttribute<T>() != null); } public static string GetSteamId(this ZNetPeer peer) { return peer.m_rpc.GetSteamId(); } public static string GetSteamId(this ZRpc rpc) { return rpc.GetSocket().GetHostName(); } public static bool IsEmpty(this Inventory inventory) { return inventory.NrOfItems() <= 0; } public static void ClearDamage(this HitData hitData) { hitData.ApplyModifier(0f); hitData.m_damage.m_damage = 0f; } public static T GetPrefabComponent<T>(this ZNetScene netScene, string name) where T : Component { GameObject prefab = netScene.GetPrefab(name); return (prefab != null) ? prefab.GetComponent<T>() : default(T); } public static bool IsLocalPlayer(this Character character) { return (Object)(object)character == (Object)(object)Player.m_localPlayer; } public static ZDO GetZDO(this Player player) { return ((Character)player).m_nview.GetZDO(); } public static bool HaveAccess(this PrivateArea privateArea, Player player) { long playerID = player.GetPlayerID(); return privateArea.HaveAccess(playerID); } public static bool HaveAccess(this PrivateArea privateArea, long playerId) { return privateArea.m_piece.GetCreator() == playerId || privateArea.IsPermitted(playerId); } } internal class Helper { public static bool IsDebugBuild => true; public static Stream GetResourcesStream(string fileName) { Assembly executingAssembly = Assembly.GetExecutingAssembly(); string name = executingAssembly.GetManifestResourceNames().First((string n) => n.EndsWith(fileName)); return executingAssembly.GetManifestResourceStream(name); } public static string LoadTextFromResources(string fileName) { using Stream stream = GetResourcesStream(fileName); using StreamReader streamReader = new StreamReader(stream); return streamReader.ReadToEnd(); } public static void WatchConfigFileChanges(ConfigFile config, Action onChanged = null) { string configFilePath = config.ConfigFilePath; WatchFileChanges(configFilePath, (Action)config.Reload); config.SettingChanged += delegate { onChanged?.Invoke(); }; } public static void WatchFileChanges(string path, Action onChanged) { FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(); string directoryName = Path.GetDirectoryName(path); string fileName = Path.GetFileName(path); fileSystemWatcher.Path = directoryName; fileSystemWatcher.Filter = fileName; fileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite; fileSystemWatcher.Changed += delegate { onChanged?.Invoke(); }; fileSystemWatcher.Deleted += delegate { onChanged?.Invoke(); }; fileSystemWatcher.Created += delegate { onChanged?.Invoke(); }; fileSystemWatcher.Renamed += delegate { onChanged?.Invoke(); }; fileSystemWatcher.EnableRaisingEvents = true; } public static void WatchFolderChanges(string path, Action onChanged) { WatchFileChanges(Path.Combine(path, "*.*"), onChanged); } } internal class Log { private static Log _instance; private ManualLogSource _source; public static Log CreateInstance(ManualLogSource source) { _instance = new Log { _source = source }; return _instance; } private Log() { } public static void Info(object msg) { _instance._source.LogInfo((object)FormatMessage(msg)); } public static void Message(object msg) { _instance._source.LogMessage((object)FormatMessage(msg)); } public static void Debug(object msg) { _instance._source.LogDebug((object)FormatMessage(msg)); } public static void Warning(object msg) { _instance._source.LogWarning((object)FormatMessage(msg)); } public static void Error(object msg) { _instance._source.LogError((object)FormatMessage(msg)); } public static void Fatal(object msg) { _instance._source.LogFatal((object)FormatMessage(msg)); } private static string FormatMessage(object msg) { return $"[{DateTime.UtcNow}] {msg}"; } } internal static class ThreadinUtil { private class DisposableThread : IDisposable { private Thread _thread; internal DisposableThread(Thread thread) { _thread = thread; } public void Dispose() { _thread.Abort(); } } private class MainThreadDispatcher : MonoBehaviour { private static MainThreadDispatcher _instance; private ConcurrentQueue<Action> _queue = new ConcurrentQueue<Action>(); private ConcurrentQueue<IEnumerator> _coroutinesQueue = new ConcurrentQueue<IEnumerator>(); public static MainThreadDispatcher GetInstante() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Expected O, but got Unknown if ((Object)(object)_instance == (Object)null) { GameObject val = new GameObject("MainThreadDispatcher", new Type[1] { typeof(MainThreadDispatcher) }); Object.DontDestroyOnLoad((Object)(object)val); _instance = val.GetComponent<MainThreadDispatcher>(); } return _instance; } public void AddAction(Action action) { _queue.Enqueue(action); } public void AddCoroutine(IEnumerator coroutine) { _coroutinesQueue.Enqueue(coroutine); } private void Update() { Action result; while (_queue.Count > 0 && _queue.TryDequeue(out result)) { result?.Invoke(); } IEnumerator result2; while (_coroutinesQueue.Count > 0 && _coroutinesQueue.TryDequeue(out result2)) { ((MonoBehaviour)this).StartCoroutine(result2); } } } internal static IDisposable RunPeriodical(Action action, int periodMilliseconds) { Thread thread = new Thread((ParameterizedThreadStart)delegate { while (true) { action?.Invoke(); Thread.Sleep(periodMilliseconds); } }); thread.Start(); return new DisposableThread(thread); } internal static void RunInMainThread(Action action) { MainThreadDispatcher.GetInstante().AddAction(action); } internal static void RunCoroutine(IEnumerator coroutine) { MainThreadDispatcher.GetInstante().AddCoroutine(coroutine); } internal static void RunDelayed(float delay, Action action) { MainThreadDispatcher.GetInstante().AddCoroutine(DelayedActionCoroutine(delay, action)); } internal static IDisposable RunThread(Action action) { Thread thread = new Thread(action.Invoke); thread.Start(); return new DisposableThread(thread); } internal static IEnumerator DelayedActionCoroutine(float delay, Action action) { yield return (object)new WaitForSeconds(delay); action?.Invoke(); } } namespace DiscordTools { internal static class DiscordTool { public static void SendMessageToDiscord(string url, string name, string message) { HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url); httpWebRequest.ContentType = "application/json"; httpWebRequest.Method = "POST"; using (StreamWriter streamWriter = new StreamWriter(httpWebRequest.GetRequestStream())) { string value = "{\"username\":\"" + name + "\",\"content\":\"" + message + "\"}"; streamWriter.Write(value); } httpWebRequest.GetResponse(); } } } namespace ValheimPvPTweaks { internal class Configuration { public enum ConsoleMode { Disabled, AdminOnly, Enabled } public enum SkillLossType { Vanilla, NoLossInPvp, NoLoss } public ConfigEntry<int> MaxDeathPingRadius; public ConfigEntry<int> CombatDuration; public ConfigEntry<float> CombatHealthRegen; public ConfigEntry<bool> AllowTeleportInCombat; public ConfigEntry<bool> AllowChangeEquipmentInCombat; public ConfigEntry<bool> CreaturesProtectWard; public ConfigEntry<bool> FollowCreaturesProtectWard; public ConfigEntry<float> TamedCreaturesHealthMultiplier; public ConfigEntry<float> TamedCreaturesDamageMultiplier; public ConfigEntry<bool> ResetBossPowerOnChange; public ConfigEntry<string> BonemassConfig; public ConfigEntry<string> EikthyrConfig; public ConfigEntry<string> YagluthConfig; public ConfigEntry<string> ModerConfig; public ConfigEntry<string> ElderConfig; public ConfigEntry<string> SeekerQueenConfig; public ConfigEntry<string> FaderConfig; public ConfigEntry<string> StaffShieldAbsorbtionConfig; public ConfigEntry<string> SiegeBombDamageConfig; public ConfigEntry<string> RamDamageConfig; public ConfigEntry<string> DiscordWebhook; public ConfigEntry<string> KillFeedName; public ConfigEntry<string> DeadMessageFormat; public ConfigEntry<string> KilledMessageFormat; public ConfigEntry<string> KilledInPvpMessageFormat; public ConfigEntry<string> DisconnectedInCombatFormat; public ConfigEntry<ConsoleMode> Console; public ConfigEntry<bool> EnableTombStoneBoost; public ConfigEntry<SkillLossType> SkillsLoss; public Configuration(ConfigFile config, ConfigSync sync) { config.SaveOnConfigSet = true; MaxDeathPingRadius = Bind(config, sync, "Death ping", "Radius", 20000, "Max radius where players will see death of another player. 20k+ - everywhere on map, 0 - nowhere"); CombatDuration = Bind(config, sync, "Combat", "Duration", 120, "Combat effect duration in seconds"); AllowTeleportInCombat = Bind(config, sync, "Combat", "Allow teleportation", value: false, "Allow teleporation via portals while player in combat"); AllowChangeEquipmentInCombat = Bind(config, sync, "Combat", "Allow change equipment", value: true, "Allow equip or unequip clothes, cape and accessories in combat"); CombatHealthRegen = Bind(config, sync, "Combat", "Health regen", 1f, "Health regen multiplier during combat"); CreaturesProtectWard = Bind(config, sync, "Tamed creatures", "Protect ward", value: true, "Allow tamed creatures attack players who do something agressive inside the ward they defending"); FollowCreaturesProtectWard = Bind(config, sync, "Tamed creatures", "Protect ward when follow", value: true, "Allow tamed creatures that following player attack other players who do something agressive inside the player's ward"); TamedCreaturesHealthMultiplier = Bind(config, sync, "Tamed creatures", "Health multiplier", 1.25f, "Increase health of tamed creatures"); TamedCreaturesDamageMultiplier = Bind(config, sync, "Tamed creatures", "Damage multiplier", 1f, "Increase tamed creatures damage"); ResetBossPowerOnChange = Bind(config, sync, "Boss power", "Reset cooldown", value: false, "If true then cooldown of boss power will be reset when change active boss power"); BonemassConfig = Bind(config, sync, "Boss power", "Bonemass", "300:1200", "Duration and cooldown of Bonemass power (leave empty to leave vanilla values)"); YagluthConfig = Bind(config, sync, "Boss power", "Yagluth", "300:1200", "Duration and cooldown of Yagluth power (leave empty to leave vanilla values)"); EikthyrConfig = Bind(config, sync, "Boss power", "Eikthyr", "300:1200", "Duration and cooldown of Eikthyr power (leave empty to leave vanilla values)"); ModerConfig = Bind(config, sync, "Boss power", "Moder", "300:1200", "Duration and cooldown of Moder power (leave empty to leave vanilla values)"); ElderConfig = config.Bind<string>("Boss power", "The Elder", "300:1200", "Duration and cooldown of The Elder power (leave empty to leave vanilla values)"); SeekerQueenConfig = config.Bind<string>("Boss power", "Seeker Queen", "300:1200", "Duration and cooldown of Seeker Queen power (leave empty to leave vanilla values)"); FaderConfig = config.Bind<string>("Boss power", "Fader", "300:1200", "Duration and cooldown of Fader power (leave empty to leave vanilla values)"); StaffShieldAbsorbtionConfig = Bind(config, sync, "Staffs", "Staff shield", "", "Shield resistance - Min:Max (with skill level 100). Recommended: 150:400. Leave empty to leave vanilla values"); SiegeBombDamageConfig = Bind(config, sync, "Raid", "Siege bomb damage", "", "Damage of siege bomb aoe (chop:pickaxe:damage:blunt:pierce:slash:fire:frost:lighting:poison:spirit).\nVanilla - 100:400:100:0:0:0:0:0:0:0:0\nLeave empty to leave vanilla values"); RamDamageConfig = Bind(config, sync, "Raid", "Ram damage", "", "Damage of Ram aoe (chop:pickaxe:damage:blunt:pierce:slash:fire:frost:lighting:poison:spirit).\nVanilla - 0:600:0:0:0:0:0:0:0:0:0\nLeave empty to leave vanilla values"); Console = Bind(config, sync, "Console", "Mode", ConsoleMode.AdminOnly, "Allow to force enable or disable console"); DiscordWebhook = Bind(config, sync, "Discord (Server only)", "Webhook url", "", "Discord webhook url to show killfeed", syncWithServer: false); KillFeedName = Bind(config, sync, "Discord (Server only)", "Username", "KillFeed", "The name on whose behalf the mod will post the killfeed in the discord channel", syncWithServer: false); KilledMessageFormat = Bind(config, sync, "Discord (Server only)", "Killed format", "{player} killed by {attacker}", "Message when player killed by creature", syncWithServer: false); KilledInPvpMessageFormat = Bind(config, sync, "Discord (Server only)", "Killed in PvP format", "{player} killed by {attacker}", "Message when player killed by another player", syncWithServer: false); DeadMessageFormat = Bind(config, sync, "Discord (Server only)", "Dead format", "{player} is dead", "Message when player dead by other reasons (suicide, drowned etc.)", syncWithServer: false); DisconnectedInCombatFormat = Bind(config, sync, "Discord (Server only)", "Disconnected in combat", "{player} ran away like a coward!", "Message when player logged out with combat effect", syncWithServer: false); EnableTombStoneBoost = Bind(config, sync, "Tweaks", "Tomb stone boost", value: true, "Tomb stone boost can be disabled to prevent abuse it to get an advantage in pvp."); SkillsLoss = Bind(config, sync, "Tweaks", "Skills loss", SkillLossType.Vanilla, $"{SkillLossType.Vanilla} - loose skills as in vanilla game\n" + $"{SkillLossType.NoLossInPvp} - if you die in pvp you dont loose skills\n" + $"{SkillLossType.NoLoss} - you dont loose skills on death at all"); } private static ConfigEntry<T> Bind<T>(ConfigFile config, ConfigSync sync, string section, string key, T value, string description, bool syncWithServer = true) { ConfigEntry<T> val = config.Bind<T>(section, key, value, description); if (syncWithServer) { sync.AddConfigEntry<T>(val); } return val; } } internal static class Constants { internal const string InCombatZdoKey = "VPT_PlayerInCombat"; internal const string CharacterDeadRpc = "VPT_CharacterDeadRpc"; } public static class Extensions { public static string Localize(this string text) { return Localization.instance.Localize(text); } public static bool InCombat(this Player player) { return (Object)(object)((Character)player).m_nview != (Object)null && ((Character)player).m_nview.IsValid() && ((Character)player).m_nview.GetZDO().GetBool("VPT_PlayerInCombat", false); } public static ZDO GetZdo(this ZNetPeer peer) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return ZDOMan.instance.GetZDO(peer.m_characterID); } public static bool InCombat(this ZNetPeer peer) { ZDO zdo = peer.GetZdo(); return zdo != null && zdo.GetBool("VPT_PlayerInCombat", false); } } [BepInPlugin("org.tristan.pvptweaks", "Valheim PvP Tweaks", "1.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] internal class Plugin : BaseUnityPlugin { [HarmonyPatch] private class ApplyConfigPatch { [HarmonyFinalizer] [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPatch(typeof(ZNet), "Start")] private static void ZNet_Finalizers(ZNet __instance) { ApplyConfiguration(); } } [HarmonyPatch] private class BossPowerPatch { [HarmonyPatch(typeof(ItemStand), "DelayedPowerActivation")] [HarmonyPostfix] private static void ItemStand_DelayedPowerActivation() { if ((Object)(object)Player.m_localPlayer != (Object)null && Configuration.ResetBossPowerOnChange.Value) { Player.m_localPlayer.m_guardianPowerCooldown = 0f; } } } private const string Guid = "org.tristan.pvptweaks"; public const string Name = "Valheim PvP Tweaks"; public const string Version = "1.1.0"; internal static Configuration Configuration { get; private set; } internal static ConfigSync Sync { get; private set; } private void Awake() { Sync = new ConfigSync("org.tristan.pvptweaks") { DisplayName = "Valheim PvP Tweaks", CurrentVersion = "1.1.0", MinimumRequiredVersion = "1.1.0" }; Configuration = new Configuration(((BaseUnityPlugin)this).Config, Sync); Log.CreateInstance(((BaseUnityPlugin)this).Logger); Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "org.tristan.pvptweaks"); LoadLocalization(); SynchronizationManager.OnAdminStatusChanged += OnAdminStatusChanged; } private void LoadLocalization() { CustomLocalization localization = LocalizationManager.Instance.GetLocalization(); localization.AddJsonFile("English", Helper.LoadTextFromResources("English.json")); localization.AddJsonFile("Russian", Helper.LoadTextFromResources("Russian.json")); } private static void OnAdminStatusChanged() { Configuration.ConsoleMode value = Configuration.Console.Value; bool flag = SynchronizationManager.Instance.PlayerIsAdmin || ZNet.instance.IsServer(); Console.SetConsoleEnabled(value == Configuration.ConsoleMode.Enabled || (value == Configuration.ConsoleMode.AdminOnly && flag)); Log.Debug($"Admin status changed {flag}"); } private static void RefreshStatusEffects(Configuration config) { SetStatusEffectConfig(config.BonemassConfig.Value, "GP_Bonemass"); SetStatusEffectConfig(config.YagluthConfig.Value, "GP_Yagluth"); SetStatusEffectConfig(config.EikthyrConfig.Value, "GP_Eikthyr"); SetStatusEffectConfig(config.ModerConfig.Value, "GP_Moder"); SetStatusEffectConfig(config.ElderConfig.Value, "GP_TheElder"); SetStatusEffectConfig(config.SeekerQueenConfig.Value, "GP_Queen"); SetStatusEffectConfig(config.FaderConfig.Value, "GP_Fader"); Log.Debug("Boss powers updated"); } private static void SetShieldStaffConfig(string name, string config) { GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(name); ItemDrop val = default(ItemDrop); if ((Object)(object)itemPrefab != (Object)null && itemPrefab.TryGetComponent<ItemDrop>(ref val)) { if (TryParseTwoValues(config, out var value, out var value2)) { StatusEffect attackStatusEffect = val.m_itemData.m_shared.m_attackStatusEffect; SE_Shield val2 = (SE_Shield)(object)((attackStatusEffect is SE_Shield) ? attackStatusEffect : null); val2.m_absorbDamage = value; float absorbDamagePerSkillLevel = (float)(value2 - value) / 100f; val2.m_absorbDamagePerSkillLevel = absorbDamagePerSkillLevel; Log.Debug($"Staff shield updated {val2.m_absorbDamage}:{val2.m_absorbDamagePerSkillLevel}"); } else if (!string.IsNullOrEmpty(config)) { Log.Warning("Cannot update shield staff values. Cannot parse config " + config); } } } private static void SetStatusEffectConfig(string config, string effectName) { StatusEffect statusEffect = ObjectDB.instance.GetStatusEffect(StringExtensionMethods.GetStableHashCode(effectName)); if (TryParseTwoValues(config, out var value, out var value2) && (Object)(object)statusEffect != (Object)null) { statusEffect.m_ttl = value; statusEffect.m_cooldown = value2; } else if (!string.IsNullOrEmpty(config)) { Log.Warning($"Error updating {effectName} - {statusEffect}"); } } private static void ApplyConfiguration() { if (!ZNet.instance.IsDedicated()) { RefreshStatusEffects(Configuration); SetShieldStaffConfig("StaffShield", Configuration.StaffShieldAbsorbtionConfig.Value); SetSiegeItemsConfiguration(Configuration); Log.Message("Config applied"); } } private static void SetSiegeItemsConfiguration(Configuration configuration) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) if (TryParseDamage(configuration.SiegeBombDamageConfig.Value, out var damage)) { Aoe prefabComponent = ZNetScene.instance.GetPrefabComponent<Aoe>("siegebomb_explosion"); prefabComponent.m_damage = damage; } else if (!string.IsNullOrEmpty(configuration.SiegeBombDamageConfig.Value)) { Log.Error("Cannot parse damage for Siege bomb"); } if (TryParseDamage(configuration.RamDamageConfig.Value, out var damage2)) { SiegeMachine prefabComponent2 = ZNetScene.instance.GetPrefabComponent<SiegeMachine>("BatteringRam"); Aoe[] componentsInChildren = prefabComponent2.m_aoe.GetComponentsInChildren<Aoe>(true); foreach (Aoe val in componentsInChildren) { val.m_damage = damage2; } } else if (!string.IsNullOrEmpty(configuration.RamDamageConfig.Value)) { Log.Error("Cannot parse damage for Ram"); } } private static bool TryParseTwoValues(string text, out int value1, out int value2) { try { string[] array = text.Split(new char[1] { ':' }); value1 = int.Parse(array[0]); value2 = int.Parse(array[1]); return true; } catch { value1 = (value2 = 0); return false; } } private static bool TryParseDamage(string text, out DamageTypes damage) { //IL_00dc: 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_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) try { string[] array = text.Split(new char[1] { ':' }); damage = new DamageTypes { m_pickaxe = int.Parse(array[0]), m_chop = int.Parse(array[1]), m_damage = int.Parse(array[2]), m_blunt = int.Parse(array[3]), m_pierce = int.Parse(array[4]), m_slash = int.Parse(array[5]), m_fire = int.Parse(array[6]), m_frost = int.Parse(array[7]), m_lightning = int.Parse(array[8]), m_poison = int.Parse(array[9]), m_spirit = int.Parse(array[10]) }; return true; } catch { damage = default(DamageTypes); return false; } } } } namespace ValheimPvPTweaks.Tweaks { [HarmonyPatch] internal class SkillsLossPatches { [HarmonyPrefix] [HarmonyPatch(typeof(Skills), "OnDeath")] private static bool Skills_OnDeath() { return Plugin.Configuration.SkillsLoss.Value switch { Configuration.SkillLossType.NoLoss => false, Configuration.SkillLossType.NoLossInPvp => !Player.m_localPlayer.InCombat(), Configuration.SkillLossType.Vanilla => true, _ => true, }; } [HarmonyPatch(typeof(Player), "OnDeath")] [HarmonyPostfix] private static void Player_OnDeath(Player __instance) { switch (Plugin.Configuration.SkillsLoss.Value) { case Configuration.SkillLossType.NoLossInPvp: if (!__instance.InCombat()) { break; } goto case Configuration.SkillLossType.NoLoss; case Configuration.SkillLossType.NoLoss: __instance.m_timeSinceDeath = __instance.m_hardDeathCooldown; break; } } } [HarmonyPatch(typeof(TombStone))] internal class TombStoneBoostPatches { [HarmonyPrefix] [HarmonyPatch("GiveBoost")] private static bool TombStone_GiveBoost() { return Plugin.Configuration.EnableTombStoneBoost.Value; } } } namespace ValheimPvPTweaks.PvpCombat { [HarmonyPatch] internal class Patches { [HarmonyPatch(typeof(Player), "OnSpawned")] [HarmonyPostfix] private static void Player_OnSpawned(Player __instance) { ((Component)__instance).gameObject.AddComponent<PlayerCombatHandler>(); } [HarmonyPostfix] [HarmonyPatch(typeof(ObjectDB), "Awake")] private static void ObjectDB_Awake(ObjectDB __instance) { __instance.m_StatusEffects.Add((StatusEffect)(object)SE_Combat.Create()); } [HarmonyPostfix] [HarmonyPatch(typeof(Game), "Start")] private static void Game_Start() { StatusEffect statusEffect = ObjectDB.instance.GetStatusEffect(StringExtensionMethods.GetStableHashCode("SE_Combat")); statusEffect.m_icon = ((Component)InventoryGui.instance.m_pvp).GetComponent<ToggleImage>().m_onImage; } [HarmonyPostfix] [HarmonyPatch(typeof(Humanoid), "IsTeleportable")] private static void Player_IsTeleportable(Humanoid __instance, ref bool __result) { if (!Plugin.Configuration.AllowTeleportInCombat.Value) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null) { __result &= !val.InCombat(); } } } [HarmonyPrefix] [HarmonyPatch(typeof(TeleportWorld), "Teleport")] private static bool TeleportWorld_Teleport(TeleportWorld __instance, Player player) { if (Plugin.Configuration.AllowTeleportInCombat.Value || !__instance.TargetFound() || (Object)(object)player == (Object)null || !player.InCombat()) { return true; } ((Character)player).Message((MessageType)2, "$vpo_msg_noteleport_combat", 0, (Sprite)null); return false; } [HarmonyPrefix] [HarmonyPatch(typeof(Humanoid), "Pickup")] private static void Humanoid_Pickup(Humanoid __instance, ref bool autoequip) { if (((Character)__instance).IsPlayer() && autoequip && !Plugin.Configuration.AllowChangeEquipmentInCombat.Value) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (((Character)(object)val).IsLocalPlayer() && val.InCombat()) { autoequip = false; } } } [HarmonyPrefix] [HarmonyPatch(typeof(Player), "ToggleEquipped")] private static bool Player_ToggleEquipped(Player __instance, ItemData item, ref bool __result) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Invalid comparison between Unknown and I4 //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Invalid comparison between Unknown and I4 //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Invalid comparison between Unknown and I4 if (!((Character)(object)__instance).IsLocalPlayer() || !__instance.InCombat() || !item.IsEquipable() || Plugin.Configuration.AllowChangeEquipmentInCombat.Value) { return true; } ItemType itemType = item.m_shared.m_itemType; ItemType val = itemType; if (val - 6 <= 1 || val - 11 <= 1 || val - 17 <= 1) { ((Character)__instance).Message((MessageType)2, "$vpo_msg_noequip_combat", 0, (Sprite)null); __result = true; return false; } return true; } } [HarmonyPatch] internal class PlayerAgressiveBehaviourPatches { [HarmonyFinalizer] [HarmonyPatch(typeof(Character), "Damage")] public static void Character_Damage(Character __instance, HitData hit) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) if (__instance.IsTamed() && IsAgressiveLocalPlayerHit(hit) && !PrivateArea.CheckAccess(((Component)__instance).transform.position, 0f, true, false)) { PlayerCombatHandler.EnterCombat((Character)(object)Player.m_localPlayer); } } [HarmonyPatch(typeof(TreeBase), "Damage")] [HarmonyPatch(typeof(MineRock5), "Damage")] [HarmonyPatch(typeof(MineRock), "Damage")] [HarmonyPatch(typeof(Destructible), "Damage")] [HarmonyFinalizer] [HarmonyPatch(typeof(TreeLog), "Damage")] public static void Destructible_Damage(Component __instance, HitData hit) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) if (IsAgressiveLocalPlayerHit(hit) && !PrivateArea.CheckAccess(__instance.transform.position, 0f, true, false)) { PlayerCombatHandler.EnterCombat((Character)(object)Player.m_localPlayer); } } [HarmonyPatch(typeof(WearNTear), "Damage")] [HarmonyFinalizer] public static void WearNTear_Damage(Component __instance, HitData hit) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (hit.HaveAttacker() && hit.GetAttacker().IsLocalPlayer() && !PrivateArea.CheckAccess(__instance.transform.position, 0f, true, false)) { PlayerCombatHandler.EnterCombat((Character)(object)Player.m_localPlayer); } } [HarmonyPatch(typeof(Pickable), "Interact")] [HarmonyFinalizer] public static void Pickable_Interact(Pickable __instance, Humanoid character, ref bool __result) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) if ((((Character)(object)character).IsLocalPlayer() && !PrivateArea.CheckAccess(((Component)__instance).transform.position, 0f, true, false)) & __result) { PlayerCombatHandler.EnterCombat((Character)(object)character); } } private static bool IsAgressiveLocalPlayerHit(HitData hit) { return hit.HaveAttacker() && hit.GetTotalDamage() > 0f && (Object)(object)hit.GetAttacker() == (Object)(object)Player.m_localPlayer; } } internal class PlayerCombatHandler : MonoBehaviour { private const string EnterCombatRpc = "EnterCombatRpc"; private Player _player; private void Awake() { _player = ((Component)this).GetComponent<Player>(); ((Character)_player).m_onDamaged = (Action<float, Character>)Delegate.Combine(((Character)_player).m_onDamaged, new Action<float, Character>(OnDamaged)); if ((Object)(object)((Character)_player).m_nview != (Object)null && ((Character)_player).m_nview.IsValid()) { ((Character)_player).m_nview.Register("EnterCombatRpc", (Action<long>)OnEnteredPvpMode); } } private void OnDamaged(float damage, Character character) { if (!(damage <= 0f) && !((Object)(object)character == (Object)null) && character.IsPlayer()) { OnEnteredPvpMode(0L); character.m_nview.InvokeRPC("EnterCombatRpc", Array.Empty<object>()); } } private void OnEnteredPvpMode(long obj) { ((Character)_player).GetSEMan().AddStatusEffect(StringExtensionMethods.GetStableHashCode("SE_Combat"), true, 0, 0f); } public static void EnterCombat(Character character) { if ((Object)(object)character != (Object)null && (Object)(object)character.m_nview != (Object)null && character.m_nview.IsValid()) { character.m_nview.InvokeRPC("EnterCombatRpc", Array.Empty<object>()); } } } public class SE_Combat : SE_Stats { public const string Name = "SE_Combat"; public override bool CanAdd(Character character) { return character.IsPlayer(); } public override void Setup(Character character) { ((SE_Stats)this).Setup(character); base.m_healthRegenMultiplier = Plugin.Configuration.CombatHealthRegen.Value; ((StatusEffect)this).m_ttl = Plugin.Configuration.CombatDuration.Value; ZNetView nview = ((StatusEffect)this).m_character.m_nview; if (nview != null) { ZDO zDO = nview.GetZDO(); if (zDO != null) { zDO.Set("VPT_PlayerInCombat", true); } } } public override void Stop() { ((StatusEffect)this).Stop(); if (Game.instance.IsShuttingDown()) { return; } ZNetView nview = ((StatusEffect)this).m_character.m_nview; if (nview != null) { ZDO zDO = nview.GetZDO(); if (zDO != null) { zDO.Set("VPT_PlayerInCombat", false); } } } public static SE_Combat Create() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) SE_Combat sE_Combat = ScriptableObject.CreateInstance<SE_Combat>(); ((Object)sE_Combat).name = "SE_Combat"; ((StatusEffect)sE_Combat).m_name = "$vpo_se_combat_name"; ((StatusEffect)sE_Combat).m_startMessage = "$vpo_se_combat_start_msg"; ((StatusEffect)sE_Combat).m_startMessageType = (MessageType)2; ((StatusEffect)sE_Combat).m_stopMessage = "$vpo_se_combat_stop_msg"; ((StatusEffect)sE_Combat).m_stopMessageType = (MessageType)1; return sE_Combat; } } internal class TamedAnimalCombatHandler : MonoBehaviour { private Character _character; private ZNetView _view; private HashSet<string> _markedPlayersEnemy = new HashSet<string>(); private PrivateArea _defendingArea; private static event Action<Character, Player> _onPlayerAttackerCharacter; private void Awake() { _character = ((Component)this).GetComponent<Character>(); _view = _character.m_nview; ((MonoBehaviour)this).InvokeRepeating("RefreshDefendingArea", 0f, 5f); } private bool TryGetTameable(out Tameable tameable) { return ((Component)this).TryGetComponent<Tameable>(ref tameable); } private void OnEnable() { _onPlayerAttackerCharacter += CheckAttackedTarget; } private void OnDisable() { _onPlayerAttackerCharacter -= CheckAttackedTarget; } internal static void OnPlayerAttackedCharacter(Character attacked, Player attacker) { TamedAnimalCombatHandler._onPlayerAttackerCharacter?.Invoke(attacked, attacker); } private void CheckAttackedTarget(Character character, Player player) { } public bool IsFollowingPlayer() { Tameable tameable; return TryGetTameable(out tameable) && (Object)(object)tameable.m_monsterAI.GetFollowTarget() != (Object)null; } public bool IsDefendingArea() { return (Object)(object)_defendingArea != (Object)null; } public Player GetFollowPlayer() { object result; if (!TryGetTameable(out var tameable)) { result = null; } else { MonsterAI monsterAI = tameable.m_monsterAI; if (monsterAI == null) { result = null; } else { GameObject followTarget = monsterAI.GetFollowTarget(); result = ((followTarget != null) ? followTarget.GetComponent<Player>() : null); } } return (Player)result; } public bool CheckIsEnemy(TamedAnimalCombatHandler character) { Player followPlayer = character.GetFollowPlayer(); Player followPlayer2 = GetFollowPlayer(); if ((Object)(object)followPlayer != (Object)null && CheckIsEnemy(followPlayer) && (Object)(object)followPlayer2 != (Object)(object)followPlayer) { return true; } if ((Object)(object)followPlayer2 != (Object)null && character.CheckIsEnemy(followPlayer2)) { return true; } return false; } public bool CheckIsEnemy(Player player) { if (IsEnemy(player.GetPlayerName())) { return true; } if (IsDefendingArea() && !_defendingArea.HaveAccess(player) && player.InCombat()) { _markedPlayersEnemy.Add(player.GetPlayerName()); return true; } return false; } private bool IsEnemy(string name) { return _markedPlayersEnemy.Contains(name); } private void RefreshDefendingArea() { if ((Object)(object)_view == (Object)null || !_view.IsValid() || !_view.IsOwner()) { return; } Player followPlayer = GetFollowPlayer(); if ((Object)(object)followPlayer == (Object)null) { _defendingArea = ((IEnumerable<PrivateArea>)PrivateArea.m_allAreas).FirstOrDefault((Func<PrivateArea, bool>)IsActiveAndInside); return; } if (!Plugin.Configuration.FollowCreaturesProtectWard.Value) { _defendingArea = null; return; } _defendingArea = ((IEnumerable<PrivateArea>)PrivateArea.m_allAreas).FirstOrDefault((Func<PrivateArea, bool>)((PrivateArea p) => IsPermitted(p, followPlayer.GetPlayerID()) && IsActiveAndInside(p))); } private bool IsActiveAndInside(PrivateArea privateArea) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) return privateArea.IsEnabled() && privateArea.IsInside(((Component)this).transform.position, 0f); } private bool IsPermitted(PrivateArea privateArea, long playerId) { return privateArea.IsPermitted(playerId) || ((Object)(object)privateArea.m_piece != (Object)null && privateArea.m_piece.GetCreator() == playerId); } } [HarmonyPatch] internal class TamedAnimalsPvpPatches { [HarmonyPatch(typeof(Humanoid), "Start")] [HarmonyFinalizer] [HarmonyPatch(typeof(Character), "Start")] private static void Character_Start(Character __instance) { Character_SetTamed(__instance, __instance.IsTamed()); } [HarmonyPrefix] [HarmonyPatch(typeof(Character), "Damage")] private static void Character_Damage_Prefix(Character __instance, HitData hit) { if (__instance.IsTamed()) { Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); TamedAnimalCombatHandler animal = default(TamedAnimalCombatHandler); if (val != null && ((Component)__instance).TryGetComponent<TamedAnimalCombatHandler>(ref animal) && !CanTakeControl(animal, (Character)(object)val) && ((Character)(object)val).IsLocalPlayer() && ((Humanoid)val).GetCurrentWeapon().m_shared.m_tamedOnly) { hit.ClearDamage(); } } } [HarmonyFinalizer] [HarmonyPatch(typeof(Character), "Damage")] private static void Character_Damage(Character __instance, HitData hit) { Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if (val != null && hit.GetTotalDamage() > 0f) { TamedAnimalCombatHandler.OnPlayerAttackedCharacter(__instance, val); } } [HarmonyFinalizer] [HarmonyPatch(typeof(Character), "SetTamed")] private static void Character_SetTamed(Character __instance, bool tamed) { TamedAnimalCombatHandler tamedAnimalCombatHandler = default(TamedAnimalCombatHandler); bool flag = ((Component)__instance).TryGetComponent<TamedAnimalCombatHandler>(ref tamedAnimalCombatHandler); if (flag != tamed) { if (tamed) { ((Component)__instance).gameObject.AddComponent<TamedAnimalCombatHandler>(); } else { Object.Destroy((Object)(object)tamedAnimalCombatHandler); } } __instance.SetupMaxHealth(); } [HarmonyPrefix] [HarmonyPatch(typeof(Tameable), "Interact")] private static bool Tameable_Interact(Character __instance, Humanoid user) { TamedAnimalCombatHandler animal = default(TamedAnimalCombatHandler); if (((Component)__instance).TryGetComponent<TamedAnimalCombatHandler>(ref animal) && !CanTakeControl(animal, (Character)(object)user)) { ((Character)user).Message((MessageType)2, "$piece_noaccess", 0, (Sprite)null); return false; } return true; } [HarmonyPostfix] [HarmonyPatch(typeof(Tameable), "GetHoverText")] private static void Tameable_GetHoverText_Postfix(Character __instance, ref string __result) { TamedAnimalCombatHandler tamedAnimalCombatHandler = default(TamedAnimalCombatHandler); if (((Component)__instance).TryGetComponent<TamedAnimalCombatHandler>(ref tamedAnimalCombatHandler)) { if (!CanTakeControl(tamedAnimalCombatHandler, (Character)(object)Player.m_localPlayer)) { __result += "\n$piece_noaccess".Localize(); } if (tamedAnimalCombatHandler.IsDefendingArea()) { __result += "\n$vpo_tamed_msg_defending".Localize(); } if (tamedAnimalCombatHandler.IsFollowingPlayer()) { __result += ("\n$vpo_tamed_msg_following <color=yellow>" + tamedAnimalCombatHandler.GetFollowPlayer().GetPlayerName() + "</color>").Localize(); } } } [HarmonyPatch(typeof(BaseAI), "IsEnemy", new Type[] { typeof(Character), typeof(Character) })] [HarmonyPostfix] private static void BaseAI_IsEnemey(Character a, Character b, ref bool __result) { if (__result || !Plugin.Configuration.CreaturesProtectWard.Value || (Object)(object)a == (Object)null || (Object)(object)b == (Object)null) { return; } Character val; Character val2; if (a.IsTamed()) { val = a; val2 = b; } else { if (!b.IsTamed()) { return; } val = b; val2 = a; } TamedAnimalCombatHandler tamedAnimalCombatHandler = default(TamedAnimalCombatHandler); if (((Component)val).TryGetComponent<TamedAnimalCombatHandler>(ref tamedAnimalCombatHandler)) { Player val3 = (Player)(object)((val2 is Player) ? val2 : null); TamedAnimalCombatHandler character = default(TamedAnimalCombatHandler); if (val3 != null) { __result |= tamedAnimalCombatHandler.CheckIsEnemy(val3); } else if (val2.IsTamed() && ((Component)val2).TryGetComponent<TamedAnimalCombatHandler>(ref character)) { __result |= tamedAnimalCombatHandler.CheckIsEnemy(character); } } } [HarmonyPatch(typeof(Attack), "GetLevelDamageFactor")] [HarmonyPostfix] private static void Attack_GetLevelDamageFactor(Attack __instance, ref float __result) { if ((Object)(object)__instance.m_character != (Object)null && ((Character)__instance.m_character).IsTamed()) { __result *= Plugin.Configuration.TamedCreaturesDamageMultiplier.Value; } } [HarmonyPatch(typeof(Character), "SetMaxHealth")] [HarmonyPrefix] private static void Character_SetMaxHealth(Character __instance, ref float health) { if (__instance.IsTamed()) { health *= Plugin.Configuration.TamedCreaturesHealthMultiplier.Value; } } private static bool CanTakeControl(TamedAnimalCombatHandler animal, Character character) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)animal.GetFollowPlayer() == (Object)(object)character) { return true; } if (animal.IsFollowingPlayer() || !PrivateArea.CheckAccess(((Component)animal).transform.position, 0f, false, false)) { return false; } return true; } } } namespace ValheimPvPTweaks.KillFeed { public class CharacterKillTracker : MonoBehaviour { private const int AttackTime = 15; private Character _character; private Character _lastAttacker; private string _lastAttackerWeapon; private bool _isDead = false; private double _lastAttackTime; public static event Action<string, string> OnCharacterKilled; public static event Action<Character, Character, string> OnCharacterDead; private void Awake() { _character = ((Component)this).GetComponent<Character>(); _character.m_onDamaged = (Action<float, Character>)Delegate.Combine(_character.m_onDamaged, new Action<float, Character>(OnDamaged)); if ((Object)(object)_character.m_nview != (Object)null && _character.m_nview.IsValid()) { _character.m_nview.Register<string, string>("VPT_CharacterDeadRpc", (Action<long, string, string>)RPC_CharacterKilled); } } private void RPC_CharacterKilled(long uid, string name, string weapon) { CharacterKillTracker.OnCharacterKilled?.Invoke(name, weapon); } private void OnDeath() { //IL_004e: 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_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_lastAttacker != (Object)null && ZNet.instance.GetTimeSeconds() - _lastAttackTime < 15.0) { ZRoutedRpc.instance.InvokeRoutedRPC("VPT_CharacterDeadRpc", new object[3] { _character.GetZDOID(), _lastAttacker.GetZDOID(), _lastAttackerWeapon }); CharacterKillTracker.OnCharacterDead?.Invoke(_character, _lastAttacker, _lastAttackerWeapon); if (_lastAttacker is Player) { Character character = _character; Player val = (Player)(object)((character is Player) ? character : null); string text = ((val != null) ? val.GetPlayerName() : _character.m_name); _lastAttacker.m_nview.InvokeRPC("VPT_CharacterDeadRpc", new object[2] { text, _lastAttackerWeapon }); } } else { if (_character.IsPlayer()) { ZRoutedRpc.instance.InvokeRoutedRPC("VPT_CharacterDeadRpc", new object[3] { _character.GetZDOID(), ZDOID.None, "" }); } CharacterKillTracker.OnCharacterDead?.Invoke(_character, null, ""); } } private void OnDamaged(float damage, Character character) { if (!_isDead) { if ((Object)(object)character != (Object)null) { _lastAttacker = character; _lastAttackTime = ZNet.instance.GetTimeSeconds(); Player val = (Player)(object)((character is Player) ? character : null); _lastAttackerWeapon = ((val != null) ? ((Humanoid)val).GetCurrentWeapon().m_shared.m_name : ""); } if (_character.GetHealth() <= 0f) { _isDead = true; OnDeath(); } } } } public class KillFeed : MonoBehaviour { public struct KillData { public struct CharacterData { public string displayName; public string prefabName; public ZNetPeer peer; public bool isPlayer; public ZDO zdo; } public CharacterData killed; public CharacterData killer; public string weapon; } private const string KillFeedRpc = "VPT_KillFeedRpc"; private static UserInfo KillFeedUser = new UserInfo { Name = "Kill feed" }; private int _playerPrefabHash; private StringBuilder _builder = new StringBuilder(); public static event Action<KillData> OnCharacterKilled; private void Awake() { _playerPrefabHash = StringExtensionMethods.GetStableHashCode(((Object)Game.instance.m_playerPrefab).name); if (ZNet.instance.IsServer()) { ZRoutedRpc.instance.Register<ZDOID, ZDOID, string>("VPT_CharacterDeadRpc", (Action<long, ZDOID, ZDOID, string>)RPC_CharacterKilled); OnCharacterKilled += SendKillNotifications; OnCharacterKilled += SendDiscordNotification; } else { ZRoutedRpc.instance.Register<string, string, Vector3>("VPT_KillFeedRpc", (Action<long, string, string, Vector3>)RPC_KillFeed); } } internal void OnPlayerDisconnected(ZNetPeer peer) { string url = Plugin.Configuration.DiscordWebhook.Value; if (ZNet.instance.IsServer() && peer.InCombat() && !string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(Plugin.Configuration.DisconnectedInCombatFormat.Value)) { Log.Info("Player " + peer.GetSteamId() + ":" + peer.m_playerName + " disconnected in combat"); string message = Plugin.Configuration.DisconnectedInCombatFormat.Value.Replace("{player}", peer.m_playerName); ThreadinUtil.RunThread(delegate { DiscordTool.SendMessageToDiscord(url, "KillFeed", message); }); } } private void RPC_KillFeed(long arg1, string characterName, string attackerName, Vector3 position) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(characterName)) { if (!string.IsNullOrEmpty(attackerName)) { string text = (characterName + " $vpo_kill_feed_msg_killed_by " + attackerName).Localize(); ((Terminal)Chat.instance).AddString(KillFeedUser.Name, text, (Type)2, false); Chat.instance.AddInworldText((GameObject)null, arg1, position, (Type)2, KillFeedUser, text); } else { ((Terminal)Chat.instance).AddString(KillFeedUser.Name, (characterName + " $vpo_kill_feed_msg_dead").Localize(), (Type)1, false); } Chat.instance.m_hideTimer = 0f; } } private void RPC_CharacterKilled(long uid, ZDOID characterId, ZDOID attackerId, string weapon) { //IL_0021: 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_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: 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_01ee: Unknown result type (might be due to invalid IL or missing references) //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_0209: Unknown result type (might be due to invalid IL or missing references) ZNetPeer peer = ZNet.instance.GetPeer(uid); try { if (((ZDOID)(ref characterId)).IsNone() || !ZDOMan.instance.m_objectsByID.ContainsKey(characterId)) { Log.Debug($"Received invalid killed character id {characterId} from {peer?.m_playerName}:{peer?.GetSteamId()}"); return; } bool isPlayer; ZDO zdo; ZNetPeer peer2; string name = GetName(characterId, out isPlayer, out zdo, out peer2); bool isPlayer2; ZDO zdo2; ZNetPeer peer3; string name2 = GetName(attackerId, out isPlayer2, out zdo2, out peer3); Log.Debug($"Killed {name} ({characterId}) by {name2} ({attackerId}) with {weapon}"); if (zdo2 == zdo) { return; } Action<KillData> onCharacterKilled = KillFeed.OnCharacterKilled; if (onCharacterKilled != null) { KillData obj = default(KillData); KillData.CharacterData killed = new KillData.CharacterData { displayName = name, isPlayer = isPlayer, peer = peer2, zdo = zdo }; GameObject prefab = ZNetScene.instance.GetPrefab(zdo.GetPrefab()); killed.prefabName = ((prefab != null) ? ((Object)prefab).name : null); obj.killed = killed; killed = new KillData.CharacterData { displayName = name2, isPlayer = isPlayer2, peer = peer3, zdo = zdo2 }; object prefabName; if (zdo2 == null) { prefabName = string.Empty; } else { GameObject prefab2 = ZNetScene.instance.GetPrefab(zdo2.GetPrefab()); prefabName = ((prefab2 != null) ? ((Object)prefab2).name : null); } killed.prefabName = (string)prefabName; obj.killer = killed; obj.weapon = weapon; onCharacterKilled(obj); } } catch (Exception ex) { Log.Warning($"Cannot handle kill received from {peer?.m_playerName}:{peer?.GetSteamId()} ({peer?.m_characterID}) {attackerId} {characterId}: {ex.Message}\n{ex.StackTrace}]"); } } private void SendKillNotifications(KillData killData) { //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_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) KillData.CharacterData killed = killData.killed; if (!killed.isPlayer) { return; } Vector3 position = killed.zdo.GetPosition(); int value = Plugin.Configuration.MaxDeathPingRadius.Value; if (value <= 0) { return; } foreach (ZNetPeer peer in ZNet.instance.m_peers) { if (peer.IsReady() && !(Utils.DistanceXZ(peer.GetRefPos(), position) >= (float)value)) { ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "VPT_KillFeedRpc", new object[3] { killed.displayName, killData.killer.displayName, position }); } } } private void SendDiscordNotification(KillData killData) { string url = Plugin.Configuration.DiscordWebhook.Value; KillData.CharacterData killed = killData.killed; KillData.CharacterData killer = killData.killer; if (string.IsNullOrEmpty(url) || !killed.isPlayer) { return; } Log.Info("Sending discord notifiaction " + killed.displayName + " killed by " + killer.displayName); string messageFormat = GetMessageFormat(killData); if (!string.IsNullOrEmpty(messageFormat)) { _builder.Clear(); _builder.Append(messageFormat); if (!string.IsNullOrEmpty(killer.displayName)) { string newValue = Localization.instance.Localize(killer.displayName); _builder.Replace("{attacker}", newValue); } _builder.Replace("{player}", killed.displayName); ThreadinUtil.RunThread(delegate { DiscordTool.SendMessageToDiscord(url, Plugin.Configuration.KillFeedName.Value, _builder.ToString()); }); } } private string GetMessageFormat(KillData killData) { KillData.CharacterData killed = killData.killed; KillData.CharacterData killer = killData.killer; if (string.IsNullOrEmpty(killer.displayName)) { return Plugin.Configuration.DeadMessageFormat.Value; } return (killer.isPlayer && killed.isPlayer) ? Plugin.Configuration.KilledInPvpMessageFormat.Value : Plugin.Configuration.KilledMessageFormat.Value; } private string GetName(ZDOID id, out bool isPlayer, out ZDO zdo, out ZNetPeer peer) { //IL_0007: 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) //IL_0018: 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_002d: Unknown result type (might be due to invalid IL or missing references) isPlayer = false; zdo = ZDOMan.instance.GetZDO(id); peer = null; if (id == ZDOID.None) { return string.Empty; } if (zdo.GetPrefab() == _playerPrefabHash) { peer = ZNet.instance.m_peers.Find((ZNetPeer p) => p.m_characterID == id); if (peer != null) { isPlayer = true; return peer.m_playerName; } return string.Empty; } GameObject prefab = ZNetScene.instance.GetPrefab(zdo.GetPrefab()); if ((Object)(object)prefab == (Object)null) { return string.Empty; } Character val = default(Character); if (prefab.TryGetComponent<Character>(ref val)) { return val.m_name; } return ((Object)prefab).name; } } [HarmonyPatch] internal class Patches { private static KillFeed _killFeed; [HarmonyPostfix] [HarmonyPatch(typeof(Character), "Start")] [HarmonyPatch(typeof(Humanoid), "Start")] private static void Character_Start(Character __instance) { CharacterKillTracker characterKillTracker = default(CharacterKillTracker); if (!((Component)__instance).TryGetComponent<CharacterKillTracker>(ref characterKillTracker)) { ((Component)__instance).gameObject.AddComponent<CharacterKillTracker>(); } } [HarmonyPatch(typeof(Game), "Start")] [HarmonyPostfix] private static void Game_Start(Game __instance) { _killFeed = ((Component)__instance).gameObject.AddComponent<KillFeed>(); } [HarmonyPrefix] [HarmonyPatch(typeof(ZNet), "Disconnect")] private static void ZNet_Disconnect(Game __instance, ZNetPeer peer) { _killFeed.OnPlayerDisconnected(peer); } } } namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [Microsoft.CodeAnalysis.Embedded] [CompilerGenerated] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ServerSync { [PublicAPI] internal abstract class OwnConfigEntryBase { public object? LocalBaseValue; public bool SynchronizedConfig = true; public abstract ConfigEntryBase BaseConfig { get; } } [PublicAPI] internal class SyncedConfigEntry<T> : OwnConfigEntryBase { public readonly ConfigEntry<T> SourceConfig; public override ConfigEntryBase BaseConfig => (ConfigEntryBase)(object)SourceConfig; public T Value { get { return SourceConfig.Value; } set { SourceConfig.Value = value; } } public SyncedConfigEntry(ConfigEntry<T> sourceConfig) { SourceConfig = sourceConfig; } public void AssignLocalValue(T value) { if (LocalBaseValue == null) { Value = value; } else { LocalBaseValue = value; } } } internal abstract class CustomSyncedValueBase { public object? LocalBaseValue; public readonly string Identifier; public readonly Type Type; private object? boxedValue; protected bool localIsOwner; public readonly int Priority; public object? BoxedValue { get { return boxedValue; } set { boxedValue = value; this.ValueChanged?.Invoke(); } } public event Action? ValueChanged; protected CustomSyncedValueBase(ConfigSync configSync, string identifier, Type type, int priority) { Priority = priority; Identifier = identifier; Type = type; configSync.AddCustomValue(this); localIsOwner = configSync.IsSourceOfTruth; configSync.SourceOfTruthChanged += delegate(bool truth) { localIsOwner = truth; }; } } [PublicAPI] internal sealed class CustomSyncedValue<T> : CustomSyncedValueBase { public T Value { get { return (T)base.BoxedValue; } set { base.BoxedValue = value; } } public CustomSyncedValue(ConfigSync configSync, string identifier, T value = default(T), int priority = 0) : base(configSync, identifier, typeof(T), priority) { Value = value; } public void AssignLocalValue(T value) { if (localIsOwner) { Value = value; } else { LocalBaseValue = value; } } } internal class ConfigurationManagerAttributes { [UsedImplicitly] public bool? ReadOnly = false; } [PublicAPI] internal class ConfigSync { [HarmonyPatch(typeof(ZRpc), "HandlePackage")] private static class SnatchCurrentlyHandlingRPC { public static ZRpc? currentRpc; [HarmonyPrefix] private static void Prefix(ZRpc __instance) { currentRpc = __instance; } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class RegisterRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { isServer = __instance.IsServer(); foreach (ConfigSync configSync2 in configSyncs) { ZRoutedRpc.instance.Register<ZPackage>(configSync2.Name + " ConfigSync", (Action<long, ZPackage>)configSync2.RPC_FromOtherClientConfigSync); if (isServer) { configSync2.InitialSyncDone = true; Debug.Log((object)("Registered '" + configSync2.Name + " ConfigSync' RPC - waiting for incoming connections")); } } if (isServer) { ((MonoBehaviour)__instance).StartCoroutine(WatchAdminListChanges()); } static void SendAdmin(List<ZNetPeer> peers, bool isAdmin) { ZPackage package = ConfigsToPackage(null, null, new PackageEntry[1] { new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = isAdmin } }); ConfigSync configSync = configSyncs.First(); if (configSync != null) { ((MonoBehaviour)ZNet.instance).StartCoroutine(configSync.sendZPackage(peers, package)); } } static IEnumerator WatchAdminListChanges() { SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); List<string> CurrentList = new List<string>(adminList.GetList()); while (true) { yield return (object)new WaitForSeconds(30f); if (!adminList.GetList().SequenceEqual(CurrentList)) { CurrentList = new List<string>(adminList.GetList()); List<ZNetPeer> adminPeer = (from p in ZNet.instance.GetPeers() where adminList.Contains(p.m_rpc.GetSocket().GetHostName()) select p).ToList(); List<ZNetPeer> nonAdminPeer = ZNet.instance.GetPeers().Except(adminPeer).ToList(); SendAdmin(nonAdminPeer, isAdmin: false); SendAdmin(adminPeer, isAdmin: true); } } } } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class RegisterClientRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { return; } foreach (ConfigSync configSync in configSyncs) { peer.m_rpc.Register<ZPackage>(configSync.Name + " ConfigSync", (Action<ZRpc, ZPackage>)configSync.RPC_FromServerConfigSync); } } } private class ParsedConfigs { public readonly Dictionary<OwnConfigEntryBase, object?> configValues = new Dictionary<OwnConfigEntryBase, object>(); public readonly Dictionary<CustomSyncedValueBase, object?> customValues = new Dictionary<CustomSyncedValueBase, object>(); } [HarmonyPatch(typeof(ZNet), "Shutdown")] private class ResetConfigsOnShutdown { [HarmonyPostfix] private static void Postfix() { ProcessingServerUpdate = true; foreach (ConfigSync configSync in configSyncs) { configSync.resetConfigsFromServer(); configSync.IsSourceOfTruth = true; configSync.InitialSyncDone = false; } ProcessingServerUpdate = false; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private class SendConfigsAfterLogin { private class BufferingSocket : ISocket { public volatile bool finished = false; public volatile int versionMatchQueued = -1; public readonly List<ZPackage> Package = new List<ZPackage>(); public readonly ISocket Original; public BufferingSocket(ISocket original) { Original = original; } public bool IsConnected() { return Original.IsConnected(); } public ZPackage Recv() { return Original.Recv(); } public int GetSendQueueSize() { return Original.GetSendQueueSize(); } public int GetCurrentSendRate() { return Original.GetCurrentSendRate(); } public bool IsHost() { return Original.IsHost(); } public void Dispose() { Original.Dispose(); } public bool GotNewData() { return Original.GotNewData(); } public void Close() { Original.Close(); } public string GetEndPointString() { return Original.GetEndPointString(); } public void GetAndResetStats(out int totalSent, out int totalRecv) { Original.GetAndResetStats(ref totalSent, ref totalRecv); } public void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { Original.GetConnectionQuality(ref localQuality, ref remoteQuality, ref ping, ref outByteSec, ref inByteSec); } public ISocket Accept() { return Original.Accept(); } public int GetHostPort() { return Original.GetHostPort(); } public bool Flush() { return Original.Flush(); } public string GetHostName() { return Original.GetHostName(); } public void VersionMatch() { if (finished) { Original.VersionMatch(); } else { versionMatchQueued = Package.Count; } } public void Send(ZPackage pkg) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown int pos = pkg.GetPos(); pkg.SetPos(0); int num = pkg.ReadInt(); if ((num == StringExtensionMethods.GetStableHashCode("PeerInfo") || num == StringExtensionMethods.GetStableHashCode("RoutedRPC") || num == StringExtensionMethods.GetStableHashCode("ZDOData")) && !finished) { ZPackage val = new ZPackage(pkg.GetArray()); val.SetPos(pos); Package.Add(val); } else { pkg.SetPos(pos); Original.Send(pkg); } } } [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(ref Dictionary<Assembly, BufferingSocket>? __state, ZNet __instance, ZRpc rpc) { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Invalid comparison between Unknown and I4 if (__instance.IsServer()) { BufferingSocket value = new BufferingSocket(rpc.GetSocket()); AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, value); object? obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (val != null && (int)ZNet.m_onlineBackend > 0) { AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket").SetValue(val, value); } if (__state == null) { __state = new Dictionary<Assembly, BufferingSocket>(); } __state[Assembly.GetExecutingAssembly()] = value; } } [HarmonyPostfix] private static void Postfix(Dictionary<Assembly, BufferingSocket> __state, ZNet __instance, ZRpc rpc) { ZRpc rpc2 = rpc; ZNet __instance2 = __instance; Dictionary<Assembly, BufferingSocket> __state2 = __state; ZNetPeer peer; if (__instance2.IsServer()) { object obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance2, new object[1] { rpc2 }); peer = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (peer == null) { SendBufferedData(); } else { ((MonoBehaviour)__instance2).StartCoroutine(sendAsync()); } } void SendBufferedData() { if (rpc2.GetSocket() is BufferingSocket bufferingSocket) { AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc2, bufferingSocket.Original); object? obj2 = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance2, new object[1] { rpc2 }); ZNetPeer val = (ZNetPeer)((obj2 is ZNetPeer) ? obj2 : null); if (val != null) { AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket").SetValue(val, bufferingSocket.Original); } } BufferingSocket bufferingSocket2 = __state2[Assembly.GetExecutingAssembly()]; bufferingSocket2.finished = true; for (int i = 0; i < bufferingSocket2.Package.Count; i++) { if (i == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } bufferingSocket2.Original.Send(bufferingSocket2.Package[i]); } if (bufferingSocket2.Package.Count == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } } IEnumerator sendAsync() { foreach (ConfigSync configSync in configSyncs) { List<PackageEntry> entries = new List<PackageEntry>(); if (configSync.CurrentVersion != null) { entries.Add(new PackageEntry { section = "Internal", key = "serverversion", type = typeof(string), value = configSync.CurrentVersion }); } MethodInfo listContainsId = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); entries.Add(new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = (((object)listContainsId == null) ? ((object)adminList.Contains(rpc2.GetSocket().GetHostName())) : listContainsId.Invoke(ZNet.instance, new object[2] { adminList, rpc2.GetSocket().GetHostName() })) }); ZPackage package = ConfigsToPackage(configSync.allConfigs.Select((OwnConfigEntryBase c) => c.BaseConfig), configSync.allCustomValues, entries, partial: false); yield return ((MonoBehaviour)__instance2).StartCoroutine(configSync.sendZPackage(new List<ZNetPeer> { peer }, package)); } SendBufferedData(); } } } private class PackageEntry { public string section = null; public string key = null; public Type type = null; public object? value; } [HarmonyPatch(typeof(ConfigEntryBase), "GetSerializedValue")] private static class PreventSavingServerInfo { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, ref string __result) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || isWritableConfig(ownConfigEntryBase)) { return true; } __result = TomlTypeConverter.ConvertToString(ownConfigEntryBase.LocalBaseValue, __instance.SettingType); return false; } } [HarmonyPatch(typeof(ConfigEntryBase), "SetSerializedValue")] private static class PreventConfigRereadChangingValues { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, string value) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || ownConfigEntryBase.LocalBaseValue == null) { return true; } try { ownConfigEntryBase.LocalBaseValue = TomlTypeConverter.ConvertToValue(value, __instance.SettingType); } catch (Exception ex) { Debug.LogWarning((object)$"Config value of setting \"{__instance.Definition}\" could not be parsed and will be ignored. Reason: {ex.Message}; Value: {value}"); } return false; } } private class InvalidDeserializationTypeException : Exception { public string expected = null; public string received = null; public string field = ""; } public static bool ProcessingServerUpdate; public readonly string Name; public string? DisplayName; public string? CurrentVersion; public string? MinimumRequiredVersion; public bool ModRequired = false; private bool? forceConfigLocking; private bool isSourceOfTruth = true; private static readonly HashSet<ConfigSync> configSyncs; private readonly HashSet<OwnConfigEntryBase> allConfigs = new HashSet<OwnConfigEntryBase>(); private HashSet<CustomSyncedValueBase> allCustomValues = new HashSet<CustomSyncedValueBase>(); private static bool isServer; private static bool lockExempt; private OwnConfigEntryBase? lockedConfig = null; private const byte PARTIAL_CONFIGS = 1; private const byte FRAGMENTED_CONFIG = 2; private const byte COMPRESSED_CONFIG = 4; private readonly Dictionary<string, SortedDictionary<int, byte[]>> configValueCache = new Dictionary<string, SortedDictionary<int, byte[]>>(); private readonly List<KeyValuePair<long, string>> cacheExpirations = new List<KeyValuePair<long, string>>(); private static long packageCounter; public bool IsLocked { get { bool? flag = forceConfigLocking; bool num; if (!flag.HasValue) { if (lockedConfig == null) { goto IL_0052; } num = ((IConvertible)lockedConfig.BaseConfig.BoxedValue).ToInt32(CultureInfo.InvariantCulture) != 0; } else { num = flag.GetValueOrDefault(); } if (!num) { goto IL_0052; } int result = ((!lockExempt) ? 1 : 0); goto IL_0053; IL_0053: return (byte)result != 0; IL_0052: result = 0; goto IL_0053; } set { forceConfigLocking = value; } } public bool IsAdmin => lockExempt || isSourceOfTruth; public bool IsSourceOfTruth { get { return isSourceOfTruth; } private set { if (value != isSourceOfTruth) { isSourceOfTruth = value; this.SourceOfTruthChanged?.Invoke(value); } } } public bool InitialSyncDone { get; private set; } = false; public event Action<bool>? SourceOfTruthChanged; private event Action? lockedConfigChanged; static ConfigSync() { ProcessingServerUpdate = false; configSyncs = new HashSet<ConfigSync>(); lockExempt = false; packageCounter = 0L; RuntimeHelpers.RunClassConstructor(typeof(VersionCheck).TypeHandle); } public ConfigSync(string name) { Name = name; configSyncs.Add(this); new VersionCheck(this); } public SyncedConfigEntry<T> AddConfigEntry<T>(ConfigEntry<T> configEntry) { ConfigEntry<T> configEntry2 = configEntry; OwnConfigEntryBase ownConfigEntryBase = configData((ConfigEntryBase)(object)configEntry2); SyncedConfigEntry<T> syncedEntry = ownConfigEntryBase as SyncedConfigEntry<T>; if (syncedEntry == null) { syncedEntry = new SyncedConfigEntry<T>(configEntry2); AccessTools.DeclaredField(typeof(ConfigDescription), "<Tags>k__BackingField").SetValue(((ConfigEntryBase)configEntry2).Description, new object[1] { new ConfigurationManagerAttributes() }.Concat(((ConfigEntryBase)configEntry2).Description.Tags ?? Array.Empty<object>()).Concat(new SyncedConfigEntry<T>[1] { syncedEntry }).ToArray()); configEntry2.SettingChanged += delegate { if (!ProcessingServerUpdate && syncedEntry.SynchronizedConfig) { Broadcast(ZRoutedRpc.Everybody, (ConfigEntryBase)configEntry2); } }; allConfigs.Add(syncedEntry); } return syncedEntry; } public SyncedConfigEntry<T> AddLockingConfigEntry<T>(ConfigEntry<T> lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry<T>(lockingConfig); lockingConfig.SettingChanged += delegate { this.lockedConfigChanged?.Invoke(); }; return (SyncedConfigEntry<T>)lockedConfig; } internal void AddCustomValue(CustomSyncedValueBase customValue) { CustomSyncedValueBase customValue2 = customValue; if (allCustomValues.Select((CustomSyncedValueBase v) => v.Identifier).Concat(new string[1] { "serverversion" }).Contains(customValue2.Identifier)) { throw new Exception("Cannot have multiple settings with the same name or with a reserved name (serverversion)"); } allCustomValues.Add(customValue2); allCustomValues = new HashSet<CustomSyncedValueBase>(allCustomValues.OrderByDescending((CustomSyncedValueBase v) => v.Priority)); customValue2.ValueChanged += delegate { if (!ProcessingServerUpdate) { Broadcast(ZRoutedRpc.Everybody, customValue2); } }; } private void RPC_FromServerConfigSync(ZRpc rpc, ZPackage package) { lockedConfigChanged += serverLockedSettingChanged; IsSourceOfTruth = false; if (HandleConfigSyncRPC(0L, package, clientUpdate: false)) { InitialSyncDone = true; } } private void RPC_FromOtherClientConfigSync(long sender, ZPackage package) { HandleConfigSyncRPC(sender, package, clientUpdate: true); } private bool HandleConfigSyncRPC(long sender, ZPackage package, bool clientUpdate) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Expected O, but got Unknown //IL_024e: Unknown result type (might be due to invalid IL or missing references) //IL_0255: Expected O, but got Unknown //IL_01e8: Unknown result type (might be due to invalid IL or missing references) //IL_01ef: Expected O, but got Unknown try { if (isServer && IsLocked) { ZRpc? currentRpc = SnatchCurrentlyHandlingRPC.currentRpc; object obj; if (currentRpc == null) { obj = null; } else { ISocket socket = currentRpc.GetSocket(); obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (text != null) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList val = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); if (!(((object)methodInfo == null) ? val.Contains(text) : ((bool)methodInfo.Invoke(ZNet.instance, new object[2] { val, text })))) { return false; } } } cacheExpirations.RemoveAll(delegate(KeyValuePair<long, string> kv) { if (kv.Key < DateTimeOffset.Now.Ticks) { configValueCache.Remove(kv.Value); return true; } return false; }); byte b = package.ReadByte(); if ((b & 2u) != 0) { long num = package.ReadLong(); string text2 = sender.ToString() + num; if (!configValueCache.TryGetValue(text2, out SortedDictionary<int, byte[]> value)) { value = new SortedDictionary<int, byte[]>(); configValueCache[text2] = value; cacheExpirations.Add(new KeyValuePair<long, string>(DateTimeOffset.Now.AddSeconds(60.0).Ticks, text2)); } int key = package.ReadInt(); int num2 = package.ReadInt(); value.Add(key, package.ReadByteArray()); if (value.Count < num2) { return false; } configValueCache.Remove(text2); package = new ZPackage(value.Values.SelectMany((byte[] a) => a).ToArray()); b = package.ReadByte(); } ProcessingServerUpdate = true; if ((b & 4u) != 0) { byte[] buffer = package.ReadByteArray(); MemoryStream stream = new MemoryStream(buffer); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) { deflateStream.CopyTo(memoryStream); } package = new ZPackage(memoryStream.ToArray()); b = package.ReadByte(); } if ((b & 1) == 0) { resetConfigsFromServer(); } ParsedConfigs parsedConfigs = ReadConfigsFromPackage(package); foreach (KeyValuePair<OwnConfigEntryBase, object> configValue in parsedConfigs.configValues) { if (!isServer && configValue.Key.LocalBaseValue == null) { configValue.Key.LocalBaseValue = configValue.Key.BaseConfig.BoxedValue; } configValue.Key.BaseConfig.BoxedValue = configValue.Value; } foreach (KeyValuePair<CustomSyncedValueBase, object> customValue in parsedConfigs.customValues) { if (!isServer) { CustomSyncedValueBase key2 = customValue.Key; if (key2.LocalBaseValue == null) { key2.LocalBaseValue = customValue.Key.BoxedValue; } } customValue.Key.BoxedValue = customValue.Value; } Debug.Log((object)string.Format("Received {0} configs and {1} custom values from {2} for mod {3}", parsedConfigs.configValues.Count, parsedConfigs.customValues.Count, (isServer || clientUpdate) ? $"client {sender}" : "the server", DisplayName ?? Name)); if (!isServer) { serverLockedSettingChanged(); } return true; } finally { ProcessingServerUpdate = false; } } private ParsedConfigs ReadConfigsFromPackage(ZPackage package) { ParsedConfigs parsedConfigs = new ParsedConfigs(); Dictionary<string, OwnConfigEntryBase> dictionary = allConfigs.Where((OwnConfigEntryBase c) => c.SynchronizedConfig).ToDictionary((OwnConfigEntryBase c) => c.BaseConfig.Definition.Section + "_" + c.BaseConfig.Definition.Key, (OwnConfigEntryBase c) => c); Dictionary<string, CustomSyncedValueBase> dictionary2 = allCustomValues.ToDictionary((CustomSyncedValueBase c) => c.Identifier, (CustomSyncedValueBase c) => c); int num = package.ReadInt(); for (int i = 0; i < num; i++) { string text = package.ReadString(); string text2 = package.ReadString(); string text3 = package.ReadString(); Type type = Type.GetType(text3); if (text3 == "" || type != null) { object obj; try { obj = ((text3 == "") ? null : ReadValueWithTypeFromZPackage(package, type)); } catch (InvalidDeserializationTypeException ex) { Debug.LogWarning((object)("Got unexpected struct internal type " + ex.received + " for field " + ex.field + " struct " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + ex.expected)); continue; } OwnConfigEntryBase value2; if (text == "Internal") { CustomSyncedValueBase value; if (text2 == "serverversion") { if (obj?.ToString() != CurrentVersion) { Debug.LogWarning((object)("Received server version is not equal: server version = " + (obj?.ToString() ?? "null") + "; local version = " + (CurrentVersion ?? "unknown"))); } } else if (text2 == "lockexempt") { if (obj is bool flag) { lockExempt = flag; } } else if (dictionary2.TryGetValue(text2, out value)) { if ((text3 == "" && (!value.Type.IsValueType || Nullable.GetUnderlyingType(value.Type) != null)) || GetZPackageTypeString(value.Type) == text3) { parsedConfigs.customValues[value] = obj; continue; } Debug.LogWarning((object)("Got unexpected type " + text3 + " for internal value " + text2 + " for mod " + (DisplayName ?? Name) + ", expecting " + value.Type.AssemblyQualifiedName)); } } else if (dictionary.TryGetValue(text + "_" + text2, out value2)) { Type type2 = configType(value2.BaseConfig); if ((text3 == "" && (!type2.IsValueType || Nullable.GetUnderlyingType(type2) != null)) || GetZPackageTypeString(type2) == text3) { parsedConfigs.configValues[value2] = obj; continue; } Debug.LogWarning((object)("Got unexpected type " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + type2.AssemblyQualifiedName)); } else { Debug.LogWarning((object)("Received unknown config entry " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ". This may happen if client and server versions of the mod do not match.")); } continue; } Debug.LogWarning((object)("Got invalid type " + text3 + ", abort reading of received configs")); return new ParsedConfigs(); } return parsedConfigs; } private static bool isWritableConfig(OwnConfigEntryBase config) { OwnConfigEntryBase config2 = config; ConfigSync configSync = configSyncs.FirstOrDefault((ConfigSync cs) => cs.allConfigs.Contains(config2)); if (configSync == null) { return true; } return configSync.IsSourceOfTruth || !config2.SynchronizedConfig || config2.LocalBaseValue == null || (!configSync.IsLocked && (config2 != configSync.lockedConfig || lockExempt)); } private void serverLockedSettingChanged() { foreach (OwnConfigEntryBase allConfig in allConfigs) { configAttribute<ConfigurationManagerAttributes>(allConfig.BaseConfig).ReadOnly = !isWritableConfig(allConfig); } } private void resetConfigsFromServer() { foreach (OwnConfigEntryBase item in allConfigs.Where((OwnConfigEntryBase config) => config.LocalBaseValue != null)) { item.BaseConfig.BoxedValue = item.LocalBaseValue; item.LocalBaseValue = null; } foreach (CustomSyncedValueBase item2 in allCustomValues.Where((CustomSyncedValueBase config) => config.LocalBaseValue != null)) { item2.BoxedValue = item2.LocalBaseValue; item2.LocalBaseValue = null; } lockedConfigChanged -= serverLockedSettingChanged; serverLockedSettingChanged(); } private IEnumerator<bool> distributeConfigToPeers(ZNetPeer peer, ZPackage package) { ZNetPeer peer2 = peer; ZRoutedRpc rpc = ZRoutedRpc.instance; if (rpc == null) { yield break; } byte[] data = package.GetArray(); if (data != null && data.LongLength > 250000) { int fragments = (int)(1 + (data.LongLength - 1) / 250000); long packageIdentifier = ++packageCounter; int fragment = 0; while (fragment < fragments) { foreach (bool item in waitForQueue()) { yield return item; } if (peer2.m_socket.IsConnected()) { ZPackage fragmentedPackage = new ZPackage(); fragmentedPackage.Write((byte)2); fragmentedPackage.Write(packageIdentifier); fragmentedPackage.Write(fragment); fragmentedPackage.Write(fragments); fragmentedPackage.Write(data.Skip(250000 * fragment).Take(250000).ToArray()); SendPackage(fragmentedPackage); if (fragment != fragments - 1) { yield return true; } int num = fragment + 1; fragment = num; continue; } break; } yield break; } foreach (bool item2 in waitForQueue()) { yield return item2; } SendPackage(package); void SendPackage(ZPackage pkg) { string text = Name + " ConfigSync"; if (isServer) { peer2.m_rpc.Invoke(text, new object[1] { pkg }); } else { rpc.InvokeRoutedRPC(peer2.m_server ? 0 : peer2.m_uid, text, new object[1] { pkg }); } } IEnumerable<bool> waitForQueue() { float timeout = Time.time + 30f; while (peer2.m_socket.GetSendQueueSize() > 20000) { if (Time.time > timeout) { Debug.Log((object)$"Disconnecting {peer2.m_uid} after 30 seconds config sending timeout"); peer2.m_rpc.Invoke("Error", new object[1] { (object)(ConnectionStatus)5 }); ZNet.instance.Disconnect(peer2); break; } yield return false; } } } private IEnumerator sendZPackage(long target, ZPackage package) { if (!Object.op_Implicit((Object)(object)ZNet.instance)) { return Enumerable.Empty<object>().GetEnumerator(); } List<ZNetPeer> list = (List<ZNetPeer>)AccessTools.DeclaredField(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance); if (target != ZRoutedRpc.Everybody) { list = list.Where((ZNetPeer p) => p.m_uid == target).ToList(); } return sendZPackage(list, package); } private IEnumerator sendZPackage(List<ZNetPeer> peers, ZPackage package) { ZPackage package2 = package; if (!Object.op_Implicit((Object)(object)ZNet.instance)) { yield break; } byte[] rawData = package2.GetArray(); if (rawData != null && rawData.LongLength > 10000) { ZPackage compressedPackage = new ZPackage(); compressedPackage.Write((byte)4); MemoryStream output = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(output, CompressionLevel.Optimal)) { deflateStream.Write(rawData, 0, rawData.Length); } compressedPackage.Write(output.ToArray()); package2 = compressedPackage; } List<IEnumerator<bool>> writers = (from peer in peers where peer.IsReady() select peer into p select distributeConfigToPeers(p, package2)).ToList(); writers.RemoveAll((IEnumerator<bool> writer) => !writer.MoveNext()); while (writers.Count > 0) { yield return null; writers.RemoveAll((IEnumerator<bool> writer) => !writer.MoveNext()); } } private void Broadcast(long target, params ConfigEntryBase[] configs) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(configs); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(sendZPackage(target, package)); } } } private void Broadcast(long target, params CustomSyncedValueBase[] customValues) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(null, customValues); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(sendZPackage(target, package)); } } } private static OwnConfigEntryBase? configData(ConfigEntryBase config) { return config.Description.Tags?.OfType<OwnConfigEntryBase>().SingleOrDefault(); } public static SyncedConfigEntry<T>? ConfigData<T>(ConfigEntry<T> config) { return ((ConfigEntryBase)config).Description.Tags?.OfType<SyncedConfigEntry<T>>().SingleOrDefault(); } private static T configAttribute<T>(ConfigEntryBase config) { return config.Description.Tags.OfType<T>().First(); } private static Type configType(ConfigEntryBase config) { return configType(config.SettingType); } private static Type configType(Type type) { return type.IsEnum ? Enum.GetUnderlyingType(type) : type; } private static ZPackage ConfigsToPackage(IEnumerable<ConfigEntryBase>? configs = null, IEnumerable<CustomSyncedValueBase>? customValues = null, IEnumerable<PackageEntry>? packageEntries = null, bool partial = true) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown List<ConfigEntryBase> list = configs?.Where((ConfigEntryBase config) => configData(config).SynchronizedConfig).ToList() ?? new List<ConfigEntryBase>(); List<CustomSyncedValueBase> list2 = customValues?.ToList() ?? new List<CustomSyncedValueBase>(); ZPackage val = new ZPackage(); val.Write((byte)(partial ? 1 : 0)); val.Write(list.Count + list2.Count + (packageEntries?.Count() ?? 0)); foreach (PackageEntry item in packageEntries ?? Array.Empty<PackageEntry>()) { AddEntryToPackage(val, item); } foreach (CustomSyncedValueBase item2 in list2) { AddEntryToPackage(val, new PackageEntry { section = "Internal", key = item2.Identifier, type = item2.Type, value = item2.BoxedValue }); } foreach (ConfigEntryBase item3 in list) { AddEntryToPackage(val, new PackageEntry { section = item3.Definition.Section, key = item3.Definition.Key, type = configType(item3), value = item3.BoxedValue }); } return val; } private static void AddEntryToPackage(ZPackage package, PackageEntry entry) { package.Write(entry.section); package.Write(entry.key); package.Write((entry.value == null) ? "" : GetZPackageTypeString(entry.type)); AddValueToZPackage(package, entry.value); } private static string GetZPackageTypeString(Type type) { return type.AssemblyQualifiedName; } private static void AddValueToZPackage(ZPackage package, object? value) { Type type = value?.GetType(); if (value is Enum) { value = ((IConvertible)value).ToType(Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture); } else { if (value is ICollection collection) { package.Write(collection.Count); { foreach (object item in collection) { AddValueToZPackage(package, item); } return; } } if ((object)type != null && type.IsValueType && !type.IsPrimitive) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); package.Write(fields.Length); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { package.Write(GetZPackageTypeString(fieldInfo.FieldType)); AddValueToZPackage(package, fieldInfo.GetValue(value)); } return; } } ZRpc.Serialize(new object[1] { value }, ref package); } private static object ReadValueWithTypeFromZPackage(ZPackage package, Type type) { if ((object)type != null && type.IsValueType && !type.IsPrimitive && !type.IsEnum) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); int num = package.ReadInt(); if (num != fields.Length) { throw new InvalidDeserializationTypeException { received = $"(field count: {num})", expected = $"(field count: {fields.Length})" }; } object uninitializedObject = FormatterServices.GetUninitializedObject(type); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { string text = package.ReadString(); if (text != GetZPackageTypeString(fieldInfo.FieldType)) { throw new InvalidDeserializationTypeException { received = text, expected = GetZPackageTypeString(fieldInfo.FieldType), field = fieldInfo.Name }; } fieldInfo.SetValue(uninitializedObject, ReadValueWithTypeFromZPackage(package, fieldInfo.FieldType)); } return uninitializedObject; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<, >)) { int num2 = package.ReadInt(); IDictionary dictionary = (IDictionary)Activator.CreateInstance(type); Type type2 = typeof(KeyValuePair<, >).MakeGenericType(type.GenericTypeArguments); FieldInfo field = type2.GetField("key", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field2 = type2.GetField("value", BindingFlags.Instance | BindingFlags.NonPublic); for (int j = 0; j < num2; j++) { object obj = ReadValueWithTypeFromZPackage(package, type2); dictionary.Add(field.GetValue(obj), field2.GetValue(obj)); } return dictionary; } if (type != typeof(List<string>) && type.IsGenericType) { Type type3 = typeof(ICollection<>).MakeGenericType(type.GenericTypeArguments[0]); if ((object)type3 != null && type3.IsAssignableFrom(type)) { int num3 = package.ReadInt(); object obj2 = Activator.CreateInstance(type); MethodInfo method = type3.GetMethod("Add"); for (int k = 0; k < num3; k++) { method.Invoke(obj2, new object[1] { ReadValueWithTypeFromZPackage(package, type.GenericTypeArguments[0]) }); } return obj2; } } ParameterInfo parameterInfo = (ParameterInfo)FormatterServices.GetUninitializedObject(typeof(ParameterInfo)); AccessTools.DeclaredField(typeof(ParameterInfo), "ClassImpl").SetValue(parameterInfo, type); List<object> source = new List<object>(); ZRpc.Deserialize(new ParameterInfo[2] { null, parameterInfo }, package, ref source); return source.First(); } } [PublicAPI] [HarmonyPatch] internal class VersionCheck { private static readonly HashSet<VersionCheck> versionChecks; private static readonly Dictionary<string, string> notProcessedNames; public string Name; private string? displayName; private string? currentVersion; private string? minimumRequiredVersion; public bool ModRequired = true; private string? ReceivedCurrentVersion; private string? ReceivedMinimumRequiredVersion; private readonly List<ZRpc> ValidatedClients = new List<ZRpc>(); private ConfigSync? ConfigSync; public string DisplayName { get { return displayName ?? Name; } set { displayName = value; } } public string CurrentVersion { get { return currentVersion ?? "0.0.0"; } set { currentVersion = value; } } public string MinimumRequiredVersion { get { return minimumRequiredVersion ?? (ModRequired ? CurrentVersion : "0.0.0"); } set { minimumRequiredVersion = value; } } private static void PatchServerSync() { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown Patches patchInfo = PatchProcessor.GetPatchInfo((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "Awake", (Type[])null, (Type[])null)); if (patchInfo != null && patchInfo.Postfixes.Count((Patch p) => p.PatchMethod.DeclaringType == typeof(ConfigSync.RegisterRPCPatch)) > 0) { return; } Harmony val = new Harmony("org.bepinex.helpers.ServerSync"); foreach (Type item in from t in typeof(ConfigSync).GetNestedTypes(BindingFlags.NonPublic).Concat(new Type[1] { typeof(VersionCheck) }) where t.IsClass select t) { val.PatchAll(item); } } static VersionCheck() { versionChecks = new HashSet<VersionCheck>(); notProcessedNames = new Dictionary<string, string>(); typeof(ThreadingHelper).GetMethod("StartSyncInvoke").Invoke(ThreadingHelper.Instance, new object[1] { new Action(PatchServerSync) }); } public VersionCheck(string name) { Name = name; ModRequired = true; versionChecks.Add(this); } public VersionCheck(ConfigSync configSync) { ConfigSync = configSync; Name = ConfigSync.Name; versionChecks.Add(this); } public void Initialize() { ReceivedCurrentVersion = null; ReceivedMinimumRequiredVersion = null; if (ConfigSync != null) { Name = ConfigSync.Name; DisplayName = ConfigSync.DisplayName; CurrentVersion = ConfigSync.CurrentVersion; MinimumRequiredVersion = ConfigSync.MinimumRequiredVersion; ModRequired = ConfigSync.ModRequired; } } private bool IsVersionOk() { if (ReceivedMinimumRequiredVersion == null || ReceivedCurrentVersion == null) { return !ModRequired; } bool flag = new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion); bool flag2 = new Version(ReceivedCurrentVersion) >= new Version(MinimumRequiredVersion); return flag && flag2; } private string ErrorClient() { if (ReceivedMinimumRequiredVersion == null) { return "Mod " + DisplayName + " must not be installed."; } return (new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion)) ? ("Mod " + DisplayName + " requires maximum " + ReceivedCurrentVersion + ". Installed is version " + CurrentVersion + ".") : ("Mod " + DisplayName + " requires minimum " + ReceivedMinimumRequiredVersion + ". Installed is version " + CurrentVersion + "."); } private string ErrorServer(ZRpc rpc) { return "Disconnect: The client (" + rpc.GetSocket().GetHostName() + ") doesn't have the correct " + DisplayName + " version " + MinimumRequiredVersion; } private string Error(ZRpc? rpc = null) { return (rpc == null) ? ErrorClient() : ErrorServer(rpc); } private static VersionCheck[] GetFailedClient() { return versionChecks.Where((VersionCheck check) => !check.IsVersionOk()).ToArray(); } private static VersionCheck[] GetFailedServer(ZRpc rpc) { ZRpc rpc2 = rpc; return versionChecks.Where((VersionCheck check) => check.ModRequired && !check.ValidatedClients.Contains(rpc2)).ToArray(); } private static void Logout() { Game.instance.Logout(true, true); AccessTools.DeclaredField(typeof(ZNet), "m_connectionStatus").SetValue(null, (object)(ConnectionStatus)3); } private static void Disconn