Compare commits

..
38 Commits
Author SHA1 Message Date
XtraCube cf0bd6ec38 fixed presets 2025-06-20 08:43:11 -04:00
XtraCube 130236e899 oops fix bad code 2025-06-20 08:38:31 -04:00
XtraCube 3b7b247ac0 refresh presets when opening preset menu 2025-06-20 08:32:24 -04:00
XtraCube 52de9a22de fix preset loading networking 2025-06-19 23:24:02 -04:00
XtraCube b94472ee07 save options scroll states 2025-05-16 19:42:59 -04:00
XtraCube f8056c29da Merge branch 'dev' into presets 2025-05-16 19:27:26 -04:00
XtraCube 8d4f2baf1e Merge remote-tracking branch 'origin/presets' into presets 2025-05-16 19:24:52 -04:00
XtraCube 11ba3dae64 save scroll position in roles menu 2025-05-16 19:24:47 -04:00
AlchlcDvl 86145f94f4 Merge branch 'dev' into presets 2025-05-04 22:29:23 -04:00
XtraCube e0da386567 fix lag and broken roles saving/loading 2025-05-04 18:10:34 -04:00
XtraCube 6f1da2de9f add roles to presets 2025-05-02 19:25:03 -04:00
XtraCube b85358e304 Merge branch '2025.3.25' into presets 2025-05-02 19:19:33 -04:00
XtraCube 2c901bb9b9 remove unused thingy 2025-05-02 14:56:28 -04:00
XtraCube 68f40a96b7 fix preset system 2025-05-02 14:54:42 -04:00
XtraCube a96ffc42f2 fix saving default preset only 2025-04-29 17:23:05 -04:00
XtraCube 0907b14893 Merge branch 'feat-addressables-2' into presets 2025-04-29 17:22:57 -04:00
ang-xd 32a44d646d Futher progress on presets UI 2025-04-12 23:29:09 -04:00
ang-xd 8f590b94a8 Merge branch 'feat-addressables-2' into presets 2025-04-12 20:54:35 -04:00
XtraCube 2e769d79f2 try fix 2025-03-30 14:56:18 -04:00
XtraCube 41305ad0e2 Merge branch 'dev' into presets 2025-03-30 12:09:32 -04:00
XtraCube 2418e17af6 this doesnt work 2025-03-29 21:29:46 -04:00
XtraCube 6fb0afe14e add folder button 2025-03-29 21:29:40 -04:00
XtraCube ab0063dd99 update preset system 2025-03-29 14:56:07 -04:00
XtraCube a35bf60870 add rounded box sprite 2025-03-29 14:26:15 -04:00
XtraCube 692b9d7180 improve spritetools 2025-03-29 14:26:06 -04:00
XtraCube b99efaa420 create loadable asset wrapper 2025-03-29 14:25:57 -04:00
XtraCube 2ecfa267d6 Merge branch 'dev' into presets
# Conflicts:
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
2025-03-29 13:55:18 -04:00
XtraCube 9afe821b0f fix stupid innerscuff code 2025-03-27 18:09:27 -04:00
XtraCube a8d076656d more WIP presets 2025-03-26 18:45:48 -04:00
XtraCube 429b64fc03 preset v2 2025-03-23 17:28:22 -04:00
XtraCube bf5dadc6a0 that def doesnt work 2025-03-23 15:34:20 -04:00
XtraCube b2137230da Merge branch 'dev' into presets 2025-03-23 15:19:51 -04:00
XtraCube 9e8c05f21d text input WIP 2025-03-23 15:01:31 -04:00
XtraCube 2f56c95b39 Merge branch 'dev' into presets
# Conflicts:
#	MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
2025-03-23 13:45:49 -04:00
XtraCube 38f72c42a3 presets v1 2025-03-23 13:44:40 -04:00
XtraCube 5b608f5b3a preset WIP 2025-03-21 15:21:18 -04:00
XtraCube 2248726450 IncludeInPreset property 2025-03-17 09:12:26 -04:00
XtraCube c60537cbd1 add comments to imoddedoption 2025-03-17 09:02:24 -04:00
82 changed files with 1393 additions and 785 deletions

No files matched your search

+2 -1
View File
@@ -1 +1,2 @@
[*.{cs,vb}] [*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
@@ -1,31 +0,0 @@
using System;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.PluginLoading;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
namespace MiraAPI.Example.Extension;
[BepInAutoPlugin("mira.example.extension", "MiraExampleExtensionMod")]
[BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)]
[BepInDependency(MiraApiPlugin.Id)]
[BepInDependency(ExamplePlugin.Id)]
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class ExampleExtensionPlugin : BasePlugin, IMiraExtension
{
public Harmony Harmony { get; } = new(Id);
public override void Load()
{
Harmony.PatchAll();
}
public Type GetBasePluginType()
{
return typeof(ExamplePlugin);
}
}
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
<Description>Example extension for Mira API</Description>
<Authors>Angxl, XtraCube</Authors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MiraAPI\MiraAPI.csproj" />
<ProjectReference Include="..\MiraAPI.Example\MiraAPI.Example.csproj" />
</ItemGroup>
<Import Project="..\AmongUs.props" />
</Project>
@@ -1,16 +0,0 @@
using MiraAPI.Example.Extension.Options;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers.Types;
namespace MiraAPI.Example.Extension.Modifiers;
public class ExtensionModifier : GameModifier
{
public override string ModifierName => "Extension Modifier";
public override string GetDescription() => "This is from the extension mod!";
public override int GetAssignmentChance() => (int)OptionGroupSingleton<ExtensionModifierOptions>.Instance.ModifierChance;
public override int GetAmountPerGame() => (int)OptionGroupSingleton<ExtensionModifierOptions>.Instance.ModifierLimit;
}
@@ -1,13 +0,0 @@
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.Example.Extension.Options;
public class ExtendedFreezerOptions : AbstractOptionGroup<FreezerRole>
{
public override string GroupName => "Extended Freezer Role Options";
[ModdedNumberOption("Freeze Uses", 1, 5)]
public float FreezeUses { get; set; } = 1;
}
@@ -1,17 +0,0 @@
using MiraAPI.Example.Extension.Modifiers;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Utilities;
namespace MiraAPI.Example.Extension.Options;
public class ExtensionModifierOptions : AbstractOptionGroup<ExtensionModifier>
{
public override string GroupName => "Extension Modifier Options";
[ModdedNumberOption("Modifier Chance", 0, 100, 10, MiraNumberSuffixes.Percent)]
public float ModifierChance { get; set; } = 100f;
[ModdedNumberOption("Modifier Limit", 0, 10)]
public float ModifierLimit { get; set; } = 0;
}
@@ -1,15 +0,0 @@
using HarmonyLib;
using MiraAPI.Example.Buttons.Freezer;
using MiraAPI.Example.Extension.Options;
using MiraAPI.GameOptions;
namespace MiraAPI.Example.Extension.Patches;
[HarmonyPatch(typeof(FreezeButton), nameof(FreezeButton.MaxUses), MethodType.Getter)]
public static class FreezeButtonPatch
{
public static void Postfix(ref int __result)
{
__result = (int)OptionGroupSingleton<ExtendedFreezerOptions>.Instance.FreezeUses;
}
}
@@ -16,7 +16,7 @@ public class FreezeButton : CustomActionButton<PlayerControl>
public override float Cooldown => OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeDuration; public override float Cooldown => OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeDuration;
public override int MaxUses => 1; public override int MaxUses => (int)OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeUses;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton; public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
@@ -1,20 +1,17 @@
using MiraAPI.Example.Extension.GameOver; using MiraAPI.Example.GameOver;
using MiraAPI.Example.Extension.Roles; using MiraAPI.Example.Roles;
using MiraAPI.GameEnd; using MiraAPI.GameEnd;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using UnityEngine; using UnityEngine;
namespace MiraAPI.Example.Extension.Buttons; namespace MiraAPI.Example.Buttons;
public class NeutralKillerButton : CustomActionButton public class NeutralKillerButton : CustomActionButton
{ {
public override string Name => "Win Game"; public override string Name => "Win Game";
public override float Cooldown => 0f; public override float Cooldown => 0f;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton; public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
protected override void OnClick() protected override void OnClick()
{ {
CustomGameOver.Trigger<NeutralKillerGameOver>([PlayerControl.LocalPlayer.Data]); CustomGameOver.Trigger<NeutralKillerGameOver>([PlayerControl.LocalPlayer.Data]);
+5 -5
View File
@@ -9,7 +9,10 @@ using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Example.Buttons.Freezer; using MiraAPI.Example.Buttons.Freezer;
using MiraAPI.Example.Roles; using MiraAPI.Example.Roles;
using MiraAPI.Utilities; using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities; using Reactor.Utilities;
using TMPro;
using UnityEngine;
namespace MiraAPI.Example; namespace MiraAPI.Example;
@@ -18,7 +21,7 @@ public static class ExampleEventHandlers
public static void Initialize() public static void Initialize()
{ {
// You can register event handlers with the MiraEventManager class. // You can register event handlers with the MiraEventManager class.
var handle = MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event => MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
{ {
Logger<ExamplePlugin>.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}"); Logger<ExamplePlugin>.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}");
}); });
@@ -32,9 +35,6 @@ public static class ExampleEventHandlers
{ {
Logger<ExamplePlugin>.Info($"{@event.Player.Data.PlayerName} completed {@event.Task.TaskType.ToString()}"); Logger<ExamplePlugin>.Info($"{@event.Player.Data.PlayerName} completed {@event.Task.TaskType.ToString()}");
}); });
// You can also unregister event handlers using the handle returned from RegisterEventHandler.
MiraEventManager.UnregisterEventHandler(handle);
} }
// If you want to add extra votes to a player, do something like this. // If you want to add extra votes to a player, do something like this.
@@ -51,7 +51,7 @@ public static class ExampleEventHandlers
[RegisterEvent(15)] [RegisterEvent(15)]
public static void HandleVoteEvent(HandleVoteEvent @event) public static void HandleVoteEvent(HandleVoteEvent @event)
{ {
if (@event.VoteData.Owner.Data.Role is not FreezerRole) return; if (@event.VoteData.Owner.Data.Role is not NeutralKillerRole) return;
@event.VoteData.SetRemainingVotes(0); @event.VoteData.SetRemainingVotes(0);
@@ -1,9 +1,9 @@
using MiraAPI.Example.Extension.Roles; using MiraAPI.Example.Roles;
using MiraAPI.GameEnd; using MiraAPI.GameEnd;
using MiraAPI.Utilities; using MiraAPI.Utilities;
using UnityEngine; using UnityEngine;
namespace MiraAPI.Example.Extension.GameOver; namespace MiraAPI.Example.GameOver;
public class NeutralKillerGameOver : CustomGameOver public class NeutralKillerGameOver : CustomGameOver
{ {
+4 -1
View File
@@ -1,9 +1,12 @@
using MiraAPI.Example.Options.Modifiers; using Il2CppSystem.Collections.Generic;
using MiraAPI.Example.Options.Modifiers;
using MiraAPI.GameOptions; using MiraAPI.GameOptions;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types; using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using UnityEngine; using UnityEngine;
using Random = System.Random;
namespace MiraAPI.Example.Modifiers; namespace MiraAPI.Example.Modifiers;
@@ -1,4 +1,6 @@
using MiraAPI.Modifiers.Types; using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Modifiers; namespace MiraAPI.Example.Modifiers;
@@ -11,4 +11,7 @@ public class FreezerRoleSettings : AbstractOptionGroup<FreezerRole>
[ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)] [ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)]
public float FreezeDuration { get; set; } = 5; public float FreezeDuration { get; set; } = 5;
[ModdedNumberOption("Freeze Uses", 1, 5)]
public float FreezeUses { get; set; } = 1;
} }
+13 -35
View File
@@ -1,5 +1,4 @@
using AmongUs.GameOptions; using AmongUs.GameOptions;
using MiraAPI.Patches.Stubs;
using MiraAPI.Roles; using MiraAPI.Roles;
using Reactor.Utilities; using Reactor.Utilities;
using TMPro; using TMPro;
@@ -15,8 +14,6 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public Color RoleColor => Palette.AcceptedGreen; public Color RoleColor => Palette.AcceptedGreen;
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
private bool _shouldHide;
public CustomRoleConfiguration Configuration => new(this) public CustomRoleConfiguration Configuration => new(this)
{ {
OptionsScreenshot = ExampleAssets.Banner, OptionsScreenshot = ExampleAssets.Banner,
@@ -25,54 +22,35 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public override void Initialize(PlayerControl player) public override void Initialize(PlayerControl player)
{ {
Logger<ExamplePlugin>.Info("Initializing ChamelonRole for player: " + player.PlayerId); Player = player;
RoleBehaviourStubs.Initialize(this, player); Logger<ExamplePlugin>.Error("Initializing ChamelonRole for player: " + player.PlayerId);
_shouldHide = true;
} }
public void FixedUpdate() public void PlayerControlFixedUpdate(PlayerControl playerControl)
{ {
if (!Player || !_shouldHide) if (playerControl.MyPhysics.Velocity.magnitude > 0)
{ {
return; SpriteRenderer rend = playerControl.cosmetics.currentBodySprite.BodySprite;
} TextMeshPro tmp = playerControl.cosmetics.nameText;
if (Player.MyPhysics.Velocity.magnitude > 0)
{
var rend = Player.cosmetics.currentBodySprite.BodySprite;
var tmp = Player.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, 1), Time.deltaTime * 4f); tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, 1), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f); rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f);
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>()) foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{ {
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f); cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f);
} }
} }
else else
{ {
SpriteRenderer rend = Player.cosmetics.currentBodySprite.BodySprite; SpriteRenderer rend = playerControl.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = Player.cosmetics.nameText; TextMeshPro tmp = playerControl.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f); tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f); rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>()) foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{ {
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f); cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
} }
} }
} }
public override void Deinitialize(PlayerControl targetPlayer)
{
Logger<ExamplePlugin>.Info("Deinitializing ChamelonRole for player: " + targetPlayer.PlayerId);
RoleBehaviourStubs.Deinitialize(this, targetPlayer);
_shouldHide = false;
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>(true))
{
cosmetic.color = Color.white;
}
Player.cosmetics.currentBodySprite.BodySprite.color = Color.white;
Player.cosmetics.nameText.color = Color.white;
}
} }
@@ -1,20 +1,16 @@
using MiraAPI.Example.Extension.GameOver; using MiraAPI.Example.GameOver;
using MiraAPI.GameEnd; using MiraAPI.GameEnd;
using MiraAPI.Roles; using MiraAPI.Roles;
using UnityEngine; using UnityEngine;
namespace MiraAPI.Example.Extension.Roles; namespace MiraAPI.Example.Roles;
public class NeutralKillerRole : ImpostorRole, ICustomRole public class NeutralKillerRole : ImpostorRole, ICustomRole
{ {
public string RoleName => "Outcast Killer"; public string RoleName => "Outcast Killer";
public string RoleDescription => "Outcast who can kill."; public string RoleDescription => "Outcast who can kill.";
public string RoleLongDescription => RoleDescription;
public string RoleLongDescription => this.RoleDescription;
public Color RoleColor => Color.magenta; public Color RoleColor => Color.magenta;
public ModdedRoleTeams Team => ModdedRoleTeams.Custom; public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
public CustomRoleConfiguration Configuration => new(this) public CustomRoleConfiguration Configuration => new(this)
-6
View File
@@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiraAPI", "MiraAPI\MiraAPI.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example", "MiraAPI.Example\MiraAPI.Example.csproj", "{19B76855-16CC-43CC-A0A6-5A7FFB43F077}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example", "MiraAPI.Example\MiraAPI.Example.csproj", "{19B76855-16CC-43CC-A0A6-5A7FFB43F077}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example.Extension", "MiraAPI.Example.Extension\MiraAPI.Example.Extension.csproj", "{526FF017-80E1-4094-804D-741AF35AA226}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -23,10 +21,6 @@ Global
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.Build.0 = Release|Any CPU {19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.Build.0 = Release|Any CPU
{526FF017-80E1-4094-804D-741AF35AA226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{526FF017-80E1-4094-804D-741AF35AA226}.Debug|Any CPU.Build.0 = Debug|Any CPU
{526FF017-80E1-4094-804D-741AF35AA226}.Release|Any CPU.ActiveCfg = Release|Any CPU
{526FF017-80E1-4094-804D-741AF35AA226}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
-22
View File
@@ -1,22 +0,0 @@
using System;
namespace MiraAPI.Events;
/// <summary>
/// Event handle to use when unregistering an event.
/// </summary>
public class MiraEventHandle
{
/// <summary>
/// Gets the type of event.
/// </summary>
public Type EventType { get; }
internal MiraEventWrapper EventWrapper { get; }
internal MiraEventHandle(Type type, MiraEventWrapper eventWrapper)
{
EventType = type;
EventWrapper = eventWrapper;
}
}
+6 -47
View File
@@ -28,14 +28,7 @@ public static class MiraEventManager
foreach (var handler in handlers) foreach (var handler in handlers)
{ {
try ((Action<T>)handler.EventHandler).Invoke(eventInstance);
{
((Action<T>)handler.EventHandler).Invoke(eventInstance);
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
}
} }
return true; return true;
@@ -57,14 +50,7 @@ public static class MiraEventManager
foreach (var handler in handlers) foreach (var handler in handlers)
{ {
try handler.EventHandler.DynamicInvoke(eventInstance);
{
handler.EventHandler.DynamicInvoke(eventInstance);
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
}
} }
return true; return true;
@@ -76,12 +62,12 @@ public static class MiraEventManager
/// <param name="type">The type of event.</param> /// <param name="type">The type of event.</param>
/// <param name="methodInfo">The MethodInfo of the event handler.</param> /// <param name="methodInfo">The MethodInfo of the event handler.</param>
/// <param name="priority">The priority of the event handler. Lower values are called first.</param> /// <param name="priority">The priority of the event handler. Lower values are called first.</param>
/// <returns>An event handle to use when unregistering the event.</returns> public static void RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
public static MiraEventHandle RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
{ {
if (!type.IsSubclassOf(typeof(MiraEvent))) if (!type.IsSubclassOf(typeof(MiraEvent)))
{ {
throw new InvalidOperationException($"Type must be a subclass of MiraEvent: {type.FullName}"); Logger<MiraApiPlugin>.Error($"Type must be a subclass of MiraEvent: {type.FullName}");
return;
} }
EventWrappers.TryAdd(type, []); EventWrappers.TryAdd(type, []);
@@ -98,7 +84,6 @@ public static class MiraEventManager
} }
handlers.Insert(index, eventWrapper); handlers.Insert(index, eventWrapper);
return new MiraEventHandle(type, eventWrapper);
} }
/// <summary> /// <summary>
@@ -107,8 +92,7 @@ public static class MiraEventManager
/// <param name="handler">The callback method/handler for the event.</param> /// <param name="handler">The callback method/handler for the event.</param>
/// <param name="priority">The priority of the event handler. Lower values are called first.</param> /// <param name="priority">The priority of the event handler. Lower values are called first.</param>
/// <typeparam name="T">Type of event.</typeparam> /// <typeparam name="T">Type of event.</typeparam>
/// <returns>An event handle to use when unregistering the event.</returns> public static void RegisterEventHandler<T>(Action<T> handler, int priority = 0) where T : MiraEvent
public static MiraEventHandle RegisterEventHandler<T>(Action<T> handler, int priority = 0) where T : MiraEvent
{ {
EventWrappers.TryAdd(typeof(T), []); EventWrappers.TryAdd(typeof(T), []);
@@ -123,30 +107,5 @@ public static class MiraEventManager
} }
handlers.Insert(index, eventWrapper); handlers.Insert(index, eventWrapper);
return new MiraEventHandle(typeof(T), eventWrapper);
}
/// <summary>
/// Unregister an event using the event handle.
/// </summary>
/// <param name="eventHandle">A handle to the event.</param>
/// <returns>True if the event was unregistered successfully, false otherwise.</returns>
public static bool UnregisterEventHandler(MiraEventHandle eventHandle)
{
if (!EventWrappers.TryGetValue(eventHandle.EventType, out var handlers))
{
return false;
}
if (!handlers.Remove(eventHandle.EventWrapper))
{
return false;
}
if (handlers.Count == 0)
{
EventWrappers.Remove(eventHandle.EventType);
}
return true;
} }
} }
@@ -15,21 +15,14 @@ public class AfterMurderEvent : MiraEvent
/// </summary> /// </summary>
public PlayerControl Target { get; } public PlayerControl Target { get; }
/// <summary>
/// Gets the player's body, if it exists.
/// </summary>
public DeadBody? DeadBody { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AfterMurderEvent"/> class. /// Initializes a new instance of the <see cref="AfterMurderEvent"/> class.
/// </summary> /// </summary>
/// <param name="source">The killer.</param> /// <param name="source">The killer.</param>
/// <param name="target">The killed player.</param> /// <param name="target">The killed player.</param>
/// <param name="deadBody">The player's vody, if it exists.</param> public AfterMurderEvent(PlayerControl source, PlayerControl target)
public AfterMurderEvent(PlayerControl source, PlayerControl target, DeadBody? deadBody)
{ {
Source = source; Source = source;
Target = target; Target = target;
DeadBody = deadBody;
} }
} }
@@ -24,7 +24,6 @@ public class UpdateSystemEvent : MiraCancelableEvent
/// Initializes a new instance of the <see cref="UpdateSystemEvent"/> class. /// Initializes a new instance of the <see cref="UpdateSystemEvent"/> class.
/// </summary> /// </summary>
/// <param name="systemType">The SystemType being updated.</param> /// <param name="systemType">The SystemType being updated.</param>
/// <param name="player">The player that is updating the system.</param>
/// <param name="amount">Amount to update System to.</param> /// <param name="amount">Amount to update System to.</param>
public UpdateSystemEvent(SystemTypes systemType, PlayerControl player, byte amount) public UpdateSystemEvent(SystemTypes systemType, PlayerControl player, byte amount)
{ {
@@ -15,21 +15,14 @@ public class PlayerDeathEvent : MiraEvent
/// </summary> /// </summary>
public DeathReason DeathReason { get; } public DeathReason DeathReason { get; }
/// <summary>
/// Gets the dead body associated with the player, if any.
/// </summary>
public DeadBody? DeadBody { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PlayerDeathEvent"/> class. /// Initializes a new instance of the <see cref="PlayerDeathEvent"/> class.
/// </summary> /// </summary>
/// <param name="player">The player who died.</param> /// <param name="player">The player who died.</param>
/// <param name="reason">The reason the player died.</param> /// <param name="reason">The reason the player died.</param>
/// <param name="deadBody">The player's body, if it exists.</param> public PlayerDeathEvent(PlayerControl player, DeathReason reason)
public PlayerDeathEvent(PlayerControl player, DeathReason reason, DeadBody? deadBody)
{ {
Player = player; Player = player;
DeathReason = reason; DeathReason = reason;
DeadBody = deadBody;
} }
} }
+1 -1
View File
@@ -21,7 +21,7 @@ public static class CustomGameModeManager
} }
/// <summary> /// <summary>
/// Gets the current gamemode. /// Current gamemode.
/// </summary> /// </summary>
public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode(); public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode();
+24
View File
@@ -46,6 +46,11 @@ public interface IModdedOption
/// </summary> /// </summary>
Func<bool> Visible { get; set; } Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option should be included with presets.
/// </summary>
bool IncludeInPreset { get; set; }
/// <summary> /// <summary>
/// Gets or sets the ConfigDefinition for the option, used for BepInEx configuration. /// Gets or sets the ConfigDefinition for the option, used for BepInEx configuration.
/// </summary> /// </summary>
@@ -78,4 +83,23 @@ public interface IModdedOption
/// </summary> /// </summary>
/// <param name="data">>The byte array representing the network data.</param> /// <param name="data">>The byte array representing the network data.</param>
void HandleNetData(byte[] data); void HandleNetData(byte[] data);
/// <summary>
/// Saves the option to a preset configuration file.
/// </summary>
/// <param name="presetConfig">The ConfigFile representing the preset configuration.</param>
/// <param name="saveDefault">Indicates whether to save the default value instead of the current value.</param>
void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
/// <summary>
/// Binds the option to a configuration file.
/// </summary>
/// <param name="config">The ConfigFile to bind the option to.</param>
void Bind(ConfigFile config);
/// <summary>
/// Loads the option from a preset configuration file, applying the values to the option's configuration.
/// </summary>
/// <param name="presetConfig">The ConfigFile representing the preset configuration.</param>
void LoadFromPreset(ConfigFile presetConfig);
} }
+6 -8
View File
@@ -42,12 +42,10 @@ public static class ModdedOptionsManager
Groups.Add(group); Groups.Add(group);
TypeToGroup.Add(type, group); TypeToGroup.Add(type, group);
pluginInfo.OptionGroups.Add(group); pluginInfo.InternalOptionGroups.Add(group);
typeof(OptionGroupSingleton<>).MakeGenericType(type) typeof(OptionGroupSingleton<>).MakeGenericType(type)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, group); .SetValue(null, group);
return true; return true;
@@ -121,7 +119,7 @@ public static class ModdedOptionsManager
option.ConfigDefinition = new ConfigDefinition(groupName, propertyName); option.ConfigDefinition = new ConfigDefinition(groupName, propertyName);
option.ParentMod = pluginInfo.MiraPlugin; option.ParentMod = pluginInfo.MiraPlugin;
pluginInfo.Options.Add(option); pluginInfo.InternalOptions.Add(option);
ModdedOptions.Add(option.Id, option); ModdedOptions.Add(option.Id, option);
group.Options.Add(option); group.Options.Add(option);
} }
@@ -142,7 +140,7 @@ public static class ModdedOptionsManager
// we dont know how other plugins handle their configs // we dont know how other plugins handle their configs
// this way, all the options are saved at once, instead of one by one // this way, all the options are saved at once, instead of one by one
var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>(); var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>();
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{ {
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet); oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
plugin.PluginConfig.SaveOnConfigSet = false; plugin.PluginConfig.SaveOnConfigSet = false;
@@ -158,7 +156,7 @@ public static class ModdedOptionsManager
option.HandleNetData(netData.Data); option.HandleNetData(netData.Data);
} }
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{ {
plugin.PluginConfig.Save(); plugin.PluginConfig.Save();
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin]; plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
@@ -175,7 +173,7 @@ public static class ModdedOptionsManager
/// </summary> /// </summary>
/// <param name="__originalMethod">The original setter method.</param> /// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param> /// <param name="value">The new object value.</param>
// ReSharper disable InconsistentNaming // ReSharper disable once InconsistentNaming
public static void PropertySetterPatch(MethodBase __originalMethod, object value) public static void PropertySetterPatch(MethodBase __originalMethod, object value)
{ {
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value; var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
@@ -188,8 +186,8 @@ public static class ModdedOptionsManager
/// <param name="__originalMethod">The original getter method.</param> /// <param name="__originalMethod">The original getter method.</param>
/// <param name="__result">The result of the property getter.</param> /// <param name="__result">The result of the property getter.</param>
/// <returns>False so the original getter gets skipped.</returns> /// <returns>False so the original getter gets skipped.</returns>
// ReSharper disable InconsistentNaming
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result) public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
// ReSharper restore InconsistentNaming
{ {
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value; var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
__result = attribute.GetValue(); __result = attribute.GetValue();
@@ -2,9 +2,7 @@
namespace MiraAPI.GameOptions; namespace MiraAPI.GameOptions;
#pragma warning disable CA1852
internal class ModifierOptionGroup : AbstractOptionGroup internal class ModifierOptionGroup : AbstractOptionGroup
#pragma warning restore CA1852
{ {
public override string GroupName { get; } public override string GroupName { get; }
@@ -18,7 +18,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// <summary> /// <summary>
/// Gets the string values of the enum. /// Gets the string values of the enum.
/// </summary> /// </summary>
public string[]? Values { get; } public string[] Values { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModdedEnumOption"/> class. /// Initializes a new instance of the <see cref="ModdedEnumOption"/> class.
@@ -27,7 +27,8 @@ public class ModdedEnumOption : ModdedOption<int>
/// <param name="defaultValue">The default value as an int.</param> /// <param name="defaultValue">The default value as an int.</param>
/// <param name="enumType">The Enum type.</param> /// <param name="enumType">The Enum type.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param> /// <param name="values">An option list of string values to use in place of the enum name.</param>
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null) : base(title, defaultValue) /// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{ {
Values = values ?? Enum.GetNames(enumType); Values = values ?? Enum.GetNames(enumType);
Data = ScriptableObject.CreateInstance<StringGameSetting>(); Data = ScriptableObject.CreateInstance<StringGameSetting>();
@@ -37,7 +38,7 @@ public class ModdedEnumOption : ModdedOption<int>
data.Type = global::OptionTypes.String; data.Type = global::OptionTypes.String;
data.Values = values is null ? data.Values = values is null ?
Enum.GetNames(enumType).Select(CustomStringName.CreateAndRegister).ToArray() Enum.GetNames(enumType).Select(CustomStringName.CreateAndRegister).ToArray()
: values.Select(CustomStringName.CreateAndRegister).ToArray(); : [.. values.Select(CustomStringName.CreateAndRegister)];
data.Index = Value; data.Index = Value;
} }
@@ -87,7 +88,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// <inheritdoc /> /// <inheritdoc />
protected override void OnValueChanged(int newValue) protected override void OnValueChanged(int newValue)
{ {
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data?.GetValueString(newValue), false); HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
if (!OptionBehaviour) if (!OptionBehaviour)
{ {
return; return;
@@ -109,7 +110,7 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
/// <summary> /// <summary>
/// Gets the string values of the enum. /// Gets the string values of the enum.
/// </summary> /// </summary>
public string[]? Values { get; } public string[] Values { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModdedEnumOption{T}"/> class. /// Initializes a new instance of the <see cref="ModdedEnumOption{T}"/> class.
@@ -117,7 +118,8 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
/// <param name="title">The title of the option.</param> /// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as an int.</param> /// <param name="defaultValue">The default value as an int.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param> /// <param name="values">An option list of string values to use in place of the enum name.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue) /// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{ {
Values = values ?? Enum.GetNames(typeof(T)); Values = values ?? Enum.GetNames(typeof(T));
Data = ScriptableObject.CreateInstance<StringGameSetting>(); Data = ScriptableObject.CreateInstance<StringGameSetting>();
@@ -127,7 +129,7 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
data.Type = global::OptionTypes.String; data.Type = global::OptionTypes.String;
data.Values = values is null ? data.Values = values is null ?
Enum.GetNames(typeof(T)).Select(CustomStringName.CreateAndRegister).ToArray() Enum.GetNames(typeof(T)).Select(CustomStringName.CreateAndRegister).ToArray()
: values.Select(CustomStringName.CreateAndRegister).ToArray(); : [.. values.Select(CustomStringName.CreateAndRegister)];
data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo); data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo);
} }
@@ -48,6 +48,7 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="suffixType">The suffix type.</param> /// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param> /// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param> /// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedNumberOption( public ModdedNumberOption(
string title, string title,
float defaultValue, float defaultValue,
@@ -56,7 +57,8 @@ public class ModdedNumberOption : ModdedOption<float>
float increment, float increment,
MiraNumberSuffixes suffixType, MiraNumberSuffixes suffixType,
string? formatString = null, string? formatString = null,
bool zeroInfinity = false) : base(title, defaultValue) bool zeroInfinity = false,
bool includeInPreset = true) : base(title, defaultValue, includeInPreset)
{ {
Min = min; Min = min;
Max = max; Max = max;
+53 -47
View File
@@ -4,6 +4,7 @@ using MiraAPI.Networking;
using MiraAPI.PluginLoading; using MiraAPI.PluginLoading;
using Reactor.Localization.Utilities; using Reactor.Localization.Utilities;
using Reactor.Networking.Rpc; using Reactor.Networking.Rpc;
using Reactor.Utilities;
using UnityEngine; using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes; namespace MiraAPI.GameOptions.OptionTypes;
@@ -16,29 +17,19 @@ public abstract class ModdedOption<T> : IModdedOption
{ {
private IMiraPlugin? _parentMod; private IMiraPlugin? _parentMod;
/// <summary> /// <inheritdoc />
/// Gets the unique identifier of the option.
/// </summary>
public uint Id { get; } public uint Id { get; }
/// <summary> /// <inheritdoc />
/// Gets the title of the option.
/// </summary>
public string Title { get; } public string Title { get; }
/// <summary> /// <inheritdoc />
/// Gets the StringName object of the option.
/// </summary>
public StringNames StringName { get; } public StringNames StringName { get; }
/// <summary> /// <inheritdoc />
/// Gets or sets the BaseGameSetting data of the option.
/// </summary>
public BaseGameSetting Data { get; protected set; } = null!; public BaseGameSetting Data { get; protected set; } = null!;
/// <summary> /// <inheritdoc />
/// Gets or sets the parent mod of the option.
/// </summary>
public IMiraPlugin? ParentMod public IMiraPlugin? ParentMod
{ {
get => _parentMod; get => _parentMod;
@@ -66,19 +57,16 @@ public abstract class ModdedOption<T> : IModdedOption
/// </summary> /// </summary>
public Action<T>? ChangedEvent { get; set; } public Action<T>? ChangedEvent { get; set; }
/// <summary> /// <inheritdoc />
/// Gets or sets the visibility of the option.
/// </summary>
public Func<bool> Visible { get; set; } public Func<bool> Visible { get; set; }
/// <summary> /// <inheritdoc />
/// Gets or sets the option behaviour of the option. public bool IncludeInPreset { get; set; }
/// </summary>
/// <inheritdoc />
public OptionBehaviour? OptionBehaviour { get; protected set; } public OptionBehaviour? OptionBehaviour { get; protected set; }
/// <summary> /// <inheritdoc />
/// Gets or sets the config definition of the option.
/// </summary>
public ConfigDefinition? ConfigDefinition { get; set; } public ConfigDefinition? ConfigDefinition { get; set; }
/// <summary> /// <summary>
@@ -86,7 +74,8 @@ public abstract class ModdedOption<T> : IModdedOption
/// </summary> /// </summary>
/// <param name="title">The option title.</param> /// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param> /// <param name="defaultValue">The default value.</param>
protected ModdedOption(string title, T defaultValue) /// <param name="includeInPreset">Whether to include the option in the preset.</param>
protected ModdedOption(string title, T defaultValue, bool includeInPreset = true)
{ {
Id = ModdedOptionsManager.NextId; Id = ModdedOptionsManager.NextId;
Title = title; Title = title;
@@ -94,6 +83,7 @@ public abstract class ModdedOption<T> : IModdedOption
Value = defaultValue; Value = defaultValue;
StringName = CustomStringName.CreateAndRegister(Title); StringName = CustomStringName.CreateAndRegister(Title);
Visible = () => true; Visible = () => true;
IncludeInPreset = includeInPreset;
} }
internal void ValueChanged(OptionBehaviour optionBehaviour) internal void ValueChanged(OptionBehaviour optionBehaviour)
@@ -105,7 +95,8 @@ public abstract class ModdedOption<T> : IModdedOption
/// Sets the value of the option. /// Sets the value of the option.
/// </summary> /// </summary>
/// <param name="newValue">The new value.</param> /// <param name="newValue">The new value.</param>
public void SetValue(T newValue) /// <param name="sendRpc">Whether to send the value to other players.</param>
public void SetValue(T newValue, bool sendRpc = true)
{ {
var oldVal = Value; var oldVal = Value;
Value = newValue; Value = newValue;
@@ -115,7 +106,7 @@ public abstract class ModdedOption<T> : IModdedOption
ChangedEvent?.Invoke(Value); ChangedEvent?.Invoke(Value);
} }
if (AmongUsClient.Instance.AmHost) if (sendRpc && AmongUsClient.Instance.AmHost)
{ {
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true) if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
{ {
@@ -128,22 +119,44 @@ public abstract class ModdedOption<T> : IModdedOption
OnValueChanged(newValue); OnValueChanged(newValue);
} }
/// <summary> /// <inheritdoc />
/// Gets the float data of the option. public void SaveToPreset(ConfigFile presetConfig, bool saveDefault=false)
/// </summary> {
/// <returns>A float object representing the option's value.</returns> if (ConfigDefinition is null)
{
Logger<MiraApiPlugin>.Error($"Attempted to save {Title} to preset, but ConfigDefinition is null.");
return;
}
Bind(presetConfig);
presetConfig[ConfigDefinition].BoxedValue = saveDefault ? DefaultValue : Value;
}
/// <inheritdoc />
public void Bind(ConfigFile config)
{
config.Bind(ConfigDefinition, DefaultValue);
}
/// <inheritdoc />
public void LoadFromPreset(ConfigFile presetConfig)
{
if (presetConfig.TryGetEntry(ConfigDefinition, out ConfigEntry<T> entry))
{
SetValue(entry.Value, false);
}
else
{
Logger<MiraApiPlugin>.Error($"Attempted to load {Title} from preset, but ConfigDefinition is not found in preset.");
}
}
/// <inheritdoc />
public abstract float GetFloatData(); public abstract float GetFloatData();
/// <summary> /// <inheritdoc />
/// Gets the net data of the option.
/// </summary>
/// <returns>A NetData object representing this option's data.</returns>
public abstract NetData GetNetData(); public abstract NetData GetNetData();
/// <summary> /// <inheritdoc />
/// Handles incoming net data.
/// </summary>
/// <param name="data">The NetData's byte array.</param>
public abstract void HandleNetData(byte[] data); public abstract void HandleNetData(byte[] data);
/// <summary> /// <summary>
@@ -159,14 +172,7 @@ public abstract class ModdedOption<T> : IModdedOption
/// <returns>The value.</returns> /// <returns>The value.</returns>
public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour); public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour);
/// <summary> /// <inheritdoc />
/// Creates the option behaviour.
/// </summary>
/// <param name="toggleOpt">The ToggleOption prefab.</param>
/// <param name="numberOpt">The NumberOption prefab.</param>
/// <param name="stringOpt">The StringOption prefab.</param>
/// <param name="container">The options container.</param>
/// <returns>A new OptionBehaviour for this modded option.</returns>
public abstract OptionBehaviour CreateOption( public abstract OptionBehaviour CreateOption(
ToggleOption toggleOpt, ToggleOption toggleOpt,
NumberOption numberOpt, NumberOption numberOpt,
@@ -15,7 +15,8 @@ public class ModdedToggleOption : ModdedOption<bool>
/// </summary> /// </summary>
/// <param name="title">The option title.</param> /// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param> /// <param name="defaultValue">The default value.</param>
public ModdedToggleOption(string title, bool defaultValue) : base(title, defaultValue) /// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedToggleOption(string title, bool defaultValue, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{ {
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>(); Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
+7 -7
View File
@@ -180,7 +180,7 @@ public abstract class CustomActionButton
if (!moveButton) return; if (!moveButton) return;
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null) if (HudManagerPatches._bottomLeft == null || HudManagerPatches._bottomRight == null)
{ {
return; return;
} }
@@ -188,17 +188,17 @@ public abstract class CustomActionButton
switch (location) switch (location)
{ {
case ButtonLocation.BottomLeft: case ButtonLocation.BottomLeft:
var gridArrange = HudManagerPatches.BottomLeft.GetComponent<GridArrange>(); var gridArrange = HudManagerPatches._bottomLeft.GetComponent<GridArrange>();
var aspectPosition = HudManagerPatches.BottomLeft.GetComponent<AspectPosition>(); var aspectPosition = HudManagerPatches._bottomLeft.GetComponent<AspectPosition>();
Button.transform.SetParent(HudManagerPatches.BottomLeft.transform); Button.transform.SetParent(HudManagerPatches._bottomLeft.transform);
gridArrange.Start(); gridArrange.Start();
gridArrange.ArrangeChilds(); gridArrange.ArrangeChilds();
aspectPosition.AdjustPosition(); aspectPosition.AdjustPosition();
break; break;
case ButtonLocation.BottomRight: case ButtonLocation.BottomRight:
Button.transform.SetParent(HudManagerPatches.BottomRight); Button.transform.SetParent(HudManagerPatches._bottomRight);
break; break;
} }
} }
@@ -435,11 +435,11 @@ public abstract class CustomActionButton
{ {
if (CanUse()) if (CanUse())
{ {
Button!.SetEnabled(); Button.SetEnabled();
} }
else else
{ {
Button!.SetDisabled(); Button.SetDisabled();
} }
if (EffectActive) if (EffectActive)
+1 -3
View File
@@ -29,11 +29,9 @@ public static class CustomButtonManager
} }
CustomButtons.Add(button); CustomButtons.Add(button);
pluginInfo.Buttons.Add(button); pluginInfo.InternalButtons.Add(button);
typeof(CustomButtonSingleton<>).MakeGenericType(buttonType) typeof(CustomButtonSingleton<>).MakeGenericType(buttonType)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, button); .SetValue(null, button);
ButtonEventTypes.Add(buttonType, typeof(MiraButtonClickEvent<>).MakeGenericType(buttonType)); ButtonEventTypes.Add(buttonType, typeof(MiraButtonClickEvent<>).MakeGenericType(buttonType));
+3 -3
View File
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Linq;
using System.Linq;
namespace MiraAPI.Hud; namespace MiraAPI.Hud;
@@ -14,6 +13,7 @@ public static class CustomButtonSingleton<T> where T : CustomActionButton
/// <summary> /// <summary>
/// Gets the instance of the button. /// Gets the instance of the button.
/// </summary> /// </summary>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a custom action button.")] #pragma warning disable CA1000
public static T Instance => _instance ??= CustomButtonManager.CustomButtons.OfType<T>().Single(); public static T Instance => _instance ??= CustomButtonManager.CustomButtons.OfType<T>().Single();
#pragma warning restore CA1000
} }
+114
View File
@@ -0,0 +1,114 @@
using System;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace MiraAPI.Hud;
/// <summary>
/// Simple input box for user input.
/// </summary>
[RegisterInIl2Cpp]
public class InputBox : MonoBehaviour
{
public InputField inputField = null!;
public void CreateDialog(string title, string placeholder, string submitText, Action<string> onSubmit)
{
var canvasObj = new GameObject("DialogCanvas");
canvasObj.transform.SetParent(transform);
canvasObj.layer = LayerMask.NameToLayer("UI");
var canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.overrideSorting = true;
canvas.sortingOrder = 1000; // Ensure the canvas is on top of other UI elements
canvasObj.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
canvasObj.AddComponent<GraphicRaycaster>();
var panelObj = new GameObject("DialogPanel");
panelObj.transform.SetParent(canvasObj.transform);
var panelRect = panelObj.AddComponent<RectTransform>();
var img = panelObj.AddComponent<Image>();
img.color = new Color(1, 1, 1, 0.9f);
img.raycastTarget = true;
img.sprite = MiraAssets.RoundedBox.LoadAsset();
img.type = Image.Type.Sliced;
panelRect.sizeDelta = new Vector2(300, 200);
panelRect.anchoredPosition = Vector2.zero;
var titleText = CreateText(panelObj, title);
titleText.alignment = TextAnchor.MiddleCenter;
titleText.fontSize = 26;
titleText.fontStyle = FontStyle.Bold;
titleText.color = Color.white;
var titleRect = titleText.GetComponent<RectTransform>();
titleRect.sizeDelta = new Vector2(280, 50);
titleRect.anchoredPosition = new Vector2(0, 70);
var inputObj = new GameObject("InputField");
inputObj.transform.SetParent(panelObj.transform);
var inputRect = inputObj.AddComponent<RectTransform>();
inputRect.sizeDelta = new Vector2(220, 40);
inputRect.anchoredPosition = new Vector2(0, 20);
var inputBg = inputObj.AddComponent<Image>();
inputBg.color = new Color(1, 1, 1, 0.8f);
inputField = inputObj.AddComponent<InputField>();
inputField.textComponent = CreateText(inputObj, string.Empty, 18, Color.black);
inputField.placeholder = CreateText(inputObj, placeholder, 18, new Color(0.5f, 0.5f, 0.5f));
var buttonObj = new GameObject("SubmitButton");
buttonObj.transform.SetParent(panelObj.transform);
var button = buttonObj.AddComponent<Button>();
var buttonRect = buttonObj.AddComponent<RectTransform>();
buttonRect.sizeDelta = new Vector2(150, 50);
buttonRect.anchoredPosition = new Vector2(0, -50);
var buttonImg = buttonObj.AddComponent<Image>();
buttonImg.color = new Color(1f, 1f, 1f, 1f);
buttonImg.sprite = MiraAssets.RoundedBox.LoadAsset();
buttonImg.type = Image.Type.Sliced;
var buttonText = CreateText(buttonObj, submitText, 20, Color.white);
buttonText.alignment = TextAnchor.MiddleCenter;
var colors = button.colors;
colors.highlightedColor = Color.green;
colors.pressedColor = MiraAssets.AcceptedTeal;
button.colors = colors;
button.onClick.AddListener((UnityAction)(() =>
{
onSubmit(inputField.text);
DestroyImmediate(canvasObj);
}));
inputField.ActivateInputField();
}
private static Text CreateText(GameObject parent, string textContent = "", int fontSize = 20, Color? textColor = null, Font? font = null)
{
var textObj = new GameObject("Text");
textObj.transform.SetParent(parent.transform);
var text = textObj.AddComponent<Text>();
text.font = font ?? Resources.GetBuiltinResource<Font>("Arial.ttf");
text.text = textContent;
text.color = textColor ?? Color.white;
text.fontSize = fontSize;
text.alignment = TextAnchor.MiddleCenter;
var rect = textObj.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(200, 40);
rect.anchoredPosition = Vector2.zero;
return text;
}
}
+2 -2
View File
@@ -40,8 +40,8 @@ public abstract class BaseModifier : IOptionable
/// Gets the parent mod of the modifier. /// Gets the parent mod of the modifier.
/// </summary> /// </summary>
public MiraPluginInfo ParentMod => Array.Find( public MiraPluginInfo ParentMod => Array.Find(
MiraPluginManager.Instance.RegisteredPlugins(), MiraPluginManager.Instance.RegisteredPlugins,
x => x.Modifiers.Exists(y => y.TypeId == TypeId) x => x.InternalModifiers.Exists(y => y.TypeId == TypeId)
) ?? throw new InvalidOperationException("Modifier is not registered."); ) ?? throw new InvalidOperationException("Modifier is not registered.");
/// <summary> /// <summary>
+7 -33
View File
@@ -3,11 +3,14 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text;
using Il2CppInterop.Runtime.Attributes; using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers.ModifierDisplay; using MiraAPI.Modifiers.ModifierDisplay;
using MiraAPI.Modifiers.Types; using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities;
using Reactor.Utilities; using Reactor.Utilities;
using Reactor.Utilities.Attributes; using Reactor.Utilities.Attributes;
using TMPro;
using UnityEngine; using UnityEngine;
namespace MiraAPI.Modifiers; namespace MiraAPI.Modifiers;
@@ -69,15 +72,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
foreach (var modifier in _toRemove.ToArray()) foreach (var modifier in _toRemove.ToArray())
{ {
_toRemove.Remove(modifier); _toRemove.Remove(modifier);
try modifier.OnDeactivate();
{
modifier.OnDeactivate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
}
Modifiers.Remove(modifier); Modifiers.Remove(modifier);
} }
@@ -87,14 +82,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_toAdd.Remove(modifier); _toAdd.Remove(modifier);
Modifiers.Add(modifier); Modifiers.Add(modifier);
modifier.Initialized = true; modifier.Initialized = true;
try modifier.OnActivate();
{
modifier.OnActivate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
}
if (modifier is TimedModifier { AutoStart: true } timer) if (modifier is TimedModifier { AutoStart: true } timer)
{ {
@@ -114,14 +102,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
foreach (var modifier in ActiveModifiers) foreach (var modifier in ActiveModifiers)
{ {
try modifier.FixedUpdate();
{
modifier.FixedUpdate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
}
} }
if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists) if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists)
@@ -134,14 +115,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{ {
foreach (var modifier in ActiveModifiers) foreach (var modifier in ActiveModifiers)
{ {
try modifier.Update();
{
modifier.Update();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
}
} }
} }
@@ -17,7 +17,7 @@ namespace MiraAPI.Modifiers.ModifierDisplay;
/// The code used to display Mira modifiers. /// The code used to display Mira modifiers.
/// </summary> /// </summary>
[RegisterInIl2Cpp] [RegisterInIl2Cpp]
public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
{ {
/// <summary> /// <summary>
/// Gets the instance of the Modifier Display. /// Gets the instance of the Modifier Display.
@@ -34,8 +34,8 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
private PassiveButton _nextButton = null!; private PassiveButton _nextButton = null!;
private PassiveButton _backButton = null!; private PassiveButton _backButton = null!;
private int _currentPage; private int _currentPage = 0;
private const int ItemsPerPage = 3; private const int _itemsPerPage = 3;
private readonly Dictionary<BaseModifier, ModifierUiComponent> _modifiers = []; private readonly Dictionary<BaseModifier, ModifierUiComponent> _modifiers = [];
@@ -142,10 +142,10 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
private void UpdatePage() private void UpdatePage()
{ {
var totalPages = Mathf.CeilToInt((float)_modifiers.Count / ItemsPerPage); var totalPages = Mathf.CeilToInt((float)_modifiers.Count / _itemsPerPage);
_currentPage = Mathf.Clamp(_currentPage, 0, Mathf.Max(0, totalPages - 1)); _currentPage = Mathf.Clamp(_currentPage, 0, Mathf.Max(0, totalPages - 1));
if (_modifiers.Count <= ItemsPerPage) if (_modifiers.Count <= _itemsPerPage)
{ {
_modifiers.Do(x => x.Value.gameObject.SetActive(true)); _modifiers.Do(x => x.Value.gameObject.SetActive(true));
_pagination.gameObject.SetActive(false); _pagination.gameObject.SetActive(false);
@@ -162,8 +162,8 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
_backButton.gameObject.SetActive(true); _backButton.gameObject.SetActive(true);
_modifiers.Do(x => x.Value.gameObject.SetActive(false)); _modifiers.Do(x => x.Value.gameObject.SetActive(false));
var start = _currentPage * ItemsPerPage; var start = _currentPage * _itemsPerPage;
var end = Mathf.Min(start + ItemsPerPage, _modifiers.Count); var end = Mathf.Min(start + _itemsPerPage, _modifiers.Count);
var modifiers = _modifiers.ToArray(); var modifiers = _modifiers.ToArray();
for (var i = start; i < end; i++) for (var i = start; i < end; i++)
@@ -9,7 +9,7 @@ namespace MiraAPI.Modifiers.ModifierDisplay;
/// The code placed on every Modifier HUD object. Used to handle updating. /// The code placed on every Modifier HUD object. Used to handle updating.
/// </summary> /// </summary>
[RegisterInIl2Cpp] [RegisterInIl2Cpp]
public class ModifierUiComponent(nint cppPtr) : MonoBehaviour(cppPtr) public class ModifierUiComponent(nint ptr) : MonoBehaviour(ptr)
{ {
/// <summary> /// <summary>
/// Gets the modifier which this component is for. /// Gets the modifier which this component is for.
@@ -17,13 +17,12 @@ public class ModifierUiComponent(nint cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp] [HideFromIl2Cpp]
public BaseModifier? Modifier { get; internal set; } public BaseModifier? Modifier { get; internal set; }
// ReSharper disable InconsistentNaming
private SpriteRenderer modBg = null!; private SpriteRenderer modBg = null!;
private TextMeshPro desc = null!; private TextMeshPro desc = null!;
private SpriteRenderer icon = null!; private SpriteRenderer icon = null!;
private TextMeshPro nameText = null!; private TextMeshPro nameText = null!;
private RectTransform descRect = null!; private RectTransform descRect = null!;
// ReSharper restore InconsistentNaming
private void Awake() private void Awake()
{ {
if (Modifier == null) return; if (Modifier == null) return;
+4 -6
View File
@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using MiraAPI.Utilities; using MiraAPI.Utilities;
@@ -12,7 +11,7 @@ namespace MiraAPI.Modifiers;
/// </summary> /// </summary>
public static class ModifierFactory public static class ModifierFactory
{ {
private static readonly Dictionary<(Type, Type[]), Func<object[], BaseModifier>> ConstructorCache = []; private static readonly Dictionary<(Type, Type[]), Func<object[], BaseModifier>> _constructorCache = [];
private static Func<object[], BaseModifier> CreateConstructor(Type type, params object[] args) private static Func<object[], BaseModifier> CreateConstructor(Type type, params object[] args)
{ {
@@ -46,13 +45,13 @@ public static class ModifierFactory
/// <returns>An instance of the modifier.</returns> /// <returns>An instance of the modifier.</returns>
public static BaseModifier CreateInstance(Type type, params object[] args) public static BaseModifier CreateInstance(Type type, params object[] args)
{ {
var argTypes = args.Select(arg => arg.GetType()).ToArray(); var argTypes = args.Select(arg => arg?.GetType() ?? typeof(object)).ToArray();
var key = (type, argTypes); var key = (type, argTypes);
if (!ConstructorCache.TryGetValue(key, out var constructor)) if (!_constructorCache.TryGetValue(key, out var constructor))
{ {
constructor = CreateConstructor(type, args); constructor = CreateConstructor(type, args);
ConstructorCache[key] = constructor; _constructorCache[key] = constructor;
} }
return constructor(args); return constructor(args);
@@ -96,7 +95,6 @@ public static class ModifierFactory<T> where T : BaseModifier
/// </summary> /// </summary>
/// <param name="args">Parameters for the constructor.</param> /// <param name="args">Parameters for the constructor.</param>
/// <returns>An instance of the modifier.</returns> /// <returns>An instance of the modifier.</returns>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a factory class.")]
public static T CreateInstance(params object[] args) public static T CreateInstance(params object[] args)
{ {
return Constructor(args); return Constructor(args);
+2 -1
View File
@@ -5,6 +5,7 @@ using System.Runtime.Serialization;
using MiraAPI.Modifiers.Types; using MiraAPI.Modifiers.Types;
using MiraAPI.PluginLoading; using MiraAPI.PluginLoading;
using MiraAPI.Roles; using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities; using Reactor.Utilities;
using Reactor.Utilities.Extensions; using Reactor.Utilities.Extensions;
using Random = System.Random; using Random = System.Random;
@@ -74,7 +75,7 @@ public static class ModifierManager
modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType);
} }
info.Modifiers.Add(modifier); info.InternalModifiers.Add(modifier);
if (modifier is not GameModifier gameModifier) if (modifier is not GameModifier gameModifier)
{ {
+1 -1
View File
@@ -264,7 +264,7 @@ public static class CustomMurderRpc
deadBody.enabled = true; deadBody.enabled = true;
} }
var afterMurderEvent = new AfterMurderEvent(source, target, deadBody); var afterMurderEvent = new AfterMurderEvent(source, target);
MiraEventManager.InvokeEvent(afterMurderEvent); MiraEventManager.InvokeEvent(afterMurderEvent);
if (!isParticipant) if (!isParticipant)
+1 -1
View File
@@ -46,7 +46,7 @@ public enum MiraRpc : uint
RemoveVote, RemoveVote,
/// <summary> /// <summary>
/// Custom RPC to replace Among Us CastVote. /// Custom RPC to replace Among Us CastVote
/// </summary> /// </summary>
CastVote, CastVote,
} }
-5
View File
@@ -1,5 +0,0 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CS1591.severity = none
dotnet_diagnostic.SA1611.severity = none
dotnet_diagnostic.SA1615.severity = none
@@ -1,7 +1,6 @@
using HarmonyLib; using HarmonyLib;
using MiraAPI.Events; using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events; namespace MiraAPI.Patches.Events;
@@ -20,7 +19,7 @@ public static class KillAnimationPatches
[HarmonyPatch(typeof(KillAnimation), nameof(KillAnimation.CoPerformKill))] [HarmonyPatch(typeof(KillAnimation), nameof(KillAnimation.CoPerformKill))]
public static void KillAnimPostfix(PlayerControl source, PlayerControl target) public static void KillAnimPostfix(PlayerControl source, PlayerControl target)
{ {
var afterMurderEvent = new AfterMurderEvent(source, target, Helpers.GetBodyById(target.PlayerId)); var afterMurderEvent = new AfterMurderEvent(source, target);
MiraEventManager.InvokeEvent(afterMurderEvent); MiraEventManager.InvokeEvent(afterMurderEvent);
} }
} }
+7 -7
View File
@@ -67,9 +67,9 @@ internal static class TaskAdderPatches
modifiersFolder.FolderName = "Modifiers"; modifiersFolder.FolderName = "Modifiers";
modifiersFolder.name = "ModifiersFolder"; modifiersFolder.name = "ModifiersFolder";
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{ {
if (plugin.CustomRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true })) if (plugin.InternalRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true }))
{ {
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
newFolder.name = plugin.PluginId+":Roles"; newFolder.name = plugin.PluginId+":Roles";
@@ -78,7 +78,7 @@ internal static class TaskAdderPatches
rolesFolder.SubFolders.Add(newFolder); rolesFolder.SubFolders.Add(newFolder);
} }
if (plugin.Modifiers.Exists(x => x.ShowInFreeplay)) if (plugin.InternalModifiers.Exists(x => x.ShowInFreeplay))
{ {
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
newFolder.name = plugin.PluginId+":Modifiers"; newFolder.name = plugin.PluginId+":Modifiers";
@@ -317,9 +317,9 @@ internal static class TaskAdderPatches
ref float num2, ref float num2,
ref float num3) ref float num3)
{ {
for (var m = 0; m < plugin.Modifiers.Count; m++) for (var m = 0; m < plugin.InternalModifiers.Count; m++)
{ {
var modifier = plugin.Modifiers[m]; var modifier = plugin.InternalModifiers[m];
if (!modifier.ShowInFreeplay) if (!modifier.ShowInFreeplay)
{ {
continue; continue;
@@ -368,9 +368,9 @@ internal static class TaskAdderPatches
private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3) private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3)
{ {
for (var m = 0; m < plugin.CustomRoles.Count; m++) for (var m = 0; m < plugin.InternalRoles.Count; m++)
{ {
var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value; var roleBehaviour = plugin.InternalRoles.ElementAt(m).Value;
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false }) roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false })
{ {
+1 -1
View File
@@ -4,7 +4,7 @@ using MiraAPI.Hud;
namespace MiraAPI.Patches.Hud; namespace MiraAPI.Patches.Hud;
/// <summary> /// <summary>
/// Reset button patches. /// Reset button patches
/// </summary> /// </summary>
[HarmonyPatch] [HarmonyPatch]
public static class ButtonResetPatches public static class ButtonResetPatches
+15 -15
View File
@@ -13,9 +13,9 @@ namespace MiraAPI.Patches;
public static class HudManagerPatches public static class HudManagerPatches
{ {
// Custom buttons parent. // Custom buttons parent.
public static GameObject? BottomLeft { get; private set; } internal static GameObject? _bottomLeft;
public static Transform? BottomRight { get; private set; } internal static Transform? _bottomRight;
public static Transform? Buttons { get; private set; } internal static Transform? _buttons;
/* /*
/// <summary> /// <summary>
@@ -36,30 +36,30 @@ public static class HudManagerPatches
[HarmonyPatch(nameof(HudManager.Start))] [HarmonyPatch(nameof(HudManager.Start))]
public static void StartPostfix(HudManager __instance) public static void StartPostfix(HudManager __instance)
{ {
if (Buttons == null) if (_buttons == null)
{ {
Buttons = __instance.transform.Find("Buttons"); _buttons = __instance.transform.Find("Buttons");
} }
if (BottomRight == null) if (_bottomRight == null)
{ {
BottomRight = Buttons.Find("BottomRight"); _bottomRight = _buttons.Find("BottomRight");
} }
if (BottomLeft == null) if (_bottomLeft == null)
{ {
BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons); _bottomLeft = Object.Instantiate(_bottomRight.gameObject, _buttons);
} }
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true)) foreach (var t in _bottomLeft.GetComponentsInChildren<ActionButton>(true))
{ {
t.gameObject.Destroy(); t.gameObject.Destroy();
} }
var gridArrange = BottomLeft.GetComponent<GridArrange>(); var gridArrange = _bottomLeft.GetComponent<GridArrange>();
var aspectPosition = BottomLeft.GetComponent<AspectPosition>(); var aspectPosition = _bottomLeft.GetComponent<AspectPosition>();
BottomLeft.name = "BottomLeft"; _bottomLeft.name = "BottomLeft";
gridArrange.Alignment = GridArrange.StartAlign.Right; gridArrange.Alignment = GridArrange.StartAlign.Right;
aspectPosition.Alignment = AspectPosition.EdgeAlignments.LeftBottom; aspectPosition.Alignment = AspectPosition.EdgeAlignments.LeftBottom;
@@ -67,8 +67,8 @@ public static class HudManagerPatches
{ {
var location = button.Location switch var location = button.Location switch
{ {
ButtonLocation.BottomLeft => BottomLeft.transform, ButtonLocation.BottomLeft => _bottomLeft.transform,
ButtonLocation.BottomRight => BottomRight, ButtonLocation.BottomRight => _bottomRight,
_ => null, _ => null,
}; };
+1 -1
View File
@@ -108,7 +108,7 @@ public static class NameplatesTabPatches
var x = (NamePlateViewData viewdata) => var x = (NamePlateViewData viewdata) =>
{ {
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewdata?.Image; colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewdata?.Image;
// (colorChip as NameplateChip).image.sprite = viewdata?.Image; //(colorChip as NameplateChip).image.sprite = viewdata?.Image;
}; };
__instance.StartCoroutine(AddressableAssetExtensions.CoLoadAssetAsync<NamePlateViewData>(__instance, namePlate.GetAssetReference(), x)); __instance.StartCoroutine(AddressableAssetExtensions.CoLoadAssetAsync<NamePlateViewData>(__instance, namePlate.GetAssetReference(), x));
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f); colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
+1 -1
View File
@@ -3,7 +3,7 @@
namespace MiraAPI.Patches; namespace MiraAPI.Patches;
/// <summary> /// <summary>
/// Used to fix the Minigame crash errors (credits Submerged/PH Gaming for the fix). /// Used to fix the Minigame crash errors (credits Submerged/PH Gaming for the fix)
/// </summary> /// </summary>
[HarmonyPatch] [HarmonyPatch]
public static class MinigameBeginErrorPatch public static class MinigameBeginErrorPatch
@@ -14,18 +14,12 @@ using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options; namespace MiraAPI.Patches.Options;
/// <summary>
/// Patches the GameOptionsMenu to add custom options.
/// </summary>
[HarmonyPatch(typeof(GameOptionsMenu))] [HarmonyPatch(typeof(GameOptionsMenu))]
public static class GameOptionsMenuPatch internal static class GameOptionsMenuPatch
{ {
/// <summary>
/// Update patch for the GameOptionsMenu.
/// </summary>
/// <param name="__instance">The GameOptionsMenu instance.</param>
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.Update))] [HarmonyPatch(nameof(GameOptionsMenu.Update))]
// ReSharper disable once InconsistentNaming
public static void UpdatePatch(GameOptionsMenu __instance) public static void UpdatePatch(GameOptionsMenu __instance)
{ {
if (GameSettingMenuPatches.SelectedModIdx == 0) if (GameSettingMenuPatches.SelectedModIdx == 0)
@@ -41,7 +35,7 @@ public static class GameOptionsMenuPatch
else else
{ {
var filteredGroups = var filteredGroups =
GameSettingMenuPatches.SelectedMod?.OptionGroups GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? []; .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
foreach (var group in filteredGroups) foreach (var group in filteredGroups)
@@ -107,7 +101,7 @@ public static class GameOptionsMenuPatch
private static void ModifiersUpdate(ref float num) private static void ModifiersUpdate(ref float num)
{ {
var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
foreach (var modGroup in groups) foreach (var modGroup in groups)
@@ -118,7 +112,7 @@ public static class GameOptionsMenuPatch
private static void ModifiersCreate(GameOptionsMenu menu) private static void ModifiersCreate(GameOptionsMenu menu)
{ {
var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
foreach (var group in groups) foreach (var group in groups)
{ {
@@ -128,6 +122,7 @@ public static class GameOptionsMenuPatch
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))] [HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))]
// ReSharper disable once InconsistentNaming
public static bool SettingsPatch(GameOptionsMenu __instance) public static bool SettingsPatch(GameOptionsMenu __instance)
{ {
if (GameSettingMenuPatches.SelectedModIdx == 0) if (GameSettingMenuPatches.SelectedModIdx == 0)
@@ -143,7 +138,7 @@ public static class GameOptionsMenuPatch
return false; return false;
} }
var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? []; .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
foreach (var group in filteredGroups) foreach (var group in filteredGroups)
@@ -156,6 +151,7 @@ public static class GameOptionsMenuPatch
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch(nameof(GameOptionsMenu.Initialize))] [HarmonyPatch(nameof(GameOptionsMenu.Initialize))]
// ReSharper disable once InconsistentNaming
public static bool InitPatch(GameOptionsMenu __instance) public static bool InitPatch(GameOptionsMenu __instance)
{ {
if (__instance.Children != null && __instance.Children.Count != 0) if (__instance.Children != null && __instance.Children.Count != 0)
@@ -0,0 +1,348 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
internal static class GamePresetsTabPatches
{
private static GameObject _saveButton = null!;
private static GameObject _refreshButton = null!;
private static GameObject _folderButton = null!;
private static GameObject _newDivider = null!;
private static GameObject _presetHolder = null!;
private static GridArrange _arrange = null!;
private static readonly object Lock = new();
public static void DivideSize(GameObject obj, float amount)
{
foreach (var collider in obj.GetComponentsInChildren<Collider2D>(true))
{
if (collider.TryCast<BoxCollider2D>() is { } col)
{
col.size = new Vector2(col.size.x / amount, col.size.y);
}
}
foreach (var rend in obj.GetComponentsInChildren<SpriteRenderer>(true))
{
rend.size = new Vector2(rend.size.x / amount, rend.size.y);
}
}
[HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnEnable))]
public static class GamePresetsOnEnablePatch
{
// ReSharper disable once InconsistentNaming
public static void Postfix(GamePresetsTab __instance)
{
if (!GameSettingMenu.Instance)
{
return;
}
CreatePresetMenu(__instance);
__instance.StandardPresetButton.gameObject.SetActive(GameSettingMenuPatches.SelectedModIdx == 0);
__instance.SecondPresetButton.gameObject.SetActive(GameSettingMenuPatches.SelectedModIdx == 0);
if (GameSettingMenuPatches.SelectedModIdx != 0)
{
__instance.PresetDescriptionText.text = "Select or create an options preset for this mod";
}
else
{
__instance.SetSelectedText();
}
// Show the save and refresh buttons
if (_saveButton)
{
_saveButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
}
if (_refreshButton)
{
_refreshButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
}
if (_folderButton)
{
_folderButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
}
if (_newDivider)
{
_newDivider.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
}
var prefab = GameSettingMenu.Instance.GameSettingsButton;
foreach (var preset in MiraPluginManager.Instance.RegisteredPlugins.SelectMany(x => x.Presets))
{
var button = Object.Instantiate(prefab, _presetHolder.transform);
button.buttonText.text = preset.Name;
if (button.buttonText.TryGetComponent<TextTranslatorTMP>(out var tmp))
{
tmp.DestroyImmediate();
}
button.buttonText.alignment = TextAlignmentOptions.Center;
button.OnClick = new Button.ButtonClickedEvent();
button.OnClick.AddListener(
(UnityAction)(() =>
{
preset.LoadPreset();
}));
button.gameObject.SetActive(false);
preset.PresetButton = button.gameObject;
}
foreach (var mod in MiraPluginManager.Instance.RegisteredPlugins)
{
foreach (var button in mod.Presets.Select(x => x.PresetButton))
{
if (button != null)
{
button.SetActive(mod == GameSettingMenuPatches.SelectedMod);
}
}
}
_arrange.Start();
_arrange.ArrangeChilds();
}
}
[HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnDisable))]
public static class GamePresetsOnDisablePatch
{
// ReSharper disable once InconsistentNaming
public static void Postfix()
{
Logger<MiraApiPlugin>.Error("OnDisable called");
if (_saveButton)
{
_saveButton.SetActive(false);
}
if (_refreshButton)
{
_refreshButton.SetActive(false);
}
if (_folderButton)
{
_folderButton.SetActive(false);
}
if (_newDivider)
{
_newDivider.SetActive(false);
}
foreach (var mod in MiraPluginManager.Instance.RegisteredPlugins)
{
foreach (var button in mod.Presets.Select(x => x.PresetButton))
{
if (button != null)
{
button.SetActive(false);
}
}
}
}
}
public static void CreatePresetMenu(GamePresetsTab presetTab)
{
var prefab = GameSettingMenu.Instance.GameSettingsButton;
if (prefab == null)
{
Logger<MiraApiPlugin>.Error("GameSettingsButton prefab is null");
return;
}
var tmpTranslator = presetTab.PresetDescriptionText.gameObject.GetComponent<TextTranslatorTMP>();
if (tmpTranslator)
{
tmpTranslator.Destroy();
}
if (!_newDivider)
{
var oldDiv = presetTab.transform.FindChild("DividerImage");
_newDivider = Object.Instantiate(oldDiv.gameObject, oldDiv.transform.parent);
_newDivider.transform.localPosition = new Vector3(1.85f, 0.13f, 0f);
_newDivider.transform.localScale = new Vector3(1.13f, 1, 1);
_newDivider.transform.localRotation = Quaternion.Euler(new Vector3(0, 0, 90));
}
PassiveButton saveButton;
if (!_saveButton)
{
saveButton = Object.Instantiate(prefab, presetTab.transform);
_saveButton = saveButton.gameObject;
_saveButton.gameObject.name = "SaveButton";
_saveButton.transform.localPosition = new Vector3(3.2f, 1.7f, -2);
// set the button text and alignment
var saveText = saveButton.buttonText;
saveText.text = "Save";
saveText.GetComponent<TextTranslatorTMP>().Destroy();
saveText.alignment = TextAlignmentOptions.Center;
saveText.transform.parent.localPosition = new Vector3(
-.525f,
saveText.transform.parent.localPosition.y,
saveText.transform.parent.localPosition.z);
DivideSize(saveButton.gameObject, 2f);
saveButton.OnClick = new Button.ButtonClickedEvent();
saveButton.OnClick.AddListener(
(UnityAction)(() =>
{
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
return;
}
SavePresetPopup.CreatePopup(
name =>
{
if (GameSettingMenuPatches.SelectedMod == null)
{
return;
}
if (name == string.Empty)
{
return;
}
var presetFile = new ConfigFile(
Path.Combine(
PresetManager.PresetDirectory,
GameSettingMenuPatches.SelectedMod.PluginId,
$"{name}.cfg"),
false)
{
SaveOnConfigSet = false,
};
foreach (var option in GameSettingMenuPatches.SelectedMod.InternalOptions.Where(x => x.IncludeInPreset))
{
option.SaveToPreset(presetFile);
}
foreach (var role in GameSettingMenuPatches.SelectedMod.InternalRoles.Values.OfType<ICustomRole>().Where(x=>!x.Configuration.HideSettings))
{
role.SaveToPreset(presetFile);
}
presetFile.Save();
Refresh();
});
}));
}
saveButton = _saveButton.GetComponent<PassiveButton>();
PassiveButton refreshButton;
if (!_refreshButton)
{
refreshButton = Object.Instantiate(saveButton, presetTab.transform);
_refreshButton = refreshButton.gameObject;
_refreshButton.name = "RefreshButton";
_refreshButton.transform.localPosition = new Vector3(2.85f, 1.1f, -2);
var icon = new GameObject("Sprite");
icon.transform.SetParent(refreshButton.transform);
icon.transform.localPosition = new Vector3(-0.47f, -0.08f, -5f);
icon.transform.localScale = new Vector3(0.5f, 0.5f, 1);
icon.layer = refreshButton.gameObject.layer;
var iconSpriteRend = icon.AddComponent<SpriteRenderer>();
iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset();
DivideSize(refreshButton.gameObject, 2f);
refreshButton.buttonText.gameObject.Destroy();
refreshButton.OnClick = new Button.ButtonClickedEvent();
refreshButton.OnClick.AddListener((UnityAction)Refresh);
}
refreshButton = _refreshButton.GetComponent<PassiveButton>();
if (!_folderButton)
{
var openFolderButton = Object.Instantiate(refreshButton, presetTab.transform);
_folderButton = openFolderButton.gameObject;
_folderButton.name = "OpenFolderButton";
_folderButton.transform.localPosition = new Vector3(3.55f, 1.1f, -2);
var folderRend = openFolderButton.transform.FindChild("Sprite").gameObject.GetComponent<SpriteRenderer>();
folderRend.sprite = MiraAssets.FolderIcon.LoadAsset();
folderRend.transform.localScale = new Vector3(0.4f, 0.4f, 1);
openFolderButton.OnClick = new Button.ButtonClickedEvent();
openFolderButton.OnClick.AddListener(
(UnityAction)(() =>
{
var directory = Path.Combine(
PresetManager.PresetDirectory,
GameSettingMenuPatches.SelectedMod?.PluginId ?? string.Empty);
Directory.CreateDirectory(directory);
Process.Start(
new ProcessStartInfo
{
FileName = directory,
UseShellExecute = true,
Verb = "open",
});
}));
}
_saveButton.SetActive(false);
_refreshButton.SetActive(false);
_folderButton.SetActive(false);
_newDivider.SetActive(false);
if (!_presetHolder)
{
_presetHolder = new GameObject("PresetHolder");
_presetHolder.transform.SetParent(presetTab.transform, false);
_presetHolder.transform.localScale = new Vector3(0.9f, 0.9f, 1);
_presetHolder.transform.localPosition = new Vector3(-1.9f, 1.7f, 0);
_arrange = _presetHolder.AddComponent<GridArrange>();
_arrange.Alignment = GridArrange.StartAlign.Right;
_arrange.CellSize = new Vector2(2.15f, -.55f);
_arrange.MaxColumns = 2;
}
}
private static void Refresh()
{
Logger<MiraApiPlugin>.Error("Refreshing presets");
if (GameSettingMenuPatches.SelectedMod == null)
{
Logger<MiraApiPlugin>.Error("Selected mod is null, cannot refresh presets");
return;
}
lock (Lock)
{
PresetManager.LoadPresets(GameSettingMenuPatches.SelectedMod);
}
GameSettingMenu.Instance.ChangeTab(0, false); // refresh the tab
}
}
+107 -76
View File
@@ -4,6 +4,7 @@ using System.Linq;
using HarmonyLib; using HarmonyLib;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.PluginLoading; using MiraAPI.PluginLoading;
using MiraAPI.Presets;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions; using Reactor.Utilities.Extensions;
using TMPro; using TMPro;
@@ -31,15 +32,20 @@ internal static class GameSettingMenuPatches
private static PassiveButton? _modifiersButton; private static PassiveButton? _modifiersButton;
private static PassiveButton? _smallRolesButton; private static PassiveButton? _smallRolesButton;
private static Dictionary<int, Vector3> OptionsPositions { get; } = []; private static Dictionary<int, Vector3> OptionScrolls = [];
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))] [HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
// ReSharper disable once InconsistentNaming
public static void ChangeTabPostfix(GameSettingMenu __instance, int tabNum, bool previewOnly) public static void ChangeTabPostfix(GameSettingMenu __instance, int tabNum, bool previewOnly)
{ {
if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly) if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly)
{ {
if (_modifiersTab) _modifiersTab!.gameObject.SetActive(tabNum == 3); if (_modifiersTab)
{
_modifiersTab!.gameObject.SetActive(tabNum == 3);
}
_modifiersButton?.SelectButton(tabNum == 3); _modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2); _smallRolesButton?.SelectButton(tabNum == 2);
if (tabNum == 3) if (tabNum == 3)
@@ -63,7 +69,10 @@ internal static class GameSettingMenuPatches
_modifiersButton?.SelectButton(tabNum == 3); _modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2); _smallRolesButton?.SelectButton(tabNum == 2);
if (tabNum != 3) return; if (tabNum != 3)
{
return;
}
if (__instance.RoleSettingsButton.gameObject.active) if (__instance.RoleSettingsButton.gameObject.active)
{ {
@@ -81,6 +90,7 @@ internal static class GameSettingMenuPatches
/// <param name="__instance">The GameSettingMenu instance.</param> /// <param name="__instance">The GameSettingMenu instance.</param>
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch(nameof(GameSettingMenu.Start))] [HarmonyPatch(nameof(GameSettingMenu.Start))]
// ReSharper disable once InconsistentNaming
public static void StartPrefix(GameSettingMenu __instance) public static void StartPrefix(GameSettingMenu __instance)
{ {
_roleBtnOgPos = __instance.RoleSettingsButton.transform.localPosition; _roleBtnOgPos = __instance.RoleSettingsButton.transform.localPosition;
@@ -113,13 +123,13 @@ internal static class GameSettingMenuPatches
passiveButton.OnClick.AddListener( passiveButton.OnClick.AddListener(
(UnityAction)(() => (UnityAction)(() =>
{ {
if (!__instance.RoleSettingsTab.AdvancedRolesSettings.active) RoleSettingMenuPatches.Scrolls[SelectedModIdx] =
{ __instance.RoleSettingsTab.scrollBar.Inner.localPosition;
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = __instance.RoleSettingsTab.scrollBar.Inner.localPosition; OptionScrolls[SelectedModIdx] =
} __instance.GameSettingsTab.scrollBar.Inner.localPosition;
OptionsPositions[SelectedModIdx] = __instance.GameSettingsTab.scrollBar.Inner.localPosition;
SelectedModIdx += 1; SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length)
{ {
SelectedModIdx = 0; SelectedModIdx = 0;
} }
@@ -136,15 +146,15 @@ internal static class GameSettingMenuPatches
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener( backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
(UnityAction)(() => (UnityAction)(() =>
{ {
if (!__instance.RoleSettingsTab.AdvancedRolesSettings.active) RoleSettingMenuPatches.Scrolls[SelectedModIdx] =
{ __instance.RoleSettingsTab.scrollBar.Inner.localPosition;
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = __instance.RoleSettingsTab.scrollBar.Inner.localPosition; OptionScrolls[SelectedModIdx] =
} __instance.GameSettingsTab.scrollBar.Inner.localPosition;
OptionsPositions[SelectedModIdx] = __instance.GameSettingsTab.scrollBar.Inner.localPosition;
SelectedModIdx -= 1; SelectedModIdx -= 1;
if (SelectedModIdx < 0) if (SelectedModIdx < 0)
{ {
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins.Length;
} }
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
@@ -221,54 +231,61 @@ internal static class GameSettingMenuPatches
_modifierBtnOgPos = _modifiersButton.transform.localPosition; _modifierBtnOgPos = _modifiersButton.transform.localPosition;
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
PresetManager.LoadPresets(plugin);
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
} }
private static void ChangeRoleSettingButton(bool replace, GameSettingMenu __instance) private static void ChangeRoleSettingButton(bool replace, GameSettingMenu menu)
{ {
__instance.RoleSettingsButton.OnClick = new ButtonClickedEvent(); menu.RoleSettingsButton.OnClick = new ButtonClickedEvent();
__instance.RoleSettingsButton.OnMouseOver = new UnityEvent(); menu.RoleSettingsButton.OnMouseOver = new UnityEvent();
if (replace) if (replace)
{ {
__instance.RoleSettingsButton.buttonText.text = "Modifiers Settings"; menu.RoleSettingsButton.buttonText.text = "Modifiers Settings";
__instance.RoleSettingsButton.OnClick.AddListener( menu.RoleSettingsButton.OnClick.AddListener(
(UnityAction)(() => (UnityAction)(() =>
{ {
__instance.ChangeTab(3, false); menu.ChangeTab(3, false);
})); }));
__instance.RoleSettingsButton.OnMouseOver.AddListener( menu.RoleSettingsButton.OnMouseOver.AddListener(
(UnityAction)(() => (UnityAction)(() =>
{ {
__instance.ChangeTab(3, true); menu.ChangeTab(3, true);
})); }));
} }
else else
{ {
__instance.RoleSettingsButton.buttonText.text = "Roles Settings"; menu.RoleSettingsButton.buttonText.text = "Roles Settings";
__instance.RoleSettingsButton.OnClick.AddListener( menu.RoleSettingsButton.OnClick.AddListener(
(UnityAction)(() => (UnityAction)(() =>
{ {
__instance.ChangeTab(2, false); menu.ChangeTab(2, false);
})); }));
__instance.RoleSettingsButton.OnMouseOver.AddListener( menu.RoleSettingsButton.OnMouseOver.AddListener(
(UnityAction)(() => (UnityAction)(() =>
{ {
__instance.ChangeTab(2, true); menu.ChangeTab(2, true);
})); }));
} }
} }
private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles) private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles)
{ {
if (_text is not null && SelectedModIdx == 0) if (_text is not null && SelectedModIdx == 0)
{ {
_text.text = "Default"; _text.text = "Default";
_text.fontSizeMax = 3.2f; _text.fontSizeMax = 3.2f;
SelectedMod = null;
} }
else if (_text is not null) else if (_text is not null)
{ {
_text.fontSizeMax = 2.3f; _text.fontSizeMax = 2.3f;
SelectedMod = MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1]; SelectedMod = MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1];
var name = SelectedMod.MiraPlugin.OptionsTitleText; var name = SelectedMod.MiraPlugin.OptionsTitleText;
_text.text = name[..Math.Min(name.Length, 25)]; _text.text = name[..Math.Min(name.Length, 25)];
@@ -281,9 +298,11 @@ internal static class GameSettingMenuPatches
if (SelectedModIdx != 0) if (SelectedModIdx != 0)
{ {
var modHasRoles = SelectedMod!.CustomRoles.Count != 0; var modHasRoles = SelectedMod!.InternalRoles.Count != 0;
var modHasModifiers = SelectedMod.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true); var modHasModifiers = SelectedMod.InternalOptionGroups.Exists(
var modHasOptions = SelectedMod.OptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu); x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
var modHasOptions =
SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu);
_modifiersButton.gameObject.SetActive(true); _modifiersButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(true); _smallRolesButton.gameObject.SetActive(true);
@@ -323,6 +342,7 @@ internal static class GameSettingMenuPatches
{ {
menu.ChangeTab(0, false); menu.ChangeTab(0, false);
} }
// If the mod has roles, we can enable the bigger role button and disable the small one. // If the mod has roles, we can enable the bigger role button and disable the small one.
if (modHasRoles) if (modHasRoles)
{ {
@@ -379,6 +399,11 @@ internal static class GameSettingMenuPatches
ChangeRoleSettingButton(replaceWithModifiers, menu); ChangeRoleSettingButton(replaceWithModifiers, menu);
} }
if (menu.PresetsTab.gameObject.active)
{
menu.ChangeTab(0, false);
}
CleanupTab(settings, roles); CleanupTab(settings, roles);
} }
@@ -390,13 +415,52 @@ internal static class GameSettingMenuPatches
{ {
continue; continue;
} }
child.gameObject.DestroyImmediate(); child.gameObject.DestroyImmediate();
} }
options.Clear(); options.Clear();
} }
private static void CleanupTab(GameOptionsMenu settings, RolesSettingsMenu roles) private static void CleanupTab(GameOptionsMenu settings, RolesSettingsMenu roles)
{ {
if (roles.roleChances != null)
{
CleanupRoleSettings(roles);
}
if (settings.Children != null)
{
CleanupSettings(settings);
}
if (_modifiersTab?.Children?.Count > 0)
{
CleanupSettings(_modifiersTab);
}
void CleanupSettings(GameOptionsMenu gameOptMenu)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
gameOptMenu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
if (OptionScrolls.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
gameOptMenu.scrollBar.UpdateScrollBars();
}
else
{
gameOptMenu.scrollBar.ScrollToTop();
}
}
void CleanupRoleSettings(RolesSettingsMenu rolesMenu) void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
{ {
if (rolesMenu.advancedSettingChildren != null) if (rolesMenu.advancedSettingChildren != null)
@@ -415,57 +479,24 @@ internal static class GameSettingMenuPatches
{ {
role.gameObject.DestroyImmediate(); role.gameObject.DestroyImmediate();
} }
rolesMenu.roleChances?.Clear(); rolesMenu.roleChances?.Clear();
rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false); rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false);
rolesMenu.RoleChancesSettings.gameObject.SetActive(true); rolesMenu.RoleChancesSettings.gameObject.SetActive(true);
rolesMenu.SetQuotaTab(); rolesMenu.SetQuotaTab();
if (RoleSettingMenuPatches.RolePositions.TryGetValue(SelectedModIdx, out var pos)) if (SelectedModIdx == 0)
{ {
rolesMenu.scrollBar.Inner.localPosition = pos; if (RoleSettingMenuPatches.Scrolls.TryGetValue(0, out var pos))
rolesMenu.scrollBar.UpdateScrollBars(); {
rolesMenu.scrollBar.Inner.localPosition = pos;
rolesMenu.scrollBar.UpdateScrollBars();
}
else
{
rolesMenu.scrollBar.ScrollToTop();
}
} }
else
{
rolesMenu.scrollBar.ScrollToTop();
}
}
void CleanupSettings(GameOptionsMenu gameOptMenu)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
gameOptMenu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
if (OptionsPositions.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
gameOptMenu.scrollBar.UpdateScrollBars();
}
else
{
gameOptMenu.scrollBar.ScrollToTop();
}
}
if (roles.roleChances != null && SelectedMod?.CustomRoles.Count > 0)
{
CleanupRoleSettings(roles);
}
if (settings.Children != null && SelectedMod?.OptionGroups.Count > 0)
{
CleanupSettings(settings);
}
if (_modifiersTab?.Children?.Count > 0)
{
CleanupSettings(_modifiersTab);
} }
} }
} }
@@ -24,7 +24,7 @@ public static class LobbyViewPanePatches
private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0 private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0
? null ? null
: MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1]; : MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1];
private static PassiveButton? ModifiersTabButton { get; set; } private static PassiveButton? ModifiersTabButton { get; set; }
@@ -74,7 +74,7 @@ public static class LobbyViewPanePatches
(UnityAction)(() => (UnityAction)(() =>
{ {
SelectedModIdx += 1; SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length)
{ {
SelectedModIdx = 0; SelectedModIdx = 0;
} }
@@ -98,7 +98,7 @@ public static class LobbyViewPanePatches
SelectedModIdx -= 1; SelectedModIdx -= 1;
if (SelectedModIdx < 0) if (SelectedModIdx < 0)
{ {
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins.Length;
} }
Refresh(__instance); Refresh(__instance);
@@ -127,7 +127,7 @@ public static class LobbyViewPanePatches
__instance.rolesTabButton.SelectButton(false); __instance.rolesTabButton.SelectButton(false);
ModifiersTabButton?.SelectButton(true); ModifiersTabButton?.SelectButton(true);
var filteredGroups = SelectedMod.OptionGroups var filteredGroups = SelectedMod.InternalOptionGroups
.Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true)); .Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true));
DrawOptions(__instance, filteredGroups); DrawOptions(__instance, filteredGroups);
return false; return false;
@@ -142,7 +142,7 @@ public static class LobbyViewPanePatches
return true; return true;
} }
var filteredGroups = SelectedMod.OptionGroups var filteredGroups = SelectedMod.InternalOptionGroups
.Where(x => x.OptionableType == null && x.GroupVisible.Invoke()); .Where(x => x.OptionableType == null && x.GroupVisible.Invoke());
DrawOptions(__instance, filteredGroups); DrawOptions(__instance, filteredGroups);
return false; return false;
@@ -250,7 +250,7 @@ public static class LobbyViewPanePatches
var list = new List<Type>(); var list = new List<Type>();
var roleGroups = SelectedMod.CustomRoles.Values.OfType<ICustomRole>() var roleGroups = SelectedMod.InternalRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup); .ToLookup(x => x.RoleOptionsGroup);
// sort the groups by priority // sort the groups by priority
@@ -315,7 +315,7 @@ public static class LobbyViewPanePatches
viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one; viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f); viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
var advancedRoleOptions = SelectedMod.OptionGroups var advancedRoleOptions = SelectedMod.InternalOptionGroups
.Where(x => x.OptionableType == customRole.GetType()) .Where(x => x.OptionableType == customRole.GetType())
.SelectMany(x => x.Options) .SelectMany(x => x.Options)
.ToList(); .ToList();
@@ -402,7 +402,7 @@ public static class LobbyViewPanePatches
return 0; return 0;
} }
var role = SelectedMod.CustomRoles.Values.FirstOrDefault(x => x.GetType() == roleType); var role = SelectedMod.InternalRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
if (role == null) if (role == null)
{ {
@@ -425,7 +425,7 @@ public static class LobbyViewPanePatches
var num = viewPanel.yPosStart; var num = viewPanel.yPosStart;
var num2 = 1.08f; var num2 = 1.08f;
var filteredOptions = SelectedMod.OptionGroups var filteredOptions = SelectedMod.InternalOptionGroups
.Where(x => x.OptionableType == roleType) .Where(x => x.OptionableType == roleType)
.SelectMany(x=>x.Options) .SelectMany(x=>x.Options)
.ToList(); .ToList();
@@ -2,8 +2,6 @@
using HarmonyLib; using HarmonyLib;
using Il2CppSystem; using Il2CppSystem;
using MiraAPI.Roles; using MiraAPI.Roles;
using UnityEngine;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options; namespace MiraAPI.Patches.Options;
@@ -25,13 +23,15 @@ public static class NotificationPopperPatch
return true; return true;
} }
var textColor = customRole.OptionsMenuColor.ToTextColor(); var text = teamType == RoleTeamTypes.Crewmate
? Palette.CrewmateSettingChangeText.ToTextColor()
: Palette.ImpostorRed.ToTextColor();
var item = TranslationController.Instance.GetString( var item = TranslationController.Instance.GetString(
StringNames.LobbyChangeSettingNotificationRole, StringNames.LobbyChangeSettingNotificationRole,
string.Concat( string.Concat(
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">", "<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">",
textColor, text,
TranslationController.Instance.GetString(key, Array.Empty<Object>()), TranslationController.Instance.GetString(key, Array.Empty<Object>()),
"</color></font>" "</color></font>"
), ),
@@ -22,17 +22,16 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(RolesSettingsMenu))] [HarmonyPatch(typeof(RolesSettingsMenu))]
public static class RoleSettingMenuPatches public static class RoleSettingMenuPatches
{ {
public static Dictionary<int, Vector3> RolePositions { get; set; } = []; public static Dictionary<int, Vector3> Scrolls { get; } = [];
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; } = [];
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; set; } = []; private static List<GameObject> Headers { get; } = [];
private static List<GameObject> Headers { get; set; } = []; private static List<RoleOptionSetting> RoleOptionSettings { get; } = [];
private static List<RoleOptionSetting> RoleOptionSettings { get; set; } = [];
private static float ScrollerNum { get; set; } = 0.522f; private static float ScrollerNum { get; set; } = 0.522f;
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static bool PatchStart(RolesSettingsMenu __instance) public static bool SetQuotaTabPatch(RolesSettingsMenu __instance)
{ {
Headers.ForEach(Object.Destroy); Headers.ForEach(Object.Destroy);
RoleOptionSettings.ForEach(Object.Destroy); RoleOptionSettings.ForEach(Object.Destroy);
@@ -69,7 +68,7 @@ public static class RoleSettingMenuPatches
var num3 = 0; var num3 = 0;
var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType<ICustomRole>() var roleGroups = GameSettingMenuPatches.SelectedMod?.InternalRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup); .ToLookup(x => x.RoleOptionsGroup);
if (roleGroups is null) if (roleGroups is null)
@@ -234,7 +233,7 @@ public static class RoleSettingMenuPatches
private static void SetScrollBounds(this Scroller scroller) private static void SetScrollBounds(this Scroller scroller)
{ {
scroller.CalculateAndSetYBounds(1 + 1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f); scroller.CalculateAndSetYBounds(1 + 1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f);
if (RolePositions.TryGetValue(GameSettingMenuPatches.SelectedModIdx, out var scroll)) if (Scrolls.TryGetValue(GameSettingMenuPatches.SelectedModIdx, out var scroll))
{ {
scroller.Inner.localPosition = scroll; scroller.Inner.localPosition = scroll;
scroller.UpdateScrollBars(); scroller.UpdateScrollBars();
@@ -256,7 +255,7 @@ public static class RoleSettingMenuPatches
[HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))] [HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))]
public static void OpenChancesTabPostfix(RolesSettingsMenu __instance) public static void OpenChancesTabPostfix(RolesSettingsMenu __instance)
{ {
if (GameSettingMenuPatches.SelectedModIdx == 0 || !__instance.scrollBar) if (GameSettingMenuPatches.SelectedModIdx == 0)
{ {
return; return;
} }
@@ -319,7 +318,7 @@ public static class RoleSettingMenuPatches
var num = hasImage ? -0.872f : -1; var num = hasImage ? -0.872f : -1;
// TODO: create sub groups under the role settings. // TODO: create sub groups under the role settings.
var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups var filteredOptions = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x=> x.OptionableType == role.GetType()) .Where(x=> x.OptionableType == role.GetType())
.SelectMany(x=>x.Options) .SelectMany(x=>x.Options)
.ToList() ?? []; .ToList() ?? [];
@@ -354,6 +353,7 @@ public static class RoleSettingMenuPatches
newOpt.Initialize(); newOpt.Initialize();
} }
Scrolls[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
__instance.scrollBar.CalculateAndSetYBounds(__instance.advancedSettingChildren.Count + 3, 1f, 6f, 0.45f); __instance.scrollBar.CalculateAndSetYBounds(__instance.advancedSettingChildren.Count + 3, 1f, 6f, 0.45f);
__instance.scrollBar.ScrollToTop(); __instance.scrollBar.ScrollToTop();
} }
@@ -366,7 +366,7 @@ public static class RoleSettingMenuPatches
return; return;
} }
RolePositions[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition; Scrolls[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
__instance.roleDescriptionText.text = customRole.RoleLongDescription; __instance.roleDescriptionText.text = customRole.RoleLongDescription;
__instance.roleTitleText.text = TranslationController.Instance.GetString( __instance.roleTitleText.text = TranslationController.Instance.GetString(
@@ -485,7 +485,7 @@ public static class RoleSettingMenuPatches
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left; roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
if (GameSettingMenuPatches.SelectedMod is null || if (GameSettingMenuPatches.SelectedMod is null ||
GameSettingMenuPatches.SelectedMod.OptionGroups GameSettingMenuPatches.SelectedMod.InternalOptionGroups
.Exists(x => x.OptionableType == role.GetType())) .Exists(x => x.OptionableType == role.GetType()))
{ {
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform); var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
@@ -517,7 +517,7 @@ public static class RoleSettingMenuPatches
roleOptionSetting.ChancePlusBtn.gameObject.SetActive(false); roleOptionSetting.ChancePlusBtn.gameObject.SetActive(false);
} }
if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1) if (index < GameSettingMenuPatches.SelectedMod?.InternalRoles.Count - 1)
{ {
ScrollerNum -= 0.43f; ScrollerNum -= 0.43f;
} }
+7 -2
View File
@@ -5,7 +5,7 @@ using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Player; using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Utilities; using MiraAPI.Roles;
using MiraAPI.Voting; using MiraAPI.Voting;
using Reactor.Utilities.Extensions; using Reactor.Utilities.Extensions;
@@ -41,7 +41,7 @@ internal static class PlayerControlPatches
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason) public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason)
{ {
var deathEvent = new PlayerDeathEvent(__instance, reason, Helpers.GetBodyById(__instance.PlayerId)); var deathEvent = new PlayerDeathEvent(__instance, reason);
MiraEventManager.InvokeEvent(deathEvent); MiraEventManager.InvokeEvent(deathEvent);
var modifiersComponent = __instance.GetComponent<ModifierComponent>(); var modifiersComponent = __instance.GetComponent<ModifierComponent>();
@@ -81,6 +81,11 @@ internal static class PlayerControlPatches
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance) public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance)
{ {
if (__instance.Data?.Role is ICustomRole customRole)
{
customRole.PlayerControlFixedUpdate(__instance);
}
if (!__instance.AmOwner) if (!__instance.AmOwner)
{ {
return; return;
+3 -1
View File
@@ -1,4 +1,6 @@
using HarmonyLib; using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using MiraAPI.Events; using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Meeting; using MiraAPI.Events.Vanilla.Meeting;
+1 -1
View File
@@ -10,7 +10,7 @@ public static class GameStartupPatch
private static bool _runOnce; private static bool _runOnce;
/// <summary> /// <summary>
/// This is used for registering roles when the game opens, might be a janky solution, but it works. /// This is used for registering roles when the game opens, might be a janky solution, but it works
/// </summary> /// </summary>
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(nameof(MainMenuManager.Start))] [HarmonyPatch(nameof(MainMenuManager.Start))]
+1 -1
View File
@@ -24,4 +24,4 @@ public static class HauntMenuMinigamePatch
__instance.FilterText.text = role.NiceName; __instance.FilterText.text = role.NiceName;
__instance.FilterText.color = color; __instance.FilterText.color = color;
} }
} }
@@ -62,6 +62,8 @@ public static class HudManagerPatches
if (role is ICustomRole { Configuration.RoleHintType: RoleHintType.RoleTab } customRole) if (role is ICustomRole { Configuration.RoleHintType: RoleHintType.RoleTab } customRole)
{ {
customRole.HudUpdate(__instance);
if (_roleTab == null) if (_roleTab == null)
{ {
_roleTab = CustomRoleManager.CreateRoleTab(customRole); _roleTab = CustomRoleManager.CreateRoleTab(customRole);
@@ -12,7 +12,7 @@ namespace MiraAPI.Patches.Roles;
public static class RoleOptionsCollectionPatch public static class RoleOptionsCollectionPatch
{ {
/// <summary> /// <summary>
/// Set the role chance for custom Launchpad roles based on config. /// Set the role chance for custom Launchpad roles based on config
/// </summary> /// </summary>
/// <returns>Return false to skip original method, true to not.</returns> /// <returns>Return false to skip original method, true to not.</returns>
[HarmonyPrefix] [HarmonyPrefix]
@@ -42,7 +42,7 @@ public static class RoleOptionsCollectionPatch
} }
/// <summary> /// <summary>
/// Set the amount for custom Launchpad roles based on config. /// Set the amount for custom Launchpad roles based on config
/// </summary> /// </summary>
/// <returns>Return false to skip original method, true to not.</returns> /// <returns>Return false to skip original method, true to not.</returns>
[HarmonyPrefix] [HarmonyPrefix]
+1 -1
View File
@@ -4,7 +4,7 @@ using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles; namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(SabotageButton))] [HarmonyPatch(typeof(SabotageButton))]
public static class SabotageButtonPatch public class SabotageButtonPatch
{ {
/// <summary> /// <summary>
/// Patches the Sabotage button to check if the player's custom role can use sabotage. /// Patches the Sabotage button to check if the player's custom role can use sabotage.
@@ -8,6 +8,7 @@ using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Utilities; using MiraAPI.Utilities;
using MiraAPI.Voting; using MiraAPI.Voting;
using Reactor.Utilities;
namespace MiraAPI.Patches.Voting; namespace MiraAPI.Patches.Voting;
-8
View File
@@ -1,8 +0,0 @@
using System;
namespace MiraAPI.PluginLoading;
public interface IMiraExtension
{
public Type GetBasePluginType();
}
+30 -31
View File
@@ -6,7 +6,9 @@ using MiraAPI.GameModes;
using MiraAPI.GameOptions; using MiraAPI.GameOptions;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Presets;
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace MiraAPI.PluginLoading; namespace MiraAPI.PluginLoading;
/// <summary> /// <summary>
@@ -23,61 +25,58 @@ public class MiraPluginInfo
} }
/// <summary> /// <summary>
/// Get a read only collection of this plugin's modifiers. This is probably not safe because they are uninitialized. /// Gets a read only collection of this plugin's modifiers. This is not safe for modifiers with constructors.
/// </summary> /// </summary>
/// <returns>Readonly collection of modifiers.</returns> public IReadOnlyCollection<BaseModifier> Modifiers { get; private set; } = null!;
public IReadOnlyCollection<BaseModifier> GetModifiers()
{
return Modifiers.AsReadOnly();
}
/// <summary> /// <summary>
/// Get a read only collection of this plugin's Option Groups. /// Gets a read only collection of this plugin's Option Groups.
/// </summary> /// </summary>
/// <returns>Readonly collection of option groups.</returns> public IReadOnlyCollection<AbstractOptionGroup> OptionGroups { get; private set; } = null!;
public IReadOnlyCollection<AbstractOptionGroup> GetOptionGroups()
{
return OptionGroups.AsReadOnly();
}
/// <summary> /// <summary>
/// Gets a read only collection of this plugin's options. /// Gets a read only collection of this plugin's options.
/// </summary> /// </summary>
/// <returns>Read only collection of options.</returns> public IReadOnlyCollection<IModdedOption> Options { get; private set; } = null!;
public IReadOnlyCollection<IModdedOption> GetOptions()
{
return Options.AsReadOnly();
}
/// <summary> /// <summary>
/// Gets a read only dictionary of Role IDs and the RoleBehaviour object they are associated with. /// Gets a read only dictionary of Role IDs and the RoleBehaviour object they are associated with.
/// </summary> /// </summary>
/// <returns>Read only dictionary of IDs and Roles.</returns> public ReadOnlyDictionary<ushort, RoleBehaviour> Roles { get; private set; } = null!;
public ReadOnlyDictionary<ushort, RoleBehaviour> GetRoles()
{
return new ReadOnlyDictionary<ushort, RoleBehaviour>(CustomRoles);
}
/// <summary> /// <summary>
/// Gets a read only collection of this plugin's custom buttons. /// Gets a read only collection of this plugin's custom buttons.
/// </summary> /// </summary>
/// <returns>Read only collection of buttons.</returns> public IReadOnlyCollection<CustomActionButton> Buttons { get; private set; } = null!;
public IReadOnlyCollection<CustomActionButton> GetButtons()
/// <summary>
/// Gets a read only dictionary of this plugin's custom game modes.
/// </summary>
public IReadOnlyCollection<OptionPreset> Presets { get; internal set; } = null!;
internal void SavePublicCollections()
{ {
return Buttons.AsReadOnly(); Presets = [..InternalPresets];
Modifiers = [..InternalModifiers];
OptionGroups = [..InternalOptionGroups];
Options = [..InternalOptions];
Roles = new ReadOnlyDictionary<ushort, RoleBehaviour>(InternalRoles);
Buttons = [..InternalButtons];
} }
internal List<AbstractOptionGroup> OptionGroups { get; } = []; internal List<OptionPreset> InternalPresets { get; } = [];
internal List<IModdedOption> Options { get; } = []; internal List<AbstractOptionGroup> InternalOptionGroups { get; } = [];
internal List<BaseModifier> Modifiers { get; } = []; internal List<IModdedOption> InternalOptions { get; } = [];
internal Dictionary<ushort, RoleBehaviour> CustomRoles { get; } = []; internal List<BaseModifier> InternalModifiers { get; } = [];
internal Dictionary<int, CustomGameMode> GameModes { get; } = []; internal Dictionary<ushort, RoleBehaviour> InternalRoles { get; } = [];
internal List<CustomActionButton> Buttons { get; } = []; internal Dictionary<int, CustomGameMode> InternalGameModes { get; } = [];
internal List<CustomActionButton> InternalButtons { get; } = [];
/// <summary> /// <summary>
/// Gets the plugin's ID, as defined in the plugin's BepInEx metadata. /// Gets the plugin's ID, as defined in the plugin's BepInEx metadata.
+80 -111
View File
@@ -4,7 +4,6 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using BepInEx;
using BepInEx.Unity.IL2CPP; using BepInEx.Unity.IL2CPP;
using MiraAPI.Colors; using MiraAPI.Colors;
using MiraAPI.Events; using MiraAPI.Events;
@@ -13,6 +12,7 @@ using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.Attributes;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Presets;
using MiraAPI.Roles; using MiraAPI.Roles;
using MiraAPI.Utilities; using MiraAPI.Utilities;
using Reactor.Networking; using Reactor.Networking;
@@ -27,8 +27,9 @@ public sealed class MiraPluginManager
{ {
private readonly Dictionary<Assembly, MiraPluginInfo> _registeredPlugins = []; private readonly Dictionary<Assembly, MiraPluginInfo> _registeredPlugins = [];
internal MiraPluginInfo[] RegisteredPlugins() => [.. _registeredPlugins.Values]; internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!;
internal Dictionary<MiraPluginInfo, List<Type>> QueuedRoleRegistrations { get; } = new();
internal Dictionary<MiraPluginInfo, List<Type>> QueuedRoleRegistrations { get; } = [];
internal static MiraPluginManager Instance { get; private set; } = new(); internal static MiraPluginManager Instance { get; private set; } = new();
internal void Initialize() internal void Initialize()
@@ -36,29 +37,91 @@ public sealed class MiraPluginManager
Instance = this; Instance = this;
IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) =>
{ {
if (plugin is IMiraPlugin miraPlugin) if (plugin is not IMiraPlugin miraPlugin)
{ {
var miraInfo = new MiraPluginInfo(miraPlugin, pluginInfo); return;
RegisterPlugin(miraInfo, assembly);
_registeredPlugins.Add(assembly, miraInfo);
} }
if (plugin is IMiraExtension miraExtension)
var info = new MiraPluginInfo(miraPlugin, pluginInfo);
var roles = new List<Type>();
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
info.PluginConfig.SaveOnConfigSet = false;
foreach (var type in assembly.GetTypes())
{ {
var basePlugin = miraExtension.GetBasePluginType(); if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
var basePluginInfo = _registeredPlugins.Values.FirstOrDefault(x => x.MiraPlugin.GetType() == basePlugin);
if (basePluginInfo == null)
{ {
Logger<MiraApiPlugin>.Error($"Base plugin {basePlugin.Name} not found for extension {pluginInfo.Metadata.GUID}."); continue;
return;
} }
RegisterPlugin(basePluginInfo, assembly); foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute != null)
{
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
}
if (RegisterModifier(type, info))
{
continue;
}
if (RegisterOptions(type, info))
{
continue;
}
if (RegisterRoleAttribute(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButtonAttribute(type, info))
{
continue;
}
if (RegisterGameOver(type))
{
continue;
}
RegisterColorClasses(type);
} }
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
}; };
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
IL2CPPChainloader.Instance.Finished += () => IL2CPPChainloader.Instance.Finished += () =>
{ {
// Save all buttons into a read-only collection for easy access
CustomButtonManager.Buttons = new ReadOnlyCollection<CustomActionButton>(CustomButtonManager.CustomButtons); CustomButtonManager.Buttons = new ReadOnlyCollection<CustomActionButton>(CustomButtonManager.CustomButtons);
// Cache all the registered plugins into an array for easy access
RegisteredPlugins = [.._registeredPlugins.Values];
}; };
} }
@@ -72,84 +135,6 @@ public sealed class MiraPluginManager
return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId); return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId);
} }
private void RegisterPlugin(MiraPluginInfo info, Assembly assembly)
{
var roles = new List<Type>();
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
info.PluginConfig.SaveOnConfigSet = false;
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
{
continue;
}
foreach (var method in type.GetMethods())
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
{
continue;
}
if (!method.IsStatic)
{
Logger<MiraApiPlugin>.Error($"Event method {method.Name} in {type.Name} must be static.");
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
if (RegisterModifier(type, info))
{
continue;
}
if (RegisterOptions(type, info))
{
continue;
}
if (RegisterRole(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButton(type, info))
{
continue;
}
if (RegisterGameOver(type))
{
continue;
}
RegisterColorClasses(type);
}
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.TryAdd(info, []);
QueuedRoleRegistrations[info].AddRange(roles);
Logger<MiraApiPlugin>.Info($"Registering mod {info.PluginId} with Mira API.");
}
private static bool RegisterGameOver(Type type) private static bool RegisterGameOver(Type type)
{ {
try try
@@ -179,12 +164,6 @@ public sealed class MiraPluginManager
foreach (var property in type.GetProperties()) foreach (var property in type.GetProperties())
{ {
if (property.GetMethod?.IsStatic == true)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} must not be static.");
continue;
}
if (property.PropertyType.IsAssignableTo(typeof(IModdedOption))) if (property.PropertyType.IsAssignableTo(typeof(IModdedOption)))
{ {
ModdedOptionsManager.RegisterPropertyOption(type, property, pluginInfo); ModdedOptionsManager.RegisterPropertyOption(type, property, pluginInfo);
@@ -200,21 +179,16 @@ public sealed class MiraPluginManager
ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo); ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo);
} }
foreach (var field in type.GetFields().Where(f=>f.FieldType.IsAssignableTo(typeof(IModdedOption))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for options.");
}
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e.ToString()}"); Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e}");
} }
return false; return false;
} }
private static bool RegisterRole(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role) private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role)
{ {
role = null; role = null;
try try
@@ -270,11 +244,6 @@ public sealed class MiraPluginManager
PaletteManager.CustomColors.Add(color); PaletteManager.CustomColors.Add(color);
} }
foreach (var field in type.GetFields().Where(f=>f.FieldType.IsAssignableTo(typeof(CustomColor))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for colors.");
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -295,7 +264,7 @@ public sealed class MiraPluginManager
} }
} }
private static bool RegisterButton(Type type, MiraPluginInfo pluginInfo) private static bool RegisterButtonAttribute(Type type, MiraPluginInfo pluginInfo)
{ {
try try
{ {
+74
View File
@@ -0,0 +1,74 @@
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Presets;
/// <summary>
/// Represents a preset of game options that can be applied to the game.
/// </summary>
public class OptionPreset
{
/// <summary>
/// Gets the name of the preset.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the plugin associated with the preset.
/// </summary>
public MiraPluginInfo Plugin { get; }
/// <summary>
/// Gets the configuration file of the plugin associated with the preset.
/// </summary>
public ConfigFile PluginConfig { get; }
/// <summary>
/// Gets the configuration file for the preset.
/// </summary>
public ConfigFile PresetConfig { get; }
/// <summary>
/// Gets or sets the button associated with the preset in the UI.
/// </summary>
public GameObject? PresetButton { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="OptionPreset"/> class with the specified name and configuration file.
/// </summary>
/// <param name="name">>The name of the preset.</param>
/// <param name="plugin">The plugin associated with the preset.</param>
/// <param name="presetConfig">>The configuration file for the preset.</param>
public OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetConfig)
{
Name = name;
Plugin = plugin;
PluginConfig = plugin.PluginConfig;
PresetConfig = presetConfig;
}
/// <summary>
/// Loads the preset by applying the values from the preset configuration to the plugin configuration.
/// </summary>
public void LoadPreset()
{
PresetConfig.Reload();
foreach (var option in Plugin.InternalOptions)
{
option.LoadFromPreset(PresetConfig);
}
ModdedOptionsManager.SyncAllOptions();
foreach (var role in Plugin.InternalRoles.Values.OfType<ICustomRole>())
{
role.LoadFromPreset(PresetConfig);
}
CustomRoleManager.SyncAllRoleSettings();
}
}
+95
View File
@@ -0,0 +1,95 @@
using System.IO;
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Presets;
/// <summary>
/// Provides functionality to manage and load game option presets for plugins.
/// </summary>
public static class PresetManager
{
/// <summary>
/// Gets the directory where the presets are stored.
/// </summary>
public static string PresetDirectory { get; } = Path.GetFullPath("mira_presets", Application.persistentDataPath);
internal static void CreateDefaultPreset(MiraPluginInfo plugin)
{
var presetPath = Path.Combine(PresetDirectory, plugin.PluginId);
if (!Directory.Exists(presetPath))
{
Directory.CreateDirectory(presetPath);
}
var presetConfig = new ConfigFile(Path.Combine(presetPath, "Default.cfg"), false);
foreach (var option in plugin.InternalOptions.Where(x => x.IncludeInPreset))
{
option.SaveToPreset(presetConfig, true);
}
foreach (var role in plugin.InternalRoles.Values.OfType<ICustomRole>())
{
role.SaveToPreset(presetConfig);
}
presetConfig.Save();
}
/// <summary>
/// Loads the presets for the specified plugin by reading the configuration files from the preset directory.
/// </summary>
/// <param name="plugin">>The plugin for which the presets should be loaded.</param>
public static void LoadPresets(MiraPluginInfo plugin)
{
foreach (var btn in plugin.InternalPresets.Select(x=>x.PresetButton))
{
if (btn != null)
{
Object.DestroyImmediate(btn);
}
}
plugin.InternalPresets.Clear();
if (!Directory.Exists(PresetDirectory))
{
Directory.CreateDirectory(PresetDirectory);
}
var pluginPresetPath = Path.Combine(PresetDirectory, plugin.PluginId);
if (!Directory.Exists(pluginPresetPath))
{
Directory.CreateDirectory(pluginPresetPath);
}
foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
{
Logger<MiraApiPlugin>.Info($"Loading preset file {file}");
var presetName = Path.GetFileNameWithoutExtension(file);
var presetConfig = new ConfigFile(file, false)
{
SaveOnConfigSet = false,
};
foreach (var option in plugin.InternalOptions.Where(x=>x.IncludeInPreset))
{
option.Bind(presetConfig);
}
foreach (var role in plugin.InternalRoles.Values.OfType<ICustomRole>().Where(x=>!x.Configuration.HideSettings))
{
role.BindConfig(presetConfig);
}
presetConfig.Save();
plugin.InternalPresets.Add(new OptionPreset(presetName, plugin, presetConfig));
}
plugin.Presets = [.. plugin.InternalPresets];
}
}
+91
View File
@@ -0,0 +1,91 @@
using System;
using Il2CppInterop.Runtime.InteropTypes.Fields;
using MiraAPI.Modifiers.ModifierDisplay;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
[RegisterInIl2Cpp]
public class SavePresetPopup(nint ptr) : Minigame(ptr)
{
private PassiveButton saveButton;
private PassiveButton closeButton;
private TextMeshPro textBoxText;
private TextBoxTMP textBox;
private Action<string> onSave;
// Cleanup holder object
public void OnDestroy()
{
transform.parent.gameObject.Destroy();
}
public override void Close()
{
}
private void Awake()
{
var textboxHolder = transform.GetChild(1).GetChild(1);
saveButton = transform.FindChild("SaveButton").GetComponent<PassiveButton>();
closeButton = transform.FindChild("CloseButton").GetComponent<PassiveButton>();
textBox = textboxHolder.GetChild(0).GetComponent<TextBoxTMP>();
textBoxText = textBox.transform.GetChild(0).GetComponent<TextMeshPro>();
saveButton.OnClick = new Button.ButtonClickedEvent();
saveButton.OnClick.AddListener((UnityAction)(() =>
{
onSave.Invoke(textBoxText.text);
this.BaseClose();
}));
closeButton.OnClick = new Button.ButtonClickedEvent();
closeButton.OnClick.AddListener((UnityAction)(() =>
{
this.BaseClose();
}));
textBox.OnChange = new Button.ButtonClickedEvent();
textBox.OnChange.AddListener((UnityAction)(() =>
{
textBoxText.text = textBoxText.text.Replace(" ", string.Empty);
}));
textBox.GetComponent<PassiveButton>().OnClick.AddListener((UnityAction)(() =>
{
textBox.GiveFocus();
}));
foreach (var tmp in gameObject.GetComponentsInChildren<TextMeshPro>())
{
if (tmp.gameObject.transform.parent.name == "Textbox") continue;
var text = tmp.text;
tmp.text = $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF RadialMenu Material\">{text}</font>";
}
}
public static void CreatePopup(Action<string> saveAction)
{
// Because innerscuff doesnt account for Z value in Close animation
var holder = new GameObject("PopupHolder")
{
transform =
{
parent = Camera.main!.transform,
},
};
holder.transform.localPosition = new Vector3(0, -1f, -500);
var gameObject = Instantiate(MiraAssets.PresetSavePopup.LoadAsset(), holder.transform);
var popup = gameObject.GetComponent<SavePresetPopup>();
popup.onSave = saveAction;
popup.Begin(null);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.
+4 -4
View File
@@ -72,7 +72,7 @@ public static class CustomRoleManager
continue; continue;
} }
pluginInfo.CustomRoles.Add((ushort)role.Role, role); pluginInfo.InternalRoles.Add((ushort)role.Role, role);
} }
pluginInfo.PluginConfig.Save(); pluginInfo.PluginConfig.Save();
@@ -161,7 +161,7 @@ public static class CustomRoleManager
/// <returns>A MiraPluginInfo object representing the parent mod of the role.</returns> /// <returns>A MiraPluginInfo object representing the parent mod of the role.</returns>
public static MiraPluginInfo FindParentMod(ICustomRole role) public static MiraPluginInfo FindParentMod(ICustomRole role)
{ {
return MiraPluginManager.Instance.RegisteredPlugins().First(plugin => plugin.CustomRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException())); return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
} }
/// <summary> /// <summary>
@@ -236,7 +236,7 @@ public static class CustomRoleManager
// we dont know how other plugins handle their configs // we dont know how other plugins handle their configs
// this way, all the options are saved at once, instead of one by one // this way, all the options are saved at once, instead of one by one
var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>(); var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>();
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{ {
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet); oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
plugin.PluginConfig.SaveOnConfigSet = false; plugin.PluginConfig.SaveOnConfigSet = false;
@@ -271,7 +271,7 @@ public static class CustomRoleManager
} }
} }
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{ {
plugin.PluginConfig.Save(); plugin.PluginConfig.Save();
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin]; plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
+5 -5
View File
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Linq;
using System.Linq;
namespace MiraAPI.Roles; namespace MiraAPI.Roles;
@@ -9,11 +8,12 @@ namespace MiraAPI.Roles;
/// <typeparam name="T">The role you are trying to access.</typeparam> /// <typeparam name="T">The role you are trying to access.</typeparam>
public static class CustomRoleSingleton<T> where T : ICustomRole public static class CustomRoleSingleton<T> where T : ICustomRole
{ {
private static T? _instance; private static ICustomRole? _instance;
/// <summary> /// <summary>
/// Gets the instance of the role. /// Gets the instance of the role.
/// </summary> /// </summary>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a custom role.")] #pragma warning disable CA1000
public static T Instance => _instance ??= CustomRoleManager.CustomRoles.Values.OfType<T>().Single(); public static ICustomRole Instance => _instance ??= CustomRoleManager.CustomRoles.Values.OfType<T>().Single();
#pragma warning restore CA1000
} }
+55
View File
@@ -73,9 +73,56 @@ public interface ICustomRole : IOptionable
/// </summary> /// </summary>
MiraPluginInfo ParentMod => CustomRoleManager.FindParentMod(this); MiraPluginInfo ParentMod => CustomRoleManager.FindParentMod(this);
/// <summary>
/// This method runs on the PlayerControl.FixedUpdate method for ALL players with this role.
/// </summary>
/// <param name="playerControl">The PlayerControl that has this role.</param>
void PlayerControlFixedUpdate(PlayerControl playerControl)
{
}
internal ConfigDefinition NumConfigDefinition => new("Roles", $"Num {GetType().FullName}"); internal ConfigDefinition NumConfigDefinition => new("Roles", $"Num {GetType().FullName}");
internal ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance {GetType().FullName}"); internal ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance {GetType().FullName}");
/// <summary>
/// Binds the configuration options for this role to the provided ConfigFile.
/// </summary>
/// <param name="config">The ConfigFile to bind the options to.</param>
public virtual void BindConfig(ConfigFile config)
{
config.Bind(NumConfigDefinition, Configuration.DefaultRoleCount);
config.Bind(ChanceConfigDefinition, Configuration.DefaultChance);
}
/// <summary>
/// Saves the role's configuration to a preset ConfigFile.
/// </summary>
/// <param name="presetConfig">The ConfigFile to save the preset configuration to.</param>
/// <param name="useDefault">Whether to use the default values for the configuration.</param>
public virtual void SaveToPreset(ConfigFile presetConfig, bool useDefault=false)
{
BindConfig(presetConfig);
presetConfig[NumConfigDefinition].BoxedValue = useDefault ? Configuration.DefaultRoleCount : GetCount();
presetConfig[ChanceConfigDefinition].BoxedValue = useDefault ? Configuration.DefaultChance : GetChance();
}
/// <summary>
/// Loads the role's configuration from a preset ConfigFile.
/// </summary>
/// <param name="presetConfig">The ConfigFile containing the preset configuration.</param>
public virtual void LoadFromPreset(ConfigFile presetConfig)
{
if (presetConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry<int> numEntry))
{
SetCount(numEntry.Value);
}
if (presetConfig.TryGetEntry(ChanceConfigDefinition, out ConfigEntry<int> chanceEntry))
{
SetChance(chanceEntry.Value);
}
}
/// <summary> /// <summary>
/// Gets the role chance option. /// Gets the role chance option.
/// </summary> /// </summary>
@@ -175,6 +222,14 @@ public interface ICustomRole : IOptionable
return true; return true;
} }
/// <summary>
/// This method runs on the HudManager.Update method ONLY when the LOCAL player has this role.
/// </summary>
/// <param name="hudManager">Reference to HudManager instance.</param>
void HudUpdate(HudManager hudManager)
{
}
/// <summary> /// <summary>
/// Gets a custom ejection message for the role. Return null to use the default message. /// Gets a custom ejection message for the role. Return null to use the default message.
/// </summary> /// </summary>
-14
View File
@@ -1,22 +1,8 @@
namespace MiraAPI.Roles; namespace MiraAPI.Roles;
/// <summary>
/// Enum for modded role teams.
/// </summary>
public enum ModdedRoleTeams public enum ModdedRoleTeams
{ {
/// <summary>
/// The crewmate team.
/// </summary>
Crewmate, Crewmate,
/// <summary>
/// The impostor team.
/// </summary>
Impostor, Impostor,
/// <summary>
/// Custom team, catchall for any other teams.
/// </summary>
Custom, Custom,
} }
+1 -16
View File
@@ -2,18 +2,9 @@
namespace MiraAPI.Roles; namespace MiraAPI.Roles;
/// <summary>
/// Utility class to get the ID of a role.
/// </summary>
public static class RoleId public static class RoleId
{ {
/// <summary> public static ushort Get<T>() where T : RoleBehaviour
/// Gets the ID of a role.
/// </summary>
/// <typeparam name="T">The type of the role.</typeparam>
/// <returns>The role ID as a ushort.</returns>
/// <exception cref="InvalidOperationException">Thrown when the role is not registered.</exception>
public static ushort Get<T>() where T : ICustomRole
{ {
if (!CustomRoleManager.RoleIds.TryGetValue(typeof(T), out var roleId)) if (!CustomRoleManager.RoleIds.TryGetValue(typeof(T), out var roleId))
{ {
@@ -23,12 +14,6 @@ public static class RoleId
return roleId; return roleId;
} }
/// <summary>
/// Gets the ID of a role.
/// </summary>
/// <param name="type">The type of the role.</param>
/// <returns>The ID as a ushort.</returns>
/// <exception cref="InvalidOperationException">Thrown when the role is not registered.</exception>
public static ushort Get(Type type) public static ushort Get(Type type)
{ {
if (!CustomRoleManager.RoleIds.TryGetValue(type, out var roleId)) if (!CustomRoleManager.RoleIds.TryGetValue(type, out var roleId))
@@ -0,0 +1,25 @@
using Reactor.Utilities.Extensions;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// Wrapper class to turn any UnityEngine.Object into a <see cref="LoadableAsset{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the asset to be loaded. Must be a subclass of <see cref="UnityEngine.Object"/>.</typeparam>
public class LoadableAssetWrapper<T> : LoadableAsset<T> where T : UnityEngine.Object
{
/// <summary>
/// Initializes a new instance of the <see cref="LoadableAssetWrapper{T}"/> class.
/// </summary>
/// <param name="asset">The asset to wrap. This should be a reference to a UnityEngine.Object.</param>
public LoadableAssetWrapper(T asset)
{
LoadedAsset = asset.DontDestroy();
}
/// <inheritdoc />
public override T LoadAsset()
{
return LoadedAsset ?? throw new System.InvalidOperationException("The asset could not be loaded!");
}
}
@@ -7,7 +7,6 @@ namespace MiraAPI.Utilities.Assets;
/// A utility class for loading assets from embedded resources. /// A utility class for loading assets from embedded resources.
/// </summary> /// </summary>
/// <param name="path">The path to the embedded resource.</param> /// <param name="path">The path to the embedded resource.</param>
/// <param name="pixelsPerUnit">The pixels per unit for the loaded sprite.</param>
public class LoadableResourceAsset(string path, float pixelsPerUnit=100f) : LoadableAsset<Sprite> public class LoadableResourceAsset(string path, float pixelsPerUnit=100f) : LoadableAsset<Sprite>
{ {
private readonly Assembly _assembly = Assembly.GetCallingAssembly(); private readonly Assembly _assembly = Assembly.GetCallingAssembly();
@@ -15,9 +14,9 @@ public class LoadableResourceAsset(string path, float pixelsPerUnit=100f) : Load
/// <inheritdoc /> /// <inheritdoc />
public override Sprite LoadAsset() public override Sprite LoadAsset()
{ {
if (LoadedAsset) if (LoadedAsset != null)
{ {
return LoadedAsset!; return LoadedAsset;
} }
return LoadedAsset = SpriteTools.LoadSpriteFromPath(path, _assembly, pixelsPerUnit); return LoadedAsset = SpriteTools.LoadSpriteFromPath(path, _assembly, pixelsPerUnit);
+41 -23
View File
@@ -4,57 +4,75 @@ using UnityEngine;
namespace MiraAPI.Utilities.Assets; namespace MiraAPI.Utilities.Assets;
/// <summary> /// <summary>
/// A static class that contains various assets used in the Mira API. /// Various assets used throughout Mira API.
/// </summary> /// </summary>
public static class MiraAssets public static class MiraAssets
{ {
static MiraAssets()
{
var boxTex = SpriteTools.LoadTextureFromResourcePath(
"MiraAPI.Resources.RoundedBox.png",
System.Reflection.Assembly.GetCallingAssembly());
var boxSprite = Sprite.Create(
boxTex,
new Rect(0, 0, boxTex.width, boxTex.height),
new Vector2(0.5f, 0.5f),
100f,
0U,
SpriteMeshType.Tight,
new Vector4(20, 20, 20, 20));
RoundedBox = new LoadableAssetWrapper<Sprite>(boxSprite);
}
internal static readonly AssetBundle MiraAssetBundle = AssetBundleManager.Load("mirabundle");
internal static readonly LoadableAsset<GameObject> ModifierDisplay = new LoadableBundleAsset<GameObject>("Modifiers", MiraAssetBundle);
internal static readonly LoadableAsset<GameObject> PresetSavePopup = new LoadableBundleAsset<GameObject>("PresetSavePopup", MiraAssetBundle);
internal static readonly LoadableAsset<Sprite> RefreshIcon = new LoadableBundleAsset<Sprite>("refresh", MiraAssetBundle);
internal static readonly LoadableAsset<Sprite> FolderIcon = new LoadableBundleAsset<Sprite>("freePlay_folderTaskRoom", MiraAssetBundle);
/// <summary> /// <summary>
/// Gets the color used for teal highlighting in UI. /// Gets the accepted teal color used for various selections in the UI.
/// </summary> /// </summary>
public static Color32 AcceptedTeal { get; } = new(43, 233, 198, 255); public static Color32 AcceptedTeal { get; } = new(43, 233, 198, 255);
/// <summary> /// <summary>
/// Gets the Mira API asset bundle. /// Gets the empty sprite.
/// </summary> /// </summary>
public static AssetBundle MiraAssetBundle { get; } = AssetBundleManager.Load("mirabundle"); public static LoadableAsset<Sprite> Empty { get; } = new LoadableResourceAsset("MiraAPI.Resources.Empty.png");
/// <summary> /// <summary>
/// Gets the ModifierDisplay prefab. /// Gets the RoundedBox sprite, which is a rounded rectangle used for UI elements.
/// </summary> /// </summary>
public static LoadableAsset<GameObject> ModifierDisplay { get; } = new LoadableBundleAsset<GameObject>("Modifiers", MiraAssetBundle); public static LoadableAsset<Sprite> RoundedBox { get; }
/// <summary> /// <summary>
/// Gets the empty sprite asset. /// Gets the NextButton sprite.
/// </summary> /// </summary>
public static LoadableResourceAsset Empty { get; } = new("MiraAPI.Resources.Empty.png"); public static LoadableAsset<Sprite> NextButton { get; } = new LoadableResourceAsset("MiraAPI.Resources.NextButton.png");
/// <summary> /// <summary>
/// Gets the Next Button sprite. /// Gets the active variant of the NextButton sprite.
/// </summary> /// </summary>
public static LoadableResourceAsset NextButton { get; } = new("MiraAPI.Resources.NextButton.png"); public static LoadableAsset<Sprite> NextButtonActive { get; } = new LoadableResourceAsset("MiraAPI.Resources.NextButtonActive.png");
/// <summary> /// <summary>
/// Gets the highlighted Next Button sprite. /// Gets the Cog icon sprite.
/// </summary> /// </summary>
public static LoadableResourceAsset NextButtonActive { get; } = new("MiraAPI.Resources.NextButtonActive.png"); public static LoadableAsset<Sprite> Cog { get; } = new LoadableResourceAsset("MiraAPI.Resources.Cog.png");
/// <summary> /// <summary>
/// Gets the Cog icon used in Role Settings Menu. /// Gets the Checkmark icon sprite.
/// </summary> /// </summary>
public static LoadableResourceAsset Cog { get; } = new("MiraAPI.Resources.Cog.png"); public static LoadableAsset<Sprite> Checkmark { get; } = new LoadableResourceAsset("MiraAPI.Resources.Checkmark.png");
/// <summary> /// <summary>
/// Gets the Checkmark Box used in the Settings Menu. /// Gets the Checkmark Box sprite.
/// </summary> /// </summary>
public static LoadableResourceAsset CheckmarkBox { get; } = new("MiraAPI.Resources.CheckMarkBox.png"); public static LoadableAsset<Sprite> CheckmarkBox { get; } = new LoadableResourceAsset("MiraAPI.Resources.CheckMarkBox.png");
/// <summary> /// <summary>
/// Gets the Checkmark used in the Settings Menu. /// Gets the custom category header sprite used in the options menu.
/// </summary> /// </summary>
public static LoadableResourceAsset Checkmark { get; } = new("MiraAPI.Resources.Checkmark.png"); public static LoadableAsset<Sprite> CategoryHeader { get; } = new LoadableResourceAsset("MiraAPI.Resources.CategoryHeader.png");
/// <summary>
/// Gets the white CategoryHeader used in the Settings Menu.
/// </summary>
public static LoadableResourceAsset CategoryHeader { get; } = new("MiraAPI.Resources.CategoryHeader.png");
} }
+19 -7
View File
@@ -11,14 +11,13 @@ namespace MiraAPI.Utilities.Assets;
public static class SpriteTools public static class SpriteTools
{ {
/// <summary> /// <summary>
/// Load a sprite from a resource path. /// Loads and returns a texture from a resource path using the specified assembly.
/// </summary> /// </summary>
/// <param name="resourcePath">The path to the resource.</param> /// <param name="resourcePath">The path to the resource within the assembly.</param>
/// <param name="assembly">The assembly containing the resource.</param> /// <param name="assembly">The assembly from which to load the resource.</param>
/// <param name="pixelsPerUnit">The pixels per unit for the loaded sprite.</param> /// <returns>A <see cref="Texture2D"/> object loaded from the specified resource path.</returns>
/// <returns>A sprite made from the resource.</returns> /// <exception cref="ArgumentException">Thrown when the resource cannot be found in the specified assembly.</exception>
/// <exception cref="Exception">The resource cannot be found.</exception> public static Texture2D LoadTextureFromResourcePath(string resourcePath, Assembly assembly)
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
{ {
var tex = new Texture2D(1, 1, TextureFormat.ARGB32, false); var tex = new Texture2D(1, 1, TextureFormat.ARGB32, false);
var myStream = assembly.GetManifestResourceStream(resourcePath); var myStream = assembly.GetManifestResourceStream(resourcePath);
@@ -33,6 +32,19 @@ public static class SpriteTools
} }
tex.name = resourcePath; tex.name = resourcePath;
return tex;
}
/// <summary>
/// Loads and returns a <see cref="Sprite"/> from a resource path using the specified assembly.
/// </summary>
/// <param name="resourcePath">The path to the resource within the assembly.</param>
/// <param name="assembly">The assembly from which to load the resource.</param>
/// <param name="pixelsPerUnit">The number of pixels per unit for the sprite.</param>
/// <returns>A <see cref="Sprite"/> object created from the texture loaded from the specified resource path.</returns>
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
{
var tex = LoadTextureFromResourcePath(resourcePath, assembly);
var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
sprite.name = resourcePath; sprite.name = resourcePath;
return sprite; return sprite;
+43
View File
@@ -41,6 +41,49 @@ public static class Extensions
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
} }
/// <summary>
/// Used if you override Minigame.Close.
/// </summary>
/// <param name="self">The minigame.</param>
public static void BaseClose(this Minigame self)
{
bool isComplete;
if (self.amClosing == Minigame.CloseState.Closing)
{
UnityEngine.Object.Destroy(self.gameObject);
return;
}
if (self.CloseSound && Constants.ShouldPlaySfx())
{
SoundManager.Instance.PlaySound(self.CloseSound, false, 1f, null);
}
if (PlayerControl.LocalPlayer.Data.Role.TeamType == RoleTeamTypes.Crewmate)
{
GameManager.Instance.LogicMinigame.OnMinigameClose();
}
if (PlayerControl.LocalPlayer)
{
PlayerControl.HideCursorTemporarily();
}
self.amClosing = Minigame.CloseState.Closing;
self.logger.Info(string.Concat("Closing minigame ", self.GetType().Name));
IAnalyticsReporter analytics = DestroyableSingleton<DebugAnalytics>.Instance.Analytics;
NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data;
TaskTypes taskType = self.TaskType;
float realtimeSinceStartup = Time.realtimeSinceStartup - self.timeOpened;
PlayerTask myTask = self.MyTask;
if (myTask != null)
{
isComplete = myTask.IsComplete;
}
else
{
isComplete = false;
}
analytics.MinigameClosed(data, taskType, realtimeSinceStartup, isComplete);
self.StartCoroutine(self.CoDestroySelf());
}
/// <summary> /// <summary>
/// Gets a PlayerControl from their PlayerVoteArea in a meeting. /// Gets a PlayerControl from their PlayerVoteArea in a meeting.
/// </summary> /// </summary>
-19
View File
@@ -1,27 +1,8 @@
namespace MiraAPI.Utilities; namespace MiraAPI.Utilities;
/// <summary>
/// Suffixes used for options numbers.
/// </summary>
public enum MiraNumberSuffixes public enum MiraNumberSuffixes
{ {
/// <summary>
/// No suffix.
/// </summary>
None, None,
/// <summary>
/// Multiplier suffix (x).
/// </summary>
Multiplier, Multiplier,
/// <summary>
/// Seconds suffix (s).
/// </summary>
Seconds, Seconds,
/// <summary>
/// Percentage suffix (%).
/// </summary>
Percent, Percent,
} }