Decompiled source of LethalSpectator v0.2.0

LethalSpectator.dll

Decompiled 4 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using Dissonance;
using Dissonance.Integrations.Unity_NFGO;
using GameNetcodeStuff;
using HarmonyLib;
using LethalCompanyInputUtils.Api;
using LethalSpectator.Config;
using LethalSpectator.Core;
using LethalSpectator.Core.Abstractions;
using LethalSpectator.Modules.Compat;
using LethalSpectator.Modules.Danger;
using LethalSpectator.Modules.Hud;
using LethalSpectator.Modules.Input;
using LethalSpectator.Modules.SpectateCamera;
using LethalSpectator.Modules.VoiceActivity;
using Microsoft.CodeAnalysis;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;

[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("LethalSpectator")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Activity-based auto spectate camera for Lethal Company")]
[assembly: AssemblyFileVersion("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+16c075f6f4cfce48c86c16a0d303d7979c7c76f1")]
[assembly: AssemblyProduct("LethalSpectator")]
[assembly: AssemblyTitle("LethalSpectator")]
[assembly: AssemblyVersion("0.2.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace LethalSpectator
{
	[BepInPlugin("Haaylo.LethalSpectator", "LethalSpectator", "0.2.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		private Harmony _harmony;

		private GameObject _runnerObject;

		public static Plugin Instance { get; private set; }

		public static ManualLogSource Log { get; private set; }

		private void Awake()
		{
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Expected O, but got Unknown
			//IL_017f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0189: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			try
			{
				ModConfig.Bind(((BaseUnityPlugin)this).Config);
			}
			catch (Exception arg)
			{
				Log.LogError((object)$"Config binding failed; using defaults where possible: {arg}");
			}
			try
			{
				_harmony = new Harmony("Haaylo.LethalSpectator");
				_harmony.PatchAll(typeof(Plugin).Assembly);
				int num = 0;
				foreach (MethodBase patchedMethod in _harmony.GetPatchedMethods())
				{
					_ = patchedMethod;
					num++;
				}
				Log.LogInfo((object)string.Format("{0} {1}: {2} game methods patched.", "LethalSpectator", "0.2.0", num));
			}
			catch (Exception arg2)
			{
				Log.LogError((object)$"Harmony patching failed; the mod will stay idle: {arg2}");
				return;
			}
			bool flag = Chainloader.PluginInfos.ContainsKey("com.rune580.LethalCompanyInputUtils");
			bool flag2 = Chainloader.PluginInfos.ContainsKey("SpectateEnemy");
			Log.LogInfo((object)$"InputUtils present: {flag}; SpectateEnemies present: {flag2}; config: {((BaseUnityPlugin)this).Config.ConfigFilePath}");
			LobbyCompatibilityBridge.TryRegister();
			try
			{
				SpectateCameraGateway gateway = new SpectateCameraGateway();
				SpectateEnemiesBridge external = new SpectateEnemiesBridge();
				VoiceActivitySampler sampler = new VoiceActivitySampler();
				EnemyProximitySource dangerSource = new EnemyProximitySource();
				KillerCamDriver killerCam = new KillerCamDriver();
				HudStatus hud = new HudStatus();
				ToggleInput input = new ToggleInput(gateway, external);
				AutoSpectateController controller = new AutoSpectateController();
				SpectatorRunner.Wire(gateway, sampler, dangerSource, killerCam, external, input, hud, controller);
				_runnerObject = new GameObject("LethalSpectator.Runner");
				Object.DontDestroyOnLoad((Object)(object)_runnerObject);
				((Object)_runnerObject).hideFlags = (HideFlags)61;
				_runnerObject.AddComponent<SpectatorRunner>();
			}
			catch (Exception arg3)
			{
				Log.LogError((object)$"Could not create the runner object; the mod will stay idle: {arg3}");
			}
		}
	}
	public static class PluginInfo
	{
		public const string GUID = "Haaylo.LethalSpectator";

		public const string NAME = "LethalSpectator";

		public const string VERSION = "0.2.0";

		public const string INPUT_UTILS_GUID = "com.rune580.LethalCompanyInputUtils";

		public const string SPECTATE_ENEMIES_GUID = "SpectateEnemy";
	}
}
namespace LethalSpectator.Modules.VoiceActivity
{
	public sealed class VoiceActivitySampler : IVoiceActivitySource
	{
		private sealed class SlotCache
		{
			public PlayerControllerB PlayerRef;

			public NfgoPlayer Nfgo;

			public VoicePlayerState State;
		}

		private SlotCache[] _cache = new SlotCache[0];

		private float _lastRefreshTime = -999f;

		private float _lastUnavailableLog = -999f;

		private const float RefreshIntervalSeconds = 2f;

		private const float MinVolumeForNormalize = 0.05f;

		public void Sample(CandidateSnapshot[] candidates, int count, VoiceSample[] output, float now)
		{
			EnsureCapacity(candidates, count);
			bool flag = false;
			bool flag2 = false;
			for (int i = 0; i < count; i++)
			{
				output[i] = VoiceSample.Unavailable;
				try
				{
					int slot = candidates[i].Slot;
					SlotCache slotCache = _cache[slot];
					PlayerControllerB player = candidates[i].Player;
					if (slotCache.PlayerRef != player)
					{
						slotCache.PlayerRef = player;
						slotCache.Nfgo = null;
						slotCache.State = null;
					}
					if ((Object)(object)slotCache.Nfgo == (Object)null)
					{
						slotCache.Nfgo = ((Component)player).GetComponentInChildren<NfgoPlayer>();
					}
					if ((Object)(object)slotCache.Nfgo == (Object)null)
					{
						flag = true;
						continue;
					}
					string playerId = slotCache.Nfgo.PlayerId;
					if (string.IsNullOrEmpty(playerId))
					{
						flag = true;
						continue;
					}
					VoicePlayerState val = slotCache.State;
					if (val == null || !string.Equals(val.Name, playerId, StringComparison.Ordinal))
					{
						val = null;
						VoicePlayerState voicePlayerState = player.voicePlayerState;
						if (voicePlayerState != null && string.Equals(voicePlayerState.Name, playerId, StringComparison.Ordinal))
						{
							val = voicePlayerState;
						}
						else if (now - _lastRefreshTime >= 2f || flag2)
						{
							flag2 = true;
							_lastRefreshTime = now;
							DissonanceComms val2 = StartOfRound.Instance?.voiceChatModule;
							if ((Object)(object)val2 != (Object)null)
							{
								val = val2.FindPlayer(playerId);
							}
						}
						slotCache.State = val;
					}
					if (val == null || !val.IsConnected)
					{
						flag = true;
						continue;
					}
					float volume = val.Volume;
					float amplitude = val.Amplitude / Mathf.Max(0.05f, volume);
					output[i].Available = true;
					output[i].IsSpeaking = val.IsSpeaking;
					output[i].Amplitude = amplitude;
					output[i].IsMuted = val.IsLocallyMuted;
				}
				catch (Exception)
				{
					flag = true;
				}
			}
			if (flag && now - _lastUnavailableLog > 60f)
			{
				_lastUnavailableLog = now;
				Plugin.Log.LogDebug((object)"Voice state unavailable for one or more candidates (normal during joins/level loads).");
			}
		}

		private void EnsureCapacity(CandidateSnapshot[] candidates, int count)
		{
			int num = 0;
			for (int i = 0; i < count; i++)
			{
				if (candidates[i].Slot + 1 > num)
				{
					num = candidates[i].Slot + 1;
				}
			}
			if (_cache.Length < num)
			{
				SlotCache[] array = new SlotCache[num];
				for (int j = 0; j < _cache.Length; j++)
				{
					array[j] = _cache[j];
				}
				for (int k = _cache.Length; k < num; k++)
				{
					array[k] = new SlotCache();
				}
				_cache = array;
			}
		}
	}
}
namespace LethalSpectator.Modules.SpectateCamera
{
	public sealed class KillerCamDriver : IKillerCamera
	{
		internal static KillerCamDriver Instance;

		private static readonly MethodInfo RaycastPivot = AccessTools.Method(typeof(PlayerControllerB), "RaycastSpectateCameraAroundPivot", (Type[])null, (Type[])null);

		private readonly object[] _noArgs = new object[0];

		private bool _hasPending;

		private PlayerControllerB _pendingVictim;

		private EnemyAI _pendingAnimEnemy;

		private Vector3 _pendingPosition;

		private int _pendingCause;

		private float _pendingTime;

		private EnemyAI _killer;

		private float _engageEndTime;

		private int _errorCount;

		private float _lastErrorLog = -999f;

		public bool Engaged { get; private set; }

		public string KillerName
		{
			get
			{
				try
				{
					EnemyAI killer = _killer;
					return ((Object)(object)killer != (Object)null && (Object)(object)killer.enemyType != (Object)null) ? killer.enemyType.enemyName : "Enemy";
				}
				catch (Exception)
				{
					return "Enemy";
				}
			}
		}

		public KillerCamDriver()
		{
			Instance = this;
			if (RaycastPivot == null)
			{
				Plugin.Log.LogWarning((object)"RaycastSpectateCameraAroundPivot not found (game update?); killer cam disabled.");
			}
		}

		internal static void NotifyPlayerKilled(int playerId, int causeOfDeath)
		{
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			KillerCamDriver instance = Instance;
			if (instance == null)
			{
				return;
			}
			try
			{
				StartOfRound instance2 = StartOfRound.Instance;
				if (!((Object)(object)instance2 == (Object)null) && instance2.allPlayerScripts != null && playerId >= 0 && playerId < instance2.allPlayerScripts.Length)
				{
					PlayerControllerB val = instance2.allPlayerScripts[playerId];
					if (!((Object)(object)val == (Object)null))
					{
						instance._pendingVictim = val;
						instance._pendingAnimEnemy = val.inAnimationWithEnemy;
						instance._pendingPosition = ((Component)val).transform.position;
						instance._pendingCause = causeOfDeath;
						instance._pendingTime = Time.unscaledTime;
						instance._hasPending = true;
					}
				}
			}
			catch (Exception ex)
			{
				instance.LogOnce(ex);
			}
		}

		public bool TryEngage(PlayerControllerB currentTarget, float now)
		{
			if (Engaged || !_hasPending || RaycastPivot == null)
			{
				return false;
			}
			try
			{
				if (!ModConfig.KillerCamEnabled.Value)
				{
					_hasPending = false;
					return false;
				}
				if (now - _pendingTime > 1.5f)
				{
					_hasPending = false;
					return false;
				}
				if ((Object)(object)currentTarget == (Object)null)
				{
					return false;
				}
				if (_pendingVictim != currentTarget)
				{
					_hasPending = false;
					return false;
				}
				_hasPending = false;
				string attribution;
				EnemyAI val = ResolveKiller(out attribution);
				if ((Object)(object)val == (Object)null)
				{
					return false;
				}
				_killer = val;
				Engaged = true;
				float num = Mathf.Max(0.5f, ModConfig.KillerCamDurationSeconds.Value);
				_engageEndTime = now + num;
				Plugin.Log.LogInfo((object)$"Killer cam: following '{KillerName}' for {num:F1}s ({attribution}, cause {_pendingCause}).");
				return true;
			}
			catch (Exception ex)
			{
				LogOnce(ex);
				_hasPending = false;
				return false;
			}
		}

		private EnemyAI ResolveKiller(out string attribution)
		{
			//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
			attribution = "animation";
			EnemyAI pendingAnimEnemy = _pendingAnimEnemy;
			if ((Object)(object)pendingAnimEnemy != (Object)null && !pendingAnimEnemy.isEnemyDead)
			{
				return pendingAnimEnemy;
			}
			if (!IsEnemyishCause(_pendingCause))
			{
				attribution = "none";
				return null;
			}
			EnemyAI val = null;
			float num = 0f;
			float num2 = Mathf.Max(1f, ModConfig.KillerCamMaxAttributionDistance.Value);
			float num3 = num2 * num2;
			List<EnemyAI> list = (((Object)(object)RoundManager.Instance != (Object)null) ? RoundManager.Instance.SpawnedEnemies : null);
			if (list == null)
			{
				attribution = "none";
				return null;
			}
			for (int i = 0; i < list.Count; i++)
			{
				EnemyAI val2 = list[i];
				if ((Object)(object)val2 == (Object)null || val2.isEnemyDead)
				{
					continue;
				}
				EnemyType enemyType = val2.enemyType;
				if (!((Object)(object)enemyType == (Object)null) && !enemyType.isDaytimeEnemy)
				{
					Vector3 val3 = ((Component)val2).transform.position - _pendingPosition;
					float sqrMagnitude = ((Vector3)(ref val3)).sqrMagnitude;
					if (sqrMagnitude <= num3 && ((Object)(object)val == (Object)null || sqrMagnitude < num))
					{
						val = val2;
						num = sqrMagnitude;
					}
				}
			}
			attribution = (((Object)(object)val != (Object)null) ? $"nearest {Mathf.Sqrt(num):F1}m" : "none");
			return val;
		}

		private static bool IsEnemyishCause(int cause)
		{
			switch (cause)
			{
			case 0:
			case 1:
			case 4:
			case 5:
			case 6:
			case 7:
			case 8:
			case 12:
			case 13:
			case 14:
			case 17:
			case 18:
				return true;
			default:
				return false;
			}
		}

		public void Tick(float now)
		{
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
			if (!Engaged)
			{
				return;
			}
			try
			{
				PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null);
				EnemyAI killer = _killer;
				if (now >= _engageEndTime)
				{
					Release("duration elapsed");
					return;
				}
				if ((Object)(object)killer == (Object)null || killer.isEnemyDead || !((Component)killer).gameObject.activeInHierarchy)
				{
					Release("killer gone");
					return;
				}
				if ((Object)(object)val == (Object)null || (Object)(object)val.spectateCameraPivot == (Object)null)
				{
					Release("no pivot");
					return;
				}
				Transform eye = killer.eye;
				val.spectateCameraPivot.position = (((Object)(object)eye != (Object)null) ? eye.position : ((Component)killer).transform.position);
				RaycastPivot.Invoke(val, _noArgs);
			}
			catch (Exception ex)
			{
				LogOnce(ex);
				Release("error");
			}
		}

		public void Release(string reason)
		{
			if (Engaged)
			{
				Engaged = false;
				_killer = null;
				Plugin.Log.LogDebug((object)("Killer cam released: " + reason + "."));
			}
		}

		public void ResetSession()
		{
			Engaged = false;
			_killer = null;
			_hasPending = false;
			_pendingVictim = null;
			_pendingAnimEnemy = null;
		}

		private void LogOnce(Exception ex)
		{
			_errorCount++;
			float unscaledTime = Time.unscaledTime;
			if (unscaledTime - _lastErrorLog > 60f)
			{
				_lastErrorLog = unscaledTime;
				Plugin.Log.LogError((object)$"Killer cam error #{_errorCount}: {ex}");
			}
		}
	}
	[HarmonyPatch(typeof(PlayerControllerB), "KillPlayerClientRpc")]
	internal static class PlayerControllerB_KillPlayerClientRpc_Patch
	{
		private static void Prefix(int playerId, int causeOfDeath)
		{
			try
			{
				KillerCamDriver.NotifyPlayerKilled(playerId, causeOfDeath);
			}
			catch (Exception)
			{
			}
		}
	}
	[HarmonyPatch(typeof(PlayerControllerB), "SetWhoToSpectate")]
	internal static class PlayerControllerB_SetWhoToSpectate_Patch
	{
		private static bool Prefix()
		{
			try
			{
				KillerCamDriver instance = KillerCamDriver.Instance;
				return instance == null || !instance.Engaged;
			}
			catch (Exception)
			{
				return true;
			}
		}
	}
	[HarmonyPatch(typeof(PlayerControllerB), "SpectateNextPlayer")]
	internal static class PlayerControllerB_SpectateNextPlayer_Patch
	{
		private static void Postfix(PlayerControllerB __instance, bool getClosest)
		{
			try
			{
				SpectateCameraGateway.NotifyVanillaSpectateNext(__instance, getClosest);
			}
			catch (Exception)
			{
			}
		}
	}
	public sealed class SpectateCameraGateway : ISpectateCameraGateway
	{
		internal static SpectateCameraGateway Instance;

		private static readonly FieldRef<PlayerControllerB, float> DeadTimerRef = ResolveDeadTimer();

		private static bool _resolveFailed;

		private bool _faulted;

		private int _faultCount;

		private float _lastFaultLog = -999f;

		private bool _applyingSwitch;

		private bool _pendingVanillaSwitch;

		private PlayerControllerB _pendingTarget;

		private bool _pendingManual;

		public bool Faulted => _faulted;

		public bool IsLocalPlayerSpectating
		{
			get
			{
				try
				{
					GameNetworkManager instance = GameNetworkManager.Instance;
					if ((Object)(object)instance == (Object)null)
					{
						return false;
					}
					PlayerControllerB localPlayerController = instance.localPlayerController;
					if ((Object)(object)localPlayerController == (Object)null || (Object)(object)StartOfRound.Instance == (Object)null)
					{
						return false;
					}
					return ((NetworkBehaviour)localPlayerController).IsOwner && localPlayerController.isPlayerDead && (!((NetworkBehaviour)localPlayerController).IsServer || localPlayerController.isHostPlayerObject);
				}
				catch (Exception e)
				{
					Fault(e);
					return false;
				}
			}
		}

		public bool IsVanillaCameraLocked
		{
			get
			{
				try
				{
					StartOfRound instance = StartOfRound.Instance;
					if ((Object)(object)instance == (Object)null)
					{
						return true;
					}
					if (instance.overrideSpectateCamera || instance.shipIsLeaving || instance.allPlayersDead)
					{
						return true;
					}
					PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
					if ((Object)(object)val == (Object)null)
					{
						return true;
					}
					return val.isInGameOverAnimation > 0f && ((Object)(object)val.deadBody != (Object)null || (Object)(object)val.overrideGameOverSpectatePivot != (Object)null);
				}
				catch (Exception e)
				{
					Fault(e);
					return true;
				}
			}
		}

		public PlayerControllerB CurrentTarget
		{
			get
			{
				try
				{
					return GameNetworkManager.Instance?.localPlayerController?.spectatedPlayerScript;
				}
				catch (Exception e)
				{
					Fault(e);
					return null;
				}
			}
		}

		public int SlotCount
		{
			get
			{
				try
				{
					return (StartOfRound.Instance?.allPlayerScripts?.Length).GetValueOrDefault();
				}
				catch (Exception e)
				{
					Fault(e);
					return 0;
				}
			}
		}

		public SpectateCameraGateway()
		{
			Instance = this;
			if (_resolveFailed)
			{
				_faulted = true;
			}
		}

		private static FieldRef<PlayerControllerB, float> ResolveDeadTimer()
		{
			try
			{
				return AccessTools.FieldRefAccess<PlayerControllerB, float>("spectatedPlayerDeadTimer");
			}
			catch (Exception ex)
			{
				_resolveFailed = true;
				Plugin.Log.LogError((object)("Could not resolve PlayerControllerB.spectatedPlayerDeadTimer (game update?): " + ex.Message));
				return null;
			}
		}

		public int GetCandidates(CandidateSnapshot[] buffer)
		{
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				StartOfRound instance = StartOfRound.Instance;
				PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
				if ((Object)(object)instance == (Object)null || (Object)(object)val == (Object)null || instance.allPlayerScripts == null)
				{
					return 0;
				}
				PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts;
				int num = 0;
				for (int i = 0; i < allPlayerScripts.Length; i++)
				{
					if (num >= buffer.Length)
					{
						break;
					}
					PlayerControllerB val2 = allPlayerScripts[i];
					if (!((Object)(object)val2 == (Object)null) && !val2.isPlayerDead && val2.isPlayerControlled && val2 != val)
					{
						buffer[num].Player = val2;
						buffer[num].Slot = i;
						buffer[num].Position = ((Component)val2).transform.position;
						buffer[num].InsideFactory = val2.isInsideFactory;
						buffer[num].InElevator = val2.isInElevator;
						buffer[num].InShipRoom = val2.isInHangarShipRoom;
						num++;
					}
				}
				return num;
			}
			catch (Exception e)
			{
				Fault(e);
				return 0;
			}
		}

		public bool SwitchTo(PlayerControllerB target)
		{
			if (_faulted || DeadTimerRef == null)
			{
				return false;
			}
			try
			{
				PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
				if ((Object)(object)val == (Object)null || !IsLocalPlayerSpectating)
				{
					return false;
				}
				if ((Object)(object)target == (Object)null || target.isPlayerDead || !target.isPlayerControlled)
				{
					return false;
				}
				if (target == val.spectatedPlayerScript)
				{
					return false;
				}
				_applyingSwitch = true;
				try
				{
					val.spectatedPlayerScript = target;
					DeadTimerRef.Invoke(val) = 0f;
					StartOfRound.Instance.SetPlayerSafeInShip();
					val.SetSpectatedPlayerEffects(false);
				}
				finally
				{
					_applyingSwitch = false;
				}
				return true;
			}
			catch (Exception e)
			{
				Fault(e);
				return false;
			}
		}

		public bool ConsumeVanillaSwitch(out PlayerControllerB newTarget, out bool wasManual)
		{
			newTarget = _pendingTarget;
			wasManual = _pendingManual;
			bool pendingVanillaSwitch = _pendingVanillaSwitch;
			_pendingVanillaSwitch = false;
			_pendingTarget = null;
			return pendingVanillaSwitch;
		}

		internal static void NotifyVanillaSpectateNext(PlayerControllerB instance, bool getClosest)
		{
			SpectateCameraGateway instance2 = Instance;
			if (instance2 == null || instance2._applyingSwitch)
			{
				return;
			}
			try
			{
				PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
				if (!((Object)(object)val == (Object)null) && instance == val)
				{
					instance2._pendingVanillaSwitch = true;
					instance2._pendingTarget = val.spectatedPlayerScript;
					instance2._pendingManual = !getClosest;
				}
			}
			catch (Exception e)
			{
				instance2.Fault(e);
			}
		}

		private void Fault(Exception e)
		{
			_faultCount++;
			_faulted = true;
			float unscaledTime = Time.unscaledTime;
			if (unscaledTime - _lastFaultLog > 60f)
			{
				_lastFaultLog = unscaledTime;
				Plugin.Log.LogError((object)$"Spectate camera gateway fault #{_faultCount}; auto-spectate paused: {e}");
			}
		}
	}
}
namespace LethalSpectator.Modules.Input
{
	internal static class InputUtilsBindings
	{
		private static LethalSpectatorInputActions _actions;

		public static void Init(Action onRawPress)
		{
			if (_actions != null)
			{
				return;
			}
			_actions = new LethalSpectatorInputActions();
			_actions.Toggle.performed += delegate(CallbackContext ctx)
			{
				if (((CallbackContext)(ref ctx)).performed)
				{
					onRawPress();
				}
			};
		}

		public static string GetDisplayName()
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: 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)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			InputAction val = _actions?.Toggle;
			if (val == null || val.bindings.Count == 0)
			{
				return null;
			}
			InputBinding val2 = val.bindings[0];
			return InputControlPath.ToHumanReadableString(((InputBinding)(ref val2)).effectivePath, (HumanReadableStringOptions)2, (InputControl)null);
		}
	}
	public class LethalSpectatorInputActions : LcInputActions
	{
		[InputAction("<Keyboard>/h", Name = "Toggle auto spectate", GamepadPath = "<Gamepad>/leftStickPress")]
		public InputAction Toggle { get; set; }
	}
	public sealed class ToggleInput : IToggleInput
	{
		private readonly ISpectateCameraGateway _gateway;

		private readonly IExternalCameraOwner _external;

		private readonly bool _viaInputUtils;

		private Key _fallbackKey = (Key)22;

		private bool _fallbackDisabled;

		private string _displayName;

		private float _displayNameRefreshedAt = -999f;

		private float _lastGuardErrorLog = -999f;

		public string BindingDisplayName
		{
			get
			{
				float unscaledTime = Time.unscaledTime;
				if (_displayName == null || unscaledTime - _displayNameRefreshedAt >= 1f)
				{
					_displayNameRefreshedAt = unscaledTime;
					string text = null;
					if (_viaInputUtils)
					{
						try
						{
							text = InputUtilsBindings.GetDisplayName();
						}
						catch (Exception)
						{
						}
					}
					_displayName = ((!string.IsNullOrEmpty(text)) ? text : (ModConfig.ToggleKey?.Value ?? "H"));
				}
				return _displayName;
			}
		}

		public event Action TogglePressed;

		public ToggleInput(ISpectateCameraGateway gateway, IExternalCameraOwner external)
		{
			//IL_0003: Unknown result type (might be due to invalid IL or missing references)
			_gateway = gateway;
			_external = external;
			if (Chainloader.PluginInfos.ContainsKey("com.rune580.LethalCompanyInputUtils"))
			{
				try
				{
					InputUtilsBindings.Init(OnRawPress);
					_viaInputUtils = true;
					Plugin.Log.LogInfo((object)"Toggle bound through InputUtils (rebindable in the Keybinds menu).");
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("InputUtils binding failed; falling back to raw keyboard polling: " + ex.Message));
				}
			}
			if (!_viaInputUtils)
			{
				string text = ModConfig.ToggleKey?.Value ?? "H";
				if (!Enum.TryParse<Key>(text, ignoreCase: true, out _fallbackKey))
				{
					_fallbackDisabled = true;
					Plugin.Log.LogError((object)("General/ToggleKey '" + text + "' is not a UnityEngine.InputSystem.Key name; the toggle is disabled (IH-1.2)."));
				}
			}
		}

		public void Poll()
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			if (_viaInputUtils || _fallbackDisabled)
			{
				return;
			}
			try
			{
				Keyboard current = Keyboard.current;
				if (current != null && ((ButtonControl)current[_fallbackKey]).wasPressedThisFrame)
				{
					OnRawPress();
				}
			}
			catch (Exception ex)
			{
				_fallbackDisabled = true;
				Plugin.Log.LogError((object)("Keyboard polling failed; the toggle is disabled: " + ex.Message));
			}
		}

		private void OnRawPress()
		{
			try
			{
				if (_gateway.IsLocalPlayerSpectating)
				{
					PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
					if (!((Object)(object)val == (Object)null) && (!((Object)(object)val.quickMenuManager != (Object)null) || !val.quickMenuManager.isMenuOpen) && !val.isTypingChat && !val.inTerminalMenu && !val.inSpecialMenu && !_external.BlocksInput)
					{
						this.TogglePressed?.Invoke();
					}
				}
			}
			catch (Exception arg)
			{
				float unscaledTime = Time.unscaledTime;
				if (unscaledTime - _lastGuardErrorLog > 60f)
				{
					_lastGuardErrorLog = unscaledTime;
					Plugin.Log.LogError((object)$"Toggle guard evaluation failed: {arg}");
				}
			}
		}
	}
}
namespace LethalSpectator.Modules.Hud
{
	[HarmonyPatch(typeof(HUDManager), "Update")]
	[HarmonyAfter(new string[] { "SpectateEnemy" })]
	internal static class HUDManager_Update_Patch
	{
		private static void Postfix(HUDManager __instance)
		{
			HudStatus.AppendStatus(__instance);
		}
	}
	[HarmonyPatch(typeof(HUDManager), "SetSpectatingTextToPlayer")]
	internal static class HUDManager_SetSpectatingTextToPlayer_Patch
	{
		private static void Postfix(HUDManager __instance, PlayerControllerB playerScript)
		{
			HudStatus.AppendAutoMarker(__instance, playerScript);
		}
	}
	public sealed class HudStatus : IHudStatus
	{
		internal static HudStatus Instance;

		internal string CachedAppend;

		internal bool AutoMarker;

		private static readonly MethodInfo DisplaySpectatorTip = AccessTools.Method(typeof(HUDManager), "DisplaySpectatorTip", (Type[])null, (Type[])null);

		private readonly object[] _tipArgs = new object[1];

		private float _lastTipTime = -999f;

		private float _lastErrorLog = -999f;

		private int _errorCount;

		public HudStatus()
		{
			Instance = this;
			if (DisplaySpectatorTip == null)
			{
				Plugin.Log.LogWarning((object)"HUDManager.DisplaySpectatorTip not found (game update?); tips disabled.");
			}
		}

		public void SetStatusLine(string text)
		{
			CachedAppend = (string.IsNullOrEmpty(text) ? null : ("\n" + text));
		}

		public void SetAutoMarker(bool targetChosenByMod)
		{
			AutoMarker = targetChosenByMod;
		}

		public void ShowTip(string text)
		{
			try
			{
				if (DisplaySpectatorTip == null || !ModConfig.ShowTips.Value)
				{
					return;
				}
				float unscaledTime = Time.unscaledTime;
				if (!(unscaledTime - _lastTipTime < 2f))
				{
					HUDManager instance = HUDManager.Instance;
					if (!((Object)(object)instance == (Object)null))
					{
						_lastTipTime = unscaledTime;
						_tipArgs[0] = text;
						DisplaySpectatorTip.Invoke(instance, _tipArgs);
					}
				}
			}
			catch (Exception e)
			{
				LogOnce(e);
			}
		}

