using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Networking;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
namespace FateMusicPlayer;
internal sealed class AudioLoader : MonoBehaviour
{
private const float EdgeLead = 1.2f;
private const float EdgeLinger = 0.8f;
private const float EdgeRepeatGap = 0.3f;
private static AudioLoader host;
private static ManualLogSource log;
private static string playingSong;
private static float playingUntil;
private static string lastEdgeSong;
private static float lastEdgeAt = -99f;
private static bool listenerWasPaused;
private readonly Dictionary<string, AudioClip> cache = new Dictionary<string, AudioClip>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, float> requested = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, float> failed = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
private string loadingPath;
private float loadingStartedAt;
private int previewRequest;
private readonly List<UnityWebRequest> live = new List<UnityWebRequest>();
private AudioSource previewSource;
private AudioSource edgeTail;
private AudioSource edgeHead;
private Coroutine playingRoutine;
internal static AudioLoader Host => host;
internal static void Create(ManualLogSource logger)
{
EnsureHost(logger);
}
internal static AudioLoader EnsureHost(ManualLogSource logger)
{
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
//IL_0034: Expected O, but got Unknown
if (logger != null)
{
log = logger;
}
if ((Object)(object)host != (Object)null)
{
return host;
}
GameObject val = new GameObject("FATE Music Player audio");
Object.DontDestroyOnLoad((Object)(object)val);
host = val.AddComponent<AudioLoader>();
if (log != null)
{
log.LogInfo((object)"Audio host ready for previews.");
}
return host;
}
internal static AudioClip Cached(string path)
{
if ((Object)(object)host == (Object)null || string.IsNullOrEmpty(path))
{
return null;
}
AudioClip value;
return host.cache.TryGetValue(path, out value) ? value : null;
}
internal static void Request(string path)
{
if (!((Object)(object)host == (Object)null) && !string.IsNullOrEmpty(path) && !host.cache.ContainsKey(path) && (!host.requested.TryGetValue(path, out var value) || !(Time.unscaledTime - value < 5f)) && (!host.failed.TryGetValue(path, out var value2) || !(Time.unscaledTime - value2 < 10f)))
{
host.requested[path] = Time.unscaledTime;
((MonoBehaviour)host).StartCoroutine(host.Load(path, null));
}
}
internal static bool IsPlaying(string songId)
{
return songId != null && playingSong == songId && Time.unscaledTime < playingUntil;
}
internal static void StopPreview()
{
if ((Object)(object)host != (Object)null)
{
host.Halt();
}
}
internal static void PreviewClip(string songId, AudioClip clip, float seconds)
{
if (!((Object)(object)clip == (Object)null))
{
AudioLoader audioLoader = EnsureHost(null);
if (!((Object)(object)audioLoader == (Object)null))
{
audioLoader.PlayPreview(songId, clip, seconds);
}
}
}
internal static void Preview(string songId, string path, float seconds)
{
if (string.IsNullOrEmpty(path) || (Object)(object)EnsureHost(null) == (Object)null)
{
return;
}
AudioClip val = Cached(path);
if ((Object)(object)val != (Object)null)
{
host.PlayPreview(songId, val, seconds);
}
else
{
if (host.loadingPath == path && Time.unscaledTime - host.loadingStartedAt < 1.5f)
{
return;
}
host.loadingPath = path;
host.loadingStartedAt = Time.unscaledTime;
int request = ++host.previewRequest;
((MonoBehaviour)host).StartCoroutine(host.Load(path, delegate(AudioClip loaded)
{
if ((Object)(object)loaded != (Object)null && host.previewRequest == request)
{
host.PlayPreview(songId, loaded, seconds);
}
}));
}
}
internal static void PreviewLoopEdge(string songId, string path, AudioClip clip, float fade)
{
AudioLoader audioLoader = EnsureHost(null);
if ((Object)(object)audioLoader == (Object)null)
{
return;
}
host = audioLoader;
if ((Object)(object)clip == (Object)null)
{
if (string.IsNullOrEmpty(path))
{
return;
}
AudioClip val = Cached(path);
if (!((Object)(object)val != (Object)null))
{
if (host.loadingPath == path && Time.unscaledTime - host.loadingStartedAt < 1.5f)
{
return;
}
host.loadingPath = path;
host.loadingStartedAt = Time.unscaledTime;
int request = ++host.previewRequest;
((MonoBehaviour)host).StartCoroutine(host.Load(path, delegate(AudioClip loaded)
{
if ((Object)(object)loaded != (Object)null && host.previewRequest == request)
{
PreviewLoopEdge(songId, path, loaded, fade);
}
}));
return;
}
clip = val;
}
if (!(songId == lastEdgeSong) || !(Time.unscaledTime - lastEdgeAt < 0.3f))
{
lastEdgeSong = songId;
lastEdgeAt = Time.unscaledTime;
host.PlayEdge(songId, clip, fade);
}
}
private void PlayPreview(string songId, AudioClip clip, float seconds)
{
Halt();
if ((Object)(object)previewSource == (Object)null)
{
previewSource = NewSource("FATE Music Player preview");
}
previewSource.clip = clip;
previewSource.loop = false;
GameMusic.Route(previewSource);
previewSource.volume = GameMusic.MusicScale;
GameMusic.MutePlayback();
BeginAudible();
try
{
previewSource.time = 0f;
}
catch
{
}
previewSource.Play();
playingSong = songId;
playingUntil = Time.unscaledTime + Mathf.Max(0.5f, seconds);
playingRoutine = ((MonoBehaviour)this).StartCoroutine(StopAfter(seconds));
if (log != null)
{
log.LogInfo((object)("Preview: " + ((Object)clip).name + " (" + clip.length.ToString("0.0") + "s) for " + Format(seconds) + "s"));
}
}
private void PlayEdge(string songId, AudioClip clip, float fade)
{
Halt();
if ((Object)(object)edgeTail == (Object)null)
{
edgeTail = NewSource("FATE Music Player loop tail");
}
if ((Object)(object)edgeHead == (Object)null)
{
edgeHead = NewSource("FATE Music Player loop head");
}
GameMusic.Route(edgeTail);
GameMusic.Route(edgeHead);
float length = clip.length;
fade = Mathf.Clamp(fade, 0f, Mathf.Max(0f, length * 0.5f));
float num = Mathf.Max(0f, length - fade - 1.2f);
float num2 = Mathf.Max(0.05f, length - fade - num);
float num3 = Mathf.Min(0.8f, Mathf.Max(0.2f, fade));
GameMusic.MutePlayback();
BeginAudible();
playingSong = songId;
playingUntil = Time.unscaledTime + num2 + fade + num3;
playingRoutine = ((MonoBehaviour)this).StartCoroutine(EdgeBlend(clip, fade, num, num2, num3));
if (log != null)
{
log.LogInfo((object)("Loop blend: " + ((Object)clip).name + " (clip " + length.ToString("0.0") + "s, overlap " + fade.ToString("0.0") + "s)"));
}
}
private IEnumerator EdgeBlend(AudioClip clip, float fade, float tailStart, float tailFor, float linger)
{
float scale = GameMusic.MusicScale;
edgeTail.clip = clip;
edgeTail.loop = false;
edgeTail.volume = scale;
try
{
edgeTail.time = tailStart;
}
catch
{
edgeTail.time = 0f;
}
edgeTail.Play();
float tailEnd = Time.unscaledTime + tailFor;
while (Time.unscaledTime < tailEnd)
{
yield return null;
}
edgeHead.clip = clip;
edgeHead.loop = false;
edgeHead.volume = ((fade <= 0.001f) ? scale : 0f);
try
{
edgeHead.time = 0f;
}
catch
{
}
edgeHead.Play();
if (fade > 0.001f)
{
float start = Time.unscaledTime;
while (true)
{
float t = (Time.unscaledTime - start) / fade;
if (t >= 1f)
{
break;
}
edgeHead.volume = scale * t;
edgeTail.volume = scale * (1f - t);
yield return null;
}
}
edgeTail.Stop();
edgeHead.volume = scale;
float lingerEnd = Time.unscaledTime + linger;
while (Time.unscaledTime < lingerEnd)
{
yield return null;
}
edgeHead.Stop();
playingSong = null;
playingUntil = 0f;
GameMusic.RestorePlayback();
EndAudible();
playingRoutine = null;
}
private IEnumerator StopAfter(float seconds)
{
float end = Time.unscaledTime + Mathf.Max(0.5f, seconds);
while (Time.unscaledTime < end)
{
yield return null;
}
if ((Object)(object)previewSource != (Object)null)
{
previewSource.Stop();
}
playingSong = null;
playingUntil = 0f;
GameMusic.RestorePlayback();
EndAudible();
playingRoutine = null;
}
private void Halt()
{
previewRequest++;
if (playingRoutine != null)
{
((MonoBehaviour)this).StopCoroutine(playingRoutine);
playingRoutine = null;
}
if ((Object)(object)previewSource != (Object)null)
{
previewSource.Stop();
}
if ((Object)(object)edgeTail != (Object)null)
{
edgeTail.Stop();
}
if ((Object)(object)edgeHead != (Object)null)
{
edgeHead.Stop();
}
playingSong = null;
playingUntil = 0f;
GameMusic.RestorePlayback();
EndAudible();
}
private static void BeginAudible()
{
listenerWasPaused = AudioListener.pause;
if (AudioListener.pause)
{
AudioListener.pause = false;
}
}
private static void EndAudible()
{
if (listenerWasPaused)
{
AudioListener.pause = true;
}
listenerWasPaused = false;
}
private void Update()
{
if (playingSong != null)
{
if (AudioListener.pause)
{
AudioListener.pause = false;
}
if ((Object)(object)previewSource != (Object)null)
{
previewSource.volume = GameMusic.MusicScale;
}
}
}
private AudioSource NewSource(string name)
{
//IL_0002: Unknown result type (might be due to invalid IL or missing references)
//IL_0008: Expected O, but got Unknown
GameObject val = new GameObject(name);
val.transform.SetParent(((Component)this).transform, false);
AudioSource val2 = val.AddComponent<AudioSource>();
val2.playOnAwake = false;
val2.loop = false;
val2.spatialBlend = 0f;
return val2;
}
private IEnumerator Load(string path, Action<AudioClip> done)
{
AudioType type = AudioTypeFor(path);
if ((int)type == 0)
{
if (log != null)
{
log.LogWarning((object)("Unsupported audio file: " + path));
}
Fail(path);
done?.Invoke(null);
yield break;
}
string uri = new Uri(Path.GetFullPath(path)).AbsoluteUri;
UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(uri, type);
yield return request.SendWebRequest();
if (request.error != null)
{
if (log != null)
{
log.LogWarning((object)("Could not read " + path + ": " + request.error + " (will try again in 10s)"));
}
Fail(path);
request.Dispose();
done?.Invoke(null);
yield break;
}
DownloadHandler downloadHandler = request.downloadHandler;
DownloadHandlerAudioClip handler = (DownloadHandlerAudioClip)(object)((downloadHandler is DownloadHandlerAudioClip) ? downloadHandler : null);
AudioClip clip = ((handler == null) ? null : handler.audioClip);
if ((Object)(object)clip == (Object)null)
{
if (log != null)
{
log.LogWarning((object)("No audio in " + path + " (will try again in 10s)"));
}
Fail(path);
request.Dispose();
done?.Invoke(null);
}
else
{
((Object)clip).name = Path.GetFileNameWithoutExtension(path);
cache[path] = clip;
failed.Remove(path);
live.Add(request);
FinishLoading(path);
done?.Invoke(clip);
}
}
private void Fail(string path)
{
cache.Remove(path);
failed[path] = Time.unscaledTime;
FinishLoading(path);
}
private void FinishLoading(string path)
{
if (string.Equals(loadingPath, path, StringComparison.OrdinalIgnoreCase))
{
loadingPath = null;
}
}
private static string Format(float seconds)
{
return seconds.ToString("0.0");
}
private void OnDestroy()
{
GameMusic.RestorePlayback();
EndAudible();
foreach (UnityWebRequest item in live)
{
if (item != null)
{
item.Dispose();
}
}
live.Clear();
host = null;
}
internal static AudioType AudioTypeFor(string path)
{
//IL_0067: Unknown result type (might be due to invalid IL or missing references)
//IL_006a: Unknown result type (might be due to invalid IL or missing references)
//IL_0055: Unknown result type (might be due to invalid IL or missing references)
//IL_005a: Unknown result type (might be due to invalid IL or missing references)
//IL_005f: Unknown result type (might be due to invalid IL or missing references)
//IL_0063: Unknown result type (might be due to invalid IL or missing references)
switch (Path.GetExtension(path).ToLowerInvariant())
{
case ".ogg":
return (AudioType)14;
case ".wav":
return (AudioType)20;
case ".mp3":
return (AudioType)13;
case ".aiff":
case ".aif":
return (AudioType)2;
default:
return (AudioType)0;
}
}
}
[BepInDependency(/*Could not decode attribute arguments.*/)]
[BepInPlugin("mikhailkillszombies.musicplayer", "FATE Music Player", "0.4.3")]
public sealed class FateMusicPlayerPlugin : BaseUnityPlugin
{
internal const string Guid = "mikhailkillszombies.musicplayer";
internal const string Version = "0.4.3";
internal static ManualLogSource LogSource;
internal static bool ModEnabled = true;
internal static string LastToast;
internal static float LastToastAt;
private ConfigEntry<bool> enabledSetting;
internal static void Toast(string message)
{
LastToast = message;
LastToastAt = Time.unscaledTime;
}
private void Awake()
{
//IL_0099: Unknown result type (might be due to invalid IL or missing references)
//IL_00a3: Expected O, but got Unknown
LogSource = ((BaseUnityPlugin)this).Logger;
enabledSetting = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Let the music player choose tracks for the slots you edited.");
ModEnabled = enabledSetting.Value;
enabledSetting.SettingChanged += delegate
{
ModEnabled = enabledSetting.Value;
};
AudioLoader.Create(((BaseUnityPlugin)this).Logger);
Library.Init(Paths.ConfigPath, Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location), ((BaseUnityPlugin)this).Logger);
ModMenuBridge.Init(((BaseUnityPlugin)this).Logger);
MusicRuntime.Init(((BaseUnityPlugin)this).Logger, new Harmony("mikhailkillszombies.musicplayer"));
((BaseUnityPlugin)this).Logger.LogInfo((object)"FATE Music Player 0.4.3 loaded; waiting for the game's audio manager.");
}
private void Update()
{
MusicRuntime.Tick();
}
}
internal sealed class BucketInfo
{
public int Value;
public string Name;
public AudioClip[] Clips;
public bool Loop;
public float Volume;
public string EditorName;
}
internal static class GameMusic
{
internal static bool Bound;
private static Type managerType;
private static Type dataType;
private static Type clipNameType;
private static PropertyInfo instanceProperty;
private static FieldInfo instanceField;
private static FieldInfo musicListField;
private static FieldInfo channelsField;
private static FieldInfo currentMusicField;
private static FieldInfo stoppedField;
private static FieldInfo dataClipNameField;
private static FieldInfo dataClipsField;
private static FieldInfo dataLoopField;
private static FieldInfo dataVolumeField;
private static FieldInfo dataEditorNameField;
private static MethodInfo freeMusicSourceMethod;
private static Type levelType;
private static FieldInfo levelSourceField;
private static Type menuType;
private static FieldInfo mixerField;
private static PropertyInfo settingsSaveProperty;
private static FieldInfo settingsMusicVolumeField;
private static AudioMixerGroup musicGroup;
private static object musicGroupFor;
private static bool musicGroupTried;
private static bool warnedNoGroup;
private static ManualLogSource logSource;
private static float settingsVolume = 1f;
private static float nextSettingsRead;
private static readonly List<KeyValuePair<AudioSource, float>> muted = new List<KeyValuePair<AudioSource, float>>();
internal static Type ManagerType => managerType;
internal static Type ClipNameType => clipNameType;
internal static float MusicScale
{
get
{
if ((Object)(object)MusicGroup() != (Object)null)
{
return 1f;
}
float unscaledTime = Time.unscaledTime;
if (unscaledTime < nextSettingsRead)
{
return settingsVolume;
}
nextSettingsRead = unscaledTime + 0.25f;
try
{
object obj = ((settingsSaveProperty == null) ? null : settingsSaveProperty.GetValue(null, null));
if (obj != null && settingsMusicVolumeField != null)
{
settingsVolume = Mathf.Clamp01(Convert.ToSingle(settingsMusicVolumeField.GetValue(obj)));
}
}
catch
{
}
return settingsVolume;
}
}
internal static bool Bind(ManualLogSource log)
{
if (log != null)
{
logSource = log;
}
if (Bound)
{
return true;
}
managerType = AccessTools.TypeByName("Tableflip.AudioManager");
dataType = AccessTools.TypeByName("AudioData");
clipNameType = AccessTools.TypeByName("AudioData+AudioClipName");
if (managerType == null || dataType == null || clipNameType == null)
{
if (log != null)
{
log.LogWarning((object)"FATE AudioManager was not found yet; music slots are unavailable.");
}
return false;
}
instanceProperty = FindStaticProperty(managerType, "Instance");
instanceField = FindInstanceField(managerType, managerType);
musicListField = AccessTools.Field(managerType, "mAudioDataMusicList");
channelsField = AccessTools.Field(managerType, "audioSourceMusicChannels");
currentMusicField = AccessTools.Field(managerType, "currentMusicData");
stoppedField = AccessTools.Field(managerType, "hasStoppedByScript");
dataClipNameField = AccessTools.Field(dataType, "mAudioClipName");
dataClipsField = AccessTools.Field(dataType, "mAudioClip");
dataLoopField = AccessTools.Field(dataType, "mLoop");
dataVolumeField = AccessTools.Field(dataType, "mVolume");
dataEditorNameField = AccessTools.Field(dataType, "editorName");
freeMusicSourceMethod = AccessTools.Method(managerType, "GetFreeMusicAudioSource", new Type[0], (Type[])null);
mixerField = AccessTools.Field(managerType, "masterMixer");
Type type = AccessTools.TypeByName("Tableflip.SaveData");
settingsSaveProperty = ((type == null) ? null : type.GetProperty("SettingsSave", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic));
settingsMusicVolumeField = ((settingsSaveProperty == null) ? null : AccessTools.Field(settingsSaveProperty.PropertyType, "musicVolume"));
levelType = AccessTools.TypeByName("Tableflip.LevelGenerator");
levelSourceField = ((levelType == null) ? null : AccessTools.Field(levelType, "audioSource"));
menuType = AccessTools.TypeByName("Tableflip.MainMenuUI");
Bound = instanceProperty != null && musicListField != null && channelsField != null;
if (!Bound && log != null)
{
log.LogWarning((object)"FATE AudioManager layout changed; music slots are unavailable.");
}
else if (Bound && log != null)
{
log.LogInfo((object)("AudioManager instance read from " + ((instanceField != null) ? ("field " + instanceField.Name) : "the Instance property") + "; music list field " + ((musicListField != null) ? "found" : "missing") + "."));
}
return Bound;
}
private static PropertyInfo FindStaticProperty(Type type, string name)
{
Type type2 = type;
while (type2 != null)
{
PropertyInfo property = type2.GetProperty(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (property != null)
{
return property;
}
type2 = type2.BaseType;
}
return null;
}
internal static object Manager()
{
try
{
if (instanceField != null)
{
return instanceField.GetValue(null);
}
return (instanceProperty == null) ? null : instanceProperty.GetValue(null, null);
}
catch
{
return null;
}
}
private static FieldInfo FindInstanceField(Type type, Type instanceType)
{
Type type2 = type;
while (type2 != null)
{
FieldInfo[] fields = type2.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo[] array = fields;
foreach (FieldInfo fieldInfo in array)
{
if (fieldInfo.FieldType == instanceType && fieldInfo.Name.IndexOf("instance", StringComparison.OrdinalIgnoreCase) >= 0)
{
return fieldInfo;
}
}
type2 = type2.BaseType;
}
type2 = type;
while (type2 != null)
{
FieldInfo[] fields = type2.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo[] array = fields;
foreach (FieldInfo fieldInfo in array)
{
if (fieldInfo.FieldType == instanceType)
{
return fieldInfo;
}
}
type2 = type2.BaseType;
}
return null;
}
internal static List<BucketInfo> Buckets()
{
List<BucketInfo> list = new List<BucketInfo>();
if (!Bound)
{
return list;
}
object obj = Manager();
if (obj == null || musicListField == null)
{
return list;
}
if (!(musicListField.GetValue(obj) is Array array))
{
return list;
}
foreach (object item in array)
{
if (item != null)
{
object obj2 = ((dataClipNameField == null) ? null : dataClipNameField.GetValue(item));
if (obj2 != null)
{
BucketInfo bucketInfo = new BucketInfo();
bucketInfo.Value = Convert.ToInt32(obj2);
bucketInfo.Name = obj2.ToString();
bucketInfo.Clips = (AudioClip[])((dataClipsField == null) ? new AudioClip[0] : (((object)(dataClipsField.GetValue(item) as AudioClip[])) ?? ((object)new AudioClip[0])));
bucketInfo.Loop = dataLoopField == null || Convert.ToBoolean(dataLoopField.GetValue(item));
bucketInfo.Volume = ((dataVolumeField == null) ? 1f : Convert.ToSingle(dataVolumeField.GetValue(item)));
bucketInfo.EditorName = ((dataEditorNameField == null) ? null : (dataEditorNameField.GetValue(item) as string));
list.Add(bucketInfo);
}
}
}
return list;
}
internal static BucketInfo FindBucket(int value)
{
foreach (BucketInfo item in Buckets())
{
if (item.Value == value)
{
return item;
}
}
return null;
}
internal static AudioMixerGroup MusicGroup()
{
object obj = Manager();
if (obj == null)
{
return null;
}
if (object.ReferenceEquals(musicGroupFor, obj) && ((Object)(object)musicGroup != (Object)null || musicGroupTried))
{
return musicGroup;
}
musicGroupFor = obj;
musicGroupTried = true;
musicGroup = null;
if (mixerField == null)
{
return null;
}
try
{
object? value = mixerField.GetValue(obj);
AudioMixer val = (AudioMixer)((value is AudioMixer) ? value : null);
if ((Object)(object)val == (Object)null)
{
return null;
}
AudioMixerGroup[] array = val.FindMatchingGroups("Music");
if (array != null && array.Length > 0)
{
musicGroup = array[0];
}
}
catch
{
musicGroup = null;
}
if ((Object)(object)musicGroup == (Object)null && !warnedNoGroup)
{
warnedNoGroup = true;
if (logSource != null)
{
logSource.LogWarning((object)"The game's Music mixer group was not found; the in-game music volume slider cannot reach this mod's music.");
}
}
return musicGroup;
}
internal static void Route(AudioSource source)
{
if ((Object)(object)source == (Object)null)
{
return;
}
AudioMixerGroup val = MusicGroup();
if ((Object)(object)val == (Object)null)
{
return;
}
try
{
if ((Object)(object)source.outputAudioMixerGroup != (Object)(object)val)
{
source.outputAudioMixerGroup = val;
}
}
catch
{
}
}
internal static AudioSource FreeMusicSource()
{
object obj = Manager();
if (obj == null || freeMusicSourceMethod == null)
{
return null;
}
try
{
object? obj2 = freeMusicSourceMethod.Invoke(obj, null);
return (AudioSource)((obj2 is AudioSource) ? obj2 : null);
}
catch
{
return null;
}
}
internal static AudioSource[] MusicChannels()
{
object obj = Manager();
if (obj == null || channelsField == null)
{
return (AudioSource[])(object)new AudioSource[0];
}
try
{
return (AudioSource[])(((object)(channelsField.GetValue(obj) as AudioSource[])) ?? ((object)new AudioSource[0]));
}
catch
{
return (AudioSource[])(object)new AudioSource[0];
}
}
internal static void StopAllMusic()
{
LoopPlayer.Stop();
AudioSource[] array = MusicChannels();
foreach (AudioSource val in array)
{
if ((Object)(object)val != (Object)null)
{
val.Stop();
}
}
}
internal static void MutePlayback()
{
if (muted.Count > 0)
{
return;
}
LoopPlayer.SetSilenced(value: true);
AudioSource[] array = MusicChannels();
foreach (AudioSource val in array)
{
if (!((Object)(object)val == (Object)null) && !(val.volume <= 0f))
{
muted.Add(new KeyValuePair<AudioSource, float>(val, val.volume));
val.volume = 0f;
}
}
}
internal static void RestorePlayback()
{
for (int i = 0; i < muted.Count; i++)
{
AudioSource key = muted[i].Key;
if ((Object)(object)key != (Object)null)
{
key.volume = muted[i].Value;
}
}
muted.Clear();
LoopPlayer.SetSilenced(value: false);
}
internal static void RememberSlot(int bucketValue, bool stopped)
{
object obj = Manager();
if (obj == null)
{
return;
}
try
{
if (currentMusicField != null)
{
currentMusicField.SetValue(obj, Enum.ToObject(clipNameType, bucketValue));
}
if (stoppedField != null)
{
stoppedField.SetValue(obj, stopped);
}
}
catch
{
}
}
private static List<MethodInfo> StopMethods()
{
List<MethodInfo> list = new List<MethodInfo>();
string[] array = new string[2] { "StopMusic", "ForceStopMusic" };
foreach (string text in array)
{
MethodInfo methodInfo = AccessTools.Method(managerType, text, new Type[0], (Type[])null);
if (methodInfo != null)
{
list.Add(methodInfo);
}
MethodInfo methodInfo2 = ((clipNameType == null) ? null : AccessTools.Method(managerType, text, new Type[1] { clipNameType }, (Type[])null));
if (methodInfo2 != null)
{
list.Add(methodInfo2);
}
}
return list;
}
internal static int DungeonMusicValue()
{
if (clipNameType == null)
{
return -1;
}
try
{
return Convert.ToInt32(Enum.Parse(clipNameType, "DungeonMusic"));
}
catch
{
return -1;
}
}
internal static void SilenceDungeonSource(object level)
{
if (level == null || levelSourceField == null)
{
return;
}
try
{
object? value = levelSourceField.GetValue(level);
AudioSource val = (AudioSource)((value is AudioSource) ? value : null);
if ((Object)(object)val != (Object)null)
{
val.Stop();
}
}
catch
{
}
}
internal static bool Patch(Harmony harmony, ManualLogSource log)
{
//IL_00ba: Unknown result type (might be due to invalid IL or missing references)
//IL_00c8: Expected O, but got Unknown
//IL_0140: Unknown result type (might be due to invalid IL or missing references)
//IL_014e: Expected O, but got Unknown
//IL_034a: Unknown result type (might be due to invalid IL or missing references)
//IL_0357: Expected O, but got Unknown
//IL_02b6: Unknown result type (might be due to invalid IL or missing references)
//IL_02c3: Expected O, but got Unknown
//IL_0211: Unknown result type (might be due to invalid IL or missing references)
//IL_021e: Expected O, but got Unknown
if (!Bind(log))
{
return false;
}
bool result = false;
string[] array = new string[2] { "PlayMusic", "ForcePlayMusic" };
foreach (string text in array)
{
MethodInfo methodInfo = AccessTools.Method(managerType, text, new Type[2]
{
clipNameType,
typeof(bool)
}, (Type[])null);
if (methodInfo == null)
{
if (log != null)
{
log.LogWarning((object)("AudioManager." + text + " was not found; that slot keeps playing vanilla music."));
}
continue;
}
try
{
harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(MusicEngine), "BeforePlayMusic", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
result = true;
}
catch (Exception ex)
{
if (log != null)
{
log.LogError((object)("Could not patch AudioManager." + text + ": " + ex.Message));
}
}
}
foreach (MethodInfo item in StopMethods())
{
try
{
harmony.Patch((MethodBase)item, new HarmonyMethod(typeof(MusicEngine), "BeforeGameStopsMusic", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
result = true;
}
catch (Exception ex)
{
if (log != null)
{
log.LogWarning((object)("Could not patch AudioManager." + item.Name + ": " + ex.Message));
}
}
}
MethodInfo methodInfo2 = ((levelType == null) ? null : AccessTools.Method(levelType, "PlayDungeonMusic", new Type[0], (Type[])null));
if (methodInfo2 == null)
{
if (log != null)
{
log.LogWarning((object)"LevelGenerator.PlayDungeonMusic was not found; songs in the dungeon slot cannot play.");
}
}
else
{
try
{
harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(MusicEngine), "AfterDungeonMusic", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
result = true;
}
catch (Exception ex)
{
if (log != null)
{
log.LogWarning((object)("Could not patch LevelGenerator.PlayDungeonMusic: " + ex.Message));
}
}
}
MethodInfo methodInfo3 = ((menuType == null) ? null : AccessTools.Method(menuType, "Start", new Type[0], (Type[])null));
if (methodInfo3 == null)
{
if (log != null)
{
log.LogWarning((object)"MainMenuUI.Start was not found; the title screen keeps its own music.");
}
}
else
{
try
{
harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(typeof(MusicEngine), "AfterMainMenuMusic", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
result = true;
}
catch (Exception ex)
{
if (log != null)
{
log.LogWarning((object)("Could not patch MainMenuUI.Start: " + ex.Message));
}
}
}
MethodInfo methodInfo4 = AccessTools.Method(managerType, "Update", new Type[0], (Type[])null);
if (methodInfo4 == null)
{
if (log != null)
{
log.LogWarning((object)"AudioManager.Update was not found; the mod's own ticker object drives the player instead.");
}
}
else
{
try
{
harmony.Patch((MethodBase)methodInfo4, (HarmonyMethod)null, new HarmonyMethod(typeof(MusicRuntime), "AfterAudioManagerUpdate", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
result = true;
}
catch (Exception ex)
{
if (log != null)
{
log.LogWarning((object)("Could not hook AudioManager.Update: " + ex.Message));
}
}
}
return result;
}
}
[DataContract]
public sealed class SongSetting
{
[DataMember(Name = "id")]
public string Id;
[DataMember(Name = "enabled")]
public bool Enabled;
[DataMember(Name = "selected")]
public bool Selected;
[DataMember(Name = "loopSeconds")]
public float? LoopSeconds;
}
[DataContract]
public sealed class UserSong
{
[DataMember(Name = "id")]
public string Id;
[DataMember(Name = "title")]
public string Title;
[DataMember(Name = "bucket")]
public string Bucket;
[DataMember(Name = "file")]
public string File;
[DataMember(Name = "loop")]
public bool? Loop;
[DataMember(Name = "volume")]
public float? Volume;
[DataMember(Name = "loopSeconds")]
public float? LoopSeconds;
}
[DataContract]
public sealed class LibraryFile
{
[DataMember(Name = "version")]
public int Version = 1;
[DataMember(Name = "settings")]
public List<SongSetting> Settings = new List<SongSetting>();
[DataMember(Name = "userSongs")]
public List<UserSong> Songs = new List<UserSong>();
}
[DataContract]
public sealed class PackFile
{
[DataMember(Name = "id")]
public string Id;
[DataMember(Name = "name")]
public string Name;
[DataMember(Name = "author")]
public string Author;
[DataMember(Name = "version")]
public string Version;
[DataMember(Name = "songs")]
public List<PackSong> Songs;
}
[DataContract]
public sealed class PackSong
{
[DataMember(Name = "title")]
public string Title;
[DataMember(Name = "category")]
public string Category;
[DataMember(Name = "bucket")]
public string Bucket;
[DataMember(Name = "file")]
public string File;
[DataMember(Name = "loop")]
public bool? Loop;
[DataMember(Name = "volume")]
public float? Volume;
[DataMember(Name = "loopSeconds")]
public float? LoopSeconds;
}
internal sealed class Song
{
public string Id;
public string Title;
public string Category;
public string BucketName;
public int BucketValue;
public string PackId;
public string FilePath;
public AudioClip GameClip;
public bool Loop = true;
public float Volume = 1f;
public float LoopSeconds;
public bool Enabled = true;
public bool Selected;
public bool IsCustom => (Object)(object)GameClip == (Object)null;
}
internal static class Library
{
private const float StepTenth = 0.1f;
private const float StepOne = 1f;
internal const float MaxLoopSeconds = 10f;
private static readonly List<Song> songs = new List<Song>();
private static readonly List<string> categories = new List<string>();
private static LibraryFile data = new LibraryFile();
private static string configPath;
private static string packsPath;
private static string pluginPath;
private static ManualLogSource log;
private static bool savePending;
private static float saveAt;
internal static string PacksPath => packsPath;
internal static string ConfigPath => configPath;
internal static List<Song> Songs => songs;
internal static bool HasGameSongs => songs.Any((Song song) => (Object)(object)song.GameClip != (Object)null);
internal static List<string> Categories => categories;
internal static LibraryFile Data => data;
internal static void Init(string configDirectory, string pluginDirectory, ManualLogSource logger)
{
log = logger;
configPath = Path.Combine(configDirectory, "library.json");
pluginPath = pluginDirectory;
packsPath = Path.Combine(pluginDirectory, "packs");
try
{
Directory.CreateDirectory(packsPath);
}
catch (Exception ex)
{
log.LogWarning((object)("Could not create " + packsPath + ": " + ex.Message));
}
foreach (string item in PackRoots())
{
log.LogInfo((object)("Looking for song packs in " + item));
}
Load();
Scan();
}
internal static List<Song> SongsForBucket(int bucketValue)
{
return songs.Where((Song song) => song.BucketValue == bucketValue).ToList();
}
internal static List<int> CategorySlots(string category)
{
List<int> list = new List<int>();
foreach (Song song in songs)
{
if (song != null && string.Equals(song.Category, category, StringComparison.Ordinal) && !list.Contains(song.BucketValue))
{
list.Add(song.BucketValue);
}
}
list.Sort();
return list;
}
private static void Load()
{
//IL_0035: Unknown result type (might be due to invalid IL or missing references)
//IL_003b: Expected O, but got Unknown
data = new LibraryFile();
if (!File.Exists(configPath))
{
return;
}
try
{
using FileStream fileStream = File.OpenRead(configPath);
DataContractJsonSerializer val = new DataContractJsonSerializer(typeof(LibraryFile));
data = ((LibraryFile)((XmlObjectSerializer)val).ReadObject((Stream)fileStream)) ?? new LibraryFile();
}
catch (Exception ex)
{
log.LogWarning((object)("Could not read " + configPath + ": " + ex.Message + ". Starting with defaults."));
data = new LibraryFile();
}
if (data.Settings == null)
{
data.Settings = new List<SongSetting>();
}
if (data.Songs == null)
{
data.Songs = new List<UserSong>();
}
}
internal static void SaveLater()
{
savePending = true;
saveAt = Time.unscaledTime + 0.5f;
}
internal static void FlushSaves()
{
if (savePending && !(Time.unscaledTime < saveAt))
{
savePending = false;
Save();
}
}
internal static void Save()
{
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
try
{
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
using FileStream fileStream = File.Create(configPath);
((XmlObjectSerializer)new DataContractJsonSerializer(typeof(LibraryFile))).WriteObject((Stream)fileStream, (object)data);
}
catch (Exception ex)
{
log.LogWarning((object)("Could not save " + configPath + ": " + ex.Message));
}
}
internal static void Scan()
{
DropDuplicateImports();
songs.Clear();
categories.Clear();
Dictionary<string, SongSetting> dictionary = new Dictionary<string, SongSetting>(StringComparer.OrdinalIgnoreCase);
foreach (SongSetting setting in data.Settings)
{
if (setting != null && !string.IsNullOrEmpty(setting.Id))
{
dictionary[setting.Id] = setting;
}
}
foreach (BucketInfo item in MusicBuckets())
{
string category = CategoryForBucket(item);
for (int i = 0; i < item.Clips.Length; i++)
{
AudioClip val = item.Clips[i];
if (!((Object)(object)val == (Object)null))
{
Song song = new Song();
song.Id = "game:" + item.Value + ":" + i + ":" + Sanitize(((Object)val).name);
song.Title = FriendlyTitle(((Object)val).name);
song.Category = category;
song.BucketName = item.Name;
song.BucketValue = item.Value;
song.GameClip = val;
song.Loop = item.Loop;
song.Volume = item.Volume;
song.LoopSeconds = 0f;
Add(song, dictionary);
}
}
if (item.Clips.Length == 0 && !string.IsNullOrEmpty(item.EditorName))
{
Song song2 = new Song();
song2.Id = "slot:" + item.Value;
song2.Title = FriendlyTitle(item.EditorName);
song2.Category = category;
song2.BucketName = item.Name;
song2.BucketValue = item.Value;
song2.Loop = item.Loop;
song2.Volume = item.Volume;
song2.LoopSeconds = 0f;
song2.Enabled = false;
Add(song2, dictionary);
}
}
foreach (UserSong song3 in data.Songs)
{
if (song3 != null && !string.IsNullOrEmpty(song3.File))
{
Add(UserSongRow(song3), dictionary);
}
}
foreach (KeyValuePair<string, List<Song>> item2 in ScanPacks())
{
foreach (Song item3 in item2.Value)
{
Add(item3, dictionary);
}
}
HashSet<string> live = new HashSet<string>(songs.Select((Song song3) => song3.Id), StringComparer.OrdinalIgnoreCase);
int num = data.Settings.RemoveAll((SongSetting setting) => setting != null && !string.IsNullOrEmpty(setting.Id) && setting.Id.StartsWith("pack:", StringComparison.OrdinalIgnoreCase) && !live.Contains(setting.Id));
if (num > 0)
{
log.LogInfo((object)("Removed " + num + " setting(s) belonging to uninstalled packs."));
Save();
}
foreach (string category2 in categories)
{
List<Song> list = songs;
Predicate<Song> match = (Song row) => row != null && row.Category == category2;
List<Song> list2 = list.FindAll(match);
List<int> list3 = new List<int>();
foreach (Song item4 in list2)
{
if (!list3.Contains(item4.BucketValue))
{
list3.Add(item4.BucketValue);
}
}
list3.Sort();
string text = category2 + ": " + list2.Count + " song(s)";
if (list3.Count > 0)
{
text = text + " in slot(s) " + string.Join(", ", list3.ConvertAll((int value) => value.ToString()).ToArray());
}
log.LogInfo((object)text);
}
log.LogInfo((object)("Music library: " + songs.Count + " song(s) in " + categories.Count + " category(ies)."));
}
private static Song UserSongRow(UserSong user)
{
Song song = new Song();
song.Id = user.Id;
song.Title = (string.IsNullOrEmpty(user.Title) ? Path.GetFileNameWithoutExtension(user.File) : user.Title);
song.FilePath = user.File;
BucketInfo bucketInfo = ResolveBucket(user.Bucket);
song.BucketName = ((bucketInfo != null) ? bucketInfo.Name : user.Bucket);
song.BucketValue = bucketInfo?.Value ?? BucketValue(user.Bucket);
song.Category = ((bucketInfo != null) ? CategoryForBucket(bucketInfo) : CategoryForBucketName(user.Bucket));
song.Loop = !user.Loop.HasValue || user.Loop.Value;
song.Volume = ((!user.Volume.HasValue) ? 1f : Mathf.Clamp01(user.Volume.Value));
song.LoopSeconds = ((!user.LoopSeconds.HasValue) ? 0f : Mathf.Clamp(user.LoopSeconds.Value, 0f, 10f));
return song;
}
private static void DropDuplicateImports()
{
HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
int num = 0;
for (int num2 = data.Songs.Count - 1; num2 >= 0; num2--)
{
UserSong song = data.Songs[num2];
if (song != null && !string.IsNullOrEmpty(song.File) && !hashSet.Add(ImportKey(song)))
{
data.Songs.RemoveAt(num2);
data.Settings.RemoveAll((SongSetting setting) => setting != null && string.Equals(setting.Id, song.Id, StringComparison.OrdinalIgnoreCase));
num++;
}
}
if (num != 0)
{
log.LogInfo((object)("Removed " + num + " duplicate song import(s)."));
Save();
}
}
private static string ImportKey(UserSong song)
{
return FileKey(song.File) + "|" + PlaceKey(song.Bucket);
}
private static string FileKey(string path)
{
try
{
return Path.GetFullPath(path).TrimEnd('\\', '/');
}
catch
{
return path;
}
}
private static string PlaceKey(string bucket)
{
int num = BucketValue(bucket);
return (num >= 0) ? ("slot:" + num) : ("name:" + (bucket ?? string.Empty));
}
private static void Add(Song song, Dictionary<string, SongSetting> settings)
{
if (settings.TryGetValue(song.Id, out var value))
{
song.Enabled = value.Enabled;
song.Selected = value.Selected;
if (value.LoopSeconds.HasValue)
{
song.LoopSeconds = Mathf.Clamp(value.LoopSeconds.Value, 0f, 10f);
}
}
if (!songs.Any((Song existing) => existing.Id == song.Id))
{
songs.Add(song);
}
if (!categories.Contains(song.Category))
{
categories.Add(song.Category);
}
}
private static List<string> PackRoots()
{
List<string> list = new List<string>();
if (!string.IsNullOrEmpty(packsPath))
{
list.Add(packsPath);
}
string text = PluginsDirectory(pluginPath);
if (text != null)
{
try
{
string text2 = Path.Combine(text, "FateMusicPlayer", "packs");
if (Directory.Exists(text2))
{
list.Add(text2);
}
string[] directories = Directory.GetDirectories(text);
foreach (string path in directories)
{
string text3 = Path.Combine(path, "FateMusicPlayer", "packs");
if (Directory.Exists(text3))
{
list.Add(text3);
}
}
}
catch (Exception ex)
{
log.LogWarning((object)("Could not list " + text + ": " + ex.Message));
}
}
List<string> list2 = new List<string>();
foreach (string item in list)
{
bool flag = false;
foreach (string item2 in list2)
{
if (string.Equals(item2, item, StringComparison.OrdinalIgnoreCase))
{
flag = true;
break;
}
}
if (!flag)
{
list2.Add(item);
}
}
return list2;
}
private static string PluginsDirectory(string directory)
{
string text = directory;
for (int i = 0; i < 6; i++)
{
if (string.IsNullOrEmpty(text))
{
break;
}
string text2 = text.TrimEnd('\\', '/');
if (string.Equals(Path.GetFileName(text2), "plugins", StringComparison.OrdinalIgnoreCase))
{
return text2;
}
text = Path.GetDirectoryName(text2);
}
return null;
}
private static IEnumerable<KeyValuePair<string, List<Song>>> ScanPacks()
{
//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
List<KeyValuePair<string, List<Song>>> list = new List<KeyValuePair<string, List<Song>>>();
HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (string item in PackRoots())
{
string[] directories;
try
{
directories = Directory.GetDirectories(item);
}
catch (Exception ex)
{
log.LogWarning((object)("Could not look in " + item + ": " + ex.Message));
continue;
}
string[] array = directories;
foreach (string text in array)
{
string path = Path.Combine(text, "pack.json");
if (!File.Exists(path))
{
continue;
}
PackFile packFile;
try
{
using FileStream fileStream = File.OpenRead(path);
packFile = (PackFile)((XmlObjectSerializer)new DataContractJsonSerializer(typeof(PackFile))).ReadObject((Stream)fileStream);
}
catch (Exception ex)
{
log.LogWarning((object)("Skipping pack " + Path.GetFileName(text) + ": " + ex.Message));
continue;
}
if (packFile == null || packFile.Songs == null)
{
continue;
}
string text2 = (string.IsNullOrEmpty(packFile.Id) ? Path.GetFileName(text) : Sanitize(packFile.Id));
if (!hashSet.Add(text2))
{
continue;
}
List<Song> list2 = new List<Song>();
foreach (PackSong song2 in packFile.Songs)
{
if (song2 != null && !string.IsNullOrEmpty(song2.File))
{
string text3 = Path.Combine(text, song2.File);
if (!File.Exists(text3))
{
log.LogWarning((object)("Pack " + text2 + " is missing " + song2.File));
continue;
}
Song song = new Song();
song.Id = "pack:" + text2 + ":" + Sanitize(song2.File);
song.Title = (string.IsNullOrEmpty(song2.Title) ? Path.GetFileNameWithoutExtension(song2.File) : song2.Title);
BucketInfo bucketInfo = ResolveBucket(song2.Bucket);
song.Category = ((bucketInfo != null) ? CategoryForBucket(bucketInfo) : (string.IsNullOrEmpty(song2.Category) ? ("Pack: " + text2) : song2.Category));
song.BucketName = ((bucketInfo != null) ? bucketInfo.Name : song2.Bucket);
song.BucketValue = bucketInfo?.Value ?? BucketValue(song2.Bucket);
song.PackId = text2;
song.FilePath = text3;
song.Loop = !song2.Loop.HasValue || song2.Loop.Value;
song.Volume = ((!song2.Volume.HasValue) ? 1f : Mathf.Clamp01(song2.Volume.Value));
song.LoopSeconds = ((!song2.LoopSeconds.HasValue) ? 0f : Mathf.Clamp(song2.LoopSeconds.Value, 0f, 10f));
list2.Add(song);
}
}
list.Add(new KeyValuePair<string, List<Song>>(text2, list2));
if (list2.Count > 0)
{
log.LogInfo((object)("Pack " + text2 + ": " + list2.Count + " song(s) from " + text));
}
else
{
log.LogWarning((object)("Pack " + text2 + " in " + text + " has no song it can play."));
}
}
}
return list;
}
internal static bool SetEnabled(Song song, bool enabled)
{
song.Enabled = enabled;
Remember(song);
return true;
}
internal static void ResetCategory(string category)
{
ResetWhere((Song song) => song.Category == category);
}
internal static void ResetAll()
{
ResetWhere(null);
}
private static void ResetWhere(Func<Song, bool> match)
{
foreach (Song song in songs)
{
if (match == null || match(song))
{
song.Enabled = true;
song.Selected = false;
Remember(song);
}
}
Save();
}
internal static void SetLoopSeconds(Song song, float seconds)
{
float fade = (song.LoopSeconds = Mathf.Round(Mathf.Clamp(seconds, 0f, 10f) / 0.1f) * 0.1f);
Remember(song, save: false);
SaveLater();
AudioLoader.PreviewLoopEdge(song.Id, song.FilePath, song.GameClip, fade);
}
internal static bool RemoveUserSong(Song song)
{
if (song == null || string.IsNullOrEmpty(song.Id) || (Object)(object)song.GameClip != (Object)null)
{
return false;
}
if (!string.IsNullOrEmpty(song.PackId))
{
return false;
}
int num = data.Songs.RemoveAll((UserSong entry) => entry != null && string.Equals(entry.Id, song.Id, StringComparison.OrdinalIgnoreCase));
data.Settings.RemoveAll((SongSetting setting) => setting != null && string.Equals(setting.Id, song.Id, StringComparison.OrdinalIgnoreCase));
if (num == 0)
{
return false;
}
Save();
songs.RemoveAll((Song row) => row != null && string.Equals(row.Id, song.Id, StringComparison.OrdinalIgnoreCase));
if (!songs.Any((Song row) => row != null && row.Category == song.Category))
{
categories.Remove(song.Category);
}
log.LogInfo((object)("Removed " + song.Title + " from " + song.Category + "."));
return true;
}
internal static Song AddUserSong(string path, string category, string bucket, out List<string> replaced)
{
replaced = new List<string>();
UserSong userSong = new UserSong();
userSong.Id = "user:" + DateTime.UtcNow.Ticks + ":" + Sanitize(Path.GetFileName(path));
userSong.Title = Path.GetFileNameWithoutExtension(path);
userSong.Bucket = bucket;
userSong.File = path;
userSong.Loop = true;
userSong.Volume = 1f;
string b = ImportKey(userSong);
for (int num = data.Songs.Count - 1; num >= 0; num--)
{
UserSong old = data.Songs[num];
if (old != null && !string.IsNullOrEmpty(old.File) && string.Equals(ImportKey(old), b, StringComparison.OrdinalIgnoreCase))
{
replaced.Add(old.Id);
data.Songs.RemoveAt(num);
data.Settings.RemoveAll((SongSetting setting) => setting != null && string.Equals(setting.Id, old.Id, StringComparison.OrdinalIgnoreCase));
songs.RemoveAll((Song row) => row != null && string.Equals(row.Id, old.Id, StringComparison.OrdinalIgnoreCase));
}
}
data.Songs.Add(userSong);
Save();
Song song = UserSongRow(userSong);
Dictionary<string, SongSetting> dictionary = new Dictionary<string, SongSetting>(StringComparer.OrdinalIgnoreCase);
foreach (SongSetting setting in data.Settings)
{
if (setting != null && !string.IsNullOrEmpty(setting.Id))
{
dictionary[setting.Id] = setting;
}
}
Add(song, dictionary);
log.LogInfo((object)("Added " + song.Title + " to " + song.Category + "."));
return song;
}
internal static string BucketForCategory(string category)
{
foreach (Song song in songs)
{
if (song.Category == category)
{
return song.BucketName;
}
}
return null;
}
private static BucketInfo ResolveBucket(string text)
{
if (string.IsNullOrEmpty(text))
{
return null;
}
if (int.TryParse(text, out var result))
{
return GameMusic.FindBucket(result);
}
List<BucketInfo> list = MusicBuckets();
foreach (BucketInfo item in list)
{
if (string.Equals(item.Name, text, StringComparison.OrdinalIgnoreCase))
{
return item;
}
}
foreach (BucketInfo item2 in list)
{
if (string.Equals(CategoryForBucket(item2), text, StringComparison.OrdinalIgnoreCase))
{
return item2;
}
}
foreach (BucketInfo item3 in list)
{
string a = CategoryForBucket(item3).Replace(" Music", string.Empty);
if (string.Equals(a, text, StringComparison.OrdinalIgnoreCase))
{
return item3;
}
}
return null;
}
internal static int BucketValue(string name)
{
if (string.IsNullOrEmpty(name))
{
return -1;
}
if (int.TryParse(name, out var result))
{
return result;
}
foreach (BucketInfo item in GameMusic.Buckets())
{
if (string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase))
{
return item.Value;
}
}
return -1;
}
private static void Remember(Song song)
{
Remember(song, save: true);
}
private static void Remember(Song song, bool save)
{
SongSetting songSetting = data.Settings.FirstOrDefault((SongSetting s) => s != null && string.Equals(s.Id, song.Id, StringComparison.OrdinalIgnoreCase));
if (song.Enabled && !song.Selected && !(song.LoopSeconds > 0.01f))
{
if (songSetting != null)
{
data.Settings.Remove(songSetting);
}
if (save)
{
Save();
}
return;
}
if (songSetting == null)
{
songSetting = new SongSetting();
songSetting.Id = song.Id;
data.Settings.Add(songSetting);
}
songSetting.Enabled = song.Enabled;
songSetting.Selected = song.Selected;
songSetting.LoopSeconds = ((song.LoopSeconds > 0.01f) ? new float?(song.LoopSeconds) : ((float?)null));
if (save)
{
Save();
}
}
internal static string FriendlyTitle(string raw)
{
if (string.IsNullOrEmpty(raw))
{
return "Untitled";
}
string text = raw.Replace('_', ' ').Replace('-', ' ').Trim();
StringBuilder stringBuilder = new StringBuilder(text.Length + 8);
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (i > 0 && char.IsUpper(c) && !char.IsUpper(text[i - 1]) && text[i - 1] != ' ')
{
stringBuilder.Append(' ');
}
stringBuilder.Append(c);
}
return stringBuilder.ToString();
}
private static List<BucketInfo> MusicBuckets()
{
List<BucketInfo> list = GameMusic.Buckets();
List<BucketInfo> list2 = new List<BucketInfo>();
foreach (BucketInfo item in list)
{
if (IsMusic(item))
{
list2.Add(item);
}
}
if (list2.Count == 0)
{
list2 = list;
}
list2.Sort(CompareBuckets);
return list2;
}
private static bool IsMusic(BucketInfo bucket)
{
if (bucket == null)
{
return false;
}
if (IsIntro(bucket))
{
return true;
}
if (Contains(bucket.Name, "Music"))
{
return true;
}
return Contains(bucket.EditorName, "Music");
}
private static bool IsIntro(BucketInfo bucket)
{
if (bucket == null)
{
return false;
}
if (Contains(bucket.Name, "Intro"))
{
return true;
}
return Contains(bucket.EditorName, "Intro");
}
private static int CompareBuckets(BucketInfo left, BucketInfo right)
{
int num = Rank(left).CompareTo(Rank(right));
if (num != 0)
{
return num;
}
return left.Value.CompareTo(right.Value);
}
private static int Rank(BucketInfo bucket)
{
if (IsIntro(bucket))
{
return 0;
}
if (IsTown(bucket))
{
return 1;
}
if (Contains(bucket.Name, "Dungeon"))
{
return 2;
}
return 3;
}
private static bool IsTown(BucketInfo bucket)
{
return Contains(bucket.Name, "Temple") || Contains(bucket.Name, "Town");
}
private static bool Contains(string text, string value)
{
return !string.IsNullOrEmpty(text) && text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string CategoryForBucket(BucketInfo bucket)
{
if (IsIntro(bucket))
{
return "Intro Music";
}
if (IsTown(bucket))
{
return "Town Music";
}
if (Contains(bucket.Name, "Dungeon"))
{
return "Dungeon Music";
}
if (Contains(bucket.Name, "FateChoice"))
{
return "Fate Choice Music";
}
if (!string.IsNullOrEmpty(bucket.EditorName))
{
return FriendlyTitle(bucket.EditorName);
}
return CategoryForBucketName(bucket.Name);
}
internal static string CategoryForBucketName(string bucketName)
{
if (!string.IsNullOrEmpty(bucketName))
{
string text = bucketName.ToLowerInvariant();
if (text.Contains("intro"))
{
return "Intro Music";
}
if (text.Contains("dungeon"))
{
return "Dungeons";
}
if (text.Contains("templefate") || text.Contains("town") || text.Contains("village"))
{
return "Town";
}
if (text.Contains("fatechoice"))
{
return "Fate events";
}
if (text.StartsWith("music_") || text.Contains("mainmenu") || text.Contains("menu"))
{
return "Main menu";
}
if (text.Contains("boss"))
{
return "Bosses";
}
return "Other music";
}
return "Other music";
}
internal static string Sanitize(string text)
{
if (string.IsNullOrEmpty(text))
{
return "item";
}
StringBuilder stringBuilder = new StringBuilder(text.Length);
foreach (char c in text)
{
stringBuilder.Append((char.IsLetterOrDigit(c) || c == '.' || c == '_' || c == '-') ? c : '_');
}
string text2 = stringBuilder.ToString();
return (text2.Length > 48) ? text2.Substring(0, 48) : text2;
}
}
internal sealed class LoopPlayer : MonoBehaviour
{
private static LoopPlayer host;
private AudioSource first;
private AudioSource second;
private AudioClip clip;
private float level = 1f;
private float fade;
private int active;
private bool fading;
private float fadeStartedAt;
private bool silenced;
internal static bool Running => (Object)(object)host != (Object)null && (Object)(object)host.clip != (Object)null;
private float Effective => level * GameMusic.MusicScale;
internal static void Play(AudioClip clip, float volume, float fadeSeconds)
{
//IL_002c: Unknown result type (might be due to invalid IL or missing references)
//IL_0032: Expected O, but got Unknown
if (!((Object)(object)clip == (Object)null))
{
if ((Object)(object)host == (Object)null)
{
GameObject val = new GameObject("FATE Music Player loop");
Object.DontDestroyOnLoad((Object)(object)val);
host = val.AddComponent<LoopPlayer>();
host.first = host.NewSource("loop A");
host.second = host.NewSource("loop B");
}
host.clip = clip;
host.level = Mathf.Clamp01(volume);
host.fade = Mathf.Clamp(fadeSeconds, 0f, Mathf.Max(0f, clip.length * 0.5f));
host.active = 0;
host.fading = false;
host.silenced = false;
GameMusic.Route(host.first);
GameMusic.Route(host.second);
host.first.Stop();
host.second.Stop();
host.first.clip = clip;
host.first.loop = false;
host.first.volume = host.Effective;
host.first.time = 0f;
host.first.Play();
}
}
internal static void Stop()
{
if (!((Object)(object)host == (Object)null))
{
host.clip = null;
host.fading = false;
host.first.Stop();
host.second.Stop();
}
}
internal static void SetSilenced(bool value)
{
if (!((Object)(object)host == (Object)null))
{
host.silenced = value;
if ((Object)(object)host.first != (Object)null)
{
host.first.volume = (value ? 0f : host.Effective);
}
if ((Object)(object)host.second != (Object)null)
{
host.second.volume = 0f;
}
}
}
private AudioSource NewSource(string name)
{
//IL_0002: Unknown result type (might be due to invalid IL or missing references)
//IL_0008: Expected O, but got Unknown
GameObject val = new GameObject(name);
val.transform.SetParent(((Component)this).transform, false);
AudioSource val2 = val.AddComponent<AudioSource>();
val2.playOnAwake = false;
val2.loop = false;
val2.spatialBlend = 0f;
return val2;
}
private void Update()
{
if ((Object)(object)clip == (Object)null || silenced)
{
return;
}
AudioSource val = ((active == 0) ? first : second);
AudioSource val2 = ((active == 0) ? second : first);
if (!fading)
{
val.volume = Effective;
float num = clip.length - val.time;
if (!(num > fade))
{
if (fade <= 0.001f)
{
val.time = 0f;
return;
}
val2.clip = clip;
val2.loop = false;
val2.time = 0f;
val2.volume = 0f;
val2.Play();
fading = true;
fadeStartedAt = Time.unscaledTime;
}
}
else
{
float num2 = (Time.unscaledTime - fadeStartedAt) / fade;
if (num2 >= 1f)
{
val.Stop();
val.volume = 0f;
val2.volume = Effective;
active = 1 - active;
fading = false;
}
else
{
float effective = Effective;
val2.volume = effective * num2;
val.volume = effective * (1f - num2);
}
}
}
private void OnDestroy()
{
host = null;
}
}
internal static class ModMenuBridge
{
private const int MenuCap = 256;
private const int PacksRows = 3;
private static object module;
private static float nextAttempt;
private static float rebuildAt = -1f;
private static float missingSince = -1f;
private static bool warnedMissing;
private static bool warnedLegacy;
private static bool warnedNoRows;
private static ManualLogSource log;
private static MethodInfo trackRow;
private static MethodInfo removeRow;
private static MethodInfo simpleToggle;
private static MethodInfo simpleAction;
private static MethodInfo simpleSlider;
internal static void Init(ManualLogSource logger)
{
log = logger;
}
internal static void Tick()
{
if (module == null)
{
if (!(Time.unscaledTime < nextAttempt))
{
nextAttempt = Time.unscaledTime + 2f;
Register();
}
}
else if (rebuildAt > 0f && Time.unscaledTime >= rebuildAt)
{
rebuildAt = -1f;
Register();
}
}
internal static void RebuildSoon()
{
rebuildAt = Time.unscaledTime + 0.25f;
}
private static void Register()
{
try
{
Type type = AccessTools.TypeByName("ZombieGhost.ModMenu.ModMenuApi");
MethodInfo methodInfo = ((type == null) ? null : type.GetMethod("RegisterModule", new Type[2]
{
typeof(string),
typeof(string)
}));
if (type == null || methodInfo == null)
{
if (missingSince < 0f)
{
missingSince = Time.unscaledTime;
}
else if (!warnedMissing && Time.unscaledTime - missingSince > 10f)
{
warnedMissing = true;
if (log != null)
{
log.LogWarning((object)("FATE Mod Menu API was not found (" + ((type == null) ? "ZombieGhost.ModMenu.ModMenuApi type" : "RegisterModule method") + "); the player has no settings screen. Install FATE_ModMenu to configure it."));
}
}
return;
}
Dispose();
module = methodInfo.Invoke(null, new object[2] { "mikhailkillszombies.musicplayer", "FATE Music Player" });
if (module != null)
{
trackRow = (removeRow = (simpleToggle = (simpleAction = (simpleSlider = null))));
Type type2 = module.GetType();
MethodInfo method = type2.GetMethod("RegisterNote");
MethodInfo method2 = type2.GetMethod("RegisterTrack");
MethodInfo method3 = type2.GetMethod("RegisterWideAction");
if (method2 != null && method3 != null)
{
RegisterTrackRows(type2, method2, method3, method, type);
}
else
{
RegisterSimpleRows(type2, method, type);
}
missingSince = -1f;
warnedMissing = false;
}
}
catch (Exception ex)
{
if (log != null)
{
log.LogWarning((object)("Could not register with FATE Mod Menu: " + ex.Message));
}
module = null;
}
}
private static void RegisterTrackRows(Type type, MethodInfo track, MethodInfo wide, MethodInfo note, Type api)
{
MethodInfo method = type.GetMethod("SetCategoryResetCallback");
MethodInfo method2 = type.GetMethod("SetResetCallback");
MethodInfo method3 = type.GetMethod("RegisterTrackRemoveAction");
trackRow = track;
removeRow = method3;
simpleToggle = (simpleAction = (simpleSlider = null));
if (method != null)
{
method.Invoke(module, new object[1]
{
new Action<string>(Library.ResetCategory)
});
}
if (method2 != null)
{
method2.Invoke(module, new object[1]
{
new Action(Library.ResetAll)
});
}
List<Song> songs = Library.Songs;
List<string> list = songs.Select((Song song) => song.Category).Distinct().ToList();
int num = 253 - list.Count;
int num2 = 0;
foreach (string item in list)
{
string label = item;
foreach (Song item2 in songs.Where((Song s) => s.Category == label))
{
if (num2 >= num)
{
break;
}
num2++;
RegisterSongRow(track, method3, item2);
}
wide.Invoke(module, new object[4]
{
Id(label, "add"),
"ADD SONG",
(Action)delegate
{
AddSong(label);
},
label
});
}
RegisterPackRows(wide, note, num2, num, songs.Count);
if (log != null)
{
log.LogInfo((object)("Registered with FATE Mod Menu: " + num2 + " song row(s) in " + list.Count + " category(ies)."));
}
}
private static void RegisterSimpleRows(Type type, MethodInfo note, Type api)
{
MethodInfo method = type.GetMethod("RegisterToggle");
MethodInfo method2 = type.GetMethod("RegisterAction");
MethodInfo method3 = type.GetMethod("RegisterSlider");
if (method == null || method2 == null)
{
if (!warnedNoRows)
{
warnedNoRows = true;
if (log != null)
{
log.LogWarning((object)"FATE Mod Menu has no RegisterToggle/RegisterAction; the player has no settings screen.");
}
}
Dispose();
return;
}
simpleToggle = method;
simpleAction = method2;
simpleSlider = method3;
trackRow = (removeRow = null);
if (!warnedLegacy)
{
warnedLegacy = true;
if (log != null)
{
log.LogWarning((object)string.Concat("FATE Mod Menu API ", ApiVersion(api), " is older than the player: showing the compact song list (three rows per song). Install FATE_ModMenu 0.4.0 for one row per song."));
}
}
List<Song> songs = Library.Songs;
List<string> list = songs.Select((Song song) => song.Category).Distinct().ToList();
int num = 84;
int num2 = 0;
foreach (string item in list)
{
string label = item;
foreach (Song item2 in songs.Where((Song s) => s.Category == label))
{
if (num2 >= num)
{
break;
}
num2++;
RegisterSimpleSong(method, method2, method3, item2);
}
method2.Invoke(module, new object[4]
{
Id(label, "add"),
"ADD SONG",
(Action)delegate
{
AddSong(label);
},
label
});
}
RegisterPackRows(method2, note, num2, num, songs.Count);
if (log != null)
{
log.LogInfo((object)("Registered with FATE Mod Menu (compact): " + num2 + " song row(s) in " + list.Count + " category(ies)."));
}
}
private static void RegisterSimpleSong(MethodInfo toggle, MethodInfo action, MethodInfo slider, Song song)
{
toggle.Invoke(module, new object[5]
{
Id("on", song.Id),
song.Title,
(Func<bool>)(() => song.Enabled),
(Action<bool>)delegate(bool value)
{
Library.SetEnabled(song, value);
},
song.Category
});
action.Invoke(module, new object[4]
{
Id("preview", song.Id),
" → Play first 5 seconds",
(Action)delegate
{
Preview(song);
},
song.Category
});
if (!(slider == null))
{
slider.Invoke(module, new object[10]
{
Id("loop", song.Id),
" → Loop timer (seconds)",
(Func<float>)(() => song.LoopSeconds),
(Action<float>)delegate(float value)
{
Library.SetLoopSeconds(song, value);
},
0f,
10f,
0.1f,
song.Category,
null,
1600f
});
}
}
private static void RegisterSongRow(MethodInfo registerTrack, MethodInfo removeAction, Song song)
{
string text = Id("song", song.Id);
registerTrack.Invoke(module, new object[15]
{
text,
song.Title,
(Func<bool>)(() => song.Enabled),
(Action<bool>)delegate(bool value)
{
Library.SetEnabled(song, value);
},
(Action)delegate
{
Preview(song);
},
(Action)delegate
{
AudioLoader.StopPreview();
},
(Func<bool>)(() => AudioLoader.IsPlaying(song.Id)),
null,
null,
(Func<float>)(() => song.LoopSeconds),
(Action<float>)delegate(float value)
{
Library.SetLoopSeconds(song, value);
},
0f,
10f,
0.1f,
song.Category
});
if (!(removeAction == null) && song.IsCustom && string.IsNullOrEmpty(song.PackId))
{
removeAction.Invoke(module, new object[2]
{
text,
(Action)delegate
{
RemoveSong(song);
}
});
}
}
private static void RegisterPackRows(MethodInfo action, MethodInfo note, int shown, int budget, int total)
{
action.Invoke(module, new object[4]
{
"packs.export",
"EXPORT SONG PACK",
(Action)delegate
{
PackExporter.Export();
},
"Song packs"
});
action.Invoke(module, new object[4]
{
"packs.rescan",
"RESCAN SONG PACKS",
(Action)delegate
{
Library.Scan();
RebuildSoon();
Toast("Song packs rescanned.");
},
"Song packs"
});
if (!(note == null))
{
note.Invoke(module, new object[3] { "packs.note", "A pack installs into BepInEx/plugins/FateMusicPlayer/packs and adds its songs without touching yours. Removing the package removes its songs again.", "Song packs" });
if (shown >= budget && total > shown)
{
note.Invoke(module, new object[3]
{
"limit.note",
"This list is capped at " + shown + " songs by the menu's 256-setting limit; the rest stay in the pack or library but are not shown.",
"Song packs"
});
}
}
}
private static object ApiVersion(Type api)
{
try
{
PropertyInfo propertyInfo = ((api == null) ? null : api.GetProperty("ApiVersion"));
return (propertyInfo == null) ? "?" : propertyInfo.GetValue(null, null);
}
catch
{
return "?";
}
}
private static void Preview(Song song)
{
if (song == null)
{
return;
}
if (song.IsCustom)
{
if (log != null)
{
log.LogInfo((object)("Preview asked for " + song.Title + " (file: " + song.FilePath + ")"));
}
AudioLoader.Preview(song.Id, song.FilePath, 5f);
}
else if ((Object)(object)song.GameClip == (Object)null)
{
if (log != null)
{
log.LogWarning((object)("Preview asked for " + song.Title + " but it has no game clip."));
}
Toast("That song has nothing to preview.");
}
else
{
if (log != null)
{
log.LogInfo((object)("Preview asked for " + song.Title + " (game clip)"));
}
AudioLoader.PreviewClip(song.Id, song.GameClip, 5f);
}
}
private static void RemoveSong(Song song)
{
string rowId = ((song == null) ? null : Id("song", song.Id));
if (!Library.RemoveUserSong(song))
{
Toast("That song could not be removed.");
return;
}
AudioLoader.StopPreview();
if (!DropRow(rowId))
{
RebuildSoon();
}
Toast("Removed " + song.Title + ".");
}
private static bool DropRow(string rowId)
{
if (module == null || string.IsNullOrEmpty(rowId))
{
return false;
}
try
{
IList list = Settings();
if (list == null)
{
return false;
}
object obj = FindRow(list, rowId);
if (obj == null)
{
return false;
}
FieldInfo fieldInfo = FindField(obj.GetType(), "Removed");
if (fieldInfo != null)
{
fieldInfo.SetValue(obj, true);
}
list.Remove(obj);
RequestRefresh();
return true;
}
catch (Exception ex)
{
if (log != null)
{
log.LogWarning((object)("Could not take the row out in place, rebuilding the list instead: " + ex.Message));
}
return false;
}
}
private static IList Settings()
{
if (module == null)
{
return null;
}
FieldInfo field = module.GetType().GetField("Settings", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
return (field == null) ? null : (field.GetValue(module) as IList);
}
private static object FindRow(IList settings, string rowId)
{
if (settings == null || string.IsNullOrEmpty(rowId))
{
return null;
}
foreach (object setting in settings)
{
if (setting != null)
{
PropertyInfo property = setting.GetType().GetProperty("Id");
if (!(property == null) && string.Equals(property.GetValue(setting, null) as string, rowId, StringComparison.Ordinal))
{
return setting;
}
}
}
return null;
}
private static bool AppendSongRow(Song song)
{
if (module == null || song == null)
{
return false;
}
try
{
if (trackRow != null)
{
RegisterSongRow(trackRow, removeRow, song);
MoveBeforeAddRow(Id("song", song.Id), Id(song.Category, "add"));
}
else
{
if (!(simpleToggle != null) || !(simpleAction != null))
{
return false;
}
RegisterSimpleSong(simpleToggle, simpleAction, simpleSlider, song);
MoveBeforeAddRow(Id("on", song.Id), Id(song.Category, "add"));
MoveBeforeAddRow(Id("preview", song.Id), Id(song.Category, "add"));
MoveBeforeAddRow(Id("loop", song.Id), Id(song.Category, "add"));
}
RequestRefresh();
return true;
}
catch (Exception ex)
{
if (log != null)
{
log.LogWarning((object)("Could not add the row in place, rebuilding the list instead: " + ex.Message));
}
return false;
}
}
private static void MoveBeforeAddRow(string rowId, string addRowId)
{
IList list = Settings();
if (list != null)
{
object obj = FindRow(list, rowId);
object obj2 = FindRow(list, addRowId);
if (obj != null && obj2 != null)
{
int index = list.IndexOf(obj2);
list.Remove(obj);
list.Insert(index, obj);
}
}
}
private static FieldInfo FindField(Type type, string name)
{
Type type2 = type;
while (type2 != null)
{
FieldInfo field = type2.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field != null)
{
return field;
}
type2 = type2.BaseType;
}
return null;
}
private static void RequestRefresh()
{
Type type = AccessTools.TypeByName("ZombieGhost.ModMenu.ModMenuPlugin");
MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "RequestRefresh", new Type[0], (Type[])null));
if (methodInfo == null)
{
return;
}
try
{
methodInfo.Invoke(null, null);
}
catch
{
}
}
private static void AddSong(string category)
{
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
//IL_0049: Invalid comparison between Unknown and I4
string text = Library.BucketForCategory(category);
if (string.IsNullOrEmpty(text))
{
Toast("This category has no game music slot to play in.");
return;
}
string text2 = Dialogs.PickAudioFile();
if (string.IsNullOrEmpty(text2))
{
return;
}
if ((int)AudioLoader.AudioTypeFor(text2) == 0)
{
Toast("Use an .ogg, .wav or .mp3 file.");
return;
}
List<string> replaced;
Song song = Library.AddUserSong(text2, category, text, out replaced);
foreach (string item in replaced)
{
DropRow(Id("song", item));
DropRow(Id("on", item));
DropRow(Id("preview", item));
DropRow(Id("loop", item));
}
if (song == null || !AppendSongRow(song))
{
RebuildSoon();
}
Toast("Added " + Path.GetFileName(text2) + ".");
}
private static string Id(string prefix, string value)
{
string text = (prefix + "." + value).Replace('|', '_');
return (text.Length > 96) ? text.Substring(0, 96) : text;
}
private static void Toast(string message)
{
FateMusicPlayerPlugin.Toast(message);
}
private static void Dispose()
{
if (module != null)
{
try
{
(module as IDisposable).Dispose();
}
catch
{
}
module = null;
}
}
}
internal static class MusicEngine
{
private static float lastComplaint;
private static bool BeforePlayMusic(object __instance, object[] __args)
{
try
{
if (!FateMusicPlayerPlugin.ModEnabled)
{
return true;
}
if (__args == null || __args.Length < 2)
{
return true;
}
int bucketValue = Convert.ToInt32(__args[0]);
bool stopOthers = Convert.ToBoolean(__args[1]);
return Intercept(bucketValue, stopOthers);
}
catch (Exception ex)
{
Complain("playing music failed, falling back to vanilla: " + ex.Message, throttle: true);
return true;
}
}
internal static bool Intercept(int bucketValue, bool stopOthers)
{
List<Song> list = Library.SongsForBucket(bucketValue);
if (list.Count == 0)
{
LoopPlayer.Stop();
return true;
}
List<Song> list2 = list.Where((Song song2) => song2.Enabled).ToList();
Song song = list2.FirstOrDefault((Song song2) => song2.Selected);
if (!list.Any((Song song2) => song2.IsCustom) && song == null && list2.Count == list.Count)
{
LoopPlayer.Stop();
return true;
}
if (list2.Count == 0)
{
GameMusic.StopAllMusic();
GameMusic.RememberSlot(bucketValue, stopped: true);
return false;
}
Song chosen = song ?? list2[Random.Range(0, list2.Count)];
return Play(chosen, bucketValue, stopOthers);
}
private static bool Play(Song chosen, int bucketValue, bool stopOthers)
{
AudioClip val = chosen.GameClip;
if ((Object)(object)val == (Object)null && chosen.IsCustom)
{
val = AudioLoader.Cached(chosen.FilePath);
if ((Object)(object)val == (Object)null)
{
AudioLoader.Request(chosen.FilePath);
if (stopOthers)
{
GameMusic.StopAllMusic();
}
GameMusic.RememberSlot(bucketValue, stopped: false);
return false;
}
}
if ((Object)(object)val == (Object)null)
{
LoopPlayer.Stop();
return true;
}
if (stopOthers)
{
GameMusic.StopAllMusic();
}
if (chosen.Loop && chosen.LoopSeconds > 0.01f)
{
LoopPlayer.Play(val, chosen.Volume, chosen.LoopSeconds);
GameMusic.RememberSlot(bucketValue, stopped: true);
return false;
}
LoopPlayer.Stop();
AudioSource val2 = GameMusic.FreeMusicSource();
if ((Object)(object)val2 == (Object)null)
{
return false;
}
val2.clip = val;
val2.loop = chosen.Loop;
val2.volume = Mathf.Clamp01(chosen.Volume);
val2.time = 0f;
val2.Play();
GameMusic.RememberSlot(bucketValue, stopped: false);
return false;
}
private static void AfterDungeonMusic(object __instance)
{
try
{
int num = GameMusic.DungeonMusicValue();
if (num >= 0)
{
List<Song> source = Library.SongsForBucket(num);
List<Song> list = source.Where((Song song) => (Object)(object)song.GameClip != (Object)null || !string.IsNullOrEmpty(song.FilePath)).ToList();
if (list.Count != 0 && (list.Any((Song song) => (Object)(object)song.GameClip == (Object)null) || list.Any((Song song) => !song.Enabled || song.Selected)))
{
GameMusic.SilenceDungeonSource(__instance);
Intercept(num, stopOthers: true);
}
}
}
catch (Exception ex)
{
Complain("taking over the dungeon music failed, the floor's own track plays: " + ex.Message, throttle: false);
}
}
private static void AfterMainMenuMusic()
{
try
{
List<int> list = Library.CategorySlots("Intro Music");
if (list.Count == 0)
{
return;
}
List<Song> list2 = new List<Song>();
foreach (int item in list)
{
list2.AddRange(Library.SongsForBucket(item));
}
List<Song> list3 = list2.Where((Song song2) => (Object)(object)song2.GameClip != (Object)null || !string.IsNullOrEmpty(song2.FilePath)).ToList();
if (list3.Count == 0 || (!list3.Any((Song song2) => song2.IsCustom) && !list3.Any((Song song2) => !song2.Enabled || song2.Selected)))
{
return;
}
List<Song> list4 = list3.Where((Song song2) => song2.Enabled).ToList();
if (list4.Count == 0)
{
GameMusic.StopAllMusic();
return;
}
List<Song> list5 = (list4.Any((Song song2) => song2.IsCustom) ? list4.Where((Song song2) => song2.IsCustom).ToList() : list4);
Song song = list5.FirstOrDefault((Song song2) => song2.Selected) ?? list5[Random.Range(0, list5.Count)];
GameMusic.StopAllMusic();
Play(song, song.BucketValue, stopOthers: true);
if (FateMusicPlayerPlugin.LogSource != null)
{
FateMusicPlayerPlugin.LogSource.LogInfo((object)("Title music: " + song.Title + " (slot " + song.BucketValue + ")."));
}
}
catch (Exception ex)
{
Complain("taking over the title music failed, the game's own plays: " + ex.Message, throttle: false);
}
}
private static void BeforeGameStopsMusic()
{
LoopPlayer.Stop();
}
internal static void Complain(string message, bool throttle)
{
if (!throttle || !(Time.unscaledTime - lastComplaint < 10f))
{
lastComplaint = Time.unscaledTime;
if (FateMusicPlayerPlugin.LogSource != null)
{
FateMusicPlayerPlugin.LogSource.LogWarning((object)message);
}
}
}
}
internal static class MusicRuntime
{
private static ManualLogSource log;
private static Harmony harmony;
private static bool patched;
private static bool scanned;
private static bool tickerCreated;
private static float nextPatchAttempt;
private static float nextTick;
private static float nextScan;
private static int emptyScans;
internal static void Init(ManualLogSource logger, Harmony instance)
{
log = logger;
harmony = instance;
EnsureTicker();
patched = GameMusic.Patch(harmony, log);
nextPatchAttempt = Time.unscaledTime + 3f;
}
private static void EnsureTicker()
{
//IL_001a: Unknown result type (might be due to invalid IL or missing references)
//IL_0020: Expected O, but got Unknown
if (!tickerCreated)
{
tickerCreated = true;
GameObject val = new GameObject("FATE Music Player ticker");
Object.DontDestroyOnLoad((Object)(object)val);
val.AddComponent<MusicTicker>();
}
}
internal static void Tick()
{
float unscaledTime = Time.unscaledTime;
if (unscaledTime < nextTick)
{
return;
}
nextTick = unscaledTime + 0.2f;
EnsureTicker();
AudioLoader.EnsureHost(log);
if (!patched && unscaledTime >= nextPatchAttempt)
{
nextPatchAttempt = unscaledTime + 3f;
patched = GameMusic.Patch(harmony, log);
}
if (GameMusic.Manager() != null)
{
if (!scanned)
{
scanned = true;
Library.Scan();
ModMenuBridge.RebuildSoon();
if (log != null)
{
log.LogInfo((object)("Music library built: " + Library.Songs.Count + " song(s) in " + Library.Categories.Count + " category(ies)."));
}
}
else if (!Library.HasGameSongs && emptyScans < 30 && unscaledTime >= nextScan)
{
nextScan = unscaledTime + 2f;
emptyScans++;
Library.Scan();
if (Library.HasGameSongs)
{
ModMenuBridge.RebuildSoon();
if (log != null)
{
log.LogInfo((object)("Music library built after the audio manager filled in: " + Library.Songs.Count + " song(s) in " + Library.Categories.Count + " category(ies)."));
}
}
}
}
Library.FlushSaves();
ModMenuBridge.Tick();
}
internal static void AfterAudioManagerUpdate()
{
Tick();
}
internal static void Rescan()
{
scanned = true;
Library.Scan();
ModMenuBridge.RebuildSoon();
}
}
internal sealed class MusicTicker : MonoBehaviour
{
private void Update()
{
MusicRuntime.Tick();
}
}
internal static class Dialogs
{
internal static string PickAudioFile()
{
string result = null;
Run(delegate
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0007: Expected O, but got Unknown
//IL_0031: Unknown result type (might be due to invalid IL or missing references)
//IL_0037: Invalid comparison between Unknown and I4
OpenFileDialog val = new OpenFileDialog();
try
{
((FileDialog)val).Title = "Add a song";
((FileDialog)val).Filter = "Audio files (*.ogg;*.wav;*.mp3;*.aiff)|*.ogg;*.wav;*.mp3;*.aiff|All files (*.*)|*.*";
((FileDialog)val).CheckFileExists = true;
val.Multiselect = false;
if ((int)((CommonDialog)val).ShowDialog() == 1)
{
result = ((FileDialog)val).FileName;
}
}
finally
{
((IDisposable)val)?.Dispose();
}
});
return result;
}
internal static string PickExportFile(string suggested)
{
string result = null;
Run(delegate
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0007: Expected O, but got Unknown
//IL_0036: Unknown result type (might be due to invalid IL or missing references)
//IL_003c: Invalid comparison between Unknown and I4
SaveFileDialog val = new SaveFileDialog();
try
{
((FileDialog)val).Title = "Export song pack";
((FileDialog)val).Filter = "Thunderstore package (*.zip)|*.zip";
((FileDialog)val).FileName = suggested;
val.OverwritePrompt = true;
if ((int)((CommonDialog)val).ShowDialog() == 1)
{
result = ((FileDialog)val).FileName;
}
}
finally
{
((IDisposable)val)?.Dispose();
}
});
return result;
}
private static void Run(Action work)
{
Exception failure = null;
Thread thread = new Thread((ThreadStart)delegate
{
try
{
work();
}
catch (Exception ex)
{
failure = ex;
}
});
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (failure != null && FateMusicPlayerPlugin.LogSource != null)
{
FateMusicPlayerPlugin.LogSource.LogError((object)("The file window could not open: " + failure.Message));
}
}
}
internal static class PackExporter
{
private const string Author = "MikhailKillsZombies";
private const string ModName = "FATE_MusicPlayer";
private const string ModId = "MikhailKillsZombies-FATE_MusicPlayer";
internal static string Export()
{
List<Song> list = Library.Songs.Where((Song song) => (Object)(object)song.GameClip == (Object)null && string.IsNullOrEmpty(song.PackId) && !string.IsNullOrEmpty(song.FilePath)).ToList();
if (list.Count == 0)
{
Message("There are no songs of your own to export yet. Use ADD SONG under a category first.");
return null;
}
string text = Dialogs.PickExportFile("FATE Music Pack.zip");
if (string.IsNullOrEmpty(text))
{
return null;
}
try
{
string text2 = Library.Sanitize(Path.GetFileNameWithoutExtension(text));
if (string.IsNullOrEmpty(text2))
{
text2 = "SongPack";
}
int num = WritePack(text, text2, list);
Message("Exported " + num + " song(s) to " + text + ". Requires MikhailKillsZombies-FATE_MusicPlayer-0.4.3.");
return text;
}
catch (Exception ex)
{
if (FateMusicPlayerPlugin.LogSource != null)
{
FateMusicPlayerPlugin.LogSource.LogError((object)("Export failed: " + ex));
}
Message("Export failed: " + ex.Message);
return null;
}
}
private static int WritePack(string zipPath, string packId, List<Song> songs)
{
string text = "BepInEx/plugins/FateMusicPlayer/packs/" + packId + "/";
List<KeyValuePair<Song, string>> list = new List<KeyValuePair<Song, string>>();
HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
int num = 0;
foreach (Song song in songs)
{
if (!File.Exists(song.FilePath))
{
num++;
if (FateMusicPlayerPlugin.LogSource != null)
{
FateMusicPlayerPlugin.LogSource.LogWarning((object)("Not exporting " + song.Title + ": " + song.FilePath + " is no longer there."));
}
continue;
}
string fileName = Path.GetFileName(song.FilePath);
string text2 = fileName;
int num2 = 2;
while (!hashSet.Add(text2))
{
text2 = Path.GetFileNameWithoutExtension(fileName) + "-" + num2 + Path.GetExtension(fileName);
num2++;
}
list.Add(new KeyValuePair<Song, string>(song, text2));
}
if (list.Count == 0)
{
throw new IOException("none of the song files are still on disk");
}
byte[] array = new byte[81920];
using (FileStream stream = File.Create(zipPath))
{
using ZipArchive zipArchive = new ZipArchive(stream, ZipArchiveMode.Create);
WriteEntry(zipArchive, "manifest.json", BuildManifest(packId, list.Count));
WriteEntry(zipArchive, "README.md", BuildReadme(packId, list));
WriteEntry(zipArchive, text + "pack.json", BuildPackJson(packId, list));
foreach (KeyValuePair<Song, string> item in list)
{
ZipArchiveEntry zipArchiveEntry = zipArchive.CreateEntry(text + item.Value, CompressionLevel.Optimal);
using Stream stream2 = zipArchiveEntry.Open();
using FileStream fileStream = File.OpenRead(item.Key.FilePath);
int count;
while ((count = fileStream.Read(array, 0, array.Length)) > 0)
{
stream2.Write(array, 0, count);
}
}
}
Verify(zipPath, text);
if (num > 0)
{
Message("Skipped " + num + " song(s) whose file is no longer on disk.");
}
return list.Count;
}
private static void Verify(string zipPath, string packRoot)
{
string[] array = new string[3]
{
"manifest.json",
"README.md",
packRoot + "pack.json"
};
using ZipArchive zipArchive = new ZipArchive(File.OpenRead(zipPath), ZipArchiveMode.Read);
string[] array2 = array;
foreach (string text in array2)
{
if (zipArchive.GetEntry(text) == null)
{
throw new InvalidDataException("the package is missing " + text);
}
}
using StreamReader streamReader = new StreamReader(zipArchive.GetEntry("manifest.json").Open());
string text2 = streamReader.ReadToEnd();
if (text2.IndexOf("MikhailKillsZombies-FATE_MusicPlayer-", StringComparison.Ordinal) < 0)
{
throw new InvalidDataException("the package does not depend on MikhailKillsZombies-FATE_MusicPlayer");
}
string text = ManifestName(text2);
if (!IsThunderstoreName(text))
{
throw new InvalidDataException("Thunderstore cannot accept the package name \"" + text + "\"");
}
}
private static void WriteEntry(ZipArchive archive, string name, string content)
{
ZipArchiveEntry zipArchiveEntry = archive.CreateEntry(name, CompressionLevel.Optimal);
using StreamWriter streamWriter = new StreamWriter(zipArchiveEntry.Open(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
streamWriter.Write(content);
}
private static string BuildPackJson(string packId, List<KeyValuePair<Song, string>> songs)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("{\n");
stringBuilder.Append(" \"id\": \"").Append(Json(packId)).Append("\",\n");
stringBuilder.Append(" \"name\": \"").Append(Json(packId)).Append("\",\n");
stringBuilder.Append(" \"author\": \"\",\n");
stringBuilder.Append(" \"version\": \"1.0.0\",\n");
stringBuilder.Append(" \"songs\": [\n");
for (int i = 0; i < songs.Count; i++)
{
Song key = songs[i].Key;
stringBuilder.Append(" {\n");
stringBuilder.Append(" \"title\": \"").Append(Json(string.IsNullOrEmpty(key.Title) ? songs[i].Value : key.Title)).Append("\",\n");
stringBuilder.Append(" \"category\": \"").Append(Json(key.Category)).Append("\",\n");
stringBuilder.Append(" \"bucket\": \"").Append(Json(key.BucketName)).Append("\",\n");
stringBuilder.Append(" \"file\": \"").Append(Json(songs[i].Value)).Append("\",\n");
stringBuilder.Append(" \"loop\": ").Append(key.Loop ? "true" : "false").Append(",\n");
stringBuilder.Append(" \"volume\": ").Append(Mathf.Clamp01(key.Volume).ToString("0.###", CultureInfo.InvariantCulture)).Append(",\n");
stringBuilder.Append(" \"loopSeconds\": ").Append(Mathf.Clamp(key.LoopSeconds, 0f, 10f).ToString("0.0", CultureInfo.InvariantCulture)).Append("\n");
stringBuilder.Append(" }").Append((i == songs.Count - 1) ? "\n" : ",\n");
}
stringBuilder.Append(" ]\n}\n");
return stringBuilder.ToString();
}
private static string BuildManifest(string packId, int count)
{
return "{\n \"name\": \"" + Json(PackageName(packId)) + "\",\n \"version_number\": \"1.0.0\",\n \"website_url\": \"\",\n \"description\": \"Song pack for FATE Music Player: " + count + " track(s).\",\n \"dependencies\": [\n \"MikhailKillsZombies-FATE_MusicPlayer-0.4.3\"\n ]\n}\n";
}
private static string PackageName(string packId)
{
StringBuilder stringBuilder = new StringBuilder(packId?.Length ?? 0);
string text = packId ?? "SongPack";
foreach (char c in text)
{
stringBuilder.Append(IsNameCharacter(c) ? c : '_');
}
string text2 = stringBuilder.ToString().Trim(new char[1] { '_' });
if (text2.Length > 64)
{
text2 = text2.Substring(0, 64);
}
return (text2.Length < 3) ? "SongPack" : text2;
}
private static bool IsNameCharacter(char character)
{
return (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_';
}
private static bool IsThunderstoreName(string name)
{
if (string.IsNullOrEmpty(name) || name.Length > 128)
{
return false;
}
foreach (char character in name)
{
if (!IsNameCharacter(character))
{
return false;
}
}
return true;
}
private static string ManifestName(string manifest)
{
int num = manifest.IndexOf("\"name\": \"", StringComparison.Ordinal);
if (num < 0)
{
return null;
}
num += "\"name\": \"".Length;
int num2 = manifest.IndexOf('"', num);
return (num2 < 0) ? null : manifest.Substring(num, num2 - num);
}
private static string BuildReadme(string packId, List<KeyValuePair<Song, string>> songs)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("# ").Append(packId).Append("\n\n");
stringBuilder.Append("A song pack for **FATE Music Player**. Install it with r2modman or Thunderstore Mod Manager.\n\n");
stringBuilder.Append("It declares MikhailKillsZombies-FATE_MusicPlayer-0.4.3 as a dependency, so the player is installed with it.\n\n");
stringBuilder.Append("Its songs appear in their own categories and play in the location slots they were exported for. ");
stringBuilder.Append("It adds rows only; it never changes the songs that already exist, and removing this package removes exactly these tracks again.\n\n");
stringBuilder.Append("## Songs\n\n");
foreach (KeyValuePair<Song, string> song in songs)
{
stringBuilder.Append("- ").Append(string.IsNullOrEmpty(song.Key.Title) ? song.Value : song.Key.Title).Append("\n");
}
return stringBuilder.ToString();
}
private static string Json(string value)
{
if (value == null)
{
return "";
}
return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n")
.Replace("\r", "");
}
internal static void Message(string text)
{
if (FateMusicPlayerPlugin.LogSource != null)
{
FateMusicPlayerPlugin.LogSource.LogInfo((object)text);
}
FateMusicPlayerPlugin.Toast(text);
}
}