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;
}
}
}