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 BoomboxCart v1.1.1
BoomBoxCartMod.dll
Decompiled a year agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using BepInEx; using BepInEx.Logging; using BoomBoxCartMod.Patches; using BoomBoxCartMod.Util; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; 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: AssemblyTitle("BoomBoxCartMod")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("BoomBoxCartMod")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("37e852e0-b511-4315-8182-68c0a54e1ba9")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 BoomBoxCartMod { public class Boombox : MonoBehaviourPunCallbacks { [CompilerGenerated] private sealed class <DownloadTimeoutCoroutine>d__35 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string requestId; public string url; public Boombox <>4__this; private Player[] <>s__1; private int <>s__2; private Player <player>5__3; private string <timeoutMessage>5__4; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <DownloadTimeoutCoroutine>d__35(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>s__1 = null; <player>5__3 = null; <timeoutMessage>5__4 = null; <>1__state = -2; } private bool MoveNext() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(40f); <>1__state = 1; return true; case 1: <>1__state = -1; if (currentRequestId == requestId && isDownloadInProgress) { Logger.LogWarning((object)("Download timeout for url: " + url)); if (PhotonNetwork.IsMasterClient) { Logger.LogInfo((object)"Master client initiating timeout recovery"); <>4__this.isTimeoutRecovery = true; <>s__1 = PhotonNetwork.PlayerList; for (<>s__2 = 0; <>s__2 < <>s__1.Length; <>s__2++) { <player>5__3 = <>s__1[<>s__2]; if (!downloadsReady[url].Contains(<player>5__3.ActorNumber)) { if (!downloadErrors.ContainsKey(url)) { downloadErrors[url] = new HashSet<int>(); } downloadErrors[url].Add(<player>5__3.ActorNumber); Logger.LogWarning((object)$"Player {<player>5__3.ActorNumber} timed out during download"); } <player>5__3 = null; } <>s__1 = null; if (downloadsReady.ContainsKey(url) && downloadsReady[url].Count > 0) { <timeoutMessage>5__4 = $"Some players timed out. Continuing playback for {downloadsReady[url].Count} players."; <>4__this.photonView.RPC("NotifyPlayersOfErrors", (RpcTarget)0, new object[1] { <timeoutMessage>5__4 }); <>4__this.photonView.RPC("SyncPlayback", (RpcTarget)0, new object[2] { url, PhotonNetwork.LocalPlayer.ActorNumber }); <timeoutMessage>5__4 = null; } else { <>4__this.photonView.RPC("NotifyPlayersOfErrors", (RpcTarget)0, new object[1] { "Download timed out for all players." }); } isDownloadInProgress = false; currentDownloadUrl = ""; currentRequestId = ""; <>4__this.isTimeoutRecovery = false; } } <>4__this.timeoutCoroutines.Remove(requestId); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private static BoomBoxCartMod Instance = BoomBoxCartMod.instance; public PhotonView photonView; public AudioSource audioSource; public float maxVolumeLimit = 0.8f; private float minDistance = 3f; private float maxDistanceBase = 10f; private float maxDistanceAddition = 20f; private static bool isDownloadInProgress = false; private static string currentDownloadUrl = ""; private static string currentRequestId = ""; public string currentSongUrl = ""; public string currentSongTitle = "No song playing"; private bool isAwaitingSyncPlayback = false; public bool isPlaying = false; private bool isTimeoutRecovery = false; private AudioLowPassFilter lowPassFilter; public int qualityLevel = 3; private static Dictionary<string, AudioClip> downloadedClips = new Dictionary<string, AudioClip>(); private static Dictionary<string, string> songTitles = new Dictionary<string, string>(); private static Dictionary<string, HashSet<int>> downloadsReady = new Dictionary<string, HashSet<int>>(); private static Dictionary<string, HashSet<int>> downloadErrors = new Dictionary<string, HashSet<int>>(); private const float DOWNLOAD_TIMEOUT = 40f; private Dictionary<string, Coroutine> timeoutCoroutines = new Dictionary<string, Coroutine>(); private static readonly Regex[] supportedVideoUrlRegexes = new Regex[5] { new Regex("^((?:https?:)?\\/\\/)?((?:www|m)\\.)?((?:youtube(?:-nocookie)?\\.com|youtu\\.be))(\\/(?:[\\w\\-]+\\?v=|embed\\/|live\\/|v\\/)?)([\\w\\-]+)(\\S+)?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), new Regex("^((?:https?:)?\\/\\/)?((?:www)?\\.?)(rutube\\.ru)(\\/video\\/)([\\w\\-]+)(\\S+)?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), new Regex("^((?:https?:)?\\/\\/)?((?:www)?\\.?)(music\\.yandex\\.ru)(\\/album\\/\\d+\\/track\\/)([\\w\\-]+)(\\S+)?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), new Regex("^((?:https?:)?\\/\\/)?((?:www|m)\\.)?(bilibili\\.com)(\\/video\\/)([\\w\\-]+)(\\S+)?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), new Regex("^((?:https?:)?\\/\\/)?((?:www|m)\\.)?(soundcloud\\.com|snd\\.sc)\\/([\\w\\-]+\\/[\\w\\-]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled) }; private static ManualLogSource Logger => Instance.logger; private void Awake() { audioSource = ((Component)this).gameObject.AddComponent<AudioSource>(); audioSource.volume = 0.15f; audioSource.spatialBlend = 1f; audioSource.playOnAwake = false; audioSource.rolloffMode = (AudioRolloffMode)2; audioSource.spread = 90f; audioSource.dopplerLevel = 0f; audioSource.reverbZoneMix = 1f; audioSource.spatialize = true; lowPassFilter = ((Component)this).gameObject.AddComponent<AudioLowPassFilter>(); ((Behaviour)lowPassFilter).enabled = false; UpdateAudioRangeBasedOnVolume(audioSource.volume); photonView = ((Component)this).GetComponent<PhotonView>(); isAwaitingSyncPlayback = false; if ((Object)(object)photonView == (Object)null) { Logger.LogError((object)"PhotonView not found on Boombox object."); return; } if ((Object)(object)((Component)this).GetComponent<BoomboxController>() == (Object)null) { ((Component)this).gameObject.AddComponent<BoomboxController>(); } Logger.LogInfo((object)$"Boombox initialized on this cart. AudioSource: {audioSource}, PhotonView: {photonView}"); } private void Update() { if (isAwaitingSyncPlayback && audioSource.isPlaying) { audioSource.Stop(); isPlaying = false; } } private void OnDestroy() { foreach (Coroutine value in timeoutCoroutines.Values) { if (value != null) { ((MonoBehaviour)this).StopCoroutine(value); } } timeoutCoroutines.Clear(); } public bool IsDownloadInProgress() { return isDownloadInProgress; } public string GetCurrentDownloadUrl() { return currentDownloadUrl; } public static bool IsValidVideoUrl(string url) { return !string.IsNullOrWhiteSpace(url) && supportedVideoUrlRegexes.Any((Regex regex) => regex.IsMatch(url)); } [PunRPC] public async void RequestSong(string url, int requesterId) { if (!IsValidVideoUrl(url)) { Logger.LogError((object)("Invalid Video URL: " + url)); if (requesterId == PhotonNetwork.LocalPlayer.ActorNumber) { UpdateUIStatus("Error: Invalid Video URL."); } return; } string requestId = Guid.NewGuid().ToString(); if (isDownloadInProgress && requesterId != PhotonNetwork.MasterClient.ActorNumber) { if (requesterId == PhotonNetwork.LocalPlayer.ActorNumber) { UpdateUIStatus("Please wait for the current download to complete."); } return; } isDownloadInProgress = true; isAwaitingSyncPlayback = true; currentDownloadUrl = url; currentRequestId = requestId; CleanupCurrentPlayback(); timeoutCoroutines[requestId] = ((MonoBehaviour)this).StartCoroutine(DownloadTimeoutCoroutine(requestId, url)); if (requesterId == PhotonNetwork.LocalPlayer.ActorNumber) { UpdateUIStatus("Downloading audio from " + url + "..."); } if (downloadsReady.ContainsKey(url)) { downloadsReady[url].Clear(); } else { downloadsReady[url] = new HashSet<int>(); } if (downloadErrors.ContainsKey(url)) { downloadErrors[url].Clear(); } else { downloadErrors[url] = new HashSet<int>(); } if (!downloadedClips.ContainsKey(url)) { try { var (filePath, title) = await YoutubeDL.DownloadAudioWithTitleAsync(url); songTitles[url] = title; photonView.RPC("SetSongTitle", (RpcTarget)3, new object[2] { url, title }); if (requesterId == PhotonNetwork.LocalPlayer.ActorNumber) { UpdateUIStatus("Processing audio: " + title); } AudioClip clip = await GetAudioClipAsync(filePath); downloadedClips[url] = clip; Logger.LogInfo((object)("Downloaded and cached clip for video: " + title)); } catch (Exception ex2) { Exception ex = ex2; if (requesterId == PhotonNetwork.LocalPlayer.ActorNumber) { UpdateUIStatus("Error: " + ex.Message); } photonView.RPC("ReportDownloadError", (RpcTarget)0, new object[3] { PhotonNetwork.LocalPlayer.ActorNumber, url, ex.Message }); isAwaitingSyncPlayback = false; return; } } photonView.RPC("ReportDownloadComplete", (RpcTarget)0, new object[2] { PhotonNetwork.LocalPlayer.ActorNumber, url }); if (requesterId == PhotonNetwork.LocalPlayer.ActorNumber) { UpdateUIStatus("Waiting for all players to be ready..."); } if (requesterId == PhotonNetwork.LocalPlayer.ActorNumber || (PhotonNetwork.IsMasterClient && (requesterId != PhotonNetwork.LocalPlayer.ActorNumber || isTimeoutRecovery))) { await WaitForPlayersReadyOrFailed(url); if (currentRequestId != requestId) { Logger.LogWarning((object)("Request ID mismatch. Current: " + currentRequestId + ", This request: " + requestId)); return; } if (downloadErrors.ContainsKey(url) && downloadErrors[url].Count > 0) { string errorMessage = $"Some players had download errors. Continuing playback for {downloadsReady[url].Count} players."; Logger.LogWarning((object)errorMessage); photonView.RPC("NotifyPlayersOfErrors", (RpcTarget)0, new object[1] { errorMessage }); } Logger.LogInfo((object)$"All players ready for playback. Initiating sync playback for {downloadsReady[url].Count} players."); photonView.RPC("SyncPlayback", (RpcTarget)0, new object[2] { url, requesterId }); } if (timeoutCoroutines.TryGetValue(requestId, out var coroutine) && coroutine != null) { ((MonoBehaviour)this).StopCoroutine(coroutine); timeoutCoroutines.Remove(requestId); } if (currentRequestId == requestId) { isDownloadInProgress = false; currentDownloadUrl = ""; currentRequestId = ""; } } [PunRPC] public void NotifyPlayersOfErrors(string message) { Logger.LogWarning((object)message); UpdateUIStatus(message); } [PunRPC] public void ReportDownloadError(int actorNumber, string url, string errorMessage) { Logger.LogError((object)$"Player {actorNumber} reported download error for {url}: {errorMessage}"); if (!downloadErrors.ContainsKey(url)) { downloadErrors[url] = new HashSet<int>(); } downloadErrors[url].Add(actorNumber); if (actorNumber == PhotonNetwork.LocalPlayer.ActorNumber) { UpdateUIStatus("Error: " + errorMessage); } } [IteratorStateMachine(typeof(<DownloadTimeoutCoroutine>d__35))] private IEnumerator DownloadTimeoutCoroutine(string requestId, string url) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <DownloadTimeoutCoroutine>d__35(0) { <>4__this = this, requestId = requestId, url = url }; } [PunRPC] public void SetSongTitle(string url, string title) { songTitles[url] = title; if (currentSongUrl == url) { currentSongTitle = title; UpdateUIStatus("Now playing: " + title); } } [PunRPC] public void ReportDownloadComplete(int actorNumber, string url) { if (!downloadsReady.ContainsKey(url)) { downloadsReady[url] = new HashSet<int>(); } downloadsReady[url].Add(actorNumber); } private async Task WaitForPlayersReadyOrFailed(string url) { int totalPlayers = PhotonNetwork.PlayerList.Length; int readyCount; int errorCount; while (true) { readyCount = (downloadsReady.ContainsKey(url) ? downloadsReady[url].Count : 0); errorCount = (downloadErrors.ContainsKey(url) ? downloadErrors[url].Count : 0); bool flag = readyCount + errorCount >= totalPlayers; bool flag2 = flag; if (!flag2) { bool flag3 = readyCount > 0 && errorCount > 0; bool flag4 = flag3; if (flag4) { flag4 = await Task.Delay(5000).ContinueWith((Task _) => true); } flag2 = flag4; } if (flag2) { break; } await Task.Delay(100); } Logger.LogInfo((object)$"Ready to proceed with playback. Ready: {readyCount}, Errors: {errorCount}, Total: {totalPlayers}"); } [PunRPC] public void SyncPlayback(string url, int requesterId) { if (isPlaying && currentSongUrl == url && audioSource.isPlaying) { return; } if (!downloadedClips.ContainsKey(url)) { Logger.LogError((object)("Clip not found for url: " + url)); return; } CleanupCurrentPlayback(); currentSongUrl = url; if (songTitles.TryGetValue(url, out var value)) { currentSongTitle = value; } isAwaitingSyncPlayback = false; audioSource.clip = downloadedClips[url]; SetQuality(qualityLevel); UpdateAudioRangeBasedOnVolume(audioSource.volume); audioSource.Play(); isPlaying = true; UpdateUIStatus("Now playing: " + currentSongTitle); } private void CleanupCurrentPlayback() { if (audioSource.isPlaying) { audioSource.Stop(); isPlaying = false; } audioSource.clip = null; } public void RemoveFromCache(string url) { if (downloadedClips.ContainsKey(url)) { AudioClip val = downloadedClips[url]; downloadedClips.Remove(url); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } } if (songTitles.ContainsKey(url)) { songTitles.Remove(url); } if (downloadsReady.ContainsKey(url)) { downloadsReady.Remove(url); } if (downloadErrors.ContainsKey(url)) { downloadErrors.Remove(url); } } public void SetQuality(int level) { qualityLevel = Mathf.Clamp(level, 0, 4); switch (qualityLevel) { case 0: ((Behaviour)lowPassFilter).enabled = true; lowPassFilter.cutoffFrequency = 1500f; break; case 1: ((Behaviour)lowPassFilter).enabled = true; lowPassFilter.cutoffFrequency = 3000f; break; case 2: ((Behaviour)lowPassFilter).enabled = true; lowPassFilter.cutoffFrequency = 4500f; break; case 3: ((Behaviour)lowPassFilter).enabled = true; lowPassFilter.cutoffFrequency = 6000f; break; case 4: ((Behaviour)lowPassFilter).enabled = false; break; } } [PunRPC] public void UpdateQuality(int level, int requesterId) { BoomboxController component = ((Component)this).GetComponent<BoomboxController>(); SetQuality(level); } [PunRPC] public void UpdateVolume(float volume, int requesterId) { BoomboxController component = ((Component)this).GetComponent<BoomboxController>(); float volume2 = volume * maxVolumeLimit; audioSource.volume = volume2; UpdateAudioRangeBasedOnVolume(volume2); } private void UpdateAudioRangeBasedOnVolume(float volume) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Expected O, but got Unknown float num = Mathf.Lerp(maxDistanceBase, maxDistanceBase + maxDistanceAddition, volume); audioSource.minDistance = minDistance; audioSource.maxDistance = num; AnimationCurve val = new AnimationCurve((Keyframe[])(object)new Keyframe[3] { new Keyframe(0f, 1f), new Keyframe(minDistance, 0.9f), new Keyframe(num, 0f) }); audioSource.SetCustomCurve((AudioSourceCurveType)0, val); } [PunRPC] public void PausePlayback(int requesterId) { if (audioSource.isPlaying) { audioSource.Pause(); isPlaying = false; UpdateUIStatus("Paused: " + currentSongTitle); } } [PunRPC] public void StopPlayback(int requesterId) { if (audioSource.isPlaying) { audioSource.Stop(); isPlaying = false; UpdateUIStatus("Stopped: " + currentSongTitle); } } private void UpdateUIStatus(string message) { BoomboxUI component = ((Component)this).GetComponent<BoomboxUI>(); if ((Object)(object)component != (Object)null && component.IsUIVisible()) { component.UpdateStatus(message); } } public override void OnPlayerLeftRoom(Player otherPlayer) { if (isDownloadInProgress && !string.IsNullOrEmpty(currentDownloadUrl) && downloadsReady.ContainsKey(currentDownloadUrl) && !downloadsReady[currentDownloadUrl].Contains(otherPlayer.ActorNumber)) { if (!downloadErrors.ContainsKey(currentDownloadUrl)) { downloadErrors[currentDownloadUrl] = new HashSet<int>(); } downloadErrors[currentDownloadUrl].Add(otherPlayer.ActorNumber); Logger.LogInfo((object)$"Player {otherPlayer.ActorNumber} left during download - marking as error"); } ((MonoBehaviourPunCallbacks)this).OnPlayerLeftRoom(otherPlayer); } public override void OnPlayerEnteredRoom(Player newPlayer) { ((MonoBehaviourPunCallbacks)this).OnPlayerEnteredRoom(newPlayer); if (PhotonNetwork.IsMasterClient && isPlaying && !string.IsNullOrEmpty(currentSongUrl)) { Logger.LogInfo((object)$"New player {newPlayer.ActorNumber} joined - syncing current playback state"); if (songTitles.ContainsKey(currentSongUrl)) { photonView.RPC("SetSongTitle", newPlayer, new object[2] { currentSongUrl, songTitles[currentSongUrl] }); } photonView.RPC("SyncPlayback", newPlayer, new object[2] { currentSongUrl, PhotonNetwork.LocalPlayer.ActorNumber }); photonView.RPC("UpdateQuality", newPlayer, new object[2] { qualityLevel, PhotonNetwork.LocalPlayer.ActorNumber }); } } public static async Task<AudioClip> GetAudioClipAsync(string filePath) { if (!File.Exists(filePath)) { throw new Exception("Audio file not found at path: " + filePath); } Uri fileUri = new Uri(filePath); string uri = fileUri.AbsoluteUri; UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(uri, (AudioType)13); try { www.timeout = 40; TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); UnityWebRequestAsyncOperation operation = www.SendWebRequest(); ((AsyncOperation)operation).completed += delegate { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if ((int)www.result != 1) { Logger.LogError((object)("Web request failed: " + www.error + ", URI: " + uri)); tcs.SetException(new Exception("Failed to load audio file: " + www.error)); } else { tcs.SetResult(result: true); } }; await tcs.Task; AudioClip clip = DownloadHandlerAudioClip.GetContent(www); if ((Object)(object)clip != (Object)null) { try { await Task.Delay(500); if (Directory.Exists(Path.GetDirectoryName(filePath))) { Directory.Delete(Path.GetDirectoryName(filePath), recursive: true); } } catch (Exception ex2) { Exception ex = ex2; Logger.LogWarning((object)("Failed to clean up temp directory: " + ex.Message)); } } return clip; } finally { if (www != null) { ((IDisposable)www).Dispose(); } } } public void ResetBoombox(bool sendRPC = true) { try { if (isDownloadInProgress) { Logger.LogWarning((object)"Resetting ongoing download process"); ((MonoBehaviour)this).StopAllCoroutines(); } isDownloadInProgress = false; isAwaitingSyncPlayback = false; isTimeoutRecovery = false; if ((Object)(object)audioSource != (Object)null) { audioSource.Stop(); audioSource.clip = null; } isPlaying = false; currentSongUrl = ""; currentSongTitle = "No song playing"; if (!string.IsNullOrEmpty(currentDownloadUrl)) { if (downloadsReady.ContainsKey(currentDownloadUrl)) { downloadsReady[currentDownloadUrl].Clear(); } if (downloadErrors.ContainsKey(currentDownloadUrl)) { downloadErrors[currentDownloadUrl].Clear(); } RemoveFromCache(currentDownloadUrl); } currentDownloadUrl = ""; currentRequestId = ""; foreach (Coroutine value in timeoutCoroutines.Values) { if (value != null) { ((MonoBehaviour)this).StopCoroutine(value); } } timeoutCoroutines.Clear(); UpdateUIStatus("Boombox reset completed."); if (sendRPC && (Object)(object)photonView != (Object)null) { photonView.RPC("SoftResetBoombox", (RpcTarget)0, new object[1] { PhotonNetwork.LocalPlayer.ActorNumber }); } } catch (Exception ex) { Logger.LogError((object)("Error during boombox reset: " + ex.Message)); } } } public class BoomboxController : MonoBehaviourPun { private static BoomBoxCartMod Instance = BoomBoxCartMod.instance; private PhysGrabCart cart; private BoomboxUI boomboxUI; private Boombox boombox; private int currentControllerId = -1; private static ManualLogSource Logger => Instance.logger; private void Awake() { cart = ((Component)this).GetComponent<PhysGrabCart>(); boombox = ((Component)this).GetComponent<Boombox>(); if ((Object)(object)cart == (Object)null) { Logger.LogError((object)"BoomboxController: PhysGrabCart component not found!"); } else if ((Object)(object)boombox == (Object)null) { Logger.LogError((object)"BoomboxController: Boombox component not found!"); } } private bool IsLocalPlayerGrabbingCart() { return PlayerGrabbingTracker.IsLocalPlayerGrabbingCart(((Component)this).gameObject); } public void RequestBoomboxControl() { if (IsLocalPlayerGrabbingCart()) { int actorNumber = PhotonNetwork.LocalPlayer.ActorNumber; ((MonoBehaviourPun)this).photonView.RPC("RequestControl", (RpcTarget)2, new object[1] { actorNumber }); } } [PunRPC] private void RequestControl(int requesterId) { if (PhotonNetwork.IsMasterClient) { bool flag = true; if (requesterId == PhotonNetwork.LocalPlayer.ActorNumber) { flag = PlayerGrabbingTracker.IsLocalPlayerGrabbingCart(((Component)this).gameObject); } if (currentControllerId == -1 && flag) { ((MonoBehaviourPun)this).photonView.RPC("SetController", (RpcTarget)0, new object[1] { requesterId }); } } } [PunRPC] private void SetController(int controllerId) { try { currentControllerId = controllerId; if (controllerId == PhotonNetwork.LocalPlayer.ActorNumber) { EnsureBoomboxUIExists(); if ((Object)(object)boomboxUI != (Object)null) { boomboxUI.ShowUI(); } else { Logger.LogError((object)"Failed to create BoomboxUI component"); } } else if ((Object)(object)boomboxUI != (Object)null && boomboxUI.IsUIVisible()) { boomboxUI.HideUI(); } } catch (Exception ex) { Logger.LogError((object)("Error in SetController: " + ex.Message + "\n" + ex.StackTrace)); } } private void EnsureBoomboxUIExists() { if ((Object)(object)boomboxUI == (Object)null) { boomboxUI = ((Component)this).gameObject.GetComponent<BoomboxUI>(); if ((Object)(object)boomboxUI == (Object)null) { boomboxUI = ((Component)this).gameObject.AddComponent<BoomboxUI>(); } } } public void ReleaseControl() { if (currentControllerId == PhotonNetwork.LocalPlayer.ActorNumber) { ((MonoBehaviourPun)this).photonView.RPC("RequestRelease", (RpcTarget)2, new object[1] { PhotonNetwork.LocalPlayer.ActorNumber }); } } [PunRPC] private void RequestRelease(int releaserId) { if (PhotonNetwork.IsMasterClient && currentControllerId == releaserId) { ((MonoBehaviourPun)this).photonView.RPC("SetController", (RpcTarget)0, new object[1] { -1 }); } } private void OnPlayerReleasedCart(int playerActorNumber) { if (playerActorNumber == currentControllerId && PhotonNetwork.IsMasterClient) { ((MonoBehaviourPun)this).photonView.RPC("SetController", (RpcTarget)0, new object[1] { -1 }); } } public void LocalPlayerReleasedCart() { int actorNumber = PhotonNetwork.LocalPlayer.ActorNumber; if (currentControllerId == actorNumber) { ReleaseControl(); } } } public class BoomboxUI : MonoBehaviourPun { private static BoomBoxCartMod Instance = BoomBoxCartMod.instance; public PhotonView photonView; private bool showUI = false; private string urlInput = ""; private float normalizedVolume = 0.3f; private float lastSentNormalizedVolume = 0.3f; private bool isSliderBeingDragged = false; private int qualityLevel = 3; private string[] qualityLabels = new string[5] { "REALLY Low (You Freak)", "Low", "Medium-Low", "Medium-High (Recommended)", "High" }; private bool isQualitySliderBeingDragged = false; private int lastSentQualityLevel = 3; private Rect windowRect; private Boombox boombox; private BoomboxController controller; private GUIStyle windowStyle; private GUIStyle headerStyle; private GUIStyle buttonStyle; private GUIStyle smallButtonStyle; private GUIStyle textFieldStyle; private GUIStyle labelStyle; private GUIStyle sliderStyle; private GUIStyle statusStyle; private GUIStyle scrollViewStyle; private Texture2D backgroundTexture; private Texture2D buttonTexture; private Texture2D sliderBackgroundTexture; private Texture2D sliderThumbTexture; private Texture2D textFieldBackgroundTexture; private Vector2 urlScrollPosition = Vector2.zero; private float textFieldVisibleWidth = 350f; private string errorMessage = ""; private float errorMessageTime = 0f; private string statusMessage = ""; private CursorLockMode previousLockMode; private bool previousCursorVisible; private bool stylesInitialized = false; private Vector2 scrollPosition = Vector2.zero; private bool shouldClearFocus = false; private static ManualLogSource Logger => Instance.logger; private void Awake() { //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) try { boombox = ((Component)this).GetComponent<Boombox>(); if ((Object)(object)boombox != (Object)null) { photonView = boombox.photonView; } else { Logger.LogError((object)"BoomboxUI: Failed to find Boombox component"); photonView = ((Component)this).GetComponent<PhotonView>(); } controller = ((Component)this).GetComponent<BoomboxController>(); if ((Object)(object)photonView == (Object)null) { Logger.LogError((object)"BoomboxUI: Failed to find PhotonView component"); } windowRect = new Rect((float)(Screen.width / 2 - 200), (float)(Screen.height / 2 - 175), 400f, 550f); } catch (Exception ex) { Logger.LogError((object)("Error in BoomboxUI.Awake: " + ex.Message + "\n" + ex.StackTrace)); } } private void Update() { if (Time.time > errorMessageTime && !string.IsNullOrEmpty(errorMessage)) { errorMessage = ""; } if (showUI && Keyboard.current != null && ((ButtonControl)Keyboard.current.escapeKey).wasPressedThisFrame) { if ((Object)(object)controller != (Object)null) { controller.ReleaseControl(); } else { HideUI(); } } if (isSliderBeingDragged && Input.GetMouseButtonUp(0)) { isSliderBeingDragged = false; SendVolumeUpdate(); } if (isQualitySliderBeingDragged && Input.GetMouseButtonUp(0)) { isQualitySliderBeingDragged = false; SendQualityUpdate(); } } public void ShowUI() { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) if (showUI) { return; } if ((Object)(object)boombox == (Object)null || (Object)(object)photonView == (Object)null) { Logger.LogError((object)"Cannot show UI - boombox or photonView is null"); return; } showUI = true; previousLockMode = Cursor.lockState; previousCursorVisible = Cursor.visible; Cursor.visible = true; Cursor.lockState = (CursorLockMode)0; if ((Object)(object)boombox != (Object)null) { normalizedVolume = boombox.audioSource.volume / boombox.maxVolumeLimit; lastSentNormalizedVolume = normalizedVolume; qualityLevel = boombox.qualityLevel; lastSentQualityLevel = qualityLevel; } UpdateStatusFromBoombox(); } public void UpdateStatusFromBoombox() { if ((Object)(object)boombox != (Object)null) { if (boombox.IsDownloadInProgress()) { statusMessage = "Downloading audio from " + boombox.GetCurrentDownloadUrl() + "..."; } else if (boombox.isPlaying) { statusMessage = "Now playing: " + boombox.currentSongTitle; } else if (!string.IsNullOrEmpty(boombox.currentSongUrl)) { statusMessage = "Ready to play: " + boombox.currentSongTitle; } else { statusMessage = "Ready to play music! Enter a Video URL"; } } } private void SendVolumeUpdate() { if (normalizedVolume != lastSentNormalizedVolume) { lastSentNormalizedVolume = normalizedVolume; if ((Object)(object)boombox.audioSource != (Object)null) { float volume = normalizedVolume * boombox.maxVolumeLimit; boombox.audioSource.volume = volume; } if ((Object)(object)photonView != (Object)null) { photonView.RPC("UpdateVolume", (RpcTarget)3, new object[2] { normalizedVolume, PhotonNetwork.LocalPlayer.ActorNumber }); } } } private void SendQualityUpdate() { if (qualityLevel != lastSentQualityLevel) { lastSentQualityLevel = qualityLevel; if ((Object)(object)boombox != (Object)null) { boombox.SetQuality(qualityLevel); } if ((Object)(object)photonView != (Object)null) { photonView.RPC("UpdateQuality", (RpcTarget)3, new object[2] { qualityLevel, PhotonNetwork.LocalPlayer.ActorNumber }); } } } public void HideUI() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) if (showUI) { showUI = false; Cursor.lockState = previousLockMode; Cursor.visible = previousCursorVisible; } } public bool IsUIVisible() { return showUI; } public void UpdateStatus(string message) { statusMessage = message; } private void InitializeStyles() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Expected O, but got Unknown //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Expected O, but got Unknown //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Expected O, but got Unknown //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Expected O, but got Unknown //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Unknown result type (might be due to invalid IL or missing references) //IL_01a7: Expected O, but got Unknown //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Expected O, but got Unknown //IL_01f4: Unknown result type (might be due to invalid IL or missing references) //IL_0224: Unknown result type (might be due to invalid IL or missing references) //IL_023f: Unknown result type (might be due to invalid IL or missing references) //IL_0255: Unknown result type (might be due to invalid IL or missing references) //IL_026b: Unknown result type (might be due to invalid IL or missing references) //IL_0290: Unknown result type (might be due to invalid IL or missing references) //IL_029a: Expected O, but got Unknown //IL_02a5: Unknown result type (might be due to invalid IL or missing references) //IL_02af: Expected O, but got Unknown //IL_02c4: Unknown result type (might be due to invalid IL or missing references) //IL_02ce: Expected O, but got Unknown //IL_02d8: Unknown result type (might be due to invalid IL or missing references) //IL_02e2: Expected O, but got Unknown //IL_02fc: Unknown result type (might be due to invalid IL or missing references) //IL_0306: Expected O, but got Unknown //IL_0337: Unknown result type (might be due to invalid IL or missing references) //IL_035c: Unknown result type (might be due to invalid IL or missing references) //IL_0366: Expected O, but got Unknown //IL_0372: Unknown result type (might be due to invalid IL or missing references) //IL_037c: Expected O, but got Unknown //IL_039d: Unknown result type (might be due to invalid IL or missing references) //IL_03a7: Expected O, but got Unknown //IL_03b2: Unknown result type (might be due to invalid IL or missing references) //IL_03bc: Expected O, but got Unknown //IL_03c8: Unknown result type (might be due to invalid IL or missing references) //IL_03d2: Expected O, but got Unknown //IL_03dd: Unknown result type (might be due to invalid IL or missing references) //IL_0401: Unknown result type (might be due to invalid IL or missing references) //IL_040b: Expected O, but got Unknown //IL_0417: Unknown result type (might be due to invalid IL or missing references) //IL_0421: Expected O, but got Unknown //IL_042c: Unknown result type (might be due to invalid IL or missing references) //IL_046a: Unknown result type (might be due to invalid IL or missing references) //IL_0474: Expected O, but got Unknown if (!stylesInitialized) { backgroundTexture = CreateColorTexture(new Color(0.1f, 0.1f, 0.1f, 0.9f)); buttonTexture = CreateColorTexture(new Color(0.2f, 0.2f, 0.3f, 1f)); sliderBackgroundTexture = CreateColorTexture(new Color(0.15f, 0.15f, 0.2f, 1f)); sliderThumbTexture = CreateColorTexture(new Color(0.7f, 0.7f, 0.8f, 1f)); textFieldBackgroundTexture = CreateColorTexture(new Color(0.15f, 0.17f, 0.2f, 1f)); windowStyle = new GUIStyle(GUI.skin.window); windowStyle.normal.background = backgroundTexture; windowStyle.onNormal.background = backgroundTexture; windowStyle.border = new RectOffset(10, 10, 10, 10); windowStyle.padding = new RectOffset(15, 15, 20, 15); headerStyle = new GUIStyle(GUI.skin.label); headerStyle.fontSize = 18; headerStyle.fontStyle = (FontStyle)1; headerStyle.normal.textColor = Color.white; headerStyle.alignment = (TextAnchor)4; headerStyle.margin = new RectOffset(0, 0, 10, 20); buttonStyle = new GUIStyle(GUI.skin.button); buttonStyle.normal.background = buttonTexture; buttonStyle.hover.background = CreateColorTexture(new Color(0.3f, 0.3f, 0.4f, 1f)); buttonStyle.active.background = CreateColorTexture(new Color(0.4f, 0.4f, 0.5f, 1f)); buttonStyle.normal.textColor = Color.white; buttonStyle.hover.textColor = Color.white; buttonStyle.active.textColor = Color.white; buttonStyle.fontSize = 14; buttonStyle.padding = new RectOffset(15, 15, 8, 8); buttonStyle.margin = new RectOffset(5, 5, 5, 5); buttonStyle.alignment = (TextAnchor)4; smallButtonStyle = new GUIStyle(buttonStyle); smallButtonStyle.padding = new RectOffset(8, 8, 4, 4); smallButtonStyle.fontSize = 12; textFieldStyle = new GUIStyle(GUI.skin.textField); textFieldStyle.normal.background = textFieldBackgroundTexture; textFieldStyle.normal.textColor = new Color(1f, 1f, 1f); textFieldStyle.fontSize = 14; textFieldStyle.padding = new RectOffset(10, 10, 8, 8); scrollViewStyle = new GUIStyle(GUI.skin.scrollView); scrollViewStyle.normal.background = textFieldBackgroundTexture; scrollViewStyle.border = new RectOffset(2, 2, 2, 2); scrollViewStyle.padding = new RectOffset(0, 0, 0, 0); labelStyle = new GUIStyle(GUI.skin.label); labelStyle.normal.textColor = Color.white; labelStyle.fontSize = 14; labelStyle.margin = new RectOffset(0, 0, 10, 5); statusStyle = new GUIStyle(GUI.skin.label); statusStyle.normal.textColor = Color.cyan; statusStyle.fontSize = 14; statusStyle.wordWrap = true; statusStyle.alignment = (TextAnchor)4; sliderStyle = new GUIStyle(GUI.skin.horizontalSlider); sliderStyle.normal.background = sliderBackgroundTexture; stylesInitialized = true; } } private Texture2D CreateColorTexture(Color color) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Expected O, but got Unknown //IL_000c: Unknown result type (might be due to invalid IL or missing references) Texture2D val = new Texture2D(1, 1); val.SetPixel(0, 0, color); val.Apply(); return val; } private void OnGUI() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown //IL_0048: 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) if (showUI) { if (!stylesInitialized) { InitializeStyles(); } windowRect = GUILayout.Window(0, windowRect, new WindowFunction(DrawUI), "Boombox Controller", windowStyle, Array.Empty<GUILayoutOption>()); } } private void DrawUI(int windowID) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_0197: Unknown result type (might be due to invalid IL or missing references) //IL_02a3: Unknown result type (might be due to invalid IL or missing references) //IL_02aa: Unknown result type (might be due to invalid IL or missing references) //IL_02af: Unknown result type (might be due to invalid IL or missing references) //IL_02b8: Unknown result type (might be due to invalid IL or missing references) //IL_03b6: Unknown result type (might be due to invalid IL or missing references) //IL_03d8: Unknown result type (might be due to invalid IL or missing references) //IL_0635: Unknown result type (might be due to invalid IL or missing references) GUILayout.Label("Control The Boombox In The Cart", headerStyle, Array.Empty<GUILayoutOption>()); scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, false, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(true) }); GUILayout.Space(10f); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label("Enter Video URL:", labelStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); if (GUILayout.Button("Clear", smallButtonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(60f) })) { urlInput = ""; GUI.FocusControl((string)null); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); urlScrollPosition = GUILayout.BeginScrollView(urlScrollPosition, false, false, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(60f) }); urlInput = GUILayout.TextField(urlInput, textFieldStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) }); urlInput = Regex.Replace(urlInput, "\\s+", ""); GUILayout.EndScrollView(); GUILayout.EndHorizontal(); GUILayout.Space(15f); float num = normalizedVolume * 100f; GUILayout.Label($"Volume: {Mathf.Round(num)}%", labelStyle, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); Rect lastRect; if ((int)Event.current.type == 0) { lastRect = GUILayoutUtility.GetLastRect(); if (((Rect)(ref lastRect)).Contains(Event.current.mousePosition)) { isSliderBeingDragged = true; } } float num2 = GUILayout.HorizontalSlider(normalizedVolume, 0f, 1f, sliderStyle, GUI.skin.horizontalSliderThumb, Array.Empty<GUILayoutOption>()); if (num2 != normalizedVolume && !isSliderBeingDragged) { isSliderBeingDragged = true; } normalizedVolume = num2; if ((Object)(object)boombox != (Object)null && (Object)(object)boombox.audioSource != (Object)null) { float volume = normalizedVolume * boombox.maxVolumeLimit; boombox.audioSource.volume = volume; } GUILayout.EndHorizontal(); GUILayout.Space(15f); GUILayout.Label("Audio Quality: " + qualityLabels[qualityLevel], labelStyle, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if ((int)Event.current.type == 0) { lastRect = GUILayoutUtility.GetLastRect(); if (((Rect)(ref lastRect)).Contains(Event.current.mousePosition)) { isQualitySliderBeingDragged = true; } } float num3 = GUILayout.HorizontalSlider((float)qualityLevel, 0f, 4f, sliderStyle, GUI.skin.horizontalSliderThumb, Array.Empty<GUILayoutOption>()); int num4 = Mathf.RoundToInt(num3); if (num4 != qualityLevel && !isQualitySliderBeingDragged) { isQualitySliderBeingDragged = true; } qualityLevel = num4; if ((Object)(object)boombox != (Object)null) { boombox.SetQuality(qualityLevel); } GUILayout.EndHorizontal(); GUILayout.Space(15f); if (!string.IsNullOrEmpty(statusMessage)) { GUILayout.Label(statusMessage, statusStyle, Array.Empty<GUILayoutOption>()); GUILayout.Space(5f); } if (!string.IsNullOrEmpty(errorMessage)) { GUI.color = Color.red; GUILayout.Label(errorMessage, labelStyle, Array.Empty<GUILayoutOption>()); GUI.color = Color.white; GUILayout.Space(5f); } GUILayout.Space(10f); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUI.enabled = (Object)(object)boombox != (Object)null && !boombox.IsDownloadInProgress(); if (GUILayout.Button("▶ PLAY", buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(40f) })) { if (IsValidVideoUrl(urlInput)) { photonView.RPC("RequestSong", (RpcTarget)0, new object[2] { urlInput, PhotonNetwork.LocalPlayer.ActorNumber }); GUI.FocusControl((string)null); } else { ShowErrorMessage("Invalid Video URL!"); } } GUI.enabled = true; GUI.enabled = (Object)(object)boombox != (Object)null && boombox.isPlaying; if (GUILayout.Button("■ STOP", buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(40f) })) { photonView.RPC("StopPlayback", (RpcTarget)0, new object[1] { PhotonNetwork.LocalPlayer.ActorNumber }); GUI.FocusControl((string)null); } GUI.enabled = true; GUILayout.EndHorizontal(); if ((Object)(object)boombox != (Object)null && boombox.IsDownloadInProgress()) { GUILayout.Space(10f); GUILayout.Label("Download in progress...", statusStyle, Array.Empty<GUILayoutOption>()); } GUILayout.EndScrollView(); GUILayout.Space(10f); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.FlexibleSpace(); if (GUILayout.Button("Close", buttonStyle, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(100f), GUILayout.Height(30f) })) { if ((Object)(object)controller != (Object)null) { controller.ReleaseControl(); } else { HideUI(); } } GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); GUI.DragWindow(new Rect(0f, 0f, ((Rect)(ref windowRect)).width, 30f)); } private bool IsValidVideoUrl(string url) { return Boombox.IsValidVideoUrl(url); } private void ShowErrorMessage(string message) { Debug.LogError((object)message); errorMessage = message; errorMessageTime = Time.time + 3f; } private void OnDestroy() { if ((Object)(object)backgroundTexture != (Object)null) { Object.Destroy((Object)(object)backgroundTexture); } if ((Object)(object)buttonTexture != (Object)null) { Object.Destroy((Object)(object)buttonTexture); } if ((Object)(object)sliderBackgroundTexture != (Object)null) { Object.Destroy((Object)(object)sliderBackgroundTexture); } if ((Object)(object)sliderThumbTexture != (Object)null) { Object.Destroy((Object)(object)sliderThumbTexture); } } } [BepInPlugin("ColtG5.BoomboxCart", "BoomboxCart", "1.1.0")] public class BoomBoxCartMod : BaseUnityPlugin { private const string modGUID = "ColtG5.BoomboxCart"; private const string modName = "BoomboxCart"; private const string modVersion = "1.1.0"; private readonly Harmony harmony = new Harmony("ColtG5.BoomboxCart"); internal static BoomBoxCartMod instance; internal ManualLogSource logger; private void Awake() { if ((Object)(object)instance == (Object)null) { instance = this; } logger = Logger.CreateLogSource("ColtG5.BoomboxCart"); logger.LogInfo((object)"BoomBoxCartMod loaded!"); Task.Run(delegate { YoutubeDL.InitializeAsync().Wait(); }); harmony.PatchAll(); } } } namespace BoomBoxCartMod.Util { public static class YoutubeDL { private static BoomBoxCartMod Instance = BoomBoxCartMod.instance; private static readonly string baseFolder = Path.Combine(Directory.GetCurrentDirectory(), "BoomboxedCart"); private const string YTDLP_URL = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"; private const string FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"; private static readonly string ytDlpPath = Path.Combine(baseFolder, "yt-dlp.exe"); private static readonly string ffmpegFolder = Path.Combine(baseFolder, "ffmpeg"); private static string ffmpegBinPath = Path.Combine(ffmpegFolder, "ffmpeg-master-latest-win64-gpl", "bin", "ffmpeg.exe"); private static ManualLogSource Logger => Instance.logger; public static async Task InitializeAsync() { if (!Directory.Exists(baseFolder)) { Directory.CreateDirectory(baseFolder); } if (!File.Exists(ytDlpPath)) { Logger.LogInfo((object)"yt-dlp not found. Downloading..."); await DownloadFileAsync("https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe", ytDlpPath); } bool needsFFmpeg = !File.Exists(ffmpegBinPath); if (!needsFFmpeg && !Directory.Exists(Path.GetDirectoryName(ffmpegBinPath))) { needsFFmpeg = true; } if (needsFFmpeg) { Logger.LogInfo((object)"ffmpeg not found. Downloading and extracting..."); await DownloadAndExtractFFmpegAsync(); } if (!File.Exists(ffmpegBinPath)) { throw new Exception("ffmpeg executable was not found at " + ffmpegBinPath + " after extraction. Internet problem? Not on Windows problem?"); } Logger.LogInfo((object)"Initialization complete."); } private static async Task DownloadFileAsync(string url, string destinationPath) { HttpClient client = new HttpClient(); try { File.WriteAllBytes(destinationPath, await client.GetByteArrayAsync(url)); } finally { ((IDisposable)client)?.Dispose(); } } private static async Task DownloadAndExtractFFmpegAsync() { string zipPath = Path.Combine(baseFolder, "ffmpeg.zip"); try { if (Directory.Exists(ffmpegFolder)) { try { Directory.Delete(ffmpegFolder, recursive: true); Directory.CreateDirectory(ffmpegFolder); } catch (Exception ex3) { Exception ex2 = ex3; Logger.LogWarning((object)("Failed to clean ffmpeg folder: " + ex2.Message)); } } Logger.LogInfo((object)"Downloading FFmpeg from https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip..."); await DownloadFileAsync("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip", zipPath); if (!File.Exists(zipPath)) { throw new Exception("FFmpeg zip file not downloaded properly."); } Logger.LogInfo((object)"Downloaded FFmpeg zip file. Extracting..."); ZipFile.ExtractToDirectory(zipPath, ffmpegFolder); File.Delete(zipPath); if (!File.Exists(ffmpegBinPath)) { Logger.LogInfo((object)"FFmpeg not found at expected path. Searching for ffmpeg.exe in extracted files..."); string[] ffmpegFiles = Directory.GetFiles(ffmpegFolder, "ffmpeg.exe", SearchOption.AllDirectories); if (ffmpegFiles.Length == 0) { Logger.LogError((object)"ffmpeg.exe not found in extracted files. Uh oh!"); throw new Exception("ffmpeg.exe not found in extracted files. Uh oh!"); } string newPath = ffmpegFiles[0]; Logger.LogInfo((object)("Found ffmpeg.exe at: " + newPath)); ffmpegBinPath = newPath; } Logger.LogInfo((object)"FFmpeg extracted successfully."); } catch (Exception ex3) { Exception ex = ex3; Logger.LogError((object)("Error downloading or extracting FFmpeg: " + ex.Message)); throw; } } public static async Task<(string filePath, string title)> DownloadAudioWithTitleAsync(string videoUrl) { await InitializeAsync(); string tempFolder = Path.Combine(baseFolder, Guid.NewGuid().ToString()); Directory.CreateDirectory(tempFolder); Logger.LogInfo((object)("Getting title and downloading audio for " + videoUrl + "...")); return await Task.Run(async delegate { try { string title = await GetVideoTitleInternalAsync(videoUrl); if (string.IsNullOrEmpty(title)) { title = "Unknown Title"; } title = title.Replace("\n", "").Replace("\r", ""); string noIckySpecialCharsFileName = $"audio_{DateTime.Now.Ticks}.%(ext)s"; string command = "-x --audio-format mp3 --audio-quality 192K --ffmpeg-location \"" + ffmpegBinPath + "\" --output \"" + Path.Combine(tempFolder, noIckySpecialCharsFileName) + "\" " + videoUrl; ProcessStartInfo processInfo = new ProcessStartInfo { FileName = ytDlpPath, Arguments = command, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8 }; using (Process process = Process.Start(processInfo)) { if (process == null) { throw new Exception("Failed to start yt-dlp process."); } await process.StandardOutput.ReadToEndAsync(); string error = await process.StandardError.ReadToEndAsync(); process.WaitForExit(); if (!string.IsNullOrEmpty(error)) { Logger.LogInfo((object)"An error recorded in yt-dlp download, error is probably not fatal though"); } if (process.ExitCode != 0) { throw new Exception($"yt-dlp download failed. Exit Code: {process.ExitCode}. Error: {error}"); } } await Task.Delay(1000); string audioFilePath = Directory.GetFiles(tempFolder, "*.mp3").FirstOrDefault(); if (audioFilePath == null) { string[] allFiles = Directory.GetFiles(tempFolder); Logger.LogError((object)$"No MP3 files found. Total files: {allFiles.Length}"); string[] array = allFiles; foreach (string file in array) { Logger.LogError((object)("Found file: " + file)); } throw new Exception("Audio download failed. No MP3 file created."); } return (audioFilePath, title); } catch (Exception ex2) { Exception ex = ex2; Logger.LogError((object)$"Download Error: {ex}"); Logger.LogError((object)("Stack Trace: " + ex.StackTrace)); if (Directory.Exists(tempFolder)) { try { Directory.Delete(tempFolder, recursive: true); } catch (Exception ex2) { Exception cleanupEx = ex2; Logger.LogError((object)$"Failed to clean up temp folder: {cleanupEx}"); } } throw; } }); } private static async Task<string> GetVideoTitleInternalAsync(string url) { try { ProcessStartInfo psi = new ProcessStartInfo { FileName = ytDlpPath, Arguments = "--get-title " + url, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; Process process = new Process { StartInfo = psi }; try { new TaskCompletionSource<string>(); process.Start(); string title = await process.StandardOutput.ReadToEndAsync(); await process.StandardError.ReadToEndAsync(); Task timeoutTask = Task.Delay(10000); Task processExitTask = Task.Run(delegate { process.WaitForExit(); }); if (await Task.WhenAny(new Task[2] { processExitTask, timeoutTask }) == timeoutTask) { try { process.Kill(); } catch { } Logger.LogWarning((object)"yt-dlp title fetch timed out"); return "Unknown Title (Timeout)"; } if (process.ExitCode != 0) { Logger.LogError((object)$"yt-dlp error code: {process.ExitCode}"); return "Unknown Title"; } Logger.LogInfo((object)("Got video title: " + title)); if (!string.IsNullOrEmpty(title)) { try { title = new string(title.Where((char c) => !char.IsControl(c) || c == '\n' || c == '\r' || c == '\t').ToArray()); } catch (Exception ex3) { Exception ex2 = ex3; Logger.LogWarning((object)("Error sanitizing title: " + ex2.Message)); } } return string.IsNullOrEmpty(title) ? "Unknown Title" : title; } finally { if (process != null) { ((IDisposable)process).Dispose(); } } } catch (Exception ex3) { Exception ex = ex3; Logger.LogError((object)("Error getting video title: " + ex.Message)); return "Unknown Title"; } } } } namespace BoomBoxCartMod.Patches { [HarmonyPatch(typeof(PhysGrabCart))] internal class PhysGrabCartPatch { private static BoomBoxCartMod Instance = BoomBoxCartMod.instance; private static ManualLogSource Logger => Instance.logger; [HarmonyPatch("Start")] [HarmonyPostfix] private static void PatchPhysGrabCartStart(PhysGrabCart __instance) { if (!((Object)(object)RunManager.instance.levelCurrent == (Object)(object)RunManager.instance.levelShop)) { if ((Object)(object)((Component)__instance).GetComponent<Boombox>() == (Object)null) { ((Component)__instance).gameObject.AddComponent<Boombox>(); } if ((Object)(object)((Component)__instance).GetComponent<BoomboxController>() == (Object)null) { ((Component)__instance).gameObject.AddComponent<BoomboxController>(); } } } } public static class PlayerGrabbingTracker { public static Dictionary<int, GameObject> playerGrabbingMap = new Dictionary<int, GameObject>(); public static bool IsLocalPlayerGrabbingCart(GameObject cart) { int actorNumber = PhotonNetwork.LocalPlayer.ActorNumber; return playerGrabbingMap.ContainsKey(actorNumber) && (Object)(object)playerGrabbingMap[actorNumber] == (Object)(object)cart; } public static void SetLocalPlayerGrabbing(GameObject obj) { int actorNumber = PhotonNetwork.LocalPlayer.ActorNumber; if ((Object)(object)obj != (Object)null) { playerGrabbingMap[actorNumber] = obj; } else if (playerGrabbingMap.ContainsKey(actorNumber)) { playerGrabbingMap.Remove(actorNumber); } } } [HarmonyPatch(typeof(PlayerController))] internal class PlayerControllerPatch { private static BoomBoxCartMod Instance = BoomBoxCartMod.instance; private static ManualLogSource Logger => Instance.logger; [HarmonyPatch("Update")] [HarmonyPostfix] private static void PatchPlayerControllerUpdate(PlayerController __instance) { if ((Object)(object)__instance.physGrabObject != (Object)null) { PlayerGrabbingTracker.SetLocalPlayerGrabbing(__instance.physGrabObject.gameObject); if (Keyboard.current != null && ((ButtonControl)Keyboard.current.yKey).wasPressedThisFrame && (Object)(object)__instance.physGrabObject.GetComponent<Boombox>() != (Object)null) { BoomboxController component = __instance.physGrabObject.GetComponent<BoomboxController>(); if ((Object)(object)component != (Object)null) { component.RequestBoomboxControl(); } } } else { PlayerGrabbingTracker.SetLocalPlayerGrabbing(null); } } } }