		public void SetSpectatingLabel(string text)
		{
			try
			{
				if (!string.IsNullOrEmpty(text))
				{
					HUDManager instance = HUDManager.Instance;
					if (!((Object)(object)instance == (Object)null) && !((Object)(object)instance.spectatingPlayerText == (Object)null))
					{
						((TMP_Text)instance.spectatingPlayerText).text = text;
					}
				}
			}
			catch (Exception e)
			{
				LogOnce(e);
			}
		}

		internal static void AppendStatus(HUDManager hud)
		{
			HudStatus instance = Instance;
			if (instance == null || instance.CachedAppend == null)
			{
				return;
			}
			try
			{
				if (!ModConfig.ShowHudStatus.Value)
				{
					return;
				}
				PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
				if ((Object)(object)val == (Object)null || !val.isPlayerDead)
				{
					return;
				}
				StartOfRound instance2 = StartOfRound.Instance;
				if (!((Object)(object)instance2 == (Object)null) && !instance2.shipIsLeaving)
				{
					TextMeshProUGUI holdButtonToEndGameEarlyText = hud.holdButtonToEndGameEarlyText;
					if (!((Object)(object)holdButtonToEndGameEarlyText == (Object)null))
					{
						((TMP_Text)holdButtonToEndGameEarlyText).text = ((TMP_Text)holdButtonToEndGameEarlyText).text + instance.CachedAppend;
					}
				}
			}
			catch (Exception e)
			{
				instance.LogOnce(e);
			}
		}

		internal static void AppendAutoMarker(HUDManager hud, PlayerControllerB playerScript)
		{
			HudStatus instance = Instance;
			if (instance == null || !instance.AutoMarker)
			{
				return;
			}
			try
			{
				if (ModConfig.ShowHudStatus.Value && !((Object)(object)playerScript == (Object)null))
				{
					TextMeshProUGUI spectatingPlayerText = hud.spectatingPlayerText;
					if (!((Object)(object)spectatingPlayerText == (Object)null))
					{
						((TMP_Text)spectatingPlayerText).text = ((TMP_Text)spectatingPlayerText).text + " [auto]";
					}
				}
			}
			catch (Exception e)
			{
				instance.LogOnce(e);
			}
		}

		private void LogOnce(Exception e)
		{
			_errorCount++;
			float unscaledTime = Time.unscaledTime;
			if (unscaledTime - _lastErrorLog > 60f)
			{
				_lastErrorLog = unscaledTime;
				Plugin.Log.LogError((object)$"HUD status error #{_errorCount} (HUD output degraded): {e}");
			}
		}
	}
}
namespace LethalSpectator.Modules.Danger
{
	public sealed class EnemyProximitySource : IDangerSource
	{
		private const float RecomputeIntervalSeconds = 0.25f;

		private DangerSample[] _slotCache = new DangerSample[0];

		private float _lastComputeTime = -999f;

		private int _errorCount;

		private float _lastErrorLog = -999f;

		public void Sample(CandidateSnapshot[] candidates, int count, DangerSample[] output, float now)
		{
			if (!ModConfig.DangerEnabled.Value)
			{
				for (int i = 0; i < count; i++)
				{
					output[i] = default(DangerSample);
				}
				return;
			}
			EnsureSlots(candidates, count);
			if (now - _lastComputeTime >= 0.25f)
			{
				_lastComputeTime = now;
				Recompute(candidates, count);
			}
			for (int j = 0; j < count; j++)
			{
				output[j] = _slotCache[candidates[j].Slot];
			}
		}

		private void Recompute(CandidateSnapshot[] candidates, int count)
		{
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ae: 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)
			//IL_00d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
			for (int i = 0; i < count; i++)
			{
				_slotCache[candidates[i].Slot] = default(DangerSample);
			}
			try
			{
				RoundManager instance = RoundManager.Instance;
				List<EnemyAI> list = (((Object)(object)instance != (Object)null) ? instance.SpawnedEnemies : null);
				if (list == null)
				{
					return;
				}
				for (int j = 0; j < list.Count; j++)
				{
					EnemyAI val = list[j];
					if ((Object)(object)val == (Object)null || val.isEnemyDead)
					{
						continue;
					}
					EnemyType enemyType = val.enemyType;
					if ((Object)(object)enemyType == (Object)null || enemyType.isDaytimeEnemy)
					{
						continue;
					}
					bool flag = !val.isOutside;
					Vector3 position = ((Component)val).transform.position;
					for (int k = 0; k < count; k++)
					{
						if (candidates[k].InsideFactory == flag)
						{
							Vector3 val2 = candidates[k].Position - position;
							float sqrMagnitude = ((Vector3)(ref val2)).sqrMagnitude;
							int slot = candidates[k].Slot;
							if (!_slotCache[slot].HasThreat || sqrMagnitude < _slotCache[slot].NearestEnemyDistance)
							{
								_slotCache[slot].HasThreat = true;
								_slotCache[slot].NearestEnemyDistance = sqrMagnitude;
							}
						}
					}
				}
				for (int l = 0; l < count; l++)
				{
					int slot2 = candidates[l].Slot;
					if (_slotCache[slot2].HasThreat)
					{
						_slotCache[slot2].NearestEnemyDistance = Mathf.Sqrt(_slotCache[slot2].NearestEnemyDistance);
					}
				}
			}
			catch (Exception ex)
			{
				for (int m = 0; m < count; m++)
				{
					_slotCache[candidates[m].Slot] = default(DangerSample);
				}
				_errorCount++;
				float unscaledTime = Time.unscaledTime;
				if (unscaledTime - _lastErrorLog > 60f)
				{
					_lastErrorLog = unscaledTime;
					Plugin.Log.LogError((object)$"Danger scan error #{_errorCount} (degrading to no-threat, LC-5.2): {ex.Message}");
				}
			}
		}

		private void EnsureSlots(CandidateSnapshot[] candidates, int count)
		{
			int num = 0;
			for (int i = 0; i < count; i++)
			{
				if (candidates[i].Slot + 1 > num)
				{
					num = candidates[i].Slot + 1;
				}
			}
			if (_slotCache.Length < num)
			{
				DangerSample[] array = new DangerSample[num];
				for (int j = 0; j < _slotCache.Length; j++)
				{
					array[j] = _slotCache[j];
				}
				_slotCache = array;
			}
		}
	}
}
namespace LethalSpectator.Modules.Compat
{
	public static class LobbyCompatibilityBridge
	{
		private const string GUID = "BMX.LobbyCompatibility";

		public static void TryRegister()
		{
			try
			{
				if (!Chainloader.PluginInfos.TryGetValue("BMX.LobbyCompatibility", out var value) || (Object)(object)value.Instance == (Object)null)
				{
					return;
				}
				Assembly assembly = ((object)value.Instance).GetType().Assembly;
				Type type = assembly.GetType("LobbyCompatibility.Features.PluginHelper");
				Type type2 = assembly.GetType("LobbyCompatibility.Enums.CompatibilityLevel");
				Type type3 = assembly.GetType("LobbyCompatibility.Enums.VersionStrictness");
				if (!(type == null) && !(type2 == null) && !(type3 == null))
				{
					MethodInfo method = type.GetMethod("RegisterPlugin", BindingFlags.Static | BindingFlags.Public, null, new Type[4]
					{
						typeof(string),
						typeof(Version),
						type2,
						type3
					}, null);
					if (!(method == null))
					{
						method.Invoke(null, new object[4]
						{
							"Haaylo.LethalSpectator",
							Version.Parse("0.2.0"),
							Enum.Parse(type2, "ClientOnly"),
							Enum.Parse(type3, "None")
						});
						Plugin.Log.LogInfo((object)"Registered with LobbyCompatibility (ClientOnly / None).");
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogDebug((object)("LobbyCompatibility registration skipped: " + ex.Message));
			}
		}
	}
	public sealed class SpectateEnemiesBridge : IExternalCameraOwner
	{
		private bool _presenceChecked;

		private bool _present;

		private bool _resolved;

		private int _throwCount;

		private FieldInfo _instanceField;

		private FieldRef<object, bool> _spectatingRef;

		private MethodInfo _isMenuOpenMethod;

		private object _cachedInstance;

		private Func<bool> _isMenuOpenDelegate;

		private int _sampledFrame = -1;

		private bool _ownsCamera;

		private bool _blocksInput;

		public string Name => "SpectateEnemies";

		public bool IsPresent
		{
			get
			{
				EnsureResolved();
				return _present;
			}
		}

		public bool OwnsCamera
		{
			get
			{
				SampleFrame();
				return _ownsCamera;
			}
		}

		public bool BlocksInput
		{
			get
			{
				SampleFrame();
				return _blocksInput;
			}
		}

		private void EnsureResolved()
		{
			if (_presenceChecked)
			{
				return;
			}
			_presenceChecked = true;
			try
			{
				if (!Chainloader.PluginInfos.TryGetValue("SpectateEnemy", out var value) || (Object)(object)value.Instance == (Object)null)
				{
					_present = false;
					return;
				}
				Type type = ((object)value.Instance).GetType().Assembly.GetType("SpectateEnemy.SpectateEnemies");
				if (type == null)
				{
					throw new MissingMemberException("type SpectateEnemy.SpectateEnemies");
				}
				_instanceField = type.GetField("Instance", BindingFlags.Static | BindingFlags.Public) ?? throw new MissingMemberException("SpectateEnemies.Instance");
				_spectatingRef = AccessTools.FieldRefAccess<bool>(type, "SpectatingEnemies");
				_isMenuOpenMethod = type.GetMethod("IsMenuOpen", BindingFlags.Instance | BindingFlags.Public) ?? throw new MissingMemberException("SpectateEnemies.IsMenuOpen");
				_present = true;
				_resolved = true;
				Plugin.Log.LogInfo((object)"SpectateEnemies bridge bound (yielding the camera while it spectates enemies).");
			}
			catch (Exception ex)
			{
				_present = false;
				Plugin.Log.LogWarning((object)("SpectateEnemies present but its API could not be bound; treating as absent: " + ex.Message));
			}
		}

		private void SampleFrame()
		{
			int frameCount = Time.frameCount;
			if (frameCount == _sampledFrame)
			{
				return;
			}
			_sampledFrame = frameCount;
			_ownsCamera = false;
			_blocksInput = false;
			EnsureResolved();
			if (!_present || !_resolved)
			{
				return;
			}
			try
			{
				object value = _instanceField.GetValue(null);
				if (value != null)
				{
					_ownsCamera = _spectatingRef.Invoke(value);
					if (value != _cachedInstance)
					{
						_cachedInstance = value;
						_isMenuOpenDelegate = (Func<bool>)Delegate.CreateDelegate(typeof(Func<bool>), value, _isMenuOpenMethod);
					}
					_blocksInput = _isMenuOpenDelegate();
				}
			}
			catch (Exception ex)
			{
				_throwCount++;
				if (_throwCount >= 3)
				{
					_present = false;
					Plugin.Log.LogError((object)$"SpectateEnemies bridge failed {_throwCount} times; treating as absent: {ex.Message}");
				}
			}
		}
	}
}
namespace LethalSpectator.Core
{
	public sealed class ActivityScorer
	{
		private CandidateState[] _states = new CandidateState[0];

