Compare commits

...
Author SHA1 Message Date
Pietro420 fecdd9f4f7 optimization and caching
most of the caching stuff here can likely be removed
2026-04-15 22:13:26 -03: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
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 e8536e5d4b Merge pull request #80 from All-Of-Us-Mods/dev
## What's New
- Individual game options can be reset if they are a part of a preset.
- A new meeting-specific murder rpc is now available for use, checking for kills happening within and outside of a meeting.
- ## What's Changed
- Framerate Slider has been renamed to be more accurate
- Memory usage per frame has been improved
2026-02-08 16:30:05 -06:00
Atony 50030c113b Merge pull request #78 from All-Of-Us-Mods/dev
## What's New
- Screen shake no longer interferes with scrolling in the host options menu
## What's Changed
- Sabotage flashes no longer break on Starlight
- Using the button scale setting no longer breaks the UI.
- If a role fails to register to a marshalling error (nullables) the game will no longer crash.
2026-01-04 12:41:46 -06:00
Atony fe3bfcdbc4 Merge pull request #77 from All-Of-Us-Mods/dev
- Added Vanilla Button Click Event (and respective Cancel Event)
- Added accessibility settings for sabotage flashes and blares
- Added an in-game slider for resizing ability buttons
2025-12-28 18:00:57 -06:00
Atony 7ad003ed1e Merge pull request #75 from All-Of-Us-Mods/dev
- Adds custom game tabs, with their own names and description blurbs.
- All keybinds have been moved to the right for consistency and to prevent hide and seek vent uses being obscured.
- Holding shift will change most options to be split in half, like 2.5s -> 1.25s
- Vanilla roles can be blacklisted from spawning or appearing in menus via other mira mods.
- Roles can check if they can spawn on the current gamemode or under specific conditions, such as the current map.
- Adds options that can have customizable text for -1 and 0 values respectively.
- Adds support for modded roles in Hide and Seek
- Fixes vanilla roles bypassing murder attempts
- Fixes keybinds not being hidden on buttons
- Fixes buttons used via keybinds bypassing ButtonClickEvents
- Fixes crashing for Epic Games users when opening the keybinds menu.
2025-12-14 07:58:01 -06:00
Atony 056a9b6102 Merge pull request #72 from All-Of-Us-Mods/dev
Fix Logger error for v0.3.3
2025-10-14 13:53:21 -05:00
Atony ae917a9c77 Merge pull request #71 from All-Of-Us-Mods/dev
MiraAPI v0.3.3
2025-10-14 13:46:58 -05:00
Atony fc9e9740be Merge pull request #70 from All-Of-Us-Mods/dev
MiraAPI v0.3.2
2025-10-14 09:08:53 -05:00
Atony f9096b6160 Merge pull request #69 from All-Of-Us-Mods/dev
MiraAPI v0.3.1
2025-09-28 13:17:06 -05:00
12 changed files with 272 additions and 285 deletions

No files matched your search

+24 -6
View File
@@ -21,6 +21,8 @@ namespace MiraAPI.GameOptions;
public static class ModdedOptionsManager
{
private static readonly Dictionary<PropertyInfo, ModdedOptionAttribute> OptionAttributes = [];
private static readonly Dictionary<MethodBase, ModdedOptionAttribute> SetterAttributes = [];
private static readonly Dictionary<MethodBase, ModdedOptionAttribute> GetterAttributes = [];
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
@@ -100,12 +102,20 @@ public static class ModdedOptionsManager
}
var setterOriginal = property.GetSetMethod();
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
if (setterOriginal != null)
{
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
SetterAttributes[setterOriginal] = attribute;
}
var getterOriginal = property.GetGetMethod();
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
if (getterOriginal != null)
{
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
GetterAttributes[getterOriginal] = attribute;
}
OptionAttributes.Add(property, attribute);
attribute.HolderOption = option;
@@ -183,7 +193,11 @@ public static class ModdedOptionsManager
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
#pragma warning restore CA1707
{
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
if (!SetterAttributes.TryGetValue(__originalMethod, out var attribute))
{
return;
}
attribute.SetValue(value);
}
@@ -198,7 +212,11 @@ public static class ModdedOptionsManager
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
#pragma warning restore CA1707
{
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
if (!GetterAttributes.TryGetValue(__originalMethod, out var attribute))
{
return true;
}
__result = attribute.GetValue();
return false;
}
+2 -2
View File
@@ -127,7 +127,7 @@ public static class CustomMurderRpc
/// <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.Before)]
[MethodRpc((uint)MiraRpc.ConfirmCustomMurder, LocalHandling = RpcLocalHandling.After)]
public static void RpcConfirmCustomMurder(
this PlayerControl host,
PlayerControl source,
@@ -282,7 +282,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>
@@ -5,7 +5,6 @@ using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
@@ -352,11 +351,6 @@ internal static class GameSettingMenuPatches
_modifierBtnOgPos = _modifiersButton.transform.localPosition;
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
PresetManager.LoadPresets(plugin);
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}
@@ -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);
@@ -29,6 +29,7 @@ public static class RoleSettingMenuPatches
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; set; } = [];
private static List<GameObject> Headers { get; set; } = [];
private static List<RoleOptionSetting> RoleOptionSettings { get; set; } = [];
private static Dictionary<RoleBehaviour, Sprite> RoleScreenshotSprites { get; } = [];
private static float ScrollerNum { get; set; } = 0.522f;
@@ -506,11 +507,17 @@ 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);
if (!RoleScreenshotSprites.TryGetValue(role, out var screenshotSprite) || screenshotSprite == null)
{
screenshotSprite = Sprite.Create(
role.RoleScreenshot.texture,
new Rect(0, 0, 370, 230),
Vector2.one / 2,
100);
RoleScreenshotSprites[role] = screenshotSprite;
}
__instance.roleScreenshot.sprite = screenshotSprite;
__instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced;
}
+67 -38
View File
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using MiraAPI.Colors;
@@ -30,6 +32,9 @@ namespace MiraAPI.PluginLoading;
/// </summary>
public sealed class MiraPluginManager
{
private const BindingFlags DeclaredInstanceMembers = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
private const BindingFlags DeclaredStaticMembers = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
private readonly Dictionary<Assembly, MiraPluginInfo> _registeredPlugins = [];
internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!;
@@ -49,41 +54,24 @@ public sealed class MiraPluginManager
var info = new MiraPluginInfo(miraPlugin, pluginInfo);
var roles = new List<Type>();
var registrationTimer = Stopwatch.StartNew();
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
info.PluginConfig.SaveOnConfigSet = false;
foreach (var type in assembly.GetTypes())
foreach (var type in GetAssemblyTypes(assembly))
{
if (type == null)
{
continue;
}
if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
{
continue;
}
foreach (var method in type.GetMethods())
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
{
continue;
}
if (!method.IsStatic)
{
Error($"Event method {method.Name} in {type.Name} must be static.");
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
RegisterEvents(type);
if (RegisterModifier(type, info))
{
@@ -131,7 +119,9 @@ public sealed class MiraPluginManager
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
registrationTimer.Stop();
Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API in {registrationTimer.ElapsedMilliseconds}ms.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
IL2CPPChainloader.Instance.Finished += () =>
@@ -159,6 +149,45 @@ public sealed class MiraPluginManager
return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId);
}
private static void RegisterEvents(Type type)
{
foreach (var method in type.GetMethods(DeclaredStaticMembers))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
{
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !typeof(MiraEvent).IsAssignableFrom(parameters[0].ParameterType))
{
Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
MiraEventManager.RegisterEventHandler(parameters[0].ParameterType, method, eventAttribute.Priority);
}
}
private static Type?[] GetAssemblyTypes(Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException exception)
{
return exception.Types;
}
}
private static bool IsAutoPropertyBackingField(FieldInfo field)
{
return field.IsDefined(typeof(CompilerGeneratedAttribute), false) &&
field.Name.Contains("k__BackingField", StringComparison.Ordinal);
}
private static bool RegisterGameOver(Type type)
{
try
@@ -186,7 +215,7 @@ public sealed class MiraPluginManager
return false;
}
foreach (var property in type.GetProperties())
foreach (var property in type.GetProperties(DeclaredInstanceMembers))
{
if (property.GetMethod?.IsStatic == true)
{
@@ -209,7 +238,7 @@ public sealed class MiraPluginManager
ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo);
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption))))
foreach (var field in type.GetFields(DeclaredInstanceMembers).Where(f => !IsAutoPropertyBackingField(f) && f.FieldType.IsAssignableTo(typeof(IModdedOption))))
{
Error($"{field.Name} is a field, not a property. Use properties for options.");
}
@@ -264,7 +293,7 @@ public sealed class MiraPluginManager
return;
}
foreach (var property in type.GetProperties())
foreach (var property in type.GetProperties(DeclaredStaticMembers))
{
if (property.PropertyType != typeof(CustomColor))
{
@@ -280,7 +309,7 @@ public sealed class MiraPluginManager
PaletteManager.CustomColors.Add(color);
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor))))
foreach (var field in type.GetFields(DeclaredStaticMembers).Where(f => !IsAutoPropertyBackingField(f) && f.FieldType.IsAssignableTo(typeof(CustomColor))))
{
Error($"{field.Name} is a field, not a property. Use properties for colors.");
}
@@ -332,13 +361,13 @@ public sealed class MiraPluginManager
return false;
}
foreach (var property in type.GetProperties())
if (!LocalSettingsManager.TypeToTab.TryGetValue(type, out var tabInstance))
{
if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance)
{
continue;
}
return false;
}
foreach (var property in type.GetProperties(DeclaredInstanceMembers))
{
if (property.GetCustomAttribute<LocalSettingsButtonAttribute>() != null &&
property.PropertyType.IsAssignableTo(typeof(LocalSettingsButton)))
{
@@ -374,7 +403,7 @@ public sealed class MiraPluginManager
attribute.CreateSetting(type, configEntry);
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute<LocalSettingAttribute>() != null))
foreach (var field in type.GetFields(DeclaredInstanceMembers).Where(f => !IsAutoPropertyBackingField(f) && f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute<LocalSettingAttribute>() != null))
{
Error($"{field.Name} is a field, not a property. Use properties for local settings.");
}
@@ -404,7 +433,7 @@ public sealed class MiraPluginManager
return;
}
foreach (var property in type.GetProperties())
foreach (var property in type.GetProperties(DeclaredStaticMembers))
{
if (property.PropertyType != typeof(MiraKeybind))
{
@@ -428,7 +457,7 @@ public sealed class MiraPluginManager
}
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(MiraKeybind))))
foreach (var field in type.GetFields(DeclaredStaticMembers).Where(f => !IsAutoPropertyBackingField(f) && f.FieldType.IsAssignableTo(typeof(MiraKeybind))))
{
Error($"{field.Name} is a field, not a property. Use properties for keybinds.");
}
-6
View File
@@ -4,7 +4,6 @@ using System.Linq;
using BepInEx.Configuration;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Presets;
@@ -79,8 +78,6 @@ public static class PresetManager
foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
{
var fileName = Path.GetFileName(file);
Info($"Loading preset file {fileName}");
var presetName = Path.GetFileNameWithoutExtension(file);
var presetConfig = new ConfigFile(file, false)
{
@@ -131,9 +128,6 @@ public static class PresetManager
foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
{
var fileName = Path.GetFileName(file);
Info($"Loading preset file {fileName}");
var presetName = Path.GetFileNameWithoutExtension(file);
var presetConfig = new ConfigFile(file, false)
{
SaveOnConfigSet = false,
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Resources;
@@ -13,6 +14,8 @@ namespace MiraAPI.Utilities.Assets;
/// <param name="path">The path of the wave file.</param>
public class LoadableAudioResourceAsset(string path) : LoadableAsset<AudioClip>
{
private static readonly object CacheLock = new();
private static readonly Dictionary<(Assembly Assembly, string Path), AudioClip> AudioClipCache = [];
private readonly Assembly _assembly = Assembly.GetCallingAssembly();
/// <summary>
@@ -23,6 +26,20 @@ public class LoadableAudioResourceAsset(string path) : LoadableAsset<AudioClip>
/// <exception cref="MissingManifestResourceException">Stream failed to load. Check if the name of your asset was correct.</exception>
public override AudioClip LoadAsset()
{
if (LoadedAsset != null)
{
return LoadedAsset;
}
lock (CacheLock)
{
if (AudioClipCache.TryGetValue((_assembly, path), out var cachedClip) && cachedClip != null)
{
LoadedAsset = cachedClip;
return cachedClip;
}
}
using var assetStream = _assembly.GetManifestResourceStream(path)
?? throw new MissingManifestResourceException($"Stream failed to load. Check if the asset name is correct: {path}");
@@ -92,6 +109,13 @@ public class LoadableAudioResourceAsset(string path) : LoadableAsset<AudioClip>
var audioClip = AudioClip.Create(path, samples.Length / channels, channels, sampleRate, false);
audioClip.SetData(samples, 0);
LoadedAsset = audioClip;
lock (CacheLock)
{
AudioClipCache[(_assembly, path)] = audioClip;
}
return audioClip;
}
@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using Reactor.Utilities.Extensions;
using UnityEngine;
@@ -12,6 +13,9 @@ namespace MiraAPI.Utilities.Assets;
/// <typeparam name="T">The type of the asset to be loaded.</typeparam>
public class LoadableBundleAsset<T>(string name, AssetBundle bundle) : LoadableAsset<T> where T : UnityEngine.Object
{
private static readonly object CacheLock = new();
private static readonly Dictionary<(int BundleId, Type AssetType, string Name), UnityEngine.Object> BundleAssetCache = [];
/// <summary>
/// Loads the asset from the asset bundle.
/// </summary>
@@ -24,6 +28,16 @@ public class LoadableBundleAsset<T>(string name, AssetBundle bundle) : LoadableA
return LoadedAsset;
}
var cacheKey = (bundle.GetInstanceID(), typeof(T), name);
lock (CacheLock)
{
if (BundleAssetCache.TryGetValue(cacheKey, out var cachedAsset) && cachedAsset is T typedAsset)
{
LoadedAsset = typedAsset;
return typedAsset;
}
}
LoadedAsset = bundle.LoadAsset<T>(name);
if (LoadedAsset == null)
@@ -31,6 +45,11 @@ public class LoadableBundleAsset<T>(string name, AssetBundle bundle) : LoadableA
throw new InvalidOperationException($"INVALID ASSET: {name}");
}
lock (CacheLock)
{
BundleAssetCache[cacheKey] = LoadedAsset;
}
return LoadedAsset;
}
}
+36 -2
View File
@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using System.Reflection;
using Reactor.Utilities.Extensions;
using UnityEngine;
@@ -10,6 +11,10 @@ namespace MiraAPI.Utilities.Assets;
/// </summary>
public static class SpriteTools
{
private static readonly object CacheLock = new();
private static readonly Dictionary<(Assembly Assembly, string ResourcePath), Texture2D> TextureCache = [];
private static readonly Dictionary<(Assembly Assembly, string ResourcePath, float PixelsPerUnit), Sprite> SpriteCache = [];
/// <summary>
/// Loads and returns a texture from a resource path using the specified assembly.
/// </summary>
@@ -19,11 +24,20 @@ public static class SpriteTools
/// <exception cref="ArgumentException">Thrown when the resource cannot be found in the specified assembly.</exception>
public static Texture2D LoadTextureFromResourcePath(string resourcePath, Assembly assembly)
{
lock (CacheLock)
{
if (TextureCache.TryGetValue((assembly, resourcePath), out var cachedTexture) && cachedTexture != null)
{
return cachedTexture;
}
}
var tex = new Texture2D(1, 1, TextureFormat.ARGB32, false)
{
wrapMode = TextureWrapMode.Clamp,
};
var myStream = assembly.GetManifestResourceStream(resourcePath);
using var myStream = assembly.GetManifestResourceStream(resourcePath);
if (myStream != null)
{
var buttonTexture = myStream.ReadFully();
@@ -35,6 +49,12 @@ public static class SpriteTools
}
tex.name = resourcePath;
lock (CacheLock)
{
TextureCache[(assembly, resourcePath)] = tex;
}
return tex;
}
@@ -47,9 +67,23 @@ public static class SpriteTools
/// <returns>A <see cref="Sprite"/> object created from the texture loaded from the specified resource path.</returns>
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
{
lock (CacheLock)
{
if (SpriteCache.TryGetValue((assembly, resourcePath, pixelsPerUnit), out var cachedSprite) && cachedSprite != null)
{
return cachedSprite;
}
}
var tex = LoadTextureFromResourcePath(resourcePath, assembly);
var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
sprite.name = resourcePath;
lock (CacheLock)
{
SpriteCache[(assembly, resourcePath, pixelsPerUnit)] = sprite;
}
return sprite;
}
}
+25
View File
@@ -132,6 +132,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>
+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)