Decompiled source of EchoMimics v0.1.1

BepInEx/plugins/EchoMimics/EchoMimics.dll

Decompiled 2 months ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Dissonance;
using Dissonance.Audio.Capture;
using GameNetcodeStuff;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;

[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("EchoMimics")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Local context-aware voice mimics for Lethal Company")]
[assembly: AssemblyFileVersion("0.1.1.0")]
[assembly: AssemblyInformationalVersion("0.1.1+7f8ccba6c6b20c110fb4ce508ca0f4d2375192a0")]
[assembly: AssemblyProduct("EchoMimics")]
[assembly: AssemblyTitle("EchoMimics")]
[assembly: AssemblyVersion("0.1.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace EchoMimics
{
	internal static class AmbientCapture
	{
		private const int Rate = 16000;

		private const int LoopSeconds = 20;

		private const int MinSamples = 8000;

		private const int MaxSamples = 128000;

		private const int SilenceSamples = 11200;

		private static AudioClip _clip;

		private static string _device;

		private static int _readPosition;

		private static bool _speaking;

		private static int _silent;

		private static float _nextStart;

		private static readonly List<float> Utterance = new List<float>(64000);

		internal static void Tick()
		{
			if (!CanCapture())
			{
				Stop();
				return;
			}
			if ((Object)(object)_clip == (Object)null)
			{
				if (Time.unscaledTime >= _nextStart)
				{
					Start();
				}
				return;
			}
			int position = Microphone.GetPosition(_device);
			if (position < 0)
			{
				Stop();
				return;
			}
			int num = ((position >= _readPosition) ? (position - _readPosition) : (_clip.samples - _readPosition + position));
			if (num <= 0 || num > _clip.samples)
			{
				return;
			}
			float[] array = new float[num * _clip.channels];
			int num2 = Math.Min(num, _clip.samples - _readPosition);
			float[] array2 = new float[num2 * _clip.channels];
			if (!_clip.GetData(array2, _readPosition))
			{
				return;
			}
			Array.Copy(array2, 0, array, 0, array2.Length);
			if (num2 < num)
			{
				float[] array3 = new float[(num - num2) * _clip.channels];
				if (!_clip.GetData(array3, 0))
				{
					return;
				}
				Array.Copy(array3, 0, array, array2.Length, array3.Length);
			}
			_readPosition = position;
			Consume(array, _clip.channels);
		}

		private static bool CanCapture()
		{
			NetworkManager singleton = NetworkManager.Singleton;
			PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController;
			if (Plugin.Enabled.Value && Plugin.CaptureConsent.Value && (Object)(object)singleton != (Object)null && singleton.IsListening && (Object)(object)val != (Object)null && val.isPlayerControlled)
			{
				return !val.isPlayerDead;
			}
			return false;
		}

		private static void Start()
		{
			try
			{
				_device = ResolveDevice();
				_clip = Microphone.Start(_device, true, 20, 16000);
				_readPosition = 0;
				_speaking = false;
				_silent = 0;
				Utterance.Clear();
				Plugin.Log.LogInfo((object)("Local voice capture started on '" + (_device ?? "system default") + "'."));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Voice capture unavailable: " + ex.Message));
				_nextStart = Time.unscaledTime + 10f;
			}
		}

		internal static void Stop()
		{
			if ((Object)(object)_clip != (Object)null)
			{
				try
				{
					Microphone.End(_device);
				}
				catch
				{
				}
				Object.Destroy((Object)(object)_clip);
			}
			_clip = null;
			_speaking = false;
			_silent = 0;
			Utterance.Clear();
			_nextStart = Time.unscaledTime + 2f;
		}

		private static void Consume(float[] input, int channels)
		{
			int num = input.Length / Math.Max(1, channels);
			for (int i = 0; i < num; i++)
			{
				float num2 = 0f;
				for (int j = 0; j < channels; j++)
				{
					num2 += input[i * channels + j];
				}
				num2 /= (float)channels;
				if (Mathf.Abs(num2) >= Plugin.VadThreshold.Value)
				{
					_speaking = true;
					_silent = 0;
				}
				else if (_speaking)
				{
					_silent++;
				}
				if (_speaking)
				{
					Utterance.Add(num2);
				}
				if (_speaking && (_silent >= 11200 || Utterance.Count >= 128000))
				{
					Finish();
				}
			}
		}

		private static void Finish()
		{
			int num = Math.Max(0, Utterance.Count - _silent);
			if (num >= 8000)
			{
				MimicTransport.SendUtterance(AudioCodec.EncodeMonoWav(Utterance.GetRange(0, num).ToArray(), 16000));
			}
			Utterance.Clear();
			_speaking = false;
			_silent = 0;
		}

		private static string ResolveDevice()
		{
			string text = Plugin.InputDevice.Value?.Trim();
			if (!string.IsNullOrEmpty(text))
			{
				string[] devices = Microphone.devices;
				foreach (string text2 in devices)
				{
					if (string.Equals(text2, text, StringComparison.OrdinalIgnoreCase))
					{
						return text2;
					}
				}
			}
			try
			{
				DissonanceComms obj = Object.FindObjectOfType<DissonanceComms>();
				object obj2;
				if (obj == null)
				{
					obj2 = null;
				}
				else
				{
					IMicrophoneCapture microphoneCapture = obj.MicrophoneCapture;
					obj2 = ((microphoneCapture != null) ? microphoneCapture.Device : null);
				}
				string text3 = (string)obj2;
				if (!string.IsNullOrEmpty(text3))
				{
					string[] devices = Microphone.devices;
					foreach (string text4 in devices)
					{
						if (string.Equals(text4, text3, StringComparison.OrdinalIgnoreCase))
						{
							return text4;
						}
					}
				}
			}
			catch
			{
			}
			return null;
		}
	}
	internal static class AudioCodec
	{
		internal static byte[] EncodeMonoWav(float[] samples, int sampleRate)
		{
			if (samples == null || samples.Length == 0)
			{
				return null;
			}
			byte[] array = new byte[44 + samples.Length * 2];
			WriteAscii(array, 0, "RIFF");
			WriteInt(array, 4, array.Length - 8);
			WriteAscii(array, 8, "WAVE");
			WriteAscii(array, 12, "fmt ");
			WriteInt(array, 16, 16);
			WriteShort(array, 20, 1);
			WriteShort(array, 22, 1);
			WriteInt(array, 24, sampleRate);
			WriteInt(array, 28, sampleRate * 2);
			WriteShort(array, 32, 2);
			WriteShort(array, 34, 16);
			WriteAscii(array, 36, "data");
			WriteInt(array, 40, samples.Length * 2);
			for (int i = 0; i < samples.Length; i++)
			{
				WriteShort(array, 44 + i * 2, (short)Mathf.RoundToInt(Mathf.Clamp(samples[i], -0.98f, 0.98f) * 32767f));
			}
			return array;
		}

		internal static bool DecodeWav(byte[] wav, out float[] samples, out int sampleRate)
		{
			samples = null;
			sampleRate = 0;
			if (wav == null || wav.Length < 44 || Encoding.ASCII.GetString(wav, 0, 4) != "RIFF")
			{
				return false;
			}
			int num = BitConverter.ToInt16(wav, 22);
			sampleRate = BitConverter.ToInt32(wav, 24);
			int num2 = BitConverter.ToInt16(wav, 34);
			if (num < 1 || num > 2 || num2 != 16)
			{
				return false;
			}
			int num3;
			for (int i = 12; i + 8 <= wav.Length; i += 8 + num3 + (num3 & 1))
			{
				string text = Encoding.ASCII.GetString(wav, i, 4);
				num3 = BitConverter.ToInt32(wav, i + 4);
				if (!(text == "data") || num3 < 2 || i + 8 + num3 > wav.Length)
				{
					continue;
				}
				int num4 = num3 / (2 * num);
				samples = new float[num4];
				for (int j = 0; j < num4; j++)
				{
					int num5 = 0;
					for (int k = 0; k < num; k++)
					{
						num5 += BitConverter.ToInt16(wav, i + 8 + (j * num + k) * 2);
					}
					samples[j] = (float)num5 / (32768f * (float)num);
				}
				if (sampleRate >= 8000)
				{
					return sampleRate <= 48000;
				}
				return false;
			}
			return false;
		}

		private static void WriteAscii(byte[] target, int offset, string value)
		{
			Encoding.ASCII.GetBytes(value).CopyTo(target, offset);
		}

		private static void WriteInt(byte[] target, int offset, int value)
		{
			BitConverter.GetBytes(value).CopyTo(target, offset);
		}

		private static void WriteShort(byte[] target, int offset, short value)
		{
			BitConverter.GetBytes(value).CopyTo(target, offset);
		}
	}
	internal sealed class RuntimeRequest
	{
		public string session_id;

		public string speaker_id;

		public string speaker_name;

		public string mimic_id;

		public string copied_player_id;

		public string copied_player_name;

		public string scene;

		public string language;

		public string audio_wav;
	}
	internal sealed class RuntimeResponse
	{
		public string heard { get; set; } = "";

		public string reply { get; set; } = "";

		public string audio_wav { get; set; } = "";

		public string error { get; set; } = "";

		public int sample_rate { get; set; }
	}
	internal static class LocalRuntimeClient
	{
		private static readonly HttpClient Client = new HttpClient
		{
			Timeout = TimeSpan.FromSeconds(75.0)
		};

		internal static async Task<RuntimeResponse> TurnAsync(RuntimeRequest request)
		{
			return await PostAsync("/v1/turn", request).ConfigureAwait(continueOnCapturedContext: false);
		}

		internal static async Task<RuntimeResponse> ObserveAsync(RuntimeRequest request)
		{
			return await PostAsync("/v1/observe", request).ConfigureAwait(continueOnCapturedContext: false);
		}

		internal static async Task ClearSessionAsync(string sessionId)
		{
			string requestUri = Plugin.RuntimeUrl.Value.TrimEnd('/') + "/v1/session/clear";
			using HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, requestUri);
			string text = Plugin.RuntimeToken.Value?.Trim();
			if (!string.IsNullOrEmpty(text))
			{
				message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", text);
			}
			message.Content = new StringContent(JsonConvert.SerializeObject((object)new
			{
				session_id = sessionId
			}), Encoding.UTF8, "application/json");
			using HttpResponseMessage httpResponseMessage = await Client.SendAsync(message).ConfigureAwait(continueOnCapturedContext: false);
			httpResponseMessage.EnsureSuccessStatusCode();
		}

		private static async Task<RuntimeResponse> PostAsync(string path, RuntimeRequest request)
		{
			string requestUri = Plugin.RuntimeUrl.Value.TrimEnd('/') + path;
			using HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, requestUri);
			string text = Plugin.RuntimeToken.Value?.Trim();
			if (!string.IsNullOrEmpty(text))
			{
				message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", text);
			}
			message.Content = new StringContent(JsonConvert.SerializeObject((object)request), Encoding.UTF8, "application/json");
			using HttpResponseMessage response = await Client.SendAsync(message).ConfigureAwait(continueOnCapturedContext: false);
			RuntimeResponse runtimeResponse = JsonConvert.DeserializeObject<RuntimeResponse>(await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false)) ?? new RuntimeResponse();
			if (!response.IsSuccessStatusCode)
			{
				throw new InvalidOperationException(runtimeResponse.error ?? $"runtime HTTP {(int)response.StatusCode}");
			}
			return runtimeResponse;
		}
	}
	internal sealed class MimicIdentity
	{
		internal MaskedPlayerEnemy Enemy;

		internal ulong CopiedClientId;

		internal string CopiedName;

		internal float NextReplyAt;
	}
	internal static class MimicDirector
	{
		private static readonly Dictionary<ulong, MimicIdentity> Mimics = new Dictionary<ulong, MimicIdentity>();

		private static float _nextScan;

		internal static void Tick()
		{
			NetworkManager singleton = NetworkManager.Singleton;
			if (!((Object)(object)singleton == (Object)null) && singleton.IsServer && singleton.IsListening && !(Time.unscaledTime < _nextScan))
			{
				_nextScan = Time.unscaledTime + 2f;
				MaskedPlayerEnemy[] array = Object.FindObjectsOfType<MaskedPlayerEnemy>();
				for (int i = 0; i < array.Length; i++)
				{
					Register(array[i]);
				}
				ulong[] array2 = Mimics.Keys.Where((ulong id) => (Object)(object)Mimics[id].Enemy == (Object)null).ToArray();
				foreach (ulong key in array2)
				{
					Mimics.Remove(key);
				}
			}
		}

		private static void Register(MaskedPlayerEnemy enemy)
		{
			if ((Object)(object)enemy == (Object)null || (Object)(object)((NetworkBehaviour)enemy).NetworkObject == (Object)null)
			{
				return;
			}
			ulong networkObjectId = ((NetworkBehaviour)enemy).NetworkObjectId;
			if (Mimics.ContainsKey(networkObjectId))
			{
				return;
			}
			PlayerControllerB val = null;
			try
			{
				val = enemy.mimickingPlayer;
			}
			catch
			{
			}
			if ((Object)(object)val == (Object)null)
			{
				PlayerControllerB[] array = StartOfRound.Instance?.allPlayerScripts?.Where((PlayerControllerB p) => (Object)(object)p != (Object)null && p.isPlayerControlled).ToArray();
				if (array != null && array.Length != 0)
				{
					val = array[(uint)(networkObjectId % (ulong)array.Length)];
				}
			}
			if (!((Object)(object)val == (Object)null))
			{
				Mimics[networkObjectId] = new MimicIdentity
				{
					Enemy = enemy,
					CopiedClientId = val.actualClientId,
					CopiedName = (val.playerUsername ?? "crewmate")
				};
				Plugin.Log.LogInfo((object)$"Masked {networkObjectId} copied {val.playerUsername} ({val.actualClientId}).");
			}
		}

		internal static MimicIdentity ChooseForSpeaker(ulong clientId)
		{
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ae: Unknown result type (might be due to invalid IL or missing references)
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton == (Object)null || !singleton.IsServer || !singleton.ConnectedClients.TryGetValue(clientId, out var value))
			{
				return null;
			}
			NetworkObject playerObject = value.PlayerObject;
			PlayerControllerB val = ((playerObject != null) ? ((Component)playerObject).GetComponent<PlayerControllerB>() : null);
			if ((Object)(object)val == (Object)null)
			{
				return null;
			}
			MimicIdentity mimicIdentity = null;
			float num = float.MaxValue;
			foreach (MimicIdentity value2 in Mimics.Values)
			{
				if (!((Object)(object)value2.Enemy == (Object)null) && !((EnemyAI)value2.Enemy).isEnemyDead && !(Time.unscaledTime < value2.NextReplyAt))
				{
					float num2 = Vector3.Distance(((Component)val).transform.position, ((Component)value2.Enemy).transform.position);
					if (num2 <= Plugin.HearingRange.Value && num2 < num)
					{
						mimicIdentity = value2;
						num = num2;
					}
				}
			}
			if (mimicIdentity == null || Random.value > Mathf.Clamp01(Plugin.ReplyChance.Value))
			{
				return null;
			}
			mimicIdentity.NextReplyAt = Time.unscaledTime + Mathf.Max(2f, Plugin.ReplyCooldown.Value);
			return mimicIdentity;
		}

		internal static string BuildScene(ulong speakerId, MimicIdentity mimic)
		{
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			PlayerControllerB val = ResolvePlayer(speakerId);
			List<string> list = new List<string>();
			PlayerControllerB[] array = StartOfRound.Instance?.allPlayerScripts ?? Array.Empty<PlayerControllerB>();
			foreach (PlayerControllerB val2 in array)
			{
				if (!((Object)(object)val2 == (Object)null) && val2.isPlayerControlled)
				{
					float num = Vector3.Distance(((Component)mimic.Enemy).transform.position, ((Component)val2).transform.position);
					if (num <= 45f)
					{
						list.Add(string.Format("{0}: {1}, {2:0}m away, {3}", val2.playerUsername, val2.isPlayerDead ? "dead" : "alive", num, val2.isInsideFactory ? "inside" : "outside"));
					}
				}
			}
			string text = StartOfRound.Instance?.currentLevel?.PlanetName ?? "unknown moon";
			return "Moon: " + text + ". Mimic is " + (((EnemyAI)mimic.Enemy).isOutside ? "outside" : "inside") + ". Speaker is " + (((Object)(object)val != (Object)null && val.isInsideFactory) ? "inside" : "outside") + ". Nearby crew: " + string.Join("; ", list) + ".";
		}

		internal static PlayerControllerB ResolvePlayer(ulong clientId)
		{
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton != (Object)null && singleton.ConnectedClients.TryGetValue(clientId, out var value))
			{
				NetworkObject playerObject = value.PlayerObject;
				if (playerObject == null)
				{
					return null;
				}
				return ((Component)playerObject).GetComponent<PlayerControllerB>();
			}
			return null;
		}

		internal static MaskedPlayerEnemy ResolveMimic(ulong networkId)
		{
			if (Mimics.TryGetValue(networkId, out var value) && (Object)(object)value.Enemy != (Object)null)
			{
				return value.Enemy;
			}
			MaskedPlayerEnemy[] array = Object.FindObjectsOfType<MaskedPlayerEnemy>();
			foreach (MaskedPlayerEnemy val in array)
			{
				if (((NetworkBehaviour)val).NetworkObjectId == networkId)
				{
					return val;
				}
			}
			return null;
		}

		internal static void Reset()
		{
			Mimics.Clear();
			_nextScan = 0f;
		}
	}
	internal static class MimicTransport
	{
		private sealed class IncomingSpeech
		{
			internal byte[] Data;

			internal int Received;

			internal ulong MimicId;

			internal float ExpiresAt;

			internal readonly HashSet<int> Offsets = new HashSet<int>();
		}

		[CompilerGenerated]
		private static class <>O
		{
			public static HandleNamedMessageDelegate <0>__OnUtterance;

			public static HandleNamedMessageDelegate <1>__OnSpeechStart;

			public static HandleNamedMessageDelegate <2>__OnSpeechChunk;
		}

		private const string UtteranceMessage = "EchoMimics_Utterance_v1";

		private const string SpeechStartMessage = "EchoMimics_SpeechStart_v1";

		private const string SpeechChunkMessage = "EchoMimics_SpeechChunk_v1";

		private const int MaxUtteranceBytes = 307200;

		private const int MaxSpeechBytes = 921600;

		private const int ChunkBytes = 7000;

		private static readonly SemaphoreSlim InferenceGate = new SemaphoreSlim(1, 1);

		private static readonly Dictionary<ulong, IncomingSpeech> Incoming = new Dictionary<ulong, IncomingSpeech>();

		private static NetworkManager _registeredOn;

		private static ulong _nextTransferId = 1uL;

		private static string _sessionId = Guid.NewGuid().ToString("N");

		internal static void Tick()
		{
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton == (Object)null || !singleton.IsListening || singleton.CustomMessagingManager == null)
			{
				_registeredOn = null;
				Incoming.Clear();
				return;
			}
			if ((Object)(object)_registeredOn != (Object)(object)singleton)
			{
				Register(singleton);
			}
			ulong[] array = (from p in Incoming
				where p.Value.ExpiresAt < Time.unscaledTime
				select p.Key).ToArray();
			foreach (ulong key in array)
			{
				Incoming.Remove(key);
			}
		}

		private static void Register(NetworkManager nm)
		{
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Expected O, but got Unknown
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Expected O, but got Unknown
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bd: Expected O, but got Unknown
			try
			{
				nm.CustomMessagingManager.UnregisterNamedMessageHandler("EchoMimics_Utterance_v1");
			}
			catch
			{
			}
			try
			{
				nm.CustomMessagingManager.UnregisterNamedMessageHandler("EchoMimics_SpeechStart_v1");
			}
			catch
			{
			}
			try
			{
				nm.CustomMessagingManager.UnregisterNamedMessageHandler("EchoMimics_SpeechChunk_v1");
			}
			catch
			{
			}
			CustomMessagingManager customMessagingManager = nm.CustomMessagingManager;
			object obj4 = <>O.<0>__OnUtterance;
			if (obj4 == null)
			{
				HandleNamedMessageDelegate val = OnUtterance;
				<>O.<0>__OnUtterance = val;
				obj4 = (object)val;
			}
			customMessagingManager.RegisterNamedMessageHandler("EchoMimics_Utterance_v1", (HandleNamedMessageDelegate)obj4);
			CustomMessagingManager customMessagingManager2 = nm.CustomMessagingManager;
			object obj5 = <>O.<1>__OnSpeechStart;
			if (obj5 == null)
			{
				HandleNamedMessageDelegate val2 = OnSpeechStart;
				<>O.<1>__OnSpeechStart = val2;
				obj5 = (object)val2;
			}
			customMessagingManager2.RegisterNamedMessageHandler("EchoMimics_SpeechStart_v1", (HandleNamedMessageDelegate)obj5);
			CustomMessagingManager customMessagingManager3 = nm.CustomMessagingManager;
			object obj6 = <>O.<2>__OnSpeechChunk;
			if (obj6 == null)
			{
				HandleNamedMessageDelegate val3 = OnSpeechChunk;
				<>O.<2>__OnSpeechChunk = val3;
				obj6 = (object)val3;
			}
			customMessagingManager3.RegisterNamedMessageHandler("EchoMimics_SpeechChunk_v1", (HandleNamedMessageDelegate)obj6);
			_registeredOn = nm;
		}

		internal unsafe static void SendUtterance(byte[] wav)
		{
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			if (wav == null || wav.Length < 44 || wav.Length > 307200)
			{
				return;
			}
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton == (Object)null || !singleton.IsListening || singleton.CustomMessagingManager == null)
			{
				return;
			}
			if (singleton.IsServer)
			{
				ProcessUtterance(singleton.LocalClientId, wav);
				return;
			}
			FastBufferWriter val = default(FastBufferWriter);
			((FastBufferWriter)(ref val))..ctor(wav.Length + 16, (Allocator)2, -1);
			try
			{
				int num = wav.Length;
				((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num, default(ForPrimitives));
				((FastBufferWriter)(ref val)).WriteBytesSafe(wav, wav.Length, 0);
				singleton.CustomMessagingManager.SendNamedMessage("EchoMimics_Utterance_v1", 0uL, val, (NetworkDelivery)4);
			}
			finally
			{
				((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose();
			}
		}

		private static void OnUtterance(ulong senderId, FastBufferReader reader)
		{
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				NetworkManager singleton = NetworkManager.Singleton;
				if (!((Object)(object)singleton == (Object)null) && singleton.IsServer && senderId != 0L && singleton.ConnectedClientsIds.Contains(senderId))
				{
					int num = default(int);
					((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives));
					if (num >= 44 && num <= 307200 && ((FastBufferReader)(ref reader)).Length - ((FastBufferReader)(ref reader)).Position >= num)
					{
						byte[] wav = new byte[num];
						((FastBufferReader)(ref reader)).ReadBytesSafe(ref wav, num, 0);
						ProcessUtterance(senderId, wav);
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Rejected remote utterance: " + ex.Message));
			}
		}

		private static void ProcessUtterance(ulong senderId, byte[] wav)
		{
			PlayerControllerB val = MimicDirector.ResolvePlayer(senderId);
			if (!((Object)(object)val == (Object)null))
			{
				MimicIdentity mimicIdentity = MimicDirector.ChooseForSpeaker(senderId);
				string scene = ((mimicIdentity == null) ? BasicScene(val) : MimicDirector.BuildScene(senderId, mimicIdentity));
				RuntimeRequest obj = new RuntimeRequest
				{
					session_id = _sessionId,
					speaker_id = senderId.ToString(),
					speaker_name = (val.playerUsername ?? $"Crewmate {senderId}"),
					scene = scene,
					language = Plugin.Language.Value,
					audio_wav = Convert.ToBase64String(wav)
				};
				object obj2;
				if (mimicIdentity == null)
				{
					obj2 = null;
				}
				else
				{
					MaskedPlayerEnemy enemy = mimicIdentity.Enemy;
					obj2 = ((enemy != null) ? ((NetworkBehaviour)enemy).NetworkObjectId.ToString() : null);
				}
				if (obj2 == null)
				{
					obj2 = "none";
				}
				obj.mimic_id = (string)obj2;
				obj.copied_player_id = mimicIdentity?.CopiedClientId.ToString() ?? senderId.ToString();
				obj.copied_player_name = mimicIdentity?.CopiedName ?? val.playerUsername ?? "crewmate";
				RunInference(obj, mimicIdentity);
			}
		}

		private static async Task RunInference(RuntimeRequest request, MimicIdentity mimic)
		{
			if (!(await InferenceGate.WaitAsync(0).ConfigureAwait(continueOnCapturedContext: false)))
			{
				return;
			}
			try
			{
				RuntimeResponse runtimeResponse = ((mimic != null) ? (await LocalRuntimeClient.TurnAsync(request).ConfigureAwait(continueOnCapturedContext: false)) : (await LocalRuntimeClient.ObserveAsync(request).ConfigureAwait(continueOnCapturedContext: false)));
				RuntimeResponse runtimeResponse2 = runtimeResponse;
				if (!string.IsNullOrWhiteSpace(runtimeResponse2.heard))
				{
					Plugin.Log.LogInfo((object)("Heard " + request.speaker_name + ": " + runtimeResponse2.heard));
				}
				if (mimic != null && !string.IsNullOrEmpty(runtimeResponse2.audio_wav))
				{
					byte[] audio = Convert.FromBase64String(runtimeResponse2.audio_wav);
					if (audio.Length > 921600)
					{
						throw new InvalidOperationException("generated speech exceeded network limit");
					}
					ulong mimicId = ((NetworkBehaviour)mimic.Enemy).NetworkObjectId;
					Plugin.MainThread.Enqueue(delegate
					{
						SpatialVoice.Play(mimicId, audio);
						BroadcastSpeech(mimicId, audio);
					});
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Local mimic inference: " + ex.Message));
			}
			finally
			{
				InferenceGate.Release();
			}
		}

		private static string BasicScene(PlayerControllerB speaker)
		{
			string text = StartOfRound.Instance?.currentLevel?.PlanetName ?? "unknown moon";
			return "Moon: " + text + ". " + speaker.playerUsername + " is " + (speaker.isInsideFactory ? "inside the facility" : "outside") + ".";
		}

		private unsafe static void BroadcastSpeech(ulong mimicId, byte[] wav)
		{
			//IL_005e: 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)
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: 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_00a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
			//IL_0104: Unknown result type (might be due to invalid IL or missing references)
			//IL_0111: Unknown result type (might be due to invalid IL or missing references)
			//IL_0117: Unknown result type (might be due to invalid IL or missing references)
			//IL_0124: Unknown result type (might be due to invalid IL or missing references)
			//IL_012a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0149: Unknown result type (might be due to invalid IL or missing references)
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton == (Object)null || !singleton.IsServer || singleton.CustomMessagingManager == null)
			{
				return;
			}
			ulong num = _nextTransferId++;
			FastBufferWriter val = default(FastBufferWriter);
			FastBufferWriter val2 = default(FastBufferWriter);
			foreach (ulong connectedClientsId in singleton.ConnectedClientsIds)
			{
				if (connectedClientsId == 0L)
				{
					continue;
				}
				((FastBufferWriter)(ref val))..ctor(32, (Allocator)2, -1);
				try
				{
					((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref num, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref mimicId, default(ForPrimitives));
					int num2 = wav.Length;
					((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num2, default(ForPrimitives));
					singleton.CustomMessagingManager.SendNamedMessage("EchoMimics_SpeechStart_v1", connectedClientsId, val, (NetworkDelivery)4);
				}
				finally
				{
					((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose();
				}
				for (int i = 0; i < wav.Length; i += 7000)
				{
					int num3 = Math.Min(7000, wav.Length - i);
					byte[] array = new byte[num3];
					Buffer.BlockCopy(wav, i, array, 0, num3);
					((FastBufferWriter)(ref val2))..ctor(num3 + 32, (Allocator)2, -1);
					try
					{
						((FastBufferWriter)(ref val2)).WriteValueSafe<ulong>(ref num, default(ForPrimitives));
						((FastBufferWriter)(ref val2)).WriteValueSafe<int>(ref i, default(ForPrimitives));
						((FastBufferWriter)(ref val2)).WriteValueSafe<int>(ref num3, default(ForPrimitives));
						((FastBufferWriter)(ref val2)).WriteBytesSafe(array, num3, 0);
						singleton.CustomMessagingManager.SendNamedMessage("EchoMimics_SpeechChunk_v1", connectedClientsId, val2, (NetworkDelivery)4);
					}
					finally
					{
						((IDisposable)(*(FastBufferWriter*)(&val2))/*cast due to .constrained prefix*/).Dispose();
					}
				}
			}
		}

		private static void OnSpeechStart(ulong senderId, FastBufferReader reader)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: 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_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)
			NetworkManager singleton = NetworkManager.Singleton;
			if (!((Object)(object)singleton == (Object)null) && !singleton.IsServer && senderId == 0L)
			{
				ulong num = default(ulong);
				((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref num, default(ForPrimitives));
				ulong mimicId = default(ulong);
				((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref mimicId, default(ForPrimitives));
				int num2 = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num2, default(ForPrimitives));
				if (num != 0L && num2 >= 44 && num2 <= 921600)
				{
					Incoming[num] = new IncomingSpeech
					{
						Data = new byte[num2],
						MimicId = mimicId,
						ExpiresAt = Time.unscaledTime + 15f
					};
				}
			}
		}

		private static void OnSpeechChunk(ulong senderId, FastBufferReader reader)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: 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_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)
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton == (Object)null || singleton.IsServer || senderId != 0L)
			{
				return;
			}
			ulong key = default(ulong);
			((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref key, default(ForPrimitives));
			int num = default(int);
			((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives));
			int num2 = default(int);
			((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num2, default(ForPrimitives));
			if (Incoming.TryGetValue(key, out var value) && num2 > 0 && num2 <= 7000 && num >= 0 && num + num2 <= value.Data.Length && value.Offsets.Add(num))
			{
				byte[] src = new byte[num2];
				((FastBufferReader)(ref reader)).ReadBytesSafe(ref src, num2, 0);
				Buffer.BlockCopy(src, 0, value.Data, num, num2);
				value.Received += num2;
				if (value.Received == value.Data.Length)
				{
					Incoming.Remove(key);
					SpatialVoice.Play(value.MimicId, value.Data);
				}
			}
		}

		internal static void ResetSession()
		{
			string sessionId = _sessionId;
			_sessionId = Guid.NewGuid().ToString("N");
			Incoming.Clear();
			LocalRuntimeClient.ClearSessionAsync(sessionId).ContinueWith(delegate(Task task)
			{
				if (task.IsFaulted)
				{
					Plugin.Log.LogDebug((object)("Session cleanup skipped: " + task.Exception?.GetBaseException().Message));
				}
			});
		}
	}
	[BepInPlugin("com.testyee.echomimics", "EchoMimics", "0.1.1")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "com.testyee.echomimics";

		public const string Name = "EchoMimics";

		public const string Version = "0.1.1";

		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<bool> CaptureConsent;

		internal static ConfigEntry<string> RuntimeUrl;

		internal static ConfigEntry<string> RuntimeToken;

		internal static ConfigEntry<string> InputDevice;

		internal static ConfigEntry<float> VadThreshold;

		internal static ConfigEntry<float> HearingRange;

		internal static ConfigEntry<float> ReplyChance;

		internal static ConfigEntry<float> ReplyCooldown;

		internal static ConfigEntry<float> VoiceVolume;

		internal static ConfigEntry<string> Language;

		internal static readonly ConcurrentQueue<Action> MainThread = new ConcurrentQueue<Action>();

		private Harmony _harmony;

		private void Awake()
		{
			//IL_0195: Unknown result type (might be due to invalid IL or missing references)
			//IL_019f: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable EchoMimics.");
			CaptureConsent = ((BaseUnityPlugin)this).Config.Bind<bool>("Privacy", "AllowVoiceCapture", false, "Required opt-in. Captures speech during active rounds for local transcription and voice cloning. Raw clips remain on the host computer.");
			RuntimeUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Local runtime", "Url", "http://127.0.0.1:17841", "Loopback URL for the local EchoMimics runtime.");
			RuntimeToken = ((BaseUnityPlugin)this).Config.Bind<string>("Local runtime", "Token", "", "Optional bearer token shared with the local runtime.");
			InputDevice = ((BaseUnityPlugin)this).Config.Bind<string>("Voice", "InputDevice", "", "Microphone name; blank follows the game's selected input where possible.");
			VadThreshold = ((BaseUnityPlugin)this).Config.Bind<float>("Voice", "ActivityThreshold", 0.018f, "RMS threshold used to detect speech.");
			HearingRange = ((BaseUnityPlugin)this).Config.Bind<float>("Mimics", "HearingRange", 32f, "Maximum distance at which a Masked can hear a crewmate.");
			ReplyChance = ((BaseUnityPlugin)this).Config.Bind<float>("Mimics", "ReplyChance", 0.58f, "Chance that an eligible Masked answers an utterance.");
			ReplyCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Mimics", "ReplyCooldownSeconds", 8f, "Minimum time between replies from one Masked.");
			VoiceVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Voice", "MimicVolume", 1.2f, "Spatial playback volume.");
			Language = ((BaseUnityPlugin)this).Config.Bind<string>("Voice", "Language", "en", "Whisper language code; blank enables detection.");
			_harmony = new Harmony("com.testyee.echomimics");
			_harmony.PatchAll();
			((Component)this).gameObject.AddComponent<EchoMimicsHost>();
			Log.LogInfo((object)string.Format("{0} {1} loaded. Voice capture consent: {2}", "EchoMimics", "0.1.1", CaptureConsent.Value));
		}

		private void OnDestroy()
		{
			AmbientCapture.Stop();
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
	internal sealed class EchoMimicsHost : MonoBehaviour
	{
		private void Update()
		{
			Action result;
			while (Plugin.MainThread.TryDequeue(out result))
			{
				try
				{
					result();
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("Main-thread action: " + ex.Message));
				}
			}
			if (Plugin.Enabled.Value)
			{
				MimicTransport.Tick();
				AmbientCapture.Tick();
				MimicDirector.Tick();
				SpatialVoice.Tick();
			}
		}
	}
	[HarmonyPatch(typeof(StartOfRound), "ShipLeave")]
	internal static class ShipLeavePatch
	{
		private static void Prefix()
		{
			MimicDirector.Reset();
			MimicTransport.ResetSession();
			SpatialVoice.Reset();
		}
	}
	[HarmonyPatch(typeof(RoundManager), "DespawnPropsAtEndOfRound")]
	internal static class RoundEndPatch
	{
		private static void Prefix()
		{
			AmbientCapture.Stop();
			MimicDirector.Reset();
			MimicTransport.ResetSession();
			SpatialVoice.Reset();
		}
	}
	internal static class SpatialVoice
	{
		private static readonly Dictionary<ulong, AudioSource> Sources = new Dictionary<ulong, AudioSource>();

		internal static void Play(ulong mimicId, byte[] wav)
		{
			MaskedPlayerEnemy val = MimicDirector.ResolveMimic(mimicId);
			if (!((Object)(object)val == (Object)null) && AudioCodec.DecodeWav(wav, out var samples, out var sampleRate))
			{
				AudioClip val2 = AudioClip.Create($"EchoMimics-{mimicId}", samples.Length, 1, sampleRate, false);
				val2.SetData(samples, 0);
				if (!Sources.TryGetValue(mimicId, out var value) || (Object)(object)value == (Object)null)
				{
					value = ((Component)val).gameObject.AddComponent<AudioSource>();
					value.spatialBlend = 1f;
					value.rolloffMode = (AudioRolloffMode)1;
					value.minDistance = 2f;
					value.maxDistance = 45f;
					Sources[mimicId] = value;
				}
				value.volume = Mathf.Clamp(Plugin.VoiceVolume.Value, 0f, 2f);
				value.PlayOneShot(val2);
				Object.Destroy((Object)(object)val2, val2.length + 1f);
			}
		}

		internal static void Tick()
		{
			List<ulong> list = new List<ulong>();
			foreach (KeyValuePair<ulong, AudioSource> source in Sources)
			{
				if ((Object)(object)source.Value == (Object)null)
				{
					list.Add(source.Key);
				}
			}
			foreach (ulong item in list)
			{
				Sources.Remove(item);
			}
		}

		internal static void Reset()
		{
			Sources.Clear();
		}
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "EchoMimics";

		public const string PLUGIN_NAME = "EchoMimics";

		public const string PLUGIN_VERSION = "0.1.1";
	}
}