Compare commits

...
Author SHA1 Message Date
Atony 83be29d6a2 Add wip caching 2026-06-13 12:10:21 -05:00
Atony e3b0eb68c0 Temporary measures for clearing up memory leaks 2026-06-12 18:03:13 -05:00
amber 90921ff786 Remove UseVanillaVentButton as it's unnecessary 2026-06-11 22:37:10 -04:00
amber c1d0bb5d26 Seperate vanilla vent data receiving from the actual button 2026-06-11 22:34:27 -04:00
Atony c59ed8eaa6 Fix screenshake within options menu 2026-05-31 09:17:26 -05:00
Atony d2fafc3650 Fix errors in How to Play menu 2026-05-04 09:55:37 -05:00
Atony 0c0157fed2 Add soft-support for Corsac Cosmetics 2026-05-04 07:43:53 -05:00
Atony 90bf402cbc Fix UI resizing with the top right buttons 2026-05-03 11:57:04 -05:00
Atony fed3abafb2 Bump version 2026-04-18 08:31:59 -05:00
Atony 925d47a275 Fix framerate slider 2026-04-18 08:29:11 -05:00
Atony 45a3597ab2 Update dependencies 2026-04-04 20:50:27 -05:00
Chipseq faeb39d3de use AccessTools to allow any method to be an event handler 2026-04-04 22:05:41 +02:00
Atony 6b46cffe5e Add isKilling check for basic kills 2026-04-03 20:31:27 -05:00
Atony 00a60bb7ad Add methods for StateMachineWrapper 2026-04-03 09:46:44 -05:00
Atony d89bce6956 Merge branch 'master' into dev 2026-04-01 21:33:21 -05:00
Atony 7f92e94293 Fix options menu error on mobile 2026-04-01 21:30:28 -05:00
Atony 6199e55110 Add Guardian Angel protection checks 2026-04-01 21:30:19 -05:00
Pietro420 eac442443b Random Snapshot for kill anim 2026-03-29 22:34:43 -03:00
Pietro420 6321753ec0 Don't use nullable for kill anim 2026-03-29 22:26:07 -03:00
XtraCube e66d45bd21 revert ID change for now 2026-03-23 19:33:16 -04:00
XtraCube 1e00181220 Change plugin identifier for Mira API 2026-03-23 12:07:08 -04:00
Atony fcd7856571 Merge pull request #82 from All-Of-Us-Mods/dev
MiraAPI 0.3.9
2026-03-21 16:25:04 -05:00
Atony 5680d27db1 Merge branch 'master' into dev 2026-03-21 16:22:27 -05:00
Atony 49d77fcf40 Fix issues with PlayerControl.Die race condition 2026-03-11 15:40:50 -05:00
Atony dcb4a3876f Fix neutral color in view pane 2026-03-11 00:23:32 -05:00
Atony 9270de408d Verify that target isn't already dead or dc'd 2026-03-10 18:04:22 -05:00
Atony 8940de76bb Add one last murder check 2026-03-10 00:18:55 -05:00
Atony 1813fe5a84 Adjust functions 2026-03-09 09:43:00 -05:00
Atony 272b14f0e0 Change murders to check with the host beforehand 2026-03-09 09:35:36 -05:00
XtraCube 6cc1d4b762 updated readme 2026-02-27 12:11:41 -05:00
Atony 9dfad78df6 Merge pull request #81 from All-Of-Us-Mods/dev
MiraAPI 0.3.8
2026-02-11 11:50:15 -06:00
Atony 7f506000a4 Check for vanilla kills 2026-02-11 11:40:55 -06:00
Atony 795b6a81d7 Merge branch 'dev' of https://github.com/All-Of-Us-Mods/MiraAPI into dev 2026-02-10 14:00:51 -06:00
Atony bd8d7a3208 Add lobby checks for kills 2026-02-10 14:00:47 -06:00
34 changed files with 708 additions and 354 deletions

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
<Project>
<ItemGroup>
<PackageReference Include="Reactor" Version="2.3.1" />
<PackageReference Include="Reactor" Version="2.5.0-ci.371" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.10.14" PrivateAssets="all" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.3.31" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime" />
+1 -1
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.3.7</VersionPrefix>
<VersionPrefix>0.4.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
@@ -4,6 +4,7 @@ using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
@@ -137,7 +138,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
internal void DestroyComponent(ModifierUiComponent component)
{
_modifiers.Remove(component.Modifier!);
component.gameObject.DestroyImmediate();
component.gameObject.DeepDestroy();
RefreshModifiers();
}
@@ -207,7 +208,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
continue;
}
mod.Value.gameObject.Destroy();
mod.Value.gameObject.DeepDestroy();
_modifiers.Remove(mod.Key);
}
+72 -5
View File
@@ -7,10 +7,12 @@ using Assets.CoreScripts;
using BepInEx.Unity.IL2CPP.Utils;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using Rewired;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -22,7 +24,7 @@ namespace MiraAPI.Networking;
public static class CustomMurderRpc
{
/// <summary>
/// Networked Custom Murder method.
/// Networked Custom Murder method, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
@@ -54,7 +56,7 @@ public static class CustomMurderRpc
}
/// <summary>
/// Networked Custom Murder method, which checks for meetings as well.
/// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
@@ -77,6 +79,10 @@ public static class CustomMurderRpc
bool showKillAnim = true,
bool playKillSound = true)
{
if (LobbyBehaviour.Instance)
{
return;
}
var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError;
var beforeMurderEvent = new BeforeMurderEvent(source, target, inMeeting);
@@ -87,11 +93,68 @@ public static class CustomMurderRpc
beforeMurderEvent.Cancel();
}
if (beforeMurderEvent.IsCancelled)
if (target.ProtectedByGa())
{
beforeMurderEvent.Cancel();
murderResultFlags = MurderResultFlags.FailedProtected;
}
else if (beforeMurderEvent.IsCancelled)
{
murderResultFlags = MurderResultFlags.FailedError;
}
if (beforeMurderEvent.IsCancelled && source.AmOwner)
{
source.isKilling = true;
}
if (!PlayerControl.LocalPlayer.IsHost())
{
return;
}
RpcConfirmCustomMurder(
PlayerControl.LocalPlayer,
source,
target,
murderResultFlags,
resetKillTimer,
createDeadBody,
teleportMurderer,
showKillAnim,
playKillSound);
}
/// <summary>
/// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="host">The game host.</param>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
/// <param name="murderResultFlags">End result for the murder.</param>
/// <param name="resetKillTimer">Should the kill timer be reset.</param>
/// <param name="createDeadBody">Should a dead body be created.</param>
/// <param name="teleportMurderer">Should the killer be snapped to the dead player.</param>
/// <param name="showKillAnim">Should the kill animation be shown.</param>
/// <param name="playKillSound">Should the kill sound be played.</param>
[MethodRpc((uint)MiraRpc.ConfirmCustomMurder, LocalHandling = RpcLocalHandling.After)]
public static void RpcConfirmCustomMurder(
this PlayerControl host,
PlayerControl source,
PlayerControl target,
MurderResultFlags murderResultFlags,
bool resetKillTimer = true,
bool createDeadBody = true,
bool teleportMurderer = true,
bool showKillAnim = true,
bool playKillSound = true)
{
if (LobbyBehaviour.Instance || !host.IsHost() || target.Data.IsDead || target.Data.Disconnected)
{
source.isKilling = false;
return;
}
var murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags;
source.CustomMurder(
@@ -126,7 +189,7 @@ public static class CustomMurderRpc
bool playKillSound = true)
{
source.isKilling = false;
if (resultFlags.HasFlag(MurderResultFlags.FailedError))
if (resultFlags.HasFlag(MurderResultFlags.FailedError) || LobbyBehaviour.Instance)
{
return;
}
@@ -230,7 +293,7 @@ public static class CustomMurderRpc
source.CurrentOutfitType == PlayerOutfitType.Shapeshifted,
source.shapeshiftTargetPlayerId,
target.PlayerId);
source.MyPhysics.StartCoroutine(source.KillAnimations.Random()?.CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
source.MyPhysics.StartCoroutine(source.KillAnimations.RandomSnapshot().CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
}
/// <summary>
@@ -249,6 +312,10 @@ public static class CustomMurderRpc
bool createDeadBody = true,
bool teleportMurderer = true)
{
if (LobbyBehaviour.Instance)
{
yield break;
}
var cam = Camera.main?.GetComponent<FollowerCamera>();
var isParticipant = source.AmOwner || target.AmOwner;
var sourcePhys = source.MyPhysics;
+5
View File
@@ -35,6 +35,11 @@ public enum MiraRpc : uint
/// </summary>
CustomMurder,
/// <summary>
/// Custom Murder RPC Confirmation.
/// </summary>
ConfirmCustomMurder,
/// <summary>
/// Custom Game Over RPC.
/// </summary>
+2 -1
View File
@@ -276,8 +276,9 @@ public static class TaskAdderPatches
__instance.transform.FindChild("TitleText_TMP")?.gameObject.DestroyImmediate();
}
__instance.ActiveItems.ToArray().Do(x => x.gameObject.Destroy());
__instance.ActiveItems.ToArray().Do(x => x.gameObject.DeepDestroy(false));
__instance.ActiveItems.Clear();
Utilities.Extensions.ClearGarbageCollector();
float num = 0f;
float num2 = 0f;
+8 -7
View File
@@ -52,12 +52,7 @@ public static class HudManagerPatches
continue;
}
if (aspect.gameObject.transform.parent.name == "TopRight")
{
continue;
}
if (aspect.gameObject.transform.parent.transform.parent.name == "TopRight")
if (aspect.gameObject.name.Contains("TopRight"))
{
continue;
}
@@ -89,6 +84,11 @@ public static class HudManagerPatches
continue;
}
if (arrange.gameObject.name.Contains("TopRight"))
{
continue;
}
arrange.gameObject.SetActive(!arrange.isActiveAndEnabled);
arrange.CellSize = new Vector2(scaleFactor, scaleFactor);
arrange.gameObject.SetActive(!arrange.isActiveAndEnabled);
@@ -143,8 +143,9 @@ public static class HudManagerPatches
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true))
{
t.gameObject.Destroy();
t.gameObject.DeepDestroy(false);
}
Utilities.Extensions.ClearGarbageCollector();
var gridArrange = BottomLeft.GetComponent<GridArrange>();
var aspectPosition = BottomLeft.GetComponent<AspectPosition>();
@@ -16,8 +16,15 @@ public static class OptionsMenuBehaviourPatch
private static void OpenPostfix(OptionsMenuBehaviour __instance)
{
_remap_rollover = GameObject.Find("Remap Controls")?.GetComponent<ButtonRolloverHandler>()!;
_remap_background = _remap_rollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
_remap_background.color = _conflicts ? Color.red : Color.white;
try
{
_remap_background = _remap_rollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
_remap_background.color = _conflicts ? Color.red : Color.white;
}
catch
{
// ignored, this normally breaks when on mobile since there's no remap button screen.
}
}
[HarmonyPostfix]
+11 -4
View File
@@ -11,8 +11,6 @@ using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
@@ -41,6 +39,10 @@ public static class HatsTabPatches
[HarmonyPrefix]
public static bool OnEnablePrefix(HatsTab __instance)
{
if (!AddressablesLoader.AddressableHatsExist)
{
return true;
}
__instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
@@ -66,6 +68,10 @@ public static class HatsTabPatches
[HarmonyPrefix]
public static void UpdatePrefix(HatsTab __instance)
{
if (!AddressablesLoader.AddressableHatsExist)
{
return;
}
if (sortedHats.Count == 0) return;
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
@@ -86,9 +92,10 @@ public static class HatsTabPatches
if (loadRoutine != null) Coroutines.Stop(loadRoutine);
hatIndex = 0;
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(false);
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
Utilities.Extensions.ClearGarbageCollector();
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
+11 -1
View File
@@ -1,6 +1,8 @@
using HarmonyLib;
using System.Collections;
using HarmonyLib;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Patches.Menu;
@@ -19,6 +21,14 @@ public static class MainMenuManagerPatches
public static void AwakePostfix()
{
AddressablesLoader.LoadAll();
Coroutines.Start(SetFps());
}
private static IEnumerator SetFps()
{
Application.targetFrameRate = (int)LocalSettingsTabSingleton<MiraApiSettings>.Instance.SetFpsSlider.Value;
yield return new WaitForSeconds(1f);
Application.targetFrameRate = (int)LocalSettingsTabSingleton<MiraApiSettings>.Instance.SetFpsSlider.Value;
}
}
+12 -2
View File
@@ -6,6 +6,7 @@ using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -44,6 +45,10 @@ public static class NameplatesTabPatches
[HarmonyPrefix]
public static bool OnEnablePrefix(NameplatesTab __instance)
{
if (!AddressablesLoader.AddressableNameplatesExist)
{
return true;
}
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
@@ -60,6 +65,10 @@ public static class NameplatesTabPatches
public static void UpdatePrefix(NameplatesTab __instance)
{
if (!AddressablesLoader.AddressableNameplatesExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
@@ -72,9 +81,10 @@ public static class NameplatesTabPatches
private static void GenerateHats(NameplatesTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(false);
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
Utilities.Extensions.ClearGarbageCollector();
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
+12 -2
View File
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -36,6 +37,10 @@ public static class SkinsTabPatches
[HarmonyPrefix]
public static bool OnEnablePrefix(SkinsTab __instance)
{
if (!AddressablesLoader.AddressableSkinsExist)
{
return true;
}
__instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId;
var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList();
@@ -62,6 +67,10 @@ public static class SkinsTabPatches
public static void UpdatePrefix(SkinsTab __instance)
{
if (!AddressablesLoader.AddressableSkinsExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
@@ -74,9 +83,10 @@ public static class SkinsTabPatches
private static void GenerateHats(SkinsTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(false);
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
Utilities.Extensions.ClearGarbageCollector();
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
+12 -2
View File
@@ -5,6 +5,7 @@ using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -44,6 +45,10 @@ public static class VisorsTabPatches
[HarmonyPrefix]
public static bool OnEnablePrefix(VisorsTab __instance)
{
if (!AddressablesLoader.AddressableVisorsExist)
{
return true;
}
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
@@ -60,6 +65,10 @@ public static class VisorsTabPatches
public static void UpdatePrefix(VisorsTab __instance)
{
if (!AddressablesLoader.AddressableVisorsExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
@@ -72,9 +81,10 @@ public static class VisorsTabPatches
private static void GenerateHats(VisorsTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(false);
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
Utilities.Extensions.ClearGarbageCollector();
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
+67 -17
View File
@@ -139,11 +139,25 @@ internal static class GameOptionsMenuPatch
private static void CustomMenuOneCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
foreach (var group in groups)
var existing = GameSettingMenuPatches.CachedGameSettings.FirstOrDefault(x => x.ModId == GameSettingMenuPatches.SelectedModIdx && x.Category == MenuCategory.CustomOne);
if (existing != null)
{
CreateGroup(menu, group);
GameSettingMenuPatches.ShowGameOptions(existing, menu);
}
else
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
GameSettingMenuPatches.AddGameSettings(
MenuCategory.CustomOne,
menu.Children,
menu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList());
}
}
@@ -160,11 +174,25 @@ internal static class GameOptionsMenuPatch
private static void CustomMenuTwoCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
foreach (var group in groups)
var existing = GameSettingMenuPatches.CachedGameSettings.FirstOrDefault(x => x.ModId == GameSettingMenuPatches.SelectedModIdx && x.Category == MenuCategory.CustomTwo);
if (existing != null)
{
CreateGroup(menu, group);
GameSettingMenuPatches.ShowGameOptions(existing, menu);
}
else
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
GameSettingMenuPatches.AddGameSettings(
MenuCategory.CustomTwo,
menu.Children,
menu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList());
}
}
@@ -181,11 +209,25 @@ internal static class GameOptionsMenuPatch
private static void ModifiersCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? [];
foreach (var group in groups)
var existing = GameSettingMenuPatches.CachedGameSettings.FirstOrDefault(x => x.ModId == GameSettingMenuPatches.SelectedModIdx && x.Category == MenuCategory.Modifiers);
if (existing != null)
{
CreateGroup(menu, group);
GameSettingMenuPatches.ShowGameOptions(existing, menu);
}
else
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
GameSettingMenuPatches.AddGameSettings(
MenuCategory.Modifiers,
menu.Children,
menu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList());
}
}
@@ -217,12 +259,20 @@ internal static class GameOptionsMenuPatch
return false;
}
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
var existing = GameSettingMenuPatches.CachedGameSettings.FirstOrDefault(x => x.ModId == GameSettingMenuPatches.SelectedModIdx && x.Category == MenuCategory.Game);
if (existing != null)
{
CreateGroup(__instance, group);
GameSettingMenuPatches.ShowGameOptions(existing, __instance);
}
else
{
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
{
CreateGroup(__instance, group);
}
}
return false;
@@ -262,7 +262,7 @@ internal static class GamePresetsTabPatches
iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset();
Helpers.DivideSize(refreshButton.gameObject, 2f);
refreshButton.buttonText.gameObject.Destroy();
refreshButton.buttonText.gameObject.DeepDestroy();
refreshButton.OnClick = new Button.ButtonClickedEvent();
refreshButton.OnClick.AddListener((UnityAction)Refresh);
+146 -38
View File
@@ -6,6 +6,7 @@ using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
@@ -40,6 +41,140 @@ internal static class GameSettingMenuPatches
private static PassiveButton? _customTwoButton;
private static GameObject? _nextModButton;
private static GameObject? _previousModButton;
public static List<GameSettingsCache> CachedGameSettings { get; } = [];
public static List<RoleSettingsCache> CachedRoleSettings { get; } = [];
public sealed class GameSettingsCache
{
public int ModId { get; set; }
public MenuCategory Category { get; set; } = MenuCategory.Game;
public Il2CppSystem.Collections.Generic.List<OptionBehaviour> Options { get; set; }
public List<CategoryHeaderMasked> CategoryHeaders { get; set; }
public GameObject CacheHolder { get; set; }
}
public static GameSettingsCache GetCurrentSettings(MenuCategory category)
{
var existing = CachedGameSettings.FirstOrDefault(x => x.ModId == SelectedModIdx && x.Category == category);
if (existing != null)
{
return existing;
}
var menu = category switch
{
MenuCategory.Modifiers => _modifiersTab!,
MenuCategory.CustomOne => _customOneTab!,
MenuCategory.CustomTwo => _customTwoTab!,
_ => GameSettingMenu.Instance.GameSettingsTab,
};
return AddGameSettings(
category,
menu.Children,
menu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList());
}
public static GameSettingsCache AddGameSettings(MenuCategory category, Il2CppSystem.Collections.Generic.List<OptionBehaviour> options, List<CategoryHeaderMasked> categoryHeaders)
{
var modName = SelectedMod?.MiraPlugin.OptionsTitleText ?? "Vanilla";
var newHolder = new GameObject($"{modName} {category}");
newHolder.DontUnload().DontDestroy();
newHolder.SetActive(false);
var newCache = new GameSettingsCache
{
ModId = SelectedModIdx,
Category = category,
Options = options,
CategoryHeaders = categoryHeaders,
CacheHolder = newHolder,
};
CachedGameSettings.Add(newCache);
return newCache;
}
public static void HideGameOptions(GameSettingsCache cached, GameOptionsMenu gameOptMenu)
{
foreach (var obj in gameOptMenu.scrollBar.Inner.GetComponentsInChildren<Transform>())
{
if (obj.gameObject.name == "MapPicker" || !obj.gameObject)
{
continue;
}
obj.SetParent(cached.CacheHolder.transform);
}
}
public static void ShowGameOptions(GameSettingsCache cached, GameOptionsMenu gameOptMenu)
{
foreach (var obj in cached.CacheHolder.GetComponentsInChildren<Transform>())
{
if (!obj.gameObject)
{
continue;
}
obj.SetParent(gameOptMenu.scrollBar.Inner);
}
}
public sealed class RoleSettingsCache
{
public int ModId { get; set; }
public Il2CppSystem.Collections.Generic.List<OptionBehaviour> Options { get; set; }
public List<CategoryHeaderEditRole> RoleHeaders { get; set; }
public List<RoleOptionSetting> RoleOptions { get; set; }
public List<CategoryHeaderEditRole> CategoryHeaders { get; set; }
public GameObject CacheHolder { get; set; }
}
public static RoleSettingsCache GetCurrentRoleSettings()
{
var existing = CachedRoleSettings.FirstOrDefault(x => x.ModId == SelectedModIdx);
if (existing != null)
{
return existing;
}
var menu = GameSettingMenu.Instance.RoleSettingsTab;
return AddRoleSettings(
menu.advancedSettingChildren,
menu.RoleChancesSettings
.GetComponentsInChildren<CategoryHeaderEditRole>()
.ToList());
}
public static RoleSettingsCache AddRoleSettings(Il2CppSystem.Collections.Generic.List<OptionBehaviour> options, List<CategoryHeaderEditRole> categoryHeaders)
{
var modName = SelectedMod?.MiraPlugin.OptionsTitleText ?? "Vanilla";
var newHolder = new GameObject($"{modName} Roles");
newHolder.DontUnload().DontDestroy();
newHolder.SetActive(false);
var newCache = new RoleSettingsCache
{
ModId = SelectedModIdx,
Options = options,
CategoryHeaders = categoryHeaders,
CacheHolder = newHolder,
};
CachedRoleSettings.Add(newCache);
return newCache;
}
public static void HideRoleOptions(RoleSettingsCache cached, RolesSettingsMenu gameOptMenu)
{
foreach (var obj in gameOptMenu.scrollBar.Inner.GetComponentsInChildren<Transform>())
{
if (obj.gameObject.name == "MapPicker" || !obj.gameObject)
{
continue;
}
obj.SetParent(cached.CacheHolder.transform);
}
}
public static void ShowRoleOptions(RoleSettingsCache cached, RolesSettingsMenu gameOptMenu)
{
foreach (var obj in cached.CacheHolder.GetComponentsInChildren<Transform>())
{
if (!obj.gameObject)
{
continue;
}
obj.SetParent(gameOptMenu.scrollBar.Inner);
}
}
private static Dictionary<int, Vector3> OptionsPositions { get; } = [];
private static Dictionary<int, Vector3> ModifiersPositions { get; } = [];
@@ -564,21 +699,6 @@ internal static class GameSettingMenuPatches
CleanupTab(settings, roles);
}
private static void ClearOptions(Il2CppSystem.Collections.Generic.List<OptionBehaviour> options)
{
foreach (var child in options)
{
if (child.TryCast<GameOptionsMapPicker>() || !child.gameObject)
{
continue;
}
child.gameObject.DestroyImmediate();
}
options.Clear();
}
private static void CleanupTab(GameOptionsMenu settings, RolesSettingsMenu roles)
{
if (roles.roleChances != null)
@@ -608,24 +728,10 @@ internal static class GameSettingMenuPatches
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
{
if (rolesMenu.advancedSettingChildren != null)
{
ClearOptions(rolesMenu.advancedSettingChildren);
rolesMenu.advancedSettingChildren = null;
}
HideRoleOptions(GetCurrentRoleSettings(), rolesMenu);
rolesMenu.advancedSettingChildren = null;
rolesMenu.RoleChancesSettings
.transform
.GetComponentsInChildren<CategoryHeaderEditRole>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
foreach (var role in rolesMenu.roleChances)
{
role.gameObject.DestroyImmediate();
}
rolesMenu.roleChances?.Clear();
rolesMenu.roleChances.Clear();
rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false);
rolesMenu.RoleChancesSettings.gameObject.SetActive(true);
@@ -643,14 +749,16 @@ internal static class GameSettingMenuPatches
void CleanupSettings(GameOptionsMenu gameOptMenu, MenuCategory menuCategory = MenuCategory.Roles)
{
ClearOptions(gameOptMenu.Children);
if (menuCategory is MenuCategory.Roles)
{
HideRoleOptions(GetCurrentRoleSettings(), gameOptMenu.RolesMenu);
}
else
{
HideGameOptions(GetCurrentSettings(menuCategory), gameOptMenu);
}
gameOptMenu.Children = null;
gameOptMenu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
var positions = OptionsPositions;
switch (menuCategory)
@@ -210,12 +210,19 @@ public static class LobbyViewPanePatches
list.Add(roleBehaviour);
}
var color = (i == 0) ? Palette.CrewmateRoleBlue : Palette.ImpostorRoleRed;
if (roleBehaviour is ICustomRole custom && (custom.Team is not ModdedRoleTeams.Crewmate &&
custom.Team is not ModdedRoleTeams.Impostor))
{
color = Color.grey;
}
viewSettingsInfoPanelRoleVariant.SetInfo(
roleBehaviour.NiceName,
numPerGame,
chancePerGame,
61,
(i == 0) ? Palette.CrewmateRoleBlue : Palette.ImpostorRoleRed,
color,
roleBehaviour.RoleIconSolid,
i == 0,
flag);
@@ -513,6 +520,34 @@ public static class LobbyViewPanePatches
instance.scrollBar.SetYBoundsMax(-num);
}
public static void SetModdedHeader(this CategoryHeaderRoleVariant header, StringNames roleName, int maskLayer, ModdedRoleTeams team, Sprite? roleIcon = null)
{
header.SetHeader(roleName, maskLayer);
if (team is ModdedRoleTeams.Crewmate)
{
header.Background.color = Palette.CrewmateRoleHeaderBlue;
header.Divider.color = Palette.CrewmateRoleHeaderBlue;
header.Title.color = Palette.CrewmateRoleHeaderTextBlue;
}
else if (team is ModdedRoleTeams.Impostor)
{
header.Background.color = Palette.ImpostorRoleHeaderRed;
header.Divider.color = Palette.ImpostorRoleHeaderRed;
header.Title.color = Palette.ImpostorRoleHeaderTextRed;
}
else
{
header.Background.color = Color.grey;
header.Divider.color = Color.grey;
header.Title.color = new Color32(50, 50, 50, 255);
}
if (roleIcon != null && header.icon != null)
{
header.icon.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
header.icon.sprite = roleIcon;
}
}
private static float SetUpAdvancedRoleViewPanel(
AdvancedRoleViewPanel viewPanel,
Type roleType,
@@ -536,10 +571,10 @@ public static class LobbyViewPanePatches
return 0;
}
viewPanel.header.SetHeader(
viewPanel.header.SetModdedHeader(
role.StringName,
maskLayer,
role.TeamType == RoleTeamTypes.Crewmate,
customRole.Team,
customRole.Configuration.Icon != null ? customRole.Configuration.Icon.LoadAsset() : MiraAssets.Empty.LoadAsset());
viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f);
viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
@@ -41,12 +41,6 @@ public static class RoleSettingMenuPatches
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.OnDisable))]
public static void ClosePatch(RolesSettingsMenu __instance)
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = true;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
@@ -59,9 +53,11 @@ public static class RoleSettingMenuPatches
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static bool SetQuotaTabPatch(RolesSettingsMenu __instance)
{
Headers.ForEach(Object.Destroy);
RoleOptionSettings.ForEach(Object.Destroy);
GameSettingMenuPatches.HideRoleOptions(GameSettingMenuPatches.GetCurrentRoleSettings(), __instance);
__instance.advancedSettingChildren = null;
Headers.Clear();
__instance.roleChances.Clear();
RoleOptionSettings.Clear();
CurrentRole = null;
CurrentRoleOptions = null;
@@ -139,6 +135,7 @@ public static class RoleSettingMenuPatches
num3++;
}
Utilities.Extensions.ClearGarbageCollector();
return false;
}
@@ -160,6 +157,7 @@ public static class RoleSettingMenuPatches
if (roleGroups is null)
{
Utilities.Extensions.ClearGarbageCollector();
return false;
}
@@ -210,9 +208,9 @@ public static class RoleSettingMenuPatches
var blankLabel = quotaInst.transform.FindChild("BlankLabel").gameObject;
var chanceLabel = quotaInst.transform.FindChild("Chance Label").gameObject;
var countLabel = quotaInst.transform.FindChild("# Label").gameObject;
blankLabel.Destroy();
chanceLabel.Destroy();
countLabel.Destroy();
blankLabel.DeepDestroy(false);
chanceLabel.DeepDestroy(false);
countLabel.DeepDestroy(false);
categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear;
@@ -295,15 +293,10 @@ public static class RoleSettingMenuPatches
{
RoleGroupHidden[group] = !groupHidden;
}
foreach (var header in Headers)
{
header.Destroy();
}
GameSettingMenuPatches.HideRoleOptions(GameSettingMenuPatches.GetCurrentRoleSettings(), __instance);
__instance.advancedSettingChildren = null;
Headers.Clear();
foreach (var option in RoleOptionSettings)
{
option.gameObject.Destroy();
}
__instance.roleChances.Clear();
RoleOptionSettings.Clear();
__instance.SetQuotaTab();
}));
@@ -314,6 +307,7 @@ public static class RoleSettingMenuPatches
ScrollerNum -= 0.4f;
}
}
Utilities.Extensions.ClearGarbageCollector();
__instance.scrollBar.SetScrollBounds(__instance);
return false;
@@ -428,8 +422,9 @@ public static class RoleSettingMenuPatches
{
foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren<OptionBehaviour>())
{
optBehaviour.gameObject.DestroyImmediate();
optBehaviour.gameObject.DeepDestroy(false);
}
Utilities.Extensions.ClearGarbageCollector();
CurrentRole = role;
__instance.advancedSettingChildren.Clear();
@@ -624,7 +619,7 @@ public static class RoleSettingMenuPatches
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
newButton.name = "ConfigButton";
newButton.transform.localPosition = new Vector3(0.4473f, -0.3f, -2f);
newButton.transform.FindChild("Text_TMP").gameObject.DestroyImmediate();
newButton.transform.FindChild("Text_TMP").gameObject.DeepDestroy();
newButton.activeSprites.Destroy();
var btnRend = newButton.transform.FindChild("ButtonSprite").GetComponent<SpriteRenderer>();
+14
View File
@@ -117,6 +117,20 @@ internal static class PlayerControlPatches
__instance.RpcMurderPlayer(target, true);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
[HarmonyPatch(nameof(PlayerControl.MurderPlayer))]
// ReSharper disable once InconsistentNaming
public static bool MurderPlayerPrefix(PlayerControl __instance)
{
if (LobbyBehaviour.Instance)
{
return false;
}
return true;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.FixedUpdate))]
// ReSharper disable once InconsistentNaming
+28 -13
View File
@@ -22,22 +22,18 @@ internal static class HowToPlayPatch
__instance.startPage.SetActive(false);
if (__instance.roleButtonsParent.childCount == 0)
{
using (IEnumerator<RoleBehaviour> enumerator = RoleManager.Instance.AllRoles.ToArray().Where(x => !x.IsCustomRole()).GetEnumerator())
foreach (var role in RoleManager.Instance.AllRoles.ToArray().Where(x => !x.IsCustomRole()))
{
while (enumerator.MoveNext())
if (!role.IsSimpleRole && role.Role != RoleTypes.CrewmateGhost && role.Role != RoleTypes.ImpostorGhost)
{
RoleBehaviour role = enumerator.Current;
if (!role.IsSimpleRole && role.Role != RoleTypes.CrewmateGhost && role.Role != RoleTypes.ImpostorGhost)
HowToPlayRoleButton component = Object.Instantiate(__instance.roleButtonPrefab, __instance.roleButtonsParent).GetComponent<HowToPlayRoleButton>();
Sprite roleIcon = __instance.rolesScenes.ToArray().First(r => r.role == role.Role).roleIcon;
component.SetRoleInfo(role, roleIcon);
component.SetButtonAction((Il2CppSystem.Action)(() =>
{
HowToPlayRoleButton component = Object.Instantiate(__instance.roleButtonPrefab, __instance.roleButtonsParent).GetComponent<HowToPlayRoleButton>();
Sprite roleIcon = __instance.rolesScenes.ToArray().First(r => r.role == role.Role).roleIcon;
component.SetRoleInfo(role, roleIcon);
component.SetButtonAction((Il2CppSystem.Action)(() =>
{
__instance.OpenRolePage(role.Role);
}));
__instance.controllerSelectables.Add(component.GetComponent<PassiveButton>());
}
OpenRolePage(__instance, role.Role);
}));
__instance.controllerSelectables.Add(component.GetComponent<PassiveButton>());
}
}
foreach (UiElement uiElement in __instance.controllerSelectables)
@@ -50,4 +46,23 @@ internal static class HowToPlayPatch
__instance.roleSelectionScene.SetActive(true);
ControllerManager.Instance.SetDefaultSelection(__instance.defaultButtonSelected, null);
}
public static void OpenRolePage(HowToPlayScene instance, RoleTypes roleType)
{
instance.category = HowToPlayScene.HowToPlayCategory.Roles;
var newList = instance.rolesScenes.ToArray().ToList();
var buttonList = instance.roleButtons;
instance.sceneIndex = newList.FindIndex(r => r.role == roleType);
if (roleType != RoleTypes.Crewmate)
{
foreach (var button in buttonList)
{
if (button.GetRole().Role == roleType)
{
instance.previouslySelectedRoleButton = button.GetComponent<PassiveButton>();
}
}
}
instance.SetupDots(instance.rolesScenes[instance.sceneIndex].rolePages.Count);
instance.ChangeScene(0);
}
}
+4 -2
View File
@@ -1,7 +1,9 @@
using HarmonyLib;
using InnerNet;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
@@ -69,9 +71,9 @@ public static class HudManagerPatches
CustomRoleManager.UpdateRoleTab(RoleTab, customRole);
}
else if (RoleTab)
else if (RoleTab != null)
{
RoleTab?.gameObject.Destroy();
RoleTab.gameObject.DeepDestroy();
}
}
}
+2 -1
View File
@@ -6,6 +6,7 @@ using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
@@ -32,7 +33,7 @@ public static class RoleManagerPatches
if (data.Role)
{
data.Role.Deinitialize(targetPlayer);
Object.Destroy(data.Role.gameObject);
data.Role.gameObject.DeepDestroy();
}
var roleBehaviour = Object.Instantiate<RoleBehaviour>(__instance.AllRoles.ToArray().First(r => r.Role == roleType), data.gameObject.transform);
roleBehaviour.Initialize(targetPlayer);
+1 -1
View File
@@ -28,7 +28,7 @@ internal static class VentCanUsePatch
var @object = pc.Object;
var role = @object.Data.Role;
var canVent = role is ICustomRole customRole ? customRole.Configuration.CanUseVent : role.CanVent;
var canVent = role is ICustomRole customRole ? customRole.Configuration.CanUseVent || customRole.Configuration.GetsVentData : role.CanVent;
couldUse = canVent;
var modifiers = @object.GetModifierComponent().ActiveModifiers;
+2 -2
View File
@@ -6,6 +6,7 @@ using System.Linq;
using System.Reflection;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.Colors;
using MiraAPI.Events;
using MiraAPI.GameEnd;
@@ -21,7 +22,6 @@ using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Networking;
using Reactor.Utilities;
using Rewired;
namespace MiraAPI.PluginLoading;
@@ -60,7 +60,7 @@ public sealed class MiraPluginManager
continue;
}
foreach (var method in type.GetMethods())
foreach (var method in AccessTools.GetDeclaredMethods(type))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
+3 -1
View File
@@ -4,6 +4,7 @@ using System.Linq;
using BepInEx.Configuration;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities;
using UnityEngine;
@@ -61,9 +62,10 @@ public static class PresetManager
{
if (btn != null)
{
Object.DestroyImmediate(btn);
btn.DeepDestroy(false);
}
}
Utilities.Extensions.ClearGarbageCollector();
plugin.InternalPresets.Clear();
if (!Directory.Exists(PresetDirectory))
+1 -1
View File
@@ -26,7 +26,7 @@ public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr)
// Cleanup holder object
public void OnDestroy()
{
transform.parent.gameObject.Destroy();
transform.parent.gameObject.DeepDestroy();
}
public override void Close()
+6 -1
View File
@@ -109,10 +109,15 @@ public record struct CustomRoleConfiguration
public bool UseVanillaKillButton { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the role can use vents.
/// Gets or sets a value indicating whether the role can use vents. Also sends vanilla vent data.
/// </summary>
public bool CanUseVent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the role can receive vanilla vent data.
/// </summary>
public bool GetsVentData { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the role can use the sabotage button.
/// </summary>
+2 -2
View File
@@ -123,7 +123,7 @@ public static class CustomRoleManager
if (roleBehaviour is not ICustomRole customRole)
{
roleBehaviour.gameObject.Destroy();
roleBehaviour.gameObject.DeepDestroy();
return null;
}
@@ -139,7 +139,7 @@ public static class CustomRoleManager
roleBehaviour.CanBeKilled = customRole.Configuration.CanGetKilled;
roleBehaviour.CanUseKillButton = customRole.Configuration.UseVanillaKillButton;
roleBehaviour.TasksCountTowardProgress = customRole.Configuration.TasksCountForProgress;
roleBehaviour.CanVent = customRole.Configuration.CanUseVent;
roleBehaviour.CanVent = customRole.Configuration.CanUseVent || customRole.Configuration.GetsVentData;
roleBehaviour.DefaultGhostRole = customRole.Configuration.GhostRole;
roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount;
roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot?.LoadAsset();
@@ -24,6 +24,26 @@ public static class AddressablesLoader
private static readonly List<(string, string)> RegisteredNameplateKeys = [];
private static readonly List<string> RegisteredSkinKeys = [];
/// <summary>
/// Gets a value indicating whether hats have been loaded by the addressables system.
/// </summary>
public static bool AddressableHatsExist { get; private set; }
/// <summary>
/// Gets a value indicating whether visors have been loaded by the addressables system.
/// </summary>
public static bool AddressableVisorsExist { get; private set; }
/// <summary>
/// Gets a value indicating whether nameplates have been loaded by the addressables system.
/// </summary>
public static bool AddressableNameplatesExist { get; private set; }
/// <summary>
/// Gets a value indicating whether skins have been loaded by the addressables system.
/// </summary>
public static bool AddressableSkinsExist { get; private set; }
/// <summary>
/// Registers a specific addressables package to load asynchronously at the start of the game, when possible.
/// </summary>
@@ -52,6 +72,7 @@ public static class AddressablesLoader
return;
}
AddressableHatsExist = true;
RegisteredHatKeys.Add(addressablesKey);
}
@@ -67,6 +88,7 @@ public static class AddressablesLoader
return;
}
AddressableSkinsExist = true;
RegisteredSkinKeys.Add(addressablesKey);
}
@@ -83,6 +105,7 @@ public static class AddressablesLoader
return;
}
AddressableNameplatesExist = true;
RegisteredNameplateKeys.Add((addressablesKey, groupTitle));
}
@@ -99,6 +122,7 @@ public static class AddressablesLoader
return;
}
AddressableVisorsExist = true;
RegisteredVisorKeys.Add((addressablesKey, groupTitle));
}
+152 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -53,7 +54,7 @@ public static class Extensions
bool isComplete;
if (self.amClosing == Minigame.CloseState.Closing)
{
UnityEngine.Object.Destroy(self.gameObject);
self.gameObject.DeepDestroy();
return;
}
if (self.CloseSound && Constants.ShouldPlaySfx())
@@ -132,6 +133,31 @@ public static class Extensions
button.SetCooldownFill(num);
}
/// <summary>
/// Returns a random element from the specified sequence.
/// </summary>
/// <param name="input">
/// The sequence to select an element from.
/// </param>
/// <typeparam name="T">
/// The type of elements in the sequence.
/// </typeparam>
/// <returns>
/// A randomly selected element from <paramref name="input"/>.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown when <paramref name="input"/> is empty.
/// </exception>
public static T RandomSnapshot<T>(this IEnumerable<T> input)
{
var list = input.ToList();
if (list.Count == 0)
throw new InvalidOperationException("Cannot get random element from an empty collection.");
return list[UnityEngine.Random.Range(0, list.Count)];
}
/// <summary>
/// Gets a PlayerControl from their PlayerVoteArea in a meeting.
/// </summary>
@@ -156,6 +182,16 @@ public static class Extensions
return TutorialManager.InstanceExists || AmongUsClient.Instance.HostId == playerControl.OwnerId;
}
/// <summary>
/// Checks if a PlayerControl is protected by a Guardian Angel.
/// </summary>
/// <param name="playerControl">The player you're checking for.</param>
/// <returns>If the player is protected, true, else false.</returns>
public static bool ProtectedByGa(this PlayerControl playerControl)
{
return playerControl.protectedByGuardianId > -1;
}
/// <summary>
/// Used to convert a System.Collections.Generic.List to Il2cppSystem.
/// </summary>
@@ -184,6 +220,121 @@ public static class Extensions
return Mathf.Approximately(number, Mathf.Round(number));
}
/// <summary>
/// Destroys the object properly.
/// </summary>
/// <param name="obj">The object to destroy.</param>
/// <param name="clearGc">Whether to run the garbage collector immediately.</param>
public static void DeepDestroy(this GameObject? obj, bool clearGc = true)
{
Coroutines.Start(Nuke(obj, clearGc));
}
private static IEnumerator Nuke(GameObject? go, bool clearGc)
{
if (go == null)
yield break;
try
{
go.transform.SetParent(null, false);
}
catch
{
// ignored
}
try
{
go.SetActive(false);
}
catch
{
// ignored
}
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
if (mb == null)
continue;
try
{
mb.StopAllCoroutines();
}
catch
{
// ignored
}
try
{
mb.enabled = false;
}
catch
{
// ignored
}
}
foreach (var renderer in go.GetComponentsInChildren<Renderer>(true))
{
if (renderer == null)
continue;
try
{
foreach (var mat in renderer.materials)
{
if (mat != null)
UnityEngine.Object.Destroy(mat);
}
}
catch
{
// ignored
}
}
foreach (var filter in go.GetComponentsInChildren<MeshFilter>(true))
{
if (filter == null)
continue;
try
{
var mesh = filter.mesh;
if (mesh != null)
UnityEngine.Object.Destroy(mesh);
}
catch
{
// ignored
}
}
UnityEngine.Object.Destroy(go);
yield return null;
if (clearGc)
{
yield return CoFreeResources();
}
}
public static void ClearGarbageCollector()
{
Coroutines.Start(CoFreeResources());
}
private static IEnumerator CoFreeResources()
{
yield return Resources.UnloadUnusedAssets();
GC.Collect(0, GCCollectionMode.Forced, blocking: true);
GC.WaitForPendingFinalizers();
GC.Collect(0, GCCollectionMode.Forced, blocking: true);
}
/// <summary>
/// Gets a cache of player's vote data components to improve performance.
/// </summary>
+1 -1
View File
@@ -539,7 +539,7 @@ public static class Helpers
}
/// <summary>
/// Returns whether the role is blacklisted from appearing and spawning. (Only applicable to vanilla roles)
/// Returns whether the role is blacklisted from appearing and spawning. Only applicable to vanilla roles.
/// </summary>
/// <param name="role">The role to check.</param>
/// <returns>The role's blacklist status.</returns>
+16 -1
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Utilities;
@@ -17,6 +18,7 @@ public class StateMachineWrapper<T> where T : Il2CppObjectBase
// normally it is fields, but IL2CPP turns them into properties
private readonly PropertyInfo _thisProperty;
private readonly PropertyInfo _stateProperty;
private readonly PropertyInfo _currentProperty;
private readonly Dictionary<string, PropertyInfo> _propertyCache;
private T? _parentInstance;
@@ -37,8 +39,9 @@ public class StateMachineWrapper<T> where T : Il2CppObjectBase
var type = _stateMachine.GetType();
_thisProperty = AccessTools.Property(type, "__4__this");
_stateProperty = AccessTools.Property(type, "__1__state");
_currentProperty = AccessTools.Property(type, "__2__current");
if (_thisProperty == null || _stateProperty == null)
if (_thisProperty == null || _stateProperty == null || _currentProperty == null)
{
throw new MissingMemberException($"Could not find required properties in type '{type}'.");
}
@@ -52,6 +55,18 @@ public class StateMachineWrapper<T> where T : Il2CppObjectBase
/// <returns>The current state as an integer.</returns>
public int GetState() => (int)_stateProperty.GetValue(_stateMachine)!;
/// <summary>
/// Sets the current state of the state machine.
/// </summary>
/// <param name="newState">The new state to use.</param>
public void SetState(int newState) => _stateProperty.SetValue(_stateMachine, newState);
/// <summary>
/// Sets the newest yield return of the state machine.
/// </summary>
/// <param name="newReturn">The new return to use.</param>
public void SetRecentReturn(Object newReturn) => _currentProperty.SetValue(_stateMachine, newReturn);
/// <summary>
/// Gets a parameter from the state machine by its name.
/// </summary>
+24 -216
View File
@@ -5,6 +5,7 @@
# Mira API
A thorough, but simple, Among Us modding API and utility library that covers:
- Roles
- Options
- Modifiers
@@ -13,6 +14,9 @@ A thorough, but simple, Among Us modding API and utility library that covers:
- Events
- Voting
- Assets
- Keybinds
- Local Settings
- Custom Game Over Conditions
- Compatibility
- ~~Game Modes~~ (coming soon)
@@ -24,9 +28,10 @@ The result is a less intrusive, better modding API that covers general use cases
# Usage
To start using Mira API, you need to:
1. Add a reference to Mira API either through a [DLL](https://github.com/All-Of-Us-Mods/MiraAPI/releases), project reference, or [NuGet package](https://www.nuget.org/packages/AllOfUs.MiraAPI).
2. Add a BepInDependency on your plugin class like this: `[BepInDependency(MiraApiPlugin.Id)]`
3. Implement the IMiraPlugin interface in your plugin class.
3. Implement the `IMiraPlugin` interface on your plugin class.
Mira API also depends on [Reactor](https://github.com/NuclearPowered/Reactor) to function properly!
Remember to include it as a reference and `BepInDependency`!
@@ -34,8 +39,10 @@ Remember to include it as a reference and `BepInDependency`!
For a full example, see [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExamplePlugin.cs).
## Recommended Project Structure
It is highly recommended to follow this project structure when using Mira API to keep your code clean and organized.
You can also view the Example Mod in this repository for some guidance.
```
MyMiraMod/
├── Buttons/
@@ -60,220 +67,21 @@ MyMiraMod/
└── MyModAssets.cs
```
## Roles
Roles are straightforward in Mira API. There are two things you need to do to create a custom role:
1. Create a class that inherits from a base game role (like `CrewmateRole`, `ImpostorRole`, etc.)
2. Implement the `ICustomRole` interface from Mira API.
# Documentation
**Disclaimer: Make sure your plugin class has the following attribute `[ReactorModFlags(ModFlags.RequireOnAllClients)]` or else your roles will not register correctly.**
Full documentation for every Mira API feature is available on the **[wiki](https://github.com/All-Of-Us-Mods/MiraAPI/wiki)**:
Note: For step 1, if you are making neutral roles,
you can choose either `CrewmateRole` or `ImpostorRole`,
or even `RoleBehaviour` if you are okay with starting from scratch.
Mira API handles everything else, from adding the proper options to the settings menu,
to managing the role assignment at the start of the game.
There are no extra steps on the developer's part.
See [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Roles/CustomRole.cs) for a code example.
## Modifiers
Mira API uses a different definition of 'modifiers' than other Among Us mods.
For example, in Town Of Us, a modifier is an extra "ability" that is applied on top of the base role.
However, in Mira API, modifiers are very flexible.
A modifier is anything that "modifiers" a player's abilities or interactions.
Mira provides three classes for working with modifiers:
- `BaseModifier`: The base for every modifier. You MUST add and remove this modifier from a player manually!
- `TimedModifier`: A modifier that has a time limit. This modifier has to be added manually, but will automatically remove itself after the time limit.
- `GameModifier`: This works like the typical TOU modifier, where it is automatically applied at the beginning of the game, then removed at the end.
Modifiers provide various overridable functions and properties for custom behavior.
They can also be used for "tagging" a player.
You can check if a player has a modifier through the extension method `HasModifier` on a `PlayerControl` object.
To start using a modifier, pick one of the base classes above and create a class that inherits from it. Implement the properties and methods you would like, then add the `[RegisterModifier]` attribute to the class.
An example Game modifier can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/GameModifierExample.cs).
An example Timer modifier can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/ModifierTimerExample.cs).
## Options
Options are also simple in Mira API.
Mira API handles all the hard work behind the scenes,
so developers only have to follow a few steps to create their custom options.
The Options API is split up into Groups and Options.
Every Option needs to be in a Group.
To create a group, you need to create a class that inherits from the `AbstractOptionGroup` abstract class.
Groups contain some properties for developers to control,
including `GroupName`, `GroupColor`, `GroupVisible`, and `OptionableType`.
The full list of properties can be found in the `AbstractOptionGroup` class.
Only the `GroupName` is required.
Here is an example of a group class:
```csharp
public class MyOptionsGroup : AbstractOptionGroup
{
public override string GroupName => "My Options"; // this is required
[ModdedNumberOption("My Number Option", min: 0, max: 10)]
public float MyNumberOption { get; set; } = 5f;
}
```
You can access any group class using the `OptionGroupSingleton` class like this:
```csharp
// MyOptionsGroup is a class that inherits from AbstractOptionGroup
var myGroup = OptionGroupSingleton<MyOptionsGroup>.Instance; // gets the instance of the group
Logger<MyPlugin>.Info(myGroup.MyNumberOption); // prints the value of the option to the console
```
Once you have an options group, there are two ways to make the actual options:
- Use an Option Attribute with a property.
- Create a ModdedOption property.
### Option Attributes
This is an example of using an Option Attribute on a property:
```csharp
// The first parameter is always the name of the option. The rest are dependent on the type of option.
[ModdedNumberOption("Sussy level", min: 0, max: 10)]
public float SussyLevel { get; set; } = 4f; // You can set a default value here.
```
Here are the available Option Attributes and their signatures:
```csharp
ModdedEnumOption(string name, Type enumType, string[]? values = null, Type? roleType = null)
ModdedNumberOption(
string name,
float min,
float max,
float increment=1
NumberSuffixes suffixType = NumberSuffixes.None,
bool zeroInfinity = false,
Type? roleType = null)
ModdedToggleOption(string name, Type? roleType = null)
```
### ModdedOption Properties
And this is an example of a ModdedOption property:
```csharp
public ModdedToggleOption YeezusAbility { get; } = new ModdedToggleOption("Yeezus Ability", false);
```
There are currently three types of ModdedOptions you can create:
- `ModdedEnumOption`
- `ModdedNumberOption`
- `ModdedToggleOption`
To see a full example of an option class,
see [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/ExampleOptions.cs).
### Role Options
You can also specify a role type for an option or option group.
To set the role type for an entire group, set the `OptionableType` property on that group like this:
```csharp
public class MyOptionsGroup : AbstractOptionGroup
{
public override string GroupName => "My Options";
public override Type? OptionableType => typeof(MyRole); // this is the role that will have these options
[ModdedNumberOption("Ability Uses", min: 0, max: 10)]
public float AbilityUses { get; set; } = 5f;
}
```
You can also use a modifier type for the `OptionableType` property.
This will associate that option group with the specified modifier type
and display it in the modifier options menu.
An example can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/Roles/CustomRoleSettings.cs).
## Custom Murders
Mira API provides its own implementation for murders.
Our implementation allows for more customization on kills, and helps bypass server checks.
You can use `PlayerControl.RpcCustomMurder` to perform a networked custom murder, or `PlayerControl.CustomMurder` to normally perform a custom murder.
For example:
```cs
PlayerControl.LocalPlayer.RpcCustomMurder(Target, createDeadBody: false, teleportMurderer: false, playKillSound: false, resetKillTimer: false, showKillAnim: false);
```
This will kill a player without creating a dead body and without teleporting the murderer.
## Buttons
Mira API provides a simple interface for adding ability buttons to the game.
The only thing you need to do is create a class
that inherits from the `CustomActionButton` class and implement the properties and methods.
Mira API handles all other tasks and logic required to add the button to the game.
In case you need to access your `CustomActionButton` instance from another class,
you can use the `CustomButtonSingleton` class like this:
```csharp
var myButton = CustomButtonSingleton<MyCoolButton>.Instance;
```
The button API is straightforward, but provides a lot of flexibility.
There are various methods you can override to customize the behavior of your button.
See [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI/Hud/CustomActionButton.cs)
for a full list of methods you can override.
An example button can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Buttons/ExampleButton.cs).
## Custom Colors
Mira provides a simple Custom Color API that allows you to add custom player colors to the game.
Creating custom colors isn't difficult, but there are some requirements for Mira to register your colors.
1. Create a ***STATIC*** class to house all your `CustomColor` objects.
2. Inside this class, create a `CustomColor` property for each color you intend to add.
3. Add the `[RegisterCustomColors]` attribute to the class.
Here is an example of a custom color class:
```csharp
[RegisterCustomColors]
public static class MyCustomColors
{
public static CustomColor Cerulean { get; } = new CustomColor("Cerulean", new Color(0.0f, 0.48f, 0.65f));
public static CustomColor Rose { get; } = new CustomColor("Rose", new Color(0.98f, 0.26f, 0.62f));
public static CustomColor Gold { get; } = new CustomColor("Gold", new Color(1.0f, 0.84f, 0.0f));
}
```
## Assets
Mira API provides a simple, but expandable asset system.
The core of the system is the `LoadableAsset<T>` class.
This is a generic abstract class that provides a pattern for loading assets.
Mira API comes with a few asset loaders:
1. `LoadableBundleAsset<T>`: This is used for loading assets from AssetBundles.
2. `LoadableAddressableAsset<T>`: This is used for loading assets from Addressables.
3. `LoadableResourceAsset`: This is used for loading **only sprites** from the Embedded Resources within a mod.
4. `LoadableAudioResourceAsset`: This is used for loading **only audio clips** from the Embedded Resources within a mod.
The code below shows how to use an asset loaders:
```csharp
// Load a sprite from an AssetBundle
AssetBundle bundle = AssetBundleManager.Load("MyBundle"); // AssetBundleManager is a utility provided by Reactor
LoadableAsset<Sprite> mySpriteAsset = new LoadableBundleAsset<Sprite>("MySprite", bundle);
Sprite sprite = mySpriteAsset.LoadAsset();
// Load a sprite from an Embedded Resource
// Make sure to set the Build Action of your image to Embedded Resource!
LoadableAsset<Sprite> buttonAsset = new LoadableResourceAsset("ExampleMod.Resources.MyButton.png");
Sprite button = buttonSpriteAsset.LoadAsset();
```
You can view an example file [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExampleAssets.cs).
You can create your own asset loaders by inheriting from `LoadableAsset<T>` and implementing the `LoadAsset` method.
- [Get Started / IMiraPlugin](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Home)
- [Custom Roles](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Custom-Roles)
- [Options](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Options)
- [Modifiers](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Modifiers)
- [Custom Buttons](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Custom-Buttons)
- [Custom Colors](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Colors)
- [Events](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Events)
- [Meetings and Voting](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Meetings-and-Voting)
- [Assets](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Assets)
- [Keybinds](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Keybinds)
- [Local Settings](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Local-Settings)
- [Game Modes](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Game-Modes)
- [Custom Game Over](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Game-Over)
- [Utilities](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Utilities)
Binary file not shown.