		public float[] Scores = new float[0];

		public int[] Neighbors = new int[0];

		private const float StaleSeconds = 5f;

		private const float TalkTimeClamp = 3f;

		public void EnsureCapacity(int slotCount, int candidateCapacity)
		{
			if (_states.Length < slotCount)
			{
				CandidateState[] array = new CandidateState[slotCount];
				for (int i = 0; i < _states.Length; i++)
				{
					array[i] = _states[i];
				}
				for (int j = _states.Length; j < slotCount; j++)
				{
					array[j] = new CandidateState();
				}
				_states = array;
			}
			if (Scores.Length < candidateCapacity)
			{
				Scores = new float[candidateCapacity];
				Neighbors = new int[candidateCapacity];
			}
		}

		public void ResetAll(float now)
		{
			for (int i = 0; i < _states.Length; i++)
			{
				_states[i].Reset(null, now);
			}
		}

		public CandidateState GetState(int slot)
		{
			if (slot < 0 || slot >= _states.Length)
			{
				return null;
			}
			return _states[slot];
		}

		public void Update(CandidateSnapshot[] candidates, int count, VoiceSample[] samples, DangerSample[] danger, float now, float dt, in DecisionSettings s)
		{
			//IL_0229: Unknown result type (might be due to invalid IL or missing references)
			//IL_0236: Unknown result type (might be due to invalid IL or missing references)
			//IL_023b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0240: Unknown result type (might be due to invalid IL or missing references)
			EnsureCapacity(RequiredSlots(candidates, count), count);
			for (int i = 0; i < count; i++)
			{
				int slot = candidates[i].Slot;
				CandidateState candidateState = _states[slot];
				if (candidateState.PlayerRef != candidates[i].Player || now - candidateState.LastSeenTime > 5f)
				{
					candidateState.Reset(candidates[i].Player, now);
				}
				candidateState.LastSeenTime = now;
				VoiceSample voiceSample = samples[i];
				float num = (voiceSample.Available ? voiceSample.Amplitude : 0f);
				if (num >= candidateState.Envelope)
				{
					candidateState.Envelope = num;
					candidateState.HoldUntil = now + s.EnvelopeHoldSeconds;
				}
				else if (now >= candidateState.HoldUntil)
				{
					candidateState.Envelope *= Mathf.Exp((0f - dt) / Mathf.Max(0.01f, s.SmoothingReleaseSeconds));
					if (candidateState.Envelope < 1E-05f)
					{
						candidateState.Envelope = 0f;
					}
				}
				bool flag = voiceSample.IsMuted && !s.IncludeLocallyMuted;
				bool flag2 = voiceSample.Available && voiceSample.IsSpeaking && !flag;
				if (!candidateState.Talking)
				{
					if (flag2 && candidateState.Envelope >= s.SpeakingOnThreshold)
					{
						candidateState.Talking = true;
						candidateState.TalkStartTime = now;
					}
				}
				else if (!flag2 || candidateState.Envelope < s.SpeakingOffThreshold)
				{
					candidateState.Talking = false;
				}
				if (candidateState.Talking)
				{
					candidateState.TalkTime = Mathf.Min(3f, candidateState.TalkTime + dt);
					candidateState.QuietTime = 0f;
					continue;
				}
				candidateState.QuietTime += dt;
				if (candidateState.QuietTime >= s.InactivityTimeoutSeconds)
				{
					candidateState.TalkTime = 0f;
				}
			}
			float num2 = s.GroupRadiusMeters * s.GroupRadiusMeters;
			for (int j = 0; j < count; j++)
			{
				int num3 = 0;
				for (int k = 0; k < count; k++)
				{
					if (k != j && (!s.RequireSameInterior || candidates[k].InsideFactory == candidates[j].InsideFactory))
					{
						Vector3 val = candidates[k].Position - candidates[j].Position;
						if (((Vector3)(ref val)).sqrMagnitude <= num2)
						{
							num3++;
						}
					}
				}
				Neighbors[j] = num3;
				CandidateState candidateState2 = _states[candidates[j].Slot];
				float num4 = Mathf.Min(s.GroupBonusCap, s.GroupBonusPerNeighbor * (float)num3);
				float num5 = (candidateState2.Talking ? (candidateState2.Envelope * (1f + num4)) : 0f);
				if (num5 > 0f && s.DangerEnabled && s.DangerWeight > 0f && danger[j].HasThreat)
				{
					float num6 = Mathf.Clamp01(1f - danger[j].NearestEnemyDistance / Mathf.Max(1f, s.DangerRadiusMeters));
					num5 *= 1f + s.DangerWeight * num6;
				}
				Scores[j] = num5;
			}
		}

		private static int RequiredSlots(CandidateSnapshot[] candidates, int count)
		{
			int num = 0;
			for (int i = 0; i < count; i++)
			{
				if (candidates[i].Slot + 1 > num)
				{
					num = candidates[i].Slot + 1;
				}
			}
			return num;
		}
	}
	public sealed class AutoSpectateController
	{
		public readonly ActivityScorer Scorer = new ActivityScorer();

		private float _lastSwitchTime;

		private float _lastManualSwitchTime;

		private float _lastDangerSwitchTime;

		private bool _pendingResume;

		private float _pendingResumeUntil;

		private const float NegInf = -1E+09f;

		public bool TargetChosenByMod { get; private set; }

		public void StartSession(float now)
		{
			_lastSwitchTime = now;
			_lastManualSwitchTime = -1E+09f;
			_lastDangerSwitchTime = -1E+09f;
			_pendingResume = false;
			TargetChosenByMod = false;
			Scorer.ResetAll(now);
		}

		public void NotifyVanillaSwitch(bool wasManual, float now)
		{
			_lastSwitchTime = now;
			if (wasManual)
			{
				_lastManualSwitchTime = now;
			}
			TargetChosenByMod = false;
		}

		public void NotifyExternalReleased(float now)
		{
			_lastSwitchTime = -1E+09f;
			_lastManualSwitchTime = -1E+09f;
			_pendingResume = true;
			_pendingResumeUntil = now + 1.5f;
		}

		public void CommitSwitch(float now, DecisionReason reason)
		{
			_lastSwitchTime = now;
			TargetChosenByMod = true;
			_pendingResume = false;
			if (reason == DecisionReason.Danger)
			{
				_lastDangerSwitchTime = now;
			}
		}

		public float ManualHoldRemaining(float now, in DecisionSettings s)
		{
			float num = s.ManualHoldSeconds - (now - _lastManualSwitchTime);
			if (!(num > 0f))
			{
				return 0f;
			}
			return num;
		}

		public SpectateDecision Tick(float now, float dt, CandidateSnapshot[] candidates, int count, VoiceSample[] samples, DangerSample[] danger, PlayerControllerB currentTarget, in DecisionSettings s)
		{
			Scorer.Update(candidates, count, samples, danger, now, dt, in s);
			int num = -1;
			if (currentTarget != null)
			{
				for (int i = 0; i < count; i++)
				{
					if (candidates[i].Player == currentTarget)
					{
						num = i;
						break;
					}
				}
			}
			bool flag = now - _lastManualSwitchTime < s.ManualHoldSeconds;
			bool flag2 = now - _lastSwitchTime < s.MinHoldSeconds;
			SpectateDecision none = SpectateDecision.None;
			if (num < 0)
			{
				int num2 = BestCandidate(candidates, count, -1, requireSustained: false, in s);
				if (num2 < 0)
				{
					num2 = MostEndangered(candidates, count, danger, -1, in s);
				}
				if (num2 >= 0)
				{
					none.SwitchTo = candidates[num2].Player;
					none.SwitchSlot = candidates[num2].Slot;
					none.Reason = DecisionReason.Retarget;
					none.ChallengerScore = Scorer.Scores[num2];
				}
			}
			else if (!flag && !flag2)
			{
				CandidateState state = Scorer.GetState(candidates[num].Slot);
				float num3 = Scorer.Scores[num];
				bool num4 = state.QuietTime >= s.InactivityTimeoutSeconds;
				int num5 = BestCandidate(candidates, count, num, requireSustained: false, in s);
				int num6 = BestCandidate(candidates, count, num, requireSustained: true, in s);
				if (num4)
				{
					int num7 = (s.SnapToFirstSpeaker ? num5 : num6);
					if (num7 >= 0)
					{
						none.Reason = DecisionReason.Inactivity;
					}
					else if (num5 < 0 && s.DangerEnabled && now - _lastDangerSwitchTime >= s.DangerHoldSeconds)
					{
						num7 = MostEndangered(candidates, count, danger, num, in s);
						if (num7 >= 0 && danger[num].HasThreat && danger[num].NearestEnemyDistance <= danger[num7].NearestEnemyDistance)
						{
							num7 = -1;
						}
						if (num7 >= 0)
						{
							none.Reason = DecisionReason.Danger;
						}
					}
					if (none.Reason != DecisionReason.None)
					{
						none.SwitchTo = candidates[num7].Player;
						none.SwitchSlot = candidates[num7].Slot;
						none.ChallengerScore = Scorer.Scores[num7];
						none.IncumbentScore = num3;
						none.IncumbentQuietTime = state.QuietTime;
					}
				}
				else if (state.Talking && num6 >= 0 && Scorer.Scores[num6] >= num3 * s.LouderSwitchRatio)
				{
					none.Reason = DecisionReason.Louder;
					none.SwitchTo = candidates[num6].Player;
					none.SwitchSlot = candidates[num6].Slot;
					none.ChallengerScore = Scorer.Scores[num6];
					none.IncumbentScore = num3;
					none.IncumbentQuietTime = state.QuietTime;
				}
			}
			if (none.Reason != DecisionReason.None && none.Reason != DecisionReason.Danger && _pendingResume && now <= _pendingResumeUntil)
			{
				none.Reason = DecisionReason.ResumeFromExternal;
			}
			if (_pendingResume && now > _pendingResumeUntil)
			{
				_pendingResume = false;
			}
			return none;
		}

		private static int MostEndangered(CandidateSnapshot[] candidates, int count, DangerSample[] danger, int excludeIdx, in DecisionSettings s)
		{
			if (!s.DangerEnabled)
			{
				return -1;
			}
			int num = -1;
			for (int i = 0; i < count; i++)
			{
				if (i != excludeIdx && danger[i].HasThreat && !(danger[i].NearestEnemyDistance > s.DangerSwitchDistance) && (num < 0 || danger[i].NearestEnemyDistance < danger[num].NearestEnemyDistance || (danger[i].NearestEnemyDistance == danger[num].NearestEnemyDistance && candidates[i].Slot < candidates[num].Slot)))
				{
					num = i;
				}
			}
			return num;
		}

		private int BestCandidate(CandidateSnapshot[] candidates, int count, int excludeIdx, bool requireSustained, in DecisionSettings s)
		{
			int num = -1;
			float num2 = 0f;
			float num3 = 0f;
			for (int i = 0; i < count; i++)
			{
				if (i == excludeIdx)
				{
					continue;
				}
				CandidateState state = Scorer.GetState(candidates[i].Slot);
				if (state != null && state.Talking && (!requireSustained || !(state.TalkTime < s.MinSpeechBeforeSwitchSeconds)))
				{
					float num4 = Scorer.Scores[i];
					if (!(num4 <= 0f) && (num < 0 || num4 > num2 || (num4 == num2 && (state.TalkStartTime > num3 || (state.TalkStartTime == num3 && candidates[i].Slot < candidates[num].Slot)))))
					{
						num = i;
						num2 = num4;
						num3 = state.TalkStartTime;
					}
				}
			}
			return num;
		}
	}
	public sealed class CandidateState
	{
		public PlayerControllerB PlayerRef;

		public float Envelope;

		public float HoldUntil;

		public bool Talking;

		public float TalkTime;

		public float QuietTime;

		public float TalkStartTime;

		public float LastSeenTime;

		public void Reset(PlayerControllerB player, float now)
		{
			PlayerRef = player;
			Envelope = 0f;
			HoldUntil = 0f;
			Talking = false;
			TalkTime = 0f;
			QuietTime = 0f;
			TalkStartTime = 0f;
			LastSeenTime = now;
		}
	}
	public enum DecisionReason
	{
		None,
		Retarget,
		Inactivity,
		Louder,
		ResumeFromExternal,
		Danger
	}
	public struct DecisionSettings
	{
		public float SmoothingReleaseSeconds;

		public float EnvelopeHoldSeconds;

		public float SpeakingOnThreshold;

		public float SpeakingOffThreshold;

		public float MinSpeechBeforeSwitchSeconds;

		public bool IncludeLocallyMuted;

		public float MinHoldSeconds;

		public float InactivityTimeoutSeconds;

		public float LouderSwitchRatio;

		public float ManualHoldSeconds;

		public bool SnapToFirstSpeaker;

		public bool DangerEnabled;

		public float DangerRadiusMeters;

		public float DangerWeight;

		public float DangerSwitchDistance;

		public float DangerHoldSeconds;

		public float GroupRadiusMeters;

		public float GroupBonusPerNeighbor;

		public float GroupBonusCap;

		public bool RequireSameInterior;

		public static DecisionSettings FromConfig()
		{
			DecisionSettings result = default(DecisionSettings);
			result.SmoothingReleaseSeconds = ModConfig.SmoothingReleaseSeconds.Value;
			result.EnvelopeHoldSeconds = ModConfig.EnvelopeHoldSeconds.Value;
			result.SpeakingOnThreshold = ModConfig.SpeakingOnThreshold.Value;
			result.SpeakingOffThreshold = ModConfig.SpeakingOffThreshold.Value;
			result.MinSpeechBeforeSwitchSeconds = ModConfig.MinSpeechBeforeSwitchSeconds.Value;
			result.IncludeLocallyMuted = ModConfig.IncludeLocallyMuted.Value;
			result.MinHoldSeconds = ModConfig.MinHoldSeconds.Value;
			result.InactivityTimeoutSeconds = ModConfig.InactivityTimeoutSeconds.Value;
			result.LouderSwitchRatio = ModConfig.LouderSwitchRatio.Value;
			result.ManualHoldSeconds = ModConfig.ManualHoldSeconds.Value;
			result.SnapToFirstSpeaker = ModConfig.SnapToFirstSpeaker.Value;
			result.DangerEnabled = ModConfig.DangerEnabled.Value;
			result.DangerRadiusMeters = ModConfig.DangerRadiusMeters.Value;
			result.DangerWeight = ModConfig.DangerWeight.Value;
			result.DangerSwitchDistance = ModConfig.DangerSwitchDistance.Value;
			result.DangerHoldSeconds = ModConfig.DangerHoldSeconds.Value;
			result.GroupRadiusMeters = ModConfig.GroupRadiusMeters.Value;
			result.GroupBonusPerNeighbor = ModConfig.GroupBonusPerNeighbor.Value;
			result.GroupBonusCap = ModConfig.GroupBonusCap.Value;
			result.RequireSameInterior = ModConfig.RequireSameInterior.Value;
			return result;
		}
	}
	public struct SpectateDecision
	{
		public PlayerControllerB SwitchTo;

		public int SwitchSlot;

		public DecisionReason Reason;

		public float ChallengerScore;

		public float IncumbentScore;

		public float IncumbentQuietTime;

		public static readonly SpectateDecision None = new SpectateDecision
		{
			SwitchSlot = -1
		};
	}
	public class SpectatorRunner : MonoBehaviour
	{
		private enum StatusKind
		{
			None,
			On,
			Off,
			ManualHold,
			PausedExternal,
			KillerCam,
			Unavailable
		}

		private static ISpectateCameraGateway _sGateway;

		private static IVoiceActivitySource _sSampler;

		private static IDangerSource _sDanger;

		private static IKillerCamera _sKillerCam;

		private static IExternalCameraOwner _sExternal;

		private static IToggleInput _sInput;

		private static IHudStatus _sHud;

		private static AutoSpectateController _sController;

		private ISpectateCameraGateway _gateway;

		private IVoiceActivitySource _sampler;

		private IDangerSource _danger;

		private IKillerCamera _killerCam;

		private IExternalCameraOwner _external;

		private IToggleInput _input;

		private IHudStatus _hud;

		private AutoSpectateController _controller;

		private CandidateSnapshot[] _candidates = new CandidateSnapshot[0];

		private VoiceSample[] _samples = new VoiceSample[0];

		private DangerSample[] _dangerSamples = new DangerSample[0];

		private bool _enabled;

		private bool _wasSpectating;

		private bool _sessionStarted;

		private bool _prevOwnsCamera;

		private bool _loggedGameVersion;

		private bool _faultAnnounced;

		private StatusKind _statusKind;

		private int _statusSeconds;

		private string _statusBinding;

		private float _lastScoreDump;

		private float _lastTickErrorLog = -999f;

		private int _tickErrorCount;

		private readonly StringBuilder _dump = new StringBuilder(512);

		public static void Wire(ISpectateCameraGateway gateway, IVoiceActivitySource sampler, IDangerSource dangerSource, IKillerCamera killerCam, IExternalCameraOwner external, IToggleInput input, IHudStatus hud, AutoSpectateController controller)
		{
			_sGateway = gateway;
			_sSampler = sampler;
			_sDanger = dangerSource;
			_sKillerCam = killerCam;
			_sExternal = external;
			_sInput = input;
			_sHud = hud;
			_sController = controller;
		}

