Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of NetworkPerformanceSystem v1.18.1
plugins/NetworkPerformanceSystem.dll
Decompiled 17 minutes ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Permissions; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Entities; using Jotunn.Managers; using NetworkPerformanceSystem.Patches; using NetworkPerformanceSystem.Runtime; using Splatform; using Steamworks; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("NetworkPerformanceSystem")] [assembly: AssemblyDescription("Latency-aware networking for Valheim: per-peer send windows and simulation authority placement.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("MidnightsFX")] [assembly: AssemblyProduct("NetworkPerformanceSystem")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("e3243d22-4307-4008-ba36-9f326008cde5")] [assembly: AssemblyFileVersion("1.18.1")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.18.1.0")] namespace NetworkPerformanceSystem { internal static class ConfigFileWatcher { private class WatchEntry { internal DateTime LastWriteUTC; internal long FileLength; internal Action<string> Callback; } internal class ConfigFileWatcherBehaviour : MonoBehaviour { private float nextPollTime; public void Update() { if (!(Time.unscaledTime < nextPollTime)) { nextPollTime = Time.unscaledTime + PollInterval(); Poll(); } } private static float PollInterval() { if (ValConfig.ConfigPollIntervalSeconds == null) { return 30f; } return ValConfig.ConfigPollIntervalSeconds.Value; } private static void Poll() { if (WatchedFiles.Count == 0) { return; } string[] array = WatchedFiles.Keys.ToArray(); foreach (string text in array) { if (!WatchedFiles.TryGetValue(text, out var value) || !File.Exists(text)) { continue; } FileInfo fileInfo = new FileInfo(text); DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; long length = fileInfo.Length; if (!(lastWriteTimeUtc == value.LastWriteUTC) || length != value.FileLength) { value.LastWriteUTC = lastWriteTimeUtc; value.FileLength = length; try { value.Callback?.Invoke(text); } catch (Exception ex) { Logger.LogWarning("ConfigFileWatcher callback for " + text + " threw: " + ex.Message); } } } } } private const float FallbackPollSeconds = 30f; private static readonly Dictionary<string, WatchEntry> WatchedFiles = new Dictionary<string, WatchEntry>(StringComparer.OrdinalIgnoreCase); private static ConfigFileWatcherBehaviour watchProcess; internal static void Initialize() { //IL_0013: 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_001e: Expected O, but got Unknown //IL_001e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)watchProcess != (Object)null)) { GameObject val = new GameObject("NetworkPerformanceSystem_ConfigFileWatcher"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; watchProcess = val.AddComponent<ConfigFileWatcherBehaviour>(); Logger.LogDebug("ConfigFileWatcher initialized."); } } internal static void Register(string fullPath, Action<string> onChanged) { if (!string.IsNullOrEmpty(fullPath)) { DateTime lastWriteUTC = DateTime.MinValue; long fileLength = 0L; if (File.Exists(fullPath)) { FileInfo fileInfo = new FileInfo(fullPath); lastWriteUTC = fileInfo.LastWriteTimeUtc; fileLength = fileInfo.Length; } WatchedFiles[fullPath] = new WatchEntry { LastWriteUTC = lastWriteUTC, FileLength = fileLength, Callback = onChanged }; Logger.LogDebug("ConfigFileWatcher watching " + fullPath); } } internal static void RefreshStamp(string fullPath) { if (string.IsNullOrEmpty(fullPath) || !WatchedFiles.TryGetValue(fullPath, out var value)) { return; } try { FileInfo fileInfo = new FileInfo(fullPath); value.LastWriteUTC = fileInfo.LastWriteTimeUtc; value.FileLength = fileInfo.Length; } catch (Exception) { value.LastWriteUTC = DateTime.MinValue; value.FileLength = 0L; } } } internal class Logger { public static LogLevel Level = (LogLevel)16; public static void EnableDebugLogging(object sender, EventArgs e) { CheckEnableDebugLogging(); } public static void CheckEnableDebugLogging() { //IL_0016: 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) if (ValConfig.EnableDebugMode.Value) { Level = (LogLevel)32; } else { Level = (LogLevel)16; } } public static void SetDebugLogging(bool state) { //IL_000d: 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) if (state) { Level = (LogLevel)32; } else { Level = (LogLevel)16; } } public static void LogDebug(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)Level >= 32) { NetworkPerformanceSystem.Log.LogInfo((object)("[DEBUG]" + message)); } } public static void LogInfo(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)Level >= 16) { NetworkPerformanceSystem.Log.LogInfo((object)message); } } public static void LogWarning(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 if ((int)Level >= 4) { NetworkPerformanceSystem.Log.LogWarning((object)message); } } public static void LogError(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 if ((int)Level >= 2) { NetworkPerformanceSystem.Log.LogError((object)message); } } } internal class ValConfig { public static ConfigFile cfg; public static ConfigEntry<bool> EnableDebugMode; public static ConfigEntry<bool> EnableLatencyCompensation; public static ConfigEntry<float> LatencyCompensationStrength; public static ConfigEntry<float> LatencyCompensationMaxMeters; public static ConfigEntry<bool> EnableDebugOverlay; public static ConfigEntry<bool> EnableGhostWatchdog; public static ConfigEntry<bool> AllowMonitoringUpload; public static ConfigEntry<bool> EnableEarlyZdoDataGuard; public static ConfigEntry<float> ConfigPollIntervalSeconds; public static ConfigEntry<bool> EnableSendWindowSizing; public static ConfigEntry<float> SendWindowBdpFactor; public static ConfigEntry<int> SendWindowMaxBytes; public static ConfigEntry<bool> EnableSchedulerFix; public static ConfigEntry<float> SendIntervalSeconds; public static ConfigEntry<float> SendSchedulerFrameBudgetMs; public static ConfigEntry<int> SendSchedulerMinPeersPercent; public static ConfigEntry<int> SendSchedulerMaxCpuPercent; public static ConfigEntry<bool> EnableOwnershipArbitration; public static ConfigEntry<bool> OwnershipAllowHostOwner; public static ConfigEntry<float> OwnershipMinHoldSeconds; public static ConfigEntry<int> OwnershipChallengeMarginMs; public static ConfigEntry<int> OwnershipMaxReassignsPerPass; public static ConfigEntry<float> OwnershipLoadPenaltyMs; public static ConfigEntry<int> OwnershipUnmeasuredRttMs; public static ConfigEntry<bool> UseInGamePing; public static ConfigEntry<bool> EnableInteractiveOwnership; public static ConfigEntry<float> OwnershipInteractiveClaimRadius; public static ConfigEntry<float> OwnershipInteractiveChallengeMargin; public static ConfigEntry<float> OwnershipInteractiveMinHoldSeconds; public static ConfigEntry<int> OwnershipInteractiveMaxReassignsPerPass; public static ConfigEntry<bool> EnableCreatureProximityOwnership; public static ConfigEntry<float> CreatureProximityRadius; public static ConfigEntry<bool> OwnershipArbitrateCreatures; public static ConfigEntry<bool> OwnershipFollowersStayWithLeader; public static ConfigEntry<bool> OwnershipKeepWhileLoaded; public static ConfigEntry<bool> BalanceCreaturesByFrameRate; public static ConfigEntry<bool> BalanceCreaturesByUpload; public static ConfigEntry<float> CreatureLoadMinOwnerFps; public static ConfigEntry<int> CreatureLoadMinAllowance; public static ConfigEntry<bool> RejectStaleOwnerUpdates; public static ConfigEntry<bool> ShipOwnershipFollowsHelmsman; public static ConfigEntry<bool> UseCharacterRefPos; public static ConfigEntry<bool> EnableRoutedRpcFilter; public static ConfigEntry<bool> LimitTargetedRelayByDistance; public static ConfigEntry<bool> EnableStationRpcRouting; public static ConfigEntry<bool> EnableCreatureHitRouting; public static ConfigEntry<bool> EnableStructureHitRouting; public static ConfigEntry<bool> EnableTerrainEditRouting; public static ConfigEntry<bool> EnableItemPickupRouting; public static ConfigEntry<bool> EnableInteractionRouting; public static ConfigEntry<bool> LimitRepeatedStatusEffects; public static ConfigEntry<string> DamageNumbers; public static ConfigEntry<bool> EnableSyncListCache; public static ConfigEntry<float> SyncListCacheMs; public static ConfigEntry<bool> QuietIdleCreatures; public static ConfigEntry<bool> PaceCreatureSends; public static ConfigEntry<bool> SendFightingCreaturesFirst; public static ConfigEntry<bool> QuietWildlifeUpdates; public static ConfigEntry<bool> QuietFloatingObjects; public static ConfigEntry<bool> HoldUnchangedStructures; public static ConfigEntry<float> StructureMaxHoldSeconds; public static ConfigEntry<bool> EnableSteamTransportTuning; public static ConfigEntry<int> SteamSendRateKBps; public static ConfigEntry<int> SteamNagleMicros; public static ConfigEntry<bool> EnableLossBackoff; public static ConfigEntry<float> LossBackoffThreshold; public static ConfigEntry<int> LossBackoffFloorKBps; public static ConfigEntry<int> LossBackoffHoldSeconds; public static ConfigEntry<int> LossBackoffRecoverSeconds; public static ConfigEntry<bool> AutoSendRate; public static ConfigEntry<int> AutoSendRateMaxKBps; public static ConfigEntry<bool> AutoPlayerUploads; public static ConfigEntry<int> ServerUploadLimitKBps; public static ConfigEntry<int> ServerDownloadLimitKBps; public static ConfigEntry<bool> EnablePlayerLimitOverride; public static ConfigEntry<int> MaxPlayers; public static ConfigEntry<bool> SendPlayerHistoryOnlyWhenChanged; public static ConfigEntry<bool> MapPinsFollowPlayers; public static ConfigEntry<bool> SendMapPositionsRightAway; public static ConfigEntry<int> MapPositionAccuracyMeters; public static ConfigEntry<bool> EnableConnectionTimeoutTuning; public static ConfigEntry<int> ConnectTimeoutSeconds; public static ConfigEntry<int> ConnectionTimeoutSeconds; public static ConfigEntry<int> LoadingTimeoutSeconds; public static ConfigEntry<bool> KeepPlayersInBackground; public static ConfigEntry<int> BackgroundTimeoutSeconds; public static ConfigEntry<bool> EnableBackgroundTimeout; public static ConfigEntry<bool> KeepPlayersWithoutTheMod; public static ConfigEntry<bool> TellDroppedCrossplayPlayers; public static ConfigEntry<bool> ResumeAfterCrossplayReset; public static ConfigEntry<bool> EvictGhostOwners; public static ConfigEntry<float> GhostOwnerEvictSeconds; public static ConfigEntry<bool> ReportVanillaQueueSize; public static ConfigEntry<bool> EnableDeserializeFastPath; public static ConfigEntry<bool> EnablePacketReadFastPath; public static ConfigEntry<bool> EnableSendPackageReuse; public static ConfigEntry<bool> EnableRpcInvokeFastPath; public static ConfigEntry<bool> EnableRelaySendReuse; public static ConfigEntry<bool> EnableMonitoring; public static ConfigEntry<bool> MonitoringCollectFromClients; public static ConfigEntry<int> MonitoringClientBytesPerSecond; public static ConfigEntry<int> MonitoringMaxDiskMB; public const string cfgFolder = "NetworkPerformanceSystem"; private static List<string> reloadChanges; public ValConfig(ConfigFile cf) { cfg = cf; cfg.SaveOnConfigSet = false; CreateConfigValues(cf); Logger.SetDebugLogging(EnableDebugMode.Value); } public static void SaveOnSet(bool enabled) { cfg.SaveOnConfigSet = enabled; cfg.Save(); } public static void WatchConfigFile() { ConfigFileWatcher.Register(cfg.ConfigFilePath, OnConfigFileChanged); ConfigFileWatcher.Initialize(); } private static void OnConfigFileChanged(string _) { if (!((Object)(object)ZNet.instance != (Object)null) || ZNet.instance.IsServer()) { ReloadFromDisk(); } } internal static void ReloadFromDisk() { bool saveOnConfigSet = cfg.SaveOnConfigSet; cfg.SaveOnConfigSet = false; List<string> list = (reloadChanges = new List<string>()); try { cfg.Reload(); } finally { reloadChanges = null; cfg.SaveOnConfigSet = saveOnConfigSet; } if (list.Count == 0) { Logger.LogInfo("Config file changed on disk and was reloaded; no setting changed."); } else { Logger.LogInfo(string.Format("Config file changed on disk and was reloaded; {0} setting(s) changed: {1}", list.Count, string.Join(", ", list))); } } private void CreateConfigValues(ConfigFile Config) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Expected O, but got Unknown //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Expected O, but got Unknown //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Expected O, but got Unknown //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Expected O, but got Unknown //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Expected O, but got Unknown //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_0192: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Expected O, but got Unknown //IL_019f: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Expected O, but got Unknown //IL_01da: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Expected O, but got Unknown //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_01f6: Expected O, but got Unknown EnableDebugMode = Config.Bind<bool>("Client config", "EnableDebugMode", false, new ConfigDescription("Enables Debug logging.", (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdvanced = true } })); EnableDebugMode.SettingChanged += Logger.EnableDebugLogging; Logger.CheckEnableDebugLogging(); EnableLatencyCompensation = Config.Bind<bool>("Client config", "EnableLatencyCompensation", true, new ConfigDescription("Render entities owned by other players at their estimated current position rather than their last-received one. Requires the server to be running this mod.", (AcceptableValueBase)null, Array.Empty<object>())); LatencyCompensationStrength = Config.Bind<float>("Client config", "LatencyCompensationStrength", 1f, new ConfigDescription("How much of the measured path latency to correct for. 1.0 corrects fully. Lower values trade accuracy for less overshooting. 0 Disables it.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); LatencyCompensationMaxMeters = Config.Bind<float>("Client config", "LatencyCompensationMaxMeters", 3f, new ConfigDescription("How far objects can be displaced through latency compensation.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 4.5f), Array.Empty<object>())); EnableDebugOverlay = Config.Bind<bool>("Client config", "EnableDebugOverlay", false, new ConfigDescription("Show the per-entity latency compensation overlay (owner, estimated staleness, applied displacement).", (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdvanced = true } })); EnableGhostWatchdog = Config.Bind<bool>("Client config", "EnableGhostWatchdog", true, new ConfigDescription("Warn when the server stops answering, and return to the menu once it is certain. Otherwise you can continue to play on a disconnected world and any changes made would be lost.", (AcceptableValueBase)null, Array.Empty<object>())); AllowMonitoringUpload = Config.Bind<bool>("Client config", "AllowMonitoringUpload", true, new ConfigDescription("When the server turns on monitoring, this allows your client to upload data. As the client, this is your hard-opt out, server can't override it.", (AcceptableValueBase)null, Array.Empty<object>())); EnableEarlyZdoDataGuard = Config.Bind<bool>("Client config", "EnableEarlyZdoDataGuard", true, new ConfigDescription("Buffers updates that land before their associated receiving RCPs, prevents data loss on connection.", (AcceptableValueBase)null, new object[1] { (object)new ConfigurationManagerAttributes { IsAdvanced = true } })); ConfigPollIntervalSeconds = Config.Bind<float>("Config File", "Poll Interval Seconds", 30f, new ConfigDescription("Seconds between checks for edits to this file on disk. An edit is applied without a restart on a server, or in the main menu; a server then sends its new values to the connected players. Lower reacts faster to a hand edit.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 300f), new object[1] { (object)new ConfigurationManagerAttributes { IsAdvanced = true } })); EnableSendWindowSizing = BindServerConfig("Send Window", "Enable BDP Window Sizing", value: true, "Size each peer's in-flight ZDO window from their measured round-trip time and the rate Steam sends to them at. Low-latency peers are unaffected; high-latency peers stop being throttled by their distance."); SendWindowBdpFactor = BindServerConfig("Send Window", "BDP Factor", 1.25f, "Multiplier on the round-trip part of the window. 1.0 allows exactly one round trip of data in flight at Steam's send rate, on top of the one send interval of data every window carries so the link stays busy until the next send.", advanced: false, 1f, 3f); SendWindowMaxBytes = BindServerConfig("Send Window", "Max Window Bytes", 65536, "Upper bound on the send window. A player needs send rate x (round-trip time x BDP Factor + Send Interval Seconds) to receive at the full rate: at the default 150 KB/s this cap covers about 300ms of ping. Raise it together with Steam Transport's Send Rate KBps.", advanced: true, 10240, 262144); EnableSchedulerFix = BindServerConfig("Send Scheduler", "Enable Scheduler Fix", value: true, "Send to every peer each tick instead of one peer per rendered frame. Without this the effective per-peer send rate degrades linearly with player count."); SendIntervalSeconds = BindServerConfig("Send Scheduler", "Send Interval Seconds", 0.033f, "Seconds between ZDO send rounds. Vanilla is 0.05, lower = more updates, higher = slower updates.", advanced: true, 0.02f, 0.2f); SendSchedulerFrameBudgetMs = BindServerConfig("Send Scheduler", "Frame Budget Ms", 4f, "Maximum milliseconds per frame the host spends sending ZDOs to peers, once Min Players Per Frame Percent of them have been sent to. Peers are serviced in round-robin order until the budget runs out and the remainder is owed to the next frame, so nobody is starved. This spreads the sends across frames so no single frame stutters; Max Send CPU Percent is what caps their total. Raise it to trade server frame time for send rate, lower it on a CPU-constrained host. nps_stats shows the effective rate and how often the budget is hit.", advanced: true, 0.5f, 16f); SendSchedulerMinPeersPercent = BindServerConfig("Send Scheduler", "Min Players Per Frame Percent", 25, "The frame budget never stops a frame's sends before this share of the connected players has been sent to (rounded up, at least one). On a big server each send costs more, and the budget alone can cut a frame down to one or two players, leaving each player waiting dozens of server frames for an update. With this, every player is sent to at least once every few server frames however many have joined: 25 is at least once every 4 frames. It only adds sends while the budget is what is limiting, those sends cost server frame time, and they never go past Max Send CPU Percent. 0 leaves the budget alone in charge. nps_stats shows how many sends it adds and what each one costs.", advanced: true, 0, 100); SendSchedulerMaxCpuPercent = BindServerConfig("Send Scheduler", "Max Send CPU Percent", 25, "The most of each second, in percent, the game's main thread may spend sending to players, all players together. When a full round of sends - every player once, at the measured cost of one send - would take more than this, everyone's send interval is stretched until it fits, so a big server in big bases gets fewer updates per player instead of a pegged CPU core. Small servers with cheap sends never reach it and keep Send Interval Seconds. Each update still carries everything that changed. 0 turns the limit off. nps_stats shows the limit, the interval it sets and the share actually spent.", advanced: true, 0, 100); EnableOwnershipArbitration = BindServerConfig("Ownership", "Enable Latency-Aware Ownership", value: true, "Assign ZDO ownership to minimise how stale the object looks to the players who can actually see it, instead of vanilla's first-peer-wins ordering. This is what makes it 'feel like singleplayer'"); OwnershipAllowHostOwner = BindServerConfig("Ownership", "Allow Host As Owner", value: true, "Let the host compete for ownership of contested ZDOs in the zones it has loaded. (This is ONLY around spawn by default)."); OwnershipMinHoldSeconds = BindServerConfig("Ownership", "Min Hold Seconds", 2f, "Minimum time an owner keeps a ZDO before it can be challenged. This prevents constant ZDO change thrash.", advanced: false, 0f, 60f); OwnershipChallengeMarginMs = BindServerConfig("Ownership", "Challenge Margin Ms", 25, "A challenger must improve estimated staleness by at least this many milliseconds to take ownership.", advanced: false, 0, 250); OwnershipMaxReassignsPerPass = BindServerConfig("Ownership", "Max Reassigns Per Pass", 8, "Cap on latency-driven ownership transfers of moving objects per arbitration pass (boats, creatures, carts etc). Each transfer costs a ZDO resend.", advanced: true, 1, 128); OwnershipLoadPenaltyMs = BindServerConfig("Ownership", "Load Penalty Ms", 0.02f, "Cost added per simulated object (creatures, ships - not walls or trees) a candidate already owns nearby, in milliseconds. This prevents one user from simulating a whole area even if they have the best connection. Low priority objects will be shuffled to others.", advanced: true, 0f, 0.5f); OwnershipUnmeasuredRttMs = BindServerConfig("Ownership", "Unmeasured Peer RTT Ms", 150, "Round-trip time assumed for a player the host has no ping for: in their first few seconds, and when their game has not answered for ten seconds. With Use In-Game Ping off, crossplay players never have one either. Such a player still wins objects only they can see, but loses contested ones to any player with a lower ping. Treating unmeasured as 0ms instead would hand them everything in range.", advanced: true, 0, 1000); UseInGamePing = BindServerConfig("Ping", "Use In-Game Ping", value: true, "Time each player's ping on the server, from the game's own once-a-second ping to the player's answer, instead of asking Steam. Crossplay players get a ping too, players do not need the mod, and the figure includes everything their updates really wait for. Who runs creatures and objects goes by each player's best ping of the last few minutes, so a player whose connection is busy in a fight does not lose the creatures they run because of it; latency compensation uses their current ping. Send windows and send rates always use Steam's figure. Off goes back to Steam's figure everywhere."); EnableInteractiveOwnership = BindServerConfig("Ownership", "Interactive Object Ownership", value: true, "Also place interactable-but-stationary objects - berry bushes and other pickables, ore deposits, rocks, trees, logs and destructibles - on whoever is standing nearest them. This allows most interactables to respond to the user instantly instead of waiting on a delayed RPC."); OwnershipInteractiveClaimRadius = BindServerConfig("Ownership", "Interactive Claim Radius", 32f, "How close a player has to be, in metres, before an interactable object is placed on them.", advanced: false, 4f, 64f); OwnershipInteractiveChallengeMargin = BindServerConfig("Ownership", "Interactive Challenge Margin", 4f, "How much nearer, in metres, a player must be than the current owner before an interactable object moves to them. This is the distance equivalent of Challenge Margin Ms.", advanced: false, 0f, 32f); OwnershipInteractiveMinHoldSeconds = BindServerConfig("Ownership", "Interactive Min Hold Seconds", 15f, "Minimum time before an interactable object that has changed ownership can change again. Nothing in the game hands these objects over by itself, so there is no player-initiated claim to wait out", advanced: false, 0f, 120f); OwnershipInteractiveMaxReassignsPerPass = BindServerConfig("Ownership", "Interactive Max Reassigns Per Pass", 16, "Minimum cap on how many interactable objects may be placed on a nearer player per arbitration pass. It is a separate budget from Max Reassigns Per Pass on purpose, so a zone full of contested creatures cannot starve the handful of moves that make a berry patch local.", advanced: true, 1, 256); EnableCreatureProximityOwnership = BindServerConfig("Ownership", "Creature Proximity Ownership", value: true, "When exactly one player is near a creature, that player owns it, whatever anyone else's latency is. Prevents lag when two players are within network distance but not fighting the same creatures."); CreatureProximityRadius = BindServerConfig("Ownership", "Creature Proximity Radius", 48f, "How close, in metres, a player has to be to a creature to count as near it for Creature Proximity Ownership. Two players both inside this distance of one creature are sharing a fight; two players further apart than about twice this are not.", advanced: false, 16f, 96f); OwnershipArbitrateCreatures = BindServerConfig("Ownership", "Arbitrate Creatures", value: true, "Give creatures first call on ownership arbitration. Game defaults to creatures having low priority for network updates."); OwnershipFollowersStayWithLeader = BindServerConfig("Ownership", "Followers Stay With Their Player", value: true, "A tame that is following a player, or a creature a player summoned with a staff, is simulated on that player's machine, so it keeps up with them and fights beside them without lag. A ridden animal still goes to its rider. Needs Arbitrate Creatures."); OwnershipKeepWhileLoaded = BindServerConfig("Ownership", "Keep Objects While Loaded", value: true, "Leave an object with its owner while that player still has it loaded, instead of releasing it the moment they step one zone away and claiming it again when they step back. A player standing on a zone edge otherwise flips every object in the next zone over each time they move - over a thousand at a time in a built-up base. Creatures already work this way."); BalanceCreaturesByFrameRate = BindServerConfig("Creature Load", "Balance Creatures By Frame Rate", value: true, "Players with this mod tell the server how smoothly their game is running. When a player's frame rate stays under Min Owner FPS while they simulate a lot of creatures in a fight, the server gives them fewer creatures to run and moves the rest, a few at a time, to players whose games have room. A player whose game spends little of each second simulating is not held to fewer, since creatures cannot be what slows it. Creatures only that player is near stay with them. If moving creatures does not make their game faster, they get them back and are left alone for ten minutes, and the second time for the rest of the session. Players without the mod are treated as before. Needs Arbitrate Creatures."); BalanceCreaturesByUpload = BindServerConfig("Creature Load", "Balance Creatures By Upload", value: true, "Every creature a player runs is sent from their machine, so a player running a big fight can fill their upload to the server, and then their creatures stall for everyone else. When a player's upload stays full and a higher upload rate (Auto Player Uploads) cannot fix it soon, the server gives them fewer creatures to run and moves the rest, a few at a time, to players whose uploads have room. Creatures only that player is near stay with them. If their upload is still full at Min Creature Allowance, they get their creatures back and are left alone for ten minutes. Works with Auto Send Rate off too. Players without the mod are treated as before. Needs Arbitrate Creatures."); CreatureLoadMinOwnerFps = BindServerConfig("Creature Load", "Min Owner FPS", 25f, "Frame rate below which a player counts as struggling, held for 10 seconds. A player at or above this plus 5 counts as having room to take creatures from someone else.", advanced: false, 10f, 60f); CreatureLoadMinAllowance = BindServerConfig("Creature Load", "Min Creature Allowance", 4, "The fewest creatures a struggling player is ever held to, and the number they must own before their frame rate or upload is looked at.", advanced: true, 0, 64); RejectStaleOwnerUpdates = BindServerConfig("Ownership", "Reject Stale Owner Updates", value: true, "Prevents previous owners network updates immediately taking an owned object back, and tells that player straight away who owns it now. A creature's movement from its previous owner is dropped rather than shown to everyone else. Disabling this effectively neuters the arbiter."); ShipOwnershipFollowsHelmsman = BindServerConfig("Ownership", "Ship Ownership Follows Helmsman", value: true, "Hand a ship to whoever takes its helm, so the player steering simulates it on their own machine instead of watching it relayed through the host from another player."); UseCharacterRefPos = BindServerConfig("Reference Position", "Use Character Position", value: true, "Host-side. Take each player's position from their character, as it arrives with the game's own object updates, instead of waiting for the position their game reports every 2 seconds."); EnableRoutedRpcFilter = BindServerConfig("Routed RPC", "Enable Relay Filtering", value: true, "Relay broadcast RPCs (animation triggers, footsteps, damage numbers, object-destroyed notices, building damage and the like) only to the players that can actually use them, instead of to everyone on the server."); LimitTargetedRelayByDistance = BindServerConfig("Routed RPC", "Limit Relay By Distance", value: true, "Also stop relaying object RPCs (building damage and fragments, ward flashes, animation triggers, footsteps) to players who are too far away to have that object loaded, even if they visited it earlier in the session."); EnableStationRpcRouting = BindServerConfig("Routed RPC", "Route Station Requests To Owner", value: true, "Deliver fermenter, smelter, cooking station, fireplace, shield generator and ballista item requests (add item / ore / fuel / ammo, tap, empty) to whoever owns the object right now. Prevents RPCs being dropped and items being eaten. An ore, fuel, food or ammo item that still cannot be delivered is dropped at the station instead of being lost."); EnableCreatureHitRouting = BindServerConfig("Routed RPC", "Route Creature Hits To Owner", value: true, "Deliver hits on creatures to whoever is simulating the creature right now, prevents silently dropping hits."); EnableStructureHitRouting = BindServerConfig("Routed RPC", "Route Structure Hits To Owner", value: true, "Deliver hits, repairs, removals and snow changes on buildings, trees and rocks to whoever owns them right now. A player whose copy still names an earlier owner sends them to that player, who drops them without a word. Structure Updates only holds back owner changes while this is on."); EnableTerrainEditRouting = BindServerConfig("Routed RPC", "Route Terrain Edits To Owner", value: true, "Deliver digging, raising, levelling, cultivating and paving - and GradualTerrain's smoothing around a dig - to whoever owns that piece of ground right now. A player whose copy still names an earlier owner sends the edit to that player, who drops it without a word, and the ground can then split open along the border with the next piece of ground, which did take it."); EnableItemPickupRouting = BindServerConfig("Routed RPC", "Route Item Pick-ups To Owner", value: true, "Deliver a player's request to pick up a dropped item to whoever owns the item right now, and send the item to the player the moment it is theirs. Without this, a request sent to a player who no longer owns the item is lost, and the game waits longer each time before asking again, up to half a minute. The server never takes an item from a player who may be picking it up: it passes requests on, and only hands over an item it owns itself or one nobody owns."); EnableInteractionRouting = BindServerConfig("Routed RPC", "Route Interactions To Owner", value: true, "Deliver opening, stacking into and emptying chests, doors, picking berries and other pickables, eating from feasts, item and armour stands, naming portals, claiming beds, switching wards, saving to map tables, emptying beehives and sap collectors, the incinerator lever, and naming, saddling and commanding tames to whoever owns the object right now. A player whose copy still names an earlier owner sends them to that player, who drops them without a word. Ship and cart cargo is left to ship ownership."); LimitRepeatedStatusEffects = BindServerConfig("Routed RPC", "Limit Repeated Status Effects", value: true, "Send a status effect to a creature another player is simulating at most 4 times a second, instead of every time something asks for it - and an effect that lasts only once in a third of its duration, at least every 2 seconds, since asking again only restarts its timer. An area that gives a status effect keeps applying it, 50 times a second, to creatures that were yours when they walked in. Once one of them is handed to another player every one of those becomes a message, and a few animals in a base can fill a player's whole upload and stop the world loading for the player they were handed to. Runs on each player's game that has this mod, using the server's setting; the server also stops passing the repeats on from players who do not have it. The log names the effect either way."); DamageNumbers = BindServerConfig("Routed RPC", "Damage Numbers", "Everyone Nearby", "Who is sent the floating damage numbers. The game makes a number on whichever player's game simulates what was hit and sends it to everyone, and each player then ignores it unless it is within 30 m. 'Everyone Nearby' does what the game does, but only to players close enough to see it. 'Attacker Only' sends a hit's number only to the player who landed it; damage a player or their tame takes, blocks, heals and bonuses stay on the game that made them, and burning or poison ticks are only seen by whoever simulates the creature. 'Off' never sends them: each player sees only the numbers their own game makes. Applied by every player's game that has this mod, using the server's setting. The server also stops passing on numbers from players without the mod when this is 'Off'.", new AcceptableValueList<string>(new string[3] { "Everyone Nearby", "Attacker Only", "Off" })); EnableSyncListCache = BindServerConfig("Sync List Cache", "Enable Sector Scan Cache", value: true, "Reuse each peer's sector scan across the send sweep instead of rebuilding it on every send."); SyncListCacheMs = BindServerConfig("Sync List Cache", "Cache Ms", 100f, "How long a peer's sector scan may be reused, in milliseconds. This is the longest delay an object moving sectors from one player to another would have before being considered for ownership change etc. Vanilla rebuilds this every frame.", advanced: false, 0f, 500f); QuietIdleCreatures = BindServerConfig("Creature Updates", "Quiet Idle Creatures", value: true, "A creature standing still stops sending itself for movements too small to see - under 2 cm, 0.05 m/s, or 1 degree of tilt or turn - and for animation speed changes under 0.05. Without this, the physics engine's constant tiny wobble makes whoever simulates a creature re-send all of it up to 30 times a second, and the server passes each copy on to everyone nearby; a pen of tamed animals can fill a player's upload. Fighting creatures are not affected. Runs on each player's game that has this mod, using the server's setting."); PaceCreatureSends = BindServerConfig("Creature Updates", "Pace Creature Sends", value: true, "Server side. Send a creature that has settled down to each player at most 10 times a second (5 beyond 32 m), and one moving more than 64 m away at most 15 times a second, instead of on every update. Creatures that are fighting, hunting, being ridden or changing owner still go out at once. Also covers players who do not have this mod."); SendFightingCreaturesFirst = BindServerConfig("Creature Updates", "Send Fighting Creatures First", value: true, "When a player's upload is full, send the creatures they are simulating that are alert or chasing someone ahead of everything else they have changed, right after players and ships. Without this they wait their turn behind every fire, smelter and dropped item that player also simulates, and freeze for everyone else for a second or more. Changes only the order, never what is sent. Runs on each player's game that has this mod, using the server's setting."); QuietWildlifeUpdates = BindServerConfig("Creature Updates", "Quiet Wildlife", value: true, "Fish, seagulls and crows send themselves 5 times a second (fish) or 10 times a second (birds) instead of on every frame. They are always moving, so without this whoever simulates them re-sends each one up to 20 times a second, and on a lake or coast they can be a third of a player's upload. The server also passes them on to each player at most about 6 / 12 times a second (2 / 5 beyond 32 m), which covers players who do not have this mod. A fish on a fishing line is not affected. Runs on each player's game that has this mod and on the server, using the server's setting."); QuietFloatingObjects = BindServerConfig("Creature Updates", "Quiet Floating Objects", value: true, "Drifting ice, floating logs, dropped items and anything else bobbing in water send themselves 5 times a second instead of on every frame. The water never lets them settle, so without this whoever simulates them re-sends each one up to 30 times a second; in a fight on a coast or while sailing through sea ice they can be a quarter to half of every player's connection. Anything within 10 m of another player, or that a player is standing on, still goes out on every frame. The server also passes them on to each player at most about 6 times a second (2 beyond 32 m), which covers players who do not have this mod. Ships, carts and creatures are not affected. Runs on each player's game that has this mod and on the server, using the server's setting."); HoldUnchangedStructures = BindServerConfig("Structure Updates", "Hold Unchanged Structures", value: true, "Server side. Do not re-send a building piece, tree or rock to a player who already has it when all that changed is who owns it, the snow on it, or a value rewritten to what it already was. It goes out at once when its health or support changes, and to a player within 16 m of it; anything else waits until that player's connection has room, up to Max Hold Seconds. The game sends every building update ahead of every creature update, so while players walk through a ruin or a big base the creatures around them otherwise wait behind thousands of walls. Doors, chests, stations, fires, portals, ships and carts are never held. Also covers players who do not have this mod."); StructureMaxHoldSeconds = BindServerConfig("Structure Updates", "Max Hold Seconds", 30f, "How long a held building, tree or rock update waits before it goes out as soon as there is room, in seconds. It never waits more than twice this.", advanced: false, 1f, 120f); EnableSteamTransportTuning = BindServerConfig("Steam Transport", "Enable Transport Tuning", value: true, "Let this mod write Steam's global networking config (send rate and Nagle) and log a before/after readback of what the transport is actually doing. Send Rate KBps ships at the game's value; Nagle Micros ships at 0 rather than the game's 5000, so turning this on by itself only removes Nagle's hold-back delay. Requires the Steam backend; on crossplay-only processes it stands down quietly."); SteamSendRateKBps = BindServerConfig("Steam Transport", "Send Rate KBps", 0, "A fixed rate Steam sends at on every connection from the host, in kilobytes/sec. 0 lets 'Auto Send Rate' choose it, or leaves the game's 150 when that is off; any other value switches 'Auto Send Rate' off. Steam has no congestion control here: this is a fixed pace, not a ceiling it adapts under, and it never slows down for a player whose connection cannot keep up - that player gets packet loss and resends instead (nps_stats flags it as LOSSY). Budget the host's upload for players x this value: 10 players at 500 is 40 Mbit/s worst case. Written to both of Steam's rate bounds, on the host only - players' own uploads stay at the game's rate. Values from 1 to 31 are raised to 32. Applies immediately, no restart needed.", advanced: false, 0, 4096); AutoSendRate = BindServerConfig("Steam Transport", "Auto Send Rate", value: true, "Let the server find the rate it sends to players at by itself, between the game's 150 KB/s and 'Auto Send Rate Max KBps', instead of a fixed one. It only goes up while a player's connection is full, a step at a time, and only as far as the server's line has shown it can carry. It comes back down when several players lose packets or their ping rises together, remembers where that happened, and shares that among however many players there are, so more players means less each. A player whose own connection cannot take a higher rate is held lower by 'Enable Loss Backoff', which this needs. Off, or 'Send Rate KBps' set, means a fixed rate as before."); AutoSendRateMaxKBps = BindServerConfig("Steam Transport", "Auto Send Rate Max KBps", 500, "The most 'Auto Send Rate' will send any one player, and grant any one player for their own upload, in kilobytes/sec.", advanced: false, 150, 1000); AutoPlayerUploads = BindServerConfig("Steam Transport", "Auto Player Uploads", value: true, "Let each player's own game upload to the server faster than the game's 150 KB/s when their upload is full and arriving cleanly - usually the player running the creatures in a big fight. The server decides each player's rate the same way as its own and tells their game; it comes back down for a player whose connection starts losing packets. Players need this mod; everyone else uploads at the game's rate."); ServerUploadLimitKBps = BindServerConfig("Steam Transport", "Server Upload Limit KBps", 0, "What the server's line can send in total, in kilobytes/sec, if you know it (1 Mbit/s is about 122 KB/s). 'Auto Send Rate' then never plans for more than nine tenths of it across all players. 0 lets it find out, by backing off when the line starts losing packets.", advanced: false, 0, 1000000); ServerDownloadLimitKBps = BindServerConfig("Steam Transport", "Server Download Limit KBps", 0, "The same for what the server's line can receive, which 'Auto Player Uploads' shares among players. 0 lets it find out.", advanced: false, 0, 1000000); SteamNagleMicros = BindServerConfig("Steam Transport", "Nagle Micros", 0, "Microseconds Steam may hold a small reliable message back to coalesce it with the next one. Vanilla and Steam both default to 5000 (5ms), which is up to 5ms added in each direction on every update for a saving that mattered on a modem. 0 sends immediately. This mod already batches at the ZDO layer, so there is very little left for Nagle to coalesce - which is why 0 is the default here rather than vanilla's 5000.", advanced: false, 0, 100000); EnableLossBackoff = BindServerConfig("Steam Transport", "Enable Loss Backoff", value: true, "Slow the server down for one player whose connection is losing what it is sent, without touching anyone else's rate. A player still losing as much at Loss Backoff Floor KBps goes back to full speed and is not slowed down again until the server restarts."); LossBackoffThreshold = BindServerConfig("Steam Transport", "Loss Backoff Threshold", 0.95f, "The share of the server's packets that must reach a player, as a fraction. Below it for Loss Backoff Hold Seconds, that player's connection is stepped down. This is also the line nps_stats marks as LOSSY.", advanced: false, 0.5f, 0.999f); LossBackoffFloorKBps = BindServerConfig("Steam Transport", "Loss Backoff Floor KBps", 32, "Never step a player's rate below this, in kilobytes/sec. 32 is the same floor Send Rate KBps has. A floor at or above Send Rate KBps leaves nothing to step down to, which switches the back-off off in effect.", advanced: false, 32, 4096); LossBackoffHoldSeconds = BindServerConfig("Steam Transport", "Loss Backoff Hold Seconds", 10, "How long a player must stay under the threshold before each step down, in seconds. Measured from the last step, so a connection is never lowered faster than this.", advanced: true, 3, 120); LossBackoffRecoverSeconds = BindServerConfig("Steam Transport", "Loss Backoff Recover Seconds", 60, "How long a player must stay clean - two points above the threshold - before each step back up, in seconds. Measured from the last step, so a connection is never raised faster than this.", advanced: true, 10, 600); EnablePlayerLimitOverride = BindServerConfig("Player Limit", "Enable Player Limit Override", value: true, "Enables changing the player limit from vanillas 10."); MaxPlayers = BindServerConfig("Player Limit", "Max Players", 60, "How many players the server accepts. 10 is vanilla. Crossplay servers cant go above 128, steam can. Setting above 128 for Crossplay caps it at 128.", advanced: false, 1, 255); SendPlayerHistoryOnlyWhenChanged = BindServerConfig("Player List", "Send Player History Only When Changed", value: true, "Server side. Send the list of everyone who has ever joined this world to a player only when it differs from the copy they already have. The game sends the whole list - about 60 bytes for every player the world has seen - to every player whenever it marks it changed, and on some servers it marks it changed on every 2-second update without anything changing: 20 KB to every player every 2 seconds on a world that has seen 340 players, over a third of the server's upload. Players who join still get it, and a real change still goes to everyone. Also covers players who do not have this mod. The log says what keeps marking it changed either way."); MapPinsFollowPlayers = BindServerConfig("Player List", "Map Pins Follow Players", value: true, "Each player's own map. Another player's icon jumps straight to where they went after a portal trip, a respawn or a dungeon door, instead of sliding there at 200 m/s - which for a long portal trip takes a minute or more. Players loaded near you are drawn where their character is, and on the host's own map every player is drawn where the host has them, instead of where the last 2-second player list put them. Players who hide their position stay hidden. Works on any server for players who have this mod."); SendMapPositionsRightAway = BindServerConfig("Player List", "Send Map Positions Right Away", value: true, "Server side. Tell players where everyone is as soon as their map is more than Map Position Accuracy out of date, in a small message of their own (24 bytes a player), instead of waiting for the game's player list every 2 seconds. A portal trip always goes out at once. Players with this mod also tell the server about their own portal trip or respawn straight away instead of within 2 seconds, so the new area starts arriving sooner. Players without the mod keep the game's 2-second list."); MapPositionAccuracyMeters = BindServerConfig("Player List", "Map Position Accuracy", 20, "How far, in metres, another player may move from where everyone's map shows them before the server sends their new position. Lower is more exact and sends a little more for every moving player.", advanced: true, 5, 64); EnableConnectionTimeoutTuning = BindServerConfig("Connection Timeout", "Enable Timeout Tuning", value: true, "Let this mod set how long a connection may go quiet before either end hangs up, instead of the game's fixed 30 seconds."); ConnectTimeoutSeconds = BindServerConfig("Connection Timeout", "Connect Timeout Seconds", 10, "How long a connection attempt may take before Steam abandons it, in seconds. 10 is Steam's own default, which the game never changes. This covers only the handshake, before the connection exists.", advanced: false, 5, 600); ConnectionTimeoutSeconds = BindServerConfig("Connection Timeout", "Connection Timeout Seconds", 30, "How long a player who is already in the world may go without a packet before they are dropped, in seconds. 30 is vanilla. Keep it short: a player whose game or connection has died is sent back to the menu and their slot freed this much sooner, instead of being left in a frozen world. Players still joining get 'Loading Timeout Seconds' instead. On a crossplay server the game never drops anyone in under 90.", advanced: false, 10, 600); LoadingTimeoutSeconds = BindServerConfig("Connection Timeout", "Loading Timeout Seconds", 90, "How long a player who is still joining - connecting, downloading and loading the world, until their character appears - may go without a packet before they are dropped, in seconds. Loading a large modded world can freeze a player's game for a minute or more, so this is kept apart from 'Connection Timeout Seconds'. Never shorter than that setting.", advanced: true, 30, 900); KeepPlayersInBackground = BindServerConfig("Connection Timeout", "Keep Players In The Background", value: true, "Do not drop a player for going quiet while their game is in the background - alt-tabbed or minimised. On some computers the game stops running entirely while it is not the active window, and the player then comes back to a lost connection once 'Connection Timeout Seconds' has passed. With this on, a player whose game said it went into the background gets 'Background Timeout Seconds' instead, for as long as it stays there. Turn it off to drop them like anyone else. Players need this mod for the server to know; their creatures are still handed to other players after 'Ghost Owner Evict Seconds'. On a server hosted from the game itself it works the other way too: players with this mod wait for the host's game if it stops in the background, instead of all being dropped. A host whose game keeps running there, as it usually does in a window, is only waited for during its first few seconds in the background."); BackgroundTimeoutSeconds = BindServerConfig("Connection Timeout", "Background Timeout Seconds", 600, "How long a player whose game is in the background may go without a packet before they are dropped, in seconds, when 'Keep Players In The Background' is on. Never shorter than 'Loading Timeout Seconds'. With 'Enable Background Timeout' off, this is only how long a computer that has gone to sleep, lost its connection or crashed is waited for.", advanced: false, 30, 3600); EnableBackgroundTimeout = BindServerConfig("Connection Timeout", "Enable Background Timeout", value: true, "Drop a player whose game has been in the background for longer than 'Background Timeout Seconds'. Turn it off to keep them connected however long their game stays in the background - alt-tabbed for an hour, say - for as long as their computer is still connected. A player whose computer goes to sleep, loses its connection or whose game crashes while it is in the background is still dropped once 'Background Timeout Seconds' has passed. Their slot is held all the while; their creatures are still handed to other players after 'Ghost Owner Evict Seconds'. Needs 'Keep Players In The Background'. On a server hosted from the game itself, players wait for the host's game the same way however long it is stopped in the background."); KeepPlayersWithoutTheMod = BindServerConfig("Connection Timeout", "Keep Players Without The Mod", value: true, "On a server hosted from the game itself: while the host's game has stopped in the background and players with this mod are waiting for it, keep players without this mod (or with an older version of it) connected as well, by answering their game's connection check for it. They see the game's own connection warning icon instead of a message. Needs 'Keep Players In The Background'. Turn it off to have them dropped after 30 seconds as before."); EvictGhostOwners = BindServerConfig("Connection Timeout", "Evict Ghost Owners", value: true, "Stop giving objects to a player who has stopped answering. A peer that goes quiet keeps its slot for the full 'Connection Timeout Seconds' so it can come back, but the things it was simulating - creatures especially - are handed to players who are actually there."); GhostOwnerEvictSeconds = BindServerConfig("Connection Timeout", "Ghost Owner Evict Seconds", 10f, "How long a player may be silent before their objects are given to someone else, in seconds. The game pings every peer once a second, so ten seconds is ten missed replies.", advanced: true, 3f, 60f); TellDroppedCrossplayPlayers = BindServerConfig("Crossplay", "Tell Dropped Players", value: true, "Server side. When the server drops a crossplay player - a mod's join check refusing them, an admin tool or a mod kicking them - send them the game's own disconnect message first, along with anything else still waiting to go to them. The game tells a crossplay player nothing when the server drops them, so they are left in a half-loaded world until their own game gives up, and a message a mod sends just before dropping them is often lost too. With this they go back to the main menu straight away, as Steam players do. Players do not need this mod. Not sent to a player who timed out, left by themselves or was kicked by the game, which tells them itself."); ResumeAfterCrossplayReset = BindServerConfig("Crossplay", "Resume After Connection Reset", value: true, "When nothing has arrived from the host for 26 seconds, a crossplay player's game resets its connection. Once it is back, give the host 26 seconds again to start sending, instead of resetting the connection again on the very next frame - which repeats every few seconds and lets nothing through. A connection that never comes back still ends at the game's own timeout. Runs on each player's game that has this mod, using the server's setting."); ReportVanillaQueueSize = BindServerConfig("Compatibility", "Report Vanilla Queue Size", value: true, "Some mods - ServerSync and every mod that bundles it, ConditionalConfigSync, ServerCharacters, Jotunn - wait for a player's send queue to fall under a fixed 10000-20000 bytes before sending, and disconnect that player after 30 seconds if it never does. This adjusts that to handled changes made by this mod."); EnableDeserializeFastPath = BindServerConfig("Allocation", "Enable ZDO Deserialize Fast Path", value: true, "Read a received ZDO's fields directly instead of through the fourteen delegates the game allocates for every single one."); EnablePacketReadFastPath = BindServerConfig("Allocation", "Enable Packet Read Fast Path", value: true, "Read each incoming ZDO's payload straight into the buffer that is about to hold it instead of copying twice."); EnableSendPackageReuse = BindServerConfig("Allocation", "Enable Send Package Reuse", value: true, "Reuse the two packet buffers the send path builds, instead of constructing and discarding both on every send to every peer."); EnableRpcInvokeFastPath = BindServerConfig("Allocation", "Enable RPC Invoke Fast Path", value: true, "Call an incoming RPC's handler directly when its signature is the common one, instead of going through reflection for every message."); EnableRelaySendReuse = BindServerConfig("Allocation", "Enable Relay Send Reuse", value: true, "Write each relayed RPC (footsteps, hits, damage numbers, chat and the rest) once and hand the same bytes to every player it goes to, instead of rebuilding and re-copying the whole message for each one. The bytes sent are identical. Host-side only."); EnableMonitoring = BindServerConfig("Monitoring", "Enable Network Monitoring", value: false, "Record what the network is doing to BepInEx/NpsMonitoring, for sending to the mod author with a bug or optimization report."); MonitoringCollectFromClients = BindServerConfig("Monitoring", "Collect From Clients", value: true, "While monitoring is on, also ask every player's game for its network related stats"); MonitoringClientBytesPerSecond = BindServerConfig("Monitoring", "Client Upload Bytes Per Second", 2048, "The most each client may send the server in monitoring records, averaged over time. Records beyond it are dropped on the client and counted. Sent in 4KB batches that are held back whenever the client's link to the server is already near its send window, so they never delay the game's own traffic.", advanced: true, 256, 4096); MonitoringMaxDiskMB = BindServerConfig("Monitoring", "Max Disk MB", 4096, "The most BepInEx/NpsMonitoring may hold, across every session in it. Recording stops when it is reached and says so in the log. Nothing already recorded is ever deleted to make room. Files are compressed as they are closed.", advanced: false, 64, 65536); Config.SettingChanged += OnAnySettingChanged; EnableSteamTransportTuning.SettingChanged += OnSteamTransportSettingChanged; SteamSendRateKBps.SettingChanged += OnSteamTransportSettingChanged; SteamNagleMicros.SettingChanged += OnSteamTransportSettingChanged; EnableLossBackoff.SettingChanged += OnLossBackoffSettingChanged; LossBackoffThreshold.SettingChanged += OnLossBackoffSettingChanged; LossBackoffFloorKBps.SettingChanged += OnLossBackoffSettingChanged; LossBackoffHoldSeconds.SettingChanged += OnLossBackoffSettingChanged; LossBackoffRecoverSeconds.SettingChanged += OnLossBackoffSettingChanged; DamageNumbers.SettingChanged += OnDamageNumbersSettingChanged; BalanceCreaturesByFrameRate.SettingChanged += OnCreatureLoadSettingChanged; BalanceCreaturesByUpload.SettingChanged += OnCreatureLoadSettingChanged; CreatureLoadMinOwnerFps.SettingChanged += OnCreatureLoadSettingChanged; CreatureLoadMinAllowance.SettingChanged += OnCreatureLoadSettingChanged; EnableConnectionTimeoutTuning.SettingChanged += OnConnectionTimeoutSettingChanged; ConnectTimeoutSeconds.SettingChanged += OnConnectionTimeoutSettingChanged; ConnectionTimeoutSeconds.SettingChanged += OnConnectionTimeoutSettingChanged; LoadingTimeoutSeconds.SettingChanged += OnConnectionTimeoutSettingChanged; KeepPlayersInBackground.SettingChanged += OnConnectionTimeoutSettingChanged; BackgroundTimeoutSeconds.SettingChanged += OnConnectionTimeoutSettingChanged; EnableBackgroundTimeout.SettingChanged += OnConnectionTimeoutSettingChanged; EnableSendWindowSizing.SettingChanged += OnJotunnQueueSettingChanged; SendWindowMaxBytes.SettingChanged += OnJotunnQueueSettingChanged; } private static void OnSteamTransportSettingChanged(object sender, EventArgs e) { SteamTransport.OnConfigChanged(); } private static void OnLossBackoffSettingChanged(object sender, EventArgs e) { LossBackoff.OnConfigChanged(); } private static void OnDamageNumbersSettingChanged(object sender, EventArgs e) { global::NetworkPerformanceSystem.Runtime.DamageNumbers.OnConfigChanged(); } private static void OnCreatureLoadSettingChanged(object sender, EventArgs e) { PeerCapacity.OnConfigChanged(); } private static void OnJotunnQueueSettingChanged(object sender, EventArgs e) { JotunnSendQueue.OnConfigChanged(); } private static void OnConnectionTimeoutSettingChanged(object sender, EventArgs e) { ConnectionTimeout.OnConfigChanged(); } private static void OnAnySettingChanged(object sender, SettingChangedEventArgs e) { reloadChanges?.Add($"[{e.ChangedSetting.Definition.Section}] {e.ChangedSetting.Definition.Key} = {e.ChangedSetting.BoxedValue}"); Monitoring.OnSettingChanged(e.ChangedSetting); } public static ConfigEntry<float[]> BindServerConfig(string category, string key, float[] value, string description, bool advanced = false, float valMin = 0f, float valMax = 150f) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return cfg.Bind<float[]>(category, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<float>(valMin, valMax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<bool> BindServerConfig(string category, string key, bool value, string description, AcceptableValueBase acceptableValues = null, bool advanced = false) { //IL_0013: 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_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002d: 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 return cfg.Bind<bool>(category, key, value, new ConfigDescription(description, acceptableValues, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<int> BindServerConfig(string category, string key, int value, string description, bool advanced = false, int valMin = 0, int valMax = 150) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return cfg.Bind<int>(category, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<int>(valMin, valMax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<float> BindServerConfig(string category, string key, float value, string description, bool advanced = false, float valMin = 0f, float valMax = 150f) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return cfg.Bind<float>(category, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<float>(valMin, valMax), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } public static ConfigEntry<string> BindServerConfig(string category, string key, string value, string description, AcceptableValueList<string> acceptableValues = null, bool advanced = false) { //IL_0013: 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_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002d: 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 return cfg.Bind<string>(category, key, value, new ConfigDescription(description, (AcceptableValueBase)(object)acceptableValues, new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced } })); } } [BepInPlugin("MidnightsFX.NetworkPerformanceSystem", "NetworkPerformanceSystem", "1.18.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInIncompatibility("com.Fire.FiresGhettoNetworkMod")] [BepInIncompatibility("VitByr.VBNetTweaks")] [BepInIncompatibility("sighsorry.SkadiNet")] [BepInIncompatibility("CW_Jesse.BetterNetworking")] [BepInIncompatibility("org.bepinex.plugins.network")] [BepInIncompatibility("Searica.Valheim.NetworkTweaks")] [BepInIncompatibility("dzk.warheimnetwork")] [BepInIncompatibility("com.maxsch.valheim.TimeoutLimit")] internal class NetworkPerformanceSystem : BaseUnityPlugin { public const string PluginGUID = "MidnightsFX.NetworkPerformanceSystem"; public const string PluginName = "NetworkPerformanceSystem"; public const string PluginVersion = "1.18.1"; internal static ManualLogSource Log; internal static Harmony HarmonyInstance; internal ValConfig cfg; public void Awake() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; cfg = new ValConfig(((BaseUnityPlugin)this).Config); HarmonyInstance = new Harmony("MidnightsFX.NetworkPerformanceSystem"); HarmonyInstance.PatchAll(typeof(NetworkPerformanceSystem).Assembly); PlayerLimitPatches.ApplyPlayFabCapacityPatch(HarmonyInstance); CrossplayDisconnectsPatches.Apply(HarmonyInstance); JotunnSendQueue.OnStartup(); ValConfig.SaveOnSet(enabled: true); ValConfig.WatchConfigFile(); } public void Start() { SendSchedulerPatches.CheckForReturnToSender(); RoutedRpcPatches.CheckForOverlappingMods(); RpcOwnerRouterPatches.CheckForOverlappingMods(); GhostWatchdog.CheckForRival(); PatchGuard.VerifyAfterPatching(); } public void OnGUI() { //IL_0037: Unknown result type (might be due to invalid IL or missing references) if (ValConfig.EnableDebugOverlay != null && ValConfig.EnableDebugOverlay.Value && NpsEnv.NetReady() && !NpsEnv.IsDedicated()) { GUI.Label(new Rect(10f, 10f, 1400f, 24f), NetworkStats.BuildOverlay()); } } public void OnDestroy() { Monitoring.Shutdown(); JotunnSendQueue.Restore(); Harmony harmonyInstance = HarmonyInstance; if (harmonyInstance != null) { harmonyInstance.UnpatchSelf(); } } } } namespace NetworkPerformanceSystem.Runtime { internal static class AllocationRelief { internal static long DeserializeFast; internal static long PacketReadFast; internal static long SendPackagesReused; internal static long RpcFastPath; internal static long RelaySendsReused; internal static bool DeserializeHookInstalled; internal static bool PacketReadHookInstalled; internal static bool RpcInvokeHookInstalled; internal static bool DeserializeWanted { get { if (ValConfig.EnableDeserializeFastPath != null) { return ValConfig.EnableDeserializeFastPath.Value; } return false; } } internal static bool PacketReadWanted { get { if (ValConfig.EnablePacketReadFastPath != null) { return ValConfig.EnablePacketReadFastPath.Value; } return false; } } internal static bool SendPackageReuseWanted { get { if (ValConfig.EnableSendPackageReuse != null) { return ValConfig.EnableSendPackageReuse.Value; } return false; } } internal static bool RpcInvokeWanted { get { if (ValConfig.EnableRpcInvokeFastPath != null) { return ValConfig.EnableRpcInvokeFastPath.Value; } return false; } } internal static bool RelaySendWanted { get { if (ValConfig.EnableRelaySendReuse != null) { return ValConfig.EnableRelaySendReuse.Value; } return false; } } internal static bool DeserializeActive { get { if (PatchGuard.IsActive(Mechanism.DeserializeAlloc)) { return DeserializeWanted; } return false; } } internal static bool PacketReadActive { get { if (PatchGuard.IsActive(Mechanism.PacketReadAlloc)) { return PacketReadWanted; } return false; } } internal static bool SendPackageReuseActive { get { if (PatchGuard.IsActive(Mechanism.SendPacketReuse)) { return SendPackageReuseWanted; } return false; } } internal static bool RpcInvokeActive { get { if (PatchGuard.IsActive(Mechanism.RpcInvokeFastPath)) { return RpcInvokeWanted; } return false; } } internal static bool RelaySendActive { get { if (PatchGuard.IsActive(Mechanism.RelaySendReuse)) { return RelaySendWanted; } return false; } } internal static bool TryGetZdoRates(out int sentPerSecond, out int recvPerSecond) { ZDOMan instance = ZDOMan.instance; if (instance == null) { sentPerSecond = 0; recvPerSecond = 0; return false; } sentPerSecond = instance.GetSentZDOs(); recvPerSecond = instance.GetRecvZDOs(); return true; } } internal static class AutoSendRate { private sealed class Player { internal string Key; internal long Uid; internal uint Handle; internal string Name; internal float LastSampleAt = float.NegativeInfinity; internal readonly LinkTrack Out = new LinkTrack(); internal readonly LinkTrack In = new LinkTrack(); internal readonly GrantState Grant = new GrantState(); internal int SentGrant; internal float SentAt = float.NegativeInfinity; internal float QueueSince = float.NegativeInfinity; internal bool GrantCapable; } internal struct View { internal bool Full; internal float Quality; internal float PingMs; internal float BaselineMs; internal bool UploadFull; internal float UploadQuality; internal int GrantBytesPerSec; internal int GrantCeilingBytesPerSec; internal bool GrantCapable; } private const float EvaluateSeconds = 1f; private const float StaleSampleSeconds = 5f; private const float HitchSeconds = 0.5f; private const float HitchGraceSeconds = 15f; private const float HealthyFrameMs = 50f; private const float OverloadedFrameMs = 66f; private const int FrameWindowSeconds = 10; private const float ReportFreshSeconds = 10f; private const float GrantTtlSeconds = 40f; private const float GrantRefreshSeconds = 10f; private const float QueueHoldSeconds = 10f; private const float PauseWhileVotingSeconds = 3f; private const float PauseAfterDownSeconds = 30f; private const int BytesPerKB = 1024; private static readonly Dictionary<string, Player> Players = new Dictionary<string, Player>(); private static readonly Dictionary<long, Player> ByUid = new Dictionary<long, Player>(); private static readonly OutState Out = new OutState(); private static readonly InState In = new InState(); private static readonly List<Player> Active = new List<Player>(); private static readonly List<LinkVote> OutVotes = new List<LinkVote>(); private static readonly List<LinkVote> InVotes = new List<LinkVote>(); private static readonly List<GrantState> Grants = new List<GrantState>(); private static readonly List<GrantChange> Changes = new List<GrantChange>(); private static readonly float[] FrameSeconds = new float[10]; private static readonly int[] FrameCounts = new int[10]; private static int _frameSlot; private static float _slotSeconds; private static float _evalTimer; private static float _lastHitchAt = float.NegativeInfinity; private static float _overloadedSince = float.NegativeInfinity; private static bool _uploadsLive; private static int _grantCap = 153600; internal static RateDecision LastOut { get; private set; } internal static float MeanFrameMs { get; private set; } internal static bool LastUsable { get; private set; } internal static bool LastHealthy { get; private set; } internal static long TotalUps { get; private set; } internal static long TotalDowns { get; private set; } internal static long TotalGrantChanges { get; private set; } internal static bool Wanted { get { if (NpsEnv.IsHost() && PatchGuard.IsActive(Mechanism.AutoSendRate) && PatchGuard.IsActive(Mechanism.SteamTransport) && ValConfig.EnableSteamTransportTuning.Value && ValConfig.AutoSendRate.Value) { return ValConfig.SteamSendRateKBps.Value <= 0; } return false; } } internal static bool RaiseWanted { get { if (Wanted) { return LossBackoff.Wanted; } return false; } } internal static bool UploadsWanted { get { if (Wanted) { return ValConfig.AutoPlayerUploads.Value; } return false; } } internal static bool Measuring { get { if (!Wanted) { if (NpsEnv.IsHost() && PatchGuard.IsActive(Mechanism.AutoSendRate)) { return PeerCapacity.BalancingUploads; } return false; } return true; } } internal static int MaxBytesPerSec => Mathf.Max(153600, ValConfig.AutoSendRateMaxKBps.Value * 1024); internal static OutState OutSnapshot => Out; internal static InState InSnapshot => In; internal static void Observe(ZNetPeer peer, RttProbe.LinkStatus link, int pingMs) { if (peer != null && peer.m_uid != 0L && Measuring && RttProbe.TryGetConnectionHandle(peer.m_socket, out var handle)) { float realtimeSinceStartup = Time.realtimeSinceStartup; Player player = PlayerFor(peer, handle); player.LastSampleAt = realtimeSinceStartup; ThroughputRules.Observe(player.Out, realtimeSinceStartup, link.OutBytesPerSec, link.QualityRemote, link.PendingBytes, link.SendRateBytesPerSec, pingMs); int pending = -1; player.GrantCapable = false; if (PeerCapacity.TryGetView(peer.m_uid, out var view) && view.HasReport && view.AgeSeconds < 10f && view.Last.HasUpload) { pending = view.Last.UploadPending; player.GrantCapable = true; } ThroughputRules.Observe(player.In, realtimeSinceStartup, link.InBytesPerSec, link.QualityLocal, pending, player.Grant.Grant, pingMs); } } private static Player PlayerFor(ZNetPeer peer, uint handle) { if (ByUid.TryGetValue(peer.m_uid, out var value) && value.Handle == handle) { return value; } string key = PlayerKey(peer); if (!Players.TryGetValue(key, out var value2)) { value2 = new Player { Key = key }; Players[key] = value2; } else if (value2.Uid != peer.m_uid || value2.Handle != handle) { ThroughputRules.Reconnected(value2.Out); ThroughputRules.Reconnected(value2.In); if (value2.Uid != 0L) { ByUid.Remove(value2.Uid); } value2.Grant.Grant = 153600; value2.Grant.Previous = 153600; value2.SentGrant = 0; value2.SentAt = float.NegativeInfinity; value2.QueueSince = float.NegativeInfinity; } value2.Uid = peer.m_uid; value2.Handle = handle; value2.Name = (string.IsNullOrEmpty(peer.m_playerName) ? $"peer {peer.m_uid}" : peer.m_playerName); ByUid[peer.m_uid] = value2; return value2; } internal static void Tick(float dt) { float realtimeSinceStartup = Time.realtimeSinceStartup; NoteFrame(dt, realtimeSinceStartup); if (!Wanted) { _uploadsLive = false; if (Out.Rate != 153600 || SteamTransport.AutoRateBytesPerSec != 0) { StandDown("switched off"); } return; } _evalTimer += dt; if (!(_evalTimer < 1f)) { _evalTimer = 0f; Evaluate(realtimeSinceStartup); } } private static void NoteFrame(float dt, float now) { if (dt >= 0.5f) { _lastHitchAt = now; } FrameSeconds[_frameSlot] += dt; FrameCounts[_frameSlot]++; _slotSeconds += dt; if (!(_slotSeconds < 1f)) { _slotSeconds = 0f; float num = 0f; int num2 = 0; for (int i = 0; i < 10; i++) { num += FrameSeconds[i]; num2 += FrameCounts[i]; } MeanFrameMs = ((num2 > 0) ? (num * 1000f / (float)num2) : 0f); _frameSlot = (_frameSlot + 1) % 10; FrameSeconds[_frameSlot] = 0f; FrameCounts[_frameSlot] = 0; } } private static void Evaluate(float now) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return; } Active.Clear(); OutVotes.Clear(); InVotes.Clear(); Grants.Clear(); List<ZNetPeer> peers = instance.GetPeers(); for (int i = 0; i < peers.Count; i++) { ZNetPeer val = peers[i]; if (val != null && ByUid.TryGetValue(val.m_uid, out var value) && !(now - value.LastSampleAt > 5f) && !PeerLiveness.IsGhost(val.m_uid)) { bool flag = !val.IsReady() || Loading(val.m_uid); LossBackoff.TryGetView(val.m_uid, out var view); bool flag2 = view.OverrideBytesPerSec > 0 && !view.HeldAtBase; Active.Add(value); OutVotes.Add(new LinkVote { Eligible = (!flag && !view.Exempt && !flag2 && ThroughputRules.Settled(value.Out) && value.Out.Baseline > 0f), Full = ThroughputRules.IsFull(value.Out), Held = (flag2 || view.HeldAtBase), Loss = ThroughputRules.LossVote(value.Out), Rtt = ThroughputRules.RttVote(value.Out), Quality = value.Out.Quality, Bps = value.Out.Bps, Delivered = value.Out.Delivered }); InVotes.Add(new LinkVote { Eligible = (!flag && ThroughputRules.Settled(value.In) && value.In.Baseline > 0f), Full = (value.GrantCapable && ThroughputRules.IsFull(value.In)), Held = !value.GrantCapable, Loss = ThroughputRules.LossVote(value.In), Rtt = ThroughputRules.RttVote(value.In), Quality = value.In.Quality, Bps = value.In.Bps, Delivered = value.In.Delivered }); Grants.Add(value.Grant); } } bool flag3 = now - _lastHitchAt > 15f && !instance.IsSaving() && !PeerLiveness.LocalFaultWithin(30f); bool flag4 = MeanFrameMs > 0f && MeanFrameMs < 50f; if (MeanFrameMs >= 66f) { if (_overloadedSince == float.NegativeInfinity) { _overloadedSince = now; } } else { _overloadedSince = float.NegativeInfinity; } bool hostOverloaded = _overloadedSince > float.NegativeInfinity && now - _overloadedSince >= 10f; LastUsable = flag3; LastHealthy = flag4; if (Active.Count != 0) { RateInputs input = new RateInputs { Now = now, Usable = flag3, HostHealthy = (flag4 && RaiseWanted), HostOverloaded = hostOverloaded, Max = MaxBytesPerSec, AdminLimit = (float)ValConfig.ServerUploadLimitKBps.Value * 1024f }; DecideOut(input, now); _uploadsLive = UploadsWanted; if (_uploadsLive) { input.HostHealthy = flag4; input.AdminLimit = (float)ValConfig.ServerDownloadLimitKBps.Value * 1024f; DecideIn(input, now); _grantCap = ThroughputRules.PerPlayerLimit(ThroughputRules.LimitTotal(In.Limits, input.AdminLimit), Active.Count, input.Max); } } } private static void DecideOut(RateInputs input, float now) { RateDecision d = (LastOut = ThroughputRules.StepOut(Out, input, OutVotes)); if (d.Vote.Shared && d.Busy) { LossBackoff.PauseFor(3f); } if (d.Action != RateAction.None) { if (d.Action == RateAction.Down) { LossBackoff.PauseFor(30f); } for (int i = 0; i < Active.Count; i++) { ThroughputRules.NoteRateChange(Active[i].Out, now, d.Action == RateAction.Up); } if (d.Action == RateAction.Up) { TotalUps++; } else { TotalDowns++; } SteamTransport.SetAutoRate(d.Rate, "auto send rate"); Announce("Auto send rate", d, Out.Limits, OutVotes); Record("out", d, Out.Limits, 0L); } else { for (int j = 0; j < Active.Count; j++) { ThroughputRules.RefreshReference(Active[j].Out, now, d.Vote.Shared); } } for (int k = 0; k < Active.Count; k++) { Player player = Active[k]; LinkVote linkVote = OutVotes[k]; if (!linkVote.Full || linkVote.Held || !linkVote.Rtt || d.Vote.Shared || Out.Rate <= 153600) { player.QueueSince = float.NegativeInfinity; continue; } if (player.QueueSince == float.NegativeInfinity) { player.QueueSince = now; } if (!(now - player.QueueSince < 10f)) { player.QueueSince = float.NegativeInfinity; ZNetPeer peer = ZNet.instance.GetPeer(player.Uid); if (peer != null) { LossBackoff.StepDownForQueue(peer, player.Out.Ping, player.Out.Baseline); } } } } private static void DecideIn(RateInputs input, float now) { Changes.Clear(); ThroughputRules.StepIn(In, input, InVotes, Grants, Changes); for (int i = 0; i < Changes.Count; i++) { GrantChange grantChange = Changes[i]; Player player = Active[grantChange.Index]; ThroughputRules.NoteRateChange(player.In, now, grantChange.To > grantChange.From); TotalGrantChanges++; Logger.LogInfo("Auto send rate: " + player.Name + "'s upload " + Kb(grantChange.From) + " -> " + Kb(grantChange.To) + " (" + grantChange.Reason + "; delivering " + Pct(player.In.Quality) + ")."); if (Monitoring.Active) { Monitoring.OnAutoRate("in", (grantChange.To > grantChange.From) ? "up" : "down", grantChange.From, grantChange.To, Active.Count, 0, player.In.Delivered, In.Limits.CommittedPeak, In.Limits.Ceiling, grantChange.Reason, player.Uid); } } for (int j = 0; j < Active.Count; j++) { if (Changes.Count == 0) { ThroughputRules.RefreshReference(Active[j].In, now, congested: false); } SendGrant(Active[j], now); } } private static void SendGrant(Player p, float now) { int grant = p.Grant.Grant; bool flag = grant > 153600; if ((grant != p.SentGrant || (flag && !(now - p.SentAt < 10f))) && (flag || p.SentGrant != 0)) { ZNetPeer peer = ZNet.instance.GetPeer(p.Uid); if (peer?.m_rpc != null) { peer.m_rpc.Invoke("Nps.UploadRate", new object[1] { UploadGrant.BuildPackage(grant, 40f) }); p.SentGrant = (flag ? grant : 0); p.SentAt = now; } } } private static bool Loading(long uid) { if (PeerCapacity.TryGetView(uid, out var view) && view.HasReport) { return (view.Last.Flags & 2) != 0; } return false; } private static void Announce(string what, RateDecision d, RateLimits l, List<LinkVote> votes) { int num = 0; for (int i = 0; i < votes.Count; i++) { if (votes[i].Full && !votes[i].Held) { num++; } } string text = d.Action switch { RateAction.Up => $"{num} of {votes.Count} players full, line clean at {Kb(d.Total)}", RateAction.Down => $"the server's line is congested: {d.Vote.Voters} of {d.Vote.Eligible} players losing packets or queueing at {Kb(d.Total)}", RateAction.Limit => $"{votes.Count} players share {Kb(d.LimitTotal)}", RateAction.Overload => $"the server's frames average {MeanFrameMs:F0}ms", RateAction.NoGain => "the last step did not raise what full players receive - something else limits them", _ => d.Action.ToString(), }; string text2 = what + ": " + Kb(d.From) + " -> " + Kb(d.Rate) + " per player (" + text + ")."; if (d.LearnedCeiling) { Logger.LogWarning(text2 + $" The server's upload looks to carry about {Kb(l.Ceiling)}; rates stay under that share for {l.CeilingHold / 60f:F0} minutes."); } else { Logger.LogInfo(text2); } } private static void Record(string dir, RateDecision d, RateLimits l, long uid) { if (!Monitoring.Active) { return; } int num = 0; for (int i = 0; i < OutVotes.Count; i++) { if (OutVotes[i].Full && !OutVotes[i].Held) { num++; } } Monitoring.OnAutoRate(dir, d.Action.ToString().ToLowerInvariant(), d.From, d.Rate, OutVotes.Count, num, d.Total, l.CommittedPeak, l.Ceiling, d.Vote.Shared ? "shared" : "", uid); } internal static bool TryGetView(long uid, out View view) { view = default(View); if (!ByUid.TryGetValue(uid, out var value)) { return false; } view = new View { Full = ThroughputRules.IsFull(value.Out), Quality = value.Out.Quality, PingMs = value.Out.Ping, BaselineMs = value.Out.Baseline, UploadFull = (value.GrantCapable && ThroughputRules.IsFull(value.In)), UploadQuality = value.In.Quality, GrantBytesPerSec = value.Grant.Grant, GrantCeilingBytesPerSec = value.Grant.Ceiling, GrantCapable = value.GrantCapable }; return true; } internal static bool TryGetUpload(long uid, float now, out UploadReading reading) { reading = default(UploadReading); if (!ByUid.TryGetValue(uid, out var value)) { return false; } if (now - value.LastSampleAt > 5f || value.In.Samples < 4) { return false; } reading = new UploadReading { Known = true, Full = ThroughputRules.IsFull(value.In), Delivered = value.In.Delivered, Rate = value.Grant.Grant, Quality = value.In.Quality, GrantMayRise = (_uploadsLive && value.GrantCapable && ThroughputRules.GrantMayRise(value.Grant, value.In, _grantCap)), GrantChangedAt = value.Grant.LastChangeAt }; return true; } private static void StandDown(string why) { if (Out.Rate != 153600) { Logger.LogInfo("Auto send rate: " + Kb(Out.Rate) + " -> the game's " + Kb(153600f) + " (" + why + ")."); } Out.Rate = 153600; SteamTransport.SetAutoRate(0, why); foreach (Player value in Players.Values) { value.Grant.Grant = 153600; } } internal static void ForgetPeer(long uid) { ByUid.Remove(uid); } internal static void Reset() { Players.Clear(); ByUid.Clear(); Active.Clear(); Out.Rate = 153600; Out.PreviousRate = 153600; Out.LastChangeAt = float.NegativeInfinity; Out.LastUpAt = float.NegativeInfinity; Out.UpCheckAt = float.NegativeInfinity; Out.NoGainUntil = float.NegativeInfinity; Out.NoGainHold = 600f; ResetLimits(Out.Limits); In.LastUpAt = float.NegativeInfinity; ResetLimits(In.Limits); LastOut = default(RateDecision); _evalTimer = 0f; _lastHitchAt = float.NegativeInfinity; _overloadedSince = float.NegativeInfinity; _uploadsLive = false; _grantCap = 153600; } private static void ResetLimits(RateLimits l) { l.WindowSum = 0f; l.WindowCount = 0; l.WindowStart = float.NegativeInfinity; l.CandidatePeak = 0f; l.CandidateAt = float.NegativeInfinity; l.CommittedPeak = 0f; l.Ceiling = -1f; l.CeilingUntil = float.NegativeInfinity; l.CeilingHold = 600f; l.Reprobing = false; l.ReprobeFrom = -1f; l.LastCongestionAt = float.NegativeInfinity; l.Votes = 0; } private static string PlayerKey(ZNetPeer peer) { string text = null; try { ISocket socket = peer.m_socket; text = ((socket != null) ? socket.GetHostName() : null); } catch (Exception) { } if (!string.IsNullOrEmpty(text)) { return text; } return "peer:" + peer.m_uid; } internal static string Kb(float bytesPerSec) { return $"{bytesPerSec / 1024f:F0} KB/s"; } private static string Pct(float share) { if (!(share < 0f)) { return $"{share * 100f:F1}%"; } return "-"; } } internal static class ConnectionTimeout { private sealed class PeerState { internal bool InWorld; internal bool InBackground; internal bool Held; } internal const float VanillaRpcTimeoutSeconds = 30f; internal const float VanillaRpcLongTimeoutSeconds = 90f; internal const int VanillaSteamConnectedMillis = 30000; internal const int VanillaSteamInitialMillis = 10000; private const int MillisPerSecond = 1000; private static bool _longMode; private static bool _rpcSeamSeen; private static bool _steamUp; private static bool _perPeer; private static ConditionalWeakTable<ZRpc, PeerState> _peers = new ConditionalWeakTable<ZRpc, PeerState>(); private static string _lastLoggedRpc; internal const string RpcHostFocus = "Nps.HostFocus"; private const byte HostFocusWait = 1; private const byte HostFocusInBackground = 2; private const float HostFocusRepeatSeconds = 2f; internal const float HostRunningProofSeconds = 5f; private const float MinHostStallSeconds = 5f; private static byte _hostToldFlags; private static float _hostFocusRepeatAt; private static bool _hostWasBackground; private static float _hostBackgroundSince; private static float _hostLastTickAt = -1f; internal static float InWorldTimeoutSeconds { get; private set; } = 30f; internal static float JoiningTimeoutSeconds { get; private set; } = 30f; internal static float BackgroundTimeoutSeconds { get; private set; } internal static bool BackgroundUnlimited { get; private set; } internal static float BackgroundDeadlineSeconds { get { if (!BackgroundUnlimited) { return BackgroundTimeoutSeconds; } return float.PositiveInfinity; } } internal static string LastSteamReadback { get; private set; } internal static bool HostInBackground { get; private set; } internal static bool HostToldWait { get; private set; } internal static bool HostStopsInBackground { get; private set; } internal static bool Active { get { if (PatchGuard.IsActive(Mechanism.ConnectionTimeout) && ValConfig.EnableConnectionTimeoutTuning != null && ValConfig.ConnectionTimeoutSeconds != null && ValConfig.ConnectTimeoutSeconds != null && ValConfig.LoadingTimeoutSeconds != null) { return ValConfig.EnableConnectionTimeoutTuning.Value; } return false; } } private static bool KeepInBackground { get { if (Active && ValConfig.KeepPlayersInBackground != null && ValConfig.BackgroundTimeoutSeconds != null) { return ValConfig.KeepPlayersInBackground.Value; } return false; } } internal static long BackgroundHolds { get; private set; } private static float HostStallSeconds => Math.Max(5f, InWorldTimeoutSeconds * 0.5f); internal static float DeadlineFor(ZRpc rpc) { if (rpc == null || !_peers.TryGetValue(rpc, out var value)) { return JoiningTimeoutSeconds; } return DeadlineOf(value); } internal static float UsualDeadlineFor(ZRpc rpc) { if (rpc == null || !_peers.TryGetValue(rpc, out var value)) { return JoiningTimeoutSeconds; } return BaseDeadlineOf(value); } private static float BaseDeadlineOf(PeerState state) { if (!state.InWorld) { return JoiningTimeoutSeconds; } return InWorldTimeoutSeconds; } private static float DeadlineOf(PeerState state) { float num = BaseDeadlineOf(state); float backgroundDeadlineSeconds = BackgroundDeadlineSeconds; if (!state.InBackground || !(backgroundDeadlineSeconds > num)) { return num; } return backgroundDeadlineSeconds; } internal static bool IsInBackground(ZRpc rpc) { if (rpc != null && _peers.TryGetValue(rpc, out var value)) { return value.InBackground; } return false; } internal static void BeforeRpcUpdate(ZRpc rpc) { if (!_perPeer) { return; } if (rpc == null || !_peers.TryGetValue(rpc, out var value)) { ZRpc.m_timeout = JoiningTimeoutSeconds; return; } float deadline = (ZRpc.m_timeout = DeadlineOf(value)); if (value.InBackground) { NoteBackgroundSilence(rpc, value, deadline); } } private static void NoteBackgroundSilence(ZRpc rpc, PeerState state, float deadline) { float timeSinceLastPing = rpc.GetTimeSinceLastPing(); if (state.Held) { if (timeSinceLastPing < 1f) { state.Held = false; } return; } float num = BaseDeadlineOf(state); if (deadline <= num || timeSinceLastPing <= num) { return; } state.Held = true; BackgroundHolds++; if (!NpsEnv.IsHost()) { Logger.LogInfo($"The host's game is in the background and has not answered for {num:F0}s. " + "Waiting for it " + HowLong(deadline) + " (Keep Players In The Background)."); return; } ZNetPeer val = (((Object)(object)ZNet.instance != (Object)null) ? NetworkChannelPatches.FindPeerByRpc(rpc) : null); string arg = ((val != null && !string.IsNullOrEmpty(val.m_playerName)) ? val.m_playerName : "A player"); Logger.LogInfo($"{arg}'s game is in the background and has not answered for {num:F0}s. " + "Keeping them connected " + HowLong(deadline) + " (Keep Players In The Background)."); if (val != null && Monitoring.Active) { Monitoring.OnBackgroundHold(val.m_uid, num, deadline); } } private static string HowLong(float deadline) { if (!float.IsPositiveInfinity(deadline)) { return $"for up to {deadline:F0}s while it stays there"; } return "for as long as it stays there"; } internal static void NoteInWorld(ZRpc rpc) { if (rpc != null) { _peers.GetOrCreateValue(rpc).InWorld = true; } } internal static void NoteFocus(ZRpc rpc, bool inBackground) { if (rpc != null) { PeerState value; if (inBackground) { value = _peers.GetOrCreateValue(rpc); } else if (!_peers.TryGetValue(rpc, out value)) { return; } value.InBackground = inBackground; if (!inBackground) { value.Held = false; } } } internal static void TickHostFocus() { if (!NpsEnv.IsDedicated()) { Player localPlayer = Player.m_localPlayer; bool loading = (Object)(object)localPlayer == (Object)null || ((Character)localPlayer).IsTeleporting(); if (StepHostFocus(KeepInBackground && !Application.isFocused, loading, Time.realtimeSinceStartup, out var flags)) { SendHostFocus(flags); } } } internal static bool StepHostFocus(bool background, bool loading, float now, out byte flags) { float num = ((_hostLastTickAt >= 0f) ? (now - _hostLastTickAt) : 0f); _hostLastTickAt = now; if (background && !_hostWasBackground) { _hostBackgroundSince = now; } else if (background && !loading && !HostStopsInBackground && num >= HostStallSeconds) { HostStopsInBackground = true; Logger.LogInfo($"This game stopped for {num:F0}s while it was in the background. For the rest of the session, " + "players with this mod will wait for it whenever it is in the background (Keep Players In The Background)."); } _hostWasBackground = background; flags = 0; if (background) { flags = 2; if (HostStopsInBackground || now - _hostBackgroundSince < 5f) { flags |= 1; } } HostInBackground = background; HostToldWait = (flags & 1) != 0; if (flags != _hostToldFlags) { _hostToldFlags = flags; _hostFocusRepeatAt = now; return true; } if (flags == 0 || now - _hostFocusRepeatAt < 2f) { return false; } _hostFocusRepeatAt = now; return true; } private static void SendHostFocus(byte flags) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown List<ZNetPeer> peers = ZNet.instance.GetPeers(); for (int i = 0; i < peers.Count; i++) { ZNetPeer val = peers[i]; if (val != null && val.IsReady() && val.m_rpc != null) { ZPackage val2 = new ZPackage(); val2.Write(flags); val.m_rpc.Invoke("Nps.HostFocus", new object[1] { val2 }); } } } internal static void RPC_HostFocus(ZRpc rpc, ZPackage pkg) { if (!NpsEnv.IsHost() && rpc != null && TryReadHostFocus(pkg, out var wait, out var inBackground)) { HostInBackground = inBackground; NoteFocus(rpc, wait); } } internal static bool TryReadHostFocus(ZPackage pkg, out bool wait, out bool inBackground) { wait = false; inBackground = false; if (pkg == null) { return false; } try { byte b = pkg.ReadByte(); wait = (b & 1) != 0; inBackground = wait || (b & 2) != 0; return true; } catch (Exception) { return false; } } internal static void OnVanillaRpcTimeoutSet(bool longMode) { _longMode = longMode; _rpcSeamSeen = true; ApplyRpc(longMode ? "SetLongTimeout(true)" : "SetLongTimeout(false)"); } internal static void OnGlobalCallbacksRegistered() { _steamUp = true; ApplySteam("RegisterGlobalCallbacks"); } internal static void OnConfigChanged() { if (_rpcSeamSeen) { ApplyRpc("config changed"); } if (_steamUp) { ApplySteam("config changed"); } } internal static void Reset() { _longMode = false; _lastLoggedRpc = null; _peers = new ConditionalWeakTable<ZRpc, PeerState>(); BackgroundHolds = 0L; HostInBackground = false; HostToldWait = false; HostStopsInBackground = false; _hostToldFlags = 0; _hostFocusRepeatAt = 0f; _hostWasBackground = false; _hostBackgroundSince = 0f; _hostLastTickAt = -1f; } private static void ApplyRpc(string reason) { float num = (_longMode ? 90f : 30f); _perPeer = Active; if (_perPeer) { InWorldTimeoutSeconds = Math.Max(ValConfig.ConnectionTimeoutSeconds.Value, _longMode ? num : 0f); JoiningTimeoutSeconds = Math.Max(ValConfig.LoadingTimeoutSeconds.Value, InWorldTimeoutSeconds); BackgroundTimeoutSeconds = (KeepInBackground ? Math.Max(ValConfig.BackgroundTimeoutSeconds.Value, JoiningTimeoutSeconds) : 0f); BackgroundUnlimited = KeepInBackground && ValConfig.EnableBackgroundTimeout != null && !ValConfig.EnableBackgroundTimeout.Value; } else { InWorldTimeoutSeconds = num; JoiningTimeoutSeconds = num; BackgroundTimeoutSeconds = 0f; BackgroundUnlimited = false; } float timeout = ZRpc.m_timeout; ZRpc.m_timeout = JoiningTimeoutSeconds; string text = (BackgroundUnlimited ? $", no limit while their game is in the background (Steam still drops a computer silent for {BackgroundTimeoutSeconds}s)" : ((BackgroundTimeoutSeconds > 0f) ? $", {BackgroundTimeoutSeconds}s while their game is in the background" : "")); string text2 = (_perPeer ? $"ZRpc ping timeout {timeout}s -> {JoiningTimeoutSeconds}s while joining, {InWorldTimeoutSeconds}s once in the world{text}" : $"ZRpc ping timeout {timeout}s -> {ZRpc.m_timeout}s (vanilla)"); if (_longMode) { text2 += " [crossplay]"; } if (!(text2 == _lastLoggedRpc)) { _lastLoggedRpc = text2; Logger.LogInfo("Connection timeout (" + reason + "): " + text2); } } private static void ApplySteam(string reason) { if (SteamNetConfig.Available) { int value = (Active ? (ValConfig.ConnectTimeoutSeconds.Value * 1000) : 10000); int value2 = (Active ? (Math.Max(Math.Max(Va