diff --git a/MiraAPI.Example/ExampleColors.cs b/MiraAPI.Example/ExampleColors.cs index 6833d3c..d61eb49 100644 --- a/MiraAPI.Example/ExampleColors.cs +++ b/MiraAPI.Example/ExampleColors.cs @@ -6,7 +6,10 @@ namespace MiraAPI.Example; [RegisterCustomColors] public static class ExampleColors { - public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f)); + public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f)) + { + ColorBrightness = CustomColorBrightness.Lighter, + }; public static CustomColor Rose { get; } = new("Rose", new Color(0.98f, 0.26f, 0.62f)); diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs index 9abf375..bc3c910 100644 --- a/MiraAPI.Example/Options/ExampleOptions2.cs +++ b/MiraAPI.Example/Options/ExampleOptions2.cs @@ -10,6 +10,8 @@ public class ExampleOptions2 : AbstractOptionGroup public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue. + public ModdedPlayerOption PlayerOption2 { get; } = new("YOU MUST CHOSE A PLAYER", false); + public ModdedPlayerOption PlayerOption { get; } = new("Eh whatever"); public ModdedToggleOption ToggleOpt1 { get; } = new("Toggle Option 1", false); public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false) diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs index 7bbe87a..89663d1 100644 --- a/MiraAPI/Colors/CustomColor.cs +++ b/MiraAPI/Colors/CustomColor.cs @@ -13,19 +13,24 @@ namespace MiraAPI.Colors; public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor) { /// - /// Gets the main color. + /// Gets or sets the main color. /// - public Color32 MainColor { get; } = mainColor; + public Color32 MainColor { get; set; } = mainColor; /// - /// Gets the shadow color. + /// Gets or sets the shadow color. /// - public Color32 ShadowColor { get; } = shadowColor; + public Color32 ShadowColor { get; set; } = shadowColor; /// - /// Gets the name of the color. + /// Gets or sets the name of the color. /// - public StringNames Name { get; } = name; + public StringNames Name { get; set; } = name; + + /// + /// Gets or sets whether the color is lighter or darker. Can be used to help with colorblind stuff. + /// + public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker; /// public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60)) @@ -42,3 +47,19 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha { } } + +/// +/// Can be used to help with colorblind stuff. +/// +public enum CustomColorBrightness +{ + /// + /// A lighter color. + /// + Lighter, + + /// + /// A darker color. + /// + Darker, +} diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 95173fb..947b6ff 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -62,9 +62,10 @@ public interface IModdedOption /// The ToggleOption template. /// The NumberOption template. /// The StringOption template. + /// The PlayerOption template. /// >The Transform container for the option. /// >The created OptionBehaviour object. - OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container); + OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container); /// /// Gets the value as a float. diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 0adc461..86e2006 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -6,6 +6,7 @@ using System.Reflection; using BepInEx.Configuration; using HarmonyLib; using MiraAPI.GameOptions.Attributes; +using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Networking; using MiraAPI.PluginLoading; using MiraAPI.Utilities; @@ -22,6 +23,7 @@ public static class ModdedOptionsManager private static readonly Dictionary OptionAttributes = []; private static readonly Dictionary TypeToGroup = []; + internal static readonly Dictionary CreatedPlayerOptions = []; internal static readonly Dictionary ModdedOptions = []; internal static readonly List Groups = []; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 18f13c6..0bec22f 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -44,7 +44,7 @@ public class ModdedEnumOption : ModdedOption } /// - public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) { var stringOption = Object.Instantiate(stringOpt, container); @@ -135,7 +135,7 @@ public class ModdedEnumOption : ModdedOption where T : Enum } /// - public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) { var stringOption = Object.Instantiate(stringOpt, container); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index a01ffe8..53378e1 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -93,6 +93,7 @@ public class ModdedNumberOption : ModdedOption ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, + PlayerOption playerOpt, Transform container) { var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 3c98d24..311f294 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -37,6 +37,7 @@ public abstract class ModdedOption : IModdedOption { if (_parentMod != null || value == null) return; _parentMod = value; + var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue); Value = entry.Value; } @@ -177,6 +178,7 @@ public abstract class ModdedOption : IModdedOption ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, + PlayerOption playerOpt, Transform container); /// diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs new file mode 100644 index 0000000..6069a2c --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs @@ -0,0 +1,138 @@ +using System; +using System.Linq; +using Il2CppSystem.Collections.Generic; +using MiraAPI.Networking; +using MiraAPI.Utilities; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.GameOptions.OptionTypes; + +/// +/// An option for selecting an ingame player. It returns an index of the values list, NOT A PLAYER ID. To get a player out of this, index the Values list. +/// +public class ModdedPlayerOption : ModdedOption +{ + /// + /// Gets or sets the included players. + /// + public List Values { get; set; } + + /// + /// Gets or sets a filter for the players included in the option. + /// + public Func? PlayerFilter { get; set; } + + /// + /// Gets or sets a value indicating whether the option includes a "None" option. If None is selected, value will return -1. + /// + public bool AllowNone { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// Whether the option includes a none option. If None is selected, value will return -1. + public ModdedPlayerOption(string title, bool allowNone = true) : base(title, allowNone ? -1 : 0, false) + { + Data = ScriptableObject.CreateInstance(); + var data = (PlayerSelectionGameSetting)Data; + + data.Title = StringName; + data.Type = global::OptionTypes.Player; + + Values = new List(); + AllowNone = allowNone; + Value = DefaultValue; + } + + /// + public override OptionBehaviour CreateOption( + ToggleOption toggleOpt, + NumberOption numberOpt, + StringOption stringOpt, + PlayerOption playerOpt, + Transform container) + { + var playerOption = Object.Instantiate(playerOpt, container); + + var filteredList = GetFilteredPlayers(); + Values = filteredList.ToIl2CppList(); + + playerOption.SetUpFromData(Data, 20); + playerOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + + playerOption.Title = StringName; + playerOption.TitleText.text = TranslationController.Instance.GetString(StringName); + playerOption.Values = Values; + playerOption.Value = Value; + playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value); + + OptionBehaviour = playerOption; + ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this); + + playerOption.SetValueText(); + playerOption.AdjustButtonsActiveState(); + + return playerOption; + } + + /// + /// Returns a player ran through the filter. + /// + /// A list of filtered players. + public System.Collections.Generic.List GetFilteredPlayers() + { + return GameData.Instance.AllPlayers.ToArray() + .Where(x => PlayerFilter?.Invoke(x) ?? true) + .ToList(); + } + + /// + /// Returns a player from the value. If AllowNone is enabled and none is enabled this will return null. + /// + /// A player or null if chosen none. + public NetworkedPlayerInfo? GetPlayerValue() + { + return Value < 0 ? null : Values[Value]; + } + + /// + public override float GetFloatData() + { + return Value; + } + + /// + public override NetData GetNetData() + { + return new NetData(Id, BitConverter.GetBytes(Value)); + } + + /// + public override void HandleNetData(byte[] data) + { + SetValue(BitConverter.ToInt32(data)); + } + + /// + public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) + { + return optionBehaviour.GetInt(); + } + + /// + protected override void OnValueChanged(int newValue) + { + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false); + if (!OptionBehaviour) + { + return; + } + + if (OptionBehaviour is PlayerOption opt) + { + opt.Value = newValue; + } + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 26ecc74..2cdd0e1 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -26,7 +26,7 @@ public class ModdedToggleOption : ModdedOption } /// - public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) { var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container); diff --git a/MiraAPI/Patches/ChatControllerPatch.cs b/MiraAPI/Patches/ChatControllerPatch.cs new file mode 100644 index 0000000..a30307c --- /dev/null +++ b/MiraAPI/Patches/ChatControllerPatch.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using UnityEngine; + +namespace MiraAPI.Patches; + +/// +/// Allows players to paste text into chat. +/// Source: https://github.com/CallOfCreator/NewMod/blob/main/NewMod/Patches/ClipboardPatch.cs +/// +[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] +public static class ChatControllerPatch +{ + public static void Prefix(ChatController __instance) + { + if (!HudManager.Instance.Chat.IsOpenOrOpening) return; + + var ctrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); + + if (!ctrlPressed || !Input.GetKeyDown(KeyCode.V)) return; + + var clipboard = GUIUtility.systemCopyBuffer; + + if (string.IsNullOrWhiteSpace(clipboard)) return; + clipboard = clipboard.Replace("<", string.Empty) + .Replace(">", string.Empty) + .Replace("\r", string.Empty); + + if (!Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift)) + clipboard = clipboard.Replace("\n", string.Empty); + + __instance.freeChatField.textArea.SetText(__instance.freeChatField.textArea.text + clipboard); + } +} diff --git a/MiraAPI/Patches/Hud/ButtonResetPatches.cs b/MiraAPI/Patches/Hud/ButtonResetPatches.cs index 0859bea..29b190d 100644 --- a/MiraAPI/Patches/Hud/ButtonResetPatches.cs +++ b/MiraAPI/Patches/Hud/ButtonResetPatches.cs @@ -1,37 +1,25 @@ using HarmonyLib; using MiraAPI.Hud; +using Reactor.Utilities; namespace MiraAPI.Patches.Hud; -/// -/// Reset button patches. -/// -[HarmonyPatch] +[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] +[HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))] public static class ButtonResetPatches { - /// - /// Resets the cooldown and effect of all custom buttons when the meeting starts. - /// - [HarmonyPostfix] - [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] - public static void MeetingHudStartPostfix() + public static void Postfix() { foreach (var customActionButton in CustomButtonManager.CustomButtons) { - customActionButton.ResetCooldownAndOrEffect(); - } - } - - /// - /// Resets the cooldown and effect of all custom buttons after the exile screen is closed. - /// - [HarmonyPostfix] - [HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))] - public static void ExileControllerReEnableGameplayPostfix() - { - foreach (var customActionButton in CustomButtonManager.CustomButtons) - { - customActionButton.ResetCooldownAndOrEffect(); + try + { + customActionButton.ResetCooldownAndOrEffect(); + } + catch (System.Exception ex) + { + Logger.Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}"); + } } } } diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 27a8026..14c2444 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,5 +1,6 @@ using HarmonyLib; using MiraAPI.Hud; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using UnityEngine; using Object = UnityEngine.Object; @@ -77,7 +78,14 @@ public static class HudManagerPatches continue; } - button.CreateButton(location); + try + { + button.CreateButton(location); + } + catch (System.Exception e) + { + Logger.Error($"Failed to create custom button {button.GetType().Name}: {e}"); + } } gridArrange.Start(); @@ -105,7 +113,14 @@ public static class HudManagerPatches foreach (var button in CustomButtonManager.CustomButtons) { - button.SetActive(isActive, role); + try + { + button.SetActive(isActive, role); + } + catch (System.Exception e) + { + Logger.Error($"Failed to set custom button {button.GetType().Name} active: {e}"); + } } } } diff --git a/MiraAPI/Patches/Menu/HatsTabPatches.cs b/MiraAPI/Patches/Menu/HatsTabPatches.cs index daf87b1..f985027 100644 --- a/MiraAPI/Patches/Menu/HatsTabPatches.cs +++ b/MiraAPI/Patches/Menu/HatsTabPatches.cs @@ -114,7 +114,7 @@ public static class HatsTabPatches yield return null; } - __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f; + __instance.SetScrollerBounds(); yield return new WaitForSeconds(0.01f); } __instance.currentHatIsEquipped = true; diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs index d66c8f3..bb5482a 100644 --- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs +++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs @@ -91,7 +91,7 @@ public static class NameplatesTabPatches hatIndex += 1; } - __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f; + __instance.SetScrollerBounds(); __instance.currentNameplateIsEquipped = true; } diff --git a/MiraAPI/Patches/Menu/SkinsTabPatches.cs b/MiraAPI/Patches/Menu/SkinsTabPatches.cs index b102fb1..2e9ef2e 100644 --- a/MiraAPI/Patches/Menu/SkinsTabPatches.cs +++ b/MiraAPI/Patches/Menu/SkinsTabPatches.cs @@ -94,7 +94,7 @@ public static class SkinsTabPatches hatIndex += 1; } - __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f; + __instance.SetScrollerBounds(); __instance.currentSkinIsEquipped = true; } diff --git a/MiraAPI/Patches/Menu/VisorsTabPatches.cs b/MiraAPI/Patches/Menu/VisorsTabPatches.cs index 81845c6..d88cdab 100644 --- a/MiraAPI/Patches/Menu/VisorsTabPatches.cs +++ b/MiraAPI/Patches/Menu/VisorsTabPatches.cs @@ -90,7 +90,7 @@ public static class VisorsTabPatches hatIndex += 1; } - __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f; + __instance.SetScrollerBounds(); __instance.currentVisorIsEquipped = true; } diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 0d88920..155b50d 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -220,6 +220,7 @@ internal static class GameOptionsMenuPatch menu.checkboxOrigin, menu.numberOptionOrigin, menu.stringOptionOrigin, + menu.playerOptionOrigin, menu.settingsContainer)); foreach (var newOpt in options) diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 7313206..fd9c78b 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -288,7 +288,7 @@ internal static class GameSettingMenuPatches { if (_text is not null && SelectedModIdx == 0) { - _text.text = "Default"; + _text.text = $"(Page 0/{MiraPluginManager.Instance.RegisteredPlugins.Length})\nDefault"; _text.fontSizeMax = 3.2f; SelectedMod = null; } @@ -298,7 +298,7 @@ internal static class GameSettingMenuPatches SelectedMod = MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1]; var name = SelectedMod.MiraPlugin.OptionsTitleText; - _text.text = name[..Math.Min(name.Length, 25)]; + _text.text = $"(Page {SelectedModIdx}/{MiraPluginManager.Instance.RegisteredPlugins.Length})\n" + name[..Math.Min(name.Length, 25)]; } bool replaceWithModifiers = true; diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index c61613c..469a053 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -1,12 +1,161 @@ -using HarmonyLib; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; using Il2CppSystem; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Roles; using MiraAPI.Utilities; +using UnityEngine; +using Object = Il2CppSystem.Object; namespace MiraAPI.Patches.Options; [HarmonyPatch] public static class OptionsPatches { + [HarmonyPrefix] + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseChance))] + public static bool RoleIncreaseChancePrefix(RoleOptionSetting __instance) + { + if (!__instance.Role.IsCustomRole()) + { + return true; + } + + if (__instance.roleChance == 0) + { + __instance.roleMaxCount = 1; + } + + var increment = Input.GetKey(KeyCode.LeftShift) ? 5 : 10; + __instance.roleChance += increment; + + if (__instance.roleChance > 100) + { + __instance.roleChance = 0; + } + + __instance.OnValueChanged.Invoke(__instance); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.DecreaseChance))] + public static bool RoleDecreaseChancePrefix(RoleOptionSetting __instance) + { + if (!__instance.Role.IsCustomRole()) + { + return true; + } + + if (__instance.roleChance == 0) + { + __instance.roleMaxCount = 1; + } + + var increment = Input.GetKey(KeyCode.LeftShift) ? 5 : 10; + __instance.roleChance -= increment; + + if (__instance.roleChance < 0) + { + __instance.roleChance = 100; + } + + __instance.OnValueChanged.Invoke(__instance); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseCount))] + public static bool RoleIncreaseCountPrefix(RoleOptionSetting __instance) + { + if (!__instance.Role.IsCustomRole()) + { + return true; + } + + if (__instance.roleMaxCount == 0) + { + __instance.roleChance = 50; + } + + __instance.roleMaxCount += 1; + if (__instance.roleMaxCount > __instance.role.MaxCount) + { + __instance.roleMaxCount = 0; + } + + __instance.OnValueChanged.Invoke(__instance); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.DecreaseCount))] + public static bool RoleDecreaseCountPrefix(RoleOptionSetting __instance) + { + if (!__instance.Role.IsCustomRole()) + { + return true; + } + + if (__instance.roleMaxCount == 0) + { + __instance.roleChance = 50; + } + + __instance.roleMaxCount -= 1; + if (__instance.roleMaxCount < 0) + { + __instance.roleMaxCount = __instance.role.MaxCount; + } + + __instance.OnValueChanged.Invoke(__instance); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.AdjustButtonsActiveState))] + [HarmonyPatch(typeof(StringOption), nameof(StringOption.AdjustButtonsActiveState))] + [HarmonyPatch(typeof(PlayerOption), nameof(PlayerOption.AdjustButtonsActiveState))] + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.AdjustChanceButtonsActiveState))] + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.AdjustCountButtonsActiveState))] + public static bool AdjustButtonsPrefix(OptionBehaviour __instance) + { + if (__instance.IsCustom()) + { + if (__instance.TryCast() is { } numberOption) + { + numberOption.MinusBtn.SetInteractable(true); + numberOption.PlusBtn.SetInteractable(true); + } + if (__instance.TryCast() is { } stringOption) + { + stringOption.MinusBtn.SetInteractable(true); + stringOption.PlusBtn.SetInteractable(true); + } + if (__instance.TryCast() is { } playerOption) + { + playerOption.MinusBtn.SetInteractable(true); + playerOption.PlusBtn.SetInteractable(true); + } + + return false; + } + + if (__instance.TryCast() is { } roleOptionSetting && roleOptionSetting.Role.IsCustomRole()) + { + roleOptionSetting.CountMinusBtn.SetInteractable(true); + roleOptionSetting.CountPlusBtn.SetInteractable(true); + roleOptionSetting.ChanceMinusBtn.SetInteractable(true); + roleOptionSetting.ChancePlusBtn.SetInteractable(true); + + return false; + } + + return true; + } + [HarmonyPrefix] [HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.Initialize))] public static bool ToggleInit(ToggleOption __instance) @@ -16,8 +165,7 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = - TranslationController.Instance.GetString(__instance.Title, Array.Empty()); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()); return false; } @@ -40,19 +188,12 @@ public static class OptionsPatches [HarmonyPrefix] [HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))] [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))] + [HarmonyPatch(typeof(PlayerOption), nameof(PlayerOption.UpdateValue))] public static bool UpdateValuePrefix(OptionBehaviour __instance) { return !__instance.IsCustom(); } - [HarmonyPrefix] - [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.AdjustButtonsActiveState))] - [HarmonyPatch(typeof(StringOption), nameof(StringOption.AdjustButtonsActiveState))] - public static bool AdjustButtonsPrefix(OptionBehaviour __instance) - { - return !__instance.IsCustom(); - } - [HarmonyPrefix] [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Initialize))] public static bool NumberInit(NumberOption __instance) diff --git a/MiraAPI/Patches/Options/PlayerOptionPatches.cs b/MiraAPI/Patches/Options/PlayerOptionPatches.cs new file mode 100644 index 0000000..2b77bc1 --- /dev/null +++ b/MiraAPI/Patches/Options/PlayerOptionPatches.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using HarmonyLib; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; +using Reactor.Utilities; +using UnityEngine; +using Object = Il2CppSystem.Object; + +namespace MiraAPI.Patches.Options; + +[HarmonyPatch(typeof(PlayerOption))] +public static class PlayerOptionPatches +{ + // Updates the list of players according to the filter. + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerOption.FixedUpdate))] + public static bool PlayerOptionUpdatePatch(PlayerOption __instance) + { + if (!__instance.IsCustom()) + { + return true; + } + + if (!ModdedOptionsManager.CreatedPlayerOptions.TryGetValue(__instance, out var moddedPlrOpt)) return false; + + var filteredPlayers = moddedPlrOpt.GetFilteredPlayers(); + if (filteredPlayers.Count != __instance.Values.Count) + { + filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId)); + + __instance.Values = filteredPlayers.ToIl2CppList(); + moddedPlrOpt.Values = __instance.Values; + __instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value); + __instance.SetValueText(); + } + + __instance.CheckValueChanged(); + + return false; + } + + // Disable going under 0 if you aren't allowed to choose None. + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerOption.UpdatePlayerIndex))] + public static bool UpdatePlayerIndexPatch(PlayerOption __instance, int index) + { + if (!__instance.IsCustom() || ModdedOptionsManager.CreatedPlayerOptions[__instance].AllowNone) + { + return true; + } + + var newIndex = Mathf.Clamp(index, 0, __instance.Values.Count - 1); + __instance.playerIndex = newIndex; + __instance.Value = __instance.Values[newIndex].PlayerId; + return false; + } + + // Replace "Round Robin" with "None" in the Host Options Menu. + [HarmonyPostfix] + [HarmonyPatch(nameof(PlayerOption.SetValueText))] + public static void SetValueTextPatch(PlayerOption __instance) + { + if (__instance.playerIndex < 0) + { + __instance.ValueText.text = TranslationController.Instance.GetString(StringNames.FilterNone); + } + } + + // Innersloth hardcode literally replaces the value with the HNS impostor option OnEnable, so I have to patch that out. Smh. + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerOption.OnEnable))] + public static bool OnEnablePatch(PlayerOption __instance) + { + return !__instance.IsCustom(); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerOption.GetInt))] + public static bool GetIntPatch(PlayerOption __instance, ref int __result) + { + if (!__instance.IsCustom()) + { + return true; + } + + __result = __instance.Value; + return false; + } + + // Used to replace "Round Robin" with "None" for option notification & view pane. + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerSelectionGameSetting), nameof(PlayerSelectionGameSetting.GetValueString))] + public static bool GetStringPatch(PlayerSelectionGameSetting __instance, float value, ref string __result) + { + var option = ModdedOptionsManager.ModdedOptions.FirstOrDefault(x => x.Value.Data == __instance).Value; + if (option == null) return true; + + var networkedPlayerInfo = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(p => p.PlayerId == (int)value); + if (networkedPlayerInfo != null) + { + __result = networkedPlayerInfo.PlayerName; + return false; + } + + __result = TranslationController.Instance.GetString(StringNames.FilterNone); + return false; + } + + // "UpdateValue" was inlined and I needed to replace it, so I just rewrote the method where it's called from. + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerOption.CheckValueChanged))] + public static bool UpdateValuePatch(PlayerOption __instance) + { + if (!__instance.IsCustom()) + { + return true; + } + + if (__instance.oldValue == __instance.Value) return false; + + __instance.oldValue = __instance.Value; + __instance.SetValueText(); + + return false; + } +} diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 4dc7317..80f96c9 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -30,6 +31,9 @@ public static class RoleSettingMenuPatches private static float ScrollerNum { get; set; } = 0.522f; + private static RoleBehaviour? CurrentRole { get; set; } + private static List? CurrentRoleOptions { get; set; } + [HarmonyPostfix] [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] public static void SetQuotaTabPostfix(RolesSettingsMenu __instance) @@ -45,6 +49,8 @@ public static class RoleSettingMenuPatches RoleOptionSettings.ForEach(Object.Destroy); Headers.Clear(); RoleOptionSettings.Clear(); + CurrentRole = null; + CurrentRoleOptions = null; __instance.roleChances = new Il2CppSystem.Collections.Generic.List(); __instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List(); @@ -211,9 +217,10 @@ public static class RoleSettingMenuPatches headerBtn.OnClick.AddListener( (UnityAction)(() => { - if (RoleGroupHidden.TryGetValue(group, out var value)) + RolePositions[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition; + if (RoleGroupHidden.TryGetValue(group, out var groupHidden)) { - RoleGroupHidden[group] = !value; + RoleGroupHidden[group] = !groupHidden; } foreach (var header in Headers) { @@ -261,9 +268,31 @@ public static class RoleSettingMenuPatches [HarmonyPrefix] [HarmonyPatch(nameof(RolesSettingsMenu.Update))] - public static bool UpdatePatch() + public static bool UpdatePatch(RolesSettingsMenu __instance) { - return GameSettingMenuPatches.SelectedModIdx == 0; + if (GameSettingMenuPatches.SelectedModIdx == 0) return true; + if (CurrentRole == null || CurrentRoleOptions == null) return false; + + var hasImage = CurrentRole.RoleScreenshot != null; + var num = hasImage ? -0.872f : -1; + + foreach (var opt in CurrentRoleOptions) + { + if (opt.OptionBehaviour == null) continue; + + if (!opt.Visible.Invoke()) + { + opt.OptionBehaviour.gameObject.SetActive(false); + continue; + } + + opt.OptionBehaviour.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f); + opt.OptionBehaviour.gameObject.SetActive(true); + num += -0.45f; + } + + __instance.scrollBar.SetYBoundsMax(-num - 3); + return false; } [HarmonyPostfix] @@ -275,6 +304,8 @@ public static class RoleSettingMenuPatches return; } + CurrentRole = null; + CurrentRoleOptions = null; __instance.scrollBar.SetScrollBounds(__instance); } @@ -327,26 +358,26 @@ public static class RoleSettingMenuPatches optBehaviour.gameObject.DestroyImmediate(); } + CurrentRole = role; __instance.advancedSettingChildren.Clear(); - var hasImage = role.RoleScreenshot != null; - var num = hasImage ? -0.872f : -1; - // TODO: create sub groups under the role settings. var filteredOptions = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups - .Where(x=> x.OptionableType == role.GetType()) - .SelectMany(x=>x.Options) + .Where(x => x.GroupVisible() && x.OptionableType == role.GetType()) + .SelectMany(x => x.Options) .ToList() ?? []; + CurrentRoleOptions = filteredOptions; + foreach (var option in filteredOptions) { var newOpt = option.CreateOption( __instance.checkboxOrigin, __instance.numberOptionOrigin, __instance.stringOptionOrigin, + GameSettingMenu.Instance.GameSettingsTab.playerOptionOrigin, __instance.AdvancedRolesSettings.transform); - newOpt.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f); newOpt.SetClickMask(__instance.ButtonClickMask); SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); @@ -364,11 +395,10 @@ public static class RoleSettingMenuPatches newOpt.LabelBackground.enabled = false; __instance.advancedSettingChildren.Add(newOpt); - num += -0.45f; + newOpt.gameObject.SetActive(false); newOpt.Initialize(); } - __instance.scrollBar.SetYBoundsMax(-num - 3); __instance.scrollBar.ScrollToTop(); } @@ -492,6 +522,10 @@ public static class RoleSettingMenuPatches roleOptionSetting.labelSprite.color = customRole.OptionsMenuColor; roleOptionSetting.OnValueChanged = new Action(ValueChanged); roleOptionSetting.SetClickMask(__instance.ButtonClickMask); + roleOptionSetting.ChanceMinusBtn.SetInteractable(true); + roleOptionSetting.ChancePlusBtn.SetInteractable(true); + roleOptionSetting.CountMinusBtn.SetInteractable(true); + roleOptionSetting.CountPlusBtn.SetInteractable(true); __instance.roleChances.Add(roleOptionSetting); roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.5376f, -0.2923f, 0f); @@ -500,7 +534,7 @@ public static class RoleSettingMenuPatches if (GameSettingMenuPatches.SelectedMod is null || GameSettingMenuPatches.SelectedMod.InternalOptionGroups - .Exists(x => x.OptionableType == role.GetType())) + .Exists(x => x.GroupVisible() && x.OptionableType == role.GetType() && x.Options.Any(y => y.Visible()))) { var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform); newButton.name = "ConfigButton"; diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index c93aa6d..a0b88cd 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -7,6 +7,7 @@ using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Utilities; using MiraAPI.Voting; +using Reactor.Utilities; using Reactor.Utilities.Extensions; namespace MiraAPI.Patches; @@ -94,12 +95,19 @@ internal static class PlayerControlPatches continue; } - if (!button.Enabled(__instance.Data?.Role)) + try { - continue; - } + if (!button.Enabled(__instance.Data?.Role)) + { + continue; + } - button.FixedUpdateHandler(__instance); + button.FixedUpdateHandler(__instance); + } + catch (System.Exception e) + { + Logger.Error($"Failed to update custom button {button.GetType().Name}: {e}"); + } } } } diff --git a/MiraAPI/Patches/ServerDropdownPatch.cs b/MiraAPI/Patches/ServerDropdownPatch.cs new file mode 100644 index 0000000..0fb96e3 --- /dev/null +++ b/MiraAPI/Patches/ServerDropdownPatch.cs @@ -0,0 +1,52 @@ +using HarmonyLib; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.Patches; + +[HarmonyPatch(typeof(ServerDropdown), nameof(ServerDropdown.FillServerOptions))] +public static class ServerDropdownPatch +{ + public static bool Prefix(ServerDropdown __instance) + { + var num = 0; + __instance.background.size = new Vector2(8.4f, 4.8f); + + foreach (var regionInfo in DestroyableSingleton.Instance.AvailableRegions) + { + if (DestroyableSingleton.Instance.CurrentRegion.Equals(regionInfo)) + { + __instance.defaultButtonSelected = __instance.firstOption; + __instance.firstOption.ChangeButtonText( + DestroyableSingleton.Instance.GetStringWithDefault( + regionInfo.TranslateName, + regionInfo.Name)); + } + else + { + var region = regionInfo; + var serverListButton = __instance.ButtonPool.Get(); + var x = num % 2 == 0 ? -2 : 2; + var y = -0.55f * (num / 2); + serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f); + serverListButton.transform.localScale = Vector3.one; + serverListButton.Text.text = + DestroyableSingleton.Instance.GetStringWithDefault( + regionInfo.TranslateName, + regionInfo.Name); + serverListButton.Text.ForceMeshUpdate(); + serverListButton.Button.OnClick.RemoveAllListeners(); + serverListButton.Button.OnClick.AddListener((UnityAction)(() => { __instance.ChooseOption(region); })); + __instance.controllerSelectable.Add(serverListButton.Button); + __instance.background.transform.localPosition = new Vector3( + 0f, + __instance.initialYPos + (-0.3f * (num / 2)), + 0f); + __instance.background.size = new Vector2(__instance.background.size.x, 1.2f + (0.6f * (num / 2))); + num++; + } + } + + return false; + } +} diff --git a/MiraAPI/Presets/SavePresetPopup.cs b/MiraAPI/Presets/SavePresetPopup.cs index 9449857..87959e7 100644 --- a/MiraAPI/Presets/SavePresetPopup.cs +++ b/MiraAPI/Presets/SavePresetPopup.cs @@ -96,6 +96,3 @@ public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr) popup.Begin(null); } } - - - diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index 7f0ec29..01b402d 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using AmongUs.GameOptions; using MiraAPI.Utilities.Assets; +using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Roles; @@ -56,7 +57,21 @@ public static class CustomRoleUtils public static LoadableAsset? GetIntroSound(RoleTypes roleType) { var role = RoleManager.Instance.AllRoles.FirstOrDefault(role => role.Role == roleType); - if (role is ICustomRole customRole) return customRole.Configuration.IntroSound; + if (role is ICustomRole customRole) + { + return customRole.Configuration.IntroSound; + } + return new PreloadedAsset(role!.IntroSound); } + + /// + /// Determines if a role is a custom role or not. + /// + /// The RoleBehaviour to check. + /// True if the role is a custom role, false otherwise. + public static bool IsCustomRole(this RoleBehaviour role) + { + return CustomRoleManager.CustomRoles.ContainsKey((ushort)role.Role); + } } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 64e02f9..57887d8 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -154,6 +154,24 @@ public static class Extensions return TutorialManager.InstanceExists || AmongUsClient.Instance.HostId == playerControl.OwnerId; } + /// + /// Used to convert a System.Collections.Generic.List to Il2cppSystem. + /// + /// The list. + /// The type in the list. + /// The converted list. + public static Il2CppSystem.Collections.Generic.List ToIl2CppList(this List systemList) + { + var il2cppList = new Il2CppSystem.Collections.Generic.List(); + + foreach (var item in systemList) + { + il2cppList.Add(item); + } + + return il2cppList; + } + /// /// Determines if a float is an integer. /// diff --git a/README.md b/README.md index d4206a3..b61a3dc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A thorough, but simple, Among Us modding API and utility library that covers: Mira API strives to be comprehensive, yet straightforward, while also using as many base game elements as possible. The result is a less intrusive, better modding API that covers general use cases. -**Join the [Discord](https://discord.gg/all-of-us-launchpad-794950428756410429) for support and to stay updated on the latest releases** +**Join the [Discord](https://discord.gg/FYYqJU2bvp) for support and to stay updated on the latest releases** # Usage