diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 63b7c41..ca8a570 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -1,4 +1,6 @@ -using MiraAPI.Modifiers.Types; +using MiraAPI.Example.Options.Modifiers; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; @@ -6,13 +8,13 @@ public class CaptainModifier : GameModifier { public override string ModifierName => "Captain"; - public override int GetAmountPerGame() - { - return 1; - } - public override int GetAssignmentChance() { - return 100; + return (int)OptionGroupSingleton.Instance.Chance; + } + + public override int GetAmountPerGame() + { + return (int)OptionGroupSingleton.Instance.Amount; } } diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index c640ed8..9f13b18 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -5,6 +5,7 @@ namespace MiraAPI.Example.Modifiers; public class HighPriorityModifier : GameModifier { public override string ModifierName => "High Priority"; + public override int GetAssignmentChance() { return 100; diff --git a/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs new file mode 100644 index 0000000..0246a1d --- /dev/null +++ b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs @@ -0,0 +1,19 @@ +using MiraAPI.Example.Modifiers; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Options.Modifiers; + +public class CaptainModifierSettings : AbstractOptionGroup +{ + public override string GroupName => "Captain Settings"; + + [ModdedNumberOption("Amount", 0, 5)] + public float Amount { get; set; } = 1; + + [ModdedNumberOption("Chance", 0, 100, 10)] + public float Chance { get; set; } = 50; + + [ModdedNumberOption("Uses", 1, 5)] + public float NumUses { get; set; } = 3; +} diff --git a/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs new file mode 100644 index 0000000..13cdef9 --- /dev/null +++ b/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs @@ -0,0 +1,13 @@ +using MiraAPI.Example.Modifiers.Freezer; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Options.Modifiers; + +public class FreezeModifierSettings : AbstractOptionGroup +{ + public override string GroupName => "Freeze"; + + [ModdedToggleOption("Use Color")] + public bool UseColor { get; set; } = true; +} diff --git a/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs new file mode 100644 index 0000000..1414799 --- /dev/null +++ b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs @@ -0,0 +1,19 @@ +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Options.Modifiers; + +public class GeneralModifierOptions : AbstractOptionGroup +{ + public override string GroupName => "General"; + + public override uint GroupPriority => 0; + + public override bool ShowInModifiersMenu => true; + + [ModdedToggleOption("Some Boolean Option")] + public bool SomeBooleanOption { get; set; } = true; + + [ModdedNumberOption("Number Option", 0, 10, 1)] + public float NumberOption { get; set; } = 5; +} diff --git a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs index a99c9e0..1473068 100644 --- a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs +++ b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs @@ -1,17 +1,14 @@ -using System; -using MiraAPI.Example.Roles; +using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Utilities; namespace MiraAPI.Example.Options.Roles; -public class FreezerRoleSettings : AbstractOptionGroup +public class FreezerRoleSettings : AbstractOptionGroup { public override string GroupName => "Custom Role"; - public override Type AdvancedRole => typeof(FreezerRole); - [ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)] public float FreezeDuration { get; set; } = 5; diff --git a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs index 34ccb94..b6d02a4 100644 --- a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs +++ b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs @@ -1,5 +1,4 @@ -using System; -using MiraAPI.Example.Roles; +using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.OptionTypes; @@ -7,12 +6,10 @@ using MiraAPI.Utilities; namespace MiraAPI.Example.Options.Roles; -public class TeleporterOptions : AbstractOptionGroup +public class TeleporterOptions : AbstractOptionGroup { public override string GroupName => "Teleporter"; - public override Type AdvancedRole => typeof(TeleporterRole); - public ModdedNumberOption TeleportCooldown { get; set; } = new("Teleport Cooldown", 10, 5, 60, 2.5f, MiraNumberSuffixes.Seconds); [ModdedNumberOption("Teleport Duration", 5, 25, 1, MiraNumberSuffixes.Seconds)] diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 418f25b..45f9bd4 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -16,6 +16,17 @@ public abstract class AbstractOptionGroup /// public abstract string GroupName { get; } + /// + /// Gets the Optionable type of the group. + /// + public virtual Type? OptionableType => null; + + /// + /// Gets a value indicating whether the group should be shown in the modifiers menu. + /// + // TODO: make this not a boolean + public virtual bool ShowInModifiersMenu => false; + /// /// Gets the function that determines whether the group should be visible or not. /// @@ -32,12 +43,17 @@ public abstract class AbstractOptionGroup /// public virtual uint GroupPriority => uint.MaxValue; - /// - /// Gets the role the group is associated with. This is used for the advanced role options menu. - /// - public virtual Type? AdvancedRole => null; - internal bool AllOptionsHidden { get; set; } internal CategoryHeaderMasked? Header { get; set; } } + +/// +/// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. +/// +/// The type of the optionable that this group contains. +public abstract class AbstractOptionGroup : AbstractOptionGroup where T : IOptionable +{ + /// + public override Type OptionableType => typeof(T); +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index da6094b..54680f2 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -8,12 +8,12 @@ namespace MiraAPI.GameOptions.Attributes; /// Attribute for creating an enum option. /// [AttributeUsage(AttributeTargets.Property)] -public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null, Type? roleType = null) - : ModdedOptionAttribute(title, roleType) +public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null) + : ModdedOptionAttribute(title) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values, RoleType); + var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values); return opt; } diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index d679051..07b5cfc 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -16,13 +16,12 @@ public class ModdedNumberOptionAttribute( float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, string? formatString = null, - bool zeroInfinity = false, - Type? roleType = null) - : ModdedOptionAttribute(title, roleType) + bool zeroInfinity = false) + : ModdedOptionAttribute(title) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity, RoleType); + return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity); } /// diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs index d44a7c0..0ad64da 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs @@ -8,13 +8,12 @@ namespace MiraAPI.GameOptions.Attributes; /// Attribute for a toggle option. /// /// The option title. -/// An optional role type. [AttributeUsage(AttributeTargets.Property)] -public class ModdedToggleOptionAttribute(string title, Type? roleType = null) : ModdedOptionAttribute(title, roleType) +public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(title) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), RoleType); + var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false)); return toggleOpt; } diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 1aee3c4..f40f3cf 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -13,7 +13,6 @@ public interface IModdedOption StringNames StringName { get; } IMiraPlugin? ParentMod { get; set; } BaseGameSetting Data { get; } - Type? AdvancedRole { get; set; } OptionBehaviour? OptionBehaviour { get; } Func Visible { get; set; } ConfigDefinition? ConfigDefinition { get; set; } diff --git a/MiraAPI/GameOptions/IOptionable.cs b/MiraAPI/GameOptions/IOptionable.cs new file mode 100644 index 0000000..adc28f6 --- /dev/null +++ b/MiraAPI/GameOptions/IOptionable.cs @@ -0,0 +1,6 @@ +namespace MiraAPI.GameOptions; + +/// +/// Interface for classes that can be used for option groups. +/// +public interface IOptionable; diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index a6dd1c9..7aaa154 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -108,7 +108,7 @@ public static class ModdedOptionsManager RegisterOption(option, group, property.Name, pluginInfo); } - private static void RegisterOption( + internal static void RegisterOption( IModdedOption option, AbstractOptionGroup group, string propertyName, @@ -119,10 +119,7 @@ public static class ModdedOptionsManager option.ConfigDefinition = new ConfigDefinition(groupName, propertyName); option.ParentMod = pluginInfo.MiraPlugin; - option.AdvancedRole = group.AdvancedRole; - pluginInfo.Options.Add(option); - ModdedOptions.Add(option.Id, option); group.Options.Add(option); } diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs new file mode 100644 index 0000000..cfa2eff --- /dev/null +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace MiraAPI.GameOptions; + +internal class ModifierOptionGroup : AbstractOptionGroup +{ + public override string GroupName { get; } + + public ModifierOptionGroup(string name, IModdedOption[] options, params AbstractOptionGroup[] groups) + { + GroupName = name; + Options.AddRange(options); + Options.AddRange(groups.SelectMany(x=>x.Options)); + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index f25fda6..ff422f5 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -27,8 +27,7 @@ public class ModdedEnumOption : ModdedOption /// The default value as an int. /// The Enum type. /// An option list of string values to use in place of the enum name. - /// An optional role type to specify for a specific role. - public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType) + public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null) : base(title, defaultValue) { Values = values ?? Enum.GetNames(enumType); Data = ScriptableObject.CreateInstance(); @@ -101,7 +100,6 @@ public class ModdedEnumOption : ModdedOption } } - /// /// An option for selecting an enum value. /// @@ -118,10 +116,8 @@ public class ModdedEnumOption : ModdedOption where T : Enum /// /// The title of the option. /// The default value as an int. - /// The Enum type. /// An option list of string values to use in place of the enum name. - /// An optional role type to specify for a specific role. - public ModdedEnumOption(string title, T defaultValue, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType) + public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue) { Values = values ?? Enum.GetNames(typeof(T)); Data = ScriptableObject.CreateInstance(); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index f075662..4d38c04 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -48,7 +48,6 @@ public class ModdedNumberOption : ModdedOption /// The suffix type. /// Optional format string for the option screen. /// Whether zero is infinity or not. - /// An optional role type. public ModdedNumberOption( string title, float defaultValue, @@ -57,8 +56,7 @@ public class ModdedNumberOption : ModdedOption float increment, MiraNumberSuffixes suffixType, string? formatString = null, - bool zeroInfinity = false, - Type? roleType = null) : base(title, defaultValue, roleType) + bool zeroInfinity = false) : base(title, defaultValue) { Min = min; Max = max; @@ -77,11 +75,11 @@ public class ModdedNumberOption : ModdedOption data.Increment = Increment; data.ValidRange = new FloatRange(Min, Max); data.FormatString = formatString ?? - (!Mathf.Approximately(defaultValue, 0) || - !Mathf.Approximately(Increment % 1, 0) || - !Mathf.Approximately(Value % 1, 0) || - !Mathf.Approximately(Min % 1, 0) || - !Mathf.Approximately(Max % 1, 0) ? "0.0" : "0"); + (defaultValue.IsInteger() && + Increment.IsInteger() && + Value.IsInteger() && + Min.IsInteger() && + Max.IsInteger() ? "0" : "0.0"); data.ZeroIsInfinity = ZeroInfinity; data.SuffixType = (NumberSuffixes)SuffixType; @@ -144,7 +142,7 @@ public class ModdedNumberOption : ModdedOption Value = Mathf.Clamp(newValue, Min, Max); HudManager.Instance.Notifier.AddSettingsChangeMessage( StringName, - Data?.GetValueString(Value), + Data.GetValueString(Value), false); if (OptionBehaviour is NumberOption opt) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 9d42be0..670fb3c 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -2,7 +2,6 @@ using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; -using MiraAPI.Roles; using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; using UnityEngine; @@ -72,11 +71,6 @@ public abstract class ModdedOption : IModdedOption /// public Func Visible { get; set; } - /// - /// Gets or sets the advanced role of the option. - /// - public Type? AdvancedRole { get; set; } - /// /// Gets or sets the option behaviour of the option. /// @@ -102,8 +96,7 @@ public abstract class ModdedOption : IModdedOption /// /// The option title. /// The default value. - /// The Role Type or null if it doesn't belong to a role. - protected ModdedOption(string title, T defaultValue, Type? roleType) + protected ModdedOption(string title, T defaultValue) { Id = ModdedOptionsManager.NextId; Title = title; @@ -111,11 +104,6 @@ public abstract class ModdedOption : IModdedOption Value = defaultValue; StringName = CustomStringName.CreateAndRegister(Title); Visible = () => true; - - if (roleType is not null && roleType.IsAssignableTo(typeof(ICustomRole))) - { - AdvancedRole = roleType; - } } internal void ValueChanged(OptionBehaviour optionBehaviour) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 1773512..3495e86 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -15,8 +15,7 @@ public class ModdedToggleOption : ModdedOption /// /// The option title. /// The default value. - /// An optional role type. - public ModdedToggleOption(string title, bool defaultValue, Type? roleType = null) : base(title, defaultValue, roleType) + public ModdedToggleOption(string title, bool defaultValue) : base(title, defaultValue) { Data = ScriptableObject.CreateInstance(); diff --git a/MiraAPI/Modifiers/AssignmentConfiguration.cs b/MiraAPI/Modifiers/AssignmentConfiguration.cs new file mode 100644 index 0000000..fd4f3ba --- /dev/null +++ b/MiraAPI/Modifiers/AssignmentConfiguration.cs @@ -0,0 +1,10 @@ +namespace MiraAPI.Modifiers; + +/// +/// Represents the assignment configuration for a game modifier. +/// +/// The default amount of players that can have this modifier in a game. +/// The default chance of the modifier being assigned to a player. +/// Whether Mira should create an amount option for the modifier. +/// Whether Mira should create a chance option for the modifier. +public record struct AssignmentConfiguration(int DefaultAmount, int DefaultChance, bool CreateAmountOption, bool CreateChanceOption); diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 473c78f..0dd2149 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,11 +1,13 @@ using System; +using MiraAPI.GameOptions; +using MiraAPI.PluginLoading; namespace MiraAPI.Modifiers; /// /// Base class for all modifiers. /// -public abstract class BaseModifier +public abstract class BaseModifier : IOptionable { /// /// Gets the player that the modifier is attached to. @@ -32,6 +34,14 @@ public abstract class BaseModifier /// public uint TypeId => ModifierManager.GetModifierTypeId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered."); + /// + /// Gets the parent mod of the modifier. + /// + public MiraPluginInfo ParentMod => Array.Find( + MiraPluginManager.Instance.RegisteredPlugins(), + x => x.Modifiers.Exists(y => y.TypeId == TypeId) + ) ?? throw new InvalidOperationException("Modifier is not registered."); + /// /// Gets the modifier name. /// diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 4285a73..5f6bbff 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -63,16 +63,32 @@ public static class ModifierManager IdToTypeModifierMap.Add(GetNextTypeId(), modifierType); TypeToIdModifierMap.Add(modifierType, _nextTypeId); - var bm = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); // this probably isn't a great idea - info.Modifiers.Add(bm); - if (!typeof(GameModifier).IsAssignableFrom(modifierType)) + BaseModifier modifier; + if (modifierType.GetConstructor(Type.EmptyTypes) != null) + { + modifier = (BaseModifier)Activator.CreateInstance(modifierType)!; + } + else + { + // this is probably terrible but its good enough for now. + modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); + } + + info.Modifiers.Add(modifier); + + if (modifier is not GameModifier gameModifier) { return true; } - var mod = Activator.CreateInstance(modifierType) as GameModifier; - var priority = mod!.Priority(); + if (modifierType.GetConstructor(Type.EmptyTypes) == null) + { + Logger.Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!"); + return false; + } + + var priority = gameModifier.Priority(); if (!PrioritiesToIdsMap.TryGetValue(priority, out var list)) { diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index c641e88..aca0f2e 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,5 +1,8 @@ -using System.Linq; +using System; +using System.Linq; using HarmonyLib; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; @@ -7,6 +10,7 @@ using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; @@ -30,64 +34,98 @@ public static class GameOptionsMenuPatch } var num = 2.1f; - var filteredGroups = - GameSettingMenuPatches.SelectedMod?.OptionGroups.Where( - x => x.AdvancedRole is null) ?? []; - - foreach (var group in filteredGroups) + if (__instance.name == "MODIFIERS TAB") { - if (group.Options.Count == 0 || group.Header is null) + ModifiersUpdate(ref num); + } + else + { + var filteredGroups = + GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.OptionableType == null) ?? []; + + foreach (var group in filteredGroups) { - continue; - } - - if (!group.GroupVisible.Invoke()) - { - group.Header.gameObject.SetActive(false); - foreach (var option in group.Options) - { - option.OptionBehaviour?.gameObject.SetActive(false); - } - continue; - } - - group.Header.gameObject.SetActive(true); - group.Header.transform.localScale = Vector3.one * 0.63f; - group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); - - num -= 0.58f; - - foreach (var opt in group.Options) - { - var newOpt = opt.OptionBehaviour; - - if (newOpt is null) - { - continue; - } - - if (!opt.Visible.Invoke()) - { - newOpt.gameObject.SetActive(false); - continue; - } - - if (!group.AllOptionsHidden) - { - newOpt.gameObject.SetActive(true); - newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); - num -= 0.45f; - } - else - { - newOpt.gameObject.SetActive(false); - } + UpdateGroup(group, ref num); } } __instance.scrollBar.SetYBoundsMax(-num - 1.65f); } + private static void UpdateGroup(AbstractOptionGroup group, ref float num) + { + if (group.Options.Count == 0 || group.Header is null) + { + return; + } + + if (!group.GroupVisible.Invoke()) + { + group.Header.gameObject.SetActive(false); + foreach (var option in group.Options) + { + option.OptionBehaviour?.gameObject.SetActive(false); + } + + return; + } + + group.Header.gameObject.SetActive(true); + group.Header.transform.localScale = Vector3.one * 0.63f; + group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + + num -= 0.58f; + + foreach (var opt in group.Options) + { + var newOpt = opt.OptionBehaviour; + + if (newOpt is null) + { + continue; + } + + if (!opt.Visible.Invoke()) + { + newOpt.gameObject.SetActive(false); + continue; + } + + if (!group.AllOptionsHidden) + { + newOpt.gameObject.SetActive(true); + newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); + num -= 0.45f; + } + else + { + newOpt.gameObject.SetActive(false); + } + } + } + + private static void ModifiersUpdate(ref float num) + { + var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; + + foreach (var modGroup in groups) + { + UpdateGroup(modGroup, ref num); + } + } + + private static void ModifiersCreate(GameOptionsMenu menu) + { + var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; + foreach (var group in groups) + { + CreateGroup(menu, group); + } + } + [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))] public static bool SettingsPatch(GameOptionsMenu __instance) @@ -99,106 +137,18 @@ public static class GameOptionsMenuPatch __instance.MapPicker.gameObject.SetActive(false); - var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(x => x.AdvancedRole is null) ?? []; + if (__instance.name == "MODIFIERS TAB") + { + ModifiersCreate(__instance); + return false; + } + + var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.OptionableType == null) ?? []; foreach (var group in filteredGroups) { - var categoryHeaderMasked = Object.Instantiate( - __instance.categoryHeaderOrigin, - Vector3.zero, - Quaternion.identity, - __instance.settingsContainer); - categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); - if (group.GroupColor != Color.clear) - { - categoryHeaderMasked.Background.color = group.GroupColor; - categoryHeaderMasked.Divider.color = group.GroupColor; - categoryHeaderMasked.Title.color = group.GroupColor.GetAlternateColor(); - } - - categoryHeaderMasked.Background.size = new Vector2( - categoryHeaderMasked.Background.size.x + 1.5f, - categoryHeaderMasked.Background.size.y); - categoryHeaderMasked.gameObject.SetActive(false); - group.Header = categoryHeaderMasked; - - var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); - newText.text = "(Click to close)"; - newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); - newText.gameObject.GetComponent().Destroy(); - - var options = group.Options.Select( - opt => opt.CreateOption( - __instance.checkboxOrigin, - __instance.numberOptionOrigin, - __instance.stringOptionOrigin, - __instance.settingsContainer)); - - foreach (var newOpt in options) - { - newOpt.SetClickMask(__instance.ButtonClickMask); - - SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); - foreach (var renderer in componentsInChildren) - { - if (group.GroupColor != Color.clear) - { - renderer.color = group.GroupColor.GetAlternateColor(); - if (renderer.transform.parent.TryGetComponent(out var btn)) - { - btn.interactableColor = group.GroupColor.GetAlternateColor(); - btn.interactableHoveredColor = Color.white; - } - } - - renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); - } - - foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) - { - if (group.GroupColor != Color.clear) - { - textMeshPro.color = group.GroupColor; - } - - textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); - textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); - } - - if (newOpt is ToggleOption toggle) - { - toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); - toggle.CheckMark.color = - group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; - var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") - .GetComponent(); - rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); - rend.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; - } - - __instance.Children.Add(newOpt); - - newOpt.Initialize(); - newOpt.gameObject.SetActive(false); - } - - var boxCol = categoryHeaderMasked.gameObject.AddComponent(); - boxCol.size = new Vector2(7, 0.7f); - boxCol.offset = new Vector2(1.5f, -0.3f); - - var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); - headerBtn.ClickSound = __instance.BackButton.GetComponent().ClickSound; - headerBtn.OnMouseOver = new UnityEvent(); - headerBtn.OnMouseOut = new UnityEvent(); - headerBtn.OnClick.AddListener( - (UnityAction)(() => - { - group.AllOptionsHidden = !group.AllOptionsHidden; - newText.text = group.AllOptionsHidden - ? "(Click to open)" - : "(Click to close)"; - })); - headerBtn.SetButtonEnableState(true); + CreateGroup(__instance, group); } return false; @@ -233,9 +183,110 @@ public static class GameOptionsMenuPatch continue; } - optionBehaviour.OnValueChanged = new System.Action(__instance.ValueChanged); + optionBehaviour.OnValueChanged = new Action(__instance.ValueChanged); } return false; } + + private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group) + { + var categoryHeaderMasked = Object.Instantiate( + menu.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + menu.settingsContainer); + + categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); + if (group.GroupColor != Color.clear) + { + categoryHeaderMasked.Background.color = group.GroupColor; + categoryHeaderMasked.Divider.color = group.GroupColor; + categoryHeaderMasked.Title.color = group.GroupColor.GetAlternateColor(); + } + + categoryHeaderMasked.Background.size = new Vector2( + categoryHeaderMasked.Background.size.x + 1.5f, + categoryHeaderMasked.Background.size.y); + categoryHeaderMasked.gameObject.SetActive(false); + group.Header = categoryHeaderMasked; + + var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); + newText.text = "(Click to close)"; + newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); + newText.gameObject.GetComponent().Destroy(); + + var options = group.Options.Select( + opt => opt.CreateOption( + menu.checkboxOrigin, + menu.numberOptionOrigin, + menu.stringOptionOrigin, + menu.settingsContainer)); + + foreach (var newOpt in options) + { + newOpt.SetClickMask(menu.ButtonClickMask); + + SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); + foreach (var renderer in componentsInChildren) + { + if (group.GroupColor != Color.clear) + { + renderer.color = group.GroupColor.GetAlternateColor(); + if (renderer.transform.parent.TryGetComponent(out var btn)) + { + btn.interactableColor = group.GroupColor.GetAlternateColor(); + btn.interactableHoveredColor = Color.white; + } + } + + renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); + } + + foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) + { + if (group.GroupColor != Color.clear) + { + textMeshPro.color = group.GroupColor; + } + + textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); + } + + if (newOpt is ToggleOption toggle) + { + toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); + toggle.CheckMark.color = + group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; + var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") + .GetComponent(); + rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); + rend.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; + } + + menu.Children.Add(newOpt); + + newOpt.Initialize(); + newOpt.gameObject.SetActive(false); + } + + var boxCol = categoryHeaderMasked.gameObject.AddComponent(); + boxCol.size = new Vector2(7, 0.7f); + boxCol.offset = new Vector2(1.5f, -0.3f); + + var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); + headerBtn.ClickSound = menu.BackButton.GetComponent().ClickSound; + headerBtn.OnMouseOver = new UnityEvent(); + headerBtn.OnMouseOut = new UnityEvent(); + headerBtn.OnClick.AddListener( + (UnityAction)(() => + { + group.AllOptionsHidden = !group.AllOptionsHidden; + newText.text = group.AllOptionsHidden + ? "(Click to open)" + : "(Click to close)"; + })); + headerBtn.SetButtonEnableState(true); + } } diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 3c65092..7781bf8 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using HarmonyLib; +using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; @@ -12,26 +12,41 @@ using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; -/// -/// Patches for the to add support for custom options. -/// [HarmonyPatch(typeof(GameSettingMenu))] -public static class GameSettingMenuPatches +internal static class GameSettingMenuPatches { - /// - /// Gets the currently selected mod index. - /// public static int SelectedModIdx { get; private set; } - /// - /// Gets the currently selected mod. - /// public static MiraPluginInfo? SelectedMod { get; private set; } private static TextMeshPro? _text; private static Vector3 _roleBtnOgPos; + private static GameOptionsMenu? _modifiersTab; + private static PassiveButton? _modifiersButton; + private static PassiveButton? _smallRolesButton; + + [HarmonyPostfix] + [HarmonyPatch(nameof(GameSettingMenu.ChangeTab))] + public static void ChangeTabPostfix(GameSettingMenu __instance, int tabNum, bool previewOnly) + { + if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly) + { + if (_modifiersTab) _modifiersTab?.gameObject.SetActive(tabNum == 3); + _modifiersButton?.SelectButton(tabNum == 3); + _smallRolesButton?.SelectButton(tabNum == 2); + } + + if (previewOnly) + { + return; + } + + _modifiersButton?.SelectButton(tabNum == 3); + _smallRolesButton?.SelectButton(tabNum == 2); + } + /// /// Prefix for the method. Sets up the custom options. /// @@ -54,43 +69,116 @@ public static class GameSettingMenuPatches _text.fontSizeMax = 3.2f; _text.overflowMode = TextOverflowModes.Overflow; - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); - _text.alignment = TextAlignmentOptions.Center; var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject; nextButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f); nextButton.name = "RightArrowButton"; - nextButton.transform.FindChild("Inactive").gameObject.GetComponent().sprite = MiraAssets.NextButton.LoadAsset(); - nextButton.transform.FindChild("Active").gameObject.GetComponent().sprite = MiraAssets.NextButtonActive.LoadAsset(); + nextButton.transform.FindChild("Inactive").gameObject.GetComponent().sprite = + MiraAssets.NextButton.LoadAsset(); + nextButton.transform.FindChild("Active").gameObject.GetComponent().sprite = + MiraAssets.NextButtonActive.LoadAsset(); nextButton.gameObject.GetComponent().DestroyImmediate(); var passiveButton = nextButton.gameObject.GetComponent(); passiveButton.OnClick = new ButtonClickedEvent(); - passiveButton.OnClick.AddListener((UnityAction)(() => - { - SelectedModIdx += 1; - if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) + passiveButton.OnClick.AddListener( + (UnityAction)(() => { - SelectedModIdx = 0; - } - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); - })); + SelectedModIdx += 1; + if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) + { + SelectedModIdx = 0; + } + + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); + })); var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject; backButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f); backButton.name = "LeftArrowButton"; backButton.gameObject.GetComponent().Destroy(); - backButton.transform.FindChild("Active").gameObject.GetComponent().flipX = backButton.transform.FindChild("Inactive").gameObject.GetComponent().flipX = true; - backButton.gameObject.GetComponent().OnClick.AddListener((UnityAction)(() => - { - SelectedModIdx -= 1; - if (SelectedModIdx < 0) + backButton.transform.FindChild("Active").gameObject.GetComponent().flipX = + backButton.transform.FindChild("Inactive").gameObject.GetComponent().flipX = true; + backButton.gameObject.GetComponent().OnClick.AddListener( + (UnityAction)(() => { - SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; + SelectedModIdx -= 1; + if (SelectedModIdx < 0) + { + SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; + } + + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); + })); + + // clone game settings tab for modifiers + _modifiersTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent); + _modifiersTab.name = "MODIFIERS TAB"; + + // create button for modifiers + __instance.RoleSettingsButton.gameObject.SetActive(false); + _smallRolesButton = Object.Instantiate( + __instance.RoleSettingsButton, + __instance.RoleSettingsButton.transform.parent); + var pos = new Vector3( + -3.65f, + _smallRolesButton.transform.localPosition.y, + _smallRolesButton.transform.localPosition.z); + _smallRolesButton.transform.localPosition = pos; + _smallRolesButton.OnClick.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(2, false); + })); + _smallRolesButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(2, true); + })); + + var roleText = _smallRolesButton.buttonText; + roleText.text = "Roles"; + roleText.GetComponent().Destroy(); + roleText.alignment = TextAlignmentOptions.Center; + roleText.transform.parent.localPosition = new Vector3( + -.525f, + roleText.transform.parent.localPosition.y, + roleText.transform.parent.localPosition.z); + + foreach (var collider in _smallRolesButton.Colliders) + { + if (collider.TryCast() is { } col) + { + col.size = new Vector2(col.size.x / 2, col.size.y); } - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); - })); + } + + foreach (var rend in _smallRolesButton.GetComponentsInChildren(true)) + { + rend.size = new Vector2(rend.size.x / 2, rend.size.y); + } + + _modifiersButton = Object.Instantiate(_smallRolesButton, _smallRolesButton.transform.parent); + _modifiersButton.OnClick = new ButtonClickedEvent(); + _modifiersButton.OnClick.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(3, false); + })); + _modifiersButton.OnMouseOver = new UnityEvent(); + _modifiersButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(3, true); + })); + + _modifiersButton.buttonText.text = "Modifiers"; + pos.x = -2.27f; + _modifiersButton.transform.localPosition = pos; + _modifiersButton.name = "ModifiersButton"; + + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); } private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles) @@ -109,10 +197,30 @@ public static class GameSettingMenuPatches _text.text = name[..Math.Min(name.Length, 25)]; } - menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; - menu.GameSettingsButton.gameObject.SetActive(true); - menu.RoleSettingsButton.gameObject.SetActive(true); + _modifiersButton.gameObject.SetActive(false); + _smallRolesButton.gameObject.SetActive(false); + menu.RoleSettingsButton.gameObject.SetActive(false); + if (SelectedModIdx != 0 && + SelectedMod?.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) == true) + { + _modifiersButton.gameObject.SetActive(true); + _smallRolesButton.gameObject.SetActive(true); + } + else + { + menu.RoleSettingsButton.gameObject.SetActive(true); + if (SelectedModIdx == 0 && _modifiersTab.gameObject.active) + { + menu.ChangeTab(0, false); + } + } + //menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; + //menu.GameSettingsButton.gameObject.SetActive(true); + //menu.RoleSettingsButton.gameObject.SetActive(true); + + // TODO: reimplement this + /* if (SelectedModIdx != 0) { if (SelectedMod?.Options.Where(x=>x.AdvancedRole==null).ToList().Count == 0) @@ -129,7 +237,7 @@ public static class GameSettingMenuPatches { menu.RoleSettingsButton.transform.localPosition = menu.GameSettingsButton.transform.localPosition; } - } + }*/ if (roles.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0) { @@ -139,11 +247,13 @@ public static class GameSettingMenuPatches { child.gameObject.DestroyImmediate(); } + roles.advancedSettingChildren.Clear(); roles.advancedSettingChildren = null; } - foreach (var header in roles.RoleChancesSettings.transform.GetComponentsInChildren()) + foreach (var header in roles.RoleChancesSettings.transform + .GetComponentsInChildren()) { header.gameObject.DestroyImmediate(); } diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 4f8c060..9b2a934 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -2,11 +2,14 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -17,17 +20,38 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(LobbyViewSettingsPane))] public static class LobbyViewPanePatches { - private static int CurrentSelectedMod { get; set; } + private static int SelectedModIdx { get; set; } - private static MiraPluginInfo? SelectedMod => CurrentSelectedMod == 0 + private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0 ? null - : MiraPluginManager.Instance.RegisteredPlugins()[CurrentSelectedMod - 1]; + : MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1]; + + private static PassiveButton? ModifiersTabButton { get; set; } + + private static StringNames ModifiersTabName { get; } = CustomStringName.CreateAndRegister("ModifiersTab"); [HarmonyPostfix] [HarmonyPatch(nameof(LobbyViewSettingsPane.Awake))] public static void AwakePatch(LobbyViewSettingsPane __instance) { __instance.gameModeText.transform.localPosition = new Vector3(-2.3f, 2.4f, -2); + __instance.gameModeText.GetComponent().Destroy(); + + // create modifiers button + ModifiersTabButton = Object.Instantiate(__instance.rolesTabButton, __instance.rolesTabButton.transform.parent); + ModifiersTabButton.buttonText.gameObject.GetComponent().Destroy(); + ModifiersTabButton.name = "ModifiersTabButton"; + var pos = ModifiersTabButton.transform.localPosition; + pos.x = 2.1f; + ModifiersTabButton.transform.localPosition = pos; + ModifiersTabButton.buttonText.text = "Modifiers"; + ModifiersTabButton.OnClick = new Button.ButtonClickedEvent(); + ModifiersTabButton.OnClick.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(ModifiersTabName); + })); + ModifiersTabButton.gameObject.SetActive(SelectedModIdx!=0); // Create the next button var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject; @@ -49,14 +73,13 @@ public static class LobbyViewPanePatches passiveButton.OnClick.AddListener( (UnityAction)(() => { - CurrentSelectedMod += 1; - if (CurrentSelectedMod > MiraPluginManager.Instance.RegisteredPlugins().Length) + SelectedModIdx += 1; + if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) { - CurrentSelectedMod = 0; + SelectedModIdx = 0; } - __instance.RefreshTab(); - __instance.scrollBar.ScrollToTop(); + Refresh(__instance); })); // Create the back button @@ -72,34 +95,56 @@ public static class LobbyViewPanePatches passiveButton2.OnClick.AddListener( (UnityAction)(() => { - CurrentSelectedMod -= 1; - if (CurrentSelectedMod < 0) + SelectedModIdx -= 1; + if (SelectedModIdx < 0) { - CurrentSelectedMod = MiraPluginManager.Instance.RegisteredPlugins().Length; + SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; } - __instance.RefreshTab(); - __instance.scrollBar.ScrollToTop(); + Refresh(__instance); })); } - [HarmonyPostfix] - [HarmonyPatch(nameof(LobbyViewSettingsPane.Update))] - public static void UpdatePatch(LobbyViewSettingsPane __instance) + private static void Refresh(LobbyViewSettingsPane menu) { - __instance.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Default"; + ModifiersTabButton?.gameObject.SetActive(SelectedModIdx != 0); + menu.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Default"; + menu.RefreshTab(); + menu.scrollBar.ScrollToTop(); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(LobbyViewSettingsPane.SetTab))] + public static bool SetTabPatch(LobbyViewSettingsPane __instance) + { + if (__instance.currentTab != ModifiersTabName || SelectedMod == null) + { + ModifiersTabButton?.SelectButton(false); + return true; + } + + __instance.taskTabButton.SelectButton(false); + __instance.rolesTabButton.SelectButton(false); + + ModifiersTabButton?.SelectButton(true); + var filteredGroups = SelectedMod.OptionGroups + .Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true)); + DrawOptions(__instance, filteredGroups); + return false; } [HarmonyPrefix] [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))] public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance) { - if (CurrentSelectedMod == 0) + if (SelectedModIdx == 0 || SelectedMod == null) { return true; } - DrawOptionsTab(__instance); + var filteredGroups = SelectedMod.OptionGroups + .Where(x => x.OptionableType == null && x.GroupVisible.Invoke()); + DrawOptions(__instance, filteredGroups); return false; } @@ -107,7 +152,7 @@ public static class LobbyViewPanePatches [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawRolesTab))] public static bool DrawRolesTabPatch(LobbyViewSettingsPane __instance) { - if (CurrentSelectedMod == 0) + if (SelectedModIdx == 0) { return true; } @@ -116,29 +161,22 @@ public static class LobbyViewPanePatches return false; } - private static void DrawOptionsTab(LobbyViewSettingsPane instance) + private static void DrawOptions(LobbyViewSettingsPane menu, IEnumerable groups) { - if (SelectedMod == null) - { - return; - } - var num = 1.44f; - var filteredGroups = SelectedMod.OptionGroups.Where(x => x.GroupVisible.Invoke() && x.AdvancedRole is null); - - foreach (var group in filteredGroups) + foreach (var group in groups) { var categoryHeaderMasked = Object.Instantiate( - instance.categoryHeaderOrigin, - instance.settingsContainer, + menu.categoryHeaderOrigin, + menu.settingsContainer, true); categoryHeaderMasked.SetHeader(StringNames.Name, 61); categoryHeaderMasked.Title.text = group.GroupName; categoryHeaderMasked.transform.localScale = Vector3.one; categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, num, -2f); - instance.settingsInfo.Add(categoryHeaderMasked.gameObject); - num -= 0.85f; + menu.settingsInfo.Add(categoryHeaderMasked.gameObject); + num -= 1.05f; var i = 0; @@ -150,8 +188,8 @@ public static class LobbyViewPanePatches } var viewSettingsInfoPanel = Object.Instantiate( - instance.infoPanelOrigin, - instance.settingsContainer, + menu.infoPanelOrigin, + menu.settingsContainer, true); viewSettingsInfoPanel.transform.localScale = Vector3.one; float num2; @@ -160,7 +198,7 @@ public static class LobbyViewPanePatches num2 = -8.95f; if (i > 0) { - num -= 0.59f; + num -= 0.85f; } } else @@ -184,14 +222,14 @@ public static class LobbyViewPanePatches viewSettingsInfoPanel.SetInfo(data.Title, data.GetValueString(option.GetFloatData()), 61); } - instance.settingsInfo.Add(viewSettingsInfoPanel.gameObject); + menu.settingsInfo.Add(viewSettingsInfoPanel.gameObject); i++; } - num -= 0.59f; + num -= 0.85f; } - instance.scrollBar.CalculateAndSetYBounds(instance.settingsInfo.Count + 10, 2f, 6f, 0.59f); + menu.scrollBar.CalculateAndSetYBounds(menu.settingsInfo.Count + 10, 2f, 6f, 0.85f); } private static void DrawRolesTab(LobbyViewSettingsPane instance) @@ -277,8 +315,9 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one; viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f); - var advancedRoleOptions = SelectedMod.Options - .Where(x => x.AdvancedRole == customRole.GetType()) + var advancedRoleOptions = SelectedMod.OptionGroups + .Where(x => x.OptionableType == customRole.GetType()) + .SelectMany(x => x.Options) .ToList(); if (numPerGame > 0 && advancedRoleOptions.Count > 0) @@ -386,8 +425,9 @@ public static class LobbyViewPanePatches var num = viewPanel.yPosStart; var num2 = 1.08f; - var filteredOptions = SelectedMod.Options - .Where(x => x.AdvancedRole == roleType) + var filteredOptions = SelectedMod.OptionGroups + .Where(x => x.OptionableType == roleType) + .SelectMany(x=>x.Options) .ToList(); for (var i = 0; i < filteredOptions.Count; i++) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 33061d4..126f0b5 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -304,15 +304,14 @@ public static class RoleSettingMenuPatches var hasImage = role.RoleScreenshot != null; var num = hasImage ? -0.872f : -1; - var filteredOptions = GameSettingMenuPatches.SelectedMod?.Options.Where(x => x.AdvancedRole == role.GetType()) ?? []; + // TODO: create sub groups under the role settings. + var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x=> x.OptionableType == role.GetType()) + .SelectMany(x=>x.Options) + .ToList() ?? []; foreach (var option in filteredOptions) { - if (option.AdvancedRole is not null && option.AdvancedRole != role.GetType()) - { - continue; - } - var newOpt = option.CreateOption( __instance.checkboxOrigin, __instance.numberOptionOrigin, @@ -470,8 +469,8 @@ public static class RoleSettingMenuPatches roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left; if (GameSettingMenuPatches.SelectedMod is null || - GameSettingMenuPatches.SelectedMod.Options.Exists( - x => x.AdvancedRole != null && x.AdvancedRole.IsInstanceOfType(role))) + GameSettingMenuPatches.SelectedMod.OptionGroups + .Exists(x => x.OptionableType == role.GetType())) { var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform); newButton.name = "ConfigButton"; @@ -504,7 +503,7 @@ public static class RoleSettingMenuPatches if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1) { - ScrollerNum += -0.43f; + ScrollerNum -= 0.43f; } return roleOptionSetting; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 1fc6be7..15fe686 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using BepInEx.Unity.IL2CPP; @@ -54,7 +55,6 @@ public sealed class MiraPluginManager var eventAttribute = method.GetCustomAttribute(); if (eventAttribute != null) { - Logger.Message("Registering event"); var parameters = method.GetParameters(); if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) { @@ -79,7 +79,7 @@ public sealed class MiraPluginManager if (RegisterRoleAttribute(type, info, out var role)) { - roles.Add(role!); + roles.Add(role); continue; } @@ -173,7 +173,7 @@ public sealed class MiraPluginManager return false; } - private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, out Type? role) + private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role) { role = null; try diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 3c52fb8..27df3cd 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,5 +1,6 @@ using System.Text; using BepInEx.Configuration; +using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; @@ -11,7 +12,7 @@ namespace MiraAPI.Roles; /// /// Interface for custom roles. /// -public interface ICustomRole +public interface ICustomRole : IOptionable { /// /// Gets the name of the role. @@ -46,7 +47,7 @@ public interface ICustomRole /// /// Gets the role options group. /// - public RoleOptionsGroup RoleOptionsGroup => Team switch + RoleOptionsGroup RoleOptionsGroup => Team switch { ModdedRoleTeams.Crewmate => RoleOptionsGroup.Crewmate, ModdedRoleTeams.Impostor => RoleOptionsGroup.Impostor, @@ -57,7 +58,7 @@ public interface ICustomRole /// /// Gets the role's TeamIntroCutscene configuration. /// - public TeamIntroConfiguration? IntroConfiguration => Team switch + TeamIntroConfiguration? IntroConfiguration => Team switch { ModdedRoleTeams.Custom => TeamIntroConfiguration.Neutral, _ => null, diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 0a4fee3..a85da1d 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -39,6 +39,16 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Determines if a float is an integer. + /// + /// The float number. + /// True if the float is an integer, false otherwise. + public static bool IsInteger(this float number) + { + return Mathf.Approximately(number, Mathf.Round(number)); + } + /// /// Gets the best constructor for a type based on the specified arguments. ///