Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LethalMimics v1.0.1
BepInEx/plugins/LethalMimics/LethalMimics.dll
Decompiled 3 days agousing System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Dissonance; using Dissonance.Audio.Capture; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using NAudio.Wave; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; [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("LethalMimics")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.1.0")] [assembly: AssemblyInformationalVersion("1.0.1")] [assembly: AssemblyProduct("LethalMimics")] [assembly: AssemblyTitle("LethalMimics")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.1.0")] [module: UnverifiableCode] [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 LethalMimics { internal static class AudioCodec { private const int MuLawBias = 132; private const int MuLawClip = 32635; private static readonly float[] MuLawDecodeTable = BuildDecodeTable(); public static float[] Downmix(float[] interleaved, int channels) { if (channels <= 1) { return interleaved; } int num = interleaved.Length / channels; float[] array = new float[num]; for (int i = 0; i < num; i++) { float num2 = 0f; for (int j = 0; j < channels; j++) { num2 += interleaved[i * channels + j]; } array[i] = num2 / (float)channels; } return array; } public static float[] Resample(float[] src, int srcRate, int dstRate) { if (srcRate == dstRate || src.Length == 0) { return src; } double num = (double)srcRate / (double)dstRate; int num2 = (int)((double)src.Length / num); float[] array = new float[num2]; if (num > 1.0) { for (int i = 0; i < num2; i++) { int num3 = (int)((double)i * num); int num4 = Math.Min(src.Length, (int)((double)(i + 1) * num)); if (num4 <= num3) { num4 = num3 + 1; } float num5 = 0f; for (int j = num3; j < num4; j++) { num5 += src[j]; } array[i] = num5 / (float)(num4 - num3); } } else { for (int k = 0; k < num2; k++) { double num6 = (double)k * num; int num7 = (int)num6; int num8 = Math.Min(num7 + 1, src.Length - 1); float num9 = (float)(num6 - (double)num7); array[k] = src[num7] + (src[num8] - src[num7]) * num9; } } return array; } public static float[] TrimSilence(float[] s, int rate, float threshold = 0.012f, float padSeconds = 0.12f) { int num = Math.Max(1, rate / 100); int num2 = s.Length / num; if (num2 == 0) { return s; } int num3 = -1; int num4 = -1; for (int i = 0; i < num2; i++) { float num5 = 0f; for (int j = i * num; j < (i + 1) * num; j++) { num5 += Math.Abs(s[j]); } if (num5 / (float)num > threshold) { if (num3 < 0) { num3 = i; } num4 = i; } } if (num3 < 0) { return Array.Empty<float>(); } int num6 = (int)(padSeconds * (float)rate); int num7 = Math.Max(0, num3 * num - num6); float[] array = new float[Math.Min(s.Length, (num4 + 1) * num + num6) - num7]; Array.Copy(s, num7, array, 0, array.Length); return array; } public static void Normalize(float[] s, float targetPeak = 0.9f, float maxGain = 4f) { float num = 0f; foreach (float value in s) { num = Math.Max(num, Math.Abs(value)); } if (!(num < 0.0001f)) { float num2 = Math.Min(maxGain, targetPeak / num); for (int j = 0; j < s.Length; j++) { s[j] *= num2; } } } public static void ApplyFades(float[] s, int fadeSamples) { int num = Math.Min(fadeSamples, s.Length / 2); for (int i = 0; i < num; i++) { float num2 = (float)i / (float)num; s[i] *= num2; s[s.Length - 1 - i] *= num2; } } public static byte[] EncodeMuLaw(float[] samples) { byte[] array = new byte[samples.Length]; for (int i = 0; i < samples.Length; i++) { float num = samples[i]; if (num > 1f) { num = 1f; } else if (num < -1f) { num = -1f; } array[i] = LinearToMuLaw((int)(num * 32767f)); } return array; } public static float[] DecodeMuLaw(byte[] bytes, int offset, int count) { float[] array = new float[count]; for (int i = 0; i < count; i++) { array[i] = MuLawDecodeTable[bytes[offset + i]]; } return array; } private static byte LinearToMuLaw(int sample) { int num = ((sample < 0) ? 128 : 0); if (num != 0) { sample = -sample; } if (sample > 32635) { sample = 32635; } sample += 132; int num2 = 7; int num3 = 16384; while ((sample & num3) == 0 && num2 > 0) { num2--; num3 >>= 1; } int num4 = (sample >> num2 + 3) & 0xF; return (byte)(~(num | (num2 << 4) | num4)); } private static float[] BuildDecodeTable() { float[] array = new float[256]; for (int i = 0; i < 256; i++) { int num = ~i & 0xFF; int num2 = num & 0x80; int num3 = (num >> 4) & 7; int num4 = (((num & 0xF) << 3) + 132 << num3) - 132; array[i] = (float)((num2 != 0) ? (-num4) : num4) / 32768f; } return array; } } internal static class ClipPersistence { public readonly struct SavedClip { public readonly string Path; public readonly int SampleRate; public readonly byte[] Encoded; public SavedClip(string path, int sampleRate, byte[] encoded) { Path = path; SampleRate = sampleRate; Encoded = encoded; } } private const uint Magic = 827146572u; private static string Folder => Path.Combine(MimicConfig.DataDir, "clips"); public static string Save(int sampleRate, byte[] encoded) { try { Directory.CreateDirectory(Folder); string text = Path.Combine(Folder, $"{DateTime.UtcNow:yyyyMMdd_HHmmss_fff}.lamc"); using BinaryWriter binaryWriter = new BinaryWriter(File.Create(text)); binaryWriter.Write(827146572u); binaryWriter.Write(sampleRate); binaryWriter.Write(encoded.Length); binaryWriter.Write(encoded); return text; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not save clip: " + ex.Message)); return null; } } public static List<SavedClip> LoadAll(int max) { List<SavedClip> list = new List<SavedClip>(); if (!Directory.Exists(Folder)) { return list; } List<string> list2 = Directory.GetFiles(Folder, "*.lamc").OrderBy<string, string>((string f) => f, StringComparer.Ordinal).ToList(); while (list2.Count > max) { Delete(list2[0]); list2.RemoveAt(0); } foreach (string item in list2) { try { using BinaryReader binaryReader = new BinaryReader(File.OpenRead(item)); if (binaryReader.ReadUInt32() == 827146572) { int num = binaryReader.ReadInt32(); int num2 = binaryReader.ReadInt32(); if (num >= 4000 && num <= 48000 && num2 > 0 && num2 <= 1440000) { list.Add(new SavedClip(item, num, binaryReader.ReadBytes(num2))); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Skipping unreadable clip " + Path.GetFileName(item) + ": " + ex.Message)); } } return list; } public static void Delete(string path) { if (string.IsNullOrEmpty(path)) { return; } try { File.Delete(path); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not delete clip " + path + ": " + ex.Message)); } } } internal sealed class MimicClip { public readonly ulong Owner; public readonly uint Id; public readonly int SampleRate; public readonly byte[] Encoded; public int SampleCount => Encoded.Length; public float Duration => (float)Encoded.Length / (float)SampleRate; public MimicClip(ulong owner, uint id, int sampleRate, byte[] encoded) { Owner = owner; Id = id; SampleRate = sampleRate; Encoded = encoded; } } internal sealed class ClipStore { private readonly Dictionary<ulong, List<MimicClip>> byOwner = new Dictionary<ulong, List<MimicClip>>(); public int Count => byOwner.Values.Sum((List<MimicClip> l) => l.Count); public IEnumerable<ulong> Owners => from kv in byOwner where kv.Value.Count > 0 select kv.Key; public IEnumerable<MimicClip> All => byOwner.Values.SelectMany((List<MimicClip> l) => l); public IReadOnlyList<MimicClip> ClipsOf(ulong owner) { if (!byOwner.TryGetValue(owner, out var value)) { return Array.Empty<MimicClip>(); } return value; } public MimicClip Get(ulong owner, uint id) { if (!byOwner.TryGetValue(owner, out var value)) { return null; } return value.FirstOrDefault((MimicClip c) => c.Id == id); } public List<MimicClip> Add(MimicClip clip, int maxPerOwner = int.MaxValue) { List<MimicClip> list = new List<MimicClip>(); if (!byOwner.TryGetValue(clip.Owner, out var value)) { value = new List<MimicClip>(); byOwner[clip.Owner] = value; } if (value.Any((MimicClip c) => c.Id == clip.Id)) { return list; } value.Add(clip); while (value.Count > maxPerOwner) { list.Add(value[0]); value.RemoveAt(0); } return list; } public void Remove(ulong owner, uint id) { if (byOwner.TryGetValue(owner, out var value)) { value.RemoveAll((MimicClip c) => c.Id == id); } } public void RemoveAll(ulong owner) { byOwner.Remove(owner); } } internal sealed class MimicConfig { public readonly ConfigEntry<string> RecordKey; public readonly ConfigEntry<string> ClearClipsKey; public readonly ConfigEntry<float> MaxClipSeconds; public readonly ConfigEntry<float> MinClipSeconds; public readonly ConfigEntry<int> TransmitSampleRate; public readonly ConfigEntry<bool> TrimSilence; public readonly ConfigEntry<bool> SaveClips; public readonly ConfigEntry<int> MaxSavedClips; public readonly ConfigEntry<bool> ShowHud; public readonly ConfigEntry<string> DismissNoticeKey; public readonly ConfigEntry<bool> MimicsEnabled; public readonly ConfigEntry<int> MaxClipsPerPlayer; public readonly ConfigEntry<float> MinInterval; public readonly ConfigEntry<float> MaxInterval; public readonly ConfigEntry<float> ListenerCooldown; public readonly ConfigEntry<float> MinMimicShare; public readonly ConfigEntry<float> MimicChance; public readonly ConfigEntry<float> HearingRange; public readonly ConfigEntry<float> MinOwnerDistance; public readonly ConfigEntry<bool> AllowNearbyOwnerFallback; public readonly ConfigEntry<float> DeadOwnerWeight; public readonly ConfigEntry<float> PitchVariation; public readonly ConfigEntry<float> MinVolume; public readonly ConfigEntry<float> FragmentChance; public readonly ConfigEntry<float> RepeatChance; public readonly ConfigEntry<string> ExcludedEnemies; public readonly ConfigEntry<bool> ShareVoiceWithHostAi; public readonly ConfigEntry<int> AiStreamSampleRate; private HashSet<string> excludedCache; private string excludedCacheSource; public static string DataDir => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LethalMimics"); public MimicConfig(ConfigFile cfg) { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected O, but got Unknown //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Expected O, but got Unknown //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Expected O, but got Unknown //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_015a: Expected O, but got Unknown //IL_01d7: Unknown result type (might be due to invalid IL or missing references) //IL_01e1: Expected O, but got Unknown //IL_0210: Unknown result type (might be due to invalid IL or missing references) //IL_021a: Expected O, but got Unknown //IL_0249: Unknown result type (might be due to invalid IL or missing references) //IL_0253: Expected O, but got Unknown //IL_0282: Unknown result type (might be due to invalid IL or missing references) //IL_028c: Expected O, but got Unknown //IL_02bb: Unknown result type (might be due to invalid IL or missing references) //IL_02c5: Expected O, but got Unknown //IL_02f4: Unknown result type (might be due to invalid IL or missing references) //IL_02fe: Expected O, but got Unknown //IL_032d: Unknown result type (might be due to invalid IL or missing references) //IL_0337: Expected O, but got Unknown //IL_0366: Unknown result type (might be due to invalid IL or missing references) //IL_0370: Expected O, but got Unknown //IL_03bb: Unknown result type (might be due to invalid IL or missing references) //IL_03c5: Expected O, but got Unknown //IL_03f4: Unknown result type (might be due to invalid IL or missing references) //IL_03fe: Expected O, but got Unknown //IL_042d: Unknown result type (might be due to invalid IL or missing references) //IL_0437: Expected O, but got Unknown //IL_0466: Unknown result type (might be due to invalid IL or missing references) //IL_0470: Expected O, but got Unknown //IL_049f: Unknown result type (might be due to invalid IL or missing references) //IL_04a9: Expected O, but got Unknown //IL_0520: Unknown result type (might be due to invalid IL or missing references) //IL_052a: Expected O, but got Unknown RecordKey = cfg.Bind<string>("Recording", "RecordKey", "<Keyboard>/j", "Hold to record a mimic clip, release to finish. Unity Input System path, e.g. <Keyboard>/j, <Keyboard>/h, <Mouse>/middleButton. Your normal voice chat is muted while held."); ClearClipsKey = cfg.Bind<string>("Recording", "ClearClipsKey", "", "Optional key that deletes all of your recorded clips (local saves and everyone's copies). Empty = disabled. Example: <Keyboard>/f8"); MaxClipSeconds = cfg.Bind<float>("Recording", "MaxClipSeconds", 8f, new ConfigDescription("Recording stops automatically after this many seconds.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 20f), Array.Empty<object>())); MinClipSeconds = cfg.Bind<float>("Recording", "MinClipSeconds", 0.5f, new ConfigDescription("Clips shorter than this (after trimming silence) are discarded.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 5f), Array.Empty<object>())); TransmitSampleRate = cfg.Bind<int>("Recording", "TransmitSampleRate", 16000, new ConfigDescription("Sample rate clips are sent at. Higher = better quality, more bandwidth.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[5] { 8000, 11025, 16000, 22050, 24000 }), Array.Empty<object>())); TrimSilence = cfg.Bind<bool>("Recording", "TrimSilence", true, "Trim silence from the start and end of recordings."); SaveClips = cfg.Bind<bool>("Recording", "SaveClips", true, "Save your clips to disk and re-upload them automatically when you join a lobby. They are kept in %LOCALAPPDATA%/LethalMimics/clips, outside the mod profile, so exporting or sharing a profile never includes your voice."); MaxSavedClips = cfg.Bind<int>("Recording", "MaxSavedClips", 15, new ConfigDescription("How many of your own clips to keep. Oldest are dropped first.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100), Array.Empty<object>())); ShowHud = cfg.Bind<bool>("Recording", "ShowHud", true, "Show a recording indicator and short notices on screen."); DismissNoticeKey = cfg.Bind<string>("Recording", "DismissNoticeKey", "<Keyboard>/delete", "Press to hide the notice currently on screen (for example the AI voice sharing notice). Empty = disabled."); MimicsEnabled = cfg.Bind<bool>("Mimics (host only)", "Enabled", true, "Allow enemies to play recorded voice clips."); MaxClipsPerPlayer = cfg.Bind<int>("Mimics (host only)", "MaxClipsPerPlayer", 15, new ConfigDescription("How many clips per player the lobby keeps. Oldest are dropped first.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100), Array.Empty<object>())); MinInterval = cfg.Bind<float>("Mimics (host only)", "MinInterval", 25f, new ConfigDescription("Minimum seconds between mimics from the same enemy.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 600f), Array.Empty<object>())); MaxInterval = cfg.Bind<float>("Mimics (host only)", "MaxInterval", 80f, new ConfigDescription("Maximum seconds between mimics from the same enemy.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 900f), Array.Empty<object>())); ListenerCooldown = cfg.Bind<float>("Mimics (host only)", "ListenerCooldown", 7f, new ConfigDescription("After a player hears a mimic, no other enemy near them mimics for this many seconds. Enemies in other parts of the level can still talk at the same time.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 120f), Array.Empty<object>())); MinMimicShare = cfg.Bind<float>("Mimics (host only)", "MinMimicShare", 0.5f, new ConfigDescription("Each level rolls what share of its enemies can mimic: somewhere between this and all of them. 1 = every enemy, every level.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); MimicChance = cfg.Bind<float>("Mimics (host only)", "MimicChance", 0.65f, new ConfigDescription("Chance an enemy actually speaks when its timer is up.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); HearingRange = cfg.Bind<float>("Mimics (host only)", "HearingRange", 35f, new ConfigDescription("An enemy only mimics when a living player (other than the voice's owner) is within this many meters.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 200f), Array.Empty<object>())); MinOwnerDistance = cfg.Bind<float>("Mimics (host only)", "MinOwnerDistance", 25f, new ConfigDescription("A living player's voice is not used by an enemy closer to them than this. Farther players are preferred.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 200f), Array.Empty<object>())); AllowNearbyOwnerFallback = cfg.Bind<bool>("Mimics (host only)", "AllowNearbyOwnerFallback", false, "If every voice owner is close to the enemy, use one anyway (with low weight) instead of staying quiet."); DeadOwnerWeight = cfg.Bind<float>("Mimics (host only)", "DeadOwnerWeight", 1.5f, new ConfigDescription("Selection weight for dead players' voices relative to a far-away living player (1.0).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), Array.Empty<object>())); PitchVariation = cfg.Bind<float>("Mimics (host only)", "PitchVariation", 0.05f, new ConfigDescription("Random pitch/speed variation (+/-). Small values keep it believable.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.5f), Array.Empty<object>())); MinVolume = cfg.Bind<float>("Mimics (host only)", "MinVolume", 0.8f, new ConfigDescription("Playback volume is randomized between this and 1.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); FragmentChance = cfg.Bind<float>("Mimics (host only)", "FragmentChance", 0.25f, new ConfigDescription("Chance to play only a random piece of a clip instead of the whole thing.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); RepeatChance = cfg.Bind<float>("Mimics (host only)", "RepeatChance", 0.15f, new ConfigDescription("Chance an enemy follows up with another clip from the same player a few seconds later.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); ExcludedEnemies = cfg.Bind<string>("Mimics (host only)", "ExcludedEnemies", "Docile Locust Bees,Manticoil,Red Locust Bees", "Comma-separated enemy names that never mimic."); ShareVoiceWithHostAi = cfg.Bind<bool>("AI Voice Sharing", "ShareVoiceWithHostAi", true, "Only matters if the host runs the separate LethalMimics AI add-on and has it switched on (you get an on-screen notice). Then your voice chat - only what other players could already hear - is sent to the host's computer so enemies can clone your voice and speak as you. Set false to opt out; the host then never receives or clones your voice."); AiStreamSampleRate = cfg.Bind<int>("AI Voice Sharing", "StreamSampleRate", 16000, new ConfigDescription("Sample rate of voice sent to the host's AI.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[2] { 16000, 24000 }), Array.Empty<object>())); } public bool IsExcluded(string enemyName) { if (string.IsNullOrEmpty(enemyName)) { return false; } string text = ExcludedEnemies.Value ?? ""; if (excludedCache == null || excludedCacheSource != text) { excludedCache = new HashSet<string>(from s in text.Split(',') select s.Trim() into s where s.Length > 0 select s, StringComparer.OrdinalIgnoreCase); excludedCacheSource = text; } return excludedCache.Contains(enemyName.Trim()); } } internal sealed class MimicEmitter : MonoBehaviour { private AudioSource source; private AudioClip current; public void Play(MimicClip clip, int start, int length, float pitch, float volume, PlayerControllerB owner) { EnsureSource(); StopCurrent(); float[] array = AudioCodec.DecodeMuLaw(clip.Encoded, start, length); AudioCodec.ApplyFades(array, clip.SampleRate / 100); current = AudioClip.Create("LethalMimicsClip", array.Length, 1, clip.SampleRate, false); current.SetData(array, 0); MatchOwnerVoice(owner); source.clip = current; source.pitch = pitch; source.volume = volume; source.Play(); } private void EnsureSource() { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown if (!((Object)(object)source != (Object)null)) { EnemyAI component = ((Component)this).GetComponent<EnemyAI>(); Transform val = (((Object)(object)component != (Object)null && (Object)(object)component.eye != (Object)null) ? component.eye : ((Component)this).transform); GameObject val2 = new GameObject("LethalMimicsVoice"); val2.transform.SetParent(val, false); source = val2.AddComponent<AudioSource>(); source.playOnAwake = false; source.loop = false; source.spatialBlend = 1f; source.dopplerLevel = 0f; source.rolloffMode = (AudioRolloffMode)1; source.minDistance = 2f; source.maxDistance = 45f; val2.AddComponent<AudioLowPassFilter>().cutoffFrequency = 20000f; val2.AddComponent<OccludeAudio>(); } } private void MatchOwnerVoice(PlayerControllerB owner) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Invalid comparison between Unknown and I4 if ((Object)(object)owner == (Object)null) { return; } SoundManager instance = SoundManager.Instance; if ((Object)(object)instance != (Object)null && instance.playerVoiceMixers != null && owner.playerClientId < (ulong)instance.playerVoiceMixers.Length) { source.outputAudioMixerGroup = instance.playerVoiceMixers[owner.playerClientId]; } AudioSource currentVoiceChatAudioSource = owner.currentVoiceChatAudioSource; if (!((Object)(object)currentVoiceChatAudioSource == (Object)null)) { source.rolloffMode = currentVoiceChatAudioSource.rolloffMode; source.minDistance = currentVoiceChatAudioSource.minDistance; source.maxDistance = currentVoiceChatAudioSource.maxDistance; if ((int)currentVoiceChatAudioSource.rolloffMode == 2) { source.SetCustomCurve((AudioSourceCurveType)0, currentVoiceChatAudioSource.GetCustomCurve((AudioSourceCurveType)0)); } } } private void Update() { if ((Object)(object)current != (Object)null && (Object)(object)source != (Object)null && !source.isPlaying) { StopCurrent(); } } private void StopCurrent() { if ((Object)(object)source != (Object)null) { source.Stop(); source.clip = null; } if ((Object)(object)current != (Object)null) { Object.Destroy((Object)(object)current); current = null; } } private void OnDestroy() { if ((Object)(object)current != (Object)null) { Object.Destroy((Object)(object)current); } } } internal sealed class MimicManager : MonoBehaviour { private enum Payload : byte { Clip, Voice } private sealed class PendingTransfer { public int Total; public int Count; public int SampleRate; public int Received; public byte[] Data; public bool[] Got; public float Started; } private readonly struct OwnClip { public readonly uint Id; public readonly string Path; public OwnClip(uint id, string path) { Id = id; Path = path; } } private sealed class PendingPlay { public ulong EnemyId; public ulong Owner; public uint ClipId; public float Pitch; public float Volume; public float Received; public int Start; public int Length; } private enum MimicResult { Played, ChanceFailed, NoOpportunity } private const string MsgClip = "LethalMimics.Clip"; private const string MsgVoice = "LethalMimics.Voice"; private const string MsgRemove = "LethalMimics.Remove"; private const string MsgSync = "LethalMimics.SyncRequest"; private const string MsgPlay = "LethalMimics.Play"; private const string MsgAiState = "LethalMimics.AiState"; private const int ChunkSize = 16384; private const int MaxClipBytes = 1440000; private const uint AllClips = uint.MaxValue; private const uint GeneratedIdBit = 2147483648u; private readonly ClipStore store = new ClipStore(); private readonly Dictionary<(Payload kind, ulong owner, uint id), PendingTransfer> pending = new Dictionary<(Payload, ulong, uint), PendingTransfer>(); private readonly List<OwnClip> ownClips = new List<OwnClip>(); private readonly List<PendingPlay> pendingPlays = new List<PendingPlay>(); private uint nextLocalId = 1u; private uint nextVoiceId = 1u; private uint nextGeneratedId = 2147483648u; private bool registered; private readonly HashSet<ulong> aiOptOut = new HashSet<ulong>(); private bool clientAiActive; private bool lastBroadcastAiState; private readonly Dictionary<EnemyAI, float> nextMimicTime = new Dictionary<EnemyAI, float>(); private readonly Dictionary<EnemyAI, float> busyUntil = new Dictionary<EnemyAI, float>(); private readonly List<(ulong owner, uint id)> recentlyPlayed = new List<(ulong, uint)>(); private readonly List<MimicEvent> mimicEvents = new List<MimicEvent>(); private readonly Dictionary<EnemyAI, bool> isMimic = new Dictionary<EnemyAI, bool>(); private readonly Dictionary<ulong, float> listenerReadyTime = new Dictionary<ulong, float>(); private float levelMimicShare = -1f; private float nextEvaluation; public static MimicManager Instance { get; private set; } private static NetworkManager Net => NetworkManager.Singleton; private static bool IsServer { get { if ((Object)(object)Net != (Object)null) { return Net.IsServer; } return false; } } private static ulong LocalId => Net.LocalClientId; private static MimicConfig Cfg => Plugin.Cfg; public int OwnClipCount => ownClips.Count; public bool AiActive { get { if (!IsServer) { return clientAiActive; } return AiRunning; } } private static bool AiRunning { get { if (Ai != null) { return Ai.Active; } return false; } } private static IMimicVoiceAi Ai => MimicsApi.VoiceAi; private void Awake() { Instance = this; } private void Start() { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Expected O, but got Unknown //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Expected O, but got Unknown CustomMessagingManager val = (((Object)(object)Net != (Object)null) ? Net.CustomMessagingManager : null); if (val == null) { Plugin.Log.LogError((object)"Netcode messaging unavailable; mimic voices disabled for this session."); return; } val.RegisterNamedMessageHandler("LethalMimics.Clip", (HandleNamedMessageDelegate)delegate(ulong s, FastBufferReader r) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) OnTransferMessage(Payload.Clip, s, r); }); val.RegisterNamedMessageHandler("LethalMimics.Voice", (HandleNamedMessageDelegate)delegate(ulong s, FastBufferReader r) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) OnTransferMessage(Payload.Voice, s, r); }); val.RegisterNamedMessageHandler("LethalMimics.Remove", new HandleNamedMessageDelegate(OnRemoveMessage)); val.RegisterNamedMessageHandler("LethalMimics.SyncRequest", new HandleNamedMessageDelegate(OnSyncRequest)); val.RegisterNamedMessageHandler("LethalMimics.Play", new HandleNamedMessageDelegate(OnPlayMessage)); val.RegisterNamedMessageHandler("LethalMimics.AiState", new HandleNamedMessageDelegate(OnAiStateMessage)); registered = true; if (IsServer) { Net.OnClientDisconnectCallback += OnClientDisconnected; if (!Cfg.ShareVoiceWithHostAi.Value) { aiOptOut.Add(LocalId); } MimicsApi.RaiseHostSessionStarted(((Component)this).gameObject); lastBroadcastAiState = AiRunning; } else { SendSyncRequest(); } if (!Cfg.SaveClips.Value) { return; } List<ClipPersistence.SavedClip> list = ClipPersistence.LoadAll(Cfg.MaxSavedClips.Value); foreach (ClipPersistence.SavedClip item in list) { SubmitLocalClip(item.SampleRate, item.Encoded, item.Path); } if (list.Count > 0) { Plugin.Log.LogInfo((object)$"Uploaded {list.Count} saved mimic clip(s)."); } } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } if ((Object)(object)Net == (Object)null) { return; } if (registered && Net.CustomMessagingManager != null) { string[] array = new string[6] { "LethalMimics.Clip", "LethalMimics.Voice", "LethalMimics.Remove", "LethalMimics.SyncRequest", "LethalMimics.Play", "LethalMimics.AiState" }; foreach (string text in array) { Net.CustomMessagingManager.UnregisterNamedMessageHandler(text); } } Net.OnClientDisconnectCallback -= OnClientDisconnected; } public void SubmitLocalClip(int sampleRate, byte[] encoded, string savedPath) { if (registered) { uint id = nextLocalId++; ownClips.Add(new OwnClip(id, savedPath)); while (ownClips.Count > Cfg.MaxSavedClips.Value) { OwnClip ownClip = ownClips[0]; ownClips.RemoveAt(0); ClipPersistence.Delete(ownClip.Path); RemoveOwnClip(ownClip.Id); } MimicClip mimicClip = new MimicClip(LocalId, id, sampleRate, encoded); if (IsServer) { HostAddClip(mimicClip); } else { SendTransfer("LethalMimics.Clip", mimicClip.Owner, mimicClip.Id, mimicClip.SampleRate, mimicClip.Encoded, new ulong[1]); } } } public void ClearLocalClips() { foreach (OwnClip ownClip in ownClips) { ClipPersistence.Delete(ownClip.Path); } ownClips.Clear(); RemoveOwnClip(uint.MaxValue); } private unsafe void RemoveOwnClip(uint id) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) if (IsServer) { HostRemove(LocalId, id, LocalId); return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(16, (Allocator)2, -1); try { ulong localId = LocalId; ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref localId, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<uint>(ref id, default(ForPrimitives)); Net.CustomMessagingManager.SendNamedMessage("LethalMimics.Remove", 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } public void SubmitUtterance(float[] samples, int sampleRate) { if (!registered || !AiActive || !Cfg.ShareVoiceWithHostAi.Value || samples.Length == 0) { return; } if (IsServer) { Ai.OnSpeech(LocalId, samples, sampleRate); return; } byte[] array = new byte[samples.Length * 2]; for (int i = 0; i < samples.Length; i++) { short num = (short)Mathf.Clamp(samples[i] * 32767f, -32768f, 32767f); array[i * 2] = (byte)num; array[i * 2 + 1] = (byte)(num >> 8); } SendTransfer("LethalMimics.Voice", LocalId, nextVoiceId++, sampleRate, array, new ulong[1]); } private unsafe void SendTransfer(string message, ulong owner, uint id, int sampleRate, byte[] data, IReadOnlyList<ulong> targets) { //IL_0057: 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_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) if (targets.Count == 0) { return; } int num = data.Length; int num2 = Mathf.Max(1, (num + 16384 - 1) / 16384); FastBufferWriter val = default(FastBufferWriter); for (int i = 0; i < num2; i++) { int num3 = i * 16384; int num4 = Mathf.Min(16384, num - num3); ((FastBufferWriter)(ref val))..ctor(16448, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref owner, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<uint>(ref id, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref sampleRate, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref i, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num2, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num4, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteBytesSafe(data, num4, num3); if (IsServer) { Net.CustomMessagingManager.SendNamedMessage(message, targets, val, (NetworkDelivery)4); } else { Net.CustomMessagingManager.SendNamedMessage(message, targets[0], val, (NetworkDelivery)4); } } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } } private void OnTransferMessage(Payload kind, ulong sender, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006b: 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_007e: Unknown result type (might be due to invalid IL or missing references) ulong num = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref num, default(ForPrimitives)); uint num2 = default(uint); ((FastBufferReader)(ref reader)).ReadValueSafe<uint>(ref num2, default(ForPrimitives)); int num3 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num3, default(ForPrimitives)); int num4 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num4, default(ForPrimitives)); int num5 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num5, default(ForPrimitives)); int num6 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num6, default(ForPrimitives)); int num7 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num7, default(ForPrimitives)); if (IsServer) { if (sender == 0L) { return; } num = sender; if ((kind == Payload.Clip && (num2 & 0x80000000u) != 0) || (kind == Payload.Voice && !AiAllowedFor(sender))) { return; } } else if (kind == Payload.Voice || sender != 0L || num == LocalId) { return; } int num8 = ((kind == Payload.Voice) ? 1536000 : 1440000); int num9 = (num4 + 16384 - 1) / 16384; if (num3 < 4000 || num3 > 48000 || num4 <= 0 || num4 > num8 || num6 != num9 || num5 < 0 || num5 >= num6 || num7 <= 0 || num7 > 16384 || num5 * 16384 + num7 > num4) { Plugin.Log.LogWarning((object)$"Dropping malformed {kind} chunk from client {sender}."); } else { if (kind == Payload.Clip && store.Get(num, num2) != null) { return; } (Payload, ulong, uint) key = (kind, num, num2); if (!pending.TryGetValue(key, out var value) || value.Total != num4 || value.SampleRate != num3) { value = new PendingTransfer { Total = num4, Count = num6, SampleRate = num3, Data = new byte[num4], Got = new bool[num6], Started = Time.time }; pending[key] = value; } ((FastBufferReader)(ref reader)).ReadBytesSafe(ref value.Data, num7, num5 * 16384); if (!value.Got[num5]) { value.Got[num5] = true; value.Received++; } if (value.Received < value.Count) { return; } pending.Remove(key); if (kind == Payload.Voice) { float[] array = new float[value.Data.Length / 2]; for (int i = 0; i < array.Length; i++) { array[i] = (float)(short)(value.Data[i * 2] | (value.Data[i * 2 + 1] << 8)) / 32768f; } if (AiAllowedFor(num)) { Ai.OnSpeech(num, array, num3); } } else { MimicClip clip = new MimicClip(num, num2, num3, value.Data); if (IsServer) { HostAddClip(clip); return; } store.Add(clip); FlushPendingPlays(num, num2); } } } private void HostAddClip(MimicClip clip) { List<MimicClip> list = store.Add(clip, Cfg.MaxClipsPerPlayer.Value); SendTransfer("LethalMimics.Clip", clip.Owner, clip.Id, clip.SampleRate, clip.Encoded, ClientsExcept(clip.Owner)); foreach (MimicClip item in list) { HostRemove(item.Owner, item.Id, null); } if (AiAllowedFor(clip.Owner)) { Ai.OnRecordedClip(clip.Owner, AudioCodec.DecodeMuLaw(clip.Encoded, 0, clip.SampleCount), clip.SampleRate); } Plugin.Log.LogDebug((object)$"Stored clip {clip.Id} from client {clip.Owner} ({clip.Duration:0.0}s). Total clips: {store.Count}"); } private void OnRemoveMessage(ulong sender, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) ulong owner = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref owner, default(ForPrimitives)); uint id = default(uint); ((FastBufferReader)(ref reader)).ReadValueSafe<uint>(ref id, default(ForPrimitives)); if (IsServer) { if (sender != 0L) { HostRemove(sender, id, sender); } } else if (sender == 0L) { RemoveLocal(owner, id); } } private unsafe void HostRemove(ulong owner, uint id, ulong? except) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) RemoveLocal(owner, id); List<ulong> list = (except.HasValue ? ClientsExcept(except.Value) : ClientsExcept()); if (list.Count == 0) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(16, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref owner, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<uint>(ref id, default(ForPrimitives)); Net.CustomMessagingManager.SendNamedMessage("LethalMimics.Remove", (IReadOnlyList<ulong>)list, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private void RemoveLocal(ulong owner, uint id) { if (id == uint.MaxValue) { store.RemoveAll(owner); { foreach (var item in pending.Keys.Where(((Payload kind, ulong owner, uint id) k) => k.owner == owner).ToList()) { pending.Remove(item); } return; } } store.Remove(owner, id); } private unsafe void SendSyncRequest() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(4, (Allocator)2, -1); try { byte b = 2; ((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); bool value = Cfg.ShareVoiceWithHostAi.Value; ((FastBufferWriter)(ref val)).WriteValueSafe<bool>(ref value, default(ForPrimitives)); Net.CustomMessagingManager.SendNamedMessage("LethalMimics.SyncRequest", 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private unsafe void OnSyncRequest(ulong sender, FastBufferReader reader) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) if (!IsServer || sender == 0L) { return; } bool flag = false; if (((FastBufferReader)(ref reader)).TryBeginRead(2)) { byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValue<byte>(ref b, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValue<bool>(ref flag, default(ForPrimitives)); } if (flag) { aiOptOut.Remove(sender); } else { aiOptOut.Add(sender); } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(4, (Allocator)2, -1); try { bool aiRunning = AiRunning; ((FastBufferWriter)(ref val)).WriteValueSafe<bool>(ref aiRunning, default(ForPrimitives)); Net.CustomMessagingManager.SendNamedMessage("LethalMimics.AiState", sender, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } List<MimicClip> list = store.All.Where((MimicClip c) => c.Owner != sender).ToList(); foreach (MimicClip item in list) { SendTransfer("LethalMimics.Clip", item.Owner, item.Id, item.SampleRate, item.Encoded, new ulong[1] { sender }); } Plugin.Log.LogDebug((object)$"Sent {list.Count} clip(s) to joining client {sender} (AI sharing: {flag})."); } private void OnAiStateMessage(ulong sender, FastBufferReader reader) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) if (IsServer || sender != 0L) { return; } bool flag = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe<bool>(ref flag, default(ForPrimitives)); bool flag2 = clientAiActive; clientAiActive = flag; if (!flag) { if (flag2) { VoiceRecorder.Notify("Host turned AI voice cloning OFF - your voice is no longer being sent.", 6f); } } else if (!flag2) { VoiceRecorder.Notify(Cfg.ShareVoiceWithHostAi.Value ? "Host has AI voice cloning ON: your voice chat is sent to the host so enemies can speak as you. Set ShareVoiceWithHostAi=false to opt out." : "Host has AI voice cloning ON. You opted out, so your voice is not shared or cloned.", 25f); } } internal unsafe void BroadcastAiState() { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) if (!IsServer || !registered) { return; } bool aiRunning = AiRunning; if (aiRunning == lastBroadcastAiState) { return; } lastBroadcastAiState = aiRunning; List<ulong> list = ClientsExcept(); if (list.Count == 0) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(4, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<bool>(ref aiRunning, default(ForPrimitives)); Net.CustomMessagingManager.SendNamedMessage("LethalMimics.AiState", (IReadOnlyList<ulong>)list, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } public bool AiAllowedFor(ulong clientId) { if (AiRunning) { return !aiOptOut.Contains(clientId); } return false; } private void OnClientDisconnected(ulong clientId) { if (IsServer && clientId != 0L) { HostRemove(clientId, uint.MaxValue, clientId); aiOptOut.Remove(clientId); Ai?.OnPlayerLeft(clientId); } } private static List<ulong> ClientsExcept(params ulong[] excluded) { List<ulong> list = new List<ulong>(); foreach (ulong connectedClientsId in Net.ConnectedClientsIds) { if (connectedClientsId != 0L && !excluded.Contains(connectedClientsId)) { list.Add(connectedClientsId); } } return list; } private void OnPlayMessage(ulong sender, FastBufferReader reader) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) if (!IsServer && sender == 0L) { PendingPlay pendingPlay = new PendingPlay { Received = Time.time }; ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref pendingPlay.EnemyId, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref pendingPlay.Owner, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<uint>(ref pendingPlay.ClipId, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<float>(ref pendingPlay.Pitch, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<float>(ref pendingPlay.Volume, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref pendingPlay.Start, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref pendingPlay.Length, default(ForPrimitives)); if (pendingPlay.Owner != LocalId && !TryPlay(pendingPlay)) { pendingPlays.Add(pendingPlay); } } } private bool TryPlay(PendingPlay play) { MimicClip mimicClip = store.Get(play.Owner, play.ClipId); if (mimicClip == null) { return false; } if (Net.SpawnManager.SpawnedObjects.TryGetValue(play.EnemyId, out var value) && (Object)(object)value != (Object)null) { PlayLocal(((Component)value).GetComponentInChildren<EnemyAI>(), mimicClip, play.Pitch, play.Volume, play.Start, play.Length); } return true; } private void FlushPendingPlays(ulong owner, uint id) { for (int num = pendingPlays.Count - 1; num >= 0; num--) { PendingPlay pendingPlay = pendingPlays[num]; if (pendingPlay.Owner == owner && pendingPlay.ClipId == id) { pendingPlays.RemoveAt(num); TryPlay(pendingPlay); } } } private unsafe void HostBroadcastPlay(EnemyAI enemy, MimicClip clip, float pitch, float volume, int start, int length) { //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0123: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) float value = Time.time + (float)length / (float)clip.SampleRate / Mathf.Max(pitch, 0.1f) + Cfg.ListenerCooldown.Value; foreach (PlayerControllerB item in ListenersOf(enemy)) { listenerReadyTime[item.actualClientId] = value; } List<ulong> list = ClientsExcept(clip.Owner); if (list.Count > 0) { FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(64, (Allocator)2, -1); try { ulong networkObjectId = ((NetworkBehaviour)enemy).NetworkObjectId; ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref networkObjectId, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref clip.Owner, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<uint>(ref clip.Id, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<float>(ref pitch, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<float>(ref volume, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref start, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref length, default(ForPrimitives)); Net.CustomMessagingManager.SendNamedMessage("LethalMimics.Play", (IReadOnlyList<ulong>)list, val, (NetworkDelivery)4); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } if (clip.Owner != LocalId) { PlayLocal(enemy, clip, pitch, volume, start, length); } busyUntil[enemy] = Time.time + (float)length / (float)clip.SampleRate / Mathf.Max(0.1f, pitch) + 0.5f; mimicEvents.Add(new MimicEvent(enemy, clip.Owner, Time.time)); if ((clip.Id & 0x80000000u) == 0) { recentlyPlayed.Add((clip.Owner, clip.Id)); if (recentlyPlayed.Count > 6) { recentlyPlayed.RemoveAt(0); } } } public void HostPlayGenerated(EnemyAI enemy, ulong owner, float[] samples, int sampleRate) { if (IsServer && !((Object)(object)enemy == (Object)null) && !enemy.isEnemyDead && samples.Length != 0) { MimicClip mimicClip = new MimicClip(owner, nextGeneratedId++, sampleRate, AudioCodec.EncodeMuLaw(samples)); SendTransfer("LethalMimics.Clip", mimicClip.Owner, mimicClip.Id, mimicClip.SampleRate, mimicClip.Encoded, ClientsExcept(owner)); float num = Cfg.PitchVariation.Value * 0.5f; HostBroadcastPlay(enemy, mimicClip, 1f + Random.Range(0f - num, num), Random.Range(Mathf.Min(Cfg.MinVolume.Value, 1f), 1f), 0, mimicClip.SampleCount); ((MonoBehaviour)this).StartCoroutine(RemoveGeneratedLater(mimicClip, mimicClip.Duration + 20f)); } } private IEnumerator RemoveGeneratedLater(MimicClip clip, float delay) { yield return (object)new WaitForSeconds(delay); HostRemove(clip.Owner, clip.Id, clip.Owner); } private static void PlayLocal(EnemyAI enemy, MimicClip clip, float pitch, float volume, int start, int length) { if (!((Object)(object)enemy == (Object)null) && !enemy.isEnemyDead) { start = Mathf.Clamp(start, 0, clip.SampleCount - 1); length = Mathf.Clamp(length, 1, clip.SampleCount - start); MimicEmitter mimicEmitter = ((Component)enemy).GetComponent<MimicEmitter>(); if ((Object)(object)mimicEmitter == (Object)null) { mimicEmitter = ((Component)enemy).gameObject.AddComponent<MimicEmitter>(); } mimicEmitter.Play(clip, start, length, pitch, volume, FindPlayer(clip.Owner)); } } public static PlayerControllerB FindPlayer(ulong clientId) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return null; } PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if ((Object)(object)val != (Object)null && (val.isPlayerControlled || val.isPlayerDead) && val.actualClientId == clientId) { return val; } } return null; } public IEnumerable<MimicEvent> RecentMimics(float withinSeconds) { return from e in mimicEvents where Time.time - e.Time <= withinSeconds orderby e.Time descending select e; } public void MarkBusy(EnemyAI enemy, float seconds) { if ((Object)(object)enemy != (Object)null) { busyUntil[enemy] = Time.time + seconds; } } public EnemyAI EnemyThatCouldBe(ulong owner, PlayerControllerB listener) { RoundManager instance = RoundManager.Instance; if ((Object)(object)instance == (Object)null) { return null; } List<PlayerControllerB> alive = AlivePlayers(); return (from e in instance.SpawnedEnemies where (Object)(object)e != (Object)null && !e.isEnemyDead && ((NetworkBehaviour)e).IsSpawned && IsMimic(e) && !IsBusy(e) where Vector3.Distance(((Component)e).transform.position, ((Component)listener).transform.position) <= Cfg.HearingRange.Value where OwnerWeight(e, owner, alive) > 0f orderby Vector3.Distance(((Component)e).transform.position, ((Component)listener).transform.position) select e).FirstOrDefault(); } private bool IsBusy(EnemyAI enemy) { if (busyUntil.TryGetValue(enemy, out var value)) { return Time.time < value; } return false; } private static bool IsExcluded(EnemyAI enemy) { return Cfg.IsExcluded(((Object)(object)enemy.enemyType != (Object)null) ? enemy.enemyType.enemyName : null); } private bool IsMimic(EnemyAI enemy) { if (isMimic.TryGetValue(enemy, out var value)) { return value; } if (levelMimicShare < 0f) { levelMimicShare = Random.Range(Mathf.Clamp01(Cfg.MinMimicShare.Value), 1f); } value = !IsExcluded(enemy) && Random.value <= levelMimicShare; isMimic[enemy] = value; return value; } private static IEnumerable<PlayerControllerB> ListenersOf(EnemyAI enemy) { return from p in AlivePlayers() where Vector3.Distance(((Component)p).transform.position, ((Component)enemy).transform.position) <= Cfg.HearingRange.Value select p; } private bool ListenersOnCooldown(EnemyAI enemy, float now) { float value; return ListenersOf(enemy).Any((PlayerControllerB p) => listenerReadyTime.TryGetValue(p.actualClientId, out value) && now < value); } private static List<PlayerControllerB> AlivePlayers() { return StartOfRound.Instance.allPlayerScripts.Where((PlayerControllerB p) => (Object)(object)p != (Object)null && p.isPlayerControlled && !p.isPlayerDead).ToList(); } private void Update() { if (!registered) { return; } if (pending.Count > 0) { foreach (var item in (from kv in pending where Time.time - kv.Value.Started > 60f select kv.Key).ToList()) { pending.Remove(item); } } pendingPlays.RemoveAll((PendingPlay p) => Time.time - p.Received > 8f); mimicEvents.RemoveAll((MimicEvent e) => Time.time - e.Time > 60f); if (IsServer) { BroadcastAiState(); HostTick(); } } private void HostTick() { if (Time.time < nextEvaluation) { return; } nextEvaluation = Time.time + 0.5f; StartOfRound instance = StartOfRound.Instance; RoundManager instance2 = RoundManager.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance2 == (Object)null) { return; } if (instance.inShipPhase) { levelMimicShare = -1f; isMimic.Clear(); listenerReadyTime.Clear(); return; } bool flag = store.Count > 0 || (AiRunning && Ai.OwnersWithLines().Any()); if (!Cfg.MimicsEnabled.Value || !flag) { return; } foreach (EnemyAI item in nextMimicTime.Keys.Where((EnemyAI e) => (Object)(object)e == (Object)null || e.isEnemyDead).ToList()) { nextMimicTime.Remove(item); busyUntil.Remove(item); } foreach (EnemyAI item2 in isMimic.Keys.Where((EnemyAI e) => (Object)(object)e == (Object)null).ToList()) { isMimic.Remove(item2); } float time = Time.time; float num = Mathf.Min(Cfg.MinInterval.Value, Cfg.MaxInterval.Value); float num2 = Mathf.Max(Cfg.MinInterval.Value, Cfg.MaxInterval.Value); foreach (EnemyAI item3 in (from _ in instance2.SpawnedEnemies where (Object)(object)_ != (Object)null && !_.isEnemyDead && ((NetworkBehaviour)_).IsSpawned orderby Random.value select _).ToList()) { if (!IsMimic(item3)) { continue; } if (!nextMimicTime.TryGetValue(item3, out var value)) { nextMimicTime[item3] = time + Random.Range(num * 0.5f, num2); } else if (!(time < value) && !IsBusy(item3) && !ListenersOnCooldown(item3, time)) { switch (TryMimic(item3)) { case MimicResult.Played: nextMimicTime[item3] = time + Random.Range(num, num2); break; case MimicResult.ChanceFailed: nextMimicTime[item3] = time + Random.Range(num, num2) * 0.5f; break; case MimicResult.NoOpportunity: nextMimicTime[item3] = time + Random.Range(2f, 5f); break; } } } } private MimicResult TryMimic(EnemyAI enemy) { HashSet<ulong> hashSet = new HashSet<ulong>(store.Owners); if (AiRunning) { hashSet.UnionWith(Ai.OwnersWithLines().Where(AiAllowedFor)); } ulong? num = PickOwner(enemy, hashSet); if (!num.HasValue) { return MimicResult.NoOpportunity; } if (Random.value > Cfg.MimicChance.Value) { return MimicResult.ChanceFailed; } bool flag = store.ClipsOf(num.Value).Count > 0; if (AiRunning && AiAllowedFor(num.Value) && Ai.OwnersWithLines().Contains(num.Value) && (!flag || Random.value < Ai.LineChance) && Ai.TryTakeLine(num.Value, out var samples, out var sampleRate)) { HostPlayGenerated(enemy, num.Value, samples, sampleRate); return MimicResult.Played; } MimicClip mimicClip = PickClip(num.Value, null); if (mimicClip == null) { return MimicResult.NoOpportunity; } PlayWithVariation(enemy, mimicClip); if (Random.value < Cfg.RepeatChance.Value) { ((MonoBehaviour)this).StartCoroutine(FollowUp(enemy, mimicClip, Random.Range(1.5f, 4f))); } return MimicResult.Played; } private ulong? PickOwner(EnemyAI enemy, IEnumerable<ulong> owners) { List<PlayerControllerB> alive = AlivePlayers(); List<(ulong, float)> list = new List<(ulong, float)>(); foreach (ulong owner in owners) { float num = OwnerWeight(enemy, owner, alive); if (num > 0f) { list.Add((owner, num)); } } if (list.Count == 0) { return null; } float num2 = Random.value * list.Sum<(ulong, float)>(((ulong owner, float weight) c) => c.weight); foreach (var item in list) { num2 -= item.Item2; if (num2 <= 0f) { return item.Item1; } } return list[list.Count - 1].Item1; } private static float OwnerWeight(EnemyAI enemy, ulong owner, List<PlayerControllerB> alive) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) PlayerControllerB ownerPlayer = FindPlayer(owner); if ((Object)(object)ownerPlayer == (Object)null) { return 0f; } Vector3 enemyPos = ((Component)enemy).transform.position; List<PlayerControllerB> list = alive.Where((PlayerControllerB p) => p.actualClientId != owner && Vector3.Distance(((Component)p).transform.position, enemyPos) <= Cfg.HearingRange.Value).ToList(); if (list.Count == 0) { return 0f; } if (ownerPlayer.isPlayerDead) { return Cfg.DeadOwnerWeight.Value; } float num = ((ownerPlayer.isInsideFactory == !enemy.isOutside) ? Vector3.Distance(((Component)ownerPlayer).transform.position, enemyPos) : float.MaxValue); float num2; if (num < Cfg.MinOwnerDistance.Value) { if (!Cfg.AllowNearbyOwnerFallback.Value) { return 0f; } num2 = 0.05f; } else { num2 = 0.3f + 0.7f * Mathf.Clamp01((num - Cfg.MinOwnerDistance.Value) / 40f); } if (list.Min((PlayerControllerB p) => Vector3.Distance(((Component)p).transform.position, ((Component)ownerPlayer).transform.position)) < 8f) { num2 *= 0.35f; } return num2; } private MimicClip PickClip(ulong owner, MimicClip avoid) { IReadOnlyList<MimicClip> readOnlyList = store.ClipsOf(owner); if (readOnlyList.Count == 0) { return null; } List<MimicClip> list = readOnlyList.Where((MimicClip c) => c != avoid && !recentlyPlayed.Contains((c.Owner, c.Id))).ToList(); if (list.Count > 0) { return list[Random.Range(0, list.Count)]; } return readOnlyList.OrderBy((MimicClip c) => recentlyPlayed.IndexOf((c.Owner, c.Id))).First(); } private void PlayWithVariation(EnemyAI enemy, MimicClip clip) { float value = Cfg.PitchVariation.Value; float pitch = 1f + Random.Range(0f - value, value); float volume = Random.Range(Mathf.Min(Cfg.MinVolume.Value, 1f), 1f); int start = 0; int num = clip.SampleCount; int num2 = (int)(0.8f * (float)clip.SampleRate); if (clip.SampleCount > num2 * 2 && Random.value < Cfg.FragmentChance.Value) { num = Random.Range(Mathf.Max(num2, (int)((float)clip.SampleCount * 0.4f)), clip.SampleCount + 1); start = Random.Range(0, clip.SampleCount - num + 1); } HostBroadcastPlay(enemy, clip, pitch, volume, start, num); } private IEnumerator FollowUp(EnemyAI enemy, MimicClip previous, float gap) { float duration = previous.Duration; yield return (object)new WaitForSeconds(duration + gap); if (!((Object)(object)enemy == (Object)null) && !enemy.isEnemyDead && store.Get(previous.Owner, previous.Id) != null) { MimicClip clip = ((Random.value < 0.5f) ? previous : (PickClip(previous.Owner, previous) ?? previous)); PlayWithVariation(enemy, clip); } } } public interface IMimicVoiceAi { bool Active { get; } float LineChance { get; } void OnSpeech(ulong clientId, float[] samples, int sampleRate); void OnRecordedClip(ulong clientId, float[] samples, int sampleRate); void OnPlayerLeft(ulong clientId); IEnumerable<ulong> OwnersWithLines(); bool TryTakeLine(ulong owner, out float[] samples, out int sampleRate); } public readonly struct MimicEvent { public readonly EnemyAI Enemy; public readonly ulong Owner; public readonly float Time; public MimicEvent(EnemyAI enemy, ulong owner, float time) { Enemy = enemy; Owner = owner; Time = time; } } public static class MimicsApi { public static IMimicVoiceAi VoiceAi { get; private set; } public static float HearingRange => Plugin.Cfg.HearingRange.Value; public static event Action<GameObject> HostSessionStarted; public static void RegisterVoiceAi(IMimicVoiceAi ai) { VoiceAi = ai; } public static void UnregisterVoiceAi(IMimicVoiceAi ai) { if (VoiceAi == ai) { VoiceAi = null; } } public static void VoiceAiStateChanged() { MimicManager instance = MimicManager.Instance; if ((Object)(object)instance != (Object)null) { instance.BroadcastAiState(); } } public static bool VoiceAllowedFor(ulong clientId) { MimicManager instance = MimicManager.Instance; if ((Object)(object)instance != (Object)null) { return instance.AiAllowedFor(clientId); } return false; } public static void PlayGenerated(EnemyAI enemy, ulong owner, float[] samples, int sampleRate) { MimicManager instance = MimicManager.Instance; if ((Object)(object)instance != (Object)null) { instance.HostPlayGenerated(enemy, owner, samples, sampleRate); } } public static IEnumerable<MimicEvent> RecentMimics(float withinSeconds) { MimicManager instance = MimicManager.Instance; if (!((Object)(object)instance != (Object)null)) { return Array.Empty<MimicEvent>(); } return instance.RecentMimics(withinSeconds); } public static void MarkBusy(EnemyAI enemy, float seconds) { MimicManager instance = MimicManager.Instance; if ((Object)(object)instance != (Object)null) { instance.MarkBusy(enemy, seconds); } } public static EnemyAI EnemyThatCouldBe(ulong owner, PlayerControllerB listener) { MimicManager instance = MimicManager.Instance; if (!((Object)(object)instance != (Object)null)) { return null; } return instance.EnemyThatCouldBe(owner, listener); } public static PlayerControllerB FindPlayer(ulong clientId) { return MimicManager.FindPlayer(clientId); } public static void Notify(string text, float seconds = 4f) { VoiceRecorder.Notify(text, seconds); } internal static void RaiseHostSessionStarted(GameObject session) { try { MimicsApi.HostSessionStarted?.Invoke(session); } catch (Exception arg) { Plugin.Log.LogError((object)$"An add-on failed to start: {arg}"); } } } [HarmonyPatch] internal static class Patches { [HarmonyPatch(typeof(StartOfRound), "Start")] [HarmonyPostfix] private static void StartOfRoundStart(StartOfRound __instance) { if ((Object)(object)((Component)__instance).GetComponent<MimicManager>() == (Object)null) { ((Component)__instance).gameObject.AddComponent<MimicManager>(); } if ((Object)(object)((Component)__instance).GetComponent<VoiceRecorder>() == (Object)null) { ((Component)__instance).gameObject.AddComponent<VoiceRecorder>(); } if ((Object)(object)((Component)__instance).GetComponent<VoiceStreamer>() == (Object)null) { ((Component)__instance).gameObject.AddComponent<VoiceStreamer>(); } } [HarmonyPatch(typeof(StartOfRound), "Update")] [HarmonyPostfix] private static void StartOfRoundUpdate(StartOfRound __instance) { if (VoiceRecorder.IsRecording && !((Object)(object)__instance.voiceChatModule == (Object)null)) { __instance.voiceChatModule.IsMuted = true; if ((Object)(object)HUDManager.Instance != (Object)null && (Object)(object)HUDManager.Instance.PTTIcon != (Object)null) { ((Behaviour)HUDManager.Instance.PTTIcon).enabled = false; } } } } [BepInPlugin("brant.LethalMimics", "LethalMimics", "1.0.1")] public class Plugin : BaseUnityPlugin { public const string Guid = "brant.LethalMimics"; public const string Name = "LethalMimics"; public const string Version = "1.0.1"; internal static ManualLogSource Log; internal static MimicConfig Cfg; private void Awake() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) Log = ((BaseUnityPlugin)this).Logger; Cfg = new MimicConfig(((BaseUnityPlugin)this).Config); new Harmony("brant.LethalMimics").PatchAll(typeof(Patches)); Log.LogInfo((object)("LethalMimics 1.0.1 loaded. Hold " + Cfg.RecordKey.Value + " to record mimic voice clips.")); } } internal sealed class VoiceRecorder : MonoBehaviour, IMicrophoneSubscriber { private static VoiceRecorder instance; private InputAction recordAction; private InputAction clearAction; private InputAction dismissAction; private string dismissHint; private DissonanceComms comms; private readonly object gate = new object(); private readonly List<float> buffer = new List<float>(); private int micRate; private int micChannels; private volatile bool capturing; private float captureLimitSeconds; private float recordStart; private string hudText; private float hudUntil; private string noticeText; private float noticeUntil; private GUIStyle hudStyle; public static bool IsRecording { get; private set; } private void Awake() { instance = this; } public static void Notify(string text, float seconds = 2.5f) { Plugin.Log.LogInfo((object)text); if ((Object)(object)instance != (Object)null) { instance.ShowMessage(text, seconds); } } private void Start() { recordAction = CreateAction("LethalMimics.Record", Plugin.Cfg.RecordKey.Value); clearAction = CreateAction("LethalMimics.ClearClips", Plugin.Cfg.ClearClipsKey.Value); dismissAction = CreateAction("LethalMimics.DismissNotice", Plugin.Cfg.DismissNoticeKey.Value); if (dismissAction != null) { dismissHint = InputControlPath.ToHumanReadableString(Plugin.Cfg.DismissNoticeKey.Value.Trim(), (HumanReadableStringOptions)2, (InputControl)null); } } private static InputAction CreateAction(string name, string path) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown if (string.IsNullOrWhiteSpace(path)) { return null; } try { InputAction val = new InputAction(name, (InputActionType)1, path.Trim(), (string)null, (string)null, (string)null); val.Enable(); return val; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Invalid key binding '" + path + "' for " + name + ": " + ex.Message)); return null; } } private void Update() { if ((Object)(object)comms == (Object)null) { TrySubscribe(); } if (recordAction != null) { if (!IsRecording && recordAction.WasPressedThisFrame() && CanRecord()) { Begin(); } else if (IsRecording && (!recordAction.IsPressed() || Time.time - recordStart >= Plugin.Cfg.MaxClipSeconds.Value)) { End(); } } if (dismissAction != null && dismissAction.WasPressedThisFrame() && !IsTypingOrInMenu()) { hudUntil = 0f; noticeUntil = 0f; } if (clearAction != null && clearAction.WasPressedThisFrame() && !IsRecording && CanRecord()) { MimicManager.Instance.ClearLocalClips(); ShowMessage("Deleted all of your mimic clips"); } } private void TrySubscribe() { StartOfRound val = StartOfRound.Instance; if (!((Object)(object)val == (Object)null) && !((Object)(object)val.voiceChatModule == (Object)null)) { comms = val.voiceChatModule; comms.SubscribeToRecordedAudio((IMicrophoneSubscriber)(object)this); } } private bool CanRecord() { if ((Object)(object)comms != (Object)null && (Object)(object)MimicManager.Instance != (Object)null && (Object)(object)GameNetworkManager.Instance != (Object)null && (Object)(object)GameNetworkManager.Instance.localPlayerController != (Object)null) { return !IsTypingOrInMenu(); } return false; } private static bool IsTypingOrInMenu() { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if ((Object)(object)val == (Object)null) { return false; } if (!val.isTypingChat && !val.inTerminalMenu) { if ((Object)(object)val.quickMenuManager != (Object)null) { return val.quickMenuManager.isMenuOpen; } return false; } return true; } private void Begin() { lock (gate) { buffer.Clear(); } captureLimitSeconds = Plugin.Cfg.MaxClipSeconds.Value + 0.5f; capturing = true; IsRecording = true; recordStart = Time.time; comms.IsMuted = true; } private void End() { capturing = false; IsRecording = false; float[] raw; int rate; int channels; lock (gate) { raw = buffer.ToArray(); buffer.Clear(); rate = micRate; channels = micChannels; } try { ShowMessage(ProcessAndSubmit(raw, rate, channels)); } catch (Exception arg) { Plugin.Log.LogError((object)$"Failed to process recording: {arg}"); ShowMessage("Recording failed (see log)"); } } private static string ProcessAndSubmit(float[] raw, int rate, int channels) { MimicConfig cfg = Plugin.Cfg; if (raw.Length == 0 || rate <= 0) { return "No microphone audio captured - is your mic enabled?"; } float[] array = AudioCodec.Downmix(raw, Math.Max(1, channels)); if (cfg.TrimSilence.Value) { array = AudioCodec.TrimSilence(array, rate); } float num = (float)array.Length / (float)rate; if (num < cfg.MinClipSeconds.Value) { return "Clip too short or silent - not saved"; } int value = cfg.TransmitSampleRate.Value; float[] array2 = AudioCodec.Resample(array, rate, value); AudioCodec.Normalize(array2); AudioCodec.ApplyFades(array2, value / 200); byte[] encoded = AudioCodec.EncodeMuLaw(array2); string savedPath = (cfg.SaveClips.Value ? ClipPersistence.Save(value, encoded) : null); MimicManager.Instance.SubmitLocalClip(value, encoded, savedPath); return $"Mimic clip saved ({num:0.0}s) - {MimicManager.Instance.OwnClipCount} stored"; } void IMicrophoneSubscriber.ReceiveMicrophoneData(ArraySegment<float> data, WaveFormat format) { if (!capturing) { return; } lock (gate) { micRate = format.SampleRate; micChannels = format.Channels; int num = (int)(captureLimitSeconds * (float)micRate * (float)Math.Max(1, micChannels)); int num2 = Math.Min(data.Count, num - buffer.Count); for (int i = 0; i < num2; i++) { buffer.Add(data.Array[data.Offset + i]); } } } void IMicrophoneSubscriber.Reset() { } private void ShowMessage(string text, float seconds = 2.5f) { if (seconds > 4f) { noticeText = text; noticeUntil = Time.time + seconds; hudUntil = 0f; } else { hudText = text; hudUntil = Time.time + seconds; } } private void OnGUI() { //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: 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_0112: Expected O, but got Unknown if (!Plugin.Cfg.ShowHud.Value) { return; } string text; Color textColor = default(Color); if (IsRecording) { text = $"● REC mimic voice {Time.time - recordStart:0.0}s / {Plugin.Cfg.MaxClipSeconds.Value:0}s"; ((Color)(ref textColor))..ctor(1f, 0.25f, 0.2f); } else if (Time.time < hudUntil) { text = hudText; ((Color)(ref textColor))..ctor(1f, 0.85f, 0.4f); } else { if (!(Time.time < noticeUntil)) { return; } text = ((dismissHint != null) ? (noticeText + "\n<size=15>[" + dismissHint + "] dismiss</size>") : noticeText); ((Color)(ref textColor))..ctor(1f, 0.85f, 0.4f); } if (hudStyle == null) { hudStyle = new GUIStyle(GUI.skin.label) { fontSize = 20, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }; } hudStyle.wordWrap = true; hudStyle.richText = true; Rect val = default(Rect); ((Rect)(ref val))..ctor((float)Screen.width / 2f - 400f, (float)Screen.height * 0.76f, 800f, 90f); hudStyle.normal.textColor = new Color(0f, 0f, 0f, 0.8f); GUI.Label(new Rect(((Rect)(ref val)).x + 2f, ((Rect)(ref val)).y + 2f, ((Rect)(ref val)).width, ((Rect)(ref val)).height), text, hudStyle); hudStyle.normal.textColor = textColor; GUI.Label(val, text, hudStyle); } private void OnDestroy() { if ((Object)(object)instance == (Object)(object)this) { instance = null; } capturing = false; IsRecording = false; if ((Object)(object)comms != (Object)null) { try { comms.UnsubscribeFromRecordedAudio((IMicrophoneSubscriber)(object)this); } catch (Exception ex) { Plugin.Log.LogDebug((object)("Unsubscribe failed: " + ex.Message)); } } InputAction obj = recordAction; if (obj != null) { obj.Dispose(); } InputAction obj2 = clearAction; if (obj2 != null) { obj2.Dispose(); } InputAction obj3 = dismissAction; if (obj3 != null) { obj3.Dispose(); } } } internal sealed class VoiceStreamer : MonoBehaviour, IMicrophoneSubscriber { private const float FrameSeconds = 0.02f; private const int StartFrames = 3; private const int EndSilenceFrames = 35; private const int PrerollFrames = 10; private const float MaxUtteranceSeconds = 15f; private const float MinUtteranceSeconds = 0.8f; private DissonanceComms comms; private volatile bool open; private volatile int micRate; private volatile int micChannels; private readonly ConcurrentQueue<float[]> incoming = new ConcurrentQueue<float[]>(); private readonly List<float> pendingSamples = new List<float>(); private readonly Queue<float[]> preroll = new Queue<float[]>(); private readonly List<float> utterance = new List<float>(); private bool inSpeech; private int speechRun; private int silenceRun; private float noiseFloorDb = -60f; private void Update() { if ((Object)(object)comms == (Object)null) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.voiceChatModule == (Object)null) { return; } comms = instance.voiceChatModule; comms.SubscribeToRecordedAudio((IMicrophoneSubscriber)(object)this); } MimicManager instance2 = MimicManager.Instance; bool flag = (Object)(object)instance2 != (Object)null && instance2.AiActive && Plugin.Cfg.ShareVoiceWithHostAi.Value && !comms.IsMuted && !VoiceRecorder.IsRecording && (Object)(object)IngamePlayerSettings.Instance != (Object)null && IngamePlayerSettings.Instance.settings.micEnabled; if (!flag && open) { open = false; FinishUtterance(); ResetVad(); } open = flag; float[] result; while (incoming.TryDequeue(out result)) { if (open) { ProcessBlock(result); } } } void IMicrophoneSubscriber.ReceiveMicrophoneData(ArraySegment<float> data, WaveFormat format) { if (open) { micRate = format.SampleRate; micChannels = format.Channels; float[] array = new float[data.Count]; Array.Copy(data.Array, data.Offset, array, 0, data.Count); incoming.Enqueue(array); } } void IMicrophoneSubscriber.Reset() { } private void ProcessBlock(float[] block) { int num = micRate; if (num > 0) { float[] collection = AudioCodec.Downmix(block, Math.Max(1, micChannels)); pendingSamples.AddRange(collection); int num2 = Mathf.Max(1, (int)((float)num * 0.02f)); int num3 = 0; while (pendingSamples.Count - num3 >= num2) { float[] frame = pendingSamples.GetRange(num3, num2).ToArray(); num3 += num2; ProcessFrame(frame, num); } pendingSamples.RemoveRange(0, num3); } } private void ProcessFrame(float[] frame, int rate) { double num = 0.0; foreach (float num2 in frame) { num += (double)(num2 * num2); } float num3 = 10f * Mathf.Log10((float)(num / (double)frame.Length) + 1E-12f); noiseFloorDb = ((num3 < noiseFloorDb) ? Mathf.Lerp(noiseFloorDb, num3, 0.3f) : (noiseFloorDb + 0.01f)); bool flag = num3 > Mathf.Max(-52f, noiseFloorDb + 10f); if (!inSpeech) { preroll.Enqueue(frame); while (preroll.Count > 10) { preroll.Dequeue(); } speechRun = (flag ? (speechRun + 1) : 0); if (speechRun < 3) { return; } inSpeech = true; silenceRun = 0; utterance.Clear(); foreach (float[] item in preroll) { utterance.AddRange(item); } preroll.Clear(); } else { utterance.AddRange(frame); silenceRun = ((!flag) ? (silenceRun + 1) : 0); if (silenceRun >= 35 || (float)utterance.Count >= 15f * (float)rate) { FinishUtterance(); ResetVad(); } } } private void FinishUtterance() { int num = micRate; if (!inSpeech || num <= 0) { utterance.Clear(); return; } int num2 = Mathf.Min(utterance.Count, Mathf.Max(0, silenceRun - 10) * (int)((float)num * 0.02f)); int num3 = utterance.Count - num2; if ((float)num3 >= 0.8f * (float)num) { float[] src = utterance.GetRange(0, num3).ToArray(); int value = Plugin.Cfg.AiStreamSampleRate.Value; MimicManager instance = MimicManager.Instance; if ((Object)(object)instance != (Object)null) { instance.SubmitUtterance(AudioCodec.Resample(src, num, value), value); } } utterance.Clear(); } private void ResetVad() { inSpeech = false; speechRun = 0; silenceRun = 0; preroll.Clear(); } private void OnDestroy() { open = false; if ((Object)(object)comms == (Object)null) { return; } try { comms.UnsubscribeFromRecordedAudio((IMicrophoneSubscriber)(object)this); } catch (Exception ex) { Plugin.Log.LogDebug((object)("Unsubscribe failed: " + ex.Message)); } } } }