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);