From 6f61a75bb7711876d03b73a00dfd29da39db2ccf Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 15 Aug 2026 13:18:05 -0500
Subject: [PATCH 1/3] Add BundleSubAssetHolder for packed spritesheets
---
.../Patches/Options/RoleSettingMenuPatches.cs | 1 -
.../Assets/LoadableBundleSubAsset.cs | 37 ++++++++
.../Assets/LoadableBundleSubAssetHolder.cs | 86 +++++++++++++++++++
MiraAPI/Utilities/Assets/TmpSpriteUtils.cs | 4 +-
4 files changed, 125 insertions(+), 3 deletions(-)
create mode 100644 MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs
create mode 100644 MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs
diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
index 0dc8721..65c64f6 100644
--- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
@@ -550,7 +550,6 @@ public static class RoleSettingMenuPatches
new Rect(0, 0, 370, 230),
Vector2.one / 2,
100);
- __instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced;
}
__instance.roleHeaderSprite.color = customRole.OptionsMenuColor;
diff --git a/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs b/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs
new file mode 100644
index 0000000..ef7aea3
--- /dev/null
+++ b/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq;
+using UnityEngine;
+
+namespace MiraAPI.Utilities.Assets;
+
+///
+/// A utility class for loading multiple assets from an .
+///
+/// The name of the asset.
+/// The that contains the asset.
+public class LoadableBundleSubAsset(string name, LoadableBundleSubAssetHolder assetHolder) : LoadableAsset
+{
+ ///
+ /// Loads the asset from the .
+ ///
+ /// The asset.
+ /// The asset did not load properly.
+ public override Sprite LoadAsset()
+ {
+ if (LoadedAsset != null)
+ {
+ return LoadedAsset;
+ }
+
+ assetHolder.TryInit();
+
+ var loadedAsset = assetHolder.SubSprites.FirstOrDefault(x => x.name == name);
+
+ if (loadedAsset == null)
+ {
+ throw new InvalidOperationException($"INVALID ASSETS: {name}");
+ }
+ LoadedAsset = loadedAsset;
+ return LoadedAsset;
+ }
+}
diff --git a/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs
new file mode 100644
index 0000000..1599398
--- /dev/null
+++ b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Linq;
+using HarmonyLib;
+using Il2CppInterop.Runtime;
+using Reactor.Utilities.Extensions;
+using UnityEngine;
+
+namespace MiraAPI.Utilities.Assets;
+
+///
+/// A utility class for loading multiple assets from an .
+///
+public class LoadableBundleSubAssetHolder
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the assets to pull from.
+ /// The that contains the assets.
+ public LoadableBundleSubAssetHolder(string[] names, AssetBundle bundle)
+ {
+ Bundle = bundle;
+ SpriteNames = names;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the asset.
+ /// The that contains the assets.
+ public LoadableBundleSubAssetHolder(string name, AssetBundle bundle)
+ {
+ Bundle = bundle;
+ SpriteNames = [name];
+ }
+ internal string[] SpriteNames = [];
+ internal AssetBundle Bundle;
+ public Sprite[] SubSprites = [];
+
+ public virtual void TryInit()
+ {
+ if (SubSprites.Length == 0)
+ {
+ var newSprites = Array.Empty();
+ foreach (var name in SpriteNames)
+ {
+ var loadedAssets = Bundle.LoadAssetWithSubAssets(name, Il2CppType.From(typeof(Sprite))).ToArray();
+
+ if (loadedAssets == null)
+ {
+ throw new InvalidOperationException($"INVALID ASSETS: {name}");
+ }
+ foreach (var obj in loadedAssets)
+ {
+ var img = obj.TryCast();
+ if (img != null)
+ {
+ img.DontDestroy().DontUnload();
+ newSprites = newSprites.AddToArray(img);
+ }
+ }
+ }
+
+ SubSprites = newSprites;
+ }
+ }
+
+ ///
+ /// Unloads an asset.
+ ///
+ /// True if the asset was unloaded, false otherwise.
+ public bool UnloadAsset()
+ {
+ if (SubSprites.Length == 0)
+ {
+ return false;
+ }
+
+ foreach (var sprite in SubSprites)
+ {
+ sprite.DestroyImmediate();
+ }
+ SubSprites = [];
+ return true;
+ }
+}
diff --git a/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs b/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs
index 518e4ac..6160a01 100644
--- a/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs
+++ b/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs
@@ -71,8 +71,8 @@ public static class TmpSpriteUtils
{
name = spriteName,
hashCode = TMP_TextUtilities.GetSimpleHashCode(spriteName),
- x = 0,
- y = 0,
+ x = rect.x,
+ y = rect.y,
width = rect.width,
height = rect.height,
xOffset = -(rect.width / 3) + (scale - 1) * (rect.width / 2),
From 57d2a6a9b5f918b79b77d7ef7a5c660ccbafa2b6 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 15 Aug 2026 16:00:05 -0500
Subject: [PATCH 2/3] Expose button event types
---
MiraAPI/Events/Mira/MiraButtonClickEvent.cs | 4 ++--
MiraAPI/Hud/CustomButtonManager.cs | 14 ++++++++++++++
MiraAPI/PluginLoading/MiraPluginManager.cs | 2 ++
.../Assets/LoadableBundleSubAssetHolder.cs | 2 +-
4 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/MiraAPI/Events/Mira/MiraButtonClickEvent.cs b/MiraAPI/Events/Mira/MiraButtonClickEvent.cs
index 401cc11..0b18028 100644
--- a/MiraAPI/Events/Mira/MiraButtonClickEvent.cs
+++ b/MiraAPI/Events/Mira/MiraButtonClickEvent.cs
@@ -6,7 +6,7 @@ namespace MiraAPI.Events.Mira;
/// Button click event for s only. Do not use for vanilla s.
///
/// The type.
-public sealed class MiraButtonClickEvent : MiraCancelableEvent where T : CustomActionButton
+public class MiraButtonClickEvent : MiraCancelableEvent where T : CustomActionButton
{
///
/// Gets the that was clicked.
@@ -33,7 +33,7 @@ public sealed class MiraButtonClickEvent : MiraCancelableEvent where T : Cust
///
/// Button click event for s only. Do not use for vanilla s.
///
-public sealed class MiraButtonClickEvent : MiraCancelableEvent
+public class MiraButtonClickEvent : MiraCancelableEvent
{
///
/// Gets the that was clicked.
diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs
index 70ca14b..b1ddbce 100644
--- a/MiraAPI/Hud/CustomButtonManager.cs
+++ b/MiraAPI/Hud/CustomButtonManager.cs
@@ -17,6 +17,20 @@ public static class CustomButtonManager
///
public static ReadOnlyCollection Buttons { get; internal set; } = new([]);
+ ///
+ /// Gets a list of all registered button-specific s.
+ ///
+ public static ReadOnlyDictionary EventTypes { get; internal set; } = new(
+ new Dictionary
+ { });
+
+ ///
+ /// Gets a list of all registered button-specific s.
+ ///
+ public static ReadOnlyDictionary CancelledEventTypes { get; internal set; } = new(
+ new Dictionary
+ { });
+
internal static readonly List CustomButtons = [];
internal static readonly Dictionary ButtonEventTypes = [];
internal static readonly Dictionary ButtonCancelledEventTypes = [];
diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs
index e662859..f9439b9 100644
--- a/MiraAPI/PluginLoading/MiraPluginManager.cs
+++ b/MiraAPI/PluginLoading/MiraPluginManager.cs
@@ -140,6 +140,8 @@ public sealed class MiraPluginManager
{
// Save all buttons into a read-only collection for easy access
CustomButtonManager.Buttons = new ReadOnlyCollection(CustomButtonManager.CustomButtons);
+ CustomButtonManager.EventTypes = new ReadOnlyDictionary(CustomButtonManager.ButtonEventTypes);
+ CustomButtonManager.CancelledEventTypes = new ReadOnlyDictionary(CustomButtonManager.ButtonCancelledEventTypes);
// Cache all the registered plugins into an array for easy access
RegisteredPlugins = [.. _registeredPlugins.Values];
diff --git a/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs
index 1599398..3b84d8d 100644
--- a/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs
+++ b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs
@@ -37,7 +37,7 @@ public class LoadableBundleSubAssetHolder
internal AssetBundle Bundle;
public Sprite[] SubSprites = [];
- public virtual void TryInit()
+ public void TryInit()
{
if (SubSprites.Length == 0)
{
From 411340d9e970086a9e3b9cc0d925c7f2e04a648e Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 15 Aug 2026 16:27:49 -0500
Subject: [PATCH 3/3] Fix role screenshots for combined spritesheets
---
.../Patches/Options/RoleSettingMenuPatches.cs | 7 ++-----
MiraAPI/Roles/CustomRoleManager.cs | 17 ++++++++++++-----
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
index 65c64f6..3c1e645 100644
--- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
@@ -545,11 +545,8 @@ public static class RoleSettingMenuPatches
__instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.0675f, 0.1494f, 0.5687f);
labelBg.transform.localPosition = new Vector3(1.082f, 0.1054f, -2.5f);
- __instance.roleScreenshot.sprite = Sprite.Create(
- role.RoleScreenshot.texture,
- new Rect(0, 0, 370, 230),
- Vector2.one / 2,
- 100);
+ __instance.roleScreenshot.sprite = role.RoleScreenshot;
+ __instance.roleScreenshot.drawMode = SpriteDrawMode.Simple;
}
__instance.roleHeaderSprite.color = customRole.OptionsMenuColor;
diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs
index 6c6760b..0b2be26 100644
--- a/MiraAPI/Roles/CustomRoleManager.cs
+++ b/MiraAPI/Roles/CustomRoleManager.cs
@@ -141,11 +141,18 @@ public static class CustomRoleManager
roleBehaviour.CanVent = customRole.Configuration.CanUseVent || customRole.Configuration.GetsVentData;
roleBehaviour.DefaultGhostRole = customRole.Configuration.GhostRole;
roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount;
- roleBehaviour.RoleScreenshot = Sprite.Create(
- customRole.Configuration.OptionsScreenshot?.LoadAsset().texture,
- new Rect(0, 0, 370, 230),
- Vector2.one / 2,
- 100);
+ if (customRole.Configuration.OptionsScreenshot != null)
+ {
+ roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot.LoadAsset();
+ }
+ else
+ {
+ roleBehaviour.RoleScreenshot = Sprite.Create(
+ null,
+ new Rect(0, 0, 370, 230),
+ Vector2.one / 2,
+ 100);
+ }
_emptySettings ??= new(0);
_emptyKillAnimations ??= new(0);