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.4.0
BepInEx/plugins/SpreadTheLoad.dll
Decompiled a day agousing System; using System.Collections; 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.4.0.0")] [assembly: AssemblyInformationalVersion("0.4.0+02583c5fc65e90612e327a037a6f073d4d8d2fbf")] [assembly: AssemblyProduct("SpreadTheLoad")] [assembly: AssemblyTitle("SpreadTheLoad")] [assembly: AssemblyVersion("0.4.0.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_0054: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: 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()) { return; } ZDOMan instance2 = ZDOMan.instance; if (instance2 != null) { ZDO zDO = instance2.GetZDO(data.m_targetZDO); if (zDO != null && zDO.IsValid()) { Combat.Note(zDO.GetPosition()); } } 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 Combat { private struct Blow { internal Vector3 Where; internal float When; } private static readonly List<Blow> _blows = new List<Blow>(); private const float RadiusMeters = 40f; private const float QuietSeconds = 15f; private const int MaxBlows = 64; internal static void Note(Vector3 where) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) float unscaledTime = Time.unscaledTime; for (int i = 0; i < _blows.Count; i++) { Vector3 val = _blows[i].Where - where; if (!(((Vector3)(ref val)).sqrMagnitude > 400f)) { _blows[i] = new Blow { Where = _blows[i].Where, When = unscaledTime }; return; } } if (_blows.Count >= 64) { _blows.RemoveAt(0); } _blows.Add(new Blow { Where = where, When = unscaledTime }); } internal static bool Nearby(Vector3 point) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) if (_blows.Count == 0) { return false; } float unscaledTime = Time.unscaledTime; float num = 1600f; for (int num2 = _blows.Count - 1; num2 >= 0; num2--) { if (unscaledTime - _blows[num2].When > 15f) { _blows.RemoveAt(num2); } else { Vector3 val = _blows[num2].Where - point; if (((Vector3)(ref val)).sqrMagnitude <= num) { return true; } } } return false; } internal static void Clear() { _blows.Clear(); } } 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 (!Streaming.IsLoading(uid) && 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>(); Streaming.Tick(now); 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(); } if (Streaming.IsLoading(connectedPeer.m_uid)) { continue; } 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_002f: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) try { Conflicts.Consultations++; Proximity.Approved = false; if (SpreadTheLoadMod.Enabled == null || !SpreadTheLoadMod.Enabled.Value) { return false; } Refresh(); if (Containers.IsHeld(point)) { return false; } if (Combat.Nearby(point)) { return false; } if (_yielding.TryGetValue(ownerUid, out var value)) { for (int i = 0; i < _capable.Count; i++) { if (ZNetScene.InActiveArea(point, _capable[i].RefPos)) { Note(value); return true; } } return false; } return Proximity.ShouldTake(point, ownerUid); } 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 Pacing { private static FieldInfo _peersField; private static MethodInfo _sendZDOs; private static bool _looked; private static bool _usable; private static readonly object[] _args = new object[2]; private static float _timer; private static int _served; private static float _nextReport; private static bool Usable() { if (_looked) { return _usable; } _looked = true; try { Type type = AccessTools.Inner(typeof(ZDOMan), "ZDOPeer"); _peersField = AccessTools.Field(typeof(ZDOMan), "m_peers"); if (type != null) { _sendZDOs = AccessTools.Method(typeof(ZDOMan), "SendZDOs", new Type[2] { type, typeof(bool) }, (Type[])null); } _usable = _peersField != null && _sendZDOs != null; if (!_usable) { SpreadTheLoadMod.Log.LogWarning((object)"[SpreadTheLoad] could not reach ZDOMan.SendZDOs; update pacing is left to vanilla."); } } catch (Exception ex) { _usable = false; SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] update pacing disabled: " + ex.Message)); } return _usable; } internal static bool Run(ZDOMan man, float dt) { try { if (SpreadTheLoadMod.SendToEveryPeer == null || !SpreadTheLoadMod.SendToEveryPeer.Value) { return true; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return true; } if (!Usable()) { return true; } if (!(_peersField.GetValue(man) is IList { Count: not 0 } list)) { return false; } _timer += dt; float num = 1f / (float)Mathf.Clamp(SpreadTheLoadMod.UpdatesPerSecond.Value, 1, 60); if (_timer < num) { return false; } _timer = 0f; for (int i = 0; i < list.Count; i++) { _args[0] = list[i]; _args[1] = false; _sendZDOs.Invoke(man, _args); } _served++; Report(); return false; } catch (Exception arg) { _usable = false; SpreadTheLoadMod.Log.LogError((object)$"[SpreadTheLoad] update pacing failed and is now off; vanilla has it back: {arg}"); return true; } } private static void Report() { if (SpreadTheLoadMod.LogActivity != null && SpreadTheLoadMod.LogActivity.Value && !(Time.unscaledTime < _nextReport)) { if (_nextReport > 0f) { SpreadTheLoadMod.Log.LogInfo((object)($"[SpreadTheLoad] served every player {_served} times in the last minute " + $"({(float)_served / 60f:0.#}/s each; vanilla would manage {(float)_served / 60f:0.#} shared between them).")); } _nextReport = Time.unscaledTime + 60f; _served = 0; } } } [HarmonyPatch(typeof(ZDOMan), "SendZDOToPeers2")] internal static class SendZDOToPeers2Patch { private static bool Prefix(ZDOMan __instance, float dt) { return Pacing.Run(__instance, dt); } } 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 Proximity { internal static long Candidate; internal static bool Approved; private static readonly Dictionary<long, Vector3> _where = new Dictionary<long, Vector3>(); private static float _nextRefresh; private static int _taken; private static float _nextReport; internal static void Reset() { Candidate = 0L; Approved = false; _where.Clear(); _nextRefresh = 0f; } internal static bool ShouldTake(Vector3 point, long ownerUid) { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) if (SpreadTheLoadMod.ProximityTransfer == null || !SpreadTheLoadMod.ProximityTransfer.Value) { return false; } if (Candidate == 0L || Candidate == ownerUid) { return false; } Refresh(); if (!_where.TryGetValue(Candidate, out var value)) { return false; } if (!_where.TryGetValue(ownerUid, out var value2)) { return false; } float num = Mathf.Max(0.5f, SpreadTheLoadMod.ProximityNearMeters.Value); float num2 = Mathf.Max(num + 0.5f, SpreadTheLoadMod.ProximityFarMeters.Value); if (Flat(value, point) > num) { return false; } if (Flat(value2, point) < num2) { return false; } if (Ownership.IsYielding(Candidate)) { return false; } _taken++; Report(); Approved = true; return true; } internal static void Unhand() { if (_taken > 0) { _taken--; } } private static float Flat(Vector3 a, Vector3 b) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) float num = a.x - b.x; float num2 = a.z - b.z; return Mathf.Sqrt(num * num + num2 * num2); } private static void Refresh() { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0059: 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_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) if (Time.unscaledTime < _nextRefresh) { return; } _nextRefresh = Time.unscaledTime + 1f; _where.Clear(); try { ZNet instance = ZNet.instance; ZDOMan instance2 = ZDOMan.instance; if ((Object)(object)instance == (Object)null || instance2 == null) { return; } foreach (PlayerInfo player in instance.GetPlayerList()) { ZDO zDO = instance2.GetZDO(player.m_characterID); if (zDO != null && zDO.IsValid()) { Dictionary<long, Vector3> dictionary = _where; ZDOID characterID = player.m_characterID; dictionary[((ZDOID)(ref characterID)).UserID] = zDO.GetPosition(); } } } catch { } } private static void Report() { if (SpreadTheLoadMod.LogActivity != null && SpreadTheLoadMod.LogActivity.Value && !(Time.unscaledTime < _nextReport)) { if (_nextReport > 0f) { SpreadTheLoadMod.Log.LogInfo((object)$"[SpreadTheLoad] handed {_taken} object(s) to the player standing on them in the last minute."); } _nextReport = Time.unscaledTime + 60f; _taken = 0; } } } [HarmonyPatch(typeof(ZDOMan), "ReleaseNearbyZDOS")] internal static class ReleaseNearbyZDOSPatch { private static void Prefix(long uid) { Proximity.Candidate = uid; } private static void Postfix() { Proximity.Candidate = 0L; Proximity.Approved = false; } } 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."); } } } [HarmonyPatch(typeof(ZNet), "RPC_RequestValidSimulationDistance")] internal static class SimDistance { private static bool Prefix(ZRpc rpc, ZPackage pkg) { //IL_0042: 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_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0058: 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_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected O, but got Unknown //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) try { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return true; } int num = ((SpreadTheLoadMod.MaxSimulationDistance != null) ? SpreadTheLoadMod.MaxSimulationDistance.Value : (-1)); if (num < 0) { return true; } SimulationDistance val = SimulationDistance.Deserialize(ref pkg); SimulationDistance simulationDistance = SimulationDistance.GetSimulationDistance(num); SimulationDistance simulationDistance2 = ((val < simulationDistance) ? val : simulationDistance); ZNetPeer val2 = null; foreach (ZNetPeer peer in instance.GetPeers()) { if (peer != null && peer.m_rpc == rpc) { val2 = peer; break; } } if (val2 != null) { val2.m_simulationDistance = simulationDistance2; } ZPackage val3 = new ZPackage(); ((SimulationDistance)(ref simulationDistance2)).Serialize(ref val3); rpc.Invoke("RPC_ValidatedSimulationDistance", new object[1] { val3 }); if (val2 != null && ((SimulationDistance)(ref val)).NearSimulationDistance != ((SimulationDistance)(ref simulationDistance2)).NearSimulationDistance) { SpreadTheLoadMod.Log.LogInfo((object)("[SpreadTheLoad] " + val2.m_playerName + " asked for simulation distance " + $"{((SimulationDistance)(ref val)).NearSimulationDistance} and was given {((SimulationDistance)(ref simulationDistance2)).NearSimulationDistance} " + $"(ceiling {num}).")); } return false; } catch (Exception ex) { SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] could not answer the simulation distance request: " + ex.Message)); return true; } } } [BepInPlugin("DeathMonger.SpreadTheLoad", "Spread The Load", "0.4.0")] public class SpreadTheLoadMod : BaseUnityPlugin { public const string ModGuid = "DeathMonger.SpreadTheLoad"; public const string ModName = "Spread The Load"; public const string ModVersion = "0.4.0"; 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<int> MaxSimulationDistance; internal static ConfigEntry<bool> OwnershipFollowsAttacker; internal static ConfigEntry<float> AttackerDwellSeconds; internal static ConfigEntry<bool> ProximityTransfer; internal static ConfigEntry<float> ProximityNearMeters; internal static ConfigEntry<float> ProximityFarMeters; internal static ConfigEntry<bool> KeepInertPieces; internal static ConfigEntry<bool> SendToEveryPeer; internal static ConfigEntry<int> UpdatesPerSecond; internal static ConfigEntry<bool> AssignShipToCaptain; internal static ConfigEntry<bool> YieldingRetainsHelm; private Harmony _harmony; private float _nextJudge; private FileSystemWatcher _configWatcher; private void Awake() { //IL_0248: Unknown result type (might be due to invalid IL or missing references) //IL_0252: 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."); MaxSimulationDistance = ((BaseUnityPlugin)this).Config.Bind<int>("General", "Max Simulation Distance", -1, "The highest simulation distance this server will grant a client, or -1 to leave it to the server's own graphics setting. \"Draw distance\" in the client menu is this setting, and the server answers every client with the lower of what they asked for and this. A client asking for less still gets what it asked for. 0 Low (square 224 m), 1 Medium (288), 2 Classic (square 288, the game default), 3 High (352), 4 Very High (416), 5 Ultra (480), 6 Extreme (544). Reach is (level + 2.5) x 64 m. Raising this costs the SERVER as well as the client: it sends each peer the objects in that peer's active area, so a high ceiling multiplies that for everyone who asks for it. Raise it a step at a time and measure."); 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."); ProximityTransfer = ((BaseUnityPlugin)this).Config.Bind<bool>("Interacting", "Ownership Follows Proximity", true, "Give an object to a player standing on top of it when its owner is nowhere near. Pushing a boar you do not own is the clearest case: every shove travels to the owner and the corrected position comes back, so the animal lurches - and the owner may be a hundred metres away with no interest in it. Nothing to do with anyone's machine being slow; the object is simply in the wrong hands."); ProximityNearMeters = ((BaseUnityPlugin)this).Config.Bind<float>("Interacting", "Proximity Near Meters", 2f, "How close a player must be to take an object this way."); ProximityFarMeters = ((BaseUnityPlugin)this).Config.Bind<float>("Interacting", "Proximity Far Meters", 5f, "How far the current owner must be for it to be taken from them. The gap between this and Near is what stops an object flitting between two people: once the near player owns it they are at zero distance, so the rule cannot fire again until somebody else is nearer than Near while they are further than Far."); KeepInertPieces = ((BaseUnityPlugin)this).Config.Bind<bool>("Interacting", "Proximity Skips Inert Pieces", true, "Leave walls, floors, beams and fences with whoever owns them, instead of handing them to a player standing on them. Only the rule above is affected. Ownership of a piece that does nothing is invisible - there is no lurch to feel, and hitting one is already covered by Ownership Follows Attacker - while the cost of holding it is real: a base is thousands of objects packed within a couple of metres, so walking around one collects them by the hundred - this mod's own log reported 275 objects a minute changing hands this way, minute after minute, while one player walked around his base. Anything that interacts, ticks or moves is still handed over: chests, doors, fires, smelters, crafting stations, carts and ships, as well as every creature and dropped item. Turn this off to get the rule's previous behavior back."); SendToEveryPeer = ((BaseUnityPlugin)this).Config.Bind<bool>("Pacing", "Send To Every Peer Each Cycle", true, "Send every player their world update on each cycle, instead of one player per server frame. Vanilla serves one peer per frame, so each player hears from the server every (players + 1) frames - measured at 67 ms with one player and 200 ms with five, on a 30 Hz server. Everything a player does not own arrives at that rate and is interpolated in between, which is the one cost that grows with the size of the group. Turn this off to get vanilla pacing back."); UpdatesPerSecond = ((BaseUnityPlugin)this).Config.Bind<int>("Pacing", "Updates Per Second", 20, "How often each player is sent an update, once the setting above is on. Twenty is what vanilla's own 50 ms gate implies it was aiming for before the per-frame loop divided it by the player count. Higher costs upload bandwidth and a little server CPU; the socket refuses anything it cannot drain, so this cannot flood a client."); 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.4.0 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(); } } } internal static class StaticPieces { private static readonly Dictionary<int, bool> _inert = new Dictionary<int, bool>(); private static int _skipped; private static float _nextReport; private static bool _saidItWorks; private static bool Classify(GameObject prefab) { if ((Object)(object)prefab.GetComponent<Piece>() == (Object)null) { return false; } if (prefab.GetComponent<Interactable>() != null) { return false; } if (prefab.GetComponent<IHasHoverMenu>() != null) { return false; } if (prefab.GetComponent<IHasHoverMenuExtended>() != null) { return false; } if (prefab.GetComponent<IMonoUpdater>() != null) { return false; } if ((Object)(object)prefab.GetComponent<Rigidbody>() != (Object)null) { return false; } return true; } internal static bool IsInert(ZDO zdo) { if (zdo == null) { return false; } try { int prefab = zdo.GetPrefab(); if (_inert.TryGetValue(prefab, out var value)) { return value; } ZNetScene instance = ZNetScene.instance; if ((Object)(object)instance == (Object)null) { return false; } GameObject prefab2 = instance.GetPrefab(prefab); if ((Object)(object)prefab2 == (Object)null) { return false; } bool flag = Classify(prefab2); _inert[prefab] = flag; return flag; } catch { return false; } } internal static void Note() { _skipped++; Proximity.Unhand(); if (!_saidItWorks) { _saidItWorks = true; SpreadTheLoadMod.Log.LogInfo((object)"[SpreadTheLoad] keeping inert building pieces out of the proximity rule; the first one just stayed with its owner."); } if (SpreadTheLoadMod.LogActivity != null && SpreadTheLoadMod.LogActivity.Value && !(Time.unscaledTime < _nextReport)) { if (_nextReport > 0f) { SpreadTheLoadMod.Log.LogInfo((object)$"[SpreadTheLoad] left {_skipped} inert building piece(s) with their owner in the last minute."); } _nextReport = Time.unscaledTime + 60f; _skipped = 0; } } } [HarmonyPatch(typeof(ZDO), "SetOwner")] internal static class SetOwnerPatch { private static bool Prefix(ZDO __instance, long uid) { try { if (!Proximity.Approved) { return true; } if (Proximity.Candidate == 0L) { return true; } if (uid != Proximity.Candidate) { return true; } Proximity.Approved = false; if (SpreadTheLoadMod.KeepInertPieces == null || !SpreadTheLoadMod.KeepInertPieces.Value) { return true; } if (!StaticPieces.IsInert(__instance)) { return true; } StaticPieces.Note(); return false; } catch { return true; } } } internal static class Streaming { private sealed class Seen { internal int Zdos; internal float At; internal float StreamingUntil = -1f; } private const int StreamingZdosPerSecond = 150; private const float TailSeconds = 15f; private static FieldInfo _peersField; private static FieldInfo _zdosField; private static FieldInfo _peerField; private static bool _looked; private static bool _usable; private static readonly Dictionary<long, Seen> _seen = new Dictionary<long, Seen>(); private static bool Usable() { if (_looked) { return _usable; } _looked = true; try { Type type = AccessTools.Inner(typeof(ZDOMan), "ZDOPeer"); _peersField = AccessTools.Field(typeof(ZDOMan), "m_peers"); if (type != null) { _zdosField = AccessTools.Field(type, "m_zdos"); _peerField = AccessTools.Field(type, "m_peer"); } _usable = _peersField != null && _zdosField != null && _peerField != null; if (!_usable) { SpreadTheLoadMod.Log.LogWarning((object)"[SpreadTheLoad] could not reach ZDOMan's per-peer ZDO sets; a loading client may be mistaken for a stalling one."); } } catch (Exception ex) { _usable = false; SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] streaming detection disabled: " + ex.Message)); } return _usable; } internal static void Tick(float now) { if (!Usable()) { return; } try { ZDOMan instance = ZDOMan.instance; if (instance == null || !(_peersField.GetValue(instance) is IList list)) { return; } HashSet<long> hashSet = new HashSet<long>(); for (int i = 0; i < list.Count; i++) { object obj = list[i]; if (obj == null) { continue; } object? value = _peerField.GetValue(obj); ZNetPeer val = (ZNetPeer)((value is ZNetPeer) ? value : null); if (val == null) { continue; } long uid = val.m_uid; hashSet.Add(uid); int num = ((_zdosField.GetValue(obj) is ICollection collection) ? collection.Count : 0); if (!_seen.TryGetValue(uid, out var value2)) { _seen[uid] = new Seen { Zdos = num, At = now, StreamingUntil = now + 15f }; continue; } float num2 = now - value2.At; if (!(num2 < 0.5f)) { float num3 = (float)(num - value2.Zdos) / num2; value2.Zdos = num; value2.At = now; if (num3 >= 150f) { value2.StreamingUntil = now + 15f; } } } if (_seen.Count <= hashSet.Count) { return; } List<long> list2 = new List<long>(); foreach (KeyValuePair<long, Seen> item in _seen) { if (!hashSet.Contains(item.Key)) { list2.Add(item.Key); } } foreach (long item2 in list2) { _seen.Remove(item2); } } catch (Exception ex) { SpreadTheLoadMod.Log.LogWarning((object)("[SpreadTheLoad] could not read per-peer ZDO counts: " + ex.Message)); _usable = false; } } internal static bool IsLoading(long uid) { if (_seen.TryGetValue(uid, out var value) && value.StreamingUntil >= 0f) { return Time.unscaledTime <= value.StreamingUntil; } return false; } } }