Decompiled source of RemoteVoiceSplit v0.1.0

RemoteVoiceSplit.dll

Decompiled 6 hours ago
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Microsoft.Win32.SafeHandles;
using RemoteVoiceSplit.Core;
using RemoteVoiceSplit.Interop.Game;
using RemoteVoiceSplit.Interop.ProcessAudio;
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("aoirint")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Moves remote Lethal Company player voices to an OBS-capturable audio process.")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+405c323fff7319af684519e269fe3edb7308eeeb")]
[assembly: AssemblyProduct("Remote Voice Split")]
[assembly: AssemblyTitle("RemoteVoiceSplit")]
[assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/aoirint/RemoteVoiceSplit")]
[assembly: AssemblyVersion("0.1.0.0")]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
}
namespace RemoteVoiceSplit
{
	[BepInPlugin("com.aoirint.remotevoicesplit", "Remote Voice Split", "0.1.0")]
	[BepInProcess("Lethal Company.exe")]
	public sealed class Plugin : BaseUnityPlugin
	{
		private void Awake()
		{
			if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"Remote Voice Split requires Windows process audio capture and has been disabled on this platform.");
				return;
			}
			try
			{
				ConfigEntry<bool> enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Set to false to disable this mod. Changes made through BepInEx configuration APIs apply immediately.");
				ConfigEntry<bool> fallbackToGameOutput = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "FallbackToGameOutput", false, "Keep remote voices on the normal game output whenever separate process output cannot accept them. The default false value prevents remote voice from leaking into the game-audio recording track, but also makes remote voice inaudible until separate output recovers. Changes made through BepInEx configuration APIs apply immediately.");
				int num = AudioSettings.outputSampleRate;
				if (num <= 0)
				{
					num = 48000;
					((BaseUnityPlugin)this).Logger.LogWarning((object)"Unity did not report an output sample rate; falling back to 48000 Hz for remote voice process output.");
				}
				string audioHostPath = Path.Combine(Path.GetDirectoryName(typeof(Plugin).Assembly.Location) ?? throw new InvalidOperationException("The plugin assembly directory is unavailable."), "RemoteVoiceSplit.AudioHost.exe");
				if (PluginRuntime.Initialize(((BaseUnityPlugin)this).Logger, num, Process.GetCurrentProcess().Id, audioHostPath, enabled, fallbackToGameOutput))
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)"Remote Voice Split 0.1.0 loaded.");
				}
				else
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)"Remote Voice Split process-lifetime routing was already initialized.");
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Remote Voice Split could not initialize and was disabled: " + ex.GetType().Name + ": " + ex.Message));
			}
		}
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "com.aoirint.remotevoicesplit";

		public const string PLUGIN_NAME = "Remote Voice Split";

		public const string PLUGIN_VERSION = "0.1.0";
	}
}
namespace RemoteVoiceSplit.Interop.ProcessAudio
{
	internal static class DetachedAudioHostLauncher
	{
		private struct StartupInfo
		{
			public uint Size;

			public IntPtr Reserved;

			public IntPtr Desktop;

			public IntPtr Title;

			public uint X;

			public uint Y;

			public uint XSize;

			public uint YSize;

			public uint XCountChars;

			public uint YCountChars;

			public uint FillAttribute;

			public uint Flags;

			public ushort ShowWindow;

			public ushort ReservedSize;

			public IntPtr ReservedData;

			public IntPtr StandardInput;

			public IntPtr StandardOutput;

			public IntPtr StandardError;
		}

		private struct StartupInfoEx
		{
			public StartupInfo StartupInfo;

			public IntPtr AttributeList;
		}

		private struct ProcessInformation
		{
			public IntPtr Process;

			public IntPtr Thread;

			public uint ProcessId;

			public uint ThreadId;
		}

		private const uint ProcessCreateProcess = 128u;

		private const uint QueryLimitedInformation = 4096u;

		private const uint ExtendedStartupInfoPresent = 524288u;

		private const int ParentProcessAttribute = 131072;

		public static int Launch(string executablePath, string pipeName, int gameProcessId)
		{
			ValidateArguments(executablePath, pipeName, gameProcessId);
			uint shellProcessId = GetShellProcessId();
			using SafeProcessHandle safeProcessHandle = OpenProcess(4224u, inheritHandle: false, shellProcessId);
			if (safeProcessHandle.IsInvalid)
			{
				throw CreateWin32Exception("Windows could not open the desktop shell as the audio host parent.");
			}
			VerifyShellImage(safeProcessHandle);
			IntPtr intPtr = IntPtr.Zero;
			IntPtr intPtr2 = IntPtr.Zero;
			bool flag = false;
			try
			{
				IntPtr size = IntPtr.Zero;
				InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0u, ref size);
				if (size == IntPtr.Zero)
				{
					throw CreateWin32Exception("Windows could not size the audio host process attribute list.");
				}
				intPtr = Marshal.AllocHGlobal(size);
				if (!InitializeProcThreadAttributeList(intPtr, 1, 0u, ref size))
				{
					throw CreateWin32Exception("Windows could not initialize the audio host process attribute list.");
				}
				flag = true;
				intPtr2 = Marshal.AllocHGlobal(IntPtr.Size);
				Marshal.WriteIntPtr(intPtr2, safeProcessHandle.DangerousGetHandle());
				if (!UpdateProcThreadAttribute(intPtr, 0u, new IntPtr(131072), intPtr2, new IntPtr(IntPtr.Size), IntPtr.Zero, IntPtr.Zero))
				{
					throw CreateWin32Exception("Windows could not assign the desktop shell as the audio host parent.");
				}
				return LaunchWithAttributes(executablePath, pipeName, gameProcessId, intPtr);
			}
			finally
			{
				if (flag)
				{
					DeleteProcThreadAttributeList(intPtr);
				}
				if (intPtr2 != IntPtr.Zero)
				{
					Marshal.FreeHGlobal(intPtr2);
				}
				if (intPtr != IntPtr.Zero)
				{
					Marshal.FreeHGlobal(intPtr);
				}
			}
		}

		private static void ValidateArguments(string executablePath, string pipeName, int gameProcessId)
		{
			if (string.IsNullOrWhiteSpace(executablePath))
			{
				throw new ArgumentException("The audio host path is required.", "executablePath");
			}
			if (!File.Exists(executablePath))
			{
				throw new FileNotFoundException("The packaged audio host was not found.", executablePath);
			}
			if (string.IsNullOrWhiteSpace(pipeName))
			{
				throw new ArgumentException("The pipe name is required.", "pipeName");
			}
			if (gameProcessId <= 0)
			{
				throw new ArgumentOutOfRangeException("gameProcessId");
			}
		}

		private static uint GetShellProcessId()
		{
			IntPtr shellWindow = GetShellWindow();
			if (shellWindow == IntPtr.Zero)
			{
				throw new InvalidOperationException("Windows did not report an interactive desktop shell.");
			}
			GetWindowThreadProcessId(shellWindow, out var processId);
			if (processId == 0)
			{
				throw CreateWin32Exception("Windows did not report the desktop shell process.");
			}
			return processId;
		}

		private static void VerifyShellImage(SafeProcessHandle shellProcess)
		{
			string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
			if (string.IsNullOrWhiteSpace(folderPath))
			{
				throw new InvalidOperationException("Windows did not report its installation directory.");
			}
			string fullPath = Path.GetFullPath(Path.Combine(folderPath, "explorer.exe"));
			string fullPath2 = Path.GetFullPath(ProcessImagePath.Get(shellProcess));
			if (!string.Equals(fullPath, fullPath2, StringComparison.OrdinalIgnoreCase))
			{
				throw new InvalidOperationException("The interactive desktop shell is not the Windows Explorer executable.");
			}
		}

		private static int LaunchWithAttributes(string executablePath, string pipeName, int gameProcessId, IntPtr attributeList)
		{
			string fullPath = Path.GetFullPath(executablePath);
			StringBuilder commandLine = new StringBuilder(string.Format(CultureInfo.InvariantCulture, "{0} --pipe {1} --game-process-id {2}", QuoteCommandLineArgument(fullPath), QuoteCommandLineArgument(pipeName), gameProcessId));
			checked
			{
				StartupInfoEx startupInfo = new StartupInfoEx
				{
					StartupInfo = new StartupInfo
					{
						Size = (uint)Marshal.SizeOf<StartupInfoEx>()
					},
					AttributeList = attributeList
				};
				if (!CreateProcess(fullPath, commandLine, IntPtr.Zero, IntPtr.Zero, inheritHandles: false, 524288u, IntPtr.Zero, Path.GetDirectoryName(fullPath), ref startupInfo, out var processInformation))
				{
					throw CreateWin32Exception("Windows could not start the audio host process.");
				}
				CloseHandle(processInformation.Thread);
				CloseHandle(processInformation.Process);
				return (int)processInformation.ProcessId;
			}
		}

		private static string QuoteCommandLineArgument(string value)
		{
			StringBuilder stringBuilder = new StringBuilder(value.Length + 2);
			stringBuilder.Append('"');
			int num = 0;
			foreach (char c in value)
			{
				switch (c)
				{
				case '\\':
					num++;
					break;
				case '"':
					stringBuilder.Append('\\', checked(num * 2 + 1));
					stringBuilder.Append(c);
					num = 0;
					break;
				default:
					stringBuilder.Append('\\', num);
					num = 0;
					stringBuilder.Append(c);
					break;
				}
			}
			stringBuilder.Append('\\', checked(num * 2));
			stringBuilder.Append('"');
			return stringBuilder.ToString();
		}

		private static Win32Exception CreateWin32Exception(string message)
		{
			return new Win32Exception(Marshal.GetLastWin32Error(), message);
		}

		[DllImport("user32.dll")]
		private static extern IntPtr GetShellWindow();

		[DllImport("user32.dll", SetLastError = true)]
		private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);

		[DllImport("kernel32.dll", SetLastError = true)]
		private static extern SafeProcessHandle OpenProcess(uint desiredAccess, [MarshalAs(UnmanagedType.Bool)] bool inheritHandle, uint processId);

		[DllImport("kernel32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool InitializeProcThreadAttributeList(IntPtr attributeList, int attributeCount, uint flags, ref IntPtr size);

		[DllImport("kernel32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool UpdateProcThreadAttribute(IntPtr attributeList, uint flags, IntPtr attribute, IntPtr value, IntPtr size, IntPtr previousValue, IntPtr returnSize);

		[DllImport("kernel32.dll")]
		private static extern void DeleteProcThreadAttributeList(IntPtr attributeList);

		[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool CreateProcess(string applicationName, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint creationFlags, IntPtr environment, string? currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation);

		[DllImport("kernel32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool CloseHandle(IntPtr handle);
	}
	internal static class NamedPipeServerIdentity
	{
		public static int GetServerProcessId(NamedPipeClientStream pipe)
		{
			if (pipe == null)
			{
				throw new ArgumentNullException("pipe");
			}
			if (!GetNamedPipeServerProcessId(pipe.SafePipeHandle, out var serverProcessId))
			{
				throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows could not identify the audio host pipe server.");
			}
			return checked((int)serverProcessId);
		}

		[DllImport("kernel32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool GetNamedPipeServerProcessId(SafePipeHandle pipe, out uint serverProcessId);
	}
	internal static class ProcessImagePath
	{
		private const uint QueryLimitedInformation = 4096u;

		public static string Get(int processId)
		{
			using SafeProcessHandle safeProcessHandle = OpenProcess(4096u, inheritHandle: false, checked((uint)processId));
			if (safeProcessHandle.IsInvalid)
			{
				throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows could not open a process for image-path verification.");
			}
			return Get(safeProcessHandle);
		}

		public static string Get(SafeProcessHandle process)
		{
			if (process == null)
			{
				throw new ArgumentNullException("process");
			}
			if (process.IsInvalid || process.IsClosed)
			{
				throw new ArgumentException("A live process handle is required.", "process");
			}
			StringBuilder stringBuilder = new StringBuilder(32768);
			int size = stringBuilder.Capacity;
			if (!QueryFullProcessImageName(process, 0u, stringBuilder, ref size))
			{
				throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows could not read a process executable path.");
			}
			return stringBuilder.ToString();
		}

		[DllImport("kernel32.dll", SetLastError = true)]
		private static extern SafeProcessHandle OpenProcess(uint desiredAccess, [MarshalAs(UnmanagedType.Bool)] bool inheritHandle, uint processId);

		[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool QueryFullProcessImageName(SafeProcessHandle process, uint flags, StringBuilder executableName, ref int size);
	}
	internal static class ProcessTreeSnapshot
	{
		[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
		private struct ProcessEntry32
		{
			public uint Size;

			public uint Usage;

			public uint ProcessId;

			public IntPtr DefaultHeapId;

			public uint ModuleId;

			public uint Threads;

			public uint ParentProcessId;

			public int BasePriority;

			public uint Flags;

			[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
			public string ExecutableFile;
		}

		private sealed class SafeSnapshotHandle : SafeHandleZeroOrMinusOneIsInvalid
		{
			private SafeSnapshotHandle()
				: base(ownsHandle: true)
			{
			}

			protected override bool ReleaseHandle()
			{
				return CloseHandle(handle);
			}
		}

		private const uint SnapshotProcesses = 2u;

		public static bool IsSelfOrDescendant(int candidateProcessId, int ancestorProcessId)
		{
			return ProcessAncestry.IsSelfOrDescendant(candidateProcessId, ancestorProcessId, Capture());
		}

		private static Dictionary<int, int> Capture()
		{
			checked
			{
				using SafeSnapshotHandle safeSnapshotHandle = CreateToolhelp32Snapshot(2u, 0u);
				if (safeSnapshotHandle.IsInvalid)
				{
					throw new InvalidOperationException($"Windows could not snapshot the process tree. Error {Marshal.GetLastWin32Error()}.");
				}
				Dictionary<int, int> dictionary = new Dictionary<int, int>();
				ProcessEntry32 entry = new ProcessEntry32
				{
					Size = (uint)Marshal.SizeOf<ProcessEntry32>()
				};
				if (!Process32First(safeSnapshotHandle, ref entry))
				{
					throw new InvalidOperationException($"Windows could not read the process tree. Error {Marshal.GetLastWin32Error()}.");
				}
				do
				{
					dictionary[(int)entry.ProcessId] = (int)entry.ParentProcessId;
					entry.Size = (uint)Marshal.SizeOf<ProcessEntry32>();
				}
				while (Process32Next(safeSnapshotHandle, ref entry));
				int lastWin32Error = Marshal.GetLastWin32Error();
				if (lastWin32Error != 18)
				{
					throw new InvalidOperationException($"Windows process enumeration failed. Error {lastWin32Error}.");
				}
				return dictionary;
			}
		}

		[DllImport("kernel32.dll", SetLastError = true)]
		private static extern SafeSnapshotHandle CreateToolhelp32Snapshot(uint flags, uint processId);

		[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool Process32First(SafeSnapshotHandle snapshot, ref ProcessEntry32 entry);

		[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool Process32Next(SafeSnapshotHandle snapshot, ref ProcessEntry32 entry);

		[DllImport("kernel32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		private static extern bool CloseHandle(IntPtr handle);
	}
	internal sealed class VoiceProcessRouter : IDisposable
	{
		private const int CaptureCapacitySamples = 131072;

		private static readonly TimeSpan SessionRetryDelay = TimeSpan.FromSeconds(1.0);

		private readonly ManualLogSource _logger;

		private readonly VoiceAudioMixer _mixer = new VoiceAudioMixer();

		private readonly ManualResetEvent _stop = new ManualResetEvent(initialState: false);

		private readonly RoutingSessionGate _routingSession = new RoutingSessionGate();

		private readonly Thread _worker;

		private readonly int _sampleRate;

		private readonly int _gameProcessId;

		private readonly string _audioHostPath;

		private readonly RemoteVoiceSettingsState _settings;

		private bool _disposed;

		public bool IsReady => _routingSession.IsReady;

		public VoiceProcessRouter(ManualLogSource logger, int sampleRate, int gameProcessId, string audioHostPath, RemoteVoiceSettingsState settings)
		{
			_logger = logger ?? throw new ArgumentNullException("logger");
			if (sampleRate <= 0)
			{
				throw new ArgumentOutOfRangeException("sampleRate");
			}
			if (gameProcessId <= 0)
			{
				throw new ArgumentOutOfRangeException("gameProcessId");
			}
			if (string.IsNullOrWhiteSpace(audioHostPath))
			{
				throw new ArgumentException("The audio host path is required.", "audioHostPath");
			}
			_sampleRate = sampleRate;
			_gameProcessId = gameProcessId;
			_audioHostPath = audioHostPath;
			_settings = settings ?? throw new ArgumentNullException("settings");
			_worker = new Thread(WorkerMain)
			{
				IsBackground = true,
				Name = "RemoteVoiceSplit pipe sender"
			};
			_worker.Start();
		}

		public VoiceCaptureStream RegisterCapture()
		{
			ThrowIfDisposed();
			return _mixer.Register(131072);
		}

		public void UnregisterCapture(VoiceCaptureStream stream)
		{
			_mixer.Unregister(stream);
		}

		public RoutingSubmissionLease? TrySubmit(VoiceCaptureStream stream, float[] samples, int channels)
		{
			RoutingSubmissionLease routingSubmissionLease = _routingSession.TryBeginSubmission();
			if (routingSubmissionLease == null)
			{
				return null;
			}
			if (!stream.TryWrite(samples, channels))
			{
				routingSubmissionLease.Dispose();
				return null;
			}
			return routingSubmissionLease;
		}

		public void Dispose()
		{
			if (!_disposed)
			{
				_disposed = true;
				SetNotReady();
				_stop.Set();
				if (_worker.Join(TimeSpan.FromSeconds(7.0)))
				{
					_stop.Dispose();
				}
				else
				{
					TryLog((LogLevel)4, "The remote voice audio-host thread did not stop within seven seconds; its stop handle was left intact.");
				}
			}
		}

		private void WorkerMain()
		{
			string pipeName = $"RemoteVoiceSplit-{_gameProcessId}-{Guid.NewGuid():N}";
			int num = 0;
			string a = null;
			while (!_stop.WaitOne(0))
			{
				try
				{
					if (!IsExpectedHostProcess(num))
					{
						num = DetachedAudioHostLauncher.Launch(_audioHostPath, pipeName, _gameProcessId);
					}
					RunSession(pipeName, num);
					a = null;
				}
				catch (Exception ex)
				{
					string text = ex.GetType().Name + ": " + ex.Message;
					if (!string.Equals(a, text, StringComparison.Ordinal))
					{
						TryLog((LogLevel)2, (!_settings.Enabled) ? ("Remote voice process output is unavailable while Remote Voice Split is disabled; Unity output remains enabled. " + text) : (_settings.FallbackToGameOutput ? ("Remote voice process output is unavailable; Unity output remains enabled. " + text) : ("Remote voice process output is unavailable; remote voice remains silent until it recovers. " + text)));
						a = text;
					}
				}
				finally
				{
					SetNotReady();
				}
				_stop.WaitOne(SessionRetryDelay);
			}
		}

		private void RunSession(string pipeName, int expectedHostProcessId)
		{
			using NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
			namedPipeClientStream.Connect(5000);
			using BinaryReader reader = new BinaryReader(namedPipeClientStream, Encoding.UTF8, leaveOpen: true);
			using BinaryWriter writer = new BinaryWriter(namedPipeClientStream, Encoding.UTF8, leaveOpen: true);
			AudioHostProtocol.WriteClientHello(writer, _sampleRate);
			int num = AudioHostProtocol.ReadServerReady(reader);
			if (num != expectedHostProcessId)
			{
				throw new InvalidOperationException("The audio host handshake did not match the launched process.");
			}
			int serverProcessId = NamedPipeServerIdentity.GetServerProcessId(namedPipeClientStream);
			if (num != serverProcessId)
			{
				throw new InvalidOperationException("The audio host handshake did not match the actual pipe server process.");
			}
			VerifyExpectedHostProcess(num);
			SetReady();
			TryLog((LogLevel)16, "Remote voice process output is ready. Select 'Lethal Company Remote Voice Split' in OBS Application Audio Capture.");
			SendAudio(namedPipeClientStream, writer);
		}

		private bool IsExpectedHostProcess(int processId)
		{
			if (processId <= 0)
			{
				return false;
			}
			try
			{
				VerifyExpectedHostProcess(processId);
				return true;
			}
			catch
			{
				return false;
			}
		}

		private void VerifyExpectedHostProcess(int processId)
		{
			string fullPath = Path.GetFullPath(_audioHostPath);
			string fullPath2 = Path.GetFullPath(ProcessImagePath.Get(processId));
			if (!string.Equals(fullPath, fullPath2, StringComparison.OrdinalIgnoreCase))
			{
				throw new InvalidOperationException("The actual pipe server is not the packaged Remote Voice Split audio host.");
			}
			if (ProcessTreeSnapshot.IsSelfOrDescendant(processId, _gameProcessId))
			{
				throw new InvalidOperationException("The audio host remained inside the game process tree and cannot provide an isolated OBS source.");
			}
		}

		private void SendAudio(NamedPipeClientStream pipe, BinaryWriter writer)
		{
			checked
			{
				int num = Math.Max(1, unchecked(_sampleRate / 100)) * 2;
				if (num > 16384)
				{
					throw new InvalidOperationException("The runtime sample rate exceeds the audio host protocol block limit.");
				}
				float[] array = new float[num];
				byte[] array2 = new byte[num * 4];
				Stopwatch stopwatch = Stopwatch.StartNew();
				while (!_stop.WaitOne(TimeSpan.FromMilliseconds(10.0)))
				{
					if (_mixer.Mix(array, num))
					{
						Buffer.BlockCopy(array, 0, array2, 0, array2.Length);
						writer.Write(num);
						writer.Write(array2, 0, array2.Length);
						writer.Flush();
						stopwatch.Restart();
					}
					else if (stopwatch.Elapsed >= TimeSpan.FromSeconds(1.0))
					{
						writer.Write(0);
						writer.Flush();
						stopwatch.Restart();
					}
					if (!pipe.IsConnected)
					{
						throw new EndOfStreamException("The audio host disconnected.");
					}
				}
			}
		}

		private void SetReady()
		{
			_routingSession.Activate();
		}

		private void SetNotReady()
		{
			_routingSession.Deactivate();
			_mixer.Clear();
		}

		private void TryLog(LogLevel level, string message)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				_logger.Log(level, (object)message);
			}
			catch
			{
			}
		}

		private void ThrowIfDisposed()
		{
			if (_disposed)
			{
				throw new ObjectDisposedException("VoiceProcessRouter");
			}
		}
	}
}
namespace RemoteVoiceSplit.Interop.Game
{
	internal static class GameReflection
	{
		public static Type StartOfRoundType { get; } = RequireType("StartOfRound");

		public static FieldInfo AllPlayerScriptsField { get; } = RequireField(StartOfRoundType, "allPlayerScripts");

		public static FieldInfo LocalPlayerControllerField { get; } = RequireField(StartOfRoundType, "localPlayerController");

		public static FieldInfo IsPlayerControlledField { get; } = RequireField(RequireType("GameNetcodeStuff.PlayerControllerB"), "isPlayerControlled");

		public static FieldInfo IsPlayerDeadField { get; } = RequireField(RequireType("GameNetcodeStuff.PlayerControllerB"), "isPlayerDead");

		public static FieldInfo CurrentVoiceSourceField { get; } = RequireField(RequireType("GameNetcodeStuff.PlayerControllerB"), "currentVoiceChatAudioSource");

		public static MethodInfo StartOfRoundRefreshMethod { get; } = RequireMethod(StartOfRoundType, "RefreshPlayerVoicePlaybackObjects");

		private static Type RequireType(string name)
		{
			return AccessTools.TypeByName(name) ?? throw new TypeLoadException("Lethal Company v81 type '" + name + "' was not found.");
		}

		private static FieldInfo RequireField(Type type, string name)
		{
			return AccessTools.Field(type, name) ?? throw new MissingFieldException(type.FullName, name);
		}

		private static MethodInfo RequireMethod(Type type, string name)
		{
			return AccessTools.Method(type, name, Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException(type.FullName, name);
		}
	}
	internal static class IntegrationContext
	{
		private static ManualLogSource? _logger;

		private static VoiceRoutingContext? _routing;

		public static void Initialize(ManualLogSource logger, VoiceProcessRouter router, RemoteVoiceSettingsState settings)
		{
			_logger = logger;
			_routing = new VoiceRoutingContext(router, settings);
		}

		public static void Clear()
		{
			_routing = null;
			_logger = null;
		}

		public static bool TryGetRouting(out VoiceRoutingContext? routing)
		{
			routing = _routing;
			return routing != null;
		}

		public static void RunGuarded(string operation, Action action)
		{
			try
			{
				action();
			}
			catch (Exception ex)
			{
				TryLog((LogLevel)2, operation + " failed without interrupting the game: " + ex.GetType().Name + ": " + ex.Message);
			}
		}

		private static void TryLog(LogLevel level, string message)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				ManualLogSource? logger = _logger;
				if (logger != null)
				{
					logger.Log(level, (object)message);
				}
			}
			catch
			{
			}
		}
	}
	internal sealed class VoiceRoutingContext
	{
		public VoiceProcessRouter Router { get; }

		public RemoteVoiceSettingsState Settings { get; }

		public VoiceRoutingContext(VoiceProcessRouter router, RemoteVoiceSettingsState settings)
		{
			Router = router;
			Settings = settings;
		}
	}
	internal static class PluginRuntime
	{
		private static readonly object Gate = new object();

		private static Harmony? _harmony;

		private static ManualLogSource? _logger;

		private static VoiceProcessRouter? _router;

		private static RemoteVoiceConfiguration? _configuration;

		private static bool _initialized;

		public static bool Initialize(ManualLogSource logger, int sampleRate, int gameProcessId, string audioHostPath, ConfigEntry<bool> enabled, ConfigEntry<bool> fallbackToGameOutput)
		{
			//IL_0082: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Expected O, but got Unknown
			if (logger == null)
			{
				throw new ArgumentNullException("logger");
			}
			if (enabled == null)
			{
				throw new ArgumentNullException("enabled");
			}
			if (fallbackToGameOutput == null)
			{
				throw new ArgumentNullException("fallbackToGameOutput");
			}
			lock (Gate)
			{
				if (_initialized)
				{
					return false;
				}
				VoiceProcessRouter router = null;
				Harmony val = null;
				RemoteVoiceConfiguration remoteVoiceConfiguration = null;
				try
				{
					remoteVoiceConfiguration = new RemoteVoiceConfiguration(enabled, fallbackToGameOutput, logger);
					router = new VoiceProcessRouter(logger, sampleRate, gameProcessId, audioHostPath, remoteVoiceConfiguration.State);
					IntegrationContext.Initialize(logger, router, remoteVoiceConfiguration.State);
					val = new Harmony("com.aoirint.remotevoicesplit");
					val.PatchAll(typeof(Plugin).Assembly);
					_logger = logger;
					_router = router;
					_harmony = val;
					_configuration = remoteVoiceConfiguration;
					Application.quitting += OnApplicationQuitting;
					_initialized = true;
					return true;
				}
				catch
				{
					IntegrationContext.Clear();
					TryUnpatch(val, logger);
					TryDisposeRouter(router, logger);
					TryDisposeConfiguration(remoteVoiceConfiguration, logger);
					_harmony = null;
					_router = null;
					_logger = null;
					_configuration = null;
					throw;
				}
			}
		}

		private static void OnApplicationQuitting()
		{
			lock (Gate)
			{
				if (_initialized)
				{
					_initialized = false;
					Application.quitting -= OnApplicationQuitting;
					ManualLogSource logger = _logger;
					Harmony? harmony = _harmony;
					VoiceProcessRouter router = _router;
					RemoteVoiceConfiguration configuration = _configuration;
					_harmony = null;
					_router = null;
					_logger = null;
					_configuration = null;
					TryLog(logger, (LogLevel)16, "Remote Voice Split is stopping because the game application is quitting.");
					IntegrationContext.Clear();
					TryUnpatch(harmony, logger);
					TryDisposeRouter(router, logger);
					TryDisposeConfiguration(configuration, logger);
				}
			}
		}

		private static void TryUnpatch(Harmony? harmony, ManualLogSource? logger)
		{
			try
			{
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
			}
			catch (Exception ex)
			{
				TryLog(logger, (LogLevel)4, "Harmony cleanup failed during application shutdown: " + ex.GetType().Name + ": " + ex.Message);
			}
		}

		private static void TryDisposeRouter(VoiceProcessRouter? router, ManualLogSource? logger)
		{
			try
			{
				router?.Dispose();
			}
			catch (Exception ex)
			{
				TryLog(logger, (LogLevel)4, "Audio-router cleanup failed during application shutdown: " + ex.GetType().Name + ": " + ex.Message);
			}
		}

		private static void TryDisposeConfiguration(RemoteVoiceConfiguration? configuration, ManualLogSource? logger)
		{
			try
			{
				configuration?.Dispose();
			}
			catch (Exception ex)
			{
				TryLog(logger, (LogLevel)4, "Configuration cleanup failed during application shutdown: " + ex.GetType().Name + ": " + ex.Message);
			}
		}

		private static void TryLog(ManualLogSource? logger, LogLevel level, string message)
		{
			//IL_0004: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (logger != null)
				{
					logger.Log(level, (object)message);
				}
			}
			catch
			{
			}
		}
	}
	internal sealed class RemoteVoiceConfiguration : IDisposable
	{
		private readonly ConfigEntry<bool> _enabledEntry;

		private readonly ConfigEntry<bool> _fallbackEntry;

		private readonly ManualLogSource _logger;

		private bool _disposed;

		public RemoteVoiceSettingsState State { get; }

		public RemoteVoiceConfiguration(ConfigEntry<bool> enabledEntry, ConfigEntry<bool> fallbackEntry, ManualLogSource logger)
		{
			_enabledEntry = enabledEntry ?? throw new ArgumentNullException("enabledEntry");
			_fallbackEntry = fallbackEntry ?? throw new ArgumentNullException("fallbackEntry");
			_logger = logger ?? throw new ArgumentNullException("logger");
			State = new RemoteVoiceSettingsState(enabledEntry.Value, fallbackEntry.Value);
			_enabledEntry.SettingChanged += OnEnabledSettingChanged;
			_fallbackEntry.SettingChanged += OnFallbackSettingChanged;
			State.Update(_enabledEntry.Value, _fallbackEntry.Value);
		}

		public void Dispose()
		{
			if (!_disposed)
			{
				_disposed = true;
				_enabledEntry.SettingChanged -= OnEnabledSettingChanged;
				_fallbackEntry.SettingChanged -= OnFallbackSettingChanged;
			}
		}

		private void OnEnabledSettingChanged(object sender, EventArgs args)
		{
			if (TryApply())
			{
				TryLog((LogLevel)16, State.Enabled ? "Remote Voice Split is enabled." : "Remote Voice Split is disabled; remote voices remain on the normal game output.");
			}
		}

		private void OnFallbackSettingChanged(object sender, EventArgs args)
		{
			if (TryApply())
			{
				TryLog((LogLevel)16, State.FallbackToGameOutput ? "Remote voice fallback to normal game output is enabled." : "Remote voice fallback to normal game output is disabled; unavailable remote voice will remain silent.");
			}
		}

		private bool TryApply()
		{
			try
			{
				State.Update(_enabledEntry.Value, _fallbackEntry.Value);
				return true;
			}
			catch (Exception ex)
			{
				TryLog((LogLevel)2, "Remote voice configuration could not be applied: " + ex.GetType().Name + ": " + ex.Message);
				return false;
			}
		}

		private void TryLog(LogLevel level, string message)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				_logger.Log(level, (object)message);
			}
			catch
			{
			}
		}
	}
	internal sealed class VoiceCaptureFilter : MonoBehaviour
	{
		private sealed class CaptureRegistration
		{
			public VoiceProcessRouter Router { get; }

			public VoiceCaptureStream Stream { get; }

			public RemoteVoiceSettingsState Settings { get; }

			public AtomicCommitLease CommitLease { get; } = new AtomicCommitLease();

			public CaptureRegistration(VoiceProcessRouter router, VoiceCaptureStream stream, RemoteVoiceSettingsState settings)
			{
				Router = router;
				Stream = stream;
				Settings = settings;
			}
		}

		private readonly AtomicRegistration<CaptureRegistration> _registration = new AtomicRegistration<CaptureRegistration>();

		public void Initialize(VoiceProcessRouter router, RemoteVoiceSettingsState settings)
		{
			CaptureRegistration captureRegistration = _registration.Read();
			if (captureRegistration != null && captureRegistration.Router == router && captureRegistration.Settings == settings)
			{
				((Behaviour)this).enabled = true;
				return;
			}
			Deactivate();
			CaptureRegistration value = new CaptureRegistration(router, router.RegisterCapture(), settings);
			_registration.Exchange(value);
			((Behaviour)this).enabled = true;
		}

		public void Deactivate()
		{
			((Behaviour)this).enabled = false;
			CaptureRegistration captureRegistration = _registration.Exchange(null);
			if (captureRegistration != null)
			{
				captureRegistration.CommitLease.Retire();
				captureRegistration.Router.UnregisterCapture(captureRegistration.Stream);
			}
		}

		private void OnAudioFilterRead(float[] data, int channels)
		{
			CaptureRegistration captureRegistration = _registration.Read();
			if (captureRegistration == null)
			{
				return;
			}
			bool enabled = captureRegistration.Settings.Enabled;
			if (!enabled)
			{
				captureRegistration.Stream.Clear();
				return;
			}
			using RoutingSubmissionLease routingSubmissionLease = captureRegistration.Router.TrySubmit(captureRegistration.Stream, data, channels);
			bool flag = routingSubmissionLease != null;
			if (!RemoteVoiceOutputPolicy.ShouldClearUnityOutput(enabled, flag, captureRegistration.Settings.FallbackToGameOutput))
			{
				return;
			}
			if (!flag)
			{
				captureRegistration.Stream.Clear();
			}
			if (!captureRegistration.CommitLease.TryBegin())
			{
				captureRegistration.Stream.Clear();
				return;
			}
			try
			{
				if (!_registration.IsCurrent(captureRegistration))
				{
					captureRegistration.Stream.Clear();
				}
				else
				{
					Array.Clear(data, 0, data.Length);
				}
			}
			finally
			{
				captureRegistration.CommitLease.End();
			}
		}

		private void OnDestroy()
		{
			Deactivate();
		}
	}
	internal static class VoicePlaybackIntegration
	{
		public static void Refresh(object startOfRound, VoiceProcessRouter router, RemoteVoiceSettingsState settings)
		{
			object value = GameReflection.LocalPlayerControllerField.GetValue(startOfRound);
			if (value == null || !(GameReflection.AllPlayerScriptsField.GetValue(startOfRound) is Array array))
			{
				return;
			}
			HashSet<AudioSource> hashSet = new HashSet<AudioSource>();
			foreach (object item in array)
			{
				if (item != null)
				{
					bool isLocalPlayer = IsSameUnityObject(item, value);
					bool isPlayerControlled = (bool)(GameReflection.IsPlayerControlledField.GetValue(item) ?? ((object)false));
					bool isPlayerDead = (bool)(GameReflection.IsPlayerDeadField.GetValue(item) ?? ((object)false));
					object? value2 = GameReflection.CurrentVoiceSourceField.GetValue(item);
					AudioSource val = (AudioSource)((value2 is AudioSource) ? value2 : null);
					if (RemoteVoiceSelectionPolicy.ShouldCapture(isLocalPlayer, isPlayerControlled, isPlayerDead, (Object)(object)val != (Object)null) && !((Object)(object)val == (Object)null))
					{
						hashSet.Add(val);
						(((Component)val).GetComponent<VoiceCaptureFilter>() ?? ((Component)val).gameObject.AddComponent<VoiceCaptureFilter>()).Initialize(router, settings);
					}
				}
			}
			VoiceCaptureFilter[] array2 = Object.FindObjectsOfType<VoiceCaptureFilter>(true);
			foreach (VoiceCaptureFilter voiceCaptureFilter in array2)
			{
				AudioSource component = ((Component)voiceCaptureFilter).GetComponent<AudioSource>();
				if ((Object)(object)component == (Object)null || !hashSet.Contains(component))
				{
					voiceCaptureFilter.Deactivate();
				}
			}
		}

		private static bool IsSameUnityObject(object left, object right)
		{
			if (left == right)
			{
				return true;
			}
			Object val = (Object)((left is Object) ? left : null);
			if (val != null)
			{
				Object val2 = (Object)((right is Object) ? right : null);
				if (val2 != null)
				{
					return val == val2;
				}
			}
			return false;
		}
	}
	[HarmonyPatch]
	internal static class RefreshPlayerVoicePlaybackObjectsPatch
	{
		private static MethodInfo TargetMethod()
		{
			return GameReflection.StartOfRoundRefreshMethod;
		}

		[HarmonyPostfix]
		private static void Postfix(object __instance)
		{
			IntegrationContext.RunGuarded("Remote voice source attachment", delegate
			{
				if (IntegrationContext.TryGetRouting(out VoiceRoutingContext routing) && routing != null)
				{
					VoicePlaybackIntegration.Refresh(__instance, routing.Router, routing.Settings);
				}
			});
		}
	}
}
namespace RemoteVoiceSplit.Core
{
	internal sealed class AtomicRegistration<T> where T : class
	{
		private T? _current;

		public T? Read()
		{
			return Volatile.Read(in _current);
		}

		public T? Exchange(T? value)
		{
			return Interlocked.Exchange(ref _current, value);
		}

		public bool IsCurrent(T value)
		{
			return Volatile.Read(in _current) == value;
		}
	}
	internal sealed class AtomicCommitLease
	{
		private const int Active = 0;

		private const int Committing = 1;

		private const int Retired = 2;

		private int _state;

		public bool TryBegin()
		{
			return Interlocked.CompareExchange(ref _state, 1, 0) == 0;
		}

		public void End()
		{
			if (Interlocked.CompareExchange(ref _state, 0, 1) != 1)
			{
				throw new InvalidOperationException("The commit lease is not active.");
			}
		}

		public void Retire()
		{
			SpinWait spinWait = default(SpinWait);
			while (Interlocked.CompareExchange(ref _state, 2, 0) != 0 && Volatile.Read(in _state) != 2)
			{
				spinWait.SpinOnce();
			}
		}
	}
	internal sealed class AtomicUsageLease
	{
		private const int RetiredMask = int.MinValue;

		private const int UsageMask = int.MaxValue;

		private int _state;

		public bool TryBegin()
		{
			int num;
			do
			{
				num = Volatile.Read(in _state);
				if ((num & int.MinValue) != 0)
				{
					return false;
				}
				if ((num & 0x7FFFFFFF) == int.MaxValue)
				{
					throw new InvalidOperationException("The usage lease is exhausted.");
				}
			}
			while (Interlocked.CompareExchange(ref _state, num + 1, num) != num);
			return true;
		}

		public void End()
		{
			if ((Interlocked.Decrement(ref _state) & 0x7FFFFFFF) == int.MaxValue)
			{
				throw new InvalidOperationException("The usage lease was released without a matching acquisition.");
			}
		}

		public void Retire()
		{
			int num;
			do
			{
				num = Volatile.Read(in _state);
			}
			while ((num & int.MinValue) == 0 && Interlocked.CompareExchange(ref _state, num | int.MinValue, num) != num);
			SpinWait spinWait = default(SpinWait);
			while ((Volatile.Read(in _state) & 0x7FFFFFFF) != 0)
			{
				spinWait.SpinOnce();
			}
		}
	}
	internal static class AudioHostProtocol
	{
		public const int Magic = 827610706;

		public const int Version = 1;

		public const int Channels = 2;

		public const int Ready = 1;

		public const int MaximumBlockSamples = 16384;

		public static void WriteClientHello(BinaryWriter writer, int sampleRate)
		{
			if (writer == null)
			{
				throw new ArgumentNullException("writer");
			}
			if (sampleRate <= 0)
			{
				throw new ArgumentOutOfRangeException("sampleRate");
			}
			writer.Write(827610706);
			writer.Write(1);
			writer.Write(sampleRate);
			writer.Write(2);
			writer.Flush();
		}

		public static int ReadClientHello(BinaryReader reader)
		{
			if (reader == null)
			{
				throw new ArgumentNullException("reader");
			}
			RequireEqual(827610706, reader.ReadInt32(), "magic");
			RequireEqual(1, reader.ReadInt32(), "version");
			int num = reader.ReadInt32();
			if (num <= 0)
			{
				throw new InvalidDataException("The audio host protocol sample rate is invalid.");
			}
			RequireEqual(2, reader.ReadInt32(), "channel count");
			return num;
		}

		public static void WriteServerReady(BinaryWriter writer, int processId)
		{
			if (writer == null)
			{
				throw new ArgumentNullException("writer");
			}
			if (processId <= 0)
			{
				throw new ArgumentOutOfRangeException("processId");
			}
			writer.Write(827610706);
			writer.Write(1);
			writer.Write(1);
			writer.Write(processId);
			writer.Flush();
		}

		public static int ReadServerReady(BinaryReader reader)
		{
			if (reader == null)
			{
				throw new ArgumentNullException("reader");
			}
			RequireEqual(827610706, reader.ReadInt32(), "magic");
			RequireEqual(1, reader.ReadInt32(), "version");
			RequireEqual(1, reader.ReadInt32(), "ready state");
			int num = reader.ReadInt32();
			if (num <= 0)
			{
				throw new InvalidDataException("The audio host process identifier is invalid.");
			}
			return num;
		}

		private static void RequireEqual(int expected, int actual, string field)
		{
			if (actual != expected)
			{
				throw new InvalidDataException("The audio host protocol " + field + " is incompatible.");
			}
		}
	}
	internal sealed class AudioRingBuffer
	{
		private readonly float[] _samples;

		private readonly int _mask;

		private int _readSequence;

		private int _writeSequence;

		public int CapacitySamples => _samples.Length;

		public int AvailableSamples
		{
			get
			{
				int num = Volatile.Read(in _writeSequence);
				int num2 = Volatile.Read(in _readSequence);
				uint num3 = (uint)(num - num2);
				if (num3 > _samples.Length)
				{
					return 0;
				}
				return (int)num3;
			}
		}

		public AudioRingBuffer(int capacitySamples)
		{
			if (capacitySamples < 2 || (capacitySamples & (capacitySamples - 1)) != 0)
			{
				throw new ArgumentOutOfRangeException("capacitySamples", "Capacity must be a power of two.");
			}
			_samples = new float[capacitySamples];
			_mask = capacitySamples - 1;
		}

		public bool TryWriteStereo(float[] source, int channels)
		{
			return TryWriteStereo(source, (source != null) ? source.Length : 0, channels);
		}

		public bool TryWriteStereo(float[] source, int sourceSampleCount, int channels)
		{
			if (source == null)
			{
				throw new ArgumentNullException("source");
			}
			if (sourceSampleCount < 0 || sourceSampleCount > source.Length || channels <= 0 || sourceSampleCount % channels != 0)
			{
				return false;
			}
			int num = sourceSampleCount / channels;
			int num2 = checked(num * 2);
			int writeSequence = _writeSequence;
			int num3 = Volatile.Read(in _readSequence);
			uint num4 = (uint)(writeSequence - num3);
			if (num4 > _samples.Length || num2 > _samples.Length - (int)num4)
			{
				return false;
			}
			int num5 = writeSequence & _mask;
			for (int i = 0; i < num; i++)
			{
				int num6 = i * channels;
				float num7 = source[num6];
				float num8 = ((channels == 1) ? num7 : source[num6 + 1]);
				_samples[num5] = num7;
				_samples[(num5 + 1) & _mask] = num8;
				num5 = (num5 + 2) & _mask;
			}
			Volatile.Write(ref _writeSequence, writeSequence + num2);
			return true;
		}

		public int Read(float[] destination, int sampleCount)
		{
			return Read(destination, sampleCount, null);
		}

		internal int Read(float[] destination, int sampleCount, Action? beforePublish)
		{
			if (destination == null)
			{
				throw new ArgumentNullException("destination");
			}
			if (sampleCount < 0 || sampleCount > destination.Length)
			{
				throw new ArgumentOutOfRangeException("sampleCount");
			}
			int num = Volatile.Read(in _readSequence);
			uint num2 = (uint)(Volatile.Read(in _writeSequence) - num);
			if (num2 > _samples.Length)
			{
				return 0;
			}
			int num3 = Math.Min(sampleCount, (int)num2);
			int num4 = num & _mask;
			int num5 = Math.Min(num3, _samples.Length - num4);
			Array.Copy(_samples, num4, destination, 0, num5);
			Array.Copy(_samples, 0, destination, num5, num3 - num5);
			beforePublish?.Invoke();
			if (Interlocked.CompareExchange(ref _readSequence, num + num3, num) != num)
			{
				Array.Clear(destination, 0, num3);
				return 0;
			}
			return num3;
		}

		public void Clear()
		{
			int value = Volatile.Read(in _writeSequence);
			Interlocked.Exchange(ref _readSequence, value);
		}
	}
	internal static class ProcessAncestry
	{
		public static bool IsSelfOrDescendant(int candidateProcessId, int ancestorProcessId, IReadOnlyDictionary<int, int> parentByProcessId)
		{
			if (candidateProcessId <= 0)
			{
				throw new ArgumentOutOfRangeException("candidateProcessId");
			}
			if (ancestorProcessId <= 0)
			{
				throw new ArgumentOutOfRangeException("ancestorProcessId");
			}
			if (parentByProcessId == null)
			{
				throw new ArgumentNullException("parentByProcessId");
			}
			int value = candidateProcessId;
			HashSet<int> hashSet = new HashSet<int>();
			while (value > 0 && hashSet.Add(value))
			{
				if (value == ancestorProcessId)
				{
					return true;
				}
				if (!parentByProcessId.TryGetValue(value, out value))
				{
					return false;
				}
			}
			return false;
		}
	}
	internal static class RemoteVoiceOutputPolicy
	{
		public const bool DefaultEnabled = true;

		public const bool DefaultFallbackToGameOutput = false;

		public static bool ShouldClearUnityOutput(bool enabled, bool submissionAccepted, bool fallbackToGameOutput)
		{
			if (enabled)
			{
				if (!submissionAccepted)
				{
					return !fallbackToGameOutput;
				}
				return true;
			}
			return false;
		}
	}
	internal sealed class RemoteVoiceSettingsState
	{
		private int _enabled;

		private int _fallbackToGameOutput;

		public bool Enabled => Volatile.Read(in _enabled) != 0;

		public bool FallbackToGameOutput => Volatile.Read(in _fallbackToGameOutput) != 0;

		public RemoteVoiceSettingsState(bool enabled, bool fallbackToGameOutput)
		{
			Update(enabled, fallbackToGameOutput);
		}

		public void Update(bool enabled, bool fallbackToGameOutput)
		{
			Volatile.Write(ref _enabled, enabled ? 1 : 0);
			Volatile.Write(ref _fallbackToGameOutput, fallbackToGameOutput ? 1 : 0);
		}
	}
	internal static class RemoteVoiceSelectionPolicy
	{
		public static bool ShouldCapture(bool isLocalPlayer, bool isPlayerControlled, bool isPlayerDead, bool hasVoiceSource)
		{
			return !isLocalPlayer && (isPlayerControlled || isPlayerDead) && hasVoiceSource;
		}
	}
	internal sealed class RoutingSessionGate
	{
		private sealed class RoutingSessionEpoch
		{
			public AtomicUsageLease Usage { get; } = new AtomicUsageLease();
		}

		private readonly object _transitionGate = new object();

		private readonly AtomicRegistration<RoutingSessionEpoch> _current = new AtomicRegistration<RoutingSessionEpoch>();

		public bool IsReady => _current.Read() != null;

		public void Activate()
		{
			lock (_transitionGate)
			{
				if (_current.Read() != null)
				{
					throw new InvalidOperationException("A routing session was already active.");
				}
				_current.Exchange(new RoutingSessionEpoch());
			}
		}

		public RoutingSubmissionLease? TryBeginSubmission()
		{
			RoutingSessionEpoch routingSessionEpoch = _current.Read();
			if (routingSessionEpoch == null || !routingSessionEpoch.Usage.TryBegin())
			{
				return null;
			}
			if (!_current.IsCurrent(routingSessionEpoch))
			{
				routingSessionEpoch.Usage.End();
				return null;
			}
			return new RoutingSubmissionLease(routingSessionEpoch.Usage);
		}

		public void Deactivate()
		{
			lock (_transitionGate)
			{
				_current.Exchange(null)?.Usage.Retire();
			}
		}
	}
	internal sealed class RoutingSubmissionLease : IDisposable
	{
		private AtomicUsageLease? _usage;

		public RoutingSubmissionLease(AtomicUsageLease usage)
		{
			_usage = usage;
		}

		public void Dispose()
		{
			Interlocked.Exchange(ref _usage, null)?.End();
		}
	}
	internal sealed class VoiceAudioMixer
	{
		private readonly object _gate = new object();

		private VoiceCaptureStream[] _streams = Array.Empty<VoiceCaptureStream>();

		private float[] _scratch = Array.Empty<float>();

		public VoiceCaptureStream Register(int capacitySamples)
		{
			VoiceCaptureStream voiceCaptureStream = new VoiceCaptureStream(capacitySamples);
			lock (_gate)
			{
				VoiceCaptureStream[] streams = _streams;
				VoiceCaptureStream[] array = new VoiceCaptureStream[streams.Length + 1];
				Array.Copy(streams, array, streams.Length);
				array[streams.Length] = voiceCaptureStream;
				Volatile.Write(ref _streams, array);
				return voiceCaptureStream;
			}
		}

		public void Unregister(VoiceCaptureStream stream)
		{
			if (stream == null)
			{
				return;
			}
			lock (_gate)
			{
				VoiceCaptureStream[] streams = _streams;
				int num = Array.IndexOf(streams, stream);
				if (num < 0)
				{
					return;
				}
				VoiceCaptureStream[] array = new VoiceCaptureStream[streams.Length - 1];
				Array.Copy(streams, 0, array, 0, num);
				Array.Copy(streams, num + 1, array, num, streams.Length - num - 1);
				Volatile.Write(ref _streams, array);
			}
			stream.Clear();
		}

		public bool Mix(float[] destination, int sampleCount)
		{
			if (destination == null)
			{
				throw new ArgumentNullException("destination");
			}
			if (sampleCount < 0 || sampleCount > destination.Length || (sampleCount & 1) != 0)
			{
				throw new ArgumentOutOfRangeException("sampleCount");
			}
			Array.Clear(destination, 0, sampleCount);
			EnsureScratchCapacity(sampleCount);
			bool flag = false;
			VoiceCaptureStream[] array = Volatile.Read(in _streams);
			for (int i = 0; i < array.Length; i++)
			{
				int num = array[i].Read(_scratch, sampleCount);
				flag = flag || num > 0;
				for (int j = 0; j < num; j++)
				{
					destination[j] += _scratch[j];
				}
			}
			if (flag)
			{
				for (int k = 0; k < sampleCount; k++)
				{
					destination[k] = Math.Max(-1f, Math.Min(1f, destination[k]));
				}
			}
			return flag;
		}

		public void Clear()
		{
			VoiceCaptureStream[] array = Volatile.Read(in _streams);
			for (int i = 0; i < array.Length; i++)
			{
				array[i].Clear();
			}
		}

		private void EnsureScratchCapacity(int sampleCount)
		{
			if (_scratch.Length < sampleCount)
			{
				_scratch = new float[sampleCount];
			}
		}
	}
	internal sealed class VoiceCaptureStream
	{
		private readonly AudioRingBuffer _buffer;

		public VoiceCaptureStream(int capacitySamples)
		{
			_buffer = new AudioRingBuffer(capacitySamples);
		}

		public bool TryWrite(float[] samples, int channels)
		{
			return _buffer.TryWriteStereo(samples, channels);
		}

		public int Read(float[] destination, int sampleCount)
		{
			int num = _buffer.Read(destination, sampleCount);
			if ((num & 1) != 0)
			{
				throw new InvalidOperationException("A stereo stream returned a partial frame.");
			}
			return num;
		}

		public void Clear()
		{
			_buffer.Clear();
		}
	}
}