using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using FishNet; using FishNet.Connection; using FishNet.Managing; using FishNet.Managing.Timing; using FishNet.Object; using HarmonyLib; using HtF.Shared; using UnityEngine; using UnityEngine.Networking; [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("HtF.RadioMusic")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+76aa5112a8a5b0c9897d3f3557b28a045762e8aa")] [assembly: AssemblyProduct("HtF.RadioMusic")] [assembly: AssemblyTitle("HtF.RadioMusic")] [assembly: AssemblyVersion("1.0.0.0")] namespace HtF.Shared { public enum Language { Auto, Chinese, English } public static class Loc { private sealed class Entry { internal string NameZh; internal string NameEn; internal string DescZh; internal string DescEn; } private const string OwnerGuid = "htf.configmenu"; private const string OwnerSection = "介面"; private const string OwnerKey = "語言"; private static readonly Dictionary Entries = new Dictionary(StringComparer.Ordinal); public static Func LocalChoice; private static ConfigEntryBase _remote; private static string _remoteChoice; private static float _nextRemoteRead; public static bool IsEnglish { get; private set; } public static ConfigEntry Bind(ConfigFile file, string section, string key, T defaultValue, string nameEn, string descZh, string descEn = null, AcceptableValueBase range = null) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown Add(key, null, nameEn, descZh, descEn); return file.Bind(section, key, defaultValue, new ConfigDescription(CfgDesc(key), range, Array.Empty())); } public static void Section(string section, string nameEn) { Add(section, null, nameEn, null, null); } public static void EnumValue(object value, string nameZh, string nameEn) { if (value != null) { Add("enum." + value, nameZh, nameEn, null, null); } } public static void Add(string key, string nameZh, string nameEn, string descZh, string descEn) { if (!string.IsNullOrEmpty(key)) { Entries[key] = new Entry { NameZh = nameZh, NameEn = nameEn, DescZh = descZh, DescEn = descEn }; } } public static string Term(string key, bool english, bool wantDesc) { if (key == null || !Entries.TryGetValue(key, out var value)) { return null; } if (wantDesc) { if (!english) { return value.DescZh; } return value.DescEn; } if (!english) { return value.NameZh; } return value.NameEn; } public static string CfgDesc(string key) { if (key == null || !Entries.TryGetValue(key, out var value) || value.DescZh == null) { return ""; } if (value.DescEn != null) { return value.DescZh + "\n" + value.DescEn; } return value.DescZh; } public static void Resolve() { string text = ((LocalChoice != null) ? LocalChoice() : RemoteChoice()); if (text == "English") { IsEnglish = true; } else if (text == "Chinese") { IsEnglish = false; } else { IsEnglish = !GameSpeaksChinese(); } } private static string RemoteChoice() { if (Time.unscaledTime < _nextRemoteRead) { return _remoteChoice; } _nextRemoteRead = Time.unscaledTime + 0.5f; try { if (_remote == null) { if (!Chainloader.PluginInfos.TryGetValue("htf.configmenu", out var value)) { return _remoteChoice = null; } if (value == null || (Object)(object)value.Instance == (Object)null || value.Instance.Config == null) { return _remoteChoice = null; } ConfigEntryBase[] configEntries = value.Instance.Config.GetConfigEntries(); foreach (ConfigEntryBase val in configEntries) { if (val.Definition.Section == "介面" && val.Definition.Key == "語言") { _remote = val; break; } } if (_remote == null) { return _remoteChoice = null; } } _remoteChoice = _remote.BoxedValue?.ToString(); } catch (Exception) { _remoteChoice = null; } return _remoteChoice; } private static bool GameSpeaksChinese() { try { int curLanguage = LocalizationManager.CurLanguage; return curLanguage == 2 || curLanguage == 3; } catch (Exception) { return false; } } public static string P(string zh, string en) { if (!IsEnglish) { return zh; } return en; } } } namespace HtF.RadioMusic { internal static class MusicLibrary { private static readonly Dictionary> ByChannel = new Dictionary>(); private static readonly List Shared = new List(); private static readonly Dictionary Cursor = new Dictionary(); private static readonly List Stale = new List(); internal static bool Ready { get; private set; } internal static string Folder => Path.Combine(Paths.ConfigPath, "HtF.RadioMusic"); internal static int TotalClips { get { int num = Shared.Count; foreach (KeyValuePair> item in ByChannel) { num += item.Value.Count; } return num; } } internal static void EnsureFolder() { try { Directory.CreateDirectory(Folder); string path = Path.Combine(Folder, "把音樂放這裡.txt"); if (!File.Exists(path)) { File.WriteAllText(path, "把 .ogg / .wav / .mp3 放進這個資料夾。\r\n\r\n分配方式:\r\n 直接放在這裡 -> 依序輪流分配給各個頻道\r\n 放進子資料夾 1\\ -> 指定給第 1 個頻道\r\n 放進子資料夾 2\\ -> 指定給第 2 個頻道,依此類推\r\n\r\n一個頻道放多首的話,遊戲中按「下一首」熱鍵(預設 F7)可以切換。\r\n\r\n格式建議用 .ogg——Unity 執行期解碼最穩。\r\n.mp3 不一定所有環境都能載,載不動會在 BepInEx log 留訊息。\r\n\r\n注意:換成自訂音樂後只有你自己聽得到,其他玩家聽到的還是原曲。\r\n(頻率是同步的,音檔是本地的。)\r\n\r\n------------------------------------------------------------\r\n\r\nDrop .ogg / .wav / .mp3 files into this folder.\r\n\r\nHow they are assigned:\r\n loose in this folder -> handed out to the stations in turn\r\n in subfolder 1\\ -> assigned to station 1\r\n in subfolder 2\\ -> assigned to station 2, and so on\r\n\r\nIf a station has several tracks, the Next Track hotkey (F7 by default) cycles them.\r\n\r\n.ogg is the safest format - Unity decodes it most reliably at runtime.\r\n.mp3 does not load everywhere; failures are reported in the BepInEx log.\r\n\r\nNote: custom music is local to you. Everyone else still hears the original tracks.\r\n(The frequency is synced, the audio files are not.)\r\n"); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("建立音樂資料夾失敗:" + ex.Message)); } } internal static IEnumerator LoadAll() { Ready = false; foreach (KeyValuePair> item in ByChannel) { Stale.AddRange(item.Value); } Stale.AddRange(Shared); ByChannel.Clear(); Shared.Clear(); Cursor.Clear(); if (!Directory.Exists(Folder)) { Ready = true; yield break; } string[] dirs; try { dirs = Directory.GetDirectories(Folder); } catch (Exception) { dirs = new string[0]; } for (int i = 0; i < dirs.Length; i++) { if (!int.TryParse(Path.GetFileName(dirs[i]).Trim(), out var channel)) { continue; } channel--; if (channel < 0) { continue; } List list = new List(); foreach (string item2 in AudioFiles(dirs[i])) { AudioClip clip = null; yield return Load(item2, delegate(AudioClip c) { clip = c; }); if (Object.op_Implicit((Object)(object)clip)) { list.Add(clip); } } if (list.Count > 0) { ByChannel[channel] = list; } } foreach (string item3 in AudioFiles(Folder)) { AudioClip clip2 = null; yield return Load(item3, delegate(AudioClip c) { clip2 = c; }); if (Object.op_Implicit((Object)(object)clip2)) { Shared.Add(clip2); } } Ready = true; Plugin.Log.LogInfo((object)("音樂載入完成:共 " + TotalClips + " 首(指定頻道 " + ByChannel.Count + " 個)。")); } internal static void DisposeStale(Func inUse) { int freed = 0; Stale.RemoveAll(delegate(AudioClip clip) { if (!Object.op_Implicit((Object)(object)clip)) { return true; } if (inUse != null && inUse(clip)) { return false; } Object.Destroy((Object)(object)clip); freed++; return true; }); if (freed > 0) { Plugin.Log.LogInfo((object)("已釋放 " + freed + " 首舊曲目的記憶體。")); } } private static IEnumerable AudioFiles(string dir) { string[] all; try { all = Directory.GetFiles(dir); } catch (Exception) { yield break; } Array.Sort(all, (IComparer?)StringComparer.OrdinalIgnoreCase); for (int i = 0; i < all.Length; i++) { switch (Path.GetExtension(all[i]).ToLowerInvariant()) { case ".ogg": case ".wav": case ".mp3": yield return all[i]; break; } } } private static IEnumerator Load(string path, Action onDone) { string text = Path.GetExtension(path).ToLowerInvariant(); AudioType val = ((text == ".ogg") ? ((AudioType)14) : ((!(text == ".wav")) ? ((AudioType)13) : ((AudioType)20))); string text2 = "file:///" + path.Replace("\\", "/").Replace("#", "%23").Replace("?", "%3F"); UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip(text2, val); try { ((DownloadHandlerAudioClip)req.downloadHandler).streamAudio = false; yield return req.SendWebRequest(); if ((int)req.result != 1) { Plugin.Log.LogWarning((object)("載不動 " + Path.GetFileName(path) + ":" + req.error)); onDone(null); yield break; } AudioClip content; try { content = DownloadHandlerAudioClip.GetContent(req); } catch (Exception ex) { Plugin.Log.LogWarning((object)("解碼失敗 " + Path.GetFileName(path) + ":" + ex.Message)); onDone(null); yield break; } if (!Object.op_Implicit((Object)(object)content) || content.length <= 0.01f) { Plugin.Log.LogWarning((object)("略過 " + Path.GetFileName(path) + "(長度為 0 或解碼不完整)")); onDone(null); yield break; } ((Object)content).name = Path.GetFileNameWithoutExtension(path); onDone(content); } finally { ((IDisposable)req)?.Dispose(); } } internal static AudioClip ClipFor(int channelIndex, int totalChannels) { List list = TracksFor(channelIndex, totalChannels); if (list == null || list.Count == 0) { return null; } if (!Cursor.TryGetValue(channelIndex, out var value)) { value = 0; } return list[(value % list.Count + list.Count) % list.Count]; } internal static void Advance(int channelIndex) { Cursor.TryGetValue(channelIndex, out var value); Cursor[channelIndex] = value + 1; } internal static void AdvanceAll() { List list = new List(Cursor.Keys); for (int i = 0; i < list.Count; i++) { Cursor[list[i]] = Cursor[list[i]] + 1; } for (int j = 0; j < 16; j++) { if (!Cursor.ContainsKey(j)) { Cursor[j] = 1; } } } private static bool HasExplicit(int channelIndex) { if (ByChannel.TryGetValue(channelIndex, out var value)) { return value.Count > 0; } return false; } internal static List TracksFor(int channelIndex, int totalChannels) { if (ByChannel.TryGetValue(channelIndex, out var value) && value.Count > 0) { return value; } if (Shared.Count == 0 || totalChannels <= 0) { return null; } int num = 0; for (int i = 0; i < channelIndex; i++) { if (!HasExplicit(i)) { num++; } } int num2 = 0; for (int j = 0; j < totalChannels; j++) { if (!HasExplicit(j)) { num2++; } } if (num2 <= 0) { return null; } List list = new List(); for (int k = num; k < Shared.Count; k += num2) { list.Add(Shared[k]); } if (list.Count <= 0) { return null; } return list; } internal static int ChannelsNeeded() { int num = -1; foreach (KeyValuePair> item in ByChannel) { if (item.Value.Count > 0 && item.Key > num) { num = item.Key; } } int num2 = 0; int num3 = 0; while (num3 < Shared.Count) { if (!HasExplicit(num2)) { num3++; } num2++; } return Mathf.Max(num2, num + 1); } } [BepInPlugin("htf.radiomusic", "HtF Radio Music", "1.0.0")] public class Plugin : BaseUnityPlugin { public const string Guid = "htf.radiomusic"; internal static ManualLogSource Log; internal static ConfigEntry NoiseVolume; internal static ConfigEntry MusicVolume; internal static ConfigEntry PlayThroughBoss; internal static ConfigEntry AutoAdvance; internal static ConfigEntry SyncPlayback; internal static ConfigEntry ChannelCount; internal static ConfigEntry StationWidth; internal static ConfigEntry NextTrackKey; internal static ConfigEntry ReloadKey; private Harmony _harmony; private void Awake() { //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01f5: Unknown result type (might be due to invalid IL or missing references) //IL_0249: Unknown result type (might be due to invalid IL or missing references) //IL_0253: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Loc.Section("聲音", "Audio"); Loc.Section("頻道", "Stations"); Loc.Section("按鍵", "Keys"); NoiseVolume = Loc.Bind(((BaseUnityPlugin)this).Config, "聲音", "雜訊音量倍率", 1f, "Static Noise Volume", "收音機沒調準時的「沙沙」聲。0 = 完全關掉。\n這個雜訊是遊戲刻意做的:Radio.ApplyVolume 依「最接近頻道的準度」算出音量,\n誤差 0.5 以內完全沒有雜訊,超過 1.5 就是全雜訊。", "The hiss you hear when the radio is off-station. 0 = silence it completely.\nThat hiss is deliberate: Radio.ApplyVolume derives it from how close you are to the nearest station,\nso within 0.5 there is no noise at all, and past 1.5 it is pure noise.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 3f)); MusicVolume = Loc.Bind(((BaseUnityPlugin)this).Config, "聲音", "音樂音量倍率", 1f, "Music Volume", "頻道音量。遊戲原本的基準是 0.3。", "Station volume. The vanilla baseline is 0.3.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 5f)); PlayThroughBoss = Loc.Bind(((BaseUnityPlugin)this).Config, "聲音", "Boss 出現時繼續播放", defaultValue: false, "Keep Playing During Boss", "遊戲預設在 Boss 登場時把收音機整個靜音(頻道和雜訊都歸零)。\n打開這個選項會把音量重新算回來,讓音樂繼續。", "By default the game mutes the whole radio when a boss shows up (both stations and noise drop to zero).\nTurn this on to recompute the volume and keep the music going."); ChannelCount = Loc.Bind(((BaseUnityPlugin)this).Config, "頻道", "頻道數", 0, "Station Count", "−1 = 不改(用遊戲原本的頻道數)。0 = 自動,跟著曲目數走,但不超過波段放得下的數量。其他數字 = 指定幾個。\n頻道會平均攤在 88–108 上,所以「一首歌一個頻率」就是設 0。", "−1 = leave the game's own stations alone. 0 = automatic: one per track, capped at however many fit in the band. Any other number sets it explicitly.\nStations are spread evenly across 88–108, so \"one song per frequency\" is just 0.", (AcceptableValueBase)(object)new AcceptableValueRange(-1, 20)); StationWidth = Loc.Bind(((BaseUnityPlugin)this).Config, "頻道", "頻道寬度", 0f, "Station Width", "一台佔多寬。0 = 自動:跟著頻道間距走,讓相鄰的台剛好碰不到彼此。\n1 = 遊戲原本的寬度(誤差 0.5 以內滿音量、到 1.5 才完全消失),所以原版兩台要隔 3.0 才乾淨——88–108 只放得下 7 台。\n把它調小,乾淨間距也跟著變成 3×寬度,台數再多都分得開;代價是調台越來越精細(真的 FM 收音機就是這樣)。", "How wide one station is. 0 = automatic: derived from the station spacing so neighbours just barely stop overlapping.\n1 = the game's own width (full volume within 0.5, silent past 1.5), which is why vanilla needs 3.0 between stations — only 7 fit in 88–108.\nLower it and the clean spacing becomes 3x the width, so any number of stations can be separated; the cost is finer tuning, which is what a real FM dial feels like anyway.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 2f)); AutoAdvance = Loc.Bind(((BaseUnityPlugin)this).Config, "聲音", "自動接下一首", defaultValue: true, "Auto-Advance Playlist", "一個頻道分到多首歌時,像真的電台一樣自己接著播下去。\n關掉的話一個頻道同時只會有一首,其餘要按「下一首」熱鍵才聽得到(這是舊版的行為)。\n歌曲本來就是輪流平均分給各頻道的;打開這個之後,多出來的歌不會再被藏在熱鍵後面。\n注意:「頻道數」是自動時每個頻道本來就只有一首歌,這個設定和「下一首」熱鍵都不會有作用——要它們有意義,頻道數得少於曲目數(設 −1 用遊戲原本的頻道就是)。", "When a station has more than one track, play them back to back like a real radio station.\nTurn it off and each station only ever holds one track, with the rest reachable only through the Next Track hotkey (the old behaviour).\nTracks are spread evenly across the stations either way; this just stops the extra ones from hiding behind a hotkey."); SyncPlayback = Loc.Bind(((BaseUnityPlugin)this).Config, "聲音", "同步播放", defaultValue: false, "Listen Together", "讓同一個房間的人聽到同一首、同一個位置。\n前提:每個人都要裝這個 mod,而且音樂資料夾的內容要一模一樣——mod 沒辦法替你發音檔,這一點只能自己約好。\n原理:頻道的曲目串成一條連續的時間軸(像電台節目表),由 FishNet 的網路時間決定現在播到哪裡。純計算,不送任何封包。\n這是「自動接下一首」的延伸:節目表本身用本機時鐘就成立,打開這個只是把時鐘換成網路時間,讓所有人的節目表對齊。\n打開後「下一首」會失效——跳過是本機動作,沒辦法讓其他人跟著跳。", "Make everyone in the lobby hear the same track at the same position.\nRequires everyone to install this mod and have identical music folders — the mod cannot hand out audio files, so that part is up to you.\nHow it works: each station's tracks form one continuous timeline (like a real station's schedule), and FishNet's network time decides where playback currently is. Pure computation, no packets.\nThis builds on Auto-Advance: the schedule itself works off a local clock, and turning this on simply swaps that clock for network time so everyone's schedule lines up.\nWhile this is on, Next Track does nothing: skipping is a local action and cannot be shared."); NextTrackKey = Loc.Bind(((BaseUnityPlugin)this).Config, "按鍵", "下一首", new KeyboardShortcut((KeyCode)288, Array.Empty()), "Next Track", "跳過目前這首。「自動接下一首」開著時是把節目表推到下一首,關著時是切換這個頻道當前的曲目。", "Skip the current track. With Auto-Advance on it pushes the schedule to the next track; with it off it switches which track the station is holding."); ReloadKey = Loc.Bind(((BaseUnityPlugin)this).Config, "按鍵", "重新載入音樂", new KeyboardShortcut((KeyCode)289, Array.Empty()), "Reload Music", "不用重開遊戲就能套用資料夾裡新增的檔案。", "Pick up files you just added to the folder without restarting the game."); ((BaseUnityPlugin)this).Config.SettingChanged += delegate(object s, SettingChangedEventArgs e) { if ((object)e.ChangedSetting == ChannelCount || (object)e.ChangedSetting == StationWidth) { RadioPatcher.RebuildAll(); } }; MusicLibrary.EnsureFolder(); _harmony = new Harmony("htf.radiomusic"); _harmony.PatchAll(typeof(RadioPatcher)); RadioPatcher.VerifyTargets(); ((MonoBehaviour)this).StartCoroutine(LoadThenApply()); Log.LogInfo((object)("Radio Music 已載入。音樂資料夾:" + MusicLibrary.Folder)); } private void OnDestroy() { if (_harmony != null) { _harmony.UnpatchSelf(); } } private void Update() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: 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_0089: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = NextTrackKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { if (SyncPlayback.Value) { Log.LogInfo((object)"「同步播放」開著時不能切歌——跳過是本機動作,沒辦法讓其他人跟著跳。"); } else if (AutoAdvance.Value) { if (RadioPatcher.SkipCurrent()) { Log.LogInfo((object)"已跳過這首。"); } else { Log.LogInfo((object)"每個頻道都只有一首歌,沒有下一首可跳——「頻道數」設成自動時,一首歌就是一個頻率。"); } } else { MusicLibrary.AdvanceAll(); RadioPatcher.ApplyToAll(); Log.LogInfo((object)"已切到下一首。"); } } value = ReloadKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { ((MonoBehaviour)this).StopAllCoroutines(); ((MonoBehaviour)this).StartCoroutine(LoadThenApply()); } RadioPatcher.TickPlaylist(); } private IEnumerator LoadThenApply() { yield return MusicLibrary.LoadAll(); Sync.ClearSkips(); if (MusicLibrary.TotalClips == 0) { Log.LogInfo((object)"音樂資料夾裡沒有可用的檔案,收音機維持原曲。"); } RadioPatcher.RebuildAll(); MusicLibrary.DisposeStale(RadioPatcher.IsClipInUse); } } [HarmonyPatch] internal static class RadioPatcher { internal const string ApplyVolumeName = "ApplyVolume"; internal const string OnBossDeathName = "OnBossDeath"; private static MethodInfo _mApplyVolume; private static FieldInfo _fChannels; private static FieldInfo _fNoise; private static FieldInfo _fRadioVol; private static FieldInfo _fLocalFreq; private static FieldInfo _fChannelSource; private static FieldInfo _fStick; private static FieldInfo _fFreqMinMax; private static bool _resolved; private static readonly List Live = new List(); private const float DriftTolerance = 1f; [HarmonyPatch(typeof(Radio), "OnStartClient")] [HarmonyPostfix] private static void Radio_OnStartClient_Postfix(Radio __instance) { if (!Live.Contains(__instance)) { Live.Add(__instance); } Stations.Rebuild(__instance, Channels(__instance), MusicLibrary.ChannelsNeeded()); ApplyClips(__instance); Refresh(__instance); } internal static void SetChannels(Radio radio, RadioChannel[] channels) { Resolve(); if (!(_fChannels == null) && Object.op_Implicit((Object)(object)radio) && channels != null) { _fChannels.SetValue(radio, channels); } } internal static void SetSource(RadioChannel channel, AudioSource source) { Resolve(); if (!(_fChannelSource == null) && channel != null) { _fChannelSource.SetValue(channel, source); } } internal static Vector2 FreqRange(Radio radio) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) Resolve(); if (_fFreqMinMax == null || !Object.op_Implicit((Object)(object)radio)) { return new Vector2(88f, 108f); } return (Vector2)_fFreqMinMax.GetValue(radio); } internal static void RebuildAll() { Live.RemoveAll((Radio r) => !Object.op_Implicit((Object)(object)r)); Stations.PruneDead(); for (int num = 0; num < Live.Count; num++) { Stations.Rebuild(Live[num], Channels(Live[num]), MusicLibrary.ChannelsNeeded()); ApplyClips(Live[num]); Refresh(Live[num]); } } [HarmonyPatch(typeof(BossManager), "OnBossDeath")] [HarmonyPostfix] private static void BossManager_OnBossDeath_Postfix() { RefreshAll(); } private static void RefreshAll() { Live.RemoveAll((Radio r) => !Object.op_Implicit((Object)(object)r)); for (int num = 0; num < Live.Count; num++) { Refresh(Live[num]); } } private static void Refresh(Radio radio) { if (!Object.op_Implicit((Object)(object)radio)) { return; } if (_mApplyVolume == null) { _mApplyVolume = AccessTools.Method(typeof(Radio), "ApplyVolume", (Type[])null, (Type[])null); } if (_mApplyVolume == null) { return; } try { _mApplyVolume.Invoke(radio, null); } catch (Exception ex) { Plugin.Log.LogWarning((object)("重算收音機音量失敗:" + ex.Message)); } } [HarmonyPatch(typeof(Radio), "ApplyVolume")] [HarmonyPostfix] private static void Radio_ApplyVolume_Postfix(Radio __instance) { Resolve(); if (_fNoise == null) { return; } try { bool flag = Object.op_Implicit((Object)(object)BossManager.Boss); if (flag && Plugin.PlayThroughBoss.Value) { Recompute(__instance); return; } if (!flag && Stations.HasCustomWidth()) { Recompute(__instance); return; } object? value = _fNoise.GetValue(__instance); AudioSource val = (AudioSource)((value is AudioSource) ? value : null); if (Object.op_Implicit((Object)(object)val)) { val.volume *= Plugin.NoiseVolume.Value; } if (flag || !(Math.Abs(Plugin.MusicVolume.Value - 1f) > 0.0001f)) { return; } RadioChannel[] array = Channels(__instance); if (array == null) { return; } for (int i = 0; i < array.Length; i++) { AudioSource val2 = SourceOf(array[i]); if (Object.op_Implicit((Object)(object)val2)) { val2.volume *= Plugin.MusicVolume.Value; } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("套用音量失敗:" + ex.Message)); } } private static void Recompute(Radio radio) { RadioChannel[] array = Channels(radio); if (array == null || _fLocalFreq == null || _fRadioVol == null) { return; } float num = ((Object.op_Implicit((Object)(object)((Item)radio).Holder) && ((NetworkBehaviour)((Item)radio).Holder).Owner != (NetworkConnection)null && ((NetworkBehaviour)((Item)radio).Holder).Owner.IsLocalClient) ? ((float)_fLocalFreq.GetValue(radio)) : radio._frequency.Value); float num2 = (float)_fRadioVol.GetValue(radio); float num3 = 0f; try { num3 = (float)((NetworkBehaviour)radio).TimeManager.TicksToTime((TickType)0); } catch (Exception) { } float num4 = Mathf.Max(0.01f, Stations.WidthFactor()); float num5 = 0f; foreach (RadioChannel val in array) { if (val != null) { float num6 = Mathf.Abs(num - val.Frequency) / num4 - 0.5f; num6 = Mathf.Clamp01(1f - num6); float num7 = num6 * num2; float vol = num7 * Plugin.MusicVolume.Value; if (val.IsMuted && num7 > 0.05f) { val.ToggleMute(false, num3); } else if (!val.IsMuted && num7 < 0.05f) { val.ToggleMute(true, 0f); } val.SetVol(vol); if (num6 > num5) { num5 = num6; } } } object? value = _fNoise.GetValue(radio); AudioSource val2 = (AudioSource)((value is AudioSource) ? value : null); if (Object.op_Implicit((Object)(object)val2)) { val2.volume = (1f - num5) * 0.075f * Plugin.NoiseVolume.Value; } MoveFrequencyStick(radio, num); } private static void MoveFrequencyStick(Radio radio, float freq) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) if (_fStick == null || _fFreqMinMax == null) { return; } try { object? value = _fStick.GetValue(radio); Transform val = (Transform)((value is Transform) ? value : null); if (Object.op_Implicit((Object)(object)val)) { Vector2 val2 = (Vector2)_fFreqMinMax.GetValue(radio); val.localPosition = -Vector3.right * (Mathf.InverseLerp(val2.x, val2.y, freq) * 0.3f); } } catch (Exception) { } } internal static void ApplyToAll() { Live.RemoveAll((Radio r) => !Object.op_Implicit((Object)(object)r)); for (int num = 0; num < Live.Count; num++) { ApplyClips(Live[num]); } } internal static bool IsClipInUse(AudioClip clip) { if (!Object.op_Implicit((Object)(object)clip)) { return false; } for (int i = 0; i < Live.Count; i++) { Radio val = Live[i]; if (!Object.op_Implicit((Object)(object)val)) { continue; } RadioChannel[] array = Channels(val); if (array == null) { continue; } for (int j = 0; j < array.Length; j++) { AudioSource val2 = SourceOf(array[j]); if (Object.op_Implicit((Object)(object)val2) && (Object)(object)val2.clip == (Object)(object)clip) { return true; } } } return false; } private static void ApplyClips(Radio radio) { if (!Object.op_Implicit((Object)(object)radio) || !MusicLibrary.Ready || MusicLibrary.TotalClips == 0) { return; } Resolve(); RadioChannel[] array = Channels(radio); if (array == null) { return; } bool value = Plugin.AutoAdvance.Value; for (int i = 0; i < array.Length; i++) { if (!value || !Sync.Target(i, array.Length, Sync.Clock(i), out var clip, out var offset)) { clip = MusicLibrary.ClipFor(i, array.Length); offset = PositionIn(clip, Sync.Now()); } if (!Object.op_Implicit((Object)(object)clip)) { continue; } AudioSource val = SourceOf(array[i]); if (Object.op_Implicit((Object)(object)val) && !((Object)(object)val.clip == (Object)(object)clip)) { bool isPlaying = val.isPlaying; val.clip = clip; if (isPlaying) { val.time = offset; val.Play(); } } } } private static float PositionIn(AudioClip clip, double now) { if (!Object.op_Implicit((Object)(object)clip) || now < 0.0 || clip.length <= 0.01f) { return 0f; } return Mathf.Clamp((float)(now % (double)clip.length), 0f, clip.length - 0.01f); } internal static void TickPlaylist() { if (Plugin.AutoAdvance == null || !Plugin.AutoAdvance.Value || !MusicLibrary.Ready || MusicLibrary.TotalClips == 0) { return; } Live.RemoveAll((Radio r) => !Object.op_Implicit((Object)(object)r)); for (int num = 0; num < Live.Count; num++) { RadioChannel[] array = Channels(Live[num]); if (array == null) { continue; } for (int num2 = 0; num2 < array.Length; num2++) { if (!Sync.Target(num2, array.Length, Sync.Clock(num2), out var clip, out var offset)) { continue; } AudioSource val = SourceOf(array[num2]); if (!Object.op_Implicit((Object)(object)val)) { continue; } try { if ((Object)(object)val.clip != (Object)(object)clip) { bool isPlaying = val.isPlaying; val.clip = clip; val.time = offset; if (isPlaying) { val.Play(); } } else if (val.isPlaying && Mathf.Abs(val.time - offset) > 1f) { val.time = offset; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("套用節目表位置失敗:" + ex.Message)); } } } } internal static bool SkipCurrent() { if (!MusicLibrary.Ready || MusicLibrary.TotalClips == 0) { return false; } Live.RemoveAll((Radio r) => !Object.op_Implicit((Object)(object)r)); HashSet hashSet = new HashSet(); bool result = false; for (int num = 0; num < Live.Count; num++) { RadioChannel[] array = Channels(Live[num]); if (array == null) { continue; } for (int num2 = 0; num2 < array.Length; num2++) { if (!hashSet.Add(num2)) { continue; } List list = MusicLibrary.TracksFor(num2, array.Length); if (list != null && list.Count >= 2) { result = true; if (Sync.Target(num2, array.Length, Sync.Clock(num2), out var clip, out var offset)) { Sync.Skip(num2, clip.length - offset); } } } } TickPlaylist(); return result; } private static RadioChannel[] Channels(Radio radio) { Resolve(); if (_fChannels == null) { return null; } try { return _fChannels.GetValue(radio) as RadioChannel[]; } catch (Exception) { return null; } } internal static AudioSource SourceOf(RadioChannel channel) { Resolve(); if (_fChannelSource == null || channel == null) { return null; } try { object? value = _fChannelSource.GetValue(channel); return (AudioSource)((value is AudioSource) ? value : null); } catch (Exception) { return null; } } private static void Resolve() { if (!_resolved) { _resolved = true; _fChannels = AccessTools.Field(typeof(Radio), "_channels"); _fNoise = AccessTools.Field(typeof(Radio), "_noiseSource"); _fRadioVol = AccessTools.Field(typeof(Radio), "_radioVol"); _fLocalFreq = AccessTools.Field(typeof(Radio), "_localFrequency"); _fChannelSource = AccessTools.Field(typeof(RadioChannel), "_channelSource"); _fStick = AccessTools.Field(typeof(Radio), "_frequencyStick"); _fFreqMinMax = AccessTools.Field(typeof(Radio), "FreqMinMax"); if (_fChannels == null || _fChannelSource == null) { Plugin.Log.LogError((object)"Radio 的欄位名對不上(遊戲可能又更新了),換曲不會生效。"); } } } internal static void VerifyTargets() { if (AccessTools.Method(typeof(Radio), "ApplyVolume", (Type[])null, (Type[])null) == null) { Plugin.Log.LogWarning((object)"Radio.ApplyVolume 不存在了(遊戲更新?)——雜訊與音量設定不會生效。"); } if (AccessTools.Method(typeof(BossManager), "OnBossDeath", (Type[])null, (Type[])null) == null) { Plugin.Log.LogWarning((object)"BossManager.OnBossDeath 不存在了(遊戲更新?)——Boss 被打死之後收音機不會自己恢復。"); } } } internal static class Stations { internal const float CleanSpacing = 3f; private const int MaxAuto = 30; private const float MinWidth = 0.1f; internal static float Spacing; private static FieldInfo _fFrequency; private static bool _resolved; private static readonly Dictionary> Created = new Dictionary>(); private static readonly Dictionary Vanilla = new Dictionary(); private static bool _warnedFallback; private static int _announcedFor = -1; internal static void Rebuild(Radio radio, RadioChannel[] current, int wantedStations) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)radio) || current == null || current.Length == 0) { return; } Resolve(); if (_fFrequency == null) { return; } int num = Desired(BaseCount(radio, current), wantedStations); if (num <= 0) { return; } Vector2 band = Band(radio); try { RadioChannel[] array = Resize(radio, current, num) ?? current; Spread(array, band); RadioPatcher.SetChannels(radio, array); Announce(array.Length, band); } catch (Exception ex) { Plugin.Log.LogWarning((object)("重建收音機頻道失敗,維持原本的頻道:" + ex.Message)); } } internal static void PruneDead() { Forget(Created); Forget(Vanilla); } private static void Forget(Dictionary map) { List list = null; foreach (KeyValuePair item in map) { if (!Object.op_Implicit((Object)(object)item.Key)) { (list ?? (list = new List())).Add(item.Key); } } if (list != null) { for (int i = 0; i < list.Count; i++) { map.Remove(list[i]); } } } private static int Desired(int vanilla, int wantedStations) { int num = ((Plugin.ChannelCount != null) ? Plugin.ChannelCount.Value : (-1)); if (num < 0) { return 0; } if (num > 0) { return num; } if (wantedStations <= 0) { return 0; } return Mathf.Clamp(wantedStations, vanilla, 30); } private static int BaseCount(Radio radio, RadioChannel[] current) { if (Vanilla.TryGetValue(radio, out var value)) { return value; } List value2; int num = ((Created.TryGetValue(radio, out value2) && value2 != null) ? value2.Count : 0); value = Mathf.Max(1, current.Length - num); Vanilla[radio] = value; return value; } internal static float WidthFactor() { float num = ((Plugin.StationWidth != null) ? Plugin.StationWidth.Value : 0f); if (num > 0f) { return num; } if (Spacing <= 0f) { return 1f; } return Mathf.Clamp(Spacing / 3f, 0.1f, 1f); } internal static bool HasCustomWidth() { return Mathf.Abs(WidthFactor() - 1f) > 0.0001f; } private static RadioChannel[] Resize(Radio radio, RadioChannel[] current, int want) { //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Expected O, but got Unknown List list = new List(current); if (!Created.TryGetValue(radio, out var value)) { value = (Created[radio] = new List()); } while (list.Count > want && value.Count > 0) { AudioSource val = value[value.Count - 1]; value.RemoveAt(value.Count - 1); for (int num = list.Count - 1; num >= 0; num--) { if (!((Object)(object)RadioPatcher.SourceOf(list[num]) != (Object)(object)val)) { list.RemoveAt(num); break; } } if (Object.op_Implicit((Object)(object)val)) { Object.Destroy((Object)(object)((Component)val).gameObject); } } AudioSource val2 = RadioPatcher.SourceOf(current[0]); if (!Object.op_Implicit((Object)(object)val2)) { if (list.Count != current.Length) { return list.ToArray(); } return null; } while (list.Count < want) { AudioSource val3 = Clone(val2); if (!Object.op_Implicit((Object)(object)val3)) { break; } RadioChannel val4 = new RadioChannel(); RadioPatcher.SetSource(val4, val3); try { val4.ToggleMute(true, 0f); } catch (Exception ex) { Plugin.Log.LogWarning((object)("新頻道初始靜音失敗:" + ex.Message)); } list.Add(val4); value.Add(val3); } if (list.Count != current.Length) { return list.ToArray(); } return null; } private static AudioSource Clone(AudioSource template) { //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) GameObject gameObject = ((Component)template).gameObject; if (!Object.op_Implicit((Object)(object)gameObject.GetComponentInChildren(true))) { GameObject val = Object.Instantiate(gameObject, gameObject.transform.parent); val.transform.localPosition = gameObject.transform.localPosition; val.transform.localRotation = gameObject.transform.localRotation; val.transform.localScale = gameObject.transform.localScale; ((Object)val).name = ((Object)gameObject).name + " (HtF)"; AudioSource component = val.GetComponent(); if (!Object.op_Implicit((Object)(object)component)) { Object.Destroy((Object)(object)val); return null; } component.volume = 0f; return component; } WarnFallback(); GameObject val2 = new GameObject("RadioChannel (HtF)"); val2.transform.SetParent(gameObject.transform, false); AudioSource val3 = val2.AddComponent(); CopySettings(template, val3); val3.volume = 0f; return val3; } private static void WarnFallback() { if (!_warnedFallback) { _warnedFallback = true; Plugin.Log.LogInfo((object)"頻道的 AudioSource 和收音機掛在同一個物件上,改用逐項抄設定的方式生新頻道(不能直接複製那個物件,會連整台收音機一起複製)。新頻道的音色若和原本不一樣,原因在這裡。"); } } private static void CopySettings(AudioSource from, AudioSource to) { //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) to.clip = from.clip; to.outputAudioMixerGroup = from.outputAudioMixerGroup; to.mute = from.mute; to.bypassEffects = from.bypassEffects; to.bypassListenerEffects = from.bypassListenerEffects; to.bypassReverbZones = from.bypassReverbZones; to.playOnAwake = from.playOnAwake; to.loop = from.loop; to.priority = from.priority; to.pitch = from.pitch; to.panStereo = from.panStereo; to.spatialBlend = from.spatialBlend; to.spatialize = from.spatialize; to.spatializePostEffects = from.spatializePostEffects; to.reverbZoneMix = from.reverbZoneMix; to.dopplerLevel = from.dopplerLevel; to.spread = from.spread; to.rolloffMode = from.rolloffMode; to.minDistance = from.minDistance; to.maxDistance = from.maxDistance; to.ignoreListenerVolume = from.ignoreListenerVolume; to.ignoreListenerPause = from.ignoreListenerPause; to.velocityUpdateMode = from.velocityUpdateMode; try { to.SetCustomCurve((AudioSourceCurveType)0, from.GetCustomCurve((AudioSourceCurveType)0)); to.SetCustomCurve((AudioSourceCurveType)1, from.GetCustomCurve((AudioSourceCurveType)1)); to.SetCustomCurve((AudioSourceCurveType)2, from.GetCustomCurve((AudioSourceCurveType)2)); to.SetCustomCurve((AudioSourceCurveType)3, from.GetCustomCurve((AudioSourceCurveType)3)); } catch (Exception) { } } private static void Spread(RadioChannel[] channels, Vector2 band) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) if (channels.Length == 1) { _fFrequency.SetValue(channels[0], (band.x + band.y) * 0.5f); Spacing = band.y - band.x; return; } float num = (band.y - band.x) / (float)(channels.Length - 1); for (int i = 0; i < channels.Length; i++) { _fFrequency.SetValue(channels[i], band.x + num * (float)i); } Spacing = num; } private static Vector2 Band(Radio radio) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) Vector2 val = default(Vector2); ((Vector2)(ref val))..ctor(88f, 108f); try { Vector2 val2 = RadioPatcher.FreqRange(radio); return (val2.y - val2.x > 1f) ? val2 : val; } catch (Exception) { return val; } } private static void Announce(int count, Vector2 band) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (_announcedFor != count) { _announcedFor = count; float num = ((count > 1) ? ((band.y - band.x) / (float)(count - 1)) : 0f); float num2 = WidthFactor(); string text = "電台頻道改成 " + count + " 個," + band.x.ToString("0.#") + "–" + band.y.ToString("0.#") + " 之間每 " + num.ToString("0.##") + " 一台,頻道寬度 ×" + num2.ToString("0.###") + "。"; if (count > 1 && num < 3f * num2 - 0.001f) { Plugin.Log.LogWarning((object)(text + " 這個間距在目前的寬度下仍然會互相滲音——把「頻道寬度」調小(或設 0 讓它自動跟著間距走)就會分得開。")); } else { Plugin.Log.LogInfo((object)text); } } } private static void Resolve() { if (!_resolved) { _resolved = true; _fFrequency = AccessTools.Field(typeof(RadioChannel), "_frequency"); if (_fFrequency == null) { Plugin.Log.LogWarning((object)"找不到 RadioChannel._frequency(遊戲更新?)——頻道數設定不會生效。"); } } } } internal static class Sync { private const float MinLength = 0.05f; private static readonly Dictionary Skips = new Dictionary(); internal static double Now() { try { if (NetworkManager.Instances.Count == 0) { return -1.0; } TimeManager timeManager = InstanceFinder.TimeManager; if ((Object)(object)timeManager == (Object)null) { return -1.0; } return timeManager.TicksToTime((TickType)0); } catch (Exception) { return -1.0; } } internal static double Clock(int channelIndex) { if (Plugin.SyncPlayback != null && Plugin.SyncPlayback.Value) { double num = Now(); if (num >= 0.0) { return num; } } Skips.TryGetValue(channelIndex, out var value); return (double)Time.realtimeSinceStartup + value; } internal static void Skip(int channelIndex, float remaining) { if (!(remaining <= 0f)) { Skips.TryGetValue(channelIndex, out var value); Skips[channelIndex] = value + (double)remaining + 0.05; } } internal static void ClearSkips() { Skips.Clear(); } internal static bool Target(int channelIndex, int totalChannels, double clock, out AudioClip clip, out float offset) { clip = null; offset = 0f; if (clock < 0.0) { return false; } List list = MusicLibrary.TracksFor(channelIndex, totalChannels); if (list == null || list.Count == 0) { return false; } double num = 0.0; for (int i = 0; i < list.Count; i++) { if (Object.op_Implicit((Object)(object)list[i]) && list[i].length >= 0.05f) { num += (double)list[i].length; } } if (num < 0.05000000074505806) { return false; } double num2 = clock % num; if (num2 < 0.0) { num2 += num; } for (int j = 0; j < list.Count; j++) { AudioClip val = list[j]; if (Object.op_Implicit((Object)(object)val) && !(val.length < 0.05f)) { if (num2 < (double)val.length) { clip = val; offset = Mathf.Clamp((float)num2, 0f, val.length - 0.01f); return true; } num2 -= (double)val.length; } } for (int num3 = list.Count - 1; num3 >= 0; num3--) { if (Object.op_Implicit((Object)(object)list[num3]) && !(list[num3].length < 0.05f)) { clip = list[num3]; offset = 0f; return true; } } return false; } } internal static class ModInfo { public const string Name = "HtF Radio Music"; public const string Version = "1.0.0"; } }