using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Threading; using BepInEx; using BepInEx.Configuration; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using OpenTK; using OpenTK.Audio.OpenAL; using SSMP.Api.Client; using SSMP.Api.Client.Networking; using SSMP.Api.Command; using SSMP.Api.Command.Client; using SSMP.Api.Command.Server; using SSMP.Api.Server; using SSMP.Api.Server.Networking; using SSMP.Game; using SSMP.Game.Settings; using SSMP.Logging; using SSMP.Math; using SSMP.Networking.Packet; using SSMP.Networking.Packet.Data; using SsmpVoiceChat.Client; using SsmpVoiceChat.Client.Voice; using SsmpVoiceChat.Common; using SsmpVoiceChat.Common.Command; using SsmpVoiceChat.Common.Net; using SsmpVoiceChat.Common.Opus; using SsmpVoiceChat.Common.RNNoise; using SsmpVoiceChat.Common.WebRtcVad; using SsmpVoiceChat.Server; using TMProOld; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: Guid("0B95F3A6-6628-4FF5-8574-3CC58419572D")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SSMPVoiceChat")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.3.0")] [assembly: AssemblyInformationalVersion("0.1.3+7445a96e9285a8f0fde3d69c167c83d20ab275f9")] [assembly: AssemblyProduct("SSMPVoiceChat")] [assembly: AssemblyTitle("SSMPVoiceChat")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/BobbyTheCatfish/SSMP.VoiceChat")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.3.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.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 BepInEx { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] internal sealed class BepInAutoPluginAttribute : Attribute { public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace BepInEx.Preloader.Core.Patching { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] internal sealed class PatcherAutoPluginAttribute : Attribute { public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace SsmpVoiceChat { public static class Identifier { public const string AddonName = "ProximityChat"; public const string AddonVersion = "0.1.3"; public const uint ApiVersion = 1u; } } namespace SsmpVoiceChat.Server { public class ServerNetManager { private readonly IServerAddonNetworkSender _netSender; public event Action VoiceEvent; public ServerNetManager(ServerAddon addon, INetServer netServer) { _netSender = netServer.GetNetworkSender(addon); netServer.GetNetworkReceiver(addon, (Func)InstantiatePacket).RegisterPacketHandler(ServerPacketId.Voice, (GenericServerPacketHandler)delegate(ushort id, ServerVoicePacket packet) { this.VoiceEvent?.Invoke(id, packet.VoiceData); }); } public void SendVoiceData(ushort receiver, ushort sender, byte[] data, bool proximity) { if (data.Length > 65535) { ServerVoiceChat.Logger.Info("Voice data exceeds max size!"); return; } _netSender.SendCollectionData(ClientPacketId.Voice, new ClientVoicePacket { Id = sender, Proximity = proximity, VoiceData = data }, receiver); } private static IPacketData InstantiatePacket(ServerPacketId packetId) { if (packetId == ServerPacketId.Voice) { return (IPacketData)(object)new PacketDataCollection(); } return null; } } public class ServerSettings { private const string FileName = "voicechat_server_settings.json"; [JsonProperty("proximity_based_volume")] [SettingAlias(new string[] { "proximity", "prox" })] public bool ProximityBasedVolume { get; set; } = true; [JsonProperty("team_voices_globally")] [SettingAlias(new string[] { "teamglobal", "teamglobally" })] public bool TeamVoicesGlobally { get; set; } = true; [JsonProperty("team_voices_only")] [SettingAlias(new string[] { "teamonly" })] public bool TeamVoicesOnly { get; set; } public void SaveToFile() { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (directoryName == null) { return; } string path = Path.Combine(directoryName, "voicechat_server_settings.json"); string contents = JsonConvert.SerializeObject((object)this, (Formatting)1); try { File.WriteAllText(path, contents); } catch (Exception arg) { ServerVoiceChat.Logger.Error($"Could not write server settings to file:\n{arg}"); } } public static ServerSettings LoadFromFile() { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (directoryName == null) { return new ServerSettings(); } string path = Path.Combine(directoryName, "voicechat_server_settings.json"); if (!File.Exists(path)) { ServerSettings serverSettings = new ServerSettings(); serverSettings.SaveToFile(); return serverSettings; } try { return JsonConvert.DeserializeObject(File.ReadAllText(path)) ?? new ServerSettings(); } catch (Exception arg) { ServerVoiceChat.Logger.Error($"Could not load server settings from file:\n{arg}"); return new ServerSettings(); } } } public class ServerVoiceChat { private readonly IServerApi _serverApi; private readonly ServerNetManager _netManager; private readonly ServerSettings _settings; private readonly HashSet _broadcasters; public static ILogger Logger { get; private set; } public ServerVoiceChat(ServerAddon addon, IServerApi serverApi, ILogger logger) { Logger = logger; _serverApi = serverApi; _netManager = new ServerNetManager(addon, serverApi.NetServer); _settings = ServerSettings.LoadFromFile(); _broadcasters = new HashSet(); } public void Initialize() { ((ICommandManager)(object)_serverApi.CommandManager).RegisterCommand((IServerCommand)(object)new ServerVoiceChatCommand(_settings, _broadcasters)); _netManager.VoiceEvent += OnVoice; } private void OnVoice(ushort id, byte[] data) { //IL_0031: 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) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Invalid comparison between Unknown and I4 IServerPlayer val = default(IServerPlayer); if (!_serverApi.ServerManager.TryGetPlayer(id, ref val)) { Logger.Warn($"Could not find player '{id}' for received voice data"); return; } Team team = val.Team; foreach (IServerPlayer player in _serverApi.ServerManager.Players) { if (val == player) { continue; } if (_broadcasters.Contains(val.Id)) { _netManager.SendVoiceData(player.Id, val.Id, data, proximity: false); continue; } bool flag = team == player.Team && (int)team > 0; if (!_settings.TeamVoicesOnly || flag) { if (_settings.TeamVoicesGlobally && flag) { _netManager.SendVoiceData(player.Id, val.Id, data, proximity: false); } else if (!(val.CurrentScene != player.CurrentScene)) { _netManager.SendVoiceData(player.Id, val.Id, data, _settings.ProximityBasedVolume); } } } } } public class ServerVoiceChatCommand : IServerCommand, ICommand { private readonly ServerSettings _settings; private readonly HashSet _broadcasters; public string Trigger => "/voicechatserver"; public string[] Aliases => new string[1] { "/vcs" }; public bool AuthorizedOnly => true; public ServerVoiceChatCommand(ServerSettings settings, HashSet broadcasters) { _settings = settings; _broadcasters = broadcasters; } public void Execute(ICommandSender commandSender, string[] args) { ICommandSender commandSender2 = commandSender; if (args.Length < 2) { SendUsage(); return; } string text = args[1]; if (text == "set") { HandleSet(commandSender2, args); } else if (text == "broadcast") { HandleBroadcast(commandSender2); } else { SendUsage(); } void SendUsage() { commandSender2.SendMessage("Invalid usage: " + Trigger + " "); } } private void HandleSet(ICommandSender commandSender, string[] args) { CommandUtil.HandleSetCommand(Trigger, args, _settings, (Action)commandSender.SendMessage, (Action)delegate { _settings.SaveToFile(); }, requireSettingAliasAttribute: false); } private void HandleBroadcast(ICommandSender commandSender) { IPlayerCommandSender val = (IPlayerCommandSender)(object)((commandSender is IPlayerCommandSender) ? commandSender : null); if (val == null) { commandSender.SendMessage("Cannot execute this command as a non-player"); return; } ushort id = val.Id; if (_broadcasters.Contains(id)) { _broadcasters.Remove(id); ((ICommandSender)val).SendMessage("You are no longer broadcasting your voice"); } else { _broadcasters.Add(id); ((ICommandSender)val).SendMessage("You are now broadcasting your voice"); } } } public class VoiceChatServerAddon : ServerAddon { protected override string Name => "ProximityChat"; protected override string Version => "0.1.3"; public override uint ApiVersion => 1u; public override bool NeedsNetwork => true; public override void Initialize(IServerApi serverApi) { new ServerVoiceChat((ServerAddon)(object)this, serverApi, ((ServerAddon)this).Logger).Initialize(); } } } namespace SsmpVoiceChat.Common { public static class DataUtils { private const float FloatShortScale = 32767f; private const float FloatClip = 32766f; private const float FloatShortScalingFactor = 3.051851E-05f; public static short[] FloatsToShortsNormalized(float[] audioData) { short[] array = new short[audioData.Length]; for (int i = 0; i < audioData.Length; i++) { array[i] = (short)Math.Max(Math.Min(audioData[i] * 32767f, 32766f), -32767f); } return array; } public static byte[] ShortsToBytes(short[] shorts) { byte[] array = new byte[shorts.Length * 2]; for (int i = 0; i < shorts.Length; i++) { short num = shorts[i]; array[i * 2] = (byte)((uint)num & 0xFFu); array[i * 2 + 1] = (byte)((uint)(num >> 8) & 0xFFu); } return array; } public static short[] BytesToShorts(byte[] bytes) { if (bytes.Length % 2 != 0) { throw new ArgumentException("Byte array length must be even", "bytes"); } short[] array = new short[bytes.Length / 2]; for (int i = 0; i < bytes.Length; i += 2) { byte b = bytes[i]; byte b2 = bytes[i + 1]; array[i / 2] = (short)(((b2 & 0xFF) << 8) | (b & 0xFF)); } return array; } } public static class LibraryLoader { private static readonly List Libraries = new List(); [DllImport("kernel32", CharSet = CharSet.Ansi, SetLastError = true)] private static extern IntPtr LoadLibrary(string lpFileName); [DllImport("kernel32", CharSet = CharSet.Ansi, SetLastError = true)] private static extern bool FreeLibrary(IntPtr module); [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("dl.so.2", CharSet = CharSet.Ansi)] private static extern IntPtr dlopen(string filename, int flags); [DllImport("dl.so.2", CharSet = CharSet.Ansi)] private static extern void dlclose(IntPtr module); [DllImport("dl.so.2", CharSet = CharSet.Ansi)] private static extern IntPtr dlsym(IntPtr handle, string symbol); public static void UnloadAll() { foreach (IntPtr library in Libraries) { Free(library); } } internal static IntPtr Load(string fileName) { IntPtr intPtr = (PlatformDetails.IsWindows ? LoadLibrary(fileName) : dlopen(fileName, 1)); Libraries.Add(intPtr); return intPtr; } internal static bool Free(IntPtr module) { if (PlatformDetails.IsWindows) { return FreeLibrary(module); } dlclose(module); return true; } internal static IntPtr ResolveSymbol(IntPtr image, string symbol) { if (!PlatformDetails.IsWindows) { return dlsym(image, symbol); } return GetProcAddress(image, symbol); } } public static class PlatformDetails { public static bool IsMac { get; private set; } public static bool IsWindows { get; private set; } static PlatformDetails() { if (Directory.Exists("/Applications") && Directory.Exists("/System") && Directory.Exists("/Users") && Directory.Exists("/Volumes")) { IsMac = true; } if (Environment.OSVersion.Platform == PlatformID.Win32NT || Environment.OSVersion.Platform == PlatformID.Win32Windows) { IsWindows = true; } } } } namespace SsmpVoiceChat.Common.WebRtcVad { public class NativeMethods { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr Vad_Create_delegate(); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int Vad_Init_delegate(IntPtr vadInst); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int Vad_SetMode_delegate(IntPtr vadInst, int mode); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int Vad_ValidRateAndFrameLength_delegate(int rate, UIntPtr frameLength); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int Vad_Process_delegate(IntPtr vadInst, int fs, IntPtr audioFrame, UIntPtr frameLength); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void Vad_Free_delegate(IntPtr vadInst); internal static Vad_Create_delegate Vad_Create; internal static Vad_Init_delegate Vad_Init; internal static Vad_SetMode_delegate Vad_SetMode; internal static Vad_ValidRateAndFrameLength_delegate Vad_ValidRateAndFrameLength; internal static Vad_Process_delegate Vad_Process; internal static Vad_Free_delegate Vad_Free; static NativeMethods() { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (directoryName == null) { ClientVoiceChat.Logger.Error("Could not get path of executing assembly, cannot initialize NativeMethods for Web RTC VAD"); return; } IntPtr intPtr = (PlatformDetails.IsMac ? LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Mac", "libwebrtcvad.dylib")) : ((!PlatformDetails.IsWindows) ? LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Linux", "libwebrtcvad.so")) : LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Windows", "webrtcvad.dll")))); if (!(intPtr != IntPtr.Zero)) { return; } FieldInfo[] fields = typeof(NativeMethods).GetFields(BindingFlags.Static | BindingFlags.NonPublic); foreach (FieldInfo obj in fields) { string name = obj.Name; Type fieldType = obj.FieldType; IntPtr intPtr2 = LibraryLoader.ResolveSymbol(intPtr, name); if (intPtr2 == IntPtr.Zero) { throw new Exception("Could not resolve symbol \"" + name + "\""); } obj.SetValue(null, Marshal.GetDelegateForFunctionPointer(intPtr2, fieldType)); } } } public enum OperatingMode { HighQuality, LowBitrate, Aggressive, VeryAggressive } public class WebRtcVad : IDisposable { private IntPtr _handle; private int _sampleRate; private int _frameLength; private OperatingMode _mode; private bool _disposed; public int SampleRate { get { return _sampleRate; } set { if (!ValidateRateAndFrameLength(value, _frameLength)) { throw new InvalidOperationException("Invalid sample rate"); } _sampleRate = value; } } public int FrameLength { get { return _frameLength; } set { if (!ValidateRateAndFrameLength(_sampleRate, value)) { throw new InvalidOperationException("Invalid frame length"); } _frameLength = value; } } public OperatingMode OperatingMode { get { return _mode; } set { if (NativeMethods.Vad_SetMode(_handle, (int)value) != 0) { throw new InvalidOperationException("Invalid operating mode specified"); } _mode = value; } } public WebRtcVad() { _sampleRate = 48000; _frameLength = 20; _mode = OperatingMode.HighQuality; _handle = NativeMethods.Vad_Create(); if (NativeMethods.Vad_Init(_handle) != 0) { throw new InvalidOperationException("Could not initialize WebRtcVad"); } } public bool HasSpeech(short[] audioFrame) { return HasSpeech(audioFrame, _sampleRate, _frameLength); } private unsafe bool HasSpeech(short[] audioFrame, int sampleRate, int frameLength) { int num = CalculateSamples(sampleRate, frameLength); int num2; fixed (short* ptr = audioFrame) { num2 = NativeMethods.Vad_Process(_handle, sampleRate, (IntPtr)ptr, (UIntPtr)(ulong)num); } return num2 == 1; } private bool ValidateRateAndFrameLength(int sampleRate, int frameLength) { int num = CalculateSamples(sampleRate, frameLength); return NativeMethods.Vad_ValidRateAndFrameLength(sampleRate, (UIntPtr)(ulong)num) == 0; } private static int CalculateSamples(int sampleRate, int frameLength) { return sampleRate / 1000 * frameLength; } public void Dispose() { if (!_disposed) { if (_handle != IntPtr.Zero) { NativeMethods.Vad_Free(_handle); _handle = IntPtr.Zero; } _disposed = true; } } } } namespace SsmpVoiceChat.Common.RNNoise { public class NativeMethods { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int rnnoise_get_frame_size_delegate(); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr rnnoise_create_delegate(IntPtr model); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr rnnoise_destroy_delegate(IntPtr denoiseState); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr rnnoise_process_frame_delegate(IntPtr denoiseState, float[] processed, float[] input); internal static rnnoise_get_frame_size_delegate rnnoise_get_frame_size; internal static rnnoise_create_delegate rnnoise_create; internal static rnnoise_destroy_delegate rnnoise_destroy; internal static rnnoise_process_frame_delegate rnnoise_process_frame; static NativeMethods() { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (directoryName == null) { ClientVoiceChat.Logger.Error("Could not get path of executing assembly, cannot initialize NativeMethods for RNNoise"); return; } IntPtr intPtr = (PlatformDetails.IsMac ? LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Mac", "librnnoise.dylib")) : ((!PlatformDetails.IsWindows) ? LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Linux", "librnnoise.so")) : LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Windows", "rnnoise.dll")))); if (intPtr != IntPtr.Zero) { ClientVoiceChat.Logger.Info("RNNoise library loaded"); FieldInfo[] fields = typeof(NativeMethods).GetFields(BindingFlags.Static | BindingFlags.NonPublic); foreach (FieldInfo obj in fields) { string name = obj.Name; Type fieldType = obj.FieldType; IntPtr intPtr2 = LibraryLoader.ResolveSymbol(intPtr, name); if (intPtr2 == IntPtr.Zero) { throw new Exception("Could not resolve symbol \"" + name + "\""); } obj.SetValue(null, Marshal.GetDelegateForFunctionPointer(intPtr2, fieldType)); } } else { ClientVoiceChat.Logger.Error("RNNoise library could not be loaded"); } } } public class RNNoise : IDisposable { private IntPtr _handle; private bool _disposed; public RNNoise() { _handle = NativeMethods.rnnoise_create(IntPtr.Zero); } public short[] ProcessFrame(short[] data) { int num = NativeMethods.rnnoise_get_frame_size(); float[] array = new float[data.Length]; for (int i = 0; i < data.Length; i++) { array[i] = data[i]; } float[] array2 = new float[array.Length]; for (int j = 0; j < array.Length; j += num) { float[] array3 = new float[num]; for (int k = 0; k < num; k++) { array3[k] = array[j + k]; } float[] array4 = new float[num]; NativeMethods.rnnoise_process_frame(_handle, array4, array3); for (int l = 0; l < num; l++) { array2[j + l] = array4[l]; } } float num2 = float.MinValue; float num3 = float.MaxValue; float[] array5 = array2; foreach (float num4 in array5) { if (num4 > num2) { num2 = num4; } if (num4 < num3) { num3 = num4; } } float num5 = Math.Min(1f, 32766f / Math.Max(Math.Abs(num2), Math.Abs(num3))); short[] array6 = new short[array2.Length]; for (int n = 0; n < array2.Length; n++) { array6[n] = (short)(array2[n] * num5); } return array6; } public void Dispose() { if (!_disposed) { if (_handle != IntPtr.Zero) { NativeMethods.rnnoise_destroy(_handle); _handle = IntPtr.Zero; } _disposed = true; } } } } namespace SsmpVoiceChat.Common.Opus { public enum Application { Voip = 2048, Audio = 2049, RestrictedLowLatency = 2051 } public static class Constants { public const int DefaultAudioSampleRate = 48000; public const byte DefaultAudioSampleBits = 16; public const byte DefaultAudioSampleChannels = 1; public const ushort DefaultAudioFrameSize = 960; } internal class NativeMethods { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr opus_encoder_create_delegate(int sampleRate, int channelCount, int application, out IntPtr error); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void opus_encoder_destroy_delegate(IntPtr encoder); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int opus_encode_delegate(IntPtr encoder, IntPtr pcm, int frameSize, IntPtr data, int maxDataBytes); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr opus_decoder_create_delegate(int sampleRate, int channelCount, out IntPtr error); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void opus_decoder_destroy_delegate(IntPtr decoder); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int opus_decode_delegate(IntPtr decoder, IntPtr data, int len, IntPtr pcm, int frameSize, int decodeFec); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int opus_packet_get_nb_channels_delegate(IntPtr data); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int opus_packet_get_nb_samples_delegate(IntPtr data, int len, int sampleRate); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int opus_encoder_ctl_delegate(IntPtr encoder, Ctl request, int value); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int opus_encoder_ctl_out_delegate(IntPtr encoder, Ctl request, out int value); public enum Ctl { SetBitrateRequest = 4002, GetBitrateRequest = 4003, SetInbandFecRequest = 4012, GetInbandFecRequest = 4013 } public enum OpusErrors { Ok = 0, BadArgument = -1, BufferToSmall = -2, InternalError = -3, InvalidPacket = -4, NotImplemented = -5, InvalidState = -6, AllocFail = -7 } internal static opus_encoder_create_delegate opus_encoder_create; internal static opus_encoder_destroy_delegate opus_encoder_destroy; internal static opus_encode_delegate opus_encode; internal static opus_decoder_create_delegate opus_decoder_create; internal static opus_decoder_destroy_delegate opus_decoder_destroy; internal static opus_decode_delegate opus_decode; internal static opus_packet_get_nb_channels_delegate opus_packet_get_nb_channels; internal static opus_packet_get_nb_samples_delegate opus_packet_get_nb_samples; internal static opus_encoder_ctl_delegate opus_encoder_ctl; internal static opus_encoder_ctl_out_delegate opus_encoder_ctl_out; static NativeMethods() { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (directoryName == null) { ClientVoiceChat.Logger.Error("Could not get path of executing assembly, cannot initialize NativeMethods for Opus"); return; } IntPtr intPtr; if (PlatformDetails.IsMac) { intPtr = LibraryLoader.Load("libopus.dylib"); if (intPtr.Equals((object?)(nint)IntPtr.Zero)) { intPtr = LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Mac", "libopus.dylib")); } } else if (PlatformDetails.IsWindows) { intPtr = LibraryLoader.Load("opus.dll"); if (intPtr.Equals((object?)(nint)IntPtr.Zero)) { intPtr = LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Windows", "opus.dll")); } } else { intPtr = LibraryLoader.Load("libopus.so.0"); if (intPtr.Equals((object?)(nint)IntPtr.Zero)) { intPtr = LibraryLoader.Load(Path.Combine(directoryName, "Natives", "Linux", "libopus.so.0")); } } if (!(intPtr != IntPtr.Zero)) { return; } FieldInfo[] fields = typeof(NativeMethods).GetFields(BindingFlags.Static | BindingFlags.NonPublic); foreach (FieldInfo obj in fields) { string text = obj.Name; if (text == "opus_encoder_ctl_out") { text = "opus_encoder_ctl"; } Type fieldType = obj.FieldType; IntPtr intPtr2 = LibraryLoader.ResolveSymbol(intPtr, text); if (intPtr2 == IntPtr.Zero) { throw new Exception("Could not resolve symbol \"" + text + "\""); } obj.SetValue(null, Marshal.GetDelegateForFunctionPointer(intPtr2, fieldType)); } } } public class OpusCodec { private readonly OpusDecoder _decoder; private readonly OpusEncoder _encoder; private readonly int _sampleRate; private readonly ushort _frameSize; public OpusCodec(int sampleRate = 48000, byte channels = 1, ushort frameSize = 960) { _sampleRate = sampleRate; _frameSize = frameSize; _decoder = new OpusDecoder(sampleRate, channels) { EnableForwardErrorCorrection = true }; _encoder = new OpusEncoder(sampleRate, channels) { EnableForwardErrorCorrection = true }; } public byte[] Decode(byte[] encodedData) { if (encodedData == null) { _decoder.Decode(null, 0, 0, new byte[_sampleRate / _frameSize], 0); return null; } int samples = OpusDecoder.GetSamples(encodedData, 0, encodedData.Length, _sampleRate); if (samples < 1) { return null; } byte[] array = new byte[samples * 2]; int num = _decoder.Decode(encodedData, 0, encodedData.Length, array, 0); if (array.Length != num) { Array.Resize(ref array, num); } return array; } public byte[] Encode(byte[] data) { int num = data.Length / 2; byte[] array = new byte[_encoder.FrameSizeInBytes(num)]; int newSize = _encoder.Encode(data, 0, array, 0, num); Array.Resize(ref array, newSize); return array; } } public class OpusDecoder : IDisposable { private IntPtr _decoder; private readonly int _sampleSize; public bool EnableForwardErrorCorrection { get; set; } public OpusDecoder(int outputSampleRate, int outputChannelCount) { if (outputSampleRate != 8000 && outputSampleRate != 12000 && outputSampleRate != 16000 && outputSampleRate != 24000 && outputSampleRate != 48000) { throw new ArgumentOutOfRangeException("outputSampleRate"); } if (outputChannelCount != 1 && outputChannelCount != 2) { throw new ArgumentOutOfRangeException("outputChannelCount"); } _decoder = NativeMethods.opus_decoder_create(outputSampleRate, outputChannelCount, out var error); if ((int)error != 0) { throw new Exception($"Exception occured while creating decoder, {(NativeMethods.OpusErrors)(int)error}"); } _sampleSize = 2 * outputChannelCount; } ~OpusDecoder() { Dispose(); } public void Dispose() { if (_decoder != IntPtr.Zero) { NativeMethods.opus_decoder_destroy(_decoder); _decoder = IntPtr.Zero; } } public unsafe int Decode(byte[] srcEncodedBuffer, int srcOffset, int srcLength, byte[] dstBuffer, int dstOffset) { int frameSize = (dstBuffer.Length - dstOffset) / _sampleSize; int num; fixed (byte* value = dstBuffer) { IntPtr pcm = IntPtr.Add(new IntPtr(value), dstOffset); if (srcEncodedBuffer != null) { fixed (byte* value2 = srcEncodedBuffer) { IntPtr data = IntPtr.Add(new IntPtr(value2), srcOffset); num = NativeMethods.opus_decode(_decoder, data, srcLength, pcm, frameSize, 0); } } else { num = NativeMethods.opus_decode(_decoder, IntPtr.Zero, 0, pcm, frameSize, Convert.ToInt32(EnableForwardErrorCorrection)); } } if (num < 0) { NativeMethods.OpusErrors opusErrors = (NativeMethods.OpusErrors)num; throw new Exception("Decoding failed - " + opusErrors); } return num * _sampleSize; } public unsafe static int GetSamples(byte[] srcEncodedBuffer, int srcOffset, int srcLength, int sampleRate) { fixed (byte* value = srcEncodedBuffer) { IntPtr data = IntPtr.Add(new IntPtr(value), srcOffset); return NativeMethods.opus_packet_get_nb_samples(data, srcLength, sampleRate); } } public unsafe static int GetChannels(byte[] srcEncodedBuffer, int srcOffset) { fixed (byte* value = srcEncodedBuffer) { IntPtr data = IntPtr.Add(new IntPtr(value), srcOffset); return NativeMethods.opus_packet_get_nb_channels(data); } } } public class OpusEncoder : IDisposable { private IntPtr _encoder; private readonly int _sampleSize; private readonly float[] _permittedFrameSizesInMilliSec = new float[6] { 2.5f, 5f, 10f, 20f, 40f, 60f }; public int[] PermittedFrameSizes { get; } public int Bitrate { get { if (_encoder == IntPtr.Zero) { throw new ObjectDisposedException("OpusEncoder"); } int value; int num = NativeMethods.opus_encoder_ctl_out(_encoder, NativeMethods.Ctl.GetBitrateRequest, out value); if (num < 0) { NativeMethods.OpusErrors opusErrors = (NativeMethods.OpusErrors)num; throw new Exception("Encoder error - " + opusErrors); } return value; } set { if (_encoder == IntPtr.Zero) { throw new ObjectDisposedException("OpusEncoder"); } int num = NativeMethods.opus_encoder_ctl(_encoder, NativeMethods.Ctl.SetBitrateRequest, value); if (num < 0) { NativeMethods.OpusErrors opusErrors = (NativeMethods.OpusErrors)num; throw new Exception("Encoder error - " + opusErrors); } } } public bool EnableForwardErrorCorrection { get { if (_encoder == IntPtr.Zero) { throw new ObjectDisposedException("OpusEncoder"); } int value; int num = NativeMethods.opus_encoder_ctl_out(_encoder, NativeMethods.Ctl.GetInbandFecRequest, out value); if (num < 0) { NativeMethods.OpusErrors opusErrors = (NativeMethods.OpusErrors)num; throw new Exception("Encoder error - " + opusErrors); } return value > 0; } set { if (_encoder == IntPtr.Zero) { throw new ObjectDisposedException("OpusEncoder"); } int num = NativeMethods.opus_encoder_ctl(_encoder, NativeMethods.Ctl.SetInbandFecRequest, Convert.ToInt32(value)); if (num < 0) { NativeMethods.OpusErrors opusErrors = (NativeMethods.OpusErrors)num; throw new Exception("Encoder error - " + opusErrors); } } } public OpusEncoder(int srcSamplingRate, int srcChannelCount) { if (srcSamplingRate != 8000 && srcSamplingRate != 12000 && srcSamplingRate != 16000 && srcSamplingRate != 24000 && srcSamplingRate != 48000) { throw new ArgumentOutOfRangeException("srcSamplingRate"); } if (srcChannelCount != 1 && srcChannelCount != 2) { throw new ArgumentOutOfRangeException("srcChannelCount"); } IntPtr error; IntPtr encoder = NativeMethods.opus_encoder_create(srcSamplingRate, srcChannelCount, 2048, out error); if ((int)error != 0) { throw new Exception("Exception occured while creating encoder"); } _encoder = encoder; _sampleSize = SampleSize(16, srcChannelCount); PermittedFrameSizes = new int[_permittedFrameSizesInMilliSec.Length]; for (int i = 0; i < _permittedFrameSizesInMilliSec.Length; i++) { PermittedFrameSizes[i] = (int)((float)srcSamplingRate / 1000f * _permittedFrameSizesInMilliSec[i]); } } private static int SampleSize(int bitDepth, int channelCount) { return bitDepth / 8 * channelCount; } ~OpusEncoder() { Dispose(); } public unsafe int Encode(byte[] srcPcmSamples, int srcOffset, byte[] dstOutputBuffer, int dstOffset, int sampleCount) { if (srcPcmSamples == null) { throw new ArgumentNullException("srcPcmSamples"); } if (dstOutputBuffer == null) { throw new ArgumentNullException("dstOutputBuffer"); } if (!PermittedFrameSizes.Contains(sampleCount)) { throw new Exception("Frame size is not permitted"); } int num = _sampleSize * sampleCount; if (srcOffset + num > srcPcmSamples.Length) { throw new Exception("Not enough samples in source"); } int maxDataBytes = dstOutputBuffer.Length - dstOffset; int num2; fixed (byte* value = dstOutputBuffer) { fixed (byte* value2 = srcPcmSamples) { IntPtr data = IntPtr.Add(new IntPtr(value), dstOffset); IntPtr pcm = IntPtr.Add(new IntPtr(value2), srcOffset); num2 = NativeMethods.opus_encode(_encoder, pcm, sampleCount, data, maxDataBytes); } } if (num2 < 0) { NativeMethods.OpusErrors opusErrors = (NativeMethods.OpusErrors)num2; throw new Exception("Encoding failed - " + opusErrors); } return num2; } public int FrameSizeInBytes(int frameSizeInSamples) { return frameSizeInSamples * _sampleSize; } public void Dispose() { if (_encoder != IntPtr.Zero) { NativeMethods.opus_encoder_destroy(_encoder); _encoder = IntPtr.Zero; } } } } namespace SsmpVoiceChat.Common.Net { public class ServerVoicePacket : IPacketData { public const ushort MaxSize = ushort.MaxValue; public byte[] VoiceData { get; set; } public bool IsReliable => false; public bool DropReliableDataIfNewerExists => false; public virtual void WriteData(IPacket packet) { if (VoiceData.Length > 65535) { throw new InvalidOperationException($"Voice data exceeds maximum size of {ushort.MaxValue} bytes"); } ushort num = (ushort)VoiceData.Length; packet.Write(num); for (int i = 0; i < num; i++) { packet.Write(VoiceData[i]); } } public virtual void ReadData(IPacket packet) { ushort num = packet.ReadUShort(); VoiceData = new byte[num]; for (int i = 0; i < num; i++) { VoiceData[i] = packet.ReadByte(); } } } public class ClientVoicePacket : ServerVoicePacket { public ushort Id { get; set; } public bool Proximity { get; set; } public override void WriteData(IPacket packet) { packet.Write(Id); packet.Write(Proximity); base.WriteData(packet); } public override void ReadData(IPacket packet) { Id = packet.ReadUShort(); Proximity = packet.ReadBool(); base.ReadData(packet); } } public enum ServerPacketId { Voice } public enum ClientPacketId { Voice } } namespace SsmpVoiceChat.Common.Command { public static class CommandUtil { public static void HandleSetCommand(string trigger, string[] args, TSettings settings, Action feedbackAction, Action successAction = null, bool requireSettingAliasAttribute = false) { PropertyInfo[] properties = typeof(TSettings).GetProperties(); if (args.Length < 3) { feedbackAction?.Invoke("Available settings: " + string.Join(", ", properties.Select((PropertyInfo p) => p.Name))); return; } string text = args[2]; PropertyInfo propertyInfo = null; PropertyInfo[] array = properties; foreach (PropertyInfo propertyInfo2 in array) { SettingAliasAttribute customAttribute = ((MemberInfo)propertyInfo2).GetCustomAttribute(); if (!(customAttribute == null && requireSettingAliasAttribute)) { text = text.ToLower().Replace("_", ""); if (propertyInfo2.Name.ToLower().Equals(text)) { propertyInfo = propertyInfo2; break; } if (customAttribute != null && customAttribute.Aliases.Contains(text)) { propertyInfo = propertyInfo2; break; } } } if (propertyInfo == null || !propertyInfo.CanRead) { feedbackAction?.Invoke("Could not find setting with name: " + text); return; } if (args.Length < 4) { object value = propertyInfo.GetValue(settings); feedbackAction?.Invoke($"Setting '{text}' currently has value: {value}"); return; } if (!propertyInfo.CanWrite) { feedbackAction?.Invoke("Could not change value of setting with name: " + text + " (non-writable)"); return; } string text2 = args[3]; object obj; if (propertyInfo.PropertyType == typeof(int)) { if (!int.TryParse(text2, out var result)) { feedbackAction?.Invoke("Please provide an integer value for this setting"); return; } obj = result; } else if (propertyInfo.PropertyType == typeof(bool)) { if (!bool.TryParse(text2, out var result2)) { feedbackAction?.Invoke("Please provide a boolean value for this setting"); return; } obj = result2; } else { if (!(propertyInfo.PropertyType == typeof(float))) { feedbackAction?.Invoke("Could not change value of setting with name: " + text + " (unhandled type)"); return; } if (!float.TryParse(text2, out var result3)) { feedbackAction?.Invoke("Please provide a float value for this setting"); return; } obj = result3; } propertyInfo.SetValue(settings, obj); feedbackAction?.Invoke($"Changed setting '{text}' to: {obj}"); successAction?.Invoke(); } } } namespace SsmpVoiceChat.Client { public class ClientNetManager { private readonly IClientAddonNetworkSender _netSender; public event Action VoiceEvent; public ClientNetManager(ClientAddon addon, INetClient netClient) { _netSender = netClient.GetNetworkSender(addon); netClient.GetNetworkReceiver(addon, (Func)InstantiatePacket).RegisterPacketHandler(ClientPacketId.Voice, (GenericClientPacketHandler)delegate(ClientVoicePacket packet) { this.VoiceEvent?.Invoke(packet.Id, packet.VoiceData, packet.Proximity); }); } public void SendVoiceData(byte[] data) { if (data.Length > 65535) { ClientVoiceChat.Logger.Error("Voice data exceeds max size!"); return; } _netSender.SendCollectionData(ServerPacketId.Voice, new ServerVoicePacket { VoiceData = data }); } private static IPacketData InstantiatePacket(ClientPacketId packetId) { if (packetId == ClientPacketId.Voice) { return (IPacketData)(object)new PacketDataCollection(); } return null; } } public class ClientVoiceChat { private VoiceStatusIcon? VoiceStatusIcon; private readonly IClientApi _clientApi; private readonly ClientNetManager _netManager; private readonly MicrophoneManager _micManager; private readonly SoundManager _soundManager; private bool _muted; private bool _pushToggle; public static ILogger Logger { get; private set; } private bool Muted { get { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) if (_muted) { return true; } KeyCode pushToTalkKey = VoiceChatMod.ModSettings.PushToTalkKey; ModSettings.InputMethod inputMode = VoiceChatMod.ModSettings.InputMode; if ((int)pushToTalkKey != 0) { switch (inputMode) { case ModSettings.InputMethod.PushToTalk: return !Input.GetKey(pushToTalkKey); default: return VoiceChatMod.ToggleMuted; case ModSettings.InputMethod.Normal: break; } } return false; } } public ClientVoiceChat(ClientAddon addon, IClientApi clientApi, ILogger logger) { Logger = logger; _clientApi = clientApi; _netManager = new ClientNetManager(addon, clientApi.NetClient); _micManager = new MicrophoneManager(); _soundManager = new SoundManager(); } public void Initialize() { ClientVoiceChatCommand clientVoiceChatCommand = new ClientVoiceChatCommand(_clientApi.UiManager.ChatBox); ((ICommandManager)(object)_clientApi.CommandManager).RegisterCommand((IClientCommand)(object)clientVoiceChatCommand); VoiceChatMod.ModSettings.SetMicrophoneEvent += delegate { ReloadAudio(); }; VoiceChatMod.ModSettings.SetSpeakerEvent += delegate { ReloadAudio(); }; VoiceChatMod.ModSettings.SetMicFail += delegate { VoiceStatusIcon?.SetTalking(SsmpVoiceChat.Client.VoiceStatusIcon.Status.Error); }; clientVoiceChatCommand.ToggleMuteEvent += delegate { _muted = !_muted; _clientApi.UiManager.ChatBox.AddMessage("Microphone is now " + (_muted ? "" : "un") + "muted"); if (!_muted && Muted) { _clientApi.UiManager.ChatBox.AddMessage("Push To Talk is still enabled."); } if (!Muted) { VoiceStatusIcon?.SetTalking(SsmpVoiceChat.Client.VoiceStatusIcon.Status.NotTalking); } else if (_muted) { VoiceStatusIcon?.SetTalking(SsmpVoiceChat.Client.VoiceStatusIcon.Status.Muted); } else { VoiceStatusIcon?.SetTalking(SsmpVoiceChat.Client.VoiceStatusIcon.Status.PushMuted); } }; ReloadAudio(); _clientApi.ClientManager.ConnectEvent += OnConnect; _clientApi.ClientManager.DisconnectEvent += OnDisconnect; _clientApi.ClientManager.PlayerEnterSceneEvent += OnPlayerEnterScene; _clientApi.ClientManager.PlayerLeaveSceneEvent += OnPlayerLeaveScene; _netManager.VoiceEvent += OnVoiceReceived; } private void OnConnect() { Logger.Debug("Client is connected, starting mic capture"); VoiceStatusIcon = new VoiceStatusIcon(); _micManager.Start(); _micManager.VoiceDataEvent += OnVoiceGenerated; _micManager.VoiceOffEvent += OnVoiceStopped; ReloadAudio(); } private void OnDisconnect() { Logger.Debug("Client is disconnected, stopping mic capture"); VoiceStatusIcon?.DestroyIcon(); VoiceStatusIcon = null; _micManager.VoiceDataEvent -= OnVoiceGenerated; _micManager.Stop(); _soundManager.Close(); } private void OnVoiceGenerated(byte[] data) { if (_clientApi.NetClient.IsConnected) { if (!Muted) { VoiceStatusIcon?.SetTalking(SsmpVoiceChat.Client.VoiceStatusIcon.Status.Talking); _netManager.SendVoiceData(data); } else if (_muted) { VoiceStatusIcon?.SetTalking(SsmpVoiceChat.Client.VoiceStatusIcon.Status.Muted); } else { VoiceStatusIcon?.SetTalking(SsmpVoiceChat.Client.VoiceStatusIcon.Status.PushMuted); } } } private void OnVoiceStopped() { if (!Muted) { VoiceStatusIcon?.SetTalking(SsmpVoiceChat.Client.VoiceStatusIcon.Status.NotTalking); } } private void OnPlayerEnterScene(IClientPlayer player) { Logger.Debug("Player entered scene, adding speaker"); _soundManager.TryGetOrCreateSpeaker(player.Id, out Speaker _); } private void OnPlayerLeaveScene(IClientPlayer player) { Logger.Debug("Player left scene, closing and removing speaker"); _soundManager.TryRemoveSpeaker(player.Id); } private void OnVoiceReceived(ushort id, byte[] data, bool proximity) { //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Expected O, but got Unknown EnableRemoteStatusIcon(id); if (!_soundManager.TryGetOrCreateSpeaker(id, out Speaker speaker)) { Logger.Warn($"Could not get or create speaker for player '{id}', cannot play voice"); return; } if (!proximity) { speaker.Play(data); return; } HeroController instance = HeroController.instance; IClientPlayer val = default(IClientPlayer); if ((Object)(object)instance == (Object)null || (Object)(object)((Component)instance).gameObject == (Object)null) { Logger.Warn("Local player could not be found, cannot play voice positionally"); speaker.Play(data); } else if (!_clientApi.ClientManager.TryGetPlayer(id, ref val)) { Logger.Warn($"No player found for '{id}', cannot play voice positionally"); speaker.Play(data); } else { Vector3 position = ((Component)instance).gameObject.transform.position; Vector3 val2 = val.PlayerObject.transform.position - position; speaker.Play(data, new Vector3(val2.x, val2.y, val2.z)); } } private void EnableRemoteStatusIcon(ushort id) { IClientPlayer val = default(IClientPlayer); if (!_clientApi.ClientManager.TryGetPlayer(id, ref val)) { Logger.Warn("Local player could not be found, cannot enable status indicator"); } else { if (val == null || !val.IsInLocalScene) { return; } GameObject playerContainer = val.PlayerContainer; if ((Object)(object)playerContainer == (Object)null) { Logger.Warn("Local player could not be found, cannot enable status indicator"); return; } RemoteStatusIndicator iconOnPlayerContainer = RemoteStatusIndicator.GetIconOnPlayerContainer(playerContainer); if ((Object)(object)iconOnPlayerContainer != (Object)null) { iconOnPlayerContainer.UpdateState(talking: true); } } } private void ReloadAudio() { _micManager.Stop(); _soundManager.Close(); _soundManager.Open(); Logger.Debug("Reloading Audio"); if (_clientApi.NetClient.IsConnected) { _micManager.Start(); } } } public class ClientVoiceChatCommand : IClientCommand, ICommand { private readonly IChatBox _chatBox; private readonly Dictionary _microphoneNames; private readonly Dictionary _speakerNames; public string Trigger => "/voicechatclient"; public string[] Aliases => new string[1] { "/vcc" }; public event Action SetMicrophoneEvent; public event Action SetSpeakerEvent; public event Action ToggleMuteEvent; public ClientVoiceChatCommand(IChatBox chatBox) { _chatBox = chatBox; _microphoneNames = new Dictionary(); _speakerNames = new Dictionary(); } public void Execute(string[] args) { if (args.Length < 2) { SendUsage(); return; } string text = args[1]; if (text == "mute") { HandleMute(); } else if (text == "devices") { HandleDeviceList(args); } else { SendUsage(); } void SendUsage() { _chatBox.AddMessage("Invalid usage: " + Trigger + " "); } } private void HandleMute() { this.ToggleMuteEvent?.Invoke(); } private void SendMicList() { List allMicrophones = Microphone.GetAllMicrophones(); if (allMicrophones.Count == 0) { _chatBox.AddMessage("No microphones could be found"); return; } _microphoneNames.Clear(); _chatBox.AddMessage("Microphones (id, name):"); int num = 1; foreach (string item in allMicrophones) { _chatBox.AddMessage($"{num}: {item}"); _microphoneNames[num++] = item; } } private void SendSpeakerList() { List allDeviceSpeakers = SoundManager.GetAllDeviceSpeakers(); if (allDeviceSpeakers.Count == 0) { _chatBox.AddMessage("No speakers could be found"); return; } _speakerNames.Clear(); _chatBox.AddMessage("Speakers (id, name):"); int num = 1; foreach (string item in allDeviceSpeakers) { _chatBox.AddMessage($"{num}: {item}"); _speakerNames[num++] = item; } } private void HandleDeviceList(string[] args) { string text = ""; if (args.Length >= 4) { text = args[3]; } _chatBox.AddMessage("If you don't see the device you want in the config, you'll have to reload the game."); bool flag; switch (text) { default: if (text.Length == 0) { goto case "mics"; } goto case null; case "mics": case "mic": flag = true; break; case null: flag = false; break; } if (flag) { SendMicList(); return; } if ((text == "speakers" || text == "speaker") ? true : false) { SendSpeakerList(); return; } SendMicList(); _chatBox.AddMessage(""); SendSpeakerList(); } private void HandleDeviceSet(string[] args) { if (args.Length < 5) { SendUsage(); return; } string text = args[3]; string text2 = args[4]; if ((text == "mic" || text == "speaker") ? true : false) { int result; bool flag = int.TryParse(text2, out result); if (text == "mic") { if (flag && _microphoneNames.TryGetValue(result, out string value)) { this.SetMicrophoneEvent?.Invoke(value); _chatBox.AddMessage("Set microphone to \"" + value + "\""); } else if (_microphoneNames.Values.Contains(text2)) { this.SetMicrophoneEvent?.Invoke(text2); _chatBox.AddMessage("Set microphone to \"" + text2 + "\""); } else { _chatBox.AddMessage("Could not find microphone with ID or name: \"" + text2 + "\""); } } else if (text == "speaker") { if (flag && _speakerNames.TryGetValue(result, out string value2)) { this.SetSpeakerEvent?.Invoke(value2); _chatBox.AddMessage("Set speaker to \"" + value2 + "\""); } else if (_speakerNames.Values.Contains(text2)) { this.SetSpeakerEvent?.Invoke(text2); _chatBox.AddMessage("Set speaker to \"" + text2 + "\""); } else { _chatBox.AddMessage("Could not find speaker with ID or name: \"" + text2 + "\""); } } } else { SendUsage(); } void SendUsage() { _chatBox.AddMessage("Invalid usage: " + Trigger + " device set "); } } private void HandleSet(string[] args) { CommandUtil.HandleSetCommand(Trigger, args, VoiceChatMod.ModSettings, (Action)_chatBox.AddMessage, (Action)null, requireSettingAliasAttribute: true); } } internal class ModSettings { internal enum InputMethod { Normal, PushToTalk, PushToToggle } private ConfigEntry _microphoneDevice; private ConfigEntry _speakerDevice; private ConfigEntry _microphoneAmplification; private ConfigEntry _voiceChatVolume; private ConfigEntry _smoothChannelTransition; private ConfigEntry _pushToTalkKey; private ConfigEntry _inputMode; private ConfigEntry _talkingIndicator; private ConfigEntry _maxDistance; private ConfigEntry _rolloffFactor; public const string SystemDeviceName = "System Default"; public string MicrophoneDeviceName { get { string text = _microphoneDevice?.Value ?? ""; if (text == "System Default") { return Microphone.GetDefaultMicrophone(); } return text; } } public string SpeakerDeviceName { get { string text = _speakerDevice?.Value ?? ""; if (text == "System Default") { return SoundManager.GetDefaultDeviceSpeaker(); } return text; } } public float MicrophoneAmplification => Mathf.Clamp((float)_microphoneAmplification.Value / 5f, 0f, 3f); public float VoiceChatVolume => Mathf.Clamp((float)_voiceChatVolume.Value / 10f, 0f, 1f); public bool SmoothChannelTransition => _smoothChannelTransition.Value; public KeyCode PushToTalkKey => _pushToTalkKey.Value; public InputMethod InputMode => _inputMode.Value; public bool TalkingIndicator => _talkingIndicator?.Value ?? true; public float MaxDistance => _maxDistance?.Value ?? 60f; public float RolloffFactor => _rolloffFactor?.Value ?? 1.5f; public event Action SetMicrophoneEvent = delegate { }; public event Action SetSpeakerEvent = delegate { }; public event Action SetMicFail; public event Action OnTalkingIndicatorToggled; public ModSettings(ConfigFile config) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Expected O, but got Unknown //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Expected O, but got Unknown //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Expected O, but got Unknown //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Expected O, but got Unknown List allMicrophones = Microphone.GetAllMicrophones(); allMicrophones.Insert(0, "System Default"); ConfigDescription val = new ConfigDescription("The microphone device currently used.", (AcceptableValueBase)(object)new AcceptableValueList(allMicrophones.ToArray()), Array.Empty()); _microphoneDevice = config.Bind("Devices", "Microphone", "System Default", val); _microphoneDevice.SettingChanged += OnMicrophoneChanged; OnMicrophoneChanged(null, null); List allDeviceSpeakers = SoundManager.GetAllDeviceSpeakers(); allDeviceSpeakers.Insert(0, "System Default"); ConfigDescription val2 = new ConfigDescription("The speaker device currently used.", (AcceptableValueBase)(object)new AcceptableValueList(allDeviceSpeakers.ToArray()), Array.Empty()); _speakerDevice = config.Bind("Devices", "Speaker", "System Default", val2); _speakerDevice.SettingChanged += OnSpeakerChanged; OnSpeakerChanged(null, null); ConfigDescription val3 = new ConfigDescription("Modifies the volume of the microphone input.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 15), Array.Empty()); _microphoneAmplification = config.Bind("Volume", "Microphone Amplification", 5, val3); ConfigDescription val4 = new ConfigDescription("The volume of the voice chat of other players.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 10), Array.Empty()); _voiceChatVolume = config.Bind("Volume", "Chat Volume", 6, val4); _smoothChannelTransition = config.Bind("Volume", "Smooth Channel Transition", true, "Whether the transition between audio from a player moving from the left to the right of the local player is smooth or not"); _pushToTalkKey = config.Bind("Microphone Toggle", "Input Key", (KeyCode)0, "The key to press to enable/toggle your microphone."); _inputMode = config.Bind("Microphone Toggle", "Input Mode", InputMethod.Normal, "The method for microphone input (Push to talk / toggle / normal)"); _inputMode.SettingChanged += delegate { VoiceChatMod.ToggleMuted = false; }; _talkingIndicator = config.Bind("Visuals", "Mic Status Indicator", true, "Whether the microphone icon should be displayed or not"); _talkingIndicator.SettingChanged += delegate { this.OnTalkingIndicatorToggled?.Invoke(); }; } private void OnMicrophoneChanged(object sender, EventArgs e) { string microphoneDeviceName = MicrophoneDeviceName; bool num = Microphone.GetAllMicrophones().Contains(microphoneDeviceName); ILogger logger = ClientVoiceChat.Logger; if (logger != null) { logger.Info(string.Join(", ", Microphone.GetAllMicrophones())); } if (!num) { IChatBox chatBox = VoiceChatMod.ChatBox; if (chatBox != null) { chatBox.AddMessage("[VC]: Couldn't find a microphone with the name " + microphoneDeviceName); } ILogger logger2 = ClientVoiceChat.Logger; if (logger2 != null) { logger2.Error("[VC]: Couldn't find a microphone with the name " + microphoneDeviceName); } this.SetMicFail?.Invoke(); } else { this.SetMicrophoneEvent?.Invoke(microphoneDeviceName); } } private void OnSpeakerChanged(object sender, EventArgs e) { string speakerDeviceName = SpeakerDeviceName; if (!SoundManager.GetAllDeviceSpeakers().Contains(speakerDeviceName)) { IChatBox chatBox = VoiceChatMod.ChatBox; if (chatBox != null) { chatBox.AddMessage("[VC]: Couldn't find a speaker with the name " + speakerDeviceName); } ILogger logger = ClientVoiceChat.Logger; if (logger != null) { logger.Error("[VC]: Couldn't find a speaker with the name " + speakerDeviceName); } } else { this.SetSpeakerEvent?.Invoke(speakerDeviceName); } } } public class ModSettingsBkp { private const string FileName = "voicechat_client_settings.json"; [JsonProperty("microphone_device_name")] public string MicrophoneDeviceName { get; set; } [JsonProperty("speaker_device_name")] public string SpeakerDeviceName { get; set; } [JsonProperty("microphone_amplification")] [SettingAlias(new string[] { "micvol", "micvolume", "micamp" })] public float MicrophoneAmplification { get; set; } = 1f; [JsonProperty("voice_chat_volume")] [SettingAlias(new string[] { "speakervol", "speakervolume" })] public float VoiceChatVolume { get; set; } = 1f; [JsonProperty("smooth_channel_transition")] [SettingAlias(new string[] { "smoothaudio" })] public bool SmoothChannelTransition { get; set; } = true; public void SaveToFile() { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (directoryName == null) { return; } string path = Path.Combine(directoryName, "voicechat_client_settings.json"); string contents = JsonConvert.SerializeObject((object)this, (Formatting)1); try { File.WriteAllText(path, contents); } catch (Exception arg) { ServerVoiceChat.Logger.Error($"Could not write server settings to file:\n{arg}"); } } public static ModSettingsBkp LoadFromFile() { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (directoryName == null) { return new ModSettingsBkp(); } string path = Path.Combine(directoryName, "voicechat_client_settings.json"); if (!File.Exists(path)) { ModSettingsBkp modSettingsBkp = new ModSettingsBkp(); modSettingsBkp.SaveToFile(); return modSettingsBkp; } try { return JsonConvert.DeserializeObject(File.ReadAllText(path)) ?? new ModSettingsBkp(); } catch (Exception arg) { ServerVoiceChat.Logger.Error($"Could not load server settings from file:\n{arg}"); return new ModSettingsBkp(); } } } [RequireComponent(typeof(TextMeshPro))] internal class RemoteStatusIndicator : MonoBehaviour { public bool Talking; private TextMeshPro textComponent; private float timeout; private void Awake() { textComponent = ((Component)this).GetComponent(); } private void OnEnable() { UpdateState(talking: false); } private void OnDisable() { UpdateState(talking: false); } private void FixedUpdate() { if (Talking) { if (timeout > 0f) { timeout -= Time.deltaTime; } else { UpdateState(talking: false); } } } public void UpdateState(bool talking) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) Talking = talking; if (Talking) { ((TMP_Text)textComponent).outlineColor = Color32.op_Implicit(new Color(0.02f, 0.35f, 0f)); timeout = 0.25f; } else { ((TMP_Text)textComponent).outlineColor = Color32.op_Implicit(Color.black); timeout = 0f; } } public static RemoteStatusIndicator? GetIconOnPlayerContainer(GameObject playerContainer) { GameObject val = VoiceStatusIcon.FindChild(playerContainer.transform, "Username"); if ((Object)(object)val == (Object)null) { return null; } return Extensions.AddComponentIfNotPresent(val); } } public class VoiceChatClientAddon : ClientAddon { protected override string Name => "ProximityChat"; protected override string Version => "0.1.3"; public override uint ApiVersion => 1u; public override bool NeedsNetwork => true; public override void Initialize(IClientApi clientApi) { new ClientVoiceChat((ClientAddon)(object)this, clientApi, ((ClientAddon)this).Logger).Initialize(); VoiceChatMod.ChatBox = clientApi.UiManager.ChatBox; } } [BepInPlugin("io.github.bobbythecatfish.SSMP.VoiceChat", "SSMPVoiceChat", "0.1.3")] public class VoiceChatMod : BaseUnityPlugin { internal static ModSettings ModSettings; internal static IChatBox ChatBox; internal static bool ToggleMuted; private const string url = "https://www.openal.org/downloads"; public const string Id = "io.github.bobbythecatfish.SSMP.VoiceChat"; public static string Name => "SSMPVoiceChat"; public static string Version => "0.1.3"; public void Awake() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) try { Alc.GetError(IntPtr.Zero); } catch (DllNotFoundException) { try { Process.Start(new ProcessStartInfo("https://www.openal.org/downloads") { UseShellExecute = true }); } catch { } ((BaseUnityPlugin)this).Logger.LogError((object)"OpenAL not installed. Please install at https://www.openal.org/downloads"); SceneManager.sceneLoaded += OpenALErrorWarning; } ClientAddon.RegisterAddon((ClientAddon)(object)new VoiceChatClientAddon()); ServerAddon.RegisterAddon((ServerAddon)(object)new VoiceChatServerAddon()); ModSettings = new ModSettings(((BaseUnityPlugin)this).Config); } private void Update() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) if (ModSettings.InputMode == SsmpVoiceChat.Client.ModSettings.InputMethod.PushToToggle && Input.GetKeyDown(ModSettings.PushToTalkKey)) { ToggleMuted = !ToggleMuted; } } private void OpenALErrorWarning(Scene scene, LoadSceneMode mode) { //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Expected O, but got Unknown //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) if (!(((Scene)(ref scene)).name != "Menu_Title")) { SceneManager.sceneLoaded -= OpenALErrorWarning; Transform transform = ((Component)UIManager.instance.UICanvas).transform; Transform val = Object.Instantiate(transform.Find("MainMenuScreen").GetChild(0).GetChild(0) .GetChild(0), transform); ContentSizeFitter val2 = default(ContentSizeFitter); if (((Component)val).TryGetComponent(ref val2)) { Object.DestroyImmediate((Object)(object)val2); } FixVerticalAlign val3 = default(FixVerticalAlign); if (((Component)val).TryGetComponent(ref val3)) { Object.DestroyImmediate((Object)(object)val3); } ((Component)val).GetComponent().sizeDelta = new Vector2(1055f, 300f); Extensions.SetLocalPosition2D(val, 0f, 190f); GameObject val4 = new GameObject("Warning Background"); val4.transform.parent = transform; val4.transform.position = val.position; Extensions.SetScale2D(val4.transform, Vector2.one); Extensions.AddComponentIfNotPresent(val4).sizeDelta = new Vector2(1055f, 300f); ((Graphic)val4.AddComponent()).color = new Color(0.34f, 0.34f, 0.34f, 0.8f); Extensions.SetParentReset(val, val4.transform); Extensions.SetPositionZ(val, -15f); Text component = ((Component)val).GetComponent(); component.text = "You need to install OpenAL for SSMP Voice Chat to work. Download at OpenAL.org"; component.lineSpacing = 1f; } } } internal class VoiceStatusIcon { public enum Status { Talking, Muted, PushMuted, NotTalking, Error } private const int IMAGE_SIZE = 66; private Sprite Unmuted; private Sprite Muted; private SpriteRenderer MicStatus; private Sprite MicNotFound; private GameObject? MicrophoneIcons; private SpriteRenderer TalkingIndicator; private Status CurrentStatus = Status.NotTalking; public static Color TalkingColor = new Color(0.3f, 0.5f, 0.3f, 1f); public static Color NotTalkingColor = new Color(0.2f, 0.2f, 0.2f, 1f); public static Color MutedColor = new Color(0.9f, 0.17f, 0.15f, 1f); public static Color ErrorColor = new Color(0.5f, 0.15f, 0.9f, 1f); public VoiceStatusIcon() { CreateSprites(); CreateIconObject(); VoiceChatMod.ModSettings.OnTalkingIndicatorToggled += OnIndicatorToggled; OnIndicatorToggled(); } public static GameObject? FindChild(Transform currentObject, string path) { string[] array = path.Split('/'); foreach (string text in array) { int childCount = currentObject.childCount; for (int j = 0; j < childCount; j++) { Transform child = currentObject.GetChild(j); if (((Object)child).name == text) { currentObject = child; break; } } if (((Object)currentObject).name != text) { return null; } } return ((Component)currentObject).gameObject; } private void CreateIconObject() { //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Expected O, but got Unknown //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_018f: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01c8: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) //IL_01f1: Unknown result type (might be due to invalid IL or missing references) //IL_01f3: Unknown result type (might be due to invalid IL or missing references) //IL_01fc: Unknown result type (might be due to invalid IL or missing references) //IL_0213: Unknown result type (might be due to invalid IL or missing references) //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_0226: Unknown result type (might be due to invalid IL or missing references) //IL_0228: Unknown result type (might be due to invalid IL or missing references) //IL_0231: Unknown result type (might be due to invalid IL or missing references) //IL_0248: Unknown result type (might be due to invalid IL or missing references) //IL_024d: Unknown result type (might be due to invalid IL or missing references) //IL_025b: Unknown result type (might be due to invalid IL or missing references) //IL_025d: Unknown result type (might be due to invalid IL or missing references) //IL_0266: Unknown result type (might be due to invalid IL or missing references) //IL_027d: Unknown result type (might be due to invalid IL or missing references) //IL_0282: Unknown result type (might be due to invalid IL or missing references) //IL_0290: Unknown result type (might be due to invalid IL or missing references) //IL_0292: Unknown result type (might be due to invalid IL or missing references) //IL_029b: Unknown result type (might be due to invalid IL or missing references) //IL_02b2: Unknown result type (might be due to invalid IL or missing references) //IL_02b7: Unknown result type (might be due to invalid IL or missing references) //IL_02c5: Unknown result type (might be due to invalid IL or missing references) //IL_02c7: Unknown result type (might be due to invalid IL or missing references) //IL_030c: Unknown result type (might be due to invalid IL or missing references) //IL_0313: Expected O, but got Unknown //IL_0340: Unknown result type (might be due to invalid IL or missing references) Transform transform = FindChild(((Component)GameCameras.instance.hudCamera).transform, "In-game/Anchor TL/Hud Canvas Offset/Hud Canvas/Extras").transform; GameObject target = FindChild(transform.parent, "Thread/Spool/Thread Spool/Parent/Extender Tool/Extender Sprite"); GameObject val = FindChild(transform, "Reserve Bind/Reserve Bind Sprite"); GameObject target2 = FindChild(transform, "Lava Bell HUD/lava_bell_icon"); GameObject target3 = FindChild(transform, "Maggot Charm/Maggot Charm Sprite"); string text = "Parent/Canvas/Background Image/Radial Image"; GameObject target4 = FindChild(transform.parent, "Tool Icons/Tool Icon U/" + text); GameObject val2 = FindChild(transform.parent, "Tool Icons/Tool Icon N/" + text); GameObject target5 = FindChild(transform.parent, "Tool Icons/Tool Icon D/" + text); MicrophoneIcons = new GameObject("Microphone Icons"); MicrophoneIcons.transform.SetParent(transform, false); MicrophoneIcons.transform.localPosition = new Vector3(6.82f, -1.35f, 0f); MicrophoneIcons.layer = 5; PositionRelativeTo obj = MicrophoneIcons.AddComponent(); PositionRelativeTo component = ((Component)val.transform.parent).GetComponent(); obj.inSpace = component.inSpace; obj.target = component.target; obj.positionX = true; obj.offset = new Vector3(1.35f, 0f, 0f); obj.extensions = (ExtensionPair[])(object)new ExtensionPair[7] { new ExtensionPair { AddOffset = new Vector3(0.54f, 0f, 0f), Target = target }, new ExtensionPair { AddOffset = new Vector3(1.33f, 0f, 0f), Target = val.gameObject }, new ExtensionPair { AddOffset = new Vector3(1.2f, 0f, 0f), Target = target2 }, new ExtensionPair { AddOffset = new Vector3(1.2f, 0f, 0f), Target = target3 }, new ExtensionPair { AddOffset = new Vector3(1.34f, 0f, 0f), Target = target4 }, new ExtensionPair { AddOffset = new Vector3(1.2f, 0f, 0f), Target = val2 }, new ExtensionPair { AddOffset = new Vector3(1.2f, 0f, 0f), Target = target5 } }; SpriteRenderer val3 = MicrophoneIcons.AddComponent(); val3.sprite = Unmuted; ((Renderer)val3).sortingLayerName = "Over"; ((Renderer)val3).sortingOrder = 1; MicStatus = val3; GameObject val4 = new GameObject("Speaking Indicator"); Extensions.SetParentReset(val4.transform, MicrophoneIcons.transform); val4.transform.localPosition = new Vector3(0f, 0f, 0.23f); val4.layer = 5; TalkingIndicator = val4.AddComponent(); TalkingIndicator.sprite = val2.GetComponent().sprite; ((Renderer)TalkingIndicator).sortingLayerName = "Over"; ((Renderer)val3).sortingOrder = 0; SetTalking(Status.NotTalking); } public void DestroyIcon() { if ((Object)(object)MicrophoneIcons != (Object)null) { Object.Destroy((Object)(object)MicrophoneIcons); MicrophoneIcons = null; VoiceChatMod.ModSettings.OnTalkingIndicatorToggled -= OnIndicatorToggled; } } private void OnIndicatorToggled() { if (!((Object)(object)MicrophoneIcons == (Object)null)) { bool talkingIndicator = VoiceChatMod.ModSettings.TalkingIndicator; MicrophoneIcons.SetActive(talkingIndicator); } } public void SetTalking(Status talking) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) if (talking != CurrentStatus) { CurrentStatus = talking; switch (talking) { case Status.Talking: MicStatus.sprite = Unmuted; TalkingIndicator.color = TalkingColor; break; case Status.NotTalking: MicStatus.sprite = Unmuted; TalkingIndicator.color = NotTalkingColor; break; case Status.Muted: MicStatus.sprite = Muted; TalkingIndicator.color = MutedColor; break; case Status.PushMuted: MicStatus.sprite = Unmuted; TalkingIndicator.color = MutedColor; break; default: MicStatus.sprite = MicNotFound; TalkingIndicator.color = ErrorColor; break; } } } private void CreateSprites() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) byte[] array = File.ReadAllBytes(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "microphone_icons.png")); Texture2D val = new Texture2D(198, 66); ImageConversion.LoadImage(val, array); Vector2 val2 = default(Vector2); ((Vector2)(ref val2))..ctor(0.5f, 0.5f); Unmuted = Sprite.Create(val, new Rect(0f, 0f, 66f, 66f), val2); Muted = Sprite.Create(val, new Rect(66f, 0f, 66f, 66f), val2); MicNotFound = Sprite.Create(val, new Rect(132f, 0f, 66f, 66f), val2); } } } namespace SsmpVoiceChat.Client.Voice { public class Microphone { private readonly string _deviceName; private IntPtr _device; public bool IsOpen => _device != IntPtr.Zero; public bool IsStarted { get; private set; } public Microphone(string deviceName) { _device = IntPtr.Zero; _deviceName = deviceName; } public void Open() { if (IsOpen) { throw new Exception("Microphone already open"); } _device = OpenMic(_deviceName); } public void Start() { if (IsOpen && !IsStarted) { Alc.CaptureStart(_device); SoundManager.CheckAlcError(_device, 0); IsStarted = true; } } public void Stop() { if (!IsOpen || !IsStarted) { return; } Alc.CaptureStop(_device); SoundManager.CheckAlcError(_device, 0); IsStarted = false; short[] array = new short[Available()]; GCHandle gCHandle = GCHandle.Alloc(array, GCHandleType.Pinned); try { Alc.CaptureSamples(_device, gCHandle.AddrOfPinnedObject(), array.Length); SoundManager.CheckAlcError(_device, 1); } catch (Exception arg) { ClientVoiceChat.Logger.Error($"Exception while capturing samples:\n{arg}"); } finally { gCHandle.Free(); } } public void Close() { if (IsOpen) { Stop(); Alc.CaptureCloseDevice(_device); SoundManager.CheckAlcError(_device, 0); _device = IntPtr.Zero; } } public int Available() { int result = default(int); Alc.GetInteger(_device, (AlcGetInteger)786, 1, ref result); SoundManager.CheckAlcError(_device, 0); return result; } public short[] Read() { int num = Available(); if (num < 960) { throw new InvalidOperationException($"Failed to read from microphone: Capacity {960}, available {num}"); } short[] array = new short[960]; GCHandle gCHandle = GCHandle.Alloc(array, GCHandleType.Pinned); try { Alc.CaptureSamples(_device, gCHandle.AddrOfPinnedObject(), array.Length); SoundManager.CheckAlcError(_device, 0); } catch (Exception arg) { ClientVoiceChat.Logger.Error($"Exception while capturing samples:\n{arg}"); } finally { gCHandle.Free(); } return array; } private IntPtr OpenMic(string name) { try { return TryOpenMic(name); } catch (Exception) { if (name != null) { ClientVoiceChat.Logger.Error("Failed to open microphone '" + name + "', falling back to default microphone"); } try { return TryOpenMic(GetDefaultMicrophone()); } catch (Exception) { return TryOpenMic(null); } } } private IntPtr TryOpenMic(string name) { IntPtr intPtr = Alc.CaptureOpenDevice(name, 48000, (ALFormat)4353, 960); if (intPtr == IntPtr.Zero) { SoundManager.CheckAlcError(IntPtr.Zero, 0); throw new Exception("Failed to open microphone"); } return intPtr; } public static string GetDefaultMicrophone() { string @string = Alc.GetString(IntPtr.Zero, (AlcGetString)785); SoundManager.CheckAlcError(IntPtr.Zero, 0); return @string; } public static List GetAllMicrophones() { List list = Alc.GetString(IntPtr.Zero, (AlcGetStringList)784).ToList(); list.Sort(); SoundManager.CheckAlcError(IntPtr.Zero, 0); if (list != null) { return list.ToList(); } return new List(); } } public class MicrophoneManager { private readonly OpusCodec _encoder; private readonly RNNoise _denoiser; private readonly WebRtcVad _webRtcVad; private Thread _thread; private bool _isRunning; private Microphone _microphone; private bool _activating; private byte[] _lastBuff; public event Action VoiceDataEvent; public event Action VoiceOffEvent; public MicrophoneManager() { _encoder = new OpusCodec(48000, 1, 960); _denoiser = new RNNoise(); _webRtcVad = new WebRtcVad { SampleRate = 48000, FrameLength = 20, OperatingMode = OperatingMode.Aggressive }; } public void Start() { if (_isRunning) { Stop(); } _thread = new Thread((ThreadStart)delegate { _isRunning = true; if (GetMic()) { while (_isRunning) { try { if (PollMic(out short[] buff)) { byte[] array = DataUtils.ShortsToBytes(buff); bool flag = _webRtcVad.HasSpeech(buff); if (!_activating) { if (flag) { if (_lastBuff != null) { this.VoiceDataEvent?.Invoke(_encoder.Encode(_lastBuff)); } this.VoiceDataEvent?.Invoke(_encoder.Encode(array)); _activating = true; } } else if (!flag) { _activating = false; this.VoiceOffEvent?.Invoke(); } else { this.VoiceDataEvent?.Invoke(_encoder.Encode(array)); } _lastBuff = array; } } catch (Exception arg) { ClientVoiceChat.Logger.Error($"Error in mic thread:\n{arg}"); } } } }); _thread.Start(); } public void Stop() { if (_isRunning) { _isRunning = false; _thread.Join(100); _thread = null; _microphone.Close(); _microphone = null; } } private bool GetMic() { if (!_isRunning) { return false; } if (_microphone == null) { _microphone = new Microphone(VoiceChatMod.ModSettings.MicrophoneDeviceName); } if (!_microphone.IsOpen) { _microphone.Open(); } return true; } private bool PollMic(out short[] buff) { if (_microphone == null) { throw new InvalidOperationException("Cannot poll unknown microphone"); } if (!_microphone.IsStarted) { _microphone.Start(); } if (_microphone.Available() < 960) { Thread.Sleep(5); buff = null; return false; } buff = _microphone.Read(); if (buff == null) { Thread.Sleep(5); buff = null; return false; } buff = VolumeManager.AmplifyAudioData(buff, VoiceChatMod.ModSettings.MicrophoneAmplification); buff = _denoiser.ProcessFrame(buff); return true; } } public class SoundManager { public const int SampleRate = 48000; public const int FrameLength = 20; public const int BufferSize = 960; private IntPtr _device; private ContextHandle _context; private readonly ConcurrentDictionary _speakers; private bool IsClosed => _device == IntPtr.Zero; public SoundManager() { _speakers = new ConcurrentDictionary(); } public void Open() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) string name = ((!string.IsNullOrEmpty(VoiceChatMod.ModSettings.SpeakerDeviceName)) ? VoiceChatMod.ModSettings.SpeakerDeviceName : GetDefaultDeviceSpeaker()); _device = OpenDeviceSpeaker(name); _context = Alc.CreateContext(_device, Array.Empty()); Alc.MakeContextCurrent(_context); } public void Close() { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) foreach (Speaker value in _speakers.Values) { value.Close(); } _speakers.Clear(); if (_context != ContextHandle.Zero) { Alc.DestroyContext(_context); CheckAlcError(_device, 0); } if (_device != IntPtr.Zero) { Alc.CloseDevice(_device); } _context = ContextHandle.Zero; _device = IntPtr.Zero; } public bool TryGetOrCreateSpeaker(ushort id, [MaybeNullWhen(false)] out Speaker speaker) { if (IsClosed) { speaker = null; return false; } if (!_speakers.TryGetValue(id, out speaker)) { speaker = new Speaker(); speaker.Open(); _speakers.TryAdd(id, speaker); } return true; } public bool TryRemoveSpeaker(ushort id) { if (IsClosed) { return false; } if (_speakers.TryRemove(id, out Speaker value)) { value.Close(); } return true; } private IntPtr OpenDeviceSpeaker(string name) { try { return TryOpenDeviceSpeaker(name); } catch (Exception) { if (name != null) { ClientVoiceChat.Logger.Error("Failed to open audio channel '" + name + "', falling back to default"); } try { return TryOpenDeviceSpeaker(GetDefaultDeviceSpeaker()); } catch (Exception) { return TryOpenDeviceSpeaker(null); } } } private IntPtr TryOpenDeviceSpeaker(string name) { IntPtr intPtr = Alc.OpenDevice(name); if (intPtr == IntPtr.Zero) { throw new Exception("Failed to open audio device: Audio device not found"); } CheckAlcError(intPtr, 0); return intPtr; } public static string GetDefaultDeviceSpeaker() { string @string = Alc.GetString(IntPtr.Zero, (AlcGetString)4114); CheckAlcError(IntPtr.Zero, 0); return @string; } public static List GetAllDeviceSpeakers() { IList @string = Alc.GetString(IntPtr.Zero, (AlcGetStringList)4115); CheckAlcError(IntPtr.Zero, 0); if (@string != null) { return @string.ToList(); } return new List(); } public static bool CheckAlError(int index) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) ALError error = AL.GetError(); if ((int)error == 0) { return false; } StackFrame stackFrame = new StackFrame(1); ClientVoiceChat.Logger.Error($"VoiceChat sound manager AL error: {stackFrame.GetMethod().DeclaringType}.{stackFrame.GetMethod().Name}[{index}] {error}"); return true; } public static bool CheckAlcError(IntPtr device, int index) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) AlcError error = Alc.GetError(device); if ((int)error == 0) { return false; } StackFrame stackFrame = new StackFrame(1); ClientVoiceChat.Logger.Error($"VoiceChat sound manager ALC error: {stackFrame.GetMethod().DeclaringType}.{stackFrame.GetMethod().Name}[{index}] {error}"); return true; } } public class Speaker { private const float DefaultMaxDistance = 60f; private const int NumBuffers = 32; private readonly OpusCodec _decoder; private int _source; private int[] _buffers; private int _bufferIndex; public Speaker() { _decoder = new OpusCodec(48000, 1, 960); } public void Open() { if (!HasValidSource()) { _source = AL.GenSource(); SoundManager.CheckAlError(0); AL.Source(_source, (ALSourceb)4103, false); SoundManager.CheckAlError(1); AL.DistanceModel((ALDistanceModel)53251); SoundManager.CheckAlError(2); AL.Source(_source, (ALSourcef)4131, VoiceChatMod.ModSettings.MaxDistance); SoundManager.CheckAlError(3); AL.Source(_source, (ALSourcef)4128, 0f); SoundManager.CheckAlError(4); AL.Source(_source, (ALSource3f)4101, 0f, 0f, 0f); SoundManager.CheckAlError(5); _buffers = AL.GenBuffers(32); SoundManager.CheckAlError(6); } } public void Play(byte[] encodedData, Vector3? position = null) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Invalid comparison between Unknown and I4 //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Invalid comparison between Unknown and I4 short[] data = DataUtils.BytesToShorts(_decoder.Decode(encodedData)); RemoveProcessedBuffers(); Write(data, position); int queuedBuffers = GetQueuedBuffers(); if ((int)GetState() == 4113 || (int)GetState() == 4116 || queuedBuffers <= 1) { AL.SourcePlay(_source); SoundManager.CheckAlError(0); } } private void Write(short[] data, Vector3? position) { SetPosition(position); float voiceChatVolume = VoiceChatMod.ModSettings.VoiceChatVolume; AL.Source(_source, (ALSourcef)4110, 1f); SoundManager.CheckAlError(0); AL.Source(_source, (ALSourcef)4106, voiceChatVolume); SoundManager.CheckAlError(1); AL.Listener((ALListenerf)4106, 1f); SoundManager.CheckAlError(2); if (GetQueuedBuffers() >= _buffers.Length) { int num = default(int); AL.GetSource(_source, (ALGetSourcei)4133, ref num); SoundManager.CheckAlError(3); AL.Source(_source, (ALSourcei)4133, num + 960); SoundManager.CheckAlError(4); RemoveProcessedBuffers(); } AL.BufferData(_buffers[_bufferIndex], (ALFormat)4353, data, data.Length * 2, 48000); SoundManager.CheckAlError(5); AL.SourceQueueBuffer(_source, _buffers[_bufferIndex]); SoundManager.CheckAlError(6); _bufferIndex = (_bufferIndex + 1) % _buffers.Length; } private void LinearAttenuation() { float maxDistance = VoiceChatMod.ModSettings.MaxDistance; AL.DistanceModel((ALDistanceModel)53251); SoundManager.CheckAlError(0); AL.Source(_source, (ALSourcef)4131, maxDistance); SoundManager.CheckAlError(1); AL.Source(_source, (ALSourcef)4129, VoiceChatMod.ModSettings.RolloffFactor); SoundManager.CheckAlError(2); } private void SetPosition(Vector3? soundPos) { AL.Listener((ALListener3f)4100, 0f, 0f, 0f); SoundManager.CheckAlError(0); float[] array = new float[6] { 0f, 0f, -1f, 0f, 1f, 0f }; AL.Listener((ALListenerfv)4111, ref array); SoundManager.CheckAlError(1); if (soundPos != null) { float x = soundPos.X; float y = soundPos.Y; float num = soundPos.Z; if (VoiceChatMod.ModSettings.SmoothChannelTransition) { num = -5f; } LinearAttenuation(); AL.Source(_source, (ALSourceb)514, true); SoundManager.CheckAlError(2); AL.Source(_source, (ALSource3f)4100, x, y, num); SoundManager.CheckAlError(3); } else { LinearAttenuation(); AL.Source(_source, (ALSourceb)514, true); SoundManager.CheckAlError(4); AL.Source(_source, (ALSource3f)4100, 0f, 0f, 0f); SoundManager.CheckAlError(5); } } public void Close() { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 if (HasValidSource()) { if ((int)GetState() == 4114) { AL.SourceStop(_source); SoundManager.CheckAlError(0); } int num = default(int); AL.GetSource(_source, (ALGetSourcei)4118, ref num); SoundManager.CheckAlError(1); if (num > 0) { AL.SourceUnqueueBuffers(_source, num); SoundManager.CheckAlError(2); } AL.DeleteSource(_source); SoundManager.CheckAlError(3); AL.DeleteBuffers(_buffers); SoundManager.CheckAlError(4); } _source = 0; } private void RemoveProcessedBuffers() { int num = default(int); AL.GetSource(_source, (ALGetSourcei)4118, ref num); SoundManager.CheckAlError(0); if (num > 0) { AL.SourceUnqueueBuffers(_source, num); SoundManager.CheckAlError(1); } } private ALSourceState GetState() { int num = default(int); AL.GetSource(_source, (ALGetSourcei)4112, ref num); SoundManager.CheckAlError(0); return (ALSourceState)num; } private int GetQueuedBuffers() { int result = default(int); AL.GetSource(_source, (ALGetSourcei)4117, ref result); SoundManager.CheckAlError(0); return result; } private bool HasValidSource() { return AL.IsSource(_source); } } public static class VolumeManager { private const short MaxAmplification = 32766; private static readonly float[] MaxMultipliers; private static int _index; static VolumeManager() { MaxMultipliers = new float[50]; } public static short[] AmplifyAudioData(short[] audio, float multiplier) { MaxMultipliers[_index] = GetMaximumAmplification(audio, multiplier); _index = (_index + 1) % MaxMultipliers.Length; float num = -1f; float[] maxMultipliers = MaxMultipliers; foreach (float num2 in maxMultipliers) { if (!(num2 < 0f)) { if (num < 0f) { num = num2; } else if (num2 < num) { num = num2; } } } if (num < 0f) { num = 1f; } float num3 = Math.Min(num, multiplier); short[] array = new short[audio.Length]; for (int j = 0; j < audio.Length; j++) { array[j] = (short)((float)audio[j] * num3); } return array; } private static float GetMaximumAmplification(short[] audio, float multiplier) { short num = 0; foreach (short num2 in audio) { short num3 = ((num2 != short.MinValue) ? Math.Abs(num2) : short.MaxValue); if (num3 > num) { num = num3; } } if (num == 0) { return 1f; } return Math.Min(multiplier, 32766f / (float)num); } } }