using System;
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.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using SemiBoombox.Utils;
using UnityEngine;
using UnityEngine.Networking;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("SemiBoombox")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("Semi Boombox")]
[assembly: AssemblyTitle("SemiBoombox")]
[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 SemiBoombox
{
public class Boombox : MonoBehaviour
{
public PhotonView photonView;
public AudioSource audioSource;
private static Dictionary<string, AudioClip> downloadedClips = new Dictionary<string, AudioClip>();
private static Dictionary<string, HashSet<int>> downloadsReady = new Dictionary<string, HashSet<int>>();
private void Awake()
{
//IL_0059: Unknown result type (might be due to invalid IL or missing references)
//IL_005f: Expected O, but got Unknown
audioSource = ((Component)this).gameObject.AddComponent<AudioSource>();
audioSource.spatialBlend = 1f;
audioSource.playOnAwake = false;
audioSource.minDistance = 1f;
audioSource.maxDistance = 30f;
audioSource.rolloffMode = (AudioRolloffMode)2;
AnimationCurve val = new AnimationCurve();
val.AddKey(1f, 1f);
val.AddKey(30f, 0f);
audioSource.SetCustomCurve((AudioSourceCurveType)0, val);
photonView = ((Component)this).GetComponent<PhotonView>();
if ((Object)(object)photonView == (Object)null)
{
Debug.LogError((object)"PhotonView not found on Boombox object.");
}
else if (photonView.IsMine)
{
audioSource.volume = 0.1f;
((Component)this).gameObject.AddComponent<BoomboxUI>();
}
else
{
audioSource.volume = 0.2f;
}
}
[PunRPC]
public async void RequestSong(string url, int requesterId)
{
Debug.Log((object)$"RequestSong RPC received: url={url}, requesterId={requesterId}");
if (!downloadedClips.ContainsKey(url))
{
try
{
AudioClip value = await GetAudioClipAsync(await YoutubeDL.DownloadAudioAsync(url));
downloadedClips[url] = value;
Debug.Log((object)("Downloaded and cached clip for url: " + url));
}
catch (Exception ex)
{
Debug.LogError((object)("Failed to download audio: " + ex.Message));
return;
}
}
else
{
Debug.Log((object)("Clip already cached for url: " + url));
}
photonView.RPC("ReportDownloadComplete", (RpcTarget)0, new object[2]
{
PhotonNetwork.LocalPlayer.ActorNumber,
url
});
await WaitForAllPlayersReady(url);
photonView.RPC("SyncPlayback", (RpcTarget)0, new object[2] { url, requesterId });
}
[PunRPC]
public void ReportDownloadComplete(int actorNumber, string url)
{
if (!downloadsReady.ContainsKey(url))
{
downloadsReady[url] = new HashSet<int>();
}
downloadsReady[url].Add(actorNumber);
Debug.Log((object)$"Player {actorNumber} reported ready for url: {url}. Total ready: {downloadsReady[url].Count}");
}
private async Task WaitForAllPlayersReady(string url)
{
int totalPlayers = PhotonNetwork.PlayerList.Length;
while (!downloadsReady.ContainsKey(url) || downloadsReady[url].Count < totalPlayers)
{
await Task.Delay(100);
}
}
[PunRPC]
public void SyncPlayback(string url, int requesterId)
{
Debug.Log((object)$"SyncPlayback RPC received: url={url}, requesterId={requesterId}");
Boombox boombox = FindBoomboxForPlayer(requesterId);
if ((Object)(object)boombox == (Object)null)
{
Debug.LogError((object)$"No boombox found for player {requesterId}");
return;
}
if (!downloadedClips.ContainsKey(url))
{
Debug.LogError((object)("Clip not found for url: " + url));
return;
}
boombox.audioSource.clip = downloadedClips[url];
boombox.audioSource.Play();
}
[PunRPC]
public void StopPlayback(int requesterId)
{
if (photonView.Owner != null && photonView.Owner.ActorNumber == requesterId)
{
Debug.Log((object)$"Stopping playback on Boombox owned by player {requesterId}");
if (audioSource.isPlaying)
{
audioSource.Stop();
}
}
}
private static Boombox FindBoomboxForPlayer(int playerId)
{
Boombox[] array = Object.FindObjectsOfType<Boombox>();
foreach (Boombox boombox in array)
{
if (boombox.photonView.Owner != null && boombox.photonView.Owner.ActorNumber == playerId)
{
return boombox;
}
}
return null;
}
public static async Task<AudioClip> GetAudioClipAsync(string filePath)
{
string text = "file://" + filePath;
UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(text, (AudioType)13);
try
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
((AsyncOperation)www.SendWebRequest()).completed += delegate
{
//IL_0006: Unknown result type (might be due to invalid IL or missing references)
//IL_000c: Invalid comparison between Unknown and I4
if ((int)www.result != 1)
{
tcs.SetException(new Exception("Failed to load audio file: " + www.error));
}
else
{
tcs.SetResult(result: true);
}
};
await tcs.Task;
AudioClip content = DownloadHandlerAudioClip.GetContent(www);
if (Directory.Exists(Path.GetDirectoryName(filePath)))
{
Directory.Delete(Path.GetDirectoryName(filePath), recursive: true);
}
return content;
}
finally
{
if (www != null)
{
((IDisposable)www).Dispose();
}
}
}
}
public class BoomboxUI : MonoBehaviour
{
public PhotonView photonView;
private bool showUI;
private string urlInput = "";
private float volume = 0.15f;
private Rect windowRect = new Rect(100f, 100f, 350f, 200f);
private Boombox boombox;
private void Awake()
{
boombox = ((Component)this).GetComponent<Boombox>();
photonView = boombox.photonView;
}
private void Update()
{
if (Input.GetKeyDown((KeyCode)120))
{
showUI = !showUI;
}
}
private void OnGUI()
{
//IL_000b: Unknown result type (might be due to invalid IL or missing references)
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_0026: Expected O, but got Unknown
//IL_0021: Unknown result type (might be due to invalid IL or missing references)
//IL_0026: Unknown result type (might be due to invalid IL or missing references)
if (showUI)
{
windowRect = GUI.Window(0, windowRect, new WindowFunction(DrawUI), "Boombox Controller");
}
}
private void DrawUI(int windowID)
{
GUILayout.Label("Enter YouTube URL:", Array.Empty<GUILayoutOption>());
urlInput = GUILayout.TextField(urlInput, 200, Array.Empty<GUILayoutOption>());
GUILayout.Space(10f);
GUILayout.Label($"Volume: {Mathf.Round(volume * 100f)}%", Array.Empty<GUILayoutOption>());
volume = GUILayout.HorizontalSlider(volume, 0f, 1f, Array.Empty<GUILayoutOption>());
if ((Object)(object)boombox.audioSource != (Object)null)
{
boombox.audioSource.volume = volume;
}
GUILayout.Space(10f);
GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
if (GUILayout.Button("Play", Array.Empty<GUILayoutOption>()))
{
if (IsValidUrl(urlInput))
{
photonView.RPC("RequestSong", (RpcTarget)0, new object[2]
{
urlInput,
PhotonNetwork.LocalPlayer.ActorNumber
});
}
else
{
Debug.LogError((object)"Invalid URL!");
}
}
if (GUILayout.Button("Stop", Array.Empty<GUILayoutOption>()))
{
photonView.RPC("StopPlayback", (RpcTarget)0, new object[1] { PhotonNetwork.LocalPlayer.ActorNumber });
}
if (GUILayout.Button("Close", Array.Empty<GUILayoutOption>()))
{
showUI = false;
}
GUILayout.EndHorizontal();
GUI.DragWindow();
}
private bool IsValidUrl(string url)
{
string pattern = "^https?:\\/\\/(www\\.)?youtube\\.com\\/watch\\?v=[a-zA-Z0-9_-]+$";
return Regex.IsMatch(url, pattern);
}
}
[BepInPlugin("SemiBoombox", "Semi Boombox", "1.0.0")]
public class Plugin : BaseUnityPlugin
{
internal static ManualLogSource Logger;
private static Harmony _harmony;
private void Awake()
{
//IL_0044: Unknown result type (might be due to invalid IL or missing references)
//IL_004e: Expected O, but got Unknown
Logger = ((BaseUnityPlugin)this).Logger;
Logger.LogInfo((object)"Plugin SemiBoombox is loaded!");
Task.Run(delegate
{
YoutubeDL.InitializeAsync().Wait();
});
_harmony = new Harmony("SemiBoombox");
_harmony.PatchAll();
}
}
public static class MyPluginInfo
{
public const string PLUGIN_GUID = "SemiBoombox";
public const string PLUGIN_NAME = "Semi Boombox";
public const string PLUGIN_VERSION = "1.0.0";
}
}
namespace SemiBoombox.Utils
{
public static class YoutubeDL
{
private static readonly string baseFolder = Path.Combine(Directory.GetCurrentDirectory(), "SemiBoombox");
private const string YtDLP_URL = "https://github.com/yt-dlp/yt-dlp/releases/download/2025.02.19/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 readonly string ffmpegBinPath = Path.Combine(ffmpegFolder, "ffmpeg-master-latest-win64-gpl", "bin", "ffmpeg.exe");
public static async Task InitializeAsync()
{
if (!Directory.Exists(baseFolder))
{
Directory.CreateDirectory(baseFolder);
}
if (!File.Exists(ytDlpPath))
{
Console.WriteLine("yt-dlp not found. Downloading...");
await DownloadFileAsync("https://github.com/yt-dlp/yt-dlp/releases/download/2025.02.19/yt-dlp.exe", ytDlpPath);
}
if (!Directory.Exists(ffmpegFolder))
{
Console.WriteLine("ffmpeg not found. Downloading and extracting...");
await DownloadAndExtractFFmpegAsync();
}
if (!File.Exists(ffmpegBinPath))
{
throw new Exception("ffmpeg executable was not found after extraction.");
}
Console.WriteLine("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");
await DownloadFileAsync("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip", zipPath);
ZipFile.ExtractToDirectory(zipPath, ffmpegFolder);
File.Delete(zipPath);
}
public static async Task<string> DownloadAudioAsync(string videoUrl)
{
await InitializeAsync();
string tempFolder = Path.Combine(baseFolder, Guid.NewGuid().ToString());
Directory.CreateDirectory(tempFolder);
Console.WriteLine("Downloading audio...");
return await Task.Run(delegate
{
try
{
string arguments = "-x --audio-format mp3 --ffmpeg-location \"" + ffmpegBinPath + "\" --output \"" + Path.Combine(tempFolder, "%(title)s.%(ext)s") + "\" " + videoUrl;
using (Process process = Process.Start(new ProcessStartInfo
{
FileName = ytDlpPath,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}))
{
if (process == null)
{
throw new Exception("Failed to start yt-dlp process.");
}
process.WaitForExit();
if (process.ExitCode != 0)
{
string text = process.StandardError.ReadToEnd();
throw new Exception("yt-dlp error: " + text);
}
}
return Directory.GetFiles(tempFolder, "*.mp3").FirstOrDefault() ?? throw new Exception("Audio download failed.");
}
catch (Exception ex)
{
Directory.Delete(tempFolder, recursive: true);
throw new Exception("Error downloading audio: " + ex.Message);
}
});
}
}
}
namespace SemiBoombox.Patches
{
[HarmonyPatch(typeof(PlayerAvatar), "Awake")]
public class PlayerAvatarPatch
{
private static void Postfix(PlayerAvatar __instance)
{
if ((Object)(object)((Component)__instance).GetComponent<Boombox>() == (Object)null)
{
((Component)__instance).gameObject.AddComponent<Boombox>();
}
}
}
}