Decompiled source of KirbyScream v1.3.0

KirbyScream.dll

Decompiled a week ago
using System;
using System.Collections;
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 BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
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("KirbyScream")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.3.0.0")]
[assembly: AssemblyInformationalVersion("1.3.0+2b078dcb782410e4f233c89073c7f271d00c7f67")]
[assembly: AssemblyProduct("KirbyScream")]
[assembly: AssemblyTitle("KirbyScream")]
[assembly: AssemblyVersion("1.3.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace KirbyScream
{
	public class FallScreamController : MonoBehaviour, IOnEventCallback
	{
		private enum BroadcastPath
		{
			None,
			VoiceChat,
			ModNetwork
		}

		private static readonly string[] AudioExtensions = new string[3] { ".ogg", ".wav", ".mp3" };

		private static readonly FieldRef<CharacterBalloons, bool> ParachuteOpenField = MakeParachuteRef();

		private AudioClip _clip;

		private ScreamVoice _localVoice;

		private readonly Dictionary<int, ScreamVoice> _remoteVoices = new Dictionary<int, ScreamVoice>();

		private bool _screaming;

		private float _notFallingFor;

		private BroadcastPath _path;

		private bool _warnedVoiceFallback;

		private static FieldRef<CharacterBalloons, bool> MakeParachuteRef()
		{
			try
			{
				return AccessTools.FieldRefAccess<CharacterBalloons, bool>("_isParachuteOpen");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Could not bind CharacterBalloons._isParachuteOpen, falling back to parachute GameObject state. " + ex.Message));
				return null;
			}
		}

		private void Awake()
		{
			((MonoBehaviour)this).StartCoroutine(LoadClip());
		}

		private void OnEnable()
		{
			PhotonNetwork.AddCallbackTarget((object)this);
		}

		private void OnDisable()
		{
			PhotonNetwork.RemoveCallbackTarget((object)this);
			VoiceInjector.Current?.EndScream(0f);
			StopAllVoices();
		}

		private static string ResolveAudioPath()
		{
			string text = Plugin.AudioFile.Value?.Trim();
			if (!string.IsNullOrEmpty(text))
			{
				string text2 = (Path.IsPathRooted(text) ? text : Path.Combine(Plugin.PluginDir, text));
				if (File.Exists(text2))
				{
					return text2;
				}
				Plugin.Log.LogWarning((object)("AudioFile '" + text + "' not found, scanning the mod folder instead."));
			}
			return (from f in Directory.GetFiles(Plugin.PluginDir)
				where AudioExtensions.Contains(Path.GetExtension(f).ToLowerInvariant())
				select f).OrderBy<string, string>((string f) => f, StringComparer.OrdinalIgnoreCase).FirstOrDefault();
		}

		private static AudioType AudioTypeFor(string path)
		{
			return (AudioType)(Path.GetExtension(path).ToLowerInvariant() switch
			{
				".ogg" => 14, 
				".wav" => 20, 
				".mp3" => 13, 
				_ => 0, 
			});
		}

		private IEnumerator LoadClip()
		{
			string path = ResolveAudioPath();
			if (path == null)
			{
				Plugin.Log.LogError((object)("No scream audio found. Put a .ogg/.wav/.mp3 in: " + Plugin.PluginDir));
				yield break;
			}
			string absoluteUri = new Uri(path).AbsoluteUri;
			Plugin.Log.LogInfo((object)("Loading scream audio: " + path));
			UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip(absoluteUri, AudioTypeFor(path));
			try
			{
				yield return req.SendWebRequest();
				if ((int)req.result != 1)
				{
					Plugin.Log.LogError((object)("Failed to load '" + path + "': " + req.error));
					yield break;
				}
				AudioClip content = DownloadHandlerAudioClip.GetContent(req);
				if ((Object)(object)content == (Object)null || content.length <= 0f)
				{
					Plugin.Log.LogError((object)("'" + path + "' loaded but produced an empty AudioClip."));
					yield break;
				}
				((Object)content).name = Path.GetFileNameWithoutExtension(path);
				_clip = content;
				Plugin.Log.LogInfo((object)$"Scream ready: {((Object)content).name} ({content.length:0.00}s)");
			}
			finally
			{
				((IDisposable)req)?.Dispose();
			}
		}

		private static bool IsParachuteOpen(Character c)
		{
			CharacterBalloons val = c.refs?.balloons;
			if ((Object)(object)val != (Object)null && ParachuteOpenField != null)
			{
				return ParachuteOpenField.Invoke(val);
			}
			Parachute val2 = c.refs?.parachute;
			if ((Object)(object)val2 != (Object)null)
			{
				return ((Component)val2).gameObject.activeSelf;
			}
			return false;
		}

		private void Update()
		{
			Character localCharacter = Character.localCharacter;
			if ((Object)(object)localCharacter == (Object)null || (Object)(object)localCharacter.data == (Object)null || localCharacter.refs == null)
			{
				if (_screaming)
				{
					Stop("no local character");
				}
				return;
			}
			MaintainVoiceInjection(localCharacter);
			CharacterData data = localCharacter.data;
			string text = null;
			if (data.dead)
			{
				text = "died";
			}
			else if (data.isGrounded)
			{
				text = "landed";
			}
			else if (Plugin.StopWhenClimbing.Value && data.isClimbingAnything)
			{
				text = "grabbed something";
			}
			else if (data.isCarried || (Object)(object)data.carrier != (Object)null)
			{
				text = "being carried";
			}
			else if (Plugin.StopWhenParachuteOpens.Value && IsParachuteOpen(localCharacter))
			{
				text = "parachute open";
			}
			bool flag = false;
			if (text == null && data.avarageVelocity.y < 0f - Plugin.MinDownSpeed.Value)
			{
				bool num = (Object)(object)localCharacter.refs.movement != (Object)null && localCharacter.refs.movement.FallTime() >= Plugin.MinFallTime.Value;
				bool flag2 = Plugin.TriggerOnRagdollFall.Value && data.fallSeconds > 0f;
				flag = num || flag2;
			}
			if (flag)
			{
				_notFallingFor = 0f;
				if (!_screaming)
				{
					Begin(localCharacter);
				}
			}
			else
			{
				if (!_screaming)
				{
					return;
				}
				if (text != null)
				{
					Stop(text);
					return;
				}
				_notFallingFor += Time.deltaTime;
				if (_notFallingFor >= Plugin.StopGraceSeconds.Value)
				{
					Stop("no longer falling");
				}
			}
		}

		private void Begin(Character local)
		{
			_screaming = true;
			if ((Object)(object)_clip == (Object)null)
			{
				return;
			}
			if ((Object)(object)_localVoice == (Object)null || !_localVoice.IsFor(local))
			{
				if ((Object)(object)_localVoice != (Object)null)
				{
					_localVoice.Dispose();
				}
				_localVoice = ScreamVoice.Create(local, _clip, spatial: false);
			}
			_localVoice.Play();
			_path = StartBroadcast(local);
			if (Plugin.DebugLog.Value)
			{
				Plugin.Log.LogInfo((object)$"SCREAM start (sent via {_path})");
			}
		}

		private void Stop(string reason)
		{
			bool screaming = _screaming;
			_screaming = false;
			_notFallingFor = 0f;
			if ((Object)(object)_localVoice != (Object)null)
			{
				_localVoice.Stop();
			}
			if (screaming)
			{
				EndBroadcast();
				if (Plugin.DebugLog.Value)
				{
					Plugin.Log.LogInfo((object)("SCREAM stop (" + reason + ")"));
				}
			}
		}

		private BroadcastPath StartBroadcast(Character local)
		{
			switch (Plugin.Broadcast.Value)
			{
			case BroadcastMode.Off:
				return BroadcastPath.None;
			case BroadcastMode.VoiceChat:
			{
				VoiceInjector current = VoiceInjector.Current;
				if ((Object)(object)current != (Object)null && current.BeginScream())
				{
					return BroadcastPath.VoiceChat;
				}
				if (!_warnedVoiceFallback)
				{
					_warnedVoiceFallback = true;
					Plugin.Log.LogWarning((object)"Voice chat is not available (no microphone, voice not connected yet, or the stream format is unsupported). Falling back to ModNetwork for now.");
				}
				goto case BroadcastMode.ModNetwork;
			}
			case BroadcastMode.ModNetwork:
				if (!CanBroadcast())
				{
					return BroadcastPath.None;
				}
				Broadcast(local, start: true);
				return BroadcastPath.ModNetwork;
			default:
				return BroadcastPath.None;
			}
		}

		private void EndBroadcast()
		{
			switch (_path)
			{
			case BroadcastPath.VoiceChat:
				VoiceInjector.Current?.EndScream(Plugin.StopFadeSeconds.Value);
				break;
			case BroadcastPath.ModNetwork:
				Broadcast(Character.localCharacter, start: false);
				break;
			}
			_path = BroadcastPath.None;
		}

		private void MaintainVoiceInjection(Character local)
		{
			if (Plugin.Broadcast.Value != BroadcastMode.VoiceChat)
			{
				return;
			}
			CharacterVoiceHandler voice = local.refs.voice;
			if ((Object)(object)voice == (Object)null)
			{
				return;
			}
			VoiceInjector voiceInjector = VoiceInjector.Current;
			if ((Object)(object)voiceInjector == (Object)null || (Object)(object)((Component)voiceInjector).gameObject != (Object)(object)((Component)voice).gameObject)
			{
				if ((Object)(object)((Component)voice).GetComponent<Recorder>() == (Object)null)
				{
					return;
				}
				voiceInjector = ((Component)voice).GetComponent<VoiceInjector>();
				if ((Object)(object)voiceInjector == (Object)null)
				{
					voiceInjector = ((Component)voice).gameObject.AddComponent<VoiceInjector>();
				}
			}
			if (!((Object)(object)_clip != (Object)null) || !voiceInjector.Ready || VoiceScreamMixer.HasSamplesFor(voiceInjector.SamplingRate, voiceInjector.Channels))
			{
				return;
			}
			float[] array = VoiceScreamMixer.ConvertClip(_clip, voiceInjector.SamplingRate, voiceInjector.Channels);
			if (array != null)
			{
				VoiceScreamMixer.SetSamples(array, voiceInjector.SamplingRate, voiceInjector.Channels);
				if (Plugin.DebugLog.Value)
				{
					Plugin.Log.LogInfo((object)$"Scream converted for voice chat: {voiceInjector.SamplingRate} Hz, {voiceInjector.Channels} ch, {array.Length} samples.");
				}
			}
			else
			{
				Plugin.Log.LogWarning((object)"Could not read the scream clip's samples for voice chat.");
			}
		}

		private static bool CanBroadcast()
		{
			if (PhotonNetwork.IsConnected)
			{
				return PhotonNetwork.InRoom;
			}
			return false;
		}

		private void Broadcast(Character local, bool start)
		{
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Expected O, but got Unknown
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			if (CanBroadcast() && !((Object)(object)local == (Object)null) && !((Object)(object)((MonoBehaviourPun)local).photonView == (Object)null))
			{
				RaiseEventOptions val = new RaiseEventOptions
				{
					Receivers = (ReceiverGroup)0
				};
				PhotonNetwork.RaiseEvent((byte)Plugin.NetworkEventCode.Value, (object)new object[2]
				{
					((MonoBehaviourPun)local).photonView.ViewID,
					start
				}, val, SendOptions.SendReliable);
			}
		}

		public void OnEvent(EventData photonEvent)
		{
			Character val = default(Character);
			if (photonEvent.Code == (byte)Plugin.NetworkEventCode.Value && Plugin.HearOthers.Value && photonEvent.CustomData is object[] array && array.Length >= 2 && array[0] is int num && array[1] is bool flag && Character.GetCharacterWithPhotonID(num, ref val) && !((Object)(object)val == (Object)null) && !val.IsLocal)
			{
				if (flag)
				{
					StartRemote(num, val);
				}
				else
				{
					StopRemote(num);
				}
			}
		}

		private void StartRemote(int viewId, Character character)
		{
			if ((Object)(object)_clip == (Object)null)
			{
				return;
			}
			if (!_remoteVoices.TryGetValue(viewId, out var value) || (Object)(object)value == (Object)null || !value.IsFor(character))
			{
				if ((Object)(object)value != (Object)null)
				{
					value.Dispose();
				}
				value = ScreamVoice.Create(character, _clip, spatial: true);
				_remoteVoices[viewId] = value;
			}
			value.Play();
			if (Plugin.DebugLog.Value)
			{
				Plugin.Log.LogInfo((object)("SCREAM start (remote " + character.characterName + ")"));
			}
		}

		private void StopRemote(int viewId)
		{
			if (_remoteVoices.TryGetValue(viewId, out var value))
			{
				if ((Object)(object)value != (Object)null)
				{
					value.Stop();
				}
				if (Plugin.DebugLog.Value)
				{
					Plugin.Log.LogInfo((object)$"SCREAM stop (remote {viewId})");
				}
			}
		}

		private void StopAllVoices()
		{
			if ((Object)(object)_localVoice != (Object)null)
			{
				_localVoice.Stop();
			}
			foreach (ScreamVoice value in _remoteVoices.Values)
			{
				if ((Object)(object)value != (Object)null)
				{
					value.Dispose();
				}
			}
			_remoteVoices.Clear();
		}
	}
	public enum BroadcastMode
	{
		VoiceChat,
		ModNetwork,
		Off
	}
	[BepInPlugin("toiletking.peak.kirbyscream", "KirbyScream", "1.3.0")]
	public class Plugin : BaseUnityPlugin
	{
		public const string GUID = "toiletking.peak.kirbyscream";

		public const string NAME = "KirbyScream";

		public const string VERSION = "1.3.0";

		internal static ManualLogSource Log;

		internal static string PluginDir;

		internal static ConfigEntry<string> AudioFile;

		internal static ConfigEntry<float> Volume;

		internal static ConfigEntry<bool> Loop;

		internal static ConfigEntry<float> ResumeWindowSeconds;

		internal static ConfigEntry<bool> UseGameSfxMixer;

		internal static ConfigEntry<float> MinFallTime;

		internal static ConfigEntry<float> MinDownSpeed;

		internal static ConfigEntry<bool> TriggerOnRagdollFall;

		internal static ConfigEntry<BroadcastMode> Broadcast;

		internal static ConfigEntry<float> VoiceChatVolume;

		internal static ConfigEntry<bool> ForceTransmitWithPushToTalk;

		internal static ConfigEntry<bool> PauseEchoCancellation;

		internal static ConfigEntry<bool> HearOthers;

		internal static ConfigEntry<float> RemoteVolume;

		internal static ConfigEntry<float> MaxHearingDistance;

		internal static ConfigEntry<float> FalloffNearDistance;

		internal static ConfigEntry<float> DopplerLevel;

		internal static ConfigEntry<int> NetworkEventCode;

		internal static ConfigEntry<float> StopFadeSeconds;

		internal static ConfigEntry<float> StopGraceSeconds;

		internal static ConfigEntry<bool> StopWhenParachuteOpens;

		internal static ConfigEntry<bool> StopWhenClimbing;

		internal static ConfigEntry<bool> DebugLog;

		private void Awake()
		{
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_007c: Expected O, but got Unknown
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d9: Expected O, but got Unknown
			//IL_012c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0136: Expected O, but got Unknown
			//IL_0169: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Expected O, but got Unknown
			//IL_01e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f0: Expected O, but got Unknown
			//IL_0283: Unknown result type (might be due to invalid IL or missing references)
			//IL_028d: Expected O, but got Unknown
			//IL_02c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ca: Expected O, but got Unknown
			//IL_02fd: Unknown result type (might be due to invalid IL or missing references)
			//IL_0307: Expected O, but got Unknown
			//IL_033a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0344: Expected O, but got Unknown
			//IL_0373: Unknown result type (might be due to invalid IL or missing references)
			//IL_037d: Expected O, but got Unknown
			//IL_03b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_03ba: Expected O, but got Unknown
			//IL_03ed: Unknown result type (might be due to invalid IL or missing references)
			//IL_03f7: Expected O, but got Unknown
			//IL_0461: Unknown result type (might be due to invalid IL or missing references)
			//IL_04a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_04aa: Unknown result type (might be due to invalid IL or missing references)
			//IL_04b0: Expected O, but got Unknown
			//IL_04b0: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			PluginDir = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location);
			AudioFile = ((BaseUnityPlugin)this).Config.Bind<string>("Audio", "AudioFile", "", "File name of the scream (relative to the mod folder, or an absolute path). Leave empty to use the first .ogg / .wav / .mp3 found next to the plugin dll.");
			Volume = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "Volume", 0.6f, new ConfigDescription("Playback volume.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			Loop = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "Loop", true, "Keep looping the clip until you land or die. If false it plays once per fall.");
			ResumeWindowSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "ResumeWindowSeconds", 3f, new ConfigDescription("If you start falling again within this many seconds of the scream stopping (clipping a ledge, a short bounce), it carries on from where it left off instead of starting over. 0 = always restart.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>()));
			UseGameSfxMixer = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "UseGameSfxMixer", true, "Route through the game's SFX mixer so the in-game SFX volume slider applies.");
			MinFallTime = ((BaseUnityPlugin)this).Config.Bind<float>("Trigger", "MinFallTime", 0.3f, new ConfigDescription("Seconds of free fall (the game's own FallTime, which already discounts jumps and bounces) before screaming. The game uses 1.5 for fall damage; lower = earlier scream but may fire on big jumps.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>()));
			MinDownSpeed = ((BaseUnityPlugin)this).Config.Bind<float>("Trigger", "MinDownSpeed", 5f, new ConfigDescription("Minimum downward speed (m/s) required to scream. Max fall gravity is 20 m/s^2.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>()));
			TriggerOnRagdollFall = ((BaseUnityPlugin)this).Config.Bind<bool>("Trigger", "TriggerOnRagdollFall", true, "Also scream immediately when the game puts you into a ragdoll fall (tripped, thrown, knocked off), as long as you are moving downward.");
			Broadcast = ((BaseUnityPlugin)this).Config.Bind<BroadcastMode>("Multiplayer", "BroadcastMode", BroadcastMode.VoiceChat, "How other players hear your scream.\nVoiceChat: mixed into your microphone, so everyone hears it through normal voice chat, with or without the mod. Needs voice chat working. Falls back to ModNetwork if it is not.\nModNetwork: a network message; only players with this mod hear it, using their own sound file.\nOff: only you hear it.");
			VoiceChatVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Multiplayer", "VoiceChatVolume", 0.5f, new ConfigDescription("Loudness of the scream mixed into your voice (VoiceChat mode). 1 is full scale; lower it if friends say it clips.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			ForceTransmitWithPushToTalk = ((BaseUnityPlugin)this).Config.Bind<bool>("Multiplayer", "ForceTransmitWithPushToTalk", true, "VoiceChat mode, push-to-talk users: open your mic channel during a scream even if the key is not held. Your real microphone stays muted then; only the scream is sent.");
			PauseEchoCancellation = ((BaseUnityPlugin)this).Config.Bind<bool>("Multiplayer", "PauseEchoCancellation", true, "VoiceChat mode: switch echo cancellation off while screaming. Otherwise it can recognise the scream playing on your speakers and strip it back out of your voice.");
			HearOthers = ((BaseUnityPlugin)this).Config.Bind<bool>("Multiplayer", "HearOthers", true, "Play screams sent by players in ModNetwork mode, positioned on them like proximity voice chat. Screams sent through voice chat are unaffected by this.");
			RemoteVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Multiplayer", "RemoteVolume", 0.8f, new ConfigDescription("Volume of other players' screams before distance falloff.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			MaxHearingDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Multiplayer", "MaxHearingDistance", 1000f, new ConfigDescription("Far end of the falloff curve, matching the game's voice chat. Screams fade to silence around a fifth of this distance, so 1000 means audible out to roughly 200 m.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(50f, 2000f), Array.Empty<object>()));
			FalloffNearDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Multiplayer", "FalloffNearDistance", 10f, new ConfigDescription("Inside this distance a scream plays at full volume.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 100f), Array.Empty<object>()));
			DopplerLevel = ((BaseUnityPlugin)this).Config.Bind<float>("Multiplayer", "DopplerLevel", 0f, new ConfigDescription("Pitch shift from the falling player's speed. 0 = off, 1 = full Doppler.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			NetworkEventCode = ((BaseUnityPlugin)this).Config.Bind<int>("Multiplayer", "NetworkEventCode", 177, new ConfigDescription("Photon event code used to sync screams. Only change it if it clashes with another mod, and make sure everyone in the lobby uses the same number.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 199), Array.Empty<object>()));
			StopFadeSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Stop", "StopFadeSeconds", 0.05f, new ConfigDescription("Fade-out length when the scream stops. 0 = hard cut on impact.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>()));
			StopGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Stop", "StopGraceSeconds", 0.35f, new ConfigDescription("If you are still airborne but no longer falling fast (updraft, bounce pad, balloon...), wait this long before stopping so the scream does not stutter.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>()));
			StopWhenParachuteOpens = ((BaseUnityPlugin)this).Config.Bind<bool>("Stop", "StopWhenParachuteOpens", true, "Stop screaming once the parachute deploys.");
			StopWhenClimbing = ((BaseUnityPlugin)this).Config.Bind<bool>("Stop", "StopWhenClimbing", true, "Stop screaming if you grab a wall, rope or vine mid-fall.");
			DebugLog = ((BaseUnityPlugin)this).Config.Bind<bool>("Misc", "DebugLog", false, "Log every scream start/stop with the reason to the BepInEx console.");
			try
			{
				new Harmony("toiletking.peak.kirbyscream").PatchAll(typeof(Plugin).Assembly);
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Harmony patching failed; push-to-talk players will not transmit screams in VoiceChat mode. " + ex));
			}
			GameObject val = new GameObject("KirbyScreamController");
			Object.DontDestroyOnLoad((Object)val);
			((Object)val).hideFlags = (HideFlags)61;
			val.AddComponent<FallScreamController>();
			Log.LogInfo((object)"KirbyScream 1.3.0 loaded.");
		}
	}
	public class ScreamVoice : MonoBehaviour
	{
		private const float MaxScreamSeconds = 60f;

		private Character _character;

		private AudioSource _source;

		private bool _spatial;

		private float _playingFor;

		private Coroutine _fade;

		private bool _fading;

		private float _resumeTime = -1f;

		private float _stoppedAt;

		public bool IsPlaying
		{
			get
			{
				if ((Object)(object)_source != (Object)null)
				{
					return _source.isPlaying;
				}
				return false;
			}
		}

		public bool IsFor(Character character)
		{
			if ((Object)(object)_character != (Object)null)
			{
				return (Object)(object)_character == (Object)(object)character;
			}
			return false;
		}

		public static ScreamVoice Create(Character character, AudioClip clip, bool spatial)
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = new GameObject("KirbyScreamVoice")
			{
				hideFlags = (HideFlags)61
			};
			ScreamVoice screamVoice = val.AddComponent<ScreamVoice>();
			screamVoice._character = character;
			screamVoice._spatial = spatial;
			AudioSource val2 = val.AddComponent<AudioSource>();
			val2.clip = clip;
			val2.playOnAwake = false;
			val2.priority = 0;
			val2.dopplerLevel = Plugin.DopplerLevel.Value;
			val2.spatialBlend = (spatial ? 1f : 0f);
			if (Plugin.UseGameSfxMixer.Value && (Object)(object)SFX_Player.instance != (Object)null && (Object)(object)SFX_Player.instance.defaultMixerGroup != (Object)null)
			{
				val2.outputAudioMixerGroup = SFX_Player.instance.defaultMixerGroup;
			}
			if (spatial)
			{
				val2.rolloffMode = (AudioRolloffMode)2;
				val2.SetCustomCurve((AudioSourceCurveType)0, AnimationCurve.Constant(0f, 1f, 1f));
				val2.minDistance = 1f;
				val2.maxDistance = Plugin.MaxHearingDistance.Value;
			}
			screamVoice._source = val2;
			screamVoice.SyncPosition();
			return screamVoice;
		}

		public void Play()
		{
			if (!((Object)(object)_source == (Object)null) && !((Object)(object)_source.clip == (Object)null))
			{
				if (_fade != null)
				{
					((MonoBehaviour)this).StopCoroutine(_fade);
					_fade = null;
				}
				_fading = false;
				_playingFor = 0f;
				_source.loop = Plugin.Loop.Value;
				_source.volume = CurrentVolume();
				_source.time = ResumePoint();
				_source.Play();
			}
		}

		private float ResumePoint()
		{
			float resumeTime = _resumeTime;
			_resumeTime = -1f;
			float value = Plugin.ResumeWindowSeconds.Value;
			if (value <= 0f || resumeTime < 0f)
			{
				return 0f;
			}
			if (Time.unscaledTime - _stoppedAt > value)
			{
				return 0f;
			}
			float length = _source.clip.length;
			if (!(resumeTime < length - 0.05f))
			{
				return 0f;
			}
			return resumeTime;
		}

		public void Stop()
		{
			if ((Object)(object)_source == (Object)null || !_source.isPlaying || _fading)
			{
				return;
			}
			_resumeTime = _source.time;
			_stoppedAt = Time.unscaledTime;
			float value = Plugin.StopFadeSeconds.Value;
			if (value <= 0f)
			{
				_source.Stop();
				return;
			}
			if (_fade != null)
			{
				((MonoBehaviour)this).StopCoroutine(_fade);
			}
			_fade = ((MonoBehaviour)this).StartCoroutine(FadeOutAndStop(value));
		}

		private IEnumerator FadeOutAndStop(float seconds)
		{
			_fading = true;
			float start = _source.volume;
			float t = 0f;
			while (t < seconds && _source.isPlaying)
			{
				t += Time.unscaledDeltaTime;
				_source.volume = Mathf.Lerp(start, 0f, t / seconds);
				yield return null;
			}
			_source.Stop();
			_fading = false;
			_fade = null;
		}

		public void Dispose()
		{
			if ((Object)(object)this != (Object)null && (Object)(object)((Component)this).gameObject != (Object)null)
			{
				Object.Destroy((Object)(object)((Component)this).gameObject);
			}
		}

		private float BaseVolume()
		{
			if (!_spatial)
			{
				return Plugin.Volume.Value;
			}
			return Plugin.RemoteVolume.Value;
		}

		private float CurrentVolume()
		{
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			if (!_spatial)
			{
				return BaseVolume();
			}
			Transform val = ListenerTransform();
			if ((Object)(object)val == (Object)null)
			{
				return BaseVolume();
			}
			float num = Vector3.Distance(((Component)this).transform.position, val.position);
			float num2 = Mathf.InverseLerp(Plugin.FalloffNearDistance.Value, Plugin.MaxHearingDistance.Value, num);
			float num3 = Mathf.Clamp01(1f - Mathf.Log(Mathf.Lerp(1f, 10f, num2)));
			return BaseVolume() * num3;
		}

		private static Transform ListenerTransform()
		{
			if ((Object)(object)MainCamera.instance != (Object)null)
			{
				return ((Component)MainCamera.instance).transform;
			}
			AudioListener val = Object.FindAnyObjectByType<AudioListener>();
			if (!((Object)(object)val != (Object)null))
			{
				return null;
			}
			return ((Component)val).transform;
		}

		private void SyncPosition()
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)_character == (Object)null))
			{
				((Component)this).transform.position = _character.Head;
			}
		}

		private void LateUpdate()
		{
			if ((Object)(object)_character == (Object)null || _character.refs == null)
			{
				Stop();
				return;
			}
			SyncPosition();
			if (_source.isPlaying)
			{
				_playingFor += Time.deltaTime;
				if (_playingFor > 60f)
				{
					Plugin.Log.LogWarning((object)"Scream ran past its time limit, stopping (did the owner disconnect?).");
					Stop();
				}
				else if (_spatial && (_character.data.isGrounded || _character.data.dead))
				{
					Stop();
				}
				else if (!_fading)
				{
					_source.volume = CurrentVolume();
				}
			}
		}
	}
	public class VoiceInjector : MonoBehaviour
	{
		internal static volatile bool ForceTransmit;

		private static FieldRef<Recorder, LocalVoice> _recorderVoice;

		private static FieldRef<Recorder, IAudioDesc> _recorderInput;

		private static bool _reflectionReady;

		private Recorder _recorder;

		private CharacterVoiceHandler _handler;

		private WebRtcAudioDsp _dsp;

		private LocalVoice _installedOn;

		private bool _onAir;

		private bool _pausedAec;

		public static VoiceInjector Current { get; private set; }

		public int SamplingRate { get; private set; }

		public int Channels { get; private set; }

		public bool Ready
		{
			get
			{
				if (_installedOn != null && (Object)(object)_recorder != (Object)null)
				{
					return ((Behaviour)_recorder).isActiveAndEnabled;
				}
				return false;
			}
		}

		private static void InitReflection()
		{
			if (_reflectionReady)
			{
				return;
			}
			_reflectionReady = true;
			try
			{
				_recorderVoice = AccessTools.FieldRefAccess<Recorder, LocalVoice>("voice");
				_recorderInput = AccessTools.FieldRefAccess<Recorder, IAudioDesc>("inputSource");
			}
			catch (Exception ex)
			{
				_recorderVoice = null;
				_recorderInput = null;
				Plugin.Log.LogWarning((object)("Could not bind Recorder internals; will wait for PhotonVoiceCreated instead. " + ex.Message));
			}
		}

		private void Awake()
		{
			InitReflection();
			_recorder = ((Component)this).GetComponent<Recorder>();
			_handler = ((Component)this).GetComponent<CharacterVoiceHandler>();
			_dsp = ((Component)this).GetComponent<WebRtcAudioDsp>();
			Current = this;
			if (Plugin.DebugLog.Value)
			{
				string text = (((Object)(object)_dsp == (Object)null) ? "none" : $"bypass={_dsp.Bypass} aec={_dsp.AEC} ns={_dsp.NoiseSuppression} agc={_dsp.AGC} vad={_dsp.VAD}");
				Plugin.Log.LogInfo((object)("Voice injector attached. WebRTC DSP: " + text));
			}
		}

		private void Start()
		{
			if (_recorderVoice != null && (Object)(object)_recorder != (Object)null)
			{
				LocalVoice voice = _recorderVoice.Invoke(_recorder);
				FieldRef<Recorder, IAudioDesc> recorderInput = _recorderInput;
				Install(voice, (recorderInput != null) ? recorderInput.Invoke(_recorder) : null);
			}
		}

		private void OnDestroy()
		{
			EndScream(0f);
			if ((Object)(object)Current == (Object)(object)this)
			{
				Current = null;
			}
		}

		private void PhotonVoiceCreated(PhotonVoiceCreatedParams p)
		{
			Install((p != null) ? p.Voice : null, (p != null) ? p.AudioDesc : null);
		}

		private void PhotonVoiceRemoved()
		{
			_installedOn = null;
		}

		private void Install(LocalVoice voice, IAudioDesc input)
		{
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_0101: Unknown result type (might be due to invalid IL or missing references)
			//IL_0106: Unknown result type (might be due to invalid IL or missing references)
			//IL_0117: Unknown result type (might be due to invalid IL or missing references)
			//IL_011c: Unknown result type (might be due to invalid IL or missing references)
			if (voice == null || voice == _installedOn)
			{
				return;
			}
			LocalVoiceAudioFloat val = (LocalVoiceAudioFloat)(object)((voice is LocalVoiceAudioFloat) ? voice : null);
			if (val != null)
			{
				((LocalVoiceFramed<float>)(object)val).AddPreProcessor(new IProcessor<float>[1]
				{
					new FloatScreamProcessor()
				});
			}
			else
			{
				LocalVoiceAudioShort val2 = (LocalVoiceAudioShort)(object)((voice is LocalVoiceAudioShort) ? voice : null);
				if (val2 == null)
				{
					return;
				}
				((LocalVoiceFramed<short>)(object)val2).AddPreProcessor(new IProcessor<short>[1]
				{
					new ShortScreamProcessor()
				});
			}
			_installedOn = voice;
			bool flag = input != null && input.SamplingRate > 0 && input.Channels > 0;
			VoiceInfo info;
			int samplingRate;
			if (!flag)
			{
				info = voice.Info;
				samplingRate = ((VoiceInfo)(ref info)).SamplingRate;
			}
			else
			{
				samplingRate = input.SamplingRate;
			}
			SamplingRate = samplingRate;
			int channels;
			if (!flag)
			{
				info = voice.Info;
				channels = ((VoiceInfo)(ref info)).Channels;
			}
			else
			{
				channels = input.Channels;
			}
			Channels = channels;
			ManualLogSource log = Plugin.Log;
			string[] obj = new string[5]
			{
				"Scream mixer installed on voice stream (",
				((object)voice).GetType().Name,
				"). ",
				null,
				null
			};
			object[] obj2 = new object[4] { SamplingRate, Channels, null, null };
			info = voice.Info;
			obj2[2] = ((VoiceInfo)(ref info)).SamplingRate;
			info = voice.Info;
			obj2[3] = ((VoiceInfo)(ref info)).Channels;
			obj[3] = string.Format("Mic input {0} Hz {1} ch, encoder {2} Hz {3} ch", obj2);
			obj[4] = (flag ? "." : ". Mic format unknown, assuming encoder format; pitch may be off.");
			log.LogInfo((object)string.Concat(obj));
		}

		public bool BeginScream()
		{
			if (!Ready || !VoiceScreamMixer.HasSamplesFor(SamplingRate, Channels))
			{
				return false;
			}
			if (Plugin.PauseEchoCancellation.Value && (Object)(object)_dsp != (Object)null && !_dsp.Bypass && _dsp.AEC)
			{
				_dsp.AEC = false;
				_pausedAec = true;
			}
			VoiceScreamMixer.Start(Plugin.VoiceChatVolume.Value, Plugin.Loop.Value, Time.unscaledTime, Plugin.ResumeWindowSeconds.Value);
			ForceTransmit = Plugin.ForceTransmitWithPushToTalk.Value;
			_onAir = true;
			return true;
		}

		public void EndScream(float fadeSeconds)
		{
			if (_onAir)
			{
				_onAir = false;
				VoiceScreamMixer.Stop(fadeSeconds, Time.unscaledTime);
				ForceTransmit = false;
				if ((Object)(object)_recorder != (Object)null && (Object)(object)_handler != (Object)null)
				{
					_recorder.TransmitEnabled = _handler.transmitting && PushToTalkPatch.AllowedToCommunicate(_handler);
				}
				VoiceScreamMixer.MuteMic = false;
				if (_pausedAec && (Object)(object)_dsp != (Object)null)
				{
					_dsp.AEC = true;
				}
				_pausedAec = false;
			}
		}
	}
	[HarmonyPatch(typeof(CharacterVoiceHandler), "PushToTalk")]
	internal static class PushToTalkPatch
	{
		private static readonly FieldRef<CharacterVoiceHandler, Recorder> RecorderRef = Bind<Recorder>("m_Recorder");

		private static readonly FieldRef<CharacterVoiceHandler, Character> CharacterRef = Bind<Character>("m_character");

		private static readonly FieldRef<CharacterVoiceHandler, bool> AllowedRef = Bind<bool>("m_allowedToCommunicate");

		private static FieldRef<CharacterVoiceHandler, T> Bind<T>(string field)
		{
			try
			{
				return AccessTools.FieldRefAccess<CharacterVoiceHandler, T>(field);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Could not bind CharacterVoiceHandler." + field + "; push-to-talk players will not transmit screams. " + ex.Message));
				return null;
			}
		}

		internal static bool AllowedToCommunicate(CharacterVoiceHandler handler)
		{
			if (AllowedRef == null)
			{
				return false;
			}
			if (!AllowedRef.Invoke(handler))
			{
				return false;
			}
			FieldRef<CharacterVoiceHandler, Character> characterRef = CharacterRef;
			Character val = ((characterRef != null) ? characterRef.Invoke(handler) : null);
			if (!((Object)(object)val == (Object)null) && !((Object)(object)val.data == (Object)null))
			{
				return !val.data.isBlocked;
			}
			return true;
		}

		private static void Postfix(CharacterVoiceHandler __instance)
		{
			if (!VoiceInjector.ForceTransmit || RecorderRef == null || CharacterRef == null)
			{
				return;
			}
			Character val = CharacterRef.Invoke(__instance);
			if (!((Object)(object)val == (Object)null) && val.IsLocal && !val.isBot && AllowedToCommunicate(__instance))
			{
				Recorder val2 = RecorderRef.Invoke(__instance);
				if (!((Object)(object)val2 == (Object)null))
				{
					VoiceScreamMixer.MuteMic = !__instance.transmitting;
					val2.TransmitEnabled = true;
				}
			}
		}
	}
	internal static class VoiceScreamMixer
	{
		private static readonly object Gate = new object();

		private static float[] _samples;

		private static int _rate;

		private static int _channels;

		private static bool _playing;

		private static bool _loop;

		private static float _gain;

		private static int _pos;

		private static int _fadeLen;

		private static int _fadeLeft;

		private static int _resumePos = -1;

		private static float _stoppedAt;

		private static bool _muteMic;

		public static bool MuteMic
		{
			get
			{
				lock (Gate)
				{
					return _muteMic;
				}
			}
			set
			{
				lock (Gate)
				{
					_muteMic = value;
				}
			}
		}

		public static bool HasSamplesFor(int rate, int channels)
		{
			lock (Gate)
			{
				return _samples != null && _rate == rate && _channels == channels;
			}
		}

		public static void SetSamples(float[] samples, int rate, int channels)
		{
			lock (Gate)
			{
				_samples = samples;
				_rate = rate;
				_channels = channels;
				_pos = 0;
				_playing = false;
				_resumePos = -1;
			}
		}

		public static void Start(float gain, bool loop, float now, float resumeWindow)
		{
			lock (Gate)
			{
				if (_samples != null)
				{
					int resumePos = _resumePos;
					_resumePos = -1;
					bool num = resumeWindow > 0f && resumePos >= 0 && resumePos < _samples.Length && now - _stoppedAt <= resumeWindow;
					_gain = gain;
					_loop = loop;
					_pos = (num ? (resumePos - resumePos % Math.Max(1, _channels)) : 0);
					_fadeLen = 0;
					_fadeLeft = 0;
					_playing = true;
				}
			}
		}

		public static void Stop(float fadeSeconds, float now)
		{
			lock (Gate)
			{
				if (_playing)
				{
					if (_fadeLen == 0)
					{
						_resumePos = _pos;
						_stoppedAt = now;
					}
					int num = (int)(fadeSeconds * (float)_rate) * Math.Max(1, _channels);
					if (num <= 0)
					{
						_playing = false;
					}
					else if (_fadeLen == 0)
					{
						_fadeLen = num;
						_fadeLeft = num;
					}
				}
			}
		}

		private static float NextSample()
		{
			if (!_playing)
			{
				return 0f;
			}
			float num = _gain;
			if (_fadeLen > 0)
			{
				num *= (float)_fadeLeft / (float)_fadeLen;
				if (--_fadeLeft <= 0)
				{
					_playing = false;
				}
			}
			float result = _samples[_pos] * num;
			if (++_pos >= _samples.Length)
			{
				if (_loop)
				{
					_pos = 0;
					return result;
				}
				_playing = false;
			}
			return result;
		}

		public static void Mix(float[] buf)
		{
			if (buf == null)
			{
				return;
			}
			lock (Gate)
			{
				if (_muteMic)
				{
					Array.Clear(buf, 0, buf.Length);
				}
				if (!_playing || _samples == null)
				{
					return;
				}
				for (int i = 0; i < buf.Length; i++)
				{
					if (!_playing)
					{
						break;
					}
					float num = buf[i] + NextSample();
					buf[i] = ((num > 1f) ? 1f : ((num < -1f) ? (-1f) : num));
				}
			}
		}

		public static void Mix(short[] buf)
		{
			if (buf == null)
			{
				return;
			}
			lock (Gate)
			{
				if (_muteMic)
				{
					Array.Clear(buf, 0, buf.Length);
				}
				if (!_playing || _samples == null)
				{
					return;
				}
				for (int i = 0; i < buf.Length; i++)
				{
					if (!_playing)
					{
						break;
					}
					int num = buf[i] + (int)(NextSample() * 32767f);
					buf[i] = (short)((num > 32767) ? 32767 : ((num < -32768) ? (-32768) : num));
				}
			}
		}

		public static float[] ConvertClip(AudioClip clip, int dstRate, int dstChannels)
		{
			int num = Math.Max(1, clip.channels);
			int samples = clip.samples;
			int frequency = clip.frequency;
			if (samples <= 0 || frequency <= 0 || dstRate <= 0)
			{
				return null;
			}
			float[] array = new float[samples * num];
			if (!clip.GetData(array, 0))
			{
				return null;
			}
			float[] array2 = new float[samples];
			for (int i = 0; i < samples; i++)
			{
				float num2 = 0f;
				for (int j = 0; j < num; j++)
				{
					num2 += array[i * num + j];
				}
				array2[i] = num2 / (float)num;
			}
			int num3 = (int)((long)samples * (long)dstRate / frequency);
			dstChannels = Math.Max(1, dstChannels);
			float[] array3 = new float[num3 * dstChannels];
			double num4 = (double)frequency / (double)dstRate;
			for (int k = 0; k < num3; k++)
			{
				double num5 = (double)k * num4;
				double a = num5 + num4;
				int num6 = (int)num5;
				int num7 = Math.Min(samples - 1, (int)Math.Ceiling(a) - 1);
				float num10;
				if (num7 <= num6)
				{
					int num8 = Math.Min(num6 + 1, samples - 1);
					double num9 = num5 - (double)num6;
					num10 = (float)((double)array2[num6] * (1.0 - num9) + (double)array2[num8] * num9);
				}
				else
				{
					float num11 = 0f;
					for (int l = num6; l <= num7; l++)
					{
						num11 += array2[l];
					}
					num10 = num11 / (float)(num7 - num6 + 1);
				}
				for (int m = 0; m < dstChannels; m++)
				{
					array3[k * dstChannels + m] = num10;
				}
			}
			return array3;
		}
	}
	internal sealed class FloatScreamProcessor : IProcessor<float>, IDisposable
	{
		public float[] Process(float[] buf)
		{
			VoiceScreamMixer.Mix(buf);
			return buf;
		}

		public void Dispose()
		{
		}
	}
	internal sealed class ShortScreamProcessor : IProcessor<short>, IDisposable
	{
		public short[] Process(short[] buf)
		{
			VoiceScreamMixer.Mix(buf);
			return buf;
		}

		public void Dispose()
		{
		}
	}
}