using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using PulseLib.Cafe.Models; using PulseLib.Internal; using PulseLib.Localization; using PulseLib.Localization.Patches; using PulseLib.Patches; using SA.GoogleDoc; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("io.github.nonperforming.pulse")] [assembly: AssemblyConfiguration("Release-BepInEx5")] [assembly: AssemblyDescription("Helper library for Rhythm Doctor")] [assembly: AssemblyFileVersion("0.0.6.0")] [assembly: AssemblyInformationalVersion("0.0.6+0bd0d3b8264966a50d377aa42bbde266b6ba4226")] [assembly: AssemblyProduct("Pulse")] [assembly: AssemblyTitle("io.github.nonperforming.pulse")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.6.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 PulseLib { internal static class Configuration { internal static ConfigEntry UseCustomLocalization; internal static ConfigEntry LoadLocalizationFromDisk; internal static ConfigEntry CustomLanguage; internal static ConfigEntry LogLocalization; internal static ConfigEntry DumpLocalizationDictionary; internal static void Bind(ConfigFile config) { UseCustomLocalization = config.Bind("Localization", "UseCustomLocalization", true, "Whether to activate Pulse's localization system or not."); LoadLocalizationFromDisk = config.Bind("Localization", "LoadLocalizationFromDisk", true, "Load localization files from disk.\nLocalization files should be put under `/Localization//
.json`."); CustomLanguage = config.Bind("Localization", "CustomLanguage", "", "The custom language chosen."); LogLocalization = config.Bind("Development", "LogLocalization", false, "Log any calls that attempt to get a localized string."); DumpLocalizationDictionary = config.Bind("Development", "DumpLocalizationDictionary", false, "Dump any loaded localization dictionary to disk.\nDumped files will be found under `/Dumped/.json`."); } } public class Events { private static readonly Lazy _lazy = new Lazy(() => new Events()); public static Events Instance => _lazy.Value; public event EventHandler? LevelDeselected; private Events() { Plugin.Logger.LogInfo((object)"Events singleton created"); } public static void RaiseLevelDeselected() { Instance.LevelDeselected?.Invoke(null, EventArgs.Empty); } } [BepInPlugin("io.github.nonperforming.pulse", "Pulse", "0.0.6")] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Logger; private readonly string _localizationPath = Path.Join(Paths.GameRootPath.AsSpan(), "Localization".AsSpan()); private Harmony _harmony = new Harmony("io.github.nonperforming.pulse"); private void Awake() { Logger = ((BaseUnityPlugin)this).Logger; Logger.LogInfo((object)"Loading Pulse version 0.0.6"); Setup(); Logger.LogInfo((object)"Binding configuration"); Configuration.Bind(((BaseUnityPlugin)this).Config); if (Configuration.LoadLocalizationFromDisk.Value) { Logger.LogInfo((object)"Discovering localization"); CustomLocalizationHelper.SearchAndRegisterDirectory(_localizationPath); } Logger.LogInfo((object)"Creating links to events"); _harmony.PatchAll(typeof(EventsLink)); Logger.LogInfo((object)"Applying patches"); _harmony.PatchAll(typeof(CustomLocalizationPatch)); _harmony.PatchAll(typeof(MainMenuPatch)); Logger.LogInfo((object)"Pulse loaded!"); } private void Setup() { Directory.CreateDirectory(_localizationPath); } } public static class MyPluginInfo { public const string PLUGIN_GUID = "io.github.nonperforming.pulse"; public const string PLUGIN_NAME = "Pulse"; public const string PLUGIN_VERSION = "0.0.6"; } } namespace PulseLib.Patches { [HarmonyPatch(typeof(scnMenu))] internal class MainMenuPatch { [HarmonyPatch("Start")] [HarmonyPostfix] private static void AddModCountLabelPatch() { //IL_00ca: Unknown result type (might be due to invalid IL or missing references) Plugin.Logger.LogInfo((object)"Creating mod count label"); GameObject obj = GameObject.Find("/scnMenu/mainMenu/version"); GameObject obj2 = Object.Instantiate(obj, obj.transform.parent, true); ((Object)obj2).name = "PULSE - mod count"; Object.Destroy((Object)(object)obj2.transform.GetChild(0)); Object.Destroy((Object)(object)obj2.GetComponent()); Text component = obj2.GetComponent(); component.fontSize = 5; int num = Chainloader.PluginInfos.Count - 1; int count = Chainloader.DependencyErrors.Count; component.text = string.Format("Pulse v{0} / {1} mods loaded", "0.0.6", num); if (count != 0) { component.text += $" / {count} mods failed to load"; } obj2.transform.localPosition = new Vector3(-167f, -82f, 0f); } } } namespace PulseLib.Localization { public static class CustomLocalizationHelper { internal static Dictionary>> customLocalizedStrings = new Dictionary>>(); public static void Register(string language, string section, string key, string localized) { if (!customLocalizedStrings.ContainsKey(language)) { Plugin.Logger.LogDebug((object)("[Localization] Creating language dictionary for " + language)); customLocalizedStrings[language] = new Dictionary>(); } if (!customLocalizedStrings[language].ContainsKey(section)) { Plugin.Logger.LogDebug((object)("[Localization] Creating section dictionary for " + section + " of " + language)); customLocalizedStrings[language][section] = new Dictionary(); } customLocalizedStrings[language][section][key] = localized; Plugin.Logger.LogDebug((object)("[Localization] Registered '" + section + "." + key + "' as '" + localized + "' for " + language)); } public static void Register(string language, string section, Dictionary localized) { foreach (var (key, localized2) in localized) { Register(language, section, key, localized2); } } public unsafe static void Register(LangCode language, LangSection section, Dictionary localized) { Register(((object)(*(LangCode*)(&language))/*cast due to .constrained prefix*/).ToString(), ((object)(*(LangSection*)(§ion))/*cast due to .constrained prefix*/).ToString(), localized); } public unsafe static void Register(string language, LangSection section, Dictionary localized) { Register(language, ((object)(*(LangSection*)(§ion))/*cast due to .constrained prefix*/).ToString(), localized); } public static void Register(Dictionary>> localized) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) foreach (var (language, dictionary2) in localized) { foreach (var (section, localized2) in dictionary2) { Register(language, section, localized2); } } } public static void Register(Dictionary>> localized) { foreach (KeyValuePair>> item in localized) { item.Deconstruct(out var key, out var value); string language = key; foreach (KeyValuePair> item2 in value) { item2.Deconstruct(out key, out var value2); string section = key; Dictionary localized2 = value2; Register(language, section, localized2); } } } public static void Register(string language, Dictionary> localized) { foreach (var (section, localized2) in localized) { Register(language, section, localized2); } } public static void SearchAndRegisterDirectory(string path) { Plugin.Logger.LogDebug((object)("[Localization] Searching for localization JSONs in " + path)); Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly); foreach (string item in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)) { RegisterDirectory(path, new DirectoryInfo(item).Name); } } public static void RegisterDirectory(string path, string language) { Plugin.Logger.LogDebug((object)("[Localization] Registering localization JSONs in " + path + " for " + language)); IEnumerable source = Directory.EnumerateFiles(path, "*.json", SearchOption.AllDirectories); if (source.Count() == 1) { Register(language, (Dictionary>)Json.Deserialize(File.ReadAllText(source.ElementAt(0)))); return; } foreach (string item in Directory.EnumerateFiles(path, "*.json", SearchOption.AllDirectories)) { Register(language, Path.GetFileNameWithoutExtension(item), JsonConvert.DeserializeObject>(File.ReadAllText(item))); } } public static string GetLanguage() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrWhiteSpace(Configuration.CustomLanguage.Value)) { return ((object)Localization.Client.CurrentLanguage/*cast due to .constrained prefix*/).ToString(); } return Configuration.CustomLanguage.Value; } } } namespace PulseLib.Localization.Patches { [HarmonyPatch(typeof(LocalizationClient))] internal static class CustomLocalizationPatch { [HarmonyPatch("ExistsLocalizedString")] [HarmonyPrefix] private static void CheckCustomLocalizationExists(string token, ref bool __result, ref bool __runOriginal) { if (TryGetCustomLocalization(token, out string _)) { __result = true; __runOriginal = false; } } [HarmonyPatch("GetLocalizedString", new Type[] { typeof(string), typeof(LangSection), typeof(LangCode) })] [HarmonyPrefix] private static void GetCustomLocalization(string token, LangCode language, ref string __result, ref bool __runOriginal) { if (TryGetCustomLocalization(token, out string localized)) { __result = localized; __runOriginal = false; Plugin.Logger.LogDebug((object)("Got custom localization '" + token + "' as '" + localized + "'")); } } private static bool TryGetCustomLocalization(string token, [NotNullWhen(true)] out string? localized) { string[] array = token.Split('.'); string section = array[0]; string key = string.Join(".", array, 1, array.Length - 1); return TryGetCustomLocalization(section, key, out localized); } private static bool TryGetCustomLocalization(string section, string key, [NotNullWhen(true)] out string? localized) { if (!CustomLocalizationHelper.customLocalizedStrings.TryGetValue(CustomLocalizationHelper.GetLanguage(), out Dictionary> value) || !value.TryGetValue(section, out var value2)) { localized = null; return false; } return value2.TryGetValue(key, out localized); } } } namespace PulseLib.Internal { [HarmonyPatch] internal static class EventsLink { [HarmonyPatch(typeof(scnLevelSelect), "Update")] [HarmonyTranspiler] private static IEnumerable LinkRaiseLevelDeselectedEventPatch(IEnumerable instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Expected O, but got Unknown //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Expected O, but got Unknown //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Expected O, but got Unknown //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Expected O, but got Unknown //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Expected O, but got Unknown //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Expected O, but got Unknown //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Expected O, but got Unknown return new CodeMatcher(instructions, (ILGenerator)null).MatchForward(true, (CodeMatch[])(object)new CodeMatch[10] { new CodeMatch((OpCode?)OpCodes.Ldsfld, (object)AccessTools.Field(typeof(RDInput), "cancelPress"), (string)null), new CodeMatch((OpCode?)OpCodes.Brfalse, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldarg_0, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldfld, (object)AccessTools.Field(typeof(scnLevelSelect), "heartMonitor"), (string)null), new CodeMatch((OpCode?)OpCodes.Ldfld, (object)AccessTools.Field(typeof(HeartMonitor), "visible"), (string)null), new CodeMatch((OpCode?)OpCodes.Brfalse, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldarg_0, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldfld, (object)AccessTools.Field(typeof(scnLevelSelect), "heartMonitor"), (string)null), new CodeMatch((OpCode?)OpCodes.Ldfld, (object)AccessTools.Field(typeof(HeartMonitor), "goingToLevel"), (string)null), new CodeMatch((OpCode?)OpCodes.Brtrue, (object)null, (string)null) }).ThrowIfInvalid("Could not find RDInput.cancelPress && heartMonitor.visible && !heartMonitor.goingToLevel in scnLevelSelect.Update").InsertAndAdvance((CodeInstruction[])(object)new CodeInstruction[1] { CodeInstruction.Call(typeof(Events), "RaiseLevelDeselected", (Type[])null, (Type[])null) }) .InstructionEnumeration(); } } } namespace PulseLib.Extensions { public static class LevelExtensions { public static readonly Level[] AllStandardLevels; public static readonly Level[] AllBossLevels; public static readonly Level[] AllBonusLevels; public static readonly Level[] AllMinigameLevels; public static readonly Level[] AllIntermissionLevels; static LevelExtensions() { Level[] array = new Level[47]; RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); AllStandardLevels = (Level[])(object)array; Level[] array2 = new Level[8]; RuntimeHelpers.InitializeArray(array2, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); AllBossLevels = (Level[])(object)array2; Level[] array3 = new Level[3]; RuntimeHelpers.InitializeArray(array3, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); AllBonusLevels = (Level[])(object)array3; AllMinigameLevels = (Level[])(object)new Level[2] { (Level)17, (Level)41 }; Level[] array4 = new Level[3]; RuntimeHelpers.InitializeArray(array4, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); AllIntermissionLevels = (Level[])(object)array4; } } public static class RDStringExtensions { public static string GetWithSubstitution(string key, params KeyValuePair[] substituteWith) { string text = RDString.Get(key); foreach (KeyValuePair keyValuePair in substituteWith) { keyValuePair.Deconstruct(out var key2, out var value); string text2 = key2; string newValue = value; text = text.Replace("[" + text2 + "]", newValue, StringComparison.Ordinal); } return text; } } public static class scnBaseExtensions { public static void GoToLevelWithCustomLevelData(CustomLevelData levelData, bool twoPlayer) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 scnBase.GoToLevelWithExternalPath((twoPlayer && !levelData.isLegacyLevel && (int)levelData.settings.canBePlayedOn == 3 && !string.IsNullOrEmpty(levelData.settings.separate2PLevelFilename)) ? Path.Combine(levelData.path, levelData.settings.separate2PLevelFilename) : Path.Combine(levelData.path, "main.rdlevel")); } public static void GoToLevelWithImportLevel(ImportLevel importLevel, bool twoPlayer) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 scnBase.GoToLevelWithExternalPath((twoPlayer && (int)importLevel.settings.canBePlayedOn == 3 && !string.IsNullOrEmpty(importLevel.settings.separate2PLevelFilename)) ? Path.Combine(importLevel.path, importLevel.settings.separate2PLevelFilename) : Path.Combine(importLevel.path, "main.rdlevel")); } } } namespace PulseLib.Cafe { public static class Cafe { public const string URL = "https://rhythm.cafe/"; public const string API_URL = "https://rhythm.cafe/api/"; private static HttpClient _client = new HttpClient { BaseAddress = new Uri("https://rhythm.cafe/api/"), DefaultRequestHeaders = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") }, UserAgent = { ProductInfoHeaderValue.Parse("Pulse/0.0.6") } } }; public static async Task GetLevelData(string levelId) { using HttpResponseMessage response = await _client.GetAsync("levels/" + levelId + "/"); if (!response.IsSuccessStatusCode) { Plugin.Logger.LogError((object)("[Cafe] Could not get level data: " + response.ReasonPhrase)); return null; } return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); } public static async Task GetDailyBlend() { using HttpResponseMessage response = await _client.GetAsync("levels/todays-blend/"); if (!response.IsSuccessStatusCode) { Plugin.Logger.LogError((object)("[Cafe] Could not get daily blend: " + response.ReasonPhrase)); return null; } return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); } } } namespace PulseLib.Cafe.Models { public struct BlendData { [JsonProperty("blend")] public LevelData Blend { get; private set; } } public struct Club { [JsonProperty("id")] public string Id { get; private set; } [JsonProperty("name")] public string Name { get; private set; } } public struct LevelData { [JsonProperty("id")] public string Id { get; private set; } [JsonProperty("artist")] public string Artist { get; private set; } [JsonProperty("artist_tokens")] public string[] ArtistTokens { get; private set; } [JsonProperty("artist_raw")] public string ArtistRaw { get; private set; } [JsonProperty("song")] public string Song { get; private set; } [JsonProperty("song_alt")] public string SongAlt { get; private set; } [JsonProperty("song_raw")] public string SongRaw { get; private set; } [JsonProperty("seizure_warning")] public bool SeizureWarning { get; private set; } [JsonProperty("description")] public string Description { get; private set; } [JsonProperty("hue")] public float Hue { get; private set; } [JsonProperty("authors")] public string[] Authors { get; private set; } [JsonProperty("authors_raw")] public string AuthorsRaw { get; private set; } [JsonProperty("max_bpm")] public long MaxBPM { get; private set; } [JsonProperty("min_bpm")] public long MinBPM { get; private set; } [JsonProperty("difficulty")] public LevelDifficulty Difficulty { get; private set; } [JsonProperty("single_player")] public bool SinglePlayer { get; private set; } [JsonProperty("two_player")] public bool TwoPlayer { get; private set; } [JsonProperty("last_updated")] public DateTimeOffset LastUpdated { get; private set; } [JsonProperty("tags")] public string[] Tags { get; private set; } [JsonProperty("sha1")] public string SHA1 { get; private set; } [JsonProperty("rdlevel_sha1")] public string RDLevelSHA1 { get; private set; } [JsonProperty("rd_md5")] public string RDMD5 { get; private set; } [JsonProperty("is_animated")] public bool IsAnimated { get; private set; } [JsonProperty("rdzip_url")] public Uri RDZipUrl { get; private set; } [JsonProperty("image_url")] public Uri ImageUrl { get; private set; } [JsonProperty("thumb_url")] public Uri ThumbUrl { get; private set; } [JsonProperty("icon_url")] public string IconUrl { get; private set; } [JsonProperty("submitter")] public Submitter Submitter { get; private set; } [JsonProperty("club")] public Club Club { get; private set; } [JsonProperty("approval")] public int Approval { get; private set; } [JsonProperty("approval_notes_public")] public string ApprovalNotesPublic { get; private set; } [JsonProperty("is_private")] public bool IsPrivate { get; private set; } [JsonProperty("prefill_version")] public long PrefillVersion { get; private set; } [JsonProperty("has_classics")] public bool HasClassics { get; private set; } [JsonProperty("has_oneshots")] public bool HasOneshots { get; private set; } [JsonProperty("has_squareshots")] public bool HasSquareshots { get; private set; } [JsonProperty("has_freezeshots")] public bool HasFreezeshots { get; private set; } [JsonProperty("has_burnshots")] public bool HasBurnshots { get; private set; } [JsonProperty("has_holdshots")] public bool HasHoldshots { get; private set; } [JsonProperty("has_triangleshots")] public bool HasTriangleshots { get; private set; } [JsonProperty("has_skipshots")] public bool HasSkipshots { get; private set; } [JsonProperty("has_subdivs")] public bool HasSubdivs { get; private set; } [JsonProperty("has_synco")] public bool HasSynco { get; private set; } [JsonProperty("has_freetimes")] public bool HasFreetimes { get; private set; } [JsonProperty("has_holds")] public bool HasHolds { get; private set; } [JsonProperty("has_window_dance")] public bool HasWindowDance { get; private set; } [JsonProperty("has_rdcode")] public bool HasRDCode { get; private set; } [JsonProperty("has_cpu_rows")] public bool HasCpuRows { get; private set; } [JsonProperty("total_hits_approx")] public long TotalHitsApprox { get; private set; } [JsonProperty("is_hidden")] public bool IsHidden { get; private set; } } public struct Submitter { [JsonProperty("id")] public string Id { get; private set; } [JsonProperty("displayName")] public string DisplayName { get; private set; } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }