Decompiled source of Boombox Controller Remastered v2.0.0

BoomboxController.dll

Decompiled a day ago
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