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
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/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/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/Hud/MultiTargetMeetingButton.cs b/MiraAPI/Hud/MultiTargetButton.cs
similarity index 96%
rename from MiraAPI/Hud/MultiTargetMeetingButton.cs
rename to MiraAPI/Hud/MultiTargetButton.cs
index 436d5a2..6e4c4b8 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.
diff --git a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs
index 3af8c65..3661b30 100644
--- a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs
+++ b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using Il2CppInterop.Runtime.Attributes;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using TMPro;
@@ -52,6 +53,7 @@ public class MeetingAbilityBehaviour : MonoBehaviour
///
/// The to initialize this component for.
/// The parent .
+ [HideFromIl2Cpp]
public void Initialize(TargetedMeetingButton button, PlayerVoteArea voteArea)
{
if (_init) return;
diff --git a/MiraAPI/MeetingAbilities/MeetingActionButton.cs b/MiraAPI/MeetingAbilities/MeetingActionButton.cs
index 6844b32..9eeb0fa 100644
--- a/MiraAPI/MeetingAbilities/MeetingActionButton.cs
+++ b/MiraAPI/MeetingAbilities/MeetingActionButton.cs
@@ -67,6 +67,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.
///
@@ -103,6 +123,7 @@ public abstract class MeetingActionButton
Button.graphic.sprite = Sprite.LoadAsset();
Button.SetUsesRemaining(UsesLeft);
+ UsesSoFar = 0;
if (MaxUses <= 0)
{
Button.SetInfiniteUses();
@@ -260,7 +281,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);
}
///
@@ -283,6 +306,11 @@ public abstract class MeetingActionButton
Timer = Cooldown;
OnClick();
+ UsesSoFar++;
+ if (DisableUponUse && Button)
+ {
+ Button!.SetDisabled();
+ }
}
///
diff --git a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs
index 6bd50f4..4f5b207 100644
--- a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs
+++ b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs
@@ -99,8 +99,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();
@@ -110,5 +111,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 b1141ae..2d4766e 100644
--- a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs
+++ b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs
@@ -149,6 +149,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;
}
}
diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs
index 6ad0e47..fc3c8f1 100644
--- a/MiraAPI/MiraApiPlugin.cs
+++ b/MiraAPI/MiraApiPlugin.cs
@@ -71,6 +71,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 9e8b1bf..f14877f 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;
@@ -82,57 +84,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();
}
}
- ///
- /// Resets the button positions in the UI.
- ///
- 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);
- }
- }
- }
-
///
/// Resizes the UI as a coroutine.
///
diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs
index ca7f2be..a4878e4 100644
--- a/MiraAPI/Patches/RoleGuidePatches.cs
+++ b/MiraAPI/Patches/RoleGuidePatches.cs
@@ -1,10 +1,14 @@
-using System.Globalization;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
+using AmongUs.GameOptions;
using HarmonyLib;
using Innersloth.Assets;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using UnityEngine;
+using Object = UnityEngine.Object;
namespace MiraAPI.Patches;
@@ -77,7 +81,29 @@ public static class RoleGuidePatches
{
__instance.numOfTabs = 3;
__instance.TabButtons[0].SelectButton(true);
- __instance.CreateNormalModeSettings();
+ 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
+ {
+ DisplayNormalRoleSettings(__instance, false);
}
}
else if (__instance.HnSModeSettings.Count == 0)
@@ -98,6 +124,90 @@ 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);
+ 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,
+ amount,
+ 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);
+ if (reset)
+ {
+ instance.CreatePlayerEntries();
+ }
+ }
+
[HarmonyPrefix]
[HarmonyPriority(Priority.First)]
[HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreatePlayerEntries))]
diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs
index 90eac2e..8d12db4 100644
--- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs
+++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs
@@ -112,6 +112,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));
}
diff --git a/MiraAPI/Resources/Locale/es_419.xml b/MiraAPI/Resources/Locale/es_419.xml
index d6e11d8..dea47cd 100644
--- a/MiraAPI/Resources/Locale/es_419.xml
+++ b/MiraAPI/Resources/Locale/es_419.xml
@@ -1,14 +1,43 @@
- Escala De Botón
- Botón de Wiki debajo de la IU principal
- Roles
- Modificador
- Modificadores
- Tripulante
- Trip
- Impostor
- Imp
- Neutral
- Neut
+ Factor de Escala de la IU superior derecha
+ Botón de Wiki debajo de la IU principal
+ 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 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!
+ 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
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
diff --git a/MiraAPI/Resources/Locale/pl_PL.xml b/MiraAPI/Resources/Locale/pl_PL.xml
index e59c8b8..0142715 100644
--- a/MiraAPI/Resources/Locale/pl_PL.xml
+++ b/MiraAPI/Resources/Locale/pl_PL.xml
@@ -1,14 +1,32 @@
- 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
+ Możesz uciekać, ale nie możesz się schować!
+Przeżyj przez wymagany czas, albo zabij chowających.
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
diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs
index c7a3cb8..25b43e9 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;
@@ -20,7 +21,7 @@ public interface ICustomRole : IOptionable
string IdPart => GetType().Name;
///
- /// Gets the id part used to build the role's translation keys. It is recommended to call it ModGuid.Role.Faction
+ /// 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!;
@@ -297,9 +298,15 @@ public interface ICustomRole : IOptionable
/// if the role is able to spawn, otherwise .
virtual bool CanSpawnOnCurrentMode()
{
- return !GameManager.Instance.IsHideAndSeek();
+ return Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode);
}
+ ///
+ /// Gets 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.
+ virtual bool? ForceShowRoleOnWiki => null;
+
///
/// Gets the function that determines whether the role should be toggled on or off in the game settings.
///
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)
{
diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs
index 02e200a..7b39dcd 100644
--- a/MiraAPI/Utilities/Helpers.cs
+++ b/MiraAPI/Utilities/Helpers.cs
@@ -529,6 +529,48 @@ public static class Helpers
return role is ICustomRole custom ? custom.RoleName : 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).
///
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);
+ }
+}
diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs
index 1b2cb27..e2bd965 100644
--- a/MiraAPI/Voting/VotingUtils.cs
+++ b/MiraAPI/Voting/VotingUtils.cs
@@ -122,7 +122,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;
@@ -187,7 +187,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;
diff --git a/README.md b/README.md
index 6420a54..84823f4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
[](https://discord.gg/AEfHJGwggC)
+[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.
# Mira API
diff --git a/Translated/README_zh.md b/Translated/README_zh.md
new file mode 100644
index 0000000..7d54169
--- /dev/null
+++ b/Translated/README_zh.md
@@ -0,0 +1,91 @@
+[](https://discord.gg/AEfHJGwggC)
+
+[English](../README.md) | [简体中文]
+
+> 此模组与 Among Us 或 Innersloth LLC 无关,其中包含的内容未得到 Innersloth LLC 的认可或赞助。本文所含部分材料为 Innersloth LLC 的财产。© Innersloth LLC。
+
+> 由[太空果](https://github.com/TAIKgroup)开启了Pr并提交了初版翻译,由[林梅](https://github.com/HayashiUme)维护并提供wiki翻译。
+
+# Mira API
+
+一个全面、易上手的 Among Us 模组 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. 在你的插件类上添加 BepInDependency,如下所示:`[BepInDependency(MiraApiPlugin.Id)]`
+3. 在你的插件类上实现 `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 时,强烈建议遵循以下项目结构,以保持代码整洁有序。
+你也可以查看本仓库中的示例模组以获得一些指导。
+
+```
+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 每个功能的完整文档可在 **[wiki](https://github.com/All-Of-Us-Mods/MiraAPI/wiki)** 上找到:
+
+- [入门 / 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