Make everything more attribute-based, WIP

This commit is contained in:
ang-xd committed 2024-05-14 20:18:03 -04:00
1 parent 5f29175235
commit 658e3659c5
25 files changed
+604 -82

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Example
{
[RegisterCustomRole(ExamplePlugin.Id)]
public class CustomRole : CrewmateRole, ICustomRole
{
public string RoleName => "Sussy Baka";
public string RoleDescription => "Role decs?";
public string RoleLongDescription => "Extra role decs?";
public Color RoleColor => Palette.Orange;
public RoleTeamTypes Team => RoleTeamTypes.Crewmate;
}
}
+16
View File
@@ -0,0 +1,16 @@
using MiraAPI.API.GameOptions;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example
{
[RegisterCustomOptions(ExamplePlugin.Id)]
public class ExampleOptions : IModOptions
{
public LoadableAsset<Sprite> OptionsTabIcon => MiraAssets.Empty;
public static CustomToggleOption YeOrNa = new CustomToggleOption("Ye or Na", true);
}
}
+24
View File
@@ -0,0 +1,24 @@
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
namespace MiraAPI.Example;
[BepInAutoPlugin("mira.example", "MiraAPIExample")]
[BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)]
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class ExamplePlugin : BasePlugin
{
public Harmony Harmony { get; } = new(Id);
public static ExamplePlugin Instance { get; private set; }
public override void Load()
{
Instance = this;
Harmony.PatchAll();
}
}
+37
View File
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<DebugType>embedded</DebugType>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Description>Simple and easy to use Among Us modding API.</Description>
<Authors>Angxl, XtraCube</Authors>
</PropertyGroup>
<PropertyGroup>
<GamePlatform Condition="'$(GamePlatform)' == ''">Steam</GamePlatform>
<GameVersion Condition="'$(GamePlatform)' == 'Steam'">2024.3.5</GameVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Reactor" Version="2.2.0" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.688" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="Iced" Version="1.20.0" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2024.3.5" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MiraAPI\MiraAPI.csproj" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build" Condition="'$(AmongUs)' != ''">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(AmongUs)/BepInEx/plugins/" />
</Target>
</Project>
+13 -1
View File
@@ -1,6 +1,11 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI", "MiraAPI\MiraAPI.csproj", "{11FBC798-BAF5-4EE5-9511-BE6DB0592F99}" # Visual Studio Version 17
VisualStudioVersion = 17.9.34622.214
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiraAPI", "MiraAPI\MiraAPI.csproj", "{11FBC798-BAF5-4EE5-9511-BE6DB0592F99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example", "MiraAPI.Example\MiraAPI.Example.csproj", "{19B76855-16CC-43CC-A0A6-5A7FFB43F077}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -12,5 +17,12 @@ Global
{11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Debug|Any CPU.Build.0 = Debug|Any CPU {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|Any CPU.ActiveCfg = Release|Any CPU {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|Any CPU.Build.0 = Release|Any CPU {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|Any CPU.Build.0 = Release|Any CPU
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+3 -2
View File
@@ -1,4 +1,5 @@
using MiraAPI.Roles; using BepInEx;
using MiraAPI.Roles;
using Reactor.Localization.Utilities; using Reactor.Localization.Utilities;
using System; using System;
@@ -14,6 +15,7 @@ public abstract class AbstractGameOption
public CustomOptionGroup Group { get; set; } public CustomOptionGroup Group { get; set; }
public Func<bool> Hidden { get; set; } public Func<bool> Hidden { get; set; }
public OptionBehaviour OptionBehaviour { get; protected set; } public OptionBehaviour OptionBehaviour { get; protected set; }
public PluginInfo ParentMod { get; set; }
public void ValueChanged(OptionBehaviour optionBehaviour) public void ValueChanged(OptionBehaviour optionBehaviour)
{ {
OnValueChanged(optionBehaviour); OnValueChanged(optionBehaviour);
@@ -33,6 +35,5 @@ public abstract class AbstractGameOption
Save = save; Save = save;
Hidden = () => false; Hidden = () => false;
CustomOptionsManager.CustomOptions.Add(this);
} }
} }
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes
{
[AttributeUsage(AttributeTargets.Field)]
public class RegisterCustomOptionAttribute : Attribute
{
private static readonly HashSet<Assembly> RegisteredAssemblies = [];
public string ModId { get; }
public RegisterCustomOptionAttribute(string modId)
{
ModId = modId;
}
}
}
@@ -0,0 +1,43 @@
using BepInEx.Unity.IL2CPP;
using MiraAPI.API.GameOptions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes
{
[AttributeUsage(AttributeTargets.Class)]
public class RegisterCustomOptionsAttribute : Attribute
{
private static readonly HashSet<Assembly> RegisteredAssemblies = [];
public string ModId { get; }
public List<FieldInfo> Options;
public RegisterCustomOptionsAttribute(string modId)
{
ModId = modId;
}
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())
{
var attribute = type.GetCustomAttribute<RegisterCustomOptionsAttribute>();
if (attribute != null)
{
attribute.Options = type.GetFields(BindingFlags.Static | BindingFlags.Public)
.Where(fi => fi.FieldType.BaseType == typeof(AbstractGameOption) || fi.FieldType.BaseType == typeof(CustomOptionGroup)).ToList();
CustomOptionsManager.RegisterOptionsForMod(type, attribute.Options, attribute.ModId);
}
}
}
}
}
@@ -42,8 +42,6 @@ public class CustomNumberOption : AbstractGameOption
Logger<MiraAPIPlugin>.Warning(e.ToString()); Logger<MiraAPIPlugin>.Warning(e.ToString());
} }
} }
CustomOptionsManager.CustomNumberOptions.Add(this);
} }
public void SetValue(float newValue) public void SetValue(float newValue)
+3 -3
View File
@@ -1,4 +1,5 @@
using MiraAPI.Roles; using BepInEx;
using MiraAPI.Roles;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
@@ -11,6 +12,7 @@ public class CustomOptionGroup
public GameObject Header; public GameObject Header;
public Type AdvancedRole { get; set; } public Type AdvancedRole { get; set; }
public PluginInfo ParentMod { get; set; }
public readonly List<AbstractGameOption> Options = []; public readonly List<AbstractGameOption> Options = [];
public readonly List<CustomNumberOption> CustomNumberOptions; public readonly List<CustomNumberOption> CustomNumberOptions;
@@ -40,7 +42,5 @@ public class CustomOptionGroup
{ {
option.Group = this; option.Group = this;
} }
CustomOptionsManager.CustomGroups.Add(this);
} }
} }
+57 -1
View File
@@ -1,8 +1,14 @@
using MiraAPI.Networking.Options; using BepInEx;
using BepInEx.Unity.IL2CPP;
using MiraAPI.GameOptions;
using MiraAPI.Networking.Options;
using Reactor.Networking.Rpc; using Reactor.Networking.Rpc;
using Reactor.Utilities; using Reactor.Utilities;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using UnityEngine;
namespace MiraAPI.API.GameOptions; namespace MiraAPI.API.GameOptions;
@@ -13,6 +19,56 @@ public static class CustomOptionsManager
public static readonly List<CustomToggleOption> CustomToggleOptions = []; public static readonly List<CustomToggleOption> CustomToggleOptions = [];
public static readonly List<CustomStringOption> CustomStringOptions = []; public static readonly List<CustomStringOption> CustomStringOptions = [];
public static readonly List<CustomOptionGroup> CustomGroups = []; public static readonly List<CustomOptionGroup> CustomGroups = [];
public static readonly List<PluginInfo> RegisteredMods = [];
public static void RegisterOptionsForMod(Type type, List<FieldInfo> fields, string modId)
{
if (!typeof(IModOptions).IsAssignableFrom(type))
{
Debug.LogError($"Cannot register options for {modId}! Missing IModOptions interface.");
return;
}
PluginInfo info = IL2CPPChainloader.Instance.Plugins[modId];
if (info == null)
{
return;
}
Debug.Log($"Registering Options for {modId}");
RegisteredMods.Add(info);
foreach (FieldInfo field in fields)
{
AbstractGameOption option = (AbstractGameOption)field.GetValue(null);
if (option == null)
{
CustomOptionGroup group = (CustomOptionGroup)field.GetValue(null);
group.ParentMod = info;
CustomGroups.Add(group);
}
option.ParentMod = info;
CustomOptions.Add(option);
switch (field.FieldType.Name)
{
case nameof(CustomNumberOption):
CustomNumberOptions.Add((CustomNumberOption)option);
break;
case nameof(CustomToggleOption):
CustomToggleOptions.Add((CustomToggleOption)option);
break;
case nameof(CustomStringOption):
CustomStringOptions.Add((CustomStringOption)option);
break;
}
}
}
public static void SyncOptions() public static void SyncOptions()
{ {
if (!AmongUsClient.Instance.AmHost) if (!AmongUsClient.Instance.AmHost)
+29 -16
View File
@@ -1,38 +1,40 @@
using Reactor.Utilities.Extensions; using Reactor.Utilities.Extensions;
using System; using System;
using System.Collections.Generic;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace MiraAPI.API.GameOptions; namespace MiraAPI.API.GameOptions;
public static class CustomOptionsTab public static class CustomOptionsTab
{ {
public static GameObject CustomTab; public static List<GameObject> CustomTabs;
public static GameObject CustomScreen; public static List<GameObject> CustomScreens;
public static SpriteRenderer Rend; public static SpriteRenderer Rend;
public static GameObject Initialize(GameSettingMenu __instance) public static GameObject Initialize(GameSettingMenu __instance)
{ {
var gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject; var gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject;
var roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject; var roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject;
var screen = CreateNewMenu(__instance);
var tab = CreateCustomTab(__instance, screen, gameBtn, roleBtn);
CustomScreen = CreateNewMenu(__instance); CustomScreens.Add(screen);
CustomTab = CreateCustomTab(__instance, CustomScreen, gameBtn, roleBtn); CustomTabs.Add(tab);
Rend = CustomTab.transform.FindChild("LaunchpadBtn/Tab Background").GetComponent<SpriteRenderer>(); Rend = tab.transform.FindChild("Btn/Tab Background").GetComponent<SpriteRenderer>();
Rend.enabled = false; Rend.enabled = false;
UpdateListeners(__instance, gameBtn.GetComponentInChildren<PassiveButton>(), roleBtn.GetComponentInChildren<PassiveButton>(), Rend); UpdateListeners(__instance, gameBtn.GetComponentInChildren<PassiveButton>(), roleBtn.GetComponentInChildren<PassiveButton>(), Rend);
var container = CustomScreen.transform.FindChild("GameGroup/SliderInner"); var container = screen.transform.FindChild("GameGroup/SliderInner");
container.DestroyChildren(); container.DestroyChildren();
CreateNewResetButton(__instance, container); //CreateNewResetButton(__instance, container);
return container.gameObject; return container.gameObject;
} }
private static void CreateNewResetButton(GameSettingMenu __instance, Transform container) /*private static void CreateNewResetButton(GameSettingMenu __instance, Transform container)
{ {
var resetBtn = __instance.RegularGameSettings.transform.FindChild("GameGroup/SliderInner/ResetToDefault"); var resetBtn = __instance.RegularGameSettings.transform.FindChild("GameGroup/SliderInner/ResetToDefault");
var newResetBtn = Object.Instantiate(resetBtn.gameObject, container); var newResetBtn = Object.Instantiate(resetBtn.gameObject, container);
@@ -54,12 +56,12 @@ public static class CustomOptionsTab
toggle.OnClick.AddListener((UnityAction)(() => { CustomOptionsManager.ResetToDefault(); })); toggle.OnClick.AddListener((UnityAction)(() => { CustomOptionsManager.ResetToDefault(); }));
toggle.OnMouseOver.AddListener((UnityAction)(() => { tmp.text = "<b>Reset Options</b>"; })); toggle.OnMouseOver.AddListener((UnityAction)(() => { tmp.text = "<b>Reset Options</b>"; }));
toggle.OnMouseOut.AddListener((UnityAction)(() => { tmp.text = "Reset Options"; })); toggle.OnMouseOut.AddListener((UnityAction)(() => { tmp.text = "Reset Options"; }));
} }*/
private static void UpdateListeners(GameSettingMenu __instance, PassiveButton gameB, PassiveButton roleB, SpriteRenderer rend) private static void UpdateListeners(GameSettingMenu __instance, PassiveButton gameB, PassiveButton roleB, SpriteRenderer rend)
{ {
gameB.OnClick.RemoveAllListeners(); gameB.OnClick.RemoveAllListeners();
gameB.OnClick.AddListener((UnityAction)(() => /* gameB.OnClick.AddListener((UnityAction)(() =>
{ {
__instance.RegularGameSettings.SetActive(true); __instance.RegularGameSettings.SetActive(true);
__instance.RolesSettings.gameObject.SetActive(false); __instance.RolesSettings.gameObject.SetActive(false);
@@ -82,7 +84,7 @@ public static class CustomOptionsTab
__instance.GameSettingsHightlight.enabled = false; __instance.GameSettingsHightlight.enabled = false;
__instance.RolesSettingsHightlight.enabled = true; __instance.RolesSettingsHightlight.enabled = true;
})); }));*/
} }
public static GameObject CreateHeader(ToggleOption toggleOpt, Transform container, string title) public static GameObject CreateHeader(ToggleOption toggleOpt, Transform container, string title)
@@ -113,11 +115,11 @@ public static class CustomOptionsTab
GameObject gameTab, GameObject roleTab) GameObject gameTab, GameObject roleTab)
{ {
var newTab = Object.Instantiate(gameTab, gameTab.transform.parent); var newTab = Object.Instantiate(gameTab, gameTab.transform.parent);
newTab.name = "LaunchpadTab"; newTab.name = "Tab";
gameTab.transform.position += new Vector3(-1, 0, 0); gameTab.transform.position += new Vector3(-1, 0, 0);
var inside = newTab.transform.FindChild("ColorButton"); var inside = newTab.transform.FindChild("ColorButton");
inside.name = "LaunchpadBtn"; inside.name = "Btn";
var btn = inside.GetComponentInChildren<PassiveButton>(); var btn = inside.GetComponentInChildren<PassiveButton>();
@@ -135,6 +137,12 @@ public static class CustomOptionsTab
{ {
__instance.RegularGameSettings.SetActive(false); __instance.RegularGameSettings.SetActive(false);
__instance.RolesSettings.gameObject.SetActive(false); __instance.RolesSettings.gameObject.SetActive(false);
foreach (var screen in CustomScreens)
{
screen.gameObject.SetActive(false);
}
newSettings.gameObject.SetActive(true); newSettings.gameObject.SetActive(true);
var rend = inside.transform.FindChild("Tab Background").GetComponent<SpriteRenderer>(); var rend = inside.transform.FindChild("Tab Background").GetComponent<SpriteRenderer>();
@@ -142,6 +150,11 @@ public static class CustomOptionsTab
gameTab.transform.FindChild("ColorButton/Tab Background").gameObject.GetComponent<SpriteRenderer>().enabled = false; gameTab.transform.FindChild("ColorButton/Tab Background").gameObject.GetComponent<SpriteRenderer>().enabled = false;
roleTab.transform.FindChild("Hat Button/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;
}
} }
} }
@@ -149,13 +162,13 @@ public static class CustomOptionsTab
{ {
var gameSettings = __instance.RegularGameSettings; var gameSettings = __instance.RegularGameSettings;
var newSettings = Object.Instantiate(gameSettings, gameSettings.transform.parent); var newSettings = Object.Instantiate(gameSettings, gameSettings.transform.parent);
newSettings.name = "Launchpad Settings"; newSettings.name = "Settings";
newSettings.SetActive(false); newSettings.SetActive(false);
var launchpadGroup = newSettings.transform.FindChild("GameGroup").gameObject; var launchpadGroup = newSettings.transform.FindChild("GameGroup").gameObject;
var text = launchpadGroup.transform.FindChild("Text").gameObject.GetComponent<TextMeshPro>(); var text = launchpadGroup.transform.FindChild("Text").gameObject.GetComponent<TextMeshPro>();
text.gameObject.GetComponent<TextTranslatorTMP>().Destroy(); text.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
text.text = "Launchpad Settings"; text.text = "Settings";
return newSettings; return newSettings;
} }
@@ -22,7 +22,6 @@ public class CustomStringOption : AbstractGameOption
Options = options; Options = options;
Default = defaultValue; Default = defaultValue;
CustomOptionsManager.CustomStringOptions.Add(this);
if (Save) if (Save)
{ {
try try
@@ -27,7 +27,6 @@ public class CustomToggleOption : AbstractGameOption
Logger<MiraAPIPlugin>.Warning(e.ToString()); Logger<MiraAPIPlugin>.Warning(e.ToString());
} }
} }
CustomOptionsManager.CustomToggleOptions.Add(this);
} }
public void SetValue(bool newValue) public void SetValue(bool newValue)
+10
View File
@@ -0,0 +1,10 @@
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.GameOptions
{
public interface IModOptions
{
LoadableAsset<Sprite> OptionsTabIcon { get; }
}
}
+6 -3
View File
@@ -2,6 +2,7 @@
using BepInEx.Unity.IL2CPP; using BepInEx.Unity.IL2CPP;
using HarmonyLib; using HarmonyLib;
using MiraAPI.GameModes; using MiraAPI.GameModes;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Roles; using MiraAPI.Roles;
using Reactor; using Reactor;
@@ -11,7 +12,7 @@ using System.Reflection;
namespace MiraAPI; namespace MiraAPI;
[BepInAutoPlugin("dev.xtracube.launchpad", "MiraAPI")] [BepInAutoPlugin("mira.api", "MiraAPI")]
[BepInProcess("Among Us.exe")] [BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)] [BepInDependency(ReactorPlugin.Id)]
[ReactorModFlags(ModFlags.RequireOnAllClients)] [ReactorModFlags(ModFlags.RequireOnAllClients)]
@@ -25,8 +26,10 @@ public partial class MiraAPIPlugin : BasePlugin
Instance = this; Instance = this;
Harmony.PatchAll(); Harmony.PatchAll();
RegisterGameModeAttribute.Register(Assembly.GetExecutingAssembly()); RegisterCustomRoleAttribute.Initialize();
RegisterCustomRoleAttribute.Register(Assembly.GetExecutingAssembly()); RegisterCustomOptionsAttribute.Initialize();
RegisterGameModeAttribute.Register(Assembly.GetCallingAssembly());
RegisterButtonAttribute.Register(Assembly.GetExecutingAssembly()); RegisterButtonAttribute.Register(Assembly.GetExecutingAssembly());
} }
} }
+184
View File
@@ -0,0 +1,184 @@
using HarmonyLib;
using InnerNet;
using MiraAPI.API.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Patches.Options;
using MiraAPI.Roles;
using Reactor.Utilities.Extensions;
using UnityEngine;
namespace MiraAPI.Patches
{
[HarmonyPatch(typeof(HudManager))]
public static class HudManagerPatches
{
/// Custom buttons parent
private static GameObject _bottomLeft;
/// 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 >= CustomOptionsManager.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;
}
//CustomGameModeManager.ActiveMode.HudUpdate(__instance);
if (local.Data.Role is ICustomRole customRole)
{
customRole.HudUpdate(__instance);
if (customRole.SetTabText() != null)
{
if (_roleTab == null)
{
_roleTab = CustomRoleManager.CreateRoleTab(customRole);
}
else
{
CustomRoleManager.UpdateRoleTab(_roleTab, customRole);
}
}
else if (customRole.SetTabText() == null && _roleTab)
{
_roleTab.gameObject.Destroy();
}
}
else if (_roleTab)
{
_roleTab.gameObject.Destroy();
}
}
/* /// <summary>
/// Trigger hudstart on current custom gamemode
/// </summary>
[HarmonyPostfix, HarmonyPatch("OnGameStart")]
public static void GameStartPatch(HudManager __instance)
{
CustomGameModeManager.ActiveMode.HudStart(__instance);
}*/
/// <summary>
/// Create custom buttons parent
/// </summary>
[HarmonyPostfix, HarmonyPatch("Start")]
public static void StartPostfix(HudManager __instance)
{
if (!_bottomLeft)
{
var buttons = __instance.transform.Find("Buttons");
_bottomLeft = UnityEngine.Object.Instantiate(buttons.Find("BottomRight").gameObject, buttons);
}
foreach (var t in _bottomLeft.GetComponentsInChildren<ActionButton>(true))
{
t.gameObject.Destroy();
}
var gridArrange = _bottomLeft.GetComponent<GridArrange>();
var aspectPosition = _bottomLeft.GetComponent<AspectPosition>();
_bottomLeft.name = "BottomLeft";
gridArrange.Alignment = GridArrange.StartAlign.Right;
aspectPosition.Alignment = AspectPosition.EdgeAlignments.LeftBottom;
foreach (var button in CustomButtonManager.CustomButtons)
{
button.CreateButton(_bottomLeft.transform);
}
gridArrange.Start();
gridArrange.ArrangeChilds();
aspectPosition.AdjustPosition();
}
/// <summary>
/// Make sure all launchpad hud elements are inactive/active when appropriate
/// </summary>
[HarmonyPostfix, HarmonyPatch("SetHudActive", typeof(PlayerControl), typeof(RoleBehaviour), typeof(bool))]
public static void SetHudActivePostfix(HudManager __instance, [HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] RoleBehaviour roleBehaviour, [HarmonyArgument(2)] bool isActive)
{
if (player.Data == null)
{
return;
}
if (_roleTab)
{
_roleTab.gameObject.SetActive(isActive);
}
foreach (var button in CustomButtonManager.CustomButtons)
button.SetActive(isActive, roleBehaviour);
if (roleBehaviour is ICustomRole role)
{
__instance.ImpostorVentButton.gameObject.SetActive(isActive && role.CanUseVent);
}
}
}
}
@@ -18,11 +18,13 @@ public static class GameSettingsMenuPatches
[HarmonyPrefix, HarmonyPatch("Start")] [HarmonyPrefix, HarmonyPatch("Start")]
public static void StartPrefix(GameSettingMenu __instance) public static void StartPrefix(GameSettingMenu __instance)
{ {
if (CustomOptionsTab.CustomTab) if (CustomOptionsTab.CustomTabs != null)
{ {
return; return;
} }
CustomOptionsTab.CustomTabs = new();
__instance.Tabs.transform.position += new Vector3(0.5f, 0, 0); __instance.Tabs.transform.position += new Vector3(0.5f, 0, 0);
_gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject; _gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject;
_roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject; _roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject;
@@ -81,7 +83,7 @@ public static class GameSettingsMenuPatches
__instance.RegularGameSettings.SetActive(false); __instance.RegularGameSettings.SetActive(false);
__instance.RolesSettings.gameObject.SetActive(false); __instance.RolesSettings.gameObject.SetActive(false);
CustomOptionsTab.CustomScreen.gameObject.SetActive(true); //CustomOptionsTab.CustomScreen.gameObject.SetActive(true);
CustomOptionsTab.Rend.enabled = true; CustomOptionsTab.Rend.enabled = true;
__instance.GameSettingsHightlight.enabled = false; __instance.GameSettingsHightlight.enabled = false;
+25 -28
View File
@@ -1,7 +1,7 @@
using AmongUs.GameOptions; using AmongUs.GameOptions;
using BepInEx;
using HarmonyLib; using HarmonyLib;
using MiraAPI.API.GameOptions; using MiraAPI.API.GameOptions;
using MiraAPI.GameModes;
using MiraAPI.Roles; using MiraAPI.Roles;
using MiraAPI.Utilities; using MiraAPI.Utilities;
using System.Collections.Generic; using System.Collections.Generic;
@@ -13,11 +13,7 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(IGameOptionsExtensions), "ToHudString")] [HarmonyPatch(typeof(IGameOptionsExtensions), "ToHudString")]
public static class ToHudStringPatch public static class ToHudStringPatch
{ {
/// <summary> public static int CurrentPage = 0;
/// Show custom Launchpad options or vanilla game options
/// </summary>
public static bool ShowCustom = false;
/// <summary> /// <summary>
/// Generic method to add options /// Generic method to add options
/// </summary> /// </summary>
@@ -82,28 +78,32 @@ public static class ToHudStringPatch
return; return;
} }
foreach (var role in CustomRoleManager.CustomRoles.Values) foreach (var pair in CustomRoleManager.CustomRoles)
{ {
var customRole = role as ICustomRole; var customRole = pair.Value as ICustomRole;
if (customRole.IsGhostRole) if (customRole.IsGhostRole)
{ {
role.Role = (RoleTypes)customRole.RoleId; pair.Value.Role = (RoleTypes)pair.Key;
} }
} }
if (ShowCustom || !CustomGameModeManager.ActiveMode.CanAccessSettingsTab()) if (CurrentPage != 0)
{ {
var sb = new StringBuilder("<size=180%><b>Launchpad Options:</b></size>\n<size=130%>"); PluginInfo currentPlugin = CustomOptionsManager.RegisteredMods[CurrentPage - 1];
var groupsWithRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole != null); if (currentPlugin == null)
var groupsWithoutRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null); {
CurrentPage = 0;
return;
}
var suffix = CustomGameModeManager.ActiveMode.CanAccessSettingsTab() ? "\nPress <b>Tab</b> to view Normal Options" : var sb = new StringBuilder($"<size=180%><b>{currentPlugin.Metadata.Name} Options:</b></size>\n<size=130%>");
$"\n<b>You can not access Normal Options on {CustomGameModeManager.ActiveMode.Name} mode.</b>"; var groupsWithRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole != null && group.ParentMod == currentPlugin);
var groupsWithoutRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null && group.ParentMod == currentPlugin);
AddOptions(sb, AddOptions(sb,
CustomOptionsManager.CustomNumberOptions.Where(option => option.Group == null && !option.Hidden()), CustomOptionsManager.CustomNumberOptions.Where(option => option.Group == null && option.ParentMod == currentPlugin && !option.Hidden()),
CustomOptionsManager.CustomStringOptions.Where(option => option.Group == null && !option.Hidden()), CustomOptionsManager.CustomStringOptions.Where(option => option.Group == null && option.ParentMod == currentPlugin && !option.Hidden()),
CustomOptionsManager.CustomToggleOptions.Where(option => option.Group == null && !option.Hidden()) CustomOptionsManager.CustomToggleOptions.Where(option => option.Group == null && option.ParentMod == currentPlugin && !option.Hidden())
); );
foreach (var group in groupsWithoutRoles) foreach (var group in groupsWithoutRoles)
@@ -117,14 +117,8 @@ public static class ToHudStringPatch
AddOptions(sb, group.CustomNumberOptions, group.CustomStringOptions, group.CustomToggleOptions); AddOptions(sb, group.CustomNumberOptions, group.CustomStringOptions, group.CustomToggleOptions);
} }
if (GameManager.Instance.IsHideAndSeek())
{
__result = sb + suffix;
return;
}
var customOptionGroups = groupsWithRoles as CustomOptionGroup[] ?? groupsWithRoles.ToArray(); var customOptionGroups = groupsWithRoles as CustomOptionGroup[] ?? groupsWithRoles.ToArray();
if (customOptionGroups.Any() && CustomGameModeManager.ActiveMode.CanAccessRolesTab()) if (customOptionGroups.Any())
{ {
sb.AppendLine("\n<size=160%><b>Roles</b></size>"); sb.AppendLine("\n<size=160%><b>Roles</b></size>");
@@ -141,10 +135,13 @@ public static class ToHudStringPatch
} }
} }
__result = sb + suffix; __result = sb.ToString();
return;
} }
__result = "<size=160%><b>Normal Options:</b></size>\n<size=130%>" + __result + "\nPress <b>Tab</b> to view Launchpad Options</size>"; 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}/{CustomOptionsManager.RegisteredMods.Count})</size>";
} }
} }
@@ -141,7 +141,7 @@ public static class RolesSettingsMenuPatches
if (AmongUsClient.Instance.AmHost) if (AmongUsClient.Instance.AmHost)
{ {
Rpc<SyncRoleOptionsRpc>.Instance.Send(new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); Rpc<SyncRoleOptionsRpc>.Instance.Send(new SyncRoleOptionsRpc.Data((ushort)roleSetting.Role.Role, numEntry.Value, chanceEntry.Value));
} }
GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions; GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions;
return false; return false;
+19 -3
View File
@@ -13,6 +13,7 @@ namespace MiraAPI.Patches.Roles;
public static class TaskAdderPatch public static class TaskAdderPatch
{ {
public static TaskFolder RolesFolder; public static TaskFolder RolesFolder;
public static System.Collections.Generic.Dictionary<string, string> ModsFolders = new System.Collections.Generic.Dictionary<string, string>();
[HarmonyPostfix, HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))] [HarmonyPostfix, HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))]
public static void AddRolesFolder(TaskAdderGame __instance) public static void AddRolesFolder(TaskAdderGame __instance)
@@ -21,7 +22,22 @@ public static class TaskAdderPatch
RolesFolder.gameObject.SetActive(false); RolesFolder.gameObject.SetActive(false);
RolesFolder.FolderName = "Roles"; RolesFolder.FolderName = "Roles";
RolesFolder.name = "RolesFolder"; RolesFolder.name = "RolesFolder";
foreach (var pair in CustomRoleManager.ModRoles)
{
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform);
newFolder.FolderName = pair.Key.Metadata.Name;
newFolder.name = pair.Key.Metadata.Name;
newFolder.gameObject.SetActive(false);
RolesFolder.SubFolders.Add(newFolder);
if (!ModsFolders.ContainsKey(pair.Key.Metadata.Name))
{
ModsFolders.Add(pair.Key.Metadata.Name, pair.Key.Metadata.GUID);
}
}
__instance.Root.SubFolders.Add(RolesFolder); __instance.Root.SubFolders.Add(RolesFolder);
__instance.GoToRoot(); __instance.GoToRoot();
} }
@@ -134,11 +150,11 @@ public static class TaskAdderPatch
} }
} }
if (taskFolder.FolderName == "Roles") // idk why only this works??? if (ModsFolders.TryGetValue(taskFolder.FolderName, out string guid))
{ {
for (var m = 0; m < DestroyableSingleton<RoleManager>.Instance.AllRoles.Length; m++) for (var m = 0; m < CustomRoleManager.GetRolesRegistered(guid).Count; m++)
{ {
var roleBehaviour = DestroyableSingleton<RoleManager>.Instance.AllRoles[m]; var roleBehaviour = CustomRoleManager.GetRolesRegistered(guid)[m];
if (roleBehaviour.Role != RoleTypes.ImpostorGhost && roleBehaviour.Role != RoleTypes.CrewmateGhost && !roleBehaviour.IsDead) if (roleBehaviour.Role != RoleTypes.ImpostorGhost && roleBehaviour.Role != RoleTypes.CrewmateGhost && !roleBehaviour.IsDead)
{ {
var taskAddButton2 = Object.Instantiate(__instance.RoleButton); var taskAddButton2 = Object.Instantiate(__instance.RoleButton);
+52 -13
View File
@@ -1,4 +1,6 @@
using AmongUs.GameOptions; using AmongUs.GameOptions;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using Il2CppInterop.Runtime; using Il2CppInterop.Runtime;
using MiraAPI.Networking.Options; using MiraAPI.Networking.Options;
using Reactor.Localization.Utilities; using Reactor.Localization.Utilities;
@@ -17,14 +19,21 @@ namespace MiraAPI.Roles;
public static class CustomRoleManager public static class CustomRoleManager
{ {
public static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = []; public static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
public static readonly Dictionary<PluginInfo, List<RoleBehaviour>> ModRoles = [];
public static void RegisterInRoleManager() public static void RegisterInRoleManager()
{ {
RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray(); RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray();
} }
internal static void RegisterRole(Type roleType) internal static void RegisterRole(Type roleType, ushort roleId, string modId)
{ {
if (!IL2CPPChainloader.Instance.Plugins.TryGetValue(modId, out PluginInfo info))
{
Debug.LogError($"{modId} is not a valid mod!");
return;
}
if (!(typeof(RoleBehaviour).IsAssignableFrom(roleType) && typeof(ICustomRole).IsAssignableFrom(roleType))) if (!(typeof(RoleBehaviour).IsAssignableFrom(roleType) && typeof(ICustomRole).IsAssignableFrom(roleType)))
{ {
return; return;
@@ -42,7 +51,13 @@ public static class CustomRoleManager
return; return;
} }
roleBehaviour.Role = (RoleTypes)customRole.RoleId; if (CustomRoles.ContainsKey(roleId))
{
Debug.LogError($"Role ID conflict for {customRole.RoleName}. Use a different ID.");
return;
}
roleBehaviour.Role = (RoleTypes)roleId;
roleBehaviour.TeamType = customRole.Team; roleBehaviour.TeamType = customRole.Team;
roleBehaviour.NameColor = customRole.RoleColor; roleBehaviour.NameColor = customRole.RoleColor;
roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName); roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName);
@@ -61,7 +76,16 @@ public static class CustomRoleManager
RoleManager.GhostRoles.Add(roleBehaviour.Role); RoleManager.GhostRoles.Add(roleBehaviour.Role);
} }
CustomRoles.Add(customRole.RoleId, roleBehaviour); if (ModRoles.TryGetValue(info, out List<RoleBehaviour> roles))
{
roles.Add(roleBehaviour);
}
else
{
ModRoles.TryAdd(info, new List<RoleBehaviour>() { roleBehaviour });
}
CustomRoles.Add(roleId, roleBehaviour);
if (customRole.HideSettings) if (customRole.HideSettings)
{ {
@@ -74,6 +98,19 @@ public static class CustomRoleManager
} }
public static List<RoleBehaviour> GetRolesRegistered(string modId)
{
PluginInfo info = ModRoles.Keys.First(key => key.Metadata.GUID == modId);
if (ModRoles.TryGetValue(info, out List<RoleBehaviour> roles))
{
return roles;
}
else
{
Debug.LogError($"{modId} has no roles registered.");
return null;
}
}
public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole result) public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole result)
{ {
CustomRoles.TryGetValue((ushort)roleType, out var temp); CustomRoles.TryGetValue((ushort)roleType, out var temp);
@@ -123,33 +160,35 @@ public static class CustomRoleManager
public static void SyncRoleSettings() public static void SyncRoleSettings()
{ {
foreach (var role in CustomRoles.Values.Select(x => (ICustomRole)x)) foreach (var role in CustomRoles.Values)
{ {
if (role.HideSettings) ICustomRole customRole = role as ICustomRole;
if (customRole.HideSettings)
{ {
continue; continue;
} }
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.NumConfigDefinition, out var numEntry); PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(customRole.NumConfigDefinition, out var numEntry);
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.ChanceConfigDefinition, out var chanceEntry); PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(customRole.ChanceConfigDefinition, out var chanceEntry);
Rpc<SyncRoleOptionsRpc>.Instance.Send(new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); Rpc<SyncRoleOptionsRpc>.Instance.Send(new SyncRoleOptionsRpc.Data((ushort)role.Role, numEntry.Value, chanceEntry.Value));
} }
} }
public static void SyncRoleSettings(int targetId) public static void SyncRoleSettings(int targetId)
{ {
foreach (var role in CustomRoles.Values.Select(x => (ICustomRole)x)) foreach (var role in CustomRoles.Values)
{ {
if (role.HideSettings) ICustomRole customRole = role as ICustomRole;
if (customRole.HideSettings)
{ {
continue; continue;
} }
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.NumConfigDefinition, out var numEntry); PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(customRole.NumConfigDefinition, out var numEntry);
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.ChanceConfigDefinition, out var chanceEntry); PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(customRole.ChanceConfigDefinition, out var chanceEntry);
Rpc<SyncRoleOptionsRpc>.Instance.SendTo(targetId, new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); Rpc<SyncRoleOptionsRpc>.Instance.SendTo(targetId, new SyncRoleOptionsRpc.Data((ushort)role.Role, numEntry.Value, chanceEntry.Value));
} }
} }
} }
-2
View File
@@ -11,8 +11,6 @@ public interface ICustomRole
{ {
string RoleName { get; } string RoleName { get; }
ushort RoleId { get; }
string RoleDescription { get; } string RoleDescription { get; }
string RoleLongDescription { get; } string RoleLongDescription { get; }
+23 -2
View File
@@ -1,4 +1,6 @@
using System; using BepInEx.Unity.IL2CPP;
using MiraAPI.Utilities;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
@@ -8,6 +10,24 @@ namespace MiraAPI.Roles;
public class RegisterCustomRoleAttribute : Attribute public class RegisterCustomRoleAttribute : Attribute
{ {
private static readonly HashSet<Assembly> RegisteredAssemblies = []; private static readonly HashSet<Assembly> RegisteredAssemblies = [];
public ushort RoleId { get; }
public string ModId { get; }
public RegisterCustomRoleAttribute(string modId)
{
ModId = modId;
RoleId = (ushort)(20 + CustomRoleManager.CustomRoles.Count);
}
public RegisterCustomRoleAttribute(string modId, ushort roleId)
{
ModId = modId;
RoleId = roleId;
}
public static void Initialize()
{
IL2CPPChainloader.Instance.PluginLoad += (_, assembly, plugin) => Register(assembly);
}
public static void Register(Assembly assembly) public static void Register(Assembly assembly)
{ {
@@ -18,7 +38,8 @@ public class RegisterCustomRoleAttribute : Attribute
var attribute = type.GetCustomAttribute<RegisterCustomRoleAttribute>(); var attribute = type.GetCustomAttribute<RegisterCustomRoleAttribute>();
if (attribute != null) if (attribute != null)
{ {
CustomRoleManager.RegisterRole(type); Helpers.RegisterType(type);
CustomRoleManager.RegisterRole(type, attribute.RoleId, attribute.ModId);
} }
} }
} }
+22 -1
View File
@@ -1,4 +1,7 @@
using MiraAPI.Roles; using HarmonyLib;
using Il2CppInterop.Runtime.Injection;
using MiraAPI.Roles;
using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using UnityEngine; using UnityEngine;
@@ -14,6 +17,24 @@ namespace MiraAPI.Utilities
return ShipStatus.Instance.AllRooms.ToList().Find(room => room.roomArea.OverlapPoint(pos)); return ShipStatus.Instance.AllRooms.ToList().Find(room => room.roomArea.OverlapPoint(pos));
} }
public static void RegisterType(Type type)
{
if (ClassInjector.IsTypeRegisteredInIl2Cpp(type))
{
return;
}
try
{
ClassInjector.RegisterTypeInIl2Cpp(type);
}
catch (Exception e)
{
Debug.LogError($"Failed to register {type.FullDescription()}: {e}");
}
}
public static string GetSuffix(NumberSuffixes suffix) public static string GetSuffix(NumberSuffixes suffix)
{ {
return suffix switch return suffix switch