Decompiled source of CoopParryFix v0.4.1

CoopParryFix.dll

Decompiled 3 weeks ago
using 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
			{
			}
		}
	}
}