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 CoopParryFix v0.4.1
CoopParryFix.dll
Decompiled 3 weeks agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("CoopParryFix")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.4.1.0")] [assembly: AssemblyInformationalVersion("0.4.1+491f525efb549527f62b30ce2f55310b5860e971")] [assembly: AssemblyProduct("CoopParryFix")] [assembly: AssemblyTitle("CoopParryFix")] [assembly: AssemblyVersion("0.4.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace CoopParryFix { internal enum PlayerGrouping { Zone, WorldChunk, Radius } internal enum RttStatMode { Max, P90, Ema } internal static class ModConfig { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<bool> DebugLogs; internal static ConfigEntry<int> MillisecondsPerPlayer; internal static ConfigEntry<bool> IgnoreFirstPlayer; internal static ConfigEntry<int> MaxBonusMilliseconds; internal static ConfigEntry<PlayerGrouping> Grouping; internal static ConfigEntry<float> RadiusMeters; internal static ConfigEntry<bool> CompensateLatency; internal static ConfigEntry<float> PingScale; internal static ConfigEntry<float> JitterScale; internal static ConfigEntry<int> QualityHitchMilliseconds; internal static ConfigEntry<int> MaxLatencyBonusMilliseconds; internal static ConfigEntry<float> SampleIntervalSeconds; internal static ConfigEntry<float> WindowSeconds; internal static ConfigEntry<RttStatMode> RttStat; internal static ConfigEntry<int> HitchBudgetMilliseconds; internal static ConfigEntry<float> HitchScale; internal static ConfigEntry<int> MinRemoteOwnerMilliseconds; internal static ConfigEntry<float> PingTimeoutSeconds; internal static void Bind(ConfigFile config) { Enabled = config.Bind<bool>("General", "Enabled", true, "If off, the vanilla 250ms parry window is left unchanged."); DebugLogs = config.Bind<bool>("General", "Debug Logs", true, "Log every blocked hit: owner, ping, hitch, fallback, and the resulting parry window. Default on for new installs."); MillisecondsPerPlayer = config.Bind<int>("Parry", "Milliseconds Per Player", 0, "Extra ms per billed nearby player. Default 0 (vanilla solo timing). Set 50 only if you want crowd extra on top of latency."); IgnoreFirstPlayer = config.Bind<bool>("Parry", "Ignore First Player", true, "If on (default), you alone keep no crowd extra. Each additional player in the area adds Milliseconds Per Player."); MaxBonusMilliseconds = config.Bind<int>("Parry", "Max Bonus Milliseconds", 500, "Cap on extra parry time beyond the vanilla 250ms (crowd + latency combined). 0 = no cap."); Grouping = config.Bind<PlayerGrouping>("Area", "Grouping", PlayerGrouping.Zone, "Zone = Valheim 64m world zone (the usual 'chunk'). WorldChunk = 1.0 streaming chunk (8x8 zones, 512m). Radius = players within Radius Meters."); RadiusMeters = config.Bind<float>("Area", "Radius Meters", 64f, "Only used when Grouping is Radius."); CompensateLatency = config.Bind<bool>("Latency", "Compensate Latency", true, "When this client does not own the attacker, add measured owner-path delay so a vanilla-timed parry still counts. Locally owned attackers stay at 250ms."); PingScale = config.Bind<float>("Latency", "Ping Scale", 0.5f, "Fraction of owner-path RTT added to the window. 0.5 = combat one-way (you→server→owner→server→you, halved)."); JitterScale = config.Bind<float>("Latency", "Jitter Scale", 1f, "Multiplier on GetNetStats ping jitter. Only used when owner-path ping has no sample (missing mod / timeout)."); QualityHitchMilliseconds = config.Bind<int>("Latency", "Quality Hitch Milliseconds", 80, "GetNetStats quality extra (0 quality = this many ms). Only used when owner-path ping has no sample."); MaxLatencyBonusMilliseconds = config.Bind<int>("Latency", "Max Latency Bonus Milliseconds", 250, "Cap on the latency portion only. 0 = no extra cap beyond Max Bonus Milliseconds."); SampleIntervalSeconds = config.Bind<float>("Latency", "Sample Interval Seconds", 1f, "How often to send CPF_Ping to the server and to nearby/recent owners."); WindowSeconds = config.Bind<float>("Latency", "Window Seconds", 5f, "Seconds of ping/hitch samples kept per owner. Default 5."); RttStat = config.Bind<RttStatMode>("Latency", "Rtt Stat", RttStatMode.Max, "How to reduce RTTs in the window: Max (inclusive, default), P90, or Ema."); HitchBudgetMilliseconds = config.Bind<int>("Latency", "Hitch Budget Milliseconds", 33, "Owner frame time at or below this (default 33ms = 30 FPS) adds no hitch. Extra ms above this are added."); HitchScale = config.Bind<float>("Latency", "Hitch Scale", 1f, "Multiplier on owner hitch above Hitch Budget."); MinRemoteOwnerMilliseconds = config.Bind<int>("Latency", "Min Remote Owner Milliseconds", 40, "If the attacker is not local and no ping sample is in yet (GetNetStats 0, missing CoopParryFix on owner/server, timeout), add at least this many ms."); PingTimeoutSeconds = config.Bind<float>("Latency", "Ping Timeout Seconds", 3f, "If CPF_Pong never arrives, treat that peer as missing CoopParryFix and use fallback. Does not break parry."); } } internal static class NetStats { private const float EmaLerp = 0.2f; internal static int LastPingMs; internal static float LastLocalQuality = 1f; internal static float LastRemoteQuality = 1f; internal static float EmaPingMs; internal static float JitterMs; internal static void Sample() { ZNet instance = ZNet.instance; if (Object.op_Implicit((Object)(object)instance)) { float lastLocalQuality = default(float); float lastRemoteQuality = default(float); int val = default(int); float num = default(float); float num2 = default(float); instance.GetNetStats(ref lastLocalQuality, ref lastRemoteQuality, ref val, ref num, ref num2); LastPingMs = Math.Max(0, val); LastLocalQuality = lastLocalQuality; LastRemoteQuality = lastRemoteQuality; if (EmaPingMs <= 0f) { EmaPingMs = LastPingMs; } else { EmaPingMs = Mathf.Lerp(EmaPingMs, (float)LastPingMs, 0.2f); } JitterMs = Mathf.Lerp(JitterMs, Mathf.Abs((float)LastPingMs - EmaPingMs), 0.2f); } } } internal struct LatencyBonus { internal float Seconds; internal string Source; internal float RttLastMs; internal float RttStatMs; internal float HitchStatMs; internal long OwnerUid; } internal static class OwnerPing { private struct Sample { internal float Time; internal float RttMs; internal float FrameMs; } private struct Pending { internal long TargetUid; internal float SentAt; } private sealed class UidState { internal readonly List<Sample> Samples = new List<Sample>(); internal float LastRttMs; internal float LastHitchMs; internal float LastAttempt; internal int Timeouts; internal bool Missing; } internal const string RpcPing = "CPF_Ping"; internal const string RpcPong = "CPF_Pong"; private const int MaxPending = 32; private const int MaxSamplesPerUid = 64; private const float RecentAttackerSeconds = 30f; private const float EmaLerp = 0.2f; private const float MissingRetrySeconds = 15f; private static bool _registered; private static int _seq; private static float _sampleTimer; private static float _frameMsEma = 16f; private static readonly Dictionary<int, Pending> PendingPings = new Dictionary<int, Pending>(); private static readonly Dictionary<long, UidState> States = new Dictionary<long, UidState>(); private static readonly Dictionary<long, float> RecentOwners = new Dictionary<long, float>(); internal static void NoteAttacker(Character? attacker) { if (!Object.op_Implicit((Object)(object)attacker)) { return; } try { long owner = attacker.GetOwner(); if (owner != 0L) { RecentOwners[owner] = Time.realtimeSinceStartup; } } catch { } } internal static void Tick() { _frameMsEma = Mathf.Lerp(_frameMsEma, Time.unscaledDeltaTime * 1000f, 0.25f); if (!_registered) { TryRegister(); } PruneTimeouts(); PruneRecentOwners(); if (ModConfig.CompensateLatency.Value && Object.op_Implicit((Object)(object)Player.m_localPlayer)) { _sampleTimer += Time.unscaledDeltaTime; float num = Mathf.Max(0.25f, ModConfig.SampleIntervalSeconds.Value); if (!(_sampleTimer < num)) { _sampleTimer = 0f; SendSamplePings(); } } } internal static LatencyBonus GetBonus(Character? attacker) { LatencyBonus bonus = new LatencyBonus { Source = "none", Seconds = 0f }; if (!ModConfig.CompensateLatency.Value) { return bonus; } long num = 0L; if (Object.op_Implicit((Object)(object)attacker)) { try { num = attacker.GetOwner(); } catch { num = 0L; } } bonus.OwnerUid = num; NetStats.Sample(); bool flag = IsMissing(num); if (!flag && TryOwnerPath(num, ref bonus)) { return Cap(bonus); } long num2 = ServerUid(); bool flag2 = IsMissing(num2); if (num2 != 0L && num2 != num && !flag2 && TryOwnerPath(num2, ref bonus)) { bonus.Source = "server-" + StatName(); return Cap(bonus); } if (NetStats.LastPingMs > 0) { bonus.Source = ((flag || flag2) ? "missing-mod-netstats" : "netstats"); bonus.RttLastMs = NetStats.LastPingMs; bonus.RttStatMs = NetStats.EmaPingMs; float num3 = NetStats.EmaPingMs * ModConfig.PingScale.Value; float num4 = NetStats.JitterMs * ModConfig.JitterScale.Value; float num5 = Mathf.Clamp01(Mathf.Min(NetStats.LastLocalQuality, NetStats.LastRemoteQuality)); if (float.IsNaN(num5)) { num5 = 1f; } float num6 = (1f - num5) * (float)ModConfig.QualityHitchMilliseconds.Value; bonus.Seconds = Mathf.Max(0f, num3 + num4 + num6) / 1000f; return Cap(bonus); } int num7 = Math.Max(0, ModConfig.MinRemoteOwnerMilliseconds.Value); bonus.Source = ((flag || flag2) ? "missing-mod-floor" : "min-floor"); bonus.Seconds = (float)num7 / 1000f; return Cap(bonus); } internal static void TryRegister() { if (_registered) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { return; } try { instance.Register<int, long>("CPF_Ping", (Action<long, int, long>)OnPing); instance.Register<int, long, float>("CPF_Pong", (Action<long, int, long, float>)OnPong); _registered = true; Plugin.Log?.LogInfo("CoopParryFix routed RPCs 'CPF_Ping' / 'CPF_Pong' registered. Same DLL answers on dedicated and clients. Missing CoopParryFix on a peer times out and falls back."); } catch (Exception ex) { Plugin.Log?.LogWarning("CoopParryFix RPC register failed: " + ex.Message); } } internal static void OnRoutedRpcCreated() { _registered = false; PendingPings.Clear(); States.Clear(); TryRegister(); } private static void OnPing(long sender, int seq, long originMs) { try { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null && sender != 0L) { instance.InvokeRoutedRPC(sender, "CPF_Pong", new object[3] { seq, originMs, _frameMsEma }); } } catch (Exception ex) { Plugin.Log?.LogWarning("CoopParryFix CPF_Ping handler failed: " + ex.GetType().Name + ": " + ex.Message); } } private static void OnPong(long sender, int seq, long originMs, float ownerFrameMs) { try { if (PendingPings.TryGetValue(seq, out var value)) { PendingPings.Remove(seq); float num = NowMs() - originMs; if (num < 0f) { num = 0f; } if (float.IsNaN(ownerFrameMs) || ownerFrameMs < 0f) { ownerFrameMs = 0f; } UidState state = GetState(value.TargetUid); state.LastRttMs = num; state.LastHitchMs = ownerFrameMs; state.Timeouts = 0; state.Missing = false; state.Samples.Add(new Sample { Time = Time.realtimeSinceStartup, RttMs = num, FrameMs = ownerFrameMs }); Trim(state); if (ModConfig.DebugLogs.Value) { Plugin.Log?.LogInfo($"CPF_Pong from={sender} target={value.TargetUid} rtt={num:0}ms frame={ownerFrameMs:0}ms n={state.Samples.Count}"); } } } catch (Exception ex) { Plugin.Log?.LogWarning("CoopParryFix CPF_Pong handler failed: " + ex.GetType().Name + ": " + ex.Message); } } private static void SendSamplePings() { //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) if (!_registered || ZRoutedRpc.instance == null || !Object.op_Implicit((Object)(object)ZNet.instance)) { return; } long uID = ZNet.GetUID(); HashSet<long> hashSet = new HashSet<long>(); long num = ServerUid(); if (num != 0L && num != uID) { hashSet.Add(num); } Player localPlayer = Player.m_localPlayer; if (Object.op_Implicit((Object)(object)localPlayer)) { foreach (Player item in Player.GetAllPlayers() ?? new List<Player>()) { if (Object.op_Implicit((Object)(object)item) && !((Object)(object)item == (Object)(object)localPlayer) && SameArea(((Component)localPlayer).transform.position, ((Component)item).transform.position)) { long owner = ((Character)item).GetOwner(); if (owner != 0L && owner != uID) { hashSet.Add(owner); } } } } float realtimeSinceStartup = Time.realtimeSinceStartup; foreach (KeyValuePair<long, float> recentOwner in RecentOwners) { if (recentOwner.Key != 0L && recentOwner.Key != uID && realtimeSinceStartup - recentOwner.Value <= 30f) { hashSet.Add(recentOwner.Key); } } foreach (long item2 in hashSet) { SendPing(item2); } } private static void SendPing(long targetUid) { if (PendingPings.Count >= 32 || !Object.op_Implicit((Object)(object)ZNet.instance) || targetUid == 0L || targetUid == ZNet.GetUID()) { return; } UidState state = GetState(targetUid); float realtimeSinceStartup = Time.realtimeSinceStartup; if (state.Missing && realtimeSinceStartup - state.LastAttempt < 15f) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { return; } int num = ++_seq; long num2 = NowMs(); state.LastAttempt = realtimeSinceStartup; PendingPings[num] = new Pending { TargetUid = targetUid, SentAt = realtimeSinceStartup }; try { instance.InvokeRoutedRPC(targetUid, "CPF_Ping", new object[2] { num, num2 }); } catch (Exception ex) { PendingPings.Remove(num); state.Missing = true; if (ModConfig.DebugLogs.Value) { Plugin.Log?.LogInfo($"CPF_Ping to {targetUid} failed ({ex.GetType().Name}); will fall back."); } } } private static void PruneTimeouts() { float num = Mathf.Max(0.5f, ModConfig.PingTimeoutSeconds.Value); float realtimeSinceStartup = Time.realtimeSinceStartup; List<int> list = new List<int>(); foreach (KeyValuePair<int, Pending> pendingPing in PendingPings) { if (realtimeSinceStartup - pendingPing.Value.SentAt >= num) { list.Add(pendingPing.Key); } } foreach (int item in list) { Pending pending = PendingPings[item]; PendingPings.Remove(item); UidState state = GetState(pending.TargetUid); state.Timeouts++; bool flag = false; if (state.Timeouts >= 2 && !state.Missing) { state.Missing = true; flag = true; } if (ModConfig.DebugLogs.Value && (flag || !state.Missing)) { Plugin.Log?.LogInfo($"CPF_Ping timeout target={pending.TargetUid} timeouts={state.Timeouts} (peer may not have CoopParryFix); falling back."); } } } private static void PruneRecentOwners() { float realtimeSinceStartup = Time.realtimeSinceStartup; List<long> list = new List<long>(); foreach (KeyValuePair<long, float> recentOwner in RecentOwners) { if (realtimeSinceStartup - recentOwner.Value > 30f) { list.Add(recentOwner.Key); } } foreach (long item in list) { RecentOwners.Remove(item); } } private static bool IsMissing(long uid) { if (uid != 0L && States.TryGetValue(uid, out UidState value)) { return value.Missing; } return false; } private static bool TryOwnerPath(long uid, ref LatencyBonus bonus) { if (uid == 0L || !States.TryGetValue(uid, out UidState value)) { return false; } Trim(value); if (value.Samples.Count == 0) { return false; } bonus.OwnerUid = uid; bonus.RttLastMs = value.LastRttMs; bonus.RttStatMs = ReduceRtt(value); bonus.HitchStatMs = HitchStat(value); bonus.Source = "owner-" + StatName(); float num = ModConfig.PingScale.Value * bonus.RttStatMs + ModConfig.HitchScale.Value * bonus.HitchStatMs; if (float.IsNaN(num) || float.IsInfinity(num)) { num = 0f; } bonus.Seconds = Mathf.Max(0f, num) / 1000f; return true; } private static float ReduceRtt(UidState state) { List<float> list = new List<float>(state.Samples.Count); foreach (Sample sample in state.Samples) { list.Add(sample.RttMs); } if (list.Count == 0) { return 0f; } switch (ModConfig.RttStat.Value) { case RttStatMode.P90: { list.Sort(); int index = Mathf.Clamp(Mathf.RoundToInt((float)(list.Count - 1) * 0.9f), 0, list.Count - 1); return list[index]; } case RttStatMode.Ema: { float num2 = list[0]; for (int j = 1; j < list.Count; j++) { num2 = Mathf.Lerp(num2, list[j], 0.2f); } return num2; } default: { float num = list[0]; for (int i = 1; i < list.Count; i++) { num = Mathf.Max(num, list[i]); } return num; } } } private static float HitchStat(UidState state) { float num = 0f; foreach (Sample sample in state.Samples) { num = Mathf.Max(num, sample.FrameMs); } return Mathf.Max(0f, num - (float)ModConfig.HitchBudgetMilliseconds.Value); } private static void Trim(UidState state) { float num = Mathf.Max(1f, ModConfig.WindowSeconds.Value); float cutoff = Time.realtimeSinceStartup - num; state.Samples.RemoveAll((Sample s) => s.Time < cutoff); if (state.Samples.Count > 64) { state.Samples.RemoveRange(0, state.Samples.Count - 64); } } private static LatencyBonus Cap(LatencyBonus bonus) { int value = ModConfig.MaxLatencyBonusMilliseconds.Value; if (value > 0) { bonus.Seconds = Mathf.Min(bonus.Seconds, (float)value / 1000f); } return bonus; } private static string StatName() { return ModConfig.RttStat.Value.ToString().ToLowerInvariant(); } private static long ServerUid() { try { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { return 0L; } return instance.GetServerPeerID(); } catch { return 0L; } } private static bool SameArea(Vector3 a, Vector3 b) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) switch (ModConfig.Grouping.Value) { case PlayerGrouping.WorldChunk: { ChunkIndex zonesChunk = ZoneSystem.GetZonesChunk(ZoneSystem.GetSectorIndex(a)); return ((ChunkIndex)(ref zonesChunk)).Equals(ZoneSystem.GetZonesChunk(ZoneSystem.GetSectorIndex(b))); } case PlayerGrouping.Radius: { float value = ModConfig.RadiusMeters.Value; Vector3 val = a - b; return ((Vector3)(ref val)).sqrMagnitude <= value * value; } default: { Vector2s zone = ZoneSystem.GetZone(a); return ((Vector2s)(ref zone)).Equals(ZoneSystem.GetZone(b)); } } } private static UidState GetState(long uid) { if (!States.TryGetValue(uid, out UidState value)) { value = new UidState(); States[uid] = value; } return value; } private static long NowMs() { return (long)(Time.realtimeSinceStartup * 1000f); } } [HarmonyPatch(/*Could not decode attribute arguments.*/)] internal static class OwnerPingRpcCtorPatch { private static void Postfix() { try { OwnerPing.OnRoutedRpcCreated(); } catch { } } } internal static class ParryWindow { internal const float VanillaSeconds = 0.25f; internal const float MaxWindowSeconds = 1f; internal static float GetSeconds(Humanoid blocker, Character attacker) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) try { if (!ModConfig.Enabled.Value) { return 0.25f; } if (!Object.op_Implicit((Object)(object)blocker) || !((Character)blocker).IsPlayer()) { return 0.25f; } OwnerPing.NoteAttacker(attacker); int num = CountPlayersAt(((Component)blocker).transform.position); int num2 = (ModConfig.IgnoreFirstPlayer.Value ? Math.Max(0, num - 1) : Math.Max(0, num)); float num3 = (float)num2 * ((float)ModConfig.MillisecondsPerPlayer.Value / 1000f); bool flag = Object.op_Implicit((Object)(object)attacker) && attacker.IsOwner(); LatencyBonus latencyBonus; if (flag) { latencyBonus = new LatencyBonus { Source = "local-owner", Seconds = 0f }; try { latencyBonus.OwnerUid = attacker.GetOwner(); } catch { } } else { latencyBonus = OwnerPing.GetBonus(attacker); } float num4 = num3 + latencyBonus.Seconds; int value = ModConfig.MaxBonusMilliseconds.Value; if (value > 0) { num4 = Mathf.Min(num4, (float)value / 1000f); } float num5 = 0.25f + num4; if (float.IsNaN(num5) || float.IsInfinity(num5) || num5 < 0.25f) { num5 = 0.25f; } num5 = Mathf.Min(num5, 1f); if (ModConfig.DebugLogs.Value) { string text = (Object.op_Implicit((Object)(object)attacker) ? ((Object)attacker).name : "null"); Plugin.Log.LogInfo($"parry window {num5 * 1000f:0}ms | attacker={text} localOwner={flag} ownerUid={latencyBonus.OwnerUid} source={latencyBonus.Source} rttLast={latencyBonus.RttLastMs:0} rttStat={latencyBonus.RttStatMs:0} hitch={latencyBonus.HitchStatMs:0} | players={num} billed={num2} crowd={num3 * 1000f:0}ms netstats={NetStats.LastPingMs}ms latency={latencyBonus.Seconds * 1000f:0}ms"); } return num5; } catch (Exception arg) { Plugin.Log.LogError($"CoopParryFix GetSeconds failed: {arg}"); return 0.25f; } } internal static int CountPlayersAt(Vector3 origin) { //IL_0022: 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_0033: 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_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) List<Player> allPlayers = Player.GetAllPlayers(); if (allPlayers == null || allPlayers.Count == 0) { return 1; } PlayerGrouping value = ModConfig.Grouping.Value; int num = 0; Vector2s val = default(Vector2s); ChunkIndex val2 = default(ChunkIndex); switch (value) { case PlayerGrouping.Zone: val = ZoneSystem.GetZone(origin); break; case PlayerGrouping.WorldChunk: val2 = ZoneSystem.GetZonesChunk(ZoneSystem.GetSectorIndex(origin)); break; } float value2 = ModConfig.RadiusMeters.Value; float num2 = value2 * value2; foreach (Player item in allPlayers) { if (Object.op_Implicit((Object)(object)item)) { Vector3 position = ((Component)item).transform.position; bool flag; switch (value) { case PlayerGrouping.WorldChunk: { ChunkIndex zonesChunk = ZoneSystem.GetZonesChunk(ZoneSystem.GetSectorIndex(position)); flag = ((ChunkIndex)(ref zonesChunk)).Equals(val2); break; } case PlayerGrouping.Radius: { Vector3 val3 = position - origin; flag = ((Vector3)(ref val3)).sqrMagnitude <= num2; break; } default: { Vector2s zone = ZoneSystem.GetZone(position); flag = ((Vector2s)(ref zone)).Equals(val); break; } } if (flag) { num++; } } } return Math.Max(1, num); } } [HarmonyPatch(typeof(Humanoid), "BlockAttack")] internal static class BlockAttackPatch { private static readonly MethodInfo GetWindow = AccessTools.Method(typeof(ParryWindow), "GetSeconds", (Type[])null, (Type[])null) ?? throw new InvalidOperationException("CoopParryFix could not find ParryWindow.GetSeconds"); private static void Prefix(Humanoid __instance, Character attacker) { if (!ModConfig.DebugLogs.Value || !Object.op_Implicit((Object)(object)__instance)) { return; } try { bool flag = Object.op_Implicit((Object)(object)attacker) && attacker.IsOwner(); string text = (Object.op_Implicit((Object)(object)attacker) ? ((Object)attacker).name : "null"); Plugin.Log.LogInfo($"BlockAttack {((Object)__instance).name} isPlayer={((Character)__instance).IsPlayer()} blocking={((Character)__instance).IsBlocking()} attacker={text} localOwner={flag}"); } catch (Exception arg) { Plugin.Log.LogError($"CoopParryFix BlockAttack prefix failed: {arg}"); } } private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { bool replaced = false; foreach (CodeInstruction instruction in instructions) { if (!replaced && instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float num && Mathf.Abs(num - 0.25f) < 0.0001f) { replaced = true; yield return new CodeInstruction(OpCodes.Ldarg_0, (object)null); yield return new CodeInstruction(OpCodes.Ldarg_2, (object)null); yield return new CodeInstruction(OpCodes.Call, (object)GetWindow); } else { yield return instruction; } } if (!replaced) { Plugin.Log.LogError("CoopParryFix: Humanoid.BlockAttack no longer has the 0.25s parry window constant. The patch did not apply."); } else { Plugin.Log.LogInfo("CoopParryFix: hooked Humanoid.BlockAttack parry window."); } } } [BepInPlugin("Ageous.CoopParryFix", "CoopParryFix", "0.4.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string ModGUID = "Ageous.CoopParryFix"; public const string ModName = "CoopParryFix"; public const string ModVersion = "0.4.1"; public const string SmoothServerGUID = "Nosferatu.SmoothServer"; internal static Plugin Instance; internal static PluginLog Log; internal static Harmony Harmony; private void Awake() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown Instance = this; Log = new PluginLog(((BaseUnityPlugin)this).Logger, PluginLogPath()); ModConfig.Bind(((BaseUnityPlugin)this).Config); Harmony = new Harmony("Ageous.CoopParryFix"); Harmony.PatchAll(); LogSmoothServerCompat(); string text = (Application.isBatchMode ? "dedicated server" : "client"); Log.LogInfo("CoopParryFix 0.4.1 loaded (" + text + ", same DLL). Vanilla 250ms when you own the attacker. Otherwise CPF_Ping to the owner (install on server and all clients for best results; missing peers fall back)."); } private static void LogSmoothServerCompat() { try { if (Chainloader.PluginInfos != null && Chainloader.PluginInfos.ContainsKey("Nosferatu.SmoothServer")) { Log.LogInfo("SmoothServer detected: CoopParryFix does not use SS_Ping. Owner-path delay uses CPF_Ping/CPF_Pong."); } } catch (Exception ex) { Log.LogWarning("SmoothServer detect failed: " + ex.Message); } } private void Update() { try { OwnerPing.Tick(); } catch (Exception arg) { Log.LogError($"CoopParryFix Update failed: {arg}"); } } private void OnDestroy() { Harmony harmony = Harmony; if (harmony != null) { harmony.UnpatchSelf(); } Log?.Close(); } private string PluginLogPath() { try { string directoryName = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location); if (!string.IsNullOrEmpty(directoryName)) { return Path.Combine(directoryName, "CoopParryFix.log"); } } catch { } return Path.Combine(Paths.PluginPath, "Ageous27-CoopParryFix", "CoopParryFix.log"); } } internal sealed class PluginLog { private readonly ManualLogSource _bepInEx; private readonly StreamWriter? _file; private readonly object _gate = new object(); internal PluginLog(ManualLogSource bepInEx, string filePath) { _bepInEx = bepInEx; try { string directoryName = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } _file = new StreamWriter(filePath, append: false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true }; WriteLine("Info", "CoopParryFix log " + filePath); } catch (Exception ex) { bepInEx.LogWarning((object)("CoopParryFix could not open plugin log file: " + ex.Message)); } } internal void LogInfo(object data) { _bepInEx.LogInfo(data); WriteLine("Info", data); } internal void LogWarning(object data) { _bepInEx.LogWarning(data); WriteLine("Warning", data); } internal void LogError(object data) { _bepInEx.LogError(data); WriteLine("Error", data); } internal void Close() { lock (_gate) { try { _file?.Dispose(); } catch { } } } private void WriteLine(string level, object data) { if (_file == null) { return; } try { lock (_gate) { _file.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level,-7}] {data}"); } } catch { } } } }