Decompiled source of PlayerRadio v0.2.3
PlayerRadio.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.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