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 Boombox Controller Remastered v2.0.0
BoomboxController.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using BoomboxController.Audio; using BoomboxController.Boombox; using BoomboxController.Commands; using BoomboxController.Core; using BoomboxController.Multiplayer; using BoomboxController.Options; using BoomboxController.Save; using BoomboxController.Startups; using BoomboxController.Vision; using GameNetcodeStuff; using HarmonyLib; using Newtonsoft.Json; using TMPro; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.Utilities; using UnityEngine.Networking; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("BoomboxController")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("BoomboxController")] [assembly: AssemblyFileVersion("2.0.0.0")] [assembly: AssemblyInformationalVersion("2.0.0")] [assembly: AssemblyProduct("BoomboxController")] [assembly: AssemblyTitle("BoomboxController")] [assembly: AssemblyVersion("2.0.0.0")] namespace BoomboxController { public class BoomboxController : Variables { public static SaveManager saveManager; public void InitializationBoombox() { saveManager = new SaveManager(); Patch(typeof(BoomboxManager)); Patch(typeof(MultiplayerManager)); Patch(typeof(CommandManager)); Patch(typeof(VisionManager)); Patch(typeof(OptionManager)); Patch(typeof(StartupManager)); } private static void Patch(Type type) { try { Plugin.HarmonyLib.PatchAll(type); ModLog.Info("Patches applied: " + type.Name); } catch (Exception exception) { ModLog.Error("Patches from " + type.Name + " could not be applied; that feature is disabled", exception); } } public static void Notify(string message, string sender = "Boombox") { if (!string.IsNullOrEmpty(message)) { MainThreadDispatcher.Run(delegate { NotifyNow(message, sender); }); } } public static void DrawString(HUDManager __instance, string chatMessage, string nameOfUserWhoTyped, string name) { Notify(chatMessage, nameOfUserWhoTyped); } private static void NotifyNow(string message, string sender) { HUDManager instance = HUDManager.Instance; ModLog.Info("[chat] " + message.Replace('\n', ' ')); if ((Object)(object)instance == (Object)null || (Object)(object)instance.chatText == (Object)null || instance.ChatMessageHistory == null) { return; } try { instance.PingHUDElement(instance.Chat, 4f, 1f, 0.2f); if (instance.ChatMessageHistory.Count >= 4) { instance.ChatMessageHistory.RemoveAt(0); } instance.ChatMessageHistory.Add("<color=#FF0000>" + sender + "</color>: <color=#FFFF00>" + message + "</color>"); TextMeshProUGUI chatText = instance.chatText; ((TMP_Text)chatText).text = string.Empty; foreach (string item in instance.ChatMessageHistory) { ((TMP_Text)chatText).text = ((TMP_Text)chatText).text + "\n" + item; } } catch (Exception ex) { ModLog.Debug("Chat notification failed: " + ex.Message); } } } public class GrabbleBoombox { } public static class VisualBoombox { private const string QuadName = "QuadBoombox"; private static Texture2D _texture; private static bool _textureRejected; public static void Apply(BoomboxItem boombox) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) MeshRenderer component = ((Component)boombox).GetComponent<MeshRenderer>(); if ((Object)(object)component != (Object)null) { Material[] materials = ((Renderer)component).materials; Color color = default(Color); if (materials.Length > 3 && ColorUtility.TryParseHtmlString(Plugin.config.body.Value, ref color)) { materials[3].color = color; } if (materials.Length > 1 && ColorUtility.TryParseHtmlString(Plugin.config.otherelem.Value, ref color)) { materials[1].color = color; } } string path = Path.Combine(ModPaths.LegacyRootDir, "back.jpg"); if (!_textureRejected && File.Exists(path) && !((Object)(object)((Component)boombox).transform.Find("QuadBoombox") != (Object)null)) { MainThreadDispatcher.StartRoutine(ApplyBackground(path, boombox)); } } private static IEnumerator ApplyBackground(string path, BoomboxItem boombox) { if ((Object)(object)_texture == (Object)null) { UnityWebRequest request = UnityWebRequestTexture.GetTexture(AudioManager.ToFileUri(path)); try { yield return request.SendWebRequest(); if ((int)request.result != 1) { ModLog.Warn("back.jpg could not be loaded: " + request.error); _textureRejected = true; yield break; } Texture2D content = DownloadHandlerTexture.GetContent(request); if ((Object)(object)content == (Object)null) { yield break; } if (((Texture)content).width <= 500 || ((Texture)content).width <= ((Texture)content).height) { ModLog.Warn("back.jpg must be wider than it is tall and more than 500 px wide."); Object.Destroy((Object)(object)content); _textureRejected = true; yield break; } _texture = content; } finally { ((IDisposable)request)?.Dispose(); } } if ((Object)(object)boombox == (Object)null || (Object)(object)((Component)boombox).transform.Find("QuadBoombox") != (Object)null) { yield break; } try { GameObject obj = GameObject.CreatePrimitive((PrimitiveType)3); ((Object)obj).name = "QuadBoombox"; obj.transform.localScale = new Vector3(0.8f, 0.38f, 0.001f); obj.transform.localRotation = Quaternion.Euler(0f, -90f, 0f); Vector3 position = ((Component)boombox).transform.position; obj.transform.position = new Vector3(position.x - 0.179f, position.y, position.z); obj.transform.parent = ((Component)boombox).transform; BoxCollider component = obj.GetComponent<BoxCollider>(); if ((Object)(object)component != (Object)null) { ((Collider)component).enabled = false; } Shader val = Shader.Find("HDRP/Lit"); MeshRenderer component2 = obj.GetComponent<MeshRenderer>(); if ((Object)(object)val != (Object)null && (Object)(object)component2 != (Object)null) { ((Renderer)component2).material = new Material(val); ((Renderer)component2).material.mainTexture = (Texture)(object)_texture; } } catch (Exception ex) { ModLog.Warn("Could not create the boombox picture: " + ex.Message); } } } public static class CacheManager { private static readonly object PinLock = new object(); private static readonly object CleanupLock = new object(); private static readonly Dictionary<string, int> Pins = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private static readonly string[] FinalExtensions = new string[2] { ".ogg", ".wav" }; private static readonly TimeSpan TempFileMaxAge = TimeSpan.FromMinutes(30.0); private static int _cleanupScheduled; public static string OutputExtension { get { if (Plugin.config == null || !Plugin.config.IsWavOutput) { return ".ogg"; } return ".wav"; } } public static string GetFinalPath(string trackId) { return Path.Combine(ModPaths.CacheDir, trackId + OutputExtension); } public static bool TryGetCached(string trackId, out string path) { path = null; if (string.IsNullOrEmpty(trackId)) { return false; } try { string[] finalExtensions = FinalExtensions; foreach (string text in finalExtensions) { string text2 = Path.Combine(ModPaths.CacheDir, trackId + text); FileInfo fileInfo = new FileInfo(text2); if (fileInfo.Exists && fileInfo.Length != 0L) { Touch(text2); path = text2; return true; } } } catch (Exception ex) { ModLog.Warn("Cache lookup failed for " + trackId + ": " + ex.Message); } return false; } public static void Pin(string path) { if (string.IsNullOrEmpty(path)) { return; } lock (PinLock) { Pins.TryGetValue(path, out var value); Pins[path] = value + 1; } } public static void Unpin(string path) { if (string.IsNullOrEmpty(path)) { return; } lock (PinLock) { if (Pins.TryGetValue(path, out var value)) { if (value <= 1) { Pins.Remove(path); } else { Pins[path] = value - 1; } } } } private static bool IsPinned(string path) { lock (PinLock) { return Pins.ContainsKey(path); } } public static void ScheduleCleanup() { if (Interlocked.Exchange(ref _cleanupScheduled, 1) == 1) { return; } Task.Run(async delegate { try { await Task.Delay(3000).ConfigureAwait(continueOnCapturedContext: false); Cleanup(); } catch (Exception ex) { ModLog.Warn("Cache cleanup failed: " + ex.Message); } finally { Interlocked.Exchange(ref _cleanupScheduled, 0); } }); } public static void Cleanup() { lock (CleanupLock) { DirectoryInfo directoryInfo = new DirectoryInfo(ModPaths.CacheDir); if (!directoryInfo.Exists) { return; } DateTime utcNow = DateTime.UtcNow; long num = (long)Math.Max(50, (Plugin.config != null) ? Plugin.config.maxCacheSizeMB.Value : 1024) * 1024L * 1024; int num2 = ((Plugin.config != null) ? Plugin.config.maxCacheAgeDays.Value : 14); List<FileInfo> list = new List<FileInfo>(); FileInfo[] files = directoryInfo.GetFiles(); foreach (FileInfo fileInfo in files) { if (IsTempFile(fileInfo.Name)) { if (utcNow - fileInfo.LastWriteTimeUtc > TempFileMaxAge && !IsPinned(fileInfo.FullName)) { TryDelete(fileInfo); } } else if (FinalExtensions.Contains(fileInfo.Extension.ToLowerInvariant())) { list.Add(fileInfo); } } int num3 = 0; if (num2 > 0) { TimeSpan timeSpan = TimeSpan.FromDays(num2); foreach (FileInfo item in list.ToList()) { if (!(utcNow - item.LastWriteTimeUtc <= timeSpan) && !IsPinned(item.FullName) && TryDelete(item)) { list.Remove(item); num3++; } } } long num4 = list.Sum((FileInfo f) => f.Length); if (num4 > num) { foreach (FileInfo item2 in list.OrderBy((FileInfo f) => f.LastWriteTimeUtc)) { if (num4 <= num) { break; } if (!IsPinned(item2.FullName)) { long length = item2.Length; if (TryDelete(item2)) { num4 -= length; num3++; } } } } if (num3 > 0) { ModLog.Info("Cache cleanup removed " + num3 + " file(s); cache size now " + num4 / 1048576 + " MB."); } } } public static long ClearAll() { long num = 0L; lock (CleanupLock) { DirectoryInfo directoryInfo = new DirectoryInfo(ModPaths.CacheDir); if (!directoryInfo.Exists) { return 0L; } FileInfo[] files = directoryInfo.GetFiles(); foreach (FileInfo fileInfo in files) { if (!IsPinned(fileInfo.FullName)) { long length = fileInfo.Length; if (TryDelete(fileInfo)) { num += length; } } } return num; } } public static long GetCacheSizeBytes() { try { DirectoryInfo directoryInfo = new DirectoryInfo(ModPaths.CacheDir); return directoryInfo.Exists ? directoryInfo.GetFiles().Sum((FileInfo f) => f.Length) : 0; } catch (Exception) { return 0L; } } public static void DeleteTempFiles(string trackId) { try { DirectoryInfo directoryInfo = new DirectoryInfo(ModPaths.CacheDir); if (!directoryInfo.Exists) { return; } FileInfo[] files = directoryInfo.GetFiles(trackId + ".*"); foreach (FileInfo fileInfo in files) { if (IsTempFile(fileInfo.Name)) { TryDelete(fileInfo); } } } catch (Exception ex) { ModLog.Debug("Could not clear temp files for " + trackId + ": " + ex.Message); } } private static bool IsTempFile(string name) { if (name.IndexOf(".src.", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf(".tmp", StringComparison.OrdinalIgnoreCase) < 0 && !name.EndsWith(".part", StringComparison.OrdinalIgnoreCase)) { return name.EndsWith(".ytdl", StringComparison.OrdinalIgnoreCase); } return true; } private static void Touch(string path) { try { File.SetLastWriteTimeUtc(path, DateTime.UtcNow); } catch (Exception) { } } private static bool TryDelete(FileInfo file) { try { file.Delete(); return true; } catch (Exception ex) { ModLog.Debug("Could not delete cache file " + file.Name + ": " + ex.Message); return false; } } } public class Configs { public class Lang { public ConfigEntry<string> main_1; public ConfigEntry<string> main_2; public ConfigEntry<string> main_3; public ConfigEntry<string> main_4; public ConfigEntry<string> main_5; public ConfigEntry<string> main_6; public ConfigEntry<string> main_7; public ConfigEntry<string> main_8; public ConfigEntry<string> main_9; public ConfigEntry<string> main_10; public ConfigEntry<string> main_11; public ConfigEntry<string> main_12; public ConfigEntry<string> main_13; public ConfigEntry<string> main_14; public ConfigEntry<string> resolving; public ConfigEntry<string> queueLoaded; public ConfigEntry<string> downloadFailed; public ConfigEntry<string> noBoombox; public ConfigEntry<string> onlyHost; public ConfigEntry<string> domainBlocked; public ConfigEntry<string> localWarning; public ConfigEntry<string> localList; public ConfigEntry<string> localEmpty; public ConfigEntry<string> localNotAllowed; public ConfigEntry<string> stopped; public ConfigEntry<string> hostNoMod; public ConfigEntry<string> cacheCleared; public ConfigEntry<string> cacheInfo; public ConfigEntry<string> queueList; public ConfigEntry<string> queueEmpty; public ConfigEntry<string> rejected; public ConfigEntry<string> hoverInfo; public ConfigEntry<string> busy; public ConfigEntry<string> invalidTime; public ConfigEntry<string> toolsFailed; public ConfigEntry<string> loadingStatus; public ConfigEntry<string> failedStatus; public ConfigEntry<string> syncFixConflict; public void GetConfigRU() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown ConfigFile val = new ConfigFile(Path.Combine(ModPaths.LegacyRootDir, "lang", "boombox_ru.cfg"), true); main_1 = val.Bind<string>("General", "Main_1", "Пожалуйста, подождите, загружаются дополнительные библиотеки (yt-dlp, ffmpeg, deno).", (ConfigDescription)null); main_2 = val.Bind<string>("General", "Main_2", "Взять BoomBox[2.0.0] : [E]\n@2 - @3\n@1 громкость\nСейчас играет: @4\nДоступных треков: @5", (ConfigDescription)null); main_3 = val.Bind<string>("General", "Main_3", "Все дополнительные библиотеки загружены, теперь вы можете использовать команды для бумбокса.", (ConfigDescription)null); main_4 = val.Bind<string>("General", "Main_4", "Подождите, трек еще загружается!", (ConfigDescription)null); main_5 = val.Bind<string>("General", "Main_5", "Команды:\n/bplay <ссылка> - видео или плейлист\n/blocal [номер|all] - локальные треки\n/btime 1:30 - перемотка\n/btrack <n>, /bskip - сменить трек\n/bqueue - очередь\n/bstop - остановить\n/bvolume 0-100 - громкость\n/bcache [clear] - кэш", (ConfigDescription)null); main_6 = val.Bind<string>("General", "Main_6", "Введите правильный URL-адрес!", (ConfigDescription)null); main_7 = val.Bind<string>("General", "Main_7", "Пожалуйста подождите...", (ConfigDescription)null); main_8 = val.Bind<string>("General", "Main_8", "Трек был загружен в бумбокс", (ConfigDescription)null); main_9 = val.Bind<string>("General", "Main_9", "@1 изменил громкость трека @2", (ConfigDescription)null); main_10 = val.Bind<string>("General", "Main_10", "Введите правильную громкость трека (0 - 100)!", (ConfigDescription)null); main_11 = val.Bind<string>("General", "Main_11", "Ссылка недействительная!", (ConfigDescription)null); main_12 = val.Bind<string>("General", "Main_12", "Позиция трека изменена на @1!", (ConfigDescription)null); main_13 = val.Bind<string>("General", "Main_13", "Загрузка трека отменена!", (ConfigDescription)null); main_14 = val.Bind<string>("General", "Main_14", "Текущий трек был переключен на: @1!", (ConfigDescription)null); resolving = val.Bind<string>("V2", "Resolving", "Обработка ссылки...", (ConfigDescription)null); queueLoaded = val.Bind<string>("V2", "QueueLoaded", "Загружено треков: @1. Первый: @2", (ConfigDescription)null); downloadFailed = val.Bind<string>("V2", "DownloadFailed", "Не удалось загрузить «@1» у вас: @2", (ConfigDescription)null); noBoombox = val.Bind<string>("V2", "NoBoombox", "Бумбокс не найден. Возьмите его в руки или посмотрите на него.", (ConfigDescription)null); onlyHost = val.Bind<string>("V2", "OnlyHost", "Только хост может управлять бумбоксом.", (ConfigDescription)null); domainBlocked = val.Bind<string>("V2", "DomainBlocked", "Сайт не разрешён (@1). Разрешены: @2", (ConfigDescription)null); localWarning = val.Bind<string>("V2", "LocalWarning", "Локальные файлы не отправляются другим игрокам. Они услышат трек, только если у них есть такой же файл в папке local.", (ConfigDescription)null); localList = val.Bind<string>("V2", "LocalList", "Локальные треки (@1):\n@2", (ConfigDescription)null); localEmpty = val.Bind<string>("V2", "LocalEmpty", "Нет локальных треков в: @1", (ConfigDescription)null); localNotAllowed = val.Bind<string>("V2", "LocalNotAllowed", "Хост запретил локальные треки.", (ConfigDescription)null); stopped = val.Bind<string>("V2", "Stopped", "Пользовательская музыка остановлена.", (ConfigDescription)null); hostNoMod = val.Bind<string>("V2", "HostNoMod", "У хоста нет Boombox Controller. Треки будут слышны только вам.", (ConfigDescription)null); cacheCleared = val.Bind<string>("V2", "CacheCleared", "Кэш очищен, освобождено @1 МБ.", (ConfigDescription)null); cacheInfo = val.Bind<string>("V2", "CacheInfo", "Кэш: @1 МБ (лимит @2 МБ). /bcache clear - очистить.", (ConfigDescription)null); queueList = val.Bind<string>("V2", "QueueList", "Очередь (@1):\n@2", (ConfigDescription)null); queueEmpty = val.Bind<string>("V2", "QueueEmpty", "Очередь пуста.", (ConfigDescription)null); rejected = val.Bind<string>("V2", "Rejected", "Хост отклонил запрос: @1", (ConfigDescription)null); hoverInfo = val.Bind<string>("V2", "HoverInfo", "@1 / @2 • громкость @3\n@4\n[@5] громче [@6] тише", (ConfigDescription)null); busy = val.Bind<string>("V2", "Busy", "Подождите, предыдущая ссылка ещё обрабатывается.", (ConfigDescription)null); invalidTime = val.Bind<string>("V2", "InvalidTime", "Неверное время. Пример: /btime 1:30 или /btime 1:02:03", (ConfigDescription)null); toolsFailed = val.Bind<string>("V2", "ToolsFailed", "Не удалось подготовить yt-dlp/ffmpeg. Смотрите лог BepInEx.", (ConfigDescription)null); loadingStatus = val.Bind<string>("V2", "LoadingStatus", "загрузка...", (ConfigDescription)null); failedStatus = val.Bind<string>("V2", "FailedStatus", "ошибка загрузки", (ConfigDescription)null); syncFixConflict = val.Bind<string>("V2", "SyncFixConflict", "Удалите мод BoomboxSyncFix, чтобы использовать команды.", (ConfigDescription)null); } public void GetConfigEN() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown ConfigFile val = new ConfigFile(Path.Combine(ModPaths.LegacyRootDir, "lang", "boombox_en.cfg"), true); main_1 = val.Bind<string>("General", "Main_1", "Please wait, additional libraries (yt-dlp, ffmpeg, deno) are being downloaded.", (ConfigDescription)null); main_2 = val.Bind<string>("General", "Main_2", "Pickup BoomBox[2.0.0] : [E]\n@2 - @3\n@1 volume\nNow playing: @4\nAvailable tracks: @5", (ConfigDescription)null); main_3 = val.Bind<string>("General", "Main_3", "All libraries have loaded, now you can use the boombox commands.", (ConfigDescription)null); main_4 = val.Bind<string>("General", "Main_4", "Another track is being uploaded to the boombox!", (ConfigDescription)null); main_5 = val.Bind<string>("General", "Main_5", "Commands:\n/bplay <link> - video or playlist\n/blocal [number|all] - local tracks\n/btime 1:30 - seek\n/btrack <n>, /bskip - change track\n/bqueue - show queue\n/bstop - stop\n/bvolume 0-100 - volume\n/bcache [clear] - cache", (ConfigDescription)null); main_6 = val.Bind<string>("General", "Main_6", "Enter the correct URL!", (ConfigDescription)null); main_7 = val.Bind<string>("General", "Main_7", "Please wait...", (ConfigDescription)null); main_8 = val.Bind<string>("General", "Main_8", "The track was uploaded to the boombox", (ConfigDescription)null); main_9 = val.Bind<string>("General", "Main_9", "@1 changed the volume @2 of the boombox.", (ConfigDescription)null); main_10 = val.Bind<string>("General", "Main_10", "Enter a correct volume (0 - 100)!", (ConfigDescription)null); main_11 = val.Bind<string>("General", "Main_11", "Link is invalid!", (ConfigDescription)null); main_12 = val.Bind<string>("General", "Main_12", "Track position changed to @1!", (ConfigDescription)null); main_13 = val.Bind<string>("General", "Main_13", "Track download canceled!", (ConfigDescription)null); main_14 = val.Bind<string>("General", "Main_14", "The current track has been switched to: @1!", (ConfigDescription)null); resolving = val.Bind<string>("V2", "Resolving", "Resolving link...", (ConfigDescription)null); queueLoaded = val.Bind<string>("V2", "QueueLoaded", "Loaded @1 track(s). First: @2", (ConfigDescription)null); downloadFailed = val.Bind<string>("V2", "DownloadFailed", "Could not load \"@1\" on your game: @2", (ConfigDescription)null); noBoombox = val.Bind<string>("V2", "NoBoombox", "No boombox found. Hold one or look at one.", (ConfigDescription)null); onlyHost = val.Bind<string>("V2", "OnlyHost", "Only the host can control the boombox.", (ConfigDescription)null); domainBlocked = val.Bind<string>("V2", "DomainBlocked", "Site not allowed (@1). Allowed: @2", (ConfigDescription)null); localWarning = val.Bind<string>("V2", "LocalWarning", "Local files are never uploaded. Other players will only hear this track if they have the same file in their local folder.", (ConfigDescription)null); localList = val.Bind<string>("V2", "LocalList", "Local tracks (@1):\n@2", (ConfigDescription)null); localEmpty = val.Bind<string>("V2", "LocalEmpty", "No local tracks in: @1", (ConfigDescription)null); localNotAllowed = val.Bind<string>("V2", "LocalNotAllowed", "The host disabled local tracks.", (ConfigDescription)null); stopped = val.Bind<string>("V2", "Stopped", "Custom music stopped.", (ConfigDescription)null); hostNoMod = val.Bind<string>("V2", "HostNoMod", "The host does not have Boombox Controller. Tracks will only play for you.", (ConfigDescription)null); cacheCleared = val.Bind<string>("V2", "CacheCleared", "Cache cleared, @1 MB freed.", (ConfigDescription)null); cacheInfo = val.Bind<string>("V2", "CacheInfo", "Cache: @1 MB (limit @2 MB). /bcache clear to empty it.", (ConfigDescription)null); queueList = val.Bind<string>("V2", "QueueList", "Queue (@1):\n@2", (ConfigDescription)null); queueEmpty = val.Bind<string>("V2", "QueueEmpty", "The queue is empty.", (ConfigDescription)null); rejected = val.Bind<string>("V2", "Rejected", "Request rejected by the host: @1", (ConfigDescription)null); hoverInfo = val.Bind<string>("V2", "HoverInfo", "@1 / @2 • volume @3\n@4\n[@5] Vol+ [@6] Vol-", (ConfigDescription)null); busy = val.Bind<string>("V2", "Busy", "Please wait, the previous link is still being resolved.", (ConfigDescription)null); invalidTime = val.Bind<string>("V2", "InvalidTime", "Invalid time. Example: /btime 1:30 or /btime 1:02:03", (ConfigDescription)null); toolsFailed = val.Bind<string>("V2", "ToolsFailed", "yt-dlp/ffmpeg could not be prepared. Check the BepInEx log.", (ConfigDescription)null); loadingStatus = val.Bind<string>("V2", "LoadingStatus", "loading...", (ConfigDescription)null); failedStatus = val.Bind<string>("V2", "FailedStatus", "failed to load", (ConfigDescription)null); syncFixConflict = val.Bind<string>("V2", "SyncFixConflict", "Uninstall BoomboxSyncFix to use the boombox commands.", (ConfigDescription)null); } } public ConfigEntry<bool> requstbattery; public ConfigEntry<bool> pocketitem; public ConfigEntry<string> languages; public ConfigEntry<bool> visual; public ConfigEntry<string> body; public ConfigEntry<string> otherelem; public ConfigEntry<string> allowedDomains; public ConfigEntry<bool> onlyHostCanControl; public ConfigEntry<float> commandRadius; public ConfigEntry<float> syncInterval; public ConfigEntry<float> driftTolerance; public ConfigEntry<float> prepareTimeout; public ConfigEntry<bool> waitForAllPlayers; public ConfigEntry<bool> allowLocalTracks; public ConfigEntry<bool> autoPlayOnLoad; public ConfigEntry<bool> loopQueue; public ConfigEntry<bool> fallbackToVanilla; public ConfigEntry<int> maxPlaylistItems; public ConfigEntry<int> maxTrackMinutes; public ConfigEntry<bool> keepCompressedInMemory; public ConfigEntry<bool> autoDownloadTools; public ConfigEntry<bool> autoUpdateYtDlp; public ConfigEntry<bool> downloadDeno; public ConfigEntry<string> cookiesFromBrowser; public ConfigEntry<string> extraYtDlpArgs; public ConfigEntry<int> downloadTimeout; public ConfigEntry<int> maxConcurrentDownloads; public ConfigEntry<string> transcodeFormat; public ConfigEntry<int> vorbisQuality; public ConfigEntry<int> maxCacheSizeMB; public ConfigEntry<int> maxCacheAgeDays; public static Lang lang = new Lang(); public bool IsWavOutput { get { if (transcodeFormat != null) { return string.Equals(transcodeFormat.Value, "wav", StringComparison.OrdinalIgnoreCase); } return false; } } public Configs() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Expected O, but got Unknown //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Expected O, but got Unknown //IL_01a8: Unknown result type (might be due to invalid IL or missing references) //IL_01b2: Expected O, but got Unknown //IL_01e1: Unknown result type (might be due to invalid IL or missing references) //IL_01eb: Expected O, but got Unknown //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_02a6: Expected O, but got Unknown //IL_02ce: Unknown result type (might be due to invalid IL or missing references) //IL_02d8: Expected O, but got Unknown //IL_03b4: Unknown result type (might be due to invalid IL or missing references) //IL_03be: Expected O, but got Unknown //IL_03e1: Unknown result type (might be due to invalid IL or missing references) //IL_03eb: Expected O, but got Unknown //IL_0426: Unknown result type (might be due to invalid IL or missing references) //IL_0430: Expected O, but got Unknown //IL_0454: Unknown result type (might be due to invalid IL or missing references) //IL_045e: Expected O, but got Unknown //IL_048a: Unknown result type (might be due to invalid IL or missing references) //IL_0494: Expected O, but got Unknown //IL_04bc: Unknown result type (might be due to invalid IL or missing references) //IL_04c6: Expected O, but got Unknown ConfigFile val = new ConfigFile(Path.Combine(ModPaths.LegacyRootDir, "boombox_controller.cfg"), true); requstbattery = val.Bind<bool>("General.Toggles", "RequestBattery", false, "Enable/disable boombox battery (true = Enable; false = Disable)"); pocketitem = val.Bind<bool>("General.Toggles", "PocketItem", true, "Keep the music playing while the boombox is in your pocket."); languages = val.Bind<string>("General", "Languages", "en", "EN/RU"); visual = val.Bind<bool>("Visual", "Visual", false, "Enable/Disable Visual Elements of Boombox"); body = val.Bind<string>("Visual", "Body", "#FFFFFF", "Color body Boombox"); otherelem = val.Bind<string>("Visual", "Other", "#000000", "Color Other Elements Boombox"); allowedDomains = val.Bind<string>("Network", "AllowedDomains", "youtube.com,youtu.be,soundcloud.com", "Comma separated list of domains (sub-domains included) your game is allowed to download from. Checked on YOUR machine for every track, even if another player requested it. Use * to allow every site (not recommended: any player could make your game connect to any server)."); onlyHostCanControl = val.Bind<bool>("Network", "OnlyHostCanControl", false, "Host only: when true, only the host can change tracks, seek or stop."); commandRadius = val.Bind<float>("Network", "CommandRadius", 15f, new ConfigDescription("Max distance (meters) between a player and the boombox they control. 0 = unlimited.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 500f), Array.Empty<object>())); syncInterval = val.Bind<float>("Network", "SyncIntervalSeconds", 5f, new ConfigDescription("Host only: how often the host re-broadcasts the playback timeline.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), Array.Empty<object>())); driftTolerance = val.Bind<float>("Network", "DriftToleranceSeconds", 0.3f, new ConfigDescription("If your playback drifts more than this from the host timeline it is corrected.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 5f), Array.Empty<object>())); prepareTimeout = val.Bind<float>("Network", "PrepareTimeoutSeconds", 45f, new ConfigDescription("Host only: max time to wait for players to finish downloading before starting anyway.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 600f), Array.Empty<object>())); waitForAllPlayers = val.Bind<bool>("Network", "WaitForAllPlayers", true, "Host only: wait until every modded player downloaded the track (or the timeout expired) so everybody hears it from the start."); allowLocalTracks = val.Bind<bool>("Network", "AllowLocalTracks", true, "Host only: allow players to play tracks from their local music folder. Files are never uploaded; others hear them only if they own the same file."); autoPlayOnLoad = val.Bind<bool>("Playback", "AutoPlayOnLoad", true, "Start playing automatically when a new link finished loading."); loopQueue = val.Bind<bool>("Playback", "LoopQueue", true, "Loop a single track / restart the playlist when it ends."); fallbackToVanilla = val.Bind<bool>("Playback", "FallbackToVanillaMusic", true, "If a track fails to load on your game, play a vanilla boombox song instead of silence."); maxPlaylistItems = val.Bind<int>("Playback", "MaxPlaylistItems", 25, new ConfigDescription("Maximum number of tracks taken from a playlist link.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100), Array.Empty<object>())); maxTrackMinutes = val.Bind<int>("Playback", "MaxTrackMinutes", 60, new ConfigDescription("Tracks longer than this are rejected (memory protection).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 600), Array.Empty<object>())); keepCompressedInMemory = val.Bind<bool>("Playback", "KeepCompressedInMemory", true, "Keep clips Vorbis-compressed in RAM (a 5 min track uses ~5 MB instead of ~100 MB)."); autoDownloadTools = val.Bind<bool>("Downloader", "AutoDownloadTools", true, "Download yt-dlp and ffmpeg automatically into the plugin 'bin' folder when missing."); autoUpdateYtDlp = val.Bind<bool>("Downloader", "AutoUpdateYtDlp", true, "Run 'yt-dlp -U' at most once a day. YouTube changes often; an outdated yt-dlp is the #1 cause of HTTP 403 errors."); downloadDeno = val.Bind<bool>("Downloader", "DownloadDeno", true, "Download deno.exe (JavaScript runtime required by yt-dlp >= 2025.11 for full YouTube support)."); cookiesFromBrowser = val.Bind<string>("Downloader", "CookiesFromBrowser", "", "Optional: browser to borrow YouTube cookies from when YouTube asks to 'confirm you are not a bot' (firefox, chrome, edge, brave...). Cookies never leave your PC. A cookies.txt file in the plugin folder is also used automatically."); extraYtDlpArgs = val.Bind<string>("Downloader", "ExtraYtDlpArgs", "", "Advanced: extra arguments appended to every yt-dlp call."); downloadTimeout = val.Bind<int>("Downloader", "DownloadTimeoutSeconds", 240, new ConfigDescription("Timeout for a single yt-dlp or ffmpeg process.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(30, 1800), Array.Empty<object>())); maxConcurrentDownloads = val.Bind<int>("Downloader", "MaxConcurrentDownloads", 2, new ConfigDescription("Downloads/conversions running at the same time.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 6), Array.Empty<object>())); transcodeFormat = val.Bind<string>("Downloader", "TranscodeFormat", "ogg", new ConfigDescription("Format loaded into Unity: ogg (Vorbis, small) or wav (PCM 16-bit, large but zero decoding cost).", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[2] { "ogg", "wav" }), Array.Empty<object>())); vorbisQuality = val.Bind<int>("Downloader", "VorbisQuality", 4, new ConfigDescription("libvorbis -q:a value (0 = small, 10 = best).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 10), Array.Empty<object>())); maxCacheSizeMB = val.Bind<int>("Cache", "MaxCacheSizeMB", 1024, new ConfigDescription("The oldest cached tracks are deleted when the Cache folder grows above this size.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(50, 102400), Array.Empty<object>())); maxCacheAgeDays = val.Bind<int>("Cache", "MaxCacheAgeDays", 14, new ConfigDescription("Cached tracks not played for this many days are deleted. 0 = never.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3650), Array.Empty<object>())); } public Lang GetLang() { return lang; } } public sealed class TrackDownloadException : Exception { public TrackDownloadException(string message) : base(message) { } public TrackDownloadException(string message, Exception inner) : base(message, inner) { } } public sealed class ProcessResult { public int ExitCode; public string StdOut; public string StdErr; public bool TimedOut; } public static class ProcessRunner { private static readonly object RunningLock = new object(); private static readonly HashSet<Process> Running = new HashSet<Process>(); public static ProcessResult Run(string fileName, string arguments, string workingDirectory, TimeSpan timeout, CancellationToken cancellationToken, IDictionary<string, string> environment) { if (MainThreadDispatcher.IsMainThread) { throw new InvalidOperationException("External processes must not run on Unity's main thread."); } if (!File.Exists(fileName)) { throw new FileNotFoundException("Executable not found", fileName); } ProcessStartInfo processStartInfo = new ProcessStartInfo(fileName, arguments) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = (string.IsNullOrEmpty(workingDirectory) ? Path.GetDirectoryName(fileName) : workingDirectory), StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; if (environment != null) { foreach (KeyValuePair<string, string> item in environment) { processStartInfo.EnvironmentVariables[item.Key] = item.Value; } } StringBuilder stdout = new StringBuilder(); StringBuilder stderr = new StringBuilder(); ProcessResult processResult = new ProcessResult(); using (Process process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true }) { process.OutputDataReceived += delegate(object s, DataReceivedEventArgs e) { if (e.Data == null) { return; } lock (stdout) { stdout.AppendLine(e.Data); } }; process.ErrorDataReceived += delegate(object s, DataReceivedEventArgs e) { if (e.Data == null) { return; } lock (stderr) { stderr.AppendLine(e.Data); } }; ModLog.Debug("Running: " + Path.GetFileName(fileName) + " " + arguments); if (!process.Start()) { throw new InvalidOperationException("Could not start " + fileName); } lock (RunningLock) { Running.Add(process); } try { process.BeginOutputReadLine(); process.BeginErrorReadLine(); Stopwatch stopwatch = Stopwatch.StartNew(); while (!process.WaitForExit(200)) { if (cancellationToken.IsCancellationRequested) { Kill(process); cancellationToken.ThrowIfCancellationRequested(); } if (stopwatch.Elapsed > timeout) { Kill(process); processResult.TimedOut = true; break; } } if (!processResult.TimedOut) { process.WaitForExit(); } else { process.WaitForExit(2000); } processResult.ExitCode = (process.HasExited ? process.ExitCode : (-1)); } finally { lock (RunningLock) { Running.Remove(process); } } } lock (stdout) { processResult.StdOut = stdout.ToString(); } lock (stderr) { processResult.StdErr = stderr.ToString(); return processResult; } } public static void KillAll() { List<Process> list; lock (RunningLock) { list = Running.ToList(); } foreach (Process item in list) { Kill(item); } if (list.Count > 0) { ModLog.Info("Stopped " + list.Count + " running external process(es)."); } } private static void Kill(Process process) { try { if (!process.HasExited) { process.Kill(); } } catch (Exception ex) { ModLog.Debug("Could not kill process: " + ex.Message); } } public static string Quote(string argument) { if (string.IsNullOrEmpty(argument)) { return "\"\""; } if (argument.IndexOfAny(new char[5] { ' ', '\t', '\n', '\v', '"' }) < 0) { return argument; } StringBuilder stringBuilder = new StringBuilder("\""); int num = 0; foreach (char c in argument) { switch (c) { case '\\': num++; continue; case '"': stringBuilder.Append('\\', num * 2 + 1); stringBuilder.Append('"'); break; default: stringBuilder.Append('\\', num); stringBuilder.Append(c); break; } num = 0; } stringBuilder.Append('\\', num * 2); stringBuilder.Append('"'); return stringBuilder.ToString(); } } public static class Downloader { private const string YtDlpUrl = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"; private const string YtDlpChecksumsUrl = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS"; private const string FfmpegZipName = "ffmpeg-master-latest-win64-gpl.zip"; private const string FfmpegZipUrl = "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"; private const string FfmpegChecksumsUrl = "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/checksums.sha256"; private const string DenoZipName = "deno-x86_64-pc-windows-msvc.zip"; private const string DenoZipUrl = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip"; private const string DenoChecksumUrl = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip.sha256sum"; private const string MinYtDlpVersionForJsRuntimes = "2025.11.12"; private static readonly object ToolsLock; private static readonly ConcurrentDictionary<string, Lazy<Task<string>>> InFlight; private static readonly Regex Sha256Regex; private static readonly object SlotsLock; private static Task<bool> _toolsTask; private static SemaphoreSlim _slots; private static string _ytDlpVersion; public static bool ToolsReady { get { if (File.Exists(ModPaths.YtDlpPath)) { return File.Exists(ModPaths.FfmpegPath); } return false; } } public static bool ToolsBusy { get { Task<bool> toolsTask = _toolsTask; if (toolsTask != null) { return !toolsTask.IsCompleted; } return false; } } private static SemaphoreSlim Slots { get { lock (SlotsLock) { if (_slots == null) { int num = ((Plugin.config != null) ? Math.Max(1, Plugin.config.maxConcurrentDownloads.Value) : 2); _slots = new SemaphoreSlim(num, num); } return _slots; } } } private static TimeSpan ProcessTimeout => TimeSpan.FromSeconds((Plugin.config != null) ? Plugin.config.downloadTimeout.Value : 240); private static string UpdateStampPath => Path.Combine(ModPaths.ToolsDir, ".yt-dlp-last-update"); public static string AllowedDomainsText { get { if (Plugin.config == null) { return string.Empty; } return Plugin.config.allowedDomains.Value; } } static Downloader() { ToolsLock = new object(); InFlight = new ConcurrentDictionary<string, Lazy<Task<string>>>(); Sha256Regex = new Regex("\\b[0-9a-fA-F]{64}\\b", RegexOptions.Compiled); SlotsLock = new object(); _ytDlpVersion = string.Empty; try { ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; } catch (Exception ex) { ModLog.Warn("Could not enable TLS 1.2: " + ex.Message); } } public static Task<bool> EnsureToolsAsync() { lock (ToolsLock) { bool flag = _toolsTask != null && _toolsTask.IsCompleted && (_toolsTask.IsFaulted || _toolsTask.IsCanceled || !_toolsTask.Result); if (_toolsTask == null || flag) { _toolsTask = Task.Run(() => EnsureToolsCoreAsync(MainThreadDispatcher.ShutdownToken)); } return _toolsTask; } } private static bool WaitForTools(CancellationToken cancellationToken) { Task<bool> task = EnsureToolsAsync(); try { task.Wait(cancellationToken); return task.Result; } catch (OperationCanceledException) { throw; } catch (Exception exception) { ModLog.Error("Tool preparation failed", exception); return false; } } private static async Task<bool> EnsureToolsCoreAsync(CancellationToken ct) { ModPaths.EnsureDirectory(ModPaths.ToolsDir); MigrateLegacyTools(); bool autoDownload = Plugin.config == null || Plugin.config.autoDownloadTools.Value; try { if (!File.Exists(ModPaths.YtDlpPath)) { if (!autoDownload) { ModLog.Error("yt-dlp.exe is missing in " + ModPaths.ToolsDir + " and AutoDownloadTools is disabled."); } else { ModLog.Info("Downloading yt-dlp..."); await DownloadVerifiedAsync("https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe", ModPaths.YtDlpPath, "https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS", "yt-dlp.exe", ct).ConfigureAwait(continueOnCapturedContext: false); WriteUpdateStamp(); } } else { UpdateYtDlpIfStale(ct); } if (!File.Exists(ModPaths.FfmpegPath)) { if (!autoDownload) { ModLog.Error("ffmpeg.exe is missing in " + ModPaths.ToolsDir + " and AutoDownloadTools is disabled."); } else { ModLog.Info("Downloading ffmpeg (~100 MB, only once)..."); string zip = Path.Combine(ModPaths.ToolsDir, "ffmpeg-master-latest-win64-gpl.zip"); await DownloadVerifiedAsync("https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip", zip, "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/checksums.sha256", "ffmpeg-master-latest-win64-gpl.zip", ct).ConfigureAwait(continueOnCapturedContext: false); Dictionary<string, string> entryToDestination = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "ffmpeg.exe", ModPaths.FfmpegPath }, { "ffprobe.exe", ModPaths.FfprobePath } }; ExtractEntries(zip, entryToDestination); TryDeleteFile(zip); } } bool flag = Plugin.config == null || Plugin.config.downloadDeno.Value; if (!File.Exists(ModPaths.DenoPath) && flag && autoDownload) { try { ModLog.Info("Downloading deno (JavaScript runtime needed by yt-dlp for YouTube)..."); string zip = Path.Combine(ModPaths.ToolsDir, "deno-x86_64-pc-windows-msvc.zip"); await DownloadVerifiedAsync("https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip", zip, "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip.sha256sum", "deno-x86_64-pc-windows-msvc.zip", ct).ConfigureAwait(continueOnCapturedContext: false); ExtractEntries(zip, new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "deno.exe", ModPaths.DenoPath } }); TryDeleteFile(zip); } catch (Exception ex) { if (ex is OperationCanceledException) { throw; } ModLog.Warn("deno could not be downloaded (YouTube may return fewer formats or HTTP 403): " + ex.Message); } } else if (!File.Exists(ModPaths.DenoPath)) { ModLog.Warn("deno.exe not found in " + ModPaths.ToolsDir + ". yt-dlp >= 2025.11 needs it for full YouTube support."); } ReadYtDlpVersion(ct); } catch (OperationCanceledException) { return false; } catch (Exception exception) { ModLog.Error("Could not prepare the external tools", exception); } bool toolsReady = ToolsReady; if (toolsReady) { ModLog.Info("External tools ready (yt-dlp " + _ytDlpVersion + ", deno " + (File.Exists(ModPaths.DenoPath) ? "present" : "missing") + ")."); } else { ModLog.Error("External tools are NOT ready. See DEPENDENCIES.md to install them manually in: " + ModPaths.ToolsDir); } return toolsReady; } private static void MigrateLegacyTools() { try { string text = Path.Combine(ModPaths.LegacyToolsDir, "ffmpeg.exe"); if (!File.Exists(ModPaths.FfmpegPath) && File.Exists(text)) { File.Copy(text, ModPaths.FfmpegPath); ModLog.Info("Migrated ffmpeg.exe from the 1.x folder."); } string text2 = Path.Combine(ModPaths.LegacyToolsDir, "yt-dlp.exe"); if (!File.Exists(ModPaths.YtDlpPath) && File.Exists(text2)) { File.Copy(text2, ModPaths.YtDlpPath); TryDeleteFile(UpdateStampPath); ModLog.Info("Migrated yt-dlp.exe from the 1.x folder (it will be updated)."); } } catch (Exception ex) { ModLog.Warn("Legacy tool migration skipped: " + ex.Message); } } private static void WriteUpdateStamp() { try { File.WriteAllText(UpdateStampPath, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture)); } catch (Exception ex) { ModLog.Debug("Could not write update stamp: " + ex.Message); } } private static void UpdateYtDlpIfStale(CancellationToken ct) { if (Plugin.config != null && !Plugin.config.autoUpdateYtDlp.Value) { return; } try { if (!File.Exists(UpdateStampPath) || !long.TryParse(File.ReadAllText(UpdateStampPath).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || !(DateTime.UtcNow - new DateTime(result, DateTimeKind.Utc) < TimeSpan.FromHours(24.0))) { ModLog.Info("Checking for yt-dlp updates..."); ProcessResult processResult = ProcessRunner.Run(ModPaths.YtDlpPath, "--ignore-config -U", ModPaths.ToolsDir, TimeSpan.FromSeconds(120.0), ct, BuildEnvironment()); string text = LastNonEmptyLine(processResult.StdOut) ?? LastNonEmptyLine(processResult.StdErr) ?? "(no output)"; if (processResult.ExitCode == 0 && !processResult.TimedOut) { WriteUpdateStamp(); ModLog.Info("yt-dlp: " + text); } else { ModLog.Warn("yt-dlp self-update failed (exit " + processResult.ExitCode + "): " + text); } } } catch (OperationCanceledException) { throw; } catch (Exception ex2) { ModLog.Warn("yt-dlp update check failed: " + ex2.Message); } } private static void ReadYtDlpVersion(CancellationToken ct) { if (!File.Exists(ModPaths.YtDlpPath)) { return; } try { _ytDlpVersion = (LastNonEmptyLine(ProcessRunner.Run(ModPaths.YtDlpPath, "--ignore-config --version", ModPaths.ToolsDir, TimeSpan.FromSeconds(30.0), ct, BuildEnvironment()).StdOut) ?? string.Empty).Trim(); } catch (OperationCanceledException) { throw; } catch (Exception ex2) { ModLog.Warn("Could not read the yt-dlp version: " + ex2.Message); } } private static async Task DownloadVerifiedAsync(string url, string destination, string checksumUrl, string checksumEntry, CancellationToken ct) { string temp = destination + ".download"; TryDeleteFile(temp); try { using (WebClient client = new WebClient()) { client.Headers[HttpRequestHeader.UserAgent] = "BoomboxController/2.0.0"; int lastReported = -10; client.DownloadProgressChanged += delegate(object s, DownloadProgressChangedEventArgs e) { if (e.ProgressPercentage >= lastReported + 10) { lastReported = e.ProgressPercentage; ModLog.Info(" " + Path.GetFileName(destination) + ": " + e.ProgressPercentage + "%"); } }; using (ct.Register(client.CancelAsync)) { await client.DownloadFileTaskAsync(new Uri(url), temp).ConfigureAwait(continueOnCapturedContext: false); } } ct.ThrowIfCancellationRequested(); bool? flag = await VerifySha256Async(temp, checksumUrl, checksumEntry, ct).ConfigureAwait(continueOnCapturedContext: false); if (flag == false) { throw new InvalidDataException("SHA-256 mismatch for " + Path.GetFileName(destination) + "; the download was discarded."); } if (!flag.HasValue) { ModLog.Warn("No published checksum could be read for " + Path.GetFileName(destination) + "; accepted over HTTPS only."); } ReplaceFile(temp, destination); } finally { TryDeleteFile(temp); } } private static async Task<bool?> VerifySha256Async(string file, string checksumUrl, string entryName, CancellationToken ct) { string text; try { using WebClient client = new WebClient(); client.Headers[HttpRequestHeader.UserAgent] = "BoomboxController/2.0.0"; using (ct.Register(client.CancelAsync)) { text = await client.DownloadStringTaskAsync(new Uri(checksumUrl)).ConfigureAwait(continueOnCapturedContext: false); } } catch (Exception ex) { if (ct.IsCancellationRequested) { throw new OperationCanceledException(ct); } ModLog.Debug("Checksum download failed: " + ex.Message); return null; } string text2 = null; string[] array = text.Split('\n'); foreach (string text3 in array) { if (text3.IndexOf(entryName, StringComparison.OrdinalIgnoreCase) >= 0) { Match match = Sha256Regex.Match(text3); if (match.Success) { text2 = match.Value; break; } } } if (text2 == null) { Match match2 = Sha256Regex.Match(text); if (match2.Success) { text2 = match2.Value; } } if (text2 == null) { return null; } string b; using (SHA256 sHA = SHA256.Create()) { using FileStream inputStream = File.OpenRead(file); b = TrackInfo.ToHex(sHA.ComputeHash(inputStream)); } return string.Equals(text2, b, StringComparison.OrdinalIgnoreCase); } private static void ExtractEntries(string zipPath, IDictionary<string, string> entryToDestination) { int num = 0; using (FileStream stream = File.OpenRead(zipPath)) { using ZipArchive zipArchive = new ZipArchive(stream, ZipArchiveMode.Read); foreach (ZipArchiveEntry entry in zipArchive.Entries) { if (!entryToDestination.TryGetValue(entry.Name, out var value)) { continue; } string text = value + ".extract"; using (Stream stream2 = entry.Open()) { using FileStream destination = File.Create(text); stream2.CopyTo(destination); } ReplaceFile(text, value); num++; } } if (num == 0) { throw new InvalidDataException("None of the expected files were found in " + Path.GetFileName(zipPath)); } } public static bool TryParseAllowedUrl(string text, out Uri uri, out string reason) { uri = null; reason = null; if (string.IsNullOrWhiteSpace(text) || text.Length > 1024) { reason = "invalid URL"; return false; } if (!Uri.TryCreate(text.Trim(), UriKind.Absolute, out Uri result) || (result.Scheme != Uri.UriSchemeHttp && result.Scheme != Uri.UriSchemeHttps)) { reason = "only http(s) links are supported"; return false; } if (!IsHostAllowed(result.Host)) { reason = "domain not allowed: " + result.Host; return false; } uri = result; return true; } public static bool IsHostAllowed(string host) { string text = ((Plugin.config != null) ? Plugin.config.allowedDomains.Value : "youtube.com,youtu.be,soundcloud.com"); if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(text)) { return false; } if (text.Trim() == "*") { return true; } host = host.ToLowerInvariant().TrimEnd('.'); string[] array = text.Split(new char[3] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim().ToLowerInvariant().TrimStart('.'); if (text2.Length != 0 && (host == text2 || host.EndsWith("." + text2, StringComparison.Ordinal))) { return true; } } return false; } private static bool IsPlaylistUrl(Uri uri) { string text = uri.Host.ToLowerInvariant(); string text2 = uri.AbsolutePath.ToLowerInvariant(); if (text.Contains("youtube.com")) { return text2.StartsWith("/playlist", StringComparison.Ordinal); } if (text.Contains("soundcloud.com")) { return text2.Contains("/sets/"); } return false; } public static Task<List<TrackInfo>> ResolveAsync(string url, CancellationToken ct) { return Task.Run(() => Resolve(url, ct), ct); } private static List<TrackInfo> Resolve(string url, CancellationToken ct) { if (!TryParseAllowedUrl(url, out var uri, out var reason)) { throw new TrackDownloadException(reason); } if (!WaitForTools(ct) || !File.Exists(ModPaths.YtDlpPath)) { throw new TrackDownloadException("yt-dlp is not available"); } bool num = IsPlaylistUrl(uri); int num2 = ((Plugin.config != null) ? Plugin.config.maxPlaylistItems.Value : 25); StringBuilder stringBuilder = new StringBuilder("--ignore-config --no-warnings --encoding utf-8 --skip-download --socket-timeout 20 "); if (num) { stringBuilder.Append("--flat-playlist --playlist-items 1:").Append(num2).Append(' '); } else { stringBuilder.Append("--no-playlist "); } stringBuilder.Append("--print ").Append(ProcessRunner.Quote("%(webpage_url,url)s\t%(duration|0)s\t%(title|Unknown)s")).Append(' '); AppendCommonArguments(stringBuilder); stringBuilder.Append("-- ").Append(ProcessRunner.Quote(uri.AbsoluteUri)); ProcessResult processResult = ProcessRunner.Run(ModPaths.YtDlpPath, stringBuilder.ToString(), ModPaths.ToolsDir, TimeSpan.FromSeconds(120.0), ct, BuildEnvironment()); if (processResult.TimedOut) { throw new TrackDownloadException("yt-dlp timed out while reading the link"); } List<TrackInfo> list = new List<TrackInfo>(); string[] array = processResult.StdOut.Split('\n'); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim('\r', ' '); if (text.Length == 0) { continue; } string[] array2 = text.Split(new char[1] { '\t' }, 3); if (array2.Length < 1 || !TryParseAllowedUrl(array2[0], out var uri2, out var _)) { continue; } double result = 0.0; if (array2.Length > 1) { double.TryParse(array2[1], NumberStyles.Float, CultureInfo.InvariantCulture, out result); } string text2 = ((array2.Length > 2) ? array2[2] : uri2.AbsoluteUri); int num3 = ((Plugin.config != null) ? Plugin.config.maxTrackMinutes.Value : 60) * 60; if (result > (double)num3) { ModLog.Info("Skipping '" + text2 + "' (" + (int)result + " s is longer than MaxTrackMinutes)."); } else { list.Add(TrackInfo.CreateRemote(uri2.AbsoluteUri, text2, result)); if (list.Count >= num2) { break; } } } if (list.Count == 0) { string error = ExtractError(processResult.StdErr) ?? ("yt-dlp returned no playable track (exit " + processResult.ExitCode + ")"); ModLog.Warn("Resolve failed for " + uri.AbsoluteUri + ":\n" + Truncate(processResult.StdErr, 2000)); throw new TrackDownloadException(AddHint(error)); } ModLog.Info("Resolved " + list.Count + " track(s) from " + uri.AbsoluteUri); return list; } public static Task<string> GetTrackFileAsync(TrackInfo track, CancellationToken ct) { if (track == null) { throw new ArgumentNullException("track"); } if (CacheManager.TryGetCached(track.Id, out var path)) { return Task.FromResult(path); } return InFlight.GetOrAdd(track.Id, (string id) => new Lazy<Task<string>>(() => StartJob(track))).Value.WithCancellation(ct); } public static void Prefetch(TrackInfo track) { if (track == null || CacheManager.TryGetCached(track.Id, out var _)) { return; } GetTrackFileAsync(track, CancellationToken.None).ContinueWith(delegate(Task<string> t) { if (t.IsFaulted && t.Exception != null) { ModLog.Debug("Prefetch failed for " + track?.ToString() + ": " + t.Exception.GetBaseException().Message); } }, TaskContinuationOptions.OnlyOnFaulted); } private static Task<string> StartJob(TrackInfo track) { Task<string> task = Task.Run(() => ProduceAsync(track, MainThreadDispatcher.ShutdownToken)); task.ContinueWith(delegate(Task<string> t) { InFlight.TryRemove(track.Id, out var _); if (t.IsFaulted && t.Exception != null) { ModLog.Debug("Job for " + track.Id + " faulted: " + t.Exception.GetBaseException().Message); } }, TaskScheduler.Default); return task; } private static async Task<string> ProduceAsync(TrackInfo track, CancellationToken ct) { await Slots.WaitAsync(ct).ConfigureAwait(continueOnCapturedContext: false); try { if (CacheManager.TryGetCached(track.Id, out var path)) { return path; } Stopwatch watch = Stopwatch.StartNew(); string result = ((track.Source != TrackSource.Local) ? ProduceRemote(track, ct) : (await ProduceLocalAsync(track, ct).ConfigureAwait(continueOnCapturedContext: false))); ModLog.Info("Track ready in " + watch.Elapsed.TotalSeconds.ToString("0.0", CultureInfo.InvariantCulture) + " s: " + track.DisplayName); CacheManager.ScheduleCleanup(); return result; } finally { Slots.Release(); } } private static string ProduceRemote(TrackInfo track, CancellationToken ct) { if (!TryParseAllowedUrl(track.Url, out var uri, out var reason)) { throw new TrackDownloadException("blocked by your AllowedDomains (" + reason + ")"); } if (!WaitForTools(ct)) { throw new TrackDownloadException("yt-dlp/ffmpeg are not available"); } ModPaths.EnsureDirectory(ModPaths.CacheDir); CacheManager.DeleteTempFiles(track.Id); int num = ((Plugin.config != null) ? Plugin.config.maxTrackMinutes.Value : 60) * 60; string argument = Path.Combine(ModPaths.CacheDir, track.Id + ".src.%(ext)s"); StringBuilder stringBuilder = new StringBuilder("--ignore-config --no-warnings --encoding utf-8 --no-playlist --no-progress --no-mtime --no-simulate "); stringBuilder.Append("-f ").Append(ProcessRunner.Quote("bestaudio/best")).Append(' '); stringBuilder.Append("--max-filesize 300M "); stringBuilder.Append("--match-filter ").Append(ProcessRunner.Quote("!is_live & duration <=? " + num)).Append(' '); stringBuilder.Append("--socket-timeout 20 --retries 3 --fragment-retries 3 "); stringBuilder.Append("--ffmpeg-location ").Append(ProcessRunner.Quote(ModPaths.ToolsDir)).Append(' '); stringBuilder.Append("-o ").Append(ProcessRunner.Quote(argument)).Append(' '); stringBuilder.Append("--print after_move:filepath "); AppendCommonArguments(stringBuilder); stringBuilder.Append("-- ").Append(ProcessRunner.Quote(uri.AbsoluteUri)); ModLog.Info("Downloading: " + track.DisplayName); ProcessResult processResult = ProcessRunner.Run(ModPaths.YtDlpPath, stringBuilder.ToString(), ModPaths.CacheDir, ProcessTimeout, ct, BuildEnvironment()); if (processResult.TimedOut) { CacheManager.DeleteTempFiles(track.Id); throw new TrackDownloadException("download timed out"); } string text = FindReportedFile(processResult.StdOut) ?? FindSourceFile(track.Id); if (text == null) { string text2 = ExtractError(processResult.StdErr); if (text2 == null && processResult.ExitCode == 0) { text2 = "skipped (live stream, or longer than " + num / 60 + " min)"; } if (text2 == null) { text2 = "yt-dlp exit code " + processResult.ExitCode; } ModLog.Warn("yt-dlp failed for " + uri.AbsoluteUri + ":\n" + Truncate(processResult.StdErr, 2000)); CacheManager.DeleteTempFiles(track.Id); throw new TrackDownloadException(AddHint(text2)); } try { return Transcode(text, track.Id, ct); } finally { TryDeleteFile(text); } } private static async Task<string> ProduceLocalAsync(TrackInfo track, CancellationToken ct) { LocalLibrary.Entry entry = await LocalLibrary.FindByHashAsync(track.LocalHash, ct).ConfigureAwait(continueOnCapturedContext: false); if (entry == null) { throw new TrackDownloadException("you don't have this file in your local folder"); } WaitForTools(ct); if (File.Exists(ModPaths.FfmpegPath)) { return Transcode(entry.FullPath, track.Id, ct); } if (AudioManager.IsDirectlyLoadable(entry.FullPath)) { ModLog.Warn("ffmpeg missing; loading '" + entry.FileName + "' without conversion."); return entry.FullPath; } throw new TrackDownloadException("ffmpeg is missing and " + Path.GetExtension(entry.FullPath) + " can't be loaded by Unity directly"); } private static string Transcode(string sourcePath, string trackId, CancellationToken ct) { if (!File.Exists(ModPaths.FfmpegPath)) { throw new TrackDownloadException("ffmpeg is not available"); } int num; object obj; if (Plugin.config != null) { num = (Plugin.config.IsWavOutput ? 1 : 0); if (num != 0) { obj = ".wav"; goto IL_003a; } } else { num = 0; } obj = ".ogg"; goto IL_003a; IL_003a: string text = (string)obj; string text2 = Path.Combine(ModPaths.CacheDir, trackId + text); string text3 = Path.Combine(ModPaths.CacheDir, trackId + ".tmp" + text); TryDeleteFile(text3); int num2 = ((Plugin.config != null) ? Plugin.config.vorbisQuality.Value : 4); string text4 = ((num != 0) ? "-c:a pcm_s16le -f wav" : ("-c:a libvorbis -q:a " + num2.ToString(CultureInfo.InvariantCulture) + " -f ogg")); string arguments = "-hide_banner -nostdin -loglevel error -y -i " + ProcessRunner.Quote(sourcePath) + " -vn -sn -dn -map 0:a:0 -map_metadata -1 -ac 2 -ar 48000 " + text4 + " " + ProcessRunner.Quote(text3); ProcessResult processResult = ProcessRunner.Run(ModPaths.FfmpegPath, arguments, ModPaths.CacheDir, ProcessTimeout, ct, null); if (processResult.TimedOut || processResult.ExitCode != 0 || !File.Exists(text3) || new FileInfo(text3).Length <= 0) { TryDeleteFile(text3); string text5 = (processResult.TimedOut ? "timed out" : (LastNonEmptyLine(processResult.StdErr) ?? ("exit code " + processResult.ExitCode))); throw new TrackDownloadException("ffmpeg conversion failed: " + text5); } ReplaceFile(text3, text2); return text2; } private static void AppendCommonArguments(StringBuilder args) { if (File.Exists(ModPaths.CookiesFilePath)) { args.Append("--cookies ").Append(ProcessRunner.Quote(ModPaths.CookiesFilePath)).Append(' '); } string text = ((Plugin.config != null) ? (Plugin.config.cookiesFromBrowser.Value ?? string.Empty).Trim() : string.Empty); if (text.Length > 0) { if (Regex.IsMatch(text, "^[A-Za-z0-9:+_\\-]{1,64}$")) { args.Append("--cookies-from-browser ").Append(text).Append(' '); } else { ModLog.Warn("CookiesFromBrowser contains invalid characters and was ignored."); } } if (File.Exists(ModPaths.DenoPath) && string.CompareOrdinal(_ytDlpVersion, "2025.11.12") >= 0) { args.Append("--js-runtimes ").Append(ProcessRunner.Quote("deno:" + ModPaths.DenoPath)).Append(' '); } string text2 = ((Plugin.config != null) ? (Plugin.config.extraYtDlpArgs.Value ?? string.Empty).Trim() : string.Empty); if (text2.Length > 0) { args.Append(text2).Append(' '); } } private static IDictionary<string, string> BuildEnvironment() { string text = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; return new Dictionary<string, string> { { "PATH", ModPaths.ToolsDir + Path.PathSeparator + text }, { "PYTHONIOENCODING", "utf-8" }, { "PYTHONUTF8", "1" } }; } private static string FindReportedFile(string stdout) { if (string.IsNullOrEmpty(stdout)) { return null; } string[] array = stdout.Split('\n'); for (int num = array.Length - 1; num >= 0; num--) { string text = array[num].Trim('\r', ' ', '"'); if (text.Length != 0) { try { if (File.Exists(text)) { return text; } } catch (ArgumentException) { } } } return null; } private static string FindSourceFile(string trackId) { try { return (from f in new DirectoryInfo(ModPaths.CacheDir).GetFiles(trackId + ".src.*") where !f.Name.EndsWith(".part", StringComparison.OrdinalIgnoreCase) && !f.Name.EndsWith(".ytdl", StringComparison.OrdinalIgnoreCase) && f.Length > 0 orderby f.Length descending select f.FullName).FirstOrDefault(); } catch (Exception) { return null; } } private static string ExtractError(string stderr) { if (string.IsNullOrEmpty(stderr)) { return null; } string text = null; string[] array = stderr.Split('\n'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)) { text = text2.Substring(6).Trim(); } } if (text != null) { return Truncate(text, 250); } return null; } private static string AddHint(string error) { string text = error.ToLowerInvariant(); if (text.Contains("403") || text.Contains("forbidden")) { return error + " (YouTube blocked the request: keep yt-dlp updated and deno.exe installed)"; } if (text.Contains("sign in") || text.Contains("bot")) { return error + " (set CookiesFromBrowser in the config or place a cookies.txt in the plugin folder)"; } if (text.Contains("javascript") || text.Contains("js runtime")) { return error + " (deno.exe is missing from the plugin bin folder)"; } return error; } private static string LastNonEmptyLine(string text) { if (string.IsNullOrEmpty(text)) { return null; } string[] array = text.Split('\n'); for (int num = array.Length - 1; num >= 0; num--) { string text2 = array[num].Trim(); if (text2.Length > 0) { return text2; } } return null; } private static string Truncate(string text, int max) { if (string.IsNullOrEmpty(text) || text.Length <= max) { return text ?? string.Empty; } return text.Substring(0, max) + "..."; } private static void ReplaceFile(string source, string destination) { if (File.Exists(destination)) { File.Delete(destination); } File.Move(source, destination); } private static void TryDeleteFile(string path) { try { if (!string.IsNullOrEmpty(path) && File.Exists(path)) { File.Delete(path); } } catch (Exception ex) { ModLog.Debug("Could not delete " + path + ": " + ex.Message); } } } public class WinApi { public enum MessageBoxResult { Error = 0, Abort = 3, Cancel = 2, Continue = 11, Ignore = 5, No = 7, OK = 1, Retry = 4, TryAgain = 10, Yes = 6 } [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll", SetLastError = true)] private static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint); [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern MessageBoxResult MessageBox(IntPtr hwnd, string text, string caption, uint type); public void SizeConsole(int width, int height) { MoveWindow(GetConsoleWindow(), 200, 200, width, height, bRepaint: true); } public static MessageBoxResult SendMessageBox(string text, string caption, uint type) { return MessageBox(GetConsoleWindow(), text, caption, type); } } [BepInPlugin("KoderTech.BoomboxController", "BoomboxController", "2.0.0")] public class Plugin : BaseUnityPlugin { public const string Guid = "KoderTech.BoomboxController"; public const string Version = "2.0.0"; public static Plugin instance; public static Harmony HarmonyLib; public static Configs config; public static BoomboxController controller; public static ManualLogSource LogSource { get; private set; } private void Awake() { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Expected O, but got Unknown instance = this; LogSource = ((BaseUnityPlugin)this).Logger; try { MainThreadDispatcher.Initialize(); ModPaths.Initialize((((BaseUnityPlugin)this).Info != null) ? ((BaseUnityPlugin)this).Info.Location : null); config = new Configs(); controller = new BoomboxController(); HarmonyLib = new Harmony("com.kodertech.BoomboxController"); Startup(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("BoomboxController failed to start: " + ex)); } } public void WriteLogo() { ((BaseUnityPlugin)this).Logger.LogInfo((object)"\n \n`7MM\"\"\"Yp, *MM .g8\"\"\"bgd mm `7MM `7MM \n MM Yb MM .dP' `M MM MM MM \n MM dP ,pW\"Wq. ,pW\"Wq.`7MMpMMMb.pMMMb. MM,dMMb. ,pW\"Wq.`7M' `MF' dM' `,pW\"Wq.`7MMpMMMbmmMMmm `7Mb,od8 ,pW\"Wq. MM MM .gP\"Ya `7Mb,od8 \n MM\"\"\"bg. 6W' `Wb 6W' `Wb MM MM MM MM `Mb 6W' `Wb `VA ,V' MM 6W' `Wb MM MM MM MM' \"'6W' `Wb MM MM ,M' Yb MM' \"' \n MM `Y 8M M8 8M M8 MM MM MM MM M8 8M M8 XMX MM. 8M M8 MM MM MM MM 8M M8 MM MM 8M\"\"\"\"\"\" MM \n MM ,9 YA. ,A9 YA. ,A9 MM MM MM MM. ,M9 YA. ,A9 ,V' VA. `Mb. ,YA. ,A9 MM MM MM MM YA. ,A9 MM MM YM. , MM \n.JMMmmmd9 `Ybmd9' `Ybmd9'.JMML JMML JMMLP^YbmdP' `Ybmd9'.AM. .MA. `\"bmmmd' `Ybmd9'.JMML JMML`Mbmo.JMML. `Ybmd9'.JMML.JMML.`Mbmmd'.JMML. \n "); } public void Startup() { try { new WinApi().SizeConsole(1500, 500); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogDebug((object)("Console resize skipped: " + ex.Message)); } WriteLogo(); TryDelete(Path.Combine(ModPaths.LegacyRootDir, "lang", "boombox_ru.cfg")); TryDelete(Path.Combine(ModPaths.LegacyRootDir, "lang", "boombox_en.cfg")); SwitchLanguage(); MainThreadDispatcher.OnQuit += OnQuit; CacheManager.ScheduleCleanup(); Downloader.EnsureToolsAsync().Forget("tools bootstrap"); LocalLibrary.ScanAsync(force: false).Forget("local library scan"); controller.InitializationBoombox(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"BoomboxController 2.0.0 loaded."); } public void SwitchLanguage() { if ((config.languages.Value ?? "en").Trim().ToLowerInvariant() == "ru") { config.GetLang().GetConfigRU(); } else { config.GetLang().GetConfigEN(); } } public void Log(object message) { ((BaseUnityPlugin)this).Logger.LogInfo(message); } private static void OnQuit() { ProcessRunner.KillAll(); AudioManager.ReleaseAll(); try { CacheManager.Cleanup(); } catch (Exception ex) { ModLog.Debug("Cache cleanup on quit failed: " + ex.Message); } } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch (Exception ex) { ModLog.Debug("Could not delete " + path + ": " + ex.Message); } } } public class Variables { public static KeyControl up; public static KeyControl down; internal static bool blockcompatibility; } public static class MyPluginInfo { public const string PLUGIN_GUID = "BoomboxController"; public const string PLUGIN_NAME = "BoomboxController"; public const string PLUGIN_VERSION = "2.0.0"; } } namespace BoomboxController.Vision { public class VisionManager : BoomboxController { private const string Marker = "\u200b\u200b"; private const int VanillaInteractableMask = 832; private static FieldInfo _maskField; private static bool _maskResolved; private static bool _errorLogged; [HarmonyPatch(typeof(PlayerControllerB), "SetHoverTipAndCurrentInteractTrigger")] [HarmonyPostfix] private static void SetHoverTip_Postfix(PlayerControllerB __instance) { try { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)__instance == (Object)null || (Object)(object)__instance != (Object)(object)instance.localPlayerController || (Object)(object)__instance.cursorTip == (Object)null || (Object)(object)__instance.gameplayCamera == (Object)null || __instance.isGrabbingObjectAnimation) { return; } string text = ((TMP_Text)__instance.cursorTip).text ?? string.Empty; int num = text.IndexOf("\u200b\u200b", StringComparison.Ordinal); if (num >= 0) { text = text.Substring(0, num); } BoomboxTrackPlayer boomboxTrackPlayer = RaycastBoombox(__instance); if ((Object)(object)boomboxTrackPlayer == (Object)null || string.IsNullOrEmpty(text)) { if (num >= 0) { ((TMP_Text)__instance.cursorTip).text = text; } } else { BoomboxTrackPlayer.MarkHovered(boomboxTrackPlayer); ((TMP_Text)__instance.cursorTip).text = text + "\u200b\u200b" + BuildInfo(boomboxTrackPlayer); } } catch (Exception ex) { if (!_errorLogged) { _errorLogged = true; ModLog.Warn("Hover tooltip disabled after an error: " + ex.Message); } } } private static BoomboxTrackPlayer RaycastBoombox(PlayerControllerB player) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) Transform transform = ((Component)player.gameplayCamera).transform; RaycastHit val = default(RaycastHit); if (!Physics.Raycast(new Ray(transform.position, transform.forward), ref val, player.grabDistance, GetInteractMask(player))) { return null; } if ((Object)(object)((RaycastHit)(ref val)).collider == (Object)null) { return null; } BoomboxItem val2 = ((Component)((RaycastHit)(ref val)).collider).GetComponent<BoomboxItem>(); if ((Object)(object)val2 == (Object)null) { val2 = ((Component)((RaycastHit)(ref val)).collider).GetComponentInParent<BoomboxItem>(); } if (!((Object)(object)val2 != (Object)null)) { return null; } return ((Component)val2).GetComponent<BoomboxTrackPlayer>(); } private static int GetInteractMask(PlayerControllerB player) { if (!_maskResolved) { _maskResolved = true; _maskField = AccessTools.Field(typeof(PlayerControllerB), "interactableObjectsMask"); } if (_maskField != null) { try { return (int)_maskField.GetValue(player); } catch (Exception) { _maskField = null; } } return 832; } private static string BuildInfo(BoomboxTrackPlayer player) { AudioSource source = player.Source; Configs.Lang lang = Configs.lang; string text; if (player.HasCustomQueue) { text = ((player.CurrentTrack != null) ? player.CurrentTrack.DisplayName : "?"); text = text + " (" + (player.State.Index + 1) + "/" + player.State.Queue.Count + ")"; if (player.Status == TrackLoadStatus.Loading) { text = text + " - " + lang.loadingStatus.Value; } else if (player.Status == TrackLoadStatus.Failed) { text = text + " - " + lang.failedStatus.Value; } } else { text = (player.Boombox.isPlayingMusic ? "Vanilla" : "-"); } double seconds = (((Object)(object)source != (Object)null && source.isPlaying) ? source.time : 0f); double seconds2 = (((Object)(object)source != (Object)null && (Object)(object)source.clip != (Object)null) ? source.clip.length : 0f); if (player.HasCustomQueue && player.State.Phase == PlaybackPhase.Paused) { seconds = player.State.PausedPosition; } string newValue = Mathf.RoundToInt((((Object)(object)source != (Object)null) ? source.volume : 0f) * 100f).ToString(CultureInfo.InvariantCulture) + "%"; string newValue2 = ((Variables.up != null) ? ((InputControl)Variables.up).displayName : "PgUp"); string newValue3 = ((Variables.down != null) ? ((InputControl)Variables.down).displayName : "PgDn"); return "\n<size=75%>" + lang.hoverInfo.Value.Replace("@1", CommandManager.FormatTime(seconds)).Replace("@2", CommandManager.FormatTime(seconds2)).Replace("@3", newValue) .Replace("@4", text) .Replace("@5", newValue2) .Replace("@6", newValue3) + "</size>"; } } } namespace BoomboxController.Startups { public class StartupManager : BoomboxController { [HarmonyPatch(typeof(GameNetworkManager), "OnEnable")] [HarmonyPostfix] public static void OnEnable_GameNetworkManager(GameNetworkManager __instance) { try { using StreamWriter streamWriter = new StreamWriter(Path.Combine(ModPaths.LegacyRootDir, "logReport.txt")); streamWriter.WriteLine("Game Version: " + (((Object)(object)__instance != (Object)null) ? __instance.gameVersionNum.ToString() : "?")); streamWriter.WriteLine("BoomboxController: 2.0.0"); streamWriter.WriteLine("Plugins: " + Chainloader.PluginInfos.Count); foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { if (pluginInfo.Key == "BoomboxSyncFix") { ModLog.Warn("BoomboxSyncFix detected: uninstall it, both mods patch BoomboxItem.StartMusic. Commands are disabled."); Variables.blockcompatibility = true; } streamWriter.WriteLine(pluginInfo.Key + " " + pluginInfo.Value.Location); } } catch (Exception ex) { ModLog.Warn("Could not write logReport.txt: " + ex.Message); } } } } namespace BoomboxController.Save { public class Cache { public float Volume { get; set; } public string UpButton { get; set; } public string DownButton { get; set; } } public class SaveManager { private static readonly object FileLock = new object(); private static string CachePath => Path.Combine(ModPaths.LegacyRootDir, "cache"); public void SaveCache(float vol, string up, string down) { try { string contents = JsonConvert.SerializeObject((object)new Cache { Volume = vol, UpButton = up, DownButton = down }); lock (FileLock) { string text = CachePath + ".tmp"; File.WriteAllText(text, contents); if (File.Exists(CachePath)) { File.Delete(CachePath); } File.Move(text, CachePath); } } catch (Exception ex) { ModLog.Warn("Could not save settings: " + ex.Message); } } public Cache LoadCache() { try { lock (FileLock) { if (!File.Exists(CachePath)) { return null; } return JsonConvert.DeserializeObject<Cache>(File.ReadAllText(CachePath)); } } catch (Exception ex) { ModLog.Warn("Saved settings are unreadable and were ignored: " + ex.Message); return null; } } } } namespace BoomboxController.Multiplayer { public enum PlaybackPhase : byte { Idle, Loading, Playing, Paused } public sealed class BoomboxState { public ulong BoomboxId; public int Generation; public int Index; public PlaybackPhase Phase; public double StartServerTime; public double PausedPosition; public bool PlayWhenReady; public bool Loop; public List<TrackInfo> Queue = new List<TrackInfo>(); public TrackInfo CurrentTrack { get { if (Queue == null || Index < 0 || Index >= Queue.Count) { return null; } return Queue[Index]; } } public bool HasQueue { get { if (Phase != PlaybackPhase.Idle && Queue != null) { return Queue.Count > 0; } return false; } } public double PositionAt(double now) { if (Phase != PlaybackPhase.Playing) { return PausedPosition; } return now - StartServerTime; } public BoomboxState Clone() { BoomboxState obj = (BoomboxState)MemberwiseClone(); obj.Queue = ((Queue != null) ? new List<TrackInfo>(Queue) : new List<TrackInfo>()); return obj; } } public class MultiplayerManager : BoomboxController { private enum MessageType : byte { Hello = 1, HelloAck = 2, RequestPlay = 10, RequestSeek = 11, RequestTrack = 12, RequestStop = 13, RequestFullState = 14, LoadReport = 15, State = 20, Notice = 30 } public enum NoticeCode : byte { OnlyHost = 1, Rejected, LocalNotAllowed, RateLimited } private delegate void BodyWriter(ref FastBufferWriter writer); private sealed class ServerEntry { public BoomboxState State; public double LoadDeadline; public double NextSyncAt; public double LastSeenPlayer; public double CurrentDuration; public readonly HashSet<ulong> Reports = new HashSet<ulong>(); public void BeginLoad(double now, float timeoutSeconds) { Reports.Clear(); LoadDeadline = now + (double)timeoutSeconds; CurrentDuration = State.CurrentTrack?.DurationSeconds ?? 0.0; } } public const string Channel = "KoderTech.BoomboxController.Sync"; public const byte ProtocolVersion = 2; private const int InitialMessageBytes = 512; private const int MaxMessageBytes = 65536; private const int MaxQueueOnWire = 100; private const int MaxStringOnWire = 2048; private const double StartLeadInSeconds = 0.75; private const float HelloTimeoutSeconds = 8f; private const float MinSecondsBetweenRequests = 0.75f; private const double EndOfTrackGraceSeconds = 0.35; private const NetworkDelivery Delivery = (NetworkDelivery)4; public const int TrackNext = -1; public const int TrackPrevious = -2; private static readonly Dictionary<ulong, ServerEntry> ServerStates = new Dictionary<ulong, ServerEntry>(); private static readonly Dictionary<ulong, BoomboxState> PendingStates = new Dictionary<ulong, BoomboxState>(); private static readonly HashSet<ulong> ModdedClients = new HashSet<ulong>(); private static readonly Dictionary<ulong, float> LastRequestAt = new Dictionary<ulong, float>(); private static CustomMessagingManager _registeredOn; private static bool _tickHooked; private static bool _helloAcked; private static float _helloSentAt = -1f; private static bool _hostWithoutMod; private static bool _versionWarningShown; private static NetworkManager Net => NetworkManager.Singleton; public static bool IsOnline { get { NetworkManager net = Net; if ((Object)(object)net != (Object)null) { return net.IsListening; } return false; } } public static bool IsAuthority { get { if (IsOnline && !Net.IsServer) { return _hostWithoutMod; } return true; } } public static bool HostWithoutMod => _hostWithoutMod; public static double Now { get { //IL_0012: 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) if (!IsOnline) { return Time.realtimeSinceStartupAsDouble; } NetworkTime serverTime = Net.ServerTime; return ((NetworkTime)(ref serverTime)).Time; } } private static ulong LocalClientId { get { if (!IsOnline) { return 0uL; } return Net.LocalClientId; } } private static Configs Cfg => Plugin.config; [HarmonyPatch(typeof(StartOfRound), "Start")] [HarmonyPostfix] private static void StartOfRound_Start_Postfix() { try { BeginSession(); } catch (Exception exception) { ModLog.Error("Could not start the multiplayer session", exception); } } [HarmonyPatch(typeof(GameNetworkManager), "Disconnect")] [HarmonyPrefix] private static void GameNetworkManager_Disconnect_Prefix() { try { EndSession(); } catch (Exception exception) { ModLog.Error("Error while ending the multiplayer session", exception); } } private static void BeginSession() { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Expected O, but got Unknown if (!_tickHooked) { MainThreadDispatcher.OnTick += Tick; _tickHooked = true; } ResetSessionState(); NetworkManager net = Net; if ((Object)(object)net == (Object)null || !net.IsListening || net.CustomMessagingManager == null) { ModLog.Warn("NetworkManager is not listening; the boombox will work in local mode."); return; } if (_registeredOn != net.CustomMessagingManager) { net.CustomMessagingManager.RegisterNamedMessageHandler("KoderTech.BoomboxController.Sync", new HandleNamedMessageDelegate(OnNamedMessage)); _registeredOn = net.CustomMessagingManager; } net.OnClientDisconnectCallback -= OnClientDisconnected; net.OnClientDisconnectCallback += OnClientDisconnected; if (net.IsServer) { ModdedClients.Add(net.LocalClientId); _helloAcked = true; ModLog.Info("Multiplayer sync ready (host)."); return; } _helloSentAt = Time.realtimeSinceStartup; SendToServer(MessageType.Hello, delegate(ref FastBufferWriter w) { WriteString(ref w, "2.0.0"); }); ModLog.Info("Multiplayer sync: hello sent to host."); } private static void EndSession() { NetworkManager net = Net; try { if ((Object)(object)net != (Object)null && _registeredOn != null && net.CustomMessagingManager == _registeredOn) { net.CustomMessagingManager.UnregisterNamedMessageHandler("KoderTech.BoomboxController.Sync"); } if ((Object)(object)net != (Object)null) { net.OnClientDisconnectCallback -= OnClientDisconnected; } } catch (Exception ex) { ModLog.Debug("Unregister failed: " + ex.Message); } _registeredOn = null; ResetSessionState(); BoomboxTrackPlayer.ResetAll(); AudioManager.ReleaseAll(); } private static void ResetSessionState() { ServerStates.Clear(); PendingStates.Clear(); ModdedClients.Clear(); LastRequestAt.Clear(); _helloAcked = false; _helloSentAt = -1f; _hostWithoutMod = false; } private static void OnClientDisconnected(ulong clientId) { ModdedClients.Remove(clientId); LastRequestAt.Remove(clientId); } public static void RequestPlay(BoomboxTrackPlayer player, List<TrackInfo> tracks, int startIndex) { if ((Object)(object)player == (Object)null || tracks == null || tracks.Count == 0) { return; } ulong id = player.Id; if (IsAuthority) { HandlePlayRequest(LocalClientId, id, tracks, startIndex); return; } SendToServer(MessageType.RequestPlay, delegate(ref FastBufferWriter w) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_002a: 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) ((FastBufferWriter)(ref w)).WriteValueSafe<ulong>(ref id, default(ForPrimitives)); WriteQueue(ref w, tracks); ((FastBufferWriter)(ref w)).WriteValueSafe<int>(ref startIndex, default(ForPrimitives)); }); } public static void RequestSeek(BoomboxTrackPlayer player, double seconds) { if ((Object)(object)player == (Object)null) { return; } ulong id = player.Id; if (IsAuthority) { HandleSeekRequest(LocalClientId, id, seconds); return; } SendToServer(MessageType.RequestSeek, delegate(ref FastBufferWriter w) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) ((FastBufferWriter)(ref w)).WriteValueSafe<ulong>(ref id, default(ForPrimitives)); ((FastBufferWriter)(ref w)).WriteValueSafe<double>(ref seconds, default(ForPrimitives)); }); } public static void RequestTrack(BoomboxTrackPlayer player, int index) { if ((Object)(object)player == (Object)null) { return; } ulong id = player.Id; if (IsAuthority) { HandleTrackRequest(LocalClientId, id, index); return; } SendToServer(MessageType.RequestTrack, delegate(ref FastBufferWriter w) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) ((FastBufferWriter)(ref w)).WriteValueSafe<ulong>(ref id, default(ForPrimitives)); ((FastBufferWriter)(ref w)).WriteValueSafe<int>(ref index, default(ForPrimitives)); }); } public static void RequestStop(BoomboxTrackPlayer player) { if ((Object)(object)player == (Object)null) { return; } ulong id = player.Id; if (IsAuthority) { HandleStopRequest(LocalClientId, id); return; } SendToServer(MessageType.RequestStop, delegate(ref FastBufferWriter w) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) ((FastBufferWriter)(ref w)).WriteValueSafe<ulong>(ref id, default(ForPrimitives)); }); } public static void ReportLoadResult(BoomboxTrackPlayer player, int generation, int index, bool success, double clipLength) { if ((Object)(object)player == (Object)null) { return; } ulong id = player.Id; if (IsAuthority) { HandleLoadReport(LocalClientId, id, generation, index, success, clipLength); return; } SendToServer(MessageType.LoadReport, delegate(ref FastBufferWriter w) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: 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_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005d: 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) ((FastBufferWriter)(ref w)).WriteValueSafe<ulong>(ref id, default(ForPrimitives)); ((FastBufferWriter)(ref w)).WriteValueSafe<int>(ref generation, default(ForPrimitives)); ((FastBufferWriter)(ref w)).WriteValueSafe<int>(ref index, default(ForPrimitives)); ((FastBufferWriter)(ref w)).WriteValueSafe<bool>(ref success, default(ForPrimitives)); ((FastBufferWriter)(ref w)).WriteValueSafe<double>(ref clipLength, default(ForPrimitives)); }); } public static void OnVanillaToggle(BoomboxTrackPlayer player, bool on) { if ((Object)(object)player == (Object)null || !IsAuthority || !ServerStates.TryGetValue(player.Id, out var value)) { return; } BoomboxState state = value.State; double now = Now; bool flag = false; switch (state.Phase) { case PlaybackPhase.Playing: if (!on) { state.PausedPosition = ClampPosition(now - state.StartServerTime, value.CurrentDuration); state.Phase = PlaybackPhase.Paused; flag = true; } break; case PlaybackPhase.Paused: if (on) { state.StartServerTime = now - state.PausedPosition; state.Phase = PlaybackPhase.Playing; flag = true; } break; case PlaybackPhase.Loading: if (state.PlayWhenReady != on) { state.PlayWhenReady = on; flag = true; } break; } if (flag) { BroadcastState(value, includeQueue: false); } } public static void OnPlayerRegistered(BoomboxTrackPlayer player) { if (!((Object)(object)player == (Object)null)) { ulong id = player.Id; BoomboxState value2; if (IsAuthority && ServerStates.TryGetValue(id, out var value)) { player.ApplyState(value.State.Clone()); } else if (PendingStates.TryGetValue(id, out value2)) { PendingStates.Remove(id); player.ApplyState(value2); } } } public static void OnPlayerDestroyed(ulong id) { PendingStates.Remove(id); if (IsAuthority) { ServerStates.Remove(id); } } private static bool Authorize(ulong sender, ulong boomboxId, bool isControl, out BoomboxTrackPlayer player) { //IL_00b1: 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) player = BoomboxTrackPlayer.Find(boomboxId); if ((Object)(object)player == (Object)null) { SendNotice(sender, NoticeCode.Rejected, "boombox not found"); return false; } if (!isControl) { return true; } bool flag = sender == LocalClientId; if (!flag && Cfg.onlyHostCanControl.Value) { SendNotice(sender, NoticeCode.OnlyHost, string.Empty); return false; } if (!flag) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (LastRequestAt.TryGetValue(sender, out var value) && realtimeSinceStartup - value < 0.75f) { SendNotice(sender, NoticeCode.RateLimited, "too many requests"); return false; } LastRequestAt[sender] = realtimeSinceStartup; float value2 = Cfg.commandRadius.Value; if (value2 > 0f && TryGetPlayerPosition(sender, out var position) && Vector3.Distance(position, ((Component)player).transform.position) > value2 + 2f) { SendNotice(sender, NoticeCode.Rejected, "too far from the boombox"); return false; } } return true; } private static void HandlePlayRequest(ulong sender, ulong boomboxId, List<TrackInfo> tracks, int startIndex) { if (!Authorize(sender, boomboxId, isControl: true, out var player)) { return; } int value = Cfg.maxPlaylistItems.Value; List<TrackInfo> list = new List<TrackInfo>(); bool flag = false; foreach (TrackInfo track in tracks) { if (track == null) { continue; } Uri uri; string reason; if (track.Source == TrackSource.Local) { if (!Cfg.allowLocalTracks.Value) { flag = true; continue; } } else if (!Downloader.TryParseAllowedUrl(track.Url, out uri, out reason)) { ModLog.Warn("Host rejected a track from client " + sender + ": " + reason); continue; } list.Add(track); if (list.Count >= value) { break; } } if (list.Count == 0) { if (flag) { SendNotice(sender, NoticeCode.LocalNotAllowed, string.Empty); } else { SendNotice(sender, NoticeCode.Rejected, "no valid track (check AllowedDomains)"); } return; } ServerEntry orCreateEntry = GetOrCreateEntry(boomboxId); BoomboxState state = orCreateEntry.State; bool flag2 = (Object)(object)player.Boombox != (Object)null && player.Boombox.isPlayingMusic; state.Generation++; state.Queue = list; state.Index = Mathf.Clamp(startIndex, 0, list.Count - 1); state.Phase = PlaybackPhase.Loading; state.PlayWhenReady = Cfg.autoPlayOnLoad.Value || flag2; state.Loop = Cfg.loopQueue.Value; state.PausedPosition = 0.0; state.StartServerTime = 0.0; orCreateEntry.BeginLoad(Now, Cfg.prepareTimeout.Value); ModLog.Info("Boombox " + boomboxId + ": new queue of " + list.Count + " track(s) from client " + sender + " (generation " + state.Generation + ")."); BroadcastState(orCreateEntry, includeQueue: true); } private static void HandleSeekRequest(ulong sender, ulong boomboxId, double seconds) { if (Authorize(sender, boomboxId, isControl: true, out var _) && ServerStates.TryGetValue(boomboxId, out var value) && value.State.HasQueue && !double.IsNaN(seconds) && !double.IsInfinity(seconds)) { double num = ClampPosition(seconds, value.CurrentDuration); BoomboxState state = value.State; switch (state.Phase) { case PlaybackPhase.Playing: state.StartServerTime = Now - num; break; case PlaybackPhase.Paused: state.PausedPosition = num; break; default: SendNotice(sender, NoticeCode.Rejected, "the track is still loading"); return; } BroadcastState(value, includeQueue: false); } } private static void HandleTrackRequest(ulong sender, ulong boomboxId, int index) { if (Authorize(sender, boomboxId, isControl: true, out var _) && ServerStates.TryGetValue(boomboxId, out var val