diff --git a/BombRushRadio.csproj b/BombRushRadio.csproj index 62995fe..4d3622f 100644 --- a/BombRushRadio.csproj +++ b/BombRushRadio.csproj @@ -1,39 +1,55 @@  - - net461 - BombRushRadio - Allows adding custom music tracks to Bomb Rush Cyberfunk. - 1.7 - true - 11 - - - - $(BRCPath)/Bomb Rush Cyberfunk_Data/Managed - + + net461 + BombRushRadio + Allows adding custom music tracks to Bomb Rush Cyberfunk. + 1.7 + true + 11 + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - $(ManagedPath)/Assembly-CSharp.dll - false - true - - - - - - - + + + DEBUG;TRACE + full + true + false + + + + + TRACE + none + false + true + + + + $(BRCPath)/Bomb Rush Cyberfunk_Data/Managed + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + $(ManagedPath)/Assembly-CSharp.dll + false + true + + + + + + + \ No newline at end of file diff --git a/Patches/MusicPlayer-Patches.cs b/Patches/MusicPlayer-Patches.cs index c1dcb8f..756741a 100644 --- a/Patches/MusicPlayer-Patches.cs +++ b/Patches/MusicPlayer-Patches.cs @@ -2,6 +2,7 @@ using Reptile; using Reptile.Phone; using System; +using System.Collections.Generic; using UnityEngine; namespace BombRushRadio; @@ -61,34 +62,48 @@ public static void Refresh(MusicPlayer __instance, ChapterMusic chapterMusic, St { __instance.musicTrackQueue.ClearTracks(); - Story.ObjectiveInfo currentObjectiveInfo = Story.GetCurrentObjectiveInfo(); - if (stage == Stage.hideout) + if (!BombRushRadio.RemoveBaseGameSongs.Value) { - MusicTrack musicTrackByID = Core.Instance.AudioManager.MusicLibraryPlayer.GetMusicTrackByID(MusicTrackID.Hideout_Mixtape); - __instance.AddMusicTrack(musicTrackByID); - Debug.Log("[BRR] [BASE-GAME] Added " + musicTrackByID.Title + " to the total list."); + Story.ObjectiveInfo currentObjectiveInfo = Story.GetCurrentObjectiveInfo(); + if (stage == Stage.hideout) + { + MusicTrack musicTrackByID = Core.Instance.AudioManager.MusicLibraryPlayer.GetMusicTrackByID(MusicTrackID.Hideout_Mixtape); + __instance.AddMusicTrack(musicTrackByID); + Debug.Log("[BRR] [BASE-GAME] Added " + musicTrackByID.Title + " to the total list."); + } + else + { + MusicTrack chapterMusic2 = chapterMusic.GetChapterMusic(currentObjectiveInfo.chapter); + __instance.AddMusicTrack(chapterMusic2); + Debug.Log("[BRR] [BASE-GAME] Added " + chapterMusic2.Title + " to the total list."); + } + AUnlockable[] unlockables = WorldHandler.instance.GetCurrentPlayer().phone.GetAppInstance().Unlockables; + for (int i = 0; i < unlockables.Length; i++) + { + var musicTrack = unlockables[i] as MusicTrack; + if (Core.Instance.Platform.User.GetUnlockableSaveDataFor(musicTrack).IsUnlocked) + { + musicTrack.isRepeatable = false; + __instance.AddMusicTrack(musicTrack); + Debug.Log("[BRR] [BASE-GAME] Added " + musicTrack.Title + " to the total list."); + } + } } else { - MusicTrack chapterMusic2 = chapterMusic.GetChapterMusic(currentObjectiveInfo.chapter); - __instance.AddMusicTrack(chapterMusic2); - Debug.Log("[BRR] [BASE-GAME] Added " + chapterMusic2.Title + " to the total list."); + Debug.Log("[BRR] Base game songs removed per config setting."); } - AUnlockable[] unlockables = WorldHandler.instance.GetCurrentPlayer().phone.GetAppInstance().Unlockables; - for (int i = 0; i < unlockables.Length; i++) + + var existingTracks = new HashSet(); + foreach (var track in __instance.musicTrackQueue.currentMusicTracks) { - var musicTrack = unlockables[i] as MusicTrack; - if (Core.Instance.Platform.User.GetUnlockableSaveDataFor(musicTrack).IsUnlocked) - { - musicTrack.isRepeatable = false; - __instance.AddMusicTrack(musicTrack); - Debug.Log("[BRR] [BASE-GAME] Added " + musicTrack.Title + " to the total list."); - } + existingTracks.Add($"{track.Artist}|{track.Title}"); } foreach (MusicTrack track in BombRushRadio.Audios) { - if (__instance.musicTrackQueue.currentMusicTracks.Find(m => m.Title == track.Title && m.Artist == track.Artist) != null) + string trackKey = $"{track.Artist}|{track.Title}"; + if (existingTracks.Contains(trackKey)) { continue; } @@ -124,7 +139,8 @@ public class MusicTrackQueue_Patches { static bool Prefix(MusicTrack musicTrack) // ignore unlocking for custom stuff { - if (BombRushRadio.Audios.Find(m => musicTrack.Artist == m.Artist && musicTrack.Title == m.Title)) + string trackKey = $"{musicTrack.Artist}|{musicTrack.Title}"; + if (BombRushRadio.AudioLookup.ContainsKey(trackKey)) { return false; } @@ -177,3 +193,20 @@ static void Prefix(MusicPlayer __instance) __instance.ForcePaused(); } } + +[HarmonyPatch(typeof(MusicPlayer), nameof(MusicPlayer.EvaluateRepeatingMusicTrack))] +public class MusicPlayer_Patches_EvaluateRepeatingMusicTrack +{ + static void Postfix(ref bool __result) + { + if (BombRushRadio.Skipping) + { + Debug.Log($"[BRR] EvaluateRepeatingMusicTrack - was {__result}, forcing to false because skipping"); + } + + if (BombRushRadio.Skipping) + { + __result = false; + } + } +} \ No newline at end of file diff --git a/Patches/MusicPlayerBuffer-Patches.cs b/Patches/MusicPlayerBuffer-Patches.cs index f9bc074..edd5588 100644 --- a/Patches/MusicPlayerBuffer-Patches.cs +++ b/Patches/MusicPlayerBuffer-Patches.cs @@ -6,7 +6,7 @@ namespace BombRushRadio; [HarmonyPatch(typeof(MusicPlayerBuffer), nameof(MusicPlayerBuffer.BufferMusicTrack))] public class MusicPlayerBuffer_BufferMusicTrack_Patches { - static bool Prefix(MusicPlayerBuffer __instance, MusicTrack musicTrackToLoad) // the the game to not unload our files please lol + static bool Prefix(MusicPlayerBuffer __instance, MusicTrack musicTrackToLoad) // tell the game to not unload our files please lol { if (musicTrackToLoad == null || musicTrackToLoad.AudioClip == null) { @@ -28,11 +28,11 @@ static bool Prefix(MusicPlayerBuffer __instance, MusicTrack musicTrackToLoad) // [HarmonyPatch(typeof(MusicPlayerBuffer), nameof(MusicPlayerBuffer.UnloadMusicPlayerData))] public class MusicPlayerBuffer_Patches { - static bool Prefix(MusicPlayerData musicPlayerData) // the the game to not unload our files please lol + static bool Prefix(MusicPlayerData musicPlayerData) // tell the game to not unload our files please lol { - MusicTrack t = BombRushRadio.Audios.Find(m => musicPlayerData.Artist == m.Artist && musicPlayerData.Title == m.Title); + string trackKey = $"{musicPlayerData.Artist}|{musicPlayerData.Title}"; - if (t != null) + if (BombRushRadio.AudioLookup.ContainsKey(trackKey)) { return false; } diff --git a/Plugin.cs b/Plugin.cs index 9656889..d6be638 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -16,20 +16,36 @@ namespace BombRushRadio; public class BombRushRadio : BaseUnityPlugin { public static ConfigEntry ReloadKey; + public static ConfigEntry SkipKey; + public static ConfigEntry SkipKeyController; + public static ConfigEntry RemoveBaseGameSongs; + public static ConfigEntry StreamAudio; + public static ConfigEntry MaxConcurrentLoads; public static MusicPlayer MInstance; public static List Audios = new(); + public static Dictionary AudioLookup = new(); public int ShouldBeDone; public int Done; + private int ActiveLoads; private static readonly List Loaded = new(); public static bool InMainMenu = false; public static bool Loading; + public static bool Skipping = false; private readonly AudioType[] _trackerTypes = new[] { AudioType.IT, AudioType.MOD, AudioType.S3M, AudioType.XM }; private readonly string _songFolder = Path.Combine(Application.streamingAssetsPath, "Mods", "BombRushRadio", "Songs"); + [System.Diagnostics.Conditional("DEBUG")] + private void DebugLog(string message) + { + Logger.LogInfo(message); + } + + public int TotalFileCount; + public void SanitizeSongs() { if (Core.Instance == null || Core.Instance.audioManager == null) @@ -39,50 +55,69 @@ public void SanitizeSongs() if (Core.Instance.audioManager.musicPlayer != null) { + var currentTracks = MInstance.musicTrackQueue.currentMusicTracks; + var loadedSet = new HashSet(Loaded); var toRemove = new List(); int idx = 0; foreach (MusicTrack tr in Audios) { - if (MInstance.musicTrackQueue.currentMusicTracks.Contains(tr)) + if (currentTracks.Contains(tr)) { - MInstance.musicTrackQueue.currentMusicTracks.Remove(tr); + currentTracks.Remove(tr); } else { Logger.LogInfo("[BRR] Adding " + tr.Title); } - if (Loaded.FirstOrDefault(l => l == Helpers.FormatMetadata(new[] { tr.Artist, tr.Title }, "dash")) == null) + string trackKey = Helpers.FormatMetadata(new[] { tr.Artist, tr.Title }, "dash"); + + if (!loadedSet.Contains(trackKey)) { Logger.LogInfo("[BRR] Removing " + tr.Title); toRemove.Add(tr); } - MInstance.musicTrackQueue.currentMusicTracks.Insert(1 + idx, tr); + currentTracks.Insert(1 + idx, tr); idx++; } foreach (MusicTrack tr in toRemove) { Audios.Remove(tr); - tr.AudioClip.UnloadAudioData(); + AudioLookup.Remove($"{tr.Artist}|{tr.Title}"); + if (tr.AudioClip != null) + { + tr.AudioClip.UnloadAudioData(); + } } } } public IEnumerator LoadAudioFile(string filePath, AudioType type) { - string[] metadata = Helpers.GetMetadata(filePath, false); + while (ActiveLoads >= MaxConcurrentLoads.Value) + { + yield return null; + } + ActiveLoads++; + DebugLog($"[BRR] Starting load (active: {ActiveLoads}/{MaxConcurrentLoads.Value}): {Path.GetFileName(filePath)}"); + + string[] metadata = Helpers.GetMetadata(filePath, false); string songName = Helpers.FormatMetadata(metadata, "dash"); + string songKey = $"{metadata[0]}|{metadata[1]}"; // Escape special characters so we don't get an HTML error when we send the request filePath = UnityWebRequest.EscapeURL(filePath); using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip("file:///" + filePath, type)) { + var downloadHandler = (DownloadHandlerAudioClip) www.downloadHandler; + downloadHandler.streamAudio = StreamAudio.Value && !_trackerTypes.Contains(type); + yield return www.SendWebRequest(); if (www.result == UnityWebRequest.Result.ConnectionError) @@ -94,37 +129,48 @@ public IEnumerator LoadAudioFile(string filePath, AudioType type) Done++; MusicTrack musicTrack = ScriptableObject.CreateInstance(); - musicTrack.AudioClip = null; musicTrack.Artist = metadata[0]; musicTrack.Title = metadata[1]; musicTrack.isRepeatable = false; - var downloadHandler = (DownloadHandlerAudioClip) www.downloadHandler; - downloadHandler.streamAudio = !_trackerTypes.Contains(type); - AudioClip myClip = downloadHandler.audioClip; - myClip.name = filePath; + myClip.name = songName; musicTrack.AudioClip = myClip; Audios.Add(musicTrack); + AudioLookup[songKey] = musicTrack; Logger.LogInfo($"[BRR] Loaded {Helpers.FormatMetadata(metadata, "by")} ({Done}/{ShouldBeDone})"); Loaded.Add(songName); } } + + ActiveLoads--; + DebugLog($"[BRR] Finished load (active: {ActiveLoads}/{MaxConcurrentLoads.Value})"); } public IEnumerator LoadFile(string f) { string extension = Path.GetExtension(f).ToLowerInvariant().Substring(1); string[] metadata = Helpers.GetMetadata(f, false); + string songKey = $"{metadata[0]}|{metadata[1]}"; - if (Audios.Find(m => m.Artist == metadata[0] && m.Title == metadata[1])) + if (AudioLookup.ContainsKey(songKey)) { string songName = Helpers.FormatMetadata(metadata, "dash"); Loaded.Add(songName); - Logger.LogInfo("[BRR] " + songName + " is already loaded, skipping."); + + // prefer MP3 over other formats + if (extension == "ogg" || extension == "flac" || extension == "wav") + { + Logger.LogInfo("[BRR] " + songName + " is already loaded, deleting duplicate " + extension.ToUpper() + " file."); + File.Delete(f); + } + else + { + Logger.LogInfo("[BRR] " + songName + " is already loaded, skipping."); + } } else { @@ -169,10 +215,27 @@ public IEnumerator SearchDirectories(string path = "") yield return null; } + public IEnumerator SkipTrack() + { + DebugLog($"[BRR] Skip requested - index: {MInstance.CurrentTrackIndex}/{MInstance.musicTrackQueue.AmountOfTracks}, track: {MInstance.GetMusicTrack(MInstance.CurrentTrackIndex)?.Title}"); + + Skipping = true; + MInstance.ForcePaused(); + MInstance.PlayNext(); + + yield return new WaitForSeconds(0.3f); + Skipping = false; + + DebugLog($"[BRR] Skip complete - now at index: {MInstance.CurrentTrackIndex}, track: {MInstance.GetMusicTrack(MInstance.CurrentTrackIndex)?.Title}"); + } + public IEnumerator ReloadSongs() { + DebugLog("[BRR] ===== RELOAD STARTED ====="); Loaded.Clear(); + AudioLookup.Clear(); Loading = true; + ActiveLoads = 0; if (Audios.Count > 0) { @@ -188,12 +251,22 @@ public IEnumerator ReloadSongs() yield return StartCoroutine(SearchDirectories()); - Logger.LogInfo("[BRR] TOTAL SONGS LOADED: " + Audios.Count); + DebugLog("[BRR] Waiting for all loads to complete..."); + while (ActiveLoads > 0) + { + yield return null; + } + Logger.LogInfo($"[BRR] TOTAL SONGS LOADED: {Audios.Count}"); Logger.LogInfo("[BRR] Bomb Rush Radio has been loaded!"); + DebugLog("[BRR] ===== RELOAD COMPLETE ====="); Loading = false; - Audios.Sort((t, t2) => string.Compare(t.AudioClip.name, t2.AudioClip.name, StringComparison.OrdinalIgnoreCase)); + Audios.Sort((t1, t2) => + { + int artistCompare = string.Compare(t1.Artist, t2.Artist, StringComparison.OrdinalIgnoreCase); + return artistCompare != 0 ? artistCompare : string.Compare(t1.Title, t2.Title, StringComparison.OrdinalIgnoreCase); + }); SanitizeSongs(); } @@ -208,6 +281,11 @@ private void Awake() // bind to config ReloadKey = Config.Bind("Settings", "Reload Key", KeyCode.F1, "Keybind used for reloading songs."); + SkipKey = Config.Bind("Settings", "Skip Key", KeyCode.F2, "Keybind used for skipping to next song."); + SkipKeyController = Config.Bind("Settings", "Skip Key (Controller)", KeyCode.JoystickButton9, "Controller button for skipping to next song. R3/Right Stick Click is usually JoystickButton9."); + RemoveBaseGameSongs = Config.Bind("Settings", "Remove Base Game Songs", false, "Remove all base game songs from the music player."); + StreamAudio = Config.Bind("Settings", "Stream Audio", true, "Whether to stream audio from disk or load at runtime (Streaming is faster but more CPU intensive)"); + MaxConcurrentLoads = Config.Bind("Settings", "Max Concurrent Loads", 5, "Maximum number of songs to load simultaneously (lower = less stuttering, higher = faster loading)"); // load em StartCoroutine(ReloadSongs()); @@ -215,13 +293,30 @@ private void Awake() var harmony = new Harmony("kade.bombrushradio"); harmony.PatchAll(); Logger.LogInfo("[BRR] Patched..."); + } + + private void Update() + { + if (Input.GetKeyDown(ReloadKey.Value) && !InMainMenu) + { + StartCoroutine(ReloadSongs()); + } - Core.OnUpdate += () => + // skip to next song - idea by goatgirl + // only check controller after phone is loaded (means we're actually in game) + bool skipPressed = Input.GetKeyUp(SkipKey.Value); + if (WorldHandler.instance?.GetCurrentPlayer()?.phone != null) + { + skipPressed = skipPressed || Input.GetKeyUp(SkipKeyController.Value); + } + + if (skipPressed && !InMainMenu) { - if (Input.GetKeyDown(ReloadKey.Value) && !InMainMenu) // reload songs + DebugLog($"[BRR] Skip button pressed! Skipping: {Skipping}, IsPlaying: {MInstance?.IsPlaying}"); + if (MInstance != null && MInstance.IsPlaying && !Skipping) { - StartCoroutine(ReloadSongs()); + StartCoroutine(SkipTrack()); } - }; + } } } \ No newline at end of file diff --git a/README.md b/README.md index 0c270f1..e598f65 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,18 @@ Want to reload songs on the fly? Make some changes (additions, removals, and mod The keybind can be configured in the mod's config. +## Skip Song + +Don't like the current song? Press **F2** (or click the right stick on your controller) to skip to the next track. + +The keybind can be configured in the mod's config. *(Idea by goatgirlclover)* + +#### Warning: For reasons I can't explain the right stick option works on some machines perfectly and others it requires a controller reconnect. This matches the situation with "MusicCurator". + ## Config +BombRushRadio has several configurable options. Experiencing stuttering with a large library? Lower **Max Concurrent Loads**. Want only your custom tracks? Enable **Remove Base Game Songs**. + ``` ## Settings file was created by plugin BombRushRadio v1.7 ## Plugin GUID: BombRushRadio @@ -50,6 +60,21 @@ Stream Audio = true # Default value: F1 Reload Key = F1 +## Keybind used for skipping to next song. +# Setting type: KeyCode +# Default value: F2 +Skip Key = F2 + +## Remove all base game songs from the music player. +# Setting type: Boolean +# Default value: false +Remove Base Game Songs = false + +## Maximum number of songs to load simultaneously (lower = less stuttering, higher = faster loading) +# Setting type: Int32 +# Default value: 5 +Max Concurrent Loads = 5 + ``` ## Installation @@ -64,4 +89,4 @@ Make sure to add "https://nuget.bepinex.dev/v3/index.json" as a NuGet source. (i ![image](https://github.com/Kade-github/BombRushRadio/assets/26305836/e128d6c4-debd-4d02-a51b-85b7f8b21517) -Then just build it. +Then just build it. \ No newline at end of file