using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using FishNet; using FishNet.Broadcast; using FishNet.Managing.Client; using FishNet.Managing.Server; using FishNet.Serializing; using FishNet.Transporting; using HTF.Shared; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("HTFCustomRadio")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.3.0.0")] [assembly: AssemblyInformationalVersion("1.3.0")] [assembly: AssemblyProduct("HTFCustomRadio")] [assembly: AssemblyTitle("HTFCustomRadio")] [assembly: AssemblyVersion("1.3.0.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 HTF.Shared { internal static class AudioLoading { internal static readonly string[] SupportedExtensions = new string[3] { ".ogg", ".wav", ".mp3" }; internal static bool IsSupported(string path) { string ext = Path.GetExtension(path); return Array.Exists(SupportedExtensions, (string e) => string.Equals(e, ext, StringComparison.OrdinalIgnoreCase)); } internal static AudioType TypeOf(string path) { return (AudioType)(Path.GetExtension(path)?.ToLowerInvariant() switch { ".ogg" => 14, ".wav" => 20, ".mp3" => 13, _ => 0, }); } internal static IEnumerator Load(string source, bool stream, Action onLoaded, Action onError) { bool isRemote = source.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || source.StartsWith("https://", StringComparison.OrdinalIgnoreCase); string text; if (isRemote) { text = source; } else { if (!File.Exists(source)) { onError?.Invoke("No existe el archivo: " + source); yield break; } text = new Uri(Path.GetFullPath(source)).AbsoluteUri; } AudioType val = TypeOf(source); if ((int)val == 0) { onError?.Invoke("Formato no soportado: " + source); yield break; } UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(text, val); try { DownloadHandler downloadHandler = request.downloadHandler; DownloadHandlerAudioClip val2 = (DownloadHandlerAudioClip)(object)((downloadHandler is DownloadHandlerAudioClip) ? downloadHandler : null); if (val2 != null) { val2.streamAudio = stream || isRemote; val2.compressed = false; } yield return request.SendWebRequest(); if ((int)request.result != 1) { onError?.Invoke(request.error + " (" + source + ")"); yield break; } AudioClip content; try { content = DownloadHandlerAudioClip.GetContent(request); } catch (Exception ex) { onError?.Invoke(ex.Message + " (" + source + ")"); yield break; } if ((Object)(object)content == (Object)null || (int)content.loadState == 3) { onError?.Invoke("Unity no pudo descodificar " + Path.GetFileName(source) + ". Si es un MP3 con etiquetas raras, reexpórtalo a OGG."); yield break; } ((Object)content).name = (isRemote ? source : Path.GetFileNameWithoutExtension(source)); onLoaded?.Invoke(content); } finally { ((IDisposable)request)?.Dispose(); } } } } namespace HTFCustomRadio { internal class Station { public string Name; public List Tracks = new List(); public bool IsStream; public AudioSource Source; public float Frequency; public int Index; public bool Loading; public float LastStart = -99f; public float ExpectedEnd; public int RecentAdvances; public float WindowStart; public bool Paused; public AudioClip OwnedClip; public string CurrentTrack { get { if (Tracks.Count != 0) { return Path.GetFileNameWithoutExtension(Tracks[Index % Tracks.Count]); } return "(vacía)"; } } } [BepInPlugin("com.hatsuneexo.htfcustomradio", "HTF Custom Radio", "1.3.0")] [BepInProcess("How to Fish.exe")] public class Plugin : BaseUnityPlugin { public const string Guid = "com.hatsuneexo.htfcustomradio"; internal static ManualLogSource Log; internal static Plugin Instance; internal static ConfigEntry Shuffle; internal static ConfigEntry KeepOriginalStations; internal static ConfigEntry StreamStations; internal static ConfigEntry AnnounceTrack; internal static ConfigEntry MusicFolders; internal static ConfigEntry SplitSubfolders; internal static ConfigEntry UseMyMusicFolder; internal static ConfigEntry ReloadKey; internal static ConfigEntry SkipKey; private readonly List _stations = new List(); private readonly List _live = new List(); private object _lastRadio; private static readonly Random Rng = new Random(); private const float MinSecondsBetweenTracks = 1f; private const int MaxAdvancesPerMinute = 30; private const float ResyncToleranceSeconds = 2.5f; private const float BroadcastInterval = 8f; private float _nextBroadcast; private readonly HashSet _missingTracks = new HashSet(StringComparer.OrdinalIgnoreCase); private void Awake() { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_019e: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Instance = this; Shuffle = ((BaseUnityPlugin)this).Config.Bind("General", "Shuffle", true, "Reproduce las pistas de cada emisora en orden aleatorio."); KeepOriginalStations = ((BaseUnityPlugin)this).Config.Bind("General", "KeepOriginalStations", 0, new ConfigDescription("Cuántas emisoras originales del juego dejar intactas antes de empezar a sustituir.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 8), Array.Empty())); AnnounceTrack = ((BaseUnityPlugin)this).Config.Bind("General", "AnnounceTrack", true, "Escribe en el log qué pista empieza a sonar en cada emisora."); UseMyMusicFolder = ((BaseUnityPlugin)this).Config.Bind("General", "UseMyMusicFolder", true, "Usa tu carpeta de música de Windows como origen de emisoras, sin configurar nada. Cada subcarpeta suya se convierte en una emisora."); MusicFolders = ((BaseUnityPlugin)this).Config.Bind("General", "MusicFolders", "", "Carpetas de tu disco que quieres usar como emisoras, separadas por ';'. Ejemplo: C:\\Users\\Usuario\\Music;D:\\Descargas\\Lofi No hace falta copiar ni mover nada: el mod lee de donde estén."); SplitSubfolders = ((BaseUnityPlugin)this).Config.Bind("General", "SplitSubfolders", true, "Si una carpeta de MusicFolders tiene subcarpetas, cada subcarpeta se convierte en una emisora distinta. Si lo apagas, toda la carpeta es una sola emisora."); SkipKey = ((BaseUnityPlugin)this).Config.Bind("General", "SkipKey", (Key)103, "Salta a la siguiente canción de la emisora que estés escuchando en ese momento."); ReloadKey = ((BaseUnityPlugin)this).Config.Bind("General", "ReloadKey", (Key)102, "Vuelve a leer las carpetas sin cerrar el juego. Útil mientras montas tus emisoras."); StreamStations = ((BaseUnityPlugin)this).Config.Bind("Streaming", "StreamStations", "", "Emisoras de internet, formato Nombre=URL separadas por ';'. La URL debe apuntar a un flujo de audio directo (.mp3/.ogg), no a una página web. Ejemplo: Lofi=https://ejemplo.com/stream.mp3;Jazz=https://otro.com/jazz.ogg"); Sync.Enabled = ((BaseUnityPlugin)this).Config.Bind("Multijugador", "SyncWithHost", false, "EXPERIMENTAL. El anfitrión decide qué canción suena y todos la oyen a la vez. Requiere que TODOS tengáis el mod y los mismos archivos de música: por la red solo viaja el nombre de la canción, no el audio. Desactivado por defecto porque no ha podido probarse en una partida real con varios jugadores."); Sync.EnsureSerializers(); DiscoverStations(); if (_stations.Count == 0) { Log.LogInfo((object)"Ninguna emisora configurada. Crea una carpeta por emisora en BepInEx/plugins/HTFCustomRadio/Stations/ y mete ahí tus .ogg o .mp3"); } Harmony val = new Harmony("com.hatsuneexo.htfcustomradio"); val.PatchAll(Assembly.GetExecutingAssembly()); Log.LogInfo((object)(val.GetPatchedMethods().Any() ? "Enganchado a Radio.OnStartClient: coge una radio en la partida." : "AVISO: no encuentro Radio.OnStartClient. El mod no hará nada.")); } private void DiscoverStations() { string pluginPath = Paths.PluginPath; if (Directory.Exists(pluginPath)) { foreach (string item in Directory.GetDirectories(pluginPath, "Stations", SearchOption.AllDirectories).SelectMany(Directory.GetDirectories).OrderBy((string d) => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase)) { List list = Directory.GetFiles(item, "*.*", SearchOption.AllDirectories).Where(AudioLoading.IsSupported).OrderBy((string f) => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase) .ToList(); if (list.Count == 0) { Log.LogWarning((object)("Emisora '" + Path.GetFileName(item) + "' sin pistas soportadas (.ogg/.wav/.mp3).")); continue; } _stations.Add(new Station { Name = Path.GetFileName(item), Tracks = list }); } } foreach (string item2 in FoldersToScan()) { List list2 = (SplitSubfolders.Value ? Directory.GetDirectories(item2).OrderBy((string d) => d, StringComparer.OrdinalIgnoreCase).ToList() : new List()); foreach (string item3 in (list2.Count > 0) ? list2 : new List { item2 }) { List list3 = SafeFiles(item3); if (list3.Count != 0) { _stations.Add(new Station { Name = Path.GetFileName(item3.TrimEnd(new char[1] { Path.DirectorySeparatorChar })), Tracks = list3 }); } } } string[] array = StreamStations.Value.Split(new char[1] { ';' }); for (int num = 0; num < array.Length; num++) { string[] array2 = array[num].Split(new char[1] { '=' }, 2); if (array2.Length == 2) { string text = array2[0].Trim(); string text2 = array2[1].Trim(); if (text.Length != 0 && text2.Length != 0) { _stations.Add(new Station { Name = text, IsStream = true, Tracks = new List { text2 } }); } } } if (_stations.Count > 0) { Log.LogInfo((object)string.Format("{0} emisora(s): {1}", _stations.Count, string.Join(", ", _stations.Select((Station s) => s.Name)))); } } private List FoldersToScan() { List result = new List(); HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); if (UseMyMusicFolder.Value) { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic); if (!string.IsNullOrEmpty(folderPath) && Directory.Exists(folderPath)) { Add(folderPath, warnIfMissing: false); Log.LogInfo((object)("Usando tu carpeta de música: " + folderPath)); } } string[] array = MusicFolders.Value.Split(new char[1] { ';' }); for (int i = 0; i < array.Length; i++) { Add(array[i], warnIfMissing: true); } return result; void Add(string path, bool warnIfMissing) { if (!string.IsNullOrEmpty(path)) { string fullPath; try { fullPath = Path.GetFullPath(path.Trim().Trim(new char[1] { '"' })); } catch { Log.LogWarning((object)("Ruta inválida: " + path)); return; } if (!Directory.Exists(fullPath)) { if (warnIfMissing) { Log.LogWarning((object)("No existe la carpeta: " + fullPath)); } } else if (seen.Add(fullPath)) { result.Add(fullPath); } } } } private List SafeFiles(string dir) { try { return Directory.GetFiles(dir, "*.*", SearchOption.AllDirectories).Where(AudioLoading.IsSupported).OrderBy((string f) => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase) .ToList(); } catch (Exception ex) { Log.LogWarning((object)("No pude leer " + dir + ": " + ex.Message)); return new List(); } } private void HandleSkipKey() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) Keyboard current = Keyboard.current; if (current == null) { return; } KeyControl val = current[SkipKey.Value]; if (val == null || !((ButtonControl)val).wasPressedThisFrame) { return; } if (_live.Count == 0) { Log.LogInfo((object)"No hay ninguna emisora activa. Coge una radio primero."); return; } Station station = null; float num = 0.01f; foreach (Station item in _live) { if (!((Object)(object)item.Source == (Object)null) && !(item.Source.volume <= num)) { num = item.Source.volume; station = item; } } if (station == null) { Log.LogInfo((object)"No estás sintonizado en ninguna emisora; gira el dial."); } else if (station.IsStream) { Log.LogInfo((object)("[" + station.Name + "] es una emisora de internet: no hay pistas que saltar.")); } else if (!station.Loading) { if (station.Tracks.Count < 2) { Log.LogInfo((object)("[" + station.Name + "] solo tiene una canción; añade más a la carpeta.")); return; } Advance(station); station.ExpectedEnd = 0f; station.RecentAdvances = 0; station.Paused = false; ((MonoBehaviour)this).StartCoroutine(PlayCurrent(station)); } } private void HandleReloadKey() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) Keyboard current = Keyboard.current; if (current == null) { return; } KeyControl val = current[ReloadKey.Value]; if (val != null && ((ButtonControl)val).wasPressedThisFrame) { ((MonoBehaviour)this).StopAllCoroutines(); _live.Clear(); _stations.Clear(); DiscoverStations(); if (_lastRadio != null) { TakeOverRadio(_lastRadio); Log.LogInfo((object)"Emisoras recargadas."); } else { Log.LogInfo((object)"Emisoras recargadas. Coge una radio para oírlas."); } } } internal void TakeOverRadio(object radio) { _lastRadio = radio; if (_stations.Count == 0) { return; } ((MonoBehaviour)this).StopAllCoroutines(); foreach (Station station2 in _stations) { station2.Loading = false; station2.Paused = false; station2.RecentAdvances = 0; } if (AccessTools.Field(radio.GetType(), "_channels")?.GetValue(radio) is Array { Length: not 0 } array) { Log.LogInfo((object)$"Radio encontrada con {array.Length} canal(es)."); _live.Clear(); int num = Mathf.Clamp(KeepOriginalStations.Value, 0, array.Length); for (int i = num; i < array.Length; i++) { int num2 = i - num; if (num2 < _stations.Count) { object value = array.GetValue(i); object? obj = AccessTools.Field(value.GetType(), "_channelSource")?.GetValue(value); AudioSource val = (AudioSource)((obj is AudioSource) ? obj : null); if ((Object)(object)val == (Object)null) { Log.LogWarning((object)$"Canal {i} sin AudioSource, lo dejo como está."); continue; } object obj2 = AccessTools.Field(value.GetType(), "_frequency")?.GetValue(value); Station station = _stations[num2]; station.Source = val; station.Frequency = ((obj2 is float num3) ? num3 : 0f); station.Index = (Shuffle.Value ? Rng.Next(station.Tracks.Count) : 0); _live.Add(station); Log.LogInfo((object)$" {station.Frequency:0.0} -> {station.Name} ({station.Tracks.Count} pista(s))"); ((MonoBehaviour)this).StartCoroutine(PlayCurrent(station)); continue; } break; } } else { Log.LogWarning((object)"La radio no expone canales; ¿cambió el juego?"); } } private IEnumerator PlayCurrent(Station station, float startAt = 0f) { if (station.Loading || (Object)(object)station.Source == (Object)null) { yield break; } station.Loading = true; string path = station.Tracks[station.Index % station.Tracks.Count]; yield return AudioLoading.Load(path, station.IsStream, delegate(AudioClip clip) { if (!((Object)(object)station.Source == (Object)null)) { if ((Object)(object)station.OwnedClip != (Object)null && (Object)(object)station.OwnedClip != (Object)(object)clip) { Object.Destroy((Object)(object)station.OwnedClip); } station.OwnedClip = clip; station.Source.clip = clip; station.Source.loop = station.IsStream; station.Source.time = Mathf.Clamp(startAt, 0f, Mathf.Max(0f, clip.length - 0.1f)); station.Source.Play(); station.LastStart = Time.unscaledTime; station.ExpectedEnd = Time.unscaledTime + Mathf.Max(1f, clip.length - station.Source.time); if (AnnounceTrack.Value) { Log.LogInfo((object)("[" + station.Name + "] suena '" + ((Object)clip).name + "'")); } Sync.Announce(station.Name, path, station.Source.time); } }, delegate(string msg) { Log.LogWarning((object)("[" + station.Name + "] " + msg)); Advance(station); }); station.Loading = false; } private bool AllowAdvance(Station station) { if (station.Paused) { return false; } float unscaledTime = Time.unscaledTime; if (unscaledTime - station.WindowStart > 60f) { station.WindowStart = unscaledTime; station.RecentAdvances = 0; } station.RecentAdvances++; if (station.RecentAdvances <= 30) { return true; } station.Paused = true; Log.LogError((object)($"[{station.Name}] ha cambiado de pista {station.RecentAdvances} veces en un " + "minuto, lo cual no es normal. Emisora detenida para no agotar la memoria. Pulsa la tecla de recarga para reactivarla.")); return false; } internal void ApplySync(string stationName, string trackName, float position) { Station station = _live.FirstOrDefault((Station s) => string.Equals(s.Name, stationName, StringComparison.OrdinalIgnoreCase)); if (station == null || station.IsStream) { return; } int num = station.Tracks.FindIndex((string t) => string.Equals(Path.GetFileNameWithoutExtension(t), trackName, StringComparison.OrdinalIgnoreCase)); if (num < 0) { if (_missingTracks.Add(trackName)) { Log.LogWarning((object)("[" + stationName + "] el anfitrión puso '" + trackName + "' y tú no la tienes. Para oír lo mismo necesitáis los mismos archivos.")); } } else { AudioSource source = station.Source; if (station.Index != num || !((Object)(object)source != (Object)null) || !source.isPlaying || !(Mathf.Abs(source.time - position) < 2.5f)) { station.Index = num; station.Paused = false; station.RecentAdvances = 0; ((MonoBehaviour)this).StartCoroutine(PlayCurrent(station, position)); } } } private void BroadcastState() { if (!Sync.IsHost || Time.unscaledTime < _nextBroadcast) { return; } _nextBroadcast = Time.unscaledTime + 8f; foreach (Station item in _live) { if (!item.IsStream && !((Object)(object)item.Source == (Object)null) && !((Object)(object)item.Source.clip == (Object)null) && item.Source.isPlaying) { Sync.Announce(item.Name, item.Tracks[item.Index % item.Tracks.Count], item.Source.time); } } } private void Advance(Station station) { if (station.Tracks.Count != 0) { station.Index = ((Shuffle.Value && station.Tracks.Count > 1) ? NextRandomDifferent(station) : ((station.Index + 1) % station.Tracks.Count)); } } private static int NextRandomDifferent(Station station) { int num = Rng.Next(station.Tracks.Count - 1); if (num < station.Index) { return num; } return num + 1; } private void Update() { HandleReloadKey(); HandleSkipKey(); Sync.TryRegisterClient(); BroadcastState(); if (Sync.FollowingHost) { return; } foreach (Station item in _live) { if (!item.Loading && !item.IsStream) { AudioSource source = item.Source; if (!((Object)(object)source == (Object)null) && !((Object)(object)source.clip == (Object)null) && ((Behaviour)source).isActiveAndEnabled && !source.isPlaying && !(Time.unscaledTime < item.ExpectedEnd - 0.25f) && AllowAdvance(item)) { Advance(item); ((MonoBehaviour)this).StartCoroutine(PlayCurrent(item)); } } } } } [HarmonyPatch] internal static class RadioPatch { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("Radio"); if (!(type == null)) { return AccessTools.Method(type, "OnStartClient", (Type[])null, (Type[])null); } return null; } private static bool Prepare() { return TargetMethod() != null; } private static void Postfix(object __instance) { try { Plugin.Instance?.TakeOverRadio(__instance); } catch (Exception arg) { Plugin.Log.LogError((object)$"No pude tomar la radio: {arg}"); } } } public struct RadioSyncBroadcast : IBroadcast { public string Station; public string Track; public float Position; } internal static class Sync { internal static ConfigEntry Enabled; private static bool _serializersReady; private static bool _clientRegistered; private static bool _broken; internal static bool IsHost => InstanceFinder.IsServerStarted; internal static bool FollowingHost { get { if (Enabled != null && Enabled.Value && _clientRegistered) { return !IsHost; } return false; } } internal static void EnsureSerializers() { if (_serializersReady || _broken) { return; } try { GenericWriter.SetWrite((Action)delegate(Writer writer, RadioSyncBroadcast value) { writer.WriteRadioSync(value); }); GenericReader.SetRead((Func)((Reader reader) => reader.ReadRadioSync())); _serializersReady = true; } catch (Exception ex) { _broken = true; Plugin.Log.LogWarning((object)("No pude registrar el serializador de red: " + ex.Message + ". La sincronización queda desactivada; la radio sigue funcionando en local.")); } } internal static void TryRegisterClient() { if (_clientRegistered || _broken || Enabled == null || !Enabled.Value) { return; } ClientManager clientManager = InstanceFinder.ClientManager; if ((Object)(object)clientManager == (Object)null) { return; } try { clientManager.RegisterBroadcast((Action)OnReceived); _clientRegistered = true; Plugin.Log.LogInfo((object)"Sincronización activa: escuchando al anfitrión."); } catch (Exception ex) { _broken = true; Plugin.Log.LogWarning((object)("No pude activar la sincronización: " + ex.Message + ". La radio sigue funcionando en local.")); } } internal static void Unregister() { if (!_clientRegistered) { return; } try { ClientManager clientManager = InstanceFinder.ClientManager; if (clientManager != null) { clientManager.UnregisterBroadcast((Action)OnReceived); } } catch { } _clientRegistered = false; } internal static void Announce(string station, string trackPath, float position) { if (_broken || Enabled == null || !Enabled.Value || !IsHost) { return; } ServerManager serverManager = InstanceFinder.ServerManager; if ((Object)(object)serverManager == (Object)null) { return; } try { serverManager.Broadcast(new RadioSyncBroadcast { Station = station, Track = Path.GetFileNameWithoutExtension(trackPath), Position = position }, true, (Channel)0); } catch (Exception ex) { _broken = true; Plugin.Log.LogWarning((object)("Fallo enviando la sincronización: " + ex.Message + ". Cada uno oirá su propia música.")); } } private static void OnReceived(RadioSyncBroadcast message, Channel channel) { if (IsHost) { return; } try { Plugin.Instance?.ApplySync(message.Station, message.Track, message.Position); } catch (Exception ex) { Plugin.Log.LogWarning((object)("No pude aplicar la sincronización: " + ex.Message)); } } } public static class RadioSyncSerializer { public static void WriteRadioSync(this Writer writer, RadioSyncBroadcast value) { if (writer != null) { writer.WriteString(value.Station); writer.WriteString(value.Track); writer.WriteSingle(value.Position); } } public static RadioSyncBroadcast ReadRadioSync(this Reader reader) { return new RadioSyncBroadcast { Station = reader.ReadString(), Track = reader.ReadString(), Position = reader.ReadSingle() }; } } }