From fedba71889ab2adb8c772af7dd0dab8b98f1ef1b Mon Sep 17 00:00:00 2001 From: wangzeyu <120313401+TAIKgroup@users.noreply.github.com> Date: Fri, 23 Jan 2026 05:22:50 +0800 Subject: [PATCH 01/24] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b61a3dc..8adca64 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ [![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC) +[English] | [简体中文](README_zh.md) + > This mod is not affiliated with Among Us or Innersloth LLC, and the content contained therein is not endorsed or otherwise sponsored by Innersloth LLC. Portions of the materials contained herein are property of Innersloth LLC. © Innersloth LLC. # Mira API From 202b098857e6a9437f30c9fa1e3cc563b1a30b3c Mon Sep 17 00:00:00 2001 From: wangzeyu <120313401+TAIKgroup@users.noreply.github.com> Date: Sat, 24 Jan 2026 06:33:49 +0800 Subject: [PATCH 02/24] Create README_zh.md --- README_zh.md | 296 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 README_zh.md diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..e09956f --- /dev/null +++ b/README_zh.md @@ -0,0 +1,296 @@ +[![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC) + +[English](README.md) | [简体中文] + +> 本模组不隶属于 Among Us 或 Innersloth LLC,其包含的内容也未得到 Innersloth LLC 的认可或以其他方式赞助。 +此处包含的部分材料是Innersloth LLC的财产。 +© Innersloth LLC. + +> 此文档的简体中文翻译来自[@TAIKongguo](https://github.com/TAIKgroup) + +# Mira API + +一个全面并且简单的AmongUs开发辅助API和程序集,包括: +- 职业 +- 选项 +- 附加职业(副职业) +- 按钮 +- 自定义颜色 +- 事件 +- 投票 +- 资源 +- 兼容性 +- ~~游戏模式~~ (即将完成) + + +Mira API力求全面而简单,同时尽可能使用游戏基本元素。 +是一个为了让模组开发更简单的API,涵盖了多个方面。 + +**加入我们的 [Discord](https://discord.gg/FYYqJU2bvp) 获取支持并了解最新版本** + +# 用法 + +在使用 Mira API 之前,您需要知道: +1. 通过 [DLL](https://github.com/All-Of-Us-Mods/MiraAPI/releases),项目引用或 [NuGet包](https://www.nuget.org/packages/AllOfUs.MiraAPI)添加对Mira API的引用。 +2. 在plugin class上添加一个BepInRubstance,如下所示: `[BepInDependency(MiraApiPlugin.Id)]` +3. 在plugin class中实现IMiraPlugin接口。 + +Mira API 也依赖 [Reactor](https://github.com/NuclearPowered/Reactor) 进行工作! +不要忘记引用 它和 `BepInDependency`! + +有关完整实例,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExamplePlugin.cs). + +## 推荐项目结构 +强烈建议在使用Mira API时遵循此项目结构,以保持代码的整洁和有序。 +您还可以查看此存储库中的示例Mod以获取一些指导。 +``` +MyMiraMod/ +├── Buttons/ +│ └── MyCoolButton.cs +├── Options/ +│ ├── Roles/ +│ │ └── CoolCustomRoleOptions.cs +│ └── MainOptionGroup.cs +├── Patches/ +│ ├── Roles/ +│ │ └── CoolCustomRole/ +│ │ ├── PlayerControlPatches.cs +│ │ └── ExileControllerPatches.cs +│ └── General/ +│ └── HudManagerPatches.cs +├── Resources/ +│ ├── CoolButton.png +│ └── myAssets-win-x86.bundle +├── Roles/ +│ └── CoolCustomRole.cs +├── MyMiraModPlugin.cs +└── MyModAssets.cs +``` + +## 职业 +在Mira API中,创建自定义职业只需要需要做这两件事: +1. 创建一个 AmongUs 原版的职业类 (如 `CrewmateRole`, `ImpostorRole`, 等等。) +2. 通过 Mira API 实现 `ICustomRole`。 + +**免责声明: 请确保您的插件类具有以下属性 `[ReactorModFlags(ModFlags.RequireOnAllClients)]` 否则您将无法正常创建一个职业。** + +注意: 对于步骤 1, 如果您想创建一个中立职业,您可以选择, + `CrewmateRole` 或 `ImpostorRole`; + +如果您可以从头独立完成一个职业,您也可以选择使用 `RoleBehaviour` 。 + +Mira API 帮助您处理其他的一切事务, +包括在设置菜单中添加正确的选项,在游戏开始时正确分配每个职业。 +您可以尽情发挥您的才艺,肆意创造您心目中的职业. + + +有关完整实例,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/teams/MiraAPI.Example/Roles/CustomRole.cs)。 +(原文档链接搜索不到网页,此处进行了修改) + +## 附加职业(副职业) +Mira API 中的附加职业(副职业)与其他模组并不完全相同。 +例如,在 Town Of Us 中,附加职业(副职业)是基于主职业的额外能力。 +但是,在Mira API 中,附加职业(副职业)是非常灵活的, +它们是玩家额外的能力或可互动的其他东西。 + +Mira API 提供了3个类来处理附加职业(副职业): +- `BaseModifier`: 每个附加职业(副职业)的基础。您必须手动在玩家中添加和删除此修改器! +- `TimedModifier`: 有事件限制的附加职业(副职业)。此附加职业(副职业)必须手动添加,但会在到达时间限制后自动删除。 +- `GameModifier`: 这类似典型的 Town Of Us 的附加职业(副职业),在游戏开始时自动添加,在游戏结束后删除。 + +附加职业(副职业)为自定义模组提供了各种可覆写的函数和属性。 +它们还可以用来“标记”玩家 +您可以通过 `PlayerControl` 对象上的扩展方法 `HasModifier` 来检查玩家是否拥有附加职业(副职业)/ + +若想在游戏开始时使用附加职业(副职业),请选择上文中的一个类,并创建一个继承自它的类。 +实现您想要的效果,然后将 `[RegisterModifier]` 属性添加到这个类中。 + +有关普通附加职业(副职业)的实例,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/GameModifierExample.cs). + +有关限时附加职业(副职业)的实例,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/ModifierTimerExample.cs). + +(此文档链接搜索不到网页) + +## Options +在 Mira API 中,选项也十分简单。 +Mira API 在后台处理所有繁重的工作, +您只需要遵循几个简单的步骤就可以创建自定义选项。 +选项分为组和选项,每个选项都必须属于一个组。 + +要创建组,您需要一个继承自 `AbstractOptionGroup` (抽象类)的类。 +组包含一些供开发人员控制的属性, +包含 `GroupName`, `GroupColor`, `GroupVisible`, and `OptionableType`. +属性的完整列表可以在 `AbstractOptionGroup`类中找到。 +只需要 `GroupName`就可以创建一个组。 + +这是一个组的代码实例: +```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; +} +``` + +您可以使用 `OptionGroupSingleton` 类访问任何组,如下所示: +```csharp +// MyOptionsGroup是一个继承自AbstractOptionGroup的类 +var myGroup = OptionGroupSingleton.Instance; // gets the instance of the group +Logger.Info(myGroup.MyNumberOption); // prints the value of the option to the console +``` + +一旦你有了一个组,有两种方法可以制作选项: +- 使用 Option Attribute with a property. +- 创建一个 ModdedOption property. + +### Option Attributes + +这是在使用Option Attribute with a property的示例: +```csharp +// 第一个参数始终是选项的名称。其余的取决于选项的类型。 +[ModdedNumberOption("Sussy level", min: 0, max: 10)] +public float SussyLevel { get; set; } = 4f; // You can set a default value here. +``` + +这是使用available Option Attributes的例示: +```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 + +这是使用ModdedOption的示例: +```csharp +public ModdedToggleOption YeezusAbility { get; } = new ModdedToggleOption("Yeezus Ability", false); +``` + +目前,您可以创建三种类型的ModdedOptions: +- `ModdedEnumOption` +- `ModdedNumberOption` +- `ModdedToggleOption` + +有关完整实例,请参考[这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/ExampleOptions.cs). + +### 职业选项 + +您还可以为选项或组指定为一个职业的选项。 + +要为整个组设置职业,请按如下代码设置该组的 `OptionableType` 属性: +```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; +} +``` + +您还可以指定 `OptionableType` 为附加职业(副职业)的选项。 +这将把该组与指定附加职业(副职业)相关联, +并将其显示在附加职业(副职业)选项菜单中。 + +有关完整实例,请参考[这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/Roles/CustomRoleSettings.cs)。 + +(原文档链接搜索不到网页,此处进行了修改) + +## 自定义击杀 +Mira API 提供了自定义的击杀方式。 +我们允许开发者添加更多的击杀类别,并有助于绕过服务器检查。 +您可以使用 `PlayerControl.RpcCustomMurder` 或 `PlayerControl.CustomMurder` +进行自定义击杀,例如: + +```cs +PlayerControl.LocalPlayer.RpcCustomMurder(Target, createDeadBody: false, teleportMurderer: false, playKillSound: false, resetKillTimer: false, showKillAnim: false); +``` +这将杀死一名玩家,而不会造成尸体,也不会让击杀者瞬移。 + +## 按钮 + +Mira API 提供了一个简单的界面,用于向游戏添加功能按钮。 + +您只需要添加一个继承自 `CustomActionButton` 的类,并实现属性和方法。 + +Mira API 处理将按钮添加到游戏中所需的所有其他任务和逻辑。 + +如果你需要从另一个类访问 `CustomActionButton` 你可以使用, + `CustomButtonSingleton` ,如下所示: + +```csharp +var myButton = CustomButtonSingleton.Instance; +``` + +按钮十分简单,但提供了巨大的灵活性。 +您可以使用多种方法来自定义按钮。 +有关按钮的方法和完整列表,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI/Hud/CustomActionButton.cs) + +[这里](https://github.com/All-Of-Us-Mods/MiraAPI/blob/teams/MiraAPI.Example/Buttons/ExampleButton.cs) +是一个按钮的实例代码。 + +(原文档链接搜索不到网页,此处进行了修改) + +## 自定义颜色 + +Mira API提供了简单的自定义颜色,允许您将自定义玩家颜色添加到游戏中。 + +创建自定义颜色并不困难,但Mira API对创建您的颜色有一些要求。 + +1. 创建***STATIC***类来容纳所有 `CustomColor` 对象. +2. 在这个类中,为要添加的每种颜色创建 `CustomColor` 属性。 +3. 将 `[RegisterCustomColors]`属性 添加到这个类中。 + +以下是一个自定义颜色的示例: +```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)); +} +``` + +## 资源 + +Mira API 提供了一个简单且可扩展的资源系统。 +该系统的核心是 `LoadableAsset` 类。 +这是一个通用的抽象类,它提供了一种加载资源的模式。 + +Mira API 附带了一些资源加载器: +1. `LoadableBundleAsset`: 这适用于从 AssetBundles 加载资源。 +2. `LoadableAddressableAsset`: 这适用于从 Addressables 加载资源。 +3. `LoadableResourceAsset`: 这适用于从模组加载嵌入式资源。 +4. `LoadableAudioResourceAsset`: 这适用于从模组加载嵌入式音频资源。 + +下面的代码展示了如何使用资源加载器: +```csharp +// Load a sprite from an AssetBundle +AssetBundle bundle = AssetBundleManager.Load("MyBundle"); // AssetBundleManager is a utility provided by Reactor +LoadableAsset mySpriteAsset = new LoadableBundleAsset("MySprite", bundle); +Sprite sprite = mySpriteAsset.LoadAsset(); + +// 加载嵌入式资源 +// 确保将映像的“构建操作”设置为“嵌入式资源”! +LoadableAsset buttonAsset = new LoadableResourceAsset("ExampleMod.Resources.MyButton.png"); +Sprite button = buttonSpriteAsset.LoadAsset(); +``` + +您可以查看[例示文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExampleAssets.cs). + +您可以通过继承 `LoadableAsset` 类并实现 `LoadAsset`方法来创建自己的资产加载器。 From b100f925251d094b8edb9bd1b3bcc0f5bd461102 Mon Sep 17 00:00:00 2001 From: wangzeyu <120313401+TAIKgroup@users.noreply.github.com> Date: Sat, 24 Jan 2026 06:34:59 +0800 Subject: [PATCH 03/24] Update README_zh.md --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index e09956f..2a23baf 100644 --- a/README_zh.md +++ b/README_zh.md @@ -147,7 +147,7 @@ Logger.Info(myGroup.MyNumberOption); // prints the value of the option ### Option Attributes -这是在使用Option Attribute with a property的示例: +这是使用Option Attribute with a property的示例: ```csharp // 第一个参数始终是选项的名称。其余的取决于选项的类型。 [ModdedNumberOption("Sussy level", min: 0, max: 10)] From 35e4c7cc9de0a863939cb48db40962ef1d9b99e7 Mon Sep 17 00:00:00 2001 From: Az <56699741+AlchlcDvl@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:18:15 -0400 Subject: [PATCH 04/24] move chinese readme to translated folder --- README.md | 2 +- README_zh.md => Translated/README_zh.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename README_zh.md => Translated/README_zh.md (100%) diff --git a/README.md b/README.md index 7e7ff6e..84823f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC) -[English] | [简体中文](README_zh.md) +[English] | [简体中文](Translated/README_zh.md) > This mod is not affiliated with Among Us or Innersloth LLC, and the content contained therein is not endorsed or otherwise sponsored by Innersloth LLC. Portions of the materials contained herein are property of Innersloth LLC. © Innersloth LLC. diff --git a/README_zh.md b/Translated/README_zh.md similarity index 100% rename from README_zh.md rename to Translated/README_zh.md From 182a53442ecb9e63919aa2f1a180424b60858821 Mon Sep 17 00:00:00 2001 From: Az <56699741+AlchlcDvl@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:57:19 -0400 Subject: [PATCH 05/24] Update README_zh.md Co-Authored-By: HayashiUmi <2558527373@qq.com> --- Translated/README_zh.md | 293 ++++++---------------------------------- 1 file changed, 44 insertions(+), 249 deletions(-) diff --git a/Translated/README_zh.md b/Translated/README_zh.md index 2a23baf..7d54169 100644 --- a/Translated/README_zh.md +++ b/Translated/README_zh.md @@ -1,48 +1,52 @@ [![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC) -[English](README.md) | [简体中文] +[English](../README.md) | [简体中文] -> 本模组不隶属于 Among Us 或 Innersloth LLC,其包含的内容也未得到 Innersloth LLC 的认可或以其他方式赞助。 -此处包含的部分材料是Innersloth LLC的财产。 -© Innersloth LLC. +> 此模组与 Among Us 或 Innersloth LLC 无关,其中包含的内容未得到 Innersloth LLC 的认可或赞助。本文所含部分材料为 Innersloth LLC 的财产。© Innersloth LLC。 -> 此文档的简体中文翻译来自[@TAIKongguo](https://github.com/TAIKgroup) +> 由[太空果](https://github.com/TAIKgroup)开启了Pr并提交了初版翻译,由[林梅](https://github.com/HayashiUme)维护并提供wiki翻译。 # Mira API -一个全面并且简单的AmongUs开发辅助API和程序集,包括: -- 职业 +一个全面、易上手的 Among Us 模组 API 及实用工具库,内含: + +- 角色 - 选项 -- 附加职业(副职业) +- 附加职业 - 按钮 - 自定义颜色 - 事件 - 投票 - 资源 -- 兼容性 -- ~~游戏模式~~ (即将完成) +- 快捷键 +- 本地设置 +- 自定义游戏结束条件 +- 强兼容性 +- ~~游戏模式~~(敬请期待) +Mira API 力求全面而易于上手,同时尽可能复用原版游戏元素。 +它是一个更简洁、覆盖常见用例的模组 API,减少了对游戏源码的修改。 -Mira API力求全面而简单,同时尽可能使用游戏基本元素。 -是一个为了让模组开发更简单的API,涵盖了多个方面。 +**加入 [Discord](https://discord.gg/FYYqJU2bvp) 获取支持并了解最新版本** -**加入我们的 [Discord](https://discord.gg/FYYqJU2bvp) 获取支持并了解最新版本** +# 使用方法 -# 用法 +要开始使用 Mira API,你需要: -在使用 Mira API 之前,您需要知道: -1. 通过 [DLL](https://github.com/All-Of-Us-Mods/MiraAPI/releases),项目引用或 [NuGet包](https://www.nuget.org/packages/AllOfUs.MiraAPI)添加对Mira API的引用。 -2. 在plugin class上添加一个BepInRubstance,如下所示: `[BepInDependency(MiraApiPlugin.Id)]` -3. 在plugin class中实现IMiraPlugin接口。 +1. 通过 [DLL](https://github.com/All-Of-Us-Mods/MiraAPI/releases)、项目引用或 [NuGet 包](https://www.nuget.org/packages/AllOfUs.MiraAPI) 添加对 Mira API 的引用。 +2. 在你的插件类上添加 BepInDependency,如下所示:`[BepInDependency(MiraApiPlugin.Id)]` +3. 在你的插件类上实现 `IMiraPlugin` 接口。 -Mira API 也依赖 [Reactor](https://github.com/NuclearPowered/Reactor) 进行工作! -不要忘记引用 它和 `BepInDependency`! +Mira API 还依赖 [Reactor](https://github.com/NuclearPowered/Reactor) 才能正常运行! +记得将其作为引用和 `BepInDependency` 包含进来! -有关完整实例,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExamplePlugin.cs). +完整示例请参见 [此文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExamplePlugin.cs)。 + +## 推荐的项目结构 + +使用 Mira API 时,强烈建议遵循以下项目结构,以保持代码整洁有序。 +你也可以查看本仓库中的示例模组以获得一些指导。 -## 推荐项目结构 -强烈建议在使用Mira API时遵循此项目结构,以保持代码的整洁和有序。 -您还可以查看此存储库中的示例Mod以获取一些指导。 ``` MyMiraMod/ ├── Buttons/ @@ -67,230 +71,21 @@ MyMiraMod/ └── MyModAssets.cs ``` -## 职业 -在Mira API中,创建自定义职业只需要需要做这两件事: -1. 创建一个 AmongUs 原版的职业类 (如 `CrewmateRole`, `ImpostorRole`, 等等。) -2. 通过 Mira API 实现 `ICustomRole`。 +# 文档 -**免责声明: 请确保您的插件类具有以下属性 `[ReactorModFlags(ModFlags.RequireOnAllClients)]` 否则您将无法正常创建一个职业。** +Mira API 每个功能的完整文档可在 **[wiki](https://github.com/All-Of-Us-Mods/MiraAPI/wiki)** 上找到: -注意: 对于步骤 1, 如果您想创建一个中立职业,您可以选择, - `CrewmateRole` 或 `ImpostorRole`; - -如果您可以从头独立完成一个职业,您也可以选择使用 `RoleBehaviour` 。 - -Mira API 帮助您处理其他的一切事务, -包括在设置菜单中添加正确的选项,在游戏开始时正确分配每个职业。 -您可以尽情发挥您的才艺,肆意创造您心目中的职业. - - -有关完整实例,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/teams/MiraAPI.Example/Roles/CustomRole.cs)。 -(原文档链接搜索不到网页,此处进行了修改) - -## 附加职业(副职业) -Mira API 中的附加职业(副职业)与其他模组并不完全相同。 -例如,在 Town Of Us 中,附加职业(副职业)是基于主职业的额外能力。 -但是,在Mira API 中,附加职业(副职业)是非常灵活的, -它们是玩家额外的能力或可互动的其他东西。 - -Mira API 提供了3个类来处理附加职业(副职业): -- `BaseModifier`: 每个附加职业(副职业)的基础。您必须手动在玩家中添加和删除此修改器! -- `TimedModifier`: 有事件限制的附加职业(副职业)。此附加职业(副职业)必须手动添加,但会在到达时间限制后自动删除。 -- `GameModifier`: 这类似典型的 Town Of Us 的附加职业(副职业),在游戏开始时自动添加,在游戏结束后删除。 - -附加职业(副职业)为自定义模组提供了各种可覆写的函数和属性。 -它们还可以用来“标记”玩家 -您可以通过 `PlayerControl` 对象上的扩展方法 `HasModifier` 来检查玩家是否拥有附加职业(副职业)/ - -若想在游戏开始时使用附加职业(副职业),请选择上文中的一个类,并创建一个继承自它的类。 -实现您想要的效果,然后将 `[RegisterModifier]` 属性添加到这个类中。 - -有关普通附加职业(副职业)的实例,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/GameModifierExample.cs). - -有关限时附加职业(副职业)的实例,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/ModifierTimerExample.cs). - -(此文档链接搜索不到网页) - -## Options -在 Mira API 中,选项也十分简单。 -Mira API 在后台处理所有繁重的工作, -您只需要遵循几个简单的步骤就可以创建自定义选项。 -选项分为组和选项,每个选项都必须属于一个组。 - -要创建组,您需要一个继承自 `AbstractOptionGroup` (抽象类)的类。 -组包含一些供开发人员控制的属性, -包含 `GroupName`, `GroupColor`, `GroupVisible`, and `OptionableType`. -属性的完整列表可以在 `AbstractOptionGroup`类中找到。 -只需要 `GroupName`就可以创建一个组。 - -这是一个组的代码实例: -```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; -} -``` - -您可以使用 `OptionGroupSingleton` 类访问任何组,如下所示: -```csharp -// MyOptionsGroup是一个继承自AbstractOptionGroup的类 -var myGroup = OptionGroupSingleton.Instance; // gets the instance of the group -Logger.Info(myGroup.MyNumberOption); // prints the value of the option to the console -``` - -一旦你有了一个组,有两种方法可以制作选项: -- 使用 Option Attribute with a property. -- 创建一个 ModdedOption property. - -### Option Attributes - -这是使用Option Attribute with a property的示例: -```csharp -// 第一个参数始终是选项的名称。其余的取决于选项的类型。 -[ModdedNumberOption("Sussy level", min: 0, max: 10)] -public float SussyLevel { get; set; } = 4f; // You can set a default value here. -``` - -这是使用available Option Attributes的例示: -```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 - -这是使用ModdedOption的示例: -```csharp -public ModdedToggleOption YeezusAbility { get; } = new ModdedToggleOption("Yeezus Ability", false); -``` - -目前,您可以创建三种类型的ModdedOptions: -- `ModdedEnumOption` -- `ModdedNumberOption` -- `ModdedToggleOption` - -有关完整实例,请参考[这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/ExampleOptions.cs). - -### 职业选项 - -您还可以为选项或组指定为一个职业的选项。 - -要为整个组设置职业,请按如下代码设置该组的 `OptionableType` 属性: -```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; -} -``` - -您还可以指定 `OptionableType` 为附加职业(副职业)的选项。 -这将把该组与指定附加职业(副职业)相关联, -并将其显示在附加职业(副职业)选项菜单中。 - -有关完整实例,请参考[这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/Roles/CustomRoleSettings.cs)。 - -(原文档链接搜索不到网页,此处进行了修改) - -## 自定义击杀 -Mira API 提供了自定义的击杀方式。 -我们允许开发者添加更多的击杀类别,并有助于绕过服务器检查。 -您可以使用 `PlayerControl.RpcCustomMurder` 或 `PlayerControl.CustomMurder` -进行自定义击杀,例如: - -```cs -PlayerControl.LocalPlayer.RpcCustomMurder(Target, createDeadBody: false, teleportMurderer: false, playKillSound: false, resetKillTimer: false, showKillAnim: false); -``` -这将杀死一名玩家,而不会造成尸体,也不会让击杀者瞬移。 - -## 按钮 - -Mira API 提供了一个简单的界面,用于向游戏添加功能按钮。 - -您只需要添加一个继承自 `CustomActionButton` 的类,并实现属性和方法。 - -Mira API 处理将按钮添加到游戏中所需的所有其他任务和逻辑。 - -如果你需要从另一个类访问 `CustomActionButton` 你可以使用, - `CustomButtonSingleton` ,如下所示: - -```csharp -var myButton = CustomButtonSingleton.Instance; -``` - -按钮十分简单,但提供了巨大的灵活性。 -您可以使用多种方法来自定义按钮。 -有关按钮的方法和完整列表,请参考 [这个文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI/Hud/CustomActionButton.cs) - -[这里](https://github.com/All-Of-Us-Mods/MiraAPI/blob/teams/MiraAPI.Example/Buttons/ExampleButton.cs) -是一个按钮的实例代码。 - -(原文档链接搜索不到网页,此处进行了修改) - -## 自定义颜色 - -Mira API提供了简单的自定义颜色,允许您将自定义玩家颜色添加到游戏中。 - -创建自定义颜色并不困难,但Mira API对创建您的颜色有一些要求。 - -1. 创建***STATIC***类来容纳所有 `CustomColor` 对象. -2. 在这个类中,为要添加的每种颜色创建 `CustomColor` 属性。 -3. 将 `[RegisterCustomColors]`属性 添加到这个类中。 - -以下是一个自定义颜色的示例: -```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)); -} -``` - -## 资源 - -Mira API 提供了一个简单且可扩展的资源系统。 -该系统的核心是 `LoadableAsset` 类。 -这是一个通用的抽象类,它提供了一种加载资源的模式。 - -Mira API 附带了一些资源加载器: -1. `LoadableBundleAsset`: 这适用于从 AssetBundles 加载资源。 -2. `LoadableAddressableAsset`: 这适用于从 Addressables 加载资源。 -3. `LoadableResourceAsset`: 这适用于从模组加载嵌入式资源。 -4. `LoadableAudioResourceAsset`: 这适用于从模组加载嵌入式音频资源。 - -下面的代码展示了如何使用资源加载器: -```csharp -// Load a sprite from an AssetBundle -AssetBundle bundle = AssetBundleManager.Load("MyBundle"); // AssetBundleManager is a utility provided by Reactor -LoadableAsset mySpriteAsset = new LoadableBundleAsset("MySprite", bundle); -Sprite sprite = mySpriteAsset.LoadAsset(); - -// 加载嵌入式资源 -// 确保将映像的“构建操作”设置为“嵌入式资源”! -LoadableAsset buttonAsset = new LoadableResourceAsset("ExampleMod.Resources.MyButton.png"); -Sprite button = buttonSpriteAsset.LoadAsset(); -``` - -您可以查看[例示文件](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExampleAssets.cs). - -您可以通过继承 `LoadableAsset` 类并实现 `LoadAsset`方法来创建自己的资产加载器。 +- [入门 / IMiraPlugin](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Home) +- [自定义角色](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Custom-Roles) +- [选项](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Options) +- [附加职业](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Modifiers) +- [自定义按钮](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Custom-Buttons) +- [自定义颜色](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Colors) +- [事件](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Events) +- [会议与投票](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Meetings-and-Voting) +- [资源](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Assets) +- [快捷键](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Keybinds) +- [本地设置](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Local-Settings) +- [游戏模式](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Game-Modes) +- [自定义游戏结束](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Game-Over) +- [实用工具](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Utilities) \ No newline at end of file From c82cc0e173c6315f6367bb84ae1ac365ae4d8768 Mon Sep 17 00:00:00 2001 From: Anthony Date: Sat, 29 Aug 2026 17:11:05 +0000 Subject: [PATCH 06/24] Translated using Weblate (Spanish) Currently translated at 31.5% (12 of 38 strings) Translation: Mira API/Mira API Translate-URL: https://weblate.duikbo.at/projects/mira-api/mira-api/es/ --- MiraAPI/Resources/Locale/es_ES.xml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/MiraAPI/Resources/Locale/es_ES.xml b/MiraAPI/Resources/Locale/es_ES.xml index f0e084c..3240eed 100644 --- a/MiraAPI/Resources/Locale/es_ES.xml +++ b/MiraAPI/Resources/Locale/es_ES.xml @@ -1,12 +1,15 @@ - Roles - Modificador - Modificadores - Tripulante - Tripulante - Impostor - Impostor - Neutro - Neutro + Roles + Modificador + Modificadores + Tripulante + Tripulante + Impostor + Impostor + Neutro + Neutro + Mira API + Accesibilidad + MiraAPI From 76105b311688ecc49534a306f9bebe4acab77f76 Mon Sep 17 00:00:00 2001 From: Faulo Fault Date: Sat, 29 Aug 2026 18:19:29 +0000 Subject: [PATCH 07/24] Translated using Weblate (Spanish (Latin America)) Currently translated at 76.3% (29 of 38 strings) Translation: Mira API/Mira API Translate-URL: https://weblate.duikbo.at/projects/mira-api/mira-api/es_419/ --- MiraAPI/Resources/Locale/es_419.xml | 43 +++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/MiraAPI/Resources/Locale/es_419.xml b/MiraAPI/Resources/Locale/es_419.xml index d6e11d8..cbe53f1 100644 --- a/MiraAPI/Resources/Locale/es_419.xml +++ b/MiraAPI/Resources/Locale/es_419.xml @@ -1,14 +1,35 @@ - Escala De Botón - Botón de Wiki debajo de la IU principal - Roles - Modificador - Modificadores - Tripulante - Trip - Impostor - Imp - Neutral - Neut + Escala De Botón Superior Derecho + Mostrar Wiki en columna inferior + Roles + Modificador + Modificadores + Tripulante + Trip + Impostor + Imp + Neutral + Neut + API Mira + Visuales/IU + Accesibilidad + MiraAPI + Mostrar Modificadores al Lado Izq + Mostrar Atajos en Botones + Escala de Botón + FPS Máx + Activar Destellos de Sabotaje + Activar Alarma de Sabotaje + REGRESAR A AJUSTES DE ROL + Categoría Personalizada 1 + Categoría Personalizada 2 + Configura modificadores y sus ajustes aquí! + Aplica opciones de partida para este mod! + Clásico + La experiencia clásica de Among Us +Encuentra a los Impostores o engaña a la Tripulación. + Escondidas + ¡Puedes correr, pero no esconderte! +Sobrevive al temporizador o elimina a la Tripulación. From e95b52dbe847183bda205b145952d195d8cc7c7e Mon Sep 17 00:00:00 2001 From: Turret Date: Sat, 29 Aug 2026 18:36:54 +0000 Subject: [PATCH 08/24] Translated using Weblate (Polish) Currently translated at 68.4% (26 of 38 strings) Translation: Mira API/Mira API Translate-URL: https://weblate.duikbo.at/projects/mira-api/mira-api/pl/ --- MiraAPI/Resources/Locale/pl_PL.xml | 38 +++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/MiraAPI/Resources/Locale/pl_PL.xml b/MiraAPI/Resources/Locale/pl_PL.xml index e59c8b8..4dd44be 100644 --- a/MiraAPI/Resources/Locale/pl_PL.xml +++ b/MiraAPI/Resources/Locale/pl_PL.xml @@ -1,14 +1,30 @@ - Skala Przycisków - Przycisk Wiki Poniżej Głównego UI - Role - Modyfikator - Modyfikatory - Załogant - Zał - Intruz - Int - Neutralny - Neut + Skala Przycisków w Prawym Górnym + Przycisk Wiki Poniżej Głównego UI + Role + Modyfikator + Modyfikatory + Załogant + Zał + Intruz + Int + Neutralny + Neut + Wizualne/UI + Dostępności + Pokaż HUD Modyfikatorów Po Lewo + Pokaż Przypisane Klawisze na Przyciskach + Skala Przycisków + Limit FPS + Włącz Błyski Sabotaży + Włącz Dźwięk Sabotaży + WRÓĆ DO USTAWIEŃ RÓL + Niestandardowa Kategoria 1 + Niestandardowa Kategoria 2 + Skonfiguruj modyfikatory i ich ustawienia tutaj! + Klasyczny + Klasyczna rozgrywka w Among Us! +Znajdź Intruzów, bądź oszukaj Załogę. + Chowany From 663480c77fe0191830cc23d60272bd7815e39a04 Mon Sep 17 00:00:00 2001 From: Shade the Minior Fan Date: Sat, 29 Aug 2026 18:31:35 +0000 Subject: [PATCH 09/24] Translated using Weblate (Portuguese (Portugal)) Currently translated at 97.3% (37 of 38 strings) Translation: Mira API/Mira API Translate-URL: https://weblate.duikbo.at/projects/mira-api/mira-api/pt_PT/ --- MiraAPI/Resources/Locale/pt_PT.xml | 51 +++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/MiraAPI/Resources/Locale/pt_PT.xml b/MiraAPI/Resources/Locale/pt_PT.xml index 4bfa806..1cfec24 100644 --- a/MiraAPI/Resources/Locale/pt_PT.xml +++ b/MiraAPI/Resources/Locale/pt_PT.xml @@ -1,14 +1,43 @@ - Fator do Tamanho do Botão - Botão Debaixo do Interface Principal - Funções - Modificador - Modificadores - Tripulante - Tripulante - Impostor - Impostor - Neutro - Neutro + Fator do Tamanho do Botão + Botão Debaixo do Interface Principal + Funções + Modificador + Modificadores + Tripulante + Tripulante + Impostor + Impostor + Neutro + Neutro + Mira API + Visuais/UI + Acessibilidade + MiraAPI + Mostrar Modificadores HUD no Lado Esquerdo + Mostrar Teclas nos Botões + Fator De Escala De Botão + FPS Máximo + Ativar Brilho De Sabotagem + Ativar Som De Sabotagem + VOLTAR PARA DEFINIÇÕES DE DEFINIÇÕES + Categoria Personalizada 1 + Categoria Personalizada 2 + Configura modificadores e as suas definições aqui! + Aplica definições de jogo para este modo! + Clássico + A experiencia clássica do Among Us! +Encontra os Impostores ou engana a Tripulação. + Escondidinhas + Podes fugir, mas não podes esconder! +Vive até o tempo acabar ou elimina a tripulação. + Clica para fechar + Clica para abrir + Seleciona ou cria uma predefinição para este jogo + Salvar + Funções Dos Tripulantes + Funções Dos Impostores + Funções Dos Neutros + Outro From 1d18a2ac7be3be2c4c8f8e1d5ca1f325b242c847 Mon Sep 17 00:00:00 2001 From: Faulo Fault Date: Sat, 29 Aug 2026 20:45:34 +0000 Subject: [PATCH 10/24] Translated using Weblate (Spanish (Latin America)) Currently translated at 100.0% (38 of 38 strings) Translation: Mira API/Mira API Translate-URL: https://weblate.duikbo.at/projects/mira-api/mira-api/es_419/ --- MiraAPI/Resources/Locale/es_419.xml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/MiraAPI/Resources/Locale/es_419.xml b/MiraAPI/Resources/Locale/es_419.xml index cbe53f1..dea47cd 100644 --- a/MiraAPI/Resources/Locale/es_419.xml +++ b/MiraAPI/Resources/Locale/es_419.xml @@ -1,7 +1,7 @@ - Escala De Botón Superior Derecho - Mostrar Wiki en columna inferior + Factor de Escala de la IU superior derecha + Botón de Wiki debajo de la IU principal Roles Modificador Modificadores @@ -19,17 +19,25 @@ Mostrar Atajos en Botones Escala de Botón FPS Máx - Activar Destellos de Sabotaje + Activar Visuales de Sabotaje Activar Alarma de Sabotaje REGRESAR A AJUSTES DE ROL Categoría Personalizada 1 Categoría Personalizada 2 - Configura modificadores y sus ajustes aquí! - Aplica opciones de partida para este mod! + ¡Configura modificadores y sus ajustes aquí! + ¡Aplica opciones de partida para este mod! Clásico La experiencia clásica de Among Us Encuentra a los Impostores o engaña a la Tripulación. Escondidas ¡Puedes correr, pero no esconderte! Sobrevive al temporizador o elimina a la Tripulación. + Click para cerrar + Click para abrir + Selecciona o crea un ajuste predefinido para esta partida + Guardar + Roles de Tripulante + Roles de Impostor + Roles Neutrales + Otros From 3f7e30a5c312b6821bfae9d26ab8b458e1971761 Mon Sep 17 00:00:00 2001 From: Turret Date: Sat, 29 Aug 2026 19:14:06 +0000 Subject: [PATCH 11/24] Translated using Weblate (Polish) Currently translated at 71.0% (27 of 38 strings) Translation: Mira API/Mira API Translate-URL: https://weblate.duikbo.at/projects/mira-api/mira-api/pl/ --- MiraAPI/Resources/Locale/pl_PL.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MiraAPI/Resources/Locale/pl_PL.xml b/MiraAPI/Resources/Locale/pl_PL.xml index 4dd44be..0142715 100644 --- a/MiraAPI/Resources/Locale/pl_PL.xml +++ b/MiraAPI/Resources/Locale/pl_PL.xml @@ -27,4 +27,6 @@ Klasyczna rozgrywka w Among Us! Znajdź Intruzów, bądź oszukaj Załogę. Chowany + Możesz uciekać, ale nie możesz się schować! +Przeżyj przez wymagany czas, albo zabij chowających. From 004f62eb77e7b7708ef86a4059315dd32e928af2 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:15:30 -0500 Subject: [PATCH 12/24] Tweak system for registering changes --- MiraAPI/Roles/ICustomRole.cs | 2 +- MiraAPI/Translation/MiraLocaleManager.cs | 213 ++++++++++-------- .../Translation/MiraLocalizationProvider.cs | 6 + 3 files changed, 126 insertions(+), 95 deletions(-) diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 13020ff..d01b108 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -22,7 +22,7 @@ public interface ICustomRole : IOptionable /// /// Gets the id part used to build the role's translation keys. It is recommended to call it ModGuid.Role.Faction /// - string IdPrefix => GetType().Namespace!; + string IdPrefix => MiraLocaleManager.BuildTranslationId(GetType().Namespace!, "Role", Team.ToString()); /// /// Gets the name of the role. diff --git a/MiraAPI/Translation/MiraLocaleManager.cs b/MiraAPI/Translation/MiraLocaleManager.cs index d0a642a..90f0df4 100644 --- a/MiraAPI/Translation/MiraLocaleManager.cs +++ b/MiraAPI/Translation/MiraLocaleManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Xml; using MiraAPI.Utilities; +using MonoMod.Utils; using Reactor.Localization.Utilities; using UnityEngine; @@ -14,7 +15,9 @@ public static class MiraLocaleManager { private const string LangDirectory = "mira_languages"; - public static readonly Dictionary>> Locale = []; + // Language, Xml Name, then Value + public static Dictionary> Locale { get; } = []; + public static readonly List RegisteredModIds = []; public static readonly Dictionary RegisteredStringNames = []; internal static readonly Dictionary StringNamesLookup = []; @@ -88,22 +91,19 @@ public static class MiraLocaleManager } /// - /// Registers translations for a mod. Copies embedded XML to mira_languages/{modGuid}/ on first run, - /// then loads all XML files from that directory into memory. + /// Registers translations for a mod. /// Call once per mod during plugin Load(). /// /// The mod GUID (e.g., "mira.example"). public static void Register(string modGuid) { var callingAssembly = Assembly.GetCallingAssembly(); - var dir = GetModLangDir(modGuid); - LoadInternalStrings(callingAssembly, callingAssembly.GetName().Name!, modGuid, dir); + LoadInternalStrings(callingAssembly, callingAssembly.GetName().Name!, modGuid); } /// - /// Registers translations for a mod. Copies embedded XML to mira_languages/{modGuid}/ on first run, - /// then loads all XML files from that directory into memory. + /// Registers translations for a mod. /// Call once per mod during plugin Load(). /// /// The mod GUID (e.g., "mira.example"). @@ -111,80 +111,95 @@ public static class MiraLocaleManager public static void Register(string modGuid, string internalName) { var callingAssembly = Assembly.GetCallingAssembly(); - var dir = GetModLangDir(modGuid); - LoadInternalStrings(callingAssembly, internalName, modGuid, dir); + LoadInternalStrings(callingAssembly, internalName, modGuid); } /// /// Translates a key into the current language. - /// Searches all mods, with reverse lookup fallback. /// /// The string id to find. /// Fallback string to use if no translation is found. /// A based on the key provided. public static string Get(string key, string fallback = "") { - var currentLang = CurrentLanguage; + return Get(CurrentLanguage, key, fallback != string.Empty ? fallback : key); + } - foreach (var (_, modLocale) in Locale) + /// + /// Translates a key into the current language. + /// + /// The specific language to prioritize. + /// The string id to find. + /// Fallback string to use if no translation is found. + /// A based on the key provided. + public static string Get(MiraLanguage language, string key, string fallback = "") + { + if (Locale.TryGetValue(language, out var translations) && + translations.TryGetValue(key, out var translation)) { - if (modLocale.TryGetValue(currentLang, out var langDict) && - langDict.TryGetValue(key, out var translated)) - { - return translated; - } + return translation; } - foreach (var (_, modLocale) in Locale) + if (Locale.TryGetValue(MiraLanguage.English, out var translationsEng) && + translationsEng.TryGetValue(key, out var translationEng)) { - if (modLocale.TryGetValue(MiraLanguage.English, out var engDict) && - engDict.TryGetValue(key, out var english)) - { - return english; - } + return translationEng; } - string? bestStructuredKey = null; - foreach (var (_, modLocale) in Locale) - { - if (modLocale.TryGetValue(MiraLanguage.English, out var engDict)) - { - foreach (var (structuredKey, englishValue) in engDict) - { - if (englishValue == key) - { - bestStructuredKey = structuredKey; - break; - } - } - } + return fallback; + } - if (bestStructuredKey != null) break; + /// + /// Translates a key into the current language. + /// + /// The string id to find. + /// List of keys to change into other text. + /// Fallback string to use if no translation is found. + /// A based on the key provided. + public static string GetParsed( + string key, + Dictionary parseList, + string fallback = "") + { + return GetParsed(CurrentLanguage, key, parseList, fallback != string.Empty ? fallback : key); + } + + /// + /// Translates a key into the current language. + /// + /// The specific language to prioritize. + /// The string id to find. + /// List of keys to change into other text. + /// Fallback string to use if no translation is found. + /// A based on the key provided. + public static string GetParsed( + MiraLanguage language, + string key, + Dictionary parseList, + string fallback = "") + { + var text = fallback; + + if (Locale.TryGetValue(MiraLanguage.English, out var translationsEng) && + translationsEng.TryGetValue(key, out var translationEng)) + { + text = translationEng; } - if (bestStructuredKey != null) + if (language is not MiraLanguage.English && + Locale.TryGetValue(language, out var translations) && + translations.TryGetValue(key, out var translation)) { - foreach (var (_, modLocale) in Locale) - { - if (modLocale.TryGetValue(currentLang, out var langDict) && - langDict.TryGetValue(bestStructuredKey, out var revTranslated)) - { - return revTranslated; - } - } - - foreach (var (_, modLocale) in Locale) - { - if (modLocale.TryGetValue(MiraLanguage.English, out var engDict) && - engDict.TryGetValue(bestStructuredKey, out var revEnglish)) - { - return revEnglish; - } - } + text = translation; } - return fallback != string.Empty ? fallback : key; + foreach (var tmpText in parseList.Where(x => text.Contains(x.Key))) + { + text = text.Replace(tmpText.Key, tmpText.Value); + } + + return text; } public static string BuildTranslationId(string modId, string idPart, string suffix) @@ -215,17 +230,49 @@ public static class MiraLocaleManager return newString; } - private static void LoadInternalStrings(Assembly assembly, string internalName, string modGuid, string dir) + public static void LoadExternalLocale() + { + CheckExternalDirectory("mira.api", BepInEx.Paths.PluginPath); + CheckExternalDirectory("mira.api", BepInEx.Paths.BepInExRootPath); + CheckExternalDirectory("mira.api", BepInEx.Paths.GameRootPath); + foreach (var mod in RegisteredModIds) + { + CheckExternalDirectory(mod, GetModLangDir(mod)); + } + } + + public static void CheckExternalDirectory(string modGuid, string dir) { Directory.CreateDirectory(dir); - if (!Locale.ContainsKey(modGuid)) + foreach (var filePath in Directory.GetFiles(dir, "*.xml")) { - Locale[modGuid] = []; - } + var fileName = Path.GetFileNameWithoutExtension(filePath); + var lang = LangList.ContainsValue(fileName) + ? LangList.First(x => x.Value == fileName).Key + : (MiraLanguage)(-1); + if ((int)lang == -1) continue; + + try + { + Locale.TryAdd(lang, []); + var xmlContent = File.ReadAllText(filePath); + ParseXmlFile(xmlContent, lang, false); + } + catch (Exception e) + { + Error($"Failed to load external translation {filePath}: {e.Message}"); + } + } + } + + private static void LoadInternalStrings(Assembly assembly, string internalName, string modGuid) + { var resourcePrefix = $"{internalName}.Resources.Locale."; + RegisteredModIds.Add(modGuid); + var atLeastOneLoaded = false; foreach (var locale in LangList) { @@ -241,9 +288,8 @@ public static class MiraLocaleManager string xmlContent = reader.ReadToEnd(); try { - var dict = ParseXmlFile(xmlContent); - Locale[modGuid][locale.Key] = dict; - Info($"Loaded {locale.Key.ToDisplayString()} translation for mod {modGuid} ({dict.Count} keys)"); + Locale.TryAdd(locale.Key, []); + ParseXmlFile(xmlContent, locale.Key, true); atLeastOneLoaded = true; } catch (Exception e) @@ -256,37 +302,11 @@ public static class MiraLocaleManager { Error($"No internal strings were found for {internalName}!"); } - - if (!Locale.TryGetValue(modGuid, out _)) - { - Locale[modGuid] = []; - } - - foreach (var filePath in Directory.GetFiles(dir, "*")) - { - var fileName = Path.GetFileNameWithoutExtension(filePath); - - var lang = LangList.ContainsValue(fileName) - ? LangList.First(x => x.Value == fileName).Key - : (MiraLanguage)(-1); - if ((int)lang == -1) continue; - - try - { - var dict = ParseXmlFile(filePath); - Locale[modGuid][lang] = dict; - Info($"Loaded external {lang.ToDisplayString()} translation for mod {modGuid} ({dict.Count} keys)"); - } - catch (Exception e) - { - Error($"Failed to load external translation {filePath}: {e.Message}"); - } - } } - private static Dictionary ParseXmlFile(string xmlContent) + private static void ParseXmlFile(string xmlContent, MiraLanguage language, bool loadingInternal) { - var dict = new Dictionary(); + var dict = Locale[language]; XmlDocument xmlDoc = new(); try { @@ -295,6 +315,7 @@ public static class MiraLocaleManager if (stringNodes != null) { + var total = 0; foreach (XmlNode node in stringNodes) { if (node.Attributes?["name"] != null) @@ -316,16 +337,20 @@ public static class MiraLocaleManager value = value.Replace("", "\n").Replace("", "&"); + if (loadingInternal && Locale[language].ContainsKey(name)) + { + Error($"String for \"{name}\" in {language} was overwritten by duplicate!"); + } dict[name] = value; + total++; } } + Info($"Loaded {language.ToDisplayString()} translation with ({total} keys)"); } } catch (XmlException ex) { Error($"XML parsing error: {ex.Message}"); } - - return dict; } } diff --git a/MiraAPI/Translation/MiraLocalizationProvider.cs b/MiraAPI/Translation/MiraLocalizationProvider.cs index 47cf747..3ac1133 100644 --- a/MiraAPI/Translation/MiraLocalizationProvider.cs +++ b/MiraAPI/Translation/MiraLocalizationProvider.cs @@ -11,6 +11,7 @@ public class MiraLocalizationProvider : LocalizationProvider { public override int Priority => ReactorPriority.Normal; private static LocalizationProvider? _reactorProvider; + private static bool _loadedStrings; public override bool TryGetText(StringNames stringName, out string? result) { @@ -39,6 +40,11 @@ public class MiraLocalizationProvider : LocalizationProvider { MiraApiPlugin.Culture = new(culture); } + if (!_loadedStrings) + { + MiraLocaleManager.LoadExternalLocale(); + _loadedStrings = true; + } foreach (var tab in LocalSettingsTab.TabGroups) { From 4bdddc6f8a6507d5dcb508d42b58b3e3118eba5a Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:39:21 -0500 Subject: [PATCH 13/24] Fix IdPart shenanigans --- MiraAPI/Roles/ICustomRole.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index d01b108..13020ff 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -22,7 +22,7 @@ public interface ICustomRole : IOptionable /// /// Gets the id part used to build the role's translation keys. It is recommended to call it ModGuid.Role.Faction /// - string IdPrefix => MiraLocaleManager.BuildTranslationId(GetType().Namespace!, "Role", Team.ToString()); + string IdPrefix => GetType().Namespace!; /// /// Gets the name of the role. From b62b8ae2164267fb156ad042cb16e6354f8315ce Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:35:10 -0500 Subject: [PATCH 14/24] Add more helpers for getting role strings --- MiraAPI/Utilities/Helpers.cs | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 687298e..5a70c2c 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -542,6 +542,48 @@ public static class Helpers return role.NiceName; } + /// + /// Returns the string of a . + /// + /// The to find. + /// The 's intro blurb. + public static string GetRoleIntroBlurb(this RoleBehaviour role) + { + if (role is ICustomRole custom) + { + return custom.RoleDescription; + } + return role.Blurb; + } + + /// + /// Returns the string of a . + /// + /// The to find. + /// The 's medium description. + public static string GetRoleMedDescription(this RoleBehaviour role) + { + if (role is ICustomRole custom) + { + return custom.RoleDescription; + } + return role.BlurbMed; + } + + /// + /// Returns the string of a . + /// + /// The to find. + /// The 's long description. + public static string GetRoleLongDescription(this RoleBehaviour role) + { + if (role is ICustomRole custom) + { + return custom.RoleLongDescription; + } + return role.BlurbLong; + } + /// /// Returns whether the is blacklisted from appearing and spawning. (Only applicable to vanilla roles). /// From 28871d0a012b0ba3958b040ceea70e91239b1a9e Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:35:47 -0500 Subject: [PATCH 15/24] Refresh role info on the info guide immediately --- MiraAPI/Patches/RoleGuidePatches.cs | 68 ++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index ca7f2be..6fefa67 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1,5 +1,7 @@ -using System.Globalization; +using System.Collections.Generic; +using System.Globalization; using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; using Innersloth.Assets; using MiraAPI.Roles; @@ -77,7 +79,11 @@ public static class RoleGuidePatches { __instance.numOfTabs = 3; __instance.TabButtons[0].SelectButton(true); - __instance.CreateNormalModeSettings(); + DisplayNormalRoleSettings(__instance, true); + } + else + { + DisplayNormalRoleSettings(__instance, false); } } else if (__instance.HnSModeSettings.Count == 0) @@ -98,6 +104,64 @@ public static class RoleGuidePatches __instance.SetActiveTab(0); return false; } + + private static Dictionary _rolePanels = []; + public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) + { + if (reset) + { + _rolePanels.Clear(); + instance.CreateSettingsEntry(StringNames.GameNumImpostors, GameManager.Instance.AllGameSettingData[StringNames.GameNumImpostors].GetValueString(GameManager.Instance.LogicOptions.NumImpostors)); + instance.CreateSettingsEntry(StringNames.GameKillCooldown, GameManager.Instance.AllGameSettingData[StringNames.GameKillCooldown].GetValueString(GameManager.Instance.LogicOptions.GetKillCooldown())); + instance.CreateSettingsEntry(StringNames.GameEmergencyCooldown, GameManager.Instance.AllGameSettingData[StringNames.GameEmergencyCooldown].GetValueString((float)GameManager.Instance.LogicOptions.GetEmergencyCooldown())); + instance.CreateSettingsEntry(StringNames.GameVisualTasks, instance.GetBoolString(GameManager.Instance.LogicOptions.GetVisualTasks())); + instance.CreateSettingsEntry(StringNames.GameAnonymousVotes, instance.GetBoolString(GameManager.Instance.LogicOptions.GetAnonymousVotes())); + instance.CreateSettingsEntry(StringNames.GameConfirmImpostor, instance.GetBoolString(GameManager.Instance.LogicOptions.GetConfirmImpostor())); + instance.CreateSettingsEntry(StringNames.GameTaskBarMode, GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); + foreach (RoleBehaviour roleBehaviour in RoleManager.Instance.AllRoles) + { + if (roleBehaviour.Role != RoleTypes.Crewmate && roleBehaviour.Role != RoleTypes.Impostor && roleBehaviour.Role is not RoleTypes.CrewmateGhost && roleBehaviour.Role is not RoleTypes.ImpostorGhost) + { + var panel = Object.Instantiate( + instance.MatchInfoRolePanelPrefab, + instance.settingsTabs[2].GetComponent().Inner); + _rolePanels.Add(roleBehaviour, panel); + } + } + } + + int num = 0; + foreach (var pair in _rolePanels) + { + var role = pair.Key; + var panel = pair.Value; + var amount = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role.Role); + var chance = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(role.Role); + if (amount == 0 || chance == 0) + { + panel.gameObject.SetActive(false); + continue; + } + panel.gameObject.SetActive(true); + panel.SetPanel( + role, + amount, + chance); + num++; + } + if (num == 0) + { + instance.rolesEnabledMessage.SetActive(true); + } + instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil((float)num / 2f) + instance.RoleEntryBoundsModifier, 0f, 999f)); + instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); + instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); + if (reset) + { + instance.CreatePlayerEntries(); + } + } + [HarmonyPrefix] [HarmonyPriority(Priority.First)] [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreatePlayerEntries))] From d2f6ba9718db0c60b00acc44a9fb7331806845cc Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:55:44 -0500 Subject: [PATCH 16/24] Fix god-awful spacing in the roles guide --- MiraAPI/Patches/RoleGuidePatches.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 6fefa67..c6edf06 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -80,6 +80,24 @@ public static class RoleGuidePatches __instance.numOfTabs = 3; __instance.TabButtons[0].SelectButton(true); DisplayNormalRoleSettings(__instance, true); + __instance.MatchInfoRoleMaskArea.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + + __instance.matchInfoPlayersMaskArea.transform.localPosition = + __instance.matchInfoSettingsMaskArea.transform.localPosition = new Vector3(1.22f, -0.335f, -0.1f); + + __instance.matchInfoPlayersMaskArea.size = + __instance.matchInfoSettingsMaskArea.size = + __instance.MatchInfoRoleMaskArea.size = new Vector2(-6, 1.8f); + + __instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren().First(x => x.name.Contains("BG_Gradient")) + .GetComponent() + .maskInteraction = + __instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).GetComponent() + .maskInteraction = + __instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).GetComponent() + .maskInteraction = SpriteMaskInteraction.VisibleInsideMask; } else { @@ -153,7 +171,7 @@ public static class RoleGuidePatches { instance.rolesEnabledMessage.SetActive(true); } - instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil((float)num / 2f) + instance.RoleEntryBoundsModifier, 0f, 999f)); + instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil((float)num / 2f) * 1.3f - 1.5f, 0f, 999f)); instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); if (reset) From 027d4763f3e1b44b484ab13d5957e169a2a67431 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:49:20 -0500 Subject: [PATCH 17/24] Expose vote methods for other mods --- MiraAPI/Voting/VotingUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index 9744546..a1f989d 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -121,7 +121,7 @@ public static class VotingUtils CustomCastJudgeVote(srcPlayerId, suspectPlayerId, overruleNonce); } - private static void CustomCastJudgeVote(PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) + public static void CustomCastJudgeVote(PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) { var plr = GameData.Instance.GetPlayerById(judgePlayerId.Value); if (!plr) return; @@ -181,7 +181,7 @@ public static class VotingUtils CustomCastVote(srcPlayerId, suspectPlayerId); } - private static void CustomCastVote(byte srcPlayerId, byte suspectPlayerId) + public static void CustomCastVote(byte srcPlayerId, byte suspectPlayerId) { var plr = GameData.Instance.GetPlayerById(srcPlayerId); if (!plr) return; From 6c7b362f95a56c05f5569a6deeb67a79490d5b66 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:24:35 -0500 Subject: [PATCH 18/24] Add toggles to disable abilities upon use / vote --- .../MeetingAbilities/MeetingActionButton.cs | 30 ++++++++++++++++++- MiraAPI/Patches/Voting/MeetingHudPatches.cs | 9 ++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/MiraAPI/MeetingAbilities/MeetingActionButton.cs b/MiraAPI/MeetingAbilities/MeetingActionButton.cs index 88cc799..dd733af 100644 --- a/MiraAPI/MeetingAbilities/MeetingActionButton.cs +++ b/MiraAPI/MeetingAbilities/MeetingActionButton.cs @@ -71,6 +71,26 @@ public abstract class MeetingActionButton /// public bool TimerPaused { get; set; } + /// + /// Gets or sets a value indicating how many times the ability was used in the current meeting. + /// + public int UsesSoFar { get; set; } + + /// + /// Gets a value determining if the abiity is disabled upon being used. + /// + public virtual bool DisableUponUse => false; + + /// + /// Gets a value determining if the abiity is disabled upon voting. + /// + public virtual bool DisableUponVoting => false; + + /// + /// Gets a value determining if the abiity is hidden when votes wrapping up. + /// + public virtual bool HideUponWrapUp => false; + /// /// Gets or sets the amount of uses left. /// @@ -107,6 +127,7 @@ public abstract class MeetingActionButton Button.graphic.sprite = Sprite.LoadAsset(); Button.SetUsesRemaining(UsesLeft); + UsesSoFar = 0; if (MaxUses <= 0) { Button.SetInfiniteUses(); @@ -264,7 +285,9 @@ public abstract class MeetingActionButton /// A value that represents whether the button should light up or not. public virtual bool CanUse() { - return !LimitedUses || UsesLeft > 0; + return (!DisableUponUse || UsesSoFar < 1) && + (!DisableUponVoting || MeetingHud.Instance.CurrentState is not MeetingHud.MeetingStates.Voted) && + (!LimitedUses || UsesLeft > 0); } /// @@ -287,6 +310,11 @@ public abstract class MeetingActionButton Timer = Cooldown; OnClick(); + UsesSoFar++; + if (DisableUponUse && Button) + { + Button!.SetDisabled(); + } } /// diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index e907f34..80ab9d6 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -111,6 +111,15 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.VotingComplete))] public static void VotingCompletePatch(MeetingHud __instance) { + foreach (var meetingAbility in MeetingButtonManager.UntargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role) && + (x.DisableUponVoting || x.HideUponWrapUp))) + { + meetingAbility.UpdateHandler(__instance); + if (meetingAbility.Button) + { + meetingAbility.Button!.SetDisabled(); + } + } MiraEventManager.InvokeEvent(new VotingCompleteEvent(__instance)); } From 3e2c6df829a12cfabca0ede48f71dcb43dd171ce Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:00:57 -0500 Subject: [PATCH 19/24] Fix skip button vote area order --- MiraAPI/MeetingAbilities/MeetingButtonManager.cs | 8 +++++++- MiraAPI/MeetingAbilities/TargetedMeetingButton.cs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs index b45e00b..9f44fcf 100644 --- a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs +++ b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs @@ -102,8 +102,9 @@ public static class MeetingButtonManager { var btn = ability.CreateButton(playerVoteArea); btn.transform.SetParent(playerVoteArea.Buttons.transform); - playerVoteArea.CancelButton.transform.SetAsFirstSibling(); } + var btn2 = ability.CreateButton(meetingHud.SkipVoteButton); + btn2.transform.SetParent(meetingHud.SkipVoteButton.Buttons.transform); if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses; if (ability is MultiTargetMeetingButton multiAbility) multiAbility.Targets = new(); @@ -113,5 +114,10 @@ public static class MeetingButtonManager Error($"Failed to create targeted meeting button {ability.Name}: {e.Message}"); } } + foreach (var playerVoteArea in meetingHud.playerStates) + { + playerVoteArea.CancelButton.transform.SetAsFirstSibling(); + } + meetingHud.SkipVoteButton.CancelButton.transform.SetAsFirstSibling(); } } diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs index 324e7c7..d1d6643 100644 --- a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs @@ -150,6 +150,6 @@ public abstract class TargetedMeetingButton /// Whether the target is valid or not. public virtual bool IsTargetValid(PlayerVoteArea playerVoteArea) { - return playerVoteArea != null && playerVoteArea.PlayerId.Value != PlayerControl.LocalPlayer.Data.PlayerId; + return playerVoteArea != null && playerVoteArea.PlayerId.Value != PlayerControl.LocalPlayer.Data.PlayerId && playerVoteArea.PlayerId.Value != PlayerVoteArea.SkippedVote; } } From 85fb507a43634b8763fdbbe91ff74016224d47af Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 2 Sep 2026 02:34:01 -0500 Subject: [PATCH 20/24] Adjust gamemode role checks for info guide --- MiraAPI/Patches/RoleGuidePatches.cs | 48 +++++++++++++++++++++++------ MiraAPI/Roles/ICustomRole.cs | 9 +++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index c6edf06..a4878e4 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using AmongUs.GameOptions; @@ -7,6 +8,7 @@ using Innersloth.Assets; using MiraAPI.Roles; using MiraAPI.Utilities; using UnityEngine; +using Object = UnityEngine.Object; namespace MiraAPI.Patches; @@ -124,21 +126,41 @@ public static class RoleGuidePatches } private static Dictionary _rolePanels = []; + public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) { if (reset) { _rolePanels.Clear(); - instance.CreateSettingsEntry(StringNames.GameNumImpostors, GameManager.Instance.AllGameSettingData[StringNames.GameNumImpostors].GetValueString(GameManager.Instance.LogicOptions.NumImpostors)); - instance.CreateSettingsEntry(StringNames.GameKillCooldown, GameManager.Instance.AllGameSettingData[StringNames.GameKillCooldown].GetValueString(GameManager.Instance.LogicOptions.GetKillCooldown())); - instance.CreateSettingsEntry(StringNames.GameEmergencyCooldown, GameManager.Instance.AllGameSettingData[StringNames.GameEmergencyCooldown].GetValueString((float)GameManager.Instance.LogicOptions.GetEmergencyCooldown())); - instance.CreateSettingsEntry(StringNames.GameVisualTasks, instance.GetBoolString(GameManager.Instance.LogicOptions.GetVisualTasks())); - instance.CreateSettingsEntry(StringNames.GameAnonymousVotes, instance.GetBoolString(GameManager.Instance.LogicOptions.GetAnonymousVotes())); - instance.CreateSettingsEntry(StringNames.GameConfirmImpostor, instance.GetBoolString(GameManager.Instance.LogicOptions.GetConfirmImpostor())); - instance.CreateSettingsEntry(StringNames.GameTaskBarMode, GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); + instance.CreateSettingsEntry( + StringNames.GameNumImpostors, + GameManager.Instance.AllGameSettingData[StringNames.GameNumImpostors] + .GetValueString(GameManager.Instance.LogicOptions.NumImpostors)); + instance.CreateSettingsEntry( + StringNames.GameKillCooldown, + GameManager.Instance.AllGameSettingData[StringNames.GameKillCooldown] + .GetValueString(GameManager.Instance.LogicOptions.GetKillCooldown())); + instance.CreateSettingsEntry( + StringNames.GameEmergencyCooldown, + GameManager.Instance.AllGameSettingData[StringNames.GameEmergencyCooldown] + .GetValueString((float)GameManager.Instance.LogicOptions.GetEmergencyCooldown())); + instance.CreateSettingsEntry( + StringNames.GameVisualTasks, + instance.GetBoolString(GameManager.Instance.LogicOptions.GetVisualTasks())); + instance.CreateSettingsEntry( + StringNames.GameAnonymousVotes, + instance.GetBoolString(GameManager.Instance.LogicOptions.GetAnonymousVotes())); + instance.CreateSettingsEntry( + StringNames.GameConfirmImpostor, + instance.GetBoolString(GameManager.Instance.LogicOptions.GetConfirmImpostor())); + instance.CreateSettingsEntry( + StringNames.GameTaskBarMode, + GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); foreach (RoleBehaviour roleBehaviour in RoleManager.Instance.AllRoles) { - if (roleBehaviour.Role != RoleTypes.Crewmate && roleBehaviour.Role != RoleTypes.Impostor && roleBehaviour.Role is not RoleTypes.CrewmateGhost && roleBehaviour.Role is not RoleTypes.ImpostorGhost) + if (roleBehaviour.Role != RoleTypes.Crewmate && roleBehaviour.Role != RoleTypes.Impostor && + roleBehaviour.Role is not RoleTypes.CrewmateGhost && + roleBehaviour.Role is not RoleTypes.ImpostorGhost) { var panel = Object.Instantiate( instance.MatchInfoRolePanelPrefab, @@ -155,11 +177,15 @@ public static class RoleGuidePatches var panel = pair.Value; var amount = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role.Role); var chance = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(role.Role); - if (amount == 0 || chance == 0) + var forciblyShow = role is ICustomRole custom ? custom.ForceShowRoleOnWiki : null; + if (amount == 0 || chance == 0 || (Enum.IsDefined(role.Role) && role.IsRoleBlacklisted()) || + (role is ICustomRole custom2 && ((!custom2.CanSpawnOnCurrentMode() && forciblyShow == null) || + (forciblyShow.HasValue && !forciblyShow.Value)))) { panel.gameObject.SetActive(false); continue; } + panel.gameObject.SetActive(true); panel.SetPanel( role, @@ -167,10 +193,12 @@ public static class RoleGuidePatches chance); num++; } + if (num == 0) { instance.rolesEnabledMessage.SetActive(true); } + instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil((float)num / 2f) * 1.3f - 1.5f, 0f, 999f)); instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 13020ff..50a9224 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,6 +1,7 @@ using System; using System.Text; using BepInEx.Configuration; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; @@ -301,7 +302,13 @@ public interface ICustomRole : IOptionable /// Determines whether the role can spawn in general, accounting for gamemodes and everything else. /// /// if the role is able to spawn, otherwise . - public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek(); + public virtual bool CanSpawnOnCurrentMode() => Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode); + + /// + /// Determines whether the role is forcibly shown or disabled in the wiki screen. + /// + /// if the role is always displayed, otherwise if it is never displayable, or if it is dictated by amount and chance. + public virtual bool? ForceShowRoleOnWiki => null; /// /// Gets the function that determines whether the role should be toggled on or off in the game settings. From 81c6bac979e6fa7fd1f01d782c7bfed146e6565d Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:16:32 -0500 Subject: [PATCH 21/24] Add UiButtonResetEvent for better ui button grid --- MiraAPI/Events/Mira/UiButtonPostResetEvent.cs | 30 ++++++++ MiraAPI/Events/Mira/UiButtonResetEvent.cs | 15 ++++ MiraAPI/Hud/MiraHudHelper.cs | 2 +- MiraAPI/MiraApiPlugin.cs | 1 + MiraAPI/MiraApiSettings.cs | 48 ++----------- MiraAPI/VanillaEvents/UiResetEvents.cs | 71 +++++++++++++++++++ 6 files changed, 125 insertions(+), 42 deletions(-) create mode 100644 MiraAPI/Events/Mira/UiButtonPostResetEvent.cs create mode 100644 MiraAPI/Events/Mira/UiButtonResetEvent.cs create mode 100644 MiraAPI/VanillaEvents/UiResetEvents.cs diff --git a/MiraAPI/Events/Mira/UiButtonPostResetEvent.cs b/MiraAPI/Events/Mira/UiButtonPostResetEvent.cs new file mode 100644 index 0000000..e7221ae --- /dev/null +++ b/MiraAPI/Events/Mira/UiButtonPostResetEvent.cs @@ -0,0 +1,30 @@ +using UnityEngine; + +namespace MiraAPI.Events.Mira; + +/// +/// Event fired by Mira API to adjust the UI buttons at the top right of the screen. +/// +public class UiButtonPostResetEvent : MiraEvent +{ + /// + /// Gets the parent for the very top UI row. + /// + public GameObject MainTopUiRow { get; } + + /// + /// Gets the parent for the second top UI row. + /// + public GameObject SecondTopUiRow { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The for the top UI row. + /// The for the second UI row. + public UiButtonPostResetEvent(GameObject topUiRow, GameObject secondUiRow) + { + MainTopUiRow = topUiRow; + SecondTopUiRow = secondUiRow; + } +} diff --git a/MiraAPI/Events/Mira/UiButtonResetEvent.cs b/MiraAPI/Events/Mira/UiButtonResetEvent.cs new file mode 100644 index 0000000..2ad8698 --- /dev/null +++ b/MiraAPI/Events/Mira/UiButtonResetEvent.cs @@ -0,0 +1,15 @@ +namespace MiraAPI.Events.Mira; + +/// +/// Event fired by Mira API prior to adjusting the UI buttons at the top right of the screen. +/// +public class UiButtonResetEvent : MiraEvent +{ + /// + /// Initializes a new instance of the class. + /// + public UiButtonResetEvent() + { + // nothing! + } +} diff --git a/MiraAPI/Hud/MiraHudHelper.cs b/MiraAPI/Hud/MiraHudHelper.cs index 59a8857..b072dc8 100644 --- a/MiraAPI/Hud/MiraHudHelper.cs +++ b/MiraAPI/Hud/MiraHudHelper.cs @@ -186,7 +186,7 @@ public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) { ExtraUiTopRight = new GameObject("ExtraUiTopRight") { - layer = UiTopRight.layer + layer = UiTopRight.layer, }; ExtraUiTopRight.transform.SetParent(instance.MapButton.transform.parent.parent, false); diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index ef9b2b9..0ea0b20 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -70,6 +70,7 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin public override void Load() { Harmony.PatchAll(); + UiResetEvents.Initialize(); JudgeEvents.Initialize(); ReactorCredits.Register("Mira API", Version, IsDevBuild, ReactorCredits.AlwaysShow); diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 9999f15..3d74768 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -1,5 +1,7 @@ using System.Collections; using BepInEx.Configuration; +using MiraAPI.Events; +using MiraAPI.Events.Mira; using MiraAPI.Hud; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; @@ -80,54 +82,18 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) { var topUi = MiraHudHelper.UiTopRight; var extraTopUi = MiraHudHelper.ExtraUiTopRight; - var wikiButton = MiraHudHelper.VanillaMatchInfoButton; - var subButton = MiraHudHelper.SubmergedFloorButton; - var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; - ResetButtonPositions(); if (topUi && extraTopUi) { - var opts = LocalSettingsTabSingleton.Instance; - if (wikiButton) - { - wikiButton.transform.SetParent(opts.WikiOnBottomRow.Value ? extraTopUi.transform : topUi.transform); - } - if (subButton) - { - subButton.transform.SetParent(extraTopUi.transform); - } - if (modDisplay) - { - modDisplay.transform.SetParent(extraTopUi.transform); - } + var genericEvent = new UiButtonResetEvent(); + MiraEventManager.InvokeEvent(genericEvent); + + var genericEvent2 = new UiButtonPostResetEvent(topUi, extraTopUi); + MiraEventManager.InvokeEvent(genericEvent2); MiraHudHelper.UiGrid.ArrangeChilds(); MiraHudHelper.ExtraUiGrid.ArrangeChilds(); } } - public static void ResetButtonPositions() - { - var topUi = MiraHudHelper.UiTopRight; - var extraTopUi = MiraHudHelper.ExtraUiTopRight; - var subButton = MiraHudHelper.SubmergedFloorButton; - var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; - if (topUi && extraTopUi) - { - var wikiButton = MiraHudHelper.VanillaMatchInfoButton; - if (wikiButton) - { - wikiButton.transform.SetParent(null); - } - if (subButton) - { - subButton.transform.SetParent(null); - } - if (modDisplay) - { - modDisplay.transform.SetParent(null); - } - } - } - public static IEnumerator CoResizeSettingsUI() { while (!HudManager.Instance || !MiraHudHelper.UiGrid || !MiraHudHelper.ExtraUiGrid) diff --git a/MiraAPI/VanillaEvents/UiResetEvents.cs b/MiraAPI/VanillaEvents/UiResetEvents.cs new file mode 100644 index 0000000..fe0e6e5 --- /dev/null +++ b/MiraAPI/VanillaEvents/UiResetEvents.cs @@ -0,0 +1,71 @@ +using MiraAPI.Events; +using MiraAPI.Events.Mira; +using MiraAPI.Hud; +using MiraAPI.LocalSettings; + +namespace MiraAPI.VanillaEvents; + +public static class UiResetEvents +{ + public static void Initialize() + { + MiraEventManager.RegisterEventHandler(@event => ResetButtonParents(@event)); + MiraEventManager.RegisterEventHandler(@event => PlaceWikiButton(@event), -900); + MiraEventManager.RegisterEventHandler(@event => PlaceSubmergedButton(@event), -800); + MiraEventManager.RegisterEventHandler(@event => PlaceModifierUi(@event), -700); + } + + public static void ResetButtonParents(UiButtonResetEvent @event) + { + var wikiButton = MiraHudHelper.VanillaMatchInfoButton; + var subButton = MiraHudHelper.SubmergedFloorButton; + var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; + if (wikiButton) + { + wikiButton.transform.SetParent(null); + } + if (subButton) + { + subButton.transform.SetParent(null); + } + if (modDisplay) + { + modDisplay.transform.SetParent(null); + } + } + + public static void PlaceWikiButton(UiButtonPostResetEvent @event) + { + var wikiButton = MiraHudHelper.VanillaMatchInfoButton; + if (!wikiButton) + { + return; + } + var firstRow = @event.MainTopUiRow; + var secondRow = @event.SecondTopUiRow; + var opts = LocalSettingsTabSingleton.Instance; + wikiButton.transform.SetParent(opts.WikiOnBottomRow.Value ? secondRow.transform : firstRow.transform); + } + + public static void PlaceSubmergedButton(UiButtonPostResetEvent @event) + { + var subButton = MiraHudHelper.SubmergedFloorButton; + if (!subButton) + { + return; + } + var secondRow = @event.SecondTopUiRow; + subButton.transform.SetParent(secondRow.transform); + } + + public static void PlaceModifierUi(UiButtonPostResetEvent @event) + { + var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; + if (!modDisplay) + { + return; + } + var secondRow = @event.SecondTopUiRow; + modDisplay.transform.SetParent(secondRow.transform); + } +} From 087ea2a4bd782104bf597508473e2105cc9c415c Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:28:27 -0500 Subject: [PATCH 22/24] Clear up il2cpp warnings --- MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs | 2 ++ MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs index d41b445..b708148 100644 --- a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs +++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Linq; using BepInEx.Unity.IL2CPP.Utils.Collections; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameOptions; using MiraAPI.HnsReimplemented.Options; using MiraAPI.Utilities; @@ -245,6 +246,7 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) } } + [HideFromIl2Cpp] private IEnumerator BeepAlmostEverySecond() { while (!IsFinalCountdown) diff --git a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs index 8474757..44ba56f 100644 --- a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs +++ b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using Il2CppInterop.Runtime.Attributes; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using TMPro; @@ -53,6 +54,7 @@ public class MeetingAbilityBehaviour : MonoBehaviour /// /// The to initialize this component for. /// The parent . + [HideFromIl2Cpp] public void Initialize(TargetedMeetingButton button, PlayerVoteArea voteArea) { if (_init) return; From 7d7d6469ceafe97b563529a805cc7d115d1858d8 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:29:00 -0500 Subject: [PATCH 23/24] Bump version --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 1f8760c..462fbe9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ true - 0.4.3 + 0.5.0 dev All Of Us, XtraCube, Angxl From 9e21726ced3f3ada9c8312408326dfa9edb1cabe Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:29:05 -0500 Subject: [PATCH 24/24] Fix MultiTargetButton being renamed? --- .../Hud/{MultiTargetMeetingButton.cs => MultiTargetButton.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename MiraAPI/Hud/{MultiTargetMeetingButton.cs => MultiTargetButton.cs} (96%) diff --git a/MiraAPI/Hud/MultiTargetMeetingButton.cs b/MiraAPI/Hud/MultiTargetButton.cs similarity index 96% rename from MiraAPI/Hud/MultiTargetMeetingButton.cs rename to MiraAPI/Hud/MultiTargetButton.cs index 2a36219..d52d845 100644 --- a/MiraAPI/Hud/MultiTargetMeetingButton.cs +++ b/MiraAPI/Hud/MultiTargetButton.cs @@ -8,7 +8,7 @@ namespace MiraAPI.Hud; /// that has multiple target objects. /// /// The type of the target objects. -public abstract class MultiTargetMeetingButton : CustomActionButton where T : MonoBehaviour +public abstract class MultiTargetButton : CustomActionButton where T : MonoBehaviour { /// /// Gets or sets the list of targets of the button.