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 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 _sources = new Dictionary(); private readonly Transform _root; private bool _active; private double _startDspTime; private double _noPlayingSince = -1.0; internal IEnumerable 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 list = null; foreach (KeyValuePair source2 in _sources) { AudioSource source = source2.Value.Source; if ((Object)(object)source == (Object)null) { if (list == null) { list = new List(); } 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(); } 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 TargetClipNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "enemy spinny music", "enemy spinny music muffled", "enemy spinny music leaving" }; private ConfigEntry _modEnabled = null; private ConfigEntry _audioFileName = null; private ConfigEntry _scanInterval = null; private ConfigEntry _startVolumeThreshold = null; private ConfigEntry _endGraceSeconds = null; private ConfigEntry _syncToleranceSeconds = null; private ConfigEntry _forceLoop = null; private AudioClip? _replacementClip; private Coroutine? _monitorCoroutine; private bool _sourcesRestoredForDisabledState; private readonly Dictionary _sessions = new Dictionary(); private readonly Dictionary _sourceToSession = new Dictionary(); 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("General", "Enabled", true, "Enables replacement of Gambit's main music."); _audioFileName = ((BaseUnityPlugin)this).Config.Bind("Audio", "AudioFileName", "roulette.ogg", "Replacement track placed next to the DLL. Supported: .ogg, .wav and .mp3."); _forceLoop = ((BaseUnityPlugin)this).Config.Bind("Playback", "ForceLoop", true, "Loops the replacement track until Gambit's music sources stop or the enemy leaves."); _startVolumeThreshold = ((BaseUnityPlugin)this).Config.Bind("Playback", "StartVolumeThreshold", 0.005f, new ConfigDescription("The track restarts from 0:00 when Gambit's music first becomes audible.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 0.25f), Array.Empty())); _endGraceSeconds = ((BaseUnityPlugin)this).Config.Bind("Playback", "EndGraceSeconds", 0.6f, new ConfigDescription("Prevents a brief game-side source switch from ending the current music session.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 3f), Array.Empty())); _syncToleranceSeconds = ((BaseUnityPlugin)this).Config.Bind("Playback", "SyncToleranceSeconds", 0.35f, new ConfigDescription("Maximum allowed drift between the normal, muffled and leaving music sources.", (AcceptableValueBase)(object)new AcceptableValueRange(0.05f, 2f), Array.Empty())); _scanInterval = ((BaseUnityPlugin)this).Config.Bind("Advanced", "ScanIntervalSeconds", 0.05f, new ConfigDescription("How often the mod checks Gambit's music AudioSources.", (AcceptableValueBase)(object)new AcceptableValueRange(0.02f, 1f), Array.Empty())); ((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 list = null; foreach (KeyValuePair 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(); } 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[] 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 list = null; foreach (KeyValuePair item in _sourceToSession) { if (item.Value == sessionId) { if (list == null) { list = new List(); } 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 ""; } 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(); } } }