using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text.RegularExpressions; using System.Threading.Tasks; using BepInEx; using BepInEx.Logging; using BepInEx.Unity.IL2CPP; using ComputerMusicMod.Patches; using ComputerMusicMod.Runtime; using ComputerMusicMod.Services; using Fusion; using HarmonyLib; using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem.Collections.Generic; using Il2CppSystem.Linq; using Microsoft.CodeAnalysis; using NAudio.Wave; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("ComputerMusicMod")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("ComputerMusicMod")] [assembly: AssemblyTitle("ComputerMusicMod")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ComputerMusicMod { public static class ComputerUiCleanup { private static bool _cleanedOnce; public static void CleanupOnce() { if (_cleanedOnce) { return; } _cleanedOnce = true; Computer instance = Computer.Instance; if (!((Object)(object)instance == (Object)null)) { DestroyNamedRecursive(((Object)(object)instance.computerScreen != (Object)null) ? instance.computerScreen.transform : null, "Music Tab"); DestroyNamedRecursive(((Object)(object)instance.computerScreen != (Object)null) ? instance.computerScreen.transform : null, "MusicTabBTN"); DestroyNamedRecursive(((Object)(object)instance.computerScreen != (Object)null) ? instance.computerScreen.transform : null, "MusicTabPanel"); GameObject value = Traverse.Create((object)instance).Field("otherComputerCanvas").GetValue(); if ((Object)(object)value != (Object)null) { DestroyNamedRecursive(value.transform, "Music Tab"); DestroyNamedRecursive(value.transform, "MusicTabBTN"); DestroyNamedRecursive(value.transform, "MusicTabPanel"); } ModLog.Info("Removed legacy Music UI objects"); } } private static void DestroyNamedRecursive(Transform node, string name) { if ((Object)(object)node == (Object)null) { return; } for (int num = node.childCount - 1; num >= 0; num--) { Transform child = node.GetChild(num); if (((Object)child).name == name) { Object.Destroy((Object)(object)((Component)child).gameObject); } else { DestroyNamedRecursive(child, name); } } } } public static class ModLog { internal static ManualLogSource Source; public static void Info(string message) { ManualLogSource source = Source; if (source != null) { source.LogInfo((object)message); } } public static void Error(string message) { ManualLogSource source = Source; if (source != null) { source.LogError((object)message); } } } public static class ModPaths { public static string PluginDirectory { get; private set; } = ""; public static string CacheDirectory { get; private set; } = ""; public static string YtDlpPath { get; private set; } = ""; public static string FfmpegPath { get; private set; } = ""; public static string ProcessPathEnvironment { get; private set; } = ""; public static void Initialize(string assemblyLocation) { PluginDirectory = Path.GetDirectoryName(assemblyLocation) ?? ""; CacheDirectory = Path.Combine(PluginDirectory, "cache"); Directory.CreateDirectory(CacheDirectory); YtDlpPath = ResolveYtDlpPath(); FfmpegPath = ResolveFfmpegPath(); ProcessPathEnvironment = BuildProcessPath(); ModLog.Info("yt-dlp path: " + YtDlpPath); if (!string.IsNullOrEmpty(FfmpegPath)) { ModLog.Info("ffmpeg path: " + FfmpegPath); } ModLog.Info("cache path: " + CacheDirectory); } public static string TrackPath(string videoId) { return Path.Combine(CacheDirectory, videoId + ".wav"); } public static string FindTrackPath(string videoId) { string text = TrackPath(videoId); if (File.Exists(text)) { return text; } string text2 = Path.Combine(CacheDirectory, videoId + ".mp3"); if (File.Exists(text2)) { return text2; } if (!Directory.Exists(CacheDirectory)) { return null; } string[] files = Directory.GetFiles(CacheDirectory, videoId + ".*"); foreach (string text3 in files) { string extension = Path.GetExtension(text3); if (!string.Equals(extension, ".part", StringComparison.OrdinalIgnoreCase) && !string.Equals(extension, ".ytdl", StringComparison.OrdinalIgnoreCase)) { return text3; } } return null; } private static string BuildProcessPath() { List list = new List(); if (!string.IsNullOrEmpty(PluginDirectory)) { list.Add(PluginDirectory); } if (!string.IsNullOrEmpty(FfmpegPath)) { string directoryName = Path.GetDirectoryName(FfmpegPath); if (!string.IsNullOrEmpty(directoryName)) { list.Add(directoryName); } } string environmentVariable = Environment.GetEnvironmentVariable("PATH"); if (!string.IsNullOrEmpty(environmentVariable)) { list.Add(environmentVariable); } return string.Join(Path.PathSeparator.ToString(), list); } private static string ResolveYtDlpPath() { string text = Path.Combine(PluginDirectory, "yt-dlp.exe"); if (File.Exists(text)) { return text; } string environmentVariable = Environment.GetEnvironmentVariable("SAMMOD_YTDLP"); if (!string.IsNullOrWhiteSpace(environmentVariable) && File.Exists(environmentVariable)) { return environmentVariable; } List list = new List(); string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); list.Add(Path.Combine(folderPath, "miniconda3", "Scripts", "yt-dlp.exe")); list.Add(Path.Combine(folderPath, "miniconda3", "Library", "bin", "yt-dlp.exe")); list.Add(Path.Combine(folderPath, "anaconda3", "Scripts", "yt-dlp.exe")); list.Add(Path.Combine(folderPath, "scoop", "shims", "yt-dlp.exe")); list.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Python", "Python313", "Scripts", "yt-dlp.exe")); list.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Python", "Python312", "Scripts", "yt-dlp.exe")); list.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Python", "Python311", "Scripts", "yt-dlp.exe")); string text2 = FindOnPath("yt-dlp.exe"); if (!string.IsNullOrEmpty(text2)) { list.Add(text2); } foreach (string item in list) { if (!string.IsNullOrWhiteSpace(item) && File.Exists(item)) { return item; } } return "yt-dlp"; } private static string ResolveFfmpegPath() { string text = Path.Combine(PluginDirectory, "ffmpeg.exe"); if (File.Exists(text)) { return text; } string environmentVariable = Environment.GetEnvironmentVariable("SAMMOD_FFMPEG"); if (!string.IsNullOrWhiteSpace(environmentVariable) && File.Exists(environmentVariable)) { return environmentVariable; } List list = new List(); string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); list.Add(Path.Combine(folderPath, "miniconda3", "Scripts", "ffmpeg.exe")); list.Add(Path.Combine(folderPath, "miniconda3", "Library", "bin", "ffmpeg.exe")); list.Add(Path.Combine(folderPath, "scoop", "shims", "ffmpeg.exe")); string text2 = FindOnPath("ffmpeg.exe"); if (!string.IsNullOrEmpty(text2)) { list.Add(text2); } foreach (string item in list) { if (!string.IsNullOrWhiteSpace(item) && File.Exists(item)) { return item; } } return ""; } private static string FindOnPath(string exe) { string environmentVariable = Environment.GetEnvironmentVariable("PATH"); if (string.IsNullOrEmpty(environmentVariable)) { return null; } string[] array = environmentVariable.Split(Path.PathSeparator); foreach (string text in array) { if (!string.IsNullOrWhiteSpace(text)) { string text2 = Path.Combine(text.Trim(), exe); if (File.Exists(text2)) { return text2; } } } return null; } } public static class ModProtocol { public const string Prefix = "SAMMOD"; public const string Prep = "PREP"; public const string Ready = "READY"; public const string Play = "PLAY"; public const string Stop = "STOP"; public const string Clear = "CLEAR"; } public static class MusicTabController { private static bool _musicOpen; private static string _urlInput = ""; private static bool _urlFocused = true; private static Rect _panelRect; private static Rect _urlFieldRect; public static bool IsOpen => _musicOpen; public static void Initialize() { } public static void Update() { if (Input.GetKeyDown((KeyCode)109)) { Toggle(); } if (_musicOpen && Input.GetKeyDown((KeyCode)27)) { Close(); } if (_musicOpen) { Cursor.visible = true; Cursor.lockState = (CursorLockMode)0; if (_urlFocused) { ProcessUrlInput(); } } } public static void OnGui() { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01db: Unknown result type (might be due to invalid IL or missing references) //IL_020b: Unknown result type (might be due to invalid IL or missing references) //IL_02a0: Unknown result type (might be due to invalid IL or missing references) if (!_musicOpen) { return; } GUI.depth = -1000; float num = 520f; float num2 = 430f; float num3 = ((float)Screen.width - num) * 0.5f; float num4 = ((float)Screen.height - num2) * 0.5f; _panelRect = new Rect(num3, num4, num, num2); _urlFieldRect = new Rect(num3 + 12f, num4 + 0f, num - 80f, 26f); GUI.Box(_panelRect, "Music (M or Esc to close)"); float num5 = num3 + 12f; float y = num4 + 28f; float num6 = num - 24f; y = DrawLabel(num5, y, num6, MusicSyncService.StatusMessage); if (MusicSyncService.IsAwaitingReady) { y = DrawLabel(num5, y, num6, MusicSyncService.PendingTrackLabel); foreach (string readyStatusLine in MusicSyncService.GetReadyStatusLines()) { y = DrawLabel(num5, y, num6, readyStatusLine); } } y += 6f; _urlFieldRect = new Rect(num5, y, num6 - 62f, 26f); DrawUrlField(); if (GUI.Button(new Rect(num5 + num6 - 56f, y, 56f, 26f), "Add")) { MusicSyncService.AddFromUrl(_urlInput); } y += 32f; float num7 = (num6 - 18f) / 4f; if (GUI.Button(new Rect(num5, y, num7, 28f), "Play")) { MusicSyncService.RequestPlayCurrent(); } if (GUI.Button(new Rect(num5 + num7 + 6f, y, num7, 28f), "Skip")) { MusicSyncService.RequestPlayNext(); } if (GUI.Button(new Rect(num5 + (num7 + 6f) * 2f, y, num7, 28f), "Stop")) { MusicSyncService.RequestStop(); } if (GUI.Button(new Rect(num5 + (num7 + 6f) * 3f, y, num7, 28f), "Clear")) { MusicSyncService.ClearQueue(); } y += 34f; if (!MusicSyncService.IsHost) { y = DrawLabel(num5, y, num6, "Host controls play/stop/skip/queue"); } string text = $"Volume: {Mathf.RoundToInt(MusicPlaybackService.Volume * 100f)}% (local)"; y = DrawLabel(num5, y, num6, text); float num8 = GUI.HorizontalSlider(new Rect(num5, y, num6, 20f), MusicPlaybackService.Volume, 0f, 1f); if (Mathf.Abs(num8 - MusicPlaybackService.Volume) > 0.001f) { MusicPlaybackService.SetVolume(num8); } y += 24f; IReadOnlyList queue = MusicSyncService.GetQueue(); int num9 = Mathf.Max(0, queue.Count - 6); if (queue.Count > 6) { y = DrawLabel(num5, y, num6, $"... {queue.Count - 6} more above"); } for (int i = num9; i < queue.Count; i++) { MusicQueueItem musicQueueItem = queue[i]; string value = (YtDlpService.IsCached(musicQueueItem.VideoId) ? "[cached]" : "[pending]"); y = DrawLabel(num5, y, num6, $"{i + 1}. {musicQueueItem.Title} {value}"); } y += 4f; if (MusicPlaybackService.IsPlaying) { DrawLabel(num5, y, num6, "Now playing: " + MusicPlaybackService.CurrentVideoId); } else if (!string.IsNullOrEmpty(MusicPlaybackService.LastError)) { DrawLabel(num5, y, num6, MusicPlaybackService.LastError); } HandlePanelClick(); } private static float DrawLabel(float x, float y, float w, string text) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(text)) { return y; } GUI.Label(new Rect(x, y, w, 20f), text); return y + 22f; } private static void Toggle() { _musicOpen = !_musicOpen; if (_musicOpen) { _urlFocused = true; } } private static void Close() { _musicOpen = false; _urlFocused = false; } private static void DrawUrlField() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) GUI.Box(_urlFieldRect, ""); string text = ((_urlFocused && Time.frameCount / 20 % 2 == 0) ? "|" : ""); string text2 = (string.IsNullOrEmpty(_urlInput) ? "YouTube URL..." : (_urlInput + text)); GUI.Label(new Rect(((Rect)(ref _urlFieldRect)).x + 6f, ((Rect)(ref _urlFieldRect)).y + 4f, ((Rect)(ref _urlFieldRect)).width - 12f, ((Rect)(ref _urlFieldRect)).height - 8f), text2); } private static void HandlePanelClick() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) if ((int)Event.current.type == 0) { if (((Rect)(ref _urlFieldRect)).Contains(Event.current.mousePosition)) { _urlFocused = true; Event.current.Use(); } else if (((Rect)(ref _panelRect)).Contains(Event.current.mousePosition)) { _urlFocused = false; } } } private static void ProcessUrlInput() { string inputString = Input.inputString; if (!string.IsNullOrEmpty(inputString)) { string text = inputString; for (int i = 0; i < text.Length; i++) { char c = text[i]; if (c >= ' ' || c == '\t') { _urlInput += c; } } } if (Input.GetKeyDown((KeyCode)8) && _urlInput.Length > 0) { _urlInput = _urlInput.Substring(0, _urlInput.Length - 1); } if (Input.GetKeyDown((KeyCode)127)) { _urlInput = ""; } if ((Input.GetKey((KeyCode)306) || Input.GetKey((KeyCode)305)) && Input.GetKeyDown((KeyCode)118)) { TryPaste(); } if ((Input.GetKey((KeyCode)306) || Input.GetKey((KeyCode)305)) && Input.GetKeyDown((KeyCode)120)) { TryCut(); } } private static void TryPaste() { try { string systemCopyBuffer = GUIUtility.systemCopyBuffer; if (!string.IsNullOrEmpty(systemCopyBuffer)) { _urlInput += systemCopyBuffer.Trim(); } } catch { } } private static void TryCut() { try { if (!string.IsNullOrEmpty(_urlInput)) { GUIUtility.systemCopyBuffer = _urlInput; _urlInput = ""; } } catch { } } } [BepInPlugin("shiftatmidnight.computermusic", "Computer Music", "1.0.4")] public class Plugin : BasePlugin { public override void Load() { ModLog.Source = ((BasePlugin)this).Log; ModPaths.Initialize(Assembly.GetExecutingAssembly().Location); YtDlpService.Initialize(); MusicPlaybackService.Initialize(); MusicSyncService.Initialize(); MusicTabController.Initialize(); ModRunner.EnsureExists(); ComputerUiCleanup.CleanupOnce(); Harmony.CreateAndPatchAll(typeof(ComputerPatches), (string)null); ((BasePlugin)this).Log.LogInfo((object)"Computer Music v1 loaded"); } } public static class PluginInfo { public const string PluginGuid = "shiftatmidnight.computermusic"; public const string PluginName = "Computer Music"; public const string PluginVersion = "1.0.4"; } } namespace ComputerMusicMod.Services { public static class AudioFileLoader { public static AudioClip LoadFromFile(string path) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Expected O, but got Unknown if (path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) { return WavLoader.LoadFromFile(path); } if (path.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase)) { return LoadWaveStream((WaveStream)new Mp3FileReader(path), path); } return LoadWaveStream((WaveStream)new MediaFoundationReader(path), path); } private static AudioClip LoadWaveStream(WaveStream stream, string path) { try { ISampleProvider val = WaveExtensionMethods.ToSampleProvider((IWaveProvider)(object)stream); int sampleRate = stream.WaveFormat.SampleRate; int channels = stream.WaveFormat.Channels; float[] array = new float[sampleRate * channels]; List list = new List(sampleRate * channels * 30); int num; while ((num = val.Read(array, 0, array.Length)) > 0) { for (int i = 0; i < num; i++) { list.Add(array[i]); } } if (list.Count == 0) { throw new InvalidDataException("audio had no samples: " + path); } int num2 = list.Count / channels; AudioClip obj = AudioClip.Create(Path.GetFileNameWithoutExtension(path), num2, channels, sampleRate, false); obj.SetData(Il2CppStructArray.op_Implicit(list.ToArray()), 0); return obj; } finally { ((IDisposable)stream)?.Dispose(); } } } public static class MusicPlaybackService { private const string VolumePrefKey = "sammod_volume"; private const float DefaultVolume = 0.85f; private static AudioSource _source; private static string _currentVideoId; private static long _startTicks; private static float _waitUntil; private static bool _waitingStart; private static string _lastError = ""; private static float _volume = 0.85f; public static string CurrentVideoId => _currentVideoId; public static string LastError => _lastError; public static float Volume => _volume; public static bool IsPlaying { get { if ((Object)(object)_source != (Object)null) { return SafeIsPlaying(); } return false; } } public static void Initialize() { _volume = PlayerPrefs.GetFloat("sammod_volume", 0.85f); _volume = Mathf.Clamp01(_volume); } public static void SetVolume(float value) { _volume = Mathf.Clamp01(value); PlayerPrefs.SetFloat("sammod_volume", _volume); PlayerPrefs.Save(); ApplyVolume(); } public static void Tick() { if (_waitingStart && !(Time.unscaledTime < _waitUntil)) { _waitingStart = false; SafePlay(); ModLog.Info("Music playing: " + _currentVideoId); } } public static void Stop() { MainThreadQueue.Enqueue(StopNow); } public static void PlayCached(string videoId, long startTicks) { MainThreadQueue.Enqueue(delegate { PlayCachedNow(videoId, startTicks); }); } private static void StopNow() { _waitingStart = false; _currentVideoId = null; if ((Object)(object)_source == (Object)null) { return; } try { if (_source.isPlaying) { _source.Stop(); } } catch (Exception ex) { ModLog.Error("Music stop failed: " + ex.Message); } try { _source.clip = null; } catch (Exception ex2) { ModLog.Error("Music clip clear failed: " + ex2.Message); } } private static void PlayCachedNow(string videoId, long startTicks) { string text = ModPaths.FindTrackPath(videoId); if (text == null) { _lastError = "missing cache file for " + videoId; ModLog.Error(_lastError); return; } EnsureSource(); StopNow(); try { ModLog.Info("Music loading: " + text); AudioClip clip = AudioFileLoader.LoadFromFile(text); _source.clip = clip; _currentVideoId = videoId; _startTicks = startTicks; _lastError = ""; ApplyVolume(); double num = (double)(_startTicks - DateTime.UtcNow.Ticks) / 10000000.0; if (num > 0.05) { _waitUntil = Time.unscaledTime + (float)num; _waitingStart = true; } else { SafePlay(); ModLog.Info("Music playing: " + videoId); } } catch (Exception ex) { _lastError = ex.Message; ModLog.Error("Music load failed: " + ex.Message); } } private static void ApplyVolume() { if ((Object)(object)_source == (Object)null) { return; } try { _source.volume = _volume; } catch (Exception ex) { ModLog.Error("Music volume failed: " + ex.Message); } } private static void EnsureSource() { if (!((Object)(object)_source != (Object)null)) { ModRunner.EnsureExists(); _source = ((Component)ModRunner.Instance).GetComponent(); if ((Object)(object)_source == (Object)null) { _source = ((Component)ModRunner.Instance).gameObject.AddComponent(); } _source.playOnAwake = false; _source.loop = false; _source.spatialBlend = 0f; _source.bypassReverbZones = true; ApplyVolume(); } } private static void SafePlay() { if ((Object)(object)_source == (Object)null) { return; } try { ApplyVolume(); _source.Play(); } catch (Exception ex) { _lastError = ex.Message; ModLog.Error("Music play failed: " + ex.Message); } } private static bool SafeIsPlaying() { try { return _source.isPlaying; } catch { return false; } } } public sealed class MusicQueueItem { public string VideoId { get; } public string Source { get; } public string Title { get; } public MusicQueueItem(string videoId, string source, string title) { VideoId = videoId; Source = source; Title = title; } } public static class MusicSyncService { private static readonly Dictionary ReadyPlayers = new Dictionary(); private static readonly Dictionary ReadyErrors = new Dictionary(); private static string _pendingVideoId; private static string _pendingSource; private static string _pendingTitle; private static bool _awaitingReady; private static readonly List Queue = new List(); private static int _queueIndex = -1; public static string StatusMessage { get; private set; } = "Paste a YouTube URL and press Add."; public static string PendingTrackLabel { get; private set; } = ""; public static bool IsAwaitingReady => _awaitingReady; public static bool IsHost => NetworkUtility.IsHost(); public static IReadOnlyList GetReadyStatusLines() { if (!_awaitingReady) { return Array.Empty(); } List list = new List(); foreach (string activePlayerLabel in NetworkUtility.GetActivePlayerLabels()) { if (!ReadyPlayers.TryGetValue(activePlayerLabel, out var value)) { list.Add(activePlayerLabel + ": downloading..."); } else if (value) { list.Add(activePlayerLabel + ": ready"); } else { list.Add(activePlayerLabel + ": failed"); } } return list; } public static void Initialize() { } public static IReadOnlyList GetQueue() { return Queue; } public static void AddFromUrl(string input) { if (!YtDlpService.TryExtractVideoId(input, out var videoId)) { StatusMessage = "Invalid YouTube URL or video id."; return; } if (!NetworkUtility.IsHost()) { StatusMessage = "Only the host can edit the shared queue."; return; } Queue.Add(new MusicQueueItem(videoId, input.Trim(), videoId)); StatusMessage = "Added to queue: " + videoId; } public static void ClearQueue() { if (!NetworkUtility.IsHost()) { StatusMessage = "Only the host can clear the queue."; return; } ClearQueueLocal(); NetworkUtility.BroadcastModMessage("CLEAR|"); StatusMessage = "Queue cleared."; } public static void RequestPlayCurrent() { if (!NetworkUtility.IsHost()) { StatusMessage = "Only the host can start music."; return; } if (Queue.Count == 0) { StatusMessage = "Queue is empty."; return; } if (_queueIndex < 0 || _queueIndex >= Queue.Count) { _queueIndex = 0; } BeginPrepare(Queue[_queueIndex]); } public static void RequestPlayNext() { if (!NetworkUtility.IsHost()) { StatusMessage = "Only the host can skip tracks."; } else if (Queue.Count != 0) { _queueIndex = (_queueIndex + 1) % Queue.Count; BeginPrepare(Queue[_queueIndex]); } } public static void RequestStop() { if (!NetworkUtility.IsHost()) { StatusMessage = "Only the host can stop music."; return; } _awaitingReady = false; ReadyPlayers.Clear(); ReadyErrors.Clear(); NetworkUtility.BroadcastModMessage("STOP|"); MusicPlaybackService.Stop(); StatusMessage = "Stopped."; } private static void BeginPrepare(MusicQueueItem item) { _pendingVideoId = item.VideoId; _pendingSource = item.Source; _pendingTitle = item.Title; _awaitingReady = true; ReadyPlayers.Clear(); ReadyErrors.Clear(); PendingTrackLabel = item.Title; StatusMessage = "Preparing track for all players..."; NetworkUtility.BroadcastModMessage($"{"PREP"}|{item.VideoId}|{item.Source}|{item.Title}"); PrepareLocalAsync(item.VideoId, item.Source, item.Title, reportReady: true); } public static async Task PrepareLocalAsync(string videoId, string source, string title, bool reportReady) { MainThreadQueue.Enqueue(delegate { StatusMessage = (YtDlpService.IsCached(videoId) ? ("Ready: " + title) : ("Downloading: " + title)); }); (bool ok, string error) result = await YtDlpService.EnsureCachedAsync(videoId, source); if (!result.ok) { MainThreadQueue.Enqueue(delegate { StatusMessage = "Download failed: " + result.error; if (reportReady) { RegisterReadyFromLocal(videoId, ok: false, result.error); } }); return; } MainThreadQueue.Enqueue(delegate { StatusMessage = "Cached: " + title; if (reportReady) { RegisterReadyFromLocal(videoId, ok: true, ""); } }); } public static void HandleIncoming(string payload) { MainThreadQueue.Enqueue(delegate { HandleIncomingNow(payload); }); } private static void HandleIncomingNow(string payload) { string[] array = payload.Split('|'); if (array.Length == 0) { return; } switch (array[0]) { case "PREP": if (!NetworkUtility.IsHost() && array.Length >= 4) { string obj = array[1]; string text = array[2]; string text2 = array[3]; _pendingVideoId = obj; _pendingSource = text; _pendingTitle = text2; _awaitingReady = true; ReadyPlayers.Clear(); ReadyErrors.Clear(); PendingTrackLabel = text2; StatusMessage = "Host requested: " + text2; PrepareLocalAsync(obj, text, text2, reportReady: true); } break; case "READY": { if (array.Length < 4) { break; } string text3 = array[1]; string text4 = array[2]; bool flag = array[3] == "1"; string text5 = ((array.Length > 4) ? array[4] : ""); if (_awaitingReady && !(_pendingVideoId != text3)) { if (!NetworkUtility.IsHost()) { StatusMessage = (flag ? (text4 + " ready") : (text4 + " failed: " + text5)); break; } ReadyPlayers[text4] = flag; ReadyErrors[text4] = (flag ? "" : text5); RefreshReadyStatus(); } break; } case "PLAY": if (!NetworkUtility.IsHost() && array.Length >= 3) { string videoId = array[1]; if (!long.TryParse(array[2], out var result)) { result = 0L; } _awaitingReady = false; StatusMessage = "Playing"; MusicPlaybackService.PlayCached(videoId, result); } break; case "STOP": if (!NetworkUtility.IsHost()) { _awaitingReady = false; MusicPlaybackService.Stop(); StatusMessage = "Stopped."; } break; case "CLEAR": if (!NetworkUtility.IsHost()) { ClearQueueLocal(); StatusMessage = "Queue cleared."; } break; } } private static void ClearQueueLocal() { Queue.Clear(); _queueIndex = -1; _awaitingReady = false; ReadyPlayers.Clear(); ReadyErrors.Clear(); } public static void RegisterReadyFromLocal(string videoId, bool ok, string error) { if (_awaitingReady && !(_pendingVideoId != videoId)) { if (NetworkUtility.IsHost()) { string key = NetworkUtility.LocalPlayerLabel(); ReadyPlayers[key] = ok; ReadyErrors[key] = (ok ? "" : error); RefreshReadyStatus(); } else { NetworkUtility.SendReady(videoId, ok, error); } } } private static void RefreshReadyStatus() { if (!NetworkUtility.IsHost() || !_awaitingReady || _pendingVideoId == null) { return; } List list = (from p in NetworkUtility.GetActivePlayerLabels() where !ReadyPlayers.ContainsKey(p) select p).ToList(); List list2 = (from kv in ReadyPlayers where !kv.Value select kv.Key).ToList(); if (list2.Count > 0) { string value; string text = list2.Select((string p) => (!ReadyErrors.TryGetValue(p, out value) || string.IsNullOrWhiteSpace(value)) ? p : value).FirstOrDefault() ?? "download failed"; StatusMessage = "Cannot play: " + text; _awaitingReady = false; return; } if (list.Count > 0) { StatusMessage = "Waiting for: " + string.Join(", ", list); return; } long ticks = DateTime.UtcNow.AddSeconds(1.0).Ticks; NetworkUtility.BroadcastModMessage($"{"PLAY"}|{_pendingVideoId}|{ticks}"); StatusMessage = "Playing"; MusicPlaybackService.PlayCached(_pendingVideoId, ticks); _awaitingReady = false; } public static void Update() { if (_awaitingReady && NetworkUtility.IsHost()) { RefreshReadyStatus(); } } } public static class WavLoader { public static AudioClip LoadFromFile(string path) { using FileStream fileStream = File.OpenRead(path); using BinaryReader binaryReader = new BinaryReader(fileStream); if (new string(binaryReader.ReadChars(4)) != "RIFF") { throw new InvalidDataException("not a wav file"); } binaryReader.ReadInt32(); if (new string(binaryReader.ReadChars(4)) != "WAVE") { throw new InvalidDataException("not a wav file"); } int num = 0; int num2 = 0; int num3 = 0; byte[] array = null; while (fileStream.Position < fileStream.Length) { string text = new string(binaryReader.ReadChars(4)); int num4 = binaryReader.ReadInt32(); if (text == "fmt ") { binaryReader.ReadInt16(); num = binaryReader.ReadInt16(); num2 = binaryReader.ReadInt32(); binaryReader.ReadInt32(); binaryReader.ReadInt16(); num3 = binaryReader.ReadInt16(); int num5 = num4 - 16; if (num5 > 0) { binaryReader.ReadBytes(num5); } } else { if (text == "data") { array = binaryReader.ReadBytes(num4); break; } binaryReader.ReadBytes(num4); } } if (array == null || num <= 0 || num2 <= 0 || num3 != 16) { throw new InvalidDataException("unsupported wav format"); } int num6 = array.Length / 2 / num; float[] array2 = new float[num6 * num]; for (int i = 0; i < array2.Length; i++) { short num7 = BitConverter.ToInt16(array, i * 2); array2[i] = (float)num7 / 32768f; } AudioClip obj = AudioClip.Create(Path.GetFileNameWithoutExtension(path), num6, num, num2, false); obj.SetData(Il2CppStructArray.op_Implicit(array2), 0); return obj; } } public static class YtDlpService { private static readonly Regex IdRegex = new Regex("(?:v=|youtu\\.be/|shorts/)([A-Za-z0-9_-]{6,})", RegexOptions.Compiled); private static readonly string[] CookieBrowsers = new string[5] { "chrome", "firefox", "edge", "brave", "chromium" }; public static void Initialize() { } public static bool TryExtractVideoId(string input, out string videoId) { videoId = ""; if (string.IsNullOrWhiteSpace(input)) { return false; } input = input.Trim(); int length = input.Length; if (length >= 6 && length <= 32 && !input.Contains(' ') && !input.Contains('/')) { videoId = input; return true; } Match match = IdRegex.Match(input); if (!match.Success) { return false; } videoId = match.Groups[1].Value; return true; } public static bool IsCached(string videoId) { return ModPaths.FindTrackPath(videoId) != null; } public static async Task<(bool ok, string error)> EnsureCachedAsync(string videoId, string source) { if (IsCached(videoId)) { return (ok: true, error: ""); } List list = BuildAttempts(Path.Combine(ModPaths.CacheDirectory, videoId + ".%(ext)s"), source); string item = "download failed"; foreach (string item2 in list) { (bool, string) result = await RunAsync(item2, videoId); if (result.Item1) { return result; } item = result.Item2; ModLog.Error("yt-dlp failed: " + result.Item2); } return (ok: false, error: item); } private static List BuildAttempts(string tempPattern, string source) { List list = new List(); string text = "--no-playlist --retries 3 --fragment-retries 3 --no-warnings --no-update "; string text2 = "--extractor-args \"youtube:player_client=android,web,mweb\" "; string text3 = $"-o \"{tempPattern}\" \"{source}\""; if (!string.IsNullOrEmpty(ModPaths.FfmpegPath)) { list.Add($"{text}{text2}--ffmpeg-location \"{ModPaths.FfmpegPath}\" -x --audio-format wav --audio-quality 0 {text3}"); } list.Add(text + text2 + "-f ba/b " + text3); string[] cookieBrowsers = CookieBrowsers; foreach (string value in cookieBrowsers) { list.Add($"{text}{text2}--cookies-from-browser {value} -f ba/b {text3}"); } return list; } private static async Task<(bool ok, string error)> RunAsync(string arguments, string videoId) { _ = 1; try { ProcessStartInfo processStartInfo = new ProcessStartInfo { FileName = ModPaths.YtDlpPath, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; processStartInfo.Environment["PATH"] = ModPaths.ProcessPathEnvironment; using Process process = Process.Start(processStartInfo); if (process == null) { return (ok: false, error: "yt-dlp failed to start"); } string stderr = await process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(); if (process.ExitCode != 0) { return (ok: false, error: FormatYtDlpError(stderr, process.ExitCode)); } if (ModPaths.FindTrackPath(videoId) != null) { return (ok: true, error: ""); } return (ok: false, error: "download finished but audio missing"); } catch (Exception ex) { return (ok: false, error: ex.Message); } } private static string FormatYtDlpError(string stderr, int exitCode) { if (string.IsNullOrWhiteSpace(stderr)) { return $"yt-dlp exit {exitCode}"; } string[] array = stderr.Split('\n'); for (int num = array.Length - 1; num >= 0; num--) { string text = array[num].Trim(); if (!string.IsNullOrEmpty(text) && !text.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase)) { if (text.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)) { string text2 = text; int length = "ERROR:".Length; return text2.Substring(length, text2.Length - length).Trim(); } return text; } } return stderr.Trim(); } } } namespace ComputerMusicMod.Runtime { public static class MainThreadQueue { private static readonly Queue Queue = new Queue(); private static readonly object Lock = new object(); public static void Enqueue(Action action) { if (action == null) { return; } lock (Lock) { Queue.Enqueue(action); } } public static void Drain() { while (true) { Action action; lock (Lock) { if (Queue.Count == 0) { break; } action = Queue.Dequeue(); } try { action(); } catch (Exception value) { ModLog.Error($"MainThreadQueue: {value}"); } } } } public class ModRunner : MonoBehaviour { public static ModRunner Instance { get; private set; } public ModRunner(IntPtr ptr) : base(ptr) { } public static void EnsureExists() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown if (!((Object)(object)Instance != (Object)null)) { ClassInjector.RegisterTypeInIl2Cpp(); GameObject val = new GameObject("ComputerMusicModRunner"); Object.DontDestroyOnLoad((Object)val); Instance = val.AddComponent(); } } private void Update() { MainThreadQueue.Drain(); if ((Object)(object)Computer.Instance != (Object)null) { ComputerUiCleanup.CleanupOnce(); } MusicTabController.Update(); MusicSyncService.Update(); MusicPlaybackService.Tick(); } private void OnGUI() { MusicTabController.OnGui(); } } public static class NetworkUtility { public static NetworkRunner GetRunner() { FusionNetworkManager instance = FusionNetworkManager.Instance; if ((Object)(object)instance == (Object)null) { return null; } return instance.GetRunner(); } public static bool IsHost() { NetworkRunner runner = GetRunner(); if ((Object)(object)runner == (Object)null || !runner.IsRunning) { return true; } if (!runner.IsServer) { return runner.IsSharedModeMasterClient; } return true; } public static string LocalPlayerLabel() { //IL_001f: Unknown result type (might be due to invalid IL or missing references) NetworkRunner runner = GetRunner(); if ((Object)(object)runner == (Object)null || !runner.IsRunning) { return "Local"; } return LabelFor(runner, runner.LocalPlayer); } public static List GetActivePlayerLabels() { //IL_0046: Unknown result type (might be due to invalid IL or missing references) try { NetworkRunner runner = GetRunner(); if ((Object)(object)runner == (Object)null || !runner.IsRunning) { return new List { "Local" }; } List list = new List(); List val = Enumerable.ToList(runner.ActivePlayers); for (int i = 0; i < val.Count; i++) { list.Add(LabelFor(runner, val[i])); } if (list.Count == 0) { list.Add(LocalPlayerLabel()); } return list; } catch (Exception ex) { ModLog.Error("GetActivePlayerLabels: " + ex.Message); return new List { LocalPlayerLabel() }; } } private static string LabelFor(NetworkRunner runner, PlayerRef player) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) string playerUserId = runner.GetPlayerUserId(player); if (!string.IsNullOrWhiteSpace(playerUserId)) { return playerUserId; } return $"Player{((PlayerRef)(ref player)).PlayerId}"; } public static void BroadcastModMessage(string payload) { Computer instance = Computer.Instance; if ((Object)(object)instance == (Object)null) { ModLog.Error("Computer.Instance missing, mod message not sent."); } else { instance.Rpc_CMD_SearchOnAllClients("SAMMOD|" + payload); } } public static void SendReady(string videoId, bool ok, string error) { MainThreadQueue.Enqueue(delegate { SendReadyNow(videoId, ok, error); }); } private static void SendReadyNow(string videoId, bool ok, string error) { Computer instance = Computer.Instance; if ((Object)(object)instance == (Object)null) { ModLog.Error("Computer.Instance missing, ready not sent."); return; } string value = LocalPlayerLabel(); string value2 = (error ?? "").Replace("|", "/"); instance.Rpc_CMD_ClickResult($"{"SAMMOD"}|{"READY"}|{videoId}|{value}|{(ok ? "1" : "0")}|{value2}"); } } } namespace ComputerMusicMod.Patches { [HarmonyPatch(typeof(Computer))] public static class ComputerPatches { [HarmonyPrefix] [HarmonyPatch("Rpc_SearchOnAllClients")] private static bool Rpc_SearchOnAllClients_Prefix(string __0) { if (string.IsNullOrEmpty(__0) || !__0.StartsWith("SAMMOD|")) { return true; } int num = "SAMMOD".Length + 1; MusicSyncService.HandleIncoming(__0.Substring(num, __0.Length - num)); return false; } [HarmonyPrefix] [HarmonyPatch("Rpc_ClickResult")] private static bool Rpc_ClickResult_Prefix(string __0) { if (string.IsNullOrEmpty(__0) || !__0.StartsWith("SAMMOD|")) { return true; } if (!NetworkUtility.IsHost()) { return false; } int num = "SAMMOD".Length + 1; MusicSyncService.HandleIncoming(__0.Substring(num, __0.Length - num)); return false; } [HarmonyPostfix] [HarmonyPatch("Rpc_TurnOnComputer")] private static void Rpc_TurnOnComputer_Postfix() { ComputerUiCleanup.CleanupOnce(); } } }