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 EnhancedCoilhead v0.2.0
EnhancedCoilhead.dll
Decompiled 3 days agousing 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; } } }