Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Dyrr v1.4.2
plugins/Dyrr/Dyrr.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using Ezomic.Core; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")] [assembly: AssemblyCompany("Thijssen Software")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 Robbin Thijssen")] [assembly: AssemblyDescription("A door policy: characters that have played elsewhere do not come in.")] [assembly: AssemblyFileVersion("1.4.2.0")] [assembly: AssemblyInformationalVersion("1.4.2+f391ec6d61cce04927ddb8fb8b27cb8f9824a04d")] [assembly: AssemblyProduct("Dyrr")] [assembly: AssemblyTitle("Dyrr")] [assembly: AssemblyVersion("1.4.2.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Dyrr { internal static class CharacterNote { private static FieldInfo _fileSource; private static FieldInfo _name; private static PropertyInfo _text; private static GameObject _line; private static Component _lineText; private const float Margin = 0.35f; [HarmonyPostfix] [HarmonyPatch(typeof(FejdStartup), "UpdateCharacterList")] private static void Annotate(FejdStartup __instance) { if (!DyrrConfig.ProtectCharacter.Value) { return; } Component val = Line(__instance); if (!((Object)(object)val == (Object)null)) { PlayerProfile val2 = MenuGuard.SelectedProfile(__instance); long num = ((val2 != null) ? Home.IdOf(val2) : 0); if (num == 0L) { _line.SetActive(false); return; } string value = Note(val2, num); _text.SetValue(val, value, null); _line.SetActive(true); } } private static string Note(PlayerProfile profile, long id) { Home.Binding binding = Home.GetBinding(id); if (binding.World == 0L) { List<long> list = Facts.WorldsOf(profile); if (list.Count > 1) { return "Has played in " + Listed(Names(profile, list)); } if (list.Count == 0) { return "Not bound to a world yet"; } Home.Bind(id, profile.GetName(), list[0], WorldName(list[0], profile)); binding = Home.GetBinding(id); if (binding.World == 0L) { return "Not bound to a world yet"; } } if (!string.IsNullOrEmpty(binding.WorldName)) { return "Belongs to " + binding.WorldName; } string text = WorldName(binding.World, profile); if (!string.IsNullOrEmpty(text)) { Home.Name(id, text); return "Belongs to " + text; } return "Belongs to a world that is not on this PC"; } private static List<string> Names(PlayerProfile profile, List<long> worlds) { List<string> list = new List<string>(); try { List<World> worldList = SaveSystem.GetWorldList(); if (worldList != null) { foreach (long world in worlds) { foreach (World item in worldList) { if (item != null && item.m_uid == world && !list.Contains(item.m_name)) { list.Add(item.m_name); break; } } } } PlayerStats val = Facts.Totals(profile); if (list.Count < worlds.Count && val != null && val.m_knownWorlds != null) { foreach (KeyValuePair<string, float> knownWorld in val.m_knownWorlds) { if (list.Count < worlds.Count) { if (!list.Contains(knownWorld.Key)) { list.Add(knownWorld.Key); } continue; } break; } } } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not name a character's worlds: " + ex.Message)); } for (int i = list.Count; i < worlds.Count; i++) { list.Add(""); } return list; } private static string Listed(List<string> names) { List<string> list = new List<string>(); int num = 0; foreach (string name in names) { if (string.IsNullOrEmpty(name)) { num++; } else { list.Add(name); } } if (list.Count == 0) { return names.Count + " worlds, none of them on this PC"; } int num2 = Math.Min(list.Count, 3); string text = ""; for (int i = 0; i < num2; i++) { if (i > 0) { text += ((i == num2 - 1 && list.Count == num2 && num == 0) ? " and " : ", "); } text += list[i]; } int num3 = list.Count - num2 + num; if (num3 > 0) { text = text + " and " + num3 + " more"; } return text; } private static string WorldName(long uid, PlayerProfile profile) { try { List<World> worldList = SaveSystem.GetWorldList(); if (worldList != null) { foreach (World item in worldList) { if (item != null && item.m_uid == uid) { return item.m_name; } } } } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not read the world list: " + ex.Message)); } try { PlayerStats val = Facts.Totals(profile); if (val != null && val.m_knownWorlds != null && val.m_knownWorlds.Count == 1 && Facts.WorldsOf(profile).Count == 1) { using Dictionary<string, float>.Enumerator enumerator2 = val.m_knownWorlds.GetEnumerator(); if (enumerator2.MoveNext()) { return enumerator2.Current.Key; } } } catch (Exception ex2) { DyrrPlugin.Log.LogWarning((object)("Could not read a character's world names: " + ex2.Message)); } return ""; } private static Component Line(FejdStartup fejd) { //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Expected O, but got Unknown if ((Object)(object)_line != (Object)null && (Object)(object)_lineText != (Object)null) { return _lineText; } if (_fileSource == null) { _fileSource = AccessTools.Field(typeof(FejdStartup), "m_csFileSource"); } Component val = (Component)((_fileSource != null) ? /*isinst with value type is only supported in some contexts*/: null); if ((Object)(object)val == (Object)null) { return null; } if (_text == null) { _text = AccessTools.Property(((object)val).GetType(), "text"); } if (_text == null) { return null; } _line = Object.Instantiate<GameObject>(val.gameObject, val.transform.parent); ((Object)_line).name = "DyrrHome"; List<GameObject> list = new List<GameObject>(); foreach (Transform item in _line.transform) { Transform val2 = item; list.Add(((Component)val2).gameObject); } foreach (GameObject item2 in list) { Object.Destroy((Object)(object)item2); } Place(fejd, val); _lineText = _line.GetComponent(((object)val).GetType()); if ((Object)(object)_lineText == (Object)null) { Object.Destroy((Object)(object)_line); _line = null; return null; } return _lineText; } private static void Place(FejdStartup fejd, Component donor) { //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) Transform transform = _line.transform; RectTransform val = (RectTransform)(object)((transform is RectTransform) ? transform : null); Transform transform2 = donor.transform; RectTransform val2 = (RectTransform)(object)((transform2 is RectTransform) ? transform2 : null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val2 == (Object)null)) { if (_name == null) { _name = AccessTools.Field(typeof(FejdStartup), "m_csName"); } Component val3 = (Component)((_name != null) ? /*isinst with value type is only supported in some contexts*/: null); RectTransform val4 = (RectTransform)(((Object)(object)val3 != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null); if ((Object)(object)val4 == (Object)null || (Object)(object)((Transform)val4).parent != (Object)(object)((Transform)val2).parent) { val4 = val2; } float x = val4.anchoredPosition.x; float y = val4.anchoredPosition.y; Rect rect = val4.rect; float num = y + ((Rect)(ref rect)).height * 0.5f; rect = val.rect; val.anchoredPosition = new Vector2(x, num + ((Rect)(ref rect)).height * 0.85f); } } } internal static class CheatCommands { private static readonly HashSet<string> Ripped = new HashSet<string> { "addstatus", "adrenaline", "aggravate", "beard", "catch", "clearstatus", "cr", "debugmode", "dpsdebug", "env", "event", "exploremap", "ffsmooth", "find", "findtp", "fly", "forcedelete", "freefly", "gc", "ghost", "god", "goto", "hair", "heal", "itemset", "killall", "killenemies", "killenemycreatures", "killtame", "listkeys", "location", "model", "nextseed", "nocost", "nospawn", "players", "pos", "printcreatures", "printlocations", "printnetobj", "puke", "raiseskill", "randomevent", "recall", "removebirds", "removedrops", "removefish", "removekey", "removekeyplayer", "resetcharacter", "resetenv", "resetkeys", "resetmap", "resetskill", "resetwind", "respawntime", "setfuel", "setkey", "setkeyplayer", "setpower", "skiptime", "sleep", "stopevent", "stopfire", "stopsmoke", "tame", "test", "time", "timescale", "tod", "tombstone", "vegetation", "wind" }; private static FieldInfo _table; private static bool _warned; internal static List<string> Among(IEnumerable<string> reported) { List<string> list = new List<string>(); if (reported == null) { return list; } HashSet<string> hashSet = Live(); foreach (string item2 in reported) { if (!string.IsNullOrEmpty(item2)) { string item = item2.Trim().ToLowerInvariant(); if ((hashSet?.Contains(item) ?? Ripped.Contains(item)) && !list.Contains(item)) { list.Add(item); } } } return list; } private static HashSet<string> Live() { if (_table == null) { _table = AccessTools.Field(typeof(Terminal), "commands"); } if (_table == null) { return Fallback("Terminal.commands was not found at all, which means the game has changed - only cheat commands Dyrr already knew about are seen", wrong: true); } IDictionary dictionary; try { dictionary = _table.GetValue(null) as IDictionary; } catch (Exception ex) { return Fallback("the command table could not be read (" + ex.Message + ")", wrong: true); } if (dictionary == null || dictionary.Count == 0) { return Fallback("no console has been created in this process yet, which is ordinary on a dedicated server", wrong: false); } HashSet<string> hashSet = new HashSet<string>(); foreach (DictionaryEntry item in dictionary) { object? value = item.Value; ConsoleCommand val = (ConsoleCommand)((value is ConsoleCommand) ? value : null); if (val != null && val.IsCheat) { string text = item.Key as string; if (!string.IsNullOrEmpty(text)) { hashSet.Add(text.ToLowerInvariant()); } } } if (hashSet.Count != 0) { return hashSet; } return Fallback("the command table holds no cheat commands", wrong: true); } private static HashSet<string> Fallback(string why, bool wrong) { if (!_warned) { _warned = true; string text = "Classifying cheat commands from the list built into Dyrr rather than from the game's own table, because " + why + "."; if (wrong) { DyrrPlugin.Log.LogWarning((object)text); } else { DyrrPlugin.Log.LogInfo((object)text); } } return null; } } internal static class Commands { [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__Run; } private static bool _registered; [HarmonyPostfix] [HarmonyPatch(typeof(Terminal), "InitTerminal")] private static void Register() { //IL_003d: 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) //IL_0033: Expected O, but got Unknown if (!_registered) { _registered = true; object obj = <>O.<0>__Run; if (obj == null) { ConsoleEvent val = Run; <>O.<0>__Run = val; obj = (object)val; } new ConsoleCommand("dyrr", "Dyrr: what the door is doing. 'dyrr home' lists this machine's character bindings, 'dyrr forget <id>' unbinds one.", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } } private static void Run(ConsoleEventArgs args) { string text = ((args.Length > 1) ? args[1].ToLowerInvariant() : ""); if (text == null || text.Length != 0) { switch (text) { case "status": break; case "home": Say(args, Bindings()); return; case "forget": Say(args, ForgetOne(args)); return; default: Say(args, "dyrr - what the door is doing here\ndyrr home - which world each character on this machine belongs to\ndyrr forget <id> - unbind a character so it may be taken anywhere"); return; } } Say(args, Status()); } private static void Say(ConsoleEventArgs args, string text) { if ((Object)(object)args.Context != (Object)null) { args.Context.AddString(text); } if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { DyrrPlugin.Log.LogInfo((object)text); } } private static string Status() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Dyrr ").Append("1.4.2"); if (!DyrrPlugin.CorePresent) { stringBuilder.Append(" (standalone - no Core)"); } stringBuilder.Append('\n'); if ((Object)(object)ZNet.instance == (Object)null) { stringBuilder.Append("Not in a world. The door only has anything to say on a server."); return stringBuilder.ToString(); } if (!ZNet.instance.IsServer()) { stringBuilder.Append("This is a client, so the door is the server's. "); stringBuilder.Append((Doorman.LastRefusal == null) ? "Nothing has been refused to this machine this session." : ("Last refusal: " + Doorman.LastRefusal)); stringBuilder.Append("\n\n").Append(Bindings()); return stringBuilder.ToString(); } stringBuilder.Append("World: '").Append(ZNet.instance.GetWorldName()).Append("' (") .Append(ZNet.instance.GetWorldUID()) .Append(")\n"); if (!DyrrConfig.Enabled.Value) { stringBuilder.Append("Enabled is off - the door is not judging anything."); return stringBuilder.ToString(); } stringBuilder.Append(DyrrConfig.Enforce.Value ? "Enforce is ON - a character that fails a check does not come in.\n" : "Enforce is OFF - failures are reported here and refused to nobody.\n").Append("Checks: other worlds ").Append(On(DyrrConfig.RefuseOtherWorlds.Value)) .Append(", cheats ") .Append(On(DyrrConfig.RefuseCheats.Value)) .Append(", cheat commands ") .Append(On(DyrrConfig.RefuseCheatCommands.Value)) .Append(", tampered ") .Append(On(DyrrConfig.RefuseTampered.Value)) .Append(", unreported ") .Append(On(DyrrConfig.RefuseUnreported.Value)) .Append('\n'); stringBuilder.Append("Mods: "); string text = Mods.Tier(); if (text == "Off") { stringBuilder.Append("not checked\n"); } else { bool flag = DyrrConfig.ModPolicy.Value.Trim().Equals("Deny", StringComparison.OrdinalIgnoreCase); stringBuilder.Append(flag ? "Deny" : "Allow").Append(" (").Append(text) .Append(") - ") .Append(flag ? Mods.Own().Count : Mods.Permitted().Count) .Append(flag ? " run here, plus DeniedMods" : " permitted here") .Append("\n"); } stringBuilder.Append("Refused so far this session: ").Append(Doorman.RefusedCount).Append("\n\n"); List<ZNetPeer> peers = ZNet.instance.GetPeers(); int num = 0; if (peers != null) { foreach (ZNetPeer item in peers) { if (item != null && item.m_rpc != null && Doorman.TryVerdict(item.m_rpc, out var why)) { num++; string value = (string.IsNullOrEmpty(item.m_playerName) ? "(unnamed)" : item.m_playerName); stringBuilder.Append(" ").Append(value).Append(" "); if (why == null) { stringBuilder.Append("admitted"); } else { stringBuilder.Append(DyrrConfig.Enforce.Value ? "refused: " : "would refuse: ").Append(why); } stringBuilder.Append('\n'); } } } if (num == 0) { stringBuilder.Append(" nobody connected has been judged by this door"); } return stringBuilder.ToString(); } private static string Bindings() { StringBuilder stringBuilder = new StringBuilder("Character bindings on this machine:\n"); bool flag = false; foreach (KeyValuePair<long, Home.Binding> item in Home.All()) { flag = true; string value = (string.IsNullOrEmpty(item.Value.Character) ? "(unnamed)" : item.Value.Character); stringBuilder.Append(" ").Append(value).Append(" (id ") .Append(item.Key) .Append(") -> ") .Append(Home.Describe(item.Value)) .Append('\n'); } if (!flag) { stringBuilder.Append(" none yet - a character is bound the first time it plays in a world"); } return stringBuilder.ToString(); } private static string ForgetOne(ConsoleEventArgs args) { if (args.Length < 3 || !long.TryParse(args[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return "dyrr forget <id> - the id is the number in brackets in 'dyrr home'."; } Home.Binding binding = Home.GetBinding(result); if (binding.World == 0L) { return "No character with id " + result + " is bound to anything."; } if (!Home.Forget(result)) { return "Could not unbind " + result + "."; } DyrrPlugin.Log.LogWarning((object)("Unbound character " + result + " from world " + Home.Describe(binding) + " on request.")); return "Unbound " + result + " from world " + Home.Describe(binding) + ".\nIt may now be taken into any world - and the first one it plays in becomes its new home. Nothing about where it has already been is undone by this; the game's own record of that is permanent and no mod can clear it."; } private static string On(bool value) { if (!value) { return "off"; } return "on"; } } internal static class Doorman { private struct Report { internal bool Readable; internal int Format; internal bool Cheats; internal string Name; internal float CheatStat; internal int KnownWorlds; internal List<string> Commands; internal bool WorldsRead; internal List<long> Worlds; internal List<string> Plugins; } private const string RpcFacts = "Dyrr_Facts"; internal const string RpcRefused = "Dyrr_Refused"; private static readonly Dictionary<ZRpc, Report> Received = new Dictionary<ZRpc, Report>(); private static readonly Dictionary<ZRpc, string> Verdicts = new Dictionary<ZRpc, string>(); internal static int RefusedCount; internal static string LastRefusal; internal static bool TryVerdict(ZRpc rpc, out string why) { return Verdicts.TryGetValue(rpc, out why); } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static void Handshake(ZNetPeer peer) { peer.m_rpc.Register<ZPackage>("Dyrr_Facts", (Action<ZRpc, ZPackage>)Receive); peer.m_rpc.Register<ZPackage>("Dyrr_Inv", (Action<ZRpc, ZPackage>)Inventories.Receive); peer.m_rpc.Register<string>("Dyrr_Refused", (Action<ZRpc, string>)OnRefused); peer.m_rpc.Invoke("Dyrr_Facts", new object[1] { Facts.Gather() }); } private static void Receive(ZRpc rpc, ZPackage pkg) { Report value = new Report { Worlds = new List<long>(), Commands = new List<string>(), Plugins = new List<string>() }; try { value.Readable = pkg.ReadBool(); if (value.Readable) { value.Format = pkg.ReadInt(); if (value.Format != 4) { DyrrPlugin.Log.LogWarning((object)("A client reported facts in format " + value.Format + ", and this build speaks " + 4 + ". Treating it as unreported. Install Core to have builds checked before it gets this far.")); value.Readable = false; Received[rpc] = value; return; } value.Name = pkg.ReadString(); value.Cheats = pkg.ReadBool(); value.CheatStat = pkg.ReadSingle(); value.KnownWorlds = pkg.ReadInt(); int num = pkg.ReadInt(); for (int i = 0; i < num; i++) { value.Commands.Add(pkg.ReadString()); } value.WorldsRead = pkg.ReadBool(); int num2 = pkg.ReadInt(); for (int j = 0; j < num2; j++) { value.Worlds.Add(pkg.ReadLong()); } int num3 = pkg.ReadInt(); for (int k = 0; k < num3; k++) { value.Plugins.Add(pkg.ReadString()); } } } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not read a client's report: " + ex.Message)); value.Readable = false; } Received[rpc] = value; } private static string Who(ZRpc rpc, bool heard, Report report) { string text = ((heard && !string.IsNullOrEmpty(report.Name)) ? report.Name : null); string text2 = null; try { ISocket val = ((rpc == null) ? null : rpc.GetSocket()); if (val != null) { text2 = val.GetHostName(); } } catch (Exception) { } if (!string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(text2)) { return text + " (" + text2 + ")"; } if (!string.IsNullOrEmpty(text)) { return text; } if (!string.IsNullOrEmpty(text2)) { return text2; } return "someone"; } [HarmonyPrefix] [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private static bool Judge(ZNet __instance, ZRpc rpc) { if (!DyrrConfig.Enabled.Value) { return true; } if (!__instance.IsServer()) { return true; } Report value; bool flag = Received.TryGetValue(rpc, out value); if (flag) { LogWhatTheyBrought(value); } string text = Verdict(__instance, flag, value); Verdicts[rpc] = text; if (text == null) { return true; } string text2 = Who(rpc, flag, value); if (!DyrrConfig.Enforce.Value) { DyrrPlugin.Log.LogWarning((object)("Would have refused a connection: " + text2 + " " + text)); return true; } DyrrPlugin.Log.LogWarning((object)("Refused a connection: " + text2 + " " + text)); RefusedCount++; rpc.Invoke("Dyrr_Refused", new object[1] { DyrrConfig.RefusedMessage.Value + " It " + text + "." }); rpc.Invoke("Error", new object[1] { 12 }); return false; } private static string Verdict(ZNet net, bool heard, Report report) { if (!heard) { if (!DyrrConfig.RefuseUnreported.Value) { return null; } return "this client did not report its character"; } if (!report.Readable) { if (!DyrrConfig.RefuseUnreported.Value) { return null; } return "this character's profile could not be read"; } StringBuilder stringBuilder = new StringBuilder(); if (!report.WorldsRead && (DyrrConfig.RefuseOtherWorlds.Value || DyrrConfig.RefuseTampered.Value)) { DyrrPlugin.Log.LogWarning((object)((string.IsNullOrEmpty(report.Name) ? "A client" : report.Name) + " could not read its own travel record, so this connection is judged without the travel rules - neither refused for having played elsewhere nor for an altered travel record. That client's own log names what was found in place of PlayerProfile.m_worldData.")); } if (DyrrConfig.RefuseOtherWorlds.Value) { long worldUID = net.GetWorldUID(); int num = 0; foreach (long world in report.Worlds) { if (world != worldUID) { num++; } } if (num > 0) { stringBuilder.Append("has played on ").Append(num).Append(" other world(s)"); } } if (DyrrConfig.RefuseCheats.Value && report.Cheats) { Also(stringBuilder, "is flagged as having used cheats"); } if (DyrrConfig.RefuseCheatCommands.Value) { List<string> list = CheatCommands.Among(report.Commands); if (list.Count > 0) { Also(stringBuilder, "has run " + Listed(list, "cheat command")); } } if (DyrrConfig.RefuseTampered.Value) { if (!report.Cheats && report.CheatStat > 0f) { Also(stringBuilder, "says it has never cheated but carries a cheat count of " + report.CheatStat + " - that record has been altered"); } if (report.WorldsRead && report.KnownWorlds > report.Worlds.Count) { Also(stringBuilder, "has played in " + report.KnownWorlds + " world(s) by its own history but admits to " + report.Worlds.Count + " - its travel record has been altered"); } } string text = Mods.Tier(); if (text != "Off") { string text2 = Mods.Judge(report.Plugins); if (text2 != null) { if (text == "Refuse") { Also(stringBuilder, text2); } else { DyrrPlugin.Log.LogWarning((object)("Mods notice: a client " + text2 + ". Admitted anyway - RefuseMods is Notice, not Refuse.")); } } } if (stringBuilder.Length != 0) { return stringBuilder.ToString(); } return null; } private static void LogWhatTheyBrought(Report report) { if (report.Plugins != null && report.Plugins.Count != 0) { List<string> list = Mods.Unexpected(report.Plugins); if (list.Count != 0) { DyrrPlugin.Log.LogInfo((object)("A client brought " + list.Count + " plugin(s) this server does not run or allow: " + string.Join(", ", list.ToArray()))); } } } private static void Also(StringBuilder reasons, string reason) { if (reasons.Length > 0) { reasons.Append(", and "); } reasons.Append(reason); } private static string Listed(List<string> items, string noun) { if (items.Count == 1) { return noun + " '" + items[0] + "'"; } int num = Math.Min(items.Count, 3); string text = items.Count + " " + noun + "s: "; for (int i = 0; i < num; i++) { if (i > 0) { text += ", "; } text = text + "'" + items[i] + "'"; } if (items.Count > num) { text = text + " and " + (items.Count - num) + " more"; } return text; } private static void OnRefused(ZRpc rpc, string why) { DyrrPlugin.Log.LogError((object)("This server refused this connection: " + why)); LastRefusal = why; DyrrPlugin.Explain(why); } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "Disconnect")] private static void Forget(ZNetPeer peer) { if (peer != null && peer.m_rpc != null) { Received.Remove(peer.m_rpc); Inventories.Forget(peer.m_rpc); Verdicts.Remove(peer.m_rpc); } } } internal static class DyrrConfig { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<bool> Enforce; internal static ConfigEntry<bool> RefuseOtherWorlds; internal static ConfigEntry<bool> RefuseCheats; internal static ConfigEntry<bool> RefuseCheatCommands; internal static ConfigEntry<bool> RefuseTampered; internal static ConfigEntry<string> RefuseMods; internal static ConfigEntry<bool> WatchInventories; internal static ConfigEntry<float> InventoryInterval; internal static ConfigEntry<int> InventoryDetail; internal static ConfigEntry<bool> InventoryNamesIds; internal static ConfigEntry<bool> KickIdle; internal static ConfigEntry<int> IdleMinutes; internal static ConfigEntry<int> IdleWarnMinutes; internal static ConfigEntry<string> ModPolicy; internal static ConfigEntry<string> AllowedMods; internal static ConfigEntry<string> DeniedMods; internal static ConfigEntry<bool> RefuseUnreported; internal static ConfigEntry<string> RefusedMessage; internal static ConfigEntry<bool> ProtectCharacter; internal static ConfigEntry<bool> ProtectOnServers; internal static void Bind(ConfigFile cfg) { Enabled = cfg.Bind<bool>("Door", "Enabled", true, "Off leaves the plugin loaded and checking nothing. Only affects the server side; character protection has its own switch below."); ProtectCharacter = cfg.Bind<bool>("Protect", "ProtectCharacter", true, "Refuse to start a local world with a character that belongs to a different one, and remember which world each character belongs to.\nThis is the only half of this mod that can prevent anything. By the time the door refuses a character the damage is already permanent - the game recorded the world it visited and never removes that record. The binding lives in BepInEx/config/dyrr-home.txt, which you can edit; it only protects your own characters, so there is nothing there worth defending against you.\nIt refuses rather than asking, because a confirm dialog on an irreversible action is just a button for doing the unfixable thing."); ProtectOnServers = cfg.Bind<bool>("Protect", "ProtectOnServers", true, "Extend the check above to servers: leave the connection before spawning if the server's world is not the one this character belongs to.\nThis works whatever the server does, and whether or not the server runs this mod at all. It is the same rule the menu applies to local worlds, moved to the one moment on a join where the world is known and nothing has been written yet.\nTurn it off if you deliberately take one character between several worlds. Note that doing so is exactly what the door refuses people for."); Enforce = cfg.Bind<bool>("Door", "Enforce", false, "On refuses the connection. Off only logs what would have been refused.\nOff by default, and deliberately so. This is the one setting in the family that can lock people out of a server, including you, so it should be a thing somebody turns on having read what it does - not something that happens because a mod was installed."); RefuseOtherWorlds = cfg.Bind<bool>("Door", "RefuseOtherWorlds", true, "Refuse a character that has spawned in any world but this one.\nRead this before enabling Enforce: the game never removes entries from a character's world list, so a single visit anywhere else is permanent for that character file. Restoring a backup taken before the trip is the only way back in. That is the intended severity - it is what makes skill levels on this server mean something - but it has no undo."); RefuseCheats = cfg.Bind<bool>("Door", "RefuseCheats", true, "Refuse a character the game has flagged as having used cheats. Also permanent, and set by devcommands rather than by anything subtle."); RefuseCheatCommands = cfg.Bind<bool>("Door", "RefuseCheatCommands", true, "Refuse a character that has run a console command the game marks as a cheat.\nSeparate from RefuseCheats because it reads a different record. The game keeps the name of every command a character has ever run, in m_knownCommands, written a few lines after the cheats flag and outside the branch that sets it - so a mod that clears the flag leaves this behind. Which command was run is named in the log and in the refusal.\nThe classification is done here, from the server's own command table, so a cheat command added by another mod on this server counts too."); RefuseTampered = cfg.Bind<bool>("Door", "RefuseTampered", true, "Refuse a character whose own records disagree with each other.\nThe game writes the same facts in more than one place, at different moments, from different code: the cheats flag has a counter beside it, and the list of worlds a character has spawned in has a second list of world names written at save time. Clearing one is easy; clearing all of them so they still agree is different work, and a mod written to switch devcommands on has not done it.\nThis is the only check here that does not depend on the client being honest, only on it being consistent. A false positive would need a character whose profile the game itself wrote inconsistently, which has not been seen.\nThe world half only runs when the client could actually read its own list of worlds. A game update that moves or renames that field leaves the list empty rather than wrong, which used to read as every character having wiped it - so the whole server was refused, and told so. When that happens now, the server logs that it is judging without the travel rules and lets people in."); WatchInventories = cfg.Bind<bool>("Inventory", "WatchInventories", true, "Say so when a character comes back carrying something it did not leave with. The client reports a tally of its own inventory every InventoryInterval seconds; the server keeps the last one and compares the first report of the next session against it, so what is reported is a change made WHILE AWAY rather than ordinary play. Self-reported, and honest about it: a player's inventory is never sent to a server by the game - it lives in their own .fch - so a client built to lie can lie. This catches the ordinary case, which is somebody closing the game, editing the file and coming back richer. A crash can also trip it: the last snapshot may be a minute stale, and the game may itself roll a character back. The line says what moved so forty iron can be told from one arrow."); InventoryInterval = cfg.Bind<float>("Inventory", "InventoryInterval", 60f, "Seconds between a client's inventory reports. Lower means a crash loses less and so raises fewer false alarms; it is a small package either way. Floored at 5."); InventoryDetail = cfg.Bind<int>("Inventory", "InventoryDetail", 6, "How many changed items to name before the rest are counted. A line naming sixty items is a line nobody reads."); InventoryNamesIds = cfg.Bind<bool>("Inventory", "InventoryNamesIds", false, "Put the character's player id beside the name. Off by default because the line goes to Discord and a name is enough to ask somebody about it."); KickIdle = cfg.Bind<bool>("Idle", "KickIdle", true, "Kick players who have been genuinely still - no movement, no camera - for IdleMinutes, after a warning. Dedicated servers only: an AFK body holds a slot, keeps its zones simulated and blocks the night from being skipped, and the door works in both directions."); IdleMinutes = cfg.Bind<int>("Idle", "IdleMinutes", 5, "Minutes of complete stillness before the kick. Moving, fighting, turning the camera or sorting a chest all reset it; only a hands-off body does not."); IdleWarnMinutes = cfg.Bind<int>("Idle", "IdleWarnMinutes", 2, "Minutes of warning before the kick, said once in the player's chat. 0 kicks without warning."); RefuseMods = cfg.Bind<string>("Mods", "RefuseMods", "Refuse", "Judge what the joining client has loaded, by BepInEx plugin GUID.\nOff - do not look. Notice - look, write what was found to the log, and let everybody in anyway. Refuse - turn them away, which still needs Enforce on. The old true and false still work and mean Refuse and Off.\nNotice exists because turning Enforce off is not a way to trial this rule: Enforce is one switch over every rule at the door, so trialling the mod list that way also stops refusing cheats and altered records for as long as the trial runs. Use Notice for a week, read the log, then set Refuse.\nThis is the check that actually reaches cheating on a dedicated server. Console.IsCheatsEnabled returns ZNet.instance.IsServer(), so a client's own devcommands does nothing on somebody else's server - which means anyone cheating there is running a mod that patched around it. What the character did in the past is a weaker question than what the client is running now.\nSelf-reported like everything else here, so a purpose-built client can lie. What it catches is a cheat mod installed from Thunderstore by somebody who did not think about it."); ModPolicy = cfg.Bind<string>("Mods", "ModPolicy", "Allow", "Allow: only the mods this server runs, plus AllowedMods, may come in.\nDeny: anything may come in except what is named in DeniedMods.\nAllow is the default because a list of things to permit is knowable in advance and a list of every cheat mod that will ever exist is not. Deny suits a server that does not mind what people run as long as it is not that.\nWhichever is set, nothing is refused while Enforce is off - it is reported, and 'dyrr' prints the standing answer for everyone connected. Turn Enforce on after reading that, not before."); AllowedMods = cfg.Bind<string>("Mods", "AllowedMods", "", "Extra plugin GUIDs a client may run, separated by commas. Case is ignored.\nThe plugins this server itself runs are always allowed and do not need listing - otherwise adding a mod to the server would refuse everybody, including you. This is for the client-only ones: a map mod, an equipment bar, and whatever you personally develop with.\nOnly read when ModPolicy is Allow, and added to whatever is in BepInEx/config/dyrr-mods.txt. Prefer that file: this entry only takes effect when the server restarts, and letting one friend keep their map mod is not worth dropping everybody who is online."); DeniedMods = cfg.Bind<string>("Mods", "DeniedMods", "", "Plugin GUIDs no client may run, separated by commas. Case is ignored.\nShipped empty on purpose. A list of cheat mod GUIDs written by me would be out of date the week after it shipped and would read as complete when it was not. Build it from what actually turns up: every plugin a client brings that this server does not run is written to the log when it connects.\nOnly read when ModPolicy is Deny."); RefuseUnreported = cfg.Bind<bool>("Door", "RefuseUnreported", true, "Refuse a connection that answers nothing, or whose profile could not be read. A door that opens when the question goes unanswered is not a door - but Core's version gate should already have turned away a client without this plugin, so in practice this is a backstop."); RefusedMessage = cfg.Bind<string>("Door", "RefusedMessage", "This server refused this connection.", "Sent to the refused client so it lands in their own log, and on their screen when they have Core. Valheim's refusal screen carries no text of its own, and a player on somebody else's server can never read that server's log - so without this, being turned away is indistinguishable from a crash.\nThe specific reason is appended to whatever this says, as a sentence starting \"It\". So keep this one general. It used to read \"only accepts characters that have never played anywhere else\", which was true when travel was the only thing that could refuse anybody and became a lie the moment it was not: a client turned away for running a mod was told, on screen, that its character had played somewhere else. A fixed sentence must not claim which of six checks fired."); } } [BepInPlugin("ezomic.valheim.dyrr", "Dyrr", "1.4.2")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class DyrrPlugin : BaseUnityPlugin { public const string PluginGuid = "ezomic.valheim.dyrr"; public const string PluginName = "Dyrr"; public const string PluginVersion = "1.4.2"; public const string PluginAuthor = "Robbin Thijssen"; internal const string CoreGuid = "ezomic.valheim.core"; private static readonly string[] LegacyGuids = new string[1] { "ezomic.valheim.threshold" }; internal static bool CorePresent; internal static ManualLogSource Log; private Harmony _harmony; private static bool _awakeCompleted; private static bool _doorPatched; private static long _lastId; private static long _lastWorld; private void Awake() { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; AdoptOldConfig(); DyrrConfig.Bind(((BaseUnityPlugin)this).Config); try { TryRegisterWithCore(); } catch (Exception ex) { CorePresent = false; Log.LogError((object)("Dyrr could not register with Core, so it runs standalone this session: no version gate, and refused players see the reason only in their own log. The door itself is unaffected. " + ex)); } _harmony = new Harmony("ezomic.valheim.dyrr"); _doorPatched = Patch(typeof(Doorman)); Patch(typeof(MenuGuard)); Patch(typeof(CharacterNote)); Patch(typeof(Warden)); Patch(typeof(Commands)); if (!_doorPatched) { Log.LogError((object)"Dyrr's door is NOT applied - the Doorman patches did not take, so no connection will be judged and nobody will be kicked for idling either. The line above names the method that would not patch."); } Log.LogInfo((object)"Dyrr 1.4.2 by Robbin Thijssen - ready."); _awakeCompleted = true; } private bool Patch(Type type) { try { _harmony.PatchAll(type); return true; } catch (Exception ex) { Log.LogError((object)("Dyrr could not patch " + type.Name + ", so that part of the mod is not running at all. " + ex)); return false; } } private void Update() { if (!_awakeCompleted) { return; } if (_doorPatched) { Idle.Tick(); } Inventories.Tick(); if (!DyrrConfig.ProtectCharacter.Value || (Object)(object)ZNet.instance == (Object)null || (Object)(object)Game.instance == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null) { return; } long worldUID = ZNet.instance.GetWorldUID(); if (worldUID == 0L) { return; } PlayerProfile playerProfile = Game.instance.GetPlayerProfile(); if (playerProfile != null) { long num = Home.IdOf(playerProfile); if (num != 0L && (num != _lastId || worldUID != _lastWorld)) { _lastId = num; _lastWorld = worldUID; Home.Bind(num, playerProfile.GetName(), worldUID, ZNet.instance.GetWorldName()); } } } private void AdoptOldConfig() { string text = Path.Combine(Paths.ConfigPath, "ezomic.valheim.dyrr.cfg"); if (File.Exists(text)) { return; } string[] legacyGuids = LegacyGuids; foreach (string text2 in legacyGuids) { string text3 = Path.Combine(Paths.ConfigPath, text2 + ".cfg"); if (File.Exists(text3)) { try { File.Copy(text3, text); ((BaseUnityPlugin)this).Config.Reload(); Log.LogInfo((object)("Adopted settings from " + text3 + ". Edit " + Path.GetFileName(text) + " from now on; the old file is left alone and is no longer read.")); break; } catch (Exception ex) { Log.LogWarning((object)("Could not adopt " + text3 + " (" + ex.Message + "), so this run uses defaults - including Enforce off. Copy it to " + Path.GetFileName(text) + " by hand to keep your settings.")); break; } } } } private void TryRegisterWithCore() { CorePresent = Chainloader.PluginInfos.ContainsKey("ezomic.valheim.core"); if (!CorePresent) { Log.LogInfo((object)"Core not installed - running standalone, without the version gate. Refused players will be told why in their log, not on the screen."); } else { RegisterWithCore(); } } internal static void Explain(string why) { if (CorePresent) { ExplainOnScreen(why); } } [MethodImpl(MethodImplOptions.NoInlining)] private static void ExplainOnScreen(string why) { Suite.ExplainRefusal(why); } [MethodImpl(MethodImplOptions.NoInlining)] private void RegisterWithCore() { Suite.Register("ezomic.valheim.dyrr", "Dyrr", "1.4.2", ((BaseUnityPlugin)this).Config, (Requirement)0, (Assembly)null); Suite.Sync((ConfigEntryBase[])(object)new ConfigEntryBase[5] { (ConfigEntryBase)DyrrConfig.Enabled, (ConfigEntryBase)DyrrConfig.Enforce, (ConfigEntryBase)DyrrConfig.RefuseOtherWorlds, (ConfigEntryBase)DyrrConfig.RefuseCheats, (ConfigEntryBase)DyrrConfig.RefuseUnreported }); } private void OnDestroy() { if (_harmony != null) { _harmony.UnpatchSelf(); } } } internal static class Facts { internal const int Format = 4; private static FieldInfo _worldData; private static bool _worldsExplained; private static bool _cheatStatResolved; private static PlayerStatType _cheatStatType; private static bool _cheatStatKnown; internal static ZPackage Gather() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); List<long> list = new List<long>(); bool read = false; List<string> list2 = new List<string>(); bool flag = false; float num = 0f; int num2 = 0; string text = ""; try { PlayerProfile val2 = (((Object)(object)Game.instance != (Object)null) ? Game.instance.GetPlayerProfile() : null); if (val2 != null) { text = val2.GetName() ?? ""; flag = val2.m_usedCheats; num = CheatStat(val2); PlayerStats val3 = Totals(val2); if (val3 != null) { if (val3.m_knownWorlds != null) { num2 = val3.m_knownWorlds.Count; } if (val3.m_knownCommands != null) { foreach (KeyValuePair<string, float> knownCommand in val3.m_knownCommands) { list2.Add(knownCommand.Key); } } } list = WorldsOf(val2, out read); } } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not read this character's profile: " + ex.Message)); val.Write(false); return val; } val.Write(true); val.Write(4); val.Write(text ?? ""); val.Write(flag); val.Write(num); val.Write(num2); val.Write(list2.Count); foreach (string item in list2) { val.Write(item); } val.Write(read); val.Write(list.Count); foreach (long item2 in list) { val.Write(item2); } List<string> list3 = Plugins(); val.Write(list3.Count); foreach (string item3 in list3) { val.Write(item3); } return val; } internal static PlayerStats Totals(PlayerProfile profile) { try { if (profile == null) { return null; } PlayerStats[] playerStats = profile.m_playerStats; if (playerStats == null || playerStats.Length == 0) { return null; } return playerStats[0]; } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not reach this character's stat record: " + ex.Message)); return null; } } private static float CheatStat(PlayerProfile profile) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) if (!_cheatStatResolved) { _cheatStatResolved = true; try { _cheatStatType = (PlayerStatType)Enum.Parse(typeof(PlayerStatType), "Cheats"); _cheatStatKnown = true; } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("This game build has no PlayerStatType.Cheats, so Dyrr cannot corroborate the cheat flag against its counter. The flag itself still works; only the altered-record check loses this half. " + ex.Message)); } } if (!_cheatStatKnown) { return -1f; } PlayerStats val = Totals(profile); if (val == null || val.m_stats == null) { return -1f; } if (!val.m_stats.TryGetValue(_cheatStatType, out var value)) { return 0f; } return value; } private static List<string> Plugins() { List<string> list = new List<string>(); try { foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { list.Add(pluginInfo.Key); } } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not read the plugin list: " + ex.Message)); } return list; } internal static List<long> WorldsOf(PlayerProfile profile) { bool read; return WorldsOf(profile, out read); } internal static List<long> WorldsOf(PlayerProfile profile, out bool read) { read = false; List<long> list = new List<long>(); if (profile == null) { return list; } if (_worldData == null) { _worldData = AccessTools.Field(typeof(PlayerProfile), "m_worldData"); } if (_worldData == null) { Explain("PlayerProfile.m_worldData not found - this character's travel cannot be seen, so Dyrr will not judge anyone's travel record on this build."); return list; } object value; try { value = _worldData.GetValue(profile); } catch (Exception ex) { Explain("PlayerProfile.m_worldData could not be read (" + ex.Message + "), so Dyrr will not judge anyone's travel record on this build."); return list; } if (!(value is IDictionary dictionary)) { Explain("PlayerProfile.m_worldData is " + ((value == null) ? "null" : ("a " + value.GetType().FullName)) + " rather than a dictionary this build can enumerate, so Dyrr will not judge anyone's travel record on this build."); return list; } int num = 0; string text = null; try { foreach (DictionaryEntry item2 in dictionary) { num++; if (item2.Key is long item) { list.Add(item); } else if (text == null) { text = ((item2.Key == null) ? "null" : item2.Key.GetType().FullName); } } } catch (Exception ex2) { Explain("PlayerProfile.m_worldData is a " + dictionary.GetType().FullName + " that could not be walked entry by entry (" + ex2.Message + "), so Dyrr will not judge anyone's travel record on this build."); return list; } if (list.Count != num) { Explain("PlayerProfile.m_worldData holds " + num + " entr(ies) keyed by " + text + " rather than long, so only " + list.Count + " of them could be read. Dyrr will not judge anyone's travel record on this build."); return list; } read = true; return list; } private static void Explain(string what) { if (!_worldsExplained) { _worldsExplained = true; DyrrPlugin.Log.LogError((object)("Dyrr travel record unreadable: " + what)); } } } internal static class Home { internal struct Binding { internal long World; internal string Character; internal string WorldName; } private static readonly Dictionary<long, Binding> _homes = new Dictionary<long, Binding>(); private static FieldInfo _playerId; private static bool _loaded; private static DateTime _stamp; private static readonly string[] LegacyNames = new string[2] { "threshold-home.txt", "boon-home.txt" }; private static string HomePath => Path.Combine(Paths.ConfigPath, "dyrr-home.txt"); internal static long IdOf(PlayerProfile profile) { if (profile == null) { return 0L; } if (_playerId == null) { _playerId = AccessTools.Field(typeof(PlayerProfile), "m_playerID"); } if (_playerId == null) { DyrrPlugin.Log.LogError((object)"PlayerProfile.m_playerID not found - character protection is off."); return 0L; } object value = _playerId.GetValue(profile); if (value is long) { return (long)value; } return 0L; } internal static long Get(long playerId) { return GetBinding(playerId).World; } internal static Binding GetBinding(long playerId) { Load(); if (playerId == 0L) { return default(Binding); } if (!_homes.TryGetValue(playerId, out var value)) { return default(Binding); } return value; } internal static IEnumerable<KeyValuePair<long, Binding>> All() { Load(); return _homes; } internal static void Bind(long playerId, string name, long worldUid, string worldName) { Load(); if (playerId == 0L || worldUid == 0L) { return; } if (_homes.TryGetValue(playerId, out var value)) { if (value.World == worldUid) { string text = Clean(name); string text2 = Clean(worldName); if ((text.Length > 0 && value.Character != text) || (text2.Length > 0 && value.WorldName != text2)) { if (text.Length > 0) { value.Character = text; } if (text2.Length > 0) { value.WorldName = text2; } _homes[playerId] = value; Save(); } } else { DyrrPlugin.Log.LogWarning((object)("Character '" + name + "' (" + playerId + ") is bound to world " + Describe(value) + " but is in world " + worldName + " (" + worldUid + "). Too late to stop it - that world is now written into the character.")); } } else { _homes[playerId] = new Binding { World = worldUid, Character = Clean(name), WorldName = Clean(worldName) }; Save(); DyrrPlugin.Log.LogInfo((object)("Bound character '" + name + "' (" + playerId + ") to world '" + worldName + "' (" + worldUid + ").")); } } internal static void Name(long playerId, string worldName) { Load(); string text = Clean(worldName); if (text.Length != 0 && _homes.TryGetValue(playerId, out var value) && !(value.WorldName == text)) { value.WorldName = text; _homes[playerId] = value; Save(); } } internal static bool Forget(long playerId) { Load(); if (!_homes.Remove(playerId)) { return false; } Save(); return true; } internal static string Describe(Binding binding) { if (!string.IsNullOrEmpty(binding.WorldName)) { return "'" + binding.WorldName + "' (" + binding.World + ")"; } return binding.World.ToString(CultureInfo.InvariantCulture); } private static void Load() { if (_loaded) { if (File.Exists(HomePath)) { DateTime lastWriteTimeUtc; try { lastWriteTimeUtc = File.GetLastWriteTimeUtc(HomePath); } catch (Exception) { return; } if (!(lastWriteTimeUtc == _stamp)) { _homes.Clear(); Parse(HomePath); _stamp = lastWriteTimeUtc; DyrrPlugin.Log.LogInfo((object)("Re-read " + HomePath + " after it changed on disk; " + _homes.Count + " character(s) bound.")); } } return; } _loaded = true; string text = (File.Exists(HomePath) ? HomePath : null); if (text == null) { string[] legacyNames = LegacyNames; foreach (string path in legacyNames) { string text2 = Path.Combine(Paths.ConfigPath, path); if (File.Exists(text2)) { text = text2; break; } } } if (text != null) { bool flag = text != HomePath; if (flag) { DyrrPlugin.Log.LogInfo((object)("Adopting character bindings from " + text + "; they will be written to " + HomePath + " from now on.")); } Parse(text); if (flag) { Save(); } else { Stamp(); } } } private static void Parse(string path) { string[] array; try { array = File.ReadAllLines(path); } catch (Exception ex) { DyrrPlugin.Log.LogError((object)("Could not read " + path + ": " + ex.Message)); return; } string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].Trim(); if (text.Length != 0 && text[0] != '#') { string[] array3 = text.Split(new char[1] { '|' }); if (array3.Length >= 2 && long.TryParse(array3[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && long.TryParse(array3[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { _homes[result] = new Binding { World = result2, Character = ((array3.Length > 2) ? array3[2].Trim() : ""), WorldName = ((array3.Length > 3) ? array3[3].Trim() : "") }; } } } } private static void Save() { try { List<string> list = new List<string> { "# Which world each character belongs to.", "#", "# playerId|worldUid|character|world", "#", "# Dyrr refuses to start a character in any other world, because doing", "# so permanently records that world in the character and locks it out of", "# its own server. Delete a line to unbind that character; the game may be", "# running, the file is re-read when it changes.", "#", "# Only the two numbers are read. The names are here so the line you want", "# is the one you can recognise." }; foreach (KeyValuePair<long, Binding> home in _homes) { list.Add(home.Key + "|" + home.Value.World + "|" + home.Value.Character + "|" + home.Value.WorldName); } File.WriteAllLines(HomePath, list.ToArray()); Stamp(); } catch (Exception ex) { DyrrPlugin.Log.LogError((object)("Could not write " + HomePath + ": " + ex.Message)); } } private static void Stamp() { try { _stamp = File.GetLastWriteTimeUtc(HomePath); } catch (Exception) { _stamp = default(DateTime); } } private static string Clean(string text) { if (string.IsNullOrEmpty(text)) { return ""; } return text.Replace('|', '/').Replace('\r', ' ').Replace('\n', ' ') .Trim(); } } internal static class Idle { private sealed class Watch { public Vector3 Pos; public Quaternion Rot; public float LastMotion; public bool Warned; } private static readonly Dictionary<long, Watch> Watches = new Dictionary<long, Watch>(); private static readonly List<long> Stale = new List<long>(); private static float _nextSweep; private const string CoreChatLine = "Ezomic_Core_ChatLine"; public static void Tick() { //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01c1: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) //IL_0213: Unknown result type (might be due to invalid IL or missing references) //IL_0217: Unknown result type (might be due to invalid IL or missing references) //IL_021c: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Unknown result type (might be due to invalid IL or missing references) //IL_01ef: Unknown result type (might be due to invalid IL or missing references) //IL_01f1: Unknown result type (might be due to invalid IL or missing references) //IL_01f7: Unknown result type (might be due to invalid IL or missing references) //IL_01f9: Unknown result type (might be due to invalid IL or missing references) //IL_0231: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Unknown result type (might be due to invalid IL or missing references) //IL_024b: Unknown result type (might be due to invalid IL or missing references) //IL_024d: Unknown result type (might be due to invalid IL or missing references) //IL_0254: Unknown result type (might be due to invalid IL or missing references) //IL_0256: Unknown result type (might be due to invalid IL or missing references) if (!DyrrConfig.KickIdle.Value) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || !instance.IsDedicated() || Time.time < _nextSweep) { return; } _nextSweep = Time.time + 5f; List<ZNetPeer> peers = instance.GetPeers(); if (Watches.Count > 0) { Stale.Clear(); foreach (long key in Watches.Keys) { bool flag = false; foreach (ZNetPeer item in peers) { if (item != null && item.m_uid == key) { flag = true; break; } } if (!flag) { Stale.Add(key); } } foreach (long item2 in Stale) { Watches.Remove(item2); } } float num = (float)Mathf.Max(1, DyrrConfig.IdleMinutes.Value) * 60f; float num2 = num - (float)Mathf.Max(0, DyrrConfig.IdleWarnMinutes.Value) * 60f; foreach (ZNetPeer item3 in peers) { if (item3 == null || !item3.IsReady() || ((ZDOID)(ref item3.m_characterID)).IsNone()) { continue; } ZDO zDO = ZDOMan.instance.GetZDO(item3.m_characterID); if (zDO == null) { continue; } Vector3 position = zDO.GetPosition(); Quaternion rotation = zDO.GetRotation(); if (!Watches.TryGetValue(item3.m_uid, out var value)) { Watches[item3.m_uid] = new Watch { Pos = position, Rot = rotation, LastMotion = Time.time }; continue; } Vector3 val = position - value.Pos; bool num3 = ((Vector3)(ref val)).sqrMagnitude > 0.0025f || Quaternion.Angle(rotation, value.Rot) > 1f; value.Pos = position; value.Rot = rotation; if (num3) { value.LastMotion = Time.time; value.Warned = false; continue; } float num4 = Time.time - value.LastMotion; if (!value.Warned && num2 > 0f && num4 >= num2 && num4 < num) { value.Warned = true; Warn(item3, Mathf.Max(1, Mathf.CeilToInt((num - num4) / 60f))); } else if (!(num4 < num)) { if (item3.m_rpc != null) { item3.m_rpc.Invoke("Dyrr_Refused", new object[1] { "You were still for " + DyrrConfig.IdleMinutes.Value + " minutes, so the server freed your seat. Rejoin whenever you like." }); } DyrrPlugin.Log.LogWarning((object)("Kicked while away: " + item3.m_playerName + ", still for " + DyrrConfig.IdleMinutes.Value + " minutes.")); Watches.Remove(item3.m_uid); Kick(item3); } } } private static void Warn(ZNetPeer peer, int minutesLeft) { if (peer.m_rpc == null) { return; } try { peer.m_rpc.Invoke("Ezomic_Core_ChatLine", new object[2] { "Server", "You seem to be away - move within " + minutesLeft + " minute" + ((minutesLeft == 1) ? "" : "s") + " or you will be kicked." }); DyrrPlugin.Log.LogInfo((object)("Warned while away: " + peer.m_playerName + ", " + minutesLeft + " minute" + ((minutesLeft == 1) ? "" : "s") + " left.")); } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not warn " + peer.m_playerName + " about idling: " + ex.Message)); } } private static void Kick(ZNetPeer peer) { try { AccessTools.Method(typeof(ZNet), "InternalKick", new Type[1] { typeof(ZNetPeer) }, (Type[])null).Invoke(ZNet.instance, new object[1] { peer }); } catch (Exception ex) { DyrrPlugin.Log.LogError((object)("Could not kick " + peer.m_playerName + ": " + ex.Message)); } } } internal static class Inventories { internal const string Rpc = "Dyrr_Inv"; private const string Marker = "Inventory changed while away:"; private const string FileName = "dyrr-inventories.txt"; private static readonly Dictionary<long, string> Known = new Dictionary<long, string>(); private static readonly HashSet<ZRpc> Greeted = new HashSet<ZRpc>(); private static bool _loaded; private static float _next; internal static void Receive(ZRpc rpc, ZPackage pkg) { if (!DyrrConfig.WatchInventories.Value) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } long num; string name; string text; try { num = pkg.ReadLong(); name = pkg.ReadString(); text = pkg.ReadString(); } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not read an inventory report: " + ex.Message)); return; } if (num == 0L) { return; } Load(); string value; bool flag = Known.TryGetValue(num, out value); if (Greeted.Add(rpc) && flag && value != text) { string text2 = Difference(value, text); if (text2.Length > 0) { DyrrPlugin.Log.LogWarning((object)("Inventory changed while away: " + Display(name, num) + ", " + text2)); } } Known[num] = text; Save(); } internal static void Forget(ZRpc rpc) { Greeted.Remove(rpc); } private static string Difference(string before, string after) { Dictionary<string, int> dictionary = Parse(before); Dictionary<string, int> dictionary2 = Parse(after); List<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(); foreach (KeyValuePair<string, int> item in dictionary2) { dictionary.TryGetValue(item.Key, out var value); if (item.Value != value) { list.Add(new KeyValuePair<string, int>(item.Key, item.Value - value)); } } foreach (KeyValuePair<string, int> item2 in dictionary) { if (!dictionary2.ContainsKey(item2.Key)) { list.Add(new KeyValuePair<string, int>(item2.Key, -item2.Value)); } } if (list.Count == 0) { return ""; } list.Sort(delegate(KeyValuePair<string, int> a, KeyValuePair<string, int> b) { int num3 = Mathf.Abs(b.Value).CompareTo(Mathf.Abs(a.Value)); return (num3 == 0) ? b.Value.CompareTo(a.Value) : num3; }); int num = Mathf.Max(1, DyrrConfig.InventoryDetail.Value); StringBuilder stringBuilder = new StringBuilder(); for (int num2 = 0; num2 < list.Count && num2 < num; num2++) { if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append((list[num2].Value > 0) ? "+" : "").Append(list[num2].Value).Append(' ') .Append(list[num2].Key); } if (list.Count > num) { stringBuilder.Append(" and ").Append(list.Count - num).Append(" more"); } return stringBuilder.ToString(); } private static Dictionary<string, int> Parse(string snapshot) { Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.Ordinal); if (string.IsNullOrEmpty(snapshot)) { return dictionary; } string[] array = snapshot.Split(new char[1] { ',' }); foreach (string text in array) { int num = text.LastIndexOf(':'); if (num > 0 && int.TryParse(text.Substring(num + 1), out var result)) { string key = text.Substring(0, num); dictionary.TryGetValue(key, out var value); dictionary[key] = value + result; } } return dictionary; } private static string Display(string name, long id) { string text = (string.IsNullOrEmpty(name) ? "someone" : name); if (!DyrrConfig.InventoryNamesIds.Value) { return text; } return text + " (" + id + ")"; } private static string Path() { return System.IO.Path.Combine(Paths.ConfigPath, "dyrr-inventories.txt"); } private static void Load() { if (_loaded) { return; } _loaded = true; try { string path = Path(); if (!File.Exists(path)) { return; } string[] array = File.ReadAllLines(path); for (int i = 0; i < array.Length; i++) { string[] array2 = array[i].Split(new char[1] { '|' }, 3); if (array2.Length >= 3 && long.TryParse(array2[0], out var result)) { Known[result] = array2[2]; } } } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not read dyrr-inventories.txt: " + ex.Message)); } } private static void Save() { try { StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<long, string> item in Known) { stringBuilder.Append(item.Key).Append('|').Append('|') .Append(item.Value) .Append('\n'); } File.WriteAllText(Path(), stringBuilder.ToString()); } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not write dyrr-inventories.txt: " + ex.Message)); } } internal static void Tick() { if (!DyrrConfig.WatchInventories.Value) { return; } ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null) && !instance.IsServer()) { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && !(Time.realtimeSinceStartup < _next)) { _next = Time.realtimeSinceStartup + Mathf.Max(5f, DyrrConfig.InventoryInterval.Value); Send(localPlayer); } } } internal static void ReportSoon() { _next = 0f; } private static void Send(Player player) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown ZRpc val = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetServerRPC() : null); if (val != null) { ZPackage val2 = new ZPackage(); val2.Write(player.GetPlayerID()); val2.Write(player.GetPlayerName() ?? ""); val2.Write(Snapshot(player)); val.Invoke("Dyrr_Inv", new object[1] { val2 }); } } private static string Snapshot(Player player) { Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory == null) { return ""; } Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.Ordinal); foreach (ItemData allItem in inventory.GetAllItems()) { if (allItem != null && allItem.m_shared != null) { string text = allItem.m_shared.m_name; if (allItem.m_quality > 1) { text = text + "*" + allItem.m_quality; } dictionary.TryGetValue(text, out var value); dictionary[text] = value + allItem.m_stack; } } List<string> list = new List<string>(dictionary.Keys); list.Sort(StringComparer.Ordinal); StringBuilder stringBuilder = new StringBuilder(); foreach (string item in list) { if (stringBuilder.Length > 0) { stringBuilder.Append(','); } stringBuilder.Append(item.Replace(",", " ").Replace(":", " ")).Append(':').Append(dictionary[item]); } return stringBuilder.ToString(); } } internal static class MenuGuard { [CompilerGenerated] private static class <>O { public static PopupButtonCallback <0>__Pop; } private static FieldInfo _world; private static FieldInfo _profiles; private static FieldInfo _profileIndex; [HarmonyPrefix] [HarmonyPatch(typeof(FejdStartup), "OnWorldStart")] private static bool GuardWorldStart(FejdStartup __instance) { //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Expected O, but got Unknown //IL_0162: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Expected O, but got Unknown if (!DyrrConfig.ProtectCharacter.Value) { return true; } World val = SelectedWorld(__instance); PlayerProfile val2 = SelectedProfile(__instance); if (val == null || val2 == null) { return true; } long num = Home.IdOf(val2); Home.Binding binding = Home.GetBinding(num); if (num == 0L || binding.World == 0L || binding.World == val.m_uid) { return true; } DyrrPlugin.Log.LogWarning((object)("Character '" + val2.GetName() + "' (" + num + ") belongs to world " + Home.Describe(binding) + " but is being started in world '" + val.m_name + "' (" + val.m_uid + "). Refused.")); string text = "Character: " + val2.GetName() + " (id " + num + ")\nThis world: " + val.m_name + " (" + val.m_uid + ")\nBelongs to world: " + Home.Describe(binding) + "\n\nLoading it here records this world permanently in the character and locks it out of its own server. The only undo is restoring a backup, so this is refused rather than confirmed.\n\nIf the binding is wrong, delete the line starting " + num + " from BepInEx/config/dyrr-home.txt. You can do that now, without restarting - the file is re-read whenever it changes."; object obj = <>O.<0>__Pop; if (obj == null) { PopupButtonCallback val3 = UnifiedPopup.Pop; <>O.<0>__Pop = val3; obj = (object)val3; } UnifiedPopup.Push((PopupBase)new WarningPopup("Dyrr: wrong world for this character", text, (PopupButtonCallback)obj, false)); return false; } private static World SelectedWorld(FejdStartup fejd) { if (_world == null) { _world = AccessTools.Field(typeof(FejdStartup), "m_world"); } if (!(_world != null)) { return null; } object? value = _world.GetValue(fejd); return (World)((value is World) ? value : null); } internal static PlayerProfile SelectedProfile(FejdStartup fejd) { if (_profiles == null) { _profiles = AccessTools.Field(typeof(FejdStartup), "m_profiles"); } if (_profileIndex == null) { _profileIndex = AccessTools.Field(typeof(FejdStartup), "m_profileIndex"); } if (_profiles == null || _profileIndex == null) { return null; } if (!(_profiles.GetValue(fejd) is List<PlayerProfile> list)) { return null; } int num = (int)_profileIndex.GetValue(fejd); if (num < 0 || num >= list.Count) { return null; } return list[num]; } } internal static class Mods { internal const string Allow = "Allow"; internal const string Deny = "Deny"; internal const string Off = "Off"; internal const string Notice = "Notice"; internal const string Refuse = "Refuse"; private const string ListFile = "dyrr-mods.txt"; private static HashSet<string> _fromFile; private static DateTime _fileStamp; private static bool _fileMissingLogged; internal static string Tier() { string text = (DyrrConfig.RefuseMods.Value ?? "").Trim(); if (text.Equals("true", StringComparison.OrdinalIgnoreCase)) { return "Refuse"; } if (text.Equals("false", StringComparison.OrdinalIgnoreCase)) { return "Off"; } if (text.Equals("Off", StringComparison.OrdinalIgnoreCase)) { return "Off"; } if (text.Equals("Notice", StringComparison.OrdinalIgnoreCase)) { return "Notice"; } if (text.Equals("Refuse", StringComparison.OrdinalIgnoreCase)) { return "Refuse"; } DyrrPlugin.Log.LogWarning((object)("RefuseMods is set to '" + text + "', which is not Off, Notice or Refuse. Treating it as Refuse.")); return "Refuse"; } internal static string Judge(List<string> reported) { if (reported == null) { return null; } if (!DyrrConfig.ModPolicy.Value.Trim().Equals("Deny", StringComparison.OrdinalIgnoreCase)) { return NotAllowed(reported); } return Denied(reported); } private static string Denied(List<string> reported) { HashSet<string> hashSet = Listed(DyrrConfig.DeniedMods.Value); if (hashSet.Count == 0) { return null; } List<string> list = new List<string>(); foreach (string item in reported) { if (item != null && hashSet.Contains(item.Trim().ToLowerInvariant())) { list.Add(item); } } if (list.Count != 0) { return "is running " + Name(list) + ", which this server does not permit"; } return null; } internal static HashSet<string> Permitted() { HashSet<string> hashSet = Listed(DyrrConfig.AllowedMods.Value); foreach (string item in Own()) { hashSet.Add(item); } foreach (string item2 in FromFile()) { hashSet.Add(item2); } return hashSet; } internal static List<string> Unexpected(List<string> reported) { List<string> list = new List<string>(); if (reported == null) { return list; } HashSet<string> hashSet = (DyrrConfig.ModPolicy.Value.Trim().Equals("Deny", StringComparison.OrdinalIgnoreCase) ? Own() : Permitted()); foreach (string item in reported) { if (item != null && !hashSet.Contains(item.Trim().ToLowerInvariant())) { list.Add(item); } } return list; } private static string NotAllowed(List<string> reported) { List<string> list = Unexpected(reported); if (list.Count != 0) { return "is running " + Name(list) + ", which this server does not run and has not allowed"; } return null; } private static HashSet<string> FromFile() { string path = Path.Combine(Paths.ConfigPath, "dyrr-mods.txt"); try { if (!File.Exists(path)) { if (!_fileMissingLogged) { _fileMissingLogged = true; Seed(path); } _fromFile = new HashSet<string>(); return _fromFile; } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(path); if (_fromFile != null && lastWriteTimeUtc == _fileStamp) { return _fromFile; } _fileStamp = lastWriteTimeUtc; _fromFile = new HashSet<string>(); string[] array = File.ReadAllLines(path); foreach (string text in array) { if (text == null) { continue; } string[] array2 = text.Split(new char[1] { '#' })[0].Split(',', ';'); for (int j = 0; j < array2.Length; j++) { string text2 = array2[j].Trim().ToLowerInvariant(); if (text2.Length > 0) { _fromFile.Add(text2); } } } DyrrPlugin.Log.LogInfo((object)("dyrr-mods.txt read: " + _fromFile.Count + " extra plugin(s) permitted.")); return _fromFile; } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not read dyrr-mods.txt: " + ex.Message)); _fromFile = new HashSet<string>(); return _fromFile; } } private static void Seed(string path) { try { File.WriteAllText(path, "# Plugin GUIDs a client may run that this server does not.\r\n# One per line. Everything after a # is ignored. Case does not matter.\r\n#\r\n# Read fresh whenever this file changes, so adding a line here takes\r\n# effect on the next person to connect - no restart, nobody dropped.\r\n# The AllowedMods setting in ezomic.valheim.dyrr.cfg still works and is\r\n# added to this, but that one only takes effect when the server restarts.\r\n#\r\n# The plugins this server runs are always allowed and never need listing.\r\n#\r\n# Uncomment to let people keep the BepInEx config editor:\r\n# com.bepis.bepinex.configurationmanager\r\n"); DyrrPlugin.Log.LogInfo((object)("Wrote " + path + " - add client-only plugin GUIDs there.")); } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not write dyrr-mods.txt: " + ex.Message)); } } internal static HashSet<string> Own() { HashSet<string> hashSet = new HashSet<string>(); try { foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { if (pluginInfo.Key != null) { hashSet.Add(pluginInfo.Key.Trim().ToLowerInvariant()); } } } catch (Exception ex) { DyrrPlugin.Log.LogWarning((object)("Could not read this machine's plugin list: " + ex.Message)); } return hashSet; } private static HashSet<string> Listed(string text) { HashSet<string> hashSet = new HashSet<string>(); if (string.IsNullOrEmpty(text)) { return hashSet; } string[] array = text.Split(',', '\n', '\r', ';'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim().ToLowerInvariant(); if (text2.Length > 0) { hashSet.Add(text2); } } return hashSet; } private static string Name(List<string> guids) { if (guids.Count == 1) { return "'" + guids[0] + "'"; } int num = Math.Min(guids.Count, 3); string text = ""; for (int i = 0; i < num; i++) { if (i > 0) { text += ", "; } text = text + "'" + guids[i] + "'"; } if (guids.Count > num) { text = text + " and " + (guids.Count - num) + " more"; } return guids.Count + " mods: " + text; } } internal static class Warden { private static FieldInfo _world; private static FieldInfo _status; [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private static void GuardJoin(ZNet __instance, ZRpc rpc) { if (!DyrrConfig.ProtectCharacter.Value || !DyrrConfig.ProtectOnServers.Value || __instance.IsServer()) { return; } World val = World(__instance); if (val == null || val.m_uid == 0L || (Object)(object)Game.instance == (Object)null) { return; } PlayerProfile playerProfile = Game.instance.GetPlayerProfile(); if (playerProfile == null) { return; } long num = Home.IdOf(playerProfile); if (num != 0L) { Home.Binding binding = Home.GetBinding(num); if (binding.World != 0L && binding.World != val.m_uid) { string text = "This character belongs to world " + Home.Describe(binding) + ", and this server is world '" + val.m_name + "' (" + val.m_uid + "). Joining would record this world in the character permanently and lock it out of its own server, so Dyrr stopped the join before it happened. If the binding is wrong, delete the line starting " + num + " from BepInEx/config/dyrr-home.txt."; DyrrPlugin.Log.LogWarning((object)("Refused to join world '" + val.m_name + "' (" + val.m_uid + ") with character '" + playerProfile.GetName() + "' (" + num + "), which belongs to world " + Home.Describe(binding) + ".")); DyrrPlugin.Explain("Dyrr: " + text); Abort(__instance, rpc); } } } private static void Abort(ZNet net, ZRpc rpc) { if (_status == null) { _status = AccessTools.Field(typeof(ZNet), "m_connectionStatus"); } if (_status != null) { _status.SetValue(null, (object)(ConnectionStatus)4); } else { DyrrPlugin.Log.LogWarning((object)"ZNet.m_connectionStatus not found - disconnecting anyway, but the menu will report a generic failure."); } ZNetPeer val = PeerOf(net, rpc); if (val != null) { net.Disconnect(val); } } private static ZNetPeer PeerOf(ZNet net, ZRpc rpc) { List<ZNetPeer> peers = net.GetPeers(); if (peers == null) { return null; } foreach (ZNetPeer item in peers) { if (item != null && item.m_rpc == rpc) { return item; } } return null; } private static World World(ZNet net) { if (_world == null) { _world = AccessTools.Field(typeof(ZNet), "m_world"); } if (!(_world != null)) { return null; } object? value = _world.GetValue(net); return (World)((value is World) ? value : null); } } }