Decompiled source of sPEAKer v3.0.3

plugins/onlystar.sPEAKer.dll

Decompiled 2 days ago
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