Decompiled source of PeakReactionAudio v0.1.0

plugins/PeakReactionAudio/PeakReactionAudio.dll

Decompiled 2 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using PeakReactionAudio.Core;
using Photon.Pun;
using Photon.Voice;
using Photon.Voice.Unity;
using UnityEngine;
using UnityEngine.Networking;

[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("PeakReactionAudio")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+1ce115f241df102896063d3c931ada302edf77f7")]
[assembly: AssemblyProduct("PeakReactionAudio")]
[assembly: AssemblyTitle("PeakReactionAudio")]
[assembly: AssemblyVersion("0.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

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

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace PeakReactionAudio
{
	internal sealed class AudioAsset
	{
		public string Path { get; }

		public int SourceRate { get; }

		public float[] Mono { get; }

		public Dictionary<int, float[]> Variants { get; } = new Dictionary<int, float[]>();

		public AudioAsset(string path, int rate, float[] mono)
		{
			Path = path;
			SourceRate = rate;
			Mono = mono;
			Variants[rate] = mono;
		}
	}
	internal sealed class AudioLibrary
	{
		private readonly ManualLogSource _log;

		private readonly List<AudioAsset>[] _clips = new List<AudioAsset>[7];

		private readonly AudioAsset?[] _previous = new AudioAsset[7];

		private readonly Random _random = new Random();

		private readonly PcmBudget _budget = new PcmBudget();

		private readonly Queue<int> _rates = new Queue<int>();

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

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

		public bool Ready { get; private set; }

		public AudioLibrary(ManualLogSource log)
		{
			_log = log;
			for (int i = 0; i < _clips.Length; i++)
			{
				_clips[i] = new List<AudioAsset>();
			}
		}

		public IEnumerator Load(string root)
		{
			foreach (ReactionId id in Enum.GetValues(typeof(ReactionId)))
			{
				string path = Path.Combine(root, id.ToString());
				Directory.CreateDirectory(path);
				string[] files = Directory.GetFiles(path);
				Array.Sort(files, (IComparer<string>?)StringComparer.OrdinalIgnoreCase);
				string[] array = files;
				foreach (string file in array)
				{
					AudioType val;
					switch (Path.GetExtension(file).ToLowerInvariant())
					{
					case ".wav":
						val = (AudioType)20;
						break;
					case ".ogg":
						val = (AudioType)14;
						break;
					case ".mp3":
						val = (AudioType)13;
						break;
					default:
						continue;
					}
					UnityWebRequest request = null;
					try
					{
						request = UnityWebRequestMultimedia.GetAudioClip(new Uri(Path.GetFullPath(file)).AbsoluteUri, val);
					}
					catch (Exception ex)
					{
						_log.LogWarning((object)("Cannot open " + file + ": " + ex.Message));
					}
					if (request == null)
					{
						continue;
					}
					UnityWebRequest val2 = request;
					try
					{
						yield return request.SendWebRequest();
						if ((int)request.result != 1)
						{
							_log.LogWarning((object)("Skipped audio " + file + ": " + request.error));
							continue;
						}
						AudioClip val3 = null;
						try
						{
							val3 = DownloadHandlerAudioClip.GetContent(request);
							if ((Object)(object)val3 == (Object)null || val3.samples <= 0 || val3.channels <= 0 || val3.frequency <= 0 || (double)val3.samples / (double)val3.frequency > 60.0)
							{
								throw new InvalidDataException("Invalid audio or duration exceeds 60 seconds.");
							}
							long num = (long)val3.samples * (long)val3.channels * 4;
							long bytes = (long)val3.samples * 4L;
							if (num > 134217728 || !_budget.CanFit(bytes))
							{
								throw new InvalidDataException("128 MiB PCM budget exceeded.");
							}
							float[] array2 = new float[checked(val3.samples * val3.channels)];
							if (!val3.GetData(array2, 0))
							{
								throw new InvalidDataException("Unity could not read decoded PCM.");
							}
							float[] mono = Pcm.Downmix(array2, val3.channels);
							_clips[(int)id].Add(new AudioAsset(file, val3.frequency, mono));
							_budget.Reserve(bytes);
						}
						catch (Exception ex2)
						{
							_log.LogWarning((object)("Skipped audio " + file + ": " + ex2.Message));
						}
						finally
						{
							if ((Object)(object)val3 != (Object)null)
							{
								Object.Destroy((Object)(object)val3);
							}
						}
					}
					finally
					{
						((IDisposable)val2)?.Dispose();
					}
				}
				_log.LogInfo((object)$"{id}: {_clips[(int)id].Count} valid audio file(s).");
			}
			Ready = true;
			_log.LogInfo((object)$"Audio scan completed; {_budget.UsedBytes / 1024} KiB source PCM. No audio is sent until a local event fires.");
		}

		public void RequestRate(int rate)
		{
			if (rate > 0 && _requestedRates.Add(rate))
			{
				_rates.Enqueue(rate);
			}
		}

		public bool IsPrepared(int rate)
		{
			return _preparedRates.Contains(rate);
		}

		public IEnumerator PrepareRequestedRates()
		{
			while (true)
			{
				if (!Ready || _rates.Count == 0)
				{
					yield return null;
					continue;
				}
				int rate = _rates.Dequeue();
				yield return PrepareRate(rate);
				_preparedRates.Add(rate);
				_log.LogInfo((object)$"Reaction PCM prepared: {rate} Hz; cache={_budget.UsedBytes / 1024} KiB.");
			}
		}

		private IEnumerator PrepareRate(int rate)
		{
			if (rate <= 0)
			{
				yield break;
			}
			List<AudioAsset>[] clips = _clips;
			foreach (List<AudioAsset> list in clips)
			{
				foreach (AudioAsset clip in list)
				{
					if (clip.Variants.ContainsKey(rate))
					{
						continue;
					}
					long bytes = (long)Math.Max(1.0, Math.Round((double)clip.Mono.Length * (double)rate / (double)clip.SourceRate)) * 4;
					if (!_budget.CanFit(bytes))
					{
						_log.LogWarning((object)$"Cannot prepare {rate} Hz PCM (cache full): {clip.Path}");
						continue;
					}
					Task<float[]> conversion = Task.Run(() => Pcm.Resample(clip.Mono, clip.SourceRate, rate));
					while (!conversion.IsCompleted)
					{
						yield return null;
					}
					if (conversion.IsFaulted || conversion.IsCanceled)
					{
						_log.LogWarning((object)("Cannot resample " + clip.Path + ": " + conversion.Exception?.GetBaseException().Message));
						continue;
					}
					float[] result = conversion.Result;
					clip.Variants.Add(rate, result);
					_budget.Reserve((long)result.Length * 4L);
				}
			}
		}

		public AudioAsset? Select(ReactionId id, int rate)
		{
			List<AudioAsset> list = _clips[(int)id];
			int num = 0;
			int previous = -1;
			foreach (AudioAsset item in list)
			{
				if (item.Variants.ContainsKey(rate))
				{
					if (item == _previous[(int)id])
					{
						previous = num;
					}
					num++;
				}
			}
			if (num == 0)
			{
				return null;
			}
			int num2 = ClipChoice.Next(num, previous, _random);
			foreach (AudioAsset item2 in list)
			{
				if (item2.Variants.ContainsKey(rate) && num2-- == 0)
				{
					return item2;
				}
			}
			return null;
		}

		public void Accepted(ReactionId id, AudioAsset asset)
		{
			_previous[(int)id] = asset;
		}
	}
	internal sealed class LocalMonitor : IDisposable
	{
		private readonly AudioSource _source;

		private readonly AudioClip _clip;

		private float _gain = 0.5f;

		private int _fault;

		public MixBus Bus { get; }

		public bool Faulted => Volatile.Read(in _fault) != 0;

		public LocalMonitor(GameObject host, MixBus bus, int sampleRate)
		{
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Expected O, but got Unknown
			Bus = bus;
			_source = host.AddComponent<AudioSource>();
			try
			{
				_source.playOnAwake = false;
				_source.loop = true;
				_source.spatialBlend = 0f;
				_clip = AudioClip.Create("PeakReactionAudio monitor (effects only)", sampleRate, 1, sampleRate, true, new PCMReaderCallback(Read));
				_source.clip = _clip;
				_source.Play();
			}
			catch
			{
				Dispose();
				throw;
			}
		}

		public void SetGain(float gain)
		{
			Volatile.Write(ref _gain, gain);
		}

		private void Read(float[] data)
		{
			try
			{
				Bus.Render(data, 1, microphoneAllowed: false, Volatile.Read(in _gain));
			}
			catch
			{
				Array.Clear(data, 0, data.Length);
				Volatile.Write(ref _fault, 1);
			}
		}

		public void Dispose()
		{
			Bus.Clear();
			if ((Object)(object)_source != (Object)null)
			{
				_source.Stop();
				Object.Destroy((Object)(object)_source);
			}
			if ((Object)(object)_clip != (Object)null)
			{
				Object.Destroy((Object)(object)_clip);
			}
		}
	}
	internal sealed class EventOptions
	{
		public ConfigEntry<bool> Enabled { get; }

		public ConfigEntry<float> Volume { get; }

		public EventOptions(ConfigFile config, ReactionId id)
		{
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0066: Expected O, but got Unknown
			string text = "Event." + id;
			Enabled = config.Bind<bool>(text, "Enabled", true, "Enable this event. Restart after editing the config file.");
			Volume = config.Bind<float>(text, "Volume", 1f, new ConfigDescription("Event gain.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>()));
		}
	}
	internal sealed class ModConfig
	{
		public ConfigEntry<bool> Enabled { get; }

		public ConfigEntry<float> MasterVolume { get; }

		public ConfigEntry<float> Cooldown { get; }

		public ConfigEntry<bool> Debug { get; }

		public EventOptions[] Events { get; }

		public ModConfig(ConfigFile config)
		{
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Expected O, but got Unknown
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Expected O, but got Unknown
			Enabled = config.Bind<bool>("General", "Enabled", true, "Enable reactions. Restart after editing config files.");
			MasterVolume = config.Bind<float>("General", "MasterVolume", 0.5f, new ConfigDescription("Gain of the effects bus; microphone gain is unchanged.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>()));
			Cooldown = config.Bind<float>("General", "CooldownSeconds", 3f, new ConfigDescription("Cooldown duration for all events. Each event counts separately.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 300f), Array.Empty<object>()));
			Debug = config.Bind<bool>("General", "DebugLogging", false, "Log event IDs and skip reasons; never microphone samples.");
			Events = new EventOptions[7];
			foreach (ReactionId value in Enum.GetValues(typeof(ReactionId)))
			{
				Events[(int)value] = new EventOptions(config, value);
			}
		}

		public static float Safe(ConfigEntry<float> entry, float fallback)
		{
			return Limits.Clamp(entry.Value, 0f, 300f, fallback);
		}
	}
	internal sealed class PatchInstaller : IDisposable
	{
		private readonly Plugin _plugin;

		private readonly List<Harmony> _groups = new List<Harmony>();

		public PatchInstaller(Plugin plugin)
		{
			_plugin = plugin;
		}

		public void Install()
		{
			Group("PrePassOut", new ReactionId[1], delegate(Harmony h)
			{
				Patch(h, typeof(Character), "HandleLife", null, "LifePostfix");
			});
			Group("Pull", new ReactionId[2]
			{
				ReactionId.PullPlayer,
				ReactionId.BeingPulled
			}, delegate(Harmony h)
			{
				if (Hooks.GrabbingCharacter == null)
				{
					throw new MissingFieldException("CharacterGrabbing.character");
				}
				Patch(h, typeof(CharacterGrabbing), "Reach", "ReachPrefix", "ReachPostfix", "ReachFinalizer");
				MethodInfo methodInfo = AccessTools.Method(typeof(CharacterGrabbing), "TargetCanBeHelped", new Type[2]
				{
					typeof(Character),
					typeof(Character)
				}, (Type[])null) ?? throw new MissingMethodException("TargetCanBeHelped(Character, Character)");
				h.Patch((MethodBase)methodInfo, (HarmonyMethod)null, Callback("HelpedPostfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			});
			Group("Landing", new ReactionId[1] { ReactionId.HardLanding }, delegate(Harmony h)
			{
				if (Hooks.MovementCharacter == null)
				{
					throw new MissingFieldException("CharacterMovement.character");
				}
				Patch(h, typeof(CharacterMovement), "Land", "LandPrefix", "LandPostfix");
			});
			Group("Recovery", new ReactionId[2]
			{
				ReactionId.RegainConsciousness,
				ReactionId.Revived
			}, delegate(Harmony h)
			{
				Patch(h, typeof(Character), "UnPassOutDone", "RecoverPrefix", "RecoverPostfix");
				Patch(h, typeof(Character), "ReviveCharacter", "RecoverPrefix", "RecoverPostfix");
			});
			Group("Voice", Array.Empty<ReactionId>(), delegate(Harmony h)
			{
				Patch(h, typeof(Recorder), "StopRecording", "VoiceStopping");
				Patch(h, typeof(CharacterVoiceHandler), "PushToTalk", null, "PushToTalkPostfix");
			}, voice: true);
			Group("WebRTC", Array.Empty<ReactionId>(), delegate(Harmony h)
			{
				Patch(h, typeof(WebRtcAudioDsp), "StartProc", null, "DspStarted");
			}, voice: false, optional: true);
		}

		private void Group(string name, ReactionId[] affected, Action<Harmony> install, bool voice = false, bool optional = false)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Expected O, but got Unknown
			Harmony val = new Harmony("com.github.lllei.PeakReactionAudio." + name);
			try
			{
				install(val);
				_groups.Add(val);
			}
			catch (Exception ex)
			{
				val.UnpatchSelf();
				_plugin.Controller.Disable(affected);
				if (optional)
				{
					_plugin.Log.LogWarning((object)("Optional " + name + " hook unavailable: " + ex.Message));
				}
				else
				{
					_plugin.Log.LogError((object)$"{name} hooks unavailable: {ex}");
				}
				if (voice)
				{
					_plugin.Voice?.Fail(ex);
				}
			}
		}

		private static HarmonyMethod Callback(string name)
		{
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Expected O, but got Unknown
			return new HarmonyMethod(AccessTools.Method(typeof(Hooks), name, (Type[])null, (Type[])null));
		}

		private static void Patch(Harmony h, Type type, string name, string? pre = null, string? post = null, string? final = null)
		{
			MethodInfo methodInfo = AccessTools.Method(type, name, (Type[])null, (Type[])null) ?? throw new MissingMethodException(type.FullName, name);
			h.Patch((MethodBase)methodInfo, (pre == null) ? null : Callback(pre), (post == null) ? null : Callback(post), (HarmonyMethod)null, (final == null) ? null : Callback(final), (HarmonyMethod)null);
		}

		public void Dispose()
		{
			foreach (Harmony group in _groups)
			{
				group.UnpatchSelf();
			}
			_groups.Clear();
		}
	}
	internal static class Hooks
	{
		internal readonly struct LandingSnapshot
		{
			public Character? Character { get; }

			public float AirTime { get; }

			public float Velocity { get; }

			public bool Conscious { get; }

			public LandingSnapshot(Character? character)
			{
				Character = character;
				AirTime = (((Object)(object)character != (Object)null) ? character.data.sinceGrounded : 0f);
				Velocity = (((Object)(object)character != (Object)null) ? character.data.avarageLastFrameVelocity.y : 0f);
				Conscious = (Object)(object)character != (Object)null && character.data.fullyConscious;
			}
		}

		internal static readonly FieldInfo? GrabbingCharacter = AccessTools.Field(typeof(CharacterGrabbing), "character");

		internal static readonly FieldInfo? MovementCharacter = AccessTools.Field(typeof(CharacterMovement), "character");

		private static Character? _helper;

		private static bool _helping;

		private static bool _beingHelped;

		private static Plugin? Active
		{
			get
			{
				if (!((Object)(object)Plugin.Instance != (Object)null) || !((Behaviour)Plugin.Instance).isActiveAndEnabled)
				{
					return null;
				}
				return Plugin.Instance;
			}
		}

		private static void Event(ReactionId id, Action<ReactionController> action, ReactionId? also = null)
		{
			Plugin active = Active;
			if ((Object)(object)active == (Object)null || (!active.Controller.Available(id) && (!also.HasValue || !active.Controller.Available(also.Value))))
			{
				return;
			}
			try
			{
				action(active.Controller);
			}
			catch (Exception arg)
			{
				active.Controller.Disable(id);
				if (also.HasValue)
				{
					active.Controller.Disable(also.Value);
				}
				active.Log.LogError((object)$"{id} detection disabled: {arg}");
			}
		}

		private static void Voice(Action<VoiceRuntime> action)
		{
			VoiceRuntime voiceRuntime = Active?.Voice;
			if (voiceRuntime == null)
			{
				return;
			}
			try
			{
				action(voiceRuntime);
			}
			catch (Exception ex)
			{
				voiceRuntime.Fail(ex);
			}
		}

		public static void LifePostfix(Character __instance)
		{
			Event(ReactionId.PrePassOut, delegate(ReactionController c)
			{
				c.LifeUpdated(__instance);
			});
		}

		public static void ReachPrefix(CharacterGrabbing __instance)
		{
			Event(ReactionId.PullPlayer, delegate
			{
				object? value = GrabbingCharacter.GetValue(__instance);
				_helper = (Character?)((value is Character) ? value : null);
				_helping = (_beingHelped = false);
			}, ReactionId.BeingPulled);
		}

		public static void HelpedPostfix(Character target, Character helper, bool __result)
		{
			if (__result && !((Object)(object)_helper == (Object)null) && !((Object)(object)helper != (Object)(object)_helper) && !helper.data.isScoutmaster)
			{
				_helping |= helper.IsLocal;
				_beingHelped |= target.IsLocal;
			}
		}

		public static void ReachPostfix()
		{
			Event(ReactionId.PullPlayer, delegate(ReactionController c)
			{
				c.Helping(_helping, _beingHelped);
			}, ReactionId.BeingPulled);
		}

		public static Exception? ReachFinalizer(Exception? __exception)
		{
			_helper = null;
			_helping = (_beingHelped = false);
			return __exception;
		}

		public static void LandPrefix(CharacterMovement __instance, out LandingSnapshot __state)
		{
			object? obj = MovementCharacter?.GetValue(__instance);
			__state = new LandingSnapshot((Character?)((obj is Character) ? obj : null));
		}

		public static void LandPostfix(LandingSnapshot __state)
		{
			if ((Object)(object)__state.Character != (Object)null)
			{
				Event(ReactionId.HardLanding, delegate(ReactionController c)
				{
					c.Landed(__state.Character, __state.AirTime, __state.Velocity, __state.Conscious);
				});
			}
		}

		public static void RecoverPrefix(Character __instance, out RecoverySnapshot __state)
		{
			__state = new RecoverySnapshot(__instance.data.dead, __instance.data.passedOut || __instance.data.fullyPassedOut);
		}

		public static void RecoverPostfix(Character __instance, RecoverySnapshot __state)
		{
			Event(ReactionId.RegainConsciousness, delegate(ReactionController c)
			{
				c.Recovered(__instance, __state);
			}, ReactionId.Revived);
		}

		public static void VoiceStopping(Recorder __instance)
		{
			Voice(delegate(VoiceRuntime v)
			{
				v.RecorderStopping(__instance);
			});
		}

		public static void PushToTalkPostfix(CharacterVoiceHandler __instance)
		{
			Voice(delegate(VoiceRuntime v)
			{
				v.OnPushToTalk(__instance, __instance.transmitting);
			});
		}

		public static void DspStarted(WebRtcAudioDsp __instance)
		{
			Voice(delegate(VoiceRuntime v)
			{
				v.DspChanged(__instance);
			});
		}
	}
	[BepInPlugin("com.github.lllei.PeakReactionAudio", "PeakReactionAudio", "0.1.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "com.github.lllei.PeakReactionAudio";

		public const string Name = "PeakReactionAudio";

		public const string Version = "0.1.0";

		private AudioLibrary _library;

		private PatchInstaller? _patches;

		private bool _running;

		internal static Plugin? Instance { get; private set; }

		internal ModConfig Options { get; private set; }

		internal ReactionController Controller { get; private set; }

		internal VoiceRuntime? Voice { get; private set; }

		internal ManualLogSource Log => ((BaseUnityPlugin)this).Logger;

		private void Awake()
		{
			Instance = this;
			Options = new ModConfig(((BaseUnityPlugin)this).Config);
			_library = new AudioLibrary(((BaseUnityPlugin)this).Logger);
			Controller = new ReactionController(this);
			try
			{
				Voice = new VoiceRuntime(this, _library);
			}
			catch (Exception arg)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)$"Voice integration unavailable; reaction audio cannot start: {arg}");
			}
			_patches = new PatchInstaller(this);
			_patches.Install();
			((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1} loaded. {2} reactions; all cooldowns default to 3 seconds.", "PeakReactionAudio", "0.1.0", 7));
		}

		private void Start()
		{
			((MonoBehaviour)this).StartCoroutine(LoadAudio());
			((MonoBehaviour)this).StartCoroutine(_library.PrepareRequestedRates());
		}

		private IEnumerator LoadAudio()
		{
			IEnumerator loader = _library.Load(Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location));
			while (true)
			{
				object current;
				try
				{
					if (!loader.MoveNext())
					{
						break;
					}
					current = loader.Current;
					goto IL_0076;
				}
				catch (Exception arg)
				{
					((BaseUnityPlugin)this).Logger.LogError((object)$"Audio loading stopped: {arg}");
				}
				break;
				IL_0076:
				yield return current;
			}
			(loader as IDisposable)?.Dispose();
		}

		private void Update()
		{
			bool value = Options.Enabled.Value;
			Character val = (value ? Character.localCharacter : null);
			if ((Object)(object)val != (Object)(object)Controller.Character || value != _running)
			{
				Controller.Bind(val);
				_running = value;
			}
			Voice?.Update(val);
			if (!value || (Object)(object)val == (Object)null)
			{
				return;
			}
			try
			{
				Controller.Tick();
			}
			catch (Exception arg)
			{
				Controller.Disable(ReactionId.Falling);
				((BaseUnityPlugin)this).Logger.LogError((object)$"Falling detection disabled: {arg}");
			}
		}

		private void OnDisable()
		{
			Voice?.Unbind();
			Controller?.Bind(null);
			_running = false;
		}

		private void OnDestroy()
		{
			try
			{
				Voice?.Dispose();
			}
			finally
			{
				_patches?.Dispose();
				if ((Object)(object)Instance == (Object)(object)this)
				{
					Instance = null;
				}
			}
		}
	}
	internal sealed class ReactionController
	{
		private readonly Plugin _plugin;

		private readonly Cooldowns _cooldowns = new Cooldowns();

		private readonly PrePassOutEpisode _warning = new PrePassOutEpisode();

		private readonly ActivityEpisode _pulling = new ActivityEpisode();

		private readonly ActivityEpisode _pulled = new ActivityEpisode();

		private readonly FallingEpisode _fall = new FallingEpisode();

		private readonly bool[] _available = new bool[7] { true, true, true, true, true, true, true };

		public Character? Character { get; private set; }

		private ModConfig Options => _plugin.Options;

		public ReactionController(Plugin plugin)
		{
			_plugin = plugin;
		}

		public void Bind(Character? character)
		{
			Character = character;
			double unscaledTimeAsDouble = Time.unscaledTimeAsDouble;
			_cooldowns.Reset();
			_warning.Reset(((Object)(object)character != (Object)null) ? character.data.passOutValue : 0f);
			bool alreadyActive = (Object)(object)character != (Object)null && character.data.sinceGrabFriend < 0.35f;
			_pulling.Reset(unscaledTimeAsDouble, alreadyActive);
			_pulled.Reset(unscaledTimeAsDouble, alreadyActive);
			bool flag = (Object)(object)character != (Object)null && character.data.isClimbing && character.data.outOfStaminaFor > 0f && character.data.avarageVelocity.y <= -1f;
			_fall.Reset((Object)(object)character != (Object)null && !character.data.isGrounded && (flag || character.data.sinceGrounded >= 1.5f));
		}

		public void Disable(params ReactionId[] ids)
		{
			foreach (ReactionId reactionId in ids)
			{
				_available[(int)reactionId] = false;
			}
		}

		public bool Available(ReactionId id)
		{
			return _available[(int)id];
		}

		private bool Local(Character character)
		{
			if ((Object)(object)character != (Object)null && (Object)(object)character == (Object)(object)Character)
			{
				return character.IsLocal;
			}
			return false;
		}

		private bool Conscious(Character character)
		{
			if (Local(character) && character.data.fullyConscious)
			{
				return !character.IsGhost;
			}
			return false;
		}

		public void Tick()
		{
			if (!_available[3])
			{
				return;
			}
			Character character = Character;
			if (!((Object)(object)character == (Object)null))
			{
				bool flag = character.data.isClimbing && character.data.outOfStaminaFor > 0f;
				float y = character.data.avarageVelocity.y;
				bool flag2 = character.data.isGrounded || (character.data.isClimbingAnything && !flag);
				bool eligible = Conscious(character) && !flag2 && !character.data.isCarried;
				if (_fall.Observe(Time.unscaledTimeAsDouble, eligible, flag2, character.data.sinceGrounded, y, flag))
				{
					Debug(ReactionId.Falling, "detected " + (flag ? "wall slide" : "free fall") + ": " + $"air={character.data.sinceGrounded:F2}s, vy={y:F2}, " + $"stamina={character.data.currentStamina:F3}+{character.data.extraStamina:F3}");
					Emit(ReactionId.Falling);
				}
			}
		}

		public void LifeUpdated(Character character)
		{
			if (Local(character))
			{
				bool atRisk = character.refs.afflictions.shouldPassOut && !character.data.shouldPetrify;
				if (_warning.Observe(character.data.passOutValue, atRisk, Conscious(character)))
				{
					Emit(ReactionId.PrePassOut);
				}
			}
		}

		public void Helping(bool helping, bool beingHelped)
		{
			double unscaledTimeAsDouble = Time.unscaledTimeAsDouble;
			if (helping && _pulling.Seen(unscaledTimeAsDouble))
			{
				Emit(ReactionId.PullPlayer);
			}
			if (beingHelped && _pulled.Seen(unscaledTimeAsDouble))
			{
				Emit(ReactionId.BeingPulled);
			}
		}

		public void Landed(Character character, float airTime, float velocityY, bool wasConscious)
		{
			if (Local(character) && !character.data.isCarried && !character.IsGhost && LandingRule.Matches(wasConscious, airTime, velocityY))
			{
				Emit(ReactionId.HardLanding);
			}
		}

		public void Recovered(Character character, RecoverySnapshot before)
		{
			ReactionId? reactionId = before.Completed(Local(character) && character.data.fullyConscious);
			if (reactionId.HasValue)
			{
				_warning.Recovered();
				Emit(reactionId.Value);
			}
		}

		private void Emit(ReactionId id)
		{
			Character character = Character;
			if ((Object)(object)character == (Object)null || !Local(character) || !character.data.fullyConscious || (character.IsGhost && id != ReactionId.Revived) || !_available[(int)id] || !Options.Enabled.Value)
			{
				return;
			}
			EventOptions eventOptions = Options.Events[(int)id];
			if (eventOptions.Enabled.Value)
			{
				double unscaledTimeAsDouble = Time.unscaledTimeAsDouble;
				if (!_cooldowns.Ready(id, unscaledTimeAsDouble, ModConfig.Safe(Options.Cooldown, 3f)))
				{
					Debug(id, "cooldown");
					return;
				}
				if (_plugin.Voice == null)
				{
					Debug(id, "voice integration unavailable");
					return;
				}
				if (!_plugin.Voice.TryPlay(id, ModConfig.Safe(eventOptions.Volume, 1f), out AudioAsset selected, out string reason))
				{
					Debug(id, reason);
					return;
				}
				_cooldowns.Accepted(id, unscaledTimeAsDouble);
				Debug(id, "queued " + selected.Path + (string.IsNullOrEmpty(reason) ? string.Empty : (" (" + reason + ")")));
			}
		}

		private void Debug(ReactionId id, string message)
		{
			if (Options.Debug.Value)
			{
				_plugin.Log.LogInfo((object)$"{id}: {message}");
			}
		}
	}
	internal abstract class VoiceProcessor : IDisposable
	{
		private int _microphoneAllowed;

		private int _faulted;

		protected readonly float[] Work;

		protected readonly int Channels;

		private int _stopped;

		private int _mixedFrames;

		private int _micBlockedFrames;

		public MixBus Bus { get; }

		public int MixedFrames => Volatile.Read(in _mixedFrames);

		public int MicBlockedFrames => Volatile.Read(in _micBlockedFrames);

		public float MasterGain { get; set; } = 0.5f;

		public bool MicrophoneAllowed
		{
			get
			{
				return Volatile.Read(in _microphoneAllowed) != 0;
			}
			set
			{
				Volatile.Write(ref _microphoneAllowed, value ? 1 : 0);
			}
		}

		public bool Faulted => Volatile.Read(in _faulted) != 0;

		protected bool Stopped => Volatile.Read(in _stopped) != 0;

		public VoiceProcessor(int rate, int channels, int frameSize)
		{
			Bus = new MixBus(rate);
			Channels = channels;
			Work = new float[frameSize];
		}

		protected void CountMixedFrame(bool microphoneAllowed)
		{
			Interlocked.Increment(ref _mixedFrames);
			if (!microphoneAllowed)
			{
				Interlocked.Increment(ref _micBlockedFrames);
			}
		}

		protected void Fault()
		{
			Volatile.Write(ref _faulted, 1);
		}

		public void Stop()
		{
			Interlocked.Exchange(ref _stopped, 1);
			Bus.Clear();
		}

		public void Dispose()
		{
			Stop();
		}

		public abstract void Attach(LocalVoice voice);
	}
	internal sealed class FloatVoiceProcessor : VoiceProcessor, IProcessor<float>, IDisposable
	{
		public FloatVoiceProcessor(int rate, int channels, int frameSize)
			: base(rate, channels, frameSize)
		{
		}

		public float[] Process(float[] buffer)
		{
			if (base.Stopped || base.Faulted)
			{
				return null;
			}
			bool microphoneAllowed = base.MicrophoneAllowed;
			if (base.Bus.ActiveCount == 0)
			{
				if (!microphoneAllowed)
				{
					Array.Clear(buffer, 0, buffer.Length);
				}
				return buffer;
			}
			try
			{
				if (buffer.Length != Work.Length)
				{
					throw new InvalidOperationException("Voice frame size changed without rebinding.");
				}
				Array.Copy(buffer, Work, buffer.Length);
				base.Bus.Render(Work, Channels, microphoneAllowed, base.MasterGain);
				Array.Copy(Work, buffer, buffer.Length);
				CountMixedFrame(microphoneAllowed);
			}
			catch
			{
				Fault();
				return null;
			}
			if (!base.Stopped)
			{
				return buffer;
			}
			return null;
		}

		public override void Attach(LocalVoice voice)
		{
			((LocalVoiceFramed<float>)(object)(LocalVoiceAudio<float>)(object)voice).AddPostProcessor(new IProcessor<float>[1] { this });
		}
	}
	internal sealed class ShortVoiceProcessor : VoiceProcessor, IProcessor<short>, IDisposable
	{
		public ShortVoiceProcessor(int rate, int channels, int frameSize)
			: base(rate, channels, frameSize)
		{
		}

		public short[] Process(short[] buffer)
		{
			if (base.Stopped || base.Faulted)
			{
				return null;
			}
			bool microphoneAllowed = base.MicrophoneAllowed;
			if (base.Bus.ActiveCount == 0)
			{
				if (!microphoneAllowed)
				{
					Array.Clear(buffer, 0, buffer.Length);
				}
				return buffer;
			}
			try
			{
				if (buffer.Length != Work.Length)
				{
					throw new InvalidOperationException("Voice frame size changed without rebinding.");
				}
				for (int i = 0; i < buffer.Length; i++)
				{
					Work[i] = (float)buffer[i] / 32768f;
				}
				base.Bus.Render(Work, Channels, microphoneAllowed, base.MasterGain);
				for (int j = 0; j < buffer.Length; j++)
				{
					buffer[j] = (short)Math.Max(-32768, Math.Min(32767, (int)Math.Round(Work[j] * 32768f)));
				}
				CountMixedFrame(microphoneAllowed);
			}
			catch
			{
				Fault();
				return null;
			}
			if (!base.Stopped)
			{
				return buffer;
			}
			return null;
		}

		public override void Attach(LocalVoice voice)
		{
			((LocalVoiceFramed<short>)(object)(LocalVoiceAudio<short>)(object)voice).AddPostProcessor(new IProcessor<short>[1] { this });
		}
	}
	internal sealed class VoiceRuntime : IDisposable
	{
		private sealed class RecorderControl : IVoiceControl
		{
			private readonly LocalVoice _voice;

			public Recorder Recorder { get; }

			public CharacterVoiceHandler Handler { get; }

			public WebRtcAudioDsp? Dsp { get; }

			public LocalVoice? CurrentVoice
			{
				get
				{
					if (!((Object)(object)Recorder != (Object)null))
					{
						return null;
					}
					object? value = VoiceField.GetValue(Recorder);
					return (LocalVoice?)((value is LocalVoice) ? value : null);
				}
			}

			public bool Ready
			{
				get
				{
					//IL_004b: Unknown result type (might be due to invalid IL or missing references)
					//IL_0052: Invalid comparison between Unknown and I4
					if ((Object)(object)Recorder != (Object)null && Recorder.RecordingEnabled && Handler.Initialized)
					{
						object? value = ConnectionField.GetValue(Recorder);
						VoiceConnection val = (VoiceConnection)((value is VoiceConnection) ? value : null);
						if (val != null && (Object)(object)val != (Object)null)
						{
							return (int)val.ClientState == 9;
						}
					}
					return false;
				}
			}

			public bool Allowed
			{
				get
				{
					object value = PermissionField.GetValue(Handler);
					bool flag = default(bool);
					int num;
					if (value is bool)
					{
						flag = (bool)value;
						num = 1;
					}
					else
					{
						num = 0;
					}
					if (((uint)num & (flag ? 1u : 0u)) != 0)
					{
						object? value2 = PlayerDataField.GetValue(Handler);
						CharacterData val = (CharacterData)((value2 is CharacterData) ? value2 : null);
						if (val != null)
						{
							return !val.isBlocked;
						}
						return true;
					}
					return false;
				}
			}

			public string State
			{
				get
				{
					//IL_0059: Unknown result type (might be due to invalid IL or missing references)
					//IL_005e: Unknown result type (might be due to invalid IL or missing references)
					string text = $"recording={Recorder.RecordingEnabled}, initialized={Handler.Initialized}, ";
					object? value = ConnectionField.GetValue(Recorder);
					VoiceConnection val = (VoiceConnection)((value is VoiceConnection) ? value : null);
					return text + string.Format("voiceRoom={0}, allowed={1}", (val != null && (Object)(object)val != (Object)null) ? ((object)val.ClientState/*cast due to .constrained prefix*/).ToString() : "unavailable", Allowed);
				}
			}

			public bool TransmitEnabled
			{
				set
				{
					if (CurrentVoice == _voice)
					{
						Recorder.TransmitEnabled = value;
					}
				}
			}

			public bool VoiceDetection
			{
				get
				{
					if ((Object)(object)Recorder != (Object)null)
					{
						return Recorder.VoiceDetection;
					}
					return false;
				}
				set
				{
					if ((Object)(object)Recorder != (Object)null)
					{
						Recorder.VoiceDetection = value;
					}
				}
			}

			public bool DspVoiceDetection
			{
				get
				{
					if ((Object)(object)Dsp != (Object)null)
					{
						return Dsp.VAD;
					}
					return false;
				}
				set
				{
					if ((Object)(object)Dsp != (Object)null)
					{
						Dsp.VAD = value;
					}
				}
			}

			public RecorderControl(Recorder recorder, CharacterVoiceHandler handler, LocalVoice voice)
			{
				Recorder = recorder;
				Handler = handler;
				_voice = voice;
				Dsp = ((Component)recorder).GetComponent<WebRtcAudioDsp>();
			}

			public void RecreateRecording(bool vanillaTransmit)
			{
				if (CurrentVoice == _voice)
				{
					bool recordingEnabled = Recorder.RecordingEnabled;
					Recorder.RecordingEnabled = false;
					Recorder.TransmitEnabled = vanillaTransmit;
					Recorder.RecordingEnabled = recordingEnabled;
				}
			}
		}

		private static readonly FieldInfo VoiceField = Field(typeof(Recorder), "voice");

		private static readonly FieldInfo ConnectionField = Field(typeof(Recorder), "voiceConnection");

		private static readonly FieldInfo PermissionField = Field(typeof(CharacterVoiceHandler), "m_allowedToCommunicate");

		private static readonly FieldInfo PlayerDataField = Field(typeof(CharacterVoiceHandler), "m_characterData");

		private readonly Plugin _plugin;

		private readonly AudioLibrary _library;

		private VoiceSession? _session;

		private RecorderControl? _control;

		private LocalMonitor? _monitor;

		private bool _failed;

		private int _loggedMixedFrames;

		private double _nextSendLog;

		public VoiceRuntime(Plugin plugin, AudioLibrary library)
		{
			_plugin = plugin;
			_library = library;
		}

		private static FieldInfo Field(Type type, string name)
		{
			return AccessTools.Field(type, name) ?? throw new MissingFieldException(type.FullName, name);
		}

		public void Update(Character? character)
		{
			if (_session == null)
			{
				return;
			}
			try
			{
				if ((Object)(object)character == (Object)null || !_plugin.Options.Enabled.Value || !PhotonNetwork.InRoom || (Object)(object)_control.Recorder == (Object)null || (Object)(object)((Component)_control.Recorder).GetComponentInParent<Character>() != (Object)(object)character)
				{
					Unbind();
				}
				else if (_control.CurrentVoice != _session.Voice)
				{
					Unbind(recreateRecording: false);
				}
				else
				{
					if (_session.Processor.Faulted)
					{
						throw new InvalidOperationException("Voice audio callback failed.");
					}
					LocalMonitor? monitor = _monitor;
					if (monitor != null && monitor.Faulted)
					{
						_monitor.Dispose();
						_monitor = null;
						_plugin.Log.LogWarning((object)"Local monitor disabled after an audio callback error; voice transmission continues.");
					}
				}
				LogSendProgress();
			}
			catch (Exception ex)
			{
				Fail(ex);
			}
		}

		public void OnPushToTalk(CharacterVoiceHandler handler, bool requested)
		{
			//IL_01e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01eb: Unknown result type (might be due to invalid IL or missing references)
			if (_failed)
			{
				return;
			}
			Character componentInParent = ((Component)handler).GetComponentInParent<Character>();
			if ((Object)(object)componentInParent == (Object)null || !componentInParent.IsLocal || !handler.Initialized)
			{
				return;
			}
			if (!_plugin.Options.Enabled.Value || !PhotonNetwork.InRoom)
			{
				if ((Object)(object)_control?.Handler == (Object)(object)handler)
				{
					_session.ClearPlayback();
					_session.ApplyGate(requested, _control.Allowed);
					Unbind();
				}
				return;
			}
			Recorder component = ((Component)handler).GetComponent<Recorder>();
			if ((Object)(object)component == (Object)null || !component.RecordingEnabled)
			{
				return;
			}
			object? value = VoiceField.GetValue(component);
			LocalVoice val = (LocalVoice)((value is LocalVoice) ? value : null);
			if (!(val is LocalVoiceAudio<float>) && !(val is LocalVoiceAudio<short>))
			{
				return;
			}
			if (_session?.Voice != val)
			{
				Unbind(_control != null && (Object)(object)_control.Recorder != (Object)(object)component);
				_control = new RecorderControl(component, handler, val);
				_session = new VoiceSession(val, _control, requested, _control.Allowed);
				_loggedMixedFrames = 0;
				_nextSendLog = 0.0;
				try
				{
					_monitor = new LocalMonitor(((Component)_plugin).gameObject, _session.LocalBus, _session.SampleRate);
				}
				catch (Exception ex)
				{
					_plugin.Log.LogWarning((object)("Local monitor unavailable; voice transmission continues: " + ex.Message));
				}
				_library.RequestRate(_session.SampleRate);
				ManualLogSource log = _plugin.Log;
				object[] obj = new object[4]
				{
					((object)val).GetType().Name,
					_session.SampleRate,
					null,
					null
				};
				VoiceInfo info = val.Info;
				obj[2] = ((VoiceInfo)(ref info)).Channels;
				obj[3] = _control.State;
				log.LogInfo((object)string.Format("Bound {0}: {1} Hz, {2} channel(s); {3}.", obj));
			}
			_session.Processor.MasterGain = ModConfig.Safe(_plugin.Options.MasterVolume, 0.5f);
			_monitor?.SetGain(_session.Processor.MasterGain);
			_session.ApplyGate(requested, _control.Allowed, _control.Ready, _monitor != null);
		}

		private void LogSendProgress()
		{
			VoiceSession session = _session;
			if (session != null && _plugin.Options.Debug.Value)
			{
				int mixedFrames = session.Processor.MixedFrames;
				if (mixedFrames != _loggedMixedFrames && !(Time.unscaledTimeAsDouble < _nextSendLog))
				{
					_loggedMixedFrames = mixedFrames;
					_nextSendLog = Time.unscaledTimeAsDouble + 1.0;
					_plugin.Log.LogInfo((object)($"VoiceSend: mixedFrames={mixedFrames}, micBlockedFrames={session.Processor.MicBlockedFrames}; " + $"sdkSentFrames={session.Voice.FramesSent}, sdkSentBytes={session.Voice.FramesSentBytes}; " + $"transmit={session.Voice.TransmitEnabled}, {_control.State}. SDK totals include normal voice/EOS; receiver delivery unverified."));
				}
			}
		}

		public bool TryPlay(ReactionId id, float volume, out AudioAsset? selected, out string reason)
		{
			selected = null;
			reason = string.Empty;
			try
			{
				if (_failed)
				{
					reason = "voice integration stopped after an error";
					return false;
				}
				if (_session == null)
				{
					reason = "no local voice session";
					return false;
				}
				bool flag = _library.IsPrepared(_session.SampleRate);
				if (!PhotonNetwork.InRoom || !flag || !_control.Allowed)
				{
					reason = $"playback blocked: gameRoom={PhotonNetwork.InRoom}, pcmReady={flag}, {_control.State}";
					return false;
				}
				AudioAsset audioAsset = _library.Select(id, _session.SampleRate);
				if (audioAsset == null)
				{
					reason = $"no clip prepared at {_session.SampleRate} Hz";
					return false;
				}
				if (!_session.RequestPlay(id, audioAsset.Variants[_session.SampleRate], volume))
				{
					reason = "same event already pending/playing, or processor stopped";
					return false;
				}
				if (!_control.Ready)
				{
					reason = "local monitor only; network voice is not joined";
				}
				_library.Accepted(id, audioAsset);
				selected = audioAsset;
				return true;
			}
			catch (Exception ex)
			{
				reason = ex.Message;
				Fail(ex);
				return false;
			}
		}

		public void RecorderStopping(Recorder recorder)
		{
			if ((Object)(object)_control?.Recorder == (Object)(object)recorder)
			{
				Unbind(recreateRecording: false);
			}
		}

		public void DspChanged(WebRtcAudioDsp dsp)
		{
			if ((Object)(object)_control?.Dsp == (Object)(object)dsp)
			{
				Unbind();
			}
		}

		public void Unbind(bool recreateRecording = true)
		{
			VoiceSession session = _session;
			RecorderControl control = _control;
			LocalMonitor monitor = _monitor;
			_session = null;
			_control = null;
			_monitor = null;
			try
			{
				session?.Close(recreateRecording && control.CurrentVoice == session.Voice);
			}
			finally
			{
				monitor?.Dispose();
			}
		}

		public void Fail(Exception ex)
		{
			if (_failed)
			{
				return;
			}
			_failed = true;
			_plugin.Log.LogError((object)$"Reaction voice disabled: {ex}");
			try
			{
				Unbind();
			}
			catch (Exception arg)
			{
				_plugin.Log.LogError((object)$"Recording retirement error: {arg}");
			}
		}

		public void Dispose()
		{
			Unbind();
		}
	}
	internal interface IVoiceControl
	{
		bool TransmitEnabled { set; }

		bool VoiceDetection { get; set; }

		bool DspVoiceDetection { get; set; }

		void RecreateRecording(bool vanillaTransmit);
	}
	internal sealed class VoiceSession
	{
		private readonly IVoiceControl _control;

		private readonly DetectionOverride _detection = new DetectionOverride();

		private readonly float[]?[] _pending = new float[7][];

		private readonly float[] _gains = new float[7];

		private bool _vanillaTransmit;

		public LocalVoice Voice { get; }

		public VoiceProcessor Processor { get; }

		public MixBus LocalBus { get; }

		public int SampleRate
		{
			get
			{
				//IL_0006: Unknown result type (might be due to invalid IL or missing references)
				//IL_000b: Unknown result type (might be due to invalid IL or missing references)
				VoiceInfo info = Voice.Info;
				return ((VoiceInfo)(ref info)).SamplingRate;
			}
		}

		public bool Closed { get; private set; }

		public VoiceSession(LocalVoice voice, IVoiceControl control, bool microphoneRequested, bool allowed)
		{
			//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)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
			Voice = voice;
			_control = control;
			VoiceInfo info;
			object obj;
			if (!(voice is LocalVoiceAudio<float>))
			{
				info = voice.Info;
				int samplingRate = ((VoiceInfo)(ref info)).SamplingRate;
				info = voice.Info;
				int channels = ((VoiceInfo)(ref info)).Channels;
				info = voice.Info;
				obj = new ShortVoiceProcessor(samplingRate, channels, ((VoiceInfo)(ref info)).FrameSize);
			}
			else
			{
				info = voice.Info;
				int samplingRate2 = ((VoiceInfo)(ref info)).SamplingRate;
				info = voice.Info;
				int channels2 = ((VoiceInfo)(ref info)).Channels;
				info = voice.Info;
				obj = new FloatVoiceProcessor(samplingRate2, channels2, ((VoiceInfo)(ref info)).FrameSize);
			}
			Processor = (VoiceProcessor)obj;
			info = voice.Info;
			LocalBus = new MixBus(((VoiceInfo)(ref info)).SamplingRate);
			_vanillaTransmit = microphoneRequested && allowed;
			Processor.MicrophoneAllowed = _vanillaTransmit;
			Processor.Attach(voice);
		}

		public bool Busy(ReactionId id)
		{
			if (_pending[(int)id] == null && !Processor.Bus.IsActive(id))
			{
				return LocalBus.IsActive(id);
			}
			return true;
		}

		public bool RequestPlay(ReactionId id, float[] pcm, float gain)
		{
			if (Closed || Processor.Faulted || pcm.Length == 0 || Busy(id))
			{
				return false;
			}
			_pending[(int)id] = pcm;
			_gains[(int)id] = gain;
			return true;
		}

		public void ApplyGate(bool microphoneRequested, bool allowed, bool networkReady = true, bool localMonitorReady = true)
		{
			if (Closed)
			{
				return;
			}
			_vanillaTransmit = microphoneRequested && allowed;
			Processor.MicrophoneAllowed = _vanillaTransmit;
			if (!allowed)
			{
				ClearPlayback();
			}
			else
			{
				if (!networkReady)
				{
					Processor.Bus.Clear();
				}
				for (int i = 0; i < _pending.Length; i++)
				{
					float[] array = _pending[i];
					if (array != null)
					{
						if (networkReady)
						{
							Processor.Bus.TryStart((ReactionId)i, array, _gains[i]);
						}
						if (localMonitorReady)
						{
							LocalBus.TryStart((ReactionId)i, array, _gains[i]);
						}
						_pending[i] = null;
					}
				}
			}
			bool flag = Processor.Bus.ActiveCount > 0;
			if (flag)
			{
				_detection.Enter(_control.VoiceDetection, _control.DspVoiceDetection);
				_control.VoiceDetection = false;
				_control.DspVoiceDetection = false;
			}
			else
			{
				RestoreDetection();
			}
			_control.TransmitEnabled = new TransmissionGate(microphoneRequested, allowed, flag).TransmitEnabled;
		}

		public void ClearPlayback()
		{
			Array.Clear(_pending, 0, _pending.Length);
			Processor.Bus.Clear();
			LocalBus.Clear();
		}

		private void RestoreDetection()
		{
			if (_detection.Release(out var voiceDetection, out var dspVad))
			{
				_control.VoiceDetection = voiceDetection;
				_control.DspVoiceDetection = dspVad;
			}
		}

		public void Close(bool recreateRecording)
		{
			if (Closed)
			{
				return;
			}
			Closed = true;
			Processor.Stop();
			ClearPlayback();
			Voice.TransmitEnabled = false;
			try
			{
				RestoreDetection();
			}
			finally
			{
				if (recreateRecording)
				{
					_control.RecreateRecording(_vanillaTransmit);
				}
				else
				{
					_control.TransmitEnabled = _vanillaTransmit;
				}
			}
		}
	}
}
namespace PeakReactionAudio.Core
{
	internal readonly struct RecoverySnapshot
	{
		private readonly bool _dead;

		private readonly bool _knockedOut;

		public RecoverySnapshot(bool dead, bool knockedOut)
		{
			_dead = dead;
			_knockedOut = knockedOut;
		}

		public ReactionId? Completed(bool conscious)
		{
			if (conscious)
			{
				if (!_dead)
				{
					if (!_knockedOut)
					{
						return null;
					}
					return ReactionId.RegainConsciousness;
				}
				return ReactionId.Revived;
			}
			return null;
		}
	}
	internal sealed class PrePassOutEpisode
	{
		private float _previous;

		private bool _consumed;

		public void Reset(float progress)
		{
			_previous = progress;
			_consumed = true;
		}

		public void Recovered()
		{
			_previous = 0f;
			_consumed = false;
		}

		public bool Observe(float progress, bool atRisk, bool conscious)
		{
			if (progress <= 0f && conscious)
			{
				_consumed = false;
			}
			bool num = !_consumed && conscious && atRisk && _previous < 0.5f && progress >= 0.5f;
			_previous = progress;
			if (num)
			{
				_consumed = true;
			}
			return num;
		}
	}
	internal sealed class ActivityEpisode
	{
		public const float EndGap = 0.35f;

		private double _lastSeen = double.NegativeInfinity;

		private bool _active;

		public void Reset(double now, bool alreadyActive)
		{
			_active = alreadyActive;
			_lastSeen = (alreadyActive ? now : double.NegativeInfinity);
		}

		public bool Seen(double now)
		{
			bool result = !_active || now - _lastSeen >= 0.3499999940395355;
			_active = true;
			_lastSeen = now;
			return result;
		}
	}
	internal sealed class FallingEpisode
	{
		public const float MinAirTime = 1.5f;

		public const float MinSlideSpeed = 1f;

		private bool _consumed;

		private double _descendingSince = double.NaN;

		public void Reset(bool alreadyFalling)
		{
			_consumed = alreadyFalling;
			_descendingSince = double.NaN;
		}

		public bool Observe(double now, bool eligible, bool stable, float airTime, float velocityY, bool exhaustedClimb = false)
		{
			if (stable)
			{
				_consumed = false;
			}
			bool flag = (exhaustedClimb ? (velocityY <= -1f) : (airTime >= 1.5f && velocityY <= -6f));
			if (!eligible || stable || !flag)
			{
				_descendingSince = double.NaN;
				return false;
			}
			if (double.IsNaN(_descendingSince))
			{
				_descendingSince = now;
			}
			if (_consumed || now - _descendingSince < 0.20000000298023224)
			{
				return false;
			}
			_consumed = true;
			return true;
		}
	}
	internal static class LandingRule
	{
		public static bool Matches(bool conscious, float airTime, float velocityY)
		{
			if (conscious && airTime >= 0.8f)
			{
				return velocityY <= -10f;
			}
			return false;
		}
	}
	internal sealed class MixBus
	{
		private sealed class Slot
		{
			public float[]? Samples;

			public int Cursor;

			public float Gain;
		}

		private readonly object _sync = new object();

		private readonly Slot[] _slots = new Slot[7];

		private readonly int _sampleRate;

		private int _active;

		public int ActiveCount => Volatile.Read(in _active);

		public MixBus(int sampleRate)
		{
			_sampleRate = sampleRate;
			for (int i = 0; i < _slots.Length; i++)
			{
				_slots[i] = new Slot();
			}
		}

		public bool IsActive(ReactionId id)
		{
			lock (_sync)
			{
				return _slots[(int)id].Samples != null;
			}
		}

		public bool TryStart(ReactionId id, float[] samples, float gain)
		{
			if (samples.Length == 0)
			{
				return false;
			}
			lock (_sync)
			{
				Slot slot = _slots[(int)id];
				if (slot.Samples != null)
				{
					return false;
				}
				slot.Samples = samples;
				slot.Cursor = 0;
				slot.Gain = gain;
				Volatile.Write(ref _active, _active + 1);
				return true;
			}
		}

		public void Clear()
		{
			lock (_sync)
			{
				Slot[] slots = _slots;
				foreach (Slot obj in slots)
				{
					obj.Samples = null;
					obj.Cursor = 0;
				}
				Volatile.Write(ref _active, 0);
			}
		}

		public void Render(float[] buffer, int channels, bool microphoneAllowed, float masterGain)
		{
			if (channels <= 0 || buffer.Length % channels != 0)
			{
				throw new ArgumentException("Invalid output shape.");
			}
			lock (_sync)
			{
				int active = _active;
				if (active == 0)
				{
					if (!microphoneAllowed)
					{
						Array.Clear(buffer, 0, buffer.Length);
					}
					return;
				}
				float num = masterGain / (float)Math.Sqrt(active);
				int num2 = Math.Max(1, _sampleRate / 200);
				int num3 = Math.Max(1, _sampleRate / 50);
				float num4 = 0f;
				for (int i = 0; i < buffer.Length / channels; i++)
				{
					float num5 = 0f;
					Slot[] slots = _slots;
					foreach (Slot slot in slots)
					{
						float[] samples = slot.Samples;
						if (samples != null)
						{
							int cursor = slot.Cursor;
							float num6 = Math.Min(1f, Math.Min(((float)cursor + 1f) / (float)num2, (float)(samples.Length - cursor) / (float)num3));
							num5 += samples[cursor] * slot.Gain * num6;
							if (++slot.Cursor == samples.Length)
							{
								slot.Samples = null;
								_active--;
							}
						}
					}
					num5 *= num;
					for (int k = 0; k < channels; k++)
					{
						int num7 = i * channels + k;
						float num8 = (microphoneAllowed ? Limits.Clamp(buffer[num7], -1f, 1f, 0f) : 0f);
						buffer[num7] = num8 + num5;
						num4 = Math.Max(num4, Math.Abs(buffer[num7]));
					}
				}
				if (num4 > 0.98f)
				{
					float num9 = 0.98f / num4;
					for (int l = 0; l < buffer.Length; l++)
					{
						buffer[l] *= num9;
					}
				}
				Volatile.Write(ref _active, _active);
			}
		}
	}
	internal static class Pcm
	{
		public static float[] Downmix(float[] input, int channels)
		{
			if (channels <= 0 || input.Length == 0 || input.Length % channels != 0)
			{
				throw new ArgumentException("Invalid PCM shape.");
			}
			float[] array = new float[input.Length / channels];
			for (int i = 0; i < array.Length; i++)
			{
				double num = 0.0;
				for (int j = 0; j < channels; j++)
				{
					num += (double)Limits.Clamp(input[i * channels + j], -1f, 1f, 0f);
				}
				array[i] = (float)(num / (double)channels);
			}
			return array;
		}

		public static float[] Resample(float[] mono, int sourceRate, int targetRate)
		{
			if (sourceRate <= 0 || targetRate <= 0 || mono.Length == 0)
			{
				throw new ArgumentException("Invalid sample rate.");
			}
			if (sourceRate == targetRate)
			{
				return mono;
			}
			int num = checked((int)Math.Max(1.0, Math.Round((double)mono.Length * (double)targetRate / (double)sourceRate)));
			float[] array = new float[num];
			double num2 = (double)sourceRate / (double)targetRate;
			double num3 = Math.Min(1.0, (double)targetRate / (double)sourceRate) * 0.94;
			for (int i = 0; i < num; i++)
			{
				double num4 = (double)i * num2;
				double num5 = 0.0;
				double num6 = 0.0;
				int num7 = (int)num4;
				for (int j = num7 - 24 + 1; j <= num7 + 24; j++)
				{
					if (j >= 0 && j < mono.Length)
					{
						double num8 = num4 - (double)j;
						if (!(Math.Abs(num8) >= 24.0))
						{
							double num9 = Math.PI * num8 * num3;
							double num10 = ((Math.Abs(num9) < 1E-10) ? 1.0 : (Math.Sin(num9) / num9));
							double num11 = num3 * num10 * (0.5 + 0.5 * Math.Cos(Math.PI * num8 / 24.0));
							num5 += (double)mono[j] * num11;
							num6 += num11;
						}
					}
				}
				array[i] = Limits.Clamp((float)((Math.Abs(num6) > 1E-10) ? (num5 / num6) : 0.0), -1f, 1f, 0f);
			}
			return array;
		}
	}
	internal sealed class PcmBudget
	{
		public long UsedBytes { get; private set; }

		public bool CanFit(long bytes)
		{
			if (bytes > 0)
			{
				return bytes <= 134217728 - UsedBytes;
			}
			return false;
		}

		public bool Reserve(long bytes)
		{
			if (!CanFit(bytes))
			{
				return false;
			}
			UsedBytes += bytes;
			return true;
		}
	}
	internal enum ReactionId
	{
		PrePassOut,
		PullPlayer,
		BeingPulled,
		Falling,
		HardLanding,
		RegainConsciousness,
		Revived
	}
	internal static class Limits
	{
		public const int ReactionCount = 7;

		public const float DefaultCooldown = 3f;

		public const long CacheBytes = 134217728L;

		public const int MaxClipSeconds = 60;

		public static float Clamp(float value, float min, float max, float fallback)
		{
			if (float.IsNaN(value) || float.IsInfinity(value))
			{
				value = fallback;
			}
			return Math.Max(min, Math.Min(max, value));
		}
	}
	internal sealed class Cooldowns
	{
		private readonly double[] _last = new double[7];

		public Cooldowns()
		{
			Reset();
		}

		public bool Ready(ReactionId id, double now, double seconds)
		{
			return now - _last[(int)id] >= seconds;
		}

		public void Accepted(ReactionId id, double now)
		{
			_last[(int)id] = now;
		}

		public void Reset()
		{
			for (int i = 0; i < _last.Length; i++)
			{
				_last[i] = double.NegativeInfinity;
			}
		}
	}
	internal static class ClipChoice
	{
		public static int Next(int count, int previous, Random random)
		{
			if (count <= 0)
			{
				return -1;
			}
			if (count == 1)
			{
				return 0;
			}
			if (previous < 0 || previous >= count)
			{
				return random.Next(count);
			}
			int num = random.Next(count - 1);
			if (num < previous)
			{
				return num;
			}
			return num + 1;
		}
	}
	internal readonly struct TransmissionGate
	{
		public bool MicrophoneAllowed { get; }

		public bool TransmitEnabled { get; }

		public TransmissionGate(bool requestedMicrophone, bool communicationAllowed, bool hasEffect)
		{
			MicrophoneAllowed = communicationAllowed && requestedMicrophone;
			TransmitEnabled = communicationAllowed && (requestedMicrophone || hasEffect);
		}
	}
	internal sealed class DetectionOverride
	{
		private bool _voiceDetection;

		private bool _dspVad;

		public bool Active { get; private set; }

		public void Enter(bool voiceDetection, bool dspVad)
		{
			if (!Active)
			{
				_voiceDetection = voiceDetection;
				_dspVad = dspVad;
				Active = true;
			}
		}

		public bool Release(out bool voiceDetection, out bool dspVad)
		{
			voiceDetection = _voiceDetection;
			dspVad = _dspVad;
			if (!Active)
			{
				return false;
			}
			Active = false;
			return true;
		}
	}
}