		private void Awake()
		{
			_gateway = _sGateway;
			_sampler = _sSampler;
			_danger = _sDanger;
			_killerCam = _sKillerCam;
			_external = _sExternal;
			_input = _sInput;
			_hud = _sHud;
			_controller = _sController;
			_enabled = !ModConfig.IsBound || ModConfig.EnabledByDefault.Value;
			if (_input != null)
			{
				_input.TogglePressed += OnTogglePressed;
			}
			Plugin.Log.LogInfo((object)("Runner alive; auto spectate starts " + (_enabled ? "ON" : "OFF") + "."));
		}

		private void OnTogglePressed()
		{
			_enabled = !_enabled;
			Plugin.Log.LogInfo((object)("Auto spectate toggled " + (_enabled ? "ON" : "OFF") + "."));
			_hud.ShowTip(_enabled ? "Auto spectate ON" : "Auto spectate OFF");
		}

		private void Update()
		{
			try
			{
				LogGameVersionOnce();
				if (_gateway == null)
				{
					return;
				}
				if (_gateway.Faulted)
				{
					if (!_faultAnnounced)
					{
						_faultAnnounced = true;
						_hud.SetStatusLine("Auto spectate: unavailable");
						Plugin.Log.LogError((object)"Gateway faulted; auto spectate is unavailable for this session.");
					}
					return;
				}
				if (!_gateway.IsLocalPlayerSpectating)
				{
					if (_wasSpectating)
					{
						EndSession();
					}
					return;
				}
				_wasSpectating = true;
				float unscaledTime = Time.unscaledTime;
				float unscaledDeltaTime = Time.unscaledDeltaTime;
				_input.Poll();
				bool isVanillaCameraLocked = _gateway.IsVanillaCameraLocked;
				if (!_sessionStarted)
				{
					if (isVanillaCameraLocked)
					{
						return;
					}
					_sessionStarted = true;
					_controller.StartSession(unscaledTime);
					_killerCam.ResetSession();
					_hud.SetAutoMarker(targetChosenByMod: false);
				}
				PlayerControllerB newTarget;
				bool wasManual;
				bool flag = _gateway.ConsumeVanillaSwitch(out newTarget, out wasManual);
				if (flag)
				{
					_controller.NotifyVanillaSwitch(wasManual, unscaledTime);
					if (wasManual)
					{
						Plugin.Log.LogInfo((object)"Manual spectate cycle observed; holding auto switching (DC-4.1).");
					}
				}
				bool ownsCamera = _external.OwnsCamera;
				if (_prevOwnsCamera && !ownsCamera)
				{
					_controller.NotifyExternalReleased(unscaledTime);
				}
				_prevOwnsCamera = ownsCamera;
				if (isVanillaCameraLocked)
				{
					_killerCam.Release("vanilla lock");
					UpdateStatus(StatusKindFor(unscaledTime), unscaledTime);
					return;
				}
				if (ownsCamera)
				{
					_killerCam.Release("SpectateEnemies");
					UpdateStatus(StatusKind.PausedExternal, unscaledTime);
					return;
				}
				if (!_enabled)
				{
					_killerCam.Release("toggle off");
					UpdateStatus(StatusKind.Off, unscaledTime);
					return;
				}
				if (_killerCam.Engaged)
				{
					if (flag)
					{
						_killerCam.Release("manual/vanilla switch");
					}
					else
					{
						_killerCam.Tick(unscaledTime);
					}
					if (_killerCam.Engaged)
					{
						UpdateStatus(StatusKind.KillerCam, unscaledTime);
						return;
					}
				}
				else if (_killerCam.TryEngage(_gateway.CurrentTarget, unscaledTime))
				{
					_hud.SetSpectatingLabel("(Watching: " + _killerCam.KillerName + ")");
					_killerCam.Tick(unscaledTime);
					UpdateStatus(StatusKind.KillerCam, unscaledTime);
					return;
				}
				DecisionSettings s = DecisionSettings.FromConfig();
				EnsureBuffers();
				int candidates = _gateway.GetCandidates(_candidates);
				if (candidates > 0)
				{
					_sampler.Sample(_candidates, candidates, _samples, unscaledTime);
					_danger.Sample(_candidates, candidates, _dangerSamples, unscaledTime);
					SpectateDecision spectateDecision = _controller.Tick(unscaledTime, unscaledDeltaTime, _candidates, candidates, _samples, _dangerSamples, _gateway.CurrentTarget, in s);
					if (spectateDecision.SwitchTo != null)
					{
						_hud.SetAutoMarker(targetChosenByMod: true);
						bool num = _gateway.SwitchTo(spectateDecision.SwitchTo);
						_hud.SetAutoMarker(targetChosenByMod: false);
						if (num)
						{
							_controller.CommitSwitch(unscaledTime, spectateDecision.Reason);
							Plugin.Log.LogInfo((object)($"Auto switch -> {spectateDecision.SwitchTo.playerUsername} ({spectateDecision.Reason}); " + $"challenger {spectateDecision.ChallengerScore:F4} vs incumbent {spectateDecision.IncumbentScore:F4}, " + $"incumbent quiet {spectateDecision.IncumbentQuietTime:F1}s"));
						}
					}
					DumpScores(unscaledTime, candidates, in s);
				}
				UpdateStatus(StatusKindFor(unscaledTime), unscaledTime);
			}
			catch (Exception arg)
			{
				_tickErrorCount++;
				float unscaledTime2 = Time.unscaledTime;
				if (unscaledTime2 - _lastTickErrorLog > 60f)
				{
					_lastTickErrorLog = unscaledTime2;
					Plugin.Log.LogError((object)$"Runner tick error #{_tickErrorCount}: {arg}");
				}
			}
		}

		private StatusKind StatusKindFor(float now)
		{
			if (!_enabled)
			{
				return StatusKind.Off;
			}
			DecisionSettings s = DecisionSettings.FromConfig();
			if (!(_controller.ManualHoldRemaining(now, in s) > 0f))
			{
				return StatusKind.On;
			}
			return StatusKind.ManualHold;
		}

		private void EndSession()
		{
			_wasSpectating = false;
			_sessionStarted = false;
			_prevOwnsCamera = false;
			_killerCam.ResetSession();
			_hud.SetStatusLine(null);
			_hud.SetAutoMarker(targetChosenByMod: false);
			_statusKind = StatusKind.None;
		}

		private void EnsureBuffers()
		{
			int slotCount = _gateway.SlotCount;
			if (slotCount > _candidates.Length)
			{
				_candidates = new CandidateSnapshot[slotCount];
				_samples = new VoiceSample[slotCount];
				_dangerSamples = new DangerSample[slotCount];
			}
		}

		private void UpdateStatus(StatusKind kind, float now)
		{
			string bindingDisplayName = _input.BindingDisplayName;
			int num = 0;
			if (kind == StatusKind.ManualHold)
			{
				DecisionSettings s = DecisionSettings.FromConfig();
				num = Mathf.CeilToInt(_controller.ManualHoldRemaining(now, in s));
				if (num <= 0)
				{
					kind = StatusKind.On;
				}
			}
			if (kind != _statusKind || num != _statusSeconds || !string.Equals(bindingDisplayName, _statusBinding))
			{
				_statusKind = kind;
				_statusSeconds = num;
				_statusBinding = bindingDisplayName;
				switch (kind)
				{
				case StatusKind.On:
					_hud.SetStatusLine("Auto spectate: ON [" + bindingDisplayName + "]");
					break;
				case StatusKind.Off:
					_hud.SetStatusLine("Auto spectate: OFF [" + bindingDisplayName + "]");
					break;
				case StatusKind.ManualHold:
					_hud.SetStatusLine($"Auto spectate: manual hold ({num}s) [{bindingDisplayName}]");
					break;
				case StatusKind.PausedExternal:
					_hud.SetStatusLine("Auto spectate: paused (SpectateEnemies) [" + bindingDisplayName + "]");
					break;
				case StatusKind.KillerCam:
					_hud.SetStatusLine("Auto spectate: killer cam [" + bindingDisplayName + "]");
					break;
				default:
					_hud.SetStatusLine(null);
					break;
				}
			}
		}

		private void DumpScores(float now, int count, in DecisionSettings settings)
		{
			if (!ModConfig.LogScores.Value || now - _lastScoreDump < Mathf.Max(0.2f, ModConfig.LogScoresIntervalSeconds.Value))
			{
				return;
			}
			_lastScoreDump = now;
			_dump.Length = 0;
			_dump.Append("scores:");
			for (int i = 0; i < count; i++)
			{
				CandidateState state = _controller.Scorer.GetState(_candidates[i].Slot);
				if (state != null)
				{
					_dump.Append(" [").Append(_candidates[i].Player.playerUsername).Append(" amp=")
						.Append(_samples[i].Available ? _samples[i].Amplitude.ToString("F4") : "n/a")
						.Append(" env=")
						.Append(state.Envelope.ToString("F4"))
						.Append(state.Talking ? " talk=" : " quiet=")
						.Append((state.Talking ? state.TalkTime : state.QuietTime).ToString("F1"))
						.Append(" score=")
						.Append(_controller.Scorer.Scores[i].ToString("F4"))
						.Append(" nb=")
						.Append(_controller.Scorer.Neighbors[i])
						.Append(" enemy=")
						.Append(_dangerSamples[i].HasThreat ? _dangerSamples[i].NearestEnemyDistance.ToString("F1") : "-")
						.Append(']');
				}
			}
			Plugin.Log.LogDebug((object)_dump.ToString());
		}

		private void LogGameVersionOnce()
		{
			if (!_loggedGameVersion)
			{
				GameNetworkManager instance = GameNetworkManager.Instance;
				if (!((Object)(object)instance == (Object)null))
				{
					_loggedGameVersion = true;
					Plugin.Log.LogInfo((object)$"Game version (GameNetworkManager.gameVersionNum): {instance.gameVersionNum}");
				}
			}
		}
	}
}
namespace LethalSpectator.Core.Abstractions
{
	public struct CandidateSnapshot
	{
		public PlayerControllerB Player;

		public int Slot;

		public Vector3 Position;

		public bool InsideFactory;

		public bool InElevator;

		public bool InShipRoom;
	}
	public struct DangerSample
	{
		public bool HasThreat;

		public float NearestEnemyDistance;
	}
	public interface IDangerSource
	{
		void Sample(CandidateSnapshot[] candidates, int count, DangerSample[] output, float now);
	}
	public interface IExternalCameraOwner
	{
		string Name { get; }

		bool IsPresent { get; }

		bool OwnsCamera { get; }

		bool BlocksInput { get; }
	}
	public interface IHudStatus
	{
		void SetStatusLine(string text);

		void SetAutoMarker(bool targetChosenByMod);

		void ShowTip(string text);

		void SetSpectatingLabel(string text);
	}
	public interface IKillerCamera
	{
		bool Engaged { get; }

		string KillerName { get; }

		bool TryEngage(PlayerControllerB currentTarget, float now);

		void Tick(float now);

		void Release(string reason);

		void ResetSession();
	}
	public interface ISpectateCameraGateway
	{
		bool Faulted { get; }

		bool IsLocalPlayerSpectating { get; }

		bool IsVanillaCameraLocked { get; }

		PlayerControllerB CurrentTarget { get; }

		int SlotCount { get; }

		int GetCandidates(CandidateSnapshot[] buffer);

		bool SwitchTo(PlayerControllerB target);

		bool ConsumeVanillaSwitch(out PlayerControllerB newTarget, out bool wasManual);
	}
	public interface IToggleInput
	{
		string BindingDisplayName { get; }

		event Action TogglePressed;

		void Poll();
	}
	public interface IVoiceActivitySource
	{
		void Sample(CandidateSnapshot[] candidates, int count, VoiceSample[] output, float now);
	}
	public struct VoiceSample
	{
		public bool Available;

		public bool IsSpeaking;

		public float Amplitude;

		public bool IsMuted;

		public static readonly VoiceSample Unavailable;
	}
}
namespace LethalSpectator.Config
{
	public static class ModConfig
	{
		public static ConfigEntry<bool> EnabledByDefault;

		public static ConfigEntry<string> ToggleKey;

		public static ConfigEntry<bool> ShowHudStatus;

		public static ConfigEntry<bool> ShowTips;

		public static ConfigEntry<bool> LogScores;

		public static ConfigEntry<float> LogScoresIntervalSeconds;

		public static ConfigEntry<float> SmoothingReleaseSeconds;

		public static ConfigEntry<float> EnvelopeHoldSeconds;

		public static ConfigEntry<float> SpeakingOnThreshold;

		public static ConfigEntry<float> SpeakingOffThreshold;

		public static ConfigEntry<float> MinSpeechBeforeSwitchSeconds;

		public static ConfigEntry<bool> IncludeLocallyMuted;

		public static ConfigEntry<float> MinHoldSeconds;

		public static ConfigEntry<float> InactivityTimeoutSeconds;

		public static ConfigEntry<float> LouderSwitchRatio;

		public static ConfigEntry<float> ManualHoldSeconds;

		public static ConfigEntry<bool> SnapToFirstSpeaker;

		public static ConfigEntry<bool> DangerEnabled;

		public static ConfigEntry<float> DangerRadiusMeters;

		public static ConfigEntry<float> DangerWeight;

		public static ConfigEntry<float> DangerSwitchDistance;

		public static ConfigEntry<float> DangerHoldSeconds;

		public static ConfigEntry<bool> KillerCamEnabled;

		public static ConfigEntry<float> KillerCamDurationSeconds;

		public static ConfigEntry<float> KillerCamMaxAttributionDistance;

		public static ConfigEntry<float> GroupRadiusMeters;

		public static ConfigEntry<float> GroupBonusPerNeighbor;

		public static ConfigEntry<float> GroupBonusCap;

		public static ConfigEntry<bool> RequireSameInterior;

		public static bool IsBound { get; private set; }

		public static void Bind(ConfigFile config)
		{
			EnabledByDefault = config.Bind<bool>("General", "EnabledByDefault", true, "Auto spectate starts ON when the game launches. The toggle key changes it for the rest of the session.");
			ToggleKey = config.Bind<string>("General", "ToggleKey", "H", "Fallback toggle key when InputUtils is not installed (UnityEngine.InputSystem.Key name, e.g. H, F6). With InputUtils, rebind in the in-game Keybinds menu instead.");
			ShowHudStatus = config.Bind<bool>("General", "ShowHudStatus", true, "Show the 'Auto spectate: ...' status line under the spectator controls and the [auto] marker next to the spectated name.");
			ShowTips = config.Bind<bool>("General", "ShowTips", true, "Show a short on-screen tip when auto spectate is toggled or paused.");
			LogScores = config.Bind<bool>("Debug", "LogScores", false, "Periodically log every candidate's voice level and score to the BepInEx log (for tuning thresholds).");
			LogScoresIntervalSeconds = config.Bind<float>("Debug", "LogScoresIntervalSeconds", 1f, "Seconds between score dumps when Debug/LogScores is on.");
			BindDecisionSections(config);
			IsBound = true;
		}

		private static void BindDecisionSectionsImpl(ConfigFile config)
		{
			SmoothingReleaseSeconds = config.Bind<float>("Voice", "SmoothingReleaseSeconds", 0.4f, "How quickly a player's smoothed voice level falls after they stop making sound (exponential release time constant).");
			EnvelopeHoldSeconds = config.Bind<float>("Voice", "EnvelopeHoldSeconds", 0.6f, "How long the smoothed voice level holds its peak before starting to fall (bridges the gaps between words).");
			SpeakingOnThreshold = config.Bind<float>("Voice", "SpeakingOnThreshold", 0.008f, "Smoothed, volume-normalized voice level above which a player counts as talking (the game's own HUD icon uses 0.005 raw).");
			SpeakingOffThreshold = config.Bind<float>("Voice", "SpeakingOffThreshold", 0.004f, "Level below which a talking player counts as quiet again (lower than the on-threshold so the state does not flicker).");
			MinSpeechBeforeSwitchSeconds = config.Bind<float>("Voice", "MinSpeechBeforeSwitchSeconds", 0.35f, "A player must have been talking this long before they can take the camera (rejects coughs and one-word blips).");
			IncludeLocallyMuted = config.Bind<bool>("Voice", "IncludeLocallyMuted", false, "Count players you have locally muted as talking (their voice level usually reads zero anyway).");
			MinHoldSeconds = config.Bind<float>("Switching", "MinHoldSeconds", 2.5f, "Never switch away from the current player sooner than this after any switch.");
			InactivityTimeoutSeconds = config.Bind<float>("Switching", "InactivityTimeoutSeconds", 2f, "Quiet time on the current player before another talker may take the camera.");
			LouderSwitchRatio = config.Bind<float>("Switching", "LouderSwitchRatio", 2f, "While the current player is still talking, a challenger takes the camera early only when their score is at least this many times higher.");
			ManualHoldSeconds = config.Bind<float>("Switching", "ManualHoldSeconds", 8f, "After you cycle players manually (left click), auto switching waits this long before taking over again.");
			SnapToFirstSpeaker = config.Bind<bool>("Switching", "SnapToFirstSpeaker", true, "When everyone has been quiet, jump to the first person who starts talking immediately so you hear the word from the start (an isolated cough can briefly take the camera).");
			DangerEnabled = config.Bind<bool>("Danger", "Enabled", true, "Prefer players who have an enemy close to them (most enemies attack by closing in).");
			DangerRadiusMeters = config.Bind<float>("Danger", "DangerRadiusMeters", 20f, "An enemy within this distance starts boosting a talking player's priority (closer = stronger).");
			DangerWeight = config.Bind<float>("Danger", "DangerWeight", 0.5f, "How strongly enemy proximity boosts a talking player's score (0 disables the boost, 1 doubles it at point blank).");
			DangerSwitchDistance = config.Bind<float>("Danger", "DangerSwitchDistance", 8f, "During total silence, switch to a player whose nearest enemy is within this distance.");
			DangerHoldSeconds = config.Bind<float>("Danger", "DangerHoldSeconds", 3f, "Minimum time between danger-only switches so two chased players do not ping-pong.");
			KillerCamEnabled = config.Bind<bool>("KillerCam", "Enabled", true, "When the player you are watching is killed by an enemy, follow that enemy for a moment before moving on.");
			KillerCamDurationSeconds = config.Bind<float>("KillerCam", "DurationSeconds", 3f, "How long the camera follows the killer.");
			KillerCamMaxAttributionDistance = config.Bind<float>("KillerCam", "MaxAttributionDistance", 10f, "When the killer is not directly known, blame the nearest living enemy within this distance of the victim.");
			GroupRadiusMeters = config.Bind<float>("Grouping", "GroupRadiusMeters", 10f, "Other players within this distance of a speaker count toward the group bonus (10 is the game's own near-player radius).");
			GroupBonusPerNeighbor = config.Bind<float>("Grouping", "GroupBonusPerNeighbor", 0.15f, "Score bonus per nearby player, making the camera prefer and hold speakers who are in a group.");
			GroupBonusCap = config.Bind<float>("Grouping", "GroupBonusCap", 0.45f, "Maximum total group bonus.");
			RequireSameInterior = config.Bind<bool>("Grouping", "RequireSameInterior", true, "Only count nearby players who are on the same side of the factory walls (inside vs outside).");
		}

		private static void BindDecisionSections(ConfigFile config)
		{
			BindDecisionSectionsImpl(config);
		}
	}
}