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()
{
}
}
}