using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using ServerSync; using SmoothServer.Map; using SmoothServer.Net; using Splatform; using Steamworks; using TMPro; using UnityEngine; using ZstdSharp; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: IgnoresAccessChecksTo("assembly_guiutils")] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: IgnoresAccessChecksTo("com.rlabrecque.steamworks.net")] [assembly: AssemblyCompany("SmoothServer")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.3.1.0")] [assembly: AssemblyInformationalVersion("0.3.1+ddd86e171e72d6f9592d3315cc1062b43d8e8398")] [assembly: AssemblyProduct("SmoothServer")] [assembly: AssemblyTitle("SmoothServer")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.3.1.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 ServerSync { [PublicAPI] public abstract class OwnConfigEntryBase { public object? LocalBaseValue; public bool SynchronizedConfig = true; public abstract ConfigEntryBase BaseConfig { get; } } [PublicAPI] public class SyncedConfigEntry(ConfigEntry sourceConfig) : OwnConfigEntryBase() { public readonly ConfigEntry SourceConfig = sourceConfig; public override ConfigEntryBase BaseConfig => (ConfigEntryBase)(object)SourceConfig; public T Value { get { return SourceConfig.Value; } set { SourceConfig.Value = value; } } public void AssignLocalValue(T value) { if (LocalBaseValue == null) { Value = value; } else { LocalBaseValue = value; } } } public abstract class CustomSyncedValueBase { public object? LocalBaseValue; public readonly string Identifier; public readonly Type Type; private object? boxedValue; protected bool localIsOwner; public readonly int Priority; public object? BoxedValue { get { return boxedValue; } set { boxedValue = value; this.ValueChanged?.Invoke(); } } public event Action? ValueChanged; protected CustomSyncedValueBase(ConfigSync configSync, string identifier, Type type, int priority) { Priority = priority; Identifier = identifier; Type = type; configSync.AddCustomValue(this); localIsOwner = configSync.IsSourceOfTruth; configSync.SourceOfTruthChanged += delegate(bool truth) { localIsOwner = truth; }; } } [PublicAPI] public sealed class CustomSyncedValue : CustomSyncedValueBase { public T Value { get { return (T)base.BoxedValue; } set { base.BoxedValue = value; } } public CustomSyncedValue(ConfigSync configSync, string identifier, T value = default(T), int priority = 0) : base(configSync, identifier, typeof(T), priority) { Value = value; } public void AssignLocalValue(T value) { if (localIsOwner) { Value = value; } else { LocalBaseValue = value; } } } internal class ConfigurationManagerAttributes { [UsedImplicitly] public bool? ReadOnly = false; } [PublicAPI] public class ConfigSync { [HarmonyPatch(typeof(ZRpc), "HandlePackage")] private static class SnatchCurrentlyHandlingRPC { public static ZRpc? currentRpc; [HarmonyPrefix] private static void Prefix(ZRpc __instance) { currentRpc = __instance; } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class RegisterRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { isServer = __instance.IsServer(); foreach (ConfigSync configSync2 in configSyncs) { ZRoutedRpc.instance.Register(configSync2.Name + " ConfigSync", (Action)configSync2.RPC_FromOtherClientConfigSync); if (isServer) { configSync2.InitialSyncDone = true; Debug.Log((object)("Registered '" + configSync2.Name + " ConfigSync' RPC - waiting for incoming connections")); } } if (isServer) { ((MonoBehaviour)__instance).StartCoroutine(WatchAdminListChanges()); } static void SendAdmin(List peers, bool isAdmin) { ZPackage package = ConfigsToPackage(null, null, new PackageEntry[1] { new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = isAdmin } }); ConfigSync configSync = configSyncs.First(); if (configSync != null) { ((MonoBehaviour)ZNet.instance).StartCoroutine(configSync.sendZPackage(peers, package)); } } static IEnumerator WatchAdminListChanges() { MethodInfo listContainsId = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); List CurrentList = new List(adminList.GetList()); while (true) { yield return (object)new WaitForSeconds(30f); if (!adminList.GetList().SequenceEqual(CurrentList)) { CurrentList = new List(adminList.GetList()); List list = ZNet.instance.GetPeers().Where(delegate(ZNetPeer p) { string hostName = p.m_rpc.GetSocket().GetHostName(); return ((object)listContainsId != null) ? ((bool)listContainsId.Invoke(ZNet.instance, new object[2] { adminList, hostName })) : adminList.Contains(hostName); }).ToList(); SendAdmin(ZNet.instance.GetPeers().Except(list).ToList(), isAdmin: false); SendAdmin(list, isAdmin: true); } } } } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class RegisterClientRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { return; } foreach (ConfigSync configSync in configSyncs) { peer.m_rpc.Register(configSync.Name + " ConfigSync", (Action)configSync.RPC_FromServerConfigSync); } } } private class ParsedConfigs { public readonly Dictionary configValues = new Dictionary(); public readonly Dictionary customValues = new Dictionary(); } [HarmonyPatch(typeof(ZNet), "Shutdown")] private class ResetConfigsOnShutdown { [HarmonyPostfix] private static void Postfix() { ProcessingServerUpdate = true; foreach (ConfigSync configSync in configSyncs) { configSync.resetConfigsFromServer(); configSync.IsSourceOfTruth = true; configSync.InitialSyncDone = false; } ProcessingServerUpdate = false; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private class SendConfigsAfterLogin { private class BufferingSocket : ZPlayFabSocket, ISocket { public volatile bool finished; public volatile int versionMatchQueued = -1; public readonly List Package = new List(); public readonly ISocket Original; public BufferingSocket(ISocket original) { Original = original; ((ZPlayFabSocket)this)..ctor(); } public bool IsConnected() { return Original.IsConnected(); } public ZPackage Recv() { return Original.Recv(); } public int GetSendQueueSize() { return Original.GetSendQueueSize(); } public int GetCurrentSendRate() { return Original.GetCurrentSendRate(); } public bool IsHost() { return Original.IsHost(); } public void Dispose() { Original.Dispose(); } public bool GotNewData() { return Original.GotNewData(); } public void Close() { Original.Close(); } public string GetEndPointString() { return Original.GetEndPointString(); } public void GetAndResetStats(out int totalSent, out int totalRecv) { Original.GetAndResetStats(ref totalSent, ref totalRecv); } public void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { Original.GetConnectionQuality(ref localQuality, ref remoteQuality, ref ping, ref outByteSec, ref inByteSec); } public ISocket Accept() { return Original.Accept(); } public int GetHostPort() { return Original.GetHostPort(); } public bool Flush() { return Original.Flush(); } public string GetHostName() { return Original.GetHostName(); } public void VersionMatch() { if (finished) { Original.VersionMatch(); } else { versionMatchQueued = Package.Count; } } public void Send(ZPackage pkg) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown int pos = pkg.GetPos(); pkg.SetPos(0); int num = pkg.ReadInt(); if ((num == StringExtensionMethods.GetStableHashCode("PeerInfo") || num == StringExtensionMethods.GetStableHashCode("RoutedRPC") || num == StringExtensionMethods.GetStableHashCode("ZDOData")) && !finished) { ZPackage val = new ZPackage(pkg.GetArray()); val.SetPos(pos); Package.Add(val); } else { pkg.SetPos(pos); Original.Send(pkg); } } } [HarmonyPriority(800)] [HarmonyPrefix] private static void Prefix(ref Dictionary? __state, ZNet __instance, ZRpc rpc) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) if (!__instance.IsServer()) { return; } BufferingSocket bufferingSocket = new BufferingSocket(rpc.GetSocket()); AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket); object? obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (val != null && (int)ZNet.m_onlineBackend != 0) { FieldInfo fieldInfo = AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket"); object? value = fieldInfo.GetValue(val); ZPlayFabSocket val2 = (ZPlayFabSocket)((value is ZPlayFabSocket) ? value : null); if (val2 != null) { typeof(ZPlayFabSocket).GetField("m_remotePlayerId").SetValue(bufferingSocket, val2.m_remotePlayerId); } fieldInfo.SetValue(val, bufferingSocket); } if (__state == null) { __state = new Dictionary(); } __state[Assembly.GetExecutingAssembly()] = bufferingSocket; } [HarmonyPostfix] private static void Postfix(Dictionary __state, ZNet __instance, ZRpc rpc) { ZNetPeer peer; if (__instance.IsServer()) { object obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); peer = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (peer == null) { SendBufferedData(); } else { ((MonoBehaviour)__instance).StartCoroutine(sendAsync()); } } void SendBufferedData() { if (rpc.GetSocket() is BufferingSocket bufferingSocket) { AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket.Original); object? obj2 = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj2 is ZNetPeer) ? obj2 : null); if (val != null) { AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket").SetValue(val, bufferingSocket.Original); } } BufferingSocket bufferingSocket2 = __state[Assembly.GetExecutingAssembly()]; bufferingSocket2.finished = true; for (int i = 0; i < bufferingSocket2.Package.Count; i++) { if (i == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } bufferingSocket2.Original.Send(bufferingSocket2.Package[i]); } if (bufferingSocket2.Package.Count == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } } IEnumerator sendAsync() { foreach (ConfigSync configSync in configSyncs) { List list = new List(); if (configSync.CurrentVersion != null) { list.Add(new PackageEntry { section = "Internal", key = "serverversion", type = typeof(string), value = configSync.CurrentVersion }); } MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList val = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); list.Add(new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = (((object)methodInfo == null) ? ((object)val.Contains(rpc.GetSocket().GetHostName())) : methodInfo.Invoke(ZNet.instance, new object[2] { val, rpc.GetSocket().GetHostName() })) }); ZPackage package = ConfigsToPackage(configSync.allConfigs.Select((OwnConfigEntryBase c) => c.BaseConfig), configSync.allCustomValues, list, partial: false); yield return ((MonoBehaviour)__instance).StartCoroutine(configSync.sendZPackage(new List { peer }, package)); } SendBufferedData(); } } } private class PackageEntry { public string section; public string key; public Type type; public object? value; } [HarmonyPatch(typeof(ConfigEntryBase), "GetSerializedValue")] private static class PreventSavingServerInfo { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, ref string __result) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || isWritableConfig(ownConfigEntryBase)) { return true; } __result = TomlTypeConverter.ConvertToString(ownConfigEntryBase.LocalBaseValue, __instance.SettingType); return false; } } [HarmonyPatch(typeof(ConfigEntryBase), "SetSerializedValue")] private static class PreventConfigRereadChangingValues { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, string value) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || ownConfigEntryBase.LocalBaseValue == null) { return true; } try { ownConfigEntryBase.LocalBaseValue = TomlTypeConverter.ConvertToValue(value, __instance.SettingType); } catch (Exception ex) { Debug.LogWarning((object)$"Config value of setting \"{__instance.Definition}\" could not be parsed and will be ignored. Reason: {ex.Message}; Value: {value}"); } return false; } } private class InvalidDeserializationTypeException : Exception { public string expected; public string received; public string field = ""; } public static bool ProcessingServerUpdate; public readonly string Name; public string? DisplayName; public string? CurrentVersion; public string? MinimumRequiredVersion; public bool ModRequired; private bool? forceConfigLocking; private bool isSourceOfTruth = true; private static readonly HashSet configSyncs; private readonly HashSet allConfigs = new HashSet(); private HashSet allCustomValues = new HashSet(); private static bool isServer; private static bool lockExempt; private OwnConfigEntryBase? lockedConfig; private const byte PARTIAL_CONFIGS = 1; private const byte FRAGMENTED_CONFIG = 2; private const byte COMPRESSED_CONFIG = 4; private readonly Dictionary> configValueCache = new Dictionary>(); private readonly List> cacheExpirations = new List>(); private static long packageCounter; public bool IsLocked { get { bool? flag = forceConfigLocking; bool num; if (!flag.HasValue) { if (lockedConfig == null) { goto IL_0051; } num = ((IConvertible)lockedConfig.BaseConfig.BoxedValue).ToInt32(CultureInfo.InvariantCulture) != 0; } else { num = flag == true; } if (num) { return !lockExempt; } goto IL_0051; IL_0051: return false; } set { forceConfigLocking = value; } } public bool IsAdmin { get { if (!lockExempt) { return isSourceOfTruth; } return true; } } public bool IsSourceOfTruth { get { return isSourceOfTruth; } private set { if (value != isSourceOfTruth) { isSourceOfTruth = value; this.SourceOfTruthChanged?.Invoke(value); } } } public bool InitialSyncDone { get; private set; } public event Action? SourceOfTruthChanged; private event Action? lockedConfigChanged; static ConfigSync() { ProcessingServerUpdate = false; configSyncs = new HashSet(); lockExempt = false; packageCounter = 0L; RuntimeHelpers.RunClassConstructor(typeof(VersionCheck).TypeHandle); } public ConfigSync(string name) { Name = name; configSyncs.Add(this); new VersionCheck(this); } public SyncedConfigEntry AddConfigEntry(ConfigEntry configEntry) { OwnConfigEntryBase ownConfigEntryBase = configData((ConfigEntryBase)(object)configEntry); SyncedConfigEntry syncedEntry = ownConfigEntryBase as SyncedConfigEntry; if (syncedEntry == null) { syncedEntry = new SyncedConfigEntry(configEntry); AccessTools.DeclaredField(typeof(ConfigDescription), "k__BackingField").SetValue(((ConfigEntryBase)configEntry).Description, new object[1] { new ConfigurationManagerAttributes() }.Concat(((ConfigEntryBase)configEntry).Description.Tags ?? Array.Empty()).Concat(new SyncedConfigEntry[1] { syncedEntry }).ToArray()); configEntry.SettingChanged += delegate { if (!ProcessingServerUpdate && syncedEntry.SynchronizedConfig) { Broadcast(ZRoutedRpc.Everybody, (ConfigEntryBase)configEntry); } }; allConfigs.Add(syncedEntry); } return syncedEntry; } public SyncedConfigEntry AddLockingConfigEntry(ConfigEntry lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry(lockingConfig); lockingConfig.SettingChanged += delegate { this.lockedConfigChanged?.Invoke(); }; return (SyncedConfigEntry)lockedConfig; } internal void AddCustomValue(CustomSyncedValueBase customValue) { if (allCustomValues.Select((CustomSyncedValueBase v) => v.Identifier).Concat(new string[1] { "serverversion" }).Contains(customValue.Identifier)) { throw new Exception("Cannot have multiple settings with the same name or with a reserved name (serverversion)"); } allCustomValues.Add(customValue); allCustomValues = new HashSet(allCustomValues.OrderByDescending((CustomSyncedValueBase v) => v.Priority)); customValue.ValueChanged += delegate { if (!ProcessingServerUpdate) { Broadcast(ZRoutedRpc.Everybody, customValue); } }; } private void RPC_FromServerConfigSync(ZRpc rpc, ZPackage package) { lockedConfigChanged += serverLockedSettingChanged; IsSourceOfTruth = false; if (HandleConfigSyncRPC(0L, package, clientUpdate: false)) { InitialSyncDone = true; } } private void RPC_FromOtherClientConfigSync(long sender, ZPackage package) { HandleConfigSyncRPC(sender, package, clientUpdate: true); } private bool HandleConfigSyncRPC(long sender, ZPackage package, bool clientUpdate) { //IL_0209: Unknown result type (might be due to invalid IL or missing references) //IL_0210: Expected O, but got Unknown //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Expected O, but got Unknown //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown try { if (isServer && IsLocked) { ZRpc? currentRpc = SnatchCurrentlyHandlingRPC.currentRpc; object obj; if (currentRpc == null) { obj = null; } else { ISocket socket = currentRpc.GetSocket(); obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (text != null) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList val = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); if (!(((object)methodInfo == null) ? val.Contains(text) : ((bool)methodInfo.Invoke(ZNet.instance, new object[2] { val, text })))) { return false; } } } cacheExpirations.RemoveAll(delegate(KeyValuePair kv) { if (kv.Key < DateTimeOffset.Now.Ticks) { configValueCache.Remove(kv.Value); return true; } return false; }); byte b = package.ReadByte(); if ((b & 2) != 0) { long num = package.ReadLong(); string text2 = sender.ToString() + num; if (!configValueCache.TryGetValue(text2, out SortedDictionary value)) { value = new SortedDictionary(); configValueCache[text2] = value; cacheExpirations.Add(new KeyValuePair(DateTimeOffset.Now.AddSeconds(60.0).Ticks, text2)); } int key = package.ReadInt(); int num2 = package.ReadInt(); value.Add(key, package.ReadByteArray()); if (value.Count < num2) { return false; } configValueCache.Remove(text2); package = new ZPackage(value.Values.SelectMany((byte[] a) => a).ToArray()); b = package.ReadByte(); } ProcessingServerUpdate = true; if ((b & 4) != 0) { MemoryStream stream = new MemoryStream(package.ReadByteArray()); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) { deflateStream.CopyTo(memoryStream); } package = new ZPackage(memoryStream.ToArray()); b = package.ReadByte(); } if ((b & 1) == 0) { resetConfigsFromServer(); } ParsedConfigs parsedConfigs = ReadConfigsFromPackage(package); ConfigFile val2 = null; bool saveOnConfigSet = false; foreach (KeyValuePair configValue in parsedConfigs.configValues) { if (!isServer && configValue.Key.LocalBaseValue == null) { configValue.Key.LocalBaseValue = configValue.Key.BaseConfig.BoxedValue; } if (val2 == null) { val2 = configValue.Key.BaseConfig.ConfigFile; saveOnConfigSet = val2.SaveOnConfigSet; val2.SaveOnConfigSet = false; } configValue.Key.BaseConfig.BoxedValue = configValue.Value; } if (val2 != null) { val2.SaveOnConfigSet = saveOnConfigSet; val2.Save(); } foreach (KeyValuePair customValue in parsedConfigs.customValues) { if (!isServer) { CustomSyncedValueBase key2 = customValue.Key; if (key2.LocalBaseValue == null) { key2.LocalBaseValue = customValue.Key.BoxedValue; } } customValue.Key.BoxedValue = customValue.Value; } Debug.Log((object)string.Format("Received {0} configs and {1} custom values from {2} for mod {3}", parsedConfigs.configValues.Count, parsedConfigs.customValues.Count, (isServer || clientUpdate) ? $"client {sender}" : "the server", DisplayName ?? Name)); if (!isServer) { serverLockedSettingChanged(); } return true; } finally { ProcessingServerUpdate = false; } } private ParsedConfigs ReadConfigsFromPackage(ZPackage package) { ParsedConfigs parsedConfigs = new ParsedConfigs(); Dictionary dictionary = allConfigs.Where((OwnConfigEntryBase c) => c.SynchronizedConfig).ToDictionary((OwnConfigEntryBase c) => c.BaseConfig.Definition.Section + "_" + c.BaseConfig.Definition.Key, (OwnConfigEntryBase c) => c); Dictionary dictionary2 = allCustomValues.ToDictionary((CustomSyncedValueBase c) => c.Identifier, (CustomSyncedValueBase c) => c); int num = package.ReadInt(); for (int num2 = 0; num2 < num; num2++) { string text = package.ReadString(); string text2 = package.ReadString(); string text3 = package.ReadString(); Type type = Type.GetType(text3); if (text3 == "" || type != null) { object obj; try { obj = ((text3 == "") ? null : ReadValueWithTypeFromZPackage(package, type)); } catch (InvalidDeserializationTypeException ex) { Debug.LogWarning((object)("Got unexpected struct internal type " + ex.received + " for field " + ex.field + " struct " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + ex.expected)); continue; } OwnConfigEntryBase value2; if (text == "Internal") { CustomSyncedValueBase value; if (text2 == "serverversion") { if (obj?.ToString() != CurrentVersion) { Debug.LogWarning((object)("Received server version is not equal: server version = " + (obj?.ToString() ?? "null") + "; local version = " + (CurrentVersion ?? "unknown"))); } } else if (text2 == "lockexempt") { if (obj is bool flag) { lockExempt = flag; } } else if (dictionary2.TryGetValue(text2, out value)) { if ((text3 == "" && (!value.Type.IsValueType || Nullable.GetUnderlyingType(value.Type) != null)) || GetZPackageTypeString(value.Type) == text3) { parsedConfigs.customValues[value] = obj; continue; } Debug.LogWarning((object)("Got unexpected type " + text3 + " for internal value " + text2 + " for mod " + (DisplayName ?? Name) + ", expecting " + value.Type.AssemblyQualifiedName)); } } else if (dictionary.TryGetValue(text + "_" + text2, out value2)) { Type type2 = configType(value2.BaseConfig); if ((text3 == "" && (!type2.IsValueType || Nullable.GetUnderlyingType(type2) != null)) || GetZPackageTypeString(type2) == text3) { parsedConfigs.configValues[value2] = obj; continue; } Debug.LogWarning((object)("Got unexpected type " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + type2.AssemblyQualifiedName)); } else { Debug.LogWarning((object)("Received unknown config entry " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ". This may happen if client and server versions of the mod do not match.")); } continue; } Debug.LogWarning((object)("Got invalid type " + text3 + ", abort reading of received configs")); return new ParsedConfigs(); } return parsedConfigs; } private static bool isWritableConfig(OwnConfigEntryBase config) { ConfigSync configSync = configSyncs.FirstOrDefault((ConfigSync cs) => cs.allConfigs.Contains(config)); if (configSync == null) { return true; } if (!configSync.IsSourceOfTruth && config.SynchronizedConfig && config.LocalBaseValue != null) { if (!configSync.IsLocked) { if (config == configSync.lockedConfig) { return lockExempt; } return true; } return false; } return true; } private void serverLockedSettingChanged() { foreach (OwnConfigEntryBase allConfig in allConfigs) { configAttribute(allConfig.BaseConfig).ReadOnly = !isWritableConfig(allConfig); } } private void resetConfigsFromServer() { ConfigFile val = null; bool saveOnConfigSet = false; foreach (OwnConfigEntryBase item in allConfigs.Where((OwnConfigEntryBase config) => config.LocalBaseValue != null)) { if (val == null) { val = item.BaseConfig.ConfigFile; saveOnConfigSet = val.SaveOnConfigSet; val.SaveOnConfigSet = false; } item.BaseConfig.BoxedValue = item.LocalBaseValue; item.LocalBaseValue = null; } if (val != null) { val.SaveOnConfigSet = saveOnConfigSet; } foreach (CustomSyncedValueBase item2 in allCustomValues.Where((CustomSyncedValueBase config) => config.LocalBaseValue != null)) { item2.BoxedValue = item2.LocalBaseValue; item2.LocalBaseValue = null; } lockedConfigChanged -= serverLockedSettingChanged; serverLockedSettingChanged(); } private IEnumerator distributeConfigToPeers(ZNetPeer peer, ZPackage package) { ZRoutedRpc rpc = ZRoutedRpc.instance; if (rpc == null) { yield break; } byte[] data = package.GetArray(); if (data != null && data.LongLength > 250000) { int fragments = (int)(1 + (data.LongLength - 1) / 250000); long packageIdentifier = ++packageCounter; int fragment = 0; while (fragment < fragments) { foreach (bool item in waitForQueue()) { yield return item; } if (peer.m_socket.IsConnected()) { ZPackage val = new ZPackage(); val.Write((byte)2); val.Write(packageIdentifier); val.Write(fragment); val.Write(fragments); val.Write(data.Skip(250000 * fragment).Take(250000).ToArray()); SendPackage(val); if (fragment != fragments - 1) { yield return true; } int num = fragment + 1; fragment = num; continue; } break; } yield break; } foreach (bool item2 in waitForQueue()) { yield return item2; } SendPackage(package); void SendPackage(ZPackage pkg) { string text = Name + " ConfigSync"; if (isServer) { peer.m_rpc.Invoke(text, new object[1] { pkg }); } else { rpc.InvokeRoutedRPC(peer.m_server ? 0 : peer.m_uid, text, new object[1] { pkg }); } } IEnumerable waitForQueue() { float timeout = Time.time + 30f; while (peer.m_socket.GetSendQueueSize() > 20000) { if (Time.time > timeout) { Debug.Log((object)$"Disconnecting {peer.m_uid} after 30 seconds config sending timeout"); peer.m_rpc.Invoke("Error", new object[1] { (object)(ConnectionStatus)5 }); ZNet.instance.Disconnect(peer); break; } yield return false; } } } private IEnumerator sendZPackage(long target, ZPackage package) { if (!Object.op_Implicit((Object)(object)ZNet.instance)) { return Enumerable.Empty().GetEnumerator(); } List list = (List)AccessTools.DeclaredField(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance); if (target != ZRoutedRpc.Everybody) { list = list.Where((ZNetPeer p) => p.m_uid == target).ToList(); } return sendZPackage(list, package); } private IEnumerator sendZPackage(List peers, ZPackage package) { if (!Object.op_Implicit((Object)(object)ZNet.instance)) { yield break; } byte[] array = package.GetArray(); if (array != null && array.LongLength > 10000) { ZPackage val = new ZPackage(); val.Write((byte)4); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionLevel.Optimal)) { deflateStream.Write(array, 0, array.Length); } val.Write(memoryStream.ToArray()); package = val; } List> writers = (from p in peers where p.IsReady() select distributeConfigToPeers(p, package)).ToList(); writers.RemoveAll((IEnumerator writer) => !writer.MoveNext()); while (writers.Count > 0) { yield return null; writers.RemoveAll((IEnumerator writer) => !writer.MoveNext()); } } private void Broadcast(long target, params ConfigEntryBase[] configs) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(configs); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(sendZPackage(target, package)); } } } private void Broadcast(long target, params CustomSyncedValueBase[] customValues) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(null, customValues); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(sendZPackage(target, package)); } } } private static OwnConfigEntryBase? configData(ConfigEntryBase config) { return config.Description.Tags?.OfType().SingleOrDefault(); } public static SyncedConfigEntry? ConfigData(ConfigEntry config) { return ((ConfigEntryBase)config).Description.Tags?.OfType>().SingleOrDefault(); } private static T configAttribute(ConfigEntryBase config) { return config.Description.Tags.OfType().First(); } private static Type configType(ConfigEntryBase config) { return configType(config.SettingType); } private static Type configType(Type type) { if (!type.IsEnum) { return type; } return Enum.GetUnderlyingType(type); } private static ZPackage ConfigsToPackage(IEnumerable? configs = null, IEnumerable? customValues = null, IEnumerable? packageEntries = null, bool partial = true) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown List list = configs?.Where((ConfigEntryBase config) => configData(config).SynchronizedConfig).ToList() ?? new List(); List list2 = customValues?.ToList() ?? new List(); ZPackage val = new ZPackage(); val.Write(partial ? ((byte)1) : ((byte)0)); val.Write(list.Count + list2.Count + (packageEntries?.Count() ?? 0)); foreach (PackageEntry item in packageEntries ?? Array.Empty()) { AddEntryToPackage(val, item); } foreach (CustomSyncedValueBase item2 in list2) { AddEntryToPackage(val, new PackageEntry { section = "Internal", key = item2.Identifier, type = item2.Type, value = item2.BoxedValue }); } foreach (ConfigEntryBase item3 in list) { AddEntryToPackage(val, new PackageEntry { section = item3.Definition.Section, key = item3.Definition.Key, type = configType(item3), value = item3.BoxedValue }); } return val; } private static void AddEntryToPackage(ZPackage package, PackageEntry entry) { package.Write(entry.section); package.Write(entry.key); package.Write((entry.value == null) ? "" : GetZPackageTypeString(entry.type)); AddValueToZPackage(package, entry.value); } private static string GetZPackageTypeString(Type type) { return type.AssemblyQualifiedName; } private static void AddValueToZPackage(ZPackage package, object? value) { Type type = value?.GetType(); if (value is Enum) { value = ((IConvertible)value).ToType(Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture); } else { if (value is ICollection collection) { package.Write(collection.Count); { foreach (object item in collection) { AddValueToZPackage(package, item); } return; } } if ((object)type != null && type.IsValueType && !type.IsPrimitive) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); package.Write(fields.Length); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { package.Write(GetZPackageTypeString(fieldInfo.FieldType)); AddValueToZPackage(package, fieldInfo.GetValue(value)); } return; } } ZRpc.Serialize(new object[1] { value }, ref package); } private static object ReadValueWithTypeFromZPackage(ZPackage package, Type type) { if ((object)type != null && type.IsValueType && !type.IsPrimitive && !type.IsEnum) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); int num = package.ReadInt(); if (num != fields.Length) { throw new InvalidDeserializationTypeException { received = $"(field count: {num})", expected = $"(field count: {fields.Length})" }; } object uninitializedObject = FormatterServices.GetUninitializedObject(type); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { string text = package.ReadString(); if (text != GetZPackageTypeString(fieldInfo.FieldType)) { throw new InvalidDeserializationTypeException { received = text, expected = GetZPackageTypeString(fieldInfo.FieldType), field = fieldInfo.Name }; } fieldInfo.SetValue(uninitializedObject, ReadValueWithTypeFromZPackage(package, fieldInfo.FieldType)); } return uninitializedObject; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<, >)) { int num2 = package.ReadInt(); IDictionary dictionary = (IDictionary)Activator.CreateInstance(type); Type type2 = typeof(KeyValuePair<, >).MakeGenericType(type.GenericTypeArguments); FieldInfo field = type2.GetField("key", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field2 = type2.GetField("value", BindingFlags.Instance | BindingFlags.NonPublic); for (int j = 0; j < num2; j++) { object obj = ReadValueWithTypeFromZPackage(package, type2); dictionary.Add(field.GetValue(obj), field2.GetValue(obj)); } return dictionary; } if (type != typeof(List) && type.IsGenericType) { Type type3 = typeof(ICollection<>).MakeGenericType(type.GenericTypeArguments[0]); if ((object)type3 != null && type3.IsAssignableFrom(type)) { int num3 = package.ReadInt(); object obj2 = Activator.CreateInstance(type); MethodInfo method = type3.GetMethod("Add"); for (int k = 0; k < num3; k++) { method.Invoke(obj2, new object[1] { ReadValueWithTypeFromZPackage(package, type.GenericTypeArguments[0]) }); } return obj2; } } ParameterInfo parameterInfo = (ParameterInfo)FormatterServices.GetUninitializedObject(typeof(ParameterInfo)); AccessTools.DeclaredField(typeof(ParameterInfo), "ClassImpl").SetValue(parameterInfo, type); List source = new List(); ZRpc.Deserialize(new ParameterInfo[2] { null, parameterInfo }, package, ref source); return source.First(); } } [PublicAPI] [HarmonyPatch] public class VersionCheck { private static readonly HashSet versionChecks; private static readonly Dictionary notProcessedNames; public string Name; private string? displayName; private string? currentVersion; private string? minimumRequiredVersion; public bool ModRequired = true; private string? ReceivedCurrentVersion; private string? ReceivedMinimumRequiredVersion; private readonly List ValidatedClients = new List(); private ConfigSync? ConfigSync; public string DisplayName { get { return displayName ?? Name; } set { displayName = value; } } public string CurrentVersion { get { return currentVersion ?? "0.0.0"; } set { currentVersion = value; } } public string MinimumRequiredVersion { get { string text = minimumRequiredVersion; if (text == null) { if (!ModRequired) { return "0.0.0"; } text = CurrentVersion; } return text; } set { minimumRequiredVersion = value; } } private static void PatchServerSync() { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown Patches patchInfo = PatchProcessor.GetPatchInfo((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "Awake", (Type[])null, (Type[])null)); if (patchInfo != null && patchInfo.Postfixes.Count((Patch p) => p.PatchMethod.DeclaringType == typeof(ConfigSync.RegisterRPCPatch)) > 0) { return; } Harmony val = new Harmony("org.bepinex.helpers.ServerSync"); foreach (Type item in from t in typeof(ConfigSync).GetNestedTypes(BindingFlags.NonPublic).Concat(new Type[1] { typeof(VersionCheck) }) where t.IsClass select t) { val.PatchAll(item); } } static VersionCheck() { versionChecks = new HashSet(); notProcessedNames = new Dictionary(); typeof(ThreadingHelper).GetMethod("StartSyncInvoke").Invoke(ThreadingHelper.Instance, new object[1] { new Action(PatchServerSync) }); } public VersionCheck(string name) { Name = name; ModRequired = true; versionChecks.Add(this); } public VersionCheck(ConfigSync configSync) { ConfigSync = configSync; Name = ConfigSync.Name; versionChecks.Add(this); } public void Initialize() { ReceivedCurrentVersion = null; ReceivedMinimumRequiredVersion = null; if (ConfigSync != null) { Name = ConfigSync.Name; DisplayName = ConfigSync.DisplayName; CurrentVersion = ConfigSync.CurrentVersion; MinimumRequiredVersion = ConfigSync.MinimumRequiredVersion; ModRequired = ConfigSync.ModRequired; } } private bool IsVersionOk() { if (ReceivedMinimumRequiredVersion == null || ReceivedCurrentVersion == null) { return !ModRequired; } bool num = new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion); bool flag = new Version(ReceivedCurrentVersion) >= new Version(MinimumRequiredVersion); return num && flag; } private string ErrorClient() { if (ReceivedMinimumRequiredVersion == null) { return DisplayName + " is not installed on the server."; } if (!(new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion))) { return DisplayName + " needs to be at least version " + ReceivedMinimumRequiredVersion + ". You have version " + CurrentVersion + "."; } return DisplayName + " may not be higher than version " + ReceivedCurrentVersion + ". You have version " + CurrentVersion + "."; } private string ErrorServer(ZRpc rpc) { return "Disconnect: The client (" + rpc.GetSocket().GetHostName() + ") doesn't have the correct " + DisplayName + " version " + MinimumRequiredVersion; } private string Error(ZRpc? rpc = null) { if (rpc != null) { return ErrorServer(rpc); } return ErrorClient(); } private static VersionCheck[] GetFailedClient() { return versionChecks.Where((VersionCheck check) => !check.IsVersionOk()).ToArray(); } private static VersionCheck[] GetFailedServer(ZRpc rpc) { return versionChecks.Where((VersionCheck check) => check.ModRequired && !check.ValidatedClients.Contains(rpc)).ToArray(); } private static void Logout() { Game.instance.Logout(true, true); AccessTools.DeclaredField(typeof(ZNet), "m_connectionStatus").SetValue(null, (object)(ConnectionStatus)3); } private static void DisconnectClient(ZRpc rpc) { rpc.Invoke("Error", new object[1] { 3 }); } private static void CheckVersion(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, null); } private static void CheckVersion(ZRpc rpc, ZPackage pkg, Action? original) { string text = pkg.ReadString(); string text2 = pkg.ReadString(); string text3 = pkg.ReadString(); bool flag = false; foreach (VersionCheck versionCheck in versionChecks) { if (!(text != versionCheck.Name)) { Debug.Log((object)("Received " + versionCheck.DisplayName + " version " + text3 + " and minimum version " + text2 + " from the " + (ZNet.instance.IsServer() ? "client" : "server") + ".")); versionCheck.ReceivedMinimumRequiredVersion = text2; versionCheck.ReceivedCurrentVersion = text3; if (ZNet.instance.IsServer() && versionCheck.IsVersionOk()) { versionCheck.ValidatedClients.Add(rpc); } flag = true; } } if (flag) { return; } pkg.SetPos(0); if (original != null) { original(rpc, pkg); if (pkg.GetPos() == 0) { notProcessedNames.Add(text, text3); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPrefix] private static bool RPC_PeerInfo(ZRpc rpc, ZNet __instance) { VersionCheck[] array = (__instance.IsServer() ? GetFailedServer(rpc) : GetFailedClient()); if (array.Length == 0) { return true; } VersionCheck[] array2 = array; for (int i = 0; i < array2.Length; i++) { Debug.LogWarning((object)array2[i].Error(rpc)); } if (__instance.IsServer()) { DisconnectClient(rpc); } else { Logout(); } return false; } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] [HarmonyPrefix] private static void RegisterAndCheckVersion(ZNetPeer peer, ZNet __instance) { //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Expected O, but got Unknown notProcessedNames.Clear(); IDictionary dictionary = (IDictionary)typeof(ZRpc).GetField("m_functions", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(peer.m_rpc); if (dictionary.Contains(StringExtensionMethods.GetStableHashCode("ServerSync VersionCheck"))) { object obj = dictionary[StringExtensionMethods.GetStableHashCode("ServerSync VersionCheck")]; Action action = (Action)obj.GetType().GetField("m_action", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj); peer.m_rpc.Register("ServerSync VersionCheck", (Action)delegate(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, action); }); } else { peer.m_rpc.Register("ServerSync VersionCheck", (Action)CheckVersion); } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.Initialize(); if (versionCheck.ModRequired || __instance.IsServer()) { Debug.Log((object)("Sending " + versionCheck.DisplayName + " version " + versionCheck.CurrentVersion + " and minimum version " + versionCheck.MinimumRequiredVersion + " to the " + (__instance.IsServer() ? "client" : "server") + ".")); ZPackage val = new ZPackage(); val.Write(versionCheck.Name); val.Write(versionCheck.MinimumRequiredVersion); val.Write(versionCheck.CurrentVersion); peer.m_rpc.Invoke("ServerSync VersionCheck", new object[1] { val }); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] [HarmonyPrefix] private static void RemoveDisconnected(ZNetPeer peer, ZNet __instance) { if (!__instance.IsServer()) { return; } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.ValidatedClients.Remove(peer.m_rpc); } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] [HarmonyPostfix] private static void ShowConnectionError(FejdStartup __instance) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01be: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) //IL_01de: Unknown result type (might be due to invalid IL or missing references) //IL_01e3: Unknown result type (might be due to invalid IL or missing references) //IL_01ee: Unknown result type (might be due to invalid IL or missing references) if (!__instance.m_connectionFailedPanel.activeSelf || (int)ZNet.GetConnectionStatus() != 3) { return; } bool flag = false; VersionCheck[] failedClient = GetFailedClient(); if (failedClient.Length != 0) { string text = string.Join("\n", failedClient.Select((VersionCheck check) => check.Error())); TMP_Text connectionFailedError = __instance.m_connectionFailedError; connectionFailedError.text = connectionFailedError.text + "\n" + text; flag = true; } foreach (KeyValuePair item in notProcessedNames.OrderBy, string>((KeyValuePair kv) => kv.Key)) { if (!__instance.m_connectionFailedError.text.Contains(item.Key)) { TMP_Text connectionFailedError2 = __instance.m_connectionFailedError; connectionFailedError2.text = connectionFailedError2.text + "\nServer expects you to have " + item.Key + " (Version: " + item.Value + ") installed."; flag = true; } } if (flag) { RectTransform component = ((Component)__instance.m_connectionFailedPanel.transform.Find("Image")).GetComponent(); Vector2 sizeDelta = component.sizeDelta; sizeDelta.x = 675f; component.sizeDelta = sizeDelta; __instance.m_connectionFailedError.ForceMeshUpdate(false, false); float num = __instance.m_connectionFailedError.renderedHeight + 105f; RectTransform component2 = ((Component)((Component)component).transform.Find("ButtonOk")).GetComponent(); component2.anchoredPosition = new Vector2(component2.anchoredPosition.x, component2.anchoredPosition.y - (num - component.sizeDelta.y) / 2f); sizeDelta = component.sizeDelta; sizeDelta.y = num; component.sizeDelta = sizeDelta; } } } } namespace SmoothServer { internal sealed class ConfigWatcher : IDisposable { private const double DebounceSeconds = 0.5; private const double PollIntervalSeconds = 0.5; private const double IgnoreAfterReloadSeconds = 1.0; private readonly ConfigFile _cfg; private readonly ManualLogSource _log; private readonly string _fileName; private readonly string _logPrefix; private FileSystemWatcher _fsw; private volatile bool _pending; private DateTime _lastEventUtc; private DateTime _lastReloadUtc = DateTime.MinValue; private DateTime _lastPollUtc = DateTime.MinValue; private DateTime _lastKnownWriteUtc = DateTime.MinValue; private static readonly List _statReloads = new List(); internal static List ConsumeReloadSummaries() { List result = new List(_statReloads); _statReloads.Clear(); return result; } public ConfigWatcher(ConfigFile cfg, ManualLogSource log, string logPrefix = "[Config]") { _cfg = cfg; _log = log; _logPrefix = logPrefix; _fileName = Path.GetFileName(cfg.ConfigFilePath); _lastKnownWriteUtc = SafeGetLastWriteUtc(); string directoryName = Path.GetDirectoryName(cfg.ConfigFilePath); if (string.IsNullOrEmpty(directoryName)) { _log.LogWarning((object)(_logPrefix + " watcher: could not resolve a directory for " + cfg.ConfigFilePath)); return; } try { _fsw = new FileSystemWatcher(directoryName) { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite), IncludeSubdirectories = false }; _fsw.Changed += OnFsEvent; _fsw.Created += OnFsEvent; _fsw.Renamed += OnFsRenamed; _fsw.Error += OnFsError; _fsw.EnableRaisingEvents = true; } catch (Exception ex) { _log.LogWarning((object)(_logPrefix + " could not start FileSystemWatcher, relying on polling: " + ex.Message)); _fsw = null; } _log.LogInfo((object)(_logPrefix + " watching " + cfg.ConfigFilePath + " for live edits")); } private void OnFsEvent(object sender, FileSystemEventArgs e) { if (string.Equals(e.Name, _fileName, StringComparison.OrdinalIgnoreCase)) { Schedule(); } } private void OnFsRenamed(object sender, RenamedEventArgs e) { if (string.Equals(e.Name, _fileName, StringComparison.OrdinalIgnoreCase)) { Schedule(); } } private void OnFsError(object sender, ErrorEventArgs e) { _log.LogWarning((object)(_logPrefix + " watcher error (falling back to polling): " + e.GetException())); } private void Schedule() { if (!((DateTime.UtcNow - _lastReloadUtc).TotalSeconds < 1.0)) { _pending = true; _lastEventUtc = DateTime.UtcNow; } } private DateTime SafeGetLastWriteUtc() { try { return File.GetLastWriteTimeUtc(_cfg.ConfigFilePath); } catch { return DateTime.MinValue; } } public void Pump() { if ((DateTime.UtcNow - _lastPollUtc).TotalSeconds >= 0.5) { _lastPollUtc = DateTime.UtcNow; DateTime dateTime = SafeGetLastWriteUtc(); if (dateTime != DateTime.MinValue && dateTime != _lastKnownWriteUtc) { _lastKnownWriteUtc = dateTime; Schedule(); } } if (!_pending || (DateTime.UtcNow - _lastEventUtc).TotalSeconds < 0.5) { return; } _pending = false; try { DoReload(); } catch (Exception ex) { _log.LogError((object)(_logPrefix + " reload failed: " + ex)); } } private void DoReload() { Dictionary before = Snapshot(); bool saveOnConfigSet = _cfg.SaveOnConfigSet; _cfg.SaveOnConfigSet = false; try { _cfg.Reload(); } finally { _cfg.SaveOnConfigSet = saveOnConfigSet; } _lastReloadUtc = DateTime.UtcNow; _lastKnownWriteUtc = SafeGetLastWriteUtc(); List list = Diff(before); string text = ((list.Count == 0) ? "no changes" : string.Join(", ", list.ToArray())); _statReloads.Add(text); _log.LogInfo((object)((list.Count == 0) ? (_logPrefix + " reloaded: no changes") : (_logPrefix + " reloaded: " + text))); } private Dictionary Snapshot() { Dictionary dictionary = new Dictionary(); foreach (KeyValuePair item in _cfg) { try { dictionary[item.Key] = item.Value.GetSerializedValue(); } catch { } } return dictionary; } private List Diff(Dictionary before) { List list = new List(); foreach (KeyValuePair item in _cfg) { string serializedValue; try { serializedValue = item.Value.GetSerializedValue(); } catch { continue; } if (!before.TryGetValue(item.Key, out string value)) { value = "?"; } if (value != serializedValue) { list.Add("[" + item.Key.Section + "] " + item.Key.Key + " " + value + " -> " + serializedValue); } } return list; } public void Dispose() { if (_fsw != null) { try { _fsw.EnableRaisingEvents = false; _fsw.Changed -= OnFsEvent; _fsw.Created -= OnFsEvent; _fsw.Renamed -= OnFsRenamed; _fsw.Error -= OnFsError; _fsw.Dispose(); } catch { } _fsw = null; } } } internal sealed class CreateBudgetModule : FeatureModule { private const int VanillaMax = 10; private ConfigEntry _max; internal static bool Active; internal static int MaxCreatedPerFrame = 10; public override string Name => "CreateBudget"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("CreateBudget", "Enabled", true, "Make ZNetScene's per-frame object instantiation budget configurable."); _max = cfg.Bind("CreateBudget", "MaxCreatedPerFrame", 10, "Objects ZNetScene may instantiate per frame outside the loading screen. Vanilla 10 - the default is deliberately vanilla, raise it to test."); Watch(_max); } internal static int GetMaxCreatedPerFrame() { if (!Active || !FeatureModule.ServerActive()) { return 10; } return MaxCreatedPerFrame; } protected override void ApplyPatches() { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown MaxCreatedPerFrame = Math.Max(1, _max.Value); MethodInfo methodInfo = AccessTools.Method(typeof(ZNetScene), "CreateObjects", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer CreateBudget: ZNetScene.CreateObjects not found"); } Harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(CreateBudgetModule), "Transpiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); Active = true; FeatureModule.Log.LogInfo((object)("[CreateBudget] maxCreatedPerFrame=" + MaxCreatedPerFrame + ((MaxCreatedPerFrame == 10) ? " (vanilla value, no behaviour change)" : ""))); } public override void Disable() { Active = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _max) { MaxCreatedPerFrame = Math.Max(1, _max.Value); FeatureModule.Log.LogInfo((object)("[CreateBudget] maxCreatedPerFrame=" + MaxCreatedPerFrame + ((MaxCreatedPerFrame == 10) ? " (vanilla value, no behaviour change)" : ""))); } } private static IEnumerable Transpiler(IEnumerable instructions) { List list = new List(instructions); int num = 0; for (int i = 0; i < list.Count; i++) { if (ILUtil.TryGetI4(list[i], out var value) && value == 10) { ILUtil.ReplaceWithCall(list[i], typeof(CreateBudgetModule), "GetMaxCreatedPerFrame"); num++; } } if (num != 1) { string text = "SmoothServer CreateBudget transpiler: expected exactly 1x " + 10 + " in ZNetScene.CreateObjects, found " + num + " - game IL changed, refusing to patch"; SmoothServerPlugin.Log.LogError((object)text); throw new Exception(text); } SmoothServerPlugin.Log.LogInfo((object)"[CreateBudget] transpiler OK: 1x maxCreatedPerFrame replaced (assertion 1 passed)"); return list; } } internal enum ModuleSide { Server, Client, Both } internal abstract class FeatureModule { public ConfigEntry EnabledCfg; protected Harmony Harmony; protected ConfigFile Cfg; public string Status = "not-run"; public bool Applied; public abstract string Name { get; } public virtual ModuleSide Side => ModuleSide.Server; public virtual string Section => Name; public virtual bool DefaultEnabled => true; protected virtual string EnabledDescription => "Enable the " + Name + " module."; public bool Enabled { get { if (EnabledCfg != null) { return EnabledCfg.Value; } return false; } } public bool IsActive { get { if (Applied) { return Enabled; } return false; } } protected static ManualLogSource Log => SmoothServerPlugin.Log; public virtual void Configure(ConfigFile cfg) { Cfg = cfg; EnabledCfg = BindSynced(Section, "Enabled", DefaultEnabled, EnabledDescription); Bind(); } public void TryEnable(string guidPrefix) { TryEnable(guidPrefix, SmoothServerPlugin.RunningSide); } public void TryEnable(string guidPrefix, ModuleSide runningSide) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown if (Side != ModuleSide.Both && Side != runningSide) { Applied = false; Status = "disabled(side)"; return; } if (EnabledCfg == null || !EnabledCfg.Value) { Status = "disabled"; Applied = false; return; } try { Harmony = new Harmony(guidPrefix + "." + Name); ApplyPatches(); Applied = true; Status = "applied"; Log.LogInfo((object)("[" + Name + "] applied")); } catch (Exception ex) { Applied = false; string text = ((ex.InnerException != null) ? ex.InnerException.Message : ex.Message); Status = "FAILED(" + text + ")"; Log.LogError((object)("[" + Name + "] FAILED to patch: " + ex)); Disable(); } } protected virtual void Bind() { } protected abstract void ApplyPatches(); public virtual void OnConfigChanged(ConfigEntryBase entry) { } public virtual string StatusDetail() { return null; } protected void Watch(ConfigEntry entry) { entry.SettingChanged += delegate { try { OnConfigChanged((ConfigEntryBase)(object)entry); } catch (Exception ex) { Log.LogError((object)("[" + Name + "] OnConfigChanged threw: " + ex)); } }; } public virtual void Disable() { Applied = false; try { if (Harmony != null) { Harmony.UnpatchSelf(); } } catch (Exception ex) { Log.LogWarning((object)("[" + Name + "] unpatch failed: " + ex.Message)); } } protected ConfigEntry BindSynced(string section, string key, T defaultValue, string description) { return SmoothServerPlugin.BindSynced(section, key, defaultValue, description, this); } protected ConfigEntry BindSynced(string key, T defaultValue, string description) { return SmoothServerPlugin.BindSynced(Section, key, defaultValue, description, this); } protected ConfigEntry BindLocal(string section, string key, T defaultValue, string description) { return SmoothServerPlugin.BindLocal(section, key, defaultValue, description, this); } protected ConfigEntry BindLocal(string key, T defaultValue, string description) { return SmoothServerPlugin.BindLocal(Section, key, defaultValue, description, this); } protected internal static bool ServerActive() { if ((Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } protected internal static bool ClientActive() { if ((Object)(object)ZNet.instance != (Object)null) { return !ZNet.instance.IsDedicated(); } return false; } } internal sealed class FrameRateModule : FeatureModule { private ConfigEntry _target; internal static bool Active; internal static int TargetFrameRate; private static float _sinceStart = -1f; private static bool _reasserted; public override string Name => "FrameRate"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("FrameRate", "Enabled", true, "Allow SmoothServer to set the server's frame cap. Does nothing while TargetFrameRate is 0."); _target = cfg.Bind("FrameRate", "TargetFrameRate", 0, "Server frame rate cap. 0 = leave vanilla (Unity's headless default, ~30). Set 60 or 120 to raise it. -1 = uncapped (burns a whole core)."); Watch(_target); } protected override void ApplyPatches() { TargetFrameRate = _target.Value; _sinceStart = -1f; _reasserted = false; Active = true; } public override void Disable() { Active = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _target) { TargetFrameRate = _target.Value; Reapply(); } } internal static void OnZNetStart() { if (Active) { Reapply(); } } private static void Reapply() { if (!Active) { return; } if (!FeatureModule.ServerActive()) { SmoothServerPlugin.Log.LogInfo((object)("[FrameRate] config changed to " + TargetFrameRate + " but server not active yet, will apply at ZNet.Start")); return; } int targetFrameRate = Application.targetFrameRate; int vSyncCount = QualitySettings.vSyncCount; if (TargetFrameRate == 0) { SmoothServerPlugin.Log.LogInfo((object)("[FrameRate] TargetFrameRate=0, leaving vanilla (Application.targetFrameRate=" + targetFrameRate + ", vSyncCount=" + vSyncCount + ")")); return; } Application.targetFrameRate = TargetFrameRate; QualitySettings.vSyncCount = 0; _sinceStart = 0f; _reasserted = false; SmoothServerPlugin.Log.LogInfo((object)("[FrameRate] targetFrameRate " + targetFrameRate + " -> " + Application.targetFrameRate + ", vSyncCount " + vSyncCount + " -> " + QualitySettings.vSyncCount)); } internal static void Tick(float dt) { if (Active && !_reasserted && !(_sinceStart < 0f)) { _sinceStart += dt; if (!(_sinceStart < 5f)) { _reasserted = true; int targetFrameRate = Application.targetFrameRate; Application.targetFrameRate = TargetFrameRate; QualitySettings.vSyncCount = 0; SmoothServerPlugin.Log.LogInfo((object)("[FrameRate] re-assert after 5s: was " + targetFrameRate + ", now " + Application.targetFrameRate + ((targetFrameRate == TargetFrameRate) ? " (held)" : " (SOMETHING RESET IT)"))); } } } } internal static class ILUtil { internal static bool TryGetI4(CodeInstruction ci, out int value) { value = 0; if (ci == null || ci.operand == null) { return false; } if (ci.opcode == OpCodes.Ldc_I4 || ci.opcode == OpCodes.Ldc_I4_S) { try { value = Convert.ToInt32(ci.operand); return true; } catch { return false; } } return false; } internal static void ReplaceWithCall(CodeInstruction ci, Type owner, string method) { MethodInfo methodInfo = AccessTools.Method(owner, method, (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer: replacement method " + owner.Name + "." + method + " not found"); } ci.opcode = OpCodes.Call; ci.operand = methodInfo; } } internal sealed class AdaptiveBudgetModule : FeatureModule { private sealed class PeerBudget { public int Target; public bool Congested; public int Samples; } private ConfigEntry _floor; private ConfigEntry _ceiling; private ConfigEntry _k; private ConfigEntry _pendingBackoff; private ConfigEntry _smoothing; private ConfigEntry _logInterval; private ConfigEntry _callSiteSwap; internal static bool Active; internal static int FloorBytes = 16384; internal static int CeilingBytes = 131072; internal static float K = 2f; internal static int PendingBackoffBytes = 8192; internal static float Smoothing = 0.3f; internal static bool UseCallSiteSwap = false; private const int PingFloorMs = 5; private const int PingCeilMs = 250; private static readonly Dictionary Budgets = new Dictionary(); private static float _logAcc; private static float LogIntervalSec = 10f; private static int _savedHighWater; private static bool _swapped; private static long _currentPeerUid; public override string Name => "AdaptiveBudget"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("AdaptiveBudget", "Enabled", true, "Replace SendBudget's single HighWaterBytes number with a per-peer target derived from that peer's live RTT and throughput, backing off on real congestion."); _floor = cfg.Bind("AdaptiveBudget", "FloorBytes", 16384, "Never budget a peer below this many bytes. Vanilla is 10240 for everyone."); _ceiling = cfg.Bind("AdaptiveBudget", "CeilingBytes", 131072, "Never budget a peer above this many bytes. Steam's own send queue starts erroring well above this; 128KB is a deliberate safety margin."); _k = cfg.Bind("AdaptiveBudget", "K", 2f, "Multiplier on the bandwidth-delay product (outBytesPerSec * RTT). 1.0 exactly fills the pipe; 2.0 leaves headroom for bursts. Clamped to 0.5-8."); _pendingBackoff = cfg.Bind("AdaptiveBudget", "PendingBackoffBytes", 8192, "Back off while a peer's Steam-side PENDING bytes (queued, not yet on the wire) exceed this. In-flight/unacked bytes deliberately do NOT trigger a back-off."); _smoothing = cfg.Bind("AdaptiveBudget", "Smoothing", 0.3f, "EMA factor for the per-peer target. 1.0 = react instantly, 0.1 = very smooth."); _logInterval = cfg.Bind("AdaptiveBudget", "LogIntervalSec", 10f, "Seconds between per-peer budget lines. 0 disables the log line."); _callSiteSwap = cfg.Bind("AdaptiveBudget", "UseCallSiteSwap", false, "Legacy application path, OFF by default. Since 0.3.0 SendBudget's GetHighWaterBytes() calls AdaptiveBudgetModule.HighWaterFor() directly, so the per-peer budget already applies. true additionally swaps SendBudgetModule.HighWaterBytes around each ZDOMan.SendZDOs call - only useful if that hook call is ever removed."); Watch(_floor); Watch(_ceiling); Watch(_k); Watch(_pendingBackoff); Watch(_smoothing); Watch(_logInterval); Watch(_callSiteSwap); } protected override void ApplyPatches() { //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Expected O, but got Unknown //IL_00cf: Expected O, but got Unknown ReadConfig(); MethodInfo methodInfo = AccessTools.Method(typeof(ZDOMan), "SendZDOs", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer AdaptiveBudget: ZDOMan.SendZDOs not found"); } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length != 2 || parameters[0].Name != "peer" || parameters[1].ParameterType != typeof(bool)) { throw new Exception("SmoothServer AdaptiveBudget: ZDOMan.SendZDOs signature changed (expected (ZDOPeer peer, bool flush), got " + parameters.Length + " params) - refusing to patch"); } Harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(AdaptiveBudgetModule), "Prefix", (Type[])null) { priority = 200 }, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(AdaptiveBudgetModule), "Finalizer", (Type[])null), (HarmonyMethod)null); Budgets.Clear(); Active = true; FeatureModule.Log.LogInfo((object)("[AdaptiveBudget] floor=" + FloorBytes + "B ceiling=" + CeilingBytes + "B K=" + K.ToString("F1") + " pendingBackoff=" + PendingBackoffBytes + "B smoothing=" + Smoothing.ToString("F2") + " apply=" + (UseCallSiteSwap ? "call-site swap of SendBudget.HighWaterBytes" : "HighWaterFor() hook"))); } public override void Disable() { Active = false; Budgets.Clear(); base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { ReadConfig(); FeatureModule.Log.LogInfo((object)("[AdaptiveBudget] floor=" + FloorBytes + "B ceiling=" + CeilingBytes + "B K=" + K.ToString("F1") + " pendingBackoff=" + PendingBackoffBytes + "B")); } private void ReadConfig() { FloorBytes = Math.Max(2048, _floor.Value); CeilingBytes = Math.Max(FloorBytes, _ceiling.Value); K = Mathf.Clamp(_k.Value, 0.5f, 8f); PendingBackoffBytes = Math.Max(512, _pendingBackoff.Value); Smoothing = Mathf.Clamp(_smoothing.Value, 0.05f, 1f); LogIntervalSec = ((_logInterval == null) ? 10f : Mathf.Max(0f, _logInterval.Value)); UseCallSiteSwap = _callSiteSwap.Value; } internal static int HighWaterFor(int configured) { if (!Active || !FeatureModule.ServerActive()) { return configured; } if (_currentPeerUid == 0L) { return configured; } if (!Budgets.TryGetValue(_currentPeerUid, out PeerBudget value) || value.Samples == 0) { return configured; } return value.Target; } internal static bool TryGetBudget(long uid, out int target, out bool congested) { if (Active && Budgets.TryGetValue(uid, out PeerBudget value) && value.Samples > 0) { target = value.Target; congested = value.Congested; return true; } target = 0; congested = false; return false; } private static void Prefix(ZDOPeer peer) { _currentPeerUid = 0L; _swapped = false; if (Active && FeatureModule.ServerActive() && peer != null && peer.m_peer != null) { _currentPeerUid = peer.m_peer.m_uid; if (UseCallSiteSwap && Budgets.TryGetValue(_currentPeerUid, out PeerBudget value) && value.Samples != 0) { _savedHighWater = SendBudgetModule.HighWaterBytes; SendBudgetModule.HighWaterBytes = value.Target; _swapped = true; } } } private static void Finalizer() { if (_swapped) { SendBudgetModule.HighWaterBytes = _savedHighWater; _swapped = false; } _currentPeerUid = 0L; } internal static void Tick(float dt) { if (!Active || !FeatureModule.ServerActive()) { return; } PeerTelemetryModule.PeerStat[] array = PeerTelemetryModule.Snapshot(); if (array.Length == 0) { if (Budgets.Count > 0) { Budgets.Clear(); } return; } int highWaterBytes = SendBudgetModule.HighWaterBytes; for (int i = 0; i < array.Length; i++) { PeerTelemetryModule.PeerStat peerStat = array[i]; if (!Budgets.TryGetValue(peerStat.Uid, out PeerBudget value)) { value = new PeerBudget { Target = highWaterBytes }; Budgets[peerStat.Uid] = value; } if (!peerStat.Valid) { value.Target = highWaterBytes; value.Samples = 0; value.Congested = false; continue; } float num = (float)Mathf.Clamp(peerStat.Ping, 5, 250) / 1000f; float num2 = peerStat.OutBytesPerSec * num; float num3 = K * num2; int num4 = peerStat.PendingReliable + peerStat.PendingUnreliable; value.Congested = num4 > PendingBackoffBytes; if (value.Congested) { num3 = Mathf.Min(num3, (float)value.Target * 0.5f); } num3 = Mathf.Clamp(num3, (float)FloorBytes, (float)CeilingBytes); value.Target = Mathf.RoundToInt(Mathf.Lerp((float)value.Target, num3, Smoothing)); value.Target = Mathf.Clamp(value.Target, FloorBytes, CeilingBytes); value.Samples++; } if (Budgets.Count > array.Length) { HashSet hashSet = new HashSet(); for (int j = 0; j < array.Length; j++) { hashSet.Add(array[j].Uid); } List list = new List(); foreach (KeyValuePair budget in Budgets) { if (!hashSet.Contains(budget.Key)) { list.Add(budget.Key); } } foreach (long item in list) { Budgets.Remove(item); } } if (LogIntervalSec <= 0f) { return; } _logAcc += dt; if (_logAcc < LogIntervalSec) { return; } _logAcc = 0f; foreach (KeyValuePair budget2 in Budgets) { PeerBudget value2 = budget2.Value; SmoothServerPlugin.Log.LogInfo((object)string.Format("[AdaptiveBudget] uid={0} target={1}B ({2}) samples={3} base={4}B", budget2.Key, value2.Target, value2.Congested ? "backing off" : "steady", value2.Samples, highWaterBytes)); } } } internal sealed class AsyncSaveModule : FeatureModule { private ConfigEntry _preSize; private ConfigEntry _logStalls; private ConfigEntry _selfTestSeconds; internal static bool Active; internal static bool PreSizeClone = true; internal static bool LogStalls = true; private static readonly Stopwatch MainThreadWatch = new Stopwatch(); private static readonly Stopwatch ThreadWatch = new Stopwatch(); private static int _lastZdoCount; private static double _lastStallMs = -1.0; private static int _statSaveCount; private static double _statMaxStallMs; private static float _selfTestAt = -1f; private static int _selfTestPhase; private static float _selfTestTimer; private static double _stallVanilla = -1.0; private static double _stallOptimised = -1.0; private static bool _savedPreSize; public override string Name => "AsyncSave"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("AsyncSave", "Enabled", true, "Measure the main-thread world-save stall and shrink it. Vanilla already writes the file on a background thread; the stall is ZDOMan.PrepareSave's ZDO clone."); _preSize = cfg.Bind("AsyncSave", "PreSizeClone", true, "Pre-size ZDOMan.GetSaveClone()'s list from the live ZDO count instead of letting it grow from zero. Same output, no reallocation storm on the main thread."); _logStalls = cfg.Bind("AsyncSave", "LogStalls", true, "Log one line per world save with the main-thread stall and the background-thread time."); _selfTestSeconds = cfg.Bind("AsyncSave", "SelfTestSeconds", 0f, "0 = off. Above 0: this many seconds after the world is up AND with 0 peers connected, force two saves - one with PreSizeClone off, one on - and log both stalls and the delta. Headless proof; leave at 0 in production."); Watch(_preSize); Watch(_logStalls); Watch(_selfTestSeconds); } protected override void ApplyPatches() { //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Expected O, but got Unknown //IL_0174: Expected O, but got Unknown //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Expected O, but got Unknown //IL_01b2: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Expected O, but got Unknown PreSizeClone = _preSize.Value; LogStalls = _logStalls.Value; MethodInfo methodInfo = AccessTools.Method(typeof(ZNet), "SaveWorld", new Type[1] { typeof(bool) }, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(ZNet), "SaveWorldThread", (Type[])null, (Type[])null); MethodInfo methodInfo3 = AccessTools.Method(typeof(ZDOMan), "GetSaveClone", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer AsyncSave: ZNet.SaveWorld(bool) not found"); } if (methodInfo2 == null) { throw new Exception("SmoothServer AsyncSave: ZNet.SaveWorldThread not found - vanilla's background save thread is gone, re-check the design"); } if (methodInfo3 == null || methodInfo3.ReturnType != typeof(List)) { throw new Exception("SmoothServer AsyncSave: ZDOMan.GetSaveClone() -> List not found"); } if (AccessTools.Field(typeof(ZDOMan), "m_objectsBySector") == null || AccessTools.Field(typeof(ZDOMan), "m_objectsByOutsideSector") == null || AccessTools.Field(typeof(ZDOMan), "m_objectsByID") == null) { throw new Exception("SmoothServer AsyncSave: ZDOMan sector/ID collections not found"); } Harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(AsyncSaveModule), "SaveWorldPrefix", (Type[])null) { priority = 800 }, new HarmonyMethod(typeof(AsyncSaveModule), "SaveWorldPostfix", (Type[])null) { priority = 0 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(AsyncSaveModule), "SaveWorldThreadPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeof(AsyncSaveModule), "GetSaveClonePrefix", (Type[])null) { priority = 600 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _selfTestAt = _selfTestSeconds.Value; _selfTestPhase = ((_selfTestAt > 0f) ? 1 : 0); _selfTestTimer = 0f; Active = true; FeatureModule.Log.LogInfo((object)("[AsyncSave] vanilla already writes the world on ZNet.SaveWorldThread; instrumenting the MAIN-THREAD half (PrepareSave). preSizeClone=" + PreSizeClone + " logStalls=" + LogStalls + ((_selfTestAt > 0f) ? (" selfTest=in " + _selfTestAt.ToString("F0") + "s") : ""))); } public override void Disable() { Active = false; _selfTestPhase = 0; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _preSize) { PreSizeClone = _preSize.Value; FeatureModule.Log.LogInfo((object)("[AsyncSave] preSizeClone -> " + PreSizeClone)); } else if ((object)entry == _logStalls) { LogStalls = _logStalls.Value; } } private static void SaveWorldPrefix() { if (Active) { _lastZdoCount = ((ZDOMan.instance != null) ? ZDOMan.instance.m_objectsByID.Count : (-1)); MainThreadWatch.Reset(); MainThreadWatch.Start(); ThreadWatch.Reset(); ThreadWatch.Start(); } } private static void SaveWorldPostfix() { if (Active) { MainThreadWatch.Stop(); _lastStallMs = MainThreadWatch.Elapsed.TotalMilliseconds; _statSaveCount++; if (_lastStallMs > _statMaxStallMs) { _statMaxStallMs = _lastStallMs; } if (LogStalls) { SmoothServerPlugin.Log.LogInfo((object)$"[AsyncSave] world save: main-thread stall {_lastStallMs:F1}ms for {_lastZdoCount} ZDOs (preSizeClone={PreSizeClone}); serialisation + file write continue on ZNet.SaveWorldThread"); } } } private static void SaveWorldThreadPostfix() { if (Active) { ThreadWatch.Stop(); if (LogStalls) { SmoothServerPlugin.Log.LogInfo((object)$"[AsyncSave] background save thread finished {ThreadWatch.Elapsed.TotalMilliseconds:F0}ms after SaveWorld started"); } } } private static bool GetSaveClonePrefix(ZDOMan __instance, ref List __result) { if (!Active || !PreSizeClone) { return true; } List list = new List(__instance.m_objectsByID.Count + 64); List[] objectsBySector = __instance.m_objectsBySector; foreach (List list2 in objectsBySector) { if (list2 == null) { continue; } for (int j = 0; j < list2.Count; j++) { ZDO val = list2[j]; if (val.Persistent) { list.Add(val.Clone()); } } } foreach (List value in __instance.m_objectsByOutsideSector.Values) { for (int k = 0; k < value.Count; k++) { ZDO val2 = value[k]; if (val2.Persistent) { list.Add(val2.Clone()); } } } __result = list; return false; } internal static void ConsumeSaveStats(out int count, out double maxStallMs) { count = _statSaveCount; maxStallMs = _statMaxStallMs; _statSaveCount = 0; _statMaxStallMs = 0.0; } internal static void Tick(float dt) { if (!Active || _selfTestPhase == 0 || !FeatureModule.ServerActive()) { return; } ZNet instance = ZNet.instance; ZDOMan instance2 = ZDOMan.instance; if ((Object)(object)instance == (Object)null || instance2 == null) { return; } _selfTestTimer += dt; switch (_selfTestPhase) { case 1: if (!(_selfTestTimer < _selfTestAt)) { if (instance2.m_peers.Count > 0) { SmoothServerPlugin.Log.LogInfo((object)("[AsyncSave] self-test skipped: " + instance2.m_peers.Count + " peer(s) connected")); _selfTestPhase = 0; break; } _savedPreSize = PreSizeClone; PreSizeClone = false; SmoothServerPlugin.Log.LogInfo((object)"[AsyncSave] self-test A: forcing a save with PreSizeClone=false"); _lastStallMs = -1.0; instance.Save(false, false, false); _selfTestTimer = 0f; _selfTestPhase = 2; } break; case 2: if (!(_lastStallMs < 0.0)) { _stallVanilla = _lastStallMs; if (!(_selfTestTimer < 5f) && !instance.IsSaving()) { PreSizeClone = true; SmoothServerPlugin.Log.LogInfo((object)"[AsyncSave] self-test B: forcing a save with PreSizeClone=true"); _lastStallMs = -1.0; instance.Save(false, false, false); _selfTestTimer = 0f; _selfTestPhase = 4; } } break; case 4: if (!(_lastStallMs < 0.0)) { _stallOptimised = _lastStallMs; if (!(_selfTestTimer < 5f) && !instance.IsSaving()) { PreSizeClone = _savedPreSize; SmoothServerPlugin.Log.LogInfo((object)$"[AsyncSave] SELF-TEST RESULT: {_lastZdoCount} ZDOs, main-thread stall vanilla-clone={_stallVanilla:F1}ms, pre-sized-clone={_stallOptimised:F1}ms, delta={_stallVanilla - _stallOptimised:F1}ms ({((_stallVanilla > 0.0) ? (100.0 * (_stallVanilla - _stallOptimised) / _stallVanilla) : 0.0):F1}%). PreSizeClone restored to {PreSizeClone}."); _selfTestPhase = 0; } } break; case 3: break; } } } internal sealed class GcThrottleModule : FeatureModule { private ConfigEntry _minInterval; private ConfigEntry _offPeak; private ConfigEntry _logSkips; internal static bool Active; internal static float MinIntervalSec = 3600f; internal static bool OffPeak = true; internal static bool LogSkips = true; private static float _lastRun = -1f; private static int _skipped; private static bool _deferred; private static int _statGcCount; public override string Name => "GcThrottle"; internal static int ConsumeGcEvents() { int statGcCount = _statGcCount; _statGcCount = 0; return statGcCount; } public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("GcThrottle", "Enabled", true, "Throttle Game.CollectResources (Resources.UnloadUnusedAssets) - vanilla's hourly whole-heap sweep, measured at ~200ms of main-thread stall."); _minInterval = cfg.Bind("GcThrottle", "MinIntervalSec", 3600f, "Minimum seconds between sweeps we allow through. Vanilla's own periodic check is 3600s but CollectResourcesCheck can fire one after only 1200s."); _offPeak = cfg.Bind("GcThrottle", "OffPeak", true, "Only allow a sweep when the server is idle: 0 peers connected and no world save in flight. A sweep refused this way is retried as soon as the server goes idle."); _logSkips = cfg.Bind("GcThrottle", "LogSkips", true, "Log when a sweep is allowed through or deferred."); Watch(_minInterval); Watch(_offPeak); Watch(_logSkips); } protected override void ApplyPatches() { //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_0073: Expected O, but got Unknown ReadConfig(); MethodInfo methodInfo = AccessTools.Method(typeof(Game), "CollectResources", new Type[1] { typeof(bool) }, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer GcThrottle: Game.CollectResources(bool) not found"); } Harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(GcThrottleModule), "Prefix", (Type[])null) { priority = 600 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _lastRun = -1f; _skipped = 0; _deferred = false; Active = true; FeatureModule.Log.LogInfo((object)("[GcThrottle] Resources.UnloadUnusedAssets throttled to at most once per " + MinIntervalSec.ToString("F0") + "s" + (OffPeak ? ", idle-server only" : ""))); } public override void Disable() { Active = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { ReadConfig(); FeatureModule.Log.LogInfo((object)("[GcThrottle] minInterval=" + MinIntervalSec.ToString("F0") + "s offPeak=" + OffPeak)); } private void ReadConfig() { MinIntervalSec = Mathf.Max(0f, _minInterval.Value); OffPeak = _offPeak.Value; LogSkips = _logSkips.Value; } private static bool Prefix() { if (!Active || !FeatureModule.ServerActive()) { return true; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (_lastRun >= 0f && realtimeSinceStartup - _lastRun < MinIntervalSec) { _skipped++; if (LogSkips) { SmoothServerPlugin.Log.LogInfo((object)("[GcThrottle] skipped UnloadUnusedAssets (" + (realtimeSinceStartup - _lastRun).ToString("F0") + "s since the last one, minimum " + MinIntervalSec.ToString("F0") + "s; " + _skipped + " skipped so far)")); } return false; } if (OffPeak && !IsIdle()) { _skipped++; if (!_deferred) { _deferred = true; if (LogSkips) { SmoothServerPlugin.Log.LogInfo((object)"[GcThrottle] deferred UnloadUnusedAssets: players online (OffPeak=true); it will run once the server is idle"); } } return false; } _deferred = false; _lastRun = realtimeSinceStartup; _statGcCount++; if (LogSkips) { SmoothServerPlugin.Log.LogInfo((object)("[GcThrottle] allowing UnloadUnusedAssets through" + ((_skipped > 0) ? (" (" + _skipped + " skipped since the last one)") : ""))); } _skipped = 0; return true; } private static bool IsIdle() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return true; } if (instance.IsSaving()) { return false; } List connectedPeers = instance.GetConnectedPeers(); if (connectedPeers != null) { return connectedPeers.Count == 0; } return true; } } internal sealed class OwnershipReleaseModule : FeatureModule { private const float VanillaIntervalSec = 2f; private ConfigEntry _interval; internal static bool Active; internal static float ReleaseIntervalSec = 0.5f; public override string Name => "OwnershipRelease"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("OwnershipRelease", "Enabled", true, "Shorten ZDOMan's ownership-release sweep interval from vanilla's 2 seconds."); _interval = cfg.Bind("OwnershipRelease", "ReleaseIntervalSec", 0.5f, "Seconds between ownership-release sweeps. Vanilla 2.0. Lower = mobs are re-claimed faster when their owner leaves a zone, at the cost of more scans per second - enable VPOServer.ReleaseScanSpeedup with this. Clamped to 0.1-10."); Watch(_interval); } internal static float GetReleaseIntervalSec() { if (!Active || !FeatureModule.ServerActive()) { return 2f; } return ReleaseIntervalSec; } protected override void ApplyPatches() { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Expected O, but got Unknown ReleaseIntervalSec = Mathf.Clamp(_interval.Value, 0.1f, 10f); MethodInfo methodInfo = AccessTools.Method(typeof(ZDOMan), "ReleaseZDOS", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer OwnershipRelease: ZDOMan.ReleaseZDOS not found"); } Harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(OwnershipReleaseModule), "Transpiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); Active = true; FeatureModule.Log.LogInfo((object)("[OwnershipRelease] releaseIntervalSec=" + ReleaseIntervalSec.ToString("F2") + " (vanilla 2.00)")); } public override void Disable() { Active = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _interval) { ReleaseIntervalSec = Mathf.Clamp(_interval.Value, 0.1f, 10f); FeatureModule.Log.LogInfo((object)("[OwnershipRelease] releaseIntervalSec -> " + ReleaseIntervalSec.ToString("F2"))); } } private static IEnumerable Transpiler(IEnumerable instructions) { List list = new List(instructions); int num = 0; for (int i = 0; i < list.Count; i++) { if (ServerIL.TryGetR4(list[i], out var value) && !(Math.Abs(value - 2f) > 0.0001f)) { ILUtil.ReplaceWithCall(list[i], typeof(OwnershipReleaseModule), "GetReleaseIntervalSec"); num++; } } if (num != 1) { string text = "SmoothServer OwnershipRelease transpiler: expected exactly 1x " + 2f + "f in ZDOMan.ReleaseZDOS, found " + num + " - game IL changed, refusing to patch"; SmoothServerPlugin.Log.LogError((object)text); throw new Exception(text); } SmoothServerPlugin.Log.LogInfo((object)"[OwnershipRelease] transpiler OK: 1x releaseZDOTimer threshold replaced (assertion 1 passed)"); return list; } } internal static class ServerIL { internal static bool TryGetR4(CodeInstruction ci, out float value) { value = 0f; if (ci == null || ci.operand == null) { return false; } if (ci.opcode != OpCodes.Ldc_R4) { return false; } try { value = Convert.ToSingle(ci.operand); return true; } catch { return false; } } } internal sealed class PeerTelemetryModule : FeatureModule { internal struct PeerStat { public long Uid; public string PlayerName; public bool Valid; public int Ping; public float QualityLocal; public float QualityRemote; public float OutBytesPerSec; public float InBytesPerSec; public int PendingReliable; public int PendingUnreliable; public int SentUnackedReliable; public int SendRateBytesPerSec; public int SocketQueueBytes; public int ZdoQueue; public int ForceSend; public int InvalidSector; public float SampledAt; } private ConfigEntry _interval; private ConfigEntry _sampleInterval; internal static bool Active; internal static float IntervalSec = 10f; internal static float SampleIntervalSec = 1f; private static readonly Dictionary Stats = new Dictionary(); private static readonly List TempIds = new List(); private static float _sampleAcc; private static float _logAcc; private static int _steamIface; private static bool _ifaceLogged; private static bool _qualityAbsent; public override string Name => "PeerTelemetry"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("PeerTelemetry", "Enabled", true, "Log a per-peer network line (ping, pending/in-flight bytes, send rate, ZDO queue). Also feeds AdaptiveBudget."); _interval = cfg.Bind("PeerTelemetry", "IntervalSec", 10f, "Seconds between per-peer telemetry lines."); _sampleInterval = cfg.Bind("PeerTelemetry", "SampleIntervalSec", 1f, "Seconds between samples of the live Steam connection status. The snapshot other modules read is refreshed at this rate; the log line is printed every IntervalSec."); Watch(_interval); Watch(_sampleInterval); } protected override void ApplyPatches() { IntervalSec = Mathf.Max(1f, _interval.Value); SampleIntervalSec = Mathf.Clamp(_sampleInterval.Value, 0.1f, 10f); Stats.Clear(); _sampleAcc = 0f; _logAcc = 0f; Active = true; FeatureModule.Log.LogInfo((object)("[PeerTelemetry] sampling every " + SampleIntervalSec.ToString("F1") + "s, logging every " + IntervalSec.ToString("F1") + "s (no patches)")); } public override void Disable() { Active = false; Stats.Clear(); base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _interval) { IntervalSec = Mathf.Max(1f, _interval.Value); } else { if ((object)entry != _sampleInterval) { return; } SampleIntervalSec = Mathf.Clamp(_sampleInterval.Value, 0.1f, 10f); } _logAcc = 0f; FeatureModule.Log.LogInfo((object)("[PeerTelemetry] interval=" + IntervalSec.ToString("F1") + "s sample=" + SampleIntervalSec.ToString("F1") + "s")); } internal static bool TryGet(long uid, out PeerStat stat) { return Stats.TryGetValue(uid, out stat); } internal static PeerStat[] Snapshot() { PeerStat[] array = new PeerStat[Stats.Count]; Stats.Values.CopyTo(array, 0); return array; } internal static void Tick(float dt) { if (Active && FeatureModule.ServerActive()) { _sampleAcc += dt; _logAcc += dt; if (_sampleAcc >= SampleIntervalSec) { _sampleAcc = 0f; Sample(); } if (_logAcc >= IntervalSec) { _logAcc = 0f; Emit(); } } } private static void Sample() { //IL_01b1: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01cd: Unknown result type (might be due to invalid IL or missing references) //IL_01db: Unknown result type (might be due to invalid IL or missing references) //IL_01f2: Unknown result type (might be due to invalid IL or missing references) //IL_020e: Unknown result type (might be due to invalid IL or missing references) //IL_022a: Unknown result type (might be due to invalid IL or missing references) //IL_0246: Unknown result type (might be due to invalid IL or missing references) //IL_0262: Unknown result type (might be due to invalid IL or missing references) ZDOMan instance = ZDOMan.instance; if (instance == null) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; TempIds.Clear(); float qualityLocal = default(float); float qualityRemote = default(float); int ping = default(int); float outBytesPerSec = default(float); float inBytesPerSec = default(float); for (int i = 0; i < instance.m_peers.Count; i++) { ZDOPeer val = instance.m_peers[i]; if (val == null || val.m_peer == null) { continue; } PeerStat value = new PeerStat { Uid = val.m_peer.m_uid, PlayerName = (string.IsNullOrEmpty(val.m_peer.m_playerName) ? "?" : val.m_peer.m_playerName), ZdoQueue = val.m_zdos.Count, ForceSend = val.m_forceSend.Count, InvalidSector = val.m_invalidSector.Count, SampledAt = realtimeSinceStartup }; ISocket socket = val.m_peer.m_socket; if (socket != null) { try { value.SocketQueueBytes = socket.GetSendQueueSize(); } catch { value.SocketQueueBytes = -1; } } ZSteamSocket val2 = (ZSteamSocket)(object)((socket is ZSteamSocket) ? socket : null); if (val2 != null) { if (!_qualityAbsent) { try { val2.GetConnectionQuality(ref qualityLocal, ref qualityRemote, ref ping, ref outBytesPerSec, ref inBytesPerSec); value.QualityLocal = qualityLocal; value.QualityRemote = qualityRemote; value.Ping = ping; value.OutBytesPerSec = outBytesPerSec; value.InBytesPerSec = inBytesPerSec; } catch (Exception ex) { if (ex.Message != null && ex.Message.IndexOf("not initialized", StringComparison.OrdinalIgnoreCase) >= 0) { _qualityAbsent = true; SmoothServerPlugin.Log.LogInfo((object)("[PeerTelemetry] ZSteamSocket.GetConnectionQuality is unusable on this build (" + ex.Message.Trim() + ") - ping/quality come from GetConnectionRealTimeStatus instead")); } } } if (TryRealTimeStatus(val2, out var status)) { value.Valid = true; value.PendingReliable = status.m_cbPendingReliable; value.PendingUnreliable = status.m_cbPendingUnreliable; value.SentUnackedReliable = status.m_cbSentUnackedReliable; value.SendRateBytesPerSec = status.m_nSendRateBytesPerSecond; if (value.Ping == 0) { value.Ping = status.m_nPing; } if (value.QualityLocal == 0f) { value.QualityLocal = status.m_flConnectionQualityLocal; } if (value.QualityRemote == 0f) { value.QualityRemote = status.m_flConnectionQualityRemote; } if (value.OutBytesPerSec == 0f) { value.OutBytesPerSec = status.m_flOutBytesPerSec; } if (value.InBytesPerSec == 0f) { value.InBytesPerSec = status.m_flInBytesPerSec; } } } Stats[value.Uid] = value; TempIds.Add(value.Uid); } if (Stats.Count == TempIds.Count) { return; } List list = new List(); foreach (KeyValuePair stat in Stats) { if (!TempIds.Contains(stat.Key)) { list.Add(stat.Key); } } foreach (long item in list) { Stats.Remove(item); } } private static bool TryRealTimeStatus(ZSteamSocket zs, out SteamNetConnectionRealTimeStatus_t status) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) status = default(SteamNetConnectionRealTimeStatus_t); SteamNetConnectionRealTimeLaneStatus_t lanes = default(SteamNetConnectionRealTimeLaneStatus_t); if (_steamIface == -1) { return false; } bool isServerSide = SmoothServerPlugin.IsServerSide; if ((_steamIface == 0 || _steamIface == ((!isServerSide) ? 1 : 2)) && Probe(isServerSide, zs, ref status, ref lanes)) { return true; } if ((_steamIface == 0 || _steamIface == (isServerSide ? 1 : 2)) && Probe(!isServerSide, zs, ref status, ref lanes)) { return true; } if (_steamIface == 0) { SetIface(-1, "neither interface answered GetConnectionRealTimeStatus"); } return false; } private static bool Probe(bool gameServer, ZSteamSocket zs, ref SteamNetConnectionRealTimeStatus_t status, ref SteamNetConnectionRealTimeLaneStatus_t lanes) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Invalid comparison between Unknown and I4 try { if ((int)(gameServer ? SteamGameServerNetworkingSockets.GetConnectionRealTimeStatus(zs.m_con, ref status, 0, ref lanes) : SteamNetworkingSockets.GetConnectionRealTimeStatus(zs.m_con, ref status, 0, ref lanes)) == 1) { SetIface((!gameServer) ? 1 : 2, gameServer ? "SteamGameServerNetworkingSockets (game-server interface)" : "SteamNetworkingSockets (user interface)"); return true; } } catch { } return false; } private static void SetIface(int iface, string what) { _steamIface = iface; if (!_ifaceLogged) { _ifaceLogged = true; SmoothServerPlugin.Log.LogInfo((object)("[PeerTelemetry] Steam real-time status source: " + what)); } } private static void Emit() { ZDOMan instance = ZDOMan.instance; int num = instance?.m_peers.Count ?? 0; if (num == 0) { SmoothServerPlugin.Log.LogInfo((object)"[PeerTelemetry] peers=0 - no peers connected, nothing to measure"); return; } SmoothServerPlugin.Log.LogInfo((object)$"[PeerTelemetry] peers={num} zdosSent/s={instance.m_zdosSentLastSec} zdosRecv/s={instance.m_zdosRecvLastSec}"); foreach (KeyValuePair stat in Stats) { PeerStat value = stat.Value; SmoothServerPlugin.Log.LogInfo((object)string.Format("[PeerTelemetry] '{0}' uid={1} ping={2}ms qual={3:F2}/{4:F2} out={5:F1}kB/s in={6:F1}kB/s pending={7}B(r)+{8}B(u) inflight={9}B steamRate={10}B/s socketQueue={11}B zdos={12} force={13} invalid={14}{15}", value.PlayerName, value.Uid, value.Ping, value.QualityLocal, value.QualityRemote, value.OutBytesPerSec / 1024f, value.InBytesPerSec / 1024f, value.PendingReliable, value.PendingUnreliable, value.SentUnackedReliable, value.SendRateBytesPerSec, value.SocketQueueBytes, value.ZdoQueue, value.ForceSend, value.InvalidSector, value.Valid ? "" : " (steam status unavailable)")); } } } internal sealed class SendQueueGuardModule : FeatureModule { private sealed class GuardState { public float BlockedUntil; public bool InEpisode; public int Failures; public int Deferrals; public int Dropped; public float LastReport; public string LastReason; } internal struct DropEvent { public string Endpoint; public int Count; } private ConfigEntry _backoffMs; private ConfigEntry _maxQueuedBytes; private ConfigEntry _reportIntervalSec; internal static bool Active; internal static float BackoffSec = 0.05f; internal static int MaxQueuedBytes; internal static float ReportIntervalSec = 30f; private const int NoCheck = int.MinValue; private static readonly Dictionary States = new Dictionary(); private static readonly List _statDrops = new List(); public override string Name => "SendQueueGuard"; internal static List ConsumeDrops() { List result = new List(_statDrops); _statDrops.Clear(); return result; } public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("SendQueueGuard", "Enabled", true, "Guard ZSteamSocket's send drain so a full/erroring Steam send queue backs off instead of throwing, and logs once per episode instead of once per frame."); _backoffMs = cfg.Bind("SendQueueGuard", "BackoffMs", 50, "After a failed drain, skip this socket's send for this many milliseconds. Clamped to 0-1000."); _maxQueuedBytes = cfg.Bind("SendQueueGuard", "MaxQueuedBytes", 0, "0 = never drop (defer only). Above 0: if the socket's own byte queue grows past this, drop oldest packages until it fits. DROPPING LOSES ZDO UPDATES - ZDOMan has already marked them as sent - so only raise this if a peer is provably wedged."); _reportIntervalSec = cfg.Bind("SendQueueGuard", "ReportIntervalSec", 30f, "Minimum seconds between repeat warnings for the same socket while it stays blocked."); Watch(_backoffMs); Watch(_maxQueuedBytes); Watch(_reportIntervalSec); } protected override void ApplyPatches() { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Expected O, but got Unknown //IL_00cc: Expected O, but got Unknown ReadConfig(); MethodInfo methodInfo = AccessTools.Method(typeof(ZSteamSocket), "SendQueuedPackages", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer SendQueueGuard: ZSteamSocket.SendQueuedPackages not found"); } if (methodInfo.GetParameters().Length != 0) { throw new Exception("SmoothServer SendQueueGuard: ZSteamSocket.SendQueuedPackages signature changed (expected no parameters) - refusing to patch"); } if (AccessTools.Field(typeof(ZSteamSocket), "m_sendQueue") == null || AccessTools.Field(typeof(ZSteamSocket), "m_totalSent") == null) { throw new Exception("SmoothServer SendQueueGuard: ZSteamSocket fields m_sendQueue/m_totalSent not both present - refusing to patch (progress is measured through them)"); } Harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(SendQueueGuardModule), "Prefix", (Type[])null) { priority = 200 }, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(SendQueueGuardModule), "Finalizer", (Type[])null), (HarmonyMethod)null); States.Clear(); Active = true; FeatureModule.Log.LogInfo((object)("[SendQueueGuard] wrapping ZSteamSocket.SendQueuedPackages (vanilla still does the send, so the build's own Steam interface is used): backoff=" + (BackoffSec * 1000f).ToString("F0") + "ms maxQueuedBytes=" + ((MaxQueuedBytes == 0) ? "unlimited (defer, never drop)" : (MaxQueuedBytes + "B (drop above)")))); } public override void Disable() { Active = false; States.Clear(); base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { ReadConfig(); FeatureModule.Log.LogInfo((object)("[SendQueueGuard] backoff=" + (BackoffSec * 1000f).ToString("F0") + "ms maxQueuedBytes=" + MaxQueuedBytes)); } private void ReadConfig() { BackoffSec = (float)Mathf.Clamp(_backoffMs.Value, 0, 1000) / 1000f; MaxQueuedBytes = Math.Max(0, _maxQueuedBytes.Value); ReportIntervalSec = Mathf.Max(1f, _reportIntervalSec.Value); } private static bool Prefix(ZSteamSocket __instance, out int __state) { __state = int.MinValue; if (!Active) { return true; } try { Queue sendQueue = __instance.m_sendQueue; if (sendQueue == null || sendQueue.Count == 0) { return true; } if (!__instance.IsConnected()) { return true; } GuardState guardState = StateFor(__instance); float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup < guardState.BlockedUntil) { guardState.Deferrals++; return false; } if (MaxQueuedBytes > 0) { Trim(guardState, sendQueue, __instance, realtimeSinceStartup); } if (sendQueue.Count > 0) { __state = __instance.m_totalSent; } } catch (Exception ex) { SmoothServerPlugin.Log.LogWarning((object)("[SendQueueGuard] guard bookkeeping failed: " + ex.Message)); __state = int.MinValue; } return true; } private static Exception Finalizer(ZSteamSocket __instance, Exception __exception, int __state) { if (!Active) { return __exception; } try { GuardState guardState = StateFor(__instance); float realtimeSinceStartup = Time.realtimeSinceStartup; if (__exception != null) { Fail(guardState, __instance, realtimeSinceStartup, "send threw: " + __exception.Message); return null; } if (__state == int.MinValue) { return null; } if (__instance.m_totalSent == __state) { Fail(guardState, __instance, realtimeSinceStartup, "send blocked, " + ((__instance.m_sendQueue != null) ? __instance.m_sendQueue.Count : (-1)) + " package(s) queued (if the log above says k_EResultLimitExceeded, lower SendBudget.HighWaterBytes or AdaptiveBudget.CeilingBytes)"); return null; } if (guardState.InEpisode) { guardState.InEpisode = false; SmoothServerPlugin.Log.LogInfo((object)("[SendQueueGuard] " + Endpoint(__instance) + " recovered after " + guardState.Failures + " failed drain(s) / " + guardState.Deferrals + " deferred frame(s) (last reason: " + guardState.LastReason + ")")); guardState.Failures = 0; guardState.Deferrals = 0; } } catch { } return null; } private static void Fail(GuardState st, ZSteamSocket s, float now, string reason) { st.Failures++; st.LastReason = reason; st.BlockedUntil = now + BackoffSec; if (!st.InEpisode || now - st.LastReport > ReportIntervalSec) { st.InEpisode = true; st.LastReport = now; SmoothServerPlugin.Log.LogWarning((object)("[SendQueueGuard] " + Endpoint(s) + " " + reason + " - deferring for " + (BackoffSec * 1000f).ToString("F0") + "ms")); } } private static void Trim(GuardState st, Queue queue, ZSteamSocket s, float now) { int num = 0; foreach (byte[] item in queue) { if (item != null) { num += item.Length; } } while (num > MaxQueuedBytes && queue.Count > 1) { byte[] array = queue.Dequeue(); if (array != null) { num -= array.Length; } st.Dropped++; } if (st.Dropped > 0 && now - st.LastReport > ReportIntervalSec) { st.LastReport = now; _statDrops.Add(new DropEvent { Endpoint = Endpoint(s), Count = st.Dropped }); SmoothServerPlugin.Log.LogWarning((object)("[SendQueueGuard] DROPPED " + st.Dropped + " queued packages for " + Endpoint(s) + " (queue above MaxQueuedBytes=" + MaxQueuedBytes + "B) - those ZDO updates are lost until they change again")); } } private static GuardState StateFor(ZSteamSocket s) { if (!States.TryGetValue(s, out GuardState value)) { value = new GuardState(); States[s] = value; if (States.Count > 64) { Prune(); } } return value; } private static void Prune() { List list = new List(); foreach (KeyValuePair state in States) { if (state.Key == null || !state.Key.IsConnected()) { list.Add(state.Key); } } foreach (ZSteamSocket item in list) { States.Remove(item); } } private static string Endpoint(ZSteamSocket s) { try { return s.GetEndPointString(); } catch { return ""; } } } internal static class ServerModules { internal static void Tick(float dt) { PeerTelemetryModule.Tick(dt); AdaptiveBudgetModule.Tick(dt); VPOServerModule.Tick(dt); AsyncSaveModule.Tick(dt); SteamRatesModule.Tick(dt); SyncListCacheModule.TickStats(dt); StatsLogModule.Tick(dt); } } internal sealed class StatsLogModule : FeatureModule { private ConfigEntry _intervalSec; private ConfigEntry _retentionDays; private ConfigEntry _dir; internal static bool Active; private static float IntervalSec = 10f; private static int RetentionDays = 30; private static string Dir; private static float _frameAcc; private static int _frameCount; private static float _frameWorst; private static float _writeAcc; private static float _pollAcc; private const float PollIntervalSec = 1f; private static readonly Dictionary KnownPeers = new Dictionary(); private static readonly Dictionary KnownCongested = new Dictionary(); private static long _lastRawOut; private static long _lastWireOut; private static long _lastRawIn; private static long _lastWireIn; private static StreamWriter _statsWriter; private static StreamWriter _eventsWriter; private static string _statsDate = ""; private static string _eventsDate = ""; public override string Name => "StatsLog"; public override string Section => "StatsLog"; protected override void Bind() { _intervalSec = BindLocal("IntervalSec", 10f, "Seconds between stats-YYYY-MM-DD.jsonl snapshot records. Hot-reloadable."); _retentionDays = BindLocal("RetentionDays", 30, "Delete stats/events files older than this many days, checked on each daily rotation. Hot-reloadable."); _dir = BindLocal("Dir", "", "Directory for stats-*.jsonl / events-*.jsonl. Empty = /smoothserver/stats/. Not hot-reloadable (needs a restart)."); Watch(_intervalSec); Watch(_retentionDays); } protected override void ApplyPatches() { IntervalSec = Mathf.Max(1f, _intervalSec.Value); RetentionDays = Math.Max(1, _retentionDays.Value); Dir = (string.IsNullOrEmpty(_dir.Value) ? Path.Combine(Paths.ConfigPath, "smoothserver", "stats") : _dir.Value); Directory.CreateDirectory(Dir); _frameAcc = 0f; _frameCount = 0; _frameWorst = 0f; _writeAcc = 0f; _pollAcc = 0f; KnownPeers.Clear(); KnownCongested.Clear(); _lastRawOut = (_lastWireOut = (_lastRawIn = (_lastWireIn = 0L))); Active = true; FeatureModule.Log.LogInfo((object)("[StatsLog] logging every " + IntervalSec.ToString("F0") + "s to " + Dir + " (retention " + RetentionDays + "d)")); } public override void Disable() { Active = false; CloseWriters(); base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _intervalSec) { IntervalSec = Mathf.Max(1f, _intervalSec.Value); FeatureModule.Log.LogInfo((object)("[StatsLog] IntervalSec -> " + IntervalSec.ToString("F0"))); } else if ((object)entry == _retentionDays) { RetentionDays = Math.Max(1, _retentionDays.Value); FeatureModule.Log.LogInfo((object)("[StatsLog] RetentionDays -> " + RetentionDays)); ApplyRetention(); } } public override string StatusDetail() { if (!Applied) { return null; } return "dir=" + Dir + " intervalSec=" + IntervalSec.ToString("F0") + " retentionDays=" + RetentionDays; } internal static void Tick(float dt) { if (!Active || !FeatureModule.ServerActive()) { return; } _frameAcc += dt; _frameCount++; if (dt > _frameWorst) { _frameWorst = dt; } _pollAcc += dt; if (_pollAcc >= 1f) { _pollAcc = 0f; try { PollForEvents(); } catch (Exception ex) { FeatureModule.Log.LogError((object)("[StatsLog] event poll failed: " + ex)); } } _writeAcc += dt; if (!(_writeAcc < IntervalSec)) { _writeAcc = 0f; try { WriteStatsRecord(); } catch (Exception ex2) { FeatureModule.Log.LogError((object)("[StatsLog] write failed: " + ex2)); } _frameAcc = 0f; _frameCount = 0; _frameWorst = 0f; } } private static void PollForEvents() { PeerTelemetryModule.PeerStat[] array = PeerTelemetryModule.Snapshot(); HashSet hashSet = new HashSet(); for (int i = 0; i < array.Length; i++) { PeerTelemetryModule.PeerStat peerStat = array[i]; hashSet.Add(peerStat.Uid); if (!KnownPeers.ContainsKey(peerStat.Uid)) { KnownPeers[peerStat.Uid] = peerStat.PlayerName; WriteEvent("join", Json.Obj(Json.KV("name", peerStat.PlayerName), Json.KV("id", ShortId(peerStat.Uid)))); } int target; bool congested; bool num = AdaptiveBudgetModule.TryGetBudget(peerStat.Uid, out target, out congested); bool value; bool flag = KnownCongested.TryGetValue(peerStat.Uid, out value); if (num) { if (!flag) { KnownCongested[peerStat.Uid] = congested; } else if (congested != value) { KnownCongested[peerStat.Uid] = congested; WriteEvent(congested ? "budget_backoff_start" : "budget_backoff_end", Json.Obj(Json.KV("name", peerStat.PlayerName), Json.KV("id", ShortId(peerStat.Uid)), Json.KV("targetBytes", target))); } } } if (hashSet.Count != KnownPeers.Count) { List list = new List(); foreach (KeyValuePair knownPeer in KnownPeers) { if (!hashSet.Contains(knownPeer.Key)) { list.Add(knownPeer.Key); } } foreach (long item in list) { WriteEvent("leave", Json.Obj(Json.KV("name", KnownPeers[item]), Json.KV("id", ShortId(item)))); KnownPeers.Remove(item); KnownCongested.Remove(item); } } AsyncSaveModule.ConsumeSaveStats(out var count, out var maxStallMs); if (count > 0) { WriteEvent("save", Json.Obj(Json.KV("count", count), Json.KV("maxStallMs", maxStallMs))); } int num2 = GcThrottleModule.ConsumeGcEvents(); for (int j = 0; j < num2; j++) { WriteEvent("gc", Json.Obj()); } List list2 = SendQueueGuardModule.ConsumeDrops(); for (int k = 0; k < list2.Count; k++) { WriteEvent("queue_drop", Json.Obj(Json.KV("endpoint", list2[k].Endpoint), Json.KV("count", list2[k].Count))); } List list3 = ConfigWatcher.ConsumeReloadSummaries(); for (int l = 0; l < list3.Count; l++) { WriteEvent("config_reload", Json.Obj(Json.KV("changes", list3[l]))); } } private static void WriteStatsRecord() { ZDOMan instance = ZDOMan.instance; ZNetScene instance2 = ZNetScene.instance; int value = instance?.m_peers.Count ?? 0; int value2 = instance?.m_objectsByID.Count ?? (-1); int value3 = instance?.m_zdosSentLastSec ?? (-1); int value4 = instance?.m_zdosRecvLastSec ?? (-1); int value5 = (((Object)(object)instance2 != (Object)null) ? instance2.m_instances.Count : (-1)); float value6 = ((_frameCount > 0) ? (_frameAcc / (float)_frameCount * 1000f) : 0f); float value7 = ((_frameAcc > 0f) ? ((float)_frameCount / _frameAcc) : 0f); float value8 = _frameWorst * 1000f; PeerTelemetryModule.PeerStat[] array = PeerTelemetryModule.Snapshot(); List list = new List(array.Length); List list2 = new List(array.Length); for (int i = 0; i < array.Length; i++) { PeerTelemetryModule.PeerStat peerStat = array[i]; list.Add(peerStat.PlayerName); int target; bool congested; bool num = AdaptiveBudgetModule.TryGetBudget(peerStat.Uid, out target, out congested); bool value9 = CompressionModule.IsFramedFor(peerStat.Uid); List list3 = new List { Json.KV("name", peerStat.PlayerName), Json.KV("id", ShortId(peerStat.Uid)), Json.KV("rttMs", peerStat.Ping), Json.KV("pendingBytes", peerStat.PendingReliable + peerStat.PendingUnreliable), Json.KV("inFlightBytes", peerStat.SentUnackedReliable), Json.KV("queuedBytes", peerStat.SocketQueueBytes), Json.KV("framed", value9) }; if (num) { list3.Add(Json.KV("budgetTargetBytes", target)); list3.Add(Json.KV("budgetCongested", congested)); } list2.Add(Json.ObjRaw(list3)); } long rawOut = CompressionModule.RawOut; long wireOut = CompressionModule.WireOut; long rawIn = CompressionModule.RawIn; long wireIn = CompressionModule.WireIn; long num2 = rawOut - _lastRawOut; long num3 = wireOut - _lastWireOut; long num4 = rawIn - _lastRawIn; long num5 = wireIn - _lastWireIn; _lastRawOut = rawOut; _lastWireOut = wireOut; _lastRawIn = rawIn; _lastWireIn = wireIn; string rawJsonValue = Json.ObjRaw(new List { Json.KV("rawOutDelta", num2), Json.KV("wireOutDelta", num3), Json.KV("ratioOut", (num2 > 0) ? ((double)num3 / (double)num2) : 0.0), Json.KV("rawInDelta", num4), Json.KV("wireInDelta", num5), Json.KV("ratioIn", (num4 > 0) ? ((double)num5 / (double)num4) : 0.0), Json.KV("framedPeers", CompressionModule.FramedPeers) }); string value10 = Json.ObjRaw(new List { Json.KV("ts", NowIso()), Json.KV("uptimeSec", Time.realtimeSinceStartup), Json.KVRaw("players", Json.ObjRaw(new List { Json.KV("count", value), Json.KVArrStr("names", list) })), Json.KV("fps", value7), Json.KV("frameAvgMs", value6), Json.KV("frameWorstMs", value8), Json.KV("zdos", value2), Json.KV("zdosSentPerSec", value3), Json.KV("zdosRecvPerSec", value4), Json.KV("sceneObjs", value5), Json.KVArrRaw("peers", list2), Json.KVRaw("compression", rawJsonValue) }); StreamWriter writer = GetWriter(ref _statsWriter, ref _statsDate, "stats"); writer.WriteLine(value10); writer.Flush(); } private static void WriteEvent(string type, string fieldsObjJson) { string text = ((fieldsObjJson.Length >= 2) ? fieldsObjJson.Substring(1, fieldsObjJson.Length - 2) : ""); string value = "{\"ts\":" + Json.Str(NowIso()) + ",\"type\":" + Json.Str(type) + ((text.Length > 0) ? ("," + text) : "") + "}"; StreamWriter writer = GetWriter(ref _eventsWriter, ref _eventsDate, "events"); writer.WriteLine(value); writer.Flush(); } private static StreamWriter GetWriter(ref StreamWriter writer, ref string lastDate, string prefix) { string text = DateTime.UtcNow.ToString("yyyy-MM-dd"); if (writer != null && lastDate == text) { return writer; } if (writer != null) { try { writer.Dispose(); } catch { } } string path = Path.Combine(Dir, prefix + "-" + text + ".jsonl"); writer = new StreamWriter(new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read)); lastDate = text; if (prefix == "stats") { ApplyRetention(); } return writer; } private static void ApplyRetention() { try { if (!Directory.Exists(Dir)) { return; } DateTime dateTime = DateTime.UtcNow.Date.AddDays(-RetentionDays); string[] files = Directory.GetFiles(Dir, "*-????-??-??.jsonl"); foreach (string text in files) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text); int num = fileNameWithoutExtension.IndexOf('-'); if (num >= 0 && fileNameWithoutExtension.Length - num - 1 == 10 && DateTime.TryParseExact(fileNameWithoutExtension.Substring(num + 1), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var result) && result < dateTime) { try { File.Delete(text); FeatureModule.Log.LogInfo((object)("[StatsLog] retention: deleted " + Path.GetFileName(text))); } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[StatsLog] retention: could not delete " + text + ": " + ex.Message)); } } } } catch (Exception ex2) { FeatureModule.Log.LogWarning((object)("[StatsLog] retention sweep failed: " + ex2.Message)); } } private static void CloseWriters() { try { if (_statsWriter != null) { _statsWriter.Dispose(); } } catch { } try { if (_eventsWriter != null) { _eventsWriter.Dispose(); } } catch { } _statsWriter = null; _eventsWriter = null; _statsDate = ""; _eventsDate = ""; } private static string NowIso() { return DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); } private static string ShortId(long uid) { int num = -2128831035; for (int i = 0; i < 8; i++) { num = (num ^ (int)((uid >> i * 8) & 0xFF)) * 16777619; } uint num2 = (uint)num; return num2.ToString("x8"); } } internal static class Json { internal static string Str(string s) { if (s == null) { return "null"; } StringBuilder stringBuilder = new StringBuilder(s.Length + 2); stringBuilder.Append('"'); foreach (char c in s) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } stringBuilder.Append('"'); return stringBuilder.ToString(); } internal static string Num(double d) { if (double.IsNaN(d) || double.IsInfinity(d)) { return "0"; } return d.ToString("0.###", CultureInfo.InvariantCulture); } internal static string KV(string key, string value) { return Str(key) + ":" + Str(value); } internal static string KV(string key, bool value) { return Str(key) + ":" + (value ? "true" : "false"); } internal static string KV(string key, int value) { return Str(key) + ":" + value.ToString(CultureInfo.InvariantCulture); } internal static string KV(string key, long value) { return Str(key) + ":" + value.ToString(CultureInfo.InvariantCulture); } internal static string KV(string key, float value) { return Str(key) + ":" + Num(value); } internal static string KV(string key, double value) { return Str(key) + ":" + Num(value); } internal static string KVArrStr(string key, List values) { string[] array = new string[values.Count]; for (int i = 0; i < values.Count; i++) { array[i] = Str(values[i]); } return Str(key) + ":[" + string.Join(",", array) + "]"; } internal static string KVArrRaw(string key, List rawJsonValues) { return Str(key) + ":[" + string.Join(",", rawJsonValues.ToArray()) + "]"; } internal static string KVRaw(string key, string rawJsonValue) { return Str(key) + ":" + rawJsonValue; } internal static string Obj(params string[] kvs) { return "{" + string.Join(",", kvs) + "}"; } internal static string ObjRaw(List kvs) { return "{" + string.Join(",", kvs.ToArray()) + "}"; } } internal sealed class SteamRatesModule : FeatureModule { private const int VanillaRate = 153600; private ConfigEntry _sendRateMax; private ConfigEntry _sendRateMin; private ConfigEntry _writeGameServerUtils; private ConfigEntry _writeUserUtils; internal static bool Active; internal static int SendRateMax = 1048576; internal static int SendRateMin; internal static bool WriteGameServerUtils = true; internal static bool WriteUserUtils = true; private static bool _pending; private static bool _appliedOnce; private static bool _gsAbsent; private static bool _userAbsent; public override string Name => "SteamRates"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("SteamRates", "Enabled", true, "Raise Steam's per-connection SendRateMax above vanilla's 153600 B/s."); _sendRateMax = cfg.Bind("SteamRates", "SendRateMax", 1048576, "Upper bound (bytes/sec) on Steam's per-connection bandwidth estimate. Vanilla 153600. Raising this only lets the estimator climb during bursts - it is a ceiling, not a target. 0 = leave vanilla."); _sendRateMin = cfg.Bind("SteamRates", "SendRateMin", 0, "Lower bound (bytes/sec). 0 = LEAVE VANILLA (recommended). Raising this forbids Steam's congestion control from backing off for a peer on a weak link, which converts congestion into buffering and loss. BetterNetworking couples this to SendRateMax; we deliberately do not."); _writeGameServerUtils = cfg.Bind("SteamRates", "WriteGameServerUtils", true, "Try SteamGameServerNetworkingUtils - the interface vanilla ZSteamSocket uses on the dedicated-server build. Not initialised in a client process; refusing there is expected and is not logged as a problem."); _writeUserUtils = cfg.Bind("SteamRates", "WriteUserUtils", true, "Try SteamNetworkingUtils - the interface vanilla ZSteamSocket uses on the client build. Not initialised in a dedicated-server process; refusing there is expected and is not logged as a problem."); Watch(_sendRateMax); Watch(_sendRateMin); Watch(_writeGameServerUtils); Watch(_writeUserUtils); } protected override void ApplyPatches() { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown ReadConfig(); MethodInfo methodInfo = AccessTools.Method(typeof(ZSteamSocket), "RegisterGlobalCallbacks", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer SteamRates: ZSteamSocket.RegisterGlobalCallbacks not found"); } Harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SteamRatesModule), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Active = true; _pending = true; FeatureModule.Log.LogInfo((object)("[SteamRates] SendRateMax=" + ((SendRateMax == 0) ? ("vanilla(" + 153600 + ")") : SendRateMax.ToString()) + " SendRateMin=" + ((SendRateMin == 0) ? ("vanilla(" + 153600 + ", untouched)") : SendRateMin.ToString()) + " writeGameServerUtils=" + WriteGameServerUtils + " writeUserUtils=" + WriteUserUtils)); } public override void Disable() { Active = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { ReadConfig(); _pending = true; FeatureModule.Log.LogInfo((object)("[SteamRates] config changed -> SendRateMax=" + SendRateMax + " SendRateMin=" + SendRateMin)); } private void ReadConfig() { SendRateMax = Math.Max(0, _sendRateMax.Value); SendRateMin = Math.Max(0, _sendRateMin.Value); WriteGameServerUtils = _writeGameServerUtils.Value; WriteUserUtils = _writeUserUtils.Value; } private static void Postfix() { if (Active) { _pending = true; } } internal static void Tick(float dt) { if (Active && _pending && FeatureModule.ServerActive()) { _pending = false; Apply(); } } private static void Apply() { int num = ReadValue(gameServer: false, (ESteamNetworkingConfigValue)11); int num2 = ReadValue(gameServer: true, (ESteamNetworkingConfigValue)11); SmoothServerPlugin.Log.LogInfo((object)("[SteamRates] before: SendRateMax userUtils=" + Show(num) + " gameServerUtils=" + Show(num2))); bool flag = false; if (SendRateMax > 0) { flag |= Write((ESteamNetworkingConfigValue)11, SendRateMax); } if (SendRateMin > 0) { flag |= Write((ESteamNetworkingConfigValue)10, SendRateMin); } int num3 = ReadValue(gameServer: false, (ESteamNetworkingConfigValue)11); int num4 = ReadValue(gameServer: true, (ESteamNetworkingConfigValue)11); int v = ReadValue(gameServer: false, (ESteamNetworkingConfigValue)10); int v2 = ReadValue(gameServer: true, (ESteamNetworkingConfigValue)10); SmoothServerPlugin.Log.LogInfo((object)("[SteamRates] applied=" + flag + " -> SendRateMax userUtils=" + Show(num3) + " gameServerUtils=" + Show(num4) + " | SendRateMin userUtils=" + Show(v) + " gameServerUtils=" + Show(v2) + " (vanilla is " + 153600 + " for both)")); if (!_appliedOnce) { _appliedOnce = true; if (num3 == int.MinValue || num4 == int.MinValue) { SmoothServerPlugin.Log.LogInfo((object)"[SteamRates] only one utils interface exists in this process, so the two config stores cannot be compared here"); } else if (num == num2 && num3 == num4 && num3 != num) { SmoothServerPlugin.Log.LogInfo((object)"[SteamRates] the two utils interfaces track the same value on this build"); } else if (num3 != num4) { SmoothServerPlugin.Log.LogInfo((object)"[SteamRates] the two utils interfaces have SEPARATE config stores on this build"); } } } private static string Show(int v) { if (v != int.MinValue) { return v.ToString(); } return "n/a"; } private static void MarkAbsent(ref bool flag, string which) { if (!flag) { flag = true; SmoothServerPlugin.Log.LogInfo((object)("[SteamRates] " + which + " is not initialised in this process (expected on the " + (SmoothServerPlugin.IsServerSide ? "dedicated-server" : "client") + " build) - not using it again")); } } private static bool Write(ESteamNetworkingConfigValue key, int value) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) bool flag = false; GCHandle gCHandle = GCHandle.Alloc(value, GCHandleType.Pinned); try { if (WriteGameServerUtils && !_gsAbsent) { try { flag |= SteamGameServerNetworkingUtils.SetConfigValue(key, (ESteamNetworkingConfigScope)1, IntPtr.Zero, (ESteamNetworkingConfigDataType)1, gCHandle.AddrOfPinnedObject()); } catch { MarkAbsent(ref _gsAbsent, "SteamGameServerNetworkingUtils"); } } if (WriteUserUtils && !_userAbsent) { try { flag |= SteamNetworkingUtils.SetConfigValue(key, (ESteamNetworkingConfigScope)1, IntPtr.Zero, (ESteamNetworkingConfigDataType)1, gCHandle.AddrOfPinnedObject()); } catch { MarkAbsent(ref _userAbsent, "SteamNetworkingUtils"); } } } finally { gCHandle.Free(); } return flag; } private static int ReadValue(bool gameServer, ESteamNetworkingConfigValue key) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Invalid comparison between Unknown and I4 //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Invalid comparison between Unknown and I4 if (gameServer ? _gsAbsent : _userAbsent) { return int.MinValue; } IntPtr intPtr = Marshal.AllocHGlobal(4); try { Marshal.WriteInt32(intPtr, 0); ulong num = 4uL; ESteamNetworkingConfigDataType val2 = default(ESteamNetworkingConfigDataType); ESteamNetworkingGetConfigValueResult val = ((!gameServer) ? SteamNetworkingUtils.GetConfigValue(key, (ESteamNetworkingConfigScope)1, IntPtr.Zero, ref val2, intPtr, ref num) : SteamGameServerNetworkingUtils.GetConfigValue(key, (ESteamNetworkingConfigScope)1, IntPtr.Zero, ref val2, intPtr, ref num)); if ((int)val != 1 && (int)val != 2) { return int.MinValue; } return Marshal.ReadInt32(intPtr); } catch { if (gameServer) { MarkAbsent(ref _gsAbsent, "SteamGameServerNetworkingUtils"); } else { MarkAbsent(ref _userAbsent, "SteamNetworkingUtils"); } return int.MinValue; } finally { Marshal.FreeHGlobal(intPtr); } } } internal static class SteamSelfTest { internal static void Run() { ManualLogSource log = SmoothServerPlugin.Log; log.LogInfo((object)("[SteamSelfTest] ---- begin (side=" + (SmoothServerPlugin.IsServerSide ? "SERVER" : "CLIENT") + ") ----")); Probe("SteamGameServer.GetSteamID()", () => ((object)SteamGameServer.GetSteamID()/*cast due to .constrained prefix*/).ToString()); Probe("SteamUser.GetSteamID()", () => ((object)SteamUser.GetSteamID()/*cast due to .constrained prefix*/).ToString()); Probe("SteamGameServerNetworkingUtils.GetLocalTimestamp()", () => ((object)SteamGameServerNetworkingUtils.GetLocalTimestamp()/*cast due to .constrained prefix*/).ToString()); Probe("SteamNetworkingUtils.GetLocalTimestamp()", () => ((object)SteamNetworkingUtils.GetLocalTimestamp()/*cast due to .constrained prefix*/).ToString()); SteamNetworkingIdentity val = default(SteamNetworkingIdentity); Probe("SteamGameServerNetworkingSockets.GetIdentity()", () => (!SteamGameServerNetworkingSockets.GetIdentity(ref val)) ? "(no identity yet)" : ((object)((SteamNetworkingIdentity)(ref val)).GetSteamID()/*cast due to .constrained prefix*/).ToString()); Probe("SteamNetworkingSockets.GetIdentity()", () => (!SteamNetworkingSockets.GetIdentity(ref val)) ? "(no identity yet)" : ((object)((SteamNetworkingIdentity)(ref val)).GetSteamID()/*cast due to .constrained prefix*/).ToString()); ScanIL("SendQueuedPackages"); ScanIL("Recv"); ScanIL("RegisterGlobalCallbacks"); ScanIL("GetConnectionQuality"); log.LogInfo((object)"[SteamSelfTest] ---- end ----"); } private static void Probe(string what, Func call) { try { SmoothServerPlugin.Log.LogInfo((object)("[SteamSelfTest] OK " + what + " -> " + call())); } catch (Exception ex) { string text = ((ex.InnerException != null) ? ex.InnerException.Message : ex.Message); SmoothServerPlugin.Log.LogInfo((object)("[SteamSelfTest] THREW " + what + " -> " + text.Trim())); } } private static void ScanIL(string name) { ManualLogSource log = SmoothServerPlugin.Log; try { MethodInfo methodInfo = AccessTools.Method(typeof(ZSteamSocket), name, (Type[])null, (Type[])null); if (methodInfo == null) { log.LogInfo((object)("[SteamSelfTest] IL ZSteamSocket." + name + " -> not found")); return; } StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair item in PatchProcessor.ReadMethodBody((MethodBase)methodInfo)) { MethodBase methodBase = item.Value as MethodBase; if (methodBase == null || methodBase.DeclaringType == null) { continue; } string name2 = methodBase.DeclaringType.Name; if (!name2.StartsWith("Steam", StringComparison.Ordinal)) { continue; } string value = name2 + "." + methodBase.Name; if (!stringBuilder.ToString().Contains(value)) { if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append(value); } } log.LogInfo((object)("[SteamSelfTest] IL ZSteamSocket." + name + " calls -> " + ((stringBuilder.Length == 0) ? "(no Steam* calls)" : stringBuilder.ToString()))); } catch (Exception ex) { log.LogInfo((object)("[SteamSelfTest] IL ZSteamSocket." + name + " -> scan failed: " + ex.Message)); } } } internal sealed class SyncListCacheModule : FeatureModule { private sealed class Entry { public Vector2i Zone; public float StampedAt; public readonly List Sector = new List(); public readonly List Distant = new List(); } private ConfigEntry _cacheMs; private ConfigEntry _statsIntervalSec; internal static bool Active; internal static float CacheSec = 0.1f; internal static float StatsIntervalSec = 60f; private static readonly Dictionary Cache = new Dictionary(); private static long _hits; private static long _misses; private static float _statsAcc; public override string Name => "SyncListCache"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("SyncListCache", "Enabled", true, "Cache ZDOMan's per-peer sector scan across the 20Hz send sweep. The ShouldSend filter and the priority sort still run every send, so nothing is re-sent."); _cacheMs = cfg.Bind("SyncListCache", "CacheMs", 100f, "Milliseconds a peer's sector scan stays valid. Also invalidated immediately when the peer changes zone. 0 disables caching (still uses our own temp lists)."); _statsIntervalSec = cfg.Bind("SyncListCache", "StatsIntervalSec", 60f, "Seconds between cache hit/miss lines. 0 disables them."); Watch(_cacheMs); Watch(_statsIntervalSec); } protected override void ApplyPatches() { //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Expected O, but got Unknown ReadConfig(); MethodInfo methodInfo = AccessTools.Method(typeof(ZDOMan), "CreateSyncList", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer SyncListCache: ZDOMan.CreateSyncList not found"); } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length != 2 || parameters[0].Name != "peer" || parameters[1].ParameterType != typeof(List)) { throw new Exception("SmoothServer SyncListCache: ZDOMan.CreateSyncList signature changed (expected (ZDOPeer peer, List toSync)) - refusing to patch"); } if (AccessTools.Method(typeof(ZDOMan), "ServerSortSendZDOS", (Type[])null, (Type[])null) == null) { throw new Exception("SmoothServer SyncListCache: ZDOMan.ServerSortSendZDOS not found"); } if (AccessTools.Method(typeof(ZDOMan), "AddForceSendZdos", (Type[])null, (Type[])null) == null) { throw new Exception("SmoothServer SyncListCache: ZDOMan.AddForceSendZdos not found"); } if (AccessTools.Method(typeof(ZDOMan), "FindSectorObjects", (Type[])null, (Type[])null) == null) { throw new Exception("SmoothServer SyncListCache: ZDOMan.FindSectorObjects not found"); } Harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(SyncListCacheModule), "Prefix", (Type[])null) { priority = 600 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Cache.Clear(); _hits = 0L; _misses = 0L; _statsAcc = 0f; Active = true; FeatureModule.Log.LogInfo((object)("[SyncListCache] caching the per-peer sector scan for " + (CacheSec * 1000f).ToString("F0") + "ms (filter + sort still run every send)")); } public override void Disable() { Active = false; Cache.Clear(); base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { ReadConfig(); Cache.Clear(); FeatureModule.Log.LogInfo((object)("[SyncListCache] CacheMs -> " + (CacheSec * 1000f).ToString("F0"))); } private void ReadConfig() { CacheSec = Mathf.Clamp(_cacheMs.Value, 0f, 2000f) / 1000f; StatsIntervalSec = Mathf.Max(0f, _statsIntervalSec.Value); } private static bool Prefix(ZDOMan __instance, ZDOPeer peer, List toSync) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) if (!Active || !FeatureModule.ServerActive()) { return true; } if (peer == null || peer.m_peer == null) { return true; } Vector3 refPos = peer.m_peer.GetRefPos(); Vector2i zone = ZoneSystem.GetZone(refPos); long uid = peer.m_peer.m_uid; float realtimeSinceStartup = Time.realtimeSinceStartup; if (!Cache.TryGetValue(uid, out Entry value)) { value = new Entry(); Cache[uid] = value; if (Cache.Count > 64) { Prune(__instance); } } if (CacheSec > 0f && value.StampedAt > 0f && realtimeSinceStartup - value.StampedAt < CacheSec && value.Zone.x == zone.x && value.Zone.y == zone.y) { _hits++; } else { _misses++; value.Sector.Clear(); value.Distant.Clear(); __instance.FindSectorObjects(zone, ZoneSystem.instance.m_activeArea, ZoneSystem.instance.m_activeDistantArea, value.Sector, value.Distant); value.Zone = zone; value.StampedAt = realtimeSinceStartup; } for (int i = 0; i < value.Sector.Count; i++) { ZDO val = value.Sector[i]; if (val != null && peer.ShouldSend(val)) { toSync.Add(val); } } __instance.ServerSortSendZDOS(toSync, refPos, peer); if (toSync.Count < 10) { for (int j = 0; j < value.Distant.Count; j++) { ZDO val2 = value.Distant[j]; if (val2 != null && peer.ShouldSend(val2)) { toSync.Add(val2); } } } __instance.AddForceSendZdos(peer, toSync); return false; } private static void Prune(ZDOMan zm) { HashSet hashSet = new HashSet(); for (int i = 0; i < zm.m_peers.Count; i++) { if (zm.m_peers[i] != null && zm.m_peers[i].m_peer != null) { hashSet.Add(zm.m_peers[i].m_peer.m_uid); } } List list = new List(); foreach (KeyValuePair item in Cache) { if (!hashSet.Contains(item.Key)) { list.Add(item.Key); } } foreach (long item2 in list) { Cache.Remove(item2); } } internal static void TickStats(float dt) { if (!Active || StatsIntervalSec <= 0f) { return; } _statsAcc += dt; if (!(_statsAcc < StatsIntervalSec)) { _statsAcc = 0f; long num = _hits + _misses; if (num != 0L) { SmoothServerPlugin.Log.LogInfo((object)$"[SyncListCache] sector scans avoided: {_hits}/{num} calls ({100.0 * (double)_hits / (double)num:F0}%)"); _hits = 0L; _misses = 0L; } } } } internal sealed class VPOServerModule : FeatureModule { private sealed class CachedOwner { public WearNTear Owner; } private sealed class CachedWearNTear { public readonly HashSet OwnColliders = new HashSet(); public readonly List SupportOwners = new List(); public bool HasCenterOfMass; public Vector3 CenterOfMass; } private ConfigEntry _wearNTear; private ConfigEntry _releaseScan; private ConfigEntry _maxPhysicsSteps; internal static bool Active; internal static bool WearNTearCache = true; internal static bool ReleaseScanSpeedup = true; internal static int MaxPhysicsSteps; private static bool _physicsPending; private static float _physicsVanilla = -1f; private static ConditionalWeakTable Owners = new ConditionalWeakTable(); private static ConditionalWeakTable Caches = new ConditionalWeakTable(); private static readonly HashSet ProcessedSupportColliders = new HashSet(); private static readonly List CachedCentersOfMass = new List(); public override string Name => "VPOServer"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("VPOServer", "Enabled", true, "Port of the server-relevant ValheimPerformanceOptimizations patches (MIT, ontrigger)."); _wearNTear = cfg.Bind("VPOServer", "WearNTearCache", true, "Cache collider->WearNTear ownership and centre-of-mass during structural-integrity recalculation. The biggest server CPU win in built-up bases."); _releaseScan = cfg.Bind("VPOServer", "ReleaseScanSpeedup", true, "Resolve each ZDO's owner once per ownership-release scan instead of up to three times."); _maxPhysicsSteps = cfg.Bind("VPOServer", "MaxPhysicsStepsPerFrame", 0, "0 = leave Unity's default (up to 15 physics steps in one frame). 5-15 caps it, trading physics accuracy for a shorter worst frame. VPO's own default is 8."); Watch(_wearNTear); Watch(_releaseScan); Watch(_maxPhysicsSteps); } protected override void ApplyPatches() { //IL_02a5: Unknown result type (might be due to invalid IL or missing references) //IL_02bc: Unknown result type (might be due to invalid IL or missing references) //IL_02c7: Expected O, but got Unknown //IL_02c7: Expected O, but got Unknown //IL_02e0: Unknown result type (might be due to invalid IL or missing references) //IL_02ed: Expected O, but got Unknown //IL_0306: Unknown result type (might be due to invalid IL or missing references) //IL_0313: Expected O, but got Unknown //IL_0332: Unknown result type (might be due to invalid IL or missing references) //IL_0337: Unknown result type (might be due to invalid IL or missing references) //IL_034b: Expected O, but got Unknown ReadConfig(); MethodInfo methodInfo = AccessTools.Method(typeof(WearNTear), "UpdateSupport", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(WearNTear), "ClearCachedSupport", (Type[])null, (Type[])null); MethodInfo methodInfo3 = AccessTools.Method(typeof(WearNTear), "OnDestroy", (Type[])null, (Type[])null); MethodInfo methodInfo4 = AccessTools.Method(typeof(ZDOMan), "ReleaseNearbyZDOS", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null || methodInfo3 == null) { throw new Exception("SmoothServer VPOServer: WearNTear.UpdateSupport/ClearCachedSupport/OnDestroy not all found - refusing to patch"); } if (methodInfo4 == null) { throw new Exception("SmoothServer VPOServer: ZDOMan.ReleaseNearbyZDOS not found"); } string[] array = new string[10] { "m_supportColliders", "m_supportPositions", "m_supportValue", "m_clearCachedSupport", "m_colliders", "m_bounds", "m_support", "m_supports", "m_comOffset", "m_forceCorrectCOMCalculation" }; foreach (string text in array) { if (AccessTools.Field(typeof(WearNTear), text) == null) { throw new Exception("SmoothServer VPOServer: WearNTear." + text + " not found - refusing to patch"); } } array = new string[5] { "s_tempColliders", "s_tempSupportPoints", "s_tempSupportPointValues", "s_rayMask", "s_terrainLayer" }; foreach (string text2 in array) { if (AccessTools.Field(typeof(WearNTear), text2) == null) { throw new Exception("SmoothServer VPOServer: WearNTear." + text2 + " not found - refusing to patch"); } } if (AccessTools.Method(typeof(WearNTear), "GetMaterialProperties", (Type[])null, (Type[])null) == null || AccessTools.Method(typeof(WearNTear), "FindSupportPoint", (Type[])null, (Type[])null) == null || AccessTools.Method(typeof(WearNTear), "GetMaxSupport", (Type[])null, (Type[])null) == null || AccessTools.Method(typeof(WearNTear), "HaveSupport", (Type[])null, (Type[])null) == null || AccessTools.Method(typeof(WearNTear), "SetupColliders", (Type[])null, (Type[])null) == null) { throw new Exception("SmoothServer VPOServer: a WearNTear helper method is missing - refusing to patch"); } if (AccessTools.Method(typeof(ZDOMan), "IsInPeerActiveArea", (Type[])null, (Type[])null) == null) { throw new Exception("SmoothServer VPOServer: ZDOMan.IsInPeerActiveArea not found"); } if (WearNTearCache) { Harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(VPOServerModule), "UpdateSupportPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(VPOServerModule), "UpdateSupportFinalizer", (Type[])null), (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(VPOServerModule), "ClearCachedSupportPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(typeof(VPOServerModule), "WearNTearOnDestroyPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } if (ReleaseScanSpeedup) { Harmony.Patch((MethodBase)methodInfo4, new HarmonyMethod(typeof(VPOServerModule), "ReleaseNearbyPrefix", (Type[])null) { priority = 600 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } _physicsPending = MaxPhysicsSteps > 0; Active = true; FeatureModule.Log.LogInfo((object)("[VPOServer] wearNTearCache=" + WearNTearCache + " releaseScanSpeedup=" + ReleaseScanSpeedup + " maxPhysicsSteps=" + ((MaxPhysicsSteps == 0) ? "vanilla" : MaxPhysicsSteps.ToString()) + " (ported from ValheimPerformanceOptimizations, MIT, ontrigger)")); } public override void Disable() { Active = false; Owners = new ConditionalWeakTable(); Caches = new ConditionalWeakTable(); CachedCentersOfMass.Clear(); ProcessedSupportColliders.Clear(); if (_physicsVanilla > 0f) { Time.maximumDeltaTime = _physicsVanilla; } base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { int maxPhysicsSteps = MaxPhysicsSteps; ReadConfig(); if (MaxPhysicsSteps != maxPhysicsSteps) { _physicsPending = true; FeatureModule.Log.LogInfo((object)("[VPOServer] MaxPhysicsStepsPerFrame -> " + MaxPhysicsSteps)); } if ((object)entry == _wearNTear || (object)entry == _releaseScan) { FeatureModule.Log.LogInfo((object)("[VPOServer] WearNTearCache/ReleaseScanSpeedup changes need a server restart (patches are installed at load); current: wearNTear=" + WearNTearCache + " releaseScan=" + ReleaseScanSpeedup)); } } private void ReadConfig() { WearNTearCache = _wearNTear.Value; ReleaseScanSpeedup = _releaseScan.Value; int value = _maxPhysicsSteps.Value; MaxPhysicsSteps = ((value > 0) ? Mathf.Clamp(value, 5, 15) : 0); } internal static void Tick(float dt) { if (Active && _physicsPending && FeatureModule.ServerActive()) { _physicsPending = false; if (_physicsVanilla < 0f) { _physicsVanilla = Time.maximumDeltaTime; } if (MaxPhysicsSteps <= 0) { Time.maximumDeltaTime = _physicsVanilla; SmoothServerPlugin.Log.LogInfo((object)("[VPOServer] maximumDeltaTime restored to vanilla " + _physicsVanilla.ToString("F4") + "s")); return; } float maximumDeltaTime = Time.maximumDeltaTime; Time.maximumDeltaTime = (float)MaxPhysicsSteps * Time.fixedDeltaTime; SmoothServerPlugin.Log.LogInfo((object)("[VPOServer] maximumDeltaTime " + maximumDeltaTime.ToString("F4") + "s -> " + Time.maximumDeltaTime.ToString("F4") + "s (" + MaxPhysicsSteps + " physics steps x fixedDeltaTime " + Time.fixedDeltaTime.ToString("F4") + "s)")); } } private static WearNTear GetOrCacheOwner(Collider collider) { if ((Object)(object)collider == (Object)null) { return null; } if (Owners.TryGetValue(collider, out CachedOwner value)) { return value.Owner; } WearNTear componentInParent = ((Component)collider).GetComponentInParent(); Owners.Add(collider, new CachedOwner { Owner = componentInParent }); return componentInParent; } private static CachedWearNTear GetCache(WearNTear instance) { if (Caches.TryGetValue(instance, out CachedWearNTear value)) { return value; } value = new CachedWearNTear(); Caches.Add(instance, value); return value; } private static HashSet GetOwnColliders(WearNTear owner) { CachedWearNTear cache = GetCache(owner); if (cache.OwnColliders.Count != 0) { return cache.OwnColliders; } Collider[] colliders = owner.m_colliders; if (colliders != null) { for (int i = 0; i < colliders.Length; i++) { cache.OwnColliders.Add(colliders[i]); } } return cache.OwnColliders; } private static float GetOptimizedSupport(WearNTear instance) { ZNetView nview = instance.m_nview; if ((Object)(object)nview == (Object)null || !nview.IsValid() || !nview.HasOwner()) { return instance.GetMaxSupport(); } if (nview.IsOwner()) { return instance.m_support; } return nview.GetZDO().GetFloat(ZDOVars.s_support, instance.GetMaxSupport()); } private static Vector3 GetOptimizedCOM(WearNTear instance) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) Transform transform = ((Component)instance).transform; return transform.position + transform.rotation * instance.m_comOffset; } private static Vector3 GetCachedCOM(WearNTear instance) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) CachedWearNTear cache = GetCache(instance); if (!cache.HasCenterOfMass) { cache.HasCenterOfMass = true; cache.CenterOfMass = GetOptimizedCOM(instance); CachedCentersOfMass.Add(cache); } return cache.CenterOfMass; } private static bool UpdateSupportPrefix(WearNTear __instance) { //IL_0152: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Unknown result type (might be due to invalid IL or missing references) //IL_0187: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_0193: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_04e3: Unknown result type (might be due to invalid IL or missing references) //IL_04e8: Unknown result type (might be due to invalid IL or missing references) //IL_04ea: Unknown result type (might be due to invalid IL or missing references) //IL_04ef: Unknown result type (might be due to invalid IL or missing references) //IL_0533: Unknown result type (might be due to invalid IL or missing references) //IL_0538: Unknown result type (might be due to invalid IL or missing references) //IL_053a: Unknown result type (might be due to invalid IL or missing references) //IL_053f: Unknown result type (might be due to invalid IL or missing references) //IL_054d: Unknown result type (might be due to invalid IL or missing references) //IL_054f: Unknown result type (might be due to invalid IL or missing references) //IL_0323: Unknown result type (might be due to invalid IL or missing references) //IL_0327: Unknown result type (might be due to invalid IL or missing references) //IL_0339: Unknown result type (might be due to invalid IL or missing references) //IL_0342: Unknown result type (might be due to invalid IL or missing references) //IL_0383: Unknown result type (might be due to invalid IL or missing references) //IL_0389: Unknown result type (might be due to invalid IL or missing references) //IL_038e: Unknown result type (might be due to invalid IL or missing references) //IL_0390: Unknown result type (might be due to invalid IL or missing references) //IL_0397: Unknown result type (might be due to invalid IL or missing references) //IL_03a9: Unknown result type (might be due to invalid IL or missing references) //IL_03ab: Unknown result type (might be due to invalid IL or missing references) //IL_03ad: Unknown result type (might be due to invalid IL or missing references) //IL_03b2: Unknown result type (might be due to invalid IL or missing references) //IL_03b6: Unknown result type (might be due to invalid IL or missing references) //IL_03bb: Unknown result type (might be due to invalid IL or missing references) //IL_03bd: Unknown result type (might be due to invalid IL or missing references) //IL_0410: Unknown result type (might be due to invalid IL or missing references) //IL_0446: Unknown result type (might be due to invalid IL or missing references) //IL_03d0: Unknown result type (might be due to invalid IL or missing references) if (!Active || !WearNTearCache) { return true; } int count = __instance.m_supportColliders.Count; if (count > 0) { List supportOwners = GetCache(__instance).SupportOwners; if (supportOwners.Count == count) { int num = 0; float num2 = 0f; for (int i = 0; i < count; i++) { Collider val = __instance.m_supportColliders[i]; if ((Object)(object)val == (Object)null) { break; } WearNTear val2 = supportOwners[i]; if ((Object)(object)val2 == (Object)null || !val2.m_supports) { break; } if (((Component)val).transform.position == __instance.m_supportPositions[i]) { float optimizedSupport = GetOptimizedSupport(val2); if (optimizedSupport > num2) { num2 = optimizedSupport; } if (optimizedSupport.Equals(__instance.m_supportValue[i])) { num++; } } } if (num == __instance.m_supportPositions.Count && num2 > __instance.m_support) { return false; } } __instance.ClearCachedSupport(); } if (__instance.m_colliders == null) { __instance.SetupColliders(); } HashSet ownColliders = GetOwnColliders(__instance); List supportOwners2 = GetCache(__instance).SupportOwners; float num3 = default(float); float num4 = default(float); float num5 = default(float); float num6 = default(float); __instance.GetMaterialProperties(ref num3, ref num4, ref num5, ref num6); WearNTear.s_tempSupportPoints.Clear(); WearNTear.s_tempSupportPointValues.Clear(); ProcessedSupportColliders.Clear(); Vector3 optimizedCOM = GetOptimizedCOM(__instance); bool flag = false; float num7 = 0f; foreach (BoundData bound in __instance.m_bounds) { int num8 = Physics.OverlapBoxNonAlloc(bound.m_pos, bound.m_size, WearNTear.s_tempColliders, bound.m_rot, WearNTear.s_rayMask); if (__instance.m_clearCachedSupport) { for (int j = 0; j < num8; j++) { Collider val3 = WearNTear.s_tempColliders[j]; if ((Object)(object)val3.attachedRigidbody != (Object)null || val3.isTrigger || ownColliders.Contains(val3)) { continue; } WearNTear orCacheOwner = GetOrCacheOwner(val3); if (!((Object)(object)orCacheOwner == (Object)null)) { if (orCacheOwner.m_nview.IsOwner()) { orCacheOwner.ClearCachedSupport(); } else if (orCacheOwner.m_nview.IsValid()) { orCacheOwner.m_nview.InvokeRPC(orCacheOwner.m_nview.GetZDO().GetOwner(), "RPC_ClearCachedSupport", Array.Empty()); } } } __instance.m_clearCachedSupport = false; } for (int k = 0; k < num8; k++) { Collider val4 = WearNTear.s_tempColliders[k]; if ((Object)(object)val4.attachedRigidbody != (Object)null || val4.isTrigger || ownColliders.Contains(val4) || !ProcessedSupportColliders.Add(val4)) { continue; } if (((Component)val4).gameObject.layer == WearNTear.s_terrainLayer) { flag = true; continue; } WearNTear orCacheOwner2 = GetOrCacheOwner(val4); if ((Object)(object)orCacheOwner2 == (Object)null) { __instance.m_support = num3; __instance.ClearCachedSupport(); __instance.m_nview.GetZDO().Set(ZDOVars.s_support, __instance.m_support); return false; } if (!orCacheOwner2.m_supports) { continue; } float num9 = Vector3.Distance(optimizedCOM, GetCachedCOM(orCacheOwner2)) + 0.1f; float num10 = Vector3.Distance(optimizedCOM, ((Component)orCacheOwner2).transform.position) + 0.1f; if (num10 < num9 && !__instance.m_forceCorrectCOMCalculation) { num9 = num10; } float optimizedSupport2 = GetOptimizedSupport(orCacheOwner2); num7 = Mathf.Max(num7, optimizedSupport2 - num5 * num9 * optimizedSupport2); Vector3 val5 = WearNTear.FindSupportPoint(optimizedCOM, orCacheOwner2, val4); if (val5.y < optimizedCOM.y + 0.05f) { Vector3 val6 = val5 - optimizedCOM; Vector3 normalized = ((Vector3)(ref val6)).normalized; if (normalized.y < 0f) { float num11 = Mathf.Acos(1f - Mathf.Abs(normalized.y)) / ((float)Math.PI / 2f); float num12 = Mathf.Lerp(num5, num6, num11); num7 = Mathf.Max(num7, optimizedSupport2 - num12 * num9 * optimizedSupport2); } WearNTear.s_tempSupportPoints.Add(val5); WearNTear.s_tempSupportPointValues.Add(optimizedSupport2 - num6 * num9 * optimizedSupport2); __instance.m_supportColliders.Add(val4); __instance.m_supportPositions.Add(((Component)val4).transform.position); __instance.m_supportValue.Add(optimizedSupport2); supportOwners2.Add(orCacheOwner2); } } } if (flag) { __instance.m_support = num3; __instance.m_nview.GetZDO().Set(ZDOVars.s_support, __instance.m_support); return false; } if (WearNTear.s_tempSupportPoints.Count > 0) { int count2 = WearNTear.s_tempSupportPoints.Count; for (int l = 0; l < count2 - 1; l++) { Vector3 val7 = WearNTear.s_tempSupportPoints[l] - optimizedCOM; val7.y = 0f; for (int m = l + 1; m < count2; m++) { float num13 = (WearNTear.s_tempSupportPointValues[l] + WearNTear.s_tempSupportPointValues[m]) * 0.5f; if (!(num13 <= num7)) { Vector3 val8 = WearNTear.s_tempSupportPoints[m] - optimizedCOM; val8.y = 0f; if (Vector3.Angle(val7, val8) >= 100f) { num7 = num13; } } } } } __instance.m_support = Mathf.Min(num7, num3); __instance.m_nview.GetZDO().Set(ZDOVars.s_support, __instance.m_support); if (!__instance.HaveSupport()) { __instance.ClearCachedSupport(); } return false; } private static Exception UpdateSupportFinalizer(Exception __exception) { ProcessedSupportColliders.Clear(); for (int i = 0; i < CachedCentersOfMass.Count; i++) { CachedCentersOfMass[i].HasCenterOfMass = false; } CachedCentersOfMass.Clear(); return __exception; } private static void ClearCachedSupportPostfix(WearNTear __instance) { if (Caches.TryGetValue(__instance, out CachedWearNTear value)) { value.SupportOwners.Clear(); } } private static void WearNTearOnDestroyPostfix(WearNTear __instance) { if (Caches.TryGetValue(__instance, out CachedWearNTear value)) { Caches.Remove(__instance); CachedCentersOfMass.Remove(value); } } private static bool ReleaseNearbyPrefix(ZDOMan __instance, Vector3 refPosition, long uid) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: 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) if (!Active || !ReleaseScanSpeedup || !FeatureModule.ServerActive()) { return true; } Vector2i zone = ZoneSystem.GetZone(refPosition); List tempNearObjects = __instance.m_tempNearObjects; tempNearObjects.Clear(); __instance.FindSectorObjects(zone, ZoneSystem.instance.m_activeArea, 0, tempNearObjects, (List)null); int num = ZoneSystem.instance.m_activeArea - 1; bool flag = uid == ZDOMan.GetSessionID(); for (int i = 0; i < tempNearObjects.Count; i++) { ZDO val = tempNearObjects[i]; if (val == null || !val.Persistent) { continue; } Vector2i sector = val.GetSector(); bool flag2 = val.HasOwner(); bool flag3; long num2; if (flag) { flag3 = val.IsOwner(); num2 = ((flag3 || !flag2) ? 0 : val.GetOwner()); } else { num2 = (flag2 ? val.GetOwner() : 0); flag3 = num2 == uid; } if (flag3) { if (!ZNetScene.InActiveArea(sector, zone, num)) { val.SetOwner(0L); } } else if ((!flag2 || !__instance.IsInPeerActiveArea(sector, num2)) && ZNetScene.InActiveArea(sector, zone, num)) { val.SetOwner(uid); } } return false; } } public enum RunMode { Auto, Server, Client } [BepInPlugin("Nosferatu.SmoothServer", "SmoothServer", "0.3.1")] public class SmoothServerPlugin : BaseUnityPlugin { public const string PluginGuid = "Nosferatu.SmoothServer"; public const string PluginName = "SmoothServer"; public const string PluginVersion = "0.3.1"; internal static ManualLogSource Log; internal static ConfigFile Cfg; internal static ConfigSync ConfigSync; internal static readonly List Modules = new List(); internal static ConfigEntry ModeCfg; internal static ConfigEntry EnforceClientMod; internal static ConfigEntry HotReloadCfg; internal static ConfigEntry SteamSelfTestCfg; internal static ModuleSide RunningSide = ModuleSide.Server; internal static bool BetterNetworkingPresent; private Harmony _bootstrap; private static bool _summaryLogged; private ConfigWatcher _configWatcher; internal static bool IsServerSide => RunningSide == ModuleSide.Server; private void Awake() { //IL_0324: Unknown result type (might be due to invalid IL or missing references) //IL_032e: Expected O, but got Unknown //IL_0377: Unknown result type (might be due to invalid IL or missing references) //IL_0384: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Cfg = ((BaseUnityPlugin)this).Config; ConfigSync = new ConfigSync("Nosferatu.SmoothServer") { DisplayName = "SmoothServer", CurrentVersion = "0.3.1", MinimumRequiredVersion = "0.3.1" }; ModeCfg = BindLocal("General", "Mode", RunMode.Auto, "Which half of the mod to run. Auto = a dedicated server (-batchmode) runs the server half, everything else runs the client half. Machine-local, never synced.", null); SteamSelfTestCfg = BindLocal("General", "SteamSelfTest", defaultValue: false, "Diagnostic, off by default: at load, log which half of Steamworks is initialised in this process (SteamGameServer* vs Steam*) and which interfaces this build of ZSteamSocket actually calls, read straight out of its IL. The client and dedicated-server builds of assembly_valheim.dll differ here, which is what broke 0.3.0 - turn this on once after a game update to re-prove it. Machine-local.", null); HotReloadCfg = BindLocal("General", "HotReload", defaultValue: true, "Watch this plugin's own cfg file on disk and reload it automatically when it changes, so edits take effect without a server restart. Machine-local, never synced.", null); EnforceClientMod = BindSynced("General", "EnforceClientMod", defaultValue: false, "Server: require every connecting client to run SmoothServer 0.3.1 or newer. Vanilla clients and clients with an older version are disconnected with an explanatory message, and the synced config is locked so only the server (and admins) can change it. Default OFF: SmoothServer's server half works with no client installs at all - the client-side features (compression, client send budget, shared map) simply do not exist for a vanilla joiner.", null); ConfigSync.AddLockingConfigEntry(EnforceClientMod); EnforceClientMod.SettingChanged += delegate { ApplyEnforcement(); }; ApplyEnforcement(); Log.LogInfo((object)("ServerSync initialised: id=" + ConfigSync.Name + " display=" + ConfigSync.DisplayName + " CurrentVersion=" + ConfigSync.CurrentVersion + " MinimumRequiredVersion=" + ConfigSync.MinimumRequiredVersion + " ModRequired=" + ConfigSync.ModRequired + " EnforceClientMod=" + EnforceClientMod.Value)); RunningSide = ResolveSide(); Log.LogInfo((object)("SmoothServer 0.3.1: mode=" + ModeCfg.Value.ToString() + " -> running the " + (IsServerSide ? "SERVER" : "CLIENT") + " half (isBatchMode=" + Application.isBatchMode + ")")); if (SteamSelfTestCfg.Value) { try { SteamSelfTest.Run(); } catch (Exception ex) { Log.LogWarning((object)("[SteamSelfTest] failed: " + ex)); } } BetterNetworkingPresent = DetectBetterNetworking(); if (BetterNetworkingPresent) { Log.LogWarning((object)"BetterNetworking is installed alongside SmoothServer. Both wrap ZSteamSocket's send queue and they CANNOT coexist - SmoothServer's Compression module will refuse to patch. Uninstall BetterNetworking to use SmoothServer compression."); } DiscoverModules(); foreach (FeatureModule module in Modules) { try { module.Configure(((BaseUnityPlugin)this).Config); } catch (Exception ex2) { Log.LogError((object)("[" + module.Name + "] config bind failed: " + ex2)); } } foreach (FeatureModule module2 in Modules) { module2.TryEnable("Nosferatu.SmoothServer", RunningSide); } _bootstrap = new Harmony("Nosferatu.SmoothServer.bootstrap"); MethodInfo methodInfo = AccessTools.Method(typeof(ZNet), "Start", (Type[])null, (Type[])null); if (methodInfo == null) { Log.LogError((object)"SmoothServer: ZNet.Start not found - cannot log the module summary"); } else { _bootstrap.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SmoothServerPlugin), "ZNetStartPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } _configWatcher = new ConfigWatcher(Cfg, Log); Log.LogInfo((object)("SmoothServer 0.3.1 loaded, " + Modules.Count + " modules")); } private static ModuleSide ResolveSide() { switch (ModeCfg.Value) { case RunMode.Server: return ModuleSide.Server; case RunMode.Client: return ModuleSide.Client; default: if (!Application.isBatchMode) { return ModuleSide.Client; } return ModuleSide.Server; } } private static void ApplyEnforcement() { bool flag = EnforceClientMod != null && EnforceClientMod.Value; ConfigSync.ModRequired = flag; ConfigSync.MinimumRequiredVersion = (flag ? "0.3.1" : "0.0.0"); } private static bool DetectBetterNetworking() { try { foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { if (pluginInfo.Key != null && pluginInfo.Key.IndexOf("BetterNetworking", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } } catch (Exception ex) { Log.LogWarning((object)("could not enumerate loaded plugins: " + ex.Message)); } return false; } private static void DiscoverModules() { List list = new List(); Type[] types; try { types = Assembly.GetExecutingAssembly().GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types; } Type[] array = types; foreach (Type type in array) { if (!(type == null) && !type.IsAbstract && typeof(FeatureModule).IsAssignableFrom(type)) { try { list.Add((FeatureModule)Activator.CreateInstance(type, nonPublic: true)); } catch (Exception ex2) { Log.LogError((object)("module discovery: cannot instantiate " + type.Name + ": " + ex2)); } } } list.Sort((FeatureModule a, FeatureModule b) => string.CompareOrdinal(a.Name, b.Name)); Modules.Clear(); Modules.AddRange(list); } internal static ConfigEntry BindSynced(string section, string key, T defaultValue, string description, FeatureModule owner) { ConfigEntry val = Cfg.Bind(section, key, defaultValue, description); ConfigSync.AddConfigEntry(val).SynchronizedConfig = true; Wire(val, owner); return val; } internal static ConfigEntry BindLocal(string section, string key, T defaultValue, string description, FeatureModule owner) { ConfigEntry obj = Cfg.Bind(section, key, defaultValue, description); Wire(obj, owner); return obj; } private static void Wire(ConfigEntry entry, FeatureModule owner) { entry.SettingChanged += delegate { try { if (owner != null) { owner.OnConfigChanged((ConfigEntryBase)(object)entry); } } catch (Exception ex) { Log.LogError((object)("OnConfigChanged(" + ((object)((ConfigEntryBase)entry).Definition)?.ToString() + ") threw: " + ex)); } }; } private static void ZNetStartPostfix() { if (_summaryLogged) { return; } _summaryLogged = true; List list = new List(); foreach (FeatureModule module in Modules) { list.Add(module.Name + "=" + module.Status); } Log.LogInfo((object)("SmoothServer module summary: " + string.Join(", ", list.ToArray()))); Log.LogInfo((object)("SmoothServer 0.3.1 (" + (IsServerSide ? "server" : "client") + " half) EnforceClientMod=" + EnforceClientMod.Value + " configLocked=" + ConfigSync.IsLocked + " sourceOfTruth=" + ConfigSync.IsSourceOfTruth)); foreach (FeatureModule module2 in Modules) { string text = null; try { text = module2.StatusDetail(); } catch (Exception ex) { text = "status detail threw: " + ex.Message; } Log.LogInfo((object)(" " + module2.Name + " [" + module2.Side.ToString() + "] = " + module2.Status + (string.IsNullOrEmpty(text) ? "" : (" " + text)))); } FrameRateModule.OnZNetStart(); } private void Update() { float unscaledDeltaTime = Time.unscaledDeltaTime; TelemetryModule.Tick(unscaledDeltaTime); FrameRateModule.Tick(unscaledDeltaTime); CompressionModule.Tick(unscaledDeltaTime); SharedMapModule.Tick(unscaledDeltaTime); ServerModules.Tick(unscaledDeltaTime); if (HotReloadCfg != null && HotReloadCfg.Value) { _configWatcher?.Pump(); } } private void OnDestroy() { foreach (FeatureModule module in Modules) { module.Disable(); } _configWatcher?.Dispose(); try { if (_bootstrap != null) { _bootstrap.UnpatchSelf(); } } catch { } } } internal sealed class SendBudgetModule : FeatureModule { private const int VanillaHighWater = 10240; private const int VanillaMinChunk = 2048; private ConfigEntry _highWater; private ConfigEntry _minChunk; internal static bool Active; internal static int HighWaterBytes = 65536; internal static int MinChunkBytes = 2048; public override string Name => "SendBudget"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("SendBudget", "Enabled", true, "Raise the per-peer ZDO send queue high-water mark from vanilla's 10240 bytes."); _highWater = cfg.Bind("SendBudget", "HighWaterBytes", 65536, "Bytes of queued data above which the server stops adding ZDOs for a peer. Vanilla 10240."); _minChunk = cfg.Bind("SendBudget", "MinChunkBytes", 2048, "Minimum remaining budget worth building a packet for. Vanilla 2048."); Watch(_highWater); Watch(_minChunk); } internal static int GetHighWaterBytes() { if (!Active || !FeatureModule.ServerActive()) { return 10240; } return AdaptiveBudgetModule.HighWaterFor(HighWaterBytes); } internal static int GetMinChunkBytes() { if (!Active || !FeatureModule.ServerActive()) { return 2048; } return MinChunkBytes; } protected override void ApplyPatches() { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown HighWaterBytes = Math.Max(4096, _highWater.Value); MinChunkBytes = Math.Max(256, _minChunk.Value); MethodInfo methodInfo = AccessTools.Method(typeof(ZDOMan), "SendZDOs", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer SendBudget: ZDOMan.SendZDOs not found"); } Harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(SendBudgetModule), "Transpiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); Active = true; FeatureModule.Log.LogInfo((object)("[SendBudget] highWater=" + HighWaterBytes + "B minChunk=" + MinChunkBytes + "B")); } public override void Disable() { Active = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _highWater) { HighWaterBytes = Math.Max(4096, _highWater.Value); } else { if ((object)entry != _minChunk) { return; } MinChunkBytes = Math.Max(256, _minChunk.Value); } FeatureModule.Log.LogInfo((object)("[SendBudget] highWater=" + HighWaterBytes + "B minChunk=" + MinChunkBytes + "B")); } private static IEnumerable Transpiler(IEnumerable instructions) { List list = new List(instructions); int num = 0; int num2 = 0; for (int i = 0; i < list.Count; i++) { if (ILUtil.TryGetI4(list[i], out var value)) { switch (value) { case 10240: ILUtil.ReplaceWithCall(list[i], typeof(SendBudgetModule), "GetHighWaterBytes"); num++; break; case 2048: ILUtil.ReplaceWithCall(list[i], typeof(SendBudgetModule), "GetMinChunkBytes"); num2++; break; } } } if (num != 2 || num2 != 1) { string text = "SmoothServer SendBudget transpiler: expected exactly 2x " + 10240 + " and 1x " + 2048 + " in ZDOMan.SendZDOs, found " + num + " and " + num2 + " - game IL changed, refusing to patch"; SmoothServerPlugin.Log.LogError((object)text); throw new Exception(text); } SmoothServerPlugin.Log.LogInfo((object)("[SendBudget] transpiler OK: " + num + "x highWater, " + num2 + "x minChunk replaced (assertion 2/1 passed)")); return list; } } internal sealed class SendCadenceModule : FeatureModule { private ConfigEntry _sendHz; internal static bool Active; internal static float SendHz = 20f; public override string Name => "SendCadence"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("SendCadence", "Enabled", true, "Replace vanilla's one-peer-per-frame ZDO send round-robin with a fixed-rate sweep over all peers. While on, BetterNetworking's Update Rate option is inert."); _sendHz = cfg.Bind("SendCadence", "SendHz", 20f, "Times per second the server pushes ZDO updates to every peer. Vanilla is effectively 20Hz divided by peer count. Clamped to 1-60."); Watch(_sendHz); } protected override void ApplyPatches() { //IL_0073: 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_008c: Expected O, but got Unknown SendHz = Mathf.Clamp(_sendHz.Value, 1f, 60f); MethodInfo methodInfo = AccessTools.Method(typeof(ZDOMan), "SendZDOToPeers2", new Type[1] { typeof(float) }, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer SendCadence: ZDOMan.SendZDOToPeers2(float) not found"); } Harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(SendCadenceModule), "Prefix", (Type[])null) { priority = 600 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Active = true; FeatureModule.Log.LogInfo((object)("[SendCadence] SendHz=" + SendHz.ToString("F1") + " (interval " + (1000f / SendHz).ToString("F1") + "ms), priority=High")); } public override void Disable() { Active = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _sendHz) { SendHz = Mathf.Clamp(_sendHz.Value, 1f, 60f); FeatureModule.Log.LogInfo((object)("[SendCadence] SendHz -> " + SendHz.ToString("F1") + " (interval " + (1000f / SendHz).ToString("F1") + "ms)")); } } private static bool Prefix(ZDOMan __instance, float dt) { if (!Active) { return true; } if (!FeatureModule.ServerActive()) { return true; } List peers = __instance.m_peers; if (peers.Count == 0) { __instance.m_nextSendPeer = -1; return false; } __instance.m_sendTimer += dt; if (__instance.m_sendTimer >= 1f / SendHz) { __instance.m_sendTimer = 0f; for (int i = 0; i < peers.Count; i++) { __instance.SendZDOs(peers[i], false); } } __instance.m_nextSendPeer = -1; return false; } } internal sealed class TelemetryModule : FeatureModule { private ConfigEntry _interval; internal static bool Active; internal static float IntervalSeconds = 10f; private static float _acc; private static int _frames; private static float _worstDt; public override string Name => "Telemetry"; public override void Configure(ConfigFile cfg) { EnabledCfg = cfg.Bind("Telemetry", "Enabled", true, "Log a periodic server performance line (frame time, fps, peers, ZDO rates)."); _interval = cfg.Bind("Telemetry", "IntervalSeconds", 10f, "Seconds between telemetry lines."); Watch(_interval); } protected override void ApplyPatches() { IntervalSeconds = Mathf.Max(1f, _interval.Value); _acc = 0f; _frames = 0; _worstDt = 0f; Active = true; } public override void Disable() { Active = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == _interval) { IntervalSeconds = Mathf.Max(1f, _interval.Value); _acc = 0f; _frames = 0; _worstDt = 0f; FeatureModule.Log.LogInfo((object)("[Telemetry] IntervalSeconds -> " + IntervalSeconds.ToString("F1"))); } } internal static void Tick(float dt) { if (!Active || !FeatureModule.ServerActive()) { return; } _acc += dt; _frames++; if (dt > _worstDt) { _worstDt = dt; } if (!(_acc < IntervalSeconds) && _frames != 0) { float num = _acc / (float)_frames * 1000f; float num2 = (float)_frames / _acc; float num3 = _worstDt * 1000f; int num4 = -1; int num5 = -1; int num6 = -1; int num7 = -1; ZDOMan instance = ZDOMan.instance; if (instance != null) { num4 = instance.m_peers.Count; num5 = instance.m_zdosSentLastSec; num6 = instance.m_zdosRecvLastSec; num7 = instance.m_objectsByID.Count; } int num8 = -1; ZNetScene instance2 = ZNetScene.instance; if ((Object)(object)instance2 != (Object)null) { num8 = instance2.m_instances.Count; } SmoothServerPlugin.Log.LogInfo((object)$"[Telemetry] fps={num2:F1} frame={num:F2}ms worst={num3:F1}ms peers={num4} zdosSent/s={num5} zdosRecv/s={num6} zdos={num7} sceneObjs={num8}"); _acc = 0f; _frames = 0; _worstDt = 0f; } } } } namespace SmoothServer.Net { internal sealed class ClientNetModule : FeatureModule { private const int VanillaHighWater = 10240; private ConfigEntry _highWater; private ConfigEntry _sendRateMax; internal static bool Active2; internal static int HighWaterBytes = 49152; private int _appliedRate = -1; internal static ClientNetModule Instance; public override string Name => "ClientNet"; public override ModuleSide Side => ModuleSide.Client; public override string Section => "Client"; protected override void Bind() { _highWater = BindSynced("HighWaterBytes", 49152, "Client: bytes of queued data above which this client stops adding ZDOs for a peer. Vanilla 10240. 48 KB is BetterNetworking's largest setting and roughly 5x the vanilla bandwidth-delay ceiling at 100 ms RTT."); _sendRateMax = BindLocal("SendRateMaxBytesPerSec", 1048576, "Client: Steam's per-connection maximum send rate, in bytes/sec. Vanilla pins this to 153600. This is a ceiling for bursts, not a target - Steam's estimator still decides the real rate. SendRateMin is deliberately left at vanilla. Machine-local: it describes YOUR uplink, not the server's."); } protected override void ApplyPatches() { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Expected O, but got Unknown HighWaterBytes = Math.Max(4096, _highWater.Value); MethodInfo methodInfo = AccessTools.Method(typeof(ZDOMan), "SendZDOs", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("SmoothServer ClientNet: ZDOMan.SendZDOs not found"); } Harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(ClientNetModule), "Transpiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); MethodInfo methodInfo2 = AccessTools.Method(typeof(ZSteamSocket), "RegisterGlobalCallbacks", (Type[])null, (Type[])null); if (methodInfo2 == null) { throw new Exception("SmoothServer ClientNet: ZSteamSocket.RegisterGlobalCallbacks not found"); } Harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(ClientNetModule), "RegisterGlobalCallbacksPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Active2 = true; Instance = this; FeatureModule.Log.LogInfo((object)("[ClientNet] highWater=" + HighWaterBytes + "B sendRateMax=" + _sendRateMax.Value + "B/s")); } public override void Disable() { Active2 = false; base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == EnabledCfg) { Active2 = Applied && base.Enabled; } else if ((object)entry == _highWater) { HighWaterBytes = Math.Max(4096, _highWater.Value); } else { if ((object)entry != _sendRateMax) { return; } ApplySendRate(); } FeatureModule.Log.LogInfo((object)("[ClientNet] highWater=" + HighWaterBytes + "B sendRateMax=" + _sendRateMax.Value + "B/s")); } public override string StatusDetail() { if (!Applied) { return null; } return "highWater=" + HighWaterBytes + "B sendRateMax=" + ((_appliedRate >= 0) ? (_appliedRate + "B/s (applied)") : (_sendRateMax.Value + "B/s (pending)")); } internal static int GetHighWaterBytes() { if (!Active2 || !FeatureModule.ClientActive()) { return 10240; } return HighWaterBytes; } private static IEnumerable Transpiler(IEnumerable instructions) { List list = new List(instructions); int num = 0; for (int i = 0; i < list.Count; i++) { if (ILUtil.TryGetI4(list[i], out var value) && value == 10240) { ILUtil.ReplaceWithCall(list[i], typeof(ClientNetModule), "GetHighWaterBytes"); num++; } } if (num != 2) { string text = "SmoothServer ClientNet transpiler: expected exactly 2x " + 10240 + " in ZDOMan.SendZDOs, found " + num + " - game IL changed (or another mod patched it first), refusing to patch"; SmoothServerPlugin.Log.LogError((object)text); throw new Exception(text); } SmoothServerPlugin.Log.LogInfo((object)("[ClientNet] transpiler OK: " + num + "x highWater replaced (assertion 2 passed)")); return list; } private static void RegisterGlobalCallbacksPostfix() { if (Instance != null) { Instance.ApplySendRate(); } } private void ApplySendRate() { if (Active2 && _sendRateMax != null) { int value = Math.Max(153600, _sendRateMax.Value); int configInt = GetConfigInt((ESteamNetworkingConfigValue)11); if (SetConfigInt((ESteamNetworkingConfigValue)11, value)) { int configInt2 = GetConfigInt((ESteamNetworkingConfigValue)11); int configInt3 = GetConfigInt((ESteamNetworkingConfigValue)10); _appliedRate = configInt2; FeatureModule.Log.LogInfo((object)("[ClientNet] Steam SendRateMax: " + configInt + " -> " + configInt2 + " (attempted " + value + "); SendRateMin left at " + configInt3)); } } } private static int GetConfigInt(ESteamNetworkingConfigValue key) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) ulong num = 4uL; byte[] value = new byte[4]; GCHandle gCHandle = GCHandle.Alloc(value, GCHandleType.Pinned); try { ESteamNetworkingConfigDataType val = default(ESteamNetworkingConfigDataType); SteamNetworkingUtils.GetConfigValue(key, (ESteamNetworkingConfigScope)1, IntPtr.Zero, ref val, gCHandle.AddrOfPinnedObject(), ref num); } catch { return -1; } finally { gCHandle.Free(); } return BitConverter.ToInt32(value, 0); } private unsafe static bool SetConfigInt(ESteamNetworkingConfigValue key, int value) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) GCHandle gCHandle = GCHandle.Alloc(value, GCHandleType.Pinned); try { SteamNetworkingUtils.SetConfigValue(key, (ESteamNetworkingConfigScope)1, IntPtr.Zero, (ESteamNetworkingConfigDataType)1, gCHandle.AddrOfPinnedObject()); return true; } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[ClientNet] could not set " + ((object)(*(ESteamNetworkingConfigValue*)(&key))/*cast due to .constrained prefix*/).ToString() + ": " + ex.Message)); return false; } finally { gCHandle.Free(); } } } internal sealed class CompressionModule : FeatureModule { private sealed class PeerState { public bool CapsSeen; public bool RecvFramed; public bool SendFramed; public bool SentCaps; public bool SentReady; public int TheirProto; public int TheirDictHash; public readonly HashSet Framed = new HashSet(); } private const int Proto = 1; internal const byte TagRaw = 0; internal const byte TagSmall = 1; internal const byte TagBig = 2; internal const string RpcCaps = "SS_Caps"; internal const string RpcReady = "SS_Ready"; private const string ResSmall = "SmoothServer.dict.small"; private const string ResBig = "SmoothServer.dict.big"; private ConfigEntry _minBytes; private ConfigEntry _level; private ConfigEntry _useBigDict; internal static bool Active2; internal static int MinBytes = 256; internal static byte SendTag = 1; private static Compressor _cSmall; private static Compressor _cBig; private static Decompressor _dSmall; private static Decompressor _dBig; private static int _dictHash; private static bool _codecsReady; private static readonly Dictionary States = new Dictionary(); private static bool _rpcsRegistered; private static float _handshakeTimer; private static float _statsTimer; private static bool _selfTestLogged; internal static long RawOut; internal static long WireOut; internal static long RawIn; internal static long WireIn; internal static int FramedPeers; public override string Name => "Compression"; public override ModuleSide Side => ModuleSide.Both; public override string Section => "Compression"; internal static bool IsFramedFor(long peerUid) { if (!Active2) { return false; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return false; } ZNetPeer peer = instance.GetPeer(peerUid); ZSteamSocket val = (ZSteamSocket)((peer != null) ? /*isinst with value type is only supported in some contexts*/: null); if (val == null) { return false; } if (States.TryGetValue(val, out PeerState value)) { return value.SendFramed; } return false; } protected override void Bind() { _minBytes = BindSynced("MinBytes", 256, "Payloads smaller than this are sent raw (tag 0x00). Small packets do not compress usefully and the CPU is better spent elsewhere. Vanilla-equivalent: infinity."); _level = BindSynced("Level", 1, "zstd compression level (1-9). 1 is what BetterNetworking used and is the right answer for a real-time transport: almost all of the ratio, almost none of the CPU."); _useBigDict = BindSynced("UseBigDictionary", defaultValue: false, "Compress with the 512 KB trained dictionary instead of the 110 KB one. Slightly better ratio, 512 KB more resident memory per process. Both dictionaries are always loaded for DEcompression, so peers may disagree on this without any loss of compatibility - the frame tag says which one each message used."); } protected override void ApplyPatches() { //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Expected O, but got Unknown //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Expected O, but got Unknown //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_0197: Expected O, but got Unknown //IL_01af: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Expected O, but got Unknown if (SmoothServerPlugin.BetterNetworkingPresent) { throw new Exception("BetterNetworking is installed - it wraps the same ZSteamSocket send queue and the two cannot coexist. Uninstall BetterNetworking."); } MinBytes = Math.Max(0, _minBytes.Value); SendTag = (byte)((!_useBigDict.Value) ? 1 : 2); InitCodecs(Math.Max(1, Math.Min(9, _level.Value))); MethodInfo methodInfo = AccessTools.Method(typeof(ZSteamSocket), "SendQueuedPackages", (Type[])null, (Type[])null); if (methodInfo == null) { throw new Exception("ZSteamSocket.SendQueuedPackages not found"); } MethodInfo methodInfo2 = AccessTools.Method(typeof(ZSteamSocket), "Recv", (Type[])null, (Type[])null); if (methodInfo2 == null) { throw new Exception("ZSteamSocket.Recv not found"); } ConstructorInfo constructorInfo = AccessTools.Constructor(typeof(ZRoutedRpc), new Type[1] { typeof(bool) }, false); if (constructorInfo == null) { throw new Exception("ZRoutedRpc(bool) constructor not found"); } MethodInfo methodInfo3 = AccessTools.Method(typeof(ZNet), "Disconnect", new Type[1] { typeof(ZNetPeer) }, (Type[])null); if (methodInfo3 == null) { throw new Exception("ZNet.Disconnect(ZNetPeer) not found"); } Harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(CompressionModule), "SendPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(CompressionModule), "RecvPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)constructorInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(CompressionModule), "RoutedRpcCtorPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeof(CompressionModule), "DisconnectPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Active2 = true; SelfTest(); } public override void Disable() { Active2 = false; States.Clear(); base.Disable(); } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == EnabledCfg) { Active2 = Applied && base.Enabled; if (!Active2) { States.Clear(); } } else if ((object)entry == _minBytes) { MinBytes = Math.Max(0, _minBytes.Value); } else if ((object)entry == _useBigDict) { SendTag = (byte)((!_useBigDict.Value) ? 1 : 2); } else { if ((object)entry != _level) { return; } InitCodecs(Math.Max(1, Math.Min(9, _level.Value))); } FeatureModule.Log.LogInfo((object)("[Compression] minBytes=" + MinBytes + " sendDict=" + DictName(SendTag) + " level=" + _level.Value)); } public override string StatusDetail() { if (!Applied) { return null; } return "dict=" + DictName(SendTag) + " minBytes=" + MinBytes + " framedPeers=" + FramedPeers + " out=" + RawOut + "->" + WireOut + "B in=" + WireIn + "->" + RawIn + "B" + ((FramedPeers == 0) ? " (waiting for peers)" : ""); } private static string DictName(byte tag) { return tag switch { 1 => "small", 2 => "big", _ => "none", }; } private static byte[] LoadResource(string name) { using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name); if (stream == null) { throw new Exception("embedded resource '" + name + "' missing"); } byte[] array = new byte[stream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = stream.Read(array, i, array.Length - i); if (num <= 0) { break; } } return array; } private static void InitCodecs(int level) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown byte[] array = LoadResource("SmoothServer.dict.small"); byte[] array2 = LoadResource("SmoothServer.dict.big"); _cSmall = new Compressor(level); _cSmall.LoadDictionary(array); _cBig = new Compressor(level); _cBig.LoadDictionary(array2); _dSmall = new Decompressor(); _dSmall.LoadDictionary(array); _dBig = new Decompressor(); _dBig.LoadDictionary(array2); _dictHash = Fnv(array) * 31 + Fnv(array2); _codecsReady = true; } private static int Fnv(byte[] data) { int num = -2128831035; for (int i = 0; i < data.Length; i++) { num = (num ^ data[i]) * 16777619; } return num; } private void SelfTest() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (_selfTestLogged) { return; } _selfTestLogged = true; ZPackage val = new ZPackage(); for (int i = 0; i < 170; i++) { val.Write(-1234567890); val.Write(9000000000000000000L + i); val.Write(1.5f); val.Write(0f); val.Write(-42.25f); val.Write("Greydwarf"); } byte[] array = val.GetArray(); List list = new List(); byte[] array2 = new byte[2] { 1, 2 }; foreach (byte tag in array2) { byte[] array3 = Compress(array, tag); byte[] array4 = Decompress(array3, tag); bool flag = array4.Length == array.Length; if (flag) { for (int k = 0; k < array.Length; k++) { if (array[k] != array4[k]) { flag = false; break; } } } list.Add(DictName(tag) + "=" + array3.Length + "B (" + (100f * (float)array3.Length / (float)array.Length).ToString("0.0") + "%) roundtrip=" + (flag ? "OK" : "MISMATCH")); if (!flag) { throw new Exception("zstd round trip failed with dict/" + DictName(tag)); } } FeatureModule.Log.LogInfo((object)("[Compression] self-test: " + array.Length + "B ZPackage -> " + string.Join(", ", list.ToArray()) + "; ZstdSharp " + typeof(Compressor).Assembly.GetName().Version?.ToString() + " loaded, dictHash=" + _dictHash.ToString("x8") + ", sendDict=" + DictName(SendTag))); } internal static byte[] Compress(byte[] raw, byte tag) { return ((tag == 2) ? _cBig : _cSmall).Wrap((ReadOnlySpan)raw).ToArray(); } internal static byte[] Decompress(byte[] payload, byte tag) { return ((tag == 2) ? _dBig : _dSmall).Unwrap((ReadOnlySpan)payload, int.MaxValue).ToArray(); } private static byte[] Frame(byte[] raw) { byte b = SendTag; if (raw.Length < MinBytes) { b = 0; } byte[] array = raw; if (b != 0) { try { array = Compress(raw, b); } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[Compression] compress failed, sending raw: " + ex.Message)); b = 0; array = raw; } if (array.Length >= raw.Length) { b = 0; array = raw; } } byte[] array2 = new byte[array.Length + 1]; array2[0] = b; Buffer.BlockCopy(array, 0, array2, 1, array.Length); RawOut += raw.Length; WireOut += array2.Length; return array2; } private static byte[] Unframe(byte[] framed) { if (framed.Length < 1) { return framed; } byte b = framed[0]; byte[] array = new byte[framed.Length - 1]; Buffer.BlockCopy(framed, 1, array, 0, array.Length); byte[] array2; switch (b) { case 0: array2 = array; break; case 1: case 2: array2 = Decompress(array, b); break; default: throw new Exception("unknown frame tag 0x" + b.ToString("x2")); } WireIn += framed.Length; RawIn += array2.Length; return array2; } private static PeerState Get(ZSteamSocket s, bool create) { if (s == null) { return null; } if (States.TryGetValue(s, out PeerState value)) { return value; } if (!create) { return null; } value = new PeerState(); States[s] = value; return value; } private static void SendPrefix(ZSteamSocket __instance, ref Queue ___m_sendQueue) { if (!Active2 || !_codecsReady) { return; } PeerState peerState = Get(__instance, create: false); if (peerState == null || !peerState.SendFramed || ___m_sendQueue == null || ___m_sendQueue.Count == 0) { return; } Queue queue = new Queue(___m_sendQueue.Count); HashSet hashSet = new HashSet(); foreach (byte[] item2 in ___m_sendQueue) { if (item2 == null) { continue; } if (peerState.Framed.Contains(item2)) { queue.Enqueue(item2); hashSet.Add(item2); continue; } byte[] item; try { item = Frame(item2); } catch (Exception ex) { FeatureModule.Log.LogError((object)("[Compression] framing failed: " + ex)); queue.Enqueue(item2); continue; } queue.Enqueue(item); hashSet.Add(item); } peerState.Framed.Clear(); foreach (byte[] item3 in hashSet) { peerState.Framed.Add(item3); } ___m_sendQueue = queue; } private static void RecvPostfix(ZSteamSocket __instance, ref ZPackage __result) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Expected O, but got Unknown if (!Active2 || !_codecsReady || __result == null) { return; } PeerState peerState = Get(__instance, create: false); if (peerState == null || !peerState.RecvFramed) { return; } try { __result = new ZPackage(Unframe(__result.GetArray())); } catch (Exception ex) { peerState.RecvFramed = false; peerState.SendFramed = false; FeatureModule.Log.LogError((object)("[Compression] unframing failed for " + __instance.GetHostName() + " - compression disabled for this peer: " + ex.Message)); } } private static void DisconnectPrefix(ZNetPeer peer) { ZSteamSocket val = (ZSteamSocket)((peer != null) ? /*isinst with value type is only supported in some contexts*/: null); if (val != null && States.Remove(val)) { RecountFramed(); } } private static void RoutedRpcCtorPostfix(ZRoutedRpc __instance) { States.Clear(); FramedPeers = 0; _rpcsRegistered = false; TryRegisterRpcs(); } private static void TryRegisterRpcs() { if (_rpcsRegistered) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { return; } try { instance.Register("SS_Caps", (Action)OnCaps); instance.Register("SS_Ready", (Action)OnReady); _rpcsRegistered = true; FeatureModule.Log.LogInfo((object)"[Compression] routed RPCs 'SS_Caps' / 'SS_Ready' registered"); } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[Compression] could not register routed RPCs: " + ex.Message)); _rpcsRegistered = true; } } private static ZSteamSocket SocketOf(long peerId) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return null; } ZNetPeer peer = instance.GetPeer(peerId); if (peer == null) { return null; } ISocket socket = peer.m_socket; return (ZSteamSocket)(object)((socket is ZSteamSocket) ? socket : null); } private static void SendCaps(long peerId) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(_dictHash); val.Write(Active2 ? 1 : 0); ZRoutedRpc.instance.InvokeRoutedRPC(peerId, "SS_Caps", new object[1] { val }); } private static void OnCaps(long sender, ZPackage pkg) { if (!Active2 || !_codecsReady) { return; } ZSteamSocket val = SocketOf(sender); if (val == null) { return; } PeerState peerState = Get(val, create: true); int num = pkg.ReadInt(); int num2 = pkg.ReadInt(); int num3 = pkg.ReadInt(); peerState.CapsSeen = true; peerState.TheirProto = num; peerState.TheirDictHash = num2; if (num != 1 || num2 != _dictHash || num3 == 0) { FeatureModule.Log.LogWarning((object)("[Compression] " + val.GetHostName() + " advertises proto=" + num + " dict=" + num2.ToString("x8") + " enabled=" + num3 + " (ours proto=" + 1 + " dict=" + _dictHash.ToString("x8") + ") - staying uncompressed with this peer")); if (!peerState.SentCaps) { peerState.SentCaps = true; SendCaps(sender); } } else { peerState.RecvFramed = true; if (!peerState.SentCaps) { peerState.SentCaps = true; SendCaps(sender); } if (!peerState.SentReady) { peerState.SentReady = true; ZRoutedRpc.instance.InvokeRoutedRPC(sender, "SS_Ready", new object[0]); } } } private static void OnReady(long sender) { if (!Active2 || !_codecsReady) { return; } ZSteamSocket val = SocketOf(sender); if (val != null) { PeerState peerState = Get(val, create: true); if (!peerState.SendFramed) { peerState.SendFramed = true; RecountFramed(); FeatureModule.Log.LogInfo((object)("[Compression] " + val.GetHostName() + " negotiated: framing on (dict=" + DictName(SendTag) + ", proto " + 1 + ")")); } } } private static void RecountFramed() { int num = 0; foreach (KeyValuePair state in States) { if (state.Value.SendFramed) { num++; } } FramedPeers = num; } internal static void Tick(float dt) { if (!Active2 || !_codecsReady) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { if (States.Count > 0) { States.Clear(); FramedPeers = 0; } } else { if (ZRoutedRpc.instance == null) { return; } TryRegisterRpcs(); if (!_rpcsRegistered) { return; } _handshakeTimer += dt; if (_handshakeTimer >= 2f) { _handshakeTimer = 0f; if (!instance.IsServer()) { foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { ISocket socket = connectedPeer.m_socket; ZSteamSocket val = (ZSteamSocket)(object)((socket is ZSteamSocket) ? socket : null); if (val == null) { continue; } PeerState peerState = Get(val, create: true); if (!peerState.SentCaps && !peerState.CapsSeen) { peerState.SentCaps = true; try { SendCaps(connectedPeer.m_uid); } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[Compression] caps send failed: " + ex.Message)); } } } } List list = null; foreach (KeyValuePair state in States) { if (state.Key == null || !state.Key.IsConnected()) { (list ?? (list = new List())).Add(state.Key); } } if (list != null) { foreach (ZSteamSocket item in list) { States.Remove(item); } RecountFramed(); } } _statsTimer += dt; if (_statsTimer >= 300f) { _statsTimer = 0f; if (FramedPeers > 0 && RawOut > 0) { FeatureModule.Log.LogInfo((object)("[Compression] peers=" + FramedPeers + " out " + RawOut + "->" + WireOut + "B (" + (100f * (float)WireOut / (float)Math.Max(1L, RawOut)).ToString("0.0") + "%) in " + WireIn + "->" + RawIn + "B (" + (100f * (float)WireIn / (float)Math.Max(1L, RawIn)).ToString("0.0") + "%)")); } } } } } } namespace SmoothServer.Map { internal sealed class MapSelfTestModule : FeatureModule { private string _result = "not run"; public override string Name => "MapSelfTest"; public override ModuleSide Side => ModuleSide.Server; public override string Section => "MapSelfTest"; public override bool DefaultEnabled => true; protected override string EnabledDescription => "Run the SharedMap store unit tests at startup and log one PASS/FAIL line."; protected override void ApplyPatches() { //IL_06c1: Unknown result type (might be due to invalid IL or missing references) //IL_06c8: Expected O, but got Unknown //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Expected O, but got Unknown //IL_024c: Unknown result type (might be due to invalid IL or missing references) //IL_0256: Expected O, but got Unknown //IL_02f1: Unknown result type (might be due to invalid IL or missing references) //IL_02f8: Expected O, but got Unknown //IL_044b: Unknown result type (might be due to invalid IL or missing references) //IL_0450: Unknown result type (might be due to invalid IL or missing references) List list = new List(); List list2 = new List(); try { MapStore mapStore = new MapStore(64); if (mapStore.Bits.Length != 512) { list.Add("bitset length " + mapStore.Bits.Length + " != 512"); } if (!mapStore.Set(1000)) { list.Add("Set(1000) said already-set"); } if (mapStore.Set(1000)) { list.Add("Set(1000) twice reported new"); } if (!mapStore.Get(1000)) { list.Add("Get(1000) false after Set"); } if (mapStore.Get(1001)) { list.Add("Get(1001) true, never set"); } if (mapStore.Explored() != 1) { list.Add("Explored()=" + mapStore.Explored() + " != 1"); } MapStore mapStore2 = new MapStore(256); HashSet hashSet = new HashSet(); Random random = new Random(1234); for (int i = 0; i < 500; i++) { hashSet.Add(random.Next(0, 65536)); } ZPackage val = mapStore2.EncodeDelta(hashSet); MapStore mapStore3 = new MapStore(256); List list3 = mapStore3.MergeDelta(new ZPackage(val.GetArray())); if (list3 == null) { list.Add("MergeDelta returned null"); } else { if (list3.Count != hashSet.Count) { list.Add("delta round trip: " + list3.Count + " != " + hashSet.Count); } foreach (int item in hashSet) { if (!mapStore3.Get(item)) { list.Add("delta lost index " + item); break; } } list2.Add("delta " + hashSet.Count + "px=" + val.GetArray().Length + "B"); } List list4 = mapStore3.MergeDelta(new ZPackage(val.GetArray())); if (list4 == null || list4.Count != 0) { list.Add("re-merge reported " + ((list4 == null) ? "null" : list4.Count.ToString()) + " new"); } MapStore mapStore4 = new MapStore(2048); for (int j = 0; j < 4194304; j += 997) { mapStore4.Set(j); } int fullChunkCount = mapStore4.FullChunkCount; MapStore mapStore5 = new MapStore(2048); int num = 0; for (int k = 0; k < fullChunkCount; k++) { ZPackage val2 = new ZPackage(mapStore4.EncodeFullChunk(k).GetArray()); num += val2.GetArray().Length; if (!MapStore.DecodeFullChunk(val2, out int mapSize, out int chunkIndex, out int chunkCount, out int byteOffset, out byte[] slice)) { list.Add("full chunk " + k + " decode failed"); break; } if (mapSize != 2048 || chunkIndex != k || chunkCount != fullChunkCount) { list.Add("full chunk header wrong at " + k); } Buffer.BlockCopy(slice, 0, mapStore5.Bits, byteOffset, slice.Length); } if (mapStore5.Explored() != mapStore4.Explored()) { list.Add("full sync explored " + mapStore5.Explored() + " != " + mapStore4.Explored()); } list2.Add("full 2048^2 " + mapStore4.Explored() + "px in " + fullChunkCount + " chunks=" + num + "B"); mapStore4.Pins.Add(new SharedPin { Name = "Silver", Pos = new Vector3(1f, 2f, 3f), Type = 3, Checked = true, OwnerId = 7L }); string text = Path.Combine(Path.GetTempPath(), "smoothserver-selftest.map"); mapStore4.SaveTo(text); long length = new FileInfo(text).Length; MapStore mapStore6 = MapStore.LoadFrom(text); File.Delete(text); if (mapStore6.MapSize != 2048) { list.Add("persist mapSize " + mapStore6.MapSize); } if (mapStore6.Explored() != mapStore4.Explored()) { list.Add("persist explored " + mapStore6.Explored() + " != " + mapStore4.Explored()); } if (mapStore6.Pins.Count != 1 || mapStore6.Pins[0].Name != "Silver" || !mapStore6.Pins[0].Checked) { list.Add("persist pins wrong"); } list2.Add("persist 2048^2=" + length + "B (ServerSideMap would be 4194316B)"); string text2 = null; try { string worldSavePath = World.GetWorldSavePath((FileSource)1); if (Directory.Exists(worldSavePath)) { string[] files = Directory.GetFiles(worldSavePath, "*.mod.serversidemap.explored"); if (files.Length != 0) { text2 = files[0]; } } } catch { text2 = null; } if (text2 != null) { byte[] array = File.ReadAllBytes(text2); int exploredPixels; int version; MapStore mapStore7 = MapStore.ImportServerSideMap(array, out exploredPixels, out version); if (mapStore7.Explored() != exploredPixels) { list.Add("import count mismatch"); } long num2 = 8 + (long)mapStore7.MapSize * (long)mapStore7.MapSize + 4; list2.Add("import " + Path.GetFileName(text2) + " v" + version + " " + mapStore7.MapSize + "^2 " + array.Length + "B" + ((array.Length == num2) ? "" : (" (expected " + num2 + "B!)")) + " -> " + exploredPixels + "px"); } else { ZPackage val3 = new ZPackage(); val3.Write(3); val3.Write(16); for (int l = 0; l < 256; l++) { val3.Write(l % 5 == 0); } val3.Write(0); int exploredPixels2; int version2; MapStore mapStore8 = MapStore.ImportServerSideMap(val3.GetArray(), out exploredPixels2, out version2); if (version2 != 3 || mapStore8.MapSize != 16 || exploredPixels2 != 52) { list.Add("synthetic import v" + version2 + " size" + mapStore8.MapSize + " px" + exploredPixels2); } list2.Add("import(synthetic) 16^2 -> " + exploredPixels2 + "px"); } } catch (Exception ex) { list.Add("threw: " + ex.Message); } _result = ((list.Count == 0) ? "PASS" : ("FAIL(" + string.Join("; ", list.ToArray()) + ")")); FeatureModule.Log.LogInfo((object)("[MapSelfTest] " + _result + " - " + string.Join(", ", list2.ToArray()))); if (list.Count > 0) { throw new Exception(_result); } } public override string StatusDetail() { return _result; } } internal sealed class SharedPin { public string Name = ""; public Vector3 Pos; public int Type; public bool Checked; public long OwnerId; public string Key => Type + "|" + Mathf.RoundToInt(Pos.x) + "|" + Mathf.RoundToInt(Pos.z) + "|" + Name; } internal sealed class MapStore { internal const int Magic = 1397968208; internal const int Version = 1; internal const int ChunkBytes = 64; internal const int FullChunkBytes = 32768; public readonly List Pins = new List(); public int MapSize { get; private set; } public byte[] Bits { get; private set; } public bool Sized { get { if (MapSize > 0) { return Bits != null; } return false; } } public int PixelCount => MapSize * MapSize; public int FullChunkCount { get { if (Bits != null) { return (Bits.Length + 32768 - 1) / 32768; } return 0; } } public MapStore() { } public MapStore(int mapSize) { Resize(mapSize); } public void Resize(int mapSize) { if (mapSize <= 0) { throw new ArgumentException("mapSize must be positive"); } MapSize = mapSize; long num = (long)mapSize * (long)mapSize; Bits = new byte[(num + 7) / 8]; } public bool Get(int index) { if (Bits == null || index < 0 || index >= PixelCount) { return false; } return (Bits[index >> 3] & (1 << (index & 7))) != 0; } public bool Set(int index) { if (Bits == null || index < 0 || index >= PixelCount) { return false; } int num = index >> 3; int num2 = 1 << (index & 7); if ((Bits[num] & num2) != 0) { return false; } Bits[num] = (byte)(Bits[num] | num2); return true; } public int Explored() { if (Bits == null) { return 0; } int num = 0; for (int i = 0; i < Bits.Length; i++) { for (int num2 = Bits[i]; num2 != 0; num2 >>= 1) { num += num2 & 1; } } return num; } public ZPackage EncodeDelta(ICollection indices) { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Expected O, but got Unknown //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Expected O, but got Unknown Dictionary dictionary = new Dictionary(); foreach (int index in indices) { if (index >= 0 && index < PixelCount) { int num = index >> 3; int num2 = num / 64; if (!dictionary.TryGetValue(num2, out var value)) { value = (dictionary[num2] = new byte[64]); } value[num - num2 * 64] |= (byte)(1 << (index & 7)); } } ZPackage val = new ZPackage(); val.Write(MapSize); val.Write(dictionary.Count); foreach (KeyValuePair item in dictionary) { val.Write(item.Key); val.Write(item.Value); } ZPackage val2 = new ZPackage(); val2.WriteCompressed(val); return val2; } public List MergeDelta(ZPackage wire) { ZPackage val = wire.ReadCompressedPackage(); int num = val.ReadInt(); if (!Sized) { Resize(num); } if (num != MapSize) { return null; } int num2 = val.ReadInt(); List list = new List(); for (int i = 0; i < num2; i++) { int num3 = val.ReadInt(); byte[] array = val.ReadByteArray(); if (array == null) { continue; } int num4 = num3 * 64; for (int j = 0; j < array.Length; j++) { if (array[j] == 0) { continue; } int num5 = num4 + j; if (num5 >= Bits.Length) { break; } for (int k = 0; k < 8; k++) { if ((array[j] & (1 << k)) != 0) { int num6 = (num5 << 3) | k; if (num6 >= PixelCount) { break; } if (Set(num6)) { list.Add(num6); } } } } } return list; } public ZPackage EncodeFullChunk(int chunkIndex) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Expected O, but got Unknown int num = chunkIndex * 32768; int num2 = Math.Min(32768, Bits.Length - num); byte[] array = new byte[Math.Max(0, num2)]; if (num2 > 0) { Buffer.BlockCopy(Bits, num, array, 0, num2); } ZPackage val = new ZPackage(); val.Write(MapSize); val.Write(chunkIndex); val.Write(FullChunkCount); val.Write(num); val.Write(array); ZPackage val2 = new ZPackage(); val2.WriteCompressed(val); return val2; } public static bool DecodeFullChunk(ZPackage wire, out int mapSize, out int chunkIndex, out int chunkCount, out int byteOffset, out byte[] slice) { ZPackage val = wire.ReadCompressedPackage(); mapSize = val.ReadInt(); chunkIndex = val.ReadInt(); chunkCount = val.ReadInt(); byteOffset = val.ReadInt(); slice = val.ReadByteArray(); return slice != null; } public ZPackage Serialize() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: 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_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(Bits ?? new byte[0]); val.Write(Pins.Count); foreach (SharedPin pin in Pins) { val.Write(pin.Name ?? ""); val.Write(pin.Pos.x); val.Write(pin.Pos.y); val.Write(pin.Pos.z); val.Write(pin.Type); val.Write(pin.Checked); val.Write(pin.OwnerId); } ZPackage val2 = new ZPackage(); val2.Write(1397968208); val2.Write(1); val2.Write(MapSize); val2.WriteCompressed(val); return val2; } public static MapStore Deserialize(ZPackage pkg) { //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) int num = pkg.ReadInt(); if (num != 1397968208) { throw new Exception("not a SmoothServer map file (magic 0x" + num.ToString("x8") + ")"); } int num2 = pkg.ReadInt(); if (num2 != 1) { throw new Exception("unsupported map file version " + num2); } MapStore mapStore = new MapStore(pkg.ReadInt()); ZPackage val = pkg.ReadCompressedPackage(); byte[] array = val.ReadByteArray(); if (array != null && array.Length == mapStore.Bits.Length) { mapStore.Bits = array; } else if (array != null) { Buffer.BlockCopy(array, 0, mapStore.Bits, 0, Math.Min(array.Length, mapStore.Bits.Length)); } int num3 = val.ReadInt(); for (int i = 0; i < num3; i++) { SharedPin sharedPin = new SharedPin(); sharedPin.Name = val.ReadString(); float num4 = val.ReadSingle(); float num5 = val.ReadSingle(); float num6 = val.ReadSingle(); sharedPin.Pos = new Vector3(num4, num5, num6); sharedPin.Type = val.ReadInt(); sharedPin.Checked = val.ReadBool(); sharedPin.OwnerId = val.ReadLong(); mapStore.Pins.Add(sharedPin); } return mapStore; } public void SaveTo(string path) { string directoryName = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } byte[] array = Serialize().GetArray(); string text = path + ".tmp"; File.WriteAllBytes(text, array); if (File.Exists(path)) { File.Delete(path); } File.Move(text, path); } public static MapStore LoadFrom(string path) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown return Deserialize(new ZPackage(File.ReadAllBytes(path))); } public static MapStore ImportServerSideMap(byte[] raw, out int exploredPixels, out int version) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) ZPackage val = new ZPackage(raw); version = val.ReadInt(); int num = val.ReadInt(); if (num <= 0 || (long)num * (long)num > 67108864) { throw new Exception("implausible mapSize " + num + " in ServerSideMap file"); } MapStore mapStore = new MapStore(num); int num2 = num * num; int num3 = 0; for (int i = 0; i < num2; i++) { if (val.ReadBool()) { mapStore.Set(i); num3++; } } int num4 = 0; try { num4 = val.ReadInt(); } catch { num4 = 0; } for (int j = 0; j < num4; j++) { try { SharedPin sharedPin = new SharedPin(); sharedPin.Name = val.ReadString(); float num5 = val.ReadSingle(); float num6 = val.ReadSingle(); float num7 = val.ReadSingle(); sharedPin.Pos = new Vector3(num5, num6, num7); sharedPin.Type = val.ReadInt(); sharedPin.Checked = val.ReadBool(); mapStore.Pins.Add(sharedPin); } catch { break; } } exploredPixels = num3; return mapStore; } } internal sealed class SharedMapModule : FeatureModule { internal const string RpcHello = "SS_MapHello"; internal const string RpcFull = "SS_MapFull"; internal const string RpcDelta = "SS_MapDelta"; internal const string RpcPin = "SS_MapPin"; private const int PinTypeDeath = 4; private ConfigEntry _shareExploration; private ConfigEntry _sharePins; private ConfigEntry _sharedPinTypes; private ConfigEntry _shareDeathPins; private ConfigEntry _excludeTable; private ConfigEntry _deltaHz; private ConfigEntry _import; internal static bool Active2; private static bool _shareExpl = true; private static bool _sharePinsV = true; private static bool _deathPins; private static bool _excludeTableV; private static float _hz = 1f; private static readonly HashSet AllowedPinTypes = new HashSet(); internal static MapStore Store; private static string _storePath; private static bool _dirty; internal static string ImportSummary; private static readonly Dictionary PendingFull = new Dictionary(); private static readonly Dictionary PendingFullCount = new Dictionary(); private static readonly HashSet PendingDelta = new HashSet(); private static float _deltaTimer; private static float _helloTimer; private static bool _helloSent; private static bool _applying; private static bool _inTableImport; private static int _clientMapSize; private static bool _rpcsRegistered; private static bool _importValue = true; public override string Name => "SharedMap"; public override ModuleSide Side => ModuleSide.Both; public override string Section => "Map"; private static string StoreDir => Path.Combine(Paths.ConfigPath, "smoothserver"); protected override void Bind() { _shareExploration = BindSynced("ShareExploration", defaultValue: true, "Share explored map area between everyone on the server."); _sharePins = BindSynced("SharePins", defaultValue: true, "Share map pins between everyone on the server."); _sharedPinTypes = BindSynced("SharedPinTypes", "Icon0,Icon1,Icon2,Icon3,Icon4,Bed,Boss,Hildir1,Hildir2,Hildir3", "Comma-separated Minimap.PinType names that propagate. Death is controlled separately by ShareDeathPins. Shout/Ping/Player/EventArea/RandomEvent are transient and are never shared (vanilla creates them with save=false)."); _shareDeathPins = BindSynced("ShareDeathPins", defaultValue: false, "Also share tombstone (Death) pins. Turn this ON for a group running NoVikingLeftBehind's CorpseRunPlus - it makes a dead viking's grave visible to the whole party instead of only to the corpse's owner."); _excludeTable = BindSynced("ExcludeCartographyTable", defaultValue: false, "Do not feed exploration that arrived from a vanilla Cartography Table into the shared map. The table uses a completely separate transport (ZDO blobs via Minimap.GetSharedMapData/AddSharedMapData, not RPCs), so it coexists with SharedMap either way; this only decides whether the table's contents leak into the ambient server-wide share."); _deltaHz = BindSynced("DeltaHz", 1f, "How many times a second a client flushes its newly-explored pixels to the server. 1 Hz collapses a sprinting player's pixel burst into one compressed message. Higher costs bandwidth for a barely-perceptible latency win."); _import = BindSynced("ImportServerSideMapFile", defaultValue: true, "One-shot migration: if no .map exists yet but ServerSideMap's .mod.serversidemap.explored does, import it. Runs once - after the first save our own file exists and this does nothing."); ReadConfig(); } private void ReadConfig() { _shareExpl = _shareExploration.Value; _sharePinsV = _sharePins.Value; _deathPins = _shareDeathPins.Value; _excludeTableV = _excludeTable.Value; _hz = Mathf.Clamp(_deltaHz.Value, 0.1f, 20f); _importValue = _import.Value; AllowedPinTypes.Clear(); string[] array = (_sharedPinTypes.Value ?? "").Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0) { try { AllowedPinTypes.Add((int)Enum.Parse(typeof(PinType), text, ignoreCase: true)); } catch { FeatureModule.Log.LogWarning((object)("[SharedMap] unknown pin type '" + text + "' in SharedPinTypes")); } } } if (_deathPins) { AllowedPinTypes.Add(4); } else { AllowedPinTypes.Remove(4); } } public override void OnConfigChanged(ConfigEntryBase entry) { if ((object)entry == EnabledCfg) { Active2 = Applied && base.Enabled; } ReadConfig(); } protected override void ApplyPatches() { //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Expected O, but got Unknown //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Expected O, but got Unknown //IL_0205: Unknown result type (might be due to invalid IL or missing references) //IL_0212: Expected O, but got Unknown //IL_022a: Unknown result type (might be due to invalid IL or missing references) //IL_0238: Expected O, but got Unknown //IL_0273: Unknown result type (might be due to invalid IL or missing references) //IL_0288: Unknown result type (might be due to invalid IL or missing references) //IL_0295: Expected O, but got Unknown //IL_0295: Expected O, but got Unknown //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Expected O, but got Unknown //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_01c1: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(Minimap), "Explore", new Type[2] { typeof(int), typeof(int) }, (Type[])null); if (methodInfo == null) { throw new Exception("Minimap.Explore(int,int) not found"); } MethodInfo methodInfo2 = AccessTools.Method(typeof(Minimap), "AddPin", (Type[])null, (Type[])null); if (methodInfo2 == null) { throw new Exception("Minimap.AddPin not found"); } MethodInfo methodInfo3 = AccessTools.Method(typeof(Minimap), "RemovePin", new Type[1] { typeof(PinData) }, (Type[])null); if (methodInfo3 == null) { throw new Exception("Minimap.RemovePin(PinData) not found"); } ConstructorInfo constructorInfo = AccessTools.Constructor(typeof(ZRoutedRpc), new Type[1] { typeof(bool) }, false); if (constructorInfo == null) { throw new Exception("ZRoutedRpc(bool) constructor not found"); } Harmony.Patch((MethodBase)constructorInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SharedMapModule), "RoutedRpcCtorPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); if (SmoothServerPlugin.IsServerSide) { MethodInfo methodInfo4 = AccessTools.Method(typeof(ZNet), "LoadWorld", (Type[])null, (Type[])null); if (methodInfo4 == null) { throw new Exception("ZNet.LoadWorld not found"); } MethodInfo methodInfo5 = AccessTools.Method(typeof(ZNet), "SaveWorldThread", (Type[])null, (Type[])null); if (methodInfo5 == null) { throw new Exception("ZNet.SaveWorldThread not found"); } Harmony.Patch((MethodBase)methodInfo4, (HarmonyMethod)null, new HarmonyMethod(typeof(SharedMapModule), "LoadWorldPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo5, (HarmonyMethod)null, new HarmonyMethod(typeof(SharedMapModule), "SaveWorldPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { Harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SharedMapModule), "ExplorePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(SharedMapModule), "AddPinPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Harmony.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeof(SharedMapModule), "RemovePinPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); MethodInfo methodInfo6 = AccessTools.Method(typeof(Minimap), "AddSharedMapData", (Type[])null, (Type[])null); if (methodInfo6 != null) { Harmony.Patch((MethodBase)methodInfo6, new HarmonyMethod(typeof(SharedMapModule), "TablePrefix", (Type[])null), new HarmonyMethod(typeof(SharedMapModule), "TablePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { FeatureModule.Log.LogWarning((object)"[SharedMap] Minimap.AddSharedMapData not found - ExcludeCartographyTable will have no effect"); } } Active2 = true; } public override void Disable() { Active2 = false; base.Disable(); } public override string StatusDetail() { if (!Applied) { return null; } if (SmoothServerPlugin.IsServerSide) { if (Store == null) { return "store not loaded yet"; } return "mapSize=" + Store.MapSize + " explored=" + Store.Explored() + " pins=" + Store.Pins.Count + " file=" + _storePath + ((ImportSummary != null) ? (" " + ImportSummary) : ""); } return "deltaHz=" + _hz + " pending=" + PendingDelta.Count + " mapSize=" + _clientMapSize; } private static void LoadWorldPostfix() { if (!Active2) { return; } try { World world = ZNet.World; if (world == null) { FeatureModule.Log.LogWarning((object)"[SharedMap] no world at LoadWorld"); return; } string name = world.m_name; _storePath = Path.Combine(StoreDir, name + ".map"); if (File.Exists(_storePath)) { Store = MapStore.LoadFrom(_storePath); FeatureModule.Log.LogInfo((object)("[SharedMap] loaded " + _storePath + ": mapSize=" + Store.MapSize + " explored=" + Store.Explored() + " pins=" + Store.Pins.Count)); return; } if (_importValue) { string path = Path.ChangeExtension(world.GetDBPath(), null) + ".mod.serversidemap.explored"; if (File.Exists(path)) { byte[] array = File.ReadAllBytes(path); Store = MapStore.ImportServerSideMap(array, out var exploredPixels, out var version); ImportSummary = "imported " + exploredPixels + " explored pixels from " + Path.GetFileName(path); FeatureModule.Log.LogInfo((object)("[SharedMap] " + ImportSummary + " (ServerSideMap v" + version + ", mapSize=" + Store.MapSize + ", " + array.Length + "B on disk, " + Store.Pins.Count + " pins)")); _dirty = true; SaveNow(); return; } } Store = new MapStore(); FeatureModule.Log.LogInfo((object)("[SharedMap] no store at " + _storePath + " - waiting for the first client to report its map size")); } catch (Exception ex) { FeatureModule.Log.LogError((object)("[SharedMap] load failed: " + ex)); Store = new MapStore(); } } private static void SaveWorldPostfix() { if (Active2) { SaveNow(); } } private static void SaveNow() { try { if (Store != null && Store.Sized && !string.IsNullOrEmpty(_storePath) && _dirty) { Store.SaveTo(_storePath); _dirty = false; FileInfo fileInfo = new FileInfo(_storePath); FeatureModule.Log.LogInfo((object)("[SharedMap] saved " + _storePath + " (" + fileInfo.Length + "B, explored=" + Store.Explored() + ", pins=" + Store.Pins.Count + ")")); } } catch (Exception ex) { FeatureModule.Log.LogError((object)("[SharedMap] save failed: " + ex)); } } private static void RoutedRpcCtorPostfix() { _rpcsRegistered = false; _helloSent = false; PendingDelta.Clear(); PendingFull.Clear(); PendingFullCount.Clear(); TryRegisterRpcs(); } private static void TryRegisterRpcs() { if (_rpcsRegistered) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { return; } try { instance.Register("SS_MapHello", (Action)OnHello); instance.Register("SS_MapFull", (Action)OnFull); instance.Register("SS_MapDelta", (Action)OnDelta); instance.Register("SS_MapPin", (Action)OnPin); _rpcsRegistered = true; FeatureModule.Log.LogInfo((object)"[SharedMap] routed RPCs 'SS_MapHello' / 'SS_MapFull' / 'SS_MapDelta' / 'SS_MapPin' registered"); } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[SharedMap] could not register routed RPCs: " + ex.Message)); _rpcsRegistered = true; } } private static void OnHello(long sender, ZPackage pkg) { if (!Active2 || !FeatureModule.ServerActive() || Store == null) { return; } int num = pkg.ReadInt(); if (num <= 0) { return; } if (!Store.Sized) { Store.Resize(num); FeatureModule.Log.LogInfo((object)("[SharedMap] adopted mapSize=" + num + " from the first client")); } if (num != Store.MapSize) { FeatureModule.Log.LogWarning((object)("[SharedMap] peer " + sender + " has mapSize=" + num + " but the store is " + Store.MapSize + " - not syncing this peer")); } else { if (_shareExpl) { PendingFull[sender] = 0; PendingFullCount[sender] = Store.FullChunkCount; } if (_sharePinsV) { SendAllPins(sender); } } } private static void OnDelta(long sender, ZPackage pkg) { if (!Active2 || !_shareExpl) { return; } if (FeatureModule.ServerActive()) { if (Store == null || !Store.Sized) { return; } List list; try { list = Store.MergeDelta(pkg); } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[SharedMap] bad delta from " + sender + ": " + ex.Message)); return; } if (list == null || list.Count == 0) { return; } _dirty = true; ZPackage val = Store.EncodeDelta(list); ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return; } { foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer.m_uid != sender) { ZRoutedRpc.instance.InvokeRoutedRPC(connectedPeer.m_uid, "SS_MapDelta", new object[1] { val }); } } return; } } ApplyClientBits(pkg, full: false); } private static void OnFull(long sender, ZPackage pkg) { if (Active2 && _shareExpl && !FeatureModule.ServerActive()) { ApplyClientBits(pkg, full: true); } } private static void SendAllPins(long target) { if (Store == null) { return; } foreach (SharedPin pin in Store.Pins) { ZRoutedRpc.instance.InvokeRoutedRPC(target, "SS_MapPin", new object[1] { EncodePin(0, pin) }); } } private static void OnPin(long sender, ZPackage pkg) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) if (!Active2 || !_sharePinsV) { return; } int num = pkg.ReadInt(); SharedPin p = new SharedPin(); p.Name = pkg.ReadString(); p.Pos = new Vector3(pkg.ReadSingle(), pkg.ReadSingle(), pkg.ReadSingle()); p.Type = pkg.ReadInt(); p.Checked = pkg.ReadBool(); p.OwnerId = pkg.ReadLong(); if (!AllowedPinTypes.Contains(p.Type)) { return; } if (FeatureModule.ServerActive()) { if (Store == null) { return; } bool flag = false; if (num == 0) { if (Store.Pins.FindIndex((SharedPin q) => q.Key == p.Key) < 0) { Store.Pins.Add(p); flag = true; } } else { int num2 = Store.Pins.FindIndex((SharedPin q) => q.Key == p.Key); if (num2 >= 0) { Store.Pins.RemoveAt(num2); flag = true; } } if (!flag) { return; } _dirty = true; ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return; } { foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer.m_uid != sender) { ZRoutedRpc.instance.InvokeRoutedRPC(connectedPeer.m_uid, "SS_MapPin", new object[1] { EncodePin(num, p) }); } } return; } } ApplyClientPin(num, p); } private static ZPackage EncodePin(int op, SharedPin p) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: 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_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0054: 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_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(op); val.Write(p.Name ?? ""); val.Write(p.Pos.x); val.Write(p.Pos.y); val.Write(p.Pos.z); val.Write(p.Type); val.Write(p.Checked); val.Write(p.OwnerId); return val; } private static void ApplyClientBits(ZPackage pkg, bool full) { Minimap instance = Minimap.instance; if ((Object)(object)instance == (Object)null) { return; } int num = 0; _applying = true; try { if (full) { if (!MapStore.DecodeFullChunk(pkg, out int mapSize, out int _, out int _, out int byteOffset, out byte[] slice)) { return; } if (mapSize != instance.m_textureSize) { FeatureModule.Log.LogWarning((object)("[SharedMap] server map is " + mapSize + " but ours is " + instance.m_textureSize + " - ignoring shared exploration")); return; } for (int i = 0; i < slice.Length; i++) { if (slice[i] == 0) { continue; } for (int j = 0; j < 8; j++) { if ((slice[i] & (1 << j)) != 0) { int index = (byteOffset + i << 3) | j; if (ExploreIndex(instance, index)) { num++; } } } } } else { List list = new MapStore(instance.m_textureSize).MergeDelta(pkg); if (list == null) { return; } foreach (int item in list) { if (ExploreIndex(instance, item)) { num++; } } } } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[SharedMap] applying shared exploration failed: " + ex.Message)); } finally { _applying = false; } if (num > 0 && (Object)(object)instance.m_fogTexture != (Object)null) { instance.m_fogTexture.Apply(); } } private static bool ExploreIndex(Minimap mm, int index) { int textureSize = mm.m_textureSize; if (index < 0 || index >= textureSize * textureSize) { return false; } return mm.Explore(index % textureSize, index / textureSize); } private static void ApplyClientPin(int op, SharedPin p) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0092: 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_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: 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_005c: Unknown result type (might be due to invalid IL or missing references) Minimap instance = Minimap.instance; if ((Object)(object)instance == (Object)null) { return; } _applying = true; try { PinType val = (PinType)p.Type; if (op == 0) { foreach (PinData pin in instance.m_pins) { if (pin.m_type == val && pin.m_name == p.Name && Utils.DistanceXZ(pin.m_pos, p.Pos) < 1f) { return; } } instance.AddPin(p.Pos, val, p.Name, true, p.Checked, p.OwnerId, default(PlatformUserID)); return; } PinData val2 = null; foreach (PinData pin2 in instance.m_pins) { if (pin2.m_type == val && pin2.m_name == p.Name && Utils.DistanceXZ(pin2.m_pos, p.Pos) < 1f) { val2 = pin2; break; } } if (val2 != null) { instance.RemovePin(val2); } } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[SharedMap] applying shared pin failed: " + ex.Message)); } finally { _applying = false; } } private static void ExplorePostfix(int x, int y, bool __result) { if (__result && Active2 && _shareExpl && !_applying && (!_inTableImport || !_excludeTableV) && FeatureModule.ClientActive()) { Minimap instance = Minimap.instance; if (!((Object)(object)instance == (Object)null)) { PendingDelta.Add(y * instance.m_textureSize + x); } } } private static void AddPinPostfix(Vector3 pos, PinType type, string name, bool save, bool isChecked, long ownerID) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected I4, but got Unknown //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Expected I4, but got Unknown if (!Active2 || !_sharePinsV || _applying || !save || !FeatureModule.ClientActive() || !_rpcsRegistered || !AllowedPinTypes.Contains((int)type)) { return; } SharedPin p = new SharedPin { Name = (name ?? ""), Pos = pos, Type = (int)type, Checked = isChecked, OwnerId = ownerID }; try { ZRoutedRpc.instance.InvokeRoutedRPC("SS_MapPin", new object[1] { EncodePin(0, p) }); } catch { } } private static void RemovePinPrefix(PinData pin) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected I4, but got Unknown //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: 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_0075: Expected I4, but got Unknown if (!Active2 || !_sharePinsV || _applying || pin == null || !pin.m_save || !FeatureModule.ClientActive() || !_rpcsRegistered || !AllowedPinTypes.Contains((int)pin.m_type)) { return; } SharedPin p = new SharedPin { Name = (pin.m_name ?? ""), Pos = pin.m_pos, Type = (int)pin.m_type, Checked = pin.m_checked, OwnerId = pin.m_ownerID }; try { ZRoutedRpc.instance.InvokeRoutedRPC("SS_MapPin", new object[1] { EncodePin(1, p) }); } catch { } } private static void TablePrefix() { _inTableImport = true; } private static void TablePostfix() { _inTableImport = false; } internal static void Tick(float dt) { //IL_0233: Unknown result type (might be due to invalid IL or missing references) //IL_023a: Expected O, but got Unknown if (!Active2 || (Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { return; } TryRegisterRpcs(); if (!_rpcsRegistered) { return; } if (FeatureModule.ServerActive()) { if (PendingFull.Count == 0) { return; } List list = new List(); foreach (long item in new List(PendingFull.Keys)) { int num = PendingFull[item]; int num2 = PendingFullCount[item]; if (Store == null || !Store.Sized || num >= num2) { list.Add(item); continue; } try { ZRoutedRpc.instance.InvokeRoutedRPC(item, "SS_MapFull", new object[1] { Store.EncodeFullChunk(num) }); } catch (Exception ex) { FeatureModule.Log.LogWarning((object)("[SharedMap] full sync to " + item + " failed: " + ex.Message)); list.Add(item); continue; } PendingFull[item] = num + 1; if (num + 1 >= num2) { list.Add(item); FeatureModule.Log.LogInfo((object)("[SharedMap] full map sent to peer " + item + " (" + num2 + " chunks, " + Store.Explored() + " explored pixels)")); } } { foreach (long item2 in list) { PendingFull.Remove(item2); PendingFullCount.Remove(item2); } return; } } if (!FeatureModule.ClientActive()) { return; } Minimap instance = Minimap.instance; if ((Object)(object)instance == (Object)null) { return; } _clientMapSize = instance.m_textureSize; if (!_helloSent) { _helloTimer += dt; if (_helloTimer >= 2f) { _helloTimer = 0f; ZPackage val = new ZPackage(); val.Write(_clientMapSize); try { ZRoutedRpc.instance.InvokeRoutedRPC("SS_MapHello", new object[1] { val }); _helloSent = true; } catch (Exception ex2) { FeatureModule.Log.LogWarning((object)("[SharedMap] hello failed: " + ex2.Message)); } if (_helloSent) { FeatureModule.Log.LogInfo((object)("[SharedMap] requested the shared map (mapSize=" + _clientMapSize + ")")); } } } if (!_shareExpl || PendingDelta.Count == 0) { return; } _deltaTimer += dt; if (!(_deltaTimer < 1f / _hz)) { _deltaTimer = 0f; try { ZPackage val2 = new MapStore(_clientMapSize).EncodeDelta(PendingDelta); ZRoutedRpc.instance.InvokeRoutedRPC("SS_MapDelta", new object[1] { val2 }); } catch (Exception ex3) { FeatureModule.Log.LogWarning((object)("[SharedMap] delta send failed: " + ex3.Message)); } PendingDelta.Clear(); } } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }