Decompiled source of BetterVC v1.0.0

plugins/bobabulkerENTERPRISE.BetterVC.dll

Decompiled 2 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
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 Dissonance;
using GameNetcodeStuff;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using TMPro;
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("bobabulkerENTERPRISE.BetterVC")]
[assembly: AssemblyConfiguration("Thunderstore")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+1325548ae24a331e2854162f633f3dff7a91fc61")]
[assembly: AssemblyProduct("BetterVC")]
[assembly: AssemblyTitle("bobabulkerENTERPRISE.BetterVC")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.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.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace BetterVC
{
	public enum HudVisibilityMode
	{
		Always,
		Never,
		WhenHavingWalkie,
		WhenHoldingWalkie
	}
	[BepInPlugin("bobabulkerENTERPRISE.BetterVC", "BetterVC", "1.0.0")]
	public class Plugin : BaseUnityPlugin
	{
		internal static ConfigEntry<float> MixLowerDistance;

		internal static ConfigEntry<float> MixUpperDistance;

		internal static ConfigEntry<bool> DebugLogging;

		internal static ConfigEntry<float> NearbySoundsVolume;

		internal static ConfigEntry<float> IdleChatterVolume;

		internal static ConfigEntry<float> RadioEffectStrength;

		internal static ConfigEntry<float> WalkieVolume;

		internal static ConfigEntry<HudVisibilityMode> HudVisibility;

		internal static ConfigEntry<float> HudScale;

		private Harmony? harmony;

		internal static bool RunnerLost;

		internal static ConfigFile? ConfigRef;

		private static FileSystemWatcher? configWatcher;

		private static volatile bool configChanged;

		private static DateTime configReloadAt;

		private static DateTime ignoreUntil;

		private static bool compatChecked;

		private static int errorCount;

		internal static Plugin Instance { get; private set; }

		internal static ManualLogSource Logger { get; private set; }

		private void Awake()
		{
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Expected O, but got Unknown
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Expected O, but got Unknown
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Expected O, but got Unknown
			//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0100: Expected O, but got Unknown
			//IL_0133: Unknown result type (might be due to invalid IL or missing references)
			//IL_013d: Expected O, but got Unknown
			//IL_0170: Unknown result type (might be due to invalid IL or missing references)
			//IL_017a: Expected O, but got Unknown
			//IL_0264: Unknown result type (might be due to invalid IL or missing references)
			//IL_026e: Expected O, but got Unknown
			Instance = this;
			Logger = ((BaseUnityPlugin)this).Logger;
			MixLowerDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Routing", "MixLowerDistance", 20f, new ConfigDescription("Players closer than this (meters) are heard in person.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 200f), Array.Empty<object>()));
			MixUpperDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Routing", "MixUpperDistance", 25f, new ConfigDescription("Players farther than this (meters) are heard fully through the walkie. In between, the two blend.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 300f), Array.Empty<object>()));
			WalkieVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Walkie Audio", "WalkieVolume", 0.75f, new ConfigDescription("Volume of the walkie voice and of your own walkie's beeps. 1 = normal voice volume.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>()));
			NearbySoundsVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Walkie Audio", "NearbySoundsVolume", 0.5f, new ConfigDescription("Volume of nearby sounds that transmitting walkies repeat (footsteps, doors, music). 0 = off.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			IdleChatterVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Walkie Audio", "IdleChatterVolume", 0.5f, new ConfigDescription("Volume of the faint chatter from powered-on walkies that are not yours. 0 = off.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			RadioEffectStrength = ((BaseUnityPlugin)this).Config.Bind<float>("Walkie Audio", "RadioEffectStrength", 0.7f, new ConfigDescription("How radio-like a walkie voice sounds. 0 = crisp, like in person. 1 = the game's full walkie sound.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			DebugLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugLogging", false, "Write detailed audio and routing diagnostics to the log. Turn on when reporting a bug.");
			ConfigRef = ((BaseUnityPlugin)this).Config;
			MixLowerDistance.SettingChanged += OnMixChanged;
			MixUpperDistance.SettingChanged += OnMixChanged;
			HudVisibility = ((BaseUnityPlugin)this).Config.Bind<HudVisibilityMode>("HUD", "Visibility", HudVisibilityMode.Always, "When the list of talking players is shown.");
			HudScale = ((BaseUnityPlugin)this).Config.Bind<float>("HUD", "HudScale", 1f, "Size of the list of talking players. 1 = default.");
			((BaseUnityPlugin)this).Config.SettingChanged += OnAnySettingChanged;
			harmony = new Harmony("bobabulkerENTERPRISE.BetterVC");
			harmony.PatchAll();
			StartConfigWatcher();
			Runner.Create();
			Logger.LogInfo((object)"bobabulkerENTERPRISE.BetterVC v1.0.0 has loaded!");
		}

		private static void OnAnySettingChanged(object? sender, SettingChangedEventArgs e)
		{
			ConfigEntryBase changedSetting = e.ChangedSetting;
			if (changedSetting != null)
			{
				Logger.LogInfo((object)$"[config] {changedSetting.Definition.Section} / {changedSetting.Definition.Key} is now {changedSetting.BoxedValue}, applied immediately");
			}
			VoiceRouter.MarkDirty();
		}

		private static void OnMixChanged(object? sender, EventArgs e)
		{
			Logger.LogInfo((object)$"[config] mix distances now: in person below {MixLowerDistance.Value:F1}m, distortion ramps 0-100% up to {VoiceRouter.MixUpper:F1}m, full walkie beyond");
		}

		private void StartConfigWatcher()
		{
			try
			{
				string configFilePath = ((BaseUnityPlugin)this).Config.ConfigFilePath;
				configWatcher = new FileSystemWatcher(Path.GetDirectoryName(configFilePath), Path.GetFileName(configFilePath))
				{
					NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite)
				};
				FileSystemEventHandler handler = delegate
				{
					if (!(DateTime.UtcNow < ignoreUntil))
					{
						configReloadAt = DateTime.UtcNow.AddMilliseconds(300.0);
						configChanged = true;
					}
				};
				configWatcher.Changed += handler;
				configWatcher.Created += handler;
				configWatcher.Renamed += delegate(object s, RenamedEventArgs e)
				{
					handler(s, e);
				};
				configWatcher.EnableRaisingEvents = true;
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)("[config] could not watch the config file for edits (" + ex.Message + "); restart the game to apply edits"));
			}
		}

		private static void ReloadConfigIfChanged()
		{
			if (!configChanged || ConfigRef == null || DateTime.UtcNow < configReloadAt)
			{
				return;
			}
			configChanged = false;
			try
			{
				ConfigRef.Reload();
				ignoreUntil = DateTime.UtcNow.AddSeconds(1.0);
				Logger.LogInfo((object)"[config] config file changed on disk, reloaded");
				VoiceRouter.MarkDirty();
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)("[config] could not reload the config file: " + ex.Message));
			}
		}

		internal static void Debug(string message)
		{
			if (DebugLogging != null && DebugLogging.Value)
			{
				Logger.LogInfo((object)message);
			}
		}

		private void Update()
		{
			FrameDriver.Tick("Plugin.Update");
		}

		private void OnGUI()
		{
			FrameDriver.Draw("Plugin.OnGUI");
		}

		private void OnDisable()
		{
			Debug("[driver] the plugin component was DISABLED (harmless: the other drivers keep the mod running)");
		}

		private void OnDestroy()
		{
			Debug("[driver] the plugin component was DESTROYED (harmless: the other drivers keep the mod running)");
		}

		internal static void PerFrame(float dt)
		{
			if (RunnerLost)
			{
				RunnerLost = false;
				Runner.Create();
			}
			try
			{
				if (!compatChecked)
				{
					compatChecked = true;
					Compat.ReportOverlaps("bobabulkerENTERPRISE.BetterVC");
				}
				ReloadConfigIfChanged();
				VoiceRouter.ResetIfNewLobby();
				VoiceRouter.TickIfDirty();
				VoiceRouter.UpdateBlend(dt);
				VoiceHud.Sample(dt);
				VoiceRouter.LogSnapshot(dt);
			}
			catch (Exception arg)
			{
				if (errorCount++ < 5)
				{
					Logger.LogError((object)$"Per-frame update failed ({errorCount}): {arg}");
				}
			}
		}
	}
	internal static class AutoTransmit
	{
		private static readonly FieldInfo? HolderField = AccessTools.Field(typeof(WalkieTalkie), "previousPlayerHeldBy");

		private static bool on;

		private static bool sending;

		private static int lastPlayers = -1;

		private static bool warnedOwner;

		private static bool warnedField;

		private static bool warnedAnnounce;

		private const float AnnounceInterval = 10f;

		private const float BurstInterval = 3f;

		private const float BurstDuration = 30f;

		private static float nextAnnounce;

		private static float burstUntil;

		private static bool everTransmitted;

		internal static bool Wanted => on;

		internal static bool Sending => sending;

		internal static void Reset()
		{
			on = false;
			sending = false;
			lastPlayers = -1;
			nextAnnounce = 0f;
			burstUntil = 0f;
			everTransmitted = false;
			warnedOwner = false;
			warnedAnnounce = false;
		}

		internal static void Tick()
		{
			GameNetworkManager instance = GameNetworkManager.Instance;
			StartOfRound instance2 = StartOfRound.Instance;
			PlayerControllerB val = (((Object)(object)instance != (Object)null) ? instance.localPlayerController : null);
			if ((Object)(object)instance2 == (Object)null || (Object)(object)val == (Object)null)
			{
				return;
			}
			if (HolderField == null)
			{
				if (!warnedField)
				{
					warnedField = true;
					Plugin.Logger.LogWarning((object)(Clock.Stamp() + " [auto] WalkieTalkie.previousPlayerHeldBy was not found (game changed?). Automatic transmit is off; the walkie must be used by hand."));
				}
				return;
			}
			int num = 0;
			PlayerControllerB[] allPlayerScripts = instance2.allPlayerScripts;
			foreach (PlayerControllerB val2 in allPlayerScripts)
			{
				if ((Object)(object)val2 != (Object)null && val2.isPlayerControlled)
				{
					num++;
				}
			}
			string where;
			WalkieTalkie val3 = (val.isPlayerDead ? null : VoiceRouter.FindPoweredWalkie(val, out where));
			float unscaledTime = Time.unscaledTime;
			bool flag = num > 1;
			bool flag2 = lastPlayers >= 0 && num != lastPlayers;
			if (flag2)
			{
				burstUntil = unscaledTime + 30f;
				nextAnnounce = 0f;
			}
			float num2 = ((unscaledTime < burstUntil) ? 3f : 10f);
			if ((Object)(object)val3 != (Object)null)
			{
				val.holdingWalkieTalkie = true;
				if (!on || !val.speakingToWalkieTalkie)
				{
					Send(speaking: true, val3, val, on);
					nextAnnounce = unscaledTime + num2;
				}
				else if (flag && unscaledTime >= nextAnnounce)
				{
					Announce(speaking: true, val3, val);
					nextAnnounce = unscaledTime + num2;
					if (flag2)
					{
						Plugin.Debug($"{Clock.Stamp()} [auto] player count changed ({lastPlayers} -> {num}); transmit state re-sent (and will repeat for {30f:F0}s) so new players see it");
					}
				}
			}
			else
			{
				if (!val.isPlayerDead && val.holdingWalkieTalkie)
				{
					val.holdingWalkieTalkie = false;
				}
				if (on)
				{
					Send(speaking: false, FindCarrier(val), val, resend: false);
					nextAnnounce = unscaledTime + num2;
				}
				else if (flag && everTransmitted && unscaledTime >= nextAnnounce)
				{
					Announce(speaking: false, FindCarrier(val), val);
					nextAnnounce = unscaledTime + num2;
				}
			}
			lastPlayers = num;
		}

		private static void Announce(bool speaking, WalkieTalkie? carrier, PlayerControllerB local)
		{
			if ((Object)(object)carrier == (Object)null || !((NetworkBehaviour)carrier).IsOwner)
			{
				return;
			}
			try
			{
				int num = (int)local.playerClientId;
				if (speaking)
				{
					carrier.SetPlayerSpeakingOnWalkieTalkieServerRpc(num);
				}
				else
				{
					carrier.UnsetPlayerSpeakingOnWalkieTalkieServerRpc(num);
				}
			}
			catch (Exception ex)
			{
				if (!warnedAnnounce)
				{
					warnedAnnounce = true;
					Plugin.Logger.LogWarning((object)(Clock.Stamp() + " [auto] could not re-send the transmit state (" + ex.Message + "); other clients that missed the first message will not catch up"));
				}
			}
		}

		private static WalkieTalkie? FindCarrier(PlayerControllerB local)
		{
			List<WalkieTalkie> allWalkieTalkies = WalkieTalkie.allWalkieTalkies;
			if (allWalkieTalkies == null)
			{
				return null;
			}
			WalkieTalkie val = null;
			for (int i = 0; i < allWalkieTalkies.Count; i++)
			{
				WalkieTalkie val2 = allWalkieTalkies[i];
				if (!((Object)(object)val2 == (Object)null) && ((NetworkBehaviour)val2).IsOwner)
				{
					if ((Object)(object)((GrabbableObject)val2).playerHeldBy == (Object)(object)local)
					{
						return val2;
					}
					if (val == null)
					{
						val = val2;
					}
				}
			}
			return val;
		}

		private static void Send(bool speaking, WalkieTalkie? carrier, PlayerControllerB local, bool resend)
		{
			if ((Object)(object)carrier == (Object)null || !((NetworkBehaviour)carrier).IsOwner)
			{
				if (speaking && !warnedOwner)
				{
					warnedOwner = true;
					Plugin.Logger.LogWarning((object)(Clock.Stamp() + " [auto] this client does not own its walkie, so it cannot start transmitting yet (will retry)"));
				}
				if (!speaking)
				{
					local.speakingToWalkieTalkie = false;
					on = false;
					Plugin.Logger.LogWarning((object)(Clock.Stamp() + " [auto] no walkie to send the stop message through; cleared the flag locally only"));
				}
				return;
			}
			warnedOwner = false;
			try
			{
				HolderField.SetValue(carrier, local);
				if (resend || (speaking && local.speakingToWalkieTalkie))
				{
					local.speakingToWalkieTalkie = !speaking;
				}
				sending = true;
				carrier.SetLocalClientSpeaking(speaking);
			}
			finally
			{
				sending = false;
			}
			if (on != speaking)
			{
				Plugin.Debug(Clock.Stamp() + " [auto] " + (speaking ? "now transmitting (powered walkie carried)" : "stopped transmitting (no powered walkie)"));
			}
			on = speaking;
			if (speaking)
			{
				everTransmitted = true;
			}
		}
	}
	internal static class Clock
	{
		private static bool? lastWasNetwork;

		internal static string Stamp()
		{
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			NetworkManager singleton = NetworkManager.Singleton;
			bool flag = (Object)(object)singleton != (Object)null && singleton.IsListening;
			if (lastWasNetwork != flag)
			{
				lastWasNetwork = flag;
				Plugin.Debug(flag ? "[clock] log lines are now stamped [S...] with the network's server time (comparable across players' logs)" : "[clock] log lines are stamped [L...] with this machine's local time (not comparable across players' logs)");
			}
			if (!flag)
			{
				return $"[L{Time.realtimeSinceStartupAsDouble:F2}]";
			}
			NetworkTime serverTime = singleton.ServerTime;
			return $"[S{((NetworkTime)(ref serverTime)).Time:F2}]";
		}
	}
	internal static class Compat
	{
		internal static void ReportOverlaps(string ownHarmonyId)
		{
			try
			{
				List<string> list = new List<string>();
				foreach (MethodBase item in Harmony.GetAllPatchedMethods().ToList())
				{
					Patches patchInfo = Harmony.GetPatchInfo(item);
					if (patchInfo == null)
					{
						continue;
					}
					HashSet<string> hashSet = new HashSet<string>(patchInfo.Owners);
					if (hashSet.Remove(ownHarmonyId) && hashSet.Count != 0)
					{
						list.Add(item.DeclaringType?.Name + "." + item.Name + ": also patched by " + string.Join(", ", hashSet.OrderBy((string o) => o)));
					}
				}
				if (list.Count == 0)
				{
					Plugin.Debug("[compat] no other plugin patches a game method that this mod patches");
					return;
				}
				Plugin.Logger.LogInfo((object)$"[compat] {list.Count} game method(s) are patched by this mod and by other plugins. This is normal and usually fine; if voices sound wrong, include these lines in a bug report:");
				foreach (string item2 in list.OrderBy((string l) => l))
				{
					Plugin.Logger.LogInfo((object)("[compat]   " + item2));
				}
			}
			catch (Exception ex)
			{
				Plugin.Debug("[compat] could not list overlapping patches: " + ex.Message);
			}
		}
	}
	internal static class FrameDriver
	{
		private static readonly HashSet<string> SeenUpdate = new HashSet<string>();

		private static readonly HashSet<string> SeenGui = new HashSet<string>();

		private static int lastUpdateFrame = -1;

		private static int lastRepaintFrame = -1;

		private static bool guiFailed;

		internal static void Tick(string source)
		{
			if (SeenUpdate.Add(source))
			{
				Plugin.Debug("[driver] update driver '" + source + "' is running");
			}
			int frameCount = Time.frameCount;
			if (frameCount != lastUpdateFrame)
			{
				lastUpdateFrame = frameCount;
				Plugin.PerFrame(Time.unscaledDeltaTime);
			}
		}

		internal static void Draw(string source)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Invalid comparison between Unknown and I4
			Event current = Event.current;
			if (current == null || (int)current.type != 7)
			{
				return;
			}
			if (SeenGui.Add(source))
			{
				Plugin.Debug("[driver] OnGUI driver '" + source + "' is running");
			}
			int frameCount = Time.frameCount;
			if (frameCount == lastRepaintFrame)
			{
				return;
			}
			lastRepaintFrame = frameCount;
			if (guiFailed)
			{
				return;
			}
			try
			{
				VoiceHud.Draw();
			}
			catch (Exception arg)
			{
				guiFailed = true;
				Plugin.Logger.LogError((object)$"HUD drawing failed, disabling it: {arg}");
			}
		}
	}
	internal sealed class Runner : MonoBehaviour
	{
		private static Runner? instance;

		private static bool quitting;

		internal static void Create()
		{
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Expected O, but got Unknown
			if ((Object)(object)instance != (Object)null)
			{
				return;
			}
			if (!quitting)
			{
				Application.quitting += delegate
				{
					quitting = true;
				};
			}
			GameObject val = new GameObject("BetterVC.Runner");
			((Object)val).hideFlags = (HideFlags)61;
			Object.DontDestroyOnLoad((Object)(object)val);
			instance = val.AddComponent<Runner>();
		}

		private void Update()
		{
			FrameDriver.Tick("Runner.Update");
		}

		private void OnGUI()
		{
			FrameDriver.Draw("Runner.OnGUI");
		}

		private void OnDestroy()
		{
			instance = null;
			if (!quitting)
			{
				Plugin.Logger.LogWarning((object)"[driver] the Runner object was destroyed by something (it will be recreated)");
				Plugin.RunnerLost = true;
			}
		}
	}
	internal enum VoiceBand
	{
		InPerson,
		Mixed,
		Walkie
	}
	internal static class VoiceHud
	{
		private sealed class Entry
		{
			public string Name = "";

			public VoiceBand Band;

			public float Y = float.NaN;

			public float Presence;

			public int LastFrame;
		}

		private const float FadeOutSeconds = 0.4f;

		private const float SlideRate = 12f;

		private const float MinAmplitude = 0.002f;

		private static readonly Dictionary<ulong, Entry> Entries = new Dictionary<ulong, Entry>();

		private static readonly List<ulong> SortedIds = new List<ulong>();

		private static GUIStyle? nameStyle;

		private static Font? gameFont;

		private static bool fontSearched;

		private static float debugTimer;

		private static bool Allowed(PlayerControllerB listener)
		{
			switch (Plugin.HudVisibility.Value)
			{
			case HudVisibilityMode.Never:
				return false;
			case HudVisibilityMode.WhenHavingWalkie:
				return VoiceRouter.HasActiveWalkie(listener);
			case HudVisibilityMode.WhenHoldingWalkie:
			{
				GrabbableObject currentlyHeldObjectServer = listener.currentlyHeldObjectServer;
				WalkieTalkie val = (WalkieTalkie)(object)((currentlyHeldObjectServer is WalkieTalkie) ? currentlyHeldObjectServer : null);
				if (val != null)
				{
					return (Object)(object)val != (Object)null;
				}
				return false;
			}
			default:
				return true;
			}
		}

		internal static VoiceBand? BandFor(VoiceMode mode, float distance)
		{
			switch (mode)
			{
			case VoiceMode.Spatial:
				return VoiceBand.InPerson;
			case VoiceMode.Blend:
				return VoiceBand.Mixed;
			case VoiceMode.Walkie:
				return VoiceBand.Walkie;
			default:
				if (!(distance <= Plugin.MixLowerDistance.Value))
				{
					return null;
				}
				return VoiceBand.InPerson;
			}
		}

		internal static void Reset()
		{
			Entries.Clear();
			debugTimer = 0f;
		}

		internal static void Sample(float dt)
		{
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			StartOfRound instance = StartOfRound.Instance;
			PlayerControllerB val = VoiceRouter.CurrentListener();
			if ((Object)(object)instance == (Object)null || (Object)(object)val == (Object)null || !Allowed(val))
			{
				Entries.Clear();
				return;
			}
			debugTimer += dt;
			bool flag = Plugin.DebugLogging.Value && debugTimer >= 1f;
			if (flag)
			{
				debugTimer = 0f;
			}
			PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts;
			foreach (PlayerControllerB val2 in allPlayerScripts)
			{
				if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2 == (Object)(object)val) && val2.isPlayerControlled && !val2.isPlayerDead)
				{
					VoicePlayerState voicePlayerState = val2.voicePlayerState;
					float distance = Vector3.Distance(((Component)val).transform.position, ((Component)val2).transform.position);
					VoiceBand? voiceBand = BandFor(VoiceRouter.ModeFor(val2), distance);
					bool flag2 = voiceBand.HasValue && voicePlayerState != null && voicePlayerState.IsSpeaking && voicePlayerState.Amplitude > 0.002f;
					float num = (flag2 ? voicePlayerState.Amplitude : 0f);
					string text = (string.IsNullOrEmpty(val2.playerUsername) ? $"Player {val2.playerClientId}" : val2.playerUsername);
					Feed(val2.playerClientId, text, flag2, voiceBand.GetValueOrDefault(), dt);
					if (flag && flag2)
					{
						Plugin.Debug($"{Clock.Stamp()} [hud] {text}: {voiceBand}, raw amplitude={num:F4}");
					}
				}
			}
			List<ulong> list = null;
			foreach (KeyValuePair<ulong, Entry> entry in Entries)
			{
				Entry value = entry.Value;
				if (value.LastFrame != Time.frameCount)
				{
					value.Presence -= dt / 0.4f;
				}
				if (value.Presence <= 0f)
				{
					(list ?? (list = new List<ulong>())).Add(entry.Key);
				}
			}
			if (list == null)
			{
				return;
			}
			foreach (ulong item in list)
			{
				Entries.Remove(item);
			}
		}

		internal static void Feed(ulong id, string name, bool speaking, VoiceBand band, float dt)
		{
			if (!Entries.TryGetValue(id, out Entry value))
			{
				if (!speaking)
				{
					return;
				}
				value = new Entry();
				Entries[id] = value;
			}
			value.LastFrame = Time.frameCount;
			value.Name = name;
			if (speaking)
			{
				value.Band = band;
			}
			if (speaking)
			{
				value.Presence = 1f;
			}
			else
			{
				value.Presence -= dt / 0.4f;
			}
		}

		internal static void Draw()
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Invalid comparison between Unknown and I4
			//IL_01fd: Unknown result type (might be due to invalid IL or missing references)
			//IL_0164: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_01cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d3: Unknown result type (might be due to invalid IL or missing references)
			if ((int)Event.current.type != 7)
			{
				return;
			}
			float num = (float)Screen.height / 1080f * Plugin.HudScale.Value;
			if (Entries.Count != 0)
			{
				EnsureNameStyle();
				float num2 = 34f * num;
				float num3 = 24f * num;
				SortedIds.Clear();
				SortedIds.AddRange(Entries.Keys);
				SortedIds.Sort(delegate(ulong a, ulong b)
				{
					int num8 = Entries[a].Band.CompareTo(Entries[b].Band);
					return (num8 == 0) ? a.CompareTo(b) : num8;
				});
				float num4 = ((float)Screen.height - (float)SortedIds.Count * num2) / 2f;
				float num5 = 1f - Mathf.Exp(-12f * Time.unscaledDeltaTime);
				nameStyle.fontSize = Mathf.Max(10, Mathf.RoundToInt(24f * num));
				Rect val = default(Rect);
				for (int num6 = 0; num6 < SortedIds.Count; num6++)
				{
					Entry entry = Entries[SortedIds[num6]];
					float num7 = num4 + (float)num6 * num2;
					entry.Y = (float.IsNaN(entry.Y) ? num7 : Mathf.Lerp(entry.Y, num7, num5));
					((Rect)(ref val))..ctor(num3, entry.Y, 520f * num, num2);
					GUI.color = new Color(0f, 0f, 0f, 0.85f * entry.Presence);
					GUI.Label(new Rect(((Rect)(ref val)).x + num * 1.5f, ((Rect)(ref val)).y + num * 1.5f, ((Rect)(ref val)).width, ((Rect)(ref val)).height), entry.Name, nameStyle);
					Color color = NameColor(entry.Band);
					color.a = entry.Presence;
					GUI.color = color;
					GUI.Label(val, entry.Name, nameStyle);
				}
				GUI.color = Color.white;
			}
		}

		private static Color NameColor(VoiceBand band)
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			return (Color)(band switch
			{
				VoiceBand.Mixed => new Color(1f, 0.9f, 0.2f), 
				VoiceBand.Walkie => new Color(0.3f, 1f, 0.4f), 
				_ => Color.white, 
			});
		}

		private static Font? FindGameFont()
		{
			try
			{
				TMP_FontAsset[] array = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
				foreach (TMP_FontAsset val in array)
				{
					if ((Object)(object)val != (Object)null && ((Object)val).name.IndexOf("3270", StringComparison.OrdinalIgnoreCase) >= 0 && (Object)(object)val.sourceFontFile != (Object)null)
					{
						Plugin.Debug("[hud] using the game's font '" + ((Object)val.sourceFontFile).name + "' (from '" + ((Object)val).name + "')");
						return val.sourceFontFile;
					}
				}
				Font[] array2 = Resources.FindObjectsOfTypeAll<Font>();
				foreach (Font val2 in array2)
				{
					if ((Object)(object)val2 != (Object)null && ((Object)val2).name.IndexOf("3270", StringComparison.OrdinalIgnoreCase) >= 0)
					{
						Plugin.Debug("[hud] using the game's font '" + ((Object)val2).name + "'");
						return val2;
					}
				}
				Plugin.Logger.LogWarning((object)"[hud] the game's 3270 font was not found as a usable Font; using a monospace system font instead");
			}
			catch (Exception ex)
			{
				Plugin.Logger.LogWarning((object)("[hud] font lookup failed (" + ex.Message + "); using a monospace system font instead"));
			}
			return Font.CreateDynamicFontFromOSFont(new string[4] { "Consolas", "Lucida Console", "DejaVu Sans Mono", "Courier New" }, 24);
		}

		private static void EnsureNameStyle()
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Expected O, but got Unknown
			if (nameStyle == null)
			{
				if (!fontSearched)
				{
					fontSearched = true;
					gameFont = FindGameFont();
				}
				nameStyle = new GUIStyle(GUI.skin.label)
				{
					alignment = (TextAnchor)3,
					fontStyle = (FontStyle)0,
					wordWrap = false,
					clipping = (TextClipping)0
				};
				if ((Object)(object)gameFont != (Object)null)
				{
					nameStyle.font = gameFont;
				}
			}
		}
	}
	internal enum VoiceMode
	{
		Vanilla,
		Spatial,
		Blend,
		Walkie
	}
	internal static class VoiceRouter
	{
		private sealed class Link
		{
			public float Radio;

			public VoiceMode Mode;

			public VoiceMode Applied;

			public float OcclusionCutoff = 22000f;

			public float LastBlend = -1f;

			public float AppliedStrength = -1f;

			public int FlipFrame = -100;
		}

		private struct Fight
		{
			public int Count;

			public float WindowStart;

			public float PausedUntil;
		}

		private const float RadioLowPass = 4000f;

		private const float RadioQ = 3f;

		private const float OpenLowPass = 22000f;

		private static readonly Dictionary<ulong, Link> Links = new Dictionary<ulong, Link>();

		private static readonly Dictionary<ulong, float> HighPassDefaults = new Dictionary<ulong, float>();

		private static readonly Dictionary<ulong, float> SpreadDefaults = new Dictionary<ulong, float>();

		private const float SpreadDoneAtRadio = 0.75f;

		private static int lastPostfixFrame = -1;

		private static bool warnedBlendOverwritten;

		private static string lastCensus = "";

		private const float MaxRadioRate = 1.5f;

		private static bool dirty = true;

		private static bool inTick;

		private static StartOfRound? knownRound;

		private static bool hadRound;

		private static readonly Dictionary<ulong, string> SeenWalkie = new Dictionary<ulong, string>();

		private static float snapshotTimer;

		private static int driftCount;

		private const int FightLimit = 6;

		private const float FightWindow = 4f;

		private const float FightPause = 30f;

		private static readonly Dictionary<ulong, Fight> Fights = new Dictionary<ulong, Fight>();

		internal static float WalkieVolume => Plugin.WalkieVolume.Value;

		internal static float RadioEffectStrength => Mathf.Clamp01(Plugin.RadioEffectStrength.Value);

		internal static float MixUpper => Mathf.Max(Plugin.MixLowerDistance.Value + 0.5f, Plugin.MixUpperDistance.Value);

		private static float SpreadFor(ulong id, float radio)
		{
			float value;
			return Mathf.Lerp(SpreadDefaults.TryGetValue(id, out value) ? value : 0f, 180f, Ease(Mathf.Clamp01(radio / 0.75f)));
		}

		internal static VoiceMode ModeFor(PlayerControllerB player)
		{
			if (!Links.TryGetValue(player.playerClientId, out Link value))
			{
				return VoiceMode.Vanilla;
			}
			return value.Mode;
		}

		internal static float RadioAmountFor(PlayerControllerB player)
		{
			if (!Links.TryGetValue(player.playerClientId, out Link value))
			{
				return 0f;
			}
			return value.Radio;
		}

		internal static bool HasActiveWalkie(PlayerControllerB? player)
		{
			return FindActiveWalkie(player) != null;
		}

		internal static string? FindActiveWalkie(PlayerControllerB? player)
		{
			FindPoweredWalkie(player, out string where);
			return where;
		}

		internal static WalkieTalkie? FindPoweredWalkie(PlayerControllerB? player, out string? where)
		{
			where = null;
			if ((Object)(object)player == (Object)null)
			{
				return null;
			}
			GrabbableObject itemOnlySlot = player.ItemOnlySlot;
			WalkieTalkie val = (WalkieTalkie)(object)((itemOnlySlot is WalkieTalkie) ? itemOnlySlot : null);
			if (val != null && (Object)(object)val != (Object)null && ((GrabbableObject)val).isBeingUsed)
			{
				where = "utility slot";
				return val;
			}
			GrabbableObject[] itemSlots = player.ItemSlots;
			if (itemSlots != null)
			{
				for (int i = 0; i < itemSlots.Length; i++)
				{
					GrabbableObject obj = itemSlots[i];
					WalkieTalkie val2 = (WalkieTalkie)(object)((obj is WalkieTalkie) ? obj : null);
					if (val2 != null && (Object)(object)val2 != (Object)null && ((GrabbableObject)val2).isBeingUsed)
					{
						where = $"slot {i + 1}";
						return val2;
					}
				}
			}
			List<WalkieTalkie> allWalkieTalkies = WalkieTalkie.allWalkieTalkies;
			if (allWalkieTalkies != null)
			{
				for (int j = 0; j < allWalkieTalkies.Count; j++)
				{
					WalkieTalkie val3 = allWalkieTalkies[j];
					if ((Object)(object)val3 != (Object)null && ((GrabbableObject)val3).isBeingUsed && (Object)(object)((GrabbableObject)val3).playerHeldBy == (Object)(object)player && !((GrabbableObject)val3).isHeldByEnemy)
					{
						where = "carried (extra slot)";
						return val3;
					}
				}
			}
			return null;
		}

		private static string SlotOf(PlayerControllerB player, WalkieTalkie walkie)
		{
			if ((Object)(object)player.ItemOnlySlot == (Object)(object)walkie)
			{
				return "utility slot";
			}
			GrabbableObject[] itemSlots = player.ItemSlots;
			if (itemSlots != null)
			{
				for (int i = 0; i < itemSlots.Length; i++)
				{
					if ((Object)(object)itemSlots[i] == (Object)(object)walkie)
					{
						return $"slot {i + 1}/{itemSlots.Length}";
					}
				}
			}
			return "NOT in ItemSlots (extra-slot mod?)";
		}

		private static void LogCensus()
		{
			List<WalkieTalkie> allWalkieTalkies = WalkieTalkie.allWalkieTalkies;
			if (allWalkieTalkies == null)
			{
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 0; i < allWalkieTalkies.Count; i++)
			{
				WalkieTalkie val = allWalkieTalkies[i];
				if (!((Object)(object)val == (Object)null) && !((Object)(object)((GrabbableObject)val).playerHeldBy == (Object)null))
				{
					PlayerControllerB playerHeldBy = ((GrabbableObject)val).playerHeldBy;
					if (stringBuilder.Length > 0)
					{
						stringBuilder.Append("; ");
					}
					stringBuilder.Append(string.Format("{0}#{1} {2} {3} @ {4}, transmit flag={5}", playerHeldBy.playerUsername, playerHeldBy.playerClientId, ((GrabbableObject)val).isBeingUsed ? "ON" : "off", ((GrabbableObject)val).isPocketed ? "pocketed" : "in hand", SlotOf(playerHeldBy, val), playerHeldBy.speakingToWalkieTalkie));
				}
			}
			string text = ((stringBuilder.Length == 0) ? "nobody is carrying a walkie" : stringBuilder.ToString());
			if (!(text == lastCensus))
			{
				lastCensus = text;
				Plugin.Debug(Clock.Stamp() + " [census] walkies as seen by this client: " + text);
			}
		}

		internal static float TargetRadio(float distance)
		{
			float value = Plugin.MixLowerDistance.Value;
			return Mathf.InverseLerp(value, MixUpper, distance);
		}

		internal static VoiceMode Classify(float radio)
		{
			if (radio <= 0f)
			{
				return VoiceMode.Spatial;
			}
			if (radio >= 1f)
			{
				return VoiceMode.Walkie;
			}
			return VoiceMode.Blend;
		}

		internal static float SmoothRadio(float current, float target, float dt)
		{
			float num = Mathf.Lerp(current, target, 1f - Mathf.Exp(-6f * dt));
			float num2 = 1.5f * dt;
			num = current + Mathf.Clamp(num - current, 0f - num2, num2);
			if (!(Mathf.Abs(num - target) < 0.004f))
			{
				return num;
			}
			return target;
		}

		private static float Ease(float t)
		{
			t = Mathf.Clamp01(t);
			return t * t * (3f - 2f * t);
		}

		internal static float LogLerp(float from, float to, float t)
		{
			if (from <= 0f || to <= 0f)
			{
				return Mathf.Lerp(from, to, t);
			}
			return from * Mathf.Pow(to / from, Mathf.Clamp01(t));
		}

		internal static float LogLerpUnclamped(float from, float to, float t)
		{
			if (from <= 0f || to <= 0f)
			{
				return Mathf.LerpUnclamped(from, to, t);
			}
			return from * Mathf.Pow(to / from, t);
		}

		private static float BaseVolume(bool localDead)
		{
			if (!localDead)
			{
				return 1f;
			}
			return 0.8f;
		}

		private static PlayerControllerB? GetListener(PlayerControllerB local)
		{
			if (!local.isPlayerDead || !((Object)(object)local.spectatedPlayerScript != (Object)null))
			{
				return local;
			}
			return local.spectatedPlayerScript;
		}

		internal static PlayerControllerB? CurrentListener()
		{
			PlayerControllerB local = GetLocal();
			if (!((Object)(object)local == (Object)null))
			{
				return GetListener(local);
			}
			return null;
		}

		private static PlayerControllerB? GetLocal()
		{
			GameNetworkManager instance = GameNetworkManager.Instance;
			if (!((Object)(object)instance != (Object)null))
			{
				return null;
			}
			return instance.localPlayerController;
		}

		private static bool IsRoutable(PlayerControllerB p, PlayerControllerB local, PlayerControllerB listener)
		{
			if ((Object)(object)p == (Object)(object)local || (Object)(object)p == (Object)(object)listener)
			{
				return false;
			}
			if (!p.isPlayerControlled && !p.isPlayerDead)
			{
				return false;
			}
			if (p.isPlayerDead)
			{
				return false;
			}
			if (p.voicePlayerState == null || (Object)(object)p.currentVoiceChatAudioSource == (Object)null || (Object)(object)p.currentVoiceChatIngameSettings == (Object)null)
			{
				return false;
			}
			if (p.currentVoiceChatIngameSettings._playerState != p.voicePlayerState)
			{
				StartOfRound.Instance.RefreshPlayerVoicePlaybackObjects();
				if (p.voicePlayerState == null || p.currentVoiceChatIngameSettings._playerState != p.voicePlayerState)
				{
					if (Plugin.DebugLogging.Value)
					{
						Plugin.Logger.LogWarning((object)$"{Clock.Stamp()} [voice] {p.playerUsername} (#{p.playerClientId}): voice state does not match the audio source even after a refresh; leaving them to the base game");
					}
					return false;
				}
				Plugin.Debug($"{Clock.Stamp()} [voice] {p.playerUsername} (#{p.playerClientId}): stale voice state replaced after a rejoin");
			}
			return true;
		}

		internal static void ResetIfNewLobby()
		{
			StartOfRound instance = StartOfRound.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				if (hadRound)
				{
					hadRound = false;
					knownRound = null;
					ResetState("lobby ended");
				}
			}
			else if (instance != knownRound)
			{
				bool flag = hadRound;
				knownRound = instance;
				hadRound = true;
				if (flag)
				{
					ResetState("a different lobby started");
				}
			}
		}

		private static void ResetState(string why)
		{
			Links.Clear();
			HighPassDefaults.Clear();
			SpreadDefaults.Clear();
			SeenWalkie.Clear();
			lastCensus = "";
			lastPostfixFrame = -1;
			snapshotTimer = 0f;
			driftCount = 0;
			Fights.Clear();
			dirty = true;
			inTick = false;
			AutoTransmit.Reset();
			VoiceHud.Reset();
			Plugin.Debug(Clock.Stamp() + " [lobby] " + why + ": routing, transmit and HUD state cleared");
		}

		internal static void MarkDirty()
		{
			if (!inTick)
			{
				dirty = true;
			}
		}

		internal static void TickIfDirty()
		{
			if (dirty)
			{
				dirty = false;
				Tick();
			}
		}

		private static void Tick()
		{
			if ((Object)(object)StartOfRound.Instance == (Object)null)
			{
				dirty = true;
				return;
			}
			inTick = true;
			try
			{
				LogCensus();
				AutoTransmit.Tick();
				if (Refresh())
				{
					StartOfRound.Instance.UpdatePlayerVoiceEffects();
				}
			}
			finally
			{
				inTick = false;
			}
		}

		internal static bool Refresh()
		{
			//IL_0101: Unknown result type (might be due to invalid IL or missing references)
			//IL_010d: Unknown result type (might be due to invalid IL or missing references)
			StartOfRound instance = StartOfRound.Instance;
			PlayerControllerB local = GetLocal();
			if ((Object)(object)instance == (Object)null || (Object)(object)local == (Object)null)
			{
				return false;
			}
			PlayerControllerB listener = GetListener(local);
			string text = FindActiveWalkie(listener);
			bool flag = text != null;
			bool result = false;
			LogWalkieSeen(listener, listener, text, "you");
			PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts;
			foreach (PlayerControllerB val in allPlayerScripts)
			{
				if (!((Object)(object)val == (Object)null))
				{
					ulong playerClientId = val.playerClientId;
					string text2 = (((Object)(object)val == (Object)(object)listener || (!val.isPlayerControlled && !val.isPlayerDead)) ? null : FindActiveWalkie(val));
					if ((Object)(object)val != (Object)(object)listener)
					{
						LogWalkieSeen(listener, val, text2, null);
					}
					bool flag2 = (Object)(object)val != (Object)(object)listener && (val.speakingToWalkieTalkie || text2 != null);
					bool flag3 = IsRoutable(val, local, listener) && flag && flag2;
					bool flag4 = Links.ContainsKey(playerClientId);
					if (flag3 && !flag4)
					{
						float num = Vector3.Distance(((Component)listener).transform.position, ((Component)val).transform.position);
						float radio = TargetRadio(num);
						Links[playerClientId] = new Link
						{
							Radio = radio,
							Mode = Classify(radio)
						};
						result = true;
						Plugin.Debug(string.Format("{0} [route] {1} (#{2}): LINKED, starting as {3} (dist={4:F1}m; yours: {5}, theirs: {6}, transmit flag: {7})", Clock.Stamp(), val.playerUsername, playerClientId, Links[playerClientId].Mode, num, text, text2 ?? "none seen", val.speakingToWalkieTalkie));
					}
					else if (!flag3 && flag4)
					{
						RestoreVanilla(val);
						Links.Remove(playerClientId);
						result = true;
						Plugin.Debug(string.Format("{0} [route] {1} (#{2}): UNLINKED (yours: {3}, theirs: {4}, transmit flag: {5})", Clock.Stamp(), val.playerUsername, playerClientId, text ?? "no powered walkie", text2 ?? "no powered walkie", val.speakingToWalkieTalkie));
					}
				}
			}
			return result;
		}

		private static void LogWalkieSeen(PlayerControllerB listener, PlayerControllerB p, string? where, string? label)
		{
			string text = where ?? "no powered walkie";
			if (!SeenWalkie.TryGetValue(p.playerClientId, out string value) || !(value == text))
			{
				SeenWalkie[p.playerClientId] = text;
				string text2 = label ?? $"{p.playerUsername} (#{p.playerClientId})";
				Plugin.Debug(Clock.Stamp() + " [walkie] as seen by this client, " + text2 + ": " + text);
			}
		}

		internal static void LogSnapshot(float dt)
		{
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_014c: Unknown result type (might be due to invalid IL or missing references)
			if (!Plugin.DebugLogging.Value)
			{
				return;
			}
			snapshotTimer += dt;
			if (snapshotTimer < 3f)
			{
				return;
			}
			snapshotTimer = 0f;
			StartOfRound instance = StartOfRound.Instance;
			PlayerControllerB local = GetLocal();
			if ((Object)(object)instance == (Object)null || (Object)(object)local == (Object)null)
			{
				return;
			}
			PlayerControllerB listener = GetListener(local);
			PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts;
			foreach (PlayerControllerB val in allPlayerScripts)
			{
				if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)local) && (val.isPlayerControlled || val.isPlayerDead))
				{
					VoicePlayerState voicePlayerState = val.voicePlayerState;
					AudioSource currentVoiceChatAudioSource = val.currentVoiceChatAudioSource;
					float num = float.NaN;
					float[] array = (((Object)(object)SoundManager.Instance != (Object)null) ? SoundManager.Instance.playerVoiceVolumes : null);
					if (array != null && val.playerClientId < (ulong)array.Length)
					{
						num = array[val.playerClientId];
					}
					Plugin.Debug($"{Clock.Stamp()} [voice] {val.playerUsername} (#{val.playerClientId}) dead={val.isPlayerDead} mode={ModeFor(val)} dist={Vector3.Distance(((Component)listener).transform.position, ((Component)val).transform.position):F1}m " + string.Format("| dissonance: state={0} speaking={1} amp={2:F4} vol={3:F2} ", (voicePlayerState == null) ? "MISSING" : "ok", (voicePlayerState != null) ? new bool?(voicePlayerState.IsSpeaking) : ((bool?)null), (voicePlayerState != null) ? voicePlayerState.Amplitude : 0f, (voicePlayerState != null) ? voicePlayerState.Volume : 0f) + "| source: " + (((Object)(object)currentVoiceChatAudioSource == (Object)null) ? "MISSING" : $"playing={currentVoiceChatAudioSource.isPlaying} mute={currentVoiceChatAudioSource.mute} vol={currentVoiceChatAudioSource.volume:F2} blend={currentVoiceChatAudioSource.spatialBlend:F2} mixerVol={num:F2}"));
				}
			}
		}

		private static void RestoreVanilla(PlayerControllerB p)
		{
			if (!((Object)(object)p.currentVoiceChatAudioSource == (Object)null))
			{
				AudioHighPassFilter component = ((Component)p.currentVoiceChatAudioSource).GetComponent<AudioHighPassFilter>();
				if ((Object)(object)component != (Object)null && HighPassDefaults.TryGetValue(p.playerClientId, out var value))
				{
					component.cutoffFrequency = value;
				}
				if (SpreadDefaults.TryGetValue(p.playerClientId, out var value2))
				{
					p.currentVoiceChatAudioSource.spread = value2;
				}
				if (Plugin.DebugLogging.Value)
				{
					Plugin.Debug($"{Clock.Stamp()} [fx] {p.playerUsername} (#{p.playerClientId}): handed back to the base game");
				}
			}
		}

		internal static void UpdateBlend(float dt)
		{
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			StartOfRound instance = StartOfRound.Instance;
			PlayerControllerB local = GetLocal();
			if ((Object)(object)instance == (Object)null || (Object)(object)local == (Object)null || Links.Count == 0)
			{
				return;
			}
			PlayerControllerB listener = GetListener(local);
			if ((Object)(object)listener == (Object)null)
			{
				return;
			}
			bool isPlayerDead = local.isPlayerDead;
			PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts;
			foreach (PlayerControllerB val in allPlayerScripts)
			{
				if ((Object)(object)val == (Object)null || !Links.TryGetValue(val.playerClientId, out Link value) || (Object)(object)val.currentVoiceChatAudioSource == (Object)null || (Object)(object)val.currentVoiceChatIngameSettings == (Object)null || val.voicePlayerState == null)
				{
					continue;
				}
				float num = Vector3.Distance(((Component)listener).transform.position, ((Component)val).transform.position);
				value.Radio = SmoothRadio(value.Radio, TargetRadio(num), dt);
				VoiceMode voiceMode = Classify(value.Radio);
				if (voiceMode != value.Mode)
				{
					if (Plugin.DebugLogging.Value)
					{
						Plugin.Debug($"{Clock.Stamp()} [route] {val.playerUsername} (#{val.playerClientId}): {value.Mode} -> {voiceMode} (dist={num:F1}m, radio={value.Radio * 100f:F0}%)");
					}
					value.Mode = voiceMode;
				}
				if (value.Applied == VoiceMode.Spatial && voiceMode == VoiceMode.Spatial)
				{
					AudioLowPassFilter component = ((Component)val.currentVoiceChatAudioSource).GetComponent<AudioLowPassFilter>();
					if ((Object)(object)component != (Object)null)
					{
						value.OcclusionCutoff = component.cutoffFrequency;
					}
				}
				if (voiceMode == VoiceMode.Blend || voiceMode != value.Applied)
				{
					Apply(val, value, isPlayerDead);
				}
				else if (voiceMode == VoiceMode.Walkie)
				{
					VerifyWalkie(val, value, isPlayerDead);
				}
			}
		}

		internal static void ApplyAll()
		{
			lastPostfixFrame = Time.frameCount;
			StartOfRound instance = StartOfRound.Instance;
			PlayerControllerB local = GetLocal();
			if ((Object)(object)instance == (Object)null || (Object)(object)local == (Object)null)
			{
				return;
			}
			bool isPlayerDead = local.isPlayerDead;
			PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts;
			foreach (PlayerControllerB val in allPlayerScripts)
			{
				if (!((Object)(object)val == (Object)null) && Links.TryGetValue(val.playerClientId, out Link value) && !((Object)(object)val.currentVoiceChatAudioSource == (Object)null) && !((Object)(object)val.currentVoiceChatIngameSettings == (Object)null) && val.voicePlayerState != null)
				{
					Apply(val, value, isPlayerDead);
				}
			}
		}

		private static void RadioTargets(Link link, float highPassCutoff, float radio, out float highPassHz, out float lowPassHz, out float resonanceQ)
		{
			radio = Ease(radio);
			float num = radio * RadioEffectStrength;
			float t = Mathf.Pow(num, 0.6f);
			float num2 = num * num * num;
			highPassHz = Mathf.Clamp(LogLerp(10f, Mathf.Max(10f, highPassCutoff), t), 10f, 5000f);
			float num3 = LogLerp(link.OcclusionCutoff, 22000f, radio);
			lowPassHz = Mathf.Clamp(LogLerp(num3, 4000f, num), 300f, 22000f);
			resonanceQ = Mathf.Clamp(Mathf.Lerp(1f, 3f, num2), 1f, 10f);
		}

		private static void ApplyRadioFilters(AudioLowPassFilter lowPass, AudioHighPassFilter highPass, OccludeAudio occlude, Link link, float highPassCutoff, float radio)
		{
			RadioTargets(link, highPassCutoff, radio, out var highPassHz, out var lowPassHz, out var resonanceQ);
			((Behaviour)lowPass).enabled = true;
			((Behaviour)highPass).enabled = true;
			highPass.cutoffFrequency = highPassHz;
			occlude.overridingLowPass = true;
			occlude.lowPassOverride = lowPassHz;
			lowPass.lowpassResonanceQ = resonanceQ;
		}

		private static void VerifyWalkie(PlayerControllerB p, Link link, bool localDead)
		{
			AudioSource currentVoiceChatAudioSource = p.currentVoiceChatAudioSource;
			AudioLowPassFilter component = ((Component)currentVoiceChatAudioSource).GetComponent<AudioLowPassFilter>();
			AudioHighPassFilter component2 = ((Component)currentVoiceChatAudioSource).GetComponent<AudioHighPassFilter>();
			OccludeAudio component3 = ((Component)currentVoiceChatAudioSource).GetComponent<OccludeAudio>();
			if ((Object)(object)component == (Object)null || (Object)(object)component2 == (Object)null || (Object)(object)component3 == (Object)null || !HighPassDefaults.TryGetValue(p.playerClientId, out var value))
			{
				return;
			}
			if (Mathf.Abs(link.AppliedStrength - RadioEffectStrength) > 0.0001f)
			{
				Apply(p, link, localDead);
				return;
			}
			Fights.TryGetValue(p.playerClientId, out var value2);
			float unscaledTime = Time.unscaledTime;
			if (unscaledTime < value2.PausedUntil)
			{
				return;
			}
			RadioTargets(link, value, 1f, out var highPassHz, out var lowPassHz, out var resonanceQ);
			string text = null;
			if (!((Behaviour)component).enabled)
			{
				text = "low-pass filter was switched off";
			}
			else if (!((Behaviour)component2).enabled)
			{
				text = "high-pass filter was switched off";
			}
			else if (!component3.overridingLowPass)
			{
				text = "low-pass override was released";
			}
			else if (Mathf.Abs(component3.lowPassOverride - lowPassHz) > 1f)
			{
				text = $"low-pass target is {component3.lowPassOverride:F0} Hz, expected {lowPassHz:F0}";
			}
			else if (Mathf.Abs(component.lowpassResonanceQ - resonanceQ) > 0.01f)
			{
				text = $"resonance is {component.lowpassResonanceQ:F2}, expected {resonanceQ:F2}";
			}
			else if (Mathf.Abs(component2.cutoffFrequency - highPassHz) > 1f)
			{
				text = $"high-pass is {component2.cutoffFrequency:F0} Hz, expected {highPassHz:F0}";
			}
			else if (currentVoiceChatAudioSource.spatialBlend > 0.01f || !p.currentVoiceChatIngameSettings.set2D)
			{
				text = "voice was switched back to in-person 3D";
			}
			if (text == null)
			{
				return;
			}
			if (unscaledTime - value2.WindowStart > 4f)
			{
				value2.WindowStart = unscaledTime;
				value2.Count = 0;
			}
			value2.Count++;
			if (value2.Count > 6)
			{
				value2.Count = 0;
				value2.PausedUntil = unscaledTime + 30f;
				Fights[p.playerClientId] = value2;
				Plugin.Logger.LogWarning((object)$"{Clock.Stamp()} [compat] another plugin keeps changing {p.playerUsername}'s voice settings ({text}). Not re-applying the radio sound for {30f:F0} s to avoid a tug of war.");
				return;
			}
			Fights[p.playerClientId] = value2;
			driftCount++;
			if (driftCount <= 20 || driftCount % 100 == 0)
			{
				Plugin.Logger.LogWarning((object)$"{Clock.Stamp()} [fx] {p.playerUsername} (#{p.playerClientId}): radio filters had drifted ({text}); re-applied (#{driftCount})");
			}
			Apply(p, link, localDead);
		}

		private static float VoiceVolume(bool localDead, float radio)
		{
			return BaseVolume(localDead) * Mathf.Lerp(1f, Mathf.Max(0f, WalkieVolume), radio);
		}

		private static void Apply(PlayerControllerB p, Link link, bool localDead)
		{
			AudioSource currentVoiceChatAudioSource = p.currentVoiceChatAudioSource;
			AudioLowPassFilter component = ((Component)currentVoiceChatAudioSource).GetComponent<AudioLowPassFilter>();
			AudioHighPassFilter component2 = ((Component)currentVoiceChatAudioSource).GetComponent<AudioHighPassFilter>();
			OccludeAudio component3 = ((Component)currentVoiceChatAudioSource).GetComponent<OccludeAudio>();
			if (!HighPassDefaults.TryGetValue(p.playerClientId, out var value))
			{
				value = component2.cutoffFrequency;
				HighPassDefaults[p.playerClientId] = value;
			}
			if (!SpreadDefaults.ContainsKey(p.playerClientId))
			{
				SpreadDefaults[p.playerClientId] = currentVoiceChatAudioSource.spread;
			}
			if (link.Mode == VoiceMode.Blend && link.LastBlend >= 0f && lastPostfixFrame != Time.frameCount && Time.frameCount - link.FlipFrame > 3 && Mathf.Abs(currentVoiceChatAudioSource.spatialBlend - link.LastBlend) > 0.05f && !warnedBlendOverwritten)
			{
				warnedBlendOverwritten = true;
				Plugin.Logger.LogWarning((object)$"{Clock.Stamp()} [blend] spatialBlend of {p.playerUsername} was changed by something else (expected {link.LastBlend:F2}, found {currentVoiceChatAudioSource.spatialBlend:F2}). Another mod or setting is overriding the in-person/radio mix.");
			}
			float num = Mathf.Clamp01(link.Radio);
			link.AppliedStrength = RadioEffectStrength;
			bool flag = link.Mode == VoiceMode.Walkie;
			if (p.currentVoiceChatIngameSettings.set2D != flag)
			{
				link.FlipFrame = Time.frameCount;
			}
			currentVoiceChatAudioSource.bypassListenerEffects = false;
			currentVoiceChatAudioSource.bypassEffects = false;
			currentVoiceChatAudioSource.outputAudioMixerGroup = SoundManager.Instance.playerVoiceMixers[p.playerClientId];
			p.voicePlayerState.Volume = VoiceVolume(localDead, num);
			if (link.Mode == VoiceMode.Spatial)
			{
				((Behaviour)component).enabled = true;
				((Behaviour)component2).enabled = false;
				component2.cutoffFrequency = value;
				component3.overridingLowPass = p.voiceMuffledByEnemy;
				component.lowpassResonanceQ = 1f;
				p.currentVoiceChatIngameSettings.set2D = false;
				currentVoiceChatAudioSource.spatialBlend = 1f;
				currentVoiceChatAudioSource.panStereo = 0f;
				currentVoiceChatAudioSource.spread = SpreadFor(p.playerClientId, 0f);
			}
			else if (link.Mode == VoiceMode.Walkie)
			{
				ApplyRadioFilters(component, component2, component3, link, value, 1f);
				p.currentVoiceChatIngameSettings.set2D = true;
				currentVoiceChatAudioSource.spatialBlend = 0f;
				currentVoiceChatAudioSource.panStereo = 0f;
				currentVoiceChatAudioSource.spread = SpreadFor(p.playerClientId, 1f);
			}
			else
			{
				ApplyRadioFilters(component, component2, component3, link, value, num);
				p.currentVoiceChatIngameSettings.set2D = false;
				currentVoiceChatAudioSource.spatialBlend = 1f - num;
				currentVoiceChatAudioSource.panStereo = 0f;
				currentVoiceChatAudioSource.spread = SpreadFor(p.playerClientId, num);
			}
			if (Plugin.DebugLogging.Value && link.Applied != link.Mode)
			{
				Plugin.Debug($"{Clock.Stamp()} [fx] {p.playerUsername} (#{p.playerClientId}): applied {link.Mode} | " + $"low-pass on={((Behaviour)component).enabled} override={component3.overridingLowPass}/{component3.lowPassOverride:F0}Hz Q={component.lowpassResonanceQ:F1} | " + $"high-pass on={((Behaviour)component2).enabled} {component2.cutoffFrequency:F0}Hz | blend={currentVoiceChatAudioSource.spatialBlend:F2} set2D={p.currentVoiceChatIngameSettings.set2D} pan={currentVoiceChatAudioSource.panStereo:F2} vol={p.voicePlayerState.Volume:F2}");
			}
			link.LastBlend = currentVoiceChatAudioSource.spatialBlend;
			link.Applied = link.Mode;
		}

		internal static bool IsVoiceOfWalkiePlayer(AudioSource source)
		{
			StartOfRound instance = StartOfRound.Instance;
			if ((Object)(object)source == (Object)null || (Object)(object)instance == (Object)null)
			{
				return false;
			}
			PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts;
			foreach (PlayerControllerB val in allPlayerScripts)
			{
				if ((Object)(object)val != (Object)null && (Object)(object)val.currentVoiceChatAudioSource == (Object)(object)source && HasActiveWalkie(val))
				{
					return true;
				}
			}
			return false;
		}
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "bobabulkerENTERPRISE.BetterVC";

		public const string PLUGIN_NAME = "BetterVC";

		public const string PLUGIN_VERSION = "1.0.0";
	}
}
namespace BetterVC.Patches
{
	[HarmonyPatch(typeof(PlayerControllerB), "Update")]
	internal static class FrameDriverPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			FrameDriver.Tick("PlayerControllerB.Update");
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "SwitchWalkieTalkieOn")]
	internal static class WalkiePowerPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			VoiceRouter.MarkDirty();
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "EquipItem")]
	internal static class WalkieEquipPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			VoiceRouter.MarkDirty();
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "PocketItem")]
	internal static class WalkiePocketPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			VoiceRouter.MarkDirty();
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "DiscardItem")]
	internal static class WalkieDiscardPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			VoiceRouter.MarkDirty();
		}
	}
	[HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")]
	internal static class PlayerJoinPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			VoiceRouter.MarkDirty();
		}
	}
	[HarmonyPatch(typeof(StartOfRound), "UpdatePlayerVoiceEffects")]
	internal static class VoiceEffectsPatch
	{
		[HarmonyPostfix]
		[HarmonyPriority(0)]
		private static void Postfix()
		{
			try
			{
				VoiceRouter.ResetIfNewLobby();
				VoiceRouter.Refresh();
				VoiceRouter.ApplyAll();
				VoiceRouter.MarkDirty();
			}
			catch (Exception arg)
			{
				Plugin.Logger.LogError((object)$"Applying voice routing failed: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "GetAllAudioSourcesToReplay")]
	internal static class WalkieReplayPatch
	{
		[HarmonyPostfix]
		private static void Postfix(List<AudioSource> ___audioSourcesToReplay)
		{
			if (___audioSourcesToReplay == null || ___audioSourcesToReplay.Count == 0)
			{
				return;
			}
			try
			{
				if (Plugin.NearbySoundsVolume.Value <= 0.001f)
				{
					___audioSourcesToReplay.Clear();
					return;
				}
				int num = ___audioSourcesToReplay.RemoveAll(VoiceRouter.IsVoiceOfWalkiePlayer);
				if (num > 0 && Plugin.DebugLogging.Value)
				{
					Plugin.Debug($"{Clock.Stamp()} [overlap] kept {num} voice source(s) out of the walkie re-broadcast");
				}
			}
			catch (Exception arg)
			{
				Plugin.Logger.LogError((object)$"Replay filter failed: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "SetLocalClientSpeaking")]
	internal static class WalkieSpeakPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(bool speaking)
		{
			if (!speaking && AutoTransmit.Wanted)
			{
				return AutoTransmit.Sending;
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "TransmitOneShotAudio")]
	internal static class WalkieOneShotPatch
	{
		[HarmonyPrefix]
		private static bool Prefix()
		{
			return Plugin.NearbySoundsVolume.Value > 0.001f;
		}
	}
	internal static class WalkieChatter
	{
		private static readonly HashSet<WalkieTalkie> Muted = new HashSet<WalkieTalkie>();

		internal static bool Allowed()
		{
			if (Plugin.IdleChatterVolume.Value <= 0.001f)
			{
				return false;
			}
			PlayerControllerB player = VoiceRouter.CurrentListener();
			return !VoiceRouter.HasActiveWalkie(player);
		}

		private static bool IsListenersOwn(WalkieTalkie walkie)
		{
			return WalkieSpeaker.IsListenersOwn(walkie);
		}

		internal static void Enforce(WalkieTalkie walkie, Dictionary<AudioSource, AudioSource>? receiving)
		{
			if ((Object)(object)walkie == (Object)null || (Object)(object)walkie.target == (Object)null)
			{
				return;
			}
			if (((GrabbableObject)walkie).isBeingUsed && !IsListenersOwn(walkie) && !Allowed())
			{
				if (Muted.Add(walkie) && Plugin.DebugLogging.Value)
				{
					Plugin.Debug(Clock.Stamp() + " [chatter] silenced a walkie's replay noise (held by " + (((Object)(object)((GrabbableObject)walkie).playerHeldBy != (Object)null) ? ((GrabbableObject)walkie).playerHeldBy.playerUsername : "nobody") + ")");
				}
				walkie.target.mute = true;
				if (receiving == null)
				{
					return;
				}
				{
					foreach (AudioSource value in receiving.Values)
					{
						if ((Object)(object)value != (Object)null)
						{
							value.mute = true;
						}
					}
					return;
				}
			}
			if (!Muted.Remove(walkie))
			{
				return;
			}
			walkie.target.mute = false;
			if (receiving == null)
			{
				return;
			}
			foreach (AudioSource value2 in receiving.Values)
			{
				if ((Object)(object)value2 != (Object)null)
				{
					value2.mute = false;
				}
			}
		}

		internal static void Forget(WalkieTalkie walkie)
		{
			Muted.Remove(walkie);
		}
	}
	internal static class WalkieSpeaker
	{
		private sealed class Saved
		{
			public float Blend;

			public float Pan;

			public float Volume;

			public float BaseVolume;

			public float LastSetVolume = -1f;
		}

		private static readonly Dictionary<AudioSource, Saved> Touched = new Dictionary<AudioSource, Saved>();

		internal static bool IsListenersOwn(WalkieTalkie walkie)
		{
			PlayerControllerB val = VoiceRouter.CurrentListener();
			if ((Object)(object)val != (Object)null)
			{
				return (Object)(object)((GrabbableObject)walkie).playerHeldBy == (Object)(object)val;
			}
			return false;
		}

		internal static void Enforce(WalkieTalkie walkie, Dictionary<AudioSource, AudioSource>? receiving)
		{
			if ((Object)(object)walkie == (Object)null)
			{
				return;
			}
			bool isBeingUsed = ((GrabbableObject)walkie).isBeingUsed;
			bool flag = isBeingUsed && IsListenersOwn(walkie);
			float volumeFactor = Mathf.Clamp01(Plugin.NearbySoundsVolume.Value);
			Apply(walkie.target, isBeingUsed, flag, volumeFactor);
			Apply(walkie.thisAudio, flag, flag, Mathf.Max(0f, Plugin.WalkieVolume.Value));
			if (receiving != null)
			{
				foreach (AudioSource value in receiving.Values)
				{
					Apply(value, isBeingUsed, flag, volumeFactor);
				}
			}
			if (Touched.Count > 64)
			{
				Prune();
			}
		}

		private static void Apply(AudioSource? source, bool active, bool center, float volumeFactor)
		{
			if ((Object)(object)source == (Object)null)
			{
				return;
			}
			Saved value2;
			if (active)
			{
				if (!Touched.TryGetValue(source, out Saved value) || value == null)
				{
					value = new Saved
					{
						Blend = source.spatialBlend,
						Pan = source.panStereo,
						Volume = source.volume,
						BaseVolume = source.volume
					};
					Touched[source] = value;
				}
				if (center)
				{
					source.spatialBlend = 0f;
					source.panStereo = 0f;
				}
				else
				{
					source.spatialBlend = value.Blend;
					source.panStereo = value.Pan;
				}
				if (Mathf.Abs(source.volume - value.LastSetVolume) > 0.0001f)
				{
					value.BaseVolume = source.volume;
				}
				float lastSetVolume = (source.volume = Mathf.Clamp01(value.BaseVolume * volumeFactor));
				value.LastSetVolume = lastSetVolume;
			}
			else if (Touched.TryGetValue(source, out value2))
			{
				source.spatialBlend = value2.Blend;
				source.panStereo = value2.Pan;
				source.volume = value2.BaseVolume;
				Touched.Remove(source);
			}
		}

		private static void Prune()
		{
			List<AudioSource> list = new List<AudioSource>();
			foreach (AudioSource key in Touched.Keys)
			{
				if ((Object)(object)key == (Object)null)
				{
					list.Add(key);
				}
			}
			foreach (AudioSource item in list)
			{
				Touched.Remove(item);
			}
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "Update")]
	internal static class WalkieChatterUpdatePatch
	{
		[HarmonyPostfix]
		private static void Postfix(WalkieTalkie __instance, Dictionary<AudioSource, AudioSource> ___audioSourcesReceiving)
		{
			try
			{
				WalkieChatter.Enforce(__instance, ___audioSourcesReceiving);
				WalkieSpeaker.Enforce(__instance, ___audioSourcesReceiving);
			}
			catch (Exception arg)
			{
				Plugin.Logger.LogError((object)$"Chatter gate failed: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "SetWallAudioVolume")]
	internal static class WalkieIdleChatterPatch
	{
		private const float MaxHearingDistance = 12f;

		[HarmonyPrefix]
		private static bool Prefix(WalkieTalkie __instance)
		{
			AudioSource wallAudio = __instance.wallAudio;
			if ((Object)(object)wallAudio == (Object)null)
			{
				return true;
			}
			if (!WalkieChatter.Allowed() || !((GrabbableObject)__instance).isBeingUsed)
			{
				if (wallAudio.isPlaying)
				{
					wallAudio.Stop();
				}
				wallAudio.volume = 0f;
				return false;
			}
			AudioClip[] wallAudios = __instance.wallAudios;
			if (!wallAudio.isPlaying)
			{
				if ((Object)(object)wallAudio.clip == (Object)null && wallAudios != null && wallAudios.Length != 0)
				{
					wallAudio.clip = wallAudios[0];
				}
				if ((Object)(object)wallAudio.clip == (Object)null)
				{
					return false;
				}
				wallAudio.loop = true;
				wallAudio.spatialBlend = 1f;
				wallAudio.rolloffMode = (AudioRolloffMode)1;
				wallAudio.minDistance = 1f;
				wallAudio.maxDistance = 12f;
				wallAudio.pitch = Random.Range(0.92f, 1.08f);
				wallAudio.Play();
			}
			wallAudio.volume = Mathf.Clamp01(Plugin.IdleChatterVolume.Value);
			return false;
		}
	}
	internal static class TransmitSfx
	{
		private static readonly Dictionary<int, bool> LastAnnounced = new Dictionary<int, bool>();

		private static object? lastRound;

		internal static bool IsRepeat(int playerId, bool speaking)
		{
			object instance = StartOfRound.Instance;
			if (instance != lastRound)
			{
				LastAnnounced.Clear();
				lastRound = instance;
			}
			bool value;
			bool result = LastAnnounced.TryGetValue(playerId, out value) && value == speaking;
			LastAnnounced[playerId] = speaking;
			return result;
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "SendWalkieTalkieStartTransmissionSFX")]
	internal static class WalkieStartSfxPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(int playerId)
		{
			return !TransmitSfx.IsRepeat(playerId, speaking: true);
		}
	}
	[HarmonyPatch(typeof(WalkieTalkie), "SendWalkieTalkieEndTransmissionSFX")]
	internal static class WalkieEndSfxPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(int playerId)
		{
			return !TransmitSfx.IsRepeat(playerId, speaking: false);
		}
	}
}