Decompiled source of sPEAKer v3.0.3
plugins/onlystar.sPEAKer.dll
Decompiled 2 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Web; using AOT; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using PEAKLib.Core; using POpusCodec.Enums; using Peak; using Peak.UI; using Photon.Pun; using Photon.Realtime; using Photon.Voice; using Photon.Voice.Unity; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.DualShock; using UnityEngine.InputSystem.Switch; using UnityEngine.InputSystem.UI; using UnityEngine.InputSystem.Utilities; using UnityEngine.InputSystem.XInput; using UnityEngine.Networking; using UnityEngine.Rendering; using UnityEngine.SceneManagement; using UnityEngine.UI; using UnityEngine.Video; using Zorro.ControllerSupport; using Zorro.ControllerSupport.Glyphs; using Zorro.Core; using Zorro.Core.Serizalization; using sPEAKer.Patches; using sPEAKer.Scripts; using ytModule; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("onlystar")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyDescription("Some guy had it aboard. Does it still work?")] [assembly: AssemblyFileVersion("3.0.3.0")] [assembly: AssemblyInformationalVersion("3.0.3+9e751fea633ee6e6454714fc4a4e6afc6c6d55d8")] [assembly: AssemblyProduct("onlystar.sPEAKer")] [assembly: AssemblyTitle("sPEAKer")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("3.0.3.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 BepInEx { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] internal sealed class BepInAutoPluginAttribute : Attribute { public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace BepInEx.Preloader.Core.Patching { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] internal sealed class PatcherAutoPluginAttribute : Attribute { public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace sPEAKer { [BepInPlugin("onlystar.sPEAKer", "sPEAKer", "3.0.3")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string VERSION = "3.0.3"; private static GameObject _prefab = null; public static PeakBundle Bundle = null; public static string PluginFolder; public static TMP_FontAsset PeakFont; public static bool MilestonesEnabled; public static bool LevelingAvailable; private const float FontPollInterval = 0.25f; private const float FontPollTimeout = 300f; private ConfigEntry<string> externalAudioFolderPathConfig = null; private ConfigEntry<bool> loadDemoSongConfig = null; private ConfigEntry<bool> extendedFormatSupportConfig = null; private ConfigEntry<bool> startupLoadingIndicatorConfig = null; private ConfigEntry<bool> fastLoadConfig = null; internal ConfigEntry<bool> showUIHintConfig = null; internal ConfigEntry<bool> showTrackDurationConfig = null; internal ConfigEntry<bool> displayAuthorInfoConfig = null; internal ConfigEntry<bool> showLyricsConfig = null; internal ConfigEntry<bool> fetchThumbnailsConfig = null; private ConfigEntry<bool> shuffleOnStartConfig = null; private ConfigEntry<bool> turnOnAndPlayConfig = null; private ConfigEntry<bool> soundEffectsEnabledConfig = null; internal ConfigEntry<bool> useSpatialAudioConfig = null; private ConfigEntry<bool> renderVisualizersConfig = null; internal ConfigEntry<string> queueUIThemeConfig = null; internal ConfigEntry<string> droneFollowStyleConfig = null; internal ConfigEntry<string> gamepadSelectorStyleConfig = null; internal ConfigEntry<string> windowBreakpointConfig = null; internal ConfigEntry<string> speakerSkinConfig = null; private ConfigEntry<bool> spawnAtAirportConfig = null; private const int LOBBY_SPAWN_MAX_ATTEMPTS = 5; private const float LOBBY_SPAWN_VERIFY_DELAY = 2f; private ConfigEntry<bool> spawnAtGameStartConfig = null; private ConfigEntry<bool> spawnWithF4Config = null; private ConfigEntry<bool> recallEnabledConfig = null; private ConfigEntry<KeyCode> droneToggleKeyConfig = null; private ConfigEntry<KeyCode> recallCommandKeyConfig = null; private ConfigEntry<KeyCode> danceKeyConfig = null; private ConfigEntry<KeyCode> spawnKeyConfig = null; private ConfigEntry<KeyCode> queueUIKeyConfig = null; private ConfigEntry<KeyCode> ytKeyConfig = null; private ConfigEntry<KeyCode> hostOnlyKeyConfig = null; internal ConfigEntry<bool> autoPlayConfig = null; internal ConfigEntry<int> maxCachedSongsConfig = null; internal ConfigEntry<int> predownloadCountConfig = null; private const string SpectatorNoticeKey = "sPEAKer.SpectatorNoticeShown"; private ConfigEntry<bool> debugModeConfig = null; private const float SelectHoldThreshold = 0.3f; private static float _selectPressedAt = -1f; private static bool _selectHoldConsumed; private static float _queueKeyPressedAt = -1f; private static bool _queueKeyHoldConsumed; public static Plugin Instance { get; private set; } = null; internal static ManualLogSource LogObj { get; private set; } = null; internal static GameObject SpeakerPrefab => _prefab; public static Sprite ShuffleIcon { get; private set; } public static Sprite LoopOneIcon { get; private set; } public static Sprite LoopAllIcon { get; private set; } public static Sprite LoopOffIcon { get; private set; } public static ushort ItemID { get; private set; } = 50592; public static string DefaultMusicPath { get; private set; } = ""; public static string ExternalAudioFolderPath { get; set; } = ""; public static bool LoadDemoSong { get; set; } public static bool ExtendedFormatSupport { get; set; } public static bool StartupLoadingIndicator { get; set; } public static bool FastLoad { get; set; } public static bool ShowUIHint { get; set; } public static bool ShowTrackDuration { get; set; } public static bool DisplayAuthorInfo { get; set; } public static bool ShowLyrics { get; set; } public static bool FetchThumbnails { get; set; } public static bool ShuffleOnStart { get; set; } public static bool TurnOnAndPlay { get; set; } public static bool SoundEffectsEnabled { get; set; } public static bool UseSpatialAudio { get; set; } public static bool RenderVisualizers { get; set; } public static string QueueUITheme { get; set; } = "default"; public static string DroneFollowStyle { get; set; } = "orbiting"; public static string GamepadSelectorStyle { get; set; } = "outline"; public static string WindowBreakpoint { get; set; } = "smallportrait"; public static string SpeakerSkinId { get; set; } = "speaker"; private static bool SpawnAtAirport { get; set; } private static bool SpawnAtGameStart { get; set; } private static bool SpawnWithF4 { get; set; } private static bool RecallEnabled { get; set; } public static KeyCode DroneToggleKey { get; set; } public static KeyCode RecallCommandKey { get; set; } public static KeyCode DanceKey { get; set; } private static KeyCode SpawnKey { get; set; } public static KeyCode QueueUIKey { get; set; } public static KeyCode YouTubeKey { get; set; } private static KeyCode HostOnlyKey { get; set; } public static bool AutoPlay { get; set; } = true; public static int MaxCachedSongs { get; set; } = 30; public static int PredownloadCount { get; set; } = 5; public static bool SpectatorNoticeShown { get { return PlayerPrefs.GetInt("sPEAKer.SpectatorNoticeShown", 0) != 0; } set { PlayerPrefs.SetInt("sPEAKer.SpectatorNoticeShown", value ? 1 : 0); PlayerPrefs.Save(); } } internal static bool DebugMode { get; set; } internal static bool TextInputFocused => IsTextInputFocused(); internal static int SelectConsumedFrame { get; private set; } = -1; public static event Action OnFontLoaded; public static event Action<int, int> OnPlayerLaserHit; public static void FirePlayerLaserHit(int viewID, int effectType) { Plugin.OnPlayerLaserHit?.Invoke(viewID, effectType); } private void Awake() { Instance = this; LogObj = ((BaseUnityPlugin)this).Logger; SetUserConfigs(); Localization.Apply(); ApplyPatches(); Log("[Plugin] Plugin awakening..."); ModDefinition.GetOrCreate(((BaseUnityPlugin)this).Info.Metadata); PluginFolder = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location); LoadFont(); GlyphManager.EnsureInitialized(); FeatureWheel.EnsureInitialized(); FeatureWheel.RegisterDefaultOptions(); AudioMaster.EnsureInitialized(); Netcode.EnsureInitialized(); TheObserver.EnsureInitialized(); BingBongHost.EnsureInitialized(); MediaKeys.Install(); MilestonesEnabled = true; MilestoneTracker.EnsureInitialized(); MilestoneUI.EnsureInitialized(); LevelingAvailable = Chainloader.PluginInfos.ContainsKey("com.atomic.leveling"); if (!LevelingAvailable) { Log("Leveling is not installed. Milestones will work, but XP will be pending.", "warning"); } ytModuleCompatibility.EnsureYouTubePlayerInitialized(); ytModuleCompatibility.EnsureYouTubeQueueInitialized(); if (ytModuleCompatibility.CanDownload) { RateLimitUI.EnsureInitialized(); } Lyrics.EnsureInitialized(); try { Log("[Plugin] Attempting to load bundle..."); BundleLoader.LoadBundleWithName((BaseUnityPlugin)(object)this, "ps.peakbundle", (Action<PeakBundle>)InitSpeaker); Log("[Plugin] Bundle load request submitted"); } catch (Exception ex) { Log("[Plugin] Failed to load bundle: " + ex.Message, "error"); } } private void InitSpeaker(PeakBundle bundle) { Log("[Plugin] Initializing sPEAKer with bundle..."); Bundle = bundle; _prefab = Bundle.LoadAsset<GameObject>("assets/sPEAKer.prefab"); Log("[Plugin] Successfully loaded sPEAKer prefab."); LoadTransportIcons(); SetupMaterials(_prefab); SceneManager.sceneLoaded += OnSceneLoaded; _prefab.AddComponent<Controller>(); Bundle.Mod.RegisterContent(); Log("[Plugin] Plugin sPEAKer is loaded!"); } private static void LoadTransportIcons() { Dictionary<string, string> dictionary = new Dictionary<string, string>(); string[] allAssetNames = Bundle.GetAllAssetNames(); foreach (string text in allAssetNames) { int num = text.LastIndexOf('/'); string key = ((num >= 0) ? text.Substring(num + 1) : text); if (!dictionary.ContainsKey(key)) { dictionary[key] = text; } } ShuffleIcon = LoadIcon(dictionary, "shuffle.png"); LoopOneIcon = LoadIcon(dictionary, "loop_one.png"); LoopAllIcon = LoadIcon(dictionary, "loop_all.png"); LoopOffIcon = LoadIcon(dictionary, "loop_off.png"); } private static Sprite LoadIcon(Dictionary<string, string> pathsByFileName, string fileName) { //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) if (!pathsByFileName.TryGetValue(fileName, out var value)) { Log("[Plugin] Icon '" + fileName + "' not in bundle; the button keeps its text label.", "warning"); return null; } Texture2D val = Bundle.LoadAsset<Texture2D>(value); if ((Object)(object)val == (Object)null) { Log("[Plugin] Asset '" + value + "' is not a Texture2D; the button keeps its text label.", "warning"); return null; } Log("[Plugin] Loaded icon '" + fileName + "' from '" + value + "'."); return Sprite.Create(val, new Rect(0f, 0f, (float)((Texture)val).width, (float)((Texture)val).height), new Vector2(0.5f, 0.5f)); } private static void SetupMaterials(GameObject gameObject) { Shader val = Shader.Find("W/Character"); if ((Object)(object)val == (Object)null) { Log("[Plugin] W/Character shader not found, falling back to W/Peak_Standard", "warning"); val = Shader.Find("W/Peak_Standard"); } if ((Object)(object)val == (Object)null) { Log("[Plugin] Neither W/Character nor W/Peak_Standard shader found!", "error"); return; } Renderer[] componentsInChildren = gameObject.GetComponentsInChildren<Renderer>(); Renderer[] array = componentsInChildren; foreach (Renderer val2 in array) { Material[] materials = val2.materials; Material[] array2 = materials; foreach (Material val3 in array2) { if (((Object)val3.shader).name == "Universal Render Pipeline/Lit" || ((Object)val3.shader).name == "Standard") { Texture val4 = val3.GetTexture("_BaseMap") ?? val3.GetTexture("_MainTex"); val3.shader = val; if ((Object)(object)val4 != (Object)null) { val3.SetTexture("_BaseTexture", val4); } Log("[Plugin] Applied W/Character shader to material: " + ((Object)val3).name); } } val2.materials = materials; } } private static void LoadFont() { if (!((Object)(object)PeakFont != (Object)null) && !TryAdoptFont()) { ((MonoBehaviour)Instance).StartCoroutine(PollForFont()); } } private static IEnumerator PollForFont() { float startedAt = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - startedAt < 300f) { yield return (object)new WaitForSecondsRealtime(0.25f); if ((Object)(object)PeakFont != (Object)null || TryAdoptFont(Time.realtimeSinceStartup - startedAt)) { yield break; } } Log($"[Plugin] Daruma font not found within {300f:F0}s; UI keeps the TMP default.", "warning"); } private static bool TryAdoptFont(float elapsed = 0f) { TMP_FontAsset[] array = Resources.FindObjectsOfTypeAll<TMP_FontAsset>(); TMP_FontAsset[] array2 = array; foreach (TMP_FontAsset val in array2) { if (((Object)val).name.Contains("DarumaDropOne") || ((Object)val).name.Contains("Daruma")) { PeakFont = val; FontSafe.Reset(); Log($"[Plugin] Found game font: {((Object)val).name} after {elapsed:F2}s"); Plugin.OnFontLoaded?.Invoke(); Plugin.OnFontLoaded = null; return true; } } return false; } private void SetUserConfigs() { //IL_0452: Unknown result type (might be due to invalid IL or missing references) //IL_0485: Unknown result type (might be due to invalid IL or missing references) //IL_04bb: Unknown result type (might be due to invalid IL or missing references) //IL_04ee: Unknown result type (might be due to invalid IL or missing references) //IL_0524: Unknown result type (might be due to invalid IL or missing references) //IL_0557: Unknown result type (might be due to invalid IL or missing references) //IL_058a: Unknown result type (might be due to invalid IL or missing references) DefaultMusicPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException(), "..\\onlystar-sPEAKer Music"); externalAudioFolderPathConfig = ((BaseUnityPlugin)this).Config.Bind<string>("Audio Loading", "ExternalAudioFolderPath", DefaultMusicPath, "Folder where the mod will first try to load mixtapes and external audio from.\nIt will default to \"/plugins/onlystar-sPEAKer Music\" in case of error."); ExternalAudioFolderPath = externalAudioFolderPathConfig.Value; loadDemoSongConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio Loading", "LoadDemoSong", true, "Include a demo song in your track list."); LoadDemoSong = loadDemoSongConfig.Value; extendedFormatSupportConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio Loading", "ExtendedFormatSupport", true, "Enables loading of .mp3 and .wav files. May impact performance."); ExtendedFormatSupport = extendedFormatSupportConfig.Value; startupLoadingIndicatorConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio Loading", "StartupLoadingIndicator", true, "Shows a loading indicator when audio loading is occuring."); StartupLoadingIndicator = startupLoadingIndicatorConfig.Value; fastLoadConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio Loading", "FastLoad", true, "Makes sPEAKer load songs during gameplay instead of at game start."); FastLoad = fastLoadConfig.Value; shuffleOnStartConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Playback", "ShuffleOnStart", false, "Start with Shuffle enabled by default. Saves you a toggle if you only want shuffled playback."); ShuffleOnStart = shuffleOnStartConfig.Value; turnOnAndPlayConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Playback", "TurnOnAndPlay", false, "Make sPEAKer start playing immediately after turning it on."); TurnOnAndPlay = turnOnAndPlayConfig.Value; soundEffectsEnabledConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Playback", "SoundEffectsEnabled", true, "Enables JBL sound effects."); SoundEffectsEnabled = soundEffectsEnabledConfig.Value; useSpatialAudioConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Playback", "UseSpatialAudio", true, "Enables proximity-based audio. If disabled, playback will sound globally instead of coming from the sPEAKer."); UseSpatialAudio = useSpatialAudioConfig.Value; renderVisualizersConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Playback", "RenderVisualizers", true, "Enables rendering audio visualizers on airport screens."); RenderVisualizers = renderVisualizersConfig.Value; showUIHintConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "ShowUIHint", true, "Shows the 'Open sPEAKer' hint at the top right of the screen."); ShowUIHint = showUIHintConfig.Value; showTrackDurationConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "ShowTrackDuration", true, "Shows song's length and current time."); ShowTrackDuration = showTrackDurationConfig.Value; displayAuthorInfoConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "DisplayAuthorInfo", true, "Displays mixtape author under the stamina bar."); DisplayAuthorInfo = displayAuthorInfoConfig.Value; queueUIThemeConfig = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "QueueUITheme", "default", "Theme for the Queue UI. Options: default, royal_amethyst, deep_sea, pinky_promise, blood_moon, terminal, wood, cyberpunk, forest"); QueueUITheme = queueUIThemeConfig.Value; gamepadSelectorStyleConfig = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "GamepadSelectorStyle", "outline", "Shape of the gamepad focus selector in the Queue UI. Options: outline, solid, brackets"); GamepadSelectorStyle = gamepadSelectorStyleConfig.Value; bool flag = false; droneFollowStyleConfig = ((BaseUnityPlugin)this).Config.Bind<string>("Drone", "DroneFollowStyle", "orbiting", "How sPEAKer follows you in Drone mode. Options: orbiting, hovering"); DroneFollowStyle = droneFollowStyleConfig.Value; bool flag2 = false; spawnAtAirportConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Spawning", "SpawnAtAirport", true, "Automatically spawn sPEAKer at the airport."); SpawnAtAirport = spawnAtAirportConfig.Value; spawnAtGameStartConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Spawning", "SpawnAtGameStart", true, "Automatically spawn sPEAKer around the crash site when a game starts."); SpawnAtGameStart = spawnAtGameStartConfig.Value; spawnWithF4Config = ((BaseUnityPlugin)this).Config.Bind<bool>("Spawning", "SpawnWithF4", true, "Allows the host to spawn a sPEAKer in their hands by pressing the spawn key (see Keybinds section).\nNotes:\n - Only one sPEAKer may exist at a time (for now).\n - Only the host can spawn a sPEAKer (for now)."); SpawnWithF4 = spawnWithF4Config.Value; recallEnabledConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Spawning", "RecallEnabled", true, "Enables recalling existing sPEAKer when pressing F4.\nNotes:\n - Only works if SpawnWithF4 is enabled.\n - sPEAKer will only be recalled if it's in the ground and not following any player."); RecallEnabled = recallEnabledConfig.Value; droneToggleKeyConfig = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Keybinds", "DroneToggle", (KeyCode)112, "Drone Mode toggle."); DroneToggleKey = droneToggleKeyConfig.Value; hostOnlyKeyConfig = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Keybinds", "HostOnlyKey", (KeyCode)104, "Toggles between streaming your voice or music."); HostOnlyKey = hostOnlyKeyConfig.Value; recallCommandKeyConfig = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Keybinds", "RecallCommand", (KeyCode)306, "Hold this while pressing DroneToggle key to make sPEAKer \"recall\" to you instead of looking for people nearby."); RecallCommandKey = recallCommandKeyConfig.Value; danceKeyConfig = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Keybinds", "DanceEmote", (KeyCode)111, "Makes sPEAKer dance when Drone Mode is active."); DanceKey = danceKeyConfig.Value; spawnKeyConfig = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Keybinds", "SpawnKey", (KeyCode)285, "Spawn a sPEAKer if the corresponding spawning config is enabled."); SpawnKey = spawnKeyConfig.Value; queueUIKeyConfig = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Keybinds", "QueueUIKey", (KeyCode)117, "Tap to open Queue UI. Hold to open the feature wheel."); QueueUIKey = queueUIKeyConfig.Value; ytKeyConfig = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Keybinds", "YouTubeKey", (KeyCode)121, "Enter Youtube Radio mode. Hold Ctrl and this key to exit."); YouTubeKey = ytKeyConfig.Value; autoPlayConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("YouTube Radio", "AutoPlay", true, "When enabled, reaching the end of the queue will automatically fetch 10 more recommendations and continue playing."); AutoPlay = autoPlayConfig.Value; maxCachedSongsConfig = ((BaseUnityPlugin)this).Config.Bind<int>("YouTube Radio", "MaxCachedSongs", 30, "Maximum number of downloaded songs to keep cached. Older songs are deleted when this limit is exceeded. Range: 10-100."); MaxCachedSongs = Mathf.Clamp(maxCachedSongsConfig.Value, 10, 100); predownloadCountConfig = ((BaseUnityPlugin)this).Config.Bind<int>("YouTube Radio", "PredownloadCount", 5, "Number of upcoming tracks to predownload. Higher values use more disk space but reduce buffering. Range: 1-20."); PredownloadCount = Mathf.Clamp(predownloadCountConfig.Value, 1, 20); showLyricsConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("YouTube Radio", "ShowLyrics", true, "Displays synced lyrics as toasts while a YouTube Radio track plays. Lyrics are fetched from LRCLIB."); ShowLyrics = showLyricsConfig.Value; fetchThumbnailsConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("YouTube Radio", "FetchThumbnails", true, "Shows real YouTube thumbnails on the Storage screen. Turn off to use letter placeholders and issue no image requests."); FetchThumbnails = fetchThumbnailsConfig.Value; debugModeConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("zZzZz", "Debug", true, "Enable debug mode AKA verbose logging."); DebugMode = debugModeConfig.Value; } private static bool IsTextInputFocused() { EventSystem current = EventSystem.current; GameObject val = ((current != null) ? current.currentSelectedGameObject : null); if ((Object)(object)val == (Object)null) { return false; } TMP_InputField component = val.GetComponent<TMP_InputField>(); if ((Object)(object)component != (Object)null && component.isFocused) { return true; } InputField component2 = val.GetComponent<InputField>(); if ((Object)(object)component2 != (Object)null && component2.isFocused) { return true; } return false; } private static bool GamepadToggleCombo() { //IL_0064: Unknown result type (might be due to invalid IL or missing references) Gamepad current = Gamepad.current; if (current == null) { _selectPressedAt = -1f; _selectHoldConsumed = false; return false; } if (current.selectButton.wasPressedThisFrame) { _selectPressedAt = Time.unscaledTime; _selectHoldConsumed = false; SelectConsumedFrame = Time.frameCount; if (DebugMode) { Log($"[ControllerNav] Select pressed; scheme {InputHandler.GetCurrentUsedInputScheme()}."); } } if (_selectPressedAt < 0f) { return false; } if (current.selectButton.isPressed) { if (_selectHoldConsumed) { return false; } if (Time.unscaledTime - _selectPressedAt < 0.3f) { return false; } _selectHoldConsumed = true; FeatureWheel.OpenWheel(WheelOpener.Gamepad); return false; } bool flag = !_selectHoldConsumed; _selectPressedAt = -1f; _selectHoldConsumed = false; if (!flag) { return false; } SelectConsumedFrame = Time.frameCount; return true; } private static bool KeyboardToggleCombo() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (Input.GetKeyDown(QueueUIKey)) { _queueKeyPressedAt = Time.unscaledTime; _queueKeyHoldConsumed = false; } if (_queueKeyPressedAt < 0f) { return false; } if (Input.GetKey(QueueUIKey)) { if (_queueKeyHoldConsumed) { return false; } if (Time.unscaledTime - _queueKeyPressedAt < 0.3f) { return false; } _queueKeyHoldConsumed = true; FeatureWheel.OpenWheel(WheelOpener.KeyboardMouse); return false; } bool result = !_queueKeyHoldConsumed; _queueKeyPressedAt = -1f; _queueKeyHoldConsumed = false; return result; } private void Update() { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Invalid comparison between Unknown and I4 //IL_0325: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Invalid comparison between Unknown and I4 //IL_0184: Unknown result type (might be due to invalid IL or missing references) if (IsTextInputFocused()) { return; } if (Input.GetKeyDown(SpawnKey)) { if (!PhotonNetwork.IsMasterClient) { sPEAKerUI.Instance.ShowRejectionToast(LocalizedText.GetText("REJECT_HOST_ONLY_SPAWN", true)); } else { if (!SpawnWithF4) { return; } if (TheObserver.Instance.DoessPEAKerExist()) { if (RecallEnabled && TheObserver.Instance.CanRecallsPEAKer()) { Controller controllerInstance = TheObserver.Instance.GetControllerInstance(); controllerInstance.KillYourself(); Instance.SpawnAtHand(); } else if (RecallEnabled) { sPEAKerUI.Instance.ShowRejectionToast(DescribeRecallRejection()); } } else { Instance.SpawnAtHand(); } } } if (Input.GetKeyDown(DanceKey)) { Controller controllerInstance2 = TheObserver.Instance.GetControllerInstance(); if ((Object)(object)controllerInstance2 != (Object)null && (int)controllerInstance2.item.itemState == 0) { Netcode.Instance.SendRequestDanceEmote(); } else { Log("[Plugin] Could not find controller to initiate Dance.", "warning"); } } if (Input.GetKeyDown(DroneToggleKey)) { Controller controllerInstance3 = TheObserver.Instance.GetControllerInstance(); if ((Object)(object)controllerInstance3 != (Object)null && (int)controllerInstance3.item.itemState == 0) { bool key = Input.GetKey(RecallCommandKey); bool key2 = Input.GetKey((KeyCode)307); bool key3 = Input.GetKey((KeyCode)303); if (key2 && key3) { if (controllerInstance3.droneBehaviour.IsFloating) { controllerInstance3.CycleDroneBehaviour(); } else { controllerInstance3.ToggleFloatMode(); ((MonoBehaviour)this).StartCoroutine(DelayedManualMode(controllerInstance3)); } } else if (key) { controllerInstance3.RecallToPlayer(Character.localCharacter); } else if (IsDroneFollowingOtherPlayer(controllerInstance3)) { Character followTarget = controllerInstance3.droneBehaviour.FollowTarget; sPEAKerUI.Instance.ShowRejectionToast(string.Format(LocalizedText.GetText("REJECT_FOLLOWING_OTHER", true), ((Object)(object)followTarget != (Object)null) ? followTarget.characterName : "")); } else if (controllerInstance3.droneBehaviour.IsFloating && controllerInstance3.droneBehaviour.CurrentBehaviourMode != DroneBehaviourMode.Orbiting) { controllerInstance3.droneBehaviour.SwitchBehaviour(DroneBehaviour.ConfiguredFollowMode); controllerInstance3.ToggleFloatMode(); sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_DRONE_OFF", true)); } else { controllerInstance3.ToggleFloatMode(); } } else { Log("[Plugin] Could not find controller to initiate Drone/Recall mode.", "warning"); sPEAKerUI.Instance.ShowRejectionToast(DescribeRecallRejection()); } } bool flag = KeyboardToggleCombo(); bool flag2 = GamepadToggleCombo(); if (flag || flag2) { if (!TheObserver.Instance.DoessPEAKerExist()) { return; } QueueUI.Instance.ToggleQueue(); } if (Input.GetKeyDown(YouTubeKey)) { ytModuleCompatibility.HandleYouTubeKeyPress(); } if (Input.GetKeyDown((KeyCode)261)) { if (MilestonesEnabled && (Object)(object)MilestoneTracker.Instance != (Object)null) { MilestoneTracker.Instance.ClearAllMilestones(); sPEAKerUI.Instance.ShowToast("[RIG] Milestones cleared"); } else { Log("[Plugin] Keypad5 rig: milestones unavailable, nothing cleared.", "warning"); } } DrainMediaKeys(); } private static bool IsDroneFollowingOtherPlayer(Controller controller) { DroneBehaviour droneBehaviour = (((Object)(object)controller != (Object)null) ? controller.droneBehaviour : null); return (Object)(object)droneBehaviour != (Object)null && droneBehaviour.IsFollowingTarget() && (Object)(object)droneBehaviour.FollowTarget != (Object)null && (Object)(object)droneBehaviour.FollowTarget != (Object)(object)Character.localCharacter; } private static string DescribeRecallRejection() { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Invalid comparison between Unknown and I4 TheObserver instance = TheObserver.Instance; if (!instance.DoessPEAKerExist()) { return LocalizedText.GetText("REJECT_NO_SPEAKER", true); } Controller controllerInstance = instance.GetControllerInstance(); if ((Object)(object)controllerInstance == (Object)null) { return LocalizedText.GetText("REJECT_POCKETED", true); } if ((int)controllerInstance.item.itemState > 0) { Character val = controllerInstance.item.holderCharacter ?? controllerInstance.item.lastHolderCharacter; if ((Object)(object)val != (Object)null) { return string.Format(LocalizedText.GetText("REJECT_IN_INVENTORY", true), val.characterName); } return LocalizedText.GetText("REJECT_NOT_ON_GROUND", true); } if (IsDroneFollowingOtherPlayer(controllerInstance)) { return string.Format(LocalizedText.GetText("REJECT_FOLLOWING_OTHER", true), controllerInstance.droneBehaviour.FollowTarget.characterName); } return LocalizedText.GetText("REJECT_NOT_ON_GROUND", true); } public static string DescribePlaybackRejection() { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Invalid comparison between Unknown and I4 Controller controllerInstance = TheObserver.Instance.GetControllerInstance(); Character val = controllerInstance?.item.lastHolderCharacter; if ((Object)(object)val != (Object)null && (Object)(object)val != (Object)(object)Character.localCharacter) { return string.Format(LocalizedText.GetText("REJECT_PLAYBACK_CONTROLLED", true), val.characterName); } Character val2 = controllerInstance?.droneBehaviour?.FollowTarget; if ((Object)(object)val2 != (Object)null && (Object)(object)val2 != (Object)(object)Character.localCharacter) { return string.Format(LocalizedText.GetText("REJECT_PLAYBACK_CONTROLLED", true), val2.characterName); } if ((Object)(object)controllerInstance != (Object)null && (int)controllerInstance.item.itemState == 0) { return LocalizedText.GetText("REJECT_PLAYBACK_GRAB", true); } return LocalizedText.GetText("REJECT_PLAYBACK_GENERIC", true); } private static void DrainMediaKeys() { MediaKeyAction action; while (MediaKeys.TryDequeue(out action)) { if (!TheObserver.Instance.DoessPEAKerExist()) { continue; } if (!TheObserver.Instance.CanControlPlayback()) { sPEAKerUI.Instance.ShowRejectionToast(DescribePlaybackRejection()); continue; } switch (action) { case MediaKeyAction.PlayPause: if (AudioMaster.IsYouTubeMode) { ytModuleCompatibility.SendYouTubeTogglePauseRequest(); } else { Controller.Static_PrimaryUse(); } break; case MediaKeyAction.Next: if (AudioMaster.IsYouTubeMode) { if (Controller.Static_YouTubeQueueHasTracks()) { ytModuleCompatibility.SendYouTubeSkipRequest(); } } else { Controller.Static_NextSong(); } break; case MediaKeyAction.Previous: if (AudioMaster.IsYouTubeMode) { if (Controller.Static_YouTubeQueueHasTracks()) { ytModuleCompatibility.SendYouTubePreviousRequest(); } } else { Controller.Static_PreviousSong(); } break; } } } private static IEnumerator DelayedManualMode(Controller controller) { yield return null; if (controller.droneBehaviour.IsFloating && controller.droneBehaviour.CurrentBehaviourMode != DroneBehaviourMode.Manual && controller.droneBehaviour.SwitchBehaviour(DroneBehaviourMode.Manual)) { sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_DRONE_MANUAL", true)); } } private void OnDestroy() { Log("[Plugin] Plugin destroying..."); MediaKeys.Uninstall(); SceneManager.sceneLoaded -= OnSceneLoaded; Instance = null; LogObj = null; } private void OnApplicationQuit() { MediaKeys.Uninstall(); } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { Log("[Plugin] Scene loaded: " + ((Scene)(ref scene)).name); if (TheObserver.Instance.DoessPEAKerExist()) { TheObserver.Instance.KillsPEAKer(); } TheObserver.Instance.ResetState(); if (((Scene)(ref scene)).name == "Airport") { string path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "data"); string airportMediaPath = Path.Combine(path, "airport"); AirportEditor.Initialize(airportMediaPath); QueueUI.Instance.PreWarmUI(); bool flag = false; MilestoneUI.Instance.LoadCategoryIcons(); JunkPayload.RestorePassport(); if (SpawnAtAirport) { Log("[Plugin] Spawning at airport."); ((MonoBehaviour)this).StartCoroutine(LobbySpawn()); } } if (((Scene)(ref scene)).name.StartsWith("Level_") && SpawnAtGameStart && PhotonNetwork.IsConnected && PhotonNetwork.IsMasterClient) { ((MonoBehaviour)this).StartCoroutine(BeachSpawn()); } } private void ApplyPatches() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown Harmony val = new Harmony("onlystar.sPEAKer"); val.PatchAll(Assembly.GetExecutingAssembly()); } private static IEnumerator LobbySpawn() { yield return (object)new WaitForSeconds(6f); if (!PhotonNetwork.IsConnected || !PhotonNetwork.InRoom) { Log("[Plugin] Not in a room. Skipping airport spawn.", "warning"); yield break; } if (!PhotonNetwork.IsMasterClient) { Log("[Plugin] Not the host. Can't spawn sPEAKer at airport."); yield break; } bool asBingBong = SkinRegistry.IsBingBongSkin && (Object)(object)SkinRegistry.BingBongPrefab != (Object)null; if (SkinRegistry.IsBingBongSkin && !asBingBong) { Log("[Skin] Airport spawn suppressed: the Bing Bong skin is active but unresolvable.", "warning"); yield break; } string spawnPrefabName = (asBingBong ? ((Object)SkinRegistry.BingBongPrefab).name : ((Object)_prefab).name); Vector3 seatPosition = new Vector3(-6.89f, 1.7f, 87.27f); Quaternion seatRotation = Quaternion.Euler(0f, 90f, 0f) * (asBingBong ? SkinRegistry.FacingOffset(SkinRegistry.BingBongPrefab) : Quaternion.identity); for (int attempt = 1; attempt <= 5; attempt++) { TheObserver.Instance.RequestScan(); yield return null; if (TheObserver.Instance.DoessPEAKerExist()) { if (attempt > 1) { Log($"[Plugin] sPEAKer confirmed at the airport after {attempt - 1} attempt(s)."); } yield break; } Log($"[Plugin] Spawning at airport (attempt {attempt}/{5})."); GameObject spawned = null; try { spawned = PhotonNetwork.InstantiateItemRoom(spawnPrefabName, seatPosition, seatRotation, true); } catch (Exception ex) { Log($"[Plugin] Airport spawn attempt {attempt} threw: {ex.Message}", "error"); } if ((Object)(object)spawned != (Object)null) { Log("[Plugin] Spawned " + (asBingBong ? "Bing Bong" : "sPEAKer") + " at Airport seat."); break; } if (attempt == 5) { Log($"[Plugin] Airport spawn failed after {5} attempts. The Observer takes it from here.", "error"); yield break; } Log($"[Plugin] Airport spawn attempt {attempt} did not take. Retrying.", "warning"); yield return (object)new WaitForSeconds(2f); } if (MilestonesEnabled && AudioMaster.IsInitialized) { MilestoneTracker.Instance.CheckSoundtrack2MyLife(); } } private static IEnumerator BeachSpawn() { yield return (object)new WaitForSeconds(5f); if (SkinRegistry.IsBingBongSkin) { Log("[Skin] Beach spawn suppressed: the Bing Bong skin is active."); yield break; } Player masterClient = PhotonNetwork.MasterClient; foreach (Character character in Character.AllCharacters) { if (!object.Equals((character != null) ? ((MonoBehaviourPun)character).photonView.Owner : null, masterClient)) { continue; } Vector3 spawnPos = new Vector3(character.Center.x + Random.insideUnitSphere.z * 1.5f, character.Center.y + 5f, character.Center.z + Random.insideUnitSphere.z * 1.5f); PhotonNetwork.InstantiateItemRoom(((Object)_prefab).name, spawnPos, Quaternion.identity, true); break; } if (MilestonesEnabled && AudioMaster.IsInitialized) { MilestoneTracker.Instance.CheckSoundtrack2MyLife(); } } public void SpawnAtHand() { //IL_005a: 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) //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_0073: Unknown result type (might be due to invalid IL or missing references) string name = ((Object)_prefab).name; if (SkinRegistry.IsBingBongSkin) { Item bingBongPrefab = SkinRegistry.BingBongPrefab; if ((Object)(object)bingBongPrefab == (Object)null) { Log("[Skin] Manual spawn failed: Bing Bong could not be resolved.", "warning"); return; } name = ((Object)bingBongPrefab).name; } GameObject val = PhotonNetwork.Instantiate("0_Items/" + name, Character.localCharacter.Center + Vector3.up * 3f, Quaternion.identity, (byte)0, (object[])null); if ((Object)(object)val == (Object)null) { Log("[Skin] Manual spawn failed: PhotonNetwork.Instantiate returned null for \"" + name + "\".", "warning"); return; } Item component = val.GetComponent<Item>(); if ((Object)(object)component == (Object)null) { Log("[Skin] Manual spawn failed: no Item component on \"" + name + "\".", "warning"); } else { component.Interact(Character.localCharacter); } } public static void SetDroneFollowStyle(string style) { if (!(DroneFollowStyle == style)) { DroneFollowStyle = style; if ((Object)(object)Instance != (Object)null && Instance.droneFollowStyleConfig != null) { Instance.droneFollowStyleConfig.Value = style; } QueueUI.Instance?.RefreshDroneFollowStyle(); } } public static void SetSpeakerSkin(string skinId) { if (!(SpeakerSkinId == skinId)) { SpeakerSkinId = skinId; if ((Object)(object)Instance != (Object)null && Instance.speakerSkinConfig != null) { Instance.speakerSkinConfig.Value = skinId; } Log("[Skin] Skin set to " + skinId + "."); } } public static void SetWindowBreakpoint(LayoutBreakpoint breakpoint) { StoreWindowBreakpoint(breakpoint); QueueUI.Instance?.ApplyBreakpoint(breakpoint); } public static void StoreWindowBreakpoint(LayoutBreakpoint breakpoint) { string value = (WindowBreakpoint = WindowLayout.IdOf(breakpoint)); if ((Object)(object)Instance != (Object)null && Instance.windowBreakpointConfig != null) { Instance.windowBreakpointConfig.Value = value; } } public static void SetGamepadSelectorStyle(string style) { if (!(GamepadSelectorStyle == style)) { GamepadSelectorStyle = style; if ((Object)(object)Instance != (Object)null && Instance.gamepadSelectorStyleConfig != null) { Instance.gamepadSelectorStyleConfig.Value = style; } QueueUI.Instance?.RefreshGamepadSelectorStyle(); } } public static void Log(string message, string type = "info") { if (DebugMode) { switch (type) { case "info": LogObj.LogInfo((object)message); break; case "warning": LogObj.LogWarning((object)message); break; case "error": LogObj.LogError((object)message); break; default: LogObj.LogInfo((object)message); break; } } } } } namespace sPEAKer.Scripts { public class AudioMaster : MonoBehaviourPun { private Transform _targetTransform; private Character _targetCharacter; private bool _isFollowing; public const string UserMixtapeName = "sPEAKer Music"; private const int HASH_ALGORITHM_VERSION = 1; private static FileHashCache _fileHashCache = new FileHashCache(); private static string _cachePath = ""; private static readonly List<string> _processedFilePaths = new List<string>(); private static int _cacheHits = 0; private static int _cacheMisses = 0; private static AudioMaster _instance; private static int NonOggFiles; private Coroutine _autoSkipCoroutine; private static int _suspendedMixtapeSongIndex = -1; private static HashSet<string> _hostLibrary = new HashSet<string>(); public static bool IsInitialized { get; private set; } private static bool IsLoading { get; set; } public static AudioSource MixtapeAudioSource { get; set; } = null; private static AudioSource EffectsAudioSource { get; set; } = null; public static AudioSource YouTubeAudioSource { get; private set; } = null; public static AudioSource ActiveSource { get { if (IsYouTubeMode) { return YouTubeAudioSource; } return MixtapeAudioSource; } } public static List<AudioClip> LoadedAudioClips { get; private set; } = new List<AudioClip>(); public static List<MixtapeInfo> AvailableMixtapes { get; set; } = new List<MixtapeInfo>(); public static Dictionary<string, int> SongHashToIndex { get; private set; } = new Dictionary<string, int>(); public static Dictionary<int, string> IndexToSongHash { get; private set; } = new Dictionary<int, string>(); public static Dictionary<int, string> IndexToFilePath { get; private set; } = new Dictionary<int, string>(); public static PlaybackState CurrentPlaybackState { get; private set; } = PlaybackState.Stopped; public static int CurrentSongIndex { get; private set; } public static float CurrentPausedTime { get; private set; } public static bool QueueExhausted { get; set; } public static float CurrentPlaybackTime => CurrentPlaybackState switch { PlaybackState.Playing => ((Object)(object)ActiveSource != (Object)null) ? ActiveSource.time : CurrentPausedTime, PlaybackState.Paused => CurrentPausedTime, _ => 0f, }; private static MixtapeInfo CurrentMixtape { get; set; } public static bool IsYouTubeMode { get; private set; } public static float NetworkVolume { get; private set; } = 0.35f; public static float LocalVolumeMultiplier { get; set; } = 0.5f; public static LoopState CurrentLoopState { get; private set; } = LoopState.None; public static List<string> SongQueue { get; private set; } = new List<string>(); public static AudioMaster Instance { get { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown if ((Object)(object)_instance == (Object)null) { _instance = Object.FindAnyObjectByType<AudioMaster>(); if ((Object)(object)_instance == (Object)null) { GameObject val = new GameObject("sPEAKerAudioMaster"); _instance = val.AddComponent<AudioMaster>(); Object.DontDestroyOnLoad((Object)(object)val); Plugin.Log("[AudioMaster] Created new instance and marked as DontDestroyOnLoad."); } } return _instance; } } public static bool IsPlayingWhiteNoise => (Object)(object)SoundEffects.WhiteNoise != (Object)null && (Object)(object)ActiveSource != (Object)null && (Object)(object)ActiveSource.clip == (Object)(object)SoundEffects.WhiteNoise; public static bool CanonReceived { get; private set; } public static bool CanonKnown => !PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient || CanonReceived; private static bool HostHasUnplayableTracks => _hostLibrary.Count > 0 && _hostLibrary.Any((string hash) => !SongHashToIndex.ContainsKey(hash)); public static event Action<string> OnSongChanged; public static event Action OnQueueSynced; public static event Action<PlaybackActionType, AudioClip, float> OnPlaybackAction; public static event Action<PlaybackState> OnPlaybackStateChanged; public static event Action<LoopState> OnLoopStateChanged; public static event Action<bool> OnYouTubeModeChanged; public static void EnsureInitialized() { Plugin.Log("[AudioMaster] EnsureInitialized called."); AudioMaster instance = Instance; if (!IsInitialized && !IsLoading) { Plugin.Log("[AudioMaster] Not initialized, starting initialization..."); instance.InitializeAsync(); } } private void Awake() { Plugin.Log("[AudioMaster] Awake called."); if ((Object)(object)_instance != (Object)null && (Object)(object)_instance != (Object)(object)this) { Plugin.Log("[AudioMaster] Duplicate instance detected, destroying."); Object.Destroy((Object)(object)((Component)this).gameObject); return; } _instance = this; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); Plugin.Log("[AudioMaster] Instance set and marked as DontDestroyOnLoad."); InitializeAudioSources(); SceneManager.sceneLoaded += OnSceneLoaded; if (!IsInitialized && !IsLoading) { InitializeAsync(); } } private void InitializeAudioSources() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown AnimationCurve val = new AnimationCurve(); val.AddKey(0f, 1f); val.AddKey(0.25f, 0.85f); val.AddKey(0.6f, 0.2f); val.AddKey(1f, 0f); for (int i = 0; i < val.length; i++) { val.SmoothTangents(i, 0f); } if ((Object)(object)MixtapeAudioSource == (Object)null) { MixtapeAudioSource = ((Component)this).gameObject.AddComponent<AudioSource>(); MixtapeAudioSource.loop = false; MixtapeAudioSource.playOnAwake = false; MixtapeAudioSource.volume = NetworkVolume * LocalVolumeMultiplier; if (Plugin.UseSpatialAudio) { MixtapeAudioSource.minDistance = 2f; MixtapeAudioSource.maxDistance = 50f; MixtapeAudioSource.spatialBlend = 1f; MixtapeAudioSource.dopplerLevel = 0.1f; MixtapeAudioSource.spatialize = true; MixtapeAudioSource.rolloffMode = (AudioRolloffMode)2; MixtapeAudioSource.SetCustomCurve((AudioSourceCurveType)0, val); } else { MixtapeAudioSource.spatialBlend = 0f; } Plugin.Log("[AudioMaster] Created persistent audio source."); } if ((Object)(object)EffectsAudioSource == (Object)null) { EffectsAudioSource = ((Component)this).gameObject.AddComponent<AudioSource>(); EffectsAudioSource.playOnAwake = false; EffectsAudioSource.loop = false; if (Plugin.UseSpatialAudio) { EffectsAudioSource.spatialBlend = 1f; EffectsAudioSource.volume = 1f; EffectsAudioSource.minDistance = 2f; EffectsAudioSource.maxDistance = 25f; EffectsAudioSource.rolloffMode = (AudioRolloffMode)0; } else { EffectsAudioSource.spatialBlend = 0f; EffectsAudioSource.volume = 1f; } Plugin.Log("[AudioMaster] Created effects audio source."); } if ((Object)(object)YouTubeAudioSource == (Object)null) { YouTubeAudioSource = ((Component)this).gameObject.AddComponent<AudioSource>(); YouTubeAudioSource.playOnAwake = false; YouTubeAudioSource.loop = false; YouTubeAudioSource.volume = NetworkVolume * LocalVolumeMultiplier; if (Plugin.UseSpatialAudio) { YouTubeAudioSource.minDistance = 2f; YouTubeAudioSource.maxDistance = 50f; YouTubeAudioSource.spatialBlend = 1f; YouTubeAudioSource.dopplerLevel = 0.1f; YouTubeAudioSource.spatialize = true; YouTubeAudioSource.rolloffMode = (AudioRolloffMode)2; YouTubeAudioSource.SetCustomCurve((AudioSourceCurveType)0, val); } else { YouTubeAudioSource.spatialBlend = 0f; } Plugin.Log("[AudioMaster] Created YouTube audio source."); } } private async Task InitializeAsync() { IsLoading = true; Plugin.Log("[AudioMaster] Starting initialization... "); LoadOrCreateHashCache(); sPEAKerUI.Instance.ShowLoading("Initializing sPEAKer..."); await LoadFolderAudio(); sPEAKerUI.Instance.ShowLoading("Loading bundle audio..."); await LoadBundleAudio(); if (LoadedAudioClips.Count > 0) { SongQueue = IndexToSongHash.Values.ToList(); Plugin.Log($"[AudioMaster] Initial song queue populated with {SongQueue.Count} tracks."); if (Plugin.ShuffleOnStart) { Master_ShuffleQueue(broadcast: false); Plugin.Log("[AudioMaster] Shuffled on start."); } } sPEAKerUI.Instance.ShowLoadingComplete(); SaveChangesToCache(); IsInitialized = true; IsLoading = false; Plugin.Log("[AudioMaster] Initialization complete. Total clips loaded: " + LoadedAudioClips.Count + ", Mixtapes loaded: " + AvailableMixtapes.Count); if (Plugin.MilestonesEnabled) { if (LoadedAudioClips.Count > 1 && NonOggFiles == 0) { MilestoneTracker.Instance.ReportProgress("CrystalClear"); } MilestoneTracker.Instance.SetAbsoluteProgress("ElectricRelaxation", LoadedAudioClips.Count); MilestoneTracker.Instance.SetAbsoluteProgress("DontStop", AvailableMixtapes.Count); } } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (((Scene)(ref scene)).name == "Airport") { StopAllPlayback(); sPEAKerUI.Instance.HideDuration(); sPEAKerUI.Instance.HideAuthorInfo(); Plugin.Log("[AudioMaster] Airport scene detected - stopped audio playback"); } } private void OnDestroy() { Plugin.Log("[AudioMaster] OnDestroy called."); SceneManager.sceneLoaded -= OnSceneLoaded; if ((Object)(object)_instance == (Object)(object)this) { IsInitialized = false; IsLoading = false; } } private async Task LoadBundleAudio() { Plugin.Log("[AudioMaster] Starting bundle audio loading..."); int waitCount = 0; while (Plugin.Bundle == null && waitCount < 100) { await Task.Delay(100); waitCount++; } if (Plugin.Bundle == null) { Plugin.Log("[AudioMaster] Bundle not available after 10 seconds - skipping bundle audio loading", "error"); return; } Plugin.Log("[AudioMaster] Bundle is now available, proceeding with loading..."); string[] allAssets = Plugin.Bundle.GetAllAssetNames(); Plugin.Log("[AudioMaster] Total assets in bundle: " + allAssets.Length); string[] jblFX = allAssets.Where((string asset) => asset.Contains("jbl_fx") && asset.EndsWith(".ogg")).ToArray(); string[] array = jblFX; foreach (string fx in array) { AudioClip clip = Plugin.Bundle.LoadAsset<AudioClip>(fx); string filename = Path.GetFileNameWithoutExtension(fx); SoundEffects.SetClip(filename, clip); } string[] laserFX = allAssets.Where((string asset) => asset.Contains("laser") && asset.EndsWith(".ogg")).ToArray(); string[] array2 = laserFX; foreach (string fx2 in array2) { AudioClip clip2 = Plugin.Bundle.LoadAsset<AudioClip>(fx2); string filename2 = Path.GetFileNameWithoutExtension(fx2); SoundEffects.SetClip(filename2, clip2); } string whiteNoise = allAssets.FirstOrDefault((string asset) => asset.Contains("whitenoise")); if (!string.IsNullOrEmpty(whiteNoise)) { AudioClip whiteNoiseClip = Plugin.Bundle.LoadAsset<AudioClip>(whiteNoise); SoundEffects.SetClip("whitenoise", whiteNoiseClip); } if (Plugin.LoadDemoSong) { AudioClip demoClip = Plugin.Bundle.LoadAsset<AudioClip>("assets/speaker audio/portal.ogg"); if ((Object)(object)demoClip != (Object)null) { demoClip.LoadAudioData(); while ((int)demoClip.loadState == 1) { await Task.Yield(); } if ((int)demoClip.loadState == 2) { LoadedAudioClips.Add(demoClip); string songHash = "portalAsset"; int index = LoadedAudioClips.Count - 1; SongHashToIndex[songHash] = index; IndexToSongHash[index] = songHash; IndexToFilePath[index] = null; } else { Plugin.Log("[AudioMaster] Failed to load demo song audio data", "error"); } } } Plugin.Log("[AudioMaster] Bundle loading complete."); } private async Task LoadFolderAudio() { List<string> foldersToProcess = new List<string>(); string finalUserPath; try { string configuredPath = Plugin.ExternalAudioFolderPath; if (Directory.Exists(configuredPath) || (!Directory.Exists(configuredPath) && Directory.GetParent(configuredPath).Exists)) { finalUserPath = configuredPath; } else { Plugin.Log("[AudioMaster] Configured path '" + configuredPath + "' is invalid. Falling back to the default path.", "warning"); finalUserPath = Plugin.DefaultMusicPath; } } catch (Exception ex) { Exception e = ex; Plugin.Log("[AudioMaster] Error validating configured path '" + Plugin.ExternalAudioFolderPath + "'. Falling back to default. Error: " + e.Message, "error"); finalUserPath = Plugin.DefaultMusicPath; } try { if (!Directory.Exists(finalUserPath)) { Directory.CreateDirectory(finalUserPath); Plugin.Log("[AudioMaster] Created audio folder: " + finalUserPath); } foldersToProcess.Add(finalUserPath); } catch (Exception ex) { Exception e2 = ex; Plugin.Log("[AudioMaster] Failed to create or access the final user path '" + finalUserPath + "'. Error: " + e2.Message, "error"); } string pluginsPath = Paths.PluginPath; if (Directory.Exists(pluginsPath)) { Plugin.Log("[AudioMaster] Scanning for mixtape mods in: " + pluginsPath); string[] subDirectories = Directory.GetDirectories(pluginsPath, "*", SearchOption.AllDirectories); string[] array = subDirectories; foreach (string subDir in array) { if (File.Exists(Path.Combine(subDir, "sPEAKer.json")) && !foldersToProcess.Contains(subDir)) { foldersToProcess.Add(subDir); Plugin.Log("[AudioMaster] Found mixtape mod folder: " + subDir); } } } Plugin.Log("[AudioMaster] Total unique folders to process: " + foldersToProcess.Count); foreach (string folderPath in foldersToProcess) { await ProcessFolderForAudio(folderPath, (folderPath == finalUserPath) ? "sPEAKer Music" : null); } } private async Task ProcessFolderForAudio(string folderPath, string forcedMixtapeName = null) { List<string> searchPatterns = new List<string> { "*.ogg" }; if (Plugin.ExtendedFormatSupport) { searchPatterns.Add("*.mp3"); searchPatterns.Add("*.wav"); } List<string> looseAudioFiles = searchPatterns.SelectMany((string pattern) => Directory.GetFiles(folderPath, pattern, SearchOption.TopDirectoryOnly)).ToList(); string[] subDirectories = Directory.GetDirectories(folderPath); if (looseAudioFiles.Count > 0) { await LoadLooseFilesAsMixtape(folderPath, looseAudioFiles, forcedMixtapeName); } string[] array = subDirectories; foreach (string mixtapeDir in array) { await LoadMixtapeAsync(mixtapeDir); } } private async Task<bool> LoadMixtapeAsync(string mixtapePath) { string mixtapeName = new DirectoryInfo(mixtapePath).Name; Plugin.Log("[AudioMaster] Loading mixtape: " + mixtapeName); Plugin.Log("[AudioMaster] From path: " + mixtapePath); MixtapeInfo mixtapeInfo = new MixtapeInfo { Name = mixtapeName, Author = "Unknown", FirstSongIndex = LoadedAudioClips.Count, Path = mixtapePath }; string metadataPath = Path.Combine(mixtapePath, "mixtape.json"); if (File.Exists(metadataPath)) { try { string jsonContent = File.ReadAllText(metadataPath); MixtapeMetadata metadata = JsonUtility.FromJson<MixtapeMetadata>(jsonContent); if (!string.IsNullOrEmpty(metadata.name)) { mixtapeInfo.Name = metadata.name; sPEAKerUI.Instance.UpdateLoadingPhase("Loading " + mixtapeInfo.Name + "..."); } else { sPEAKerUI.Instance.UpdateLoadingPhase("Loading " + mixtapeName + "..."); } if (!string.IsNullOrEmpty(metadata.author)) { mixtapeInfo.Author = metadata.author; } Plugin.Log("[AudioMaster] Loaded mixtape metadata - Name: " + mixtapeInfo.Name + ", Author: " + mixtapeInfo.Author); } catch (Exception ex) { Exception e = ex; Plugin.Log("[AudioMaster] Failed to parse mixtape metadata for " + mixtapeName + ": " + e.Message, "warning"); } } string iconPath = Path.Combine(mixtapePath, "..", "icon.png"); Plugin.Log("[AudioMaster] Checking for icon at path: " + iconPath); string normalizedIconPath = Path.GetFullPath(iconPath); Plugin.Log("[AudioMaster] Normalized icon path: " + normalizedIconPath); Plugin.Log($"[AudioMaster] Icon file exists: {File.Exists(normalizedIconPath)}"); if (File.Exists(normalizedIconPath)) { try { FileInfo fileInfo = new FileInfo(normalizedIconPath); Plugin.Log($"[AudioMaster] Icon file size: {fileInfo.Length} bytes"); string iconUrl = "file://" + normalizedIconPath.Replace('\\', '/').Replace("+", "%2B").Replace("#", "%23"); Plugin.Log("[AudioMaster] Converted icon URL: " + iconUrl); UnityWebRequest www = UnityWebRequest.Get(iconUrl); try { Plugin.Log("[AudioMaster] Starting web request for icon: " + mixtapeName); await (AsyncOperation)(object)www.SendWebRequest(); Plugin.Log($"[AudioMaster] Web request result: {www.result}"); Plugin.Log($"[AudioMaster] Response code: {www.responseCode}"); if ((int)www.result == 2 || (int)www.result == 3) { Plugin.Log("[AudioMaster] Failed to load icon for mixtape " + mixtapeName + ": " + www.error, "error"); } else { byte[] iconBytes = www.downloadHandler.data; Texture2D iconTexture = new Texture2D(2, 2); if (ImageConversion.LoadImage(iconTexture, iconBytes)) { Plugin.Log($"[AudioMaster] Icon texture dimensions: {((Texture)iconTexture).width}x{((Texture)iconTexture).height}"); Plugin.Log($"[AudioMaster] Icon texture is readable: {((Texture)iconTexture).isReadable}"); mixtapeInfo.IconTexture = iconTexture; Plugin.Log("[AudioMaster] Successfully loaded and assigned icon for mixtape: " + mixtapeName); } else { Plugin.Log("[AudioMaster] Failed to load image data from bytes for: " + mixtapeName, "error"); } } } finally { ((IDisposable)www)?.Dispose(); } } catch (Exception ex) { Exception e2 = ex; Plugin.Log("[AudioMaster] Exception loading icon for mixtape " + mixtapeName + ": " + e2.Message, "error"); } } List<string> searchPatterns = new List<string> { "*.ogg" }; if (Plugin.ExtendedFormatSupport) { searchPatterns.Add("*.mp3"); searchPatterns.Add("*.wav"); } List<string> audioFiles = searchPatterns.SelectMany((string pattern) => Directory.GetFiles(mixtapePath, pattern, SearchOption.TopDirectoryOnly)).ToList(); _processedFilePaths.AddRange(audioFiles.Select(NormalizePath)); Plugin.Log("[AudioMaster] Found " + audioFiles.Count + " audio files in mixtape."); int songSuccessCount = 0; float totalDuration = 0f; foreach (string audioFile in audioFiles) { string fileName = Path.GetFileName(audioFile); if (!fileName.Contains(".ogg")) { NonOggFiles++; } Plugin.Log("[AudioMaster] Loading mixtape song: " + fileName); string url = "file://" + audioFile.Replace('\\', '/').Replace("+", "%2B").Replace("#", "%23"); AudioType audioType = GetAudioTypeFromExtension(audioFile); if ((int)audioType == 0) { Plugin.Log("[AudioMaster] Skipping unsupported file type in mixtape: " + fileName, "warning"); continue; } UnityWebRequest www2 = UnityWebRequestMultimedia.GetAudioClip(url, audioType); try { ((DownloadHandlerAudioClip)www2.downloadHandler).streamAudio = Plugin.FastLoad; await (AsyncOperation)(object)www2.SendWebRequest(); if ((int)www2.result == 2 || (int)www2.result == 3) { Plugin.Log("[AudioMaster] Failed to load mixtape audio: " + fileName + " - Error: " + www2.error, "error"); goto IL_0b95; } AudioClip clip = DownloadHandlerAudioClip.GetContent(www2); if ((Object)(object)clip != (Object)null) { ((Object)clip).name = Path.GetFileNameWithoutExtension(audioFile); string songHash = await GenerateFileHashAsync(audioFile); if (songHash == null) { continue; } clip.LoadAudioData(); while ((int)clip.loadState == 1) { await Task.Yield(); } if ((int)clip.loadState == 2) { LoadedAudioClips.Add(clip); int index = LoadedAudioClips.Count - 1; SongHashToIndex[songHash] = index; IndexToSongHash[index] = songHash; IndexToFilePath[index] = audioFile; totalDuration += clip.length; songSuccessCount++; } else { Plugin.Log("[AudioMaster] Failed to preload audio data for mixtape song: " + audioFile, "error"); } goto IL_0b95; } LogUndecodableFile(audioFile); goto IL_0b95; } finally { ((IDisposable)www2)?.Dispose(); } IL_0b95: await Task.Yield(); } mixtapeInfo.SongCount = songSuccessCount; mixtapeInfo.LastSongIndex = mixtapeInfo.FirstSongIndex + songSuccessCount - 1; mixtapeInfo.TotalLength = FormatDuration(totalDuration); if (songSuccessCount > 0) { AvailableMixtapes.Add(mixtapeInfo); Plugin.Log("[AudioMaster] Successfully loaded mixtape '" + mixtapeInfo.Name + "' by " + mixtapeInfo.Author + " - " + songSuccessCount + " songs, " + mixtapeInfo.TotalLength + " total"); return true; } Plugin.Log("[AudioMaster] Mixtape folder '" + mixtapeInfo.Name + "' contains no valid audio files.", "warning"); return false; } private async Task LoadLooseFilesAsMixtape(string folderPath, List<string> audioFiles, string forcedMixtapeName = null) { string folderName = new DirectoryInfo(folderPath).Name; Plugin.Log("[AudioMaster] Loading loose files as mixtape: " + folderName); MixtapeInfo mixtapeInfo = new MixtapeInfo { Name = folderName, Author = "Unknown", FirstSongIndex = LoadedAudioClips.Count, Path = folderPath }; string metadataPath = Path.Combine(folderPath, "mixtape.json"); if (forcedMixtapeName != null) { mixtapeInfo.Name = forcedMixtapeName; sPEAKerUI.Instance.UpdateLoadingPhase("Loading " + forcedMixtapeName + "..."); } else if (File.Exists(metadataPath)) { try { string jsonContent = File.ReadAllText(metadataPath); Plugin.Log("[AudioMaster] Raw JSON content: " + jsonContent); MixtapeMetadata metadata = JsonUtility.FromJson<MixtapeMetadata>(jsonContent); Plugin.Log("[AudioMaster] Parsed metadata - name: '" + metadata?.name + "', author: '" + metadata?.author + "'"); if (metadata != null) { if (!string.IsNullOrEmpty(metadata.name)) { mixtapeInfo.Name = metadata.name; sPEAKerUI.Instance.UpdateLoadingPhase("Loading " + mixtapeInfo.Name + "..."); } else { sPEAKerUI.Instance.UpdateLoadingPhase("Loading " + folderName + "..."); } if (!string.IsNullOrEmpty(metadata.author)) { mixtapeInfo.Author = metadata.author; } } else { Plugin.Log("[AudioMaster] JsonUtility.FromJson returned null for mixtape metadata", "warning"); } Plugin.Log("[AudioMaster] Final mixtape info - Name: " + mixtapeInfo.Name + ", Author: " + mixtapeInfo.Author); } catch (Exception ex) { Exception e = ex; Plugin.Log("[AudioMaster] Failed to parse mixtape metadata for " + folderName + ": " + e.Message, "warning"); string[] parts = folderName.Split('-', 2); if (parts.Length == 2) { mixtapeInfo.Author = parts[0].Trim(); mixtapeInfo.Name = parts[1].Trim(); Plugin.Log("[AudioMaster] Used fallback parsing - Author: " + mixtapeInfo.Author + ", Name: " + mixtapeInfo.Name); } } } else { string[] parts2 = folderName.Split('-', 2); if (parts2.Length == 2) { mixtapeInfo.Author = parts2[0].Trim(); mixtapeInfo.Name = parts2[1].Trim(); Plugin.Log("[AudioMaster] Parsed folder name - Author: " + mixtapeInfo.Author + ", Name: " + mixtapeInfo.Name); } sPEAKerUI.Instance.UpdateLoadingPhase("Loading " + mixtapeInfo.Name + "..."); } string iconPath = Path.Combine(folderPath, "icon.png"); if (File.Exists(iconPath)) { try { string iconUrl = "file://" + iconPath.Replace('\\', '/').Replace("+", "%2B").Replace("#", "%23"); UnityWebRequest www = UnityWebRequest.Get(iconUrl); try { await (AsyncOperation)(object)www.SendWebRequest(); if ((int)www.result == 2 || (int)www.result == 3) { Plugin.Log("[AudioMaster] Failed to load icon for mixtape " + folderName + ": " + www.error, "error"); } else { byte[] iconBytes = www.downloadHandler.data; Texture2D iconTexture = new Texture2D(2, 2); if (ImageConversion.LoadImage(iconTexture, iconBytes)) { mixtapeInfo.IconTexture = iconTexture; Plugin.Log("[AudioMaster] Successfully loaded icon for loose files mixtape: " + folderName); } } } finally { ((IDisposable)www)?.Dispose(); } } catch (Exception ex) { Exception e2 = ex; Plugin.Log("[AudioMaster] Exception loading icon for loose files mixtape " + folderName + ": " + e2.Message, "error"); } } _processedFilePaths.AddRange(audioFiles.Select(NormalizePath)); int songSuccessCount = 0; float totalDuration = 0f; foreach (string audioFile in audioFiles) { string fileName = Path.GetFileName(audioFile); if (!fileName.Contains(".ogg")) { NonOggFiles++; } Plugin.Log("[AudioMaster] Loading loose file as mixtape song: " + fileName); string url = "file://" + audioFile.Replace('\\', '/').Replace("+", "%2B").Replace("#", "%23"); AudioType audioType = GetAudioTypeFromExtension(audioFile); if ((int)audioType == 0) { Plugin.Log("[AudioMaster] Skipping unsupported file type: " + fileName, "warning"); continue; } UnityWebRequest www2 = UnityWebRequestMultimedia.GetAudioClip(url, audioType); try { ((DownloadHandlerAudioClip)www2.downloadHandler).streamAudio = Plugin.FastLoad; await (AsyncOperation)(object)www2.SendWebRequest(); if ((int)www2.result == 2 || (int)www2.result == 3) { Plugin.Log("[AudioMaster] Failed to load loose audio: " + fileName + " - Error: " + www2.error, "error"); goto IL_0b27; } AudioClip clip = DownloadHandlerAudioClip.GetContent(www2); if ((Object)(object)clip != (Object)null) { ((Object)clip).name = Path.GetFileNameWithoutExtension(audioFile); string songHash = await GenerateFileHashAsync(audioFile); if (songHash == null) { continue; } clip.LoadAudioData(); while ((int)clip.loadState == 1) { await Task.Yield(); } if ((int)clip.loadState == 2) { LoadedAudioClips.Add(clip); int index = LoadedAudioClips.Count - 1; SongHashToIndex[songHash] = index; IndexToSongHash[index] = songHash; IndexToFilePath[index] = audioFile; totalDuration += clip.length; songSuccessCount++; } else { Plugin.Log("[AudioMaster] Failed to preload audio data for loose file: " + audioFile, "error"); } goto IL_0b27; } LogUndecodableFile(audioFile); goto IL_0b27; } finally { ((IDisposable)www2)?.Dispose(); } IL_0b27: await Task.Yield(); } mixtapeInfo.SongCount = songSuccessCount; mixtapeInfo.LastSongIndex = mixtapeInfo.FirstSongIndex + songSuccessCount - 1; mixtapeInfo.TotalLength = FormatDuration(totalDuration); if (songSuccessCount > 0) { AvailableMixtapes.Add(mixtapeInfo); Plugin.Log("[AudioMaster] Successfully loaded loose files as mixtape '" + mixtapeInfo.Name + "' by " + mixtapeInfo.Author + " - " + songSuccessCount + " songs, " + mixtapeInfo.TotalLength + " total"); } else { Plugin.Log("[AudioMaster] Loose files in '" + folderName + "' could not be loaded as mixtape.", "warning"); } } private async Task<string> GenerateFileHashAsync(string filePath) { string normalizedPath = NormalizePath(filePath); try { FileInfo fileInfo = new FileInfo(filePath); long size = fileInfo.Length; DateTime lastModified = fileInfo.LastWriteTimeUtc; if (_fileHashCache.Hashes.TryGetValue(normalizedPath, out var cachedEntry) && cachedEntry.Size == size && cachedEntry.LastModified == lastModified) { _cacheHits++; return cachedEntry.Hash; } _cacheMisses++; string fileName = Path.GetFileName(filePath); string uniqueId = $"{fileName}|{size}"; using SHA256 sha256 = SHA256.Create(); byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(uniqueId)); string newHash = Convert.ToBase64String(hashBytes).Substring(0, 12); _fileHashCache.Hashes[normalizedPath] = new CacheEntry { Hash = newHash, LastModified = lastModified, Size = size }; return await Task.FromResult(newHash); } catch (Exception ex) { Plugin.Log("[AudioMaster] Failed to generate metadata hash for " + filePath + ": " + ex.Message, "error"); return await Task.FromResult<string>(null); } } private void LoadOrCreateHashCache() { try { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string text = Path.Combine(directoryName, "data"); _cachePath = Path.Combine(text, "file_hashes.json"); if (!Directory.Exists(text)) { Directory.CreateDirectory(text); Plugin.Log("[Cache] Created cache data folder at: " + text); } if (File.Exists(_cachePath)) { string text2 = File.ReadAllText(_cachePath); FileHashCache fileHashCache = JsonConvert.DeserializeObject<FileHashCache>(text2); if (fileHashCache != null && fileHashCache.Version == 1 && fileHashCache.AlgorithmVersion == 1) { _fileHashCache = fileHashCache; Plugin.Log($"[Cache] Successfully loaded {_fileHashCache.Hashes.Count} hashes from cache (Algo v{1})."); } else { Plugin.Log("[Cache] Cache is from an incompatible algorithm version. Regenerating.", "warning"); _fileHashCache = new FileHashCache { AlgorithmVersion = 1 }; } } else { Plugin.Log("[Cache] No cache file found. A new one will be created."); _fileHashCache = new FileHashCache { AlgorithmVersion = 1 }; } } catch (Exception ex) { Plugin.Log("[Cache] Error loading hash cache, regenerating. Error: " + ex.Message, "error"); _fileHashCache = new FileHashCache { AlgorithmVersion = 1 }; } } private void SaveChangesToCache() { try { List<string> list = _fileHashCache.Hashes.Keys.Except(_processedFilePaths).ToList(); foreach (string item in list) { _fileHashCache.Hashes.Remove(item); } Plugin.Log($"[Cache] Cleanup removed {list.Count} stale entries."); Plugin.Log($"[Cache] Stats: {_cacheHits} hits, {_cacheMisses} misses."); string contents = JsonConvert.SerializeObject((object)_fileHashCache, (Formatting)1); string text = _cachePath + ".tmp"; File.WriteAllText(text, contents); if (File.Exists(_cachePath)) { File.Replace(text, _cachePath, null); } else { File.Move(text, _cachePath); } Plugin.Log($"[Cache] Saved {_fileHashCache.Hashes.Count} hashes to cache at {_cachePath}"); } catch (Exception ex) { Plugin.Log("[Cache] Failed to save hash cache: " + ex.Message, "error"); } } public static void FirePlaybackAction(PlaybackActionType action, AudioClip clip, float time) { AudioMaster.OnPlaybackAction?.Invoke(action, clip, time); Plugin.Log(string.Format("[AudioMaster] PlaybackAction fired: {0}, clip={1}, time={2}", action, ((clip != null) ? ((Object)clip).name : null) ?? "null", time)); } public async Task<AudioClip> GetNonStreamedClipAsync(int index) { if (index < 0 || index >= LoadedAudioClips.Count) { return null; } if (!Plugin.FastLoad) { return LoadedAudioClips[index]; } if (!IndexToFilePath.TryGetValue(index, out var filePath) || string.IsNullOrEmpty(filePath)) { return LoadedAudioClips[index]; } Plugin.Log($"[AudioMaster] Loading non-streamed clip for index {index} from: {Path.GetFileName(filePath)}"); string url = "file://" + filePath.Replace('\\', '/').Replace("+", "%2B").Replace("#", "%23"); AudioType audioType = GetAudioTypeFromExtension(filePath); UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(url, audioType); try { ((DownloadHandlerAudioClip)www.downloadHandler).streamAudio = false; await (AsyncOperation)(object)www.SendWebRequest(); if ((int)www.result != 1) { Plugin.Log("[AudioMaster] Failed to reload clip non-streamed: " + www.error, "error"); return null; } AudioClip clip = DownloadHandlerAudioClip.GetContent(www); if ((Object)(object)clip == (Object)null) { return null; } ((Object)clip).name = ((Object)LoadedAudioClips[index]).name + " (HostOnly)"; clip.LoadAudioData(); while ((int)clip.loadState == 1) { await Task.Yield(); } if ((int)clip.loadState != 2) { Plugin.Log("[AudioMaster] Failed to load audio data for non-streamed clip.", "error"); return null; } Plugin.Log("[AudioMaster] Non-streamed clip ready: " + ((Object)clip).name); return clip; } finally { ((IDisposable)www)?.Dispose(); } } public static void UpdatePlaybackState(PlaybackState newState, int songIndex = -1, float pausedTime = 0f) { CurrentPlaybackState = newState; if (songIndex >= 0) { CurrentSongIndex = songIndex; } CurrentPausedTime = pausedTime; QueueExhausted = false; AudioMaster.OnPlaybackStateChanged?.Invoke(newState); Plugin.Log($"[AudioMaster] Playback state updated - State: {newState}, Song: {CurrentSongIndex}, PausedTime: {pausedTime}"); } public static void SeekActiveSource(float time) { AudioSource activeSource = ActiveSource; if (!((Object)(object)activeSource == (Object)null)) { float currentPausedTime = (activeSource.time = (((Object)(object)activeSource.clip != (Object)null) ? Mathf.Clamp(time, 0f, activeSource.clip.length) : Mathf.Max(0f, time))); CurrentPausedTime = currentPausedTime; } } private static void ResetPlaybackState() { CurrentPlaybackState = PlaybackState.Stopped; CurrentSongIndex = 0; CurrentPausedTime = 0f; QueueExhausted = false; Plugin.Log("[AudioMaster] Playback state reset to defaults"); } private void PlaySongAtIndex(int index) { if (index < 0 || index >= LoadedAudioClips.Count || (Object)(object)MixtapeAudioSource == (Object)null) { Plugin.Log($"[AudioMaster] PlaySongAtIndex called with invalid index: {index}. Aborting.", "error"); return; } string text = IndexToSongHash[index]; Plugin.Log($"[AudioMaster] PlaySongAtIndex: Playing index {index} with hash '{text}'."); AudioMaster.OnSongChanged?.Invoke(text); if (Plugin.MilestonesEnabled) { MilestoneTracker.Instance.RecordSongPlayed(text); } Plugin.Log("[AudioMaster] OnSongChanged event has been invoked for hash '" + text + "'."); AudioClip val = LoadedAudioClips[index]; MixtapeAudioSource.clip = val; MixtapeAudioSource.Play(); FirePlaybackAction(PlaybackActionType.Play, val, 0f); if (Plugin.MilestonesEnabled) { MilestoneTracker.Instance.ReportProgress("UnstoppableForce"); } CurrentMixtape = GetMixtapeForSongIndex(index); if (!string.IsNullOrEmpty(CurrentMixtape.Name)) { sPEAKerUI.Instance.ShowToast(CurrentMixtape.Name + " - " + ((Object)val).name); sPEAKerUI.Instance.ShowAuthorInfo(CurrentMixtape.Author, IsYouTubeMode); } else { sPEAKerUI.Instance.HideAuthorInfo(); sPEAKerUI.Instance.ShowToast(((Object)val).name); } sPEAKerUI.Instance.ShowDuration(val.length); } private IEnumerator MonitorForAutoSkip() { Plugin.Log("[AudioMaster] Auto-skip monitor started."); yield return (object)new WaitUntil((Func<bool>)(() => (Object)(object)MixtapeAudioSource != (Object)null && MixtapeAudioSource.isPlaying)); while (CurrentPlaybackState == PlaybackState.Playing && MixtapeAudioSource.isPlaying) { if (IsYouTubeMode) { Plugin.Log("[AudioMaster] Auto-skip monitor ended - YouTube mode active."); yield break; } yield return (object)new WaitForSeconds(0.5f); } if (CurrentPlaybackState == PlaybackState.Playing && !IsYouTubeMode && PhotonNetwork.IsMasterClient) { Plugin.Log("[AudioMaster] Song finished. Automatically playing next track."); AutoSkipToNext(); } Plugin.Log("[AudioMaster] Auto-skip monitor ended."); } private void AutoSkipToNext() { List<AudioClip> loadedAudioClips = LoadedAudioClips; if (loadedAudioClips != null && loadedAudioClips.Count <= 0) { Plugin.Log("[AudioMaster] Not enough songs to auto-skip"); return; } int num2; switch (CurrentLoopState) { case LoopState.One: num2 = CurrentSongIndex; break; case LoopState.All: num2 = GetNextSongIndex(CurrentSongIndex); break; default: { string item = IndexToSongHash[CurrentSongIndex]; int num = SongQueue.IndexOf(item); if (num == SongQueue.Count - 1) { Plugin.Log("[AudioMaster] Reached end of queue. Stopping."); Netcode.Instance.BroadcastStopPlayback(); return; } num2 = GetNextSongIndex(CurrentSongIndex); break; } } string songHash = IndexToSongHash[num2]; string name = ((Object)LoadedAudioClips[num2]).name; Netcode.Instance.BroadcastNextSong(songHash, name); } public void StartAutoSkipMonitoring() { if (_autoSkipCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_autoSkipCoroutine); } _autoSkipCoroutine = ((MonoBehaviour)this).StartCoroutine(MonitorForAutoSkip()); } public void StopAutoSkipMonitoring() { if (_autoSkipCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_autoSkipCoroutine); _autoSkipCoroutine = null; } } public void StopAllPlayback() { ResetPlaybackState(); FirePlaybackAction(PlaybackActionType.Stop, null, 0f); if ((Object)(object)MixtapeAudioSource != (Object)null && MixtapeAudioSource.isPlaying) { MixtapeAudioSource.Stop(); } if ((Object)(object)YouTubeAudioSource != (Object)null && YouTubeAudioSource.isPlaying) { YouTubeAudioSource.Stop(); } } private IEnumerator FadeOut(float playDuration, float fadeDuration) { yield return (object)new WaitForSeconds(playDuration); float startVolume = MixtapeAudioSource.volume; float elapsed = 0f; while (elapsed < fadeDuration) { elapsed += Time.deltaTime; MixtapeAudioSource.volume = Mathf.Lerp(startVolume, 0f, elapsed / fadeDuration); yield return null; } MixtapeAudioSource.volume = startVolume; } public static void PauseLocalAudio() { if ((Object)(object)MixtapeAudioSource != (Object)null && MixtapeAudioSource.isPlaying) { _suspendedMixtapeSongIndex = CurrentSongIndex; MixtapeAudioSource.Pause(); Plugin.Log("[AudioMaster] Local audio paused for YouTube mode"); } } public static void ResumeLocalAudio() { if ((Object)(object)MixtapeAudioSource != (Object)null && (Object)(object)MixtapeAudioSource.clip != (Object)null) { MixtapeAudioSource.UnPause(); if (MixtapeAudioSource.isPlaying) { int num = ((_suspendedMixtapeSongIndex >= 0) ? _suspendedMixtapeSongIndex : CurrentSongIndex); UpdatePlaybackState(PlaybackState.Playing, num); Controller controllerInstance = TheObserver.Instance.GetControllerInstance(); if ((Object)(object)controllerInstance != (Object)null) { controllerInstance.UpdateMainPrompt(PlaybackState.Playing); } if (PhotonNetwork.IsMasterClient && IndexToSongHash.TryGetValue(num, out var value) && num < LoadedAudioClips.Count && (Object)(object)LoadedAudioClips[num] != (Object)null) { Netcode.Instance.BroadcastNextSong(value, ((Object)LoadedAudioClips[num]).name); } } AirportEditor.PlayAllVisualizers(ActiveSource); Plugin.Log("[AudioMaster] Local audio resumed from YouTube mode"); } _suspendedMixtapeSongIndex = -1; } public static void DiscardSuspendedMixtapeAudio() { if ((Object)(object)MixtapeAudioSource != (Object)null) { MixtapeAudioSource.Stop(); } _suspendedMixtapeSongIndex = -1; UpdatePlaybackState(PlaybackState.Stopped, CurrentSongIndex); Controller controllerInstance = TheObserver.Instance.GetControllerInstance(); if ((Object)(object)controllerInstance != (Object)null) { controllerInstance.UpdateMainPrompt(PlaybackState.Stopped); } Plugin.Log("[AudioMaster] Left YouTube mode as client - awaiting host playback state"); } public static void ToggleLoop() { CurrentLoopState = (LoopState)((int)(CurrentLoopState + 1) % 3); switch (CurrentLoopState) { case LoopState.None: sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_LOOP_OFF", true)); break; case LoopState.All: sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_LOOP_ALL", true)); break; case LoopState.One: sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_LOOP_ONE", true)); if (Plugin.MilestonesEnabled) { MilestoneTracker.Instance.ReportProgress("OneMoreTime"); } break; } if (PhotonNetwork.IsMasterClient) { Netcode.Instance.BroadcastLoopStateChange((int)CurrentLoopState); } AudioMaster.OnLoopStateChanged?.Invoke(CurrentLoopState); Plugin.Log($"[AudioMaster] Loop state toggled: {CurrentLoopState}"); } public void SetVolume(float newVolume) { float num = Mathf.Clamp(newVolume, 0f, 1f); if (num >= 1f && NetworkVolume < 1f) { PlayFX(SoundEffects.VolMax); if (Plugin.MilestonesEnabled) { MilestoneTracker.Instance.ReportProgress("SubeElBajo"); } } if (num <= 0f && NetworkVolume > 0f && Plugin.MilestonesEnabled) { MilestoneTracker.Instance.ReportProgress("Mute"); } NetworkVolume = num; ApplyVolumeMultiplier(); Plugin.Log("[AudioMaster] Network volume set to " + NetworkVolume + " with local multiplier " + LocalVolumeMultiplier + " = final volume " + NetworkVolume * LocalVolumeMultiplier); } public static void SetLocalVolumeMultiplier(float volume) { float localVolumeMultiplier = LocalVolumeMultiplier; LocalVolumeMultiplier = Mathf.Clamp01(volume); if (Plugin.MilestonesEnabled && Mathf.Abs(localVolumeMultiplier - LocalVolumeMultiplier) > 0.01f) { MilestoneTracker.Instance.ReportProgress("MyWay"); } ApplyVolumeMultiplier(); } private static void ApplyVolumeMultiplier() { if ((Object)(object)MixtapeAudioSource != (Object)null) { MixtapeAudioSource.volume = NetworkVolume * LocalVolumeMultiplier; } if ((Object)(object)YouTubeAudioSource != (Object)null) { YouTubeAudioSource.volume = NetworkVolume * LocalVolumeMultiplier; } } public static void UpdateSpatialAudioSetting(bool useSpatial) { float spatialBlend = (useSpatial ? 1f : 0f); if ((Object)(object)MixtapeAudioSource != (Object)null) { MixtapeAudioSource.spatialBlend = spatialBlend; } if ((Object)(object)EffectsAudioSource != (Object)null) { EffectsAudioSource.spatialBlend = spatialBlend; } if ((Object)(object)YouTubeAudioSource != (Object)null) { YouTubeAudioSource.spatialBlend = spatialBlend; } Plugin.Log("[AudioMaster] Spatial audio updated: " + (useSpatial ? "enabled" : "disabled")); } public static void PlayFX(AudioClip clip) { if (Plugin.SoundEffectsEnabled && !((Object)(object)EffectsAudioSource == (Object)null) && !((Object)(object)clip == (Object)null)) { EffectsAudioSource.clip = clip; EffectsAudioSource.Play(); Plugin.Log("[AudioMaster] Playing sound effect: " + ((Object)clip).name); } } public static void PlayLaserFX() { if (Plugin.SoundEffectsEnabled && !((Object)(object)EffectsAudioSource == (Object)null)) { List<AudioClip> list = new List<AudioClip>(3) { SoundEffects.Laser1, SoundEffects.Laser2, SoundEffects.Laser3 }; int index = Random.Range(0, 2); EffectsAudioSource.volume = 0.2f; EffectsAudioSource.clip = list[index]; EffectsAudioSource.Play(); Plugin.Log("[AudioMaster] Playing laser sound effect: " + ((Object)EffectsAudioSource.clip).name); } } private static int QueueEdgeIndex(bool fromStart) { if (SongQueue.Count == 0) { return 0; } string key = (fromStart ? SongQueue[0] : SongQueue[SongQueue.Count - 1]); return SongHashToIndex.GetValueOrDefault(key, 0); } public static int QueuedStartIndex(int preferredIndex) { if (SongQueue.Count == 0) { return preferredIndex; } string valueOrDefault = IndexToSongHash.GetValueOrDefault(preferredIndex, ""); if (!string.IsNullOrEmpty(valueOrDefault) && SongQueue.Contains(valueOrDefault)) { return preferredIndex; } return QueueEdgeIndex(fromStart: true); } public static int GetNextSongIndex(int currentIndex) { if (SongQueue.Count < 2) { return currentIndex; } string valueOrDefault = IndexToSongHash.GetValueOrDefault(currentIndex, ""); if (string.IsNullOrEmpty(valueOrDefault)) { return QueueEdgeIndex(fromStart: true); } int num = SongQueue.IndexOf(valueOrDefault); if (num == -1) { return QueueEdgeIndex(fromStart: true); } int index = (num + 1) % SongQueue.Count; string key = SongQueue[index]; return SongHashToIndex.GetValueOrDefault(key, 0); } public static int GetPreviousSongIndex(int currentIndex) { if (SongQueue.Count < 2) { return currentIndex; } string valueOrDefault = IndexToSongHash.GetValueOrDefault(currentIndex, ""); if (string.IsNullOrEmpty(valueOrDefault)) { return QueueEdgeIndex(fromStart: false); } int num = SongQueue.IndexOf(valueOrDefault); if (num == -1) { return QueueEdgeIndex(fromStart: false); } int index = (num - 1 + SongQueue.Count) % SongQueue.Count; string key = SongQueue[index]; return SongHashToIndex.GetValueOrDefault(key, 0); } public static void SetYouTubeMode(bool enabled) { IsYouTubeMode = enabled; Plugin.Log($"[AudioMaster] YouTube mode set to {enabled}"); AudioMaster.OnYouTubeModeChanged?.Invoke(enabled); } public void HandleNextSong(string songHash, string songName) { if (Plugin.MilestonesEnabled) { MilestoneTracker.Instance.ReportProgress("ThankUNext"); } if (CurrentPlaybackState == PlaybackState.Stopped) { return; } if (SongHashToIndex.TryGetValue(songHash, out var value)) { CurrentSongIndex = value; PlaySongAtIndex(value); } else { Plugin.Log("[AudioMaster] Song not available for next song, playing white noise", "warning"); MixtapeAudioSource.clip = SoundEffects.WhiteNoise; MixtapeAudioSource.Play(); if (Plugin.MilestonesEnabled) { MilestoneTracker.Instance.ReportProgress("WhiteNoise"); } sPEAKerUI.Instance.ShowToast(songName); float length = SoundEffects.WhiteNoise.length; ((MonoBehaviour)this).StartCoroutine(FadeOut(length / 2f, length / 4f)); } UpdatePlaybackState(PlaybackState.Playing, CurrentSongIndex); if (PhotonNetwork.IsMasterClient && !IsYouTubeMode) { StartAutoSkipMonitoring(); } } public void HandleNetworkLoopStateChange(int loopState) { CurrentLoopState = (LoopState)loopState; if (!PhotonNetwork.IsMasterClient) { switch (CurrentLoopState) { case LoopState.None: sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_LOOP_OFF", true)); break; case LoopState.All: sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_LOOP_ALL", true)); break; case LoopState.One: sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_LOOP_ONE", true)); break; } } AudioMaster.OnLoopStateChanged?.Invoke(CurrentLoopState); Plugin.Log($"[AudioMaster] Network updated loop state: {CurrentLoopState}"); } public void HandleNetworkQueueSync(string[] newQueue) { bool wasEmpty = SongQueue.Count == 0; SongQueue = newQueue.ToList(); Plugin.Log($"[AudioMaster] Synced queue from network. {SongQueue.Count} tracks."); RepointPlayheadAfterRefill(wasEmpty); AudioMaster.OnQueueSynced?.Invoke(); } public static void RepointPlayheadAfterRefill(bool wasEmpty) { if (!wasEmpty || SongQueue.Count == 0 || IsYouTubeMode || CurrentPlaybackState == PlaybackState.Playing) { return; } int valueOrDefault = SongHashToIndex.GetValueOrDefault(SongQueue[0], -1); if (valueOrDefault >= 0) { Plugin.Log($"[AudioMaster] Queue refilled from empty — seating queue head (index {valueOrDefault}) as the current track."); AudioSource mixtapeAudioSource = MixtapeAudioSource; if ((Object)(object)mixtapeAudioSource != (Object)null) { mixtapeAudioSource.Stop(); mixtapeAudioSource.clip = null; } UpdatePlaybackState(PlaybackState.Paused, valueOrDefault); Controller controller = (((Object)(object)TheObserver.Instance != (Object)null) ? TheObserver.Instance.GetControllerInstance() : null); if ((Object)(object)controller != (Object)null) { controller.SeatCurrentTrack(valueOrDefault); } } } public void Master_PlaySongByHash(string songHash) { if (PhotonNetwork.IsMasterClient && SongHashToIndex.TryGetValue(songHash, out var value) && value >= 0 && value < LoadedAudioClips.Count) { string name = ((Object)LoadedAudioClips[value]).name; Netcode.Instance.BroadcastNextSong(songHash, name); } } public static int CountUnplayable(IEnumerable<string> submitted) { return submitted.Count((string hash) => !SongHashToIndex.ContainsKey(hash)); } public void Master_UpdateQueue(List<string> newQueueOrder, bool broadcast = true) { if (PhotonNetwork.IsMasterClient) { List<string> list = newQueueOrder.Where(SongHashToIndex.ContainsKey).ToList(); int num = newQueueOrder.Count - list.Count; if (num > 0) { Plugin.Log($"[AudioMaster] Dropped {num} queue entry/entries the host cannot play.", "warning"); } if (list.Count == 0 && newQueueOrder.Count > 0) { list = IndexToSongHash.Values.ToList(); Plugin.Log("[AudioMaster] Queue request held nothing playable — restored the host library.", "warning"); } bool wasEmpty = SongQueue.Count == 0; SongQueue = list; Plugin.Log($"[AudioMaster] Master updated queue. New count: {SongQueue.Count}"); RepointPlayheadAfterRefill(wasEmpty); if (broadcast) { Netcode.Instance.BroadcastSongQueue(SongQueue.ToArray()); } } } public void Master_ShuffleQueue(bool broadcast = true) { if (PhotonNetwork.IsMasterClient) { Random random = new Random(); SongQueue = SongQueue.OrderBy((string x) => random.Next()).ToList(); Plugin.Log("[AudioMaster] Master shuffled the queue."); sPEAKerUI.Instance.ShowToast(LocalizedText.GetText("TOAST_QUEUE_SHUFFLED", true)); if (broadcast) { Netcode.Instance.BroadcastSongQueue(SongQueue.ToArray()); } } } public void SetFollow(Transform target) { _targetTransform = target; _targetCharacter = null; _isFollowing = (Object)(object)target != (Object)null; if (_isFollowing) { Plugin.Log("[AudioMaster] Set to follow transform: " + ((Object)target).name); } else { Plugin.Log("[AudioMaster] Stopped following."); } } public void SetFollow(Character target) { _targetCharacter = target; _targetTransform = null; _isFollowing = (Object)(object)target != (Object)null; if (_isFollowing) { Plugin.Log("[AudioMaster] Set to follow character: " + ((target != null) ? ((Object)target).name : null)); } else { Plugin.Log("[AudioMaster] Stopped following."); } } public static MixtapeInfo FindMixtapeBySongIndex(int songIndex) { List<MixtapeInfo> availableMixtapes = AvailableMixtapes; int num = 0; int num2 = availableMixtapes.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; MixtapeInfo mixtapeInfo = availableMixtapes[num3]; if (songIndex < mixtapeInfo.FirstSongIndex) { num2 = num3 - 1; continue; } if (songIndex > mixtapeInfo.LastSongIndex) { num = num3 + 1; continue; } return mixtapeInfo; } return null; } public static MixtapeInfo GetMixtapeForSongIndex(int songIndex) { return FindMixtapeBySongIndex(songIndex) ?? new MixtapeInfo { Author = "unknown" }; } public Texture2D GetIconForSongHash(string songHash) { if (SongHashToIndex.TryGetValue(songHash, out var value)) { MixtapeInfo mixtapeInfo = FindMixtapeBySongIndex(value); if (mixtapeInfo != null) { return mixtapeInfo.IconTexture; } } return null; } public static Texture2D GetCurrentMixtapeIcon() { if (LoadedAudioClips.Count == 0) { return null; } return GetMixtapeForSongIndex(CurrentSongIndex)?.IconTexture; } public string GetCurrentMixtapeAuthor() { if (LoadedAudioClips.Count == 0) { return null; } return GetMixtapeForSongIndex(CurrentSongIndex)?.Author; } public static void SetHostLibrary(string[] hashes) { _hostLibrary = new HashSet<string>(hashes ?? Array.Empty<string>()); CanonReceived = true; Plugin.Log($"[AudioMaster] Received host library: {_hostLibrary.Count} track hash(es)."); } public static void ResetCanonTracking() { CanonReceived = false; _hostLibrary.Clear(); } public static MixtapeCanonState GetCanonState(MixtapeInfo mixtape) { if (!PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient) { return MixtapeCanonState.Playable; } List<string> songHashes = mixtape.GetSongHashes(); if (songHashes.Count == 0) { return MixtapeCanonState.LocalOnly; } int num = songHashes.Count(_hostLibrary.Contains); if (num < songHashes.Count) { return (num > 0) ? MixtapeCanonState.Partial : MixtapeCanonState.LocalOnly; } return HostHasUnplayableTracks ? MixtapeCanonState.HostHasMore : MixtapeCanonState.Playable; } public static int CountMissingFromCanon(MixtapeInfo mixtape) { if (!PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient) { return 0; } List<string> songHashes = mixtape.GetSongHashes(); return songHashes.Count - songHashes.Count(_hostLibrary.Contains); } public static List<string> GetAddableHashes(MixtapeInfo mixtape) { bool localIsCanon = !PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient; HashSet<string> queued = new HashSet<string>(SongQueue); return (from hash in mixtape.GetSongHashes() where !queued.Contains(hash) && (localIsCanon || _hostLibrary.Contains(hash)) select hash).ToList(); } public static void AddMixtapeToQueue(MixtapeInfo mixtape) { List<string> addableHashes = GetAddableHashes(mixtape); if (addableHashes.Count == 0) { Plugin.Log("[Library] '" + mixtape.Name + "' has no addable tracks — nothing to append."); return; } Netcode.Instance.SendRequestUpdateQueue(SongQueue.Concat(addableHashes).ToArray()); Plugin.Log($"[Library] Requested append of {addableHashes.Count} track(s) from '{mixtape.Name}'."); sPEAKerUI.Instance.ShowToast(string.Format(LocalizedText.GetText("TOAST_MIXTAPE_ADDED", true), mixtape.Name)); } public static MixtapeQueueState GetQueueState(MixtapeInfo mixtape) { bool localIsCanon = !PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient; List<string> list = (from hash in mixtape.GetSongHashes() where localIsCanon || _hostLibrary.Contains(hash) select hash).ToList(); if (list.Count == 0) { return MixtapeQueueState.NotQueued; } HashSet<string> hashSet = new HashSet<string>(SongQueue); int num = list.Count(hashSet.Contains); if (num == 0) { return MixtapeQueueState.NotQueued; } return (num != list.Count) ? MixtapeQueueState.PartiallyQueued : MixtapeQueueState.FullyQueued; } public static void RemoveMixtapeFromQueue(MixtapeInfo mixtape) { HashSet<string> doomed = new HashSet<string>(mixtape.GetSongHashes()); List<string> list = SongQueue.Where((string hash) => !doomed.Contains(hash)).ToList(); if (list