Decompiled source of GambitMainMusicReplacer v0.3.0

BepInEx\plugins\GambitMainMusicReplacer\GambitRouletteMusic.dll

Decompiled 14 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.Networking;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("GambitRouletteMusic")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("0.3.0.0")]
[assembly: AssemblyInformationalVersion("0.3.0")]
[assembly: AssemblyProduct("GambitRouletteMusic")]
[assembly: AssemblyTitle("GambitRouletteMusic")]
[assembly: AssemblyVersion("0.3.0.0")]
[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 GambitRouletteMusic
{
	internal static class AudioFileLoader
	{
		internal static IEnumerator LoadFromPluginFolder(string fileName, Action<AudioClip?> onLoaded)
		{
			string pluginDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			if (string.IsNullOrWhiteSpace(pluginDirectory))
			{
				Plugin.Log.LogError((object)"Could not determine the plugin directory.");
				onLoaded(null);
				yield break;
			}
			string fullPath = Path.Combine(pluginDirectory, fileName);
			if (!File.Exists(fullPath))
			{
				Plugin.Log.LogWarning((object)("Replacement music was not found: " + fullPath));
				Plugin.Log.LogWarning((object)"Place the configured OGG, WAV or MP3 file next to the DLL.");
				onLoaded(null);
				yield break;
			}
			AudioType audioType = GetAudioType(Path.GetExtension(fullPath));
			if ((int)audioType == 0)
			{
				Plugin.Log.LogError((object)("Unsupported audio extension: " + Path.GetExtension(fullPath)));
				onLoaded(null);
				yield break;
			}
			string fileUri = new Uri(fullPath).AbsoluteUri;
			UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(fileUri, audioType);
			try
			{
				yield return request.SendWebRequest();
				if ((int)request.result != 1)
				{
					Plugin.Log.LogError((object)("Could not load replacement music: " + request.error));
					onLoaded(null);
					yield break;
				}
				AudioClip clip = DownloadHandlerAudioClip.GetContent(request);
				if ((Object)(object)clip == (Object)null)
				{
					Plugin.Log.LogError((object)"Unity returned an empty AudioClip.");
					onLoaded(null);
				}
				else
				{
					((Object)clip).name = "GambitMainMusic_" + Path.GetFileNameWithoutExtension(fullPath);
					Plugin.Log.LogInfo((object)$"Replacement music loaded: {Path.GetFileName(fullPath)} ({clip.length:0.00}s).");
					onLoaded(clip);
				}
			}
			finally
			{
				((IDisposable)request)?.Dispose();
			}
		}

		private static AudioType GetAudioType(string extension)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: 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_0045: Unknown result type (might be due to invalid IL or missing references)
			string text = extension.ToLowerInvariant();
			if (1 == 0)
			{
			}
			AudioType result = (AudioType)(text switch
			{
				".ogg" => 14, 
				".wav" => 20, 
				".mp3" => 13, 
				_ => 0, 
			});
			if (1 == 0)
			{
			}
			return result;
		}
	}
	[BepInPlugin("com.artem.gambitroulettemusic", "Gambit Main Music Replacer", "0.3.0")]
	[BepInProcess("REPO.exe")]
	public sealed class Plugin : BaseUnityPlugin
	{
		private sealed class GambitMusicSession
		{
			private readonly Dictionary<int, TrackedSource> _sources = new Dictionary<int, TrackedSource>();

			private readonly Transform _root;

			private bool _active;

			private double _startDspTime;

			private double _noPlayingSince = -1.0;

			internal IEnumerable<int> SourceIds => _sources.Keys;

			internal bool ShouldRemove => (Object)(object)_root == (Object)null || _sources.Count == 0;

			internal GambitMusicSession(Transform root)
			{
				_root = root;
			}

			internal void AddSource(AudioSource source, AudioClip originalClip, AudioClip replacement, bool forceLoop)
			{
				int instanceID = ((Object)source).GetInstanceID();
				if (!_sources.ContainsKey(instanceID))
				{
					bool isPlaying = source.isPlaying;
					bool loop = source.loop;
					if (isPlaying)
					{
						source.Stop();
					}
					source.clip = replacement;
					if (forceLoop)
					{
						source.loop = true;
					}
					if (isPlaying)
					{
						TrySetTime(source, 0f);
						source.Play();
					}
					_sources.Add(instanceID, new TrackedSource(source, originalClip, loop, isPlaying));
				}
			}

			internal void Update(AudioClip replacement, bool forceLoop, float startVolumeThreshold, float endGraceSeconds, float syncToleranceSeconds)
			{
				RemoveDestroyedOrRepurposedSources(replacement);
				if ((Object)(object)_root == (Object)null || _sources.Count == 0)
				{
					return;
				}
				if (!((Component)_root).gameObject.activeInHierarchy)
				{
					StopSession(resetToStart: true, stopPlaying: true);
					return;
				}
				bool flag = false;
				bool flag2 = false;
				foreach (TrackedSource value in _sources.Values)
				{
					AudioSource source = value.Source;
					if ((Object)(object)source == (Object)null)
					{
						continue;
					}
					bool flag3 = EnsureReplacementClip(value, replacement, forceLoop, _active ? GetExpectedTime(replacement) : 0f);
					bool isPlaying = source.isPlaying;
					flag = flag || isPlaying;
					flag2 |= isPlaying && source.volume > startVolumeThreshold && !source.mute;
					if (_active && isPlaying)
					{
						float expectedTime = GetExpectedTime(replacement);
						bool flag4 = !value.WasPlaying;
						if (flag4 || flag3)
						{
							SeekPlayingSource(source, expectedTime);
						}
						else if (IsOutOfSync(source, expectedTime, replacement.length, syncToleranceSeconds))
						{
							SeekPlayingSource(source, expectedTime);
						}
					}
					value.WasPlaying = source.isPlaying;
				}
				if (!_active)
				{
					if (flag2)
					{
						StartSession(replacement, forceLoop);
					}
					return;
				}
				if (flag)
				{
					_noPlayingSince = -1.0;
					return;
				}
				double dspTime = AudioSettings.dspTime;
				if (_noPlayingSince < 0.0)
				{
					_noPlayingSince = dspTime;
				}
				else if (dspTime - _noPlayingSince >= (double)Math.Max(0f, endGraceSeconds))
				{
					StopSession(resetToStart: true, stopPlaying: false);
				}
			}

			private void StartSession(AudioClip replacement, bool forceLoop)
			{
				_active = true;
				_startDspTime = AudioSettings.dspTime;
				_noPlayingSince = -1.0;
				foreach (TrackedSource value in _sources.Values)
				{
					AudioSource source = value.Source;
					if (!((Object)(object)source == (Object)null))
					{
						bool isPlaying = source.isPlaying;
						if (isPlaying)
						{
							source.Stop();
						}
						source.clip = replacement;
						if (forceLoop)
						{
							source.loop = true;
						}
						if (isPlaying)
						{
							TrySetTime(source, 0f);
							source.Play();
						}
						value.WasPlaying = source.isPlaying;
					}
				}
				Log.LogInfo((object)("[MUSIC START] Gambit main music started from 0:00 at '" + GetTransformPath(_root) + "'."));
			}

			private void StopSession(bool resetToStart, bool stopPlaying)
			{
				if (!_active)
				{
					return;
				}
				_active = false;
				_noPlayingSince = -1.0;
				foreach (TrackedSource value in _sources.Values)
				{
					AudioSource source = value.Source;
					if (!((Object)(object)source == (Object)null))
					{
						if (stopPlaying && source.isPlaying)
						{
							source.Stop();
						}
						if (resetToStart && !source.isPlaying)
						{
							TrySetTime(source, 0f);
						}
						value.WasPlaying = source.isPlaying;
					}
				}
				Log.LogInfo((object)("[MUSIC STOP] Gambit main music session ended at '" + GetTransformPath(_root) + "'."));
			}

			private float GetExpectedTime(AudioClip replacement)
			{
				if (!_active || replacement.length <= 0.001f)
				{
					return 0f;
				}
				double num = Math.Max(0.0, AudioSettings.dspTime - _startDspTime);
				return (float)(num % (double)replacement.length);
			}

			private static bool EnsureReplacementClip(TrackedSource tracked, AudioClip replacement, bool forceLoop, float expectedTime)
			{
				AudioSource source = tracked.Source;
				if ((Object)(object)source == (Object)null)
				{
					return false;
				}
				if (forceLoop)
				{
					source.loop = true;
				}
				if ((Object)(object)source.clip == (Object)(object)replacement)
				{
					return false;
				}
				AudioClip clip = source.clip;
				if ((Object)(object)clip == (Object)null || !TargetClipNames.Contains(((Object)clip).name))
				{
					return false;
				}
				bool isPlaying = source.isPlaying;
				if (isPlaying)
				{
					source.Stop();
				}
				source.clip = replacement;
				TrySetTime(source, expectedTime);
				if (isPlaying)
				{
					source.Play();
				}
				return true;
			}

			private static bool IsOutOfSync(AudioSource source, float expectedTime, float clipLength, float tolerance)
			{
				if (clipLength <= 0.001f)
				{
					return false;
				}
				float time;
				try
				{
					time = source.time;
				}
				catch
				{
					return false;
				}
				float num = Mathf.Abs(time - expectedTime);
				float num2 = clipLength - num;
				float num3 = Mathf.Min(num, num2);
				return num3 > Mathf.Max(0.05f, tolerance);
			}

			private static void SeekPlayingSource(AudioSource source, float expectedTime)
			{
				bool isPlaying = source.isPlaying;
				if (isPlaying)
				{
					source.Stop();
				}
				TrySetTime(source, expectedTime);
				if (isPlaying)
				{
					source.Play();
				}
			}

			private static void TrySetTime(AudioSource source, float time)
			{
				try
				{
					source.time = Mathf.Max(0f, time);
				}
				catch
				{
					try
					{
						source.timeSamples = 0;
					}
					catch
					{
					}
				}
			}

			private void RemoveDestroyedOrRepurposedSources(AudioClip replacement)
			{
				List<int> list = null;
				foreach (KeyValuePair<int, TrackedSource> source2 in _sources)
				{
					AudioSource source = source2.Value.Source;
					if ((Object)(object)source == (Object)null)
					{
						if (list == null)
						{
							list = new List<int>();
						}
						list.Add(source2.Key);
						continue;
					}
					AudioClip clip = source.clip;
					if (!((Object)(object)clip == (Object)(object)replacement) && (!((Object)(object)clip != (Object)null) || !TargetClipNames.Contains(((Object)clip).name)))
					{
						if (list == null)
						{
							list = new List<int>();
						}
						list.Add(source2.Key);
					}
				}
				if (list == null)
				{
					return;
				}
				foreach (int item in list)
				{
					_sources.Remove(item);
				}
			}

			internal void Restore(AudioClip? replacementClip)
			{
				foreach (TrackedSource value in _sources.Values)
				{
					AudioSource source = value.Source;
					if (!((Object)(object)source == (Object)null) && !((Object)(object)source.clip != (Object)(object)replacementClip))
					{
						bool isPlaying = source.isPlaying;
						if (isPlaying)
						{
							source.Stop();
						}
						source.clip = value.OriginalClip;
						source.loop = value.OriginalLoop;
						TrySetTime(source, 0f);
						if (isPlaying)
						{
							source.Play();
						}
					}
				}
			}
		}

		private sealed class TrackedSource
		{
			internal AudioSource Source { get; }

			internal AudioClip OriginalClip { get; }

			internal bool OriginalLoop { get; }

			internal bool WasPlaying { get; set; }

			internal TrackedSource(AudioSource source, AudioClip originalClip, bool originalLoop, bool wasPlaying)
			{
				Source = source;
				OriginalClip = originalClip;
				OriginalLoop = originalLoop;
				WasPlaying = wasPlaying;
			}
		}

		public const string PluginGuid = "com.artem.gambitroulettemusic";

		public const string PluginName = "Gambit Main Music Replacer";

		public const string PluginVersion = "0.3.0";

		private static readonly HashSet<string> TargetClipNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "enemy spinny music", "enemy spinny music muffled", "enemy spinny music leaving" };

		private ConfigEntry<bool> _modEnabled = null;

		private ConfigEntry<string> _audioFileName = null;

		private ConfigEntry<float> _scanInterval = null;

		private ConfigEntry<float> _startVolumeThreshold = null;

		private ConfigEntry<float> _endGraceSeconds = null;

		private ConfigEntry<float> _syncToleranceSeconds = null;

		private ConfigEntry<bool> _forceLoop = null;

		private AudioClip? _replacementClip;

		private Coroutine? _monitorCoroutine;

		private bool _sourcesRestoredForDisabledState;

		private readonly Dictionary<int, GambitMusicSession> _sessions = new Dictionary<int, GambitMusicSession>();

		private readonly Dictionary<int, int> _sourceToSession = new Dictionary<int, int>();

		internal static ManualLogSource Log { get; private set; } = null;

		private void Awake()
		{
			//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ad: Expected O, but got Unknown
			//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Expected O, but got Unknown
			//IL_011f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0129: Expected O, but got Unknown
			//IL_015d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0167: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			_modEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enables replacement of Gambit's main music.");
			_audioFileName = ((BaseUnityPlugin)this).Config.Bind<string>("Audio", "AudioFileName", "roulette.ogg", "Replacement track placed next to the DLL. Supported: .ogg, .wav and .mp3.");
			_forceLoop = ((BaseUnityPlugin)this).Config.Bind<bool>("Playback", "ForceLoop", true, "Loops the replacement track until Gambit's music sources stop or the enemy leaves.");
			_startVolumeThreshold = ((BaseUnityPlugin)this).Config.Bind<float>("Playback", "StartVolumeThreshold", 0.005f, new ConfigDescription("The track restarts from 0:00 when Gambit's music first becomes audible.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.25f), Array.Empty<object>()));
			_endGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Playback", "EndGraceSeconds", 0.6f, new ConfigDescription("Prevents a brief game-side source switch from ending the current music session.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>()));
			_syncToleranceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Playback", "SyncToleranceSeconds", 0.35f, new ConfigDescription("Maximum allowed drift between the normal, muffled and leaving music sources.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 2f), Array.Empty<object>()));
			_scanInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Advanced", "ScanIntervalSeconds", 0.05f, new ConfigDescription("How often the mod checks Gambit's music AudioSources.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.02f, 1f), Array.Empty<object>()));
			((MonoBehaviour)this).StartCoroutine(AudioFileLoader.LoadFromPluginFolder(_audioFileName.Value, OnReplacementLoaded));
			Log.LogInfo((object)"Gambit Main Music Replacer v0.3.0 loaded.");
			Log.LogInfo((object)"The replacement starts at 0:00, stays synchronized and stops with Gambit.");
		}

		private void OnReplacementLoaded(AudioClip? clip)
		{
			_replacementClip = clip;
			if (!((Object)(object)clip == (Object)null))
			{
				if (_monitorCoroutine != null)
				{
					((MonoBehaviour)this).StopCoroutine(_monitorCoroutine);
				}
				_monitorCoroutine = ((MonoBehaviour)this).StartCoroutine(MonitorGambitMusic());
			}
		}

		private IEnumerator MonitorGambitMusic()
		{
			while (true)
			{
				if (!_modEnabled.Value)
				{
					if (!_sourcesRestoredForDisabledState)
					{
						RestoreAllSources();
						_sourcesRestoredForDisabledState = true;
					}
				}
				else
				{
					_sourcesRestoredForDisabledState = false;
					UpdateGambitMusicSessions();
				}
				yield return (object)new WaitForSecondsRealtime(Mathf.Max(0.02f, _scanInterval.Value));
			}
		}

		private void UpdateGambitMusicSessions()
		{
			AudioClip replacementClip = _replacementClip;
			if ((Object)(object)replacementClip == (Object)null)
			{
				return;
			}
			DiscoverTargetSources(replacementClip);
			List<int> list = null;
			foreach (KeyValuePair<int, GambitMusicSession> session in _sessions)
			{
				GambitMusicSession value = session.Value;
				value.Update(replacementClip, _forceLoop.Value, _startVolumeThreshold.Value, _endGraceSeconds.Value, _syncToleranceSeconds.Value);
				if (value.ShouldRemove)
				{
					if (list == null)
					{
						list = new List<int>();
					}
					list.Add(session.Key);
				}
			}
			if (list == null)
			{
				return;
			}
			foreach (int item in list)
			{
				RemoveSession(item, restoreSources: false);
			}
		}

		private void DiscoverTargetSources(AudioClip replacement)
		{
			AudioSource[] array = Resources.FindObjectsOfTypeAll<AudioSource>();
			AudioSource[] array2 = array;
			foreach (AudioSource val in array2)
			{
				if ((Object)(object)val == (Object)null)
				{
					continue;
				}
				int instanceID = ((Object)val).GetInstanceID();
				bool flag = _sourceToSession.ContainsKey(instanceID);
				AudioClip clip = val.clip;
				bool flag2 = (Object)(object)clip != (Object)null && TargetClipNames.Contains(((Object)clip).name);
				if ((!flag && !flag2) || flag)
				{
					continue;
				}
				Transform val2 = FindGambitRoot(((Component)val).transform);
				if (!((Object)(object)val2 == (Object)null) && !((Object)(object)clip == (Object)null))
				{
					int instanceID2 = ((Object)val2).GetInstanceID();
					if (!_sessions.TryGetValue(instanceID2, out GambitMusicSession value))
					{
						value = new GambitMusicSession(val2);
						_sessions.Add(instanceID2, value);
					}
					value.AddSource(val, clip, replacement, _forceLoop.Value);
					_sourceToSession[instanceID] = instanceID2;
					Log.LogInfo((object)("[TRACKED] '" + ((Object)clip).name + "' at '" + GetTransformPath(((Component)val).transform) + "'."));
				}
			}
		}

		private static Transform? FindGambitRoot(Transform? transform)
		{
			Transform val = transform;
			while ((Object)(object)val != (Object)null)
			{
				if (((Object)val).name.IndexOf("Enemy - Spinny", StringComparison.OrdinalIgnoreCase) >= 0)
				{
					return val;
				}
				val = val.parent;
			}
			return null;
		}

		private void RemoveSession(int sessionId, bool restoreSources)
		{
			if (!_sessions.TryGetValue(sessionId, out GambitMusicSession value))
			{
				return;
			}
			List<int> list = null;
			foreach (KeyValuePair<int, int> item in _sourceToSession)
			{
				if (item.Value == sessionId)
				{
					if (list == null)
					{
						list = new List<int>();
					}
					list.Add(item.Key);
				}
			}
			if (list != null)
			{
				foreach (int item2 in list)
				{
					_sourceToSession.Remove(item2);
				}
			}
			if (restoreSources)
			{
				value.Restore(_replacementClip);
			}
			_sessions.Remove(sessionId);
		}

		private void RestoreAllSources()
		{
			foreach (GambitMusicSession value in _sessions.Values)
			{
				value.Restore(_replacementClip);
			}
			_sessions.Clear();
			_sourceToSession.Clear();
		}

		private static string GetTransformPath(Transform? transform)
		{
			if ((Object)(object)transform == (Object)null)
			{
				return "<null>";
			}
			string text = ((Object)transform).name;
			Transform parent = transform.parent;
			while ((Object)(object)parent != (Object)null)
			{
				text = ((Object)parent).name + "/" + text;
				parent = parent.parent;
			}
			return text;
		}

		private void OnDestroy()
		{
			if (_monitorCoroutine != null)
			{
				((MonoBehaviour)this).StopCoroutine(_monitorCoroutine);
				_monitorCoroutine = null;
			}
			RestoreAllSources();
		}
	}
}