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 CaptainAudio v1.3.54
CaptainAudio.dll
Decompiled 2 weeks agousing 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.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; 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: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 CaptainAudio { public static class AudioLoader { private class LoadTask { public string FolderName; public string ClipName; public byte[] AudioData; public bool IsCompleted; public AudioClip LoadedClip; } private static bool _isInitialized = false; private const int MAX_RETRY_COUNT = 3; private const float RETRY_DELAY = 0.5f; private static HashSet<string> _externalOverrideFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase); public static IEnumerator InitializeAsync() { if (!_isInitialized) { Plugin.CustomMusic.Clear(); Plugin.CustomAmbient.Clear(); Plugin.CustomSFX.Clear(); Plugin.CustomMusicList.Clear(); Plugin.CustomAmbientList.Clear(); Plugin.CustomSFXList.Clear(); yield return LoadEmbeddedResourcesParallel("CaptainAudio.asset.Resources.Music", Plugin.CustomMusic, Plugin.CustomMusicList); yield return LoadEmbeddedResourcesParallel("CaptainAudio.asset.Resources.Ambient", Plugin.CustomAmbient, Plugin.CustomAmbientList); yield return LoadEmbeddedResourcesParallel("CaptainAudio.asset.Resources.SFX", Plugin.CustomSFX, Plugin.CustomSFXList); yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(LoadExternalAudioFilesAsync()); StringMatchCache.Initialize(); _isInitialized = true; Plugin.Log.LogInfo((object)$"Loaded: {Plugin.CustomMusicList.Count} folders, {Plugin.CustomMusic.Count} clips"); if ((Object)(object)MusicMan.instance != (Object)null) { MusicMan_Awake_Patch.ApplyBossMusic(MusicMan.instance); Plugin.Log.LogInfo((object)"[CaptainAudio] 보스 음악 재적용 완료"); } } } private static IEnumerator LoadEmbeddedResourcesParallel(string resourcePrefix, Dictionary<string, AudioClip> singleDict, Dictionary<string, Dictionary<string, AudioClip>> folderDict) { Assembly assembly = Assembly.GetExecutingAssembly(); string[] resourceNames = (from name in assembly.GetManifestResourceNames() where name.StartsWith(resourcePrefix) select name).ToArray(); if (resourceNames.Length == 0) { yield break; } List<LoadTask> tasks = new List<LoadTask>(); string[] array = resourceNames; foreach (string resourceName in array) { Stream stream = assembly.GetManifestResourceStream(resourceName); if (stream != null) { byte[] audioData = new byte[stream.Length]; stream.Read(audioData, 0, audioData.Length); stream.Dispose(); string fileName = resourceName.Substring(resourcePrefix.Length + 1); var (folderName, clipName) = ParseResourcePath(fileName); if (!string.IsNullOrEmpty(clipName)) { LoadTask task = new LoadTask { FolderName = folderName, ClipName = clipName, AudioData = audioData, IsCompleted = false, LoadedClip = null }; tasks.Add(task); ((MonoBehaviour)Plugin.instance).StartCoroutine(LoadSingleClipWithRetry(task)); } } } while (tasks.Any((LoadTask t) => !t.IsCompleted)) { yield return null; } foreach (LoadTask task2 in tasks) { if ((Object)(object)task2.LoadedClip == (Object)null) { continue; } if (!string.IsNullOrEmpty(task2.FolderName)) { if (!folderDict.ContainsKey(task2.FolderName)) { folderDict[task2.FolderName] = new Dictionary<string, AudioClip>(); } folderDict[task2.FolderName][task2.ClipName] = task2.LoadedClip; } else { singleDict[task2.ClipName] = task2.LoadedClip; } } tasks.Clear(); } private static IEnumerator LoadSingleClipWithRetry(LoadTask task) { int retryCount = Plugin.LoadRetryCount.Value; for (int attempt = 0; attempt < retryCount; attempt++) { bool success = false; AudioClip clip = null; yield return LoadAudioClipFromBytes(task.AudioData, task.ClipName, delegate(AudioClip loadedClip) { clip = loadedClip; success = (Object)(object)loadedClip != (Object)null; }); if (success) { task.LoadedClip = clip; task.AudioData = null; task.IsCompleted = true; yield break; } if (attempt < retryCount - 1) { yield return (object)new WaitForSeconds(0.5f); } } task.IsCompleted = true; } private static IEnumerator LoadAudioClipFromBytes(byte[] audioData, string clipName, Action<AudioClip> onComplete) { string fileExtension = DetectAudioFormat(audioData); string tempPath = Path.Combine(Application.temporaryCachePath, $"temp_audio_{clipName}_{Guid.NewGuid()}{fileExtension}"); try { Directory.CreateDirectory(Path.GetDirectoryName(tempPath)); File.WriteAllBytes(tempPath, audioData); } catch { onComplete?.Invoke(null); yield break; } AudioType audioType = GetAudioType(fileExtension); string uri = new Uri(tempPath).AbsoluteUri; UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(uri, audioType); try { DownloadHandlerAudioClip handler = (DownloadHandlerAudioClip)www.downloadHandler; handler.streamAudio = false; handler.compressed = false; www.timeout = 15; yield return www.SendWebRequest(); if ((int)www.result == 1) { AudioClip clip = DownloadHandlerAudioClip.GetContent(www); if ((Object)(object)clip != (Object)null && clip.length > 0f && clip.samples > 0 && (int)clip.loadState == 2) { ((Object)clip).name = clipName; onComplete?.Invoke(clip); } else { ManualLogSource log = Plugin.Log; object[] obj2 = new object[4] { clipName, null, null, null }; obj2[1] = ((clip != null) ? new float?(clip.length) : ((float?)null)); obj2[2] = ((clip != null) ? new int?(clip.samples) : ((int?)null)); obj2[3] = ((clip != null) ? new AudioDataLoadState?(clip.loadState) : ((AudioDataLoadState?)null)); log.LogWarning((object)string.Format("Invalid clip: {0} (length={1}, samples={2}, state={3})", obj2)); onComplete?.Invoke(null); } } else { Plugin.Log.LogWarning((object)("Failed to load: " + clipName + " - " + www.error)); onComplete?.Invoke(null); } } finally { ((IDisposable)www)?.Dispose(); } if (File.Exists(tempPath)) { try { File.Delete(tempPath); } catch { } } } private static string ResolveMusicPath(string pluginMusicPath) { if (!Plugin.GetMusicFromConfig.Value) { return pluginMusicPath; } string text = Path.Combine(Paths.ConfigPath, "CaptainAudio", "music"); try { if (!Directory.Exists(text)) { Directory.CreateDirectory(text); Plugin.Log.LogInfo((object)("[Config] music 폴더 생성됨: " + text)); } if (Directory.GetFiles(text, "*", SearchOption.AllDirectories).Any((string f) => IsValidAudioFile(f))) { return text; } Plugin.Log.LogInfo((object)("[Config] config music 폴더에 오디오 파일 없음 → plugins 폴더 사용: " + pluginMusicPath)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[Config] config music 폴더 접근 실패, plugins 폴더 사용: " + ex.Message)); } return pluginMusicPath; } private static IEnumerator LoadExternalAudioFilesAsync() { string newBasePath = Path.Combine(Paths.PluginPath, "korCaptain-CaptainAudio"); string newMusicPath = ResolveMusicPath(Path.Combine(newBasePath, "music")); string newSfxPath = Path.Combine(newBasePath, "sfx"); string newAmbientPath = Path.Combine(newBasePath, "ambient"); if (Directory.Exists(newMusicPath) || Directory.Exists(newSfxPath) || Directory.Exists(newAmbientPath)) { _externalOverrideFolders.Clear(); if (Directory.Exists(newMusicPath)) { string[] directories = Directory.GetDirectories(newMusicPath); foreach (string subFolder in directories) { string folderName = Path.GetFileName(subFolder); if (Directory.GetFiles(subFolder).Any((string f) => IsValidAudioFile(f))) { _externalOverrideFolders.Add("Music:" + folderName); Plugin.Log.LogInfo((object)("[Override] Music/" + folderName + ": 외부 음악 감지됨 → 내장 음악 대체")); } } } if (Directory.Exists(newSfxPath)) { string[] directories2 = Directory.GetDirectories(newSfxPath); foreach (string subFolder2 in directories2) { string folderName2 = Path.GetFileName(subFolder2); if (Directory.GetFiles(subFolder2).Any((string f) => IsValidAudioFile(f))) { _externalOverrideFolders.Add("SFX:" + folderName2); Plugin.Log.LogInfo((object)("[Override] SFX/" + folderName2 + ": 외부 SFX 감지됨 → 내장 SFX 대체")); } } } if (Directory.Exists(newAmbientPath)) { string[] directories3 = Directory.GetDirectories(newAmbientPath); foreach (string subFolder3 in directories3) { string folderName3 = Path.GetFileName(subFolder3); if (Directory.GetFiles(subFolder3).Any((string f) => IsValidAudioFile(f))) { _externalOverrideFolders.Add("Ambient:" + folderName3); Plugin.Log.LogInfo((object)("[Override] Ambient/" + folderName3 + ": 외부 Ambient 감지됨 → 내장 Ambient 대체")); } } } ApplyExternalOverrides(); if (Directory.Exists(newMusicPath)) { yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(CollectAudioFilesAsync(newMusicPath, Plugin.CustomMusic, Plugin.CustomMusicList)); } if (Directory.Exists(newSfxPath)) { yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(CollectAudioFilesAsync(newSfxPath, Plugin.CustomSFX, Plugin.CustomSFXList)); } if (Directory.Exists(newAmbientPath)) { yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(CollectAudioFilesAsync(newAmbientPath, Plugin.CustomAmbient, Plugin.CustomAmbientList)); } yield break; } string externalPath = Path.Combine(Paths.PluginPath, "CaptainAudio"); if (Directory.Exists(externalPath)) { _externalOverrideFolders.Clear(); ScanExternalFolders(externalPath); ApplyExternalOverrides(); string musicPath = Path.Combine(externalPath, "Music"); string sfxPath = Path.Combine(externalPath, "SFX"); string ambientPath = Path.Combine(externalPath, "Ambient"); if (Directory.Exists(musicPath)) { yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(CollectAudioFilesAsync(musicPath, Plugin.CustomMusic, Plugin.CustomMusicList)); } if (Directory.Exists(sfxPath)) { yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(CollectAudioFilesAsync(sfxPath, Plugin.CustomSFX, Plugin.CustomSFXList)); } if (Directory.Exists(ambientPath)) { yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(CollectAudioFilesAsync(ambientPath, Plugin.CustomAmbient, Plugin.CustomAmbientList)); } } } private static void ScanExternalFolders(string externalPath) { string[] array = new string[3] { "Music", "Ambient", "SFX" }; string[] array2 = array; foreach (string text in array2) { string path = Path.Combine(externalPath, text); if (!Directory.Exists(path)) { continue; } string[] directories = Directory.GetDirectories(path); foreach (string path2 in directories) { string fileName = Path.GetFileName(path2); if (Directory.GetFiles(path2).Any((string file) => IsValidAudioFile(file))) { string item = text + ":" + fileName; _externalOverrideFolders.Add(item); Plugin.Log.LogInfo((object)("[Override] " + text + "/" + fileName + ": 외부 음악 감지됨 → 내장 음악 대체")); } } } } private static bool IsValidAudioFile(string filePath) { if (string.IsNullOrEmpty(filePath)) { return false; } string text = Path.GetExtension(filePath).ToLower(); return text == ".ogg" || text == ".wav" || text == ".mp3"; } private static void ApplyExternalOverrides() { foreach (string externalOverrideFolder in _externalOverrideFolders) { string[] array = externalOverrideFolder.Split(':'); if (array.Length != 2) { continue; } string text = array[0]; string folderName = array[1]; Dictionary<string, Dictionary<string, AudioClip>> dictionary = null; switch (text) { case "Music": dictionary = Plugin.CustomMusicList; break; case "Ambient": dictionary = Plugin.CustomAmbientList; break; case "SFX": dictionary = Plugin.CustomSFXList; break; } if (dictionary != null) { string text2 = dictionary.Keys.FirstOrDefault((string k) => string.Equals(k, folderName, StringComparison.OrdinalIgnoreCase)); if (text2 != null) { int count = dictionary[text2].Count; dictionary[text2].Clear(); Plugin.Log.LogInfo((object)$"[Override] {text}/{folderName}: 내장 음악 {count}개 제거됨"); } } } } private static IEnumerator CollectAudioFilesAsync(string path, Dictionary<string, AudioClip> singleDict, Dictionary<string, Dictionary<string, AudioClip>> folderDict) { string[] files = Directory.GetFiles(path); foreach (string file in files) { yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(LoadExternalClipAsync(file, singleDict)); } string[] directories = Directory.GetDirectories(path); foreach (string folder in directories) { string folderName = Path.GetFileName(folder); folderDict[folderName] = new Dictionary<string, AudioClip>(); string[] files2 = Directory.GetFiles(folder); foreach (string file2 in files2) { yield return ((MonoBehaviour)Plugin.instance).StartCoroutine(LoadExternalClipAsync(file2, folderDict[folderName])); } } } private static IEnumerator LoadExternalClipAsync(string path, Dictionary<string, AudioClip> dict) { if (path.EndsWith(".txt") || !path.Contains(".")) { yield break; } string extension = Path.GetExtension(path).ToLower(); if (extension != ".ogg" && extension != ".wav" && extension != ".mp3") { yield break; } string uri = new Uri(path).AbsoluteUri; AudioType audioType = GetAudioType(extension); UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(uri, audioType); try { DownloadHandlerAudioClip handler = (DownloadHandlerAudioClip)www.downloadHandler; handler.streamAudio = false; handler.compressed = false; www.timeout = 15; yield return www.SendWebRequest(); if ((int)www.result == 1) { AudioClip clip = ((handler != null) ? handler.audioClip : null); if ((Object)(object)clip != (Object)null && clip.length > 0f && clip.samples > 0) { string clipName = (((Object)clip).name = Path.GetFileNameWithoutExtension(path)); dict[clipName] = clip; } else { Plugin.Log.LogWarning((object)("Invalid external clip: " + path)); } } else { Plugin.Log.LogWarning((object)("Failed to load external: " + path + " - " + www.error)); } } finally { ((IDisposable)www)?.Dispose(); } } private static (string folderName, string clipName) ParseResourcePath(string fileName) { string item = null; string item2 = null; if (fileName.Contains("\\")) { string[] array = fileName.Split('\\'); if (array.Length >= 2) { item = array[0]; item2 = Path.GetFileNameWithoutExtension(array[^1]); } } else { string[] array2 = fileName.Split('.'); if (array2.Length >= 3) { item = array2[0]; item2 = string.Join(".", array2.Skip(1).Take(array2.Length - 2)); } else if (array2.Length >= 2) { item = null; item2 = array2[0]; } } return (folderName: item, clipName: item2); } private static string DetectAudioFormat(byte[] audioData) { if (audioData.Length < 4) { return ".ogg"; } if (audioData[0] == 82 && audioData[1] == 73 && audioData[2] == 70 && audioData[3] == 70) { return ".wav"; } if ((audioData[0] == 73 && audioData[1] == 68 && audioData[2] == 51) || (audioData[0] == byte.MaxValue && (audioData[1] & 0xE0) == 224)) { return ".mp3"; } return ".ogg"; } private static AudioType GetAudioType(string extension) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) if (!(extension == ".wav")) { if (extension == ".mp3") { return (AudioType)13; } return (AudioType)14; } return (AudioType)20; } } [HarmonyPatch(typeof(MusicMan), "Awake")] public static class MusicMan_Awake_Patch { public static void Apply(MusicMan instance) { for (int i = 0; i < instance.m_music.Count; i++) { NamedMusic instance2 = instance.m_music[i]; string value = ReflectionCache.GetValue<string>(instance2, "m_name"); if (Plugin.CustomMusicList.ContainsKey(value)) { AudioClip[] value2 = Plugin.CustomMusicList[value].Values.ToArray(); ReflectionCache.SetValue(instance2, "m_clips", value2); ReflectionCache.SetValue(instance2, "m_resume", false); continue; } string text = StringMatchCache.FindMatch(value); if (text != null && Plugin.CustomMusicList.ContainsKey(text)) { AudioClip[] value3 = Plugin.CustomMusicList[text].Values.ToArray(); ReflectionCache.SetValue(instance2, "m_clips", value3); ReflectionCache.SetValue(instance2, "m_resume", false); continue; } AudioClip[] value4 = ReflectionCache.GetValue<AudioClip[]>(instance2, "m_clips"); if (value4 == null) { continue; } for (int j = 0; j < value4.Length; j++) { if ((Object)(object)value4[j] != (Object)null && Plugin.CustomMusic.ContainsKey(((Object)value4[j]).name)) { value4[j] = Plugin.CustomMusic[((Object)value4[j]).name]; } } } } public static void ApplyBossMusic(MusicMan instance) { for (int i = 0; i < instance.m_music.Count; i++) { NamedMusic instance2 = instance.m_music[i]; string value = ReflectionCache.GetValue<string>(instance2, "m_name"); if (value != null) { string text = (Plugin.CustomMusicList.ContainsKey(value) ? value : StringMatchCache.FindMatch(value)); if (text != null && text.StartsWith("Boss_") && Plugin.CustomMusicList.ContainsKey(text)) { AudioClip[] value2 = Plugin.CustomMusicList[text].Values.ToArray(); ReflectionCache.SetValue(instance2, "m_clips", value2); ReflectionCache.SetValue(instance2, "m_resume", false); } } } } private static void Postfix(MusicMan __instance) { if (Plugin.ModEnabled.Value) { Apply(__instance); } } } [HarmonyPatch(typeof(MusicMan), "UpdateMusic")] public static class MusicMan_UpdateMusic_Patch { private static void Prefix(ref object ___m_queuedMusic, object ___m_currentMusic, AudioSource ___m_musicSource) { //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Invalid comparison between Unknown and I4 //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Invalid comparison between Unknown and I4 if (!Plugin.ModEnabled.Value) { return; } if (___m_queuedMusic != null) { ReflectionCache.SetValue(___m_queuedMusic, "m_volume", Plugin.MusicVolume.Value); } if ((Object)(object)___m_musicSource != (Object)null && ___m_musicSource.loop) { if (!___m_musicSource.isPlaying) { ___m_musicSource.loop = false; } else if ((Object)(object)___m_musicSource.clip != (Object)null && (int)___m_musicSource.clip.loadState == 2) { float num = ___m_musicSource.clip.length - ___m_musicSource.time; if (num < 1.5f) { ___m_musicSource.loop = false; } } } if (___m_currentMusic != null) { ReflectionCache.SetValue(___m_currentMusic, "m_resume", false); } if (___m_queuedMusic != null) { ReflectionCache.SetValue(___m_queuedMusic, "m_resume", false); } if ((Object)(object)___m_musicSource != (Object)null && (Object)(object)___m_musicSource.clip != (Object)null && (int)___m_musicSource.clip.loadState == 2 && ___m_musicSource.time >= ___m_musicSource.clip.length) { ___m_musicSource.time = 0f; } } private static void Postfix(AudioSource ___m_musicSource) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Invalid comparison between Unknown and I4 if (Plugin.ModEnabled.Value && (Object)(object)___m_musicSource != (Object)null && (Object)(object)___m_musicSource.clip != (Object)null && (int)___m_musicSource.clip.loadState == 2 && ___m_musicSource.clip.length > 0f && ___m_musicSource.time >= ___m_musicSource.clip.length) { ___m_musicSource.time = 0f; } } } [HarmonyPatch(typeof(MusicLocation), "Awake")] public static class MusicLocation_Awake_Patch { private static void Postfix(ref AudioSource ___m_audioSource, ref float ___m_baseVolume) { if (!Plugin.ModEnabled.Value || (Object)(object)___m_audioSource == (Object)null || (Object)(object)___m_audioSource.clip == (Object)null) { return; } string name = ((Object)___m_audioSource.clip).name; if (!Plugin.CustomMusic.ContainsKey(name)) { return; } AudioClip val = Plugin.CustomMusic[name]; if (!((Object)(object)val == (Object)null) && !(val.length <= 0f)) { bool isPlaying = ___m_audioSource.isPlaying; if (isPlaying) { ___m_audioSource.Stop(); } ___m_audioSource.clip = val; ___m_audioSource.time = 0f; ___m_baseVolume *= Plugin.LocationVolMultiplier.Value; if (isPlaying) { ___m_audioSource.Play(); } } } } [HarmonyPatch(typeof(AudioMan), "Awake")] public static class AudioMan_Awake_Patch { private static void Postfix(AudioMan __instance, IList ___m_randomAmbients, AudioSource ___m_oceanAmbientSource, AudioSource ___m_windLoopSource) { if (Plugin.ModEnabled.Value) { for (int i = 0; i < ___m_randomAmbients.Count; i++) { object obj = ___m_randomAmbients[i]; string value = ReflectionCache.GetValue<string>(obj, "m_name"); ReplaceAmbientClips(obj, "m_randomAmbientClips"); ReplaceAmbientClips(obj, "m_randomAmbientClipsDay"); ReplaceAmbientClips(obj, "m_randomAmbientClipsNight"); ReplaceAmbientList(obj, value, "_day", "m_randomAmbientClipsDay"); ReplaceAmbientList(obj, value, "_night", "m_randomAmbientClipsNight"); ReplaceAmbientList(obj, value, "", "m_randomAmbientClips"); } if (Plugin.CustomAmbient.ContainsKey("ocean")) { ___m_oceanAmbientSource.clip = Plugin.CustomAmbient["ocean"]; } if (Plugin.CustomAmbient.ContainsKey("wind")) { ___m_windLoopSource.clip = Plugin.CustomAmbient["wind"]; } } } private static void ReplaceAmbientClips(object ambient, string fieldName) { IList value = ReflectionCache.GetValue<IList>(ambient, fieldName); if (value == null) { return; } for (int i = 0; i < value.Count; i++) { object? obj = value[i]; AudioClip val = (AudioClip)((obj is AudioClip) ? obj : null); if ((Object)(object)val != (Object)null && Plugin.CustomAmbient.ContainsKey(((Object)val).name)) { value[i] = Plugin.CustomAmbient[((Object)val).name]; } } } private static void ReplaceAmbientList(object ambient, string ambientName, string suffix, string fieldName) { string key = ambientName + suffix; if (!Plugin.CustomAmbientList.ContainsKey(key)) { return; } IList value = ReflectionCache.GetValue<IList>(ambient, fieldName); if (value == null) { return; } List<AudioClip> list = Plugin.CustomAmbientList[key].Values.ToList(); value.Clear(); foreach (AudioClip item in list) { value.Add(item); } } } [HarmonyPatch(typeof(AudioMan), "QueueAmbientLoop")] public static class AudioMan_QueueAmbientLoop_Patch { private static void Prefix(ref float ___m_queuedAmbientVol, ref float ___m_ambientVol, ref float vol) { if (Plugin.ModEnabled.Value) { vol = Plugin.AmbientVolume.Value; ___m_ambientVol = Plugin.AmbientVolume.Value; ___m_queuedAmbientVol = Plugin.AmbientVolume.Value; } } } [HarmonyPatch(typeof(ZSFX), "Awake")] public static class ZSFX_Awake_Patch { private static void Postfix(ZSFX __instance) { if (!Plugin.ModEnabled.Value) { return; } string zSFXName = AudioUtils.GetZSFXName(__instance); if (Plugin.CustomSFXList.TryGetValue(zSFXName, out var value)) { __instance.m_audioClips = (from x in value orderby x.Key select x.Value).ToArray(); } else { if (__instance.m_audioClips == null) { return; } for (int num = 0; num < __instance.m_audioClips.Length; num++) { if ((Object)(object)__instance.m_audioClips[num] != (Object)null && Plugin.CustomSFX.ContainsKey(((Object)__instance.m_audioClips[num]).name)) { __instance.m_audioClips[num] = Plugin.CustomSFX[((Object)__instance.m_audioClips[num]).name]; } } } } } [HarmonyPatch(typeof(TeleportWorld), "Awake")] public static class TeleportWorld_Awake_Patch { private static void Postfix(TeleportWorld __instance) { if (!Plugin.ModEnabled.Value || !Plugin.CustomSFX.ContainsKey("portal")) { return; } AudioSource componentInChildren = ((Component)__instance).GetComponentInChildren<AudioSource>(); if (!((Object)(object)componentInChildren != (Object)null)) { return; } AudioClip val = Plugin.CustomSFX["portal"]; if (!((Object)(object)val == (Object)null) && !(val.length <= 0f)) { bool isPlaying = componentInChildren.isPlaying; componentInChildren.Stop(); componentInChildren.clip = val; componentInChildren.time = 0f; if (isPlaying || componentInChildren.playOnAwake) { componentInChildren.Play(); } } } } [HarmonyPatch(typeof(Fireplace), "Start")] public static class Fireplace_Start_Patch { private static void Postfix(Fireplace __instance) { if (!Plugin.ModEnabled.Value) { return; } string name = ((Object)__instance).name; AudioSource val = null; AudioClip val2 = null; if (name.Contains("groundtorch") && Plugin.CustomSFX.ContainsKey("groundtorch")) { GameObject enabledObject = __instance.m_enabledObject; val = ((enabledObject != null) ? enabledObject.GetComponentInChildren<AudioSource>() : null); val2 = Plugin.CustomSFX["groundtorch"]; } else if (name.Contains("walltorch") && Plugin.CustomSFX.ContainsKey("walltorch")) { GameObject enabledObjectHigh = __instance.m_enabledObjectHigh; val = ((enabledObjectHigh != null) ? enabledObjectHigh.GetComponentInChildren<AudioSource>() : null); if ((Object)(object)val == (Object)null) { GameObject enabledObject2 = __instance.m_enabledObject; val = ((enabledObject2 != null) ? enabledObject2.GetComponentInChildren<AudioSource>() : null); } val2 = Plugin.CustomSFX["walltorch"]; } else if (name.Contains("fire_pit") && Plugin.CustomSFX.ContainsKey("fire_pit")) { GameObject enabledObjectHigh2 = __instance.m_enabledObjectHigh; val = ((enabledObjectHigh2 != null) ? enabledObjectHigh2.GetComponentInChildren<AudioSource>() : null); val2 = Plugin.CustomSFX["fire_pit"]; } else if (name.Contains("bonfire") && Plugin.CustomSFX.ContainsKey("bonfire")) { GameObject enabledObjectHigh3 = __instance.m_enabledObjectHigh; val = ((enabledObjectHigh3 != null) ? enabledObjectHigh3.GetComponentInChildren<AudioSource>() : null); val2 = Plugin.CustomSFX["bonfire"]; } else if (name.Contains("hearth") && Plugin.CustomSFX.ContainsKey("hearth")) { GameObject enabledObjectHigh4 = __instance.m_enabledObjectHigh; val = ((enabledObjectHigh4 != null) ? enabledObjectHigh4.GetComponentInChildren<AudioSource>() : null); val2 = Plugin.CustomSFX["hearth"]; } if ((Object)(object)val != (Object)null && (Object)(object)val2 != (Object)null && val2.length > 0f) { SafeReplaceClip(val, val2); } } private static void SafeReplaceClip(AudioSource source, AudioClip newClip) { bool isPlaying = source.isPlaying; source.Stop(); source.clip = newClip; source.time = 0f; if (isPlaying || source.playOnAwake) { source.Play(); } } } [HarmonyPatch(typeof(EnvMan), "Awake")] public static class EnvMan_Awake_Patch { private static void Postfix(EnvMan __instance) { if (!Plugin.ModEnabled.Value) { return; } for (int i = 0; i < __instance.m_environments.Count; i++) { string name = __instance.m_environments[i].m_name; foreach (string key in Plugin.CustomMusicList.Keys) { string text = name.ToLower(); string text2 = key.ToLower(); if (text == text2 || text.Contains(text2) || text2.Contains(text)) { string name2 = __instance.m_environments[i].m_name; __instance.m_environments[i].m_name = key; AddMusicToEnvironment(__instance, i, ""); __instance.m_environments[i].m_name = name2; break; } } AddMusicToEnvironment(__instance, i, "Morning"); AddMusicToEnvironment(__instance, i, "Day"); AddMusicToEnvironment(__instance, i, "Evening"); AddMusicToEnvironment(__instance, i, "Night"); } } private static void AddMusicToEnvironment(EnvMan envMan, int index, string timeOfDay) { string musicName = envMan.m_environments[index].m_name + timeOfDay; if (!Plugin.CustomMusicList.ContainsKey(musicName)) { musicName = Plugin.CustomMusicList.Keys.FirstOrDefault((string k) => string.Equals(k, musicName, StringComparison.OrdinalIgnoreCase)); if (musicName == null) { return; } } switch (timeOfDay) { case "Morning": envMan.m_environments[index].m_musicMorning = musicName; break; case "Day": envMan.m_environments[index].m_musicDay = musicName; break; case "Evening": envMan.m_environments[index].m_musicEvening = musicName; break; case "Night": envMan.m_environments[index].m_musicNight = musicName; break; } Type type = ((object)MusicMan.instance.m_music[0]).GetType(); object obj = Activator.CreateInstance(type); ReflectionCache.SetValue(obj, "m_name", musicName); ReflectionCache.SetValue(obj, "m_clips", (from x in Plugin.CustomMusicList[musicName] orderby x.Key select x.Value).ToArray()); ReflectionCache.SetValue(obj, "m_loop", true); ReflectionCache.SetValue(obj, "m_ambientMusic", true); ReflectionCache.SetValue(obj, "m_resume", false); ((IList)MusicMan.instance.m_music).Add(obj); } } [HarmonyPatch(typeof(Terminal), "InputText")] public static class Terminal_InputText_Patch { private static bool Prefix(Terminal __instance) { if (!Plugin.ModEnabled.Value) { return true; } FieldInfo field = ((object)__instance).GetType().GetField("m_input"); if (field == null) { return true; } object value = field.GetValue(__instance); if (value == null) { return true; } PropertyInfo property = value.GetType().GetProperty("text"); if (property == null) { return true; } string text = (string)property.GetValue(value); switch (text.ToLower()) { case "captainaudio reset": ((BaseUnityPlugin)Plugin.instance).Config.Reload(); ((BaseUnityPlugin)Plugin.instance).Config.Save(); AddOutput(__instance, text); AddOutput(__instance, "Captain Audio config reloaded"); return false; case "captainaudio music": AddOutput(__instance, text); if ((Object)(object)EnvMan.instance != (Object)null) { Player localPlayer = Player.m_localPlayer; string arg = ((localPlayer != null && localPlayer.IsSafeInHome()) ? "home" : EnvMan.instance.GetAmbientMusic()); AddOutput(__instance, $"Current: {arg} | Folders: {Plugin.CustomMusicList.Count} | Clips: {Plugin.CustomMusic.Count}"); } return false; case "captainaudio env": AddOutput(__instance, text); if ((Object)(object)EnvMan.instance != (Object)null) { AddOutput(__instance, "Environment: " + EnvMan.instance.GetCurrentEnvironment().m_name); } else { AddOutput(__instance, "Must be called in-game"); } return false; case "captainaudio bosses": AddOutput(__instance, text); if ((Object)(object)MusicMan.instance != (Object)null) { StringBuilder stringBuilder = new StringBuilder("Boss entries: "); bool flag = false; foreach (NamedMusic item in MusicMan.instance.m_music) { string value2 = ReflectionCache.GetValue<string>(item, "m_name"); if (value2 != null && value2.ToLower().Contains("boss")) { stringBuilder.Append(value2).Append(" | "); flag = true; } } AddOutput(__instance, flag ? stringBuilder.ToString() : "No boss entries found in MusicMan"); } else { AddOutput(__instance, "Must be called in-game"); } return false; default: return true; } } private static void AddOutput(Terminal terminal, string text) { Traverse.Create((object)terminal).Method("AddString", new object[1] { text }).GetValue(); } } [BepInPlugin("captain.CaptainAudio", "Captain Audio", "1.3.54")] public class Plugin : BaseUnityPlugin { public static Plugin instance; public static ConfigEntry<bool> ModEnabled; public static ConfigEntry<float> MusicVolume; public static ConfigEntry<float> AmbientVolume; public static ConfigEntry<float> LocationVolMultiplier; public static ConfigEntry<int> LoadRetryCount; public static ConfigEntry<bool> EnableFallback; public static ConfigEntry<bool> GetMusicFromConfig; public static Dictionary<string, AudioClip> CustomMusic = new Dictionary<string, AudioClip>(); public static Dictionary<string, Dictionary<string, AudioClip>> CustomMusicList = new Dictionary<string, Dictionary<string, AudioClip>>(); public static Dictionary<string, AudioClip> CustomAmbient = new Dictionary<string, AudioClip>(); public static Dictionary<string, Dictionary<string, AudioClip>> CustomAmbientList = new Dictionary<string, Dictionary<string, AudioClip>>(); public static Dictionary<string, AudioClip> CustomSFX = new Dictionary<string, AudioClip>(); public static Dictionary<string, Dictionary<string, AudioClip>> CustomSFXList = new Dictionary<string, Dictionary<string, AudioClip>>(); public static ManualLogSource Log { get; private set; } private void Awake() { instance = this; Log = ((BaseUnityPlugin)this).Logger; ModEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable this mod"); MusicVolume = ((BaseUnityPlugin)this).Config.Bind<float>("General", "MusicVolume", 0.6f, "Music volume (0.0 - 1.0)"); AmbientVolume = ((BaseUnityPlugin)this).Config.Bind<float>("General", "AmbientVolume", 0.3f, "Ambient volume (0.0 - 1.0)"); LocationVolMultiplier = ((BaseUnityPlugin)this).Config.Bind<float>("General", "LocationVolumeMultiplier", 5f, "Location music volume multiplier"); LoadRetryCount = ((BaseUnityPlugin)this).Config.Bind<int>("Advanced", "LoadRetryCount", 3, "Number of retries when audio loading fails"); EnableFallback = ((BaseUnityPlugin)this).Config.Bind<bool>("Advanced", "EnableFallback", true, "Use fallback to vanilla music on load failure"); GetMusicFromConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Advanced", "GetMusicFromConfig", false, "false: load music from plugins/korCaptain-CaptainAudio/music. true: load from config/CaptainAudio/music (created automatically; falls back to the plugins folder if it has no audio files)"); if (!ModEnabled.Value) { return; } try { Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), (string)null); ((MonoBehaviour)this).StartCoroutine(AudioLoader.InitializeAsync()); try { Type type = typeof(BaseUnityPlugin).Assembly.GetType("BepInEx.ConsoleManager"); MethodInfo methodInfo = type?.GetMethod("SetConsoleColor", BindingFlags.Static | BindingFlags.Public); TextWriter textWriter = type?.GetProperty("StandardOutStream", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) as TextWriter; if (methodInfo != null && textWriter != null) { methodInfo.Invoke(null, new object[1] { ConsoleColor.Cyan }); textWriter.WriteLine("[CaptainAudio] CaptainAudio 1.3.54 Loaded!"); methodInfo.Invoke(null, new object[1] { ConsoleColor.Gray }); } else { Log.LogMessage((object)"[CaptainAudio] CaptainAudio 1.3.54 Loaded!"); } } catch { Log.LogMessage((object)"[CaptainAudio] CaptainAudio 1.3.54 Loaded!"); } } catch (Exception ex) { Log.LogError((object)("Critical error: " + ex.Message)); } } } public static class AudioUtils { public static string GetZSFXName(ZSFX zsfx) { string name = ((Object)zsfx).name; char[] anyOf = new char[2] { '(', ' ' }; int num = name.IndexOfAny(anyOf); return (num != -1) ? name.Remove(num) : name; } public static void SafeSetVolume(AudioSource source, float volume) { if ((Object)(object)source != (Object)null) { source.volume = Mathf.Clamp01(volume); } } } public static class ReflectionCache { private static readonly Dictionary<string, FieldInfo> _fieldCache = new Dictionary<string, FieldInfo>(); private static readonly Dictionary<string, PropertyInfo> _propertyCache = new Dictionary<string, PropertyInfo>(); public static FieldInfo GetField(Type type, string fieldName) { string key = type.FullName + "." + fieldName; if (!_fieldCache.ContainsKey(key)) { _fieldCache[key] = type.GetField(fieldName); } return _fieldCache[key]; } public static void SetValue(object instance, string fieldName, object value) { GetField(instance.GetType(), fieldName)?.SetValue(instance, value); } public static T GetValue<T>(object instance, string fieldName) { FieldInfo field = GetField(instance.GetType(), fieldName); return (field != null) ? ((T)field.GetValue(instance)) : default(T); } } public static class StringMatchCache { private static readonly Dictionary<string, string> _lowerCaseCache = new Dictionary<string, string>(); private static readonly Dictionary<string, string> _musicMappingCache = new Dictionary<string, string>(); private static bool _initialized = false; public static string GetLowerCase(string input) { if (string.IsNullOrEmpty(input)) { return string.Empty; } if (!_lowerCaseCache.ContainsKey(input)) { _lowerCaseCache[input] = input.ToLower(); } return _lowerCaseCache[input]; } public static void Initialize() { if (!_initialized) { AddMapping("Boss_TheElder", "boss_gdking", "gdking"); AddMapping("Boss_Yagluth", "boss_goblinking", "goblinking"); AddMapping("Boss_TheQueen", "boss_queen", "queen"); _initialized = true; } } private static void AddMapping(string target, params string[] sources) { foreach (string key in sources) { _musicMappingCache[key] = target; } } public static string FindMatch(string musicName) { if (string.IsNullOrEmpty(musicName)) { return null; } if (Plugin.CustomMusicList.ContainsKey(musicName)) { return musicName; } if (_musicMappingCache.ContainsKey(musicName)) { return _musicMappingCache[musicName]; } string lowerCase = GetLowerCase(musicName); foreach (string key in Plugin.CustomMusicList.Keys) { string lowerCase2 = GetLowerCase(key); if (lowerCase.Contains(lowerCase2) || lowerCase2.Contains(lowerCase)) { _musicMappingCache[musicName] = key; return key; } } return null; } } }