Decompiled source of LethalMimics v1.0.1

BepInEx/plugins/LethalMimics/LethalMimics.dll

Decompiled 3 days ago
using 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));
			}
		}
	}
}