using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Steamworks; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("TestHarness")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("kou_fixdesync")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("kou_fixdesync")] [assembly: AssemblyTitle("kou_fixdesync")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace KouFixDesync { [BepInPlugin("kou.fixdesync", "kou_fixdesync", "1.0.0")] public class Plugin : BaseUnityPlugin { public const string GUID = "kou.fixdesync"; public const string NAME = "kou_fixdesync"; public const string VERSION = "1.0.0"; internal static Plugin Instance; internal static ManualLogSource Log; internal ConfigEntry CfgWindowEnabled; internal ConfigEntry CfgWindowBytes; internal ConfigEntry CfgMinWindowBytes; internal ConfigEntry CfgAdaptiveWindow; internal ConfigEntry CfgTargetRateKBps; internal ConfigEntry CfgCadenceEnabled; internal ConfigEntry CfgSendIntervalMs; internal ConfigEntry CfgSteamRatesEnabled; internal ConfigEntry CfgRateMinKBps; internal ConfigEntry CfgRateMaxKBps; internal ConfigEntry CfgDistantSyncEnabled; internal ConfigEntry CfgDistantThreshold; internal ConfigEntry CfgOwnershipEnabled; internal ConfigEntry CfgOwnershipPingCeiling; internal ConfigEntry CfgOwnershipPingFloor; internal ConfigEntry CfgOwnershipCooldownSec; private void Awake() { //IL_0247: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log = ((BaseUnityPlugin)this).Logger; CfgWindowEnabled = ((BaseUnityPlugin)this).Config.Bind("1.SendWindow", "Enabled", true, "Enlarge the per-peer ZDO send window (vanilla: 10240 bytes incl. unacked data - the main desync cause)."); CfgWindowBytes = ((BaseUnityPlugin)this).Config.Bind("1.SendWindow", "WindowBytes", 61440, "Send window size in bytes. Hard-clamped to 10240-262144 (Steamworks drops single messages >512KB)."); CfgMinWindowBytes = ((BaseUnityPlugin)this).Config.Bind("1.SendWindow", "MinWindowBytes", 2048, "Minimum free window required before sending a batch (vanilla: 2048). Clamped 1024-8192."); CfgAdaptiveWindow = ((BaseUnityPlugin)this).Config.Bind("1.SendWindow", "PingAdaptive", true, "Scale each peer's window with its measured round-trip time (window = TargetRate x RTT). High-ping players get bigger windows automatically."); CfgTargetRateKBps = ((BaseUnityPlugin)this).Config.Bind("1.SendWindow", "TargetRateKBps", 300, "Target per-peer send rate in KB/s used for the ping-adaptive window. Clamped 60-2048."); CfgCadenceEnabled = ((BaseUnityPlugin)this).Config.Bind("2.SendCadence", "Enabled", true, "Service ALL peers every send tick instead of vanilla's one-peer-per-frame round robin."); CfgSendIntervalMs = ((BaseUnityPlugin)this).Config.Bind("2.SendCadence", "SendIntervalMs", 50, "Send tick interval in milliseconds (vanilla: 50). Clamped 20-200."); CfgSteamRatesEnabled = ((BaseUnityPlugin)this).Config.Bind("3.SteamRates", "Enabled", true, "Raise Steam networking send rate limits (vanilla pins min=max=150 KB/s)."); CfgRateMinKBps = ((BaseUnityPlugin)this).Config.Bind("3.SteamRates", "SendRateMinKBps", 256, "Steam SendRateMin in KB/s. Clamped 150-4096."); CfgRateMaxKBps = ((BaseUnityPlugin)this).Config.Bind("3.SteamRates", "SendRateMaxKBps", 1024, "Steam SendRateMax in KB/s. Keeping max > min lets Steam's congestion control work. Clamped to >= min, <= 8192."); CfgDistantSyncEnabled = ((BaseUnityPlugin)this).Config.Bind("4.DistantSync", "Enabled", true, "Sync distant-zone objects when fewer than DistantThreshold near objects are queued (vanilla threshold: 10)."); CfgDistantThreshold = ((BaseUnityPlugin)this).Config.Bind("4.DistantSync", "DistantThreshold", 30, "Queue-size threshold below which distant objects are added to the sync list. Clamped 10-200."); CfgOwnershipEnabled = ((BaseUnityPlugin)this).Config.Bind("5.SmartOwnership", "Enabled", true, "SERVER ONLY: assign mob/object simulation ownership to the lowest-ping player covering the area, and migrate it off high-ping players."); CfgOwnershipPingCeiling = ((BaseUnityPlugin)this).Config.Bind("5.SmartOwnership", "PingCeilingMs", 150, "Migrate objects away from an owner whose ping exceeds this (ms)."); CfgOwnershipPingFloor = ((BaseUnityPlugin)this).Config.Bind("5.SmartOwnership", "PingFloorMs", 80, "Only migrate when a candidate with ping below this (ms) also covers the area."); CfgOwnershipCooldownSec = ((BaseUnityPlugin)this).Config.Bind("5.SmartOwnership", "CooldownSec", 10, "Per-object cooldown between ownership migrations, to prevent flapping."); Settings.ApplyLocal(); new Harmony("kou.fixdesync").PatchAll(); Log.LogInfo((object)"kou_fixdesync 1.0.0 loaded, patches applied"); } } internal static class Settings { public const int PkgVersion = 1; public const int AbsoluteMaxWindow = 262144; public static bool WindowEnabled = true; public static int WindowBytes = 61440; public static int MinWindowBytes = 2048; public static bool AdaptiveWindow = true; public static int TargetRateKBps = 300; public static bool CadenceEnabled = true; public static int SendIntervalMs = 50; public static bool SteamRatesEnabled = true; public static int RateMinKBps = 256; public static int RateMaxKBps = 1024; public static bool DistantSyncEnabled = true; public static int DistantThreshold = 30; public static bool OwnershipEnabled = true; public static int OwnershipPingCeiling = 150; public static int OwnershipPingFloor = 80; public static int OwnershipCooldownSec = 10; public static void ApplyLocal() { Plugin instance = Plugin.Instance; WindowEnabled = instance.CfgWindowEnabled.Value; WindowBytes = Clamp(instance.CfgWindowBytes.Value, 10240, 262144); MinWindowBytes = Clamp(instance.CfgMinWindowBytes.Value, 1024, 8192); AdaptiveWindow = instance.CfgAdaptiveWindow.Value; TargetRateKBps = Clamp(instance.CfgTargetRateKBps.Value, 60, 2048); CadenceEnabled = instance.CfgCadenceEnabled.Value; SendIntervalMs = Clamp(instance.CfgSendIntervalMs.Value, 20, 200); SteamRatesEnabled = instance.CfgSteamRatesEnabled.Value; RateMinKBps = Clamp(instance.CfgRateMinKBps.Value, 150, 4096); RateMaxKBps = Clamp(instance.CfgRateMaxKBps.Value, RateMinKBps, 8192); DistantSyncEnabled = instance.CfgDistantSyncEnabled.Value; DistantThreshold = Clamp(instance.CfgDistantThreshold.Value, 10, 200); OwnershipEnabled = instance.CfgOwnershipEnabled.Value; OwnershipPingCeiling = Clamp(instance.CfgOwnershipPingCeiling.Value, 50, 1000); OwnershipPingFloor = Clamp(instance.CfgOwnershipPingFloor.Value, 10, OwnershipPingCeiling); OwnershipCooldownSec = Clamp(instance.CfgOwnershipCooldownSec.Value, 1, 300); ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"kou_fixdesync: local config applied"); } } public static void WritePackage(ZPackage pkg) { pkg.Write(1); pkg.Write(WindowEnabled); pkg.Write(WindowBytes); pkg.Write(MinWindowBytes); pkg.Write(AdaptiveWindow); pkg.Write(TargetRateKBps); pkg.Write(CadenceEnabled); pkg.Write(SendIntervalMs); pkg.Write(SteamRatesEnabled); pkg.Write(RateMinKBps); pkg.Write(RateMaxKBps); pkg.Write(DistantSyncEnabled); pkg.Write(DistantThreshold); } public static void ReadPackage(ZPackage pkg) { int num = pkg.ReadInt(); if (num != 1) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)$"kou_fixdesync: server config version {num} != {1}, keeping local values"); } return; } WindowEnabled = pkg.ReadBool(); WindowBytes = Clamp(pkg.ReadInt(), 10240, 262144); MinWindowBytes = Clamp(pkg.ReadInt(), 1024, 8192); AdaptiveWindow = pkg.ReadBool(); TargetRateKBps = Clamp(pkg.ReadInt(), 60, 2048); CadenceEnabled = pkg.ReadBool(); SendIntervalMs = Clamp(pkg.ReadInt(), 20, 200); SteamRatesEnabled = pkg.ReadBool(); RateMinKBps = Clamp(pkg.ReadInt(), 150, 4096); RateMaxKBps = Clamp(pkg.ReadInt(), RateMinKBps, 8192); DistantSyncEnabled = pkg.ReadBool(); DistantThreshold = Clamp(pkg.ReadInt(), 10, 200); ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)($"kou_fixdesync: server-dictated config applied (window={WindowBytes} adaptive={AdaptiveWindow} " + $"cadence={CadenceEnabled}/{SendIntervalMs}ms rates={RateMinKBps}-{RateMaxKBps}KB/s distant={DistantThreshold})")); } } private static int Clamp(int v, int min, int max) { return Mathf.Clamp(v, min, max); } } } namespace KouFixDesync.Patches { internal static class ConfigSyncPatch { [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class OnNewConnectionPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown peer.m_rpc.Register("KouFixDesync_Cfg", (Action)OnConfigReceived); if (__instance.IsServer()) { ZPackage val = new ZPackage(); Settings.WritePackage(val); peer.m_rpc.Invoke("KouFixDesync_Cfg", new object[1] { val }); } } } [HarmonyPatch(typeof(ZNet), "Awake")] private static class AwakePatch { [HarmonyPostfix] private static void Postfix() { Settings.ApplyLocal(); } } private const string RpcName = "KouFixDesync_Cfg"; private static void OnConfigReceived(ZRpc rpc, ZPackage pkg) { if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer()) { Settings.ReadPackage(pkg); SteamRatePatch.Apply(); } } } [HarmonyPatch(typeof(ZDOMan), "CreateSyncList")] internal static class DistantSyncPatch { [HarmonyTranspiler] private static IEnumerable Transpiler(IEnumerable instructions) { int replaced = 0; foreach (CodeInstruction instruction in instructions) { if (replaced == 0 && instruction.opcode == OpCodes.Ldc_I4_S && Convert.ToInt32(instruction.operand) == 10) { replaced++; yield return new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(DistantSyncPatch), "GetThreshold", (Type[])null, (Type[])null)) { labels = instruction.labels, blocks = instruction.blocks }; } else { yield return instruction; } } if (replaced != 1) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"kou_fixdesync: CreateSyncList transpiler matched {replaced} (expected 1) - game update may have changed the method"); } } } public static int GetThreshold() { if (!Settings.DistantSyncEnabled) { return 10; } return Settings.DistantThreshold; } } [HarmonyPatch(typeof(ZDOMan), "ReleaseNearbyZDOS")] internal static class OwnershipPatch { private static readonly List _tempObjects = new List(); private static readonly Dictionary _lastMigration = new Dictionary(); private static float _lastCacheClean; [HarmonyPrefix] private static bool Prefix(ZDOMan __instance, Vector3 refPosition, long uid) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) if (!Settings.OwnershipEnabled || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return true; } Vector2i zone = ZoneSystem.GetZone(refPosition); _tempObjects.Clear(); __instance.FindSectorObjects(zone, ZoneSystem.instance.m_activeArea, 0, _tempObjects, (List)null); int num = ZoneSystem.instance.m_activeArea - 1; List peers = ZNet.instance.GetPeers(); float time = Time.time; CleanMigrationCache(time); foreach (ZDO tempObject in _tempObjects) { if (!tempObject.Persistent) { continue; } Vector2i sector = tempObject.GetSector(); long owner = tempObject.GetOwner(); if (owner == uid) { if (!ZNetScene.InActiveArea(sector, zone, num)) { tempObject.SetOwner(0L); } else if (uid != ZDOMan.GetSessionID()) { TryMigrate(tempObject, sector, owner, peers, time); } } else if ((!tempObject.HasOwner() || !IsInPeerActiveArea(sector, owner)) && ZNetScene.InActiveArea(sector, zone, num)) { tempObject.SetOwner(BestOwnerFor(sector, uid, peers)); } } return false; } private static long BestOwnerFor(Vector2i sector, long fallbackUid, List peers) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) long result = fallbackUid; int num = int.MaxValue; foreach (ZNetPeer peer in peers) { if (ZNetScene.InActiveArea(sector, peer.GetRefPos())) { int pingMs = GetPingMs(peer); if (pingMs < num) { num = pingMs; result = peer.m_uid; } } } return result; } private static void TryMigrate(ZDO zdo, Vector2i sector, long owner, List peers, float now) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) ZNetPeer peer = ZNet.instance.GetPeer(owner); if (peer == null || GetPingMs(peer) <= Settings.OwnershipPingCeiling || (_lastMigration.TryGetValue(zdo.m_uid, out var value) && now - value < (float)Settings.OwnershipCooldownSec)) { return; } ZNetPeer val = null; int num = int.MaxValue; foreach (ZNetPeer peer2 in peers) { if (peer2.m_uid != owner && ZNetScene.InActiveArea(sector, peer2.GetRefPos())) { int pingMs = GetPingMs(peer2); if (pingMs < num) { num = pingMs; val = peer2; } } } if (val != null && num < Settings.OwnershipPingFloor) { zdo.SetOwner(val.m_uid); _lastMigration[zdo.m_uid] = now; } } private static bool IsInPeerActiveArea(Vector2i sector, long uid) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) if (uid == ZDOMan.GetSessionID()) { return ZNetScene.InActiveArea(sector, ZNet.instance.GetReferencePosition()); } ZNetPeer peer = ZNet.instance.GetPeer(uid); if (peer == null) { return false; } return ZNetScene.InActiveArea(sector, peer.GetRefPos()); } private static int GetPingMs(ZNetPeer peer) { if (peer.m_socket == null) { return int.MaxValue; } float num = default(float); float num2 = default(float); int result = default(int); float num3 = default(float); float num4 = default(float); peer.m_socket.GetConnectionQuality(ref num, ref num2, ref result, ref num3, ref num4); return result; } private static void CleanMigrationCache(float now) { if (!(now - _lastCacheClean < 60f) && _lastMigration.Count >= 512) { _lastCacheClean = now; _lastMigration.Clear(); } } } [HarmonyPatch(typeof(ZDOMan), "SendZDOToPeers2")] internal static class SendCadencePatch { private static readonly FieldInfo PeersField = AccessTools.Field(typeof(ZDOMan), "m_peers"); private static readonly FieldInfo NextSendPeerField = AccessTools.Field(typeof(ZDOMan), "m_nextSendPeer"); private static readonly MethodInfo SendZDOsMethod = AccessTools.Method(typeof(ZDOMan), "SendZDOs", (Type[])null, (Type[])null); private static float _timer; private static readonly object[] _args = new object[2]; [HarmonyPrefix] private static bool Prefix(ZDOMan __instance, float dt) { if (!Settings.CadenceEnabled) { return true; } IList list = (IList)PeersField.GetValue(__instance); if (list == null || list.Count == 0) { return false; } _timer += dt; if (_timer < (float)Settings.SendIntervalMs / 1000f) { return false; } _timer = 0f; _args[1] = false; for (int i = 0; i < list.Count; i++) { _args[0] = list[i]; SendZDOsMethod.Invoke(__instance, _args); } NextSendPeerField.SetValue(__instance, -1); return false; } } [HarmonyPatch(typeof(ZDOMan), "SendZDOs")] internal static class SendWindowPatch { private struct PingCache { public float Time; public int Ping; } private static FieldInfo _zdoPeerPeerField; private static readonly Dictionary _pingCache = new Dictionary(); [HarmonyTranspiler] private static IEnumerable Transpiler(IEnumerable instructions) { int windows = 0; int mins = 0; foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_I4 && instruction.operand is int num && num == 10240) { windows++; yield return new CodeInstruction(OpCodes.Ldarg_1, (object)null) { labels = instruction.labels, blocks = instruction.blocks }; yield return CodeInstruction.Call(typeof(SendWindowPatch), "GetWindow", (Type[])null, (Type[])null); } else if (instruction.opcode == OpCodes.Ldc_I4 && instruction.operand is int num2 && num2 == 2048) { mins++; yield return new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(SendWindowPatch), "GetMinWindow", (Type[])null, (Type[])null)) { labels = instruction.labels, blocks = instruction.blocks }; } else { yield return instruction; } } if (windows != 2 || mins != 1) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"kou_fixdesync: SendZDOs transpiler matched {windows}x10240 (expected 2), {mins}x2048 (expected 1) - game update may have changed the method"); } } } public static int GetWindow(object zdoPeer) { if (!Settings.WindowEnabled) { return 10240; } int num = Settings.WindowBytes; if (Settings.AdaptiveWindow) { int peerPingMs = GetPeerPingMs(zdoPeer); if (peerPingMs > 0) { int num2 = (int)((long)Settings.TargetRateKBps * 1024L * peerPingMs / 1000); if (num2 > num) { num = num2; } } } return Mathf.Min(num, 262144); } public static int GetMinWindow() { if (!Settings.WindowEnabled) { return 2048; } return Settings.MinWindowBytes; } private static int GetPeerPingMs(object zdoPeer) { if (zdoPeer == null) { return 0; } float time = Time.time; if (_pingCache.TryGetValue(zdoPeer, out var value) && time - value.Time < 1f) { return value.Ping; } int num = 0; if (_zdoPeerPeerField == null) { _zdoPeerPeerField = AccessTools.Field(zdoPeer.GetType(), "m_peer"); } object? obj = _zdoPeerPeerField?.GetValue(zdoPeer); ZNetPeer val = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (val != null && val.m_socket != null) { float num2 = default(float); float num3 = default(float); float num4 = default(float); float num5 = default(float); val.m_socket.GetConnectionQuality(ref num2, ref num3, ref num, ref num4, ref num5); } if (_pingCache.Count > 64) { _pingCache.Clear(); } _pingCache[zdoPeer] = new PingCache { Time = time, Ping = num }; return num; } } [HarmonyPatch(typeof(ZSteamSocket), "RegisterGlobalCallbacks")] internal static class SteamRatePatch { [HarmonyPostfix] private static void Postfix() { Apply(); } public static void Apply() { if (!Settings.SteamRatesEnabled) { return; } try { SetGlobalInt((ESteamNetworkingConfigValue)10, Settings.RateMinKBps * 1024); SetGlobalInt((ESteamNetworkingConfigValue)11, Settings.RateMaxKBps * 1024); ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)$"kou_fixdesync: Steam send rate set to {Settings.RateMinKBps}-{Settings.RateMaxKBps} KB/s"); } } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("kou_fixdesync: could not set Steam send rates: " + ex.Message)); } } } private static void SetGlobalInt(ESteamNetworkingConfigValue key, int value) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) GCHandle gCHandle = GCHandle.Alloc(value, GCHandleType.Pinned); try { SteamNetworkingUtils.SetConfigValue(key, (ESteamNetworkingConfigScope)1, IntPtr.Zero, (ESteamNetworkingConfigDataType)1, gCHandle.AddrOfPinnedObject()); } finally { gCHandle.Free(); } } } }