Decompiled source of EnhancedCoilhead v0.2.0

EnhancedCoilhead.dll

Decompiled 4 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using GameNetcodeStuff;
using HarmonyLib;
using LethalNetworkAPI;
using LethalNetworkAPI.Utils;
using Microsoft.CodeAnalysis;
using Unity.Netcode;
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("EnhancedCoilhead")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+67d8b0466acaa8315bdcd0f44e133d852aaa9a3a")]
[assembly: AssemblyProduct("EnhancedCoilhead")]
[assembly: AssemblyTitle("EnhancedCoilhead")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.0.0")]
[module: UnverifiableCode]
[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 EnhancedCoilhead
{
	[HarmonyPatch(typeof(SpringManAI), "DoAIInterval")]
	internal static class Brain_Interval_Patch
	{
		[HarmonyPostfix]
		private static void Postfix(SpringManAI __instance)
		{
			CoilBrain.Get(__instance).OnInterval(__instance);
		}
	}
	[HarmonyPatch(typeof(SpringManAI), "Update")]
	internal static class Brain_Frame_Patch
	{
		[HarmonyPostfix]
		private static void Postfix(SpringManAI __instance)
		{
			CoilBrain.Get(__instance).OnFrame(__instance);
		}
	}
	[HarmonyPatch(typeof(EnemyAI), "DetectNoise")]
	internal static class DetectNoise_Patch
	{
		[HarmonyPostfix]
		private static void Postfix(EnemyAI __instance, Vector3 noisePosition, float noiseLoudness, int noiseID)
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			SpringManAI val = (SpringManAI)(object)((__instance is SpringManAI) ? __instance : null);
			if (val != null)
			{
				if (((NetworkBehaviour)val).IsServer)
				{
					CoilBrain.Get(val).Hear(val, noisePosition, noiseLoudness, noiseID, viaClient: false);
				}
				else if (NetSync.ShouldRelay(noiseID, noiseLoudness))
				{
					NetSync.RelayNoise(val, noisePosition, noiseLoudness, noiseID);
				}
			}
		}
	}
	[HarmonyPatch(typeof(EnemyAI), "TargetClosestPlayer")]
	internal static class TargetClosest_Patch
	{
		[HarmonyPostfix]
		private static void Postfix(EnemyAI __instance, bool __result)
		{
			if (__result)
			{
				SpringManAI val = (SpringManAI)(object)((__instance is SpringManAI) ? __instance : null);
				if (val != null)
				{
					CoilBrain.Get(val).ReTarget(val);
				}
			}
		}
	}
	internal sealed class CoilBrain
	{
		private enum Mode
		{
			Patrol,
			Investigate
		}

		private static readonly ConditionalWeakTable<SpringManAI, CoilBrain> Table = new ConditionalWeakTable<SpringManAI, CoilBrain>();

		private const float ArriveDistance = 3.5f;

		private const float WaypointTimeout = 25f;

		private const float VisitRadius = 8f;

		private Mode _mode;

		private bool _hasGoal;

		private Vector3 _goal;

		private float _goalDeadline;

		private float _goalPriority;

		private bool _hijacked;

		private int _prevState;

		private bool _hadTarget;

		private Vector3 _lastSeenPos;

		private float _nextSample;

		private readonly Dictionary<int, float> _visited = new Dictionary<int, float>();

		private PlayerControllerB _currentTarget;

		private float _lockUntil;

		private ulong _lastOwner = ulong.MaxValue;

		private float _lastRetarget = -999f;

		private float _chaseStartedAt = -999f;

		private float _lastIntervalTime = -1f;

		private readonly List<AudioSource> _srcs = new List<AudioSource>();

		private readonly List<float> _baseVol = new List<float>();

		private readonly List<float> _baseMin = new List<float>();

		private readonly List<float> _baseMax = new List<float>();

		private readonly HashSet<int> _seenSrc = new HashSet<int>();

		private float _nextAudioScan;

		public static CoilBrain Get(SpringManAI c)
		{
			return Table.GetValue(c, (SpringManAI _) => new CoilBrain());
		}

		public void OnFrame(SpringManAI c)
		{
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			//IL_00db: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_0102: Unknown result type (might be due to invalid IL or missing references)
			ApplyAudio(c);
			if (!((NetworkBehaviour)c).IsServer || ((EnemyAI)c).isEnemyDead)
			{
				return;
			}
			int currentBehaviourStateIndex = ((EnemyAI)c).currentBehaviourStateIndex;
			if (_prevState != 1 && currentBehaviourStateIndex == 1)
			{
				_chaseStartedAt = Time.time;
				_hadTarget = false;
			}
			if (currentBehaviourStateIndex == 1 && Time.time >= _nextSample)
			{
				_nextSample = Time.time + 0.25f;
				PlayerControllerB val = ChasedPlayer(c);
				if ((Object)(object)val != (Object)null && ((EnemyAI)c).PlayerIsTargetable(val, false, false, true) && ((EnemyAI)c).CheckLineOfSightForPosition(((Component)val.gameplayCamera).transform.position, 180f, 60, -1f, (Transform)null))
				{
					_lastSeenPos = ((Component)val).transform.position;
					_hadTarget = true;
				}
			}
			if (_prevState == 1 && currentBehaviourStateIndex == 0)
			{
				if (_hadTarget && Tuning.SmartPatrol.Value)
				{
					SetGoal(_lastSeenPos, Mode.Investigate, 0.5f);
					if (Tuning.LogNoises.Value)
					{
						Plugin.Log.LogInfo((object)$"[patrol] chase ended: checking last seen position {_lastSeenPos}");
					}
				}
				_hadTarget = false;
			}
			_prevState = currentBehaviourStateIndex;
		}

		private void ApplyAudio(SpringManAI c)
		{
			if (Time.time >= _nextAudioScan)
			{
				_nextAudioScan = Time.time + 3f;
				AudioSource[] componentsInChildren = ((Component)c).GetComponentsInChildren<AudioSource>(true);
				foreach (AudioSource val in componentsInChildren)
				{
					if (!((Object)(object)val == (Object)null) && _seenSrc.Add(((Object)val).GetInstanceID()))
					{
						_srcs.Add(val);
						_baseVol.Add(val.volume);
						_baseMin.Add(val.minDistance);
						_baseMax.Add(val.maxDistance);
						Plugin.Log.LogInfo((object)$"[audio] captured '{((Object)((Component)val).gameObject).name}' vol={val.volume:F2} dist={val.minDistance:F1}-{val.maxDistance:F1} loop={val.loop} playing={val.isPlaying}");
					}
				}
			}
			float value = Tuning.VolumeMultiplier.Value;
			float value2 = Tuning.AudibleDistanceMultiplier.Value;
			for (int j = 0; j < _srcs.Count; j++)
			{
				AudioSource val2 = _srcs[j];
				if (!((Object)(object)val2 == (Object)null))
				{
					val2.volume = Mathf.Clamp01(_baseVol[j] * value);
					val2.minDistance = _baseMin[j] * value2;
					val2.maxDistance = _baseMax[j] * value2;
				}
			}
		}

		public void OnInterval(SpringManAI c)
		{
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_0151: Unknown result type (might be due to invalid IL or missing references)
			//IL_0161: Unknown result type (might be due to invalid IL or missing references)
			//IL_010b: Unknown result type (might be due to invalid IL or missing references)
			if (!((NetworkBehaviour)c).IsServer)
			{
				return;
			}
			float time = Time.time;
			float num = ((_lastIntervalTime < 0f) ? 0f : Mathf.Min(time - _lastIntervalTime, 0.5f));
			_lastIntervalTime = time;
			if (((EnemyAI)c).isEnemyDead || ((EnemyAI)c).currentBehaviourStateIndex != 0)
			{
				Release(c);
				return;
			}
			if (c.onCooldownPhase > 0f)
			{
				if (_hasGoal)
				{
					_goalDeadline += num;
				}
				return;
			}
			if (!Tuning.SmartPatrol.Value && (_mode != Mode.Investigate || !_hasGoal))
			{
				Release(c);
				return;
			}
			if (_hasGoal)
			{
				bool flag = Flat(((Component)c).transform.position, _goal) < 3.5f;
				if (flag || Time.time > _goalDeadline)
				{
					if (Tuning.LogNoises.Value && _mode == Mode.Investigate)
					{
						Plugin.Log.LogInfo((object)(flag ? "[noise] arrived at noise" : "[noise] gave up (timeout)"));
					}
					MarkVisitedNear(c, _goal);
					_hasGoal = false;
					_mode = Mode.Patrol;
				}
			}
			if (!_hasGoal && (!Tuning.SmartPatrol.Value || !PickPatrolGoal(c)))
			{
				Release(c);
				return;
			}
			Hijack(c);
			if (!((EnemyAI)c).SetDestinationToPosition(_goal, true))
			{
				MarkVisitedNear(c, _goal);
				_hasGoal = false;
				_mode = Mode.Patrol;
			}
		}

		public void Hear(SpringManAI c, Vector3 pos, float loudness, int noiseID, bool viaClient)
		{
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fa: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			string text = (viaClient ? "client" : "local");
			if (Tuning.LogNoises.Value)
			{
				Plugin.Log.LogInfo((object)$"[noise] loudness={loudness:F2} id={noiseID} dist={Vector3.Distance(((Component)c).transform.position, pos):F1} state={((EnemyAI)c).currentBehaviourStateIndex} via={text}");
			}
			if (!Tuning.NoiseHearing.Value || !((NetworkBehaviour)c).IsServer || ((EnemyAI)c).isEnemyDead || ((EnemyAI)c).currentBehaviourStateIndex != 0 || c.onCooldownPhase > 0f)
			{
				return;
			}
			float num = ((noiseID == 75) ? Tuning.MinVoiceLoudness.Value : Tuning.MinNoiseLoudness.Value);
			if (!(loudness < num) && (_mode != Mode.Investigate || !_hasGoal || !(Time.time < _goalDeadline) || !(loudness < _goalPriority * 0.6f)))
			{
				SetGoal(pos, Mode.Investigate, loudness);
				if (Tuning.LogNoises.Value)
				{
					Plugin.Log.LogInfo((object)$"[noise] -> investigating {pos} (loudness {loudness:F2}, id {noiseID}, via={text})");
				}
			}
		}

		public void ReTarget(SpringManAI c)
		{
			//IL_012c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0131: Unknown result type (might be due to invalid IL or missing references)
			//IL_013b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_0145: Unknown result type (might be due to invalid IL or missing references)
			//IL_0181: Unknown result type (might be due to invalid IL or missing references)
			if (!Tuning.SmartTargeting.Value)
			{
				return;
			}
			PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts;
			if (allPlayerScripts == null)
			{
				return;
			}
			float time = Time.time;
			ulong ownerClientId = ((NetworkBehaviour)c).OwnerClientId;
			bool num = time - _lastRetarget > 1f;
			_lastRetarget = time;
			if (num || ownerClientId != _lastOwner)
			{
				_lastOwner = ownerClientId;
				bool flag = ((NetworkBehaviour)c).IsServer && time - _chaseStartedAt < 1f;
				_currentTarget = (flag ? null : PlayerByClientId(allPlayerScripts, ownerClientId));
				_lockUntil = (((Object)(object)_currentTarget != (Object)null) ? (time + Tuning.TargetHoldTime.Value) : 0f);
				if (Tuning.LogNoises.Value)
				{
					Plugin.Log.LogInfo((object)string.Format("[target] owner {0}: start holding {1}", ownerClientId, ((Object)(object)_currentTarget != (Object)null) ? _currentTarget.playerUsername : "nobody (new chase)"));
				}
			}
			if ((Object)(object)_currentTarget != (Object)null && time < _lockUntil && ((EnemyAI)c).PlayerIsTargetable(_currentTarget, false, false, true))
			{
				((EnemyAI)c).targetPlayer = _currentTarget;
				return;
			}
			Vector3 eye = ((Component)c).transform.position + Vector3.up * 0.6f;
			PlayerControllerB val = null;
			float num2 = float.MaxValue;
			float num3 = float.MaxValue;
			PlayerControllerB[] array = allPlayerScripts;
			foreach (PlayerControllerB val2 in array)
			{
				if (!((Object)(object)val2 == (Object)null) && ((EnemyAI)c).PlayerIsTargetable(val2, false, false, true))
				{
					float num4 = Score(c, val2, allPlayerScripts, eye);
					if ((Object)(object)val2 == (Object)(object)_currentTarget)
					{
						num3 = num4;
					}
					if (num4 < num2)
					{
						num2 = num4;
						val = val2;
					}
				}
			}
			if (!((Object)(object)val == (Object)null))
			{
				if ((Object)(object)_currentTarget != (Object)null && num3 < float.MaxValue && num2 > num3 * 0.75f)
				{
					val = _currentTarget;
				}
				if ((Object)(object)val != (Object)(object)_currentTarget && Tuning.LogNoises.Value)
				{
					Plugin.Log.LogInfo((object)string.Format("[target] switch {0} -> {1} (score {2:F1} -> {3:F1})", ((Object)(object)_currentTarget != (Object)null) ? _currentTarget.playerUsername : "none", val.playerUsername, num3, num2));
				}
				_currentTarget = val;
				_lockUntil = time + Tuning.TargetHoldTime.Value;
				((EnemyAI)c).targetPlayer = val;
			}
		}

		private static PlayerControllerB ChasedPlayer(SpringManAI c)
		{
			if (((NetworkBehaviour)c).OwnerClientId == 0L || ((NetworkBehaviour)c).IsOwner)
			{
				return ((EnemyAI)c).targetPlayer;
			}
			return PlayerByClientId(StartOfRound.Instance.allPlayerScripts, ((NetworkBehaviour)c).OwnerClientId);
		}

		private static PlayerControllerB PlayerByClientId(PlayerControllerB[] players, ulong clientId)
		{
			if (players == null)
			{
				return null;
			}
			foreach (PlayerControllerB val in players)
			{
				if ((Object)(object)val != (Object)null && val.isPlayerControlled && val.actualClientId == clientId)
				{
					return val;
				}
			}
			return null;
		}

		private static float Score(SpringManAI c, PlayerControllerB p, PlayerControllerB[] players, Vector3 eye)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: 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_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			float num = Vector3.Distance(((Component)c).transform.position, ((Component)p).transform.position);
			if (p.HasLineOfSightToPosition(eye, 68f, 60, -1f, -1))
			{
				num *= Tuning.WatchedPenalty.Value;
			}
			float num2 = float.MaxValue;
			foreach (PlayerControllerB val in players)
			{
				if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)p) && !val.isPlayerDead && val.isPlayerControlled)
				{
					float num3 = Vector3.Distance(((Component)p).transform.position, ((Component)val).transform.position);
					if (num3 < num2)
					{
						num2 = num3;
					}
				}
			}
			if (num2 > Tuning.IsolationRange.Value)
			{
				num *= Tuning.IsolationFactor.Value;
			}
			return num;
		}

		private void SetGoal(Vector3 pos, Mode mode, float priority)
		{
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_0009: Unknown result type (might be due to invalid IL or missing references)
			_mode = mode;
			_goal = pos;
			_hasGoal = true;
			_goalPriority = priority;
			_goalDeadline = Time.time + ((mode == Mode.Investigate) ? Tuning.InvestigateTimeout.Value : 25f);
		}

		private bool PickPatrolGoal(SpringManAI c)
		{
			//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_00f7: 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_0088: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			GameObject[] allAINodes = ((EnemyAI)c).allAINodes;
			if (allAINodes == null || allAINodes.Length == 0)
			{
				return false;
			}
			for (int i = 0; i < 2; i++)
			{
				float time = Time.time;
				Vector3 position = ((Component)c).transform.position;
				GameObject val = null;
				float num = float.MaxValue;
				GameObject[] array = allAINodes;
				foreach (GameObject val2 in array)
				{
					if ((Object)(object)val2 == (Object)null)
					{
						continue;
					}
					int instanceID = ((Object)val2).GetInstanceID();
					if (_visited.TryGetValue(instanceID, out var value) && time - value < Tuning.PatrolMemory.Value)
					{
						continue;
					}
					Vector3 val3 = val2.transform.position - position;
					float sqrMagnitude = ((Vector3)(ref val3)).sqrMagnitude;
					if (sqrMagnitude < 36f)
					{
						_visited[instanceID] = time;
						continue;
					}
					sqrMagnitude *= Random.Range(0.85f, 1.15f);
					if (sqrMagnitude < num)
					{
						num = sqrMagnitude;
						val = val2;
					}
				}
				if ((Object)(object)val != (Object)null)
				{
					SetGoal(val.transform.position, Mode.Patrol, 0f);
					return true;
				}
				_visited.Clear();
			}
			return false;
		}

		private void MarkVisitedNear(SpringManAI c, Vector3 p)
		{
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			GameObject[] allAINodes = ((EnemyAI)c).allAINodes;
			if (allAINodes == null)
			{
				return;
			}
			float time = Time.time;
			GameObject[] array = allAINodes;
			foreach (GameObject val in array)
			{
				if ((Object)(object)val != (Object)null && Flat(val.transform.position, p) < 8f)
				{
					_visited[((Object)val).GetInstanceID()] = time;
				}
			}
		}

		private void Hijack(SpringManAI c)
		{
			AISearchRoutine searchForPlayers = c.searchForPlayers;
			if (!_hijacked && searchForPlayers.inProgress)
			{
				((EnemyAI)c).StopSearch(searchForPlayers, true);
			}
			searchForPlayers.inProgress = true;
			_hijacked = true;
			((EnemyAI)c).movingTowardsTargetPlayer = false;
		}

		private void Release(SpringManAI c)
		{
			if (_hijacked)
			{
				_hijacked = false;
				c.searchForPlayers.inProgress = false;
			}
		}

		private static float Flat(Vector3 a, Vector3 b)
		{
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			a.y = 0f;
			b.y = 0f;
			return Vector3.Distance(a, b);
		}
	}
	internal static class IL
	{
		public static int ReplaceFloat(List<CodeInstruction> code, float target, string getter)
		{
			MethodInfo operand = AccessTools.Method(typeof(Tuning), getter, (Type[])null, (Type[])null);
			int num = 0;
			foreach (CodeInstruction item in code)
			{
				if (item.opcode == OpCodes.Ldc_R4 && item.operand is float num2 && Math.Abs(num2 - target) < 0.0001f)
				{
					item.opcode = OpCodes.Call;
					item.operand = operand;
					num++;
				}
			}
			return num;
		}

		public static void Report(string where, string what, int found, int expected)
		{
			if (found != expected)
			{
				Plugin.Log.LogWarning((object)$"[{where}] {what}: replaced {found}, expected {expected} (game updated?)");
			}
			else
			{
				Plugin.Log.LogInfo((object)("[" + where + "] " + what + ": ok"));
			}
		}
	}
	[HarmonyPatch(typeof(SpringManAI), "DoAIInterval")]
	internal static class DoAIInterval_Patch
	{
		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			IL.Report("DoAIInterval", "wander speed 6f", IL.ReplaceFloat(list, 6f, "Wander"), 1);
			IL.Report("DoAIInterval", "notice range 30f", IL.ReplaceFloat(list, 30f, "Notice"), 1);
			IL.Report("DoAIInterval", "lose distance 40f", IL.ReplaceFloat(list, 40f, "LoseDistance"), 1);
			IL.Report("DoAIInterval", "lose time 4.5f", IL.ReplaceFloat(list, 4.5f, "LoseTime"), 1);
			return list;
		}
	}
	[HarmonyPatch(typeof(SpringManAI), "Update")]
	internal static class Update_Patch
	{
		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			//IL_00ae: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Expected O, but got Unknown
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			IL.Report("Update", "move time 9f", IL.ReplaceFloat(list, 9f, "MoveTime"), 1);
			IL.Report("Update", "cooldown 11f (pre-RPC)", IL.ReplaceFloat(list, 11f, "Cooldown"), 1);
			FieldInfo fieldInfo = AccessTools.Field(typeof(SpringManAI), "currentChaseSpeed");
			MethodInfo methodInfo = AccessTools.Method(typeof(Tuning), "ChaseSpeed", (Type[])null, (Type[])null);
			int num = 0;
			for (int i = 0; i < list.Count; i++)
			{
				if (list[i].opcode == OpCodes.Ldfld && list[i].operand == fieldInfo)
				{
					list.Insert(i + 1, new CodeInstruction(OpCodes.Call, (object)methodInfo));
					i++;
					num++;
				}
			}
			IL.Report("Update", "currentChaseSpeed loads", num, 2);
			return list;
		}
	}
	[HarmonyPatch(typeof(SpringManAI), "SetCoilheadOnCooldownClientRpc")]
	internal static class CooldownRpc_Patch
	{
		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			IL.Report("SetCoilheadOnCooldownClientRpc", "cooldown 20f", IL.ReplaceFloat(list, 20f, "Cooldown"), 1);
			return list;
		}
	}
	[HarmonyPatch(typeof(SpringManAI), "OnCollideWithPlayer")]
	internal static class OnCollide_Patch
	{
		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			MethodInfo operand = AccessTools.Method(typeof(Tuning), "HitDamage", (Type[])null, (Type[])null);
			int num = 0;
			foreach (CodeInstruction item in list)
			{
				if ((item.opcode == OpCodes.Ldc_I4_S || item.opcode == OpCodes.Ldc_I4) && Convert.ToInt32(item.operand) == 90)
				{
					item.opcode = OpCodes.Call;
					item.operand = operand;
					num++;
				}
			}
			IL.Report("OnCollideWithPlayer", "damage 90", num, 1);
			return list;
		}
	}
	internal static class NetSync
	{
		public const int VoiceNoiseId = 75;

		public const int DropNoiseId = 941;

		private static LNetworkMessage<string> _config;

		private static LNetworkMessage<string> _request;

		private static LNetworkMessage<string> _noise;

		public static void Init(ConfigFile config)
		{
			_config = LNetworkMessage<string>.Create("EnhancedCoilhead.Config", (Action<string, ulong>)null, (Action<string>)OnConfigFromServer, (Action<string, ulong>)null);
			_request = LNetworkMessage<string>.Create("EnhancedCoilhead.ConfigRequest", (Action<string, ulong>)OnConfigRequest, (Action<string>)null, (Action<string, ulong>)null);
			_noise = LNetworkMessage<string>.Create("EnhancedCoilhead.Noise", (Action<string, ulong>)OnNoiseFromClient, (Action<string>)null, (Action<string, ulong>)null);
			config.SettingChanged += OnSettingChanged;
			LNetworkUtils.OnNetworkStart += OnNetworkStart;
		}

		private static void OnNetworkStart(bool isServer)
		{
			Tuning.ClearOverrides();
			if (isServer)
			{
				Plugin.Log.LogInfo((object)"[sync] hosting: clients will receive this machine's config");
				return;
			}
			try
			{
				_request.SendServer("0.2.0");
				Plugin.Log.LogInfo((object)"[sync] connected: requested host config");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[sync] could not request host config: " + ex.Message));
			}
		}

		internal static void OnLocalDisconnect()
		{
			if (Tuning.ClearOverrides())
			{
				Plugin.Log.LogInfo((object)"[sync] left lobby: cleared host config, back to local values");
			}
		}

		private static void OnConfigRequest(string clientVersion, ulong sender)
		{
			if (clientVersion != "0.2.0")
			{
				Plugin.Log.LogWarning((object)string.Format("[sync] client {0} runs {1} {2}, host runs {3}", sender, "EnhancedCoilhead", clientVersion, "0.2.0"));
			}
			try
			{
				_config.SendClient(Tuning.Snapshot(), sender);
				Plugin.Log.LogInfo((object)$"[sync] sent config to client {sender}");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)$"[sync] could not send config to client {sender}: {ex.Message}");
			}
		}

		private static void OnSettingChanged(object sender, SettingChangedEventArgs e)
		{
			if (!IsHostOnline() || ((e != null) ? e.ChangedSetting : null) == null || !Tuning.IsSynced(e.ChangedSetting))
			{
				return;
			}
			try
			{
				_config.SendClients(Tuning.Snapshot());
				Plugin.Log.LogInfo((object)("[sync] " + e.ChangedSetting.Definition.Section + "." + e.ChangedSetting.Definition.Key + " changed: pushed config to all clients"));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[sync] could not push config: " + ex.Message));
			}
		}

		private static void OnConfigFromServer(string snapshot)
		{
			if (!LNetworkUtils.IsConnected || !LNetworkUtils.IsHostOrServer)
			{
				int num = Tuning.ApplySnapshot(snapshot);
				Plugin.Log.LogInfo((object)$"[sync] applied host config ({num} values)");
			}
		}

		private static bool IsHostOnline()
		{
			if (LNetworkUtils.IsConnected)
			{
				return LNetworkUtils.IsHostOrServer;
			}
			return false;
		}

		private static bool IsRelayable(int noiseID)
		{
			if (noiseID != 75)
			{
				return noiseID == 941;
			}
			return true;
		}

		public static bool ShouldRelay(int noiseID, float loudness)
		{
			if (!IsRelayable(noiseID) || !Tuning.NoiseHearing.Value)
			{
				return false;
			}
			float num = ((noiseID == 75) ? Tuning.MinVoiceLoudness.Value : Tuning.MinNoiseLoudness.Value);
			return loudness >= num;
		}

		public static void RelayNoise(SpringManAI coil, Vector3 pos, float loudness, int noiseID)
		{
			if (!LNetworkUtils.IsConnected || LNetworkUtils.IsHostOrServer)
			{
				return;
			}
			CultureInfo invariantCulture = CultureInfo.InvariantCulture;
			string text = string.Join("|", ((NetworkBehaviour)coil).NetworkObjectId.ToString(invariantCulture), pos.x.ToString("R", invariantCulture), pos.y.ToString("R", invariantCulture), pos.z.ToString("R", invariantCulture), loudness.ToString("R", invariantCulture), noiseID.ToString(invariantCulture));
			try
			{
				_noise.SendServer(text);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)$"[sync] could not relay noise {noiseID}: {ex.Message}");
			}
		}

		private static void OnNoiseFromClient(string data, ulong sender)
		{
			//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				CultureInfo invariantCulture = CultureInfo.InvariantCulture;
				string[] array = data.Split('|');
				ulong key = ulong.Parse(array[0], invariantCulture);
				Vector3 pos = default(Vector3);
				((Vector3)(ref pos))..ctor(float.Parse(array[1], invariantCulture), float.Parse(array[2], invariantCulture), float.Parse(array[3], invariantCulture));
				float loudness = float.Parse(array[4], invariantCulture);
				int noiseID = ((array.Length >= 6) ? int.Parse(array[5], invariantCulture) : 75);
				if (!IsRelayable(noiseID))
				{
					return;
				}
				NetworkManager singleton = NetworkManager.Singleton;
				if (!((Object)(object)singleton == (Object)null) && singleton.IsServer && singleton.SpawnManager.SpawnedObjects.TryGetValue(key, out var value) && !((Object)(object)value == (Object)null))
				{
					SpringManAI component = ((Component)value).GetComponent<SpringManAI>();
					if (!((Object)(object)component == (Object)null))
					{
						CoilBrain.Get(component).Hear(component, pos, loudness, noiseID, viaClient: true);
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)$"[sync] bad noise message from client {sender}: {ex.Message}");
			}
		}
	}
	[HarmonyPatch(typeof(GameNetworkManager), "SetInstanceValuesBackToDefault")]
	internal static class LeaveLobby_Patch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			NetSync.OnLocalDisconnect();
		}
	}
	[BepInPlugin("com.phakin.enhancedcoilhead", "EnhancedCoilhead", "0.2.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		internal static ManualLogSource Log;

		private void Awake()
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			Tuning.Bind(((BaseUnityPlugin)this).Config);
			NetSync.Init(((BaseUnityPlugin)this).Config);
			new Harmony("com.phakin.enhancedcoilhead").PatchAll();
			Log.LogInfo((object)"EnhancedCoilhead 0.2.0 loaded");
		}
	}
	internal static class PluginInfo
	{
		public const string GUID = "com.phakin.enhancedcoilhead";

		public const string NAME = "EnhancedCoilhead";

		public const string VERSION = "0.2.0";
	}
	internal abstract class SyncedSetting
	{
		public string Key;

		public bool Synced;

		public abstract bool HasOverride { get; }

		public abstract string Serialize();

		public abstract void SetOverride(string s);

		public abstract void ClearOverride();
	}
	internal sealed class Setting<T> : SyncedSetting
	{
		public readonly ConfigEntry<T> Entry;

		private T _override;

		private bool _has;

		public T Value
		{
			get
			{
				if (!_has)
				{
					return Entry.Value;
				}
				return _override;
			}
		}

		public override bool HasOverride => _has;

		public Setting(ConfigEntry<T> entry, bool synced)
		{
			Entry = entry;
			Synced = synced;
			Key = ((ConfigEntryBase)entry).Definition.Section + "." + ((ConfigEntryBase)entry).Definition.Key;
		}

		public override string Serialize()
		{
			return Convert.ToString(Entry.Value, CultureInfo.InvariantCulture);
		}

		public override void SetOverride(string s)
		{
			_override = (T)Convert.ChangeType(s, typeof(T), CultureInfo.InvariantCulture);
			_has = true;
		}

		public override void ClearOverride()
		{
			_has = false;
		}
	}
	internal static class Tuning
	{
		public static Setting<float> ChaseSpeedMultiplier;

		public static Setting<float> WanderSpeed;

		public static Setting<float> NoticeRange;

		public static Setting<float> LoseAggroDistance;

		public static Setting<float> LoseAggroTime;

		public static Setting<float> MoveTimeBeforeCooldown;

		public static Setting<float> CooldownDuration;

		public static Setting<int> Damage;

		public static Setting<float> VolumeMultiplier;

		public static Setting<float> AudibleDistanceMultiplier;

		public static Setting<bool> SmartPatrol;

		public static Setting<float> PatrolMemory;

		public static Setting<bool> NoiseHearing;

		public static Setting<float> MinNoiseLoudness;

		public static Setting<float> MinVoiceLoudness;

		public static Setting<float> InvestigateTimeout;

		public static Setting<bool> LogNoises;

		public static Setting<bool> SmartTargeting;

		public static Setting<float> WatchedPenalty;

		public static Setting<float> IsolationRange;

		public static Setting<float> IsolationFactor;

		public static Setting<float> TargetHoldTime;

		public static readonly List<SyncedSetting> All = new List<SyncedSetting>();

		private static Setting<T> Add<T>(ConfigFile c, string section, string key, T def, string desc, bool synced = true)
		{
			Setting<T> setting = new Setting<T>(c.Bind<T>(section, key, def, desc), synced);
			All.Add(setting);
			return setting;
		}

		public static string Snapshot()
		{
			StringBuilder stringBuilder = new StringBuilder();
			foreach (SyncedSetting item in All)
			{
				if (item.Synced)
				{
					if (stringBuilder.Length > 0)
					{
						stringBuilder.Append(';');
					}
					stringBuilder.Append(item.Key).Append('=').Append(item.Serialize());
				}
			}
			return stringBuilder.ToString();
		}

		public static int ApplySnapshot(string snapshot)
		{
			int num = 0;
			string[] array = snapshot.Split(';');
			foreach (string text in array)
			{
				int num2 = text.IndexOf('=');
				if (num2 <= 0)
				{
					continue;
				}
				string text2 = text.Substring(0, num2);
				string text3 = text.Substring(num2 + 1);
				foreach (SyncedSetting item in All)
				{
					if (item.Synced && !(item.Key != text2))
					{
						try
						{
							item.SetOverride(text3);
							num++;
						}
						catch (Exception ex)
						{
							Plugin.Log.LogWarning((object)("[sync] bad value for " + text2 + ": " + text3 + " (" + ex.Message + ")"));
						}
						break;
					}
				}
			}
			return num;
		}

		public static bool ClearOverrides()
		{
			bool result = false;
			foreach (SyncedSetting item in All)
			{
				if (item.HasOverride)
				{
					result = true;
				}
				item.ClearOverride();
			}
			return result;
		}

		public static bool IsSynced(ConfigEntryBase entry)
		{
			string text = entry.Definition.Section + "." + entry.Definition.Key;
			foreach (SyncedSetting item in All)
			{
				if (item.Key == text)
				{
					return item.Synced;
				}
			}
			return false;
		}

		public static void Bind(ConfigFile c)
		{
			ChaseSpeedMultiplier = Add(c, "Chase", "ChaseSpeedMultiplier", 1.15f, "Multiplier on chase speed (vanilla 1.0)");
			WanderSpeed = Add(c, "Wander", "WanderSpeed", 6f, "Speed while searching, not chasing (vanilla 6)");
			NoticeRange = Add(c, "Wander", "NoticeRange", 45f, "Distance at which it starts chasing a player with a clear path (vanilla 30)");
			LoseAggroDistance = Add(c, "Chase", "LoseAggroDistance", 40f, "Distance beyond which it may lose a target (vanilla 40)");
			LoseAggroTime = Add(c, "Chase", "LoseAggroTime", 9f, "Seconds out of sight and far away before giving up (vanilla 4.5)");
			MoveTimeBeforeCooldown = Add(c, "Cooldown", "MoveTimeBeforeCooldown", 9f, "Moving seconds before it needs to rest (vanilla 9)");
			CooldownDuration = Add(c, "Cooldown", "CooldownDuration", 6f, "Rest seconds after a chase burst (vanilla effectively 20)");
			Damage = Add(c, "Combat", "Damage", 90, "Damage per hit (vanilla 90)");
			VolumeMultiplier = Add(c, "Audio", "VolumeMultiplier", 1f, "Coil-Head volume, relative to the game's own level (Unity caps a source at 1.0, so values above the original level have no extra effect)", synced: false);
			AudibleDistanceMultiplier = Add(c, "Audio", "AudibleDistanceMultiplier", 1f, "Scales how far away its sounds can be heard (1 = vanilla, <1 = only when close, >1 = carries further)", synced: false);
			SmartPatrol = Add(c, "Patrol", "SmartPatrol", def: true, "Sweep AI nodes systematically instead of vanilla random search");
			PatrolMemory = Add(c, "Patrol", "PatrolMemory", 90f, "Seconds before an already-swept area is considered worth revisiting");
			NoiseHearing = Add(c, "Noise", "NoiseHearing", def: true, "Investigate noises (footsteps, dropped items, noisemakers)");
			MinNoiseLoudness = Add(c, "Noise", "MinNoiseLoudness", 0.3f, "Ignore noises quieter than this (turn on LogNoises to calibrate)");
			MinVoiceLoudness = Add(c, "Noise", "MinVoiceLoudness", 0.75f, "Voice chat is ignored unless at least this loud (game range 0.6 normal talking .. 0.9 shouting)");
			InvestigateTimeout = Add(c, "Noise", "InvestigateTimeout", 20f, "Seconds before giving up on reaching a noise");
			LogNoises = Add(c, "Noise", "LogNoises", def: false, "Log every noise heard (loudness, distance) for tuning", synced: false);
			SmartTargeting = Add(c, "Targeting", "SmartTargeting", def: true, "Choose victims by situation instead of just the closest player");
			WatchedPenalty = Add(c, "Targeting", "WatchedPenalty", 1.5f, "Score multiplier (>1 = worse victim) for players who are looking at it");
			IsolationRange = Add(c, "Targeting", "IsolationRange", 15f, "A player further than this from every teammate counts as isolated");
			IsolationFactor = Add(c, "Targeting", "IsolationFactor", 0.6f, "Score multiplier (<1 = better victim) for isolated players");
			TargetHoldTime = Add(c, "Targeting", "TargetHoldTime", 4f, "Minimum seconds to stay on a chosen victim (avoids flip-flopping)");
		}

		public static float Wander()
		{
			return WanderSpeed.Value;
		}

		public static float Notice()
		{
			return NoticeRange.Value;
		}

		public static float LoseDistance()
		{
			return LoseAggroDistance.Value;
		}

		public static float LoseTime()
		{
			return LoseAggroTime.Value;
		}

		public static float MoveTime()
		{
			return MoveTimeBeforeCooldown.Value;
		}

		public static float Cooldown()
		{
			return CooldownDuration.Value;
		}

		public static int HitDamage()
		{
			return Damage.Value;
		}

		public static float ChaseSpeed(float vanilla)
		{
			return vanilla * ChaseSpeedMultiplier.Value;
		}
	}
}