Decompiled source of PlayerRadio v0.2.3

PlayerRadio.dll

Decompiled 2 weeks ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Peak;
using Photon.Pun;
using Photon.Realtime;
using TMPro;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Zorro.ControllerSupport;
using Zorro.Core;
using Zorro.Settings;
using Zorro.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("Inkslinger")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.3.0")]
[assembly: AssemblyInformationalVersion("0.2.3+eb872d1550994cbd7d60ec7c105fe5e5a3b43205")]
[assembly: AssemblyProduct("Player Radio")]
[assembly: AssemblyTitle("PlayerRadio")]
[assembly: AssemblyMetadata("AI_Assisted_Creation", "true")]
[assembly: AssemblyMetadata("AI_Model_Vendor", "Grok Bot")]
[assembly: AssemblyVersion("0.2.3.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace PlayerRadio
{
	public static class AudioLoader
	{
		public static readonly string[] Extensions = new string[3] { ".mp3", ".wav", ".ogg" };

		public static AudioType TypeFor(string ext)
		{
			return (AudioType)((ext ?? "").ToLowerInvariant() switch
			{
				".wav" => 20, 
				".ogg" => 14, 
				".mp3" => 13, 
				_ => 0, 
			});
		}

		public static IEnumerator Load(string path, Action<AudioClip, string> done)
		{
			string absoluteUri;
			try
			{
				absoluteUri = new Uri(Path.GetFullPath(path)).AbsoluteUri;
			}
			catch (Exception ex)
			{
				done(null, "bad path: " + ex.Message);
				yield break;
			}
			UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip(absoluteUri, TypeFor(Path.GetExtension(path)));
			try
			{
				DownloadHandler downloadHandler = req.downloadHandler;
				DownloadHandlerAudioClip val = (DownloadHandlerAudioClip)(object)((downloadHandler is DownloadHandlerAudioClip) ? downloadHandler : null);
				if (val != null)
				{
					val.streamAudio = false;
					val.compressed = false;
				}
				yield return req.SendWebRequest();
				if ((int)req.result != 1)
				{
					done(null, req.error);
					yield break;
				}
				AudioClip val2 = null;
				string text = null;
				try
				{
					val2 = DownloadHandlerAudioClip.GetContent(req);
				}
				catch (Exception ex2)
				{
					text = ex2.Message;
				}
				if ((Object)(object)val2 == (Object)null || val2.length <= 0f)
				{
					if ((Object)(object)val2 != (Object)null)
					{
						Object.Destroy((Object)(object)val2);
					}
					done(null, text ?? "decoded clip is empty (unsupported format?)");
					yield break;
				}
				((Object)val2).name = "PlayerRadio:" + Path.GetFileName(path);
				done(val2, null);
			}
			finally
			{
				((IDisposable)req)?.Dispose();
			}
		}
	}
	public static class Diagnostics
	{
		public static void DumpInputBindings(IEnumerable<Hotkey> ours, string when)
		{
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				InputActionAsset actions = InputSystem.actions;
				if ((Object)(object)actions == (Object)null)
				{
					Plugin.Log.LogWarning((object)("[Bindings/" + when + "] InputSystem.actions is null; cannot list game bindings."));
					return;
				}
				Dictionary<string, List<string>> dictionary = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
				int num = 0;
				Plugin.Log.LogInfo((object)("[Bindings/" + when + "] Game Input System bindings (asset '" + ((Object)actions).name + "'):"));
				Enumerator<InputActionMap> enumerator = actions.actionMaps.GetEnumerator();
				try
				{
					while (enumerator.MoveNext())
					{
						InputActionMap current = enumerator.Current;
						Enumerator<InputAction> enumerator2 = current.actions.GetEnumerator();
						try
						{
							while (enumerator2.MoveNext())
							{
								InputAction current2 = enumerator2.Current;
								List<string> list = new List<string>();
								Enumerator<InputBinding> enumerator3 = current2.bindings.GetEnumerator();
								try
								{
									while (enumerator3.MoveNext())
									{
										InputBinding current3 = enumerator3.Current;
										if (((InputBinding)(ref current3)).isComposite)
										{
											continue;
										}
										string effectivePath = ((InputBinding)(ref current3)).effectivePath;
										if (!string.IsNullOrEmpty(effectivePath))
										{
											string text = (((InputBinding)(ref current3)).isPartOfComposite ? (effectivePath + "(" + ((InputBinding)(ref current3)).name + ")") : effectivePath);
											if (!string.IsNullOrEmpty(((InputBinding)(ref current3)).groups))
											{
												text = text + "[" + ((InputBinding)(ref current3)).groups + "]";
											}
											list.Add(text);
											if (!dictionary.TryGetValue(effectivePath, out var value))
											{
												value = (dictionary[effectivePath] = new List<string>());
											}
											value.Add(current.name + "/" + current2.name);
											num++;
										}
									}
								}
								finally
								{
									((IDisposable)enumerator3/*cast due to .constrained prefix*/).Dispose();
								}
								Plugin.Log.LogInfo((object)("  " + current.name + "/" + current2.name + ": " + ((list.Count == 0) ? "(unbound)" : string.Join(", ", list))));
							}
						}
						finally
						{
							((IDisposable)enumerator2/*cast due to .constrained prefix*/).Dispose();
						}
					}
				}
				finally
				{
					((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose();
				}
				Plugin.Log.LogInfo((object)$"[Bindings/{when}] {num} binding(s) listed.");
				foreach (Hotkey our in ours)
				{
					if (our.Path != null && dictionary.TryGetValue(our.Path, out var value2))
					{
						Plugin.Log.LogWarning((object)("Hotkey " + our.Name + " (" + our.Path + ") is ALSO bound in the game to: " + string.Join(", ", value2.Distinct()) + ". Change it in the config if that's a problem."));
					}
					else
					{
						Plugin.Log.LogInfo((object)("Hotkey " + our.Name + " = " + our.Path + " (no clash with game bindings)."));
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Binding dump failed: " + ex.Message));
			}
		}
	}
	public sealed class FileTransfer
	{
		private sealed class Outgoing
		{
			public int Actor;

			public byte[] Hash;

			public string Hex;

			public Task<byte[]> Read;

			public byte[] Data;

			public int Next;

			public int Count;

			public bool HeaderSent;

			public TrackMeta Meta;

			public double RateBytes;

			public double Tokens;
		}

		private sealed class Incoming
		{
			public byte[] Hash;

			public string Hex;

			public TrackMeta Meta;

			public int OwnerHint;

			public int Holder;

			public readonly HashSet<int> Tried = new HashSet<int>();

			public bool HeaderSeen;

			public long Size;

			public int Count;

			public bool[] Got;

			public int GotCount;

			public FileStream Fs;

			public string PartPath;

			public float LastActivity;

			public Task<string> Verify;

			public float StartedAt;

			public long BytesThisSession;

			public int RequestedKBps;

			public bool Prefetch;
		}

		public const int ChunkSize = 8192;

		public const int MaxDownloads = 2;

		private const byte MsgReq = 1;

		private const byte MsgHdr = 2;

		private const byte MsgChunk = 3;

		private const byte MsgErr = 5;

		private const byte MsgRate = 6;

		private const int MaxOutgoingJobs = 6;

		private const int QueueBackoff = 40;

		private const float StallTimeout = 15f;

		private const float RetryFailedAfter = 30f;

		private static readonly SendOptions BulkSend = new SendOptions
		{
			DeliveryMode = (DeliveryMode)1,
			Channel = 1
		};

		private readonly RadioSession _session;

		private readonly List<Outgoing> _out = new List<Outgoing>();

		private readonly Dictionary<string, Incoming> _in = new Dictionary<string, Incoming>();

		private readonly Dictionary<string, float> _failedAt = new Dictionary<string, float>();

		private int _rr;

		private double _tokens;

		public int DownloadCount => _in.Count;

		public bool CanStartDownload => _in.Count < 2;

		public int UploadCount => _out.Select((Outgoing o) => o.Actor).Distinct().Count();

		public IEnumerable<string> DownloadingHexes => _in.Keys;

		public FileTransfer(RadioSession session)
		{
			_session = session;
		}

		public bool IsDownloading(string hex)
		{
			if (hex != null)
			{
				return _in.ContainsKey(hex);
			}
			return false;
		}

		public bool RecentlyFailed(string hex)
		{
			if (hex != null && _failedAt.TryGetValue(hex, out var value))
			{
				return Time.unscaledTime - value < 30f;
			}
			return false;
		}

		public string StatusFor(string hex)
		{
			if (hex == null || !_in.TryGetValue(hex, out var value))
			{
				return null;
			}
			string text = Util.Trunc(value.Meta?.Title ?? value.Hex.Substring(0, 8), 32);
			if (value.Verify != null)
			{
				return "Verifying '" + text + "'...";
			}
			if (!value.HeaderSeen)
			{
				return "Requesting '" + text + "' from " + _session.PlayerName(value.Holder) + "...";
			}
			float num = ((value.Count == 0) ? 0f : (100f * (float)value.GotCount / (float)value.Count));
			float num2 = Mathf.Max(0.5f, Time.unscaledTime - value.StartedAt);
			return string.Format("{0} '{1}' {2:0}% ({3:0} KB/s from {4})", value.Prefetch ? "Prefetching" : "Downloading", text, num, (float)value.BytesThisSession / 1024f / num2, _session.PlayerName(value.Holder));
		}

		public void Reset()
		{
			_out.Clear();
			foreach (Incoming item in _in.Values.ToList())
			{
				Abort(item, markFailed: false);
			}
			_in.Clear();
			_failedAt.Clear();
		}

		public bool Request(TrackMeta meta, int ownerHint, bool prefetch)
		{
			if (!Plugin.EnableStreaming.Value || meta == null || !CanStartDownload)
			{
				return false;
			}
			string hex = meta.Hex;
			if (_in.ContainsKey(hex) || RecentlyFailed(hex))
			{
				return false;
			}
			if (meta.Size > (long)Plugin.MaxStreamFileMB.Value * 1024L * 1024)
			{
				_failedAt[hex] = Time.unscaledTime;
				Plugin.Log.LogWarning((object)$"Not downloading '{meta.Title}': {(float)meta.Size / 1048576f:0.0} MB exceeds MaxStreamFileMB.");
				return false;
			}
			Incoming incoming = new Incoming
			{
				Hash = meta.Hash,
				Hex = hex,
				Meta = new TrackMeta
				{
					Hash = meta.Hash,
					Title = meta.Title,
					Duration = meta.Duration,
					Size = meta.Size,
					Ext = meta.Ext
				},
				OwnerHint = ownerHint,
				Prefetch = prefetch,
				StartedAt = Time.unscaledTime,
				LastActivity = Time.unscaledTime
			};
			_in[hex] = incoming;
			if (!SendRequestToNextHolder(incoming))
			{
				_in.Remove(hex);
				_failedAt[hex] = Time.unscaledTime;
				return false;
			}
			Rebalance();
			return true;
		}

		private int ShareKBps()
		{
			return Math.Max(8, Plugin.DownloadRateKBps.Value / Math.Max(1, _in.Count));
		}

		private void Rebalance()
		{
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: Expected O, but got Unknown
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			int num = ShareKBps();
			foreach (Incoming value in _in.Values)
			{
				if (value.RequestedKBps != num && value.Holder != 0)
				{
					value.RequestedKBps = num;
					byte[] array = new byte[37];
					array[0] = 6;
					Buffer.BlockCopy(value.Hash, 0, array, 1, 32);
					BitConverter.GetBytes(num).CopyTo(array, 33);
					RaiseEventOptions val = new RaiseEventOptions();
					val.TargetActors = new int[1] { value.Holder };
					PhotonNetwork.RaiseEvent((byte)172, (object)array, val, BulkSend);
				}
			}
		}

		private bool SendRequestToNextHolder(Incoming job)
		{
			//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Expected O, but got Unknown
			//IL_011b: Unknown result type (might be due to invalid IL or missing references)
			List<int> list = (from a in _session.HoldersOf(job.Hex, job.OwnerHint)
				where !job.Tried.Contains(a)
				select a).ToList();
			if (list.Count == 0)
			{
				return false;
			}
			job.Holder = list[0];
			job.Tried.Add(job.Holder);
			job.LastActivity = Time.unscaledTime;
			job.RequestedKBps = ShareKBps();
			int num = FirstMissing(job);
			byte[] array = new byte[41];
			array[0] = 1;
			Buffer.BlockCopy(job.Hash, 0, array, 1, 32);
			BitConverter.GetBytes(num).CopyTo(array, 33);
			BitConverter.GetBytes(job.RequestedKBps).CopyTo(array, 37);
			RaiseEventOptions val = new RaiseEventOptions();
			val.TargetActors = new int[1] { job.Holder };
			bool flag = PhotonNetwork.RaiseEvent((byte)172, (object)array, val, BulkSend);
			Plugin.Log.LogInfo((object)string.Format("Requesting {0}'{1}' ({2}) from {3} (actor {4}), chunk {5}, {6} KB/s (sent={7}).", job.Prefetch ? "(prefetch) " : "", job.Meta?.Title, job.Hex.Substring(0, 12), _session.PlayerName(job.Holder), job.Holder, num, job.RequestedKBps, flag));
			return flag;
		}

		private static int FirstMissing(Incoming j)
		{
			if (j.Got == null)
			{
				return 0;
			}
			for (int i = 0; i < j.Got.Length; i++)
			{
				if (!j.Got[i])
				{
					return i;
				}
			}
			return j.Got.Length;
		}

		private void Abort(Incoming j, bool markFailed)
		{
			try
			{
				j.Fs?.Dispose();
			}
			catch
			{
			}
			j.Fs = null;
			if (markFailed)
			{
				_failedAt[j.Hex] = Time.unscaledTime;
				try
				{
					if (j.PartPath != null && File.Exists(j.PartPath))
					{
						File.Delete(j.PartPath);
					}
				}
				catch
				{
				}
			}
			_in.Remove(j.Hex);
			Rebalance();
		}

		private void RetryOrAbort(Incoming j, string why)
		{
			Plugin.Log.LogInfo((object)$"Transfer of {j.Hex.Substring(0, 12)} from actor {j.Holder}: {why}; trying another holder.");
			try
			{
				j.Fs?.Flush();
			}
			catch
			{
			}
			if (!SendRequestToNextHolder(j))
			{
				Plugin.Log.LogInfo((object)$"Nobody else can send {j.Hex.Substring(0, 12)} right now; will retry in {30f:0}s.");
				Abort(j, markFailed: true);
			}
		}

		private void OnHeader(int sender, Incoming j, BinaryReader r)
		{
			if (sender != j.Holder)
			{
				return;
			}
			long num = r.ReadInt64();
			int num2 = r.ReadInt32();
			int num3 = r.ReadInt32();
			string ext = Util.SanitizeExt(r.ReadString());
			if (num2 != 8192 || num <= 0 || num > (long)Plugin.MaxStreamFileMB.Value * 1024L * 1024 || num3 != (int)((num + 8192 - 1) / 8192))
			{
				Plugin.Log.LogWarning((object)$"Rejecting transfer header from {sender} (size {num}, chunk {num2}, count {num3}).");
				Abort(j, markFailed: true);
				return;
			}
			if (!j.HeaderSeen || j.Size != num)
			{
				try
				{
					j.Fs?.Dispose();
				}
				catch
				{
				}
				j.PartPath = Path.Combine(_session.Library.CacheDir, j.Hex + ".part");
				j.Fs = new FileStream(j.PartPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
				j.Fs.SetLength(num);
				j.Size = num;
				j.Count = num3;
				j.Got = new bool[num3];
				j.GotCount = 0;
			}
			j.Meta.Ext = ext;
			j.HeaderSeen = true;
			j.LastActivity = Time.unscaledTime;
			Plugin.Verbose($"Transfer header: {j.Hex.Substring(0, 12)} {num} bytes in {num3} chunks from {sender}.");
		}

		private void OnChunk(int sender, Incoming j, byte[] payload, int offset)
		{
			if (!j.HeaderSeen || sender != j.Holder || j.Verify != null)
			{
				return;
			}
			int num = BitConverter.ToInt32(payload, offset);
			int num2 = payload.Length - offset - 4;
			if (num < 0 || num >= j.Count || num2 <= 0 || num2 > 8192 || j.Got[num])
			{
				return;
			}
			j.Fs.Seek((long)num * 8192L, SeekOrigin.Begin);
			j.Fs.Write(payload, offset + 4, num2);
			j.Got[num] = true;
			j.GotCount++;
			j.BytesThisSession += num2;
			j.LastActivity = Time.unscaledTime;
			if (j.GotCount != j.Count)
			{
				return;
			}
			j.Fs.Flush();
			j.Fs.Dispose();
			j.Fs = null;
			string part = j.PartPath;
			j.Verify = Task.Run(delegate
			{
				using SHA256 sHA = SHA256.Create();
				using FileStream inputStream = File.OpenRead(part);
				return Util.Hex(sHA.ComputeHash(inputStream));
			});
		}

		private void FinishVerify(Incoming j)
		{
			string text = (j.Verify.IsFaulted ? null : j.Verify.Result);
			if (!string.Equals(text, j.Hex, StringComparison.OrdinalIgnoreCase))
			{
				Plugin.Log.LogWarning((object)("Downloaded track " + j.Hex.Substring(0, 12) + " failed SHA-256 check (got " + text + "); discarding."));
				Abort(j, markFailed: true);
				return;
			}
			string text2 = _session.Library.CachePathFor(j.Hex, j.Meta.Ext);
			try
			{
				if (File.Exists(text2))
				{
					File.Delete(text2);
				}
				File.Move(j.PartPath, text2);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Could not move download into cache: " + ex.Message));
				Abort(j, markFailed: true);
				return;
			}
			float num = Time.unscaledTime - j.StartedAt;
			Plugin.Log.LogInfo((object)$"Downloaded '{j.Meta.Title}' ({j.Size / 1024} KB) in {num:0.0}s from actor {j.Holder}, SHA-256 verified -> {text2}");
			_in.Remove(j.Hex);
			Rebalance();
			_session.Library.RegisterCached(j.Meta, text2);
			_session.Library.EnforceCacheLimit(_session.HashesInUse());
			_session.OnDownloadComplete(j.Hex, j.Meta.Title);
		}

		private void Refuse(int sender, byte[] hash, string hex, string reason)
		{
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Expected O, but got Unknown
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			Plugin.Log.LogInfo((object)$"Refusing track request {hex.Substring(0, 12)} from actor {sender}: {reason}.");
			byte[] array = new byte[33];
			array[0] = 5;
			Buffer.BlockCopy(hash, 0, array, 1, 32);
			RaiseEventOptions val = new RaiseEventOptions();
			val.TargetActors = new int[1] { sender };
			PhotonNetwork.RaiseEvent((byte)172, (object)array, val, BulkSend);
		}

		private void OnRequest(int sender, byte[] hash, BinaryReader r)
		{
			int val = r.ReadInt32();
			int requestedKBps = r.ReadInt32();
			string hex = Util.Hex(hash);
			string text = null;
			string path;
			if (!Plugin.EnableStreaming.Value)
			{
				text = "streaming disabled";
			}
			else if (!_session.IsCompatiblePeer(sender))
			{
				text = "sender is not running a compatible Player Radio";
			}
			else if (!_session.Library.TryGetLocalPath(hex, out path))
			{
				text = "not found";
			}
			else if (_out.Count >= 6 && !_out.Any((Outgoing o) => o.Actor == sender && o.Hex == hex))
			{
				text = "busy";
			}
			TrackMeta trackMeta = _session.Library.Get(hex);
			if (text == null && trackMeta != null && trackMeta.Size > (long)Plugin.MaxStreamFileMB.Value * 1024L * 1024)
			{
				text = "file too large";
			}
			if (text != null)
			{
				Refuse(sender, hash, hex, text);
				return;
			}
			_out.RemoveAll((Outgoing o) => o.Actor == sender && o.Hex == hex);
			_session.Library.TryGetLocalPath(hex, out var path2);
			Outgoing outgoing = new Outgoing
			{
				Actor = sender,
				Hash = hash,
				Hex = hex,
				Meta = trackMeta,
				Next = Math.Max(0, val)
			};
			SetRate(outgoing, requestedKBps);
			outgoing.Read = Task.Run(() => File.ReadAllBytes(path2));
			_out.Add(outgoing);
			Plugin.Log.LogInfo((object)$"Sending '{trackMeta?.Title}' to {_session.PlayerName(sender)} (actor {sender}) from chunk {outgoing.Next} at <= {outgoing.RateBytes / 1024.0:0} KB/s.");
		}

		private static void SetRate(Outgoing job, int requestedKBps)
		{
			int value = Plugin.StreamRateKBps.Value;
			int val = ((requestedKBps <= 0) ? value : Math.Min(requestedKBps, value));
			job.RateBytes = (double)Math.Max(8, val) * 1024.0;
		}

		private void PumpOutgoing(float dt)
		{
			//IL_01d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01db: Expected O, but got Unknown
			//IL_0382: Unknown result type (might be due to invalid IL or missing references)
			//IL_0284: Unknown result type (might be due to invalid IL or missing references)
			if (_out.Count == 0)
			{
				_tokens = 0.0;
				return;
			}
			double num = (double)Plugin.StreamRateKBps.Value * 1024.0;
			_tokens = Math.Min(_tokens + num * (double)dt, 16384.0);
			foreach (Outgoing item in _out)
			{
				item.Tokens = Math.Min(item.Tokens + item.RateBytes * (double)dt, 16384.0);
			}
			int num2 = 0;
			int num3 = 0;
			while (_out.Count > 0 && _tokens >= 8192.0 && num2++ < 16 && num3 < _out.Count)
			{
				LoadBalancingClient networkingClient = PhotonNetwork.NetworkingClient;
				LoadBalancingPeer val = ((networkingClient != null) ? networkingClient.LoadBalancingPeer : null);
				if (val != null && ((PhotonPeer)val).QueuedOutgoingCommands > 40)
				{
					break;
				}
				_rr = (_rr + 1) % _out.Count;
				Outgoing outgoing = _out[_rr];
				if (outgoing.Data == null)
				{
					if (!outgoing.Read.IsCompleted)
					{
						num3++;
						continue;
					}
					if (outgoing.Read.IsFaulted)
					{
						Plugin.Log.LogWarning((object)("Read failed for " + outgoing.Hex + ": " + outgoing.Read.Exception?.GetBaseException().Message));
						_out.RemoveAt(_rr);
						continue;
					}
					outgoing.Data = outgoing.Read.Result;
					outgoing.Count = (outgoing.Data.Length + 8192 - 1) / 8192;
				}
				if (outgoing.Tokens < 8192.0)
				{
					num3++;
					continue;
				}
				num3 = 0;
				RaiseEventOptions val2 = new RaiseEventOptions();
				val2.TargetActors = new int[1] { outgoing.Actor };
				RaiseEventOptions val3 = val2;
				if (!outgoing.HeaderSent)
				{
					using (MemoryStream memoryStream = new MemoryStream())
					{
						using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
						binaryWriter.Write((byte)2);
						binaryWriter.Write(outgoing.Hash);
						binaryWriter.Write((long)outgoing.Data.Length);
						binaryWriter.Write(8192);
						binaryWriter.Write(outgoing.Count);
						binaryWriter.Write(outgoing.Meta?.Ext ?? ".mp3");
						PhotonNetwork.RaiseEvent((byte)172, (object)memoryStream.ToArray(), val3, BulkSend);
					}
					outgoing.HeaderSent = true;
				}
				if (outgoing.Next >= outgoing.Count)
				{
					Plugin.Log.LogInfo((object)$"Finished sending {outgoing.Hex.Substring(0, 12)} to actor {outgoing.Actor}.");
					_out.RemoveAt(_rr);
					continue;
				}
				int num4 = outgoing.Next * 8192;
				int num5 = Math.Min(8192, outgoing.Data.Length - num4);
				byte[] array = new byte[37 + num5];
				array[0] = 3;
				Buffer.BlockCopy(outgoing.Hash, 0, array, 1, 32);
				BitConverter.GetBytes(outgoing.Next).CopyTo(array, 33);
				Buffer.BlockCopy(outgoing.Data, num4, array, 37, num5);
				if (!PhotonNetwork.RaiseEvent((byte)172, (object)array, val3, BulkSend))
				{
					break;
				}
				outgoing.Next++;
				_tokens -= num5;
				outgoing.Tokens -= num5;
			}
		}

		public void Tick(float dt)
		{
			if (!PhotonNetwork.InRoom || PhotonNetwork.OfflineMode)
			{
				if (_out.Count > 0 || _in.Count > 0)
				{
					Reset();
				}
				return;
			}
			PumpOutgoing(dt);
			foreach (Incoming item in _in.Values.ToList())
			{
				if (item.Verify != null)
				{
					if (item.Verify.IsCompleted)
					{
						FinishVerify(item);
					}
				}
				else if (Time.unscaledTime - item.LastActivity > 15f)
				{
					RetryOrAbort(item, "stalled");
				}
			}
		}

		public void Cancel(string hex)
		{
			if (hex != null && _in.TryGetValue(hex, out var value) && value.Verify == null)
			{
				Plugin.Verbose("Cancelling download " + hex.Substring(0, 12) + " (no longer needed).");
				Abort(value, markFailed: false);
			}
		}

		public void OnPlayerLeft(int actor)
		{
			_out.RemoveAll((Outgoing o) => o.Actor == actor);
			foreach (Incoming item in _in.Values.Where((Incoming j) => j.Holder == actor && j.Verify == null).ToList())
			{
				RetryOrAbort(item, "sender left");
			}
		}

		public void OnEvent(int sender, byte[] data)
		{
			if (data == null || data.Length < 33)
			{
				return;
			}
			byte b = data[0];
			byte[] array = new byte[32];
			Buffer.BlockCopy(data, 1, array, 0, 32);
			string text = Util.Hex(array);
			try
			{
				switch (b)
				{
				case 1:
				{
					using BinaryReader r2 = new BinaryReader(new MemoryStream(data, 33, data.Length - 33));
					OnRequest(sender, array, r2);
					break;
				}
				case 6:
					if (data.Length < 37)
					{
						break;
					}
					{
						foreach (Outgoing item in _out)
						{
							if (item.Actor == sender && item.Hex == text)
							{
								SetRate(item, BitConverter.ToInt32(data, 33));
							}
						}
						break;
					}
				case 2:
				{
					if (_in.TryGetValue(text, out var value2))
					{
						using (BinaryReader r = new BinaryReader(new MemoryStream(data, 33, data.Length - 33)))
						{
							OnHeader(sender, value2, r);
							break;
						}
					}
					break;
				}
				case 3:
				{
					if (_in.TryGetValue(text, out var value3))
					{
						OnChunk(sender, value3, data, 33);
					}
					break;
				}
				case 5:
				{
					if (_in.TryGetValue(text, out var value) && value.Holder == sender && value.Verify == null)
					{
						RetryOrAbort(value, "sender refused");
					}
					break;
				}
				case 4:
					break;
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)$"Bad transfer message {b} from actor {sender}: {ex.Message}");
			}
		}
	}
	public sealed class GameMusicDucker : MonoBehaviour
	{
		private sealed class Candidate
		{
			public AudioSource Src;

			public string Via;

			public bool KnownField;

			public bool MusicGroup;

			public bool Tagged;

			public HashSet<AudioClip> MusicClips;

			public MusicDuckFilter Filter;

			public bool? Decision;

			public string DecidedClip;

			public string Reason;
		}

		private const float FadeOutSeconds = 1.5f;

		private const float FadeInSeconds = 2f;

		private const float RescanInterval = 3f;

		private const float GapGraceSeconds = 3f;

		private const float EvalInterval = 0.25f;

		internal static volatile float SharedGain = 1f;

		private float _progress;

		private float _nextScan;

		private float _nextEval;

		private float _lastAudible = -100f;

		private bool _wasWanted;

		private float _lastTarget;

		private string _lastWhy;

		private readonly List<MusicDuckFilter> _filters = new List<MusicDuckFilter>();

		private readonly Dictionary<AudioSource, Candidate> _cands = new Dictionary<AudioSource, Candidate>();

		private string _excludeRaw;

		private string[] _exclude = new string[0];

		public static GameMusicDucker Instance { get; private set; }

		public bool IsDucking => _progress > 0.001f;

		public int FilterCount => _filters.Count;

		private void Awake()
		{
			Instance = this;
			SceneManager.sceneLoaded += OnSceneLoaded;
		}

		private void OnSceneLoaded(Scene s, LoadSceneMode m)
		{
			_nextScan = 0f;
		}

		private void OnDestroy()
		{
			SceneManager.sceneLoaded -= OnSceneLoaded;
			RestoreAll("plugin unload");
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
		}

		private void OnApplicationQuit()
		{
			RestoreAll(null);
		}

		public void RestoreAll(string why)
		{
			SharedGain = 1f;
			_progress = 0f;
			int num = 0;
			foreach (MusicDuckFilter filter in _filters)
			{
				if (!((Object)(object)filter == (Object)null))
				{
					Object.Destroy((Object)(object)filter);
					num++;
				}
			}
			_filters.Clear();
			_cands.Clear();
			if (why != null && num > 0)
			{
				Plugin.Log.LogInfo((object)$"Game music restored ({why}); removed {num} duck filter(s).");
			}
		}

		private void Update()
		{
			try
			{
				Tick();
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Game music ducker error: {arg}");
				SharedGain = 1f;
			}
		}

		private void Tick()
		{
			float num = Mathf.Min(Time.unscaledDeltaTime, 0.1f);
			string why = null;
			float num2 = (Plugin.DuckGameMusic.Value ? RadioDuckTarget(out why) : 0f);
			bool flag = num2 > 0.001f;
			if (flag && !_wasWanted)
			{
				_nextScan = 0f;
				Plugin.Verbose($"Game music: ducking ({why}, depth {num2:0.00}).");
			}
			else if (!flag && _wasWanted)
			{
				Plugin.Verbose("Game music: restoring (no radio audible).");
			}
			_wasWanted = flag;
			if (_progress < num2)
			{
				_progress = Mathf.Min(num2, _progress + num / 1.5f);
			}
			else if (_progress > num2)
			{
				_progress = Mathf.Max(num2, _progress - num / 2f);
			}
			float num3 = Mathf.Clamp01(Plugin.GameMusicDuckVolume.Value);
			float num4 = _progress * _progress * (3f - 2f * _progress);
			SharedGain = Mathf.Lerp(1f, num3, num4);
			if ((flag || _progress > 0f) && Time.unscaledTime >= _nextScan)
			{
				_nextScan = Time.unscaledTime + 3f;
				Scan();
			}
			if ((flag || _progress > 0f) && Time.unscaledTime >= _nextEval)
			{
				_nextEval = Time.unscaledTime + 0.25f;
				Evaluate(summary: false);
			}
		}

		private float RadioDuckTarget(out string why)
		{
			why = null;
			RadioSession instance = RadioSession.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return 0f;
			}
			float num = instance.DuckTarget(out why);
			if (num > 0.001f)
			{
				_lastAudible = Time.unscaledTime;
				_lastTarget = num;
				_lastWhy = why;
				return num;
			}
			if (Time.unscaledTime - _lastAudible < 3f)
			{
				why = _lastWhy;
				return _lastTarget;
			}
			return 0f;
		}

		private static bool IsRadio(AudioSource s)
		{
			return (Object)(object)((Component)s).GetComponentInParent<RadioEmitter>(true) != (Object)null;
		}

		private void Scan()
		{
			_filters.RemoveAll((MusicDuckFilter f) => (Object)(object)f == (Object)null);
			foreach (AudioSource item in _cands.Keys.Where((AudioSource k) => (Object)(object)k == (Object)null).ToList())
			{
				_cands.Remove(item);
			}
			HashSet<AudioMixerGroup> hashSet = FindMusicGroups();
			Dictionary<AudioSource, Candidate> found = new Dictionary<AudioSource, Candidate>();
			AmbienceAudio[] array = Object.FindObjectsByType<AmbienceAudio>((FindObjectsInactive)1, (FindObjectsSortMode)0);
			AudioSource[] array2;
			foreach (AmbienceAudio val in array)
			{
				HashSet<AudioClip> musicClips = AmbienceMusicClips(val);
				Known(val.mainMusic, "AmbienceAudio.mainMusic");
				Known(val.stingerSource, "AmbienceAudio.stingerSource");
				array2 = (AudioSource[])(object)new AudioSource[2] { val.mainMusic, val.stingerSource };
				foreach (AudioSource val2 in array2)
				{
					if ((Object)(object)val2 != (Object)null && found.TryGetValue(val2, out var value))
					{
						value.MusicClips = musicClips;
					}
				}
			}
			MyresAmbience[] array3 = Object.FindObjectsByType<MyresAmbience>((FindObjectsInactive)1, (FindObjectsSortMode)0);
			for (int num = 0; num < array3.Length; num++)
			{
				Known(array3[num].fearMusic, "MyresAmbience.fearMusic");
			}
			ScoutmasterSoulPillar[] array4 = Object.FindObjectsByType<ScoutmasterSoulPillar>((FindObjectsInactive)1, (FindObjectsSortMode)0);
			for (int num = 0; num < array4.Length; num++)
			{
				Known(array4[num].music, "ScoutmasterSoulPillar.music");
			}
			array2 = Object.FindObjectsByType<AudioSource>((FindObjectsInactive)1, (FindObjectsSortMode)0);
			foreach (AudioSource val3 in array2)
			{
				if ((Object)(object)val3 == (Object)null || IsRadio(val3))
				{
					continue;
				}
				AudioMixerGroup outputAudioMixerGroup = val3.outputAudioMixerGroup;
				bool flag = (Object)(object)outputAudioMixerGroup != (Object)null && hashSet.Contains(outputAudioMixerGroup);
				bool flag2 = false;
				try
				{
					flag2 = ((Component)val3).gameObject.CompareTag("Music");
				}
				catch
				{
				}
				if (flag || flag2)
				{
					Candidate candidate = Get(val3);
					if (flag)
					{
						candidate.MusicGroup = true;
						candidate.Via = candidate.Via ?? ("mixer group '" + ((Object)outputAudioMixerGroup).name + "'");
					}
					if (flag2)
					{
						candidate.Tagged = true;
						candidate.Via = candidate.Via ?? "tag 'Music'";
					}
				}
			}
			int num3 = 0;
			foreach (KeyValuePair<AudioSource, Candidate> item2 in found)
			{
				if (_cands.ContainsKey(item2.Key))
				{
					continue;
				}
				GameObject gameObject = ((Component)item2.Key).gameObject;
				if (!((Object)(object)gameObject == (Object)null) && !((Object)(object)gameObject.GetComponent<AudioListener>() != (Object)null))
				{
					MusicDuckFilter musicDuckFilter = gameObject.GetComponent<MusicDuckFilter>();
					if ((Object)(object)musicDuckFilter == (Object)null)
					{
						musicDuckFilter = gameObject.AddComponent<MusicDuckFilter>();
					}
					if (!_filters.Contains(musicDuckFilter))
					{
						_filters.Add(musicDuckFilter);
					}
					item2.Value.Filter = musicDuckFilter;
					_cands[item2.Key] = item2.Value;
					num3++;
				}
			}
			if (num3 > 0)
			{
				Evaluate(summary: true);
			}
			Candidate Get(AudioSource s)
			{
				if ((Object)(object)s == (Object)null || IsRadio(s))
				{
					return null;
				}
				if (!found.TryGetValue(s, out var value2))
				{
					Dictionary<AudioSource, Candidate> dictionary = found;
					Candidate obj2 = new Candidate
					{
						Src = s
					};
					value2 = obj2;
					dictionary[s] = obj2;
				}
				return value2;
			}
			void Known(AudioSource s, string via)
			{
				Candidate candidate2 = Get(s);
				if (candidate2 != null)
				{
					candidate2.KnownField = true;
					candidate2.Via = candidate2.Via ?? via;
				}
			}
		}

		private void Evaluate(bool summary)
		{
			//IL_0295: Unknown result type (might be due to invalid IL or missing references)
			//IL_029a: Unknown result type (might be due to invalid IL or missing references)
			RefreshExcludes();
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			Dictionary<MusicDuckFilter, bool> dictionary = new Dictionary<MusicDuckFilter, bool>();
			foreach (Candidate value2 in _cands.Values)
			{
				if ((Object)(object)value2.Src == (Object)null || (Object)(object)value2.Filter == (Object)null)
				{
					continue;
				}
				string text = (((Object)(object)value2.Src.clip != (Object)null) ? ((Object)value2.Src.clip).name : null);
				if (!value2.Decision.HasValue || value2.DecidedClip != text)
				{
					string reason;
					bool flag = Decide(value2, text, out reason);
					string text2 = "'" + ((Object)((Component)value2.Src).gameObject).name + "' via " + value2.Via + ((text != null) ? (" (clip '" + text + "')") : " (no clip)");
					if (summary || !value2.Decision.HasValue)
					{
						(flag ? list : list2).Add(flag ? text2 : (text2 + ": " + reason));
					}
					else if (value2.Decision != flag)
					{
						Plugin.Log.LogInfo((object)("Game music ducking: " + (flag ? "now ducking" : "no longer ducking") + " " + text2 + (flag ? "" : (": " + reason))));
					}
					value2.Decision = flag;
					value2.DecidedClip = text;
					value2.Reason = reason;
				}
				dictionary.TryGetValue(value2.Filter, out var value);
				dictionary[value2.Filter] = value || value2.Decision == true;
			}
			foreach (KeyValuePair<MusicDuckFilter, bool> item in dictionary)
			{
				item.Key.Active = item.Value;
			}
			if (list.Count + list2.Count > 0)
			{
				ManualLogSource log = Plugin.Log;
				string[] obj = new string[8] { "Game music ducking in '", null, null, null, null, null, null, null };
				Scene activeScene = SceneManager.GetActiveScene();
				obj[1] = ((Scene)(ref activeScene)).name;
				obj[2] = "': ";
				obj[3] = $"ducked {list.Count}";
				obj[4] = ((list.Count > 0) ? (" [" + string.Join("; ", list) + "]") : "");
				obj[5] = $", skipped {list2.Count}";
				obj[6] = ((list2.Count > 0) ? (" [" + string.Join("; ", list2) + "]") : "");
				obj[7] = ".";
				log.LogInfo((object)string.Concat(obj));
			}
		}

		private bool Decide(Candidate c, string clip, out string reason)
		{
			string text = ((Object)((Component)c.Src).gameObject).name ?? "";
			string[] exclude = _exclude;
			foreach (string text2 in exclude)
			{
				if (text.IndexOf(text2, StringComparison.OrdinalIgnoreCase) >= 0)
				{
					reason = "name matches ExcludeFromDucking '" + text2 + "'";
					return false;
				}
				if (clip != null && clip.IndexOf(text2, StringComparison.OrdinalIgnoreCase) >= 0)
				{
					reason = "clip matches ExcludeFromDucking '" + text2 + "'";
					return false;
				}
			}
			if (c.MusicClips != null)
			{
				if (clip == null)
				{
					reason = "known music source (no clip yet)";
					return true;
				}
				bool flag = c.MusicClips.Any((AudioClip m) => (Object)(object)m != (Object)null && ((Object)m).name == clip);
				reason = (flag ? "AmbienceAudio music stinger" : "clip is not one of AmbienceAudio's music clips (placeholder/SFX)");
				return flag;
			}
			if (clip != null && clip.StartsWith("Au_", StringComparison.OrdinalIgnoreCase))
			{
				reason = "sound-effect clip (Au_ prefix)";
				return false;
			}
			if (c.KnownField || c.MusicGroup)
			{
				reason = (c.KnownField ? "known music source" : "routed to the music mixer group");
				return true;
			}
			bool flag2 = text.IndexOf("music", StringComparison.OrdinalIgnoreCase) >= 0 || (clip != null && clip.IndexOf("music", StringComparison.OrdinalIgnoreCase) >= 0);
			reason = (flag2 ? "tagged Music and named like music" : "tagged Music but name/clip don't look like music");
			return flag2;
		}

		private static HashSet<AudioClip> AmbienceMusicClips(AmbienceAudio a)
		{
			HashSet<AudioClip> set = new HashSet<AudioClip>();
			AddAll(a.startStinger);
			AddAll(a.sunRiseStinger);
			AddAll(a.sunSetStinger);
			AddAll(a.nightStinger);
			Add(a.climbStingerBeach);
			Add(a.climbStingerTropics);
			Add(a.climbStingerRoots);
			Add(a.climbStingerAlpine);
			Add(a.climbStingerMesa);
			Add(a.climbStingerCaldera);
			Add(a.climbStingerGloom);
			Add(a.climbStingerKiln);
			Add(a.climbStingerCitadel);
			Add(a.climbStingerNadir);
			Add(a.climbStingerPeak);
			Add(a.tombClimb);
			Add(a.tropicsSunrise);
			Add(a.tropicsSunset);
			Add(a.rootsSunrise);
			Add(a.rootsSunset);
			Add(a.alpineSunrise);
			Add(a.alpineSunset);
			Add(a.desertSunrise);
			Add(a.desertSunset);
			return set;
			void Add(AudioClip c)
			{
				if ((Object)(object)c != (Object)null)
				{
					set.Add(c);
				}
			}
			void AddAll(AudioClip[] arr)
			{
				if (arr != null)
				{
					for (int i = 0; i < arr.Length; i++)
					{
						Add(arr[i]);
					}
				}
			}
		}

		private void RefreshExcludes()
		{
			string text = Plugin.ExcludeFromDucking.Value ?? "";
			if (text == _excludeRaw)
			{
				return;
			}
			_excludeRaw = text;
			_exclude = (from x in text.Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
				select x.Trim() into x
				where x.Length > 0
				select x).ToArray();
			foreach (Candidate value in _cands.Values)
			{
				value.Decision = null;
			}
		}

		private static HashSet<AudioMixerGroup> FindMusicGroups()
		{
			HashSet<AudioMixerGroup> hashSet = new HashSet<AudioMixerGroup>();
			try
			{
				AudioMixerGroup val = (((Object)(object)SingletonAsset<StaticReferences>.Instance != (Object)null) ? SingletonAsset<StaticReferences>.Instance.masterMixerGroup : null);
				AudioMixer val2 = (((Object)(object)val != (Object)null) ? val.audioMixer : null);
				if ((Object)(object)val2 == (Object)null)
				{
					return hashSet;
				}
				AudioMixerGroup[] array = (AudioMixerGroup[])(((object)val2.FindMatchingGroups(string.Empty)) ?? ((object)new AudioMixerGroup[0]));
				foreach (AudioMixerGroup val3 in array)
				{
					if ((Object)(object)val3 != (Object)null && (((Object)val3).name == "Music" || ((Object)val3).name == "Music_Setting"))
					{
						hashSet.Add(val3);
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Verbose("Music group lookup failed: " + ex.Message);
			}
			return hashSet;
		}
	}
	public sealed class MusicDuckFilter : MonoBehaviour
	{
		private float _current = 1f;

		internal volatile bool Active;

		private void OnAudioFilterRead(float[] data, int channels)
		{
			float num = (Active ? GameMusicDucker.SharedGain : 1f);
			float current = _current;
			if (current >= 0.9999f && num >= 0.9999f)
			{
				_current = 1f;
			}
			else
			{
				if (channels <= 0)
				{
					return;
				}
				int num2 = data.Length / channels;
				if (num2 == 0)
				{
					return;
				}
				float num3 = (num - current) / (float)num2;
				float num4 = current;
				int num5 = 0;
				for (int i = 0; i < num2; i++)
				{
					num4 += num3;
					int num6 = 0;
					while (num6 < channels)
					{
						data[num5] *= num4;
						num6++;
						num5++;
					}
				}
				_current = num;
			}
		}
	}
	public sealed class Hotkey
	{
		public readonly string Name;

		private readonly ConfigEntry<string> _entry;

		private string _resolvedPath;

		private ButtonControl _control;

		private float _nextResolve;

		private bool _warned;

		public string Path => _entry.Value;

		public string Display
		{
			get
			{
				Resolve();
				if (_control != null && !string.IsNullOrEmpty(((InputControl)_control).displayName))
				{
					return ((InputControl)_control).displayName;
				}
				string text = _entry.Value ?? "";
				int num = text.LastIndexOf('/');
				return ((num >= 0) ? text.Substring(num + 1) : text).ToUpperInvariant();
			}
		}

		public Hotkey(string name, ConfigEntry<string> entry)
		{
			Name = name;
			_entry = entry;
		}

		private void Resolve()
		{
			bool num = _entry.Value != _resolvedPath;
			bool flag = _control != null && (((InputControl)_control).device == null || !((InputControl)_control).device.added);
			if (num || flag || (_control == null && !(Time.unscaledTime < _nextResolve)))
			{
				_resolvedPath = _entry.Value;
				_nextResolve = Time.unscaledTime + 5f;
				_control = (ButtonControl)(string.IsNullOrWhiteSpace(_resolvedPath) ? null : /*isinst with value type is only supported in some contexts*/);
				if (_control == null && !_warned && !string.IsNullOrWhiteSpace(_resolvedPath))
				{
					_warned = true;
					Plugin.Log.LogWarning((object)("Hotkey " + Name + ": control path '" + _resolvedPath + "' not found (no such key/device yet). Example: <Keyboard>/f5"));
				}
				else if (_control != null)
				{
					_warned = false;
					Plugin.Verbose("Hotkey " + Name + " -> " + ((InputControl)_control).path);
				}
			}
		}

		public bool Pressed()
		{
			Resolve();
			if (_control != null)
			{
				return _control.wasPressedThisFrame;
			}
			return false;
		}
	}
	public static class MixerRouting
	{
		private static bool _logged;

		private static readonly string[] KnownParams = new string[6] { "MasterVolume", "MusicVolume", "SFXVolume", "VoiceVolume", "EffectsStrength", "LoadingFade" };

		public static string ChosenDescription { get; private set; } = "(not resolved yet)";

		public static AudioMixerGroup Resolve()
		{
			try
			{
				StaticReferences instance = SingletonAsset<StaticReferences>.Instance;
				AudioMixerGroup val = (((Object)(object)instance != (Object)null) ? instance.masterMixerGroup : null);
				AudioMixer val2 = (((Object)(object)val != (Object)null) ? val.audioMixer : null);
				if ((Object)(object)val2 == (Object)null)
				{
					AudioMixerGroup val3 = (((Object)(object)SFX_Player.instance != (Object)null) ? SFX_Player.instance.defaultMixerGroup : null);
					if ((Object)(object)val3 != (Object)null)
					{
						Chosen(val3, "SFX_Player.defaultMixerGroup (master mixer not found)");
						return val3;
					}
					return null;
				}
				AudioMixerGroup[] array = (AudioMixerGroup[])(((object)val2.FindMatchingGroups(string.Empty)) ?? ((object)new AudioMixerGroup[0]));
				if (!_logged)
				{
					_logged = true;
					Plugin.Log.LogInfo((object)($"Audio mixer '{((Object)val2).name}' (master group '{((Object)val).name}') has {array.Length} group(s): " + string.Join(", ", array.Select((AudioMixerGroup g) => ((Object)g).name))));
					string[] knownParams = KnownParams;
					float num2 = default(float);
					foreach (string text in knownParams)
					{
						if (val2.GetFloat(text, ref num2))
						{
							Plugin.Log.LogInfo((object)$"  exposed param {text} = {num2:0.0} dB");
						}
						else
						{
							Plugin.Log.LogInfo((object)("  exposed param " + text + ": not found"));
						}
					}
				}
				string want = (Plugin.MixerGroupName.Value ?? "").Trim();
				if (want.Length > 0)
				{
					AudioMixerGroup val4 = ((IEnumerable<AudioMixerGroup>)array).FirstOrDefault((Func<AudioMixerGroup, bool>)((AudioMixerGroup g) => string.Equals(((Object)g).name, want, StringComparison.OrdinalIgnoreCase))) ?? ((IEnumerable<AudioMixerGroup>)array).FirstOrDefault((Func<AudioMixerGroup, bool>)((AudioMixerGroup g) => ((Object)g).name.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0));
					if ((Object)(object)val4 != (Object)null)
					{
						Chosen(val4, "mixer group '" + ((Object)val4).name + "' (matched config '" + want + "')");
						return val4;
					}
				}
				AudioMixerGroup val5 = (((Object)(object)SFX_Player.instance != (Object)null) ? SFX_Player.instance.defaultMixerGroup : null);
				if ((Object)(object)val5 != (Object)null)
				{
					Chosen(val5, "SFX_Player.defaultMixerGroup '" + ((Object)val5).name + "' (fallback: no group named '" + want + "')");
					return val5;
				}
				Chosen(val, "master group '" + ((Object)val).name + "' (fallback)");
				return val;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Mixer lookup failed (will retry): " + ex.Message));
				return null;
			}
		}

		private static void Chosen(AudioMixerGroup g, string desc)
		{
			if (ChosenDescription != desc)
			{
				Plugin.Log.LogInfo((object)("Radio audio routed to " + desc));
			}
			ChosenDescription = desc;
		}
	}
	public sealed class TrackMeta
	{
		public byte[] Hash;

		public string Title;

		public float Duration;

		public long Size;

		public string Ext;

		private string _hex;

		public string Hex => _hex ?? (_hex = Util.Hex(Hash));
	}
	public sealed class RadioState
	{
		private const byte Format = 2;

		public int Rev;

		public TrackMeta Current;

		public TrackMeta Next;

		public bool Playing;

		public double AnchorTime;

		public float AnchorPos;

		public string CurrentHex => Current?.Hex;

		public float PositionAt(double now)
		{
			if (!Playing)
			{
				return AnchorPos;
			}
			double num = now - AnchorTime;
			if (num < 0.0 || num > 86400.0)
			{
				num = 0.0;
			}
			return AnchorPos + (float)num;
		}

		public RadioState Clone()
		{
			return (RadioState)MemberwiseClone();
		}

		public byte[] Serialize()
		{
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
			binaryWriter.Write((byte)2);
			binaryWriter.Write(Rev);
			binaryWriter.Write(Playing);
			binaryWriter.Write(AnchorTime);
			binaryWriter.Write(AnchorPos);
			binaryWriter.Write(Current != null);
			if (Current != null)
			{
				Util.WriteMeta(binaryWriter, Current);
			}
			binaryWriter.Write(Next != null);
			if (Next != null)
			{
				Util.WriteMeta(binaryWriter, Next);
			}
			return memoryStream.ToArray();
		}

		public static RadioState Deserialize(byte[] data)
		{
			using BinaryReader binaryReader = new BinaryReader(new MemoryStream(data));
			if (binaryReader.ReadByte() != 2)
			{
				throw new InvalidDataException("Unknown radio state format (different Player Radio version?)");
			}
			RadioState radioState = new RadioState
			{
				Rev = binaryReader.ReadInt32(),
				Playing = binaryReader.ReadBoolean(),
				AnchorTime = binaryReader.ReadDouble(),
				AnchorPos = binaryReader.ReadSingle()
			};
			if (binaryReader.ReadBoolean())
			{
				radioState.Current = Util.ReadMeta(binaryReader);
			}
			if (binaryReader.ReadBoolean())
			{
				radioState.Next = Util.ReadMeta(binaryReader);
			}
			return radioState;
		}
	}
	public static class Util
	{
		public static string Hex(byte[] b)
		{
			if (b == null)
			{
				return null;
			}
			StringBuilder stringBuilder = new StringBuilder(b.Length * 2);
			foreach (byte b2 in b)
			{
				stringBuilder.Append(b2.ToString("x2"));
			}
			return stringBuilder.ToString();
		}

		public static byte[] FromHex(string hex)
		{
			byte[] array = new byte[hex.Length / 2];
			for (int i = 0; i < array.Length; i++)
			{
				array[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
			}
			return array;
		}

		public static bool SameHash(byte[] a, byte[] b)
		{
			if (a == null || b == null || a.Length != b.Length)
			{
				return false;
			}
			for (int i = 0; i < a.Length; i++)
			{
				if (a[i] != b[i])
				{
					return false;
				}
			}
			return true;
		}

		public static void WriteMeta(BinaryWriter w, TrackMeta m)
		{
			w.Write(m.Hash);
			w.Write(Trunc(m.Title, 60));
			w.Write(m.Duration);
			w.Write(m.Size);
			w.Write(m.Ext ?? ".mp3");
		}

		public static TrackMeta ReadMeta(BinaryReader r)
		{
			return new TrackMeta
			{
				Hash = r.ReadBytes(32),
				Title = r.ReadString(),
				Duration = r.ReadSingle(),
				Size = r.ReadInt64(),
				Ext = SanitizeExt(r.ReadString())
			};
		}

		public static string SanitizeExt(string ext)
		{
			ext = (ext ?? "").ToLowerInvariant();
			if (!(ext == ".wav") && !(ext == ".ogg"))
			{
				return ".mp3";
			}
			return ext;
		}

		public static string Trunc(string s, int n)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			if (s.Length > n)
			{
				return s.Substring(0, n);
			}
			return s;
		}

		public static string Time(float seconds)
		{
			if (seconds < 0f || float.IsNaN(seconds))
			{
				seconds = 0f;
			}
			int num = (int)seconds;
			return $"{num / 60}:{num % 60:00}";
		}
	}
	public static class MusicFolderGuide
	{
		public const string FileName = "HOW TO ADD MUSIC.txt";

		private const int GuideVersion = 3;

		private const string LegacyFileName = "_PUT_MP3_WAV_OGG_FILES_HERE.txt";

		private const string LegacyText = "Player Radio: drop .mp3, .wav or .ogg files in this folder. They are added to the shared lobby playlist.\r\nPlayers who don't have a track get it streamed to them automatically (if EnableStreaming is on).\r\n";

		private static readonly Regex Header = new Regex("\\[guide v(\\d+) #([0-9a-f]{8})\\]\\s*$");

		public static void Ensure(string musicDir)
		{
			try
			{
				RemoveLegacy(musicDir);
				string text = Path.Combine(musicDir, "HOW TO ADD MUSIC.txt");
				string text2 = BuildBody();
				string text3 = $"PLAYER RADIO - HOW TO ADD MUSIC   [guide v{3} #{Hash(text2)}]\r\n" + text2;
				if (File.Exists(text))
				{
					string text4 = File.ReadAllText(text);
					if (!(text4 == text3))
					{
						int num = text4.IndexOf('\n');
						string input = ((num >= 0) ? text4.Substring(0, num) : text4).TrimEnd('\r');
						string s = ((num >= 0) ? text4.Substring(num + 1) : "");
						Match match = Header.Match(input);
						if (!match.Success || !(match.Groups[2].Value == Hash(s)))
						{
							Plugin.Verbose("'HOW TO ADD MUSIC.txt' was edited by the user; leaving it alone.");
							return;
						}
						File.WriteAllText(text, text3);
						Plugin.Log.LogInfo((object)string.Format("Updated '{0}' (guide v{1} -> v{2}).", "HOW TO ADD MUSIC.txt", match.Groups[1].Value, 3));
					}
				}
				else
				{
					File.WriteAllText(text, text3);
					Plugin.Log.LogInfo((object)("Wrote '" + text + "'."));
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Could not write 'HOW TO ADD MUSIC.txt': " + ex.Message));
			}
		}

		private static void RemoveLegacy(string musicDir)
		{
			string path = Path.Combine(musicDir, "_PUT_MP3_WAV_OGG_FILES_HERE.txt");
			if (File.Exists(path) && !(File.ReadAllText(path).Replace("\r\n", "\n") != "Player Radio: drop .mp3, .wav or .ogg files in this folder. They are added to the shared lobby playlist.\r\nPlayers who don't have a track get it streamed to them automatically (if EnableStreaming is on).\r\n".Replace("\r\n", "\n")))
			{
				File.Delete(path);
				Plugin.Log.LogInfo((object)"Removed old '_PUT_MP3_WAV_OGG_FILES_HERE.txt' (replaced by 'HOW TO ADD MUSIC.txt').");
			}
		}

		private static string Key(ConfigEntry<string> e)
		{
			string text = e?.Value ?? "";
			int num = text.LastIndexOf('/');
			string text2 = ((num >= 0) ? text.Substring(num + 1) : text).Trim();
			if (text2.Length != 0)
			{
				return text2.ToUpperInvariant();
			}
			return "(unbound)";
		}

		private static string BuildBody()
		{
			StringBuilder sb = new StringBuilder();
			L();
			L("1. Put your songs in THIS folder. Supported formats: .mp3, .wav and .ogg.");
			L("   Subfolders are fine (for example Music\\Chill\\song.mp3).");
			L("   Other files (like this .txt, cover images or playlists) are ignored.");
			L();
			L("2. New, changed or removed files are picked up automatically about every 10 seconds.");
			L("   No restart needed.");
			L();
			L("3. These are YOUR songs: they play from YOUR radio, which sits on your character.");
			L("   Every player has their own radio with their own songs. Everyone near you hears yours,");
			L("   you hear theirs (in 3D, so it gets quieter with distance). Several radios can play at once.");
			L("   If another player doesn't have your song, it is sent to them over the game's connection");
			L("   (rate-limited, checked, then cached). Turn that off with EnableStreaming = false in the config.");
			L();
			L("4. Other players need the Player Radio mod (same 0.2.x version) to hear your radio.");
			L();
			L("5. In game, press " + Key(Plugin.KeyOpenMusicFolder) + " to open this folder.");
			L("   Your radio: " + Key(Plugin.KeyPlayPause) + " play/pause, " + Key(Plugin.KeySkip) + " next, " + Key(Plugin.KeyPrevious) + " previous,");
			L("   " + Key(Plugin.KeyVolumeDown) + "/" + Key(Plugin.KeyVolumeUp) + " its volume (just for you).");
			L("   " + Key(Plugin.KeyMuteOthers) + " mutes/unmutes everyone else's radio, just for you.");
			L("   Change keys in BepInEx\\config\\com.inkslinger.playerradio.cfg.");
			L();
			L("Mod manager users (r2modman / Gale): this folder is inside your PROFILE folder, at");
			L("   <your profile>\\BepInEx\\config\\PlayerRadio\\Music");
			L("Use the mod manager's option to open/browse the profile folder to get there.");
			L();
			L("(Delete this file to get a fresh copy. If you edit it, Player Radio will leave it alone.)");
			return sb.ToString();
			void L(string s = "")
			{
				sb.Append(s).Append("\r\n");
			}
		}

		private static string Hash(string s)
		{
			using SHA256 sHA = SHA256.Create();
			return BitConverter.ToString(sHA.ComputeHash(Encoding.UTF8.GetBytes(s.Replace("\r\n", "\n"))), 0, 4).Replace("-", "").ToLowerInvariant();
		}
	}
	[HarmonyPatch(typeof(Character), "Awake")]
	internal static class Patch_CharacterAwake
	{
		private static void Postfix(Character __instance)
		{
			try
			{
				RadioSession.Instance?.OnCharacterAwake(__instance);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Character.Awake postfix: " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(Character), "RPCA_Die")]
	internal static class Patch_CharacterDie
	{
		private static void Prefix(Character __instance)
		{
			try
			{
				RadioSession.Instance?.OnCharacterDying(__instance);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Character.RPCA_Die prefix: " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(Character), "FinishZombifying")]
	internal static class Patch_CharacterZombify
	{
		private static void Prefix(Character __instance)
		{
			try
			{
				RadioSession.Instance?.OnCharacterDying(__instance);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Character.FinishZombifying prefix: " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(CharacterInteractible), "GetName")]
	internal static class Patch_CharacterInteractibleName
	{
		private static void Postfix(CharacterInteractible __instance, ref string __result)
		{
			try
			{
				if (Plugin.MarkSourcePlayerName.Value)
				{
					RadioSession instance = RadioSession.Instance;
					if ((Object)(object)instance != (Object)null && instance.IsRadioCharacter(__instance.character))
					{
						__result += " [Radio]";
					}
				}
			}
			catch
			{
			}
		}
	}
	[HarmonyPatch(typeof(PauseMenuMainPage), "Start")]
	internal static class Patch_PauseMenuMainPageStart
	{
		private static void Postfix(PauseMenuMainPage __instance)
		{
			try
			{
				RadioPauseMenu.Install(__instance);
			}
			catch (Exception arg)
			{
				Plugin.Log.LogWarning((object)$"Pause menu RADIO page not added (hotkeys still work): {arg}");
			}
		}
	}
	[BepInPlugin("com.inkslinger.playerradio", "Player Radio", "0.2.3")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "com.inkslinger.playerradio";

		public const string Name = "Player Radio";

		public const string Version = "0.2.3";

		internal static Plugin Instance;

		internal static ManualLogSource Log;

		internal static ConfigEntry<string> KeyPlayPause;

		internal static ConfigEntry<string> KeySkip;

		internal static ConfigEntry<string> KeyPrevious;

		internal static ConfigEntry<string> KeyMuteOthers;

		internal static ConfigEntry<string> KeyVolumeDown;

		internal static ConfigEntry<string> KeyVolumeUp;

		internal static ConfigEntry<string> KeyOpenMusicFolder;

		internal static ConfigEntry<float> MyRadioVolume;

		internal static ConfigEntry<float> OtherRadiosVolume;

		internal static ConfigEntry<float> VolumeStep;

		internal static ConfigEntry<float> MaxDistance;

		internal static ConfigEntry<float> MinDistance;

		internal static ConfigEntry<string> MixerGroupName;

		internal static ConfigEntry<string> ExcludeFromDucking;

		internal static ConfigEntry<bool> DuckDuringCinematics;

		internal static ConfigEntry<bool> DuckGameMusic;

		internal static ConfigEntry<bool> DuckOnlyWhenInRange;

		internal static ConfigEntry<float> GameMusicDuckVolume;

		internal static ConfigEntry<bool> EnableStreaming;

		internal static ConfigEntry<int> StreamRateKBps;

		internal static ConfigEntry<int> DownloadRateKBps;

		internal static ConfigEntry<int> MaxStreamFileMB;

		internal static ConfigEntry<int> CacheSizeMB;

		internal static ConfigEntry<string> MusicFolderOverride;

		internal static ConfigEntry<bool> ShowHud;

		internal static ConfigEntry<bool> ShowKeyHints;

		internal static ConfigEntry<bool> ShowNearbyRadios;

		internal static ConfigEntry<bool> MarkSourcePlayerName;

		internal static ConfigEntry<bool> VerboseLogging;

		internal static ConfigEntry<bool> EnableHotkeys;

		internal static ConfigEntry<bool> PauseMenuPage;

		internal static ConfigEntry<bool> DumpPauseMenu;

		internal static ConfigEntry<string> PauseMenuButtonPosition;

		private Harmony _harmony;

		private GameObject _host;

		internal static ConfigEntry<int> ConfigVersion;

		private const int CurrentConfigVersion = 2;

		private void Awake()
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Expected O, but got Unknown
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a5: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			BindConfig();
			Log.LogInfo((object)("Player Radio 0.2.3 by Inkslinger loading (Unity " + Application.unityVersion + ")."));
			_harmony = new Harmony("com.inkslinger.playerradio");
			ApplyPatch(typeof(Patch_CharacterAwake));
			ApplyPatch(typeof(Patch_CharacterDie));
			ApplyPatch(typeof(Patch_CharacterZombify));
			ApplyPatch(typeof(Patch_CharacterInteractibleName));
			ApplyPatch(typeof(Patch_PauseMenuMainPageStart));
			_host = new GameObject("PlayerRadio");
			Object.DontDestroyOnLoad((Object)(object)_host);
			((Object)_host).hideFlags = (HideFlags)61;
			_host.AddComponent<RadioSession>();
			_host.AddComponent<RadioHud>();
			_host.AddComponent<GameMusicDucker>();
			Log.LogInfo((object)("Player Radio loaded. Music folder: " + TrackLibrary.ResolveMusicDir()));
		}

		private void OnDestroy()
		{
			GameMusicDucker.SharedGain = 1f;
			if ((Object)(object)GameMusicDucker.Instance != (Object)null)
			{
				GameMusicDucker.Instance.RestoreAll("plugin unload");
			}
		}

		private void ApplyPatch(Type patchClass)
		{
			try
			{
				_harmony.CreateClassProcessor(patchClass).Patch();
				Log.LogInfo((object)("Harmony patch applied: " + patchClass.Name));
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Harmony patch " + patchClass.Name + " failed (game update?): " + ex.Message));
			}
		}

		private void BindConfig()
		{
			//IL_014a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0154: Expected O, but got Unknown
			//IL_0187: Unknown result type (might be due to invalid IL or missing references)
			//IL_0191: Expected O, but got Unknown
			//IL_01c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ce: Expected O, but got Unknown
			//IL_0201: Unknown result type (might be due to invalid IL or missing references)
			//IL_020b: Expected O, but got Unknown
			//IL_023e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0248: Expected O, but got Unknown
			//IL_02bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c9: Expected O, but got Unknown
			//IL_0379: Unknown result type (might be due to invalid IL or missing references)
			//IL_0383: Expected O, but got Unknown
			//IL_03ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_03b6: Expected O, but got Unknown
			//IL_03e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_03f0: Expected O, but got Unknown
			//IL_041f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0429: Expected O, but got Unknown
			EnableHotkeys = ((BaseUnityPlugin)this).Config.Bind<bool>("Hotkeys", "EnableHotkeys", true, "Turn the radio hotkeys below on or off. They also work while the pause menu is open. Everything they do is also on the pause menu's RADIO page.");
			KeyPlayPause = ((BaseUnityPlugin)this).Config.Bind<string>("Hotkeys", "PlayPause", "<Keyboard>/f5", "Play / pause YOUR radio (plays your Music folder from your character; everyone nearby hears it). Unity Input System control path, e.g. <Keyboard>/f5, <Keyboard>/numpad5, <Mouse>/forwardButton.");
			KeySkip = ((BaseUnityPlugin)this).Config.Bind<string>("Hotkeys", "Skip", "<Keyboard>/f7", "Your radio: next track. Unity Input System control path, e.g. <Keyboard>/f5, <Keyboard>/numpad5, <Mouse>/forwardButton.");
			KeyPrevious = ((BaseUnityPlugin)this).Config.Bind<string>("Hotkeys", "Previous", "<Keyboard>/f6", "Your radio: previous track (restarts the current track if more than 3 s in). Unity Input System control path, e.g. <Keyboard>/f5, <Keyboard>/numpad5, <Mouse>/forwardButton.");
			KeyMuteOthers = ((BaseUnityPlugin)this).Config.Bind<string>("Hotkeys", "ToggleOtherRadios", "<Keyboard>/f8", "Mute / unmute OTHER players' radios for you only (not saved; resets on restart). Unity Input System control path, e.g. <Keyboard>/f5, <Keyboard>/numpad5, <Mouse>/forwardButton.");
			KeyVolumeDown = ((BaseUnityPlugin)this).Config.Bind<string>("Hotkeys", "VolumeDown", "<Keyboard>/f9", "Lower the volume of YOUR radio as you hear it (local only). Unity Input System control path, e.g. <Keyboard>/f5, <Keyboard>/numpad5, <Mouse>/forwardButton.");
			KeyVolumeUp = ((BaseUnityPlugin)this).Config.Bind<string>("Hotkeys", "VolumeUp", "<Keyboard>/f10", "Raise the volume of YOUR radio as you hear it (local only). Unity Input System control path, e.g. <Keyboard>/f5, <Keyboard>/numpad5, <Mouse>/forwardButton.");
			KeyOpenMusicFolder = ((BaseUnityPlugin)this).Config.Bind<string>("Hotkeys", "OpenMusicFolder", "<Keyboard>/f4", "Open your Music folder in File Explorer (works in menus too). Unity Input System control path, e.g. <Keyboard>/f5, <Keyboard>/numpad5, <Mouse>/forwardButton.");
			MyRadioVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "MyRadioVolume", 0.7f, new ConfigDescription("How loud YOUR radio is for you (local only; F9/F10). Also scaled by the game's Master/Music sliders.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			OtherRadiosVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "OtherRadiosVolume", 0.7f, new ConfigDescription("How loud OTHER players' radios are for you (local only).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			VolumeStep = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "VolumeStep", 0.1f, new ConfigDescription("Volume change per key press.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.01f, 0.5f), Array.Empty<object>()));
			MaxDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "MaxDistance", 60f, new ConfigDescription("Distance (m) at which a radio becomes inaudible.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 500f), Array.Empty<object>()));
			MinDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "MinDistance", 3f, new ConfigDescription("Distance (m) within which a radio plays at full volume.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 50f), Array.Empty<object>()));
			MixerGroupName = ((BaseUnityPlugin)this).Config.Bind<string>("Audio", "MixerGroup", "Music", "Name of the game's audio mixer group to route the radio through (so the game's volume sliders apply). Falls back to the SFX group, then Master.");
			DuckGameMusic = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "DuckGameMusic", true, "Fade the game's own background music out (~1.5 s) while your radio plays or another player's radio is audible near you (deeper the closer it is), and back in (~2 s) afterwards. Only affects your client.");
			GameMusicDuckVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "GameMusicDuckVolume", 0f, new ConfigDescription("Game music level while ducked (0 = silent, 1 = no change).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			DuckOnlyWhenInRange = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "DuckOnlyWhenInRange", false, "Stricter ducking for OTHER players' radios: only duck (fully) when you are clearly within earshot. Off = duck in proportion to how loud the nearest radio is. Your own radio always ducks.");
			ExcludeFromDucking = ((BaseUnityPlugin)this).Config.Bind<string>("Audio", "ExcludeFromDucking", "Ambience", "Comma-separated name parts: game audio objects or clips whose name contains one of these are never ducked (case-insensitive).");
			DuckDuringCinematics = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "DuckDuringCinematics", true, "Lower radios while the game plays a cinematic.");
			EnableStreaming = ((BaseUnityPlugin)this).Config.Bind<bool>("Network", "EnableStreaming", true, "Send/receive track files to/from players who are missing them (uses the game's Photon connection; kept slow on purpose).");
			StreamRateKBps = ((BaseUnityPlugin)this).Config.Bind<int>("Network", "StreamRateKBps", 64, new ConfigDescription("Upload rate limit for sending tracks to others, in KB/s (total across all recipients; each recipient also gets at most its requested rate).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(8, 256), Array.Empty<object>()));
			MaxStreamFileMB = ((BaseUnityPlugin)this).Config.Bind<int>("Network", "MaxStreamFileMB", 25, new ConfigDescription("Largest track file that will be sent or accepted, in MB.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100), Array.Empty<object>()));
			CacheSizeMB = ((BaseUnityPlugin)this).Config.Bind<int>("Network", "CacheSizeMB", 500, new ConfigDescription("Maximum size of the downloaded-track cache, in MB (oldest files are deleted first).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(50, 10000), Array.Empty<object>()));
			DownloadRateKBps = ((BaseUnityPlugin)this).Config.Bind<int>("Network", "DownloadRateKBps", 128, new ConfigDescription("Total download rate you ask senders for, in KB/s (shared between up to 2 simultaneous downloads).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(8, 512), Array.Empty<object>()));
			MusicFolderOverride = ((BaseUnityPlugin)this).Config.Bind<string>("Library", "MusicFolder", "", "Optional absolute path to your music folder. Empty = BepInEx/config/PlayerRadio/Music.");
			ShowHud = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "ShowHud", true, "Show the small radio overlay (your radio, transfers, nearby radios).");
			ShowKeyHints = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "ShowKeyHints", true, "Show the hotkey hints line in the top-right overlay (hidden anyway when EnableHotkeys is off).");
			PauseMenuPage = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "PauseMenuRadioPage", true, "Add a RADIO button to the pause menu that opens the Player Radio page (now playing, controls, volumes, song list).");
			PauseMenuButtonPosition = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "PauseMenuButtonPosition", "", "Leave empty to place the pause-menu RADIO button automatically (under the existing buttons). Or set x,y (anchoredPosition in the button column, e.g. 0,-300) if it overlaps something; see the pause-menu dump in the log.");
			ShowNearbyRadios = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "ShowNearbyRadios", true, "List other players' radios playing near you (owner, title, distance) in the overlay.");
			MarkSourcePlayerName = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "MarkRadioCarrierName", true, "Append [Radio] to the name shown when looking at a player whose radio is playing.");
			VerboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "VerboseLogging", false, "Log every state change, transfer step and drift correction.");
			DumpPauseMenu = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DumpPauseMenuHierarchy", false, "Off by default. Log the pause menu's UI hierarchy (names, positions, components) once per game launch, the first time the pause menu opens. For fixing the RADIO button layout; safe to turn off.");
			ConfigVersion = ((BaseUnityPlugin)this).Config.Bind<int>("Internal", "ConfigVersion", 1, "Used by Player Radio to upgrade old settings. Don't change.");
			MigrateConfig();
		}

		private void MigrateConfig()
		{
			//IL_011e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0124: Expected O, but got Unknown
			//IL_0130: Unknown result type (might be due to invalid IL or missing references)
			//IL_0136: Expected O, but got Unknown
			//IL_0142: Unknown result type (might be due to invalid IL or missing references)
			//IL_0148: Expected O, but got Unknown
			int value = ConfigVersion.Value;
			if (value >= 2)
			{
				return;
			}
			try
			{
				if (value < 2)
				{
					bool num = string.Equals(KeyPlayPause.Value?.Trim(), "<Keyboard>/f6", StringComparison.OrdinalIgnoreCase);
					bool flag = string.Equals(KeyPrevious.Value?.Trim(), "<Keyboard>/f5", StringComparison.OrdinalIgnoreCase);
					if (num && flag)
					{
						KeyPlayPause.Value = "<Keyboard>/f5";
						KeyPrevious.Value = "<Keyboard>/f6";
						Log.LogInfo((object)"Config upgrade: play/pause is now F5 and previous is now F6 (swapped from the old defaults).");
					}
					else
					{
						Log.LogInfo((object)("Config upgrade: keeping your play/pause (" + KeyPlayPause.Value + ") and previous (" + KeyPrevious.Value + ") keys."));
					}
					if (typeof(ConfigFile).GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(((BaseUnityPlugin)this).Config, null) is Dictionary<ConfigDefinition, string> dictionary)
					{
						int num2 = 0;
						ConfigDefinition[] array = (ConfigDefinition[])(object)new ConfigDefinition[3]
						{
							new ConfigDefinition("Audio", "Volume"),
							new ConfigDefinition("Hotkeys", "TakeOver"),
							new ConfigDefinition("Network", "SourceHandoff")
						};
						foreach (ConfigDefinition key in array)
						{
							if (dictionary.Remove(key))
							{
								num2++;
							}
						}
						if (num2 > 0)
						{
							Log.LogInfo((object)$"Config upgrade: removed {num2} setting(s) that no longer exist.");
						}
					}
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Config upgrade failed: " + ex.Message));
			}
			ConfigVersion.Value = 2;
			((BaseUnityPlugin)this).Config.Save();
		}

		internal static void Verbose(string msg)
		{
			if (VerboseLogging != null && VerboseLogging.Value)
			{
				Log.LogInfo((object)("[v] " + msg));
			}
		}
	}
	public sealed class Radio
	{
		public readonly int Actor;

		public readonly bool IsLocal;

		public RadioState State = new RadioState();

		public readonly RadioEmitter Emitter;

		public Vector3? DeathPos;

		public Vector3? LastKnownPos;

		private AudioClip _clip;

		private string _clipHex;

		private string _loadingHex;

		private readonly Dictionary<string, float> _loadFailedAt = new Dictionary<string, float>();

		private float _nextDriftCheck;

		private float _lastAudibleAt = -100f;

		public bool HasPosition { get; private set; }

		public AudioSource Source => Emitter.Source;

		public string ClipHex => _clipHex;

		public bool IsLoading => _loadingHex != null;

		public bool On
		{
			get
			{
				if (State.Playing)
				{
					return State.Current != null;
				}
				return false;
			}
		}

		public float Duration
		{
			get
			{
				TrackMeta current = State.Current;
				if (current == null)
				{
					return 0f;
				}
				if ((Object)(object)_clip != (Object)null && _clipHex == current.Hex)
				{
					return _clip.length;
				}
				return current.Duration;
			}
		}

		public Radio(int actor, bool isLocal)
		{
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Expected O, but got Unknown
			Actor = actor;
			IsLocal = isLocal;
			GameObject val = new GameObject(isLocal ? "PlayerRadio.MyRadio" : ("PlayerRadio.Radio." + actor));
			Object.DontDestroyOnLoad((Object)(object)val);
			((Object)val).hideFlags = (HideFlags)61;
			Emitter = val.AddComponent<RadioEmitter>();
		}

		public bool Sounding(float holdSeconds = 3f)
		{
			if ((Object)(object)Source != (Object)null && Source.isPlaying && Source.volume > 0.001f)
			{
				_lastAudibleAt = Time.unscaledTime;
			}
			if (On)
			{
				return Time.unscaledTime - _lastAudibleAt < holdSeconds;
			}
			return false;
		}

		public void Dispose()
		{
			try
			{
				if ((Object)(object)Source != (Object)null)
				{
					Source.Stop();
					Source.clip = null;
				}
				if ((Object)(object)_clip != (Object)null)
				{
					Object.Destroy((Object)(object)_clip);
				}
				if ((Object)(object)Emitter != (Object)null)
				{
					Object.Destroy((Object)(object)((Component)Emitter).gameObject);
				}
			}
			catch
			{
			}
			_clip = null;
			_clipHex = null;
		}

		public string Blocker(RadioSession s)
		{
			string currentHex = State.CurrentHex;
			if (currentHex == null || _clipHex == currentHex)
			{
				return null;
			}
			if (_loadingHex == currentHex)
			{
				return "Loading...";
			}
			if (s.Library.Has(currentHex))
			{
				if (!_loadFailedAt.ContainsKey(currentHex))
				{
					return "Loading...";
				}
				return "Can't decode this file";
			}
			string text = s.Transfer.StatusFor(currentHex);
			if (text != null)
			{
				return text;
			}
			if (!Plugin.EnableStreaming.Value)
			{
				return "Missing track (streaming disabled)";
			}
			if (s.Transfer.RecentlyFailed(currentHex))
			{
				return "Download failed, retrying soon";
			}
			return "Waiting to download...";
		}

		public void UpdatePlayback(RadioSession s)
		{
			AudioSource source = Source;
			RadioState state = State;
			string currentHex = state.CurrentHex;
			float unscaledTime = Time.unscaledTime;
			if (currentHex == null)
			{
				if (source.isPlaying)
				{
					source.Stop();
				}
				return;
			}
			if (_clipHex != currentHex)
			{
				if (source.isPlaying)
				{
					source.Stop();
				}
				if (_loadingHex == null && (!_loadFailedAt.TryGetValue(currentHex, out var value) || !(unscaledTime - value < 30f)) && s.Library.TryGetLocalPath(currentHex, out var path))
				{
					((MonoBehaviour)s).StartCoroutine(LoadClip(s, currentHex, path));
				}
				return;
			}
			float num = state.PositionAt(RadioSession.Now);
			float length = _clip.length;
			if (state.Playing)
			{
				if (num >= length - 0.05f)
				{
					if (source.isPlaying)
					{
						source.Stop();
					}
				}
				else if (!source.isPlaying)
				{
					if (!(num > length - 0.5f))
					{
						if ((Object)(object)source.clip != (Object)(object)_clip)
						{
							source.clip = _clip;
						}
						source.time = Mathf.Clamp(num, 0f, length - 0.01f);
						source.Play();
						_nextDriftCheck = unscaledTime + 1f;
						Plugin.Verbose($"[{s.PlayerName(Actor)}] play '{((Object)_clip).name}' from {num:0.00}s");
					}
				}
				else if (unscaledTime >= _nextDriftCheck)
				{
					_nextDriftCheck = unscaledTime + 1f;
					float num2 = source.time - num;
					if (Mathf.Abs(num2) > 0.25f)
					{
						source.time = Mathf.Clamp(num, 0f, length - 0.01f);
						Plugin.Verbose($"[{s.PlayerName(Actor)}] drift {num2:+0.00;-0.00}s corrected");
					}
				}
			}
			else if (source.isPlaying)
			{
				source.Pause();
			}
		}

		private IEnumerator LoadClip(RadioSession s, string hex, string path)
		{
			_loadingHex = hex;
			AudioClip clip = null;
			string err = null;
			yield return AudioLoader.Load(path, delegate(AudioClip c, string e)
			{
				clip = c;
				err = e;
			});
			_loadingHex = null;
			if ((Object)(object)Emitter == (Object)null)
			{
				if ((Object)(object)clip != (Object)null)
				{
					Object.Destroy((Object)(object)clip);
				}
				yield break;
			}
			if ((Object)(object)clip == (Object)null)
			{
				_loadFailedAt[hex] = Time.unscaledTime;
				Plugin.Log.LogError((object)("Could not load '" + Path.GetFileName(path) + "': " + err));
				if (IsLocal)
				{
					s.ShowToast("Radio: can't decode this track");
				}
				yield break;
			}
			AudioSource source = Source;
			if ((Object)(object)_clip != (Object)null)
			{
				source.Stop();
				source.clip = null;
				Object.Destroy((Object)(object)_clip);
			}
			_clip = clip;
			_clipHex = hex;
			source.clip = clip;
			s.Library.SetDuration(hex, clip.length);
			Plugin.Log.LogInfo((object)$"[{s.PlayerName(Actor)}'s radio] loaded '{Path.GetFileName(path)}' ({Util.Time(clip.length)}, {clip.channels}ch {clip.frequency}Hz).");
		}

		public void Place(RadioSession s)
		{
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			Vector3? pos = null;
			Character val = s.FindCharacter(Actor, IsLocal);
			string placement;
			if ((Object)(object)val != (Object)null && RadioSession.CharacterAlive(val) && !RadioSession.InDeathVoid(RadioSession.TorsoPos(val)))
			{
				pos = (LastKnownPos = RadioSession.TorsoPos(val));
				if (DeathPos.HasValue)
				{
					Plugin.Log.LogInfo((object)(s.PlayerName(Actor) + " is back up: radio reattached."));
					DeathPos = null;
				}
				placement = (IsLocal ? "on you" : ("on " + s.PlayerName(Actor)));
			}
			else if (DeathPos.HasValue || ((Object)(object)val != (Object)null && LastKnownPos.HasValue))
			{
				if (!DeathPos.HasValue)
				{
					DeathPos = LastKnownPos;
				}
				pos = DeathPos;
				placement = (IsLocal ? "where you fell" : ("where " + s.PlayerName(Actor) + " fell"));
			}
			else
			{
				placement = (IsLocal ? "no body yet (plays for you only)" : "not nearby");
			}
			HasPosition = pos.HasValue;
			float volume = ((!IsLocal) ? ((pos.HasValue && !s.OthersMuted) ? Plugin.OtherRadiosVolume.Value : 0f) : Plugin.MyRadioVolume.Value);
			Emitter.Place(pos, placement, volume);
		}

		public void OnSceneChanged()
		{
			DeathPos = null;
			LastKnownPos = null;
			Emitter.ForceMixerReassign();
		}
	}
	public static class RadioActions
	{
		private static float _lastOpen = -10f;

		public static string MusicFolder
		{
			get
			{
				TrackLibrary trackLibrary = (((Object)(object)RadioSession.Instance != (Object)null) ? RadioSession.Instance.Library : null);
				return Path.GetFullPath((trackLibrary != null) ? trackLibrary.MusicDir : TrackLibrary.ResolveMusicDir()).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
			}
		}

		public static void PlayPause()
		{
			RadioSession.Instance?.PlayPause();
		}

		public static void Next()
		{
			RadioSession.Instance?.Step(1);
		}

		public static void Previous()
		{
			RadioSession.Instance?.Step(-1);
		}

		public static void PlayTrack(TrackMeta track)
		{
			RadioSession.Instance?.PlayTrack(track);
		}

		public static void SetOthersMuted(bool muted)
		{
			RadioSession.Instance?.SetOthersMuted(muted);
		}

		public static void ToggleOthersMuted()
		{
			RadioSession instance = RadioSession.Instance;
			if ((Object)(object)instance != (Object)null)
			{
				instance.SetOthersMuted(!instance.OthersMuted);
			}
		}

		public static bool OpenMusicFolder()
		{
			if (Time.unscaledTime - _lastOpen < 1f)
			{
				return false;
			}
			_lastOpen = Time.unscaledTime;
			string text = null;
			try
			{
				text = MusicFolder;
				Directory.CreateDirectory(text);
				MusicFolderGuide.Ensure(text);
				string absoluteUri = new Uri(text + Path.DirectorySeparatorChar).AbsoluteUri;
				Application.OpenURL(absoluteUri);
				Plugin.Verbose("OpenURL " + absoluteUri);
				Plugin.Log.LogInfo((object)("Opened Music folder: " + text));
				RadioSession.Instance?.ShowToast("Opened Music folder");
				return true;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Could not open Music folder '" + text + "': " + ex.Message));
				RadioSession.Instance?.ShowToast("Could not open Music folder (see log)");
				return false;
			}
		}
	}
	public sealed class RadioEmitter : MonoBehaviour
	{
		private float _appliedMin = -1f;

		private float _appliedMax = -1f;

		private float _blend = 1f;

		private float _mixerRetryAt;

		private bool _mixerAssigned;

		private AnimationCurve _curve;

		public AudioSource Source { get; private set; }

		public string Placement { get; private set; } = "";

		public bool Spatial => _blend > 0.5f;

		private void Awake()
		{
			Source = ((Component)this).gameObject.AddComponent<AudioSource>();
			Source.playOnAwake = false;
			Source.loop = false;
			Source.spatialBlend = 1f;
			Source.dopplerLevel = 0f;
			Source.spread = 60f;
			Source.priority = 40;
			Source.rolloffMode = (AudioRolloffMode)2;
			ApplyDistances();
		}

		private void ApplyDistances()
		{
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
			//IL_0102: Expected O, but got Unknown
			float num = Mathf.Max(5f, Plugin.MaxDistance.Value);
			float num2 = Mathf.Clamp(Plugin.MinDistance.Value, 0.5f, num * 0.9f);
			if (!Mathf.Approximately(num2, _appliedMin) || !Mathf.Approximately(num, _appliedMax))
			{
				_appliedMin = num2;
				_appliedMax = num;
				Source.minDistance = num2;
				Source.maxDistance = num;
				float num3 = num2 / num;
				AnimationCurve val = new AnimationCurve((Keyframe[])(object)new Keyframe[5]
				{
					new Keyframe(0f, 1f),
					new Keyframe(num3, 1f),
					new Keyframe(num3 + (1f - num3) * 0.35f, 0.45f),
					new Keyframe(num3 + (1f - num3) * 0.7f, 0.15f),
					new Keyframe(1f, 0f)
				});
				Source.SetCustomCurve((AudioSourceCurveType)0, val);
				_curve = val;
			}
		}

		private void TryAssignMixer()
		{
			if (!_mixerAssigned && !(Time.unscaledTime < _mixerRetryAt))
			{
				_mixerRetryAt = Time.unscaledTime + 3f;
				AudioMixerGroup val = MixerRouting.Resolve();
				if ((Object)(object)val != (Object)null)
				{
					Source.outputAudioMixerGroup = val;
					_mixerAssigned = true;
				}
			}
		}

		public void ForceMixerReassign()
		{
			_mixerAssigned = false;
			_mixerRetryAt = 0f;
		}

		public float LoudnessAt(Vector3 listener)
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			if (!Spatial || _curve == null)
			{
				return 1f;
			}
			float num = Mathf.Max(0.01f, Source.maxDistance);
			float num2 = Vector3.Distance(listener, ((Component)this).transform.position);
			if (num2 >= num)
			{
				return 0f;
			}
			return Mathf.Clamp01(_curve.Evaluate(num2 / num));
		}

		public void Place(Vector3? pos, string placement, float volume)
		{
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			ApplyDistances();
			TryAssignMixer();
			Placement = placement;
			if (pos.HasValue)
			{
				((Component)this).transform.position = pos.Value;
			}
			float num = (pos.HasValue ? 1f : 0f);
			bool flag = false;
			if (Plugin.DuckDuringCinematics.Value)
			{
				PeakHandler instance = Singleton<PeakHandler>.Instance;
				flag = (Object)(object)instance != (Object)null && instance.isPlayingCinematic;
			}
			_blend = Mathf.MoveTowards(_blend, num, Time.unscaledDeltaTime * 2f);
			Source.spatialBlend = _blend;
			float num2 = (pos.HasValue ? 1f : 0.6f);
			Source.volume = Mathf.Clamp01(volume) * num2 * (flag ? 0.2f : 1f);
		}
	}
	public sealed class RadioHud : MonoBehaviour
	{
		private GUIStyle _style;

		private GUIStyle _shadow;

		private int _styleSize;

		private readonly StringBuilder _sb = new StringBuilder();

		private float _hintsUntil;

		private void Start()
		{
			_hintsUntil = Time.unscaledTime + 20f;
		}

		private void OnGUI()
		{
			//IL_0117: Unknown result type (might be due to invalid IL or missing references)
			//IL_011c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0124: Unknown result type (might be due to invalid IL or missing references)
			//IL_012b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0132: Unknown result type (might be due to invalid IL or missing references)
			//IL_013e: Expected O, but got Unknown
			//IL_015d: Unknown result type (might be due to invalid IL or missing references)
			//IL_016e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0178: Expected O, but got Unknown
			//IL_0197: Unknown result type (might be due to invalid IL or missing references)
			//IL_0731: Unknown result type (might be due to invalid IL or missing references)
			//IL_0748: Unknown result type (might be due to invalid IL or missing references)
			if (!Plugin.ShowHud.Value || RadioPausePage.Showing)
			{
				return;
			}
			RadioSession instance = RadioSession.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			bool flag = (Object)(object)Character.localCharacter != (Object)null;
			Radio mine = instance.Mine;
			bool flag2 = mine != null && mine.State.Current != null;
			bool flag3 = instance.Toast != null && Time.unscaledTime < instance.ToastUntil;
			bool flag4 = instance.Transfer.DownloadCount > 0 || instance.Transfer.UploadCount > 0;
			List<(Radio, float)> list = ((Plugin.ShowNearbyRadios.Value && flag) ? instance.NearbyRadios(4) : null);
			bool flag5 = list != null && list.Count > 0;
			if (!flag && !flag2 && !flag3 && !flag4)
			{
				return;
			}
			int num = Mathf.Clamp(Mathf.RoundToInt((float)Screen.height / 1080f * 15f), 11, 28);
			if (_style == null || num != _styleSize)
			{
				_styleSize = num;
				_style = new GUIStyle(GUI.skin.label)
				{
					fontSize = num,
					alignment = (TextAnchor)2,
					wordWrap = false,
					richText = true
				};
				_style.normal.textColor = new Color(1f, 0.95f, 0.8f, 0.95f);
				_shadow = new GUIStyle(_style);
				_shadow.normal.textColor = new Color(0f, 0f, 0f, 0.8f);
			}
			_sb.Length = 0;
			int lines = 0;
			if (flag2)
			{
				RadioState state = mine.State;
				string text = Util.Trunc(state.Current.Title, 48);
				float duration = mine.Duration;
				float seconds = Mathf.Min(state.PositionAt(RadioSession.Now), (duration > 0f) ? duration : float.MaxValue);
				string text2 = mine.Blocker(instance);
				Line("<b>Your radio</b> " + (state.Playing ? "> " : "|| ") + text);
				if (text2 != null)
				{
					Line(text2);
				}
				else
				{
					Line(Util.Time(seconds) + " / " + Util.Time(duration) + "  -  " + mine.Emitter.Placement);
				}
			}
			else if (flag)
			{
				int count = instance.MyTracks.Count;
				Line("<b>Your radio</b> off - " + count + ((count == 1) ? " song" : " songs"));
				if (!string.IsNullOrEmpty(instance.Library.ScanStatus))
				{
					Line(instance.Library.ScanStatus);
				}
			}
			foreach (string downloadingHex in instance.Transfer.DownloadingHexes)
			{
				string text3 = instance.Transfer.StatusFor(downloadingHex);
				if (text3 != null)
				{
					Line(text3);
				}
			}
			if (instance.Transfer.UploadCount > 0)
			{
				Line("Sending songs to " + instance.Transfer.UploadCount + ((instance.Transfer.UploadCount == 1) ? " player" : " players"));
			}
			Line("Vol mine " + Mathf.RoundToInt(Plugin.MyRadioVolume.Value * 100f) + "% / others " + (instance.OthersMuted ? "muted" : (Mathf.RoundToInt(Plugin.OtherRadiosVolume.Value * 100f) + "%")) + "  -  " + instance.ModeShort);
			if (flag5)
			{
				foreach (var item3 in list)
				{
					Radio item = item3.Item1;
					float item2 = item3.Item2;
					string text4 = Util.Trunc(instance.PlayerName(item.Actor), 16);
					string text5 = Util.Trunc(item.State.Current?.Title ?? "?", 32);
					string text6 = ((item2 >= 0f) ? (" (" + Mathf.RoundToInt(item2) + " m)") : "");
					string text7 = item.Blocker(instance);
					Line("<size=" + Mathf.RoundToInt((float)num * 0.9f) + ">" + text4 + ": " + text5 + text6 + ((text7 != null) ? (" - " + text7) : "") + "</size>");
				}
			}
			if (flag3)
			{
				Line("<i>" + instance.Toast + "</i>");
			}
			if (Plugin.ShowKeyHints.Value && Plugin.EnableHotkeys.Value && (Time.unscaledTime < _hintsUntil || !flag2 || flag3))
			{
				_sb.Append("<size=").Append(Mathf.RoundToInt((float)num * 0.85f)).Append('>')
					.Append('[')
					.Append(instance.KeyPlayPause.Display)
					.Append("] play/pause  [")
					.Append(instance.KeyPrevious.Display)
					.Append('/')
					.Append(instance.KeySkip.Display)
					.Append("] prev/next  [")
					.Append(instance.KeyMuteOthers.Display)
					.Append("] mute others  [")
					.Append(instance.KeyVolDown.Display)
					.Append('/')
					.Append(instance.KeyVolUp.Display)
					.Append("] volume  [")
					.Append(instance.KeyOpenFolder.Display)
					.Append("] music folder</size>");
				lines++;
			}
			string text8 = _sb.ToString().TrimEnd('\n');
			float num2 = Mathf.Min((float)Screen.width * 0.6f, (float)num * 40f);
			Rect val = default(Rect);
			((Rect)(ref val))..ctor((float)Screen.width - num2 - 16f, 12f, num2, (float)num * 1.6f * (float)(lines + 2));
			GUI.Label(new Rect(((Rect)(ref val)).x + 1.5f, ((Rect)(ref val)).y + 1.5f, ((Rect)(ref val)).width, ((Rect)(ref val)).height), StripColor(text8), _shadow);
			GUI.Label(val, text8, _style);
			void Line(string t)
			{
				_sb.Append(t).Append('\n');
				lines++;
			}
		}

		private static string StripColor(string s)
		{
			return s;
		}
	}
	internal static class RadioPauseMenu
	{
		internal const string ButtonName = "PlayerRadio_RadioButton";

		internal const string PageName = "PlayerRadio_RadioPage";

		private static bool _dumped;

		internal static void Install(PauseMenuMainPage main)
		{
			//IL_012b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0135: Expected O, but got Unknown
			//IL_015f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0164: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)main == (Object)null || !Plugin.PauseMenuPage.Value)
			{
				return;
			}
			UIPageHandler componentInParent = ((Component)main).GetComponentInParent<UIPageHandler>();
			object? obj = AccessTools.Field(typeof(PauseMenuMainPage), "m_settingsButton")?.GetValue(main);
			Button val = (Button)((obj is Button) ? obj : null);
			if ((Object)(object)componentInParent == (Object)null || (Object)(object)val == (Object)null || (Object)(object)((Component)main).transform.parent == (Object)null)
			{
				Plugin.Log.LogWarning((object)("Pause menu: " + (((Object)(object)componentInParent == (Object)null) ? "page handler" : (((Object)(object)val == (Object)null) ? "PauseMenuMainPage.m_settingsButton" : "page parent")) + " not found (game update?). No RADIO button; hotkeys still work."));
			}
			else if (!((Object)(object)((Component)main).transform.parent.Find("PlayerRadio_RadioPage") != (Object)null))
			{
				Transform parent = ((Component)val).transform.parent;
				Button val2 = CloneButton(((Component)val).gameObject, parent, "PlayerRadio_RadioButton", "RADIO");
				((Component)val2).transform.SetSiblingIndex(((Component)val).transform.GetSiblingIndex() + 1);
				Button above = PlaceButton(val2, val, parent);
				LinkNavigation(val2, above);
				RadioPausePage radioPausePage = RadioPausePage.Create(componentInParent, main, val);
				((UnityEvent)val2.onClick).AddListener(new UnityAction(radioPausePage.Open));
				GUIManager instance = GUIManager.instance;
				if ((Object)(object)instance != (Object)null)
				{
					radioPausePage.HookPauseAction(instance);
				}
				Plugin.Log.LogInfo((object)$"Pause menu: RADIO button and page added (button at {((RectTransform)((Component)val2).transform).anchoredPosition}, under '{((Object)parent).name}').");
				if (Plugin.DumpPauseMenu.Value && !_dumped)
				{
					_dumped = true;
					DumpHierarchy(((Component)componentInParent).transform);
				}
			}
		}

		internal static Button CloneButton(GameObject template, Transform parent, string name, string label)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Expected O, but got Unknown
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = Object.Instantiate<GameObject>(template, parent, false);
			((Object)val).name = name;
			StripLocalization(val);
			Button component = val.GetComponent<Button>();
			component.onClick = new ButtonClickedEvent();
			Navigation navigation = default(Navigation);
			((Navigation)(ref navigation)).mode = (Mode)3;
			((Selectable)component).navigation = navigation;
			SetLabel(val, label);
			return component;
		}

		internal static void StripLocalization(GameObject go)
		{
			LocalizedText[] componentsInChildren = go.GetComponentsInChildren<LocalizedText>(true);
			for (int i = 0; i < componentsInChildren.Length; i++)
			{
				Object.DestroyImmediate((Object)(object)componentsInChildren[i]);
			}
		}

		internal static void SetLabel(GameObject go, string label)
		{
			TMP_Text componentInChildren = go.GetComponentInChildren<TMP_Text>(true);
			if ((Object)(object)componentInChildren != (Object)null)
			{
				componentInChildren.text = label;
			}
		}

		private static Button PlaceButton(Button radio, Button settings, Transform column)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Expected O, but got Unknown
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Expected O, but got Unknown
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dd: Expected O, but got Unknown
			//IL_011c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0127: Unknown result type (might be due to invalid IL or missing references)
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_0146: Unknown result type (might be due to invalid IL or missing references)
			//IL_01cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_0261: Unknown result type (might be due to invalid IL or missing references)
			//IL_0268: Expected O, but got Unknown
			//IL_026b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0277: Unknown result type (might be due to invalid IL or missing references)
			//IL_0284: Unknown result type (might be due to invalid IL or missing references)
			//IL_0231: Unknown result type (might be due to invalid IL or missing references)
			//IL_0236: Unknown result type (might be due to invalid IL or missing references)
			RectTransform val = (RectTransform)((Component)radio).transform;
			RectTransform val2 = (RectTransform)((Component)settings).transform;
			string text = Plugin.PauseMenuButtonPosition.Value?.Trim();
			if (!string.IsNullOrEmpty(text))
			{
				string[] array = text.Split(',');
				if (array.Length == 2 && float.TryParse(array[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && float.TryParse(array[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result2))
				{
					val.anchoredPosition = new Vector2(result, result2);
					return settings;
				}
				Plugin.Log.LogWarning((object)("PauseMenuButtonPosition '" + text + "' is not x,y - placing the RADIO button automatically."));
			}
			if ((Object)(object)((Component)column).GetComponent<LayoutGroup>() != (Object)null)
			{
				return settings;
			}
			List<Button> list = new List<Button>();
			foreach (Transform item in column)
			{
				Transform val3 = item;
				if ((Object)(object)val3 == (Object)(object)((Component)radio).transform || !((Component)val3).gameObject.activeSelf)
				{
					continue;
				}
				Button component = ((Component)val3).GetComponent<Button>();
				if (!((Object)(object)component == (Object)null))
				{
					RectTransform val4 = (RectTransform)(object)((val3 is RectTransform) ? val3 : null);
					if (val4 != null && !(Mathf.Abs(val4.anchoredPosition.x - val2.anchoredPosition.x) > 5f) && !(val4.anchorMin != val2.anchorMin))
					{
						list.Add(component);
					}
				}
			}
			if (!list.Contains(settings))
			{
				list.Add(settings);
			}
			list.Sort((Button a, Button b) => ((RectTransform)((Component)b).transform).anchoredPosition.y.CompareTo(((RectTransform)((Component)a).transform).anchoredPosition.y));
			float num = 0f;
			for (int num2 = 1; num2 < list.Count; num2++)
			{
				float num3 = ((RectTransform)((Component)list[num2 - 1]).transform).anchoredPosition.y - ((RectTransform)((Component)list[num2]).transform).anchoredPosition.y;
				if (num3 > 1f && (num == 0f || num3 < num))
				{
					num = num3;
				}
			}
			if (num <= 0f)
			{
				Rect rect = val2.rect;
				num = ((Rect)(ref rect)).height * 1.15f + 4f;
			}
			Button obj = list[list.Count - 1];
			RectTransform val5 = (RectTransform)((Component)obj).transform;
			val.anchoredPosition = new Vector2(val5.anchoredPosition.x, val5.anchoredPosition.y - num);
			Plugin.Verbose(string.Format("Pause menu column: {0}; step {1}", string.Join(", ", list.Select((Button b) => ((Object)b).name + "@" + ((object)((RectTransform)((Component)b).transform).anchoredPosition/*cast due to .constrained prefix*/).ToStri