diff --git a/Extensions.cs b/Extensions.cs index 6b7a8e1..8aab9cf 100644 --- a/Extensions.cs +++ b/Extensions.cs @@ -1,9 +1,19 @@ using HarmonyLib; +using System.Reflection; namespace QualityOfSpeen { public static class Extensions { public static void PatchAll(this Harmony harmony) => harmony.PatchAll(typeof(T)); + + // Source: https://stackoverflow.com/a/46488844 + public static T GetFieldValue(this object obj, string name) + { + // Set the flags so that private and public fields from instances will be found + var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + var field = obj.GetType().GetField(name, bindingFlags); + return (T)field?.GetValue(obj); + } } } diff --git a/Features/DiscordRPCFix.cs b/Features/DiscordRPCFix.cs index 3979839..f4df19d 100644 --- a/Features/DiscordRPCFix.cs +++ b/Features/DiscordRPCFix.cs @@ -1,74 +1,91 @@ using HarmonyLib; +using System.Reflection; using UnityEngine; namespace QualityOfSpeen.Features { public class DiscordRPCFix { - public static bool UpdateDiscord; - public static bool SecretMode; + public static bool UpdateDiscord = Main.ModConfig.GetValueOrDefaultTo("Discord", "EnableRichPresence", true); + public static bool SecretMode = Main.ModConfig.GetValueOrDefaultTo("Discord", "StartInSecretMode", false); - private static void Awake() + [HarmonyPatch] + class SpinDiscordPatch { - UpdateDiscord = Main.ModConfig.GetValueOrDefaultTo("Discord", "EnableRichPresence", true); - SecretMode = Main.ModConfig.GetValueOrDefaultTo("Discord", "StartInSecretMode", false); - } - - [HarmonyPatch(typeof(SpinDiscord), nameof(SpinDiscord.UpdateActivityPresence))] - [HarmonyPrefix] - private static bool UpdateActivityPresencePrefix(ref string state, ref string details, ref string coverArt, ref string trackArtist, ref string trackTitle, ref string trackLabel, ref long endTime) - { - if (!UpdateDiscord) return false; + static MethodBase TargetMethod() + { + var type = AccessTools.TypeByName("SpinDiscord"); + return AccessTools.Method(type, "UpdateActivityPresence"); + } - if (Main.InGameState == InGameState.Editor) + static bool Prefix(ref string state, ref string details, ref string coverArt, ref string trackArtist, ref string trackTitle, ref string trackLabel, ref long endTime) { - if (SecretMode) + //Main.Logger.LogMessage("Activity Update"); + if (!UpdateDiscord) return false; + + + if (Main.InGameState == InGameState.Editor) { - details = "Editing "; - state = "Secret Mode enabled!"; - trackArtist = "Secret"; - trackTitle = "Secret"; - trackLabel = "Secret"; - endTime = 0; + if (SecretMode) + { + details = "Editing "; + state = "Secret Mode enabled!"; + trackArtist = "Secret"; + trackTitle = "Secret"; + trackLabel = "Secret"; + endTime = 0; + } + else + { + details = "Editing " + trackTitle; + endTime = 0; + } } - else + + if (Main.IsDead) { - details = "Editing " + trackTitle; + state = "Failed"; endTime = 0; } - } - if (Main.IsDead) - { - state = "Failed"; - endTime = 0; - } + if (Main.InGameState == InGameState.CustomLevelSelectMenu) + { + state = "Picking Custom Track"; + endTime = 0; + } - if (Main.InGameState == InGameState.CustomLevelSelectMenu) - { - state = "Picking Custom Track"; - endTime = 0; + return true; } + } - return true; + [HarmonyPatch(typeof(Track), "UpdateRichPresence")] + [HarmonyPrefix] + private static bool TrackUpdateRichPresencePrefix() + { + return UpdateDiscord; } [HarmonyPatch(typeof(Track), nameof(Track.Update))] [HarmonyPostfix] private static void TrackUpdatePostfix() { - if (Input.GetKeyDown(KeyCode.F5)) + if (Input.GetKeyDown(KeyCode.F4)) { SecretMode = !SecretMode; - Main.Logger.LogWarning("Secret Mode " + (SecretMode ? "Enabled" : "Disabled")); + NotificationSystemGUI.AddMessage("Secret Mode " + (SecretMode ? "Enabled" : "Disabled")); + } + if (Input.GetKeyDown(KeyCode.F3)) + { + UpdateDiscord = !UpdateDiscord; + NotificationSystemGUI.AddMessage("Discord Rich Presence Toggled " + (UpdateDiscord ? "On" : "Off")); } } - [HarmonyPatch(typeof(XDCustomLevelSelectMenu), nameof(XDCustomLevelSelectMenu.Awake))] + [HarmonyPatch(typeof(XDCustomLevelSelectMenu), "Awake")] [HarmonyPostfix] private static void XDCustomLevelSelectMenuAwakePostfix() { - Main.Logger.LogWarning("Reminder: Secret Mode is currently " + (SecretMode ? "Enabled" : "Disabled")); + NotificationSystemGUI.AddMessage("Reminder: Secret Mode is currently " + (SecretMode ? "Enabled" : "Disabled")); } } } diff --git a/Features/InstantRestartKey.cs b/Features/InstantRestartKey.cs index e046cd6..c9ebc44 100644 --- a/Features/InstantRestartKey.cs +++ b/Features/InstantRestartKey.cs @@ -6,12 +6,16 @@ public class InstantRestartKey { public static bool currentKeyState = false, previousKeyState = false; + private static XDInput inputRef; + [HarmonyPatch(typeof(Track), nameof(Track.Update))] [HarmonyPostfix] private static void RestartKey() { + if (inputRef == null) + inputRef = XDInputModule.Instance.GetFieldValue("xdInput"); previousKeyState = currentKeyState; - currentKeyState = XDInputModule.Instance.xdInput.GetButtonDown(InputMapping.SpinCommands.RestartSong); + currentKeyState = inputRef.GetButtonDown(InputMapping.SpinCommands.RestartSong); if (currentKeyState && !previousKeyState && Main.InGameState == InGameState.Playing && !Main.IsDead && !Main.HasWon && !Main.IsRestarting) { Track.Instance.RestartTrack(); diff --git a/Features/NoIntro.cs b/Features/NoIntro.cs deleted file mode 100644 index 2d6638d..0000000 --- a/Features/NoIntro.cs +++ /dev/null @@ -1,26 +0,0 @@ -using HarmonyLib; -using UnityEngine; - -namespace QualityOfSpeen.Features -{ - public class NoIntro - { - private static bool SkipIntro; - - private static void Awake() - { - SkipIntro = Main.ModConfig.GetValueOrDefaultTo("NoIntro", "AutoSkip", false); - } - - [HarmonyPatch(typeof(StartupScene), nameof(StartupScene.Update))] - [HarmonyPostfix] - private static void StartupSceneAwakePostfix(StartupScene __instance) - { - if (Input.anyKeyDown) SkipIntro = true; - if (SkipIntro) - { - __instance._animationTimer = 8f; - } - } - } -} diff --git a/IniFile.cs b/IniFile.cs index ac837af..24212e8 100644 --- a/IniFile.cs +++ b/IniFile.cs @@ -10,8 +10,12 @@ public class IniFile { private Dictionary> iniContent; private string FilePath; - public static IFormatProvider Culture; + public IFormatProvider Culture; + /// + /// Creates a new Ini file handler for the file at the given path. /!\ THE FILE MUST EXIST FIRST. IT WILL NOT BE CREATED AUTOMATICALLY IF MISSING + /// + /// The path to the file. Make sure the given path is valid! public IniFile(string filePath) { FilePath = filePath; @@ -21,9 +25,14 @@ public IniFile(string filePath) ParseFile(fileContent); } - public Dictionary this[string setting] + /// + /// Get a section as a string-string dictionary + /// + /// The section to get + /// The section as a string-string dictionary + public Dictionary this[string section] { - get { return iniContent[setting]; } + get { return iniContent[section]; } } private void ParseFile(string[] content) @@ -53,7 +62,7 @@ private void ParseFile(string[] content) private string MakeString() { StringBuilder sb = new StringBuilder(); - + foreach (KeyValuePair> pair in iniContent) { sb.AppendLine("[" + pair.Key + "]"); @@ -67,20 +76,39 @@ private string MakeString() return sb.ToString(); } - public void AddSetting(string section, string settingName, string defaultValue) - { - if (!iniContent.ContainsKey(section)) iniContent.Add(section, new Dictionary()); - iniContent[section].Add(settingName, defaultValue); - } - + /// + /// Saves the file + /// public void SaveFile() => SaveFile(FilePath); + /// + /// Saves the file at a certain path + /// + /// The path to save the file to public void SaveFile(string filePath) { File.WriteAllText(filePath, MakeString()); } - public T GetValueOrDefaultTo(string section, string setting, T defaultValue) + /// + /// Reloads the file if any external changes were made + /// + public void ReloadFile() + { + iniContent = new Dictionary>(); + ParseFile(File.ReadAllLines(FilePath)); + } + + /// + /// Safely get a value from the config file. It will automatically be cast to the requested type. If an error occurs or the config entry is missing, it can be created automatically. + /// + /// The type to cast the config entry to. Currently supported: string, bool, int, float, double, decimal + /// The config section to get the value from + /// The config entry to get the value from + /// The value returned if the entry was not found or a cast isn't supported. In the first case, this value can be used to make a new config entry + /// If set to true, the config file will be updated with the new value. Default: true + /// The setting you're looking for, or defaultValue if the value is not found + public T GetValueOrDefaultTo(string section, string setting, T defaultValue, bool setIfDoesntExist = true, bool saveIfDoesntExist = true) { try { @@ -96,6 +124,12 @@ public T GetValueOrDefaultTo(string section, string setting, T defaultValue) case TypeCode.Int32: return (T)(object)int.Parse(value); + case TypeCode.Single: + return (T)(object)float.Parse(value, Culture); + + case TypeCode.Double: + return (T)(object)double.Parse(value, Culture); + case TypeCode.Decimal: return (T)(object)decimal.Parse(value, Culture); @@ -105,13 +139,42 @@ public T GetValueOrDefaultTo(string section, string setting, T defaultValue) } catch { - if (!iniContent.ContainsKey(section)) - iniContent.Add(section, new Dictionary()); - if (!iniContent[section].ContainsKey(setting)) - iniContent[section].Add(setting, defaultValue.ToString()); - SaveFile(); + if (setIfDoesntExist) + SetValue(section, setting, defaultValue, saveIfDoesntExist); return defaultValue; } } + + /// + /// Sets a value in the config + /// + /// The type of the value + /// The section to save the value to + /// The config entry to save the value to + /// The value to save + /// If set to true, immediately save the config file afterwards. + public void SetValue(string section, string setting, T value, bool immediatelySave = true) + { + if (!iniContent.ContainsKey(section)) + iniContent.Add(section, new Dictionary()); + if (!iniContent[section].ContainsKey(setting)) + iniContent[section].Add(setting, value.ToString()); + if (immediatelySave) SaveFile(); + } + + /// + /// Checks if a section exists + /// + /// The section to check + /// True if the section exists, False otherwise + public bool Exists(string section) => iniContent.ContainsKey(section); + + /// + /// Checks if a setting exists + /// + /// The section to check + /// The setting to check + /// True if both the section and the setting exist, False otherwise + public bool Exists(string section, string setting) => iniContent.ContainsKey(section) && iniContent[section].ContainsKey(setting); } } diff --git a/Main.cs b/Main.cs index 225ce8e..cb56811 100644 --- a/Main.cs +++ b/Main.cs @@ -1,5 +1,5 @@ using BepInEx; -using BepInEx.IL2CPP; +using BepInEx.Logging; using HarmonyLib; using MewsToolbox; using System; @@ -10,22 +10,22 @@ namespace QualityOfSpeen { [BepInPlugin(MOD_ID, MOD_NAME, MOD_VERSION)] - public class Main : BasePlugin + public class Main : BaseUnityPlugin { #region Mod Metadata public const string MOD_ID = "QualityOfSpeen"; public const string MOD_NAME = "Quality of Speen"; - public const string MOD_VERSION = "1.0.1"; + public const string MOD_VERSION = "1.0.2"; #endregion #region Mod Variables - public static BepInEx.Logging.ManualLogSource Logger; + private static ManualLogSource logger; #endregion - #region BasePlugin.Load() override - public override void Load() + #region Plugin Awake + void Awake() { - Logger = Log; + logger = Logger; Harmony harmony = new Harmony(MOD_ID); // Sets Global Game Variables to use by other classes @@ -39,34 +39,39 @@ public override void Load() // Autoload all Quality of Speen Features in the Features namespace foreach (Type type in Assembly.GetExecutingAssembly().GetTypes().Where(t=>string.Equals(t.Namespace, "QualityOfSpeen.Features", StringComparison.Ordinal))) // Source: https://stackoverflow.com/questions/949246/how-can-i-get-all-classes-within-a-namespace { - Log.LogInfo($"QoS: Loading feature {type.ToString().Replace(type.Namespace+".", "")}"); + LogInfo($"QoS: Loading feature {type.ToString().Replace(type.Namespace+".", "")}"); try { - MethodInfo method; - if ((method = type.GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Static)) != null) - { - method.Invoke(null, null); - } harmony.PatchAll(type); } catch (Exception e) { - Log.LogError($"Failed to load {type}: {e}"); + LogError($"Failed to load {type}: {e}"); } } } #endregion - #region Mod Config + #region Logger + + public static void Log(LogLevel level, object msg) => logger.Log(level, msg); + public static void LogInfo(object msg) => Log(LogLevel.Info, msg); + public static void LogWarning(object msg) => Log(LogLevel.Warning, msg); + public static void LogError(object msg) => Log(LogLevel.Error, msg); + public static void LogDebug(object msg) => Log(LogLevel.Debug, msg); + public static void LogMessage(object msg) => Log(LogLevel.Message, msg); + + #endregion + #region Mod Config private static IniFile modConfig; public static IniFile ModConfig => modConfig; private static string configFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Speen Mods", "QualityOfSpeenConfig.ini"); - #endregion #region Global Game Variables - public static InGameState InGameState { get; private set; } + private static InGameState inGameState; + public static InGameState InGameState { get => inGameState; private set { LogMessage("Changed ingamestate: " + value); inGameState = value; } } public static bool IsPlayingCustom { get; private set; } public static bool IsInCustomsMenu { get; private set; } public static bool IsRestarting { get; private set; } diff --git a/QualityOfSpeen.csproj b/QualityOfSpeen.csproj index d87a5bc..2cf8d37 100644 --- a/QualityOfSpeen.csproj +++ b/QualityOfSpeen.csproj @@ -30,6 +30,15 @@ prompt 4 + + + + + + + + + ..\Libs\0Harmony.dll @@ -37,41 +46,34 @@ ..\Libs\Assembly-CSharp.dll - - ..\Libs\BepInEx.Core.dll + + ..\Libs\BepInEx.dll - - ..\Libs\BepInEx.IL2CPP.dll + + ..\Libs\BepInEx.Harmony.dll - - ..\Libs\Il2Cppmscorlib.dll + + False + ..\Libs\SSD.PlayerServiceManager.dll - - - ..\Libs\UnhollowerBaseLib.dll + + ..\Libs\UnityEngine.dll ..\Libs\UnityEngine.CoreModule.dll - + + False ..\Libs\UnityEngine.InputLegacyModule.dll - - - - - - - - - - - + + False + ..\Libs\UnityEngine.UI.dll + - - copy "$(TargetPath)" "D:\Steam\steamapps\common\Spin Rhythm\BepInEx\plugins" /Y -start "" "steam://rungameid/1058830" + copy "$(TargetPath)" "D:\SteamLibrary\steamapps\common\Spin Rhythm\BepInEx\plugins" /Y + \ No newline at end of file