Decompiled source of V0idSounds v1.0.3

V0idSounds.dll

Decompiled 5 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Networking;
using V0idSounds.Patches;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("V0idSounds")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.3.0")]
[assembly: AssemblyInformationalVersion("1.0.3")]
[assembly: AssemblyProduct("V0idSounds")]
[assembly: AssemblyTitle("V0idSounds")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.3.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace V0idSounds
{
	[BepInPlugin("com.v0id.v0idsounds", "V0idSounds", "1.0.3")]
	public class Plugin : BaseUnityPlugin
	{
		public static class PluginInfo
		{
			public const string PLUGIN_GUID = "com.v0id.v0idsounds";

			public const string PLUGIN_NAME = "V0idSounds";

			public const string PLUGIN_VERSION = "1.0.3";
		}

		private static readonly Dictionary<string, ConfigEntry<float>> SoundVolumeConfigs = new Dictionary<string, ConfigEntry<float>>(StringComparer.OrdinalIgnoreCase);

		private AudioSource _voiceAudioSource;

		private AudioSource _eventAudioSource;

		private AudioSource? _shipSpeakerAudioSource;

		private readonly Dictionary<string, List<AudioClip>> _audioPools = new Dictionary<string, List<AudioClip>>(StringComparer.OrdinalIgnoreCase);

		private readonly Dictionary<string, int> _playCountPerCategory = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

		private readonly Dictionary<string, int> _lastPlayedIndexPerCategory = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

		private readonly Random _fallbackRandom = new Random();

		private string _pluginDirectory = string.Empty;

		private Coroutine? _fadeCoroutine;

		public static Plugin Instance { get; private set; } = null;

		internal static ManualLogSource Log => ((BaseUnityPlugin)Instance).Logger;

		public static ConfigEntry<bool> ConfigEnableJoinSound { get; private set; } = null;

		public static ConfigEntry<bool> ConfigEnableLeaveSound { get; private set; } = null;

		public static ConfigEntry<bool> ConfigPlayForSelfJoin { get; private set; } = null;

		public static ConfigEntry<bool> ConfigEnableArriveNormal { get; private set; } = null;

		public static ConfigEntry<bool> ConfigEnableArriveChill { get; private set; } = null;

		public static ConfigEntry<bool> ConfigEnableLeaveSad { get; private set; } = null;

		public static ConfigEntry<bool> ConfigEnableLeaveFast { get; private set; } = null;

		public static ConfigEntry<bool> ConfigEnableLeaveChill { get; private set; } = null;

		public static ConfigEntry<bool> ConfigEnableTooLate { get; private set; } = null;

		public static ConfigEntry<bool> ConfigCutOffOnEventChange { get; private set; } = null;

		public static ConfigEntry<float> ConfigMonsterNearbyDistance { get; private set; } = null;

		public static ConfigEntry<bool> ConfigPlayEventsThroughShipSpeaker { get; private set; } = null;

		public static ConfigEntry<float> ConfigShipSpeakerSpatialBlend { get; private set; } = null;

		public static ConfigEntry<float> ConfigShipSpeakerMinDistance { get; private set; } = null;

		public static ConfigEntry<float> ConfigShipSpeakerMaxDistance { get; private set; } = null;

		public static ConfigEntry<float> ConfigMasterVolume { get; private set; } = null;

		public static ConfigEntry<float> ConfigJoinSoundVolume { get; private set; } = null;

		public static ConfigEntry<float> ConfigLeaveSoundVolume { get; private set; } = null;

		public static ConfigEntry<float> ConfigArriveNormalVolume { get; private set; } = null;

		public static ConfigEntry<float> ConfigArriveChillVolume { get; private set; } = null;

		public static ConfigEntry<float> ConfigLeaveSadVolume { get; private set; } = null;

		public static ConfigEntry<float> ConfigLeaveFastVolume { get; private set; } = null;

		public static ConfigEntry<float> ConfigLeaveChillVolume { get; private set; } = null;

		public static ConfigEntry<float> ConfigTooLateVolume { get; private set; } = null;

		private void Awake()
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			Instance = this;
			_pluginDirectory = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location) ?? Paths.PluginPath;
			InitConfig();
			SetupAudioSources();
			Harmony val = new Harmony("com.v0id.v0idsounds");
			val.PatchAll(typeof(PlayerConnectionPatches));
			val.PatchAll(typeof(ShipEventPatches));
			((MonoBehaviour)this).StartCoroutine(LoadAllAudioClips());
			Log.LogInfo((object)"V0idSounds v1.0.3 initialized successfully!");
		}

		private void InitConfig()
		{
			//IL_01b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bc: Expected O, but got Unknown
			//IL_0237: Unknown result type (might be due to invalid IL or missing references)
			//IL_0241: Expected O, but got Unknown
			ConfigEnableJoinSound = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableJoinSound", true, "Whether to play 'User joined your channel' when someone joins your multiplayer game.");
			ConfigEnableLeaveSound = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableLeaveSound", true, "Whether to play 'User left your channel' when someone leaves your multiplayer game.");
			ConfigPlayForSelfJoin = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "PlayForSelfJoin", true, "Whether to play 'User joined your channel' when you join someone else's multiplayer game.");
			ConfigEnableArriveNormal = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableArriveNormal", true, "Whether to play 'arrive_normal*' soundtrack when ship lands on a normal moon.");
			ConfigEnableArriveChill = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableArriveChill", true, "Whether to play 'arrive_chill*' soundtrack when ship lands on the Company moon.");
			ConfigEnableLeaveSad = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableLeaveSad", true, "Whether to play 'leave_sad*' soundtrack when all players have died and the ship departs on its own.");
			ConfigEnableLeaveFast = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableLeaveFast", true, "Whether to play 'leave_fast*' soundtrack when the ship departs and monsters are nearby.");
			ConfigEnableLeaveChill = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableLeaveChill", true, "Whether to play 'leave_chill*' soundtrack when the ship departs from the Company moon.");
			ConfigEnableTooLate = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableTooLate", true, "Whether to play 'toolate*' soundtrack when time runs out (midnight) and the ship departs on its own.");
			ConfigCutOffOnEventChange = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "CutOffOnEventChange", true, "Whether to cut off/stop playing sounds when events change (e.g. ship finishes landing/doors open, ship lifts off/enters orbit, round ends). When false, audio plays to completion.");
			ConfigMonsterNearbyDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Events", "MonsterNearbyDistance", 65f, "Distance in meters to check for monsters near the ship when deciding whether to play fast departure music.");
			ConfigPlayEventsThroughShipSpeaker = ((BaseUnityPlugin)this).Config.Bind<bool>("Ship Speaker", "PlayEventsThroughShipSpeaker", true, "Whether to play situational soundtracks (landing, departures, timeout) through the physical ship speaker inside the cabin.");
			ConfigShipSpeakerSpatialBlend = ((BaseUnityPlugin)this).Config.Bind<float>("Ship Speaker", "ShipSpeakerSpatialBlend", 0.85f, new ConfigDescription("Spatial blend for ship speaker audio (0.0 = pure 2D everywhere, 1.0 = fully 3D from ship speaker).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			ConfigShipSpeakerMinDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Ship Speaker", "ShipSpeakerMinDistance", 5f, "Minimum distance in meters for full volume from the ship speaker.");
			ConfigShipSpeakerMaxDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Ship Speaker", "ShipSpeakerMaxDistance", 95f, "Maximum distance in meters before ship speaker audio rolls off.");
			ConfigMasterVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Volume", "MasterVolume", 1f, new ConfigDescription("Master volume multiplier for all V0idSounds audio.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			ConfigJoinSoundVolume = BindSoundVolume("JoinSound", 0.8f, "Volume for 'User joined your channel' sound.");
			ConfigLeaveSoundVolume = BindSoundVolume("LeaveSound", 0.8f, "Volume for 'User left your channel' sound.");
			ConfigArriveNormalVolume = BindSoundVolume("ArriveNormal", 0.8f, "Volume for 'arrive_normal*' landing soundtrack.");
			ConfigArriveChillVolume = BindSoundVolume("ArriveChill", 0.8f, "Volume for 'arrive_chill*' Company landing soundtrack.");
			ConfigLeaveSadVolume = BindSoundVolume("LeaveSad", 0.8f, "Volume for 'leave_sad*' squad wiped departure soundtrack.");
			ConfigLeaveFastVolume = BindSoundVolume("LeaveFast", 0.8f, "Volume for 'leave_fast*' emergency/monsters departure soundtrack.");
			ConfigLeaveChillVolume = BindSoundVolume("LeaveChill", 0.8f, "Volume for 'leave_chill*' Company departure soundtrack.");
			ConfigTooLateVolume = BindSoundVolume("TooLate", 0.8f, "Volume for 'toolate*' midnight departure soundtrack.");
		}

		public ConfigEntry<float> BindSoundVolume(string soundName, float defaultVolume = 0.8f, string description = "")
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Expected O, but got Unknown
			if (SoundVolumeConfigs.TryGetValue(soundName, out ConfigEntry<float> value))
			{
				return value;
			}
			string text = (string.IsNullOrWhiteSpace(description) ? ("Volume level for '" + soundName + "' sound (0.0 to 1.0).") : description);
			ConfigEntry<float> val = ((BaseUnityPlugin)this).Config.Bind<float>("Volume", soundName + "Volume", defaultVolume, new ConfigDescription(text, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			SoundVolumeConfigs[soundName] = val;
			return val;
		}

		public float GetEffectiveVolume(string soundName, float fallbackVolume = 0.8f)
		{
			ConfigEntry<float> value;
			float num = (SoundVolumeConfigs.TryGetValue(soundName, out value) ? value.Value : fallbackVolume);
			return Mathf.Clamp01(ConfigMasterVolume.Value * num);
		}

		private void SetupAudioSources()
		{
			Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject);
			_voiceAudioSource = ((Component)this).gameObject.AddComponent<AudioSource>();
			_voiceAudioSource.spatialBlend = 0f;
			_voiceAudioSource.playOnAwake = false;
			_voiceAudioSource.loop = false;
			_voiceAudioSource.bypassEffects = true;
			_voiceAudioSource.bypassListenerEffects = true;
			_voiceAudioSource.bypassReverbZones = true;
			_eventAudioSource = ((Component)this).gameObject.AddComponent<AudioSource>();
			_eventAudioSource.spatialBlend = 0f;
			_eventAudioSource.playOnAwake = false;
			_eventAudioSource.loop = false;
			_eventAudioSource.bypassEffects = true;
			_eventAudioSource.bypassListenerEffects = true;
			_eventAudioSource.bypassReverbZones = true;
		}

		private AudioSource GetActiveEventAudioSource()
		{
			//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cc: Expected O, but got Unknown
			if (ConfigPlayEventsThroughShipSpeaker.Value && (Object)(object)StartOfRound.Instance != (Object)null)
			{
				Transform val = (((Object)(object)StartOfRound.Instance.speakerAudioSource != (Object)null) ? ((Component)StartOfRound.Instance.speakerAudioSource).transform : (((Object)(object)StartOfRound.Instance.shipDoorNode != (Object)null) ? StartOfRound.Instance.shipDoorNode : StartOfRound.Instance.elevatorTransform));
				if ((Object)(object)val != (Object)null)
				{
					if ((Object)(object)_shipSpeakerAudioSource == (Object)null || !Object.op_Implicit((Object)(object)_shipSpeakerAudioSource))
					{
						Transform val2 = val.Find("V0idShipSpeakerAudioSource");
						if ((Object)(object)val2 != (Object)null)
						{
							_shipSpeakerAudioSource = ((Component)val2).GetComponent<AudioSource>();
						}
						if ((Object)(object)_shipSpeakerAudioSource == (Object)null)
						{
							GameObject val3 = new GameObject("V0idShipSpeakerAudioSource");
							val3.transform.SetParent(val, false);
							_shipSpeakerAudioSource = val3.AddComponent<AudioSource>();
						}
					}
					_shipSpeakerAudioSource.spatialBlend = ConfigShipSpeakerSpatialBlend.Value;
					_shipSpeakerAudioSource.minDistance = ConfigShipSpeakerMinDistance.Value;
					_shipSpeakerAudioSource.maxDistance = ConfigShipSpeakerMaxDistance.Value;
					_shipSpeakerAudioSource.rolloffMode = (AudioRolloffMode)1;
					_shipSpeakerAudioSource.playOnAwake = false;
					_shipSpeakerAudioSource.loop = false;
					_shipSpeakerAudioSource.dopplerLevel = 0f;
					return _shipSpeakerAudioSource;
				}
			}
			return _eventAudioSource;
		}

		private IEnumerator LoadAllAudioClips()
		{
			string path = Path.Combine(_pluginDirectory, "Audio");
			List<string> list = new List<string>();
			if (Directory.Exists(path))
			{
				list.AddRange(Directory.GetFiles(path, "*.*", SearchOption.AllDirectories));
			}
			if (Directory.Exists(_pluginDirectory))
			{
				list.AddRange(Directory.GetFiles(_pluginDirectory, "*.*", SearchOption.TopDirectoryOnly));
			}
			HashSet<string> supportedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".wav", ".mp3", ".ogg" };
			List<string> list2 = list.Where((string f) => supportedExtensions.Contains(Path.GetExtension(f))).Distinct<string>(StringComparer.OrdinalIgnoreCase).ToList();
			Log.LogInfo((object)$"Found {list2.Count} candidate audio file(s) on disk.");
			foreach (string item in list2)
			{
				string fileNameWithoutExt = Path.GetFileNameWithoutExtension(item);
				string category = GetCategoryFromFileName(fileNameWithoutExt);
				yield return ((MonoBehaviour)this).StartCoroutine(LoadClipFromFile(item, delegate(AudioClip clip)
				{
					AddClipToPool(category, clip);
					Log.LogInfo((object)("Loaded audio '" + fileNameWithoutExt + "' into category '" + category + "'."));
				}));
			}
			string[] array = new string[8] { "user_joined", "user_left", "arrive_normal", "arrive_chill", "leave_sad", "leave_fast", "leave_chill", "toolate" };
			foreach (string text in array)
			{
				if (!_audioPools.ContainsKey(text) || _audioPools[text].Count == 0)
				{
					TryExtractAndLoadEmbedded(text);
				}
			}
			SortAllPools();
			Log.LogInfo((object)"All V0idSounds audio pools initialized successfully.");
		}

		private void SortAllPools()
		{
			foreach (KeyValuePair<string, List<AudioClip>> audioPool in _audioPools)
			{
				audioPool.Value.Sort((AudioClip a, AudioClip b) => string.Compare(((Object)a).name, ((Object)b).name, StringComparison.OrdinalIgnoreCase));
				string arg = string.Join(", ", audioPool.Value.Select((AudioClip c) => ((Object)c).name));
				Log.LogInfo((object)$"Audio pool '{audioPool.Key}' ({audioPool.Value.Count} tracks): [{arg}]");
			}
		}

		private string GetCategoryFromFileName(string name)
		{
			string text = name.ToLowerInvariant();
			if (text.StartsWith("user_joined"))
			{
				return "user_joined";
			}
			if (text.StartsWith("user_left"))
			{
				return "user_left";
			}
			if (text.StartsWith("arrive_chill"))
			{
				return "arrive_chill";
			}
			if (text.StartsWith("arrive_normal"))
			{
				return "arrive_normal";
			}
			if (text.StartsWith("leave_sad"))
			{
				return "leave_sad";
			}
			if (text.StartsWith("leave_fast"))
			{
				return "leave_fast";
			}
			if (text.StartsWith("leave_chill"))
			{
				return "leave_chill";
			}
			if (text.StartsWith("toolate"))
			{
				return "toolate";
			}
			return text;
		}

		private void AddClipToPool(string category, AudioClip clip)
		{
			if (!_audioPools.TryGetValue(category, out List<AudioClip> value))
			{
				value = new List<AudioClip>();
				_audioPools[category] = value;
			}
			value.Add(clip);
		}

		private IEnumerator LoadClipFromFile(string fullPath, Action<AudioClip> onLoaded)
		{
			AudioType val = (AudioType)(Path.GetExtension(fullPath).ToLowerInvariant() switch
			{
				".wav" => 20, 
				".mp3" => 13, 
				".ogg" => 14, 
				_ => 0, 
			});
			string uri = "file://" + Path.GetFullPath(fullPath).Replace("\\", "/");
			UnityWebRequest uwr = UnityWebRequestMultimedia.GetAudioClip(uri, val);
			try
			{
				yield return uwr.SendWebRequest();
				if ((int)uwr.result != 1)
				{
					Log.LogError((object)("Error loading audio from '" + uri + "': " + uwr.error));
					yield break;
				}
				AudioClip content = DownloadHandlerAudioClip.GetContent(uwr);
				if ((Object)(object)content != (Object)null)
				{
					((Object)content).name = Path.GetFileNameWithoutExtension(fullPath);
					onLoaded(content);
				}
				else
				{
					Log.LogError((object)("AudioClip from '" + uri + "' was null."));
				}
			}
			finally
			{
				((IDisposable)uwr)?.Dispose();
			}
		}

		private void TryExtractAndLoadEmbedded(string category)
		{
			string[] obj = new string[3] { ".wav", ".mp3", ".ogg" };
			Assembly executingAssembly = Assembly.GetExecutingAssembly();
			string[] array = obj;
			foreach (string ext in array)
			{
				string name = "V0idSounds.Audio." + category + ext;
				using Stream stream = executingAssembly.GetManifestResourceStream(name);
				if (stream != null)
				{
					string text = Path.Combine(_pluginDirectory, "Audio", category + ext);
					string directoryName = Path.GetDirectoryName(text);
					if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
					{
						Directory.CreateDirectory(directoryName);
					}
					using (FileStream destination = new FileStream(text, FileMode.Create, FileAccess.Write))
					{
						stream.CopyTo(destination);
					}
					((MonoBehaviour)this).StartCoroutine(LoadClipFromFile(text, delegate(AudioClip clip)
					{
						AddClipToPool(category, clip);
						SortAllPools();
						Log.LogInfo((object)("Extracted and loaded embedded audio '" + category + ext + "'."));
					}));
					break;
				}
			}
		}

		public AudioClip? GetSyncedClip(string category, int extraSalt = 0)
		{
			if (_audioPools.TryGetValue(category, out List<AudioClip> value) && value.Count > 0)
			{
				if (value.Count == 1)
				{
					return value[0];
				}
				int value2;
				int num = (_playCountPerCategory.TryGetValue(category, out value2) ? value2 : 0);
				_playCountPerCategory[category] = num + 1;
				int num2 = 0;
				if ((Object)(object)StartOfRound.Instance != (Object)null)
				{
					num2 = StartOfRound.Instance.randomMapSeed;
					if (StartOfRound.Instance.gameStats != null)
					{
						num2 += StartOfRound.Instance.gameStats.daysSpent * 7919;
					}
				}
				if (num2 == 0 && (Object)(object)TimeOfDay.Instance != (Object)null)
				{
					num2 = (int)TimeOfDay.Instance.globalTime;
				}
				if (num2 == 0)
				{
					num2 = 1337;
				}
				int hashCode = category.GetHashCode();
				Random random = new Random(num2 + num * 31337 + hashCode + extraSalt);
				int num3 = random.Next(value.Count);
				if (value.Count > 1 && _lastPlayedIndexPerCategory.TryGetValue(category, out var value3) && num3 == value3)
				{
					num3 = (num3 + 1 + random.Next(value.Count - 1)) % value.Count;
				}
				_lastPlayedIndexPerCategory[category] = num3;
				Log.LogInfo((object)$"Shuffled '{category}' -> '{((Object)value[num3]).name}' (Pool size: {value.Count}, Index: {num3}, PlayCount: {num + 1}).");
				return value[num3];
			}
			return null;
		}

		public void PlayJoinSound(ulong clientId = 0uL)
		{
			if (ConfigEnableJoinSound.Value)
			{
				AudioClip syncedClip = GetSyncedClip("user_joined", (int)clientId * 19);
				if ((Object)(object)syncedClip != (Object)null && (Object)(object)_voiceAudioSource != (Object)null)
				{
					float effectiveVolume = GetEffectiveVolume("JoinSound");
					Log.LogInfo((object)$"Playing synchronized 'user_joined' sound ({((Object)syncedClip).name}) at volume {effectiveVolume:F2}.");
					_voiceAudioSource.PlayOneShot(syncedClip, effectiveVolume);
				}
				else
				{
					Log.LogWarning((object)"No audio clip available for 'user_joined'.");
				}
			}
		}

		public void PlayLeaveSound(ulong clientId = 0uL)
		{
			if (ConfigEnableLeaveSound.Value)
			{
				AudioClip syncedClip = GetSyncedClip("user_left", (int)clientId * 23);
				if ((Object)(object)syncedClip != (Object)null && (Object)(object)_voiceAudioSource != (Object)null)
				{
					float effectiveVolume = GetEffectiveVolume("LeaveSound");
					Log.LogInfo((object)$"Playing synchronized 'user_left' sound ({((Object)syncedClip).name}) at volume {effectiveVolume:F2}.");
					_voiceAudioSource.PlayOneShot(syncedClip, effectiveVolume);
				}
				else
				{
					Log.LogWarning((object)"No audio clip available for 'user_left'.");
				}
			}
		}

		public void PlayEventSound(string category, string volumeKey, int eventSalt = 0, bool isEnabled = true)
		{
			if (!isEnabled)
			{
				return;
			}
			if (ShipEventPatches.IsTimeRanOutLeaving && !category.Equals("toolate", StringComparison.OrdinalIgnoreCase))
			{
				Log.LogInfo((object)("Suppressed event sound '" + category + "' because time ran out ('toolate') is active."));
				return;
			}
			AudioClip syncedClip = GetSyncedClip(category, eventSalt);
			if ((Object)(object)syncedClip == (Object)null)
			{
				Log.LogWarning((object)("No audio clips found in pool for event '" + category + "'."));
				return;
			}
			if (_fadeCoroutine != null)
			{
				((MonoBehaviour)this).StopCoroutine(_fadeCoroutine);
				_fadeCoroutine = null;
			}
			AudioSource activeEventAudioSource = GetActiveEventAudioSource();
			float effectiveVolume = GetEffectiveVolume(volumeKey);
			Log.LogInfo((object)string.Format("Playing synchronized event sound '{0}' ({1}) through {2} at volume {3:F2}.", category, ((Object)syncedClip).name, ((Object)(object)activeEventAudioSource == (Object)(object)_shipSpeakerAudioSource) ? "Ship Speaker" : "2D Event Audio", effectiveVolume));
			activeEventAudioSource.Stop();
			activeEventAudioSource.clip = syncedClip;
			activeEventAudioSource.volume = effectiveVolume;
			activeEventAudioSource.Play();
		}

		public void StopEventSound(bool fade = true)
		{
			if (!ConfigCutOffOnEventChange.Value)
			{
				Log.LogInfo((object)"CutOffOnEventChange is disabled; sound will continue to play.");
				return;
			}
			AudioSource[] array = (AudioSource[])(object)new AudioSource[2] { _eventAudioSource, _shipSpeakerAudioSource };
			foreach (AudioSource val in array)
			{
				if (!((Object)(object)val != (Object)null) || !val.isPlaying)
				{
					continue;
				}
				if (fade)
				{
					if (_fadeCoroutine != null)
					{
						((MonoBehaviour)this).StopCoroutine(_fadeCoroutine);
					}
					_fadeCoroutine = ((MonoBehaviour)this).StartCoroutine(FadeOutAudioSource(val, 1.2f));
				}
				else
				{
					val.Stop();
				}
			}
		}

		private IEnumerator FadeOutAudioSource(AudioSource source, float duration)
		{
			float startVol = source.volume;
			float elapsed = 0f;
			while (elapsed < duration && (Object)(object)source != (Object)null && source.isPlaying)
			{
				elapsed += Time.deltaTime;
				source.volume = Mathf.Lerp(startVol, 0f, elapsed / duration);
				yield return null;
			}
			if ((Object)(object)source != (Object)null)
			{
				source.Stop();
				source.volume = startVol;
			}
			_fadeCoroutine = null;
		}

		public void PlaySound(AudioClip clip, string soundName, float defaultVolume = 0.8f)
		{
			if (!((Object)(object)clip == (Object)null) && !((Object)(object)_voiceAudioSource == (Object)null))
			{
				BindSoundVolume(soundName, defaultVolume);
				float effectiveVolume = GetEffectiveVolume(soundName, defaultVolume);
				Log.LogInfo((object)$"Playing custom sound '{soundName}' at volume {effectiveVolume:F2}.");
				_voiceAudioSource.PlayOneShot(clip, effectiveVolume);
			}
		}
	}
}
namespace V0idSounds.Patches
{
	[HarmonyPatch(typeof(StartOfRound))]
	public static class PlayerConnectionPatches
	{
		[HarmonyPatch("OnPlayerConnectedClientRpc")]
		[HarmonyPostfix]
		public static void Postfix_OnPlayerConnectedClientRpc(ulong clientId)
		{
			try
			{
				if (!((Object)(object)NetworkManager.Singleton == (Object)null))
				{
					if (clientId != NetworkManager.Singleton.LocalClientId)
					{
						Plugin.Log.LogInfo((object)$"Player connected with ClientId #{clientId}. Triggering join sound.");
						Plugin.Instance.PlayJoinSound(clientId);
					}
					else if (Plugin.ConfigPlayForSelfJoin.Value)
					{
						Plugin.Log.LogInfo((object)$"Local player joined game (ClientId #{clientId}). Triggering join sound.");
						Plugin.Instance.PlayJoinSound(clientId);
					}
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Exception occurred in Postfix_OnPlayerConnectedClientRpc: {arg}");
			}
		}

		[HarmonyPatch("OnPlayerDC")]
		[HarmonyPostfix]
		public static void Postfix_OnPlayerDC(int playerObjectNumber, ulong clientId)
		{
			try
			{
				if (!((Object)(object)NetworkManager.Singleton == (Object)null) && (!((Object)(object)GameNetworkManager.Instance != (Object)null) || !GameNetworkManager.Instance.isDisconnecting) && clientId != NetworkManager.Singleton.LocalClientId)
				{
					Plugin.Log.LogInfo((object)$"Player #{playerObjectNumber} (ClientId #{clientId}) disconnected. Triggering leave sound.");
					Plugin.Instance.PlayLeaveSound(clientId);
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Exception occurred in Postfix_OnPlayerDC: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(StartOfRound))]
	public static class ShipEventPatches
	{
		public static bool IsTimeRanOutLeaving { get; set; }

		[HarmonyPatch("openingDoorsSequence")]
		[HarmonyPrefix]
		public static void Prefix_openingDoorsSequence(StartOfRound __instance)
		{
			try
			{
				IsTimeRanOutLeaving = false;
				if (!((Object)(object)__instance == (Object)null) && !((Object)(object)__instance.currentLevel == (Object)null))
				{
					if (IsCompanyMoon(__instance.currentLevel))
					{
						Plugin.Log.LogInfo((object)"Ship landing on Company moon -> Playing 'arrive_chill'.");
						Plugin.Instance.PlayEventSound("arrive_chill", "ArriveChill", 1122867, Plugin.ConfigEnableArriveChill.Value);
					}
					else
					{
						Plugin.Log.LogInfo((object)("Ship landing on moon '" + __instance.currentLevel.PlanetName + "' -> Playing 'arrive_normal'."));
						Plugin.Instance.PlayEventSound("arrive_normal", "ArriveNormal", 4478310, Plugin.ConfigEnableArriveNormal.Value);
					}
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Error in Prefix_openingDoorsSequence: {arg}");
			}
		}

		[HarmonyPatch("OnShipLandedMiscEvents")]
		[HarmonyPostfix]
		public static void Postfix_OnShipLandedMiscEvents()
		{
			try
			{
				Plugin.Log.LogInfo((object)"Ship doors opened / landed -> Stopping landing event sound (if cutoff enabled).");
				Plugin.Instance.StopEventSound();
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Error in Postfix_OnShipLandedMiscEvents: {arg}");
			}
		}

		[HarmonyPatch("ShipLeave")]
		[HarmonyPrefix]
		public static void Prefix_ShipLeave(StartOfRound __instance)
		{
			try
			{
				if (!((Object)(object)__instance == (Object)null) && !((Object)(object)__instance.currentLevel == (Object)null))
				{
					if (IsTimeRanOutLeaving || __instance.shipLeftAutomatically)
					{
						Plugin.Log.LogInfo((object)"Ship is leaving automatically (time ran out / squad wipe); skipping manual ShipLeave sounds.");
					}
					else if (IsCompanyMoon(__instance.currentLevel))
					{
						Plugin.Log.LogInfo((object)"Ship departing from Company moon -> Playing 'leave_chill'.");
						Plugin.Instance.PlayEventSound("leave_chill", "LeaveChill", 7833753, Plugin.ConfigEnableLeaveChill.Value);
					}
					else if (AreMonstersNearby(__instance))
					{
						Plugin.Log.LogInfo((object)"Ship departing with monsters nearby -> Playing 'leave_fast'.");
						Plugin.Instance.PlayEventSound("leave_fast", "LeaveFast", 11189196, Plugin.ConfigEnableLeaveFast.Value);
					}
					else
					{
						Plugin.Log.LogInfo((object)"Ship departing normal moon -> Playing departure sound ('leave_fast').");
						Plugin.Instance.PlayEventSound("leave_fast", "LeaveFast", 11189196, Plugin.ConfigEnableLeaveFast.Value);
					}
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Error in Prefix_ShipLeave: {arg}");
			}
		}

		[HarmonyPatch("ShipLeaveAutomatically")]
		[HarmonyPrefix]
		public static void Prefix_ShipLeaveAutomatically(StartOfRound __instance, bool leavingOnMidnight = false)
		{
			try
			{
				if (!((Object)(object)__instance == (Object)null))
				{
					if (leavingOnMidnight || (!__instance.allPlayersDead && __instance.livingPlayers > 0))
					{
						IsTimeRanOutLeaving = true;
						Plugin.Log.LogInfo((object)"Time ran out (midnight) and ship is departing -> Playing 'toolate' ONLY.");
						Plugin.Instance.PlayEventSound("toolate", "TooLate", 14544639, Plugin.ConfigEnableTooLate.Value);
					}
					else
					{
						IsTimeRanOutLeaving = false;
						Plugin.Log.LogInfo((object)"All players have died and ship is departing -> Playing 'leave_sad'.");
						Plugin.Instance.PlayEventSound("leave_sad", "LeaveSad", 10061943, Plugin.ConfigEnableLeaveSad.Value);
					}
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Error in Prefix_ShipLeaveAutomatically: {arg}");
			}
		}

		[HarmonyPatch("ShipHasLeft")]
		[HarmonyPostfix]
		public static void Postfix_ShipHasLeft()
		{
			try
			{
				Plugin.Log.LogInfo((object)"Ship has left moon -> Stopping departure sound (if cutoff enabled).");
				Plugin.Instance.StopEventSound();
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Error in Postfix_ShipHasLeft: {arg}");
			}
		}

		[HarmonyPatch("EndOfGameClientRpc")]
		[HarmonyPostfix]
		public static void Postfix_EndOfGameClientRpc()
		{
			try
			{
				IsTimeRanOutLeaving = false;
				Plugin.Instance.StopEventSound(fade: false);
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Error in Postfix_EndOfGameClientRpc: {arg}");
			}
		}

		private static bool IsCompanyMoon(SelectableLevel level)
		{
			if ((Object)(object)level == (Object)null)
			{
				return false;
			}
			if (level.levelID == 3)
			{
				return true;
			}
			if (!string.IsNullOrEmpty(level.PlanetName) && level.PlanetName.IndexOf("Gordion", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				return true;
			}
			if (!level.planetHasTime && !level.spawnEnemiesAndScrap)
			{
				return true;
			}
			return false;
		}

		private static bool AreMonstersNearby(StartOfRound sor)
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				Vector3 val = (((Object)(object)sor.shipDoorNode != (Object)null) ? sor.shipDoorNode.position : (((Object)(object)sor.elevatorTransform != (Object)null) ? sor.elevatorTransform.position : Vector3.zero));
				float value = Plugin.ConfigMonsterNearbyDistance.Value;
				if ((Object)(object)RoundManager.Instance != (Object)null && RoundManager.Instance.SpawnedEnemies != null)
				{
					foreach (EnemyAI spawnedEnemy in RoundManager.Instance.SpawnedEnemies)
					{
						if (!((Object)(object)spawnedEnemy == (Object)null) && !spawnedEnemy.isEnemyDead && Vector3.Distance(((Component)spawnedEnemy).transform.position, val) <= value)
						{
							return true;
						}
					}
				}
				EnemyAI[] array = Object.FindObjectsOfType<EnemyAI>();
				if (array != null)
				{
					EnemyAI[] array2 = array;
					foreach (EnemyAI val2 in array2)
					{
						if (!((Object)(object)val2 == (Object)null) && !val2.isEnemyDead && Vector3.Distance(((Component)val2).transform.position, val) <= value)
						{
							return true;
						}
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Error checking nearby monsters: " + ex.Message));
			}
			return false;
		}
	}
}