Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of SSMPVoiceChat v0.1.3
plugins/SSMPVoiceChat.dll
Decompiled a week agousing 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<ClientPacketId> _netSender; public event Action<ushort, byte[]> VoiceEvent; public ServerNetManager(ServerAddon addon, INetServer netServer) { _netSender = netServer.GetNetworkSender<ClientPacketId>(addon); netServer.GetNetworkReceiver<ServerPacketId>(addon, (Func<ServerPacketId, IPacketData>)InstantiatePacket).RegisterPacketHandler<ServerVoicePacket>(ServerPacketId.Voice, (GenericServerPacketHandler<ServerVoicePacket>)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<ClientVoicePacket>(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<ServerVoicePacket>(); } 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<ServerSettings>(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<ushort> _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<ushort>(); } public void Initialize() { ((ICommandManager<IServerCommand>)(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<ushort> _broadcasters; public string Trigger => "/voicechatserver"; public string[] Aliases => new string[1] { "/vcs" }; public bool AuthorizedOnly => true; public ServerVoiceChatCommand(ServerSettings settings, HashSet<ushort> 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 + " <set|broadcast>"); } } private void HandleSet(ICommandSender commandSender, string[] args) { CommandUtil.HandleSetCommand(Trigger, args, _settings, (Action<string>)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<IntPtr> Libraries = new List<IntPtr>(); [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<TSettings>(string trigger, string[] args, TSettings settings, Action<string> 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<SettingAliasAttribute>(); 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<ServerPacketId> _netSender; public event Action<ushort, byte[], bool> VoiceEvent; public ClientNetManager(ClientAddon addon, INetClient netClient) { _netSender = netClient.GetNetworkSender<ServerPacketId>(addon); netClient.GetNetworkReceiver<ClientPacketId>(addon, (Func<ClientPacketId, IPacketData>)InstantiatePacket).RegisterPacketHandler<ClientVoicePacket>(ClientPacketId.Voice, (GenericClientPacketHandler<ClientVoicePacket>)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<ServerVoicePacket>(ServerPacketId.Voice, new ServerVoicePacket { VoiceData = data }); } private static IPacketData InstantiatePacket(ClientPacketId packetId) { if (packetId == ClientPacketId.Voice) { return (IPacketData)(object)new PacketDataCollection<ClientVoicePacket>(); } 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<IClientCommand>)(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<int, string> _microphoneNames; private readonly Dictionary<int, string> _speakerNames; public string Trigger => "/voicechatclient"; public string[] Aliases => new string[1] { "/vcc" }; public event Action<string> SetMicrophoneEvent; public event Action<string> SetSpeakerEvent; public event Action ToggleMuteEvent; public ClientVoiceChatCommand(IChatBox chatBox) { _chatBox = chatBox; _microphoneNames = new Dictionary<int, string>(); _speakerNames = new Dictionary<int, string>(); } 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 + " <mute|devices>"); } } private void HandleMute() { this.ToggleMuteEvent?.Invoke(); } private void SendMicList() { List<string> 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<string> 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 <mic|speaker> <value>"); } } private void HandleSet(string[] args) { CommandUtil.HandleSetCommand(Trigger, args, VoiceChatMod.ModSettings, (Action<string>)_chatBox.AddMessage, (Action)null, requireSettingAliasAttribute: true); } } internal class ModSettings { internal enum InputMethod { Normal, PushToTalk, PushToToggle } private ConfigEntry<string> _microphoneDevice; private ConfigEntry<string> _speakerDevice; private ConfigEntry<int> _microphoneAmplification; private ConfigEntry<int> _voiceChatVolume; private ConfigEntry<bool> _smoothChannelTransition; private ConfigEntry<KeyCode> _pushToTalkKey; private ConfigEntry<InputMethod> _inputMode; private ConfigEntry<bool> _talkingIndicator; private ConfigEntry<float> _maxDistance; private ConfigEntry<float> _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<string> SetMicrophoneEvent = delegate { }; public event Action<string> 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<string> allMicrophones = Microphone.GetAllMicrophones(); allMicrophones.Insert(0, "System Default"); ConfigDescription val = new ConfigDescription("The microphone device currently used.", (AcceptableValueBase)(object)new AcceptableValueList<string>(allMicrophones.ToArray()), Array.Empty<object>()); _microphoneDevice = config.Bind<string>("Devices", "Microphone", "System Default", val); _microphoneDevice.SettingChanged += OnMicrophoneChanged; OnMicrophoneChanged(null, null); List<string> allDeviceSpeakers = SoundManager.GetAllDeviceSpeakers(); allDeviceSpeakers.Insert(0, "System Default"); ConfigDescription val2 = new ConfigDescription("The speaker device currently used.", (AcceptableValueBase)(object)new AcceptableValueList<string>(allDeviceSpeakers.ToArray()), Array.Empty<object>()); _speakerDevice = config.Bind<string>("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<int>(0, 15), Array.Empty<object>()); _microphoneAmplification = config.Bind<int>("Volume", "Microphone Amplification", 5, val3); ConfigDescription val4 = new ConfigDescription("The volume of the voice chat of other players.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 10), Array.Empty<object>()); _voiceChatVolume = config.Bind<int>("Volume", "Chat Volume", 6, val4); _smoothChannelTransition = config.Bind<bool>("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<KeyCode>("Microphone Toggle", "Input Key", (KeyCode)0, "The key to press to enable/toggle your microphone."); _inputMode = config.Bind<InputMethod>("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<bool>("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<ModSettingsBkp>(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<TextMeshPro>(); } 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<RemoteStatusIndicator>(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>(transform.Find("MainMenuScreen").GetChild(0).GetChild(0) .GetChild(0), transform); ContentSizeFitter val2 = default(ContentSizeFitter); if (((Component)val).TryGetComponent<ContentSizeFitter>(ref val2)) { Object.DestroyImmediate((Object)(object)val2); } FixVerticalAlign val3 = default(FixVerticalAlign); if (((Component)val).TryGetComponent<FixVerticalAlign>(ref val3)) { Object.DestroyImmediate((Object)(object)val3); } ((Component)val).GetComponent<RectTransform>().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<RectTransform>(val4).sizeDelta = new Vector2(1055f, 300f); ((Graphic)val4.AddComponent<Image>()).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<Text>(); 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>(); PositionRelativeTo component = ((Component)val.transform.parent).GetComponent<PositionRelativeTo>(); 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<SpriteRenderer>(); 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<SpriteRenderer>(); TalkingIndicator.sprite = val2.GetComponent<Image>().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<string> GetAllMicrophones() { List<string> list = Alc.GetString(IntPtr.Zero, (AlcGetStringList)784).ToList(); list.Sort(); SoundManager.CheckAlcError(IntPtr.Zero, 0); if (list != null) { return list.ToList(); } return new List<string>(); } } 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<byte[]> 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<ushort, Speaker> _speakers; private bool IsClosed => _device == IntPtr.Zero; public SoundManager() { _speakers = new ConcurrentDictionary<ushort, Speaker>(); } 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<int>()); 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<string> GetAllDeviceSpeakers() { IList<string> @string = Alc.GetString(IntPtr.Zero, (AlcGetStringList)4115); CheckAlcError(IntPtr.Zero, 0); if (@string != null) { return @string.ToList(); } return new List<string>(); } 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<short>(_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); } } }