Begin seperation

This commit is contained in:
ang-xd committed 2024-05-13 18:40:06 -04:00
1 parent 0271430647
commit 1c25fd401b
41 files changed
+2728

No files matched your search

+138
View File
@@ -0,0 +1,138 @@
using System.Collections.Generic;
namespace MiraAPI.GameModes;
/// <summary>
/// Base class for custom gamemodes
/// </summary>
public abstract class CustomGameMode
{
/// <summary>
/// Game mode name
/// </summary>
public abstract string Name { get; }
/// <summary>
/// Game mode description
/// </summary>
public abstract string Description { get; }
/// <summary>
/// Game mode ID
/// </summary>
public abstract int Id { get; }
/// <summary>
/// Called when Intro Cutscene is destroyed
/// </summary>
public virtual void Initialize() { }
/// <summary>
/// Called on HudManager.Start()
/// </summary>
/// <param name="instance">Instance of HudManager</param>
public virtual void HudStart(HudManager instance) { }
/// <summary>
/// Called every frame on HudManager.Update()
/// </summary>
/// <param name="instance">Instance of HudManager</param>
public virtual void HudUpdate(HudManager instance) { }
/// <summary>
/// Called when a player is killed
/// </summary>
/// <param name="player">PlayerControl that was killed</param>
public virtual void OnDeath(PlayerControl player) { }
/// <summary>
/// Calculate Game End logic
/// </summary>
/// <param name="runOriginal">Should original game end logic be used</param>
/// <param name="instance">Instance of LogicGameFlowNormal</param>
public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance)
{
runOriginal = true;
}
/// <summary>
/// Custom role assignment
/// </summary>
/// <param name="runOriginal">Should original role assignment be used</param>
/// <param name="instance">Instance of LogicRoleSelection</param>
public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance)
{
runOriginal = true;
}
/// <summary>
/// Custom murder logic
/// </summary>
/// <param name="runOriginal">Should original murder logic be used</param>
/// <param name="result">Result of custom murder logic</param>
/// <param name="target">Target player for murder</param>
public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target)
{
result = false;
runOriginal = true;
}
/// <summary>
/// Should Roles Settings be available when this gamemode is selected?
/// </summary>
public virtual bool CanAccessRolesTab() => true;
/// <summary>
/// Should Game Settings be available when this gamemode is selected?
/// </summary>
public virtual bool CanAccessSettingsTab() => true;
/// <summary>
/// Custom winner selection
/// </summary>
/// <returns>List of winners</returns>
public virtual List<GameData.PlayerInfo> CalculateWinners() => null;
/// <summary>
/// Show gamemode in Intro Cutscene
/// </summary>
public virtual bool ShowCustomRoleScreen() => false;
/// <summary>
/// Can Admin be used in this gamemode
/// </summary>
/// <param name="console">Admin Console</param>
public virtual bool CanUseMapConsole(MapConsole console) => true;
/// <summary>
/// Can a body be reported in this gamemode
/// </summary>
/// <param name="body">Target body for reporting</param>
public virtual bool CanReport(DeadBody body) => true;
/// <summary>
/// Can system consoles be used in this gamemode
/// </summary>
/// <param name="console">System Console</param>
public virtual bool CanUseSystemConsole(SystemConsole console) => true;
/// <summary>
/// Can tasks be interacted with in this gamemode
/// </summary>
/// <param name="console">Task console</param>
public virtual bool CanUseConsole(Console console) => true;
/// <summary>
/// Should the sabotage map be used when attempting to open sabotage overlay
/// </summary>
/// <param name="map">MapBehaviour object</param>
public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true;
/// <summary>
/// Can a player vent in this gamemode
/// </summary>
/// <param name="vent">Target vent</param>
/// <param name="playerInfo">Player attempting to vent</param>
public virtual bool CanVent(Vent vent, GameData.PlayerInfo playerInfo) => true;
}
@@ -0,0 +1,71 @@
using Reactor.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiraAPI.GameModes;
/// <summary>
/// Manages custom gamemodes
/// </summary>
public static class CustomGameModeManager
{
/// <summary>
/// List of registered gamemodes
/// </summary>
public static readonly Dictionary<int, CustomGameMode> GameModes = [];
public static bool IsDefault()
{
return ActiveMode.Id == 0;
}
/// <summary>
/// Current gamemode
/// </summary>
public static CustomGameMode ActiveMode;
/// <summary>
/// Set current gamemode
/// </summary>
/// <param name="id">gamemode ID</param>
public static void SetGameMode(int id)
{
if (GameModes.TryGetValue(id, out var gameMode))
{
ActiveMode = gameMode;
return;
}
Logger<MiraAPIPlugin>.Error($"No gamemode with id {id} found!");
}
/// <summary>
/// Register gamemode from type
/// </summary>
/// <param name="gameModeType">Type of gamemode class, should inherit from <see cref="CustomGameMode"/></param>
public static void RegisterGameMode(Type gameModeType)
{
if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType))
{
Logger<MiraAPIPlugin>.Warning($"{gameModeType?.Name} does not inherit CustomGameMode!");
return;
}
var gameMode = (CustomGameMode)Activator.CreateInstance(gameModeType);
if (GameModes.Any(x => x.Key == gameMode?.Id))
{
Logger<MiraAPIPlugin>.Error($"ID for gamemode {gameMode?.Name} already exists!");
return;
}
if (gameMode == null)
{
Logger<MiraAPIPlugin>.Error($"GAMEMODE WITH TYPE {gameModeType.Name} IS NULL");
return;
}
GameModes.Add(gameMode.Id, gameMode);
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace MiraAPI.GameModes;
[RegisterGameMode]
public class DefaultMode : CustomGameMode
{
public override string Name => "Default";
public override string Description => "Default Among Us GameMode";
public override int Id => 0;
}
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MiraAPI.GameModes;
public class RegisterGameModeAttribute : Attribute
{
private static readonly HashSet<Assembly> RegisteredAssemblies = [];
public static void Register(Assembly assembly)
{
if (!RegisteredAssemblies.Add(assembly)) return;
foreach (var type in assembly.GetTypes())
{
var attribute = type.GetCustomAttribute<RegisterGameModeAttribute>();
if (attribute != null)
{
CustomGameModeManager.RegisterGameMode(type);
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using MiraAPI.Roles;
using Reactor.Localization.Utilities;
using System;
namespace MiraAPI.API.GameOptions;
public abstract class AbstractGameOption
{
public string Title { get; }
public StringNames StringName { get; }
public Type AdvancedRole { get; }
public bool Save { get; }
public bool ShowInHideNSeek { get; init; }
public CustomOptionGroup Group { get; set; }
public Func<bool> Hidden { get; set; }
public OptionBehaviour OptionBehaviour { get; protected set; }
public void ValueChanged(OptionBehaviour optionBehaviour)
{
OnValueChanged(optionBehaviour);
CustomOptionsManager.SyncOptions();
}
protected abstract void OnValueChanged(OptionBehaviour optionBehaviour);
protected AbstractGameOption(string title, Type roleType, bool save)
{
Title = title;
StringName = CustomStringName.CreateAndRegister(Title);
if (roleType is not null && roleType.IsAssignableTo(typeof(ICustomRole)))
{
AdvancedRole = roleType;
}
Save = save;
Hidden = () => false;
CustomOptionsManager.CustomOptions.Add(this);
}
}
+102
View File
@@ -0,0 +1,102 @@
using BepInEx.Configuration;
using Reactor.Utilities;
using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.API.GameOptions;
public class CustomNumberOption : AbstractGameOption
{
public float Value { get; private set; }
public float Min { get; }
public float Max { get; }
public float Increment { get; }
public NumberSuffixes SuffixType { get; }
public string NumberFormat { get; }
public float Default { get; }
public bool ZeroInfinity { get; }
public ConfigEntry<float> Config { get; }
public Action<float> ChangedEvent { get; set; }
public CustomNumberOption(string title, float defaultValue, float min, float max, float increment, NumberSuffixes suffixType, bool zeroInfinity = false, string numberFormat = "0", Type role = null, bool save = true) : base(title, role, save)
{
Value = defaultValue;
Default = defaultValue;
Min = min;
Max = max;
Increment = increment;
SuffixType = suffixType;
NumberFormat = numberFormat;
ZeroInfinity = zeroInfinity;
if (Save)
{
try
{
Config = MiraAPIPlugin.Instance.Config.Bind("Number Options", title, defaultValue);
SetValue(Save ? Config.Value : defaultValue);
}
catch (Exception e)
{
Logger<MiraAPIPlugin>.Warning(e.ToString());
}
}
CustomOptionsManager.CustomNumberOptions.Add(this);
}
public void SetValue(float newValue)
{
newValue = Mathf.Clamp(newValue, Min, Max);
if (Save && (!AmongUsClient.Instance || AmongUsClient.Instance.AmHost))
{
try
{
Config.Value = newValue;
}
catch (Exception e)
{
Logger<MiraAPIPlugin>.Warning(e.ToString());
}
}
Value = newValue;
var behaviour = (NumberOption)OptionBehaviour;
if (behaviour)
{
behaviour.Value = Value;
}
ChangedEvent?.Invoke(Value);
}
public NumberOption CreateNumberOption(NumberOption original, Transform container)
{
var numberOption = Object.Instantiate(original, container);
numberOption.name = Title;
numberOption.Title = StringName;
numberOption.Value = Value;
numberOption.Increment = Increment;
numberOption.SuffixType = SuffixType;
numberOption.FormatString = NumberFormat;
numberOption.ValidRange = new FloatRange(Min, Max);
numberOption.ZeroIsInfinity = ZeroInfinity;
numberOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
numberOption.OnEnable();
OptionBehaviour = numberOption;
return numberOption;
}
protected override void OnValueChanged(OptionBehaviour optionBehaviour)
{
SetValue(optionBehaviour.GetFloat());
}
}
+46
View File
@@ -0,0 +1,46 @@
using MiraAPI.Roles;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace MiraAPI.API.GameOptions;
public class CustomOptionGroup
{
public string Title { get; }
public Func<bool> Hidden { get; set; }
public GameObject Header;
public Type AdvancedRole { get; set; }
public readonly List<AbstractGameOption> Options = [];
public readonly List<CustomNumberOption> CustomNumberOptions;
public readonly List<CustomToggleOption> CustomToggleOptions;
public readonly List<CustomStringOption> CustomStringOptions;
public CustomOptionGroup(string title, List<CustomNumberOption> numberOpt,
List<CustomToggleOption> toggleOpt, List<CustomStringOption> stringOpt, Type role = null)
{
Title = title;
if (role is not null && role.IsAssignableTo(typeof(ICustomRole)))
{
AdvancedRole = role;
}
Hidden = () => false;
CustomNumberOptions = numberOpt;
CustomToggleOptions = toggleOpt;
CustomStringOptions = stringOpt;
Options.AddRange(CustomNumberOptions);
Options.AddRange(CustomToggleOptions);
Options.AddRange(CustomStringOptions);
foreach (var option in Options)
{
option.Group = this;
}
CustomOptionsManager.CustomGroups.Add(this);
}
}
+108
View File
@@ -0,0 +1,108 @@
using MiraAPI.Networking.Options;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using System.Collections.Generic;
using System.Linq;
namespace MiraAPI.API.GameOptions;
public static class CustomOptionsManager
{
public static readonly List<AbstractGameOption> CustomOptions = [];
public static readonly List<CustomNumberOption> CustomNumberOptions = [];
public static readonly List<CustomToggleOption> CustomToggleOptions = [];
public static readonly List<CustomStringOption> CustomStringOptions = [];
public static readonly List<CustomOptionGroup> CustomGroups = [];
public static void SyncOptions()
{
if (!AmongUsClient.Instance.AmHost)
{
return;
}
var toggles = CustomToggleOptions.Select(x => x.Value).ToArray();
var numbers = CustomNumberOptions.Select(x => x.Value).ToArray();
var strings = CustomStringOptions.Select(x => x.IndexValue).ToArray();
Rpc<SyncOptionsRpc>.Instance.Send(new SyncOptionsRpc.Data(toggles, numbers, strings));
}
public static void SyncOptions(int targetId)
{
if (!AmongUsClient.Instance.AmHost)
{
return;
}
var toggles = CustomToggleOptions.Select(x => x.Value).ToArray();
var numbers = CustomNumberOptions.Select(x => x.Value).ToArray();
var strings = CustomStringOptions.Select(x => x.IndexValue).ToArray();
Rpc<SyncOptionsRpc>.Instance.SendTo(targetId, new SyncOptionsRpc.Data(toggles, numbers, strings));
}
public static void UpdateToConfig()
{
foreach (var numberOpt in CustomNumberOptions)
{
numberOpt.SetValue(numberOpt.Save ? numberOpt.Config.Value : numberOpt.Default);
}
foreach (var toggleOpt in CustomToggleOptions)
{
toggleOpt.SetValue(toggleOpt.Save ? toggleOpt.Config.Value : toggleOpt.Default);
}
foreach (var stringOpt in CustomStringOptions)
{
stringOpt.SetValue(stringOpt.Save ? stringOpt.Config.Value : stringOpt.Default);
}
}
public static void ResetToDefault()
{
foreach (var numberOpt in CustomNumberOptions)
{
numberOpt.SetValue(numberOpt.Default);
}
foreach (var stringOpt in CustomStringOptions)
{
stringOpt.SetValue(stringOpt.Default);
}
foreach (var toggleOpt in CustomToggleOptions)
{
toggleOpt.SetValue(toggleOpt.Default);
}
foreach (var option in CustomOptions)
{
if (!option.OptionBehaviour)
{
continue;
}
option.ValueChanged(option.OptionBehaviour);
}
}
public static void HandleOptionsSync(bool[] toggles, float[] numbers, int[] strings)
{
Logger<MiraAPIPlugin>.Warning("syncing options!");
for (var i = 0; i < toggles.Length; i++)
{
CustomToggleOptions[i].SetValue(toggles[i]);
}
for (var i = 0; i < numbers.Length; i++)
{
CustomNumberOptions[i].SetValue(numbers[i]);
}
for (var i = 0; i < strings.Length; i++)
{
CustomStringOptions[i].SetValue(strings[i]);
}
}
}
+162
View File
@@ -0,0 +1,162 @@
using Reactor.Utilities.Extensions;
using System;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.API.GameOptions;
public static class CustomOptionsTab
{
public static GameObject CustomTab;
public static GameObject CustomScreen;
public static SpriteRenderer Rend;
public static GameObject Initialize(GameSettingMenu __instance)
{
var gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject;
var roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject;
CustomScreen = CreateNewMenu(__instance);
CustomTab = CreateCustomTab(__instance, CustomScreen, gameBtn, roleBtn);
Rend = CustomTab.transform.FindChild("LaunchpadBtn/Tab Background").GetComponent<SpriteRenderer>();
Rend.enabled = false;
UpdateListeners(__instance, gameBtn.GetComponentInChildren<PassiveButton>(), roleBtn.GetComponentInChildren<PassiveButton>(), Rend);
var container = CustomScreen.transform.FindChild("GameGroup/SliderInner");
container.DestroyChildren();
CreateNewResetButton(__instance, container);
return container.gameObject;
}
private static void CreateNewResetButton(GameSettingMenu __instance, Transform container)
{
var resetBtn = __instance.RegularGameSettings.transform.FindChild("GameGroup/SliderInner/ResetToDefault");
var newResetBtn = Object.Instantiate(resetBtn.gameObject, container);
newResetBtn.gameObject.name = "LaunchpadReset";
var tmp = newResetBtn.GetComponentInChildren<TextMeshPro>();
tmp.text = "Reset Options";
tmp.alignment = TextAlignmentOptions.Center;
tmp.gameObject.transform.localPosition = new Vector3(0, 0, 0);
newResetBtn.GetComponent<ToggleOption>().Destroy();
newResetBtn.transform.FindChild("CheckBox").gameObject.Destroy();
var toggle = newResetBtn.GetComponent<PassiveButton>();
toggle.OnClick.RemoveAllListeners();
toggle.OnMouseOver.RemoveAllListeners();
toggle.OnMouseOut.RemoveAllListeners();
toggle.OnClick.AddListener((UnityAction)(() => { CustomOptionsManager.ResetToDefault(); }));
toggle.OnMouseOver.AddListener((UnityAction)(() => { tmp.text = "<b>Reset Options</b>"; }));
toggle.OnMouseOut.AddListener((UnityAction)(() => { tmp.text = "Reset Options"; }));
}
private static void UpdateListeners(GameSettingMenu __instance, PassiveButton gameB, PassiveButton roleB, SpriteRenderer rend)
{
gameB.OnClick.RemoveAllListeners();
gameB.OnClick.AddListener((UnityAction)(() =>
{
__instance.RegularGameSettings.SetActive(true);
__instance.RolesSettings.gameObject.SetActive(false);
CustomScreen.gameObject.SetActive(false);
rend.enabled = false;
__instance.GameSettingsHightlight.enabled = true;
__instance.RolesSettingsHightlight.enabled = false;
}));
roleB.OnClick.RemoveAllListeners();
roleB.OnClick.AddListener((UnityAction)(() =>
{
__instance.RegularGameSettings.SetActive(false);
__instance.RolesSettings.gameObject.SetActive(true);
CustomScreen.gameObject.SetActive(false);
rend.enabled = false;
__instance.GameSettingsHightlight.enabled = false;
__instance.RolesSettingsHightlight.enabled = true;
}));
}
public static GameObject CreateHeader(ToggleOption toggleOpt, Transform container, string title)
{
var header = Object.Instantiate(toggleOpt, container);
header.Title = StringNames.None;
header.TitleText.text = title;
header.name = "Header";
var checkBox = header.transform.FindChild("CheckBox")?.gameObject;
if (checkBox)
{
checkBox.Destroy();
}
var background = header.transform.FindChild("Background")?.gameObject;
if (background)
{
background.Destroy();
}
header.GetComponent<OptionBehaviour>().Destroy();
return header.gameObject;
}
public static GameObject CreateCustomTab(GameSettingMenu __instance, GameObject newSettings,
GameObject gameTab, GameObject roleTab)
{
var newTab = Object.Instantiate(gameTab, gameTab.transform.parent);
newTab.name = "LaunchpadTab";
gameTab.transform.position += new Vector3(-1, 0, 0);
var inside = newTab.transform.FindChild("ColorButton");
inside.name = "LaunchpadBtn";
var btn = inside.GetComponentInChildren<PassiveButton>();
btn.OnClick.RemoveAllListeners();
btn.OnClick.AddListener((Action)TabAction);
var spriteRend = inside.GetComponentInChildren<SpriteRenderer>();
//spriteRend.sprite = LaunchpadAssets.HackButton.LoadAsset();
spriteRend.gameObject.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f);
return newTab;
void TabAction()
{
__instance.RegularGameSettings.SetActive(false);
__instance.RolesSettings.gameObject.SetActive(false);
newSettings.gameObject.SetActive(true);
var rend = inside.transform.FindChild("Tab Background").GetComponent<SpriteRenderer>();
rend.enabled = true;
gameTab.transform.FindChild("ColorButton/Tab Background").gameObject.GetComponent<SpriteRenderer>().enabled = false;
roleTab.transform.FindChild("Hat Button/Tab Background").gameObject.GetComponent<SpriteRenderer>().enabled = false;
}
}
public static GameObject CreateNewMenu(GameSettingMenu __instance)
{
var gameSettings = __instance.RegularGameSettings;
var newSettings = Object.Instantiate(gameSettings, gameSettings.transform.parent);
newSettings.name = "Launchpad Settings";
newSettings.SetActive(false);
var launchpadGroup = newSettings.transform.FindChild("GameGroup").gameObject;
var text = launchpadGroup.transform.FindChild("Text").gameObject.GetComponent<TextMeshPro>();
text.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
text.text = "Launchpad Settings";
return newSettings;
}
}
+93
View File
@@ -0,0 +1,93 @@
using BepInEx.Configuration;
using Reactor.Localization.Utilities;
using Reactor.Utilities;
using System;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.API.GameOptions;
public class CustomStringOption : AbstractGameOption
{
public int IndexValue { get; private set; }
public string Value => Options[IndexValue];
public int Default { get; }
public string[] Options { get; private set; }
public ConfigEntry<int> Config { get; }
public Action<int, string> ChangedEvent { get; init; }
public CustomStringOption(string title, int defaultValue, string[] options, Type role = null, bool save = true) : base(title, role, save)
{
IndexValue = defaultValue;
Options = options;
Default = defaultValue;
CustomOptionsManager.CustomStringOptions.Add(this);
if (Save)
{
try
{
Config = MiraAPIPlugin.Instance.Config.Bind("String Options", title, defaultValue);
SetValue(Save ? Config.Value : defaultValue);
}
catch (Exception e)
{
Logger<MiraAPIPlugin>.Warning(e.ToString());
}
}
}
public void SetValue(int newValue)
{
if (Save && (!AmongUsClient.Instance || AmongUsClient.Instance.AmHost))
{
try
{
Config.Value = newValue;
}
catch (Exception e)
{
Logger<MiraAPIPlugin>.Warning(e.ToString());
}
}
var oldValue = IndexValue;
IndexValue = newValue;
var behaviour = (StringOption)OptionBehaviour;
if (behaviour)
{
behaviour.Value = newValue;
}
if (oldValue != newValue)
{
ChangedEvent?.Invoke(newValue, Value);
}
}
public void SetValue(string newValue) => SetValue(Options.ToList().IndexOf(newValue));
protected override void OnValueChanged(OptionBehaviour optionBehaviour)
{
SetValue(optionBehaviour.GetInt());
}
public StringOption CreateStringOption(StringOption original, Transform container)
{
var stringOption = Object.Instantiate(original, container);
stringOption.name = Title;
stringOption.Title = StringName;
stringOption.Value = Options.ToList().IndexOf(Value);
stringOption.Values = Options.Select(CustomStringName.CreateAndRegister).ToArray();
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
stringOption.OnEnable();
OptionBehaviour = stringOption;
return stringOption;
}
}
+79
View File
@@ -0,0 +1,79 @@
using BepInEx.Configuration;
using Reactor.Utilities;
using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.API.GameOptions;
public class CustomToggleOption : AbstractGameOption
{
public bool Value { get; private set; }
public bool Default { get; }
public ConfigEntry<bool> Config { get; }
public Action<bool> ChangedEvent { get; init; }
public CustomToggleOption(string title, bool defaultValue, Type role = null, bool save = true) : base(title, role, save)
{
Default = defaultValue;
if (Save)
{
try
{
Config = MiraAPIPlugin.Instance.Config.Bind("Toggle Options", title, defaultValue);
SetValue(Save ? Config.Value : defaultValue);
}
catch (Exception e)
{
Logger<MiraAPIPlugin>.Warning(e.ToString());
}
}
CustomOptionsManager.CustomToggleOptions.Add(this);
}
public void SetValue(bool newValue)
{
if (Save && (!AmongUsClient.Instance || AmongUsClient.Instance.AmHost))
{
try
{
Config.Value = newValue;
}
catch (Exception e)
{
Logger<MiraAPIPlugin>.Warning(e.ToString());
}
}
var oldValue = Value;
Value = newValue;
var behaviour = (ToggleOption)OptionBehaviour;
if (behaviour)
{
behaviour.CheckMark.enabled = newValue;
}
if (newValue != oldValue)
{
ChangedEvent?.Invoke(newValue);
}
}
protected override void OnValueChanged(OptionBehaviour optionBehaviour)
{
SetValue(optionBehaviour.GetBool());
}
public ToggleOption CreateToggleOption(ToggleOption original, Transform container)
{
var toggleOption = Object.Instantiate(original, container);
toggleOption.name = Title;
toggleOption.Title = StringName;
toggleOption.CheckMark.enabled = Value;
toggleOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
toggleOption.OnEnable();
OptionBehaviour = toggleOption;
return toggleOption;
}
}
+165
View File
@@ -0,0 +1,165 @@
using MiraAPI.Utilities;
using UnityEngine;
using UnityEngine.Events;
namespace MiraAPI.Hud;
public abstract class CustomActionButton
{
public abstract string Name { get; }
public abstract float Cooldown { get; }
public abstract float EffectDuration { get; }
public abstract int MaxUses { get; }
public abstract LoadableAsset<Sprite> Sprite { get; }
public bool HasEffect => EffectDuration > 0;
public bool LimitedUses => MaxUses > 0;
protected bool EffectActive;
protected float Timer;
protected int UsesLeft;
protected ActionButton Button;
protected DeadBody DeadBodyTarget;
public void CreateButton(Transform parent)
{
if (Button)
{
return;
}
UsesLeft = MaxUses;
Timer = Cooldown;
EffectActive = false;
Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent);
Button.name = Name + "Button";
Button.OverrideText(Name.ToUpper());
Button.graphic.sprite = Sprite.LoadAsset();
Button.SetUsesRemaining(MaxUses);
if (MaxUses <= 0)
{
Button.SetInfiniteUses();
}
var pb = Button.GetComponent<PassiveButton>();
pb.OnClick.RemoveAllListeners();
pb.OnClick.AddListener((UnityAction)ClickHandler);
}
public void ResetCooldown()
{
Timer = Cooldown;
if (EffectActive)
{
OnEffectEnd();
}
EffectActive = false;
}
public void OverrideSprite(Sprite sprite)
{
Button.graphic.sprite = sprite;
}
public void OverrideName(string name)
{
Button.OverrideText(name);
}
protected virtual void FixedUpdate(PlayerControl playerControl) { }
protected abstract void OnClick();
public abstract bool Enabled(RoleBehaviour role);
public virtual void OnEffectEnd() { }
private bool CanUseHandler()
{
return Timer <= 0 && !EffectActive && (!LimitedUses || UsesLeft > 0) && CanUse();
}
public virtual bool CanUse()
{
return true;
}
public virtual void SetActive(bool visible, RoleBehaviour role)
{
Button.ToggleVisible(visible && Enabled(role));
}
private void ClickHandler()
{
if (!CanUseHandler())
{
return;
}
if (LimitedUses)
{
UsesLeft--;
Button.SetUsesRemaining(UsesLeft);
}
OnClick();
Button.SetDisabled();
if (HasEffect)
{
EffectActive = true;
Timer = EffectDuration;
}
else
{
Timer = Cooldown;
}
}
public void FixedUpdateHandler(PlayerControl playerControl)
{
if (Timer >= 0)
{
Timer -= Time.deltaTime;
}
else
{
if (HasEffect && EffectActive)
{
EffectEndHandler();
}
}
if (CanUseHandler())
{
Button.SetEnabled();
}
else
{
Button.SetDisabled();
}
Button.SetCoolDown(Timer, EffectActive ? EffectDuration : Cooldown);
playerControl.UpdateBodies(playerControl.Data.Role.TeamColor, ref DeadBodyTarget);
FixedUpdate(playerControl);
}
private void EffectEndHandler()
{
EffectActive = false;
Timer = Cooldown;
OnEffectEnd();
}
}
+25
View File
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MiraAPI.Hud;
public static class CustomButtonManager
{
public static readonly List<CustomActionButton> CustomButtons = [];
public static void RegisterButton(Type buttonType)
{
if (!typeof(CustomActionButton).IsAssignableFrom(buttonType))
{
return;
}
var button = (CustomActionButton)Activator.CreateInstance(buttonType);
CustomButtons.Add(button);
typeof(CustomButtonSingleton<>).MakeGenericType(buttonType)
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
.SetValue(null, button);
}
}
+21
View File
@@ -0,0 +1,21 @@
#nullable enable
using System;
using System.Linq;
namespace MiraAPI.Hud;
public class CustomButtonSingleton<T> where T : CustomActionButton
{
private static T? _instance;
public static T Instance
{
get => _instance ??= CustomButtonManager.CustomButtons.OfType<T>().Single();
set
{
if (_instance == value) return;
if (_instance != null) throw new InvalidOperationException($"Instance for {typeof(T)} is already set");
_instance = value;
}
}
}
+24
View File
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MiraAPI.Hud;
public class RegisterButtonAttribute : Attribute
{
private static readonly HashSet<Assembly> RegisteredAssemblies = [];
public static void Register(Assembly assembly)
{
if (!RegisteredAssemblies.Add(assembly)) return;
foreach (var type in assembly.GetTypes())
{
var attribute = type.GetCustomAttribute<RegisterButtonAttribute>();
if (attribute != null)
{
CustomButtonManager.RegisterButton(type);
}
}
}
}
+10
View File
@@ -1,9 +1,13 @@
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.GameModes;
using MiraAPI.Hud;
using MiraAPI.Roles;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
using System.Reflection;
namespace MiraAPI;
@@ -14,9 +18,15 @@ namespace MiraAPI;
public partial class MiraAPIPlugin : BasePlugin
{
public Harmony Harmony { get; } = new(Id);
public static MiraAPIPlugin Instance { get; private set; }
public override void Load()
{
Instance = this;
Harmony.PatchAll();
RegisterGameModeAttribute.Register(Assembly.GetExecutingAssembly());
RegisterCustomRoleAttribute.Register(Assembly.GetExecutingAssembly());
RegisterButtonAttribute.Register(Assembly.GetExecutingAssembly());
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace MiraAPI.Networking
{
public enum MiraRpc : uint
{
SyncGameOptions,
SyncRoleOptions
}
}
+82
View File
@@ -0,0 +1,82 @@
using Hazel;
using MiraAPI.API.GameOptions;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
namespace MiraAPI.Networking.Options;
// METHOD RPC DOESNT WORK WITH THE ARRAYS AND STUFF SO THIS IS HOW WE WILL DO IT FOR NOW
[RegisterCustomRpc((uint)MiraRpc.SyncGameOptions)]
public class SyncOptionsRpc(MiraAPIPlugin plugin, uint id) : PlayerCustomRpc<MiraAPIPlugin, SyncOptionsRpc.Data>(plugin, id)
{
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public readonly struct Data
{
public readonly bool[] Toggles;
public readonly float[] Numbers;
public readonly int[] StringIDs;
public Data(bool[] toggles, float[] numbers, int[] stringIDs)
{
Toggles = toggles;
Numbers = numbers;
StringIDs = stringIDs;
}
}
public override void Write(MessageWriter writer, Data data)
{
writer.WritePacked((uint)data.Toggles.Length);
foreach (var t in data.Toggles)
{
writer.Write(t);
}
writer.WritePacked((uint)data.Numbers.Length);
foreach (var n in data.Numbers)
{
writer.Write(n);
}
writer.WritePacked((uint)data.StringIDs.Length);
foreach (var n in data.StringIDs)
{
writer.WritePacked(n);
}
}
public override Data Read(MessageReader reader)
{
var toggles = new bool[reader.ReadPackedUInt32()];
for (var i = 0; i < toggles.Length; i++)
{
toggles[i] = reader.ReadBoolean();
}
var numbers = new float[reader.ReadPackedUInt32()];
for (var i = 0; i < numbers.Length; i++)
{
numbers[i] = reader.ReadSingle();
}
var strings = new int[reader.ReadPackedUInt32()];
for (var i = 0; i < strings.Length; i++)
{
strings[i] = reader.ReadPackedInt32();
}
return new Data(toggles, numbers, strings);
}
public override void Handle(PlayerControl playerControl, Data data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
CustomOptionsManager.HandleOptionsSync(data.Toggles, data.Numbers, data.StringIDs);
}
}
+58
View File
@@ -0,0 +1,58 @@
using Hazel;
using MiraAPI.Roles;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using System;
namespace MiraAPI.Networking.Options;
[RegisterCustomRpc((uint)MiraRpc.SyncRoleOptions)]
public class SyncRoleOptionsRpc(MiraAPIPlugin plugin, uint id) : PlayerCustomRpc<MiraAPIPlugin, SyncRoleOptionsRpc.Data>(plugin, id)
{
public struct Data(ushort roleId, int number, int chance)
{
public readonly ushort RoleId = roleId;
public readonly int Number = number;
public readonly int Chance = chance;
}
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public override void Write(MessageWriter writer, Data data)
{
writer.Write(data.RoleId);
writer.WritePacked(data.Number);
writer.WritePacked(data.Chance);
}
public override Data Read(MessageReader reader)
{
return new Data(reader.ReadUInt16(), reader.ReadPackedInt32(), reader.ReadPackedInt32());
}
public override void Handle(PlayerControl playerControl, Data data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
if (!CustomRoleManager.CustomRoles.TryGetValue(data.RoleId, out var roleBehaviour) ||
!CustomRoleManager.GetCustomRoleBehaviour(roleBehaviour.Role, out var role)) return;
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.NumConfigDefinition, out var numEntry);
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.ChanceConfigDefinition, out var chanceEntry);
try
{
numEntry.Value = data.Number;
chanceEntry.Value = data.Chance;
}
catch (Exception e)
{
Logger<MiraAPIPlugin>.Warning(e.ToString());
}
}
}
@@ -0,0 +1,109 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.API.GameOptions;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(GameOptionsMenu))]
public static class GameOptionsMenuPatch
{
/// <summary>
/// Change the OnValueChanged action for all custom game options
/// </summary>
[HarmonyPostfix, HarmonyPatch("Start")]
public static void StartPostfix(GameOptionsMenu __instance)
{
foreach (var customOption in CustomOptionsManager.CustomOptions)
{
if (customOption.AdvancedRole is not null || !customOption.OptionBehaviour)
{
continue;
}
customOption.OptionBehaviour.OnValueChanged = (Action<OptionBehaviour>)customOption.ValueChanged;
}
}
private static GameSettingMenu menu;
/// <summary>
/// Set the position and offset of all custom game options
/// </summary>
[HarmonyPostfix, HarmonyPatch("Update")]
public static void UpdatePostfix(GameOptionsMenu __instance)
{
if (!menu)
{
menu = Object.FindObjectsOfType<GameSettingMenu>().First();
}
if (menu.RegularGameSettings.active || menu.RolesSettings.gameObject.active || menu.HideNSeekSettings.active)
{
return;
}
var startOffset = 2.15f;
__instance.GetComponentInParent<Scroller>().ContentYBounds.max = startOffset + __instance.Children.Count * 0.5f;
foreach (var option in CustomOptionsManager.CustomOptions.Where(option => option.Group == null))
{
if (!option.OptionBehaviour) continue;
option.OptionBehaviour.enabled = !option.Hidden();
option.OptionBehaviour.gameObject.SetActive(!option.Hidden());
if (!option.Hidden())
{
startOffset -= 0.55f;
}
var transform = option.OptionBehaviour.transform;
var optionPosition = transform.localPosition;
transform.localPosition = new Vector3(optionPosition.x, startOffset, optionPosition.z);
}
foreach (var group in CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null))
{
if (group.Header == null)
{
continue;
}
group.Header.SetActive(!group.Hidden());
if (!group.Hidden())
{
startOffset -= 0.5f;
}
var position = group.Header.transform.localPosition;
group.Header.transform.localPosition = new Vector3(position.x, startOffset, position.z);
foreach (var option in group.Options)
{
if (!option.OptionBehaviour)
{
continue;
}
var enabled = !group.Hidden() && !option.Hidden();
option.OptionBehaviour.enabled = enabled;
option.OptionBehaviour.gameObject.SetActive(enabled);
if (!group.Hidden() && !option.Hidden())
{
startOffset -= 0.5f;
}
var transform = option.OptionBehaviour.transform;
var optionPosition = transform.localPosition;
transform.localPosition = new Vector3(optionPosition.x, startOffset, optionPosition.z);
}
}
}
}
@@ -0,0 +1,171 @@
using HarmonyLib;
using MiraAPI.API.GameOptions;
using MiraAPI.GameModes;
using Reactor.Utilities;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(GameSettingMenu))]
public static class GameSettingsMenuPatches
{
private static GameObject _gameBtn;
private static GameObject _roleBtn;
[HarmonyPrefix, HarmonyPatch("Start")]
public static void StartPrefix(GameSettingMenu __instance)
{
if (CustomOptionsTab.CustomTab)
{
return;
}
__instance.Tabs.transform.position += new Vector3(0.5f, 0, 0);
_gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject;
_roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject;
var numberOpt = __instance.RegularGameSettings.GetComponentInChildren<NumberOption>();
var toggleOpt = Object.FindObjectOfType<ToggleOption>();
var stringOpt = __instance.RegularGameSettings.GetComponentInChildren<StringOption>();
var container = GameManager.Instance.IsNormal() ? CustomOptionsTab.Initialize(__instance).transform : __instance.HideAndSeekScroller.Inner;
foreach (var group in CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null))
{
if (GameManager.Instance.IsNormal())
{
group.Header = CustomOptionsTab.CreateHeader(toggleOpt, container, group.Title);
CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container,
group.CustomToggleOptions, group.CustomNumberOptions, group.CustomStringOptions);
continue;
}
if (!group.Options.Any(x => x.ShowInHideNSeek))
{
continue;
}
group.Header = CustomOptionsTab.CreateHeader(toggleOpt, container, group.Title);
__instance.AllHideAndSeekItems = __instance.AllHideAndSeekItems.Append(group.Header.transform).ToArray();
CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container,
group.CustomToggleOptions, group.CustomNumberOptions, group.CustomStringOptions);
}
CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container,
CustomOptionsManager.CustomToggleOptions.Where(option => option.Group == null),
CustomOptionsManager.CustomNumberOptions.Where(option => option.Group == null),
CustomOptionsManager.CustomStringOptions.Where(option => option.Group == null));
if (!numberOpt || !toggleOpt || !stringOpt)
{
Logger<MiraAPIPlugin>.Error("OPTION PREFABS MISSING");
}
}
[HarmonyPostfix, HarmonyPatch("Update")]
public static void UpdatePatch(GameSettingMenu __instance)
{
if (CustomGameModeManager.ActiveMode == null)
{
return;
}
_gameBtn.SetActive(CustomGameModeManager.ActiveMode.CanAccessSettingsTab());
_roleBtn.SetActive(CustomGameModeManager.ActiveMode.CanAccessRolesTab());
if (!CustomGameModeManager.ActiveMode.CanAccessSettingsTab())
{
__instance.RegularGameSettings.SetActive(false);
__instance.RolesSettings.gameObject.SetActive(false);
CustomOptionsTab.CustomScreen.gameObject.SetActive(true);
CustomOptionsTab.Rend.enabled = true;
__instance.GameSettingsHightlight.enabled = false;
__instance.RolesSettingsHightlight.enabled = false;
}
}
public static void CreateOptionsFor(GameSettingMenu __instance, ToggleOption togglePrefab, NumberOption numberPrefab, StringOption stringPrefab, Transform container,
IEnumerable<CustomToggleOption> toggles, IEnumerable<CustomNumberOption> numbers, IEnumerable<CustomStringOption> strings)
{
foreach (var customToggleOption in toggles)
{
if (customToggleOption.AdvancedRole is not null || customToggleOption.OptionBehaviour)
{
continue;
}
ToggleOption toggleOption;
if (GameManager.Instance.IsNormal())
{
toggleOption = customToggleOption.CreateToggleOption(togglePrefab, container);
__instance.AllItems = __instance.AllItems.AddItem(toggleOption.transform).ToArray();
continue;
}
if (!customToggleOption.ShowInHideNSeek)
{
continue;
}
toggleOption = customToggleOption.CreateToggleOption(togglePrefab, container);
__instance.AllHideAndSeekItems = __instance.AllHideAndSeekItems.AddItem(toggleOption.transform).ToArray();
}
foreach (var customNumberOption in numbers)
{
if (customNumberOption.AdvancedRole is not null || customNumberOption.OptionBehaviour)
{
continue;
}
NumberOption numberOption;
if (GameManager.Instance.IsNormal())
{
numberOption = customNumberOption.CreateNumberOption(numberPrefab, container);
__instance.AllItems = __instance.AllItems.AddItem(numberOption.transform).ToArray();
continue;
}
if (!customNumberOption.ShowInHideNSeek)
{
continue;
}
numberOption = customNumberOption.CreateNumberOption(numberPrefab, container);
__instance.AllHideAndSeekItems = __instance.AllHideAndSeekItems.AddItem(numberOption.transform).ToArray();
}
foreach (var customStringOption in strings)
{
if (customStringOption.AdvancedRole is not null || customStringOption.OptionBehaviour)
{
continue;
}
StringOption stringOption;
if (GameManager.Instance.IsNormal())
{
stringOption = customStringOption.CreateStringOption(stringPrefab, container);
__instance.AllItems = __instance.AllItems.AddItem(stringOption.transform).ToArray();
continue;
}
if (!customStringOption.ShowInHideNSeek)
{
continue;
}
stringOption = customStringOption.CreateStringOption(stringPrefab, container);
__instance.AllHideAndSeekItems = __instance.AllHideAndSeekItems.AddItem(stringOption.transform).ToArray();
}
}
}
+150
View File
@@ -0,0 +1,150 @@
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.API.GameOptions;
using MiraAPI.GameModes;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(IGameOptionsExtensions), "ToHudString")]
public static class ToHudStringPatch
{
/// <summary>
/// Show custom Launchpad options or vanilla game options
/// </summary>
public static bool ShowCustom = false;
/// <summary>
/// Generic method to add options
/// </summary>
public static void AddOptions(StringBuilder sb,
IEnumerable<CustomNumberOption> numberOptions, IEnumerable<CustomStringOption> stringOptions, IEnumerable<CustomToggleOption> toggleOptions)
{
foreach (var numberOption in numberOptions.Where(x => !x.Hidden()))
{
if (GameManager.Instance.IsHideAndSeek() && !numberOption.ShowInHideNSeek)
{
continue;
}
sb.AppendLine(numberOption.Title + ": " + numberOption.Value + Helpers.GetSuffix(numberOption.SuffixType));
}
foreach (var toggleOption in toggleOptions.Where(x => !x.Hidden()))
{
if (GameManager.Instance.IsHideAndSeek() && !toggleOption.ShowInHideNSeek)
{
continue;
}
sb.AppendLine(toggleOption.Title + ": " + (toggleOption.Value ? "On" : "Off"));
}
foreach (var stringOption in stringOptions.Where(x => !x.Hidden()))
{
if (GameManager.Instance.IsHideAndSeek() && !stringOption.ShowInHideNSeek)
{
continue;
}
sb.AppendLine(stringOption.Title + ": " + stringOption.Value);
}
}
public static void Prefix()
{
if (GameManager.Instance is null || GameManager.Instance.IsHideAndSeek())
{
return;
}
foreach (var role in CustomRoleManager.CustomRoles.Values)
{
var customRole = role as ICustomRole;
if (customRole.IsGhostRole)
{
role.Role = RoleTypes.CrewmateGhost;
}
}
}
/// <summary>
/// Update the HudOptions on the left of the screen if player is using Launchpad options
/// </summary>
public static void Postfix(IGameOptions __instance, ref string __result)
{
if (GameManager.Instance is null)
{
return;
}
foreach (var role in CustomRoleManager.CustomRoles.Values)
{
var customRole = role as ICustomRole;
if (customRole.IsGhostRole)
{
role.Role = (RoleTypes)customRole.RoleId;
}
}
if (ShowCustom || !CustomGameModeManager.ActiveMode.CanAccessSettingsTab())
{
var sb = new StringBuilder("<size=180%><b>Launchpad Options:</b></size>\n<size=130%>");
var groupsWithRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole != null);
var groupsWithoutRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null);
var suffix = CustomGameModeManager.ActiveMode.CanAccessSettingsTab() ? "\nPress <b>Tab</b> to view Normal Options" :
$"\n<b>You can not access Normal Options on {Cus tomGameModeManager.ActiveMode.Name} mode.</b>";
AddOptions(sb,
CustomOptionsManager.CustomNumberOptions.Where(option => option.Group == null && !option.Hidden()),
CustomOptionsManager.CustomStringOptions.Where(option => option.Group == null && !option.Hidden()),
CustomOptionsManager.CustomToggleOptions.Where(option => option.Group == null && !option.Hidden())
);
foreach (var group in groupsWithoutRoles)
{
if (group.Hidden() || (GameManager.Instance.IsHideAndSeek() && !group.Options.Any(x => x.ShowInHideNSeek)))
{
continue;
}
sb.AppendLine($"\n<size=160%><b>{group.Title}</b></size>");
AddOptions(sb, group.CustomNumberOptions, group.CustomStringOptions, group.CustomToggleOptions);
}
if (GameManager.Instance.IsHideAndSeek())
{
__result = sb + suffix;
return;
}
var customOptionGroups = groupsWithRoles as CustomOptionGroup[] ?? groupsWithRoles.ToArray();
if (customOptionGroups.Any() && CustomGameModeManager.ActiveMode.CanAccessRolesTab())
{
sb.AppendLine("\n<size=160%><b>Roles</b></size>");
foreach (var group in customOptionGroups)
{
if (group.Hidden())
{
continue;
}
sb.AppendLine($"<size=140%><b>{group.Title}</b></size><size=120%>");
AddOptions(sb, group.CustomNumberOptions, group.CustomStringOptions, group.CustomToggleOptions);
sb.Append("</size>\n");
}
}
__result = sb + suffix;
return;
}
__result = "<size=160%><b>Normal Options:</b></size>\n<size=130%>" + __result + "\nPress <b>Tab</b> to view Launchpad Options</size>";
}
}
+24
View File
@@ -0,0 +1,24 @@
using HarmonyLib;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(MainMenuManager))]
public static class GameStartupPatch
{
private static bool _runOnce;
/// <summary>
/// This is used for registering roles when the game opens, might be a janky solution but it works
/// </summary>
[HarmonyPostfix, HarmonyPatch("Start")]
public static void StartPostfix()
{
if (_runOnce)
{
return;
}
_runOnce = true;
CustomRoleManager.RegisterInRoleManager();
}
}
+35
View File
@@ -0,0 +1,35 @@
using HarmonyLib;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
/// <summary>
/// Set nametag color depending on option
/// </summary>
[HarmonyPatch(typeof(PlayerNameColor))]
public static class NameTagPatch
{
[HarmonyPrefix, HarmonyPatch("Get", typeof(RoleBehaviour))]
public static bool GetPatch([HarmonyArgument(0)] RoleBehaviour otherPlayerRole, ref Color __result)
{
if (PlayerControl.LocalPlayer.Data.IsDead)
{
__result = otherPlayerRole.NameColor;
return false;
}
if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && otherPlayerRole.IsImpostor)
{
return true;
}
if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null || PlayerControl.LocalPlayer.Data.Role == null || otherPlayerRole == null)
{
__result = Color.white;
}
__result = PlayerControl.LocalPlayer.Data?.Role == otherPlayerRole ? otherPlayerRole.NameColor : Color.white;
return false;
}
}
@@ -0,0 +1,24 @@
using HarmonyLib;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(RoleBehaviour))]
public static class RoleBehaviourPatches
{
/// <summary>
/// Update TeamColor text for Launchpad roles
/// </summary>
[HarmonyPrefix, HarmonyPatch("TeamColor", MethodType.Getter)]
public static bool PrefixTeamColorGetter(RoleBehaviour __instance, ref Color __result)
{
if (__instance is not ICustomRole behaviour)
{
return true;
}
__result = behaviour.RoleColor;
return false;
}
}
@@ -0,0 +1,31 @@
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(RoleManager))]
public static class RoleManagerPatches
{
[HarmonyPrefix, HarmonyPatch("AssignRoleOnDeath")]
public static bool AssignRoleOnDeath(RoleManager __instance, [HarmonyArgument(0)] PlayerControl plr)
{
if (plr == null || !plr.Data.IsDead)
{
return false;
}
if (plr.Data.Role is ICustomRole role)
{
Debug.Log(role.RoleName);
if (role.GhostRole != RoleTypes.CrewmateGhost && role.GhostRole != RoleTypes.ImpostorGhost)
{
plr.RpcSetRole(role.GhostRole);
return false;
}
}
return true;
}
}
@@ -0,0 +1,24 @@
using HarmonyLib;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(RoleOptionSetting))]
public static class RoleOptionSettingPatch
{
/// <summary>
/// Update the role details screen in role settings for Launchpad roles
/// </summary>
[HarmonyPrefix, HarmonyPatch("ShowRoleDetails")]
public static bool ShowRoleDetailsPrefix(RoleOptionSetting __instance)
{
if (__instance.Role is ICustomRole customRole)
{
GameSettingMenu.Instance.RoleName.text = __instance.Role.NiceName;
GameSettingMenu.Instance.RoleBlurb.text = __instance.Role.BlurbLong;
GameSettingMenu.Instance.RoleIcon.sprite = customRole.Icon.LoadAsset();
return false;
}
return true;
}
}
@@ -0,0 +1,52 @@
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.Roles;
using Reactor.Utilities;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(RoleOptionsCollectionV07))]
public static class RoleOptionsCollectionPatch
{
/// <summary>
/// Set the role chance for custom Launchpad roles based on config
/// </summary>
[HarmonyPrefix, HarmonyPatch("GetChancePerGame")]
public static bool GetChancePrefix([HarmonyArgument(0)] RoleTypes roleType, ref int __result)
{
if (CustomRoleManager.GetCustomRoleBehaviour(roleType, out var customRole))
{
if (customRole.HideSettings)
{
return false;
}
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(customRole.ChanceConfigDefinition, out var entry);
__result = entry.Value;
return false;
}
return true;
}
/// <summary>
/// Set the amount for custom Launchpad roles based on config
/// </summary>
[HarmonyPrefix, HarmonyPatch("GetNumPerGame")]
public static bool GetNumPrefix([HarmonyArgument(0)] RoleTypes roleType, ref int __result)
{
if (CustomRoleManager.GetCustomRoleBehaviour(roleType, out var customRole))
{
if (customRole.HideSettings)
{
return false;
}
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(customRole.NumConfigDefinition, out var entry);
__result = entry.Value;
return false;
}
return true;
}
}
@@ -0,0 +1,149 @@
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppInterop.Runtime;
using Il2CppSystem;
using MiraAPI.API.GameOptions;
using MiraAPI.Networking.Options;
using MiraAPI.Roles;
using Reactor.Networking.Rpc;
using Reactor.Utilities.Extensions;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(RolesSettingsMenu))]
public static class RolesSettingsMenuPatches
{
/// <summary>
/// Create role buttons and advanced role settings menu for every custom Launchpad role
/// </summary>
[HarmonyPrefix, HarmonyPatch("OnEnable")]
public static void OnEnablePrefix(RolesSettingsMenu __instance)
{
var tabPrefab = __instance.AllAdvancedSettingTabs.ToArray()[1].Tab;
foreach (var (key, role) in CustomRoleManager.CustomRoles)
{
if (__instance.AllAdvancedSettingTabs.ToArray().Any(x => (ushort)x.Type == key))
{
continue;
}
if (role is ICustomRole { HideSettings: true })
{
continue;
}
var numChanceOption = Object.Instantiate(__instance.SettingPrefab, __instance.ItemParent);
numChanceOption.transform.localPosition = new Vector3(0.044f, 0, 0);
numChanceOption.name = role.NiceName;
numChanceOption.Role = role;
__instance.AllRoleSettings.Add(numChanceOption);
var newTab = Object.Instantiate(tabPrefab, __instance.AdvancedRolesSettings.transform);
newTab.name = role.NiceName + " Settings";
var toggleSet = Object.Instantiate(newTab.GetComponentInChildren<ToggleOption>(true));
var numberSet = Object.Instantiate(newTab.GetComponentInChildren<NumberOption>(true));
foreach (var option in newTab.GetComponentsInChildren<OptionBehaviour>())
{
option.gameObject.Destroy();
}
var startOffset = 0.5f;
foreach (var customOption in CustomOptionsManager.CustomOptions)
{
if (customOption.AdvancedRole is not null && customOption.AdvancedRole == role.GetType())
{
startOffset -= 0.5f;
switch (customOption)
{
case CustomNumberOption numberOption:
var numOpt = numberOption.CreateNumberOption(numberSet, newTab.transform);
numOpt.transform.localPosition = new Vector3(-1.25f, startOffset, 0);
break;
case CustomToggleOption toggleOption:
var togOpt = toggleOption.CreateToggleOption(toggleSet, newTab.transform);
togOpt.transform.localPosition = new Vector3(-1.25f, startOffset, 0);
break;
}
}
}
var tmp = newTab.GetComponentInChildren<TextTranslatorTMP>();
tmp.defaultStr = role.NiceName;
tmp.TargetText = role.StringName;
var newAdvSet = new AdvancedRoleSettingsButton
{
Tab = newTab,
Type = (RoleTypes)key
};
__instance.AllAdvancedSettingTabs.Add(newAdvSet);
toggleSet.gameObject.Destroy();
numberSet.gameObject.Destroy();
}
}
/// <summary>
/// Set custom role options OnValueChanged and re-enable the role settings menu scrolling
/// </summary>
[HarmonyPostfix, HarmonyPatch("Start")]
public static void StartPostfix(RolesSettingsMenu __instance)
{
foreach (var customOption in CustomOptionsManager.CustomOptions)
{
if (customOption.AdvancedRole is not null)
{
customOption.OptionBehaviour.OnValueChanged = (Action<OptionBehaviour>)customOption.ValueChanged;
}
}
var scroll = __instance.GetComponentInChildren<Scroller>();
scroll.active = true;
scroll.ContentYBounds.max = 2 * __instance.AllRoleSettings.Count / 10f;
scroll.transform.FindChild("UI_Scrollbar").gameObject.SetActive(true);
}
/// <summary>
/// Update config when value changed
/// </summary>
[HarmonyPrefix, HarmonyPatch("ValueChanged")]
public static bool ValueChangedPrefix(RolesSettingsMenu __instance, [HarmonyArgument(0)] OptionBehaviour obj)
{
if (obj.GetIl2CppType() != Il2CppType.Of<RoleOptionSetting>())
{
return true;
}
var roleSetting = obj.Cast<RoleOptionSetting>();
if (roleSetting.Role is not ICustomRole role || role.HideSettings)
{
return true;
}
MiraAPIPlugin.Instance.Config.TryGetEntry<int>(role.NumConfigDefinition, out var numEntry);
numEntry.Value = roleSetting.RoleMaxCount;
MiraAPIPlugin.Instance.Config.TryGetEntry<int>(role.ChanceConfigDefinition, out var chanceEntry);
chanceEntry.Value = roleSetting.RoleChance;
roleSetting.UpdateValuesAndText(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions);
if (AmongUsClient.Instance.AmHost)
{
Rpc<SyncRoleOptionsRpc>.Instance.Send(new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value));
}
GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions;
return false;
}
}
+174
View File
@@ -0,0 +1,174 @@
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppSystem;
using Il2CppSystem.Collections.Generic;
using Il2CppSystem.Text;
using MiraAPI.Roles;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch]
public static class TaskAdderPatch
{
public static TaskFolder RolesFolder;
[HarmonyPostfix, HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))]
public static void AddRolesFolder(TaskAdderGame __instance)
{
RolesFolder = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform);
RolesFolder.gameObject.SetActive(false);
RolesFolder.FolderName = "Roles";
RolesFolder.name = "RolesFolder";
__instance.Root.SubFolders.Add(RolesFolder);
__instance.GoToRoot();
}
[HarmonyPostfix, HarmonyPatch(typeof(TaskAddButton), "Role", MethodType.Setter)]
public static void RoleGetterPatch(TaskAddButton __instance)
{
if (__instance.role is ICustomRole lpRole)
{
if (lpRole.IsOutcast)
{
__instance.FileImage.color = Color.gray;
}
}
__instance.RolloverHandler.OutColor = __instance.FileImage.color;
}
// yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry:
[HarmonyPrefix, HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))]
public static bool ShowPatch(TaskAdderGame __instance, [HarmonyArgument(0)] TaskFolder taskFolder)
{
var stringBuilder = new StringBuilder(64);
__instance.Hierarchy.Add(taskFolder);
for (var i = 0; i < __instance.Hierarchy.Count; i++)
{
stringBuilder.Append(__instance.Hierarchy.ToArray()[i].FolderName);
stringBuilder.Append("\\");
}
__instance.PathText.text = stringBuilder.ToString();
for (var j = 0; j < __instance.ActiveItems.Count; j++)
{
Object.Destroy(__instance.ActiveItems.ToArray()[j].gameObject);
}
__instance.ActiveItems.Clear();
var num = 0f;
var num2 = 0f;
var num3 = 0f;
for (var k = 0; k < taskFolder.SubFolders.Count; k++)
{
var taskFolder2 = Object.Instantiate(taskFolder.SubFolders.ToArray()[k], __instance.TaskParent);
var folderTransform = taskFolder2.transform;
taskFolder2.gameObject.SetActive(true);
taskFolder2.Parent = __instance;
folderTransform.localPosition = new Vector3(num, num2, 0f);
folderTransform.localScale = Vector3.one;
num3 = Mathf.Max(num3, taskFolder2.Text.bounds.size.y + 1.1f);
num += __instance.folderWidth;
if (num > __instance.lineWidth)
{
num = 0f;
num2 -= num3;
num3 = 0f;
}
__instance.ActiveItems.Add(folderTransform);
if (!taskFolder2 || !taskFolder2.Button)
{
continue;
}
ControllerManager.Instance.AddSelectableUiElement(taskFolder2.Button);
if (!string.IsNullOrEmpty(__instance.restorePreviousSelectionByFolderName) && taskFolder2.FolderName.Equals(__instance.restorePreviousSelectionByFolderName))
{
__instance.restorePreviousSelectionFound = taskFolder2.Button;
}
}
var flag = false;
var list = new List<PlayerTask>();
foreach (var item in taskFolder.Children)
{
list.Add(item);
}
for (var l = 0; l < list.Count; l++)
{
var taskAddButton = Object.Instantiate(__instance.TaskPrefab);
taskAddButton.MyTask = list.ToArray()[l];
if (taskAddButton.MyTask.TaskType == TaskTypes.DivertPower)
{
var targetSystem = taskAddButton.MyTask.Cast<DivertPowerTask>().TargetSystem;
taskAddButton.Text.text = DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.DivertPowerTo, DestroyableSingleton<TranslationController>.Instance.GetString(targetSystem));
}
else if (taskAddButton.MyTask.TaskType == TaskTypes.FixWeatherNode)
{
var nodeId = ((WeatherNodeTask)taskAddButton.MyTask).NodeId;
taskAddButton.Text.text = DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.FixWeatherNode, Array.Empty<Il2CppSystem.Object>()) + " " + DestroyableSingleton<TranslationController>.Instance.GetString(WeatherSwitchGame.ControlNames[nodeId],
Array.Empty<Il2CppSystem.Object>());
}
else
{
taskAddButton.Text.text = DestroyableSingleton<TranslationController>.Instance.GetString(taskAddButton.MyTask.TaskType);
}
__instance.AddFileAsChild(taskFolder, taskAddButton, ref num, ref num2, ref num3);
if (taskAddButton != null && taskAddButton.Button != null)
{
ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button);
if (__instance.Hierarchy.Count != 1 && !flag)
{
var component = ControllerManager.Instance.CurrentUiState.CurrentSelection.GetComponent<TaskFolder>();
if (component != null)
{
__instance.restorePreviousSelectionByFolderName = component.FolderName;
}
ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button);
flag = true;
}
}
}
if (taskFolder.FolderName == "Roles") // idk why only this works???
{
for (var m = 0; m < DestroyableSingleton<RoleManager>.Instance.AllRoles.Length; m++)
{
var roleBehaviour = DestroyableSingleton<RoleManager>.Instance.AllRoles[m];
if (roleBehaviour.Role != RoleTypes.ImpostorGhost && roleBehaviour.Role != RoleTypes.CrewmateGhost && !roleBehaviour.IsDead)
{
var taskAddButton2 = Object.Instantiate(__instance.RoleButton);
taskAddButton2.SafePositionWorld = __instance.SafePositionWorld;
taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe";
__instance.AddFileAsChild(RolesFolder, taskAddButton2, ref num, ref num2, ref num3);
taskAddButton2.Role = roleBehaviour;
if (taskAddButton2 != null && taskAddButton2.Button != null)
{
ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button);
if (m == 0 && __instance.restorePreviousSelectionFound != null)
{
ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound);
__instance.restorePreviousSelectionByFolderName = string.Empty;
__instance.restorePreviousSelectionFound = null;
}
else if (m == 0)
{
ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button);
}
}
}
}
}
if (__instance.Hierarchy.Count == 1)
{
ControllerManager.Instance.SetBackButton(__instance.BackButton);
return false;
}
ControllerManager.Instance.SetBackButton(__instance.FolderBackButton);
return false;
}
}
+58
View File
@@ -0,0 +1,58 @@
using HarmonyLib;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(TaskPanelBehaviour))]
public static class TaskPanelPatch
{
/// <summary>
/// This patch is to override the automatic updating of the y position on the tab (which is in base game),
/// because I can't change the custom tab y pos if it's being overriden every frame.
/// Im sure there is an easier/better way, but this is the fix that worked for me
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(TaskPanelBehaviour.Update))]
public static bool UpdatePrefix(TaskPanelBehaviour __instance)
{
if (__instance.gameObject.name != "RolePanel")
{
return true;
}
var transform = __instance.background.transform;
var vector = __instance.background.sprite.bounds.extents;
var vector2 = __instance.tab.sprite.bounds.extents;
transform.localScale = __instance.taskText.textBounds.size.x > 0f ? new Vector3(__instance.taskText.textBounds.size.x + 0.4f, __instance.taskText.textBounds.size.y + 0.3f, 1f) : Vector3.zero;
vector.y = -vector.y;
vector = vector.Mul(transform.localScale);
__instance.background.transform.localPosition = vector;
vector2 = vector2.Mul(__instance.tab.transform.localScale);
vector2.y = -vector2.y;
vector2.x += vector.x * 2f;
__instance.tab.transform.localPosition = vector2;
if (GameManager.Instance == null)
{
return false;
}
var closePosition = new Vector3(-__instance.background.sprite.bounds.size.x * __instance.background.transform.localScale.x, __instance.closedPosition.y, __instance.closedPosition.z);
__instance.closedPosition = closePosition;
if (__instance.open)
{
__instance.timer = Mathf.Min(1f, __instance.timer + Time.deltaTime / __instance.animationTimeSeconds);
}
else
{
__instance.timer = Mathf.Max(0f, __instance.timer - Time.deltaTime / __instance.animationTimeSeconds);
}
Vector3 relativePos;
relativePos = new Vector3(Mathf.SmoothStep(__instance.closedPosition.x, __instance.openPosition.x, __instance.timer), Mathf.SmoothStep(__instance.closedPosition.y, __instance.openPosition.y, __instance.timer), __instance.openPosition.z);
__instance.transform.localPosition = AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos);
return false;
}
}
+155
View File
@@ -0,0 +1,155 @@
using AmongUs.GameOptions;
using Il2CppInterop.Runtime;
using MiraAPI.Networking.Options;
using Reactor.Localization.Utilities;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Roles;
public static class CustomRoleManager
{
public static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
public static void RegisterInRoleManager()
{
RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray();
}
internal static void RegisterRole(Type roleType)
{
if (!(typeof(RoleBehaviour).IsAssignableFrom(roleType) && typeof(ICustomRole).IsAssignableFrom(roleType)))
{
return;
}
if (CustomRoles.Any(behaviour => behaviour.GetType() == roleType))
{
return;
}
var roleBehaviour = (RoleBehaviour)new GameObject(roleType.Name).DontDestroy().AddComponent(Il2CppType.From(roleType));
if (roleBehaviour is not ICustomRole customRole)
{
return;
}
roleBehaviour.Role = (RoleTypes)customRole.RoleId;
roleBehaviour.TeamType = customRole.Team;
roleBehaviour.NameColor = customRole.RoleColor;
roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName);
roleBehaviour.BlurbName = CustomStringName.CreateAndRegister(customRole.RoleDescription);
roleBehaviour.BlurbNameLong = CustomStringName.CreateAndRegister(customRole.RoleLongDescription);
roleBehaviour.AffectedByLightAffectors = customRole.AffectedByLight;
roleBehaviour.CanBeKilled = customRole.CanGetKilled;
roleBehaviour.CanUseKillButton = customRole.CanKill;
roleBehaviour.TasksCountTowardProgress = customRole.TasksCount;
roleBehaviour.CanVent = customRole.CanUseVent;
roleBehaviour.DefaultGhostRole = customRole.GhostRole;
roleBehaviour.MaxCount = 15;
if (customRole.IsGhostRole)
{
RoleManager.GhostRoles.Add(roleBehaviour.Role);
}
CustomRoles.Add(customRole.RoleId, roleBehaviour);
if (customRole.HideSettings)
{
return;
}
var config = PluginSingleton<MiraAPIPlugin>.Instance.Config;
config.Bind(customRole.NumConfigDefinition, 1);
config.Bind(customRole.ChanceConfigDefinition, 100);
}
public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole result)
{
CustomRoles.TryGetValue((ushort)roleType, out var temp);
if (temp is ICustomRole role)
{
result = role;
return true;
}
result = null;
return false;
}
public static TaskPanelBehaviour CreateRoleTab(ICustomRole role)
{
var ogPanel = HudManager.Instance.TaskStuff.transform.FindChild("TaskPanel").gameObject.GetComponent<TaskPanelBehaviour>();
var clonePanel = Object.Instantiate(ogPanel.gameObject, ogPanel.transform.parent);
clonePanel.name = "RolePanel";
var newPanel = clonePanel.GetComponent<TaskPanelBehaviour>();
newPanel.open = false;
var tab = newPanel.tab.gameObject;
tab.GetComponentInChildren<TextTranslatorTMP>().Destroy();
newPanel.transform.localPosition = ogPanel.transform.localPosition - new Vector3(0, 1, 0);
UpdateRoleTab(newPanel, role);
return newPanel;
}
public static void UpdateRoleTab(TaskPanelBehaviour panel, ICustomRole role)
{
var tabText = panel.tab.gameObject.GetComponentInChildren<TextMeshPro>();
var ogPanel = HudManager.Instance.TaskStuff.transform.FindChild("TaskPanel").gameObject.GetComponent<TaskPanelBehaviour>();
if (tabText.text != role.RoleName)
{
tabText.text = role.RoleName;
}
var y = ogPanel.taskText.textBounds.size.y + 1;
panel.closedPosition = new Vector3(ogPanel.closedPosition.x, ogPanel.open ? y + 0.2f : 2f, ogPanel.closedPosition.z);
panel.openPosition = new Vector3(ogPanel.openPosition.x, ogPanel.open ? y : 2f, ogPanel.openPosition.z);
panel.SetTaskText(role.SetTabText().ToString());
}
public static void SyncRoleSettings()
{
foreach (var role in CustomRoles.Values.Select(x => (ICustomRole)x))
{
if (role.HideSettings)
{
continue;
}
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.NumConfigDefinition, out var numEntry);
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.ChanceConfigDefinition, out var chanceEntry);
Rpc<SyncRoleOptionsRpc>.Instance.Send(new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value));
}
}
public static void SyncRoleSettings(int targetId)
{
foreach (var role in CustomRoles.Values.Select(x => (ICustomRole)x))
{
if (role.HideSettings)
{
continue;
}
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.NumConfigDefinition, out var numEntry);
PluginSingleton<MiraAPIPlugin>.Instance.Config.TryGetEntry<int>(role.ChanceConfigDefinition, out var chanceEntry);
Rpc<SyncRoleOptionsRpc>.Instance.SendTo(targetId, new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value));
}
}
}
+68
View File
@@ -0,0 +1,68 @@
using AmongUs.GameOptions;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using System.Text;
using UnityEngine;
namespace MiraAPI.Roles;
public interface ICustomRole
{
string RoleName { get; }
ushort RoleId { get; }
string RoleDescription { get; }
string RoleLongDescription { get; }
Color RoleColor { get; }
RoleTeamTypes Team { get; }
LoadableAsset<Sprite> Icon => MiraAssets.Empty;
ConfigDefinition NumConfigDefinition => new("Roles", $"Num{RoleName}");
ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance{RoleName}");
bool AffectedByLight => Team == RoleTeamTypes.Crewmate;
bool CanGetKilled => Team == RoleTeamTypes.Crewmate;
bool IsOutcast => false;
bool CanKill => Team == RoleTeamTypes.Impostor;
bool CanUseVent => Team == RoleTeamTypes.Impostor;
bool TasksCount => Team == RoleTeamTypes.Crewmate;
bool IsGhostRole => false;
bool TargetsBodies => false;
bool CreateCustomTab => true;
bool HideSettings => IsGhostRole;
RoleTypes GhostRole => Team == RoleTeamTypes.Crewmate ? RoleTypes.CrewmateGhost : RoleTypes.ImpostorGhost;
void CreateOptions() { }
void PlayerControlFixedUpdate(PlayerControl playerControl) { }
void HudUpdate(HudManager hudManager) { }
string GetCustomEjectionMessage(GameData.PlayerInfo player)
{
return Team == RoleTeamTypes.Impostor ? $"{player.PlayerName} was The {RoleName}" : null;
}
StringBuilder SetTabText()
{
var taskStringBuilder = Helpers.CreateForRole(this);
return taskStringBuilder;
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MiraAPI.Roles;
[AttributeUsage(AttributeTargets.Class)]
public class RegisterCustomRoleAttribute : Attribute
{
private static readonly HashSet<Assembly> RegisteredAssemblies = [];
public static void Register(Assembly assembly)
{
if (!RegisteredAssemblies.Add(assembly)) return;
foreach (var type in assembly.GetTypes())
{
var attribute = type.GetCustomAttribute<RegisterCustomRoleAttribute>();
if (attribute != null)
{
CustomRoleManager.RegisterRole(type);
}
}
}
}
@@ -0,0 +1,8 @@
namespace MiraAPI.Utilities
{
public abstract class LoadableAsset<T> where T : UnityEngine.Object
{
protected T _loadedAsset;
public abstract T LoadAsset();
}
}
@@ -0,0 +1,20 @@
using Reactor.Utilities.Extensions;
using System;
using UnityEngine;
namespace MiraAPI.Utilities
{
public class LoadableBundleAsset<T>(string name, AssetBundle bundle) : LoadableAsset<T> where T : UnityEngine.Object
{
public override T LoadAsset()
{
if (_loadedAsset != null)
{
return _loadedAsset;
}
return _loadedAsset = bundle.LoadAsset<T>(name);
throw new Exception($"INVALID ASSET: {name}");
}
}
}
@@ -0,0 +1,19 @@
using System;
using UnityEngine;
namespace MiraAPI.Utilities.Assets
{
public class LoadableResourceAsset(string name, string resourcesFolder) : LoadableAsset<Sprite>
{
public override Sprite LoadAsset()
{
if (_loadedAsset != null)
{
return _loadedAsset;
}
return _loadedAsset = SpriteTools.LoadSpriteFromPath(resourcesFolder + name);
throw new Exception($"INVALID ASSET: {name}");
}
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace MiraAPI.Utilities.Assets
{
public static class MiraAssets
{
public static readonly LoadableResourceAsset Empty = new("", "");
}
}
+39
View File
@@ -0,0 +1,39 @@
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Reactor.Utilities.Extensions;
using System;
using System.Reflection;
using UnityEngine;
namespace MiraAPI.Utilities.Assets
{
public static class SpriteTools
{
private delegate bool DLoadImage(IntPtr tex, IntPtr data, bool markNonReadable);
private static DLoadImage _iCallLoadImage;
private static bool LoadImage(Texture2D tex, byte[] data, bool markNonReadable)
{
_iCallLoadImage ??= IL2CPP.ResolveICall<DLoadImage>("UnityEngine.ImageConversion::LoadImage");
var il2CPPArray = (Il2CppStructArray<byte>)data;
return _iCallLoadImage.Invoke(tex.Pointer, il2CPPArray.Pointer, markNonReadable);
}
public static Sprite LoadSpriteFromPath(string resourcePath)
{
var tex = new Texture2D(1, 1, TextureFormat.ARGB32, false);
var assembly = Assembly.GetExecutingAssembly();
var myStream = assembly.GetManifestResourceStream(resourcePath);
if (myStream != null)
{
var buttonTexture = myStream.ReadFully();
LoadImage(tex, buttonTexture, false);
}
return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
}
}
}
+86
View File
@@ -0,0 +1,86 @@
using MiraAPI.Roles;
using Reactor.Utilities.Extensions;
using System.Linq;
using UnityEngine;
namespace MiraAPI.Utilities
{
public static class Extensions
{
public static void UpdateBodies(this PlayerControl playerControl, Color outlineColor, ref DeadBody target)
{
foreach (var body in Object.FindObjectsOfType<DeadBody>())
{
foreach (var bodyRenderer in body.bodyRenderers)
{
bodyRenderer.SetOutline(null);
}
}
if (playerControl.Data.Role is not ICustomRole { TargetsBodies: true })
{
return;
}
target = playerControl.NearestDeadBody();
if (!target)
{
return;
}
foreach (var renderer in target.bodyRenderers)
{
renderer.SetOutline(outlineColor);
}
}
public static DeadBody NearestDeadBody(this PlayerControl playerControl)
{
var results = new Il2CppSystem.Collections.Generic.List<Collider2D>();
Physics2D.OverlapCircle(playerControl.GetTruePosition(), playerControl.MaxReportDistance / 4f, Helpers.Filter, results);
return results.ToArray()
.Where(collider2D => collider2D.CompareTag("DeadBody"))
.Select(collider2D => collider2D.GetComponent<DeadBody>())
.FirstOrDefault(component => component && !component.Reported);
}
public static PlayerControl GetClosestPlayer(this PlayerControl playerControl, bool includeImpostors, float distance)
{
PlayerControl result = null;
if (!ShipStatus.Instance)
{
return null;
}
var truePosition = playerControl.GetTruePosition();
foreach (var playerInfo in GameData.Instance.AllPlayers)
{
if (playerInfo.Disconnected || playerInfo.PlayerId == playerControl.PlayerId ||
playerInfo.IsDead || !includeImpostors && playerInfo.Role.IsImpostor)
{
continue;
}
var @object = playerInfo.Object;
if (!@object)
{
continue;
}
var vector = @object.GetTruePosition() - truePosition;
var magnitude = vector.magnitude;
if (!(magnitude <= distance) || PhysicsHelpers.AnyNonTriggersBetween(truePosition,
vector.normalized,
magnitude, LayerMask.GetMask("Ship", "Objects")))
{
continue;
}
result = @object;
distance = magnitude;
}
return result;
}
}
}
+37
View File
@@ -0,0 +1,37 @@
using MiraAPI.Roles;
using System.Linq;
using System.Text;
using UnityEngine;
namespace MiraAPI.Utilities
{
public static class Helpers
{
public static readonly ContactFilter2D Filter = ContactFilter2D.CreateLegacyFilter(Constants.NotShipMask, float.MinValue, float.MaxValue);
public static PlainShipRoom GetRoom(Vector3 pos)
{
return ShipStatus.Instance.AllRooms.ToList().Find(room => room.roomArea.OverlapPoint(pos));
}
public static string GetSuffix(NumberSuffixes suffix)
{
return suffix switch
{
NumberSuffixes.None => string.Empty,
NumberSuffixes.Multiplier => "x",
NumberSuffixes.Seconds => "s",
_ => string.Empty
};
}
public static StringBuilder CreateForRole(ICustomRole role)
{
var taskStringBuilder = new StringBuilder();
taskStringBuilder.AppendLine($"{role.RoleColor.ToTextColor()}You are a <b>{role.RoleName}.</b></color>");
taskStringBuilder.Append("<size=70%>");
taskStringBuilder.AppendLine($"{role.RoleLongDescription}");
return taskStringBuilder;
}
}
}