Attribute based options API (wip)

This commit is contained in:
ang-xd committed 2024-05-18 00:10:07 -04:00
1 parent 4155b10ac6
commit 947a202c55
27 files changed
+1024 -118

No files matched your search

-9
View File
@@ -1,9 +0,0 @@
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.Example
{
public class ExOptions
{
[RegisterModdedOption("Use Thing")] public bool useThing = false;
}
}
+14
View File
@@ -0,0 +1,14 @@
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.Example
{
public class ExampleOptions : IModdedOptionGroup
{
public string GroupName => "General Group";
[ModdedToggleOption("Use Thing")] public bool useThing { get; set; } = true;
[ModdedToggleOption("Use another thing")] public bool useAnotherThing { get; set; } = false;
[ModdedNumberOption("Sussy level", min: 0, max: 10)] public float sussyLevel { get; set; } = 4f;
[ModdedStringOption("Sus choices", ["hello", "hello 2", "hello 3"])] public int susChoices { get; set; } = 0;
}
}
+9 -3
View File
@@ -1,6 +1,7 @@
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.Utilities.Assets;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
@@ -11,14 +12,19 @@ namespace MiraAPI.Example;
[BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)]
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class ExamplePlugin : BasePlugin
public partial class ExamplePlugin : BasePlugin, IMiraConfig
{
public Harmony Harmony { get; } = new(Id);
public static ExamplePlugin Instance { get; private set; }
public ModdedOptionTabSettings TabSettings => new ModdedOptionTabSettings()
{
Title = "Example Mod",
TabIcon = MiraAssets.Empty
};
public override void Load()
{
Instance = this;
Harmony.PatchAll();
}
}
}
@@ -0,0 +1,9 @@
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.Example
{
public class SomeRandomNonGroupClass
{
[ModdedStringOption("Yeah, idk", ["Idk 1", "idk 2", "idk 3", "idk 4"])] public static int yeaIdk { get; set; } = 2;
}
}
@@ -0,0 +1,44 @@
using MiraAPI.GameOptions.OptionTypes;
using System;
using System.Reflection;
using UnityEngine;
namespace MiraAPI.GameOptions.Attributes
{
public class ModdedNumberOptionAttribute : ModdedOptionAttribute
{
public float Min { get; private set; }
public float Max { get; private set; }
public float Increment { get; private set; }
public NumberSuffixes SuffixType { get; private set; }
public bool ZeroInfinity { get; private set; }
public ModdedNumberOptionAttribute(string title, float min, float max, float increment = 1, NumberSuffixes suffixType = NumberSuffixes.None, bool zeroInfinity = false, Type roleType = null) : base(title, roleType)
{
Min = min;
Max = max;
Increment = increment;
SuffixType = suffixType;
ZeroInfinity = zeroInfinity;
}
public override IModdedOption CreateOption(object value, PropertyInfo property)
{
Debug.LogError(value.ToString());
var toggleOpt = new ModdedNumberOption(Title, (float)value, Min, Max, Increment, SuffixType, ZeroInfinity, RoleType);
return toggleOpt;
}
public override void SetValue(object value)
{
ModdedNumberOption toggleOpt = HolderOption as ModdedNumberOption;
toggleOpt.SetValue((float)value);
}
public override object GetValue()
{
ModdedNumberOption toggleOpt = HolderOption as ModdedNumberOption;
return toggleOpt.Value;
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes
{
[AttributeUsage(AttributeTargets.Property)]
public abstract class ModdedOptionAttribute : Attribute
{
public IModdedOption HolderOption { get; set; }
public string Title { get; private set; }
public Type RoleType { get; private set; }
public ModdedOptionAttribute(string title, Type roleType = null)
{
Title = title;
RoleType = roleType;
}
public static void Register(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
foreach (PropertyInfo property in type.GetProperties())
{
foreach (var attribute in property.GetCustomAttributes())
{
if (attribute.GetType().BaseType == typeof(ModdedOptionAttribute))
{
ModdedOptionsManager.RegisterOption(assembly, type, (ModdedOptionAttribute)attribute, property);
}
}
}
}
}
public abstract void SetValue(object val);
public abstract object GetValue();
public abstract IModdedOption CreateOption(object value, PropertyInfo property);
}
}
@@ -0,0 +1,33 @@
using MiraAPI.GameOptions.OptionTypes;
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes
{
public class ModdedStringOptionAttribute : ModdedOptionAttribute
{
public string[] Values;
public ModdedStringOptionAttribute(string title, string[] strings, Type roleType = null) : base(title, roleType)
{
Values = strings;
}
public override IModdedOption CreateOption(object value, PropertyInfo property)
{
var opt = new ModdedStringOption(Title, (int)value, Values, RoleType);
return opt;
}
public override void SetValue(object value)
{
ModdedStringOption opt = HolderOption as ModdedStringOption;
opt.SetValue((int)value);
}
public override object GetValue()
{
ModdedStringOption opt = HolderOption as ModdedStringOption;
return opt.Value;
}
}
}
@@ -0,0 +1,33 @@
using MiraAPI.GameOptions.OptionTypes;
using System;
using System.Reflection;
using UnityEngine;
namespace MiraAPI.GameOptions.Attributes
{
public class ModdedToggleOptionAttribute : ModdedOptionAttribute
{
public ModdedToggleOptionAttribute(string title, Type roleType = null) : base(title, roleType)
{
}
public override IModdedOption CreateOption(object value, PropertyInfo property)
{
Debug.LogError(value.ToString());
var toggleOpt = new ModdedToggleOption(Title, (bool)value, RoleType);
return toggleOpt;
}
public override void SetValue(object value)
{
ModdedToggleOption toggleOpt = HolderOption as ModdedToggleOption;
toggleOpt.SetValue((bool)value);
}
public override object GetValue()
{
ModdedToggleOption toggleOpt = HolderOption as ModdedToggleOption;
return toggleOpt.Value;
}
}
}
@@ -1,41 +0,0 @@
using BepInEx.Unity.IL2CPP;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes
{
[AttributeUsage(AttributeTargets.Property)]
public class RegisterModdedOptionAttribute : Attribute
{
private static readonly HashSet<Assembly> RegisteredAssemblies = [];
public string Title { get; private set; }
public RegisterModdedOptionAttribute(string title)
{
Title = title;
}
public static void Initialize()
{
IL2CPPChainloader.Instance.PluginLoad += (_, assembly, plugin) => Register(assembly);
}
public static void Register(Assembly assembly)
{
if (!RegisteredAssemblies.Add(assembly)) return;
foreach (var type in assembly.GetTypes())
{
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public))
{
var attribute = property.GetCustomAttribute<RegisterModdedOptionAttribute>();
if (attribute != null)
{
ModdedOptionsManager.Instance.CreateOption(type, attribute, property);
}
}
}
}
}
}
+14 -2
View File
@@ -1,7 +1,19 @@
namespace MiraAPI.GameOptions
using System;
using UnityEngine;
namespace MiraAPI.GameOptions
{
public interface IModdedOption
{
public Type AdvancedRole { get; }
public OptionBehaviour OptionBehaviour { get; protected set; }
public string Title { get; }
public StringNames StringName { get; }
public Func<bool> Visible { get; set; }
public ModdedOptionGroup Group { get; set; }
public IMiraConfig ParentMod { get; set; }
public void ValueChanged(OptionBehaviour optionBehaviour);
public string GetHudStringText();
public OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
}
}
+13
View File
@@ -0,0 +1,13 @@
using System;
using UnityEngine;
namespace MiraAPI.GameOptions
{
public interface IModdedOptionGroup
{
public string GroupName { get; }
public Func<bool> GroupVisible => () => true;
public Color GroupColor => Color.white;
public Type AdvancedRole => null;
}
}
@@ -0,0 +1,20 @@
using System;
using System.Linq;
namespace MiraAPI.GameOptions
{
public class ModdedGroupSingleton<T> where T : ModdedOptionGroup
{
private static T _instance;
public static T Instance
{
get => _instance ??= ModdedOptionsManager.Groups.OfType<T>().Single();
set
{
if (_instance != null) throw new InvalidOperationException($"Instance for {typeof(T)} is already set");
_instance = value;
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
using System;
using UnityEngine;
namespace MiraAPI.GameOptions
{
public class ModdedOptionGroup
{
public IMiraConfig ParentMod;
public GameObject Header;
public string GroupName;
public Func<bool> GroupVisible;
public Color GroupColor;
public Type AdvancedRole;
}
}
+80 -41
View File
@@ -1,6 +1,6 @@
using HarmonyLib;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.GameOptions.OptionTypes;
using Reactor.Utilities;
using System;
using System.Collections.Generic;
@@ -12,62 +12,101 @@ namespace MiraAPI.GameOptions
{
public class ModdedOptionsManager
{
public static ModdedOptionsManager Instance;
public List<IModdedOption> Options = new List<IModdedOption>();
public static List<IModdedOption> Options = new();
public static List<ModdedOptionGroup> Groups = new();
private static Dictionary<PropertyInfo, ModdedOptionAttribute> OptionAttributes = new();
private static Dictionary<Type, ModdedOptionGroup> OriginalTypes = new();
public static Dictionary<Assembly, IMiraConfig> RegisteredMods = new();
private Dictionary<PropertyInfo, IModdedOption> PatchedOptions = new Dictionary<PropertyInfo, IModdedOption>();
public void CreateOption(Type type, RegisterModdedOptionAttribute attribute, PropertyInfo property)
public static void Initialize()
{
Debug.LogError(property.GetConstantValue().GetType().ToString());
switch (property.GetConstantValue())
IL2CPPChainloader.Instance.PluginLoad += (_, assembly, plugin) =>
{
case bool:
CreateToggleOption(type, attribute, property);
break;
default:
Debug.LogError($"Mira API does not support type {property.GetConstantValue().GetType()}");
break;
}
if (plugin.GetType().GetInterfaces().Contains(typeof(IMiraConfig)))
{
IMiraConfig config = (IMiraConfig)Activator.CreateInstance(plugin.GetType());
RegisteredMods.Add(assembly, config);
}
RegisterOptionGroups(assembly);
ModdedOptionAttribute.Register(assembly);
};
}
public static void PropertySetterPatch(MethodBase __originalMethod, object[] args)
private static void RegisterOptionGroups(Assembly assembly)
{
IModdedOption opt = Instance.PatchedOptions.First(pair => pair.Key.GetSetMethod().Equals(__originalMethod)).Value;
if (opt != null)
foreach (var type in assembly.GetTypes())
{
object value = args[0];
switch (value)
if (typeof(IModdedOptionGroup).IsAssignableFrom(type))
{
case bool:
ModdedToggleOption toggleOpt = opt as ModdedToggleOption;
toggleOpt.SetValue((bool)value);
break;
IModdedOptionGroup group = (IModdedOptionGroup)Activator.CreateInstance(type);
ModdedOptionGroup newGroup = new ModdedOptionGroup()
{
AdvancedRole = group.AdvancedRole,
GroupColor = group.GroupColor,
GroupName = group.GroupName,
GroupVisible = group.GroupVisible
};
Groups.Add(newGroup);
OriginalTypes.Add(type, newGroup);
newGroup.ParentMod = RegisteredMods[assembly];
}
}
}
public ModdedToggleOption CreateToggleOption(Type type, RegisterModdedOptionAttribute attribute, PropertyInfo property)
public static void RegisterOption(Assembly assembly, Type type, ModdedOptionAttribute attribute, PropertyInfo property)
{
var toggleOpt = new ModdedToggleOption(attribute.Title, (bool)property.GetValue(type));
Options.Add(toggleOpt);
if (OptionAttributes.ContainsKey(property)) return;
object newObj = Activator.CreateInstance(type);
IModdedOption result = attribute.CreateOption(property.GetValue(newObj), property);
var setterOriginal = property.GetSetMethod();
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
PluginSingleton<MiraAPIPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
/*
var getterOriginal = property.GetGetMethod();
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton<MiraAPIPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));*/
if (result != null)
{
var setterOriginal = property.GetSetMethod();
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
PluginSingleton<MiraAPIPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
toggleOpt.PropertyInfo = property;
var getterOriginal = property.GetGetMethod();
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton<MiraAPIPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
if (!PatchedOptions.ContainsKey(property))
PatchedOptions.Add(property, toggleOpt);
attribute.HolderOption = result;
result.ParentMod = RegisteredMods[assembly];
return toggleOpt;
Options.Add(result);
OptionAttributes.Add(property, attribute);
if (OriginalTypes.ContainsKey(type))
{
Debug.LogError($"grouping {attribute.Title} with {OriginalTypes[type].GroupName}");
result.Group = OriginalTypes[type];
}
}
}
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
{
ModdedOptionAttribute attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod().Equals(__originalMethod)).Value;
if (attribute != null)
{
attribute.SetValue(value);
}
}
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
{
ModdedOptionAttribute attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod().Equals(__originalMethod)).Value;
if (attribute != null)
{
__result = attribute.GetValue();
return false;
}
return true;
}
}
}
@@ -0,0 +1,64 @@
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes
{
public class ModdedNumberOption : ModdedOption<float>
{
public float Min { get; private set; }
public float Max { get; private set; }
public float Increment { get; private set; }
public NumberSuffixes SuffixType { get; private set; }
public bool ZeroInfinity { get; private set; }
public ModdedNumberOption(string title, float defaultValue, float min, float max, float increment, NumberSuffixes suffixType, bool zeroInfinity, System.Type roleType) : base(title, defaultValue, roleType)
{
Min = min;
Max = max;
Increment = increment;
SuffixType = suffixType;
ZeroInfinity = zeroInfinity;
Value = Mathf.Clamp(defaultValue, min, max);
}
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
{
var numberOption = Object.Instantiate(numberOpt, container);
numberOption.name = Title;
numberOption.Title = StringName;
numberOption.Value = Value;
numberOption.Increment = Increment;
numberOption.SuffixType = SuffixType;
numberOption.FormatString = "0";
numberOption.ValidRange = new FloatRange(Min, Max);
numberOption.ZeroIsInfinity = ZeroInfinity;
numberOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
numberOption.OnEnable();
OptionBehaviour = numberOption;
return numberOption;
}
public override string GetHudStringText()
{
return Title + ": " + Value + Helpers.GetSuffix(SuffixType);
}
public override float GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return Mathf.Clamp(optionBehaviour.GetFloat(), Min, Max);
}
public override void OnValueChanged(float newValue)
{
Value = Mathf.Clamp(newValue, Min, Max);
if (OptionBehaviour is null) return;
NumberOption opt = OptionBehaviour as NumberOption;
opt.Value = newValue;
}
}
}
@@ -1,6 +1,7 @@
using Reactor.Localization.Utilities;
using MiraAPI.Roles;
using Reactor.Localization.Utilities;
using System;
using System.Reflection;
using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes
{
@@ -9,20 +10,26 @@ namespace MiraAPI.GameOptions.OptionTypes
public T Value { get; set; }
public T DefaultValue { get; set; }
public Action<T> ChangedEvent { get; private set; }
public PropertyInfo PropertyInfo;
public string Title { get; }
public StringNames StringName { get; }
public Func<bool> Hidden { get; set; }
public OptionBehaviour OptionBehaviour { get; protected set; }
public Func<bool> Visible { get; set; }
public Type AdvancedRole { get; }
public OptionBehaviour OptionBehaviour { get; set; }
public ModdedOptionGroup Group { get; set; } = null;
public IMiraConfig ParentMod { get; set; } = null;
public ModdedOption(string title, T defaultValue)
public ModdedOption(string title, T defaultValue, Type roleType)
{
Title = title;
DefaultValue = defaultValue;
Value = defaultValue;
StringName = CustomStringName.CreateAndRegister(Title);
Hidden = () => false;
Visible = () => true;
if (roleType is not null && roleType.IsAssignableTo(typeof(ICustomRole)))
{
AdvancedRole = roleType;
}
}
public void ValueChanged(OptionBehaviour optionBehaviour)
@@ -39,9 +46,15 @@ namespace MiraAPI.GameOptions.OptionTypes
ChangedEvent.Invoke(Value);
}
PropertyInfo.SetValue(null, newValue);
OnValueChanged(newValue);
}
public abstract void OnValueChanged(T newValue);
public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour);
public abstract OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
public abstract string GetHudStringText();
}
}
@@ -0,0 +1,49 @@
using Reactor.Localization.Utilities;
using System.Linq;
using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes
{
public class ModdedStringOption : ModdedOption<int>
{
public string[] Values { get; private set; }
public ModdedStringOption(string title, int defaultValue, string[] values, System.Type roleType) : base(title, defaultValue, roleType)
{
Values = values;
}
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
stringOption.name = Title;
stringOption.Title = StringName;
stringOption.Value = Value;
stringOption.Values = Values.Select(CustomStringName.CreateAndRegister).ToArray();
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
stringOption.OnEnable();
OptionBehaviour = stringOption;
return stringOption;
}
public override string GetHudStringText()
{
return Title + ": " + Values[Value];
}
public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return optionBehaviour.GetInt();
}
public override void OnValueChanged(int newValue)
{
if (OptionBehaviour is null) return;
StringOption opt = OptionBehaviour as StringOption;
opt.Value = newValue;
}
}
}
@@ -1,14 +1,42 @@
namespace MiraAPI.GameOptions.OptionTypes
using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes
{
public class ModdedToggleOption : ModdedOption<bool>
{
public ModdedToggleOption(string title, bool defaultValue = false) : base(title, defaultValue)
public ModdedToggleOption(string title, bool defaultValue, System.Type roleType) : base(title, defaultValue, roleType)
{
}
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
{
var toggleOption = UnityEngine.Object.Instantiate(toggleOpt, container);
toggleOption.name = Title;
toggleOption.Title = StringName;
toggleOption.CheckMark.enabled = Value;
toggleOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
toggleOption.OnEnable();
OptionBehaviour = toggleOption;
return toggleOption;
}
public override string GetHudStringText()
{
return Title + ": " + (Value ? "On" : "Off");
}
public override bool GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return optionBehaviour.GetBool();
}
public override void OnValueChanged(bool newValue)
{
if (OptionBehaviour is null) return;
ToggleOption toggleOpt = OptionBehaviour as ToggleOption;
toggleOpt.CheckMark.enabled = newValue;
}
}
}
+153
View File
@@ -0,0 +1,153 @@
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.API.GameOptions;
public static class ModdedOptionsTab
{
public static List<GameObject> CustomTabs = new List<GameObject>();
public static List<GameObject> CustomScreens = new List<GameObject>();
public static GameObject InitializeForMod(IMiraConfig config, GameSettingMenu __instance)
{
var gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject;
var roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject;
var screen = CreateNewMenu(config, __instance);
var tab = CreateCustomTab(config, __instance, screen, gameBtn, roleBtn);
CustomScreens.Add(screen);
CustomTabs.Add(tab);
SpriteRenderer rend = tab.transform.FindChild("Btn/Tab Background").GetComponent<SpriteRenderer>();
rend.enabled = false;
UpdateListeners(__instance, gameBtn.GetComponentInChildren<PassiveButton>(), roleBtn.GetComponentInChildren<PassiveButton>(), rend);
var container = screen.transform.FindChild("GameGroup/SliderInner");
container.DestroyChildren();
//CreateNewResetButton(__instance, container);
return container.gameObject;
}
private static void UpdateListeners(GameSettingMenu __instance, PassiveButton gameB, PassiveButton roleB, SpriteRenderer rend)
{
gameB.OnClick.RemoveAllListeners();
gameB.OnClick.AddListener((UnityAction)(() =>
{
__instance.RegularGameSettings.SetActive(true);
__instance.RolesSettings.gameObject.SetActive(false);
foreach (var screen in CustomScreens) screen.gameObject.SetActive(false);
rend.enabled = false;
__instance.GameSettingsHightlight.enabled = true;
__instance.RolesSettingsHightlight.enabled = false;
}));
roleB.OnClick.RemoveAllListeners();
roleB.OnClick.AddListener((UnityAction)(() =>
{
__instance.RegularGameSettings.SetActive(false);
__instance.RolesSettings.gameObject.SetActive(true);
foreach (var screen in CustomScreens) screen.gameObject.SetActive(false);
rend.enabled = false;
__instance.GameSettingsHightlight.enabled = false;
__instance.RolesSettingsHightlight.enabled = true;
}));
}
public static GameObject CreateHeader(ToggleOption toggleOpt, Transform container, string title)
{
var header = Object.Instantiate(toggleOpt, container);
header.Title = StringNames.None;
header.TitleText.text = title;
header.name = "Header";
var checkBox = header.transform.FindChild("CheckBox")?.gameObject;
if (checkBox)
{
checkBox.Destroy();
}
var background = header.transform.FindChild("Background")?.gameObject;
if (background)
{
background.Destroy();
}
header.GetComponent<OptionBehaviour>().Destroy();
return header.gameObject;
}
public static GameObject CreateCustomTab(IMiraConfig options, GameSettingMenu __instance, GameObject newSettings,
GameObject gameTab, GameObject roleTab)
{
var newTab = Object.Instantiate(gameTab, gameTab.transform.parent);
newTab.name = "Tab";
newTab.transform.position = roleTab.transform.position;
newTab.transform.position += new Vector3(CustomTabs.Count + 1, 0, 0);
__instance.Tabs.transform.position -= new Vector3(0.5f, 0, 0);
var inside = newTab.transform.FindChild("ColorButton");
inside.name = "Btn";
var btn = inside.GetComponentInChildren<PassiveButton>();
btn.OnClick.RemoveAllListeners();
btn.OnClick.AddListener((Action)TabAction);
var spriteRend = inside.GetComponentInChildren<SpriteRenderer>();
// if (options.TabSettings.TabIcon != null) spriteRend.sprite = options.TabSettings.TabIcon.LoadAsset();
return newTab;
void TabAction()
{
__instance.RegularGameSettings.SetActive(false);
__instance.RolesSettings.gameObject.SetActive(false);
foreach (var screen in CustomScreens)
{
screen.gameObject.SetActive(false);
}
newSettings.gameObject.SetActive(true);
SpriteRenderer rend = newTab.transform.FindChild("Btn/Tab Background").GetComponent<SpriteRenderer>();
rend.enabled = true;
gameTab.transform.FindChild("ColorButton/Tab Background").gameObject.GetComponent<SpriteRenderer>().enabled = false;
roleTab.transform.FindChild("Hat Button/Tab Background").gameObject.GetComponent<SpriteRenderer>().enabled = false;
foreach (var tab in CustomTabs)
{
tab.transform.FindChild("Btn/Tab Background").gameObject.GetComponent<SpriteRenderer>().enabled = false;
}
}
}
public static GameObject CreateNewMenu(IMiraConfig config, GameSettingMenu __instance)
{
var gameSettings = __instance.RegularGameSettings;
var newSettings = Object.Instantiate(gameSettings, gameSettings.transform.parent);
newSettings.name = "Settings";
newSettings.SetActive(false);
var launchpadGroup = newSettings.transform.FindChild("GameGroup").gameObject;
var text = launchpadGroup.transform.FindChild("Text").gameObject.GetComponent<TextMeshPro>();
text.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
text.text = $"{config.TabSettings.Title}";
return newSettings;
}
}
+16
View File
@@ -0,0 +1,16 @@
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI
{
public interface IMiraConfig
{
public ModdedOptionTabSettings TabSettings { get; }
}
public struct ModdedOptionTabSettings(string title, LoadableAsset<Sprite> tabIcon)
{
public string Title { get; set; } = title;
public LoadableAsset<Sprite> TabIcon { get; set; } = tabIcon;
}
}
-4
View File
@@ -26,10 +26,6 @@
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="GameOptions\" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build" Condition="'$(AmongUs)' != ''">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(AmongUs)/BepInEx/plugins/" />
+2 -2
View File
@@ -2,7 +2,7 @@
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.GameModes;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Roles;
using Reactor;
@@ -27,7 +27,7 @@ public partial class MiraAPIPlugin : BasePlugin
Harmony.PatchAll();
RegisterCustomRoleAttribute.Initialize();
RegisterModdedOptionAttribute.Initialize();
ModdedOptionsManager.Initialize();
RegisterGameModeAttribute.Register(Assembly.GetCallingAssembly());
RegisterButtonAttribute.Register(Assembly.GetExecutingAssembly());
+60 -1
View File
@@ -1,6 +1,8 @@
using HarmonyLib;
using InnerNet;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Patches.Options;
using MiraAPI.Roles;
using Reactor.Utilities.Extensions;
using UnityEngine;
@@ -16,10 +18,67 @@ namespace MiraAPI.Patches
/// Custom role tab
private static TaskPanelBehaviour _roleTab;
/// Scrolling increment
private const float Increment = 0.3f;
/// Bounds for scrolling
private static FloatRange _bounds = new FloatRange(2.9f, 4.6f);
/// <summary>
/// Add options scrolling (on the hud text)
/// </summary>
public static void OptionsScrollingLogic(HudManager __instance)
{
if (Input.GetKeyDown(KeyCode.Tab))
{
if (ToHudStringPatch.CurrentPage >= ModdedOptionsManager.RegisteredMods.Count)
{
ToHudStringPatch.CurrentPage = 0;
}
else
{
ToHudStringPatch.CurrentPage += 1;
}
}
var numPlayers = GameData.Instance ? GameData.Instance.PlayerCount : 10;
HudManager.Instance.GameSettings.text = GameOptionsManager.Instance.CurrentGameOptions.ToHudString(numPlayers);
var pos = __instance.GameSettings.transform.localPosition;
if (!PlayerControl.LocalPlayer.CanMove)
{
return;
}
if (ToHudStringPatch.CurrentPage == 0)
{
__instance.GameSettings.transform.localPosition = new Vector3(pos.x, _bounds.min, pos.z);
return;
}
_bounds.max = __instance.GameSettings.rectTransform.sizeDelta.y + _bounds.min;
var delta = Input.mouseScrollDelta.y switch
{
> 0f => -Increment,
< 0f => Increment,
_ => 0f
};
pos = new Vector3(pos.x, Mathf.Clamp(pos.y + delta, _bounds.min, _bounds.max), pos.z);
__instance.GameSettings.transform.localPosition = pos;
}
[HarmonyPostfix, HarmonyPatch("Update")]
public static void UpdatePostfix(HudManager __instance)
{
var local = PlayerControl.LocalPlayer;
if (LobbyBehaviour.Instance)
{
OptionsScrollingLogic(__instance);
}
if (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started && !ShipStatus.Instance)
{
return;
@@ -122,4 +181,4 @@ namespace MiraAPI.Patches
}
}
}
}
}
@@ -0,0 +1,109 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.GameOptions;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(GameOptionsMenu))]
public static class GameOptionsMenuPatch
{
/// <summary>
/// Change the OnValueChanged action for all custom game options
/// </summary>
[HarmonyPostfix, HarmonyPatch("Start")]
public static void StartPostfix(GameOptionsMenu __instance)
{
foreach (var customOption in ModdedOptionsManager.Options)
{
if (customOption.AdvancedRole is not null || !customOption.OptionBehaviour)
{
continue;
}
customOption.OptionBehaviour.OnValueChanged = (Action<OptionBehaviour>)customOption.ValueChanged;
}
}
private static GameSettingMenu menu;
/// <summary>
/// Set the position and offset of all custom game options
/// </summary>
[HarmonyPostfix, HarmonyPatch("Update")]
public static void UpdatePostfix(GameOptionsMenu __instance)
{
if (!menu)
{
menu = Object.FindObjectsOfType<GameSettingMenu>().First();
}
if (menu.RegularGameSettings.active || menu.RolesSettings.gameObject.active || menu.HideNSeekSettings.active)
{
return;
}
var startOffset = 2.5f;
__instance.GetComponentInParent<Scroller>().ContentYBounds.max = startOffset + __instance.Children.Count * 0.5f;
foreach (var option in ModdedOptionsManager.Options.Where(option => option.Group == null))
{
if (!option.OptionBehaviour) continue;
option.OptionBehaviour.enabled = option.Visible();
option.OptionBehaviour.gameObject.SetActive(option.Visible());
if (option.Visible())
{
startOffset -= 0.55f;
}
var transform = option.OptionBehaviour.transform;
var optionPosition = transform.localPosition;
transform.localPosition = new Vector3(optionPosition.x, startOffset, optionPosition.z);
}
foreach (var group in ModdedOptionsManager.Groups.Where(group => group.AdvancedRole == null))
{
if (group.Header == null)
{
continue;
}
group.Header.SetActive(group.GroupVisible());
if (group.GroupVisible())
{
startOffset -= 0.5f;
}
var position = group.Header.transform.localPosition;
group.Header.transform.localPosition = new Vector3(position.x, startOffset, position.z);
foreach (var option in ModdedOptionsManager.Options.Where(x => x.Group == group))
{
if (!option.OptionBehaviour)
{
continue;
}
var enabled = group.GroupVisible() && option.Visible();
option.OptionBehaviour.enabled = enabled;
option.OptionBehaviour.gameObject.SetActive(enabled);
if (group.GroupVisible() && option.Visible())
{
startOffset -= 0.5f;
}
var transform = option.OptionBehaviour.transform;
var optionPosition = transform.localPosition;
transform.localPosition = new Vector3(optionPosition.x, startOffset, optionPosition.z);
}
}
}
}
@@ -0,0 +1,68 @@
using HarmonyLib;
using MiraAPI.API.GameOptions;
using MiraAPI.GameOptions;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(GameSettingMenu))]
public static class GameSettingsMenuPatches
{
private static GameObject _gameBtn;
private static GameObject _roleBtn;
[HarmonyPrefix, HarmonyPatch("Start")]
public static void StartPrefix(GameSettingMenu __instance)
{
foreach (var tab in ModdedOptionsTab.CustomTabs) tab.gameObject.Destroy();
foreach (var screen in ModdedOptionsTab.CustomScreens) screen.gameObject.Destroy();
ModdedOptionsTab.CustomScreens.Clear();
ModdedOptionsTab.CustomTabs.Clear();
_gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject;
_roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject;
var numberOpt = __instance.RegularGameSettings.GetComponentInChildren<NumberOption>();
var toggleOpt = Object.FindObjectOfType<ToggleOption>();
var stringOpt = __instance.RegularGameSettings.GetComponentInChildren<StringOption>();
foreach (var pair in ModdedOptionsManager.RegisteredMods)
{
var container = ModdedOptionsTab.InitializeForMod(pair.Value, __instance).transform;
foreach (var group in ModdedOptionsManager.Groups.Where(group => group.AdvancedRole == null && group.ParentMod == pair.Value))
{
group.Header = ModdedOptionsTab.CreateHeader(toggleOpt, container, group.GroupName);
CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container, ModdedOptionsManager.Options.Where(x => x.Group == group).ToList());
}
CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container, ModdedOptionsManager.Options.Where(x => x.Group is null).ToList());
}
if (!numberOpt || !toggleOpt || !stringOpt)
{
Logger<MiraAPIPlugin>.Error("OPTION PREFABS MISSING");
}
}
public static void CreateOptionsFor(GameSettingMenu __instance, ToggleOption togglePrefab, NumberOption numberPrefab, StringOption stringPrefab, Transform container,
List<IModdedOption> options)
{
foreach (IModdedOption option in options)
{
if (option.AdvancedRole is not null || option.OptionBehaviour)
{
continue;
}
OptionBehaviour newOption = option.CreateOption(togglePrefab, numberPrefab, stringPrefab, container);
__instance.AllItems = __instance.AllItems.AddItem(newOption.transform).ToArray();
}
}
}
+118
View File
@@ -0,0 +1,118 @@
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Roles;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(IGameOptionsExtensions), "ToHudString")]
public static class ToHudStringPatch
{
public static int CurrentPage = 0;
/// <summary>
/// Generic method to add options
/// </summary>
public static void AddOptions(StringBuilder sb,
List<IModdedOption> options)
{
foreach (var opt in options)
{
sb.AppendLine(opt.GetHudStringText());
}
}
public static void Prefix()
{
if (GameManager.Instance is null || GameManager.Instance.IsHideAndSeek())
{
return;
}
foreach (var role in CustomRoleManager.CustomRoles.Values)
{
var customRole = role as ICustomRole;
if (customRole.IsGhostRole)
{
role.Role = RoleTypes.CrewmateGhost;
}
}
}
/// <summary>
/// Update the HudOptions on the left of the screen if player is using Launchpad options
/// </summary>
public static void Postfix(IGameOptions __instance, ref string __result)
{
if (GameManager.Instance is null)
{
return;
}
foreach (var pair in CustomRoleManager.CustomRoles)
{
var customRole = pair.Value as ICustomRole;
if (customRole.IsGhostRole)
{
pair.Value.Role = (RoleTypes)pair.Key;
}
}
if (CurrentPage != 0)
{
IMiraConfig currentPlugin = ModdedOptionsManager.RegisteredMods.Values.ToArray()[CurrentPage - 1];
if (currentPlugin == null)
{
CurrentPage = 0;
return;
}
var sb = new StringBuilder($"<size=180%><b>{currentPlugin.TabSettings.Title}:</b></size>\n<size=130%>");
var groupsWithRoles = ModdedOptionsManager.Groups.Where(group => group.AdvancedRole != null && group.ParentMod == currentPlugin);
var groupsWithoutRoles = ModdedOptionsManager.Groups.Where(group => group.AdvancedRole == null && group.ParentMod == currentPlugin);
AddOptions(sb,
ModdedOptionsManager.Options.Where(option => option.Group == null && option.ParentMod == currentPlugin && option.Visible()).ToList()
);
foreach (var group in groupsWithoutRoles)
{
if (!group.GroupVisible())
{
continue;
}
sb.AppendLine($"\n<size=160%><b>{group.GroupName}</b></size>");
AddOptions(sb, ModdedOptionsManager.Options.Where(x => x.Group == group).ToList());
}
var customOptionGroups = groupsWithRoles as ModdedOptionGroup[] ?? groupsWithRoles.ToArray();
if (customOptionGroups.Any())
{
sb.AppendLine("\n<size=160%><b>Roles</b></size>");
foreach (var group in customOptionGroups)
{
if (!group.GroupVisible())
{
continue;
}
sb.AppendLine($"<size=140%><b>{group.GroupName}</b></size><size=120%>");
AddOptions(sb, ModdedOptionsManager.Options.Where(x => x.Group == group).ToList());
sb.Append("</size>\n");
}
}
__result = sb.ToString();
}
if (CurrentPage == 0)
{
__result = "<size=160%><b>Normal Options:</b></size>\n<size=130%>" + __result;
}
__result += $"\nUse <b>TAB</b> to switch pages (Page {CurrentPage}/{ModdedOptionsManager.RegisteredMods.Count})</size>";
}
}
@@ -1,5 +1,4 @@
using System;
using UnityEngine;
using UnityEngine;
namespace MiraAPI.Utilities.Assets
{
@@ -12,8 +11,15 @@ namespace MiraAPI.Utilities.Assets
return _loadedAsset;
}
return _loadedAsset = SpriteTools.LoadSpriteFromPath(resourcesFolder + name);
throw new Exception($"INVALID ASSET: {name}");
try
{
return _loadedAsset = SpriteTools.LoadSpriteFromPath(resourcesFolder + name);
}
catch
{
Debug.LogError($"Not loading, invalid asset: {name}");
return null;
}
}
}
}