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 SpreadTheLoad v0.1.4
BepInEx/plugins/SpreadTheLoad.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("SpreadTheLoad")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.4.0")] [assembly: AssemblyInformationalVersion("0.1.4+0af647e6621e02f5e0abee6e213981de0872f3e9")] [assembly: AssemblyProduct("SpreadTheLoad")] [assembly: AssemblyTitle("SpreadTheLoad")] [assembly: AssemblyVersion("0.1.4.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 SpreadTheLoad { public static class SpreadTheLoadApi { public static bool Active { get { try { return SpreadTheLoadMod.Enabled != null && SpreadTheLoadMod.Enabled.Value && Ownership.AnyYielding; } catch { return false; } } } public static bool IsYielding(long uid) { try { return Ownership.IsYielding(uid); } catch { return false; } } } internal static class Attackers { private struct Pending { internal long Attacker; internal float At; } private static readonly int DamageHash = StringExtensionMethods.GetStableHashCode("RPC_Damage"); private static readonly Dictionary<ZDOID, Pending> _pending = new Dictionary<ZDOID, Pending>(); private static readonly Dictionary<ZDOID, float> _lastMoved = new Dictionary<ZDOID, float>(); private static readonly Dictionary<int, bool> _isLiving = new Dictionary<int, bool>(); private const float SettleSeconds = 0.4f; private const int MaxTracked = 512; private static int _movedSinceReport; private static float _nextReport; internal static void Note(RoutedRPCData data) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) try { if (data == null || data.m_methodHash != DamageHash || SpreadTheLoadMod.OwnershipFollowsAttacker == null || !SpreadTheLoadMod.OwnershipFollowsAttacker.Value) { return; } ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null) && instance.IsServer()) { long senderPeerID = data.m_senderPeerID; if (senderPeerID != 0L && senderPeerID != data.m_targetPeerID && !Ownership.IsYielding(senderPeerID) && _pending.Count < 512) { _pending[data.m_targetZDO] = new Pending { Attacker = senderPeerID, At = Time.unscaledTime }; } } } catch { } } internal static void Tick(float now) { //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) try { if (_pending.Count == 0) { return; } ZNet instance = ZNet.instance; ZDOMan instance2 = ZDOMan.instance; if ((Object)(object)instance == (Object)null || instance2 == null || !instance.IsServer()) { _pending.Clear(); return; } float num = Mathf.Max(0f, SpreadTheLoadMod.AttackerDwellSeconds.Value); List<ZDOID> list = null; foreach (KeyValuePair<ZDOID, Pending> item in _pending) { if (now - item.Value.At < 0.4f) { continue; } (list ?? (list = new List<ZDOID>())).Add(item.Key); if (_lastMoved.TryGetValue(item.Key, out var value) && now - value < num) { continue; } ZDO zDO = instance2.GetZDO(item.Key); if (zDO != null && zDO.IsValid() && zDO.GetOwner() != item.Value.Attacker && !IsLiving(zDO)) { zDO.SetOwner(item.Value.Attacker); if (_lastMoved.Count < 512) { _lastMoved[item.Key] = now; } _movedSinceReport++; } } if (list != null) { foreach (ZDOID item2 in list) { _pending.Remove(item2); } } if (_lastMoved.Count >= 512) { _lastMoved.Clear(); } Report(now); } catch (Exception ex) { _pending.Clear(); SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] attacker pass failed: " + ex.Message)); } } private static bool IsLiving(ZDO zdo) { int prefab = zdo.GetPrefab(); if (_isLiving.TryGetValue(prefab, out var value)) { return value; } bool flag = true; try { GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.GetPrefab(prefab) : null); if ((Object)(object)val != (Object)null) { flag = (Object)(object)val.GetComponent<Character>() != (Object)null; } } catch { } _isLiving[prefab] = flag; return flag; } private static void Report(float now) { if (_movedSinceReport != 0 && SpreadTheLoadMod.LogActivity != null && SpreadTheLoadMod.LogActivity.Value && !(now < _nextReport)) { _nextReport = now + 60f; SpreadTheLoadMod.Log.LogInfo((object)$"[SpreadTheLoad] moved {_movedSinceReport} object(s) to the player hitting them in the last minute."); _movedSinceReport = 0; } } } [HarmonyPatch(typeof(ZRoutedRpc), "RouteRPC")] internal static class RouteRpcPatch { private static void Prefix(RoutedRPCData rpcData) { Attackers.Note(rpcData); Containers.Note(rpcData); } } internal static class Conflicts { internal static long Consultations; private static readonly List<string> _foreign = new List<string>(); private static bool _inspected; private static bool _reportedSilence; private static float _serverUpSince = -1f; private static float _peersSince = -1f; private const float InspectAfterSeconds = 15f; private const float SilenceSeconds = 60f; internal static void Tick(float now, bool isServer, int peerCount) { if (!isServer) { _serverUpSince = -1f; _peersSince = -1f; return; } if (_serverUpSince < 0f) { _serverUpSince = now; } if (!_inspected && now - _serverUpSince > 15f) { _inspected = true; Inspect(); } if (peerCount <= 0) { _peersSince = -1f; return; } if (_peersSince < 0f) { _peersSince = now; } if (!_reportedSilence && Consultations <= 0 && !(now - _peersSince < 60f) && _foreign.Count != 0) { _reportedSilence = true; SpreadTheLoadMod.Log.LogError((object)"[SpreadTheLoad] Yield Players IS NOT WORKING: players have been connected for a minute and the ownership check has never been consulted once. Another mod has almost certainly replaced ZDOMan.ReleaseNearbyZDOS - ValheimPerformanceOptimizations does exactly that. Remove one of the two. Helm ownership is unaffected and still working; it does not go through that method."); } } private static void Inspect() { try { MethodInfo methodInfo = AccessTools.Method(typeof(ZDOMan), "ReleaseNearbyZDOS", (Type[])null, (Type[])null); if (methodInfo == null) { SpreadTheLoadMod.Log.LogWarning((object)"[SpreadTheLoad] could not find ZDOMan.ReleaseNearbyZDOS to check for conflicts. The game has probably changed; this mod may no longer do anything."); return; } _foreign.Clear(); _foreign.AddRange(Foreign(methodInfo)); if (_foreign.Count != 0) { SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] another mod patches the ownership handout this mod relies on: " + string.Join(", ", _foreign.ToArray()) + ". If it replaces the method rather than adding to it, this mod will have no effect - watch for the NOT WORKING line a minute after somebody joins.")); } } catch (Exception ex) { SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] conflict check failed: " + ex.Message)); } } private static List<string> Foreign(MethodBase target) { List<string> list = new List<string>(); Patches patchInfo = Harmony.GetPatchInfo(target); if (patchInfo == null) { return list; } Collect(patchInfo.Prefixes, "prefix", list); Collect(patchInfo.Transpilers, "transpiler", list); return list; } private static void Collect(IEnumerable<Patch> patches, string kind, List<string> into) { if (patches == null) { return; } foreach (Patch patch in patches) { if (!(patch.owner == "DeathMonger.SpreadTheLoad")) { string item = patch.owner + " (" + kind + ")"; if (!into.Contains(item)) { into.Add(item); } } } } } internal static class Containers { private struct Held { internal Vector3 Position; internal float Noted; } private static readonly int OpenHash = StringExtensionMethods.GetStableHashCode("RPC_RequestOpen"); private static readonly int StackHash = StringExtensionMethods.GetStableHashCode("RPC_RequestStack"); private static readonly int TakeAllHash = StringExtensionMethods.GetStableHashCode("RPC_RequestTakeAll"); private static readonly Dictionary<ZDOID, Held> _held = new Dictionary<ZDOID, Held>(); private const float GraceSeconds = 15f; private const float MaxHoldSeconds = 600f; private const float SameSpotSqr = 0.01f; private const int MaxHeld = 32; private static int _heldSinceReport; private static float _nextReport; internal static void Note(RoutedRPCData data) { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_009e: 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) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) try { if (data == null) { return; } int methodHash = data.m_methodHash; if (methodHash != OpenHash && methodHash != StackHash && methodHash != TakeAllHash) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } long senderPeerID = data.m_senderPeerID; if (senderPeerID == 0L || !Ownership.IsYielding(senderPeerID)) { return; } ZDOMan instance2 = ZDOMan.instance; if (instance2 == null) { return; } ZDO zDO = instance2.GetZDO(data.m_targetZDO); if (zDO != null && zDO.IsValid() && (_held.Count < 32 || _held.ContainsKey(data.m_targetZDO))) { if (!_held.ContainsKey(data.m_targetZDO)) { _heldSinceReport++; } _held[data.m_targetZDO] = new Held { Position = zDO.GetPosition(), Noted = Time.unscaledTime }; } } catch { } } internal static bool IsHeld(Vector3 point) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (_held.Count == 0) { return false; } foreach (KeyValuePair<ZDOID, Held> item in _held) { Vector3 val = item.Value.Position - point; if (((Vector3)(ref val)).sqrMagnitude <= 0.01f) { return true; } } return false; } internal static void Tick(float now) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: 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_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: 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) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: 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_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) try { if (_held.Count == 0) { Report(now); return; } ZDOMan instance = ZDOMan.instance; if (instance == null) { _held.Clear(); return; } List<ZDOID> list = null; List<KeyValuePair<ZDOID, Held>> list2 = new List<KeyValuePair<ZDOID, Held>>(); foreach (KeyValuePair<ZDOID, Held> item in _held) { float num = now - item.Value.Noted; if (num > 600f) { (list ?? (list = new List<ZDOID>())).Add(item.Key); } else { if (num < 15f) { continue; } ZDO zDO = instance.GetZDO(item.Key); if (zDO == null || !zDO.IsValid() || zDO.GetInt(ZDOVars.s_inUse, 0) != 1) { (list ?? (list = new List<ZDOID>())).Add(item.Key); continue; } Vector3 position = zDO.GetPosition(); Vector3 val = position - item.Value.Position; if (((Vector3)(ref val)).sqrMagnitude > 0.01f) { list2.Add(new KeyValuePair<ZDOID, Held>(item.Key, new Held { Position = position, Noted = item.Value.Noted })); } } } if (list != null) { foreach (ZDOID item2 in list) { _held.Remove(item2); } } foreach (KeyValuePair<ZDOID, Held> item3 in list2) { _held[item3.Key] = item3.Value; } Report(now); } catch (Exception ex) { _held.Clear(); SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] container pass failed: " + ex.Message)); } } private static void Report(float now) { if (_heldSinceReport != 0 && SpreadTheLoadMod.LogActivity != null && SpreadTheLoadMod.LogActivity.Value && !(now < _nextReport)) { _nextReport = now + 60f; SpreadTheLoadMod.Log.LogInfo((object)$"[SpreadTheLoad] left {_heldSinceReport} open container(s) with the player using them in the last minute."); _heldSinceReport = 0; } } } internal static class Detection { private sealed class Peer { internal float LastArrival; internal readonly Queue<float> Stalls = new Queue<float>(); internal float QuietSince = -1f; internal bool Flagged; internal string Name = ""; } private static readonly Dictionary<long, Peer> _peers = new Dictionary<long, Peer>(); private static readonly HashSet<long> _flagged = new HashSet<long>(); private const float StallGapSeconds = 0.3f; private const float IgnoreGapSeconds = 5f; private const float WindowSeconds = 120f; private const float RecoverySeconds = 300f; internal static bool IsFlagged(long uid) { return _flagged.Contains(uid); } internal static void NoteArrival(long uid) { if (SpreadTheLoadMod.AutoDetect != null && SpreadTheLoadMod.AutoDetect.Value) { float unscaledTime = Time.unscaledTime; if (!_peers.TryGetValue(uid, out var value)) { Dictionary<long, Peer> peers = _peers; Peer obj = new Peer { LastArrival = unscaledTime }; value = obj; peers[uid] = obj; } float num = unscaledTime - value.LastArrival; value.LastArrival = unscaledTime; if (num > 0.3f && num < 5f) { value.Stalls.Enqueue(unscaledTime); } } } internal static void Tick(float now, ZNet znet) { if (SpreadTheLoadMod.AutoDetect == null || !SpreadTheLoadMod.AutoDetect.Value) { if (_flagged.Count > 0) { _flagged.Clear(); _peers.Clear(); } return; } int num = Mathf.Max(1, SpreadTheLoadMod.StallsPerMinute.Value); HashSet<long> hashSet = new HashSet<long>(); foreach (ZNetPeer connectedPeer in znet.GetConnectedPeers()) { if (connectedPeer == null || !connectedPeer.IsReady()) { continue; } hashSet.Add(connectedPeer.m_uid); if (!_peers.TryGetValue(connectedPeer.m_uid, out var value)) { continue; } value.Name = connectedPeer.m_playerName ?? ""; while (value.Stalls.Count > 0 && now - value.Stalls.Peek() > 120f) { value.Stalls.Dequeue(); } float num2 = (float)value.Stalls.Count / 2f; if (!value.Flagged) { if (!(num2 < (float)num) && WouldLeaveSomebodyCapable(znet, connectedPeer.m_uid)) { value.Flagged = true; value.QuietSince = -1f; _flagged.Add(connectedPeer.m_uid); SpreadTheLoadMod.Log.LogInfo((object)($"[SpreadTheLoad] {Describe(value, connectedPeer.m_uid)} is stalling ({num2:0.0}/min, threshold {num}); " + "steering shared objects away from them until it settles.")); } continue; } if (num2 >= (float)num) { value.QuietSince = -1f; continue; } if (value.QuietSince < 0f) { value.QuietSince = now; } if (!(now - value.QuietSince < 300f)) { value.Flagged = false; value.QuietSince = -1f; _flagged.Remove(connectedPeer.m_uid); SpreadTheLoadMod.Log.LogInfo((object)($"[SpreadTheLoad] {Describe(value, connectedPeer.m_uid)} has run clean for {5f:0} minutes; " + "giving them a normal share again.")); } } if (_flagged.Count == 0) { return; } List<long> list = new List<long>(); foreach (long item in _flagged) { if (!hashSet.Contains(item)) { list.Add(item); } } foreach (long item2 in list) { _flagged.Remove(item2); _peers.Remove(item2); } } private static string Describe(Peer p, long uid) { if (!string.IsNullOrEmpty(p.Name)) { return p.Name; } return uid.ToString(); } private static bool WouldLeaveSomebodyCapable(ZNet znet, long candidate) { foreach (ZNetPeer connectedPeer in znet.GetConnectedPeers()) { if (connectedPeer != null && connectedPeer.IsReady() && connectedPeer.m_uid != candidate && !_flagged.Contains(connectedPeer.m_uid) && !Ownership.IsYielding(connectedPeer.m_uid)) { return true; } } return false; } } [HarmonyPatch(typeof(ZDOMan), "RPC_ZDOData")] internal static class ZDODataPatch { private static void Postfix(ZRpc rpc) { try { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || rpc == null) { return; } foreach (ZNetPeer peer in instance.GetPeers()) { if (peer != null && peer.m_rpc == rpc) { Detection.NoteArrival(peer.m_uid); break; } } } catch { } } } internal static class Ownership { private struct Candidate { internal long Uid; internal Vector3 RefPos; } private static readonly List<Candidate> _capable = new List<Candidate>(); private static readonly Dictionary<long, string> _yielding = new Dictionary<long, string>(); private const float RefreshSeconds = 1f; private static float _nextRefresh; private static readonly Dictionary<string, int> _yielded = new Dictionary<string, int>(); private static float _nextLog; private const float LogEverySeconds = 60f; internal static bool AnyYielding { get { Refresh(); return _yielding.Count > 0; } } internal static bool ShouldYield(Vector3 point, long ownerUid) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) try { Conflicts.Consultations++; if (SpreadTheLoadMod.Enabled == null || !SpreadTheLoadMod.Enabled.Value) { return false; } Refresh(); if (_yielding.Count == 0) { return false; } if (!_yielding.TryGetValue(ownerUid, out var value)) { return false; } if (Containers.IsHeld(point)) { return false; } for (int i = 0; i < _capable.Count; i++) { if (ZNetScene.InActiveArea(point, _capable[i].RefPos)) { Note(value); return true; } } return false; } catch { return false; } } internal static bool IsYielding(long uid) { if (SpreadTheLoadMod.Enabled == null || !SpreadTheLoadMod.Enabled.Value) { return false; } Refresh(); return _yielding.ContainsKey(uid); } private static void Refresh() { //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) if (Time.unscaledTime < _nextRefresh) { return; } _nextRefresh = Time.unscaledTime + 1f; _capable.Clear(); _yielding.Clear(); ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } PlayerIds.Configure((SpreadTheLoadMod.YieldPlayers != null) ? SpreadTheLoadMod.YieldPlayers.Value : ""); if (PlayerIds.Empty && (SpreadTheLoadMod.AutoDetect == null || !SpreadTheLoadMod.AutoDetect.Value)) { return; } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer != null && connectedPeer.IsReady()) { string text = connectedPeer.m_playerName ?? ""; if (PlayerIds.Matches(connectedPeer.m_uid, text, NetworkId(connectedPeer)) || Detection.IsFlagged(connectedPeer.m_uid)) { _yielding[connectedPeer.m_uid] = ((text.Length > 0) ? text : connectedPeer.m_uid.ToString()); continue; } _capable.Add(new Candidate { Uid = connectedPeer.m_uid, RefPos = connectedPeer.GetRefPos() }); } } ReportActivity(); } private static string NetworkId(ZNetPeer peer) { try { return (peer.m_socket != null) ? peer.m_socket.GetHostName() : null; } catch { return null; } } private static void Note(string who) { if (SpreadTheLoadMod.LogActivity != null && SpreadTheLoadMod.LogActivity.Value) { _yielded.TryGetValue(who, out var value); _yielded[who] = value + 1; } } private static void ReportActivity() { if (SpreadTheLoadMod.LogActivity == null || !SpreadTheLoadMod.LogActivity.Value || _yielded.Count == 0 || Time.unscaledTime < _nextLog) { return; } _nextLog = Time.unscaledTime + 60f; List<string> list = new List<string>(); foreach (KeyValuePair<string, int> item in _yielded) { list.Add($"{item.Key} {item.Value}"); } _yielded.Clear(); SpreadTheLoadMod.Log.LogInfo((object)("[SpreadTheLoad] offered to nearby players in the last minute: " + string.Join(", ", list.ToArray()) + " (counts are chances offered, not objects moved; vanilla decides)")); } } [HarmonyPatch(typeof(ZDOMan), "IsInPeerActiveArea")] internal static class IsInPeerActiveAreaPatch { private static void Postfix(Vector3 point, long uid, ref bool __result) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) if (__result && Ownership.ShouldYield(point, uid)) { __result = false; } } } internal static class PlayerIds { private static readonly HashSet<string> _ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static readonly List<string> _names = new List<string>(); private static readonly Dictionary<string, string> _learned = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private static string _lastRaw = null; private static bool _loaded; private static bool _dirty; private const string FileName = "SpreadTheLoad-known-ids.txt"; internal static bool Empty { get { if (_ids.Count == 0) { return _names.Count == 0; } return false; } } private static bool LooksLikeId(string token) { if (string.IsNullOrEmpty(token) || token.Length < 6) { return false; } foreach (char c in token) { if (c < '0' || c > '9') { return false; } } return true; } internal static void Configure(string raw) { EnsureLoaded(); if (raw == _lastRaw) { return; } _lastRaw = raw; _ids.Clear(); _names.Clear(); foreach (string key in _learned.Keys) { _ids.Add(key); } if (string.IsNullOrEmpty(raw)) { return; } string[] array = raw.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0) { if (LooksLikeId(text)) { _ids.Add(text); } else { _names.Add(text); } } } } internal static bool Matches(long uid, string playerName, string networkId) { if (!string.IsNullOrEmpty(networkId) && _ids.Contains(networkId)) { return true; } if (_names.Count == 0 || string.IsNullOrEmpty(playerName)) { return false; } string text = playerName.Trim(); for (int i = 0; i < _names.Count; i++) { if (string.Equals(_names[i], text, StringComparison.OrdinalIgnoreCase)) { Learn(networkId, text); return true; } } return false; } private static void Learn(string networkId, string name) { if (SpreadTheLoadMod.RememberIds.Value && !string.IsNullOrEmpty(networkId) && !_learned.ContainsKey(networkId)) { _learned[networkId] = name; _ids.Add(networkId); _dirty = true; SpreadTheLoadMod.Log.LogInfo((object)("[SpreadTheLoad] learned " + networkId + " from the name \"" + name + "\"; that player will now be matched by id whatever they call their character. Remove the line from SpreadTheLoad-known-ids.txt to forget it.")); Save(); } } private static string Path() { try { return System.IO.Path.Combine(Paths.ConfigPath, "SpreadTheLoad-known-ids.txt"); } catch { return null; } } private static void EnsureLoaded() { if (_loaded) { return; } _loaded = true; try { string text = Path(); if (text == null || !File.Exists(text)) { return; } string[] array = File.ReadAllLines(text); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length != 0 && text2[0] != '#') { int num = text2.IndexOf('#'); string text3 = ((num >= 0) ? text2.Substring(0, num) : text2).Trim(); string value = ((num >= 0) ? text2.Substring(num + 1).Trim() : ""); if (text3.Length > 0) { _learned[text3] = value; } } } if (_learned.Count > 0) { SpreadTheLoadMod.Log.LogInfo((object)string.Format("[SpreadTheLoad] remembered {0} player id(s) from {1}.", _learned.Count, "SpreadTheLoad-known-ids.txt")); } } catch (Exception ex) { SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] could not read SpreadTheLoad-known-ids.txt: " + ex.Message)); } } private static void Save() { if (!_dirty) { return; } _dirty = false; try { string text = Path(); if (text == null) { return; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Players this mod steers object ownership away from, by network id."); stringBuilder.AppendLine("# Learned automatically the first time a name in the \"Yield Players\" setting matched"); stringBuilder.AppendLine("# somebody connected. The name after the # is only a reminder of who it was."); stringBuilder.AppendLine("# Delete a line to forget that player. One id per line."); foreach (KeyValuePair<string, string> item in _learned) { stringBuilder.AppendLine(item.Key + " # " + item.Value); } File.WriteAllText(text, stringBuilder.ToString()); } catch (Exception ex) { SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] could not write SpreadTheLoad-known-ids.txt: " + ex.Message)); } } } internal static class Ships { private static readonly List<string> _prefabs = new List<string>(); private static int _prefabCountAtScan = -1; private static FieldInfo _namedPrefabsField; private static int _prefabIndex; private static int _scanIndex; private static readonly List<ZDO> _found = new List<ZDO>(); private static float _resumeAt; private const float CycleSeconds = 5f; private static readonly Dictionary<long, long> _playerUid = new Dictionary<long, long>(); private static int _movedThisCycle; internal static void Tick(float now) { try { if (SpreadTheLoadMod.AssignShipToCaptain == null || !SpreadTheLoadMod.AssignShipToCaptain.Value) { return; } ZNet instance = ZNet.instance; ZDOMan instance2 = ZDOMan.instance; if ((Object)(object)instance == (Object)null || instance2 == null || !instance.IsServer() || now < _resumeAt) { return; } Discover(); if (_prefabs.Count == 0) { return; } if (_prefabIndex == 0 && _scanIndex == 0) { BuildPlayerMap(instance, instance2); _movedThisCycle = 0; } if (!instance2.GetAllZDOsWithPrefabIterative(_prefabs[_prefabIndex], _found, ref _scanIndex)) { return; } foreach (ZDO item in _found) { Consider(item); } _found.Clear(); _scanIndex = 0; _prefabIndex++; if (_prefabIndex >= _prefabs.Count) { _prefabIndex = 0; _resumeAt = now + 5f; Report(); } } catch (Exception ex) { _found.Clear(); _scanIndex = 0; _prefabIndex = 0; _resumeAt = now + 5f; SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] ship pass failed: " + ex.Message)); } } private static void Consider(ZDO zdo) { if (zdo != null && zdo.IsValid()) { long num = zdo.GetLong(ZDOVars.s_user, 0L); if (num != 0L && _playerUid.TryGetValue(num, out var value) && zdo.GetOwner() != value && (!Ownership.IsYielding(value) || SpreadTheLoadMod.YieldingRetainsHelm == null || SpreadTheLoadMod.YieldingRetainsHelm.Value)) { zdo.SetOwner(value); _movedThisCycle++; } } } private static void BuildPlayerMap(ZNet znet, ZDOMan zdoMan) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) _playerUid.Clear(); try { foreach (PlayerInfo player in znet.GetPlayerList()) { ZDO zDO = zdoMan.GetZDO(player.m_characterID); if (zDO != null) { long num = zDO.GetLong(ZDOVars.s_playerID, 0L); if (num != 0L) { Dictionary<long, long> playerUid = _playerUid; ZDOID characterID = player.m_characterID; playerUid[num] = ((ZDOID)(ref characterID)).UserID; } } } } catch { } } private static void Discover() { ZNetScene instance = ZNetScene.instance; if ((Object)(object)instance == (Object)null) { return; } Dictionary<int, GameObject> dictionary = NamedPrefabs(instance); int num = dictionary?.Count ?? instance.m_prefabs.Count; if (num == _prefabCountAtScan) { return; } _prefabCountAtScan = num; _prefabs.Clear(); if (dictionary != null) { foreach (GameObject value in dictionary.Values) { if (!((Object)(object)value == (Object)null) && !((Object)(object)value.GetComponent<Ship>() == (Object)null) && !_prefabs.Contains(((Object)value).name)) { _prefabs.Add(((Object)value).name); } } } else { foreach (GameObject prefab in instance.m_prefabs) { if (!((Object)(object)prefab == (Object)null) && !((Object)(object)prefab.GetComponent<Ship>() == (Object)null) && !_prefabs.Contains(((Object)prefab).name)) { _prefabs.Add(((Object)prefab).name); } } } _prefabIndex = 0; _scanIndex = 0; _found.Clear(); SpreadTheLoadMod.Log.LogInfo((object)((_prefabs.Count == 0) ? "[SpreadTheLoad] no ship prefabs found; helm ownership will do nothing." : string.Format("[SpreadTheLoad] watching {0} ship type(s) for helm ownership: {1}", _prefabs.Count, string.Join(", ", _prefabs.ToArray())))); } private static Dictionary<int, GameObject> NamedPrefabs(ZNetScene scene) { try { if (_namedPrefabsField == null) { _namedPrefabsField = AccessTools.Field(typeof(ZNetScene), "m_namedPrefabs"); if (_namedPrefabsField == null) { return null; } } return _namedPrefabsField.GetValue(scene) as Dictionary<int, GameObject>; } catch { return null; } } private static void Report() { if (_movedThisCycle != 0 && SpreadTheLoadMod.LogActivity != null && SpreadTheLoadMod.LogActivity.Value) { SpreadTheLoadMod.Log.LogInfo((object)$"[SpreadTheLoad] handed {_movedThisCycle} ship(s) to the player steering them."); } } } [BepInPlugin("DeathMonger.SpreadTheLoad", "Spread The Load", "0.1.4")] public class SpreadTheLoadMod : BaseUnityPlugin { public const string ModGuid = "DeathMonger.SpreadTheLoad"; public const string ModName = "Spread The Load"; public const string ModVersion = "0.1.4"; internal static ManualLogSource Log; internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<string> YieldPlayers; internal static ConfigEntry<bool> LogActivity; internal static ConfigEntry<bool> RememberIds; internal static ConfigEntry<bool> AutoDetect; internal static ConfigEntry<int> StallsPerMinute; internal static ConfigEntry<bool> OwnershipFollowsAttacker; internal static ConfigEntry<float> AttackerDwellSeconds; internal static ConfigEntry<bool> AssignShipToCaptain; internal static ConfigEntry<bool> YieldingRetainsHelm; private Harmony _harmony; private float _nextJudge; private FileSystemWatcher _configWatcher; private void Awake() { //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Steer ownership of shared objects away from the players named below. Turn this off to get stock Valheim behaviour back without removing the mod."); YieldPlayers = ((BaseUnityPlugin)this).Config.Bind<string>("General", "Yield Players", "", "Comma-separated list of the players whose machines should not be given objects somebody else is also standing near. They still own anything only they are near, so nothing is ever left unsimulated. Empty means the mod does nothing. Each entry is either a Steam id (the 17-digit number the server logs as \"Got connection SteamID ...\") or a character name. A character name is not an identity - the same person on a second character stops matching - so a name is used once to find the player and their id is then remembered, after which the name no longer matters. Ids are the reliable thing to enter."); RememberIds = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Remember Ids", true, "When a character name here matches a connected player, write down their network id so they keep matching on any character. Ids are kept in SpreadTheLoad-known-ids.txt beside this file; delete a line there to forget one. Turn this off to match strictly on what is typed above."); AutoDetect = ((BaseUnityPlugin)this).Config.Bind<bool>("Auto Detect", "Auto Detect Struggling Players", false, "Work out for itself which machines are struggling and steer objects away from them, without naming anyone. Detection is by stalls: the server times the gaps between the updates each client sends, and a gap means that machine stopped. It cannot tell a frozen machine from a hiccuping connection, which is fine - routing other players' work through either is a bad idea. Note that ping and connection quality are NOT used: the client this was written for had a 24 ms ping and ran at 16 fps. Flags are held in memory only, never written to the known-ids file, and are dropped when the player disconnects."); StallsPerMinute = ((BaseUnityPlugin)this).Config.Bind<int>("Auto Detect", "Stalls Per Minute", 6, "How many stalls a minute, averaged over two minutes, before a player is flagged. A stall is a gap over 0.3s in the updates they send; healthy clients manage about one update every 0.05s. Clearing the flag needs five clean minutes - deliberately harder than setting it, so a borderline machine does not flap ownership back and forth. The last healthy player on the server is never flagged."); OwnershipFollowsAttacker = ((BaseUnityPlugin)this).Config.Bind<bool>("Interacting", "Ownership Follows Attacker", true, "Give a tree, rock or ore vein to whoever is hitting it. Vanilla never does: the machine that loaded it keeps it, so every swing anyone else makes travels to that machine and back, for that tree and the next one. A chopping session is hundreds of interactions against a few objects, which makes this the commonest way a group feels somebody else's frame time. Resources only. A creature carries live AI state that is not all replicated, so moving one mid-fight can make it re-acquire its target or re-path; that is left alone until it can be measured rather than guessed at."); AttackerDwellSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Interacting", "Attacker Dwell Seconds", 5f, "How long an object stays put after being handed to somebody, so two players working the same tree cannot bounce it back and forth between them."); AssignShipToCaptain = ((BaseUnityPlugin)this).Config.Bind<bool>("Ships", "Assign Ship To Captain", true, "Give a ship to whoever is steering it. Vanilla only moves a ship when its owner is not aboard, and then picks an arbitrary passenger rather than the captain, so the person at the helm often does not own the hull their steering has to reach. Steering is batched to the owner every 0.2s and the physics runs only there, so a captain who does not own the ship waits about a quarter second for every turn."); YieldingRetainsHelm = ((BaseUnityPlugin)this).Config.Bind<bool>("Ships", "Yielding Player Retains Boat Helm Ownership", true, "Whether a player listed in Yield Players still gets the ship when they are the one steering it. On (default) they keep the helm: their steering stays responsive, at the cost of the hull lurching for everyone aboard whenever their machine stalls. Off, the ship goes to someone else and they steer through a delay - smoother for passengers, but a captain fighting a mushy helm is the one who hits the rocks. Which is better has been reasoned about but not measured; try both."); LogActivity = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Log Activity", false, "Write a line to the server log now and then saying how many objects were moved and who they came from. Off by default; it is a summary, never one line per object."); SetupConfigWatcher(); _harmony = new Harmony("DeathMonger.SpreadTheLoad"); _harmony.PatchAll(); Log.LogInfo((object)"[SpreadTheLoad] 0.1.4 loaded."); } private void SetupConfigWatcher() { try { _configWatcher = new FileSystemWatcher(Paths.ConfigPath, Path.GetFileName(((BaseUnityPlugin)this).Config.ConfigFilePath)); _configWatcher.Changed += OnConfigFileChanged; _configWatcher.Created += OnConfigFileChanged; _configWatcher.Renamed += OnConfigFileChanged; _configWatcher.SynchronizingObject = ThreadingHelper.SynchronizingObject; _configWatcher.EnableRaisingEvents = true; } catch (Exception ex) { Log.LogWarning((object)("[SpreadTheLoad] could not watch the config file for changes: " + ex.Message)); } } private void OnConfigFileChanged(object sender, FileSystemEventArgs e) { if (!File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath)) { return; } try { ((BaseUnityPlugin)this).Config.Reload(); Log.LogInfo((object)"[SpreadTheLoad] config reloaded; the new settings are in effect."); } catch (Exception ex) { Log.LogWarning((object)("[SpreadTheLoad] could not reload the config: " + ex.Message)); } } private void Update() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { Conflicts.Tick(Time.unscaledTime, isServer: false, 0); return; } try { Conflicts.Tick(Time.unscaledTime, instance.IsServer(), instance.GetConnectedPeers().Count); Ships.Tick(Time.unscaledTime); Attackers.Tick(Time.unscaledTime); if (Time.unscaledTime >= _nextJudge) { _nextJudge = Time.unscaledTime + 1f; Detection.Tick(Time.unscaledTime, instance); Containers.Tick(Time.unscaledTime); } } catch { } } private void OnDestroy() { _configWatcher?.Dispose(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } }