diff --git a/AmongUs.props b/AmongUs.props index afc343f..0819a88 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -2,7 +2,7 @@ - + @@ -11,4 +11,4 @@ - \ No newline at end of file + 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.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 4da744d..34fce19 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -13,7 +13,7 @@ namespace MiraAPI.Example.Buttons.Freezer; public class FreezeButton : CustomActionButton { - public override string Name => "Freeze"; + public override string Name => "button.freeze"; public override float Cooldown => OptionGroupSingleton.Instance.FreezeDuration; public override bool PauseTimerInVent => true; diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index 8ca1b82..d43bda6 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -9,7 +9,7 @@ namespace MiraAPI.Example.Buttons; public class MeetingButton : CustomActionButton { - public override string Name => "Call Meeting"; + public override string Name => "button.callMeeting"; public override float Cooldown => 15; diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example/Buttons/NeutralKillerButton.cs index 3763577..3771c58 100644 --- a/MiraAPI.Example/Buttons/NeutralKillerButton.cs +++ b/MiraAPI.Example/Buttons/NeutralKillerButton.cs @@ -10,7 +10,7 @@ namespace MiraAPI.Example.Buttons; public class NeutralKillerButton : CustomActionButton { - public override string Name => "Win Game"; + public override string Name => "button.winGame"; public override float Cooldown => 0f; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; public override MiraKeybind Keybind => ExampleKeybinds.NeutralWinKeybind; diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index b59d387..8dead81 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -12,7 +12,7 @@ namespace MiraAPI.Example.Buttons.Teleporter; public class TeleportButton : CustomActionButton { - public override string Name => "Teleport"; + public override string Name => "button.teleport"; public override float Cooldown => OptionGroupSingleton.Instance.TeleportCooldown.Value; diff --git a/MiraAPI.Example/Buttons/ThinkButton.cs b/MiraAPI.Example/Buttons/ThinkButton.cs index a6f6855..71013e9 100644 --- a/MiraAPI.Example/Buttons/ThinkButton.cs +++ b/MiraAPI.Example/Buttons/ThinkButton.cs @@ -1,4 +1,5 @@ using MiraAPI.Hud; +using MiraAPI.Translation; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -6,7 +7,7 @@ namespace MiraAPI.Example.Buttons; public class ThinkButton : CustomActionButton { - public override string Name => "Think"; + public override string Name => "button.think"; public override float Cooldown => 15f; public override float EffectDuration => 10f; public override int MaxUses => 1; @@ -14,13 +15,12 @@ public class ThinkButton : CustomActionButton public override LoadableAsset Sprite => ExampleAssets.ExampleButton; protected override void OnClick() { - // This button does absolutely nothing besides changing its text - Button!.OverrideText("Thinking..."); + Button!.OverrideText("button.think.thinking".Translate()); } public override void OnEffectEnd() { - Button!.OverrideText("Think"); + Button!.OverrideText("button.think".Translate()); } public override bool IsEffectCancellable() diff --git a/MiraAPI.Example/ExampleAssets.cs b/MiraAPI.Example/ExampleAssets.cs index 903afd8..b0ca484 100644 --- a/MiraAPI.Example/ExampleAssets.cs +++ b/MiraAPI.Example/ExampleAssets.cs @@ -9,5 +9,6 @@ public static class ExampleAssets // Credit to EpicHorrors for the teleport button asset. public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png"); + public static LoadableResourceAsset SharpshootButton { get; } = new("MiraAPI.Example.Resources.SharpshootButton.png"); public static LoadableResourceAsset Banner { get; } = new("MiraAPI.Example.Resources.FortniteBanner.jpeg"); } diff --git a/MiraAPI.Example/ExampleColors.cs b/MiraAPI.Example/ExampleColors.cs index d61eb49..cde5aae 100644 --- a/MiraAPI.Example/ExampleColors.cs +++ b/MiraAPI.Example/ExampleColors.cs @@ -6,12 +6,12 @@ namespace MiraAPI.Example; [RegisterCustomColors] public static class ExampleColors { - public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f)) + public static CustomColor Cerulean { get; } = new("color.cerulean", new Color(0.0f, 0.48f, 0.65f)) { ColorBrightness = CustomColorBrightness.Lighter, }; - public static CustomColor Rose { get; } = new("Rose", new Color(0.98f, 0.26f, 0.62f)); + public static CustomColor Rose { get; } = new("color.rose", new Color(0.98f, 0.26f, 0.62f)); - public static CustomColor Gold { get; } = new("Gold", new Color(1.0f, 0.84f, 0.0f)); + public static CustomColor Gold { get; } = new("color.gold", new Color(1.0f, 0.84f, 0.0f)); } diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index bac0507..319e2cf 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -8,7 +8,7 @@ namespace MiraAPI.Example; public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) { - public override string TabName => "Mira Example"; + public override string TabName => "settings.miraExample"; protected override bool ShouldCreateLabels => false; public override LocalSettingTabAppearance TabAppearance => new() @@ -18,7 +18,7 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) }; [LocalSettingsButton] - public LocalSettingsButton ExampleButton { get; private set; } = new("Example Button", OnExampleButtonClick); + public LocalSettingsButton ExampleButton { get; private set; } = new("settings.miraExample.button.exampleButton", OnExampleButtonClick); [LocalToggleSetting] public ConfigEntry ExampleToggle { get; private set; } = config.Bind("General", "Example Bool", true); diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index b20ec41..4ff4c8e 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -3,6 +3,7 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; +using MiraAPI.Translation; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; @@ -16,8 +17,13 @@ namespace MiraAPI.Example; [ReactorModFlags(ModFlags.RequireOnAllClients)] public partial class ExamplePlugin : BasePlugin, IMiraPlugin { + public ExamplePlugin() + { + MiraLocaleManager.Register("mira.example"); + } public Harmony Harmony { get; } = new(Id); public string OptionsTitleText => "Mira API\nExample Mod"; + public string CustomOptionMenuNameTwo => "angxl's Options"; public ConfigFile GetConfigFile() => Config; public override void Load() { diff --git a/MiraAPI.Example/GameModes/ExampleMode.cs b/MiraAPI.Example/GameModes/ExampleMode.cs new file mode 100644 index 0000000..c15e1d5 --- /dev/null +++ b/MiraAPI.Example/GameModes/ExampleMode.cs @@ -0,0 +1,13 @@ +using MiraAPI.GameModes; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.GameModes; + +public class ExampleMode : AbstractGameMode +{ + public override string Name => "Example Mode"; + public override string Description => "An example gamemode."; + public override LoadableAsset? Icon => ExampleAssets.ExampleButton; + public override Color Color => Color.red; +} diff --git a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs index 351d593..6cd9c38 100644 --- a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs +++ b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs @@ -1,5 +1,6 @@ using MiraAPI.Example.Roles; using MiraAPI.GameEnd; +using MiraAPI.Translation; using MiraAPI.Utilities; using UnityEngine; @@ -14,7 +15,7 @@ public class NeutralKillerGameOver : CustomGameOver public override void AfterEndGameSetup(EndGameManager endGameManager) { - endGameManager.WinText.text = "Outcast Killer Wins!"; + endGameManager.WinText.text = "gameover.outcastKiller.win".Translate(); endGameManager.WinText.color = Color.magenta; endGameManager.BackgroundBar.material.SetColor(ShaderID.Color, Color.magenta); } diff --git a/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs b/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs new file mode 100644 index 0000000..a1f446c --- /dev/null +++ b/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs @@ -0,0 +1,29 @@ +using MiraAPI.Example.Roles; +using MiraAPI.MeetingAbilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.MeetingAbilities; + +public class EndMeetingButton : MeetingActionButton +{ + public override string Name => "End Meeting"; + + public override float Cooldown => 15; + + public override float InitialCooldown => 30; + + public override int MaxUses => 3; + + public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + + public override bool Enabled(RoleBehaviour? role) + { + return PlayerControl.LocalPlayer != null && role is MayorRole; + } + + protected override void OnClick() + { + MeetingHud.Instance.RpcClose(); + } +} diff --git a/MiraAPI.Example/MeetingAbilities/ShootButton.cs b/MiraAPI.Example/MeetingAbilities/ShootButton.cs new file mode 100644 index 0000000..179829d --- /dev/null +++ b/MiraAPI.Example/MeetingAbilities/ShootButton.cs @@ -0,0 +1,37 @@ +using MiraAPI.MeetingAbilities; +using MiraAPI.Networking; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.MeetingAbilities; + +public class ShootButton : TargetedMeetingButton +{ + public override string Name => "Shoot"; + + public override int MaxUses => 3; + + public override float InitialCooldown => 5; + + public override float Cooldown => 3; + + public override LoadableAsset Sprite => ExampleAssets.SharpshootButton; + + public override Color OutlineColor => Color.red; + + public override bool Enabled(RoleBehaviour r) + { + return r.IsImpostor; + } + + protected override void OnClick(PlayerVoteArea playerVoteArea) + { + playerVoteArea.SetDisabled(); + var player = playerVoteArea.GetPlayer(); + if (player != null) + { + PlayerControl.LocalPlayer.RpcCustomMurder(player); + } + } +} diff --git a/MiraAPI.Example/MeetingAbilities/ShotgunButton.cs b/MiraAPI.Example/MeetingAbilities/ShotgunButton.cs new file mode 100644 index 0000000..906bd5d --- /dev/null +++ b/MiraAPI.Example/MeetingAbilities/ShotgunButton.cs @@ -0,0 +1,54 @@ +using MiraAPI.MeetingAbilities; +using MiraAPI.Networking; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.MeetingAbilities; + +public class ShotgunButton : MultiTargetMeetingButton +{ + public override string Name => "Shotgun"; + + public override int MaxUses => 3; + + public override float InitialCooldown => 5; + + public override float Cooldown => 3; + + public override LoadableAsset Sprite => ExampleAssets.SharpshootButton; + + public override Color OutlineColor => Color.red; + + public override bool Enabled(RoleBehaviour r) + { + return r.IsImpostor; + } + + protected override void OnClick(PlayerVoteArea playerVoteArea) + { + // Emptu because it only triggers the actual kills once all meeting abilities have been used. + } + + public override LoadableAsset SpriteActive => ExampleAssets.CallMeetingButton; + + public override int TargetsCount => 4; + + protected override void OnSelect(PlayerVoteArea playerVoteArea, bool toggle) + { + HudManager.Instance.PlayerCam.ShakeScreen(0.5f, 2); + } + + public override void OnFinish() + { + foreach (var playerVoteArea in Targets.Keys) + { + playerVoteArea.SetDisabled(); + var player = playerVoteArea.GetPlayer(); + if (player != null) + { + PlayerControl.LocalPlayer.RpcCustomMurder(player); + } + } + } +} diff --git a/MiraAPI.Example/MiraAPI.Example.csproj b/MiraAPI.Example/MiraAPI.Example.csproj index d0b23ac..675a84e 100644 --- a/MiraAPI.Example/MiraAPI.Example.csproj +++ b/MiraAPI.Example/MiraAPI.Example.csproj @@ -3,6 +3,7 @@ 1.0.0 Example mod for Mira API Angxl, XtraCube + true diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 9e20b56..ac15a69 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -2,6 +2,7 @@ using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; +using MiraAPI.Translation; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -9,7 +10,7 @@ namespace MiraAPI.Example.Modifiers; public class CaptainModifier : GameModifier { - public override string ModifierName => "Captain"; + public override string ModifierName => MiraLocaleManager.Get("modifier.captain"); public override LoadableAsset? ModifierIcon => ExampleAssets.CallMeetingButton; public override void OnDeath(DeathReason reason) @@ -19,7 +20,7 @@ public class CaptainModifier : GameModifier public override string GetDescription() { - return $"You can call a meeting from anywhere on the map."; + return MiraLocaleManager.Get("modifier.captain.TabDescription"); } public override int GetAssignmentChance() diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index 8eee41e..8cd0139 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -1,13 +1,14 @@ using Il2CppSystem; using MiraAPI.Example.Roles; using MiraAPI.Modifiers.Types; +using MiraAPI.Translation; using UnityEngine; namespace MiraAPI.Example.Modifiers.Freezer; public class FreezeModifier : TimedModifier { - public override string ModifierName => "Freezed"; + public override string ModifierName => MiraLocaleManager.Get("modifier.freezed"); public override bool HideOnUi => false; public override float Duration => 15f; public override bool AutoStart => true; diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index f2a1e03..7399e6d 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -1,14 +1,15 @@ using MiraAPI.Modifiers.Types; +using MiraAPI.Translation; namespace MiraAPI.Example.Modifiers; public class HighPriorityModifier : GameModifier { - public override string ModifierName => "High Priority"; + public override string ModifierName => MiraLocaleManager.Get("modifier.highPriority"); public override string GetDescription() { - return "You are high priority! Idrk what this means either tbh..."; + return MiraLocaleManager.Get("modifier.highPriority.TabDescription"); } public override int GetAssignmentChance() diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index 0f17f27..c0b8af9 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -8,23 +8,23 @@ namespace MiraAPI.Example.Options; public class ExampleOptions : AbstractOptionGroup { - public override string GroupName => "Example Options 1"; + public override string GroupName => "options.exampleOptions1"; public override Color GroupColor => Color.green; - [ModdedToggleOption("Toggle Opt 1")] + [ModdedToggleOption("options.exampleOptions1.toggleOpt1")] public bool ToggleOpt { get; set; } = false; - [ModdedToggleOption("Toggle Opt 2")] + [ModdedToggleOption("options.exampleOptions1.toggleOpt1")] public bool ToggleOpt2 { get; set; } = true; [ModdedToggleOption("Toggle Opt 3")] [ModdedOptionVisiblity(nameof(ToggleOpt2))] // ToggleOpt3 will be visible only when ToggleOpt2 is true public bool ToggleOpt3 { get; set; } = true; - [ModdedNumberOption("Number Opt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)] + [ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)] public float NumberOpt { get; set; } = 4f; - [ModdedEnumOption("Best API", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])] + [ModdedEnumOption("options.exampleOptions1.bestApi", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])] public BestApi Opt { get; set; } = BestApi.MiraAPI; [ModdedEnumOptionList("Value State {0}", ["None", "Primary", "Secondary"])] diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs index 6efb838..24a061c 100644 --- a/MiraAPI.Example/Options/ExampleOptions2.cs +++ b/MiraAPI.Example/Options/ExampleOptions2.cs @@ -6,25 +6,25 @@ namespace MiraAPI.Example.Options; public class ExampleOptions2 : AbstractOptionGroup { - public override string GroupName => "Example Options 2"; + public override string GroupName => "options.exampleOptions2"; public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue. public override MenuCategory ParentMenu => MenuCategory.CustomTwo; - public ModdedPlayerOption PlayerOption2 { get; } = new("YOU MUST CHOSE A PLAYER", false); - public ModdedPlayerOption PlayerOption { get; } = new("Eh whatever"); - public ModdedToggleOption ToggleOpt1 { get; } = new("Toggle Option 1", false); + public ModdedPlayerOption PlayerOption2 { get; } = new("options.exampleOptions2.youMustChoseAPlayer", false); + public ModdedPlayerOption PlayerOption { get; } = new("options.exampleOptions2.ehWhatever"); + public ModdedToggleOption ToggleOpt1 { get; } = new("options.exampleOptions2.toggleOption1", false); public ModdedOptionList PlayerNums { get; } = new(5, i => new($"PlayerNum{i}", 2, 0, 5, 1, Utilities.MiraNumberSuffixes.None)); - public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false) + public ModdedToggleOption ToggleOpt2 { get; } = new("options.exampleOptions2.toggleOption2", false) { Visible = () => OptionGroupSingleton.Instance.ToggleOpt1, // implicit cast from ModdedToggleOption to bool }; - public ModdedEnumOption EnumOpt { get; } = new("Enum Opt", 0) + public ModdedEnumOption EnumOpt { get; } = new("options.exampleOptions2.enumOpt", 0) { ChangedEvent = x => Logger.Info($"changed Enum Opt to {x}"), }; diff --git a/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs index 0246a1d..e850385 100644 --- a/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs +++ b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs @@ -6,14 +6,14 @@ namespace MiraAPI.Example.Options.Modifiers; public class CaptainModifierSettings : AbstractOptionGroup { - public override string GroupName => "Captain Settings"; + public override string GroupName => "options.captainSettings"; - [ModdedNumberOption("Amount", 0, 5)] + [ModdedNumberOption("options.captainSettings.amount", 0, 5)] public float Amount { get; set; } = 1; - [ModdedNumberOption("Chance", 0, 100, 10)] + [ModdedNumberOption("options.captainSettings.chance", 0, 100, 10)] public float Chance { get; set; } = 50; - [ModdedNumberOption("Uses", 1, 5)] + [ModdedNumberOption("options.captainSettings.uses", 1, 5)] public float NumUses { get; set; } = 3; } diff --git a/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs index 13cdef9..f6296ba 100644 --- a/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs +++ b/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs @@ -6,8 +6,8 @@ namespace MiraAPI.Example.Options.Modifiers; public class FreezeModifierSettings : AbstractOptionGroup { - public override string GroupName => "Freeze"; + public override string GroupName => "options.freezeModifierSettings"; - [ModdedToggleOption("Use Color")] + [ModdedToggleOption("options.freezeModifierSettings.useColor")] public bool UseColor { get; set; } = true; } diff --git a/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs index 0bcbcdc..8d0f82d 100644 --- a/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs +++ b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs @@ -5,15 +5,15 @@ namespace MiraAPI.Example.Options.Modifiers; public class GeneralModifierOptions : AbstractOptionGroup { - public override string GroupName => "General"; + public override string GroupName => "options.generalModifier"; public override uint GroupPriority => 0; public override MenuCategory ParentMenu => MenuCategory.Modifiers; - [ModdedToggleOption("Some Boolean Option")] + [ModdedToggleOption("options.generalModifier.someBooleanOption")] public bool SomeBooleanOption { get; set; } = true; - [ModdedNumberOption("Number Option", 0, 10, 1)] + [ModdedNumberOption("options.generalModifier.numberOption", 0, 10, 1)] public float NumberOption { get; set; } = 5; } diff --git a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs index 3c1fdeb..b87b84e 100644 --- a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs +++ b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs @@ -13,11 +13,11 @@ public class FreezerRoleSettings : AbstractOptionGroup TmpSpriteUtils.CreateSpriteAsset( MiraAssets.ImpostorFile.LoadAsset(), "ApiExample.Role.Impostor.FreezerRole")); - public override string GroupName => "Custom Role"; + public override string GroupName => "ApiExample.Role.Freezer"; - [ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)] + [ModdedNumberOption("ApiExample.Role.Freezer.Settings.FreezeDuration", 1, 15, 1, MiraNumberSuffixes.Seconds)] public float FreezeDuration { get; set; } = 5; - [ModdedNumberOption("Freeze Uses", 1, 5)] + [ModdedNumberOption("ApiExample.Role.Freezer.Settings.FreezeUses", 1, 5)] public float FreezeUses { get; set; } = 1; } diff --git a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs index d05569b..4a4f694 100644 --- a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs +++ b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs @@ -8,13 +8,13 @@ namespace MiraAPI.Example.Options.Roles; public class TeleporterOptions : AbstractRoleOptionGroup { - public override string GroupName => "Teleporter"; + public override string GroupName => "ApiExample.Role.Teleporter"; - public ModdedNumberOption TeleportCooldown { get; set; } = new("Teleport Cooldown", 10, 5, 60, 2.5f, MiraNumberSuffixes.Seconds); + public ModdedNumberOption TeleportCooldown { get; set; } = new("ApiExample.Role.Teleporter.Settings.TeleportCooldown", 10, 5, 60, 2.5f, MiraNumberSuffixes.Seconds); - [ModdedNumberOption("Teleport Duration", 5, 25, 1, MiraNumberSuffixes.Seconds)] + [ModdedNumberOption("ApiExample.Role.Teleporter.Settings.TeleportDuration", 5, 25, 1, MiraNumberSuffixes.Seconds)] public float TeleportDuration { get; set; } = 10; - [ModdedNumberOption("Zoom Distance", 4, 15)] + [ModdedNumberOption("ApiExample.Role.Teleporter.Settings.ZoomDistance", 4, 15)] public float ZoomDistance { get; set; } = 6; } diff --git a/MiraAPI.Example/Resources/Locale/en_US.xml b/MiraAPI.Example/Resources/Locale/en_US.xml new file mode 100644 index 0000000..e370dc3 --- /dev/null +++ b/MiraAPI.Example/Resources/Locale/en_US.xml @@ -0,0 +1,83 @@ + + + Teleporter + Zoom out and teleport across the map! + Zoom out and teleport across the map! + Zoom out and teleport across the map! + The Teleporter is a Crewmate role that can move around the map using their dope-ass ability. + Freezer + Get iced, nerd. + Freeze others for a duration of time. + Freeze another player for a duration of time. + The Freezer is an Impostor role that can freeze players. Or something along those lines. + Mayor + Democracy is the best policy. + Vote out the Impostors with more votes. +Your votes have double the power. + You get one extra vote in the meeting! + The Mayor is a Crewmate role that gets two votes in the meeting, instead of the singular vote other Crewmates get. + Chamelon + Become one with the environment. + Fade out of everyone's vision by not moving. + Stay invisible while not moving. + The Chamelon is a Crewmate role that slowly becomes invisible while standing still. + Outcast Killer + Kill everyone else. + Be the last outcast standing. +Eliminate every other faction. + Outcast who can kill. + The Outcast Killer is an Outcast role that can kill, like some sort of neutral killer... + Outcast + Outcast Roles + OUTCAST + You are an Outcast. You do not have a team. + Think + Thinking... + Freeze + Teleport + Call Meeting + Win Game + Captain + You can call a meeting from anywhere on the map. + Freezed + High Priority + You are high priority! Idrk what this means either tbh... + Example Options 1 + Toggle Opt 1 + Toggle Opt 2 + Number Opt + Best API + Example Options 2 + YOU MUST CHOSE A PLAYER + Eh whatever + Toggle Option 1 + Toggle Option 2 + Enum Opt + Captain Settings + Amount + Chance + Uses + Freeze + Use Color + General + Some Boolean Option + Number Option + Custom Role + Freeze Duration + Freeze Uses + Teleport Cooldown + Teleport Duration + Zoom Distance + Mira Example + Example Bool + Example Bool 2 + Slider + Example Number + Example Multplier + Example Enum + Example Button + Outcast Killer Wins! + Cerulean + Rose + Gold + diff --git a/MiraAPI.Example/Resources/Locale/zh_CN.xml b/MiraAPI.Example/Resources/Locale/zh_CN.xml new file mode 100644 index 0000000..e69f22c --- /dev/null +++ b/MiraAPI.Example/Resources/Locale/zh_CN.xml @@ -0,0 +1,66 @@ + + + 传送者 + 缩小视野并在地图上传送! + 缩小视野并在地图上传送! + 缩小视野并在地图上传送! + 冰冻者 + 将其他玩家冰冻一段时间。 + 市长 + 你拥有额外投票权! + 变色龙 + 静止不动时保持隐身。 + 流放杀手 + 可以击杀的流放者。 + 流放者 + 流放者 + 你是流放者,你没有队伍。 + 思考 + 思考中... + 冰冻 + 传送 + 召集会议 + 赢得游戏 + 队长 + 你可以在地图上任意位置召集会议。 + 已被冰冻 + 高优先级 + 你是高优先级!老实说我也不太清楚这是什么意思…… + 示例选项组 1 + 开关选项 1 + 开关选项 2 + 数字选项 + 谁是最好的API + 示例选项组 2 + 你必须选择一个玩家 + 咕咕嘎嘎 + 开关选项 1 + 开关选项 2 + 枚举选项 + 队长设置 + 数量 + 概率 + 使用次数 + 冰冻 + 使用颜色 + 通用 + 某个布尔选项 + 数字选项 + 冰冻时长 + 冰冻次数 + 传送冷却 + 传送持续时间 + 缩放距离 + Mira 示例插件 + 示例布尔 + 示例布尔 2 + 滑块 + 示例数字 + 示例倍数 + 示例枚举 + 示例按钮 + 流放杀手获胜! + 天蓝 + 玫瑰红 + 金色 + diff --git a/MiraAPI.Example/Resources/SharpshootButton.png b/MiraAPI.Example/Resources/SharpshootButton.png new file mode 100644 index 0000000..f8ccae6 Binary files /dev/null and b/MiraAPI.Example/Resources/SharpshootButton.png differ diff --git a/MiraAPI.Example/Roles/ChameleonRole.cs b/MiraAPI.Example/Roles/ChameleonRole.cs index f601551..c1d9be2 100644 --- a/MiraAPI.Example/Roles/ChameleonRole.cs +++ b/MiraAPI.Example/Roles/ChameleonRole.cs @@ -10,9 +10,8 @@ namespace MiraAPI.Example.Roles; public class ChameloenRole : CrewmateRole, ICustomRole { - public string RoleName => "Chamelon"; - public string RoleLongDescription => "Stay invisible while not moving."; - public string RoleDescription => RoleLongDescription; + public string IdPart => "Chamelon"; + public string IdPrefix => "ApiExample.Role.Crewmate"; public Color RoleColor => Palette.AcceptedGreen; public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; @@ -22,7 +21,9 @@ public class ChameloenRole : CrewmateRole, ICustomRole { OptionsScreenshot = ExampleAssets.Banner, Icon = MiraAssets.CrewmateFile, - IconTmp = TmpSpriteUtils.CreateSpriteAsset(MiraAssets.CrewmateFile.LoadAsset(), "ApiExample.Role.Impostor.ChameleonRole"), + IconTmp = TmpSpriteUtils.CreateSpriteAsset( + MiraAssets.CrewmateFile.LoadAsset(), + "ApiExample.Role.Impostor.ChameleonRole"), IntroSound = CustomRoleUtils.GetIntroSound(RoleTypes.Shapeshifter), }; @@ -56,12 +57,18 @@ public class ChameloenRole : CrewmateRole, ICustomRole { SpriteRenderer rend = Player.cosmetics.currentBodySprite.BodySprite; TextMeshPro tmp = Player.cosmetics.nameText; - tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f); + tmp.color = Color.Lerp( + tmp.color, + new Color(tmp.color.r, tmp.color.g, tmp.color.b, Player.AmOwner ? 0.3f : 0), + Time.deltaTime * 4f); rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f); foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren()) { - cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f); + cosmetic.color = Color.Lerp( + cosmetic.color, + new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), + Time.deltaTime * 4f); } } } @@ -75,6 +82,7 @@ public class ChameloenRole : CrewmateRole, ICustomRole { cosmetic.color = Color.white; } + Player.cosmetics.currentBodySprite.BodySprite.color = Color.white; Player.cosmetics.nameText.color = Color.white; } diff --git a/MiraAPI.Example/Roles/FreezerRole.cs b/MiraAPI.Example/Roles/FreezerRole.cs index 5a7f4bd..775b373 100644 --- a/MiraAPI.Example/Roles/FreezerRole.cs +++ b/MiraAPI.Example/Roles/FreezerRole.cs @@ -6,16 +6,15 @@ namespace MiraAPI.Example.Roles; public class FreezerRole : ImpostorRole, ICustomRole { - public string RoleName => "Freezer"; - public string RoleLongDescription => "Freeze another player for a duration of time."; - public string RoleDescription => RoleLongDescription; + public string IdPart => "Freezer"; + public string IdPrefix => "ApiExample.Role.Impostor"; public Color RoleColor => Palette.Blue; public ModdedRoleTeams Team => ModdedRoleTeams.Impostor; public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this) { Icon = MiraAssets.ImpostorFile, - IconTmp = TmpSpriteUtils.CreateSpriteAsset(MiraAssets.ImpostorFile.LoadAsset(), "ApiExample.Role.Impostor.FreezerRole"), + IconTmp = TmpSpriteUtils.CreateSpriteAsset(MiraAssets.ImpostorFile.LoadAsset(), "ApiExample.Role.Impostor.Freezer"), OptionsScreenshot = ExampleAssets.Banner, MaxRoleCount = 2, }; diff --git a/MiraAPI.Example/Roles/MayorRole.cs b/MiraAPI.Example/Roles/MayorRole.cs index 42c264f..45639c0 100644 --- a/MiraAPI.Example/Roles/MayorRole.cs +++ b/MiraAPI.Example/Roles/MayorRole.cs @@ -5,9 +5,8 @@ namespace MiraAPI.Example.Roles; public class MayorRole : CrewmateRole, ICustomRole { - public string RoleName => "Mayor"; - public string RoleLongDescription => "You get extra votes!"; - public string RoleDescription => RoleLongDescription; + public string IdPart => "Mayor"; + public string IdPrefix => "ApiExample.Role.Crewmate"; public Color RoleColor => new Color32(221, 176, 152, 255); public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; public CustomRoleConfiguration Configuration => new(this); diff --git a/MiraAPI.Example/Roles/NeutralKillerRole.cs b/MiraAPI.Example/Roles/NeutralKillerRole.cs index 1afa4eb..62a4cbe 100644 --- a/MiraAPI.Example/Roles/NeutralKillerRole.cs +++ b/MiraAPI.Example/Roles/NeutralKillerRole.cs @@ -8,9 +8,8 @@ namespace MiraAPI.Example.Roles; public class NeutralKillerRole : ImpostorRole, ICustomRole { - public string RoleName => "Outcast Killer"; - public string RoleDescription => "Outcast who can kill."; - public string RoleLongDescription => RoleDescription; + public string IdPart => "OutcastKiller"; + public string IdPrefix => "ApiExample.Role.Outcast"; public Color RoleColor => Color.magenta; public ModdedRoleTeams Team => ModdedRoleTeams.Custom; @@ -21,13 +20,13 @@ public class NeutralKillerRole : ImpostorRole, ICustomRole CanUseVent = true, }; - public RoleOptionsGroup RoleOptionsGroup { get; } = new("Outcast", Color.gray); + public RoleOptionsGroup RoleOptionsGroup { get; } = new("outcast.group", Color.gray); [HideFromIl2Cpp] public TeamIntroConfiguration? IntroConfiguration { get; } = new( Color.gray, - "OUTCAST", - "You are an Outcast. You do not have a team."); + "outcast.roleTitle", + "outcast.teamIntroDescription"); public override void SpawnTaskHeader(PlayerControl playerControl) { diff --git a/MiraAPI.Example/Roles/TeleporterRole.cs b/MiraAPI.Example/Roles/TeleporterRole.cs index 5b00f63..95b8b6c 100644 --- a/MiraAPI.Example/Roles/TeleporterRole.cs +++ b/MiraAPI.Example/Roles/TeleporterRole.cs @@ -5,9 +5,8 @@ namespace MiraAPI.Example.Roles; public class TeleporterRole : CrewmateRole, ICustomRole { - public string RoleName => "Teleporter"; - public string RoleLongDescription => "Zoom out and teleport across the map!"; - public string RoleDescription => RoleLongDescription; + public string IdPart => "Teleporter"; + public string IdPrefix => "ApiExample.Role.Crewmate"; public Color RoleColor => new Color32(221, 176, 152, 255); public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs index 89663d1..f1b5c49 100644 --- a/MiraAPI/Colors/CustomColor.cs +++ b/MiraAPI/Colors/CustomColor.cs @@ -1,5 +1,5 @@ -using MiraAPI.Utilities; -using Reactor.Localization.Utilities; +using MiraAPI.Translation; +using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Colors; @@ -43,7 +43,7 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha } /// - public CustomColor(string name, Color32 mainColor, Color32 shadowColor) : this(CustomStringName.CreateAndRegister(name), mainColor, shadowColor) + public CustomColor(string name, Color32 mainColor, Color32 shadowColor) : this(MiraLocaleManager.GetOrCreateLocaleString(name), mainColor, shadowColor) { } } diff --git a/MiraAPI/Colors/PaletteManager.cs b/MiraAPI/Colors/PaletteManager.cs index fe7e725..3d25b33 100644 --- a/MiraAPI/Colors/PaletteManager.cs +++ b/MiraAPI/Colors/PaletteManager.cs @@ -20,7 +20,8 @@ public static class PaletteManager { var colors = CustomColors.Select(x => x.MainColor).ToArray(); var shadowColors = CustomColors.Select(x => x.ShadowColor).ToArray(); - var stringNames = CustomColors.Select(x => x.Name).ToArray(); + var stringNames = CustomColors.Select(x => + x.Name).ToArray(); Palette.PlayerColors = Palette.PlayerColors.ToArray().AddRangeToArray(colors); Palette.ShadowColors = Palette.ShadowColors.ToArray().AddRangeToArray(shadowColors); 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/Events/MiraCancelableEvent.cs b/MiraAPI/Events/MiraCancelableEvent.cs index b4d3f05..fe772a9 100644 --- a/MiraAPI/Events/MiraCancelableEvent.cs +++ b/MiraAPI/Events/MiraCancelableEvent.cs @@ -1,8 +1,11 @@ -namespace MiraAPI.Events; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Events; /// /// Abstract class for Mira Events that can be cancelled. /// +[MiraIgnore] public abstract class MiraCancelableEvent : MiraEvent { /// diff --git a/MiraAPI/Events/MiraEvent.cs b/MiraAPI/Events/MiraEvent.cs index f0f49bd..7fdca49 100644 --- a/MiraAPI/Events/MiraEvent.cs +++ b/MiraAPI/Events/MiraEvent.cs @@ -1,6 +1,9 @@ -namespace MiraAPI.Events; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Events; /// /// Abstract class for Mira Events. /// +[MiraIgnore] public abstract class MiraEvent; diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs index 2145b32..8f29544 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs @@ -34,6 +34,11 @@ public class HandleVoteEvent : MiraCancelableEvent /// public NetworkedPlayerInfo TargetPlayerInfo { get; } + /// + /// Gets or sets a value indicating whether the vote event is an Overrule caused by a Judge. + /// + public bool IsOverruling { get; set; } + /// /// Initializes a new instance of the class. /// @@ -47,4 +52,20 @@ public class HandleVoteEvent : MiraCancelableEvent PreventVote = false; TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId); } + + /// + /// Initializes a new instance of the class. + /// + /// The voter's . + /// The target's playerId. + /// Whether the vote is actually for a Judge. + public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId, bool isOverruling) + { + VoteData = playerVoteData; + Player = playerVoteData.Owner; + TargetId = targetId; + PreventVote = false; + IsOverruling = isOverruling; + TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId); + } } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs index a61cc51..8e4894a 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs @@ -33,6 +33,16 @@ public class ProcessVotesEvent(List votes, NetworkedPlayerInfo? exil /// public List Votes { get; } = votes; + /// + /// Gets or sets a value indicating whether an overruling was done. Normally set by judge but can be overwritten. + /// + public bool OverruledVote { get; set; } + + /// + /// Gets or sets a value specifically to determine which Judge (if any) caused the overruling. Normally set by judge but can be overwritten. + /// + public ushort OverruledNonce { get; set; } + /// /// Gets or sets the player to be exiled. Will be if no player is to be exiled. /// diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs new file mode 100644 index 0000000..10dd377 --- /dev/null +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -0,0 +1,297 @@ +using System.Collections; +using System.Collections.Generic; +using AmongUs.GameOptions; +using MiraAPI.GameOptions; +using MiraAPI.PluginLoading; +using MiraAPI.Translation; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; + +namespace MiraAPI.GameModes; + +/// +/// Base class for custom gamemodes. +/// +[MiraIgnore] +public abstract class AbstractGameMode : IOptionable +{ + /// + /// Gets the game mode name. + /// + public abstract string Name { get; } + + /// + /// Gets a value indicating whether the mode is visible in the menu. Only checked at startup. + /// + public virtual bool HideMode => false; + + /// + /// Gets the game mode description. + /// + public abstract string Description { get; } + + /// + /// Gets the game mode color to display on the lobby pane. + /// + public virtual Color Color { get; } = Color.white; + + /// + /// Gets the icon of the gamemode. Will be shown in options menu underneath the gamemode option. + /// + public virtual LoadableAsset? Icon => null; + + /// + /// Gets or sets the icon of the gamemode for use on the options pop-up. + /// + public TMP_SpriteAsset TmpIcon { get; set; } = null!; + + /// + /// Gets the colored game mode name, using the color and name properties. + /// + public string ColoredName => $"{MiraLocaleManager.Get(Name)}"; + + /// + /// Gets the game mode id. + /// + public uint ID { get; internal set; } + + /// + /// Gets a value indicating whether a specific body type is used by the game mode. + /// + public virtual bool GameModeBodyTypeOverride => false; + + /// + /// Gets a value indicating whether the gamemode removes the normal game settings. + /// + public virtual bool ShowNormalGameSettings => true; + + /// + /// Gets a value indicating whether the gamemode removes the normal role settings. + /// + public virtual bool ShowNormalRoleSettings => true; + + /// + /// Gets a value indicating the default kill cooldown for impostors in this mode. + /// + public virtual float DefaultImpostorKillCooldown => GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown); + + /// + /// Called on GameManager.GetBodyType(). + /// + /// Player to get body type from. + /// Resulting body type. + public virtual PlayerBodyTypes GetBodyType(PlayerControl player) + { + if (AprilFoolsMode.ShouldHorseAround()) + { + return PlayerBodyTypes.Horse; + } + if (AprilFoolsMode.ShouldLongAround()) + { + return PlayerBodyTypes.Long; + } + return PlayerBodyTypes.Normal; + } + + /// + /// Gets a value indicating whether a custom intro sequence is implemented by the game mode. + /// + public virtual bool ShowGameModeIntroCutscene => false; + + /// + /// Called when Intro Cutscene is destroyed. + /// + public virtual void Initialize() + { + } + + /// + /// Called on HudManager.Start(). + /// + /// Instance of HudManager. + public virtual void HudStart(HudManager instance) + { + } + + /// + /// Called every frame on HudManager.Update(). + /// + /// Instance of HudManager. + public virtual void HudUpdate(HudManager instance) + { + } + + /// + /// Called when a player is killed. + /// + /// PlayerControl that was killed. + /// Determines whether to assign a ghost role on the player. + public virtual void OnPlayerDeath(PlayerControl player, bool assignGhostRole) + { + GameManager.Instance.LogicRoleSelection.OnPlayerDeath(player, assignGhostRole); + } + + /// + /// Calculate Game End logic. + /// + /// Should original game end logic be used. + /// Instance of LogicGameFlowNormal. + public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) + { + runOriginal = true; + } + + /// + /// Custom role assignment. + /// + /// Should original role assignment be used. + /// Instance of LogicRoleSelection. + public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) + { + runOriginal = true; + } + + /// + /// Custom role assignment to run after all roles are handed out. + /// + /// Instance of LogicRoleSelection. + public virtual void PostAssignRoles(LogicRoleSelectionNormal instance) + { + } + + /// + /// Custom murder logic. + /// + /// Should the original murder logic be used. + /// Result of custom murder logic. + /// Target player for murder. + public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target) + { + result = false; + runOriginal = true; + } + + /// + /// Custom winner selection. + /// + /// List of winners or null. + public virtual List? CalculateWinners() => null; + + /// + /// The IEnumerator that plays the intro cutscene for this gamemode. + /// + /// An instance of IntroCutscene. + /// An IEnumerator to run the intro cutscene instead of the base game one. + public virtual IEnumerator IntroCutscene(IntroCutscene __instance) + { + yield return new WaitForEndOfFrame(); + } + + /// + /// Can Admin be used in this gamemode. + /// + /// Admin Console. + /// True if Admin console is enabled. + public virtual bool CanUseMapConsole(MapConsole console) => true; + + /// + /// Can a body be reported in this gamemode. + /// + /// Target body for reporting. + /// True if dead bodies can be reported. + public virtual bool CanReport(DeadBody body) => true; + + /// + /// Can system consoles be used in this gamemode. + /// + /// System Console. + /// True if system consoles are enabled in this mode. + public virtual bool CanUseSystemConsole(SystemConsole console) => true; + + /// + /// Can tasks be interacted with in this gamemode. + /// + /// Task console. + /// True if tasks are enabled in this mode. + public virtual bool CanUseTasks(Console console) => true; + + /// + /// Should the sabotage map be used when attempting to open sabotage overlay. + /// + /// MapBehaviour object. + /// True if the sabotage map should be shown. + public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true; + + /// + /// Gets the to display on the map. + /// + /// The for the gamemode. + public virtual MapOptions GetMapOptions() + { + return new MapOptions + { + Mode = PlayerControl.LocalPlayer.Data.Role.IsImpostor && !MeetingHud.Instance + ? MapOptions.Modes.Sabotage + : MapOptions.Modes.Normal, + }; + } + + /// + /// Can a player vent in this gamemode. + /// + /// Target vent. + /// Player attempting to vent. + /// True if venting is enabled in this mode. + public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true; + + /// + /// Does the task bar appear in the gamemode. + /// + /// True if the task bar is enabled in this mode. + public virtual bool ShowTaskBar => true; + + public virtual void UpdateTaskPanel(TaskPanelBehaviour instance) + { + instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f) + ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f) + : Vector3.zero; + Vector3 vector = instance.background.sprite.bounds.extents; + vector.y = -vector.y; + vector = vector.Mul(instance.background.transform.localScale); + instance.background.transform.localPosition = vector; + Vector3 vector2 = instance.tab.sprite.bounds.extents; + vector2 = vector2.Mul(instance.tab.transform.localScale); + vector2.y = -vector2.y; + vector2.x += vector.x * 2f; + instance.tab.transform.localPosition = vector2; + if (GameManager.Instance == null) + { + return; + } + + var yPos = 0.6f; + var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x; + instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z); + instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z); + if (instance.open) + { + instance.timer = Mathf.Min(1f, instance.timer + Time.deltaTime / instance.animationTimeSeconds); + } + else + { + instance.timer = Mathf.Max(0f, instance.timer - Time.deltaTime / instance.animationTimeSeconds); + } + + Vector3 relativePos = new( + Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), + yPos, + instance.openPosition.z); + instance.transform.localPosition = + AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); + } + + /// + public override string ToString() => Name; +} diff --git a/MiraAPI/GameModes/ClassicMode.cs b/MiraAPI/GameModes/ClassicMode.cs new file mode 100644 index 0000000..3da9a23 --- /dev/null +++ b/MiraAPI/GameModes/ClassicMode.cs @@ -0,0 +1,20 @@ +using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.GameModes; + +/// +/// The classic game mode. +/// +[MiraIgnore] +public class ClassicMode : AbstractGameMode +{ + /// + public override string Name => "MiraApi.Gamemode.Classic"; + + /// + public override string Description => "MiraApi.Gamemode.Classic.Description"; + + public override LoadableAsset? Icon => MiraAssets.ClassicGamemodeIcon; +} diff --git a/MiraAPI/GameModes/CustomGameMode.cs b/MiraAPI/GameModes/CustomGameMode.cs deleted file mode 100644 index f4dc43d..0000000 --- a/MiraAPI/GameModes/CustomGameMode.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Collections.Generic; - -namespace MiraAPI.GameModes; - -/// -/// Base class for custom gamemodes. -/// -public abstract class CustomGameMode -{ - /// - /// Gets the game mode name. - /// - public abstract string Name { get; } - - /// - /// Gets the game mode description. - /// - public abstract string Description { get; } - - /// - /// Gets the game mode ID. - /// - public abstract int Id { get; } - - /// - /// Called when is destroyed. - /// - public virtual void Initialize() - { - } - - /// - /// Called on . - /// - /// Instance of . - public virtual void HudStart(HudManager instance) - { - } - - /// - /// Called every frame on . - /// - /// Instance of . - public virtual void HudUpdate(HudManager instance) - { - } - - /// - /// Called when a player is killed. - /// - /// that was killed. - public virtual void OnDeath(PlayerControl player) - { - } - - /// - /// Calculate Game End logic. - /// - /// Should original game end logic be used. - /// Instance of . - public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) - { - runOriginal = true; - } - - /// - /// Custom role assignment. - /// - /// Should original role assignment be used. - /// Instance of . - public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) - { - runOriginal = true; - } - - /// - /// Custom murder logic. - /// - /// Should the original murder logic be used. - /// Result of custom murder logic. - /// Target for murder. - public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target) - { - result = false; - runOriginal = true; - } - - /// - /// Should Roles Settings be available when this gamemode is selected. - /// - /// if Role Settings are enabled in this game mode. - public virtual bool AreRoleSettingsEnabled() => true; - - /// - /// Should Game Settings be available when this gamemode is selected. - /// - /// if Game Settings are enabled in this mode. - public virtual bool AreGameSettingsEnabled() => true; - - /// - /// Custom winner selection. - /// - /// List of winners or . - public virtual List? CalculateWinners() => null; - - /// - /// Show gamemode in . - /// - /// if the game mode should be shown in the intro cutscene. - public virtual bool ShowGameModeIntroCutscene() => false; - - /// - /// Can Admin be used in this gamemode. - /// - /// Admin Console. - /// if Admin console is enabled. - public virtual bool CanUseMapConsole(MapConsole console) => true; - - /// - /// Can a be reported in this gamemode. - /// - /// Target for reporting. - /// if s can be reported. - public virtual bool CanReport(DeadBody body) => true; - - /// - /// Can s be used in this gamemode. - /// - /// System Console. - /// if s are enabled in this mode. - public virtual bool CanUseSystemConsole(SystemConsole console) => true; - - /// - /// Can tasks be interacted with in this gamemode. - /// - /// Task console. - /// if tasks are enabled in this mode. - public virtual bool CanUseTasks(Console console) => true; - - /// - /// Should the sabotage map be used when attempting to open sabotage overlay. - /// - /// object. - /// if the sabotage map should be shown. - public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true; - - /// - /// Can a player vent in this gamemode. - /// - /// Target vent. - /// Player attempting to vent. - /// if venting is enabled in this mode. - public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true; -} diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 3edd028..254bf6d 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,70 +1,124 @@ using System; using System.Collections.Generic; using System.Linq; +using MiraAPI.Translation; +using MiraAPI.PluginLoading; +using Reactor.Utilities; namespace MiraAPI.GameModes; /// -/// Manages s. +/// Manages custom gamemodes. /// public static class CustomGameModeManager { - /// - /// List of registered . - /// - internal static readonly Dictionary GameModes = []; + internal static readonly Dictionary IdToModeMap = []; - public static bool IsDefault() + private static uint GetNextId() => ++LastId; + + private static uint LastId { get; set; } + + /// + /// Register gamemode from type. + /// + /// Type of gamemode class, should inherit from . + /// The custom plugin info of the mod. + /// Whether the gamemode was successfully registered. + internal static bool RegisterGameMode(Type gameModeType, MiraPluginInfo pluginInfo) { - return ActiveMode?.Id == 0; - } - - /// - /// Gets the current . - /// - public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode(); - - /// - /// Set current . - /// - /// ID. - public static void SetGameMode(int id) - { - if (GameModes.TryGetValue(id, out var gameMode)) - { - ActiveMode = gameMode; - return; - } - - Error($"No gamemode with id {id} found!"); - } - - /// - /// Register from type. - /// - /// Type of , should inherit from . - internal static void RegisterGameMode(Type gameModeType) - { - if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType)) + if (!typeof(AbstractGameMode).IsAssignableFrom(gameModeType)) { Warning($"{gameModeType.Name} does not inherit CustomGameMode!"); - return; + return false; } - var modeObj = Activator.CreateInstance(gameModeType); + var instance = Activator.CreateInstance(gameModeType); - if (modeObj is not CustomGameMode gameMode) + if (instance is not AbstractGameMode mode) { Error($"Failed to create instance of {gameModeType.Name}"); - return; + return false; } - if (GameModes.Any(x => x.Key == gameMode.Id)) + if (!mode.HideMode) { - Error($"ID for gamemode {gameMode.Name} already exists!"); + IdToModeMap.Add(GetNextId(), mode); + pluginInfo.GameModes.Add(LastId, mode); + mode.ID = LastId; + GameModeOption.AddOption(mode); + } + return true; + } + + /// + /// Checks to see if the current game mode is an instance of the classic mode. + /// + /// True if the classic mode is the current one. + public static bool IsClassic() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(ClassicMode)); + + /// + /// Checks to see if the current game mode is an instance of the HNS mode. + /// + /// True if the Hide n Seek mode is the current one. + public static bool IsHideNSeek() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(HideAndSeekMode)); + + /// + /// Checks if a provided GameMode is the current active one. + /// + /// The AbstractGameMode subclass being checked. + /// Whether the provided mode is the current active one. + public static bool IsActiveGameMode() where T : AbstractGameMode => ActiveMode is T; + + /// + /// Gets the current gamemode. + /// + public static AbstractGameMode? ActiveMode { get; private set; } + + /// + /// Gets a gamemode from an ID. + /// + /// The ID of the gamemode to fetch. + /// The gamemode matching that ID. + public static AbstractGameMode GetMode(uint id) => IdToModeMap[id]; + + /// + /// Finds the parent mod of a custom gamemode. + /// + /// The object. + /// A object representing the parent mod of the gamemode. + public static MiraPluginInfo? FindParentMod(AbstractGameMode gameMode) + { + return MiraPluginManager.Instance.RegisteredPlugins.FirstOrDefault(plugin => plugin.GameModes.ContainsValue(gameMode)); + } + + internal static void RegisterDefaultMode() + { + var defaultMode = new ClassicMode(); + IdToModeMap.Add(0, defaultMode); + // no need to add to game mode option as it already contains it + // because we cannot have the option be created with no values + defaultMode.ID = 0; + var hnsMode = new HideAndSeekMode(); + if (!hnsMode.HideMode) + { + IdToModeMap.Add(1, hnsMode); + hnsMode.ID = 1; + GameModeOption.AddOption(hnsMode); + LastId++; + } + } + + internal static void GetAndSetGameMode() + { + var id = (uint)GameModeOption.Value; + + if (IdToModeMap.TryGetValue(id, out var mode)) + { + ActiveMode = mode; return; } - GameModes.Add(gameMode.Id, gameMode); + ActiveMode = IdToModeMap[0]; + Logger.Warning($"Unable to find game mode of id {id}!"); } } diff --git a/MiraAPI/GameModes/DefaultMode.cs b/MiraAPI/GameModes/DefaultMode.cs deleted file mode 100644 index 83c11ba..0000000 --- a/MiraAPI/GameModes/DefaultMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MiraAPI.GameModes; - -public class DefaultMode : CustomGameMode -{ - public override string Name => "Default"; - public override string Description => "Default Among Us GameMode"; - public override int Id => 0; -} diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs new file mode 100644 index 0000000..3e0d2f7 --- /dev/null +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.GameOptions; +using MiraAPI.Networking; +using MiraAPI.Patches.GameModes; +using MiraAPI.Patches.Options; +using MiraAPI.Translation; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using Reactor.Localization.Utilities; +using Reactor.Networking.Attributes; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.ProBuilder; +using Object = UnityEngine.Object; + +namespace MiraAPI.GameModes; + +/// +/// The game mode option. +/// +[HarmonyPatch] +public static class GameModeOption +{ + /// + /// Gets the current index of the Game Mode Option + /// For the value as an , see . + /// + public static int Value + { + get => + OptionBehaviour != null + ? OptionBehaviour.GetInt() + : _lastValue; + private set + { + _lastValue = value; + if (OptionBehaviour == null) + return; + OptionBehaviour.Value = value; + OptionBehaviour.UpdateValue(); + _lastValue = value; + } + } + internal static StringOption OptionBehaviour { get; set; } = null!; + + internal static int _lastValue; + internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("Gamemode"); + internal static readonly StringNames CustomName = MiraLocaleManager.GetOrCreateLocaleString("Custom"); + internal static readonly Dictionary Values = new() + { + [0] = MiraLocaleManager.GetOrCreateLocaleString("MiraApi.Gamemode.Classic"), + }; + + internal static void AddOption(AbstractGameMode mode) + { + if (!Values.ContainsKey(mode.ID)) + Values.Add(mode.ID, MiraLocaleManager.GetOrCreateLocaleString(mode.Name)); + } + /*[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] + [HarmonyPostfix] + private static void CreateSettingsPatch(GameOptionsMenu __instance) + { + if (MenuState.Instance.CurrentModIdx != 0 || GameManager.Instance.IsHideAndSeek() || CustomGameModeManager.ActiveMode == null) + { + return; + } + + var num = 0.713f; + + foreach (var category in __instance.settingsContainer.GetComponentsInChildren()) + { + if (category) + category.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); + } + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); + categoryHeaderMasked.SetHeader(CustomName, 20); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); + OptionBehaviour = Object.Instantiate( + __instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + __instance.settingsContainer); + num -= 0.63f; + OptionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + OptionBehaviour.SetClickMask(__instance.ButtonClickMask); + StringGameSetting setting = ScriptableObject.CreateInstance(); + setting.Type = OptionTypes.MultipleChoice; + setting.Title = GamemodeName; + setting.Index = _lastValue; + setting.Values = new Il2CppStructArray([Values[0]]); + OptionBehaviour.SetUpFromData(setting, 20); + Set(_lastValue); + OptionBehaviour.TitleText.fontSize = 3; + foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) + { + optionBehaviour.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); + } + __instance.Children.Add(OptionBehaviour); + for (var i = 1; i < Values.Count; i++) + OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); + __instance.scrollBar.SetYBoundsMax(__instance.scrollBar.GetYBounds().max + 1); + }*/ + + internal static void Set(int val) + { + Value = val; + var previousMode = CustomGameModeManager.ActiveMode; + CustomGameModeManager.GetAndSetGameMode(); + HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); + var gm = CustomGameModeManager.ActiveMode!; + if (gm != previousMode) + { + ModdedOptionsManager.AddSettingsChangeMessage( + HudManager.Instance.Notifier, + GamemodeName, + gm.ColoredName, + new Color(0.7333f, 0.7333f, 0.7333f, 1), + gm.TmpIcon, + true); + if (MenuState.Instance) + { + // should force reset roles for gamemodes properly + foreach (var roleMenu in MenuState.Instance.QueuedRoleMenuRefresh) + { + MenuState.Instance.QueuedRoleMenuRefresh[roleMenu.Key] = true; + } + } + } + // could make Values a dict of AbstractGameMode too + } + + [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] + [HarmonyPrefix] + private static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option) + { + if (OptionBehaviour.Equals(option)) + { + Info($"Game mode changed to {option.GetInt()}"); + RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt()); + if (GameSettingMenu.Instance && CustomGameModeManager.ActiveMode != null) + { + GameOptionsMenuPatch.ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance); + GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); + } + return false; + } + return true; + } + + [MethodRpc((uint) MiraRpc.SyncGamemodeOption)] + internal static void RpcSyncGamemode(PlayerControl host, int data) + { + Set(data); + } +} diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs new file mode 100644 index 0000000..cd99eae --- /dev/null +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -0,0 +1,534 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using AmongUs.Data; +using AmongUs.GameOptions; +using HarmonyLib; +using InnerNet; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented; +using MiraAPI.HnsReimplemented.Options; +using MiraAPI.PluginLoading; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using PowerTools; +using UnityEngine; + +namespace MiraAPI.GameModes; + +/// +/// The vanilla Hide and Seek game mode, ported to Mira. +/// +[MiraIgnore] +public class HideAndSeekMode : AbstractGameMode +{ + /// + public override string Name => "MiraApi.Gamemode.HideAndSeek"; + + /// + public override string Description => "MiraApi.Gamemode.HideAndSeek.Description"; + + // TODO: Once the Hide n Seek port is ready for release, we will remove this. + public override bool HideMode => !MiraApiPlugin.IsDevBuild; + + public override Color Color { get; } = new Color32(255, 88, 90, 255); + public override LoadableAsset? Icon => MiraAssets.HnSGamemodeIcon; + public override bool CanReport(DeadBody body) => false; + public override bool ShouldShowSabotageMap(MapBehaviour map) => false; + public override bool ShowGameModeIntroCutscene => true; + public override bool GameModeBodyTypeOverride => true; + public override bool ShowNormalGameSettings => false; + public override bool ShowNormalRoleSettings => false; + public override float DefaultImpostorKillCooldown => 1f; + + public override bool ShowTaskBar => false; + public static int ImpostorPlayerID() + { + return OptionGroupSingleton.Instance.SelectedSeeker.Value; + } + + public static bool HasImpostorPlayerID() + { + return ImpostorPlayerID() > -1; + } + + public static bool ValidateImpostorPlayerID(List players) + { + return HasImpostorPlayerID() && players.Find((NetworkedPlayerInfo p) => (int)p.PlayerId == ImpostorPlayerID()) != null; + } + + public override void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) + { + runOriginal = false; + Il2CppSystem.Collections.Generic.List list = new(); + AmongUsClient.Instance.GetAllClients(list); + List list2 = list.ToArray() + .Where(c => c.Character != null && c.Character.Data != null && !c.Character.Data.Disconnected && + !c.Character.Data.IsDead).OrderBy(c => c.Id).Select(c => c.Character.Data) + .ToList(); + + foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) + { + if (networkedPlayerInfo.Object != null && networkedPlayerInfo.Object.isDummy) + { + list2.Add(networkedPlayerInfo); + } + } + IGameOptions currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; + int adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count); + AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, Math.Max(adjustedNumImpostors, 1), RoleTypes.Impostor); + AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Engineer); + } + + public static void AssignRolesForTeam( + List players, + IGameOptions opts, + RoleTeamTypes team, + int teamMax, + RoleTypes defaultRole) + { + Error($"Hide And Seek Mode - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); + int num = 0; + IRoleOptionsCollection roleOptions = opts.RoleOptions; + var source = RoleManager.Instance.AllRoles.ToArray() + .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) && + CustomRoleUtils.CanSpawnOnCurrentMode(role)); + var assignmentData = source.Where(x => !x.IsDead).Select(role => + new RoleManager.RoleAssignmentData( + role, + roleOptions.GetNumPerGame(role.Role), + roleOptions.GetChancePerGame(role.Role))).ToList(); + var source2 = CustomRoleUtils.GetPossibleRoles(assignmentData, x => x.Chance == 100); + var guaranteedRoles = source.Where(x => source2.Contains(((ushort)x.Role, 100))); + List list = new List(); + if (team == RoleTeamTypes.Crewmate) + { + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) => + new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) + { + while (true) + { + RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; + int count = roleAssignmentData2.Count; + roleAssignmentData2.Count = count - 1; + if (count <= 0) + { + break; + } + + list.Add(roleAssignmentData.Role.Role); + } + } + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); + AssignRolesFromList(players, teamMax, list, ref num); + + var list2 = source.Where(x => !x.IsDead).Select(role => + new RoleManager.RoleAssignmentData( + role, + roleOptions.GetNumPerGame(role.Role), + roleOptions.GetChancePerGame(role.Role))).ToList(); + + list.Clear(); + foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2) + { + for (int i = 0; i < roleAssignmentData3.Count; i++) + { + if (HashRandom.Next(101) < roleAssignmentData3.Chance) + { + list.Add(roleAssignmentData3.Role.Role); + } + } + } + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Potential Assignment"); + + AssignRolesFromList(players, teamMax, list, ref num); + var basicRole = RoleTypes.Engineer; + while (list.Count < players.Count && list.Count + num < teamMax) + { + list.Add(basicRole); + } + + AssignRolesFromList(players, teamMax, list, ref num); + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Fallback Assignment"); + } + else if (team == RoleTeamTypes.Impostor) + { + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + var newImpostors = new List(); + // Specified Seeker + if (HasImpostorPlayerID() && + ValidateImpostorPlayerID(players) && + !AmongUsClient.Instance.IsGamePublic) + { + NetworkedPlayerInfo networkedPlayerInfo = players.ToArray() + .First(p => p.PlayerId == ImpostorPlayerID()); + players.Remove(networkedPlayerInfo); + newImpostors.Add(networkedPlayerInfo); + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); + } + // Random Seeker + else + { + int num2 = 0; + while (num2 < teamMax && players.Count > 0) + { + PseudoRandomList pseudoRandomList = new PseudoRandomList(AmongUsClient.Instance.GameId); + players.Do(x => pseudoRandomList.Add(x)); + for (int i = 0; i < GameData.RoundsPlayedInSession; i++) + { + pseudoRandomList.PickRandom(); + } + NetworkedPlayerInfo networkedPlayerInfo = pseudoRandomList.PickRandom(); + players.Remove(networkedPlayerInfo); + newImpostors.Add(networkedPlayerInfo); + num2++; + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); + } + } + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); + foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) => + new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) + { + while (true) + { + RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; + int count = roleAssignmentData2.Count; + roleAssignmentData2.Count = count - 1; + if (count <= 0) + { + break; + } + + list.Add(roleAssignmentData.Role.Role); + } + } + AssignRolesFromList(newImpostors, teamMax, list, ref num); + + var list2 = source.Where(x => !x.IsDead).Select(role => + new RoleManager.RoleAssignmentData( + role, + roleOptions.GetNumPerGame(role.Role), + roleOptions.GetChancePerGame(role.Role))).ToList(); + + list.Clear(); + foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2) + { + for (int i = 0; i < roleAssignmentData3.Count; i++) + { + if (HashRandom.Next(101) < roleAssignmentData3.Chance) + { + list.Add(roleAssignmentData3.Role.Role); + } + } + } + + AssignRolesFromList(newImpostors, teamMax, list, ref num); + var basicRole = RoleTypes.Impostor; + while (list.Count < newImpostors.Count && list.Count + num < teamMax) + { + list.Add(basicRole); + } + + AssignRolesFromList(newImpostors, teamMax, list, ref num); + } + } + + public static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned) + { + while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax) + { + int index = HashRandom.FastNext(roleList.Count); + RoleTypes roleType = roleList[index]; + roleList.RemoveAt(index); + int index2 = HashRandom.FastNext(players.Count); + players[index2].Object.RpcSetRole(roleType, false); + players.RemoveAt(index2); + rolesAssigned++; + } + } + + public override IEnumerator IntroCutscene(IntroCutscene __instance) + { + SoundManager.Instance.PlaySound(__instance.IntroStinger, false, 1f, null); + Logger.GlobalInstance.Info("IntroCutscene :: CoBegin() :: Game Mode: Hide and Seek (MiraAPI)", null); + __instance.LogPlayerRoleData(); + __instance.HideAndSeekPanels.SetActive(true); + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + __instance.CrewmateRules.SetActive(false); + __instance.ImpostorRules.SetActive(true); + } + else + { + __instance.CrewmateRules.SetActive(true); + __instance.ImpostorRules.SetActive(false); + } + + __instance.ImpostorName.gameObject.SetActive(true); + __instance.ImpostorTitle.gameObject.SetActive(true); + __instance.BackgroundBar.enabled = false; + __instance.TeamTitle.gameObject.SetActive(false); + var impostor = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(x => x.Data.Role.IsImpostor); + if (impostor == null) + { + Logger.GlobalInstance.Error("IntroCutscene :: CoBegin() :: impostor is NULL", null); + } + + GameManager.Instance.SetSpecialCosmetics(impostor); + if (impostor != null) + { + __instance.ImpostorName.text = impostor.Data.PlayerName; + } + else + { + __instance.ImpostorName.text = "???"; + } + + yield return new WaitForSecondsRealtime(0.1f); + if (impostor != null) + { + __instance.ImpostorTitle.text = impostor.Data.Role.GetRoleName(); + } + PoolablePlayer playerSlot = null; + if (impostor != null) + { + playerSlot = __instance.CreatePlayer(1, 1, impostor.Data, false); + playerSlot.SetBodyType(PlayerBodyTypes.Normal); + playerSlot.SetFlipX(false); + playerSlot.transform.localPosition = __instance.impostorPos; + playerSlot.transform.localScale = Vector3.one * __instance.impostorScale; + } + + yield return ShipStatus.Instance.CosmeticsCache.PopulateFromPlayers(); + yield return new WaitForSecondsRealtime(6f); + if (playerSlot != null) + { + playerSlot.gameObject.SetActive(false); + } + + __instance.HideAndSeekPanels.SetActive(false); + __instance.CrewmateRules.SetActive(false); + __instance.ImpostorRules.SetActive(false); + HnsMusicHandler.Instance.StartMusicWithIntro(); + var hideTimer = 10f; + + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + __instance.HideAndSeekTimerText.gameObject.SetActive(true); + PoolablePlayer poolablePlayer; + AnimationClip anim; + if (AprilFoolsMode.ShouldHorseAround()) + { + poolablePlayer = __instance.HorseWrangleVisualSuit; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); + anim = __instance.HnSSeekerSpawnHorseAnim; + __instance.HorseWrangleVisualPlayer.SetBodyType(PlayerBodyTypes.Normal); + __instance.HorseWrangleVisualPlayer.UpdateFromPlayerData( + PlayerControl.LocalPlayer.Data, + PlayerControl.LocalPlayer.CurrentOutfitType, + PlayerMaterial.MaskType.None, + false, + null, + false); + } + else if (AprilFoolsMode.ShouldLongAround()) + { + poolablePlayer = __instance.HideAndSeekPlayerVisual; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.LongSeeker); + anim = __instance.HnSSeekerSpawnLongAnim; + } + else + { + poolablePlayer = __instance.HideAndSeekPlayerVisual; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); + anim = __instance.HnSSeekerSpawnAnim; + } + + poolablePlayer.SetBodyCosmeticsVisible(false); + poolablePlayer.UpdateFromPlayerData( + PlayerControl.LocalPlayer.Data, + PlayerControl.LocalPlayer.CurrentOutfitType, + PlayerMaterial.MaskType.None, + false, + null, + false); + SpriteAnim component = poolablePlayer.GetComponent(); + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.ToggleName(false); + component.Play(anim, 1f); + while (hideTimer > 0f) + { + __instance.HideAndSeekTimerText.text = Mathf.RoundToInt(hideTimer).ToString(); + hideTimer -= Time.deltaTime; + yield return null; + } + } + else + { + HideAndSeekHudHelper.Instance.HideCountdown = hideTimer; + if (AprilFoolsMode.ShouldHorseAround()) + { + if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnHorseInGameAnim); + } + } + else if (AprilFoolsMode.ShouldLongAround()) + { + if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnLongInGameAnim); + } + } + else if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnAnim); + impostor.cosmetics.SetBodyCosmeticsVisible(false); + } + } + ShipStatus.Instance.StartSFX(); + HnsMusicHandler.Instance.OnGameStart(); + HnsDangerMeter.Instance.OnGameStart(); + UnityEngine.Object.Destroy(__instance.gameObject); + } + public override void Initialize() + { + deadPlayerCount = 0; + ShipStatus.Instance.BreakEmergencyButton(); + PlayerControl.LocalPlayer.SetKillTimer(0.01f); + if (HudManager.InstanceExists) + { + HudManager.Instance.gameObject.AddComponent(); + HudManager.Instance.gameObject.AddComponent(); + HudManager.Instance.gameObject.AddComponent(); + } + } + public override MapOptions GetMapOptions() + { + MapOptions mapOptions = new MapOptions + { + Mode = MapOptions.Modes.Normal, + }; + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && HideAndSeekHudHelper.Instance.SeekerAdminMapEnabled(PlayerControl.LocalPlayer)) + { + mapOptions.Mode = MapOptions.Modes.CountOverlay; + mapOptions.AllowMovementWhileMapOpen = true; + mapOptions.IncludeDeadBodies = false; + mapOptions.ShowLivePlayerPosition = false; + } + return mapOptions; + } + + public override void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) + { + runOriginal = false; + if (Helpers.GetAlivePlayers().Count(x => x.Data.Role.IsImpostor) == 0) + { + instance.Manager.RpcEndGame(GameOverReason.ImpostorDisconnect, !DataManager.Player.Ads.HasPurchasedAdRemoval); + } + if (Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor) != 0) + { + if (HideAndSeekHudHelper.Instance.AllTimersExpired()) + { + instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_CrewmatesByTimer, !DataManager.Player.Ads.HasPurchasedAdRemoval); + } + return; + } + instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_ImpostorsByKills, !DataManager.Player.Ads.HasPurchasedAdRemoval); + } + + public override PlayerBodyTypes GetBodyType(PlayerControl player) + { + if (player == null || player.Data == null || player.Data.Role == null) + { + if (AprilFoolsMode.ShouldHorseAround()) + { + return PlayerBodyTypes.Horse; + } + if (AprilFoolsMode.ShouldLongAround()) + { + return PlayerBodyTypes.Long; + } + return PlayerBodyTypes.Normal; + } + else if (AprilFoolsMode.ShouldHorseAround()) + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.Normal; + } + return PlayerBodyTypes.Horse; + } + else if (AprilFoolsMode.ShouldLongAround()) + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.LongSeeker; + } + return PlayerBodyTypes.Long; + } + else + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.Seeker; + } + return PlayerBodyTypes.Normal; + } + } + public override void UpdateTaskPanel(TaskPanelBehaviour instance) + { + instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f) + ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f) + : Vector3.zero; + Vector3 vector = instance.background.sprite.bounds.extents; + vector.y = -vector.y; + vector = vector.Mul(instance.background.transform.localScale); + instance.background.transform.localPosition = vector; + Vector3 vector2 = instance.tab.sprite.bounds.extents; + vector2 = vector2.Mul(instance.tab.transform.localScale); + vector2.y = -vector2.y; + vector2.x += vector.x * 2f; + instance.tab.transform.localPosition = vector2; + if (GameManager.Instance == null) + { + return; + } + + var yPos = 1.6f; + var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x; + instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z); + instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z); + if (instance.open) + { + instance.timer = Mathf.Min(1f, instance.timer + Time.deltaTime / instance.animationTimeSeconds); + } + else + { + instance.timer = Mathf.Max(0f, instance.timer - Time.deltaTime / instance.animationTimeSeconds); + } + + Vector3 relativePos = new( + Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), + yPos, + instance.openPosition.z); + instance.transform.localPosition = + AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); + } + private int deadPlayerCount; + public override void OnPlayerDeath(PlayerControl player, bool assignGhostRole) + { + base.OnPlayerDeath(player, assignGhostRole); + HudManager.Instance.NotifyOfDeath(); + var popup = GameManagerCreator.Instance.HideAndSeekManagerPrefab.DeathPopupPrefab; + deadPlayerCount++; + var item = UnityEngine.Object.Instantiate(popup, HudManager.Instance.transform.parent); + item.Show(player, deadPlayerCount); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index 7fbb6b3..383cb49 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Translation; namespace MiraAPI.GameOptions.Attributes; @@ -13,7 +14,7 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values); + var opt = new ModdedEnumOption(Title.Translate(), (int)(value ?? 0), enumType, values); return opt; } diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index 6ff98dc..5a30db0 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Translation; using MiraAPI.Utilities; namespace MiraAPI.GameOptions.Attributes; @@ -22,7 +23,7 @@ public class ModdedNumberOptionAttribute( internal override IModdedOption CreateOption(object? value, PropertyInfo property) { return new ModdedNumberOption( - Title, + Title.Translate(), (float)(value ?? min + increment), min, max, diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index d73728f..98194f8 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -8,8 +8,9 @@ namespace MiraAPI.GameOptions.Attributes; /// /// The option title. /// Optional parameter to specify a role Type. +/// Optional parameter to specify a game mode Type. [AttributeUsage(AttributeTargets.Property)] -public abstract class ModdedOptionAttribute(string title, Type? roleType = null) : PropertyOptionAttribute +public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : PropertyOptionAttribute { internal IModdedOption? HolderOption { get; set; } @@ -21,7 +22,12 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null) /// /// Gets the role type of the option. /// - protected Type? RoleType => roleType; + protected Type? RoleType { get; private set; } = roleType; + + /// + /// Gets the game mode type of the option. + /// + protected Type? ModeType { get; private set; } = modeType; internal abstract IModdedOption? CreateOption(object? value, PropertyInfo property); } diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs index 2fccf1f..838f8b6 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Translation; namespace MiraAPI.GameOptions.Attributes; @@ -13,7 +14,7 @@ public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(t { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false)); + var toggleOpt = new ModdedToggleOption(Title.Translate(), (bool)(value ?? false)); return toggleOpt; } diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 91aa1ee..601287a 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Reflection; using BepInEx.Configuration; using HarmonyLib; +using Il2CppInterop.Runtime.Injection; +using MiraAPI.GameModes; using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Networking; @@ -27,6 +29,8 @@ public static class ModdedOptionsManager private static readonly Dictionary TypeToGroup = []; internal static readonly Dictionary CreatedPlayerOptions = []; + internal static readonly Dictionary CreatedStringOptions = []; + internal static readonly Dictionary> GameModeOptionGroups = []; internal static readonly Dictionary ModdedOptions = []; internal static readonly List Groups = []; @@ -35,7 +39,7 @@ public static class ModdedOptionsManager public static void AddSettingsChangeMessage(NotificationPopper notif, StringNames key, string value, Color textColor, TMP_SpriteAsset? sprite, bool playSound = true) { - var item = string.Empty; + string item; var text = textColor.ToTextColor(); if (sprite != null) { @@ -63,6 +67,29 @@ public static class ModdedOptionsManager notif.SettingsChangeMessageLogic(key, item, playSound); } + internal static bool RegisterGroup(Type type) + { + if (Activator.CreateInstance(type) is not AbstractOptionGroup group) + { + return false; + } + + if (TypeToGroup.ContainsKey(type)) + { + Logger.Error($"Group {type.Name} already exists."); + return false; + } + + Groups.Add(group); + TypeToGroup.Add(type, group); + + typeof(OptionGroupSingleton<>).MakeGenericType(type) + .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! + .SetValue(null, group); + + return true; + } + internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo) { if (Activator.CreateInstance(type) is not AbstractOptionGroup group) @@ -78,6 +105,17 @@ public static class ModdedOptionsManager Groups.Add(group); TypeToGroup.Add(type, group); + if (group.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true) + { + if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList)) + { + oldList.Add(group); + } + else + { + GameModeOptionGroups.Add(group.OptionableType, new List() { group }); + } + } pluginInfo.InternalOptionGroups.Add(group); typeof(OptionGroupSingleton<>).MakeGenericType(type) diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs index 3db6566..6309649 100644 --- a/MiraAPI/GameOptions/ModifierOptionGroup.cs +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -1,8 +1,10 @@ using System.Linq; +using MiraAPI.PluginLoading; namespace MiraAPI.GameOptions; #pragma warning disable CA1852 +[MiraIgnore] internal class ModifierOptionGroup : AbstractOptionGroup #pragma warning restore CA1852 { diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 30809bd..4e512af 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Networking; +using MiraAPI.Translation; using Reactor.Localization.Utilities; using UnityEngine; using Object = UnityEngine.Object; @@ -37,8 +38,8 @@ public class ModdedEnumOption : ModdedOption data.Title = StringName; data.Type = global::OptionTypes.String; data.Values = values is null ? - Enum.GetNames(enumType).Select(CustomStringName.CreateAndRegister).ToArray() - : [.. values.Select(CustomStringName.CreateAndRegister)]; + Enum.GetNames(enumType).Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray() + : [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)]; data.Index = Value; } @@ -136,8 +137,8 @@ public class ModdedEnumOption : ModdedOption where T : Enum data.Title = StringName; data.Type = global::OptionTypes.String; data.Values = values is null ? - Enum.GetNames(typeof(T)).Select(CustomStringName.CreateAndRegister).ToArray() - : [.. values.Select(CustomStringName.CreateAndRegister)]; + Enum.GetNames(typeof(T)).Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray() + : [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)]; data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo); } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index ab92a9e..2291291 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -1,7 +1,8 @@ -using System; +using System; using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; +using MiraAPI.Translation; using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; using UnityEngine; @@ -76,10 +77,10 @@ public abstract class ModdedOption : IModdedOption protected ModdedOption(string title, T defaultValue, bool includeInPreset = true) { Id = ModdedOptionsManager.NextId; - Title = title; + Title = title.Translate(); DefaultValue = defaultValue; Value = defaultValue; - StringName = CustomStringName.CreateAndRegister(Title); + StringName = MiraLocaleManager.GetOrCreateLocaleString(Title); Visible = () => true; IncludeInPreset = includeInPreset; } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs index db397bd..789368f 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs @@ -2,6 +2,7 @@ using System.Linq; using Il2CppSystem.Collections.Generic; using MiraAPI.Networking; +using MiraAPI.Translation; using MiraAPI.Utilities; using UnityEngine; using Object = UnityEngine.Object; @@ -65,7 +66,7 @@ public class ModdedPlayerOption : ModdedOption playerOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; playerOption.Title = StringName; - playerOption.TitleText.text = TranslationController.Instance.GetString(StringName); + playerOption.TitleText.text = Title.Translate(); playerOption.Values = Values; playerOption.Value = Value; playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs new file mode 100644 index 0000000..630a9ec --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -0,0 +1,177 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.Networking; +using MiraAPI.Translation; +using Reactor.Localization.Utilities; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.GameOptions.OptionTypes; + +/// +/// An option for selecting an enum value. +/// +public class ModdedStringOption : ModdedOption +{ + /// + /// Gets or sets the string values of the option. + /// + public string[] Values { get; set; } + + /// + /// Gets or sets the String Name values of the option. + /// + public StringNames[] StringNameValues { get; set; } + + /// + /// Gets the string's index. + /// + /// The option's string value. + /// The option's value in the index. + public int GetIndex(string value, int location) + { + if (Values.Contains(value)) + { + return Values.ToList().IndexOf(value); + } + Info($"Cannot find a valid index for {value} in {location}. Returning 0 instead. Values: {string.Join(", ", Values)}"); + return 0; + } + + /// + /// Adds a value to the option. + /// + /// The option's string value. + /// The additional value's index on the list. + public void RegisterExtraValue(string value, int index = -1) + { + if (!Values.Contains(value)) + { + if (index <= -1) + index = Values.Length; + + var newVals = Values.ToList(); + newVals.Insert(index, value); + Values = newVals.ToArray(); + var newStrVals = StringNameValues.ToList(); + newStrVals.Insert(index, MiraLocaleManager.GetOrCreateLocaleString(value)); + StringNameValues = newStrVals.ToArray(); + } + + if (Data != null) + { + var data = (StringGameSetting)Data; + data.Values = StringNameValues; + + data.Index = GetIndex(Value, 1); + if (OptionBehaviour != null) + { + var strOpt = OptionBehaviour.TryCast(); + if (strOpt != null) + { + strOpt.Values = StringNameValues; + strOpt.Value = GetIndex(Value, 2); + Warning("StringOption is valid"); + } + else + { + Error("StringOption is null."); + } + Warning("OptionBehaviour is valid"); + } + else + { + Error("OptionBehaviour is null."); + } + Warning("Data is valid"); + } + else + { + Error("Data is null."); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// The default value as a string. + /// An option list of string values to use. + /// Whether to include this option in the preset or not. + public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset=true) : base(title, defaultValue, includeInPreset) + { + Values = values; + Data = ScriptableObject.CreateInstance(); + var data = (StringGameSetting)Data; + + data.Title = StringName; + data.Type = global::OptionTypes.String; + StringNameValues = values.Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray(); + data.Values = StringNameValues; + + data.Index = GetIndex(Value, 3); + } + + /// + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) + { + var stringOption = Object.Instantiate(stringOpt, container); + stringOption.name = + $"{ParentMod!.OptionsTitleText}.StringOption.{TranslationController.Instance.GetString(StringName)}"; + + stringOption.SetUpFromData(Data, 20); + stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + + // SetUpFromData method doesnt work correctly so we must set the values manually + stringOption.Title = StringName; + stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray(0); + stringOption.Value = GetIndex(Value, 4); + + OptionBehaviour = stringOption; + ModdedOptionsManager.CreatedStringOptions.TryAdd(stringOption, this); + + return stringOption; + } + + /// + public override float GetFloatData() + { + return Array.IndexOf(Values, Value); + } + + /// + public override NetData GetNetData() + { + return new NetData(Id, Encoding.Unicode.GetBytes(Convert.ToString(Value, NumberFormatInfo.InvariantInfo))); + } + + /// + public override void HandleNetData(byte[] data) + { + SetValue(Encoding.Unicode.GetString(data)); + } + + /// + public override string GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) + { + return Values[optionBehaviour.GetInt()]; + } + + /// + protected override void OnValueChanged(string newValue) + { + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(GetIndex(newValue, 5)), false); + if (!OptionBehaviour) + { + return; + } + + if (OptionBehaviour is StringOption opt) + { + opt.Value = GetIndex(newValue, 5); + } + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 55b69b9..348f853 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -1,5 +1,6 @@ using System; using MiraAPI.Networking; +using MiraAPI.Translation; using UnityEngine; using Object = UnityEngine.Object; @@ -35,7 +36,7 @@ public class ModdedToggleOption : ModdedOption toggleOption.SetUpFromData(Data, 20); toggleOption.Title = StringName; - toggleOption.TitleText.text = Title; + toggleOption.TitleText.text = Title.Translate(); toggleOption.CheckMark.enabled = Value; toggleOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; diff --git a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs new file mode 100644 index 0000000..b708148 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs @@ -0,0 +1,287 @@ +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; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HideAndSeekHudHelper Instance { get; private set; } + + public AudioClip FinalHideAlertSfx; + + public AudioClip FinalHideCountdownSfx; + + private AudioClip TaskFinishedSound; + + private const int SECONDS_TO_BEEP = 10; + + private const float SECONDS_TO_SET_DIRTY = 1f; + + private HideAndSeekTimerBar timerBar; + + private float totalHideTime = float.MaxValue; + + private float currentHideTime = float.MaxValue; + + private float totalFinalHideTime = float.MaxValue; + + private float currentFinalHideTime = float.MaxValue; + + private float secondsSinceLastSetDirty; + private Coroutine beepCoroutine; + + private void Awake() + { + Instance = this; + } + + public bool IsFinalCountdown + { + get { return currentHideTime <= 0f; } + } + + public bool SeekerAdminMapEnabled(PlayerControl player) + { + int item = Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor); + return !player.inVent && player.Data != null && player.Data.Role != null && + ((!player.inVent && player.Data.Role.IsImpostor && IsFinalCountdown && + OptionGroupSingleton.Instance.FinalHideSeekMap.Value) || + (player.Data.Role.IsImpostor && + item <= + (GameData.Instance.PlayerCount - 1) / 3)); + } + + public void OnTaskComplete(float timeDeduction) + { + if (timerBar != null) + { + timerBar.TaskComplete(); + } + + AdjustEscapeTimer(timeDeduction, true); + } + + public float GetTotalRoundTime() + { + float escapeTime = OptionGroupSingleton.Instance.HidingTime.Value; + float finalCountdownTime = OptionGroupSingleton.Instance.FinalHideTime.Value; + return escapeTime + finalCountdownTime; + } + + public float GetTotalTimeRemaining() + { + return currentHideTime + currentFinalHideTime; + } + + public float GetRoundTimeElapsed() + { + return GetTotalRoundTime() - GetTotalTimeRemaining(); + } + + public void Start() + { + totalHideTime = OptionGroupSingleton.Instance.HidingTime.Value; + currentHideTime = totalHideTime; + totalFinalHideTime = OptionGroupSingleton.Instance.FinalHideTime.Value; + currentFinalHideTime = totalFinalHideTime; + if (timerBar != null) + { + Object.Destroy(timerBar); + } + + FinalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX; + FinalHideCountdownSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideCountdownSFX; + TaskFinishedSound = GameManagerCreator.Instance.HideAndSeekManagerPrefab.TaskFinishedSound; + timerBar = Object.Instantiate( + GameManagerCreator.Instance.HideAndSeekManagerPrefab.TimerBarPrefab, + HudManager.Instance.transform.parent); + } + + public void OnGameEnd() + { + if (timerBar != null) + { + Object.Destroy(timerBar.gameObject); + } + + if (beepCoroutine != null) + { + StopCoroutine(beepCoroutine); + } + + beepCoroutine = null!; + } + private float taskDirtyTimer; + public float HideCountdown { get; set; } + + public void LateUpdate() + { + if (!HudManager.InstanceExists) + { + return; + } + if (HideCountdown > 0f && taskDirtyTimer > 0.25f) + { + float num = taskDirtyTimer; + taskDirtyTimer = 0f; + if (!PlayerControl.LocalPlayer) + { + HudManager.Instance.TaskPanel.SetTaskText(string.Empty); + return; + } + NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data; + if (data == null) + { + return; + } + bool flag = data.Role != null && data.Role.IsImpostor; + HudManager.Instance.tasksString.Clear(); + if (PlayerControl.LocalPlayer.myTasks == null || PlayerControl.LocalPlayer.myTasks.Count == 0) + { + HudManager.Instance.tasksString.Append("None"); + } + else + { + for (int i = 0; i < PlayerControl.LocalPlayer.myTasks.Count; i++) + { + PlayerTask playerTask = PlayerControl.LocalPlayer.myTasks[i]; + if (playerTask) + { + if (playerTask.TaskType == TaskTypes.FixComms && !flag) + { + HudManager.Instance.tasksString.Clear(); + playerTask.AppendTaskText(HudManager.Instance.tasksString); + break; + } + playerTask.AppendTaskText(HudManager.Instance.tasksString); + } + } + if (data.Role != null) + { + data.Role.AppendTaskHint(HudManager.Instance.tasksString); + } + if (HideCountdown > 0f) + { + HideCountdown -= num; + HudManager.Instance.tasksString.Append("\n\n" + ((int)HideCountdown)); + } + HudManager.Instance.tasksString.TrimEnd(); + } + HudManager.Instance.TaskPanel.SetTaskText(HudManager.Instance.tasksString.ToString()); + } + } + public void FixedUpdate() + { + secondsSinceLastSetDirty += Time.fixedDeltaTime; + if (IsFinalCountdown) + { + AdjustFinalEscapeTimer(Time.fixedDeltaTime); + return; + } + + AdjustEscapeTimer(Time.fixedDeltaTime, false); + } + + public void OnDestroy() + { + if (timerBar != null) + { + Object.Destroy(timerBar); + } + } + + private void OnFinalCountdownTriggered() + { + foreach (PlayerControl playerControl in PlayerControl.AllPlayerControls) + { + if (!playerControl.Data.Role.IsImpostor && !playerControl.Data.IsDead) + { + playerControl.ClearTasks(); + PlayerTask.GetOrCreateTask(playerControl, 0).Text = + DestroyableSingleton.Instance.GetString(StringNames.HideActionButton); + } + } + + if (!PlayerControl.LocalPlayer.Data.IsDead && Minigame.Instance != null) + { + Minigame instance = Minigame.Instance; + if (instance != null) + { + instance.ForceClose(); + } + } + + timerBar.StartFinalHide(); + SoundManager.Instance.PlaySound(FinalHideAlertSfx, false, 1f, null); + DestroyableSingleton.Instance.SetAlertOverlay(true); + } + + private void AdjustEscapeTimer(float timeDeduction, bool forceDirty) + { + float num = currentHideTime; + currentHideTime -= timeDeduction; + currentHideTime = Mathf.Max(currentHideTime, 0f); + if (currentHideTime <= 10f && beepCoroutine == null) + { + beepCoroutine = StartCoroutine(BeepAlmostEverySecond().WrapToIl2Cpp()); + } + + if (num > 0f && currentHideTime <= 0f) + { + OnFinalCountdownTriggered(); + } + + timerBar.UpdateTimer(currentHideTime, totalHideTime); + if (forceDirty || secondsSinceLastSetDirty > 1f) + { + secondsSinceLastSetDirty = 0f; + } + } + + [HideFromIl2Cpp] + private IEnumerator BeepAlmostEverySecond() + { + while (!IsFinalCountdown) + { + float num = currentHideTime / 10f; + float pitch = 1.5f - num / 2f; + SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch, null); + yield return new WaitForSeconds(1f); + } + + yield return Effects.Wait(currentFinalHideTime - 10f); + while (currentFinalHideTime > 0f) + { + float num2 = currentFinalHideTime / 10f; + float pitch2 = 1.5f - num2 / 2f; + SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch2, null); + yield return new WaitForSeconds(1f); + } + + yield break; + } + + private void AdjustFinalEscapeTimer(float timeDeduction) + { + currentFinalHideTime -= timeDeduction; + currentFinalHideTime = Mathf.Max(currentFinalHideTime, 0f); + timerBar.UpdateTimer(currentFinalHideTime, totalFinalHideTime); + if (secondsSinceLastSetDirty > 1f) + { + secondsSinceLastSetDirty = 0f; + } + } + + public bool AllTimersExpired() + { + return currentHideTime <= 0f && currentFinalHideTime <= 0f; + } +} diff --git a/MiraAPI/HnsReimplemented/HnsDangerMeter.cs b/MiraAPI/HnsReimplemented/HnsDangerMeter.cs new file mode 100644 index 0000000..d36c198 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HnsDangerMeter.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HnsDangerMeter Instance; + + private void Awake() + { + Instance = this; + } + + private DangerMeter dangerMeter; + + private List impostors = []; + + private float scaryMusicDistance; + + private float veryScaryMusicDistance; + + private float dangerLevel1; + + private float dangerLevel2; + + private bool firstMusicActivation; + + private float firstCrossfadeCountdown; + + public void FixedUpdate() + { + PlayerControl localPlayer = PlayerControl.LocalPlayer; + if (impostors == null || localPlayer == null) + { + return; + } + + if (impostors.Count <= 0) + { + return; + } + + float num = float.MaxValue; + foreach (PlayerControl playerControl in impostors) + { + if (!(playerControl == null)) + { + float sqrMagnitude = (playerControl.transform.position - localPlayer.transform.position).sqrMagnitude; + if (sqrMagnitude < scaryMusicDistance && num > sqrMagnitude) + { + num = sqrMagnitude; + } + } + } + + if (HideAndSeekHudHelper.Instance.HideCountdown > 0f) + { + dangerLevel1 = 0f; + dangerLevel2 = 0f; + } + else + { + if (firstMusicActivation) + { + firstMusicActivation = false; + firstCrossfadeCountdown = 3f; + HnsMusicHandler.Instance.SetMusicCrossfadeSpeed(0.6f); + } + + if (firstCrossfadeCountdown > 0f) + { + firstCrossfadeCountdown -= Time.deltaTime; + if (firstCrossfadeCountdown <= 0f) + { + HnsMusicHandler.Instance.SetMusicCrossfadeSpeed(5f); + } + } + + dangerLevel1 = Mathf.Clamp01( + (scaryMusicDistance - num) / (scaryMusicDistance - veryScaryMusicDistance)); + dangerLevel2 = Mathf.Clamp01((veryScaryMusicDistance - num) / veryScaryMusicDistance); + } + + UpdateDangerMeter(); + UpdateDangerMusic(); + } + + private void UpdateDangerMusic() + { + PlayerControl localPlayer = PlayerControl.LocalPlayer; + if (localPlayer != null && localPlayer.Data != null && localPlayer.Data.IsDead) + { + HnsMusicHandler.Instance.SetTaskState(false); + HnsMusicHandler.Instance.ResetMusic(); + return; + } + + HnsMusicHandler.Instance.SetMusicValues(dangerLevel1, dangerLevel2); + } + + private void UpdateDangerMeter() + { + if (dangerMeter == null) + { + return; + } + + dangerMeter.SetDangerValue(dangerLevel1, dangerLevel2); + } + + public void OnGameStart() + { + firstMusicActivation = true; + if (!PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + dangerMeter = HudManager.Instance.DangerMeter; + dangerMeter.gameObject.SetActive(true); + } + + impostors = Helpers.GetAlivePlayers().Where(x => x.Data.Role.IsImpostor).ToList(); + + var baseSpeed = OptionGroupSingleton.Instance.PlayerSpeed.Value; + scaryMusicDistance = 55f * + baseSpeed; + veryScaryMusicDistance = 15f * + baseSpeed; + if (scaryMusicDistance < veryScaryMusicDistance) + { + float num = veryScaryMusicDistance; + float num2 = scaryMusicDistance; + scaryMusicDistance = num; + veryScaryMusicDistance = num2; + } + } +} diff --git a/MiraAPI/HnsReimplemented/HnsMusicHandler.cs b/MiraAPI/HnsReimplemented/HnsMusicHandler.cs new file mode 100644 index 0000000..e3b7d68 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HnsMusicHandler.cs @@ -0,0 +1,237 @@ +using System.Collections.Generic; +using Amongus.GameModes.HideAndSeek; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HnsMusicHandler Instance; + + private void Awake() + { + Instance = this; + } + + private HideAndSeekMusicCollection musicCollection; + + private float lastMusicSyncTime; + + private bool isDoingTask; + + private float normalVolume; + + private float taskVolume; + + private float dangerLevel1Volume; + + private float dangerLevel2Volume; + + private AudioSource normalSource; + + private AudioSource taskSource; + + private AudioSource dangerLevel1Source; + + private AudioSource dangerLevel2Source; + + private float musicLerpSpeed = 5f; + + public void Start() + { + musicCollection = GameManagerCreator.Instance.HideAndSeekManagerPrefab.MusicCollection; + } + + public void OnGameStart() + { + InitMusic(); + ResetMusic(); + } + + public void OnDestroy() + { + ResetMusic(); + } + + private void InitMusic() + { + if (normalSource == null) + { + normalSource = + SoundManager.Instance.GetNamedSfxSource(musicNames[LogicHnSMusic.HideAndSeekMusicTrack.Normal]); + } + + normalSource.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + normalSource.clip = musicCollection.NormalMusic; + normalSource.loop = true; + if (taskSource == null) + { + taskSource = + SoundManager.Instance.GetNamedSfxSource(musicNames[LogicHnSMusic.HideAndSeekMusicTrack.Task]); + } + + taskSource.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + taskSource.volume = 0f; + taskSource.clip = musicCollection.TaskMusic; + taskSource.loop = true; + if (dangerLevel1Source == null) + { + dangerLevel1Source = + SoundManager.Instance.GetNamedSfxSource( + musicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1]); + } + + dangerLevel1Source.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + dangerLevel1Source.volume = 0f; + dangerLevel1Source.clip = musicCollection.DangerLevel1Music; + dangerLevel1Source.loop = true; + if (dangerLevel2Source == null) + { + dangerLevel2Source = + SoundManager.Instance.GetNamedSfxSource( + musicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2]); + } + + dangerLevel2Source.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + dangerLevel2Source.volume = 0f; + dangerLevel2Source.clip = musicCollection.DangerLevel2Music; + dangerLevel2Source.loop = true; + normalSource.Play(); + taskSource.Play(); + dangerLevel1Source.Play(); + dangerLevel2Source.Play(); + SyncMusic(); + } + + public void StartMusicWithIntro() + { + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + AudioClip clip = OptionGroupSingleton.Instance.HidingTime.Value <= 180f + ? musicCollection.ImpostorShortMusic + : musicCollection.ImpostorLongMusic; + if (AprilFoolsMode.ShouldHorseAround()) + { + clip = musicCollection.ImpostorRanchMusic; + } + + SoundManager.Instance.PlaySound(clip, true, 1f, SoundManager.Instance.MusicChannel); + } + } + + public void SetTaskState(bool isDoingTask) + { + isDoingTask = isDoingTask; + } + + public void FixedUpdate() + { + if (normalSource == null || taskSource == null || dangerLevel1Source == null || + dangerLevel2Source == null) + { + return; + } + + if (Time.unscaledTime > lastMusicSyncTime + 1f) + { + SyncMusic(); + } + + normalSource.volume = Mathf.Lerp( + normalSource.volume, + normalVolume, + Time.fixedDeltaTime * musicLerpSpeed); + taskSource.volume = Mathf.Lerp( + taskSource.volume, + taskVolume, + Time.fixedDeltaTime * musicLerpSpeed); + dangerLevel1Source.volume = Mathf.Lerp( + dangerLevel1Source.volume, + dangerLevel1Volume, + Time.fixedDeltaTime * musicLerpSpeed); + dangerLevel2Source.volume = Mathf.Lerp( + dangerLevel2Source.volume, + dangerLevel2Volume, + Time.fixedDeltaTime * musicLerpSpeed); + } + + private void SyncMusic() + { + taskSource.timeSamples = normalSource.timeSamples; + dangerLevel1Source.timeSamples = normalSource.timeSamples; + dangerLevel2Source.timeSamples = normalSource.timeSamples; + lastMusicSyncTime = Time.unscaledTime; + } + + public void ResetMusic() + { + SetMusicValues(0f, 0f); + } + + public void SetMusicCrossfadeSpeed(float lerpSpeed) + { + musicLerpSpeed = lerpSpeed; + } + + public void SetMusicValues(float dangerLevel1, float dangerLevel2) + { + if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + return; + } + + if (normalSource == null || taskSource == null || dangerLevel1Source == null || + dangerLevel2Source == null) + { + return; + } + + normalVolume = (isDoingTask ? 0f : 1f); + taskVolume = (isDoingTask ? 1f : 0f); + dangerLevel1Volume = 0f; + dangerLevel2Volume = 0f; + if (dangerLevel1 > 0f) + { + dangerLevel1Volume = dangerLevel1; + if (isDoingTask) + { + taskVolume = 1f - dangerLevel1; + } + else + { + normalVolume = 1f - dangerLevel1; + } + } + + if (dangerLevel2 > 0f) + { + dangerLevel2Volume = dangerLevel2; + dangerLevel1Volume = 1f - dangerLevel2; + } + } + + private readonly Dictionary musicNames = + new Dictionary + { + { + LogicHnSMusic.HideAndSeekMusicTrack.Normal, + "HnS_Music_Normal" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.Task, + "HnS_Music_Task" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1, + "HnS_Music_DangerLevel1" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2, + "HnS_Music_DangerLevel2" + }, + }; +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs new file mode 100644 index 0000000..cf1a7c6 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs @@ -0,0 +1,89 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsCrewmateOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Crewmates"; + + public ModdedNumberOption PlayerSpeed { get; set; } = new( + "Player Speed", + 1f, + 0.5f, + 3f, + 0.25f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption HidingTime { get; set; } = new( + "Hiding Time", + 200, + 160, + 300, + 20, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedNumberOption CrewmateVision { get; set; } = new( + "Crewmate Vision", + 0.6f, + 0.25f, + 1, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption MaxVentUses { get; set; } = new( + "Max Vent Uses", + 1, + 0, + 5, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption MaxTimeInVent { get; set; } = new( + "Max Time in Vent", + 3, + 1, + 10, + 1, + "#", + "#", + MiraNumberSuffixes.Seconds) + { + Visible = () => (int)OptionGroupSingleton.Instance.MaxVentUses.Value > 0, + }; + + public ModdedToggleOption FlashlightMode { get; set; } = new( + "Flashlight Mode", + true); + + public ModdedNumberOption CrewmateFlashlightSize { get; set; } = new( + "Crewmate Flashlight Size", + 0.35f, + 0.1f, + 0.5f, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier) + { + Visible = () => OptionGroupSingleton.Instance.FlashlightMode.Value, + }; + + public ModdedToggleOption ShowNames { get; set; } = new( + "Show Names", + false); +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs new file mode 100644 index 0000000..009b248 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs @@ -0,0 +1,52 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsFinalHideOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Final Hide"; + + public ModdedNumberOption FinalHideTime { get; set; } = new( + "Hiding Time", + 50, + 30, + 120, + 5, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedToggleOption FinalHidePings { get; set; } = new( + "Final Hide Pings", + true); + + public ModdedNumberOption FinalHideImpostorSpeed { get; set; } = new( + "Final Hide Impostor Speed", + 1.2f, + 1f, + 3, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption PingInterval { get; set; } = new( + "Ping Interval", + 6, + 3, + 10, + 5, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedToggleOption FinalHideSeekMap { get; set; } = new( + "Final Hide Seek Map", + true); +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs new file mode 100644 index 0000000..c0c61b5 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs @@ -0,0 +1,40 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsImpostorOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Impostors"; + + public ModdedPlayerOption SelectedSeeker { get; set; } = new( + "Forced Impostor"); + + public ModdedNumberOption ImpostorVision { get; set; } = new( + "Impostor Vision", + 0.6f, + 0.25f, + 1, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption ImpostorFlashlightSize { get; set; } = new( + "Impostor Flashlight Size", + 0.35f, + 0.1f, + 0.5f, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier) + { + Visible = () => OptionGroupSingleton.Instance.FlashlightMode.Value, + }; +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs new file mode 100644 index 0000000..7be2864 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs @@ -0,0 +1,43 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsTaskOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Tasks"; + + public ModdedNumberOption CommonTasks { get; set; } = new( + "# Common Tasks", + 1, + 0, + 4, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption LongTasks { get; set; } = new( + "# Long Tasks", + 1, + 0, + 4, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption ShortTasks { get; set; } = new( + "# Short Tasks", + 2, + 0, + 8, + 1, + "#", + "#", + MiraNumberSuffixes.None); +} diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 3ba3335..208c142 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -5,6 +5,7 @@ using MiraAPI.Events.Mira; using MiraAPI.Keybinds; using MiraAPI.LocalSettings; using MiraAPI.Patches; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Rewired; @@ -150,7 +151,7 @@ public abstract class CustomActionButton Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent); Button.name = Name + "Button"; - Button.OverrideText(Name.ToUpperInvariant()); + Button.OverrideText(Name.Translate().ToUpperInvariant()); Button.graphic.sprite = Sprite.LoadAsset(); diff --git a/MiraAPI/Hud/MiraHudHelper.cs b/MiraAPI/Hud/MiraHudHelper.cs new file mode 100644 index 0000000..b072dc8 --- /dev/null +++ b/MiraAPI/Hud/MiraHudHelper.cs @@ -0,0 +1,203 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using InnerNet; +using MiraAPI.LocalSettings; +using MiraAPI.Modifiers.ModifierDisplay; +using Reactor.Utilities.Attributes; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Hud; + +[RegisterInIl2Cpp] +public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static MiraHudHelper Instance { get; private set; } + public static GameObject ModifierDisplayObject; + public static GameObject VanillaMatchInfoButton; + public static bool ModifierDisplayOnRight; + public static GameObject ClonedChatButton; + public static GameObject ExtraUiTopRight; + public static GridArrange ExtraUiGrid; + public static AspectPosition ExtraUiAspectPos; + public static GameObject UiTopRight; + public static GridArrange UiGrid; + public static AspectPosition UiAspectPos; + public static GameObject SubmergedFloorButton; + public static SpriteRenderer SubmergedFloorButtonRenderer; + public static SpriteRenderer SubmergedFloorButtonRendererHover; + public void Awake() + { + Instance = this; + } + + public void Start() + { + MiraApiSettings.OldButtonScaleFactor = + LocalSettingsTabSingleton.Instance.TopRightButtonsFactorSlider.Value; + StartCoroutine(MiraApiSettings.CoResizeSettingsUI().WrapToIl2Cpp()); + } +#pragma warning disable S2325 + #pragma warning disable CA1822 + public void FixedUpdate() + { + if (!HudManager.InstanceExists || !PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data) + { + return; + } + + var instance = HudManager.Instance; + + CreateUiRow(instance); + CreateNewUiRow(instance); + AdjustModifierTab(); + + if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data || !PlayerControl.LocalPlayer.Data.Role || + !ShipStatus.Instance || + (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started && + !TutorialManager.InstanceExists)) + { + return; + } + UpdateSubmergedButtons(instance); + } + #pragma warning restore CA1822 + #pragma warning restore S2325 + + public static Vector3 BelowOptionPos = new(0.435f, 1.25f, 0f); + public static Vector3 FullTopPos = new(0.435f, 0.475f, 0f); + public static void CreateUiRow(HudManager instance) + { + if (!UiTopRight) + { + UiTopRight = instance.MapButton.transform.parent.gameObject; + + UiGrid = UiTopRight.AddComponent(); + UiAspectPos = UiTopRight.AddComponent(); + + UiGrid.Alignment = GridArrange.StartAlign.Left; + UiGrid.CellSize = new Vector2(0.85f, 0.85f); + UiGrid.MaxColumns = 6; + UiAspectPos.Alignment = AspectPosition.EdgeAlignments.RightTop; + UiGrid.Start(); + UiAspectPos.DistanceFromEdge = FullTopPos; + var mapButton = instance.MapButton.gameObject; + mapButton.GetComponent().Destroy(); + var settingsButton = instance.SettingsButton; + settingsButton.GetComponent().Destroy(); + VanillaMatchInfoButton = instance.MatchInfoButton.gameObject; + VanillaMatchInfoButton.GetComponent().Destroy(); + var oldPos = settingsButton.transform.localPosition; + settingsButton.transform.localPosition = new Vector3(oldPos.x, oldPos.y, -100); + var chatButton = instance.Chat.chatButton.gameObject; + ClonedChatButton = Object.Instantiate(chatButton, chatButton.transform.parent); + ClonedChatButton.SetActive(false); + instance.Chat.chatButtonAspectPosition = ClonedChatButton.GetComponent(); + chatButton.GetComponent().Destroy(); + var inactivePos = settingsButton.transform.GetChild(1).transform.localPosition; + var bg = settingsButton.transform.GetChild(2).gameObject; + var bgPos = bg.transform.localPosition; + var bgSprite = bg.GetComponent().sprite; + var activePos = settingsButton.transform.GetChild(3).transform.localPosition; + var selectedPos = settingsButton.transform.GetChild(4).transform.localPosition; + chatButton.transform.GetChild(2).transform.localPosition = inactivePos; + var chatBg = chatButton.transform.GetChild(3); + chatBg.transform.localPosition = bgPos; + chatBg.GetComponent().sprite = bgSprite; + chatButton.transform.GetChild(4).transform.localPosition = activePos; + chatButton.transform.GetChild(5).transform.localPosition = selectedPos; + var collider = chatButton.GetComponent(); + collider.size = new Vector2(0.4354f, 0.4003f); + collider.offset = new Vector2(0.0025f, 0.0254f); + if (FriendsListManager.InstanceExists && !TutorialManager.InstanceExists) + { + var listButton = FriendsListManager.Instance.FriendsListButton.transform.GetChild(0); + listButton.transform.SetParent(UiTopRight.transform, false); + FriendsListManager.Instance.FriendsListButton = listButton.GetComponent(); + listButton.GetComponent().Destroy(); + listButton.localPosition = new Vector3(0, 0, 0); + } + settingsButton.transform.SetAsLastSibling(); + chatButton.transform.SetParent(UiTopRight.transform, false); + instance.Chat.chatButton = chatButton.GetComponent(); + var iconContainer = new GameObject("iconContainer") + { + layer = LayerMask.NameToLayer("UI"), + }; + iconContainer.transform.SetParent(chatButton.transform, false); + iconContainer.transform.localPosition = new Vector3(0.1f, -0.1f, 0); + instance.Chat.chatNotifyDot.transform.SetParent(iconContainer.transform, false); + instance.Chat.chatNotifyDot = iconContainer.transform.GetChild(0).GetComponent(); + } + + if (UiTopRight && UiGrid) + { + var isChatButtonVisible = HudManager.Instance.Chat.isActiveAndEnabled; + instance.Chat.chatButton.gameObject.SetActive(isChatButtonVisible); + if (VanillaMatchInfoButton) + { + VanillaMatchInfoButton.SetActive(!GameSettingMenu.Instance && !Minigame.Instance); + } + } + } + public static void UpdateSubmergedButtons(HudManager instance) + { + if (ModCompatibility.IsSubmerged() && !SubmergedFloorButton && ExtraUiTopRight) + { + if (!SubmergedFloorButton && ExtraUiTopRight) + { + var transform = instance.MapButton.transform.parent.Find(instance.MapButton.name + "(Clone)"); + if (transform != null) + { + SubmergedFloorButton = transform.gameObject; + SubmergedFloorButton.transform.SetParent(ExtraUiTopRight.transform, false); + + SubmergedFloorButtonRenderer = + SubmergedFloorButton.transform.Find("Inactive").GetComponent(); + SubmergedFloorButtonRendererHover = + SubmergedFloorButton.transform.Find("Active").GetComponent(); + + MiraApiSettings.SetUpButtonPositions(); + } + } + } + } + + public static void AdjustModifierTab() + { + if (!ModifierDisplayObject && UiTopRight && ExtraUiTopRight && ModifierDisplayComponent.Instance) + { + ModifierDisplayObject = ModifierDisplayComponent.Instance?.gameObject ?? null!; + ModifierDisplayOnRight = !LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value; + if (ModifierDisplayOnRight) + { + ModifierDisplayObject.transform.SetParent(ExtraUiTopRight.transform, false); + ModifierDisplayObject.GetComponent().Destroy(); + var oldPos = ModifierDisplayObject.transform.GetChild(0).localPosition; + ModifierDisplayObject.transform.GetChild(0).localPosition = new Vector3(-1.1757f, -2.1633f, oldPos.z); + oldPos = ModifierDisplayObject.transform.GetChild(1).localPosition; + ModifierDisplayObject.transform.GetChild(1).localPosition = new Vector3(-0.45f, 0.3f, oldPos.z); + } + MiraApiSettings.SetUpButtonPositions(); + } + } + public static void CreateNewUiRow(HudManager instance) + { + if (!ExtraUiTopRight && UiTopRight) + { + ExtraUiTopRight = new GameObject("ExtraUiTopRight") + { + layer = UiTopRight.layer, + }; + ExtraUiTopRight.transform.SetParent(instance.MapButton.transform.parent.parent, false); + + ExtraUiGrid = ExtraUiTopRight.AddComponent(); + ExtraUiAspectPos = ExtraUiTopRight.AddComponent(); + + ExtraUiGrid.Alignment = GridArrange.StartAlign.Left; + ExtraUiGrid.CellSize = new Vector2(0.85f, 0.85f); + ExtraUiAspectPos.Alignment = AspectPosition.EdgeAlignments.RightTop; + ExtraUiAspectPos.DistanceFromEdge = BelowOptionPos; + ExtraUiGrid.Start(); + } + } +} diff --git a/MiraAPI/LocalSettings/ILocalSetting.cs b/MiraAPI/LocalSettings/ILocalSetting.cs index b51a13d..9bc4bbd 100644 --- a/MiraAPI/LocalSettings/ILocalSetting.cs +++ b/MiraAPI/LocalSettings/ILocalSetting.cs @@ -39,4 +39,9 @@ public interface ILocalSetting /// Whether it's the last on the row. /// The created setting's . GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); + + /// + /// Used to refresh the setting's information, usually for localization. + /// + void RefreshOption(); } diff --git a/MiraAPI/LocalSettings/LocalSettingsButton.cs b/MiraAPI/LocalSettings/LocalSettingsButton.cs index 5a1f371..747b1ce 100644 --- a/MiraAPI/LocalSettings/LocalSettingsButton.cs +++ b/MiraAPI/LocalSettings/LocalSettingsButton.cs @@ -1,4 +1,5 @@ using System; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; using TMPro; @@ -28,6 +29,8 @@ public class LocalSettingsButton(string text, Action onClick) /// public LocalSettingsTab? Tab { get; internal set; } + private TextMeshPro _btnText { get; set; } + internal GameObject CreateButton(ToggleButtonBehaviour toggle, Transform parent, ref float offset, ref int order, bool last) { var button = Object.Instantiate(toggle, parent).GetComponent(); @@ -51,7 +54,8 @@ public class LocalSettingsButton(string text, Action onClick) button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); } - tmp.text = Text; + tmp.text = Text.Translate(); + _btnText = tmp; button.name = Text; button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); rollover.OutColor = Tab!.TabAppearance.ButtonColor; @@ -74,4 +78,9 @@ public class LocalSettingsButton(string text, Action onClick) return button.gameObject; } + + public void RefreshButton() + { + _btnText.text = Text.Translate(); + } } diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index 074d7db..eace08f 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using BepInEx.Configuration; using HarmonyLib; +using MiraAPI.LocalSettings.SettingTypes; using MiraAPI.Patches.LocalSettings; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; using TMPro; @@ -47,11 +49,21 @@ public abstract class LocalSettingsTab(ConfigFile config) /// public TabGroup? TabButton { get; internal set; } + /// + /// Gets a collection of s alongside the locale keys for them. + /// + public static Dictionary TabGroups { get; } = []; + /// /// Gets the list of s. /// public List Settings { get; } = []; + /// + /// Gets a collection of s alongside the locale keys for them. + /// + public Dictionary Labels { get; } = []; + /// /// Gets the list of s. /// @@ -73,12 +85,25 @@ public abstract class LocalSettingsTab(ConfigFile config) } /// - /// Attempts to open the tab. + /// Refreshes setting names upon opening the tab. /// Override for custom behavior. /// public virtual void Open() { - OptionsMenuPatches.Instance?.OpenTabGroup(TabIndex); + foreach (var setting in Settings) + { + setting.RefreshOption(); + } + foreach (var button in Buttons) + { + button.RefreshButton(); + } + + foreach (var label in Labels) + { + label.Key.text = + $"{label.Value.Translate()}"; + } } /// @@ -216,7 +241,8 @@ public abstract class LocalSettingsTab(ConfigFile config) tabButtonText.transform.localPosition = new Vector3(0.078f, 0, 0); tabButtonText.transform.localScale = new Vector3(0.9f, 0.9f); tabButtonText.alignment = TextAlignmentOptions.Right; - tabButtonText.text = $"{GetShortName(TabName)}"; + tabButtonText.text = $"{GetShortName(TabName.Translate())}"; + TabGroups.Add(tabButtonText, TabName); var tabButton = tabButtonObject.GetComponent(); var rollover = tabButtonObject.Rollover; @@ -244,7 +270,7 @@ public abstract class LocalSettingsTab(ConfigFile config) tabButtonText.gameObject.SetActive(true); tabButtonText.transform.localPosition = new Vector3(-0.024f, 0, 0); tabButtonText.maxVisibleCharacters = int.MaxValue; - tabButtonText.text = TabName; + tabButtonText.text = TabName.Translate(); tabButtonText.alignment = TextAlignmentOptions.Left; tabButtonRend?.gameObject.SetActive(!TabAppearance.HideIconOnHover); })); @@ -266,13 +292,13 @@ public abstract class LocalSettingsTab(ConfigFile config) tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0); tabButtonText.maxVisibleCharacters = 4; - tabButtonText.text = $"{GetShortName(TabName)}"; + tabButtonText.text = $"{GetShortName(TabName.Translate())}"; })); return tabButtonObject.gameObject; } - private static void CreateLabel(GameObject template, Transform parent, string text, ref float offset) + private void CreateLabel(GameObject template, Transform parent, string text, ref float offset) { var label = Object.Instantiate(template, parent); label.transform.localPosition = new Vector3(0, 1.85f - offset); @@ -280,7 +306,8 @@ public abstract class LocalSettingsTab(ConfigFile config) label.name = text; var tmp = label.GetComponent(); - tmp.text = $"{text}"; + Labels.Add(tmp, text); + tmp.text = $"{text.Translate()}"; tmp.alignment = TextAlignmentOptions.Center; var meshRend = label.GetComponent(); @@ -290,7 +317,7 @@ public abstract class LocalSettingsTab(ConfigFile config) offset += 0.5f; } - private static string GetShortName(string name) + internal static string GetShortName(string name) { var shortName = string.Empty; name.Split(' ').Do(x => shortName += x[0]); diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs index 78b0278..e1e0c7b 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using BepInEx.Configuration; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; using TMPro; @@ -25,6 +26,10 @@ public class LocalEnumSetting : LocalSettingBase /// public string[] Values { get; } + private SpriteRenderer _highlight { get; set; } + + private TextMeshPro _btnText { get; set; } + /// /// Initializes a new instance of the class. /// @@ -53,6 +58,7 @@ public class LocalEnumSetting : LocalSettingBase { var button = Object.Instantiate(toggle, parent).GetComponent(); var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + _btnText = tmp; var rollover = button.GetComponent(); tmp.GetComponent().Destroy(); button.gameObject.SetActive(true); @@ -62,6 +68,7 @@ public class LocalEnumSetting : LocalSettingBase var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); if (highlight != null) { + _highlight = highlight; highlight.color = Tab!.TabAppearance.EnumHoverColor; highlight.gameObject.SetActive(false); } @@ -124,9 +131,19 @@ public class LocalEnumSetting : LocalSettingBase return button.gameObject; } + /// + public override void RefreshOption() + { + _btnText.text = GetValueText(); + if (_highlight) + { + _highlight.gameObject.SetActive(false); + } + } + /// protected override string GetValueText() { - return $"{Name}: {Values[GetValue()]}"; + return $"{Name.Translate()}: {Values[GetValue()].Translate()}"; } } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs index fb86fdc..f37ea80 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs @@ -1,5 +1,6 @@ using System; using BepInEx.Configuration; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; using TMPro; @@ -34,6 +35,10 @@ public class LocalNumberSetting : LocalSettingBase /// public MiraNumberSuffixes SuffixType { get; } + private SpriteRenderer _highlight { get; set; } + + private TextMeshPro _btnText { get; set; } + /// /// Initializes a new instance of the class. /// @@ -64,6 +69,7 @@ public class LocalNumberSetting : LocalSettingBase { var button = Object.Instantiate(toggle, parent).GetComponent(); var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + _btnText = tmp; var rollover = button.GetComponent(); tmp.GetComponent().Destroy(); button.gameObject.SetActive(true); @@ -73,6 +79,7 @@ public class LocalNumberSetting : LocalSettingBase var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); if (highlight != null) { + _highlight = highlight; highlight.color = Tab!.TabAppearance.NumberHoverColor; highlight.gameObject.SetActive(false); } @@ -135,11 +142,21 @@ public class LocalNumberSetting : LocalSettingBase return button.gameObject; } + /// + public override void RefreshOption() + { + _btnText.text = GetValueText(); + if (_highlight) + { + _highlight.gameObject.SetActive(false); + } + } + /// protected override string GetValueText() { var value = GetValue(); var formated = Helpers.FormatValue(value, SuffixType, FormatString); - return $"{Name}: {formated}"; + return $"{Name.Translate()}: {formated}"; } } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index 52abe60..e26f6ce 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -1,11 +1,13 @@ using System; using System.Linq; using BepInEx.Configuration; +using MiraAPI.PluginLoading; using UnityEngine; namespace MiraAPI.LocalSettings.SettingTypes; /// +[MiraIgnore] public abstract class LocalSettingBase : ILocalSetting { /// @@ -52,6 +54,9 @@ public abstract class LocalSettingBase : ILocalSetting /// The created setting. public abstract GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); + /// + public abstract void RefreshOption(); + /// /// Returns the formated string to use in the text of the setting. /// diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs index ae6f500..2a90bfd 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs @@ -1,5 +1,6 @@ using System; using BepInEx.Configuration; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; using TMPro; @@ -39,6 +40,8 @@ public class LocalSliderSetting : LocalSettingBase /// public MiraNumberSuffixes SuffixType { get; } + private SlideBar _slider { get; set; } + /// /// Initializes a new instance of the class. /// @@ -70,6 +73,7 @@ public class LocalSliderSetting : LocalSettingBase public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) { var newSlider = Object.Instantiate(slider, parent).GetComponent(); + _slider = newSlider; var rollover = newSlider.GetComponent(); newSlider.Title = newSlider.transform.FindChild("Text_TMP").GetComponent(); // Why the hell slider has a title property that is not even assigned??? newSlider.Title.GetComponent().Destroy(); @@ -104,6 +108,12 @@ public class LocalSliderSetting : LocalSettingBase return newSlider.gameObject; } + /// + public override void RefreshOption() + { + _slider.Title.text = GetValueText(); + } + /// protected override string GetValueText() { @@ -112,9 +122,9 @@ public class LocalSliderSetting : LocalSettingBase var value = GetValue(); var formated = Helpers.FormatValue(value, SuffixType, FormatString); var maxFormated = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString); - return $"{Name}: {formated} / {maxFormated}"; + return $"{Name.Translate()}: {formated} / {maxFormated}"; } - return $"{Name}"; + return $"{Name.Translate()}"; } } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs index 54cd8ee..f28b6db 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs @@ -1,5 +1,6 @@ using System; using BepInEx.Configuration; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Localization.Utilities; using TMPro; @@ -14,6 +15,8 @@ namespace MiraAPI.LocalSettings.SettingTypes; /// public class LocalToggleSetting : LocalSettingBase { + private ToggleButtonBehaviour _toggle { get; set; } + /// /// Initializes a new instance of the class. /// @@ -31,6 +34,7 @@ public class LocalToggleSetting : LocalSettingBase public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) { var toggleObject = Object.Instantiate(toggle, parent).GetComponent(); + _toggle = toggleObject; var tmp = toggleObject.transform.FindChild("Text_TMP").GetComponent(); var passiveButton = toggleObject.GetComponent(); var rollover = toggleObject.GetComponent(); @@ -46,9 +50,9 @@ public class LocalToggleSetting : LocalSettingBase toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); } - toggleObject.BaseText = CustomStringName.CreateAndRegister(Name); + toggleObject.BaseText = MiraLocaleManager.GetOrCreateLocaleString(Name); toggleObject.UpdateText(GetValue()); - toggleObject.name = Name; + toggleObject.name = Name.Translate(); toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; passiveButton.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); rollover.OverColor = Tab!.TabAppearance.ToggleHoverColor; @@ -85,4 +89,11 @@ public class LocalToggleSetting : LocalSettingBase return toggleObject.gameObject; } + + /// + public override void RefreshOption() + { + _toggle.UpdateText(GetValue()); + _toggle.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; + } } diff --git a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs new file mode 100644 index 0000000..44ba56f --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs @@ -0,0 +1,122 @@ +using System; +using System.Globalization; +using Il2CppInterop.Runtime.Attributes; +using Reactor.Utilities.Attributes; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; + +namespace MiraAPI.MeetingAbilities; + +/// +/// A component for handling targeted meeting buttons. +/// +[RegisterInIl2Cpp] +public class MeetingAbilityBehaviour : MonoBehaviour +{ + /// + /// Gets or sets whether the component has been initialized. + /// + private bool _init; + + /// + /// The attached to this component. + /// + private TargetedMeetingButton _button = null!; + + /// + /// The of this component. + /// + public PassiveButton Button; + + /// + /// The parent . + /// + public PlayerVoteArea VoteArea; + + /// + /// The button's component. + /// + public SpriteRenderer Renderer; + + /// + /// The label used for showing the ability's cooldown. + /// + public TextMeshPro CooldownText; + + /// + /// The label used for showing the ability's remaining uses. + /// + public TextMeshPro UsesText; + + /// + /// Initializes the various properties of this component. + /// + /// The to initialize this component for. + /// The parent . + [HideFromIl2Cpp] + public void Initialize(TargetedMeetingButton button, PlayerVoteArea voteArea) + { + if (_init) return; + VoteArea = voteArea; + _button = button; + Renderer = GetComponent(); + SetFillUp(1, 1); + if (!TryGetComponent(out Button)) + { + Error($"Could not initialize MeetingButtonBehaviour for {Button.GetType().Name}, Destroying..."); + Destroy(this); + } + _init = true; + CooldownText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, transform); + CooldownText.GetComponent().Destroy(); + CooldownText.transform.localPosition = new Vector3(0, 0, -10); + CooldownText.m_maxFontSize = 3f; + CooldownText.color = new Color(1, 1, 1, 0.7f); + UsesText = Instantiate(CooldownText, transform); + UsesText.transform.localPosition = new Vector3(0.25f, 0.25f, -10); + UsesText.transform.localScale = Vector3.one * 0.7f; + + CooldownText.gameObject.SetActive(true); + UsesText.gameObject.SetActive(true); + } + + private void OnEnable() + { + if (!_init) return; + Button.enabled = true; + Renderer.enabled = true; + UsesText.enabled = true; + CooldownText.enabled = true; + if (_button.Enabled(PlayerControl.LocalPlayer.Data.Role) && _button.IsTargetValid(VoteArea)) return; + Button.enabled = false; + Renderer.enabled = false; + UsesText.enabled = false; + CooldownText.enabled = false; + } + + private void Update() + { + if (!_init) return; + if (_button.Timer < 0.01f) + { + Renderer.material.SetFloat("_Desat", 0.0f); + CooldownText.text = string.Empty; + UsesText.text = _button.LimitedUses ? _button.UsesLeft.ToString(CultureInfo.InvariantCulture) : string.Empty; + } + else + { + Renderer.material.SetFloat("_Desat", 0.5f); + CooldownText.text = _button.Timer.ToString(_button.CooldownTimerFormatString, NumberFormatInfo.InvariantInfo); + UsesText.text = string.Empty; + } + SetFillUp(_button.Timer, _button.InitialCooldown); + } + + public void SetFillUp(float timer, float maxTimer) + { + float percentCool = Mathf.Clamp(timer / maxTimer, 0.001f, 1f); + if (percentCool <= 0) percentCool = 1f; + Renderer.material.SetFloat("_Percent", percentCool); + } +} diff --git a/MiraAPI/MeetingAbilities/MeetingActionButton.cs b/MiraAPI/MeetingAbilities/MeetingActionButton.cs new file mode 100644 index 0000000..dd733af --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingActionButton.cs @@ -0,0 +1,349 @@ +using System; +using MiraAPI.Events; +using MiraAPI.Events.Mira; +using MiraAPI.Hud; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; +using Object = UnityEngine.Object; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Class for making custom meeting action buttons. +/// +public abstract class MeetingActionButton +{ + /// + /// Gets the name and text of the button. + /// + public abstract string Name { get; } + + /// + /// Gets the initial cooldown duration in seconds. + /// + public virtual float InitialCooldown => Cooldown; + + /// + /// Gets the button's cooldown duration in seconds. + /// + public abstract float Cooldown { get; } + + /// + /// Gets the sprite of the button. Use to load a sprite from a resource path. Use to load a sprite from an asset bundle. + /// + public abstract LoadableAsset Sprite { get; } + + /// + /// Gets the format string for the cooldown timer. + /// + public virtual string CooldownTimerFormatString => "0"; + + /// + /// Gets a value indicating whether the button has limited uses. + /// + public bool LimitedUses => ZeroIsInfinite ? MaxUses > 0 : MaxUses >= 0; + + /// + /// Gets the maximum number of uses the button has. If the button has infinite uses, set to 0 or -1 based on what value that ZeroIsInfinite is set to. + /// + public virtual int MaxUses => ZeroIsInfinite ? 0 : -1; + + /// + /// Gets the value indicating uses mode. + /// + public virtual MeetingButtonUsesMode UsesMode => MeetingButtonUsesMode.PerGame; + + /// + /// Gets the button's text outline . + /// + public virtual Color TextOutlineColor => Color.clear; + + /// + /// Gets or sets a value indicating whether limited uses are determined via zero or a negative number of uses. + /// + public virtual bool ZeroIsInfinite { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the timer is currently active. + /// + 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. + /// + public int UsesLeft { get; set; } + + /// + /// Gets or sets the timer variable to measure cooldowns and effects. + /// + public float Timer { get; set; } + + /// + /// Gets or sets the object in game. This is created by Mira API automatically. + /// + public ActionButton? Button { get; set; } + + /// + /// The method used to create the button. + /// + /// The parent of the button. + public virtual void CreateButton(Transform parent) + { + if (Button) + { + return; + } + + Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown; + TimerPaused = false; + + Button = Object.Instantiate(MeetingHud.Instance.MeetingAbilityButton, parent); + Button.name = Name + "Button"; + Button.OverrideText(Name.ToUpperInvariant()); + + Button.graphic.sprite = Sprite.LoadAsset(); + + Button.SetUsesRemaining(UsesLeft); + UsesSoFar = 0; + if (MaxUses <= 0) + { + Button.SetInfiniteUses(); + } + + if (TextOutlineColor != Color.clear) + { + SetTextOutline(TextOutlineColor); + } + + var pb = Button.GetComponent(); + pb.OnClick = new Button.ButtonClickedEvent(); + pb.OnClick.AddListener((UnityAction)(() => + { + ClickHandler(); + })); + Button.gameObject.SetActive(Enabled(PlayerControl.LocalPlayer.Data.Role)); + } + + /// + /// A utility function to change the outline of the button's text. + /// + /// The new . + public virtual void SetTextOutline(Color color) + { + Button?.buttonLabelText.SetOutlineColor(color); + } + + /// + /// A utility function to override the of the button. + /// + /// The new to override with. + public virtual void OverrideSprite(Sprite sprite) + { + if (Button != null) + { + Button.graphic.sprite = sprite; + } + } + + /// + /// A utility function to override the name of the button. + /// + /// The new name to override with. + public virtual void OverrideName(string name) + { + Button?.OverrideText(name); + } + + /// + /// Set the button's timer. + /// + /// The time you want to set to. + public virtual void SetTimer(float time) + { + Timer = Mathf.Clamp(time, -1, float.MaxValue); + } + + /// + /// Increase the button's timer. + /// + /// The amount you want to increase by. + public virtual void IncreaseTimer(float amount) + { + SetTimer(Timer + amount); + } + + /// + /// Decrease the button's timer. + /// + /// The amount you want to decrease by. + public virtual void DecreaseTimer(float amount) + { + SetTimer(Timer - amount); + } + + /// + /// Sets whether the timer is paused or not. + /// + /// Whether you want to pause/resume the timer. + public virtual void SetTimerPaused(bool val) + { + TimerPaused = val; + } + + /// + /// Set the amount of uses this button has left. + /// + /// The amount you want to set to. + public virtual void SetUses(int amount) + { + UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue); + Button?.SetUsesRemaining(UsesLeft); + + if (Button != null) + { + Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white; + } + } + + /// + /// Increase the amount of uses this button has left. + /// + /// The amount you want to increase by. Default: 1. + public virtual void IncreaseUses(int amount = 1) + { + SetUses(UsesLeft + amount); + } + + /// + /// Decrease the amount of uses this button has left. + /// + /// The amount you want to decrease by. Default: 1. + public virtual void DecreaseUses(int amount = 1) + { + SetUses(UsesLeft - amount); + } + + /// + /// A utility function that runs with the local player's if the button is enabled. + /// + /// the instance. + protected virtual void FixedUpdate(MeetingHud hud) + { + } + + /// + /// Callback method for the button click event. + /// + protected abstract void OnClick(); + + /// + /// This method determines if the button should be active or not. + /// means the button is active, means the button is disabled. + /// + /// The of the local player. + /// if the button is enabled, otherwise. + public abstract bool Enabled(RoleBehaviour? role); + + /// + /// When the button is usable, this method is called to determine if the button can be clicked. + /// By default, it takes into account the timer, effect, and uses. + /// You can override it to change the behavior. + /// + /// A value that represents whether the button can be clicked or not. + public virtual bool CanClick() + { + return Timer <= 0 && CanUse(); + } + + /// + /// Whether the button should light up or not. This is also the base for CanClick. + /// You can override it to change the behaviour. Do not include timer in here, that is for CanClick. + /// + /// A value that represents whether the button should light up or not. + public virtual bool CanUse() + { + return (!DisableUponUse || UsesSoFar < 1) && + (!DisableUponVoting || MeetingHud.Instance.CurrentState is not MeetingHud.MeetingStates.Voted) && + (!LimitedUses || UsesLeft > 0); + } + + /// + /// This method handles the button click event. It is a wrapper for the method. + /// This method takes into account cooldowns, effects, and uses, before calling the method. + /// It can be overridden for custom behavior. + /// + public virtual void ClickHandler() + { + if (!CanClick()) + { + return; + } + + if (LimitedUses) + { + UsesLeft--; + Button?.SetUsesRemaining(UsesLeft); + } + + Timer = Cooldown; + OnClick(); + UsesSoFar++; + if (DisableUponUse && Button) + { + Button!.SetDisabled(); + } + } + + /// + /// This method is called on the method. It is a wrapper for the method. + /// By default, it handles the cooldown and effect timers, and sets the button to enabled or disabled. + /// It can be overridden for custom behavior. + /// + /// The instance. + public virtual void UpdateHandler(MeetingHud hud) + { + if (Timer >= 0 && !TimerPaused) + { + Timer -= Time.deltaTime; + } + + if (Button) + { + if (CanUse()) + { + Button!.SetEnabled(); + } + else + { + Button!.SetDisabled(); + } + Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString); + Button.SetCoolDown(Timer, Cooldown); + } + + FixedUpdate(hud); + } +} diff --git a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs new file mode 100644 index 0000000..9f44fcf --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; +using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; +using MiraAPI.Utilities.Components; +using UnityEngine; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Class for managing all registered meeting abilities. +/// +public static class MeetingButtonManager +{ + /// + /// Gets a list if==of all registered . + /// + public static List TargetedButtons { get; internal set; } = new(); + + /// + /// Gets a list if==of all registered . + /// + public static List UntargetedButtons { get; internal set; } = new(); + + internal static bool RegisterTargetedMeetingButton(Type type, MiraPluginInfo info) + { + if (!typeof(TargetedMeetingButton).IsAssignableFrom(type)) + { + return false; + } + + var button = Activator.CreateInstance(type) as TargetedMeetingButton; + info.InternalTargetedMeetingButtons.Add(button); + TargetedButtons.Add(button); + return true; + } + + internal static bool RegisterMeetingButton(Type type, MiraPluginInfo info) + { + if (!typeof(MeetingActionButton).IsAssignableFrom(type)) + { + return false; + } + + var button = Activator.CreateInstance(type) as MeetingActionButton; + info.InternalMeetingButtons.Add(button); + UntargetedButtons.Add(button); + return true; + } + + private static CenteredGridArrange? actionButtonsParent = null!; + + public static void OnGameStart() + { + foreach (var ability in TargetedButtons.Where(x => x.UsesMode == MeetingButtonUsesMode.PerGame)) + { + ability.UsesLeft = ability.MaxUses; + } + foreach (var ability in UntargetedButtons.Where(x => x.UsesMode == MeetingButtonUsesMode.PerGame)) + { + ability.UsesLeft = ability.MaxUses; + } + } + + /// + /// Called on . Handles button creation, and ability uses. + /// + /// The instance. + public static void OnMeetingStart(MeetingHud meetingHud) + { + // Untargeted buttons parent. + actionButtonsParent = new GameObject("ActionsButtonParent").AddComponent(); + actionButtonsParent.transform.SetParent(meetingHud.MeetingAbilityButton.transform.parent, meetingHud.MeetingAbilityButton.transform.parent); + actionButtonsParent.CellSize = new Vector2(0.7f, 0.7f); + actionButtonsParent.transform.localPosition = new Vector3(0, -2.1f, -10); + meetingHud.MeetingAbilityButton.transform.SetParent(actionButtonsParent.transform); + + // Untargeted buttons creation. + foreach (var ability in UntargetedButtons) + { + try + { + ability.CreateButton(actionButtonsParent.transform); + if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses; + ability.Timer = ability.InitialCooldown; + } + catch (Exception e) + { + Error($"Failed to create meeting button {ability.Name}: {e.Message}"); + } + } + + // Targeted buttons creation. + foreach (var ability in TargetedButtons) + { + try + { + foreach (var playerVoteArea in meetingHud.playerStates) + { + var btn = ability.CreateButton(playerVoteArea); + btn.transform.SetParent(playerVoteArea.Buttons.transform); + } + 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(); + } + catch (Exception e) + { + 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/MeetingButtonUsesMode.cs b/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs new file mode 100644 index 0000000..a4ddb40 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs @@ -0,0 +1,7 @@ +namespace MiraAPI.MeetingAbilities; + +public enum MeetingButtonUsesMode +{ + PerGame, + PerMeeting +} \ No newline at end of file diff --git a/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs new file mode 100644 index 0000000..b4dcffa --- /dev/null +++ b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.UI; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Abstract class for creating multi target meeting abilities +/// +public abstract class MultiTargetMeetingButton : TargetedMeetingButton +{ + /// + /// The used for when the button is on. + /// + public abstract LoadableAsset SpriteActive { get; } + + /// + /// Gets the required targets count. + /// + public abstract int TargetsCount { get; } + + /// + /// Gets or sets all selected targets. + /// + public Dictionary Targets { get; set; } = new(); + + /// + public override void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea) + { + if (UsesLeft <= 0 && LimitedUses) return; + if (Timer > 0) return; + var toggle = !Targets.Remove(playerVoteArea); + if (toggle) Targets.Add(playerVoteArea, button); + button.Renderer.sprite = toggle ? SpriteActive.LoadAsset() : Sprite.LoadAsset(); + if (Targets.Count >= TargetsCount) + { + playerVoteArea.Cancel(); + HandleFinish(); + return; + } + OnSelect(playerVoteArea, toggle); + } + + /// + /// Handles the finish logic, increasing timer, and decreasing uses. + /// + public virtual void HandleFinish() + { + Targets = Targets.Take(TargetsCount).ToDictionary(pair => pair.Key, pair => pair.Value); + foreach (var button in Targets.Values) + { + button.Renderer.sprite = Sprite.LoadAsset(); + } + Timer = Cooldown; + UsesLeft -= 1; + OnFinish(); + Targets = new(); + } + + /// + /// Callback method for when a player is selected. + /// + /// The target . + /// Whether the button was toggled on/off. + protected abstract void OnSelect(PlayerVoteArea playerVoteArea, bool toggle); + + /// + /// Callback method for the button click. + /// USE INSTEAD!!!. + /// + /// The target . + protected override void OnClick(PlayerVoteArea playerVoteArea) + { + } + + /// + /// Callback method for when enough targets are chosen. + /// + public abstract void OnFinish(); + + /// + public override MeetingAbilityBehaviour CreateButton(PlayerVoteArea playerVoteArea) + { + Targets = new(); + var button = base.CreateButton(playerVoteArea); + button.Button.OnClick = new Button.ButtonClickedEvent(); + button.Button.OnClick.AddListener(new System.Action(() => { ClickHandler(button, playerVoteArea); })); + return button; + } +} diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs new file mode 100644 index 0000000..d1d6643 --- /dev/null +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.UI; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Abstract class for creating targeted meeting abilities. +/// +public abstract class TargetedMeetingButton +{ + /// + /// Gets the name of the . + /// + public abstract string Name { get; } + + /// + /// Gets the Max Uses of the button. + /// Set it to 0 for Infinite uses. + /// + public abstract int MaxUses { get; } + + /// + /// Gets or sets the amount of uses left for this button. + /// + public int UsesLeft { get; set; } + + /// + /// Gets a value indicating whether the has limited uses. + /// + public bool LimitedUses => MaxUses != 0; + + /// + /// Gets the cooldown time for this button upon meeting start. + /// Defaults to unless overridden. + /// + public virtual float InitialCooldown => Cooldown; + + /// + /// Gets the cooldown time for this button after being used. + /// + public abstract float Cooldown { get; } + + /// + /// Gets or sets the current cooldown timer for this button. + /// + public float Timer { get; set; } + + /// + /// Gets the cooldown time for this button. + /// + public virtual string CooldownTimerFormatString => "0"; + + /// + /// Gets the sprite used for this button. + /// + public abstract LoadableAsset Sprite { get; } + + /// + /// Gets the button's outline color. + /// + public abstract Color OutlineColor { get; } + + /// + /// Gets the for this button. + /// + public virtual MeetingButtonUsesMode UsesMode { get; } = MeetingButtonUsesMode.PerGame; + + /// + /// Determines if the button is enabled. + /// + /// The local player's . + /// Whether the button is enabled or not. + public abstract bool Enabled(RoleBehaviour r); + + /// + /// Callback method for when the button is clicked. + /// + /// The target . + protected abstract void OnClick(PlayerVoteArea playerVoteArea); + + /// + /// Handles the button creation. + /// + /// The target . + /// The instance. + public virtual MeetingAbilityBehaviour CreateButton(PlayerVoteArea playerVoteArea) + { + if (MeetingHud.Instance == null) return null!; + + var btn = Object.Instantiate(MeetingHud.Instance.PlayerButtonPrefab.ConfirmButton).GetComponent(); + btn.gameObject.name = Name + "Button"; + + var spriteRenderer = btn.GetComponent(); + spriteRenderer.sprite = Sprite.LoadAsset(); + spriteRenderer.material = new Material(HudManager.Instance.KillButton.graphic.material); + spriteRenderer.SetCooldownNormalizedUvs(); + btn.gameObject.SetActive(true); + + btn.transform.GetChild(0).GetComponent().color = OutlineColor; + + btn.OnClick = new Button.ButtonClickedEvent(); + var abilityBehaviour = btn.gameObject.AddComponent(); + abilityBehaviour.Initialize(this, playerVoteArea); + btn.OnClick.AddListener(new System.Action(() => ClickHandler(abilityBehaviour, playerVoteArea))); + Timer = InitialCooldown; + + return abilityBehaviour; + } + + /// + /// Called every . + /// Handles the timer logic. + /// + public virtual void UpdateHandler() + { + Timer -= Time.deltaTime; + Timer = Mathf.Clamp(Timer, 0, int.MaxValue); + FixedUpdate(); + } + + /// + /// Callback method for handling logic every frame. + /// + public virtual void FixedUpdate() + { + } + + /// + /// Called when the button is clicked + /// Handles increasing cooldown and decreasing uses remaining. + /// + /// The instance. + /// The target . + public virtual void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea) + { + if (UsesLeft <= 0 && LimitedUses) return; + if (Timer > 0) return; + Timer = Cooldown; + UsesLeft--; + playerVoteArea.Cancel(); + OnClick(playerVoteArea); + } + + /// + /// Determines whether the target is valid. + /// + /// The target . + /// Whether the target is valid or not. + public virtual bool IsTargetValid(PlayerVoteArea playerVoteArea) + { + return playerVoteArea != null && playerVoteArea.PlayerId.Value != PlayerControl.LocalPlayer.Data.PlayerId && playerVoteArea.PlayerId.Value != PlayerVoteArea.SkippedVote; + } +} diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs new file mode 100644 index 0000000..47dc791 --- /dev/null +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace MiraAPI.MeetingAbilities; + +/// +/// A utility class to get the instance of a . +/// +/// The type of the button you are trying to access. +public static class TargetedMeetingButtonSingleton where T : TargetedMeetingButton +{ + private static T? _instance; + + /// + /// Gets the instance of the button. + /// + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a targeted meeting button.")] + public static T Instance => _instance ??= MeetingButtonManager.TargetedButtons.OfType().Single(); +} diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 594d927..0ea0b20 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,10 +1,15 @@ global using static Reactor.Utilities.Logger; using System; +using System.Globalization; using BepInEx; +using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; +using MiraAPI.Translation; +using MiraAPI.VanillaEvents; using Reactor; +using Reactor.Localization; using Reactor.Networking; using Reactor.Networking.Attributes; using Reactor.Utilities; @@ -18,10 +23,26 @@ namespace MiraAPI; [BepInAutoPlugin("mira.api", "MiraAPI")] [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] -[BepInDependency(ModCompatibility.SubmergedId, BepInDependency.DependencyFlags.SoftDependency)] [ReactorModFlags(ModFlags.RequireOnAllClients)] -public partial class MiraApiPlugin : BasePlugin +public partial class MiraApiPlugin : BasePlugin, IMiraPlugin { + /// + public ConfigFile GetConfigFile() + { + return Config; + } + + /// + /// Gets the specified Culture for string manipulations. + /// + public static CultureInfo Culture { get; internal set; } = new("en-US"); + + /// + public string OptionsTitleText => "MiraAPI"; + + /// + public bool DisplayOnOptionsMenu => false; + /// /// Gets a value indicating whether the current device is running Starlight (on mobile). /// @@ -49,10 +70,19 @@ public partial class MiraApiPlugin : BasePlugin public override void Load() { Harmony.PatchAll(); + UiResetEvents.Initialize(); + JudgeEvents.Initialize(); ReactorCredits.Register("Mira API", Version, IsDevBuild, ReactorCredits.AlwaysShow); PluginManager = new MiraPluginManager(); - PluginManager.Initialize(); + PluginManager.Initialize(this, this); + + MiraLocaleManager.Register("mira.api"); + LocalizationManager.Register(new MiraLocalizationProvider()); + + IL2CPPChainloader.Instance.Finished += + ModCompatibility + .Initialize; // Initialise AFTER the mods are loaded to ensure maximum parity (no need for the soft dependency either then) } } diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index fbead71..3d74768 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -1,7 +1,12 @@ -using BepInEx.Configuration; +using System.Collections; +using BepInEx.Configuration; +using MiraAPI.Events; +using MiraAPI.Events.Mira; +using MiraAPI.Hud; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Patches; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -14,7 +19,7 @@ namespace MiraAPI; public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) { /// - public override string TabName => "Mira API"; + public override string TabName => "MiraApi"; /// public override LocalSettingTabAppearance TabAppearance => new() @@ -25,15 +30,21 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) }; /// - /// Gets or sets the value stored for scaling buttons properly. + /// Gets or sets the value stored for scaling ability buttons properly. /// public static float OldButtonScaleFactor { get; set; } + /// + /// Gets or sets the value stored for scaling UI buttons properly. + /// + public static float OldUiButtonScaleFactor { get; set; } + /// public override void Open() { base.Open(); OldButtonScaleFactor = ButtonUIFactorSlider.Value; + OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value; } /// @@ -49,53 +60,163 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) } OldButtonScaleFactor = ButtonUIFactorSlider.Value; } + else if (configEntry == TopRightButtonsFactorSlider) + { + if (MiraHudHelper.Instance) + { + ResizeUI(TopRightButtonsFactorSlider.Value); + } + OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value; + } + else if (configEntry == WikiOnBottomRow) + { + SetUpButtonPositions(); + } else if (configEntry == SetFpsSlider) { Application.targetFrameRate = (int)SetFpsSlider.Value; } } - /// - /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. - /// - [LocalToggleSetting] - public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Visuals/UI", "Show Modifiers HUD on Left Side", false); + public static void SetUpButtonPositions() + { + var topUi = MiraHudHelper.UiTopRight; + var extraTopUi = MiraHudHelper.ExtraUiTopRight; + if (topUi && extraTopUi) + { + var genericEvent = new UiButtonResetEvent(); + MiraEventManager.InvokeEvent(genericEvent); + + var genericEvent2 = new UiButtonPostResetEvent(topUi, extraTopUi); + MiraEventManager.InvokeEvent(genericEvent2); + MiraHudHelper.UiGrid.ArrangeChilds(); + MiraHudHelper.ExtraUiGrid.ArrangeChilds(); + } + } + + public static IEnumerator CoResizeSettingsUI() + { + while (!HudManager.Instance || !MiraHudHelper.UiGrid || !MiraHudHelper.ExtraUiGrid) + { + yield return null; + } + + yield return new WaitForSeconds(0.01f); + ResizeUI(LocalSettingsTabSingleton.Instance.TopRightButtonsFactorSlider.Value); + } + + public static void ResizeUI(float scaleFactor) + { + var alteredScale = scaleFactor * 0.85f; + var actualScaleVal = scaleFactor * 1.176470588235294f; + var actualScale = new Vector3(actualScaleVal, actualScaleVal, 1); + var baseAspect = MiraHudHelper.UiAspectPos; + var baseGrid = MiraHudHelper.UiGrid; + var baseUi = MiraHudHelper.UiTopRight; + if (baseUi && baseAspect && baseGrid) + { + baseAspect.DistanceFromEdge = new Vector2(0.435f * scaleFactor, 0.475f * scaleFactor); + + foreach (var button in baseUi.GetComponentsInChildren(true)) + { + if (button.gameObject == null) + { + continue; + } + if (button.transform.name.Contains("Friends List Button")) + { + button.gameObject.transform.localScale = new Vector3(0.2675f * actualScaleVal, 0.2675f * actualScaleVal, 1); + continue; + } + + button.gameObject.transform.localScale = actualScale; + } + + baseGrid.CellSize = new Vector2(alteredScale, alteredScale); + if (baseGrid.gameObject.transform.childCount != 0) + { + baseGrid.ArrangeChilds(); + } + baseAspect.AdjustPosition(); + } + + var extraAspect = MiraHudHelper.ExtraUiAspectPos; + var extraGrid = MiraHudHelper.ExtraUiGrid; + var extraUi = MiraHudHelper.ExtraUiTopRight; + if (extraUi && extraAspect && extraGrid) + { + extraAspect.DistanceFromEdge = new Vector3(0.435f * scaleFactor, 1.25f * scaleFactor, 0f); + + foreach (var button in extraUi.GetAllChildren()) + { + if (button.gameObject == null) + { + continue; + } + if (button.transform.name.Contains("Modifiers")) + { + button.gameObject.transform.localScale = new Vector3(0.65f * scaleFactor, 0.65f * scaleFactor, 1); + continue; + } + + button.gameObject.transform.localScale = actualScale; + } + + extraGrid.CellSize = new Vector2(alteredScale, alteredScale); + if (extraGrid.gameObject.transform.childCount != 0) + { + extraGrid.ArrangeChilds(); + } + extraAspect.AdjustPosition(); + } + } /// - /// Gets whether to show keybinds on buttons. - /// - [LocalToggleSetting] - public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Visuals/UI", "Show Keybinds on Buttons", true); - - /// - /// Gets the scale of the buttons. + /// Gets the scale of the ability buttons. /// [LocalSliderSetting(min: 0.5f, max: 1.5f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)] public ConfigEntry ButtonUIFactorSlider { get; private set; } = - config.Bind("Visuals/UI", "Button Scale Factor", 0.75f); + config.Bind("MiraApi.VisualsUi", "MiraApi.ButtonScaleFactor", 0.75f); + + /// + /// Gets the scale of the UI buttons. + /// + [LocalSliderSetting(min: 0.3f, max: 2f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)] + public ConfigEntry TopRightButtonsFactorSlider { get; private set; } = + config.Bind("MiraApi.VisualsUi", "MiraApi.TopRightButtonScaleFactor", 1f); /// /// Gets the fps specified by the player. /// [LocalSliderSetting(min: 60f, max: 240f, suffixType: MiraNumberSuffixes.None, formatString: "0", displayValue: true, roundValue: true)] public ConfigEntry SetFpsSlider { get; private set; } = - config.Bind("Visuals/UI", "Max FPS", 120f); + config.Bind("MiraApi.VisualsUi", "Max FPS", 120f); /// - /// Gets whether to apply cosmetic changes to the TaskAdder. + /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. /// [LocalToggleSetting] - public ConfigEntry PrettyTaskAdder { get; private set; } = config.Bind("Freeplay", "Pretty Task Laptop", true); + public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.ShowModifiersHudOnLeftSide", false); + + /// + /// Gets whether to show keybinds on buttons. + /// + [LocalToggleSetting] + public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.ShowKeybindsOnButtons", true); + + [LocalToggleSetting] + public ConfigEntry WikiOnBottomRow { get; private set; } = + config.Bind("MiraApi.VisualsUi", "MiraApi.WikiOnBottomRow", true); /// /// Gets whether to show the red flash from sabotages. /// [LocalToggleSetting] - public ConfigEntry EnableSabotageFlashes { get; private set; } = config.Bind("Accessibility", "Enable Sabotage Flashes", true); + public ConfigEntry EnableSabotageFlashes { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableSabotageFlashes", true); /// /// Gets whether to enable the sabotage sound effects or not. /// [LocalToggleSetting] - public ConfigEntry EnableSabotageBlares { get; private set; } = config.Bind("Accessibility", "Enable Sabotage Blare", true); + public ConfigEntry EnableSabotageBlares { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableSabotageBlare", true); } diff --git a/MiraAPI/ModCompatibility.cs b/MiraAPI/ModCompatibility.cs index 073453b..ebb1081 100644 --- a/MiraAPI/ModCompatibility.cs +++ b/MiraAPI/ModCompatibility.cs @@ -1,6 +1,6 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; +using System.Reflection; using BepInEx.Unity.IL2CPP; +using SemanticVersioning; namespace MiraAPI; @@ -14,15 +14,36 @@ public static class ModCompatibility /// public const string SubmergedId = "Submerged"; - /// - /// Checks if the Submerged mod is loaded. - /// - /// Submerged mod assembly if loaded, otherwise. - /// if the Submerged mod is loaded, otherwise. - public static bool SubmergedLoaded([NotNullWhen(true)] out Assembly? submergedAssembly) + public static Version SubVersion { get; private set; } + public static bool SubLoaded { get; private set; } + public static BasePlugin SubPlugin { get; private set; } + public static Assembly SubAssembly { get; private set; } + + public const ShipStatus.MapType SubmergedMapType = (ShipStatus.MapType)6; + + public static void Initialize() { - var result = IL2CPPChainloader.Instance.Plugins.TryGetValue(SubmergedId, out var plugin); - submergedAssembly = result ? plugin?.Instance.GetType().Assembly : null; - return result; + InitSubmerged(); + } + + public static void InitSubmerged() + { + if (!IL2CPPChainloader.Instance.Plugins.TryGetValue(SubmergedId, out var plugin)) + { + return; + } + + SubPlugin = (plugin!.Instance as BasePlugin)!; + SubVersion = plugin.Metadata.Version; + + SubAssembly = SubPlugin.GetType().Assembly; + + SubLoaded = true; + Message("Submerged was detected"); + } + + public static bool IsSubmerged() + { + return SubLoaded && ShipStatus.Instance && ShipStatus.Instance.Type == SubmergedMapType; } } diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index cea542d..d34580b 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -8,6 +8,8 @@ using MiraAPI.LocalSettings; using MiraAPI.Modifiers.ModifierDisplay; using MiraAPI.Modifiers.Types; using MiraAPI.Patches.Roles; +using MiraAPI.Translation; +using Reactor.Utilities; using Reactor.Utilities.Attributes; using UnityEngine; diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs index e93f477..73922f3 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -4,6 +4,7 @@ using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.Attributes; using MiraAPI.LocalSettings; +using MiraAPI.Translation; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; @@ -153,7 +154,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) private ModifierUiComponent CreateForModifier(BaseModifier modifier) { var newMod = Instantiate(_modTemplate, _children.transform); - newMod.transform.name = modifier.ModifierName; + newMod.transform.name = modifier.ModifierName.Translate(); var comp = newMod.AddComponent(); comp.Modifier = modifier; return comp; diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs index 66ded67..cafbd1b 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs @@ -1,4 +1,5 @@ using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Translation; using Reactor.Utilities.Attributes; using TMPro; using UnityEngine; diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 28faa8b..ee0fb5e 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.Serialization; using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; +using MiraAPI.Translation; using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; @@ -138,7 +139,7 @@ public static class ModifierManager { if (MiraApiPlugin.IsDevBuild) { - Warning($"No valid players for modifier {modifier.ModifierName}"); + Warning($"No valid players for modifier {modifier.ModifierName.Translate()}"); } continue; } @@ -163,7 +164,7 @@ public static class ModifierManager if (MiraApiPlugin.IsDevBuild) { Warning( - $"No available players for modifier {modifier.ModifierName} at assignment {i + 1}"); + $"No available players for modifier {modifier.ModifierName.Translate()} at assignment {i + 1}"); } break; @@ -174,7 +175,7 @@ public static class ModifierManager { if (MiraApiPlugin.IsDevBuild) { - Warning($"Valid player for modifier {modifier.ModifierName} disappeared"); + Warning($"Valid player for modifier {modifier.ModifierName.Translate()} disappeared"); } continue; diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index f5ae883..051ae07 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -1,8 +1,12 @@ -namespace MiraAPI.Modifiers.Types; +using MiraAPI.GameModes; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Modifiers.Types; /// /// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role. /// +[MiraIgnore] public abstract class GameModifier : BaseModifier { /// @@ -51,5 +55,5 @@ public abstract class GameModifier : BaseModifier /// Determines whether the modifier can spawn in general, accounting for gamemodes and everything else. /// /// if the modifier is able to spawn, otherwise . - public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek(); + public virtual bool CanSpawnOnCurrentMode() => CustomGameModeManager.IsClassic() && !GameManager.Instance.IsHideAndSeek(); } diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 472b3cf..cbcb13d 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -1,4 +1,6 @@ using System; +using MiraAPI.PluginLoading; +using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Modifiers.Types; @@ -6,6 +8,7 @@ namespace MiraAPI.Modifiers.Types; /// /// The base class for all timed modifiers. Timed modifiers have a duration and can be started and stopped. /// +[MiraIgnore] public abstract class TimedModifier : BaseModifier { /// diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 0622492..e840d00 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -7,6 +7,7 @@ using Assets.CoreScripts; using BepInEx.Unity.IL2CPP.Utils; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameModes; using MiraAPI.Utilities; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; @@ -629,7 +630,10 @@ public static class CustomMurderRpc if (resetKillTimer) { source.SetKillTimer( - GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown) / 2f); + CustomGameModeManager.ActiveMode != null + ? CustomGameModeManager.ActiveMode.DefaultImpostorKillCooldown / 2f + : GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown) / + 2f); } } else @@ -666,7 +670,9 @@ public static class CustomMurderRpc if (resetKillTimer) { - source.SetKillTimer(GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown)); + source.SetKillTimer(CustomGameModeManager.ActiveMode != null + ? CustomGameModeManager.ActiveMode.DefaultImpostorKillCooldown + : GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown)); } } diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index 25312e0..836e822 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -74,4 +74,14 @@ public enum MiraRpc : uint /// Custom RPC to replace Among Us CastVote. /// CastVote, + + /// + /// Custom RPC to replace Among Us QueueOverruleVotes. + /// + QueueOverruleVotes, + + /// + /// Custom RPC to sync the Gamemode option. + /// + SyncGamemodeOption, } diff --git a/MiraAPI/Patches/AmongUsClientSyncPatch.cs b/MiraAPI/Patches/AmongUsClientSyncPatch.cs index 6af91c1..a8a3dfe 100644 --- a/MiraAPI/Patches/AmongUsClientSyncPatch.cs +++ b/MiraAPI/Patches/AmongUsClientSyncPatch.cs @@ -2,6 +2,7 @@ using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Player; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Roles; @@ -32,6 +33,7 @@ internal static class AmongUsClientSyncPatch ModdedOptionsManager.SyncAllOptions(clientData.Id); CustomRoleManager.SyncAllRoleSettings(clientData.Id); + GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value); } [HarmonyPostfix] diff --git a/MiraAPI/Patches/DetectiveNotesMinigamePatch.cs b/MiraAPI/Patches/DetectiveNotesMinigamePatch.cs index a11ec7e..5f67033 100644 --- a/MiraAPI/Patches/DetectiveNotesMinigamePatch.cs +++ b/MiraAPI/Patches/DetectiveNotesMinigamePatch.cs @@ -7,7 +7,7 @@ using UnityEngine; namespace MiraAPI.Patches; -[HarmonyPatch(typeof(DetectiveNotesMinigame), nameof(DetectiveNotesMinigame.SetImpostorPopup))] +/*[HarmonyPatch(typeof(DetectiveNotesMinigame), nameof(DetectiveNotesMinigame.SetImpostorPopup))] public static class DetectiveNotesMinigamePatch { public static void Prefix(DetectiveNotesMinigame __instance, bool active) @@ -39,4 +39,4 @@ public static class DetectiveNotesMinigamePatch } ControllerManager.Instance.OpenOverlayMenu(__instance.ImpostorOverlay, __instance.impostorTypePopup, __instance.ImpostorIconControllerSelectables[0], __instance.ImpostorIconControllerSelectables, false); } -} +}*/ diff --git a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs index 3bda24d..0f582e1 100644 --- a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs +++ b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs @@ -1,7 +1,9 @@ -using System.Reflection; +using System.Linq; +using System.Reflection; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.MeetingAbilities; using MiraAPI.Utilities; namespace MiraAPI.Patches.Events; @@ -18,6 +20,7 @@ public static class FreeplayRoundStartPatch { if (!__result) { + MeetingButtonManager.OnGameStart(); MiraEventManager.InvokeEvent(new RoundStartEvent(true)); } } diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index ad0cabc..71fa411 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -5,11 +5,11 @@ using System.Text; using AmongUs.GameOptions; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; -using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; @@ -268,13 +268,9 @@ public static class TaskAdderPatches stringBuilder.Append('\\'); } __instance.PathText.text = stringBuilder.ToString(); - var prettyEnabled = LocalSettingsTabSingleton.Instance.PrettyTaskAdder.Value; - if (prettyEnabled) - { - __instance.PathText.fontSizeMin = 3; - __instance.PathText.fontSizeMax = 3; - __instance.transform.FindChild("TitleText_TMP")?.gameObject.DestroyImmediate(); - } + __instance.PathText.fontSizeMin = 3; + __instance.PathText.fontSizeMax = 3; + __instance.transform.FindChild("TitleText_TMP")?.gameObject.DestroyImmediate(); __instance.ActiveItems.ToArray().Do(x => x.gameObject.DeepDestroy(false)); __instance.ActiveItems.Clear(); @@ -354,15 +350,12 @@ public static class TaskAdderPatches { TaskAddButton roleAddButton = Object.Instantiate(__instance.RoleButton); roleAddButton.SafePositionWorld = __instance.SafePositionWorld; - roleAddButton.Text.text = prettyEnabled ? role.GetRoleName() : "Be_" + role.GetRoleName() + ".exe"; + roleAddButton.Text.text = role.GetRoleName(); roleAddButton.Role = role; - if (prettyEnabled) - { - roleAddButton.Text.fontSizeMin = 1; - roleAddButton.FileImage.sprite = role.IsImpostor - ? MiraAssets.ImpostorFile.LoadAsset() - : MiraAssets.CrewmateFile.LoadAsset(); - } + roleAddButton.Text.fontSizeMin = 1; + roleAddButton.FileImage.sprite = role.IsImpostor + ? MiraAssets.ImpostorFile.LoadAsset() + : MiraAssets.CrewmateFile.LoadAsset(); __instance.AddFileAsChildCustom(roleAddButton, ref num, ref num2, ref num3); if (roleAddButton != null && roleAddButton.Button != null) @@ -409,19 +402,16 @@ public static class TaskAdderPatches var roleAddButton = Object.Instantiate(__instance.RoleButton); roleAddButton.MyTask = null; roleAddButton.SafePositionWorld = __instance.SafePositionWorld; - roleAddButton.Text.text = prettyEnabled ? roleBehaviour.GetRoleName() : "Be_" + roleBehaviour.GetRoleName() + ".exe"; + roleAddButton.Text.text = roleBehaviour.GetRoleName(); roleAddButton.Text.EnableMasking(); roleAddButton.role = roleBehaviour; - if (prettyEnabled) - { - roleAddButton.Text.fontSizeMin = 1; - roleAddButton.FileImage.sprite = roleBehaviour.IsImpostor - ? MiraAssets.ImpostorFile.LoadAsset() - : MiraAssets.CrewmateFile.LoadAsset(); - } + roleAddButton.Text.fontSizeMin = 1; + roleAddButton.FileImage.sprite = roleBehaviour.IsImpostor + ? MiraAssets.ImpostorFile.LoadAsset() + : MiraAssets.CrewmateFile.LoadAsset(); if (roleBehaviour is ICustomRole custom) { - if (prettyEnabled && custom.Team == ModdedRoleTeams.Custom) roleAddButton.FileImage.sprite = MiraAssets.CustomTeamFile.LoadAsset(); + if (custom.Team == ModdedRoleTeams.Custom) roleAddButton.FileImage.sprite = MiraAssets.CustomTeamFile.LoadAsset(); var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray; if (custom.Team is ModdedRoleTeams.Crewmate) { @@ -458,7 +448,7 @@ public static class TaskAdderPatches taskAddButton.role = null; taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = __instance.SafePositionWorld; - taskAddButton.Text.text = modifier.ModifierName; + taskAddButton.Text.text = modifier.ModifierName.Translate(); taskAddButton.Text.fontSizeMin = 1; taskAddButton.Text.EnableMasking(); taskAddButton.FileImage.color = modifier.FreeplayFileColor; diff --git a/MiraAPI/Patches/GameModes/BodyTypePatch.cs b/MiraAPI/Patches/GameModes/BodyTypePatch.cs new file mode 100644 index 0000000..2aede92 --- /dev/null +++ b/MiraAPI/Patches/GameModes/BodyTypePatch.cs @@ -0,0 +1,20 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.CoBegin))] +internal static class BodyTypePatch +{ + public static bool Prefix(IntroCutscene __instance, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene == true) + { + __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/ConsolePatches.cs b/MiraAPI/Patches/GameModes/ConsolePatches.cs index e2f24b2..4427b16 100644 --- a/MiraAPI/Patches/GameModes/ConsolePatches.cs +++ b/MiraAPI/Patches/GameModes/ConsolePatches.cs @@ -1,14 +1,18 @@ -/* +using HarmonyLib; +using InnerNet; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch] -public static class ConsolePatches +internal static class ConsolePatches { - [HarmonyPrefix] - [HarmonyPatch(typeof(Console), nameof(Console.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(Console), nameof(Console.CanUse))] public static bool CanUsePatch(Console __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseConsole(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseTasks(__instance)) { var task = __instance.FindTask(pc.Object); @@ -31,15 +35,15 @@ public static class ConsolePatches return true; } - [HarmonyPrefix] - [HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.CanUse))] public static bool SystemCanUsePatch(SystemConsole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance)) { - return canUse = couldUse = true; + canUse = couldUse = true; + return true; } canUse = couldUse = false; @@ -50,15 +54,15 @@ public static class ConsolePatches return true; } - [HarmonyPrefix] - [HarmonyPatch(typeof(MapConsole), nameof(MapConsole.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(MapConsole), nameof(MapConsole.CanUse))] public static bool MapCanUsePatch(MapConsole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance)) { - return canUse = couldUse = true; + canUse = couldUse = true; + return true; } canUse = couldUse = false; @@ -68,4 +72,40 @@ public static class ConsolePatches canUse = couldUse = true; return true; } -}*/ + + [HarmonyPrefix, HarmonyPatch(typeof(MapConsole), nameof(MapConsole.Use))] + public static bool MapUsePatch(MapConsole __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance); + } + + return true; + } + + [HarmonyPrefix, HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.Use))] + public static bool SystemUsePatch(SystemConsole __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance); + } + + return true; + } + + [HarmonyPrefix, HarmonyPatch(typeof(Console), nameof(Console.Use))] + public static bool ConsoleUsePatch(Console __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode.CanUseTasks(__instance); + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs b/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs new file mode 100644 index 0000000..7e8144e --- /dev/null +++ b/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs @@ -0,0 +1,18 @@ +using HarmonyLib; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.SetGameMode))] +internal static class CreateOptionsPatch +{ + public static bool Prefix(CreateOptionsPicker __instance, AmongUs.GameOptions.GameModes mode) + { + if (mode <= AmongUs.GameOptions.GameModes.SeekFools) + { + return true; + } + + // TODO: finish this + return false; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs index 07a2899..fff7e40 100644 --- a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs +++ b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs @@ -1,11 +1,14 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch(typeof(DeadBody))] -public static class DeadBodyPatch +internal static class DeadBodyPatch { - [HarmonyPrefix] - [HarmonyPatch(nameof(DeadBody.OnClick))] + [HarmonyPrefix, HarmonyPatch(nameof(DeadBody.OnClick))] public static bool OnClickPatch(DeadBody __instance) { - return CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanReport(__instance); + return CustomGameModeManager.ActiveMode?.CanReport(__instance) == true; } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/GameLogicPatches.cs b/MiraAPI/Patches/GameModes/GameLogicPatches.cs index a62be83..742a56c 100644 --- a/MiraAPI/Patches/GameModes/GameLogicPatches.cs +++ b/MiraAPI/Patches/GameModes/GameLogicPatches.cs @@ -1,20 +1,78 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch] -public static class GameLogicPatches +internal static class GameLogicPatches { - [HarmonyPrefix, HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] - public static bool EndCritPatch(LogicGameFlowNormal __instance) + [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidHumansWin))] + public static bool DidHumansWin(GameOverReason reason, ref bool __result) { - var runOriginal = true; - CustomGameModeManager.ActiveMode?.CheckGameEnd(out runOriginal, __instance); - return runOriginal; + __result = reason == GameOverReason.CrewmatesByTask || reason == GameOverReason.CrewmatesByVote || + reason == GameOverReason.ImpostorDisconnect || reason == GameOverReason.HideAndSeek_CrewmatesByTimer; + return false; } - [HarmonyPrefix, HarmonyPatch(typeof(LogicRoleSelectionNormal), nameof(LogicRoleSelectionNormal.AssignRolesFromList))] - public static bool AssignRolesPatch(LogicRoleSelectionNormal __instance) + [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidImpostorsWin))] + public static bool DidImpostorsWin(GameOverReason reason, ref bool __result) { - var runOriginal = true; - CustomGameModeManager.ActiveMode?.AssignRoles(out runOriginal, __instance); - return runOriginal; + __result = reason == GameOverReason.ImpostorsByKill || reason == GameOverReason.ImpostorsBySabotage || + reason == GameOverReason.ImpostorsByVote || reason == GameOverReason.HideAndSeek_ImpostorsByKills || + reason == GameOverReason.CrewmateDisconnect; + return false; } -}*/ + + [HarmonyPrefix, HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetBodyType))] + public static bool BodyTypePatch(NormalGameManager __instance, PlayerControl player, ref PlayerBodyTypes __result) + { + if (CustomGameModeManager.ActiveMode == null || !CustomGameModeManager.ActiveMode.GameModeBodyTypeOverride) + { + return true; + } + __result = CustomGameModeManager.ActiveMode.GetBodyType(player); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] + public static bool EndGamePatch(LogicGameFlowNormal __instance) + { + if (TutorialManager.InstanceExists) + { + return true; + } + + if (!AmongUsClient.Instance.AmHost) + { + return false; + } + + if (!GameData.Instance) + { + return false; + } + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.CheckGameEnd(out var runOriginal, __instance); + + if (!runOriginal) + { + return false; + } + } + + return true; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.IsGameOverDueToDeath))] + public static void IsGameOverDueToDeathPatch(LogicGameFlowNormal __instance, ref bool __result) + { + if (CustomGameModeManager.IsActiveGameMode()) + { + __result = false; + } + } +} diff --git a/MiraAPI/Patches/GameModes/GameManagerPatches.cs b/MiraAPI/Patches/GameModes/GameManagerPatches.cs new file mode 100644 index 0000000..3f56e82 --- /dev/null +++ b/MiraAPI/Patches/GameModes/GameManagerPatches.cs @@ -0,0 +1,56 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(GameManager))] +internal static class GameManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(GameManager.OnPlayerDeath))] + public static bool OnDeathPrefix([HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] bool assignGhostRole) + { + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.OnPlayerDeath(player, assignGhostRole); + return false; + } + + return true; + } + [HarmonyPrefix, HarmonyPatch(nameof(GameManager.ShowCrewmatesKilled))] + public static bool ShowCrewmatesKilledPrefix(ref bool __result) + { + if (CustomGameModeManager.ActiveMode != null) + { + __result = CustomGameModeManager.ActiveMode is HideAndSeekMode; + return false; + } + + return true; + } + [HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetMapOptions))] + [HarmonyPrefix] + public static bool GetMapOptions(ref MapOptions __result) + { + if (CustomGameModeManager.ActiveMode == null) + { + return true; + } + + __result = CustomGameModeManager.ActiveMode.GetMapOptions(); + return false; + } +} + +[HarmonyPatch(typeof(GameStartManager))] +internal static class GameStartManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(GameStartManager.Start))] + public static void StartPatch(GameStartManager __instance) + { + if (MiraApiPlugin.IsDevBuild) + { + __instance.MinPlayers = 1; + } + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs new file mode 100644 index 0000000..4426e0a --- /dev/null +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -0,0 +1,63 @@ +using System.Collections; +using HarmonyLib; +using Il2CppSystem; +using MiraAPI.GameModes; +using MiraAPI.Translation; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(HudManager))] +internal static class HudPatches +{ + [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] + public static void HudUpdatePatch(HudManager __instance) + { + if (GameManager.Instance != null && GameManager.Instance.GameHasStarted && + CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.HudUpdate(__instance); + } + } + + + [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] + [HarmonyPostfix] + public static void PostHudStart(HudManager __instance) + { + Coroutines.Start(CoPostHudStart(__instance)); + } + + private static IEnumerator CoPostHudStart(HudManager instance) + { + yield return new WaitUntil((Func)(() => GameManager.Instance != null)); + if (GameManager.Instance.IsHideAndSeek()) + yield return null; + var infoPane = instance.gameObject.transform.FindChild("LobbyInfoPane"); + var aspect = infoPane.gameObject.transform.Find("AspectSize"); + var modeLabel = aspect!.Find("ModeLabel"); + var modeValue = aspect.Find("ModeValue"); + var modelText = modeLabel!.Find("Text_TMP").gameObject; + var modelTextClone = Object.Instantiate(modelText, modeLabel); + Object.Destroy(modelTextClone.GetComponent()); + modelTextClone.GetComponent().text = "Gamemode"; + var gmText = modeValue!.Find("GameModeText").gameObject; + var gmTextClone = Object.Instantiate(gmText, modeValue); + gmText.SetActive(false); + modelText.SetActive(false); + _text = gmTextClone.GetComponent(); + _text.text = CustomGameModeManager.ActiveMode != null ? CustomGameModeManager.ActiveMode.ColoredName : MiraLocaleManager.Get("MiraApi.Gamemode.Classic"); + CustomGameModeManager.ActiveMode?.HudStart(instance); + } + + private static TextMeshPro? _text; + internal static void SetGameModeText(string text) + { + if (_text != null) + _text.text = text; + } +} diff --git a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs index ad0d9e0..de9bbf1 100644 --- a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs +++ b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs @@ -1,9 +1,13 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + /// -/// Allow Impostors to kill each other if can kill is enabled in gamemode or friendly fire is toggled on +/// Allow Impostors to kill each other if can kill is enabled in gamemode or friendly fire is toggled on. /// [HarmonyPatch(typeof(ImpostorRole), "IsValidTarget")] -public static class ImpostorTargetPatch +internal static class ImpostorTargetPatch { public static bool Prefix(ImpostorRole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo target, ref bool __result) { @@ -16,9 +20,8 @@ public static class ImpostorTargetPatch } __result = target is { Disconnected: false, IsDead: false } && - target.PlayerId != __instance.Player.PlayerId && !(target.Role == null) && - !(target.Object == null) && !target.Object.inVent && !target.Object.inMovingPlat; + target.PlayerId != __instance.Player.PlayerId && target.Role != null && + target.Object != null && !target.Object.inVent && !target.Object.inMovingPlat; return false; - } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs b/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs new file mode 100644 index 0000000..5701014 --- /dev/null +++ b/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs @@ -0,0 +1,20 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.CoBegin))] +internal static class IntroCutscenePatch +{ + public static bool Prefix(IntroCutscene __instance, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene == true) + { + __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs index 9a4bd8b..06ea282 100644 --- a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs +++ b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs @@ -1,9 +1,12 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch(typeof(MapBehaviour))] -public static class MapBehaviourPatch +internal static class MapBehaviourPatch { - [HarmonyPrefix] - [HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))] + [HarmonyPrefix, HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))] public static bool ShowSabotagePatch(MapBehaviour __instance) { var shouldShow = CustomGameModeManager.ActiveMode?.ShouldShowSabotageMap(__instance); @@ -15,4 +18,4 @@ public static class MapBehaviourPatch return true; } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs new file mode 100644 index 0000000..e7c4f8b --- /dev/null +++ b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs @@ -0,0 +1,19 @@ +/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(NormalGameManager))] +internal static class NormalGameManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(NormalGameManager.IsNormal))] + public static void IsNormal(ref bool __result) + { + if (CustomGameModeManager.ActiveMode != null) + { + __result = !CustomGameModeManager.ActiveMode.IsHideAndSeek; + } + } +} +*/ diff --git a/MiraAPI/Patches/GameModes/OnDeathPatch.cs b/MiraAPI/Patches/GameModes/OnDeathPatch.cs deleted file mode 100644 index 99f9211..0000000 --- a/MiraAPI/Patches/GameModes/OnDeathPatch.cs +++ /dev/null @@ -1,11 +0,0 @@ -/* -[HarmonyPatch(typeof(KillAnimation))] -public static class OnDeathPatch -{ - [HarmonyPostfix] - [HarmonyPatch(nameof(KillAnimation.CoPerformKill))] - public static void OnDeathPostfix([HarmonyArgument(0)] PlayerControl source, [HarmonyArgument(1)] PlayerControl target) - { - CustomGameModeManager.ActiveMode?.OnDeath(target); - } -}*/ diff --git a/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs new file mode 100644 index 0000000..53620e0 --- /dev/null +++ b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; +[HarmonyPatch(typeof(ProgressTracker), nameof(ProgressTracker.Start))] +internal static class ProgressTrackerPatch +{ + public static bool Prefix(ProgressTracker __instance) + { + if (CustomGameModeManager.ActiveMode != null && !CustomGameModeManager.ActiveMode.ShowTaskBar) + { + __instance.gameObject.SetActive(false); + return false; + } + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/TaskPanelPatch.cs b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs new file mode 100644 index 0000000..30a30cc --- /dev/null +++ b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; +[HarmonyPatch(typeof(TaskPanelBehaviour), nameof(TaskPanelBehaviour.Update))] +internal static class TaskPanelPatch +{ + public static bool Prefix(TaskPanelBehaviour __instance) + { + if (__instance != Roles.HudManagerPatches.RoleTab && CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.UpdateTaskPanel(__instance); + return false; + } + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/VentPatches.cs b/MiraAPI/Patches/GameModes/VentPatches.cs deleted file mode 100644 index 597c40a..0000000 --- a/MiraAPI/Patches/GameModes/VentPatches.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* -[HarmonyPatch(typeof(Vent))] -public static class VentPatches -{ - [HarmonyPrefix] - [HarmonyPatch(nameof(Vent.CanUse))] - public static bool CanUseVentPatch(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse) - { - if (CustomGameModeManager.ActiveMode?.CanVent(__instance, pc) == false) - { - return couldUse = canUse = false; - } - - if (pc.Role is not ICustomRole role) - { - return couldUse = canUse = true; - } - - var num = float.MaxValue; - var @object = pc.Object; - var customRoleUsable = role.CanUseVent; - - canUse = couldUse = customRoleUsable && !pc.IsDead && (@object.CanMove || @object.inVent); - if (canUse) - { - var center = @object.Collider.bounds.center; - var position = __instance.transform.position; - num = Vector2.Distance(center, position); - canUse &= num <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(@object.Collider, center, position, Constants.ShipOnlyMask, false); - } - __result = num; - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(Vent.SetOutline))] - public static bool SetOutlinePatch(Vent __instance, [HarmonyArgument(0)] bool on, [HarmonyArgument(1)] bool mainTarget) - { - var color = PlayerControl.LocalPlayer.Data.Role is ICustomRole role - ? role.RoleColor : PlayerControl.LocalPlayer.Data.Role.IsImpostor ? Palette.ImpostorRed : Palette.CrewmateBlue; - __instance.myRend.material.SetFloat(ShaderID.Outline, on ? 1 : 0); - __instance.myRend.material.SetColor(ShaderID.OutlineColor, color); - __instance.myRend.material.SetColor(ShaderID.AddColor, mainTarget ? color : Color.clear); - - return false; - } -}*/ diff --git a/MiraAPI/Patches/GameModes/WinnersPatch.cs b/MiraAPI/Patches/GameModes/WinnersPatch.cs new file mode 100644 index 0000000..c7c9385 --- /dev/null +++ b/MiraAPI/Patches/GameModes/WinnersPatch.cs @@ -0,0 +1,27 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))] +internal static class WinnersPatch +{ + [HarmonyPostfix] + public static void ShowWinningScreen() + { + if (CustomGameModeManager.ActiveMode is null or ClassicMode) + return; + + var winners = CustomGameModeManager.ActiveMode.CalculateWinners(); + + if (winners is null) + return; + + EndGameResult.CachedWinners.Clear(); + + foreach (var data in winners) + { + EndGameResult.CachedWinners.Add(new(data)); + } + } +} diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 76adc2c..f8835a8 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,9 +1,11 @@ using System.Collections; using System.Collections.Generic; +using System.Globalization; using HarmonyLib; using MiraAPI.Hud; using MiraAPI.Keybinds; using MiraAPI.LocalSettings; +using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -40,12 +42,16 @@ public static class HudManagerPatches ResizeUI(LocalSettingsTabSingleton.Instance.ButtonUIFactorSlider.Value); } + private static Transform _storedButtonsParent; public static void ResizeUI(float scaleFactor) { - var baseButtons = HudManager.Instance.transform.FindChild("Buttons"); - if (baseButtons != null) + if (!_storedButtonsParent) { - foreach (var aspect in baseButtons.GetComponentsInChildren(true)) + _storedButtonsParent = HudManager.Instance.transform.FindChild("Buttons"); + } + if (_storedButtonsParent) + { + foreach (var aspect in _storedButtonsParent.GetComponentsInChildren(true)) { if (!aspect.gameObject) { @@ -80,9 +86,9 @@ public static class HudManagerPatches button.gameObject.SetActive(!button.isActiveAndEnabled); } - if (baseButtons != null) + if (_storedButtonsParent) { - foreach (var arrange in baseButtons.GetComponentsInChildren(true)) + foreach (var arrange in _storedButtonsParent.GetComponentsInChildren(true)) { if (!arrange.gameObject || !arrange.transform) { @@ -123,6 +129,47 @@ public static class HudManagerPatches CustomGameModeManager.ActiveMode?.HudStart(__instance); }*/ + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(FriendsListManager), nameof(FriendsListManager.SetFriendButtonColor))] + public static bool SetFriendButtonColor(FriendsListManager __instance, bool isGrayedOut) + { + __instance.FriendsListButton?.SetGlyphColor(isGrayedOut); + return false; + } + + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Toggle))] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Close))] + public static void TogglePrefix(ChatController __instance) + { + if (!MiraHudHelper.ClonedChatButton) + { + return; + } + __instance.chatButton.transform.localPosition = MiraHudHelper.ClonedChatButton.transform.localPosition + new Vector3(-0.3f, 0); + } + + [HarmonyPostfix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Toggle))] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Close))] + public static void TogglePostfix(ChatController __instance) + { + MiraHudHelper.UiGrid.ArrangeChilds(); + } + + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChatNote))] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChatWarning))] + public static void ChatBubbleUpdatePrefix(ChatController __instance) + { + __instance.chatNotifyDot.transform.localPosition = new Vector3(-0.34f, 0.373f, -1f); + } + /// /// Create custom buttons and arrange them on the hud. /// @@ -131,6 +178,7 @@ public static class HudManagerPatches [HarmonyPatch(nameof(HudManager.Start))] public static void StartPostfix(HudManager __instance) { + __instance.gameObject.AddComponent(); if (Buttons == null) { Buttons = __instance.transform.Find("Buttons"); @@ -237,6 +285,8 @@ public static class HudManagerPatches } MiraApiSettings.OldButtonScaleFactor = LocalSettingsTabSingleton.Instance.ButtonUIFactorSlider.Value; + MiraApiSettings.OldUiButtonScaleFactor = + LocalSettingsTabSingleton.Instance.TopRightButtonsFactorSlider.Value; Coroutines.Start(CoResizeUI()); } diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index 4ee990c..3a7f235 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -1,9 +1,13 @@ -using System.Reflection; +using System.Linq; +using System.Reflection; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.MeetingAbilities; +using MiraAPI.GameModes; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; namespace MiraAPI.Patches; @@ -29,6 +33,11 @@ public static class IntroCutscenePatches { var @event = new IntroBeginEvent(__instance); MiraEventManager.InvokeEvent(@event); + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.Initialize(); + } } [HarmonyPatch] @@ -78,8 +87,8 @@ public static class IntroCutscenePatches { __instance.BackgroundBar.material.SetColor(ShaderID.Color, introConfig.IntroTeamColor); __instance.TeamTitle.color = introConfig.IntroTeamColor; - __instance.TeamTitle.text = introConfig.IntroTeamTitle; - __instance.ImpostorText.text = introConfig.IntroTeamDescription; + __instance.TeamTitle.text = introConfig.IntroTeamTitle.Translate(); + __instance.ImpostorText.text = introConfig.IntroTeamDescription.Translate(); } } @@ -120,9 +129,9 @@ public static class IntroCutscenePatches { introCutscene = __instance.Cast(); } - Info("IntroCutscene ended"); + MeetingButtonManager.OnGameStart(); MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene)); var @event = new BeforeRoundStartEvent(true); diff --git a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs index a8cab15..5b89fa8 100644 --- a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -69,6 +69,7 @@ public static class OptionsMenuPatches int i = 0; int tabIdx = 0; int page = 1; + LocalSettingsTab.TabGroups.Clear(); foreach (var settings in LocalSettingsManager.AvailableTabs) { var tab = settings.CreateTab(__instance); @@ -213,6 +214,8 @@ public static class OptionsMenuPatches if (background != null) background.color = tab.TabAppearance.TabColor; + + tab.Open(); } private static void CustomClose(LocalSettingsTab tab) { diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 2bc615d..46108fc 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -3,9 +3,13 @@ using System.Collections; using System.Linq; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppSystem.Collections.Generic; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.Presets; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; @@ -13,6 +17,7 @@ using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using UnityEngine.ProBuilder; using UnityEngine.UI; using Object = UnityEngine.Object; @@ -21,6 +26,15 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(GameOptionsMenu))] internal static class GameOptionsMenuPatch { + private static List _vanillaHeaders = new(); + private static List _vanillaOptions = new(); + private static List _mainOptions = new(); + private static System.Collections.Generic.Dictionary> _gameModeGroups = new(); + private static System.Collections.Generic.Dictionary> _gameModeOptions = new(); + private static System.Collections.Generic.Dictionary> _gameModeHeaders = new(); + private static TextMeshPro _gamemodeDescription = null!; + private static SpriteRenderer _modIcon = null!; + [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] // ReSharper disable once InconsistentNaming @@ -41,15 +55,10 @@ internal static class GameOptionsMenuPatch var mapNameSetting = GameManager.Instance.GameSettingsList.MapNameSetting; __instance.MapPicker.SetUpFromData(mapNameSetting, 20); __instance.Children.Add(__instance.MapPicker); - __instance.CreateSettings(); - foreach (var optionBehaviour in __instance.Children) + CreateSettings(__instance, container); + if (CustomGameModeManager.ActiveMode != null) { - if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost) - { - optionBehaviour.SetAsPlayer(); - } - - optionBehaviour.OnValueChanged = new Action(__instance.ValueChanged); + ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance); } } else @@ -69,6 +78,314 @@ internal static class GameOptionsMenuPatch return false; } + public static float AdditionalVanillaScrollNum; + internal static void ToggleGamemodeOptions(AbstractGameMode gameMode, GameOptionsMenu instance) + { + float num = -1.217f; + instance.Children.Clear(); + foreach (var opt in _mainOptions) + { + instance.Children.Add(opt); + } + + var icon = gameMode.Icon; + if (icon != null) + { + _modIcon.gameObject.SetActive(true); + _gamemodeDescription.transform.localPosition = new Vector3(0.3f, 0, -0.01f); + _gamemodeDescription.horizontalAlignment = HorizontalAlignmentOptions.Left; + var texture = icon.LoadAsset().texture; + var scale = Mathf.Min(128f / texture.width, 128f / texture.height); + + var newSprite = Sprite.Create( + texture, + new Rect(0, 0, texture.width, texture.height), + Vector2.one / 2, + 100f / scale + ); + + _modIcon.sprite = newSprite; + _modIcon.drawMode = SpriteDrawMode.Simple; + } + else + { + _gamemodeDescription.transform.localPosition = new Vector3(0f, 0, -0.01f); + _gamemodeDescription.horizontalAlignment = HorizontalAlignmentOptions.Center; + _modIcon.gameObject.SetActive(false); + } + + _gamemodeDescription.text = MiraLocaleManager.Get(gameMode.Description); + if (gameMode.ShowNormalGameSettings) + { + foreach (var opt in _vanillaHeaders) + { + num -= 0.63f; + opt.gameObject.SetActive(true); + } + foreach (var opt in _vanillaOptions) + { + opt.gameObject.SetActive(true); + instance.Children.Add(opt); + num -= 0.45f; + } + } + else + { + foreach (var opt in _vanillaHeaders) + { + opt.gameObject.SetActive(false); + } + foreach (var opt in _vanillaOptions) + { + opt.gameObject.SetActive(false); + } + } + + foreach (var pair in _gameModeGroups) + { + if (pair.Value.Count != 0) + { + if (gameMode == pair.Key) + { + foreach (var opt in _gameModeHeaders[gameMode]) + { + num -= 0.63f; + opt.gameObject.SetActive(true); + } + + foreach (var opt in _gameModeOptions[gameMode]) + { + opt.gameObject.SetActive(true); + instance.Children.Add(opt); + num -= 0.45f; + } + } + else + { + foreach (var opt in _gameModeHeaders[pair.Key]) + { + opt.gameObject.SetActive(false); + } + + foreach (var opt in _gameModeOptions[pair.Key]) + { + opt.gameObject.SetActive(false); + } + } + } + } + + instance.ControllerSelectable.Clear(); + foreach (var obj in instance.scrollBar.GetComponentsInChildren()) + { + if (obj.gameObject.activeSelf) + { + instance.ControllerSelectable.Add(obj); + } + } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } + + + private static void CreateSettings(GameOptionsMenu instance, Transform container) + { + float num = 0.713f; + _vanillaHeaders.Clear(); + _vanillaOptions.Clear(); + _mainOptions.Clear(); + _gameModeHeaders.Clear(); + _gameModeOptions.Clear(); + _gameModeGroups.Clear(); + + CategoryHeaderMasked gmCategory = Object.Instantiate(instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, container); + gmCategory.SetHeader(GameModeOption.CustomName, 20); + gmCategory.transform.localScale = Vector3.one * 0.63f; + gmCategory.transform.localPosition = new Vector3(-0.903f, num, -2f); + GameModeOption.OptionBehaviour = Object.Instantiate( + instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + num -= 0.63f; + GameModeOption.OptionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + GameModeOption.OptionBehaviour.SetClickMask(instance.ButtonClickMask); + StringGameSetting setting = ScriptableObject.CreateInstance(); + setting.Type = OptionTypes.MultipleChoice; + setting.Title = GameModeOption.GamemodeName; + setting.Index = GameModeOption._lastValue; + setting.Values = new Il2CppStructArray([GameModeOption.Values[0]]); + GameModeOption.OptionBehaviour.SetUpFromData(setting, 20); + GameModeOption.Set(GameModeOption._lastValue); + GameModeOption.OptionBehaviour.TitleText.fontSize = 3; + instance.Children.Add(GameModeOption.OptionBehaviour); + foreach (var optionBehaviour in instance.Children) + { + _mainOptions.Add(optionBehaviour); + } + for (var i = 1; i < GameModeOption.Values.Count; i++) + { + GameModeOption.OptionBehaviour.Values = + (Il2CppStructArray)GameModeOption.OptionBehaviour.Values.Add( + GameModeOption.Values.ElementAt(i).Value); + } + + var modeInfoHolder = new GameObject("GamemodeInfo") + { + transform = + { + parent = container, + localPosition = new Vector3(1.2f, -0.675f, 1), + localScale = Vector3.one, + }, + layer = container.gameObject.layer, + }.transform; + + var gamemodeBg = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(0).gameObject, Vector3.zero, Quaternion.identity, modeInfoHolder); + gamemodeBg.transform.localPosition = Vector3.zero; + gamemodeBg.transform.localScale = new Vector3(1.35f, 0.85f, 1); + gamemodeBg.name = "Background"; + + var modIconObj = new GameObject("ModIcon") + { + transform = + { + parent = modeInfoHolder, + localPosition = new Vector3(-1.75f, 0, -0.1f), + localScale = new Vector3(0.45f, 0.45f, 1), + }, + layer = gamemodeBg.layer, + }; + _modIcon = modIconObj.AddComponent(); + _modIcon.sprite = MiraAssets.BlankSprite.LoadAsset(); + _modIcon.material = gamemodeBg.gameObject.GetComponent().material; + + var gamemodeTextObj = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(1).gameObject, Vector3.zero, Quaternion.identity, modeInfoHolder); + gamemodeTextObj.transform.localPosition = new Vector3(0.3f, 0, -0.01f); + gamemodeTextObj.transform.localScale = new Vector3(2.12f, 2.12f, 1); + _gamemodeDescription = gamemodeTextObj.GetComponentInChildren(); + _gamemodeDescription.fontSizeMin = 0.65f; + _gamemodeDescription.fontSizeMax = 0.9f; + _gamemodeDescription.alignment = TextAlignmentOptions.Left; + + num -= 1.3f; + AdditionalVanillaScrollNum = 0f; + foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) + { + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate( + instance.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + container); + categoryHeaderMasked.SetHeader(rulesCategory.CategoryName, 20); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); + _vanillaHeaders.Add(categoryHeaderMasked); + num -= 0.63f; + AdditionalVanillaScrollNum -= 0.63f; + foreach (BaseGameSetting baseGameSetting in rulesCategory.AllGameSettings) + { + switch (baseGameSetting.Type) + { + case OptionTypes.Checkbox: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.checkboxOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.String: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.Float: + case OptionTypes.Int: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.numberOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.Player: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.playerOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + } + + num -= 0.45f; + AdditionalVanillaScrollNum -= 0.45f; + } + } + foreach (var optionBehaviour in _vanillaOptions) + { + if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost) + { + optionBehaviour.SetAsPlayer(); + } + instance.Children.Add(optionBehaviour); + } + + foreach (var optionBehaviour in instance.Children) + { + optionBehaviour.OnValueChanged = new Action(instance.ValueChanged); + } + + foreach (var mode in CustomGameModeManager.IdToModeMap.Values) + { + var filteredGroups = + ModdedOptionsManager.GameModeOptionGroups + .Where(x => x.Key == mode.GetType()).Select(y => y.Value).SelectMany(y => y) ?? []; + _gameModeGroups.Add(mode, filteredGroups.ToList()); + + var optionBehaviours = new System.Collections.Generic.List(); + var categoryHeaders = new System.Collections.Generic.List(); + var newNum = mode.ShowNormalGameSettings ? num : num - AdditionalVanillaScrollNum; + foreach (var group in filteredGroups) + { + CreateGroup(instance, group, container, ref newNum, ref optionBehaviours, ref categoryHeaders); + group.Ready = true; + } + _gameModeHeaders.Add(mode, categoryHeaders); + _gameModeOptions.Add(mode, optionBehaviours); + } + + instance.ControllerSelectable.Clear(); + foreach (var obj in instance.scrollBar.GetComponentsInChildren()) + { + instance.ControllerSelectable.Add(obj); + } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } + [HarmonyPostfix] [HarmonyPatch(nameof(GameOptionsMenu.OnEnable))] public static void OpenPatch(GameOptionsMenu __instance) @@ -88,11 +405,10 @@ internal static class GameOptionsMenuPatch // ReSharper disable once InconsistentNaming public static void UpdatePatch(GameOptionsMenu __instance) { - if (MenuState.Instance.CurrentModIdx == 0) + if (MenuState.Instance.CurrentModIdx == 0 && _gameModeGroups.Count <= 0) { return; } - var num = 2.1f; switch (MenuState.Instance.CurrentMenu) @@ -107,9 +423,15 @@ internal static class GameOptionsMenuPatch CustomMenuTwoUpdate(ref num); break; default: + if (MenuState.Instance.CurrentModIdx == 0) + { + num = -1.217f; + GamemodeOptionsUpdate(ref num); + break; + } var filteredGroups = MenuState.Instance.CurrentMod.InternalOptionGroups - .Where(x => x.OptionableType == null && + .Where(x => (x.OptionableType == null || CustomGameModeManager.ActiveMode!.GetType().IsAssignableTo(x.OptionableType)) && x.ParentMenu == MenuCategory.Game) ?? []; foreach (var group in filteredGroups) @@ -122,28 +444,126 @@ internal static class GameOptionsMenuPatch __instance.scrollBar.SetYBoundsMax(-num - 1.65f); } - - private static void UpdateGroup(AbstractOptionGroup? group, ref float num) + private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref System.Collections.Generic.List optionBehaviours, ref System.Collections.Generic.List categoryHeaders) { - if (group is null || !group.Ready || group.Options.Count == 0 || group.Header is null) + var categoryHeaderMasked = Object.Instantiate( + menu.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + container); + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num + 1, -2f); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaders.Add(categoryHeaderMasked); + + num -= 0.58f; + + categoryHeaderMasked.SetHeader(MiraLocaleManager.GetOrCreateLocaleString(group.GroupName), 20); + categoryHeaderMasked.Background.color = group.GroupColor; + categoryHeaderMasked.Divider.color = group.GroupColor; + categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + + categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); + categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear; + categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp; + + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0); + categoryHeaderMasked.Background.size = new Vector2( + categoryHeaderMasked.Background.size.x + 1.5f, + categoryHeaderMasked.Background.size.y); + + categoryHeaderMasked.gameObject.SetActive(true); + group.Header = categoryHeaderMasked; + categoryHeaderMasked.transform.localPosition += Vector3.down; + + var options = group.Options.Select(opt => opt.CreateOption( + menu.checkboxOrigin, + menu.numberOptionOrigin, + menu.stringOptionOrigin, + menu.playerOptionOrigin, + container)); + + foreach (var newOpt in options) + { + newOpt.SetClickMask(menu.ButtonClickMask); + SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); + foreach (var renderer in componentsInChildren) + { + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) + { + renderer.color = group.GroupColor.FindAlternateColor(); + if (renderer.transform.parent.TryGetComponent(out var btn)) + { + btn.interactableColor = group.GroupColor.FindAlternateColor(); + btn.interactableHoveredColor = Color.white; + } + } + + renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); + } + + foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) + { + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) + { + textMeshPro.color = group.GroupColor; + } + + textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); + } + + if (newOpt is ToggleOption toggle) + { + toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); + toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") + .GetComponent(); + rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); + rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + } + + optionBehaviours.Add(newOpt); + newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); + num -= 0.45f; + + newOpt.Initialize(); + newOpt.gameObject.SetActive(true); + } + } + + internal static void UpdateGroup(AbstractOptionGroup? group, ref float num) + { + if (group is null || !group.Ready || group.Options.Count == 0 || group.Header == null) { return; } if (!group.GroupVisible.Invoke()) { - group.Header.gameObject.SetActive(false); + if (group.Header != null && group.Header.gameObject != null) + group.Header.gameObject.SetActive(false); foreach (var option in group.Options) { - option.OptionBehaviour?.gameObject.SetActive(false); + if (option.OptionBehaviour != null && option.OptionBehaviour.gameObject != null) + option.OptionBehaviour?.gameObject.SetActive(false); } return; } - group.Header.gameObject.SetActive(true); - group.Header.transform.localScale = Vector3.one * 0.63f; - group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + if (group.Header != null) + { + if (group.Header.gameObject != null) + group.Header.gameObject.SetActive(true); + group.Header.transform.localScale = Vector3.one * 0.63f; + group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + } num -= 0.58f; @@ -175,6 +595,26 @@ internal static class GameOptionsMenuPatch } } + private static void GamemodeOptionsUpdate(ref float num) + { + var mode = CustomGameModeManager.ActiveMode; + if (mode == null) return; + + if (mode.ShowNormalGameSettings) + { + num += AdditionalVanillaScrollNum; + } + + if (!_gameModeGroups.TryGetValue(mode, out var groups) || groups.Count == 0) + { + return; + } + + foreach (var group in groups) + { + UpdateGroup(group, ref num); + } + } private static void CustomMenuOneUpdate(ref float num) { var groups = MenuState.Instance.CurrentMod.InternalOptionGroups @@ -257,7 +697,7 @@ internal static class GameOptionsMenuPatch Quaternion.identity, container); - categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); + categoryHeaderMasked.SetHeader(MiraLocaleManager.GetOrCreateLocaleString(group.GroupName), 20); categoryHeaderMasked.Background.color = group.GroupColor; categoryHeaderMasked.Divider.color = group.GroupColor; categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) @@ -276,7 +716,7 @@ internal static class GameOptionsMenuPatch categoryHeaderMasked.gameObject.SetActive(false); var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); - newText.text = "(Click to close)"; + newText.text = $"({"MiraApi.GameSetting.Global.ClickToClose".Translate()})"; newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); newText.gameObject.GetComponent().Destroy(); @@ -293,8 +733,8 @@ internal static class GameOptionsMenuPatch { group.AllOptionsHidden = !group.AllOptionsHidden; newText.text = group.AllOptionsHidden - ? "(Click to open)" - : "(Click to close)"; + ? $"({"MiraApi.GameSetting.Global.ClickToOpen".Translate()})" + : $"({"MiraApi.GameSetting.Global.ClickToClose".Translate()})"; })); headerBtn.SetButtonEnableState(true); diff --git a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs index bc6dc3a..c1db00b 100644 --- a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs +++ b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs @@ -7,6 +7,7 @@ using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using MiraAPI.Presets; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; @@ -45,7 +46,7 @@ internal static class GamePresetsTabPatches if (MenuState.Instance.CurrentModIdx != 0) { - __instance.PresetDescriptionText.text = "Select or create an options preset for this mod"; + __instance.PresetDescriptionText.text = "MiraApi.GameSetting.Preset.Hint".Translate(); } else { @@ -184,7 +185,7 @@ internal static class GamePresetsTabPatches // set the button text and alignment var saveText = saveButton.buttonText; - saveText.text = "Save"; + saveText.text = "MiraApi.GameSetting.Preset.Save".Translate(); saveText.GetComponent().Destroy(); saveText.alignment = TextAlignmentOptions.Center; saveText.transform.parent.localPosition = new Vector3( diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index c31b519..d4df499 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,17 +1,4 @@ -using System; -using System.Collections.Generic; -using HarmonyLib; -using MiraAPI.GameOptions; -using MiraAPI.Modifiers; -using MiraAPI.PluginLoading; -using MiraAPI.Presets; -using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; -using TMPro; -using UnityEngine; -using UnityEngine.Events; -using static UnityEngine.UI.Button; -using Object = UnityEngine.Object; +using HarmonyLib; namespace MiraAPI.Patches.Options; diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 51e2e92..7c96f7d 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -1,12 +1,14 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; @@ -23,13 +25,49 @@ public static class LobbyViewPanePatches { private static int SelectedModIdx { get; set; } + private static MiraPluginInfo[] Plugins => MiraPluginManager.Instance.RegisteredPluginsWithOptions; + private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0 ? null - : MiraPluginManager.Instance.RegisteredPluginsWithOptions[SelectedModIdx - 1]; + : Plugins[SelectedModIdx - 1]; private static PassiveButton? ModifiersTabButton { get; set; } + internal static StringNames ModifiersTabName { get; } = MiraLocaleManager.GetOrCreateLocaleString("ModifiersTab"); - private static StringNames ModifiersTabName { get; } = CustomStringName.CreateAndRegister("ModifiersTab"); + private static AbstractGameMode? GetCustomGamemode() + { + if (!CustomGameModeManager.IsClassic() && !CustomGameModeManager.IsHideNSeek()) + { + return CustomGameModeManager.ActiveMode; + } + + return null; + } + + private static IEnumerable? GetCustomGamemodeOptions() + { + if (CustomGameModeManager.ActiveMode == null) + { + return null; + } + + if (!CustomGameModeManager.IsClassic() && !CustomGameModeManager.IsHideNSeek()) + { + var plugin = CustomGameModeManager.FindParentMod(CustomGameModeManager.ActiveMode); + + return plugin?.InternalOptionGroups + .Where(x => x.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true && x.GroupVisible()); + } + + return null; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.OnEnable))] + public static void OnEnable(LobbyViewSettingsPane __instance) + { + Refresh(__instance); + } [HarmonyPostfix] [HarmonyPatch(nameof(LobbyViewSettingsPane.Awake))] @@ -45,7 +83,7 @@ public static class LobbyViewPanePatches var pos = ModifiersTabButton.transform.localPosition; pos.x = 2.1f; ModifiersTabButton.transform.localPosition = pos; - ModifiersTabButton.buttonText.text = "Modifiers"; + ModifiersTabButton.buttonText.text = "Modifiers".Translate(); ModifiersTabButton.OnClick = new Button.ButtonClickedEvent(); ModifiersTabButton.OnClick.AddListener( (UnityAction)(() => @@ -75,7 +113,7 @@ public static class LobbyViewPanePatches (UnityAction)(() => { SelectedModIdx += 1; - if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length) + if (SelectedModIdx > Plugins.Length) { SelectedModIdx = 0; } @@ -99,7 +137,7 @@ public static class LobbyViewPanePatches SelectedModIdx -= 1; if (SelectedModIdx < 0) { - SelectedModIdx = MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length; + SelectedModIdx = Plugins.Length; } Refresh(__instance); @@ -108,8 +146,25 @@ public static class LobbyViewPanePatches private static void Refresh(LobbyViewSettingsPane menu) { - ModifiersTabButton?.gameObject.SetActive(SelectedModIdx != 0); - menu.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Main"; + if (SelectedMod == null) + { + var cgm = GetCustomGamemode(); + ModifiersTabButton?.gameObject.SetActive(false); + menu.rolesTabButton.gameObject.SetActive(cgm is null or { ShowNormalRoleSettings: true }); + menu.gameModeText.text = cgm is null ? MiraLocaleManager.Get("MiraApi.Gamemode.Classic") : cgm.ColoredName; + } + else + { + var showModifierButton = SelectedMod.InternalOptionGroups.Any(m => + m.ParentMenu is MenuCategory.Modifiers || m.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true); + var showRolesButton = SelectedMod.InternalOptionGroups.Any(r => + r.ParentMenu is MenuCategory.Roles || r.OptionableType?.IsAssignableTo(typeof(ICustomRole)) == true); + + ModifiersTabButton?.gameObject.SetActive(showModifierButton); + menu.rolesTabButton.gameObject.SetActive(showRolesButton); + menu.gameModeText.text = SelectedMod.PluginInfo.Metadata.Name; + } + menu.RefreshTab(); menu.scrollBar.ScrollToTop(); } @@ -141,8 +196,15 @@ public static class LobbyViewPanePatches [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))] public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance) { - if (SelectedModIdx == 0 || SelectedMod == null) + if (SelectedMod == null) { + var gamemodeOpts = GetCustomGamemodeOptions(); + if (gamemodeOpts != null) + { + DrawOptions(__instance, gamemodeOpts); + return false; + } + return true; } @@ -300,7 +362,7 @@ public static class LobbyViewPanePatches true); categoryHeaderMasked.SetHeader(StringNames.Name, 61); - categoryHeaderMasked.Title.text = group.GroupName; + categoryHeaderMasked.Title.text = group.GroupName.Translate(); categoryHeaderMasked.transform.localScale = Vector3.one; categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, num, -2f); menu.settingsInfo.Add(categoryHeaderMasked.gameObject); @@ -349,6 +411,13 @@ public static class LobbyViewPanePatches else { viewSettingsInfoPanel.SetInfo(data.Title, data.GetValueString(option.GetFloatData()), 61); + + if (viewSettingsInfoPanel.titleText.text.Contains("Game Mode")) + { + viewSettingsInfoPanel.titleText.text = TranslationController.Instance.GetString( + data.Title, CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic" + ); + } } menu.settingsInfo.Add(viewSettingsInfoPanel.gameObject); @@ -385,7 +454,7 @@ public static class LobbyViewPanePatches // sort the groups by priority var sortedRoleGroups = roleGroups .OrderBy(x => x.Key.Priority) - .ThenBy(x => x.Key.Name); + .ThenBy(x => x.Key.Name.Translate()); foreach (var grouping in sortedRoleGroups) { @@ -396,12 +465,7 @@ public static class LobbyViewPanePatches var group = grouping.Key; - var name = group.Name switch - { - "Crewmate" => StringNames.CrewmateRolesHeader, - "Impostor" => StringNames.ImpostorRolesHeader, - _ => CustomStringName.CreateAndRegister(group.Name), - }; + var name = MiraLocaleManager.GetOrCreateLocaleString(group.Name); var categoryHeaderRoleVariant = Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true); categoryHeaderRoleVariant.SetHeader(name, 61); diff --git a/MiraAPI/Patches/Options/MenuState.cs b/MiraAPI/Patches/Options/MenuState.cs index d459202..44be9a5 100644 --- a/MiraAPI/Patches/Options/MenuState.cs +++ b/MiraAPI/Patches/Options/MenuState.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Presets; +using MiraAPI.Translation; +using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; @@ -60,6 +64,8 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] internal Dictionary FinishedRoleMenus { get; } = []; + [HideFromIl2Cpp] + internal Dictionary QueuedRoleMenuRefresh { get; } = []; public void Awake() { @@ -144,7 +150,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) _largeButtons[MenuButton.Modifiers] = mBtn; mBtn.name = "ModifiersButton"; mBtn.buttonText.gameObject.GetComponent().Destroy(); - mBtn.buttonText.text = "Modifiers"; + mBtn.buttonText.text = "Modifiers".Translate(); mBtn.OnClick = new Button.ButtonClickedEvent(); mBtn.OnClick.AddListener( (UnityAction)(() => @@ -213,7 +219,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) })); var roleText = smRolesBtn.buttonText; - roleText.text = "Roles"; + roleText.text = "Roles".Translate(); roleText.GetComponent().Destroy(); roleText.alignment = TextAlignmentOptions.Center; roleText.transform.parent.localPosition = new Vector3( @@ -238,7 +244,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) var smModBtn = Instantiate(smRolesBtn, smRolesBtn.transform.parent); _smallButtons[MenuButton.Modifiers] = smModBtn; smModBtn.name = "SmallModifiersButton"; - smModBtn.buttonText.text = "Modifiers"; + smModBtn.buttonText.text = "Modifiers".Translate(); smModBtn.OnClick = new Button.ButtonClickedEvent(); smModBtn.OnClick.AddListener( (UnityAction)(() => @@ -383,7 +389,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) } else { - var modName = CurrentMod.MiraPlugin.OptionsTitleText; + var modName = CurrentMod.MiraPlugin.OptionsTitleText.Translate(); _text.text = $"(Page {CurrentModIdx + 1}/{ModCount + 1})\n" + modName[..Math.Min(modName.Length, 25)]; } @@ -404,14 +410,16 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) if (CurrentModIdx == 0) { Gsm.GameSettingsButton.gameObject.SetActive(true); - Gsm.RoleSettingsButton.gameObject.SetActive(true); + Gsm.RoleSettingsButton.gameObject.SetActive( + CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); } else { var hasOptions = CurrentMod.InternalOptionGroups.Exists(g => g.OptionableType == null && g.ParentMenu == MenuCategory.Game); - var hasRoles = CurrentMod.InternalRoles.Count > 0; + var customRoles = CurrentMod.InternalRoles.Values.OfType(); + var hasRoles = customRoles.Any(x => x.VisibleInSettings.Invoke()); var hasModifiers = CurrentMod.InternalOptionGroups.Exists(g => g.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true || g.ParentMenu == MenuCategory.Modifiers); @@ -485,12 +493,12 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) _smallButtons[MenuButton.CustomOne].gameObject.SetActive(true); _smallButtons[MenuButton.CustomOne].transform.localPosition = new Vector3(leftPos, position.y, position.z); - _smallButtons[MenuButton.CustomOne].buttonText.text = CurrentMod.MiraPlugin.CustomOptionMenuNameOne; + _smallButtons[MenuButton.CustomOne].buttonText.text = CurrentMod.MiraPlugin.CustomOptionMenuNameOne.Translate(); _smallButtons[MenuButton.CustomTwo].gameObject.SetActive(true); _smallButtons[MenuButton.CustomTwo].transform.localPosition = new Vector3(rightPos, position.y, position.z); - _smallButtons[MenuButton.CustomTwo].buttonText.text = CurrentMod.MiraPlugin.CustomOptionMenuNameTwo; + _smallButtons[MenuButton.CustomTwo].buttonText.text = CurrentMod.MiraPlugin.CustomOptionMenuNameTwo.Translate(); position.y -= 0.637f; } @@ -499,7 +507,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) { _largeButtons[MenuButton.CustomOne].gameObject.SetActive(true); _largeButtons[MenuButton.CustomOne].transform.localPosition = position; - _largeButtons[MenuButton.CustomOne].buttonText.text = CurrentMod.MiraPlugin.CustomOptionMenuNameOne; + _largeButtons[MenuButton.CustomOne].buttonText.text = CurrentMod.MiraPlugin.CustomOptionMenuNameOne.Translate(); position.y -= 0.637f; } else @@ -512,7 +520,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) { _largeButtons[MenuButton.CustomTwo].gameObject.SetActive(true); _largeButtons[MenuButton.CustomTwo].transform.localPosition = position; - _largeButtons[MenuButton.CustomTwo].buttonText.text = CurrentMod.MiraPlugin.CustomOptionMenuNameTwo; + _largeButtons[MenuButton.CustomTwo].buttonText.text = CurrentMod.MiraPlugin.CustomOptionMenuNameTwo.Translate(); position.y -= 0.637f; } else @@ -566,15 +574,15 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) case MenuCategory.Modifiers: menu.GameSettingsTab.gameObject.SetActive(true); - menu.MenuDescriptionText.text = CurrentMod.MiraPlugin.ModifierMenuDescription; + menu.MenuDescriptionText.text = CurrentMod.MiraPlugin.ModifierMenuDescription.Translate(); break; case MenuCategory.CustomOne: menu.GameSettingsTab.gameObject.SetActive(true); - menu.MenuDescriptionText.text = CurrentMod.MiraPlugin.CustomOptionMenuOneDescription; + menu.MenuDescriptionText.text = CurrentMod.MiraPlugin.CustomOptionMenuOneDescription.Translate(); break; case MenuCategory.CustomTwo: menu.GameSettingsTab.gameObject.SetActive(true); - menu.MenuDescriptionText.text = CurrentMod.MiraPlugin.CustomOptionMenuTwoDescription; + menu.MenuDescriptionText.text = CurrentMod.MiraPlugin.CustomOptionMenuTwoDescription.Translate(); break; } } diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index 36651f9..f6b873c 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -1,8 +1,11 @@ using HarmonyLib; using Il2CppSystem; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; using UnityEngine; using Object = Il2CppSystem.Object; @@ -162,7 +165,7 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()).Translate(); return false; } @@ -199,8 +202,7 @@ public static class OptionsPatches { return true; } - - __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate(); return false; } @@ -307,7 +309,7 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()).Translate(); __instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty()); return false; diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 3c1e645..8694bb0 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -5,9 +5,11 @@ using System.Linq; using AmongUs.GameOptions; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; @@ -74,7 +76,24 @@ public static class RoleSettingMenuPatches dividerImage.gameObject.SetActive(false); } - if (MenuState.Instance.FinishedRoleMenus.TryGetValue(MenuState.Instance.CurrentModIdx, out var finished) && finished) + var queuedRefresh = false; + if (MenuState.Instance.QueuedRoleMenuRefresh.TryGetValue(MenuState.Instance.CurrentModIdx, out var queued) && queued) + { + queuedRefresh = true; + var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); + var headers = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); + foreach (var child in roleOptionSettings) + { + Object.Destroy(child.gameObject); + } + foreach (var child in headers) + { + Object.Destroy(child.gameObject); + } + yield return new WaitForEndOfFrame(); + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; + } + if (!queuedRefresh && MenuState.Instance.FinishedRoleMenus.TryGetValue(MenuState.Instance.CurrentModIdx, out var finished) && finished) { var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); foreach (var r in roleOptionSettings) @@ -153,6 +172,17 @@ public static class RoleSettingMenuPatches ScrollerNum = 0.522f; var num4 = 0; + var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(false); + var headers = roleMenu.scrollBar.Inner.GetComponentsInChildren(false); + foreach (var child in roleOptionSettings) + { + Object.Destroy(child.gameObject); + } + foreach (var child in headers) + { + Object.Destroy(child.gameObject); + } + yield return new WaitForEndOfFrame(); var roleGroups = MenuState.Instance.CurrentMod.InternalRoles.Values.OfType() .ToLookup(x => x.RoleOptionsGroup); @@ -166,7 +196,7 @@ public static class RoleSettingMenuPatches // sort the groups by priority var sortedRoleGroups = roleGroups .OrderBy(x => x.Key.Priority) - .ThenBy(x => x.Key.Name); + .ThenBy(x => x.Key.Name.Translate()); var quotaThing = roleMenu.categoryHeaderEditRoleOrigin.transform.FindChild("QuotaHeader"); var usingNewQuota = false; @@ -176,7 +206,7 @@ public static class RoleSettingMenuPatches foreach (var grouping in sortedRoleGroups) { if (!grouping.Any() || - grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings())) + grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings() || !x.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode))) { continue; } @@ -185,12 +215,7 @@ public static class RoleSettingMenuPatches RoleGroupHidden.TryAdd(group, false); - var name = group.Name switch - { - "Crewmate" => StringNames.CrewmateRolesHeader, - "Impostor" => StringNames.ImpostorRolesHeader, - _ => CustomStringName.CreateAndRegister(group.Name), - }; + var name = MiraLocaleManager.GetOrCreateLocaleString(group.Name); var categoryHeaderMasked = Object.Instantiate( template, @@ -264,8 +289,8 @@ public static class RoleSettingMenuPatches quotaInst.gameObject.SetActive(!RoleGroupHidden[group]); var label = RoleGroupHidden[group] - ? "(Click to open)" - : "(Click to close)"; + ? $"({"MiraApi.GameSetting.Global.ClickToOpen".Translate()})" + : $"({"MiraApi.GameSetting.Global.ClickToClose".Translate()})"; var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); newText.text = $"{label}"; newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); @@ -307,6 +332,7 @@ public static class RoleSettingMenuPatches RoleGroupHidden[group] = !groupHidden; } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = false; foreach (var child in roleMenu.RoleChancesSettings.transform) @@ -326,6 +352,7 @@ public static class RoleSettingMenuPatches } } } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = true; } @@ -516,6 +543,26 @@ public static class RoleSettingMenuPatches CurrentRoleOptions = filteredOptions; } + [HarmonyPrefix] + [HarmonyPatch(nameof(RolesSettingsMenu.ChangeTab))] + public static void ChangeTabPatch(RolesSettingsMenu __instance) + { + var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); + categoryHeaderMasked.Title.text = TranslationController.Instance.GetString(StringNames.RoleSettingsLabel); + var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground"); + var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); + imgBg.gameObject.SetActive(true); + __instance.roleScreenshot.gameObject.SetActive(true); + __instance.roleDescriptionText.transform.parent.localPosition = new Vector3(2.5176f, -0.2731f, -1f); + __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.0675f, 0.1494f, 0.5687f); + labelBg.transform.localPosition = new Vector3(1.082f, 0.1054f, -2.5f); + __instance.roleScreenshot.drawMode = SpriteDrawMode.Simple; + foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren()) + { + optBehaviour.gameObject.Destroy(); + } + } + private static void ChangeTab(RoleBehaviour role, RolesSettingsMenu __instance) { if (role is not ICustomRole customRole) @@ -524,7 +571,7 @@ public static class RoleSettingMenuPatches return; } - __instance.roleDescriptionText.text = customRole.RoleLongDescription; + __instance.roleDescriptionText.text = customRole.RoleMedDescription; __instance.roleTitleText.text = role.GetRoleName(); var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); @@ -559,7 +606,7 @@ public static class RoleSettingMenuPatches comp.Destroy(); } - categoryHeaderMasked.Title.text = "RETURN TO ROLE SETTINGS"; + categoryHeaderMasked.Title.text = "MiraApi.ReturnToRoleSettings".Translate(); if (!categoryHeaderMasked.gameObject.TryGetComponent(out _)) { diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs new file mode 100644 index 0000000..a4878e4 --- /dev/null +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -0,0 +1,274 @@ +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; + +[HarmonyPatch] +public static class RoleGuidePatches +{ + [HarmonyPostfix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreateNormalModeSettings))] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreateHnSModeSettings))] + public static void CreateNormalModeSettings(MatchInfoGuide __instance) + { + __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; + } + + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] + public static void Awake(MatchInfoGuide __instance) + { + __instance.transitionOpen.targetSize = 1.3f; + } + + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Open))] + public static bool Open(MatchInfoGuide __instance) + { + if (HudManager.Instance.GameMenu.IsOpen || HudManager.Instance.Chat.IsOpenOrOpening) + { + return false; + } + + if (Minigame.Instance != null) + { + Minigame.Instance.Close(); + } + if (MapBehaviour.Instance) + { + MapBehaviour.Instance.Close(); + } + if (HudManager.InstanceExists) + { + ConsoleJoystick.SetMode_MenuAdditive(); + } + ControllerManager.Instance.OpenOverlayMenu("MatchInfoGuide", __instance.closeButton); + bool enabled = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Joystick; + __instance.glyphL.enabled = enabled; + __instance.glyphR.enabled = enabled; + if (GameManager.Instance.TryCast() != null) + { + if (__instance.NormalModeSettings.Count == 0) + { + __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 + { + DisplayNormalRoleSettings(__instance, false); + } + } + else if (__instance.HnSModeSettings.Count == 0) + { + __instance.numOfTabs = 2; + __instance.TabButtons[0].SelectButton(true); + __instance.CreateHnSModeSettings(); + } + PlayerControl.LocalPlayer.NetTransform.Halt(); + __instance.MatchInfoParent.SetActive(true); + ControllerManager instance = ControllerManager.Instance; + ControllerUiElementsState currentUiState = ControllerManager.Instance.CurrentUiState; + Il2CppSystem.Collections.Generic.List controllerSelectable = __instance.ControllerSelectable; + instance.SetUpSelectables(currentUiState, controllerSelectable[controllerSelectable.Count - 1], __instance.ControllerSelectable); + ControllerManager instance2 = ControllerManager.Instance; + Il2CppSystem.Collections.Generic.List controllerSelectable2 = __instance.ControllerSelectable; + instance2.SetCurrentSelected(controllerSelectable2[controllerSelectable2.Count - 1]); + __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))] + private static bool CreatePlayerEntries(MatchInfoGuide __instance) + { + __instance.PlayerPool.ReclaimAll(); + int num = 51; + foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) + { + PlayerIdentifierButton component = + __instance.PlayerPool.Get().GetComponent(); + component.transform.localPosition = new Vector3(0f, 0f, -1f); + component.Populate(networkedPlayerInfo); + __instance.ControllerSelectable.Add(component.Button); + component.SetTextStencil(num++); + component.PlatformIdentifier.transform.localPosition = new Vector3(0.314f, 0.088f, -2.78f); + component.NameText.transform.localPosition = new Vector3(0.3563f, 0, -2.98f); + component.NameText.text += $"\n{networkedPlayerInfo.GetPlayerColorString()}"; + var namePlate = HatManager.Instance.GetNamePlateById(networkedPlayerInfo.DefaultOutfit.NamePlateId); + var x = (NamePlateViewData viewdata) => + { + component.buttonSprite.sprite = viewdata?.Image; + component.buttonSprite.transform.localScale = new Vector3(0.7f, 1.075f, 1); + component.buttonSprite.transform.localPosition = new Vector3(-0.395f, 0, 0.1f); + }; + __instance.StartCoroutine( + AddressableAssetExtensions.CoLoadAssetAsync( + __instance, + namePlate.GetAssetReference(), + x)); + } + + return false; + } + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoRolePanel), nameof(MatchInfoRolePanel.SetPanel))] + public static bool SetPanel(MatchInfoRolePanel __instance, RoleBehaviour role, int numPerGame, int chancePerGame) + { + __instance.roleCount.text = string.Format(CultureInfo.InvariantCulture, "{0} at {1}%", numPerGame.ToString(CultureInfo.InvariantCulture), chancePerGame); + if (role is ICustomRole customRole) + { + __instance.roleName.text = customRole.RoleName; + __instance.roleDescription.text = $"{customRole.RoleFactionTitle}\n" + customRole.RoleMedDescription; + __instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset(); + __instance.roleCount.text += $" ({customRole.ParentMod.MiraPlugin.GetAbbreviatedModName()})"; + } + else + { + __instance.roleName.text = role.NiceName; + __instance.roleDescription.text = $"{TranslationController.Instance.GetString(role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor)}\n" + role.BlurbMed; + __instance.roleIcon.sprite = role.RoleIconColor; + __instance.roleCount.text += " (AU)"; + } + + __instance.roleIcon.SetSizeLimit(0.13f); + __instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); + __instance.roleName.fontMaterial.SetFloat(__instance.STENCIL_NAME, 50f); + __instance.roleDescription.fontMaterial.SetFloat(__instance.STENCIL_NAME, 50f); + __instance.roleCount.fontMaterial.SetFloat(__instance.STENCIL_NAME, 50f); + __instance.roleIcon.transform.localScale = new Vector3(4f, 4f, 1f); + return false; + } +} diff --git a/MiraAPI/Patches/Roles/GameStartupPatch.cs b/MiraAPI/Patches/Roles/GameStartupPatch.cs index 9f88bdd..94a7ec1 100644 --- a/MiraAPI/Patches/Roles/GameStartupPatch.cs +++ b/MiraAPI/Patches/Roles/GameStartupPatch.cs @@ -1,6 +1,8 @@ using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Utilities.Assets; namespace MiraAPI.Patches.Roles; @@ -22,6 +24,13 @@ public static class GameStartupPatch } _runOnce = true; + foreach (var mode in CustomGameModeManager.IdToModeMap.Values) + { + if (mode.Icon != null) + { + mode.TmpIcon = TmpSpriteUtils.CreateSpriteAsset(mode.Icon.LoadAsset(), $"MiraApi.Gamemode.{mode.Name.Replace(" ", string.Empty)}.png", 1.35f); + } + } if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) { diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index 8d13595..99359a6 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -3,6 +3,7 @@ using AmongUs.GameOptions; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameModes; using MiraAPI.Modifiers; using MiraAPI.Roles; using UnityEngine; @@ -65,14 +66,20 @@ public static class RoleManagerPatches [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static void ModifierSelectionPatches(RoleManager __instance) { - if (!AmongUsClient.Instance.AmHost || !ModifierManager.MiraAssignsModifiers) + if (AmongUsClient.Instance.AmHost && ModifierManager.MiraAssignsModifiers) + { + ModifierManager.AssignModifiers( + PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) + .ToList()); + } + + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (CustomGameModeManager.ActiveMode == null || roleSelection == null) { return; } - ModifierManager.AssignModifiers( - PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) - .ToList()); + CustomGameModeManager.ActiveMode.PostAssignRoles(roleSelection); } [HarmonyPrefix] diff --git a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs index d4d80d3..011fc28 100644 --- a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs +++ b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs @@ -8,17 +8,17 @@ namespace MiraAPI.Patches.Roles; /// /// Patches to return the correct role counts. /// -[HarmonyPatch(typeof(RoleOptionsCollectionV10))] +[HarmonyPatch(typeof(RoleOptionsCollectionV11))] public static class RoleOptionsCollectionPatch { /// - /// This patch fixes being inlined (2025.9.9) in the original code. + /// This patch fixes being inlined (2025.9.9) in the original code. /// [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV10.AnyRolesEnabled))] - public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV10 __instance, ref bool __result) + [HarmonyPatch(nameof(RoleOptionsCollectionV11.AnyRolesEnabled))] + public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV11 __instance, ref bool __result) { - foreach (KeyValuePair keyValuePair in __instance.roles) + foreach (KeyValuePair keyValuePair in __instance.roles) { if (__instance.GetNumPerGame(keyValuePair.Key) > 0) { @@ -33,7 +33,7 @@ public static class RoleOptionsCollectionPatch /// /// Return to skip original method, to not. [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV10.GetChancePerGame))] + [HarmonyPatch(nameof(RoleOptionsCollectionV11.GetChancePerGame))] public static bool GetChancePrefix(RoleTypes role, ref int __result) { if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null) @@ -63,7 +63,7 @@ public static class RoleOptionsCollectionPatch /// /// Return to skip original method, to not. [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV10.GetNumPerGame))] + [HarmonyPatch(nameof(RoleOptionsCollectionV11.GetNumPerGame))] public static bool GetNumPrefix(RoleTypes role, ref int __result) { if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null) diff --git a/MiraAPI/Patches/Roles/SelectRolesPatch.cs b/MiraAPI/Patches/Roles/SelectRolesPatch.cs index dc51f03..1d17765 100644 --- a/MiraAPI/Patches/Roles/SelectRolesPatch.cs +++ b/MiraAPI/Patches/Roles/SelectRolesPatch.cs @@ -3,6 +3,7 @@ using AmongUs.GameOptions; using HarmonyLib; using System.Linq; using InnerNet; +using MiraAPI.GameModes; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -15,6 +16,21 @@ public static class SelectRolesPatch [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static bool SelectRoles() { + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (!AmongUsClient.Instance.AmHost || roleSelection == null) + { + return true; + } + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.AssignRoles(out var runOriginal, roleSelection); + + if (!runOriginal) + { + return false; + } + } if (!ApiHandlesRoleSelect) { return true; diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 7a4338a..80ab9d6 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -1,13 +1,19 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Meeting; using MiraAPI.Events.Vanilla.Meeting.Voting; +using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Utilities; +using MiraAPI.Utilities.Components; using MiraAPI.Voting; +using UnityEngine; namespace MiraAPI.Patches.Voting; @@ -33,10 +39,46 @@ internal static class MeetingHudPatches return true; } + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.Select))] + public static bool VoteAreaSelectPatch(PlayerVoteArea __instance) + { + if (PlayerControl.LocalPlayer.Data.IsDead || __instance.AmDead || !(bool) (UnityEngine.Object) __instance.Parent) + return false; + JudgeRole? judgeRole = PlayerControl.LocalPlayer.Data.Role.TryCast(); + bool flag = judgeRole && PlayerControl.LocalPlayer.PlayerId != __instance.PlayerId; + __instance.JudgeOverruleButton?.gameObject.SetActive(flag); + if (__instance.VoteComplete || !__instance.Parent.Select((byte) __instance.PlayerId)) + return false; + __instance.Buttons.SetActive(true); + + float startPos = __instance.AnimateButtonsFromLeft ? 0.2f : 1.95f; + + Il2CppSystem.Collections.Generic.List selectableElements = new Il2CppSystem.Collections.Generic.List(); + foreach (var btn in __instance.Buttons.GetComponentsInChildren()) + { + selectableElements.Add(btn); + } + + for (int i = 0; i < selectableElements.Count; i++) + { + var button = selectableElements[i]; + float endPos = 1.3f - 0.65f * i; + float duration = 0.25f + 0.1f * i; + __instance.StartCoroutine(Effects.All(Effects.Lerp(duration, (Action) (t => + button.transform.localPosition = Vector2.Lerp(Vector2.right * startPos, Vector2.right * endPos, Effects.ExpOut(t)))))); + } + + ControllerManager.Instance.OpenOverlayMenu(__instance.name, __instance.CancelButton, selectableElements[1], selectableElements); + + return false; + } + [HarmonyPostfix] [HarmonyPatch(nameof(MeetingHud.Start))] public static void MeetingHudStartPatch(MeetingHud __instance) { + MeetingButtonManager.OnMeetingStart(__instance); foreach (var plr in PlayerControl.AllPlayerControls) { var voteData = plr.GetVoteData(); @@ -69,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)); } @@ -77,7 +128,17 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.Update))] public static void ForceSkipPatch(MeetingHud __instance) { - if (__instance.state is not (MeetingHud.VoteStates.NotVoted or MeetingHud.VoteStates.Voted)) + foreach (var targetedMeetingAbility in MeetingButtonManager.TargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role))) + { + targetedMeetingAbility.UpdateHandler(); + } + + foreach (var meetingAbility in MeetingButtonManager.UntargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role))) + { + meetingAbility.UpdateHandler(__instance); + } + + if (__instance.state is not (MeetingHud.MeetingStates.NotVoted or MeetingHud.MeetingStates.Voted)) { return; } @@ -105,7 +166,7 @@ internal static class MeetingHudPatches [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.Select))] - public static bool SelectPatch(MeetingHud __instance, int suspectStateIdx) + public static bool MeetingHudSelectPatch(MeetingHud __instance, int suspectStateIdx) { var voteData = PlayerControl.LocalPlayer.GetVoteData(); @@ -127,13 +188,13 @@ internal static class MeetingHudPatches return false; } - var playerVoteArea = __instance.playerStates.First(pv => pv.TargetPlayerId == pc.PlayerId); + var playerVoteArea = __instance.playerStates.First(pv => pv.PlayerId == pc.PlayerId); playerVoteArea.AmDead = true; playerVoteArea.Overlay.gameObject.SetActive(true); foreach (var player in Helpers.GetAlivePlayers()) { - var pva = __instance.playerStates.First(pv => pv.TargetPlayerId == player.PlayerId); + var pva = __instance.playerStates.First(pv => pv.PlayerId == player.PlayerId); var voteData = player.GetVoteData(); if (pva.AmDead || !voteData.VotedFor(pc.PlayerId)) @@ -150,7 +211,7 @@ internal static class MeetingHudPatches __instance.SetDirtyBit(1U); __instance.CheckForEndVoting(); - if (__instance.state == MeetingHud.VoteStates.Results) + if (__instance.state == MeetingHud.MeetingStates.Results) { __instance.SetupProceedButton(); } @@ -199,7 +260,7 @@ internal static class MeetingHudPatches }) ]); - __instance.RpcVotingComplete(voterStates, exiled, isTie); + __instance.RpcVotingComplete(voterStates, exiled, isTie, @event.OverruledVote, @event.OverruledNonce); return false; } @@ -223,6 +284,16 @@ internal static class MeetingHudPatches return false; } + // TODO: figure out a way to do host-authorization since right now any player can send RpcQueueOverruleVotes + [HarmonyPrefix] + [HarmonyPatch(nameof(MeetingHud.CmdQueueOverruleVotes))] + // Although this method is inlined in MeetingHud.Confirm, the next patch fixes that. + public static bool CmdQueueOverruleVotesPatch(MeetingHud __instance, PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) + { + VotingUtils.RpcQueueOverruleVotes(PlayerControl.LocalPlayer, judgePlayerId.Value, targetPlayerId.Value, overruleNonce); + return false; + } + [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.Confirm))] public static bool ConfirmPatch(MeetingHud __instance, [HarmonyArgument(0)] byte suspect) diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs index a29dd4c..21cd9d9 100644 --- a/MiraAPI/PluginLoading/IMiraPlugin.cs +++ b/MiraAPI/PluginLoading/IMiraPlugin.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Globalization; +using System.Linq; using BepInEx.Configuration; namespace MiraAPI.PluginLoading; @@ -13,6 +14,11 @@ public interface IMiraPlugin /// public virtual bool DisplayOnOptionsMenu => true; + /// + /// Gets or sets a value for the localization id to use for anything registered under this mod. + /// + public virtual string ParentLocaleId => GetType().Name.ToLower(CultureInfo.InvariantCulture); + /// /// Gets the name to display on the options menu. /// diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index db030b0..874c32b 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -5,6 +5,7 @@ using BepInEx.Configuration; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Presets; @@ -44,11 +45,30 @@ public class MiraPluginInfo /// public ReadOnlyDictionary Roles { get; private set; } = null!; + /// + /// Gets a read only dictionary of Game Mode IDs and the AbstractGameMode object they are associated with. + /// + /// Read only dictionary of IDs and Game Modes. + public ReadOnlyDictionary GetModes() + { + return new ReadOnlyDictionary(GameModes); + } + /// /// Gets a of this 's . /// public IReadOnlyCollection Buttons { get; private set; } = null!; + /// + /// Gets a of this 's . + /// + public IReadOnlyCollection TargetedMeetingButtons { get; private set; } = null!; + + /// + /// Gets a of this 's . + /// + public IReadOnlyCollection MeetingButtons { get; private set; } = null!; + /// /// Gets a of this 's s. /// @@ -62,6 +82,8 @@ public class MiraPluginInfo Options = [..InternalOptions]; Roles = new ReadOnlyDictionary(InternalRoles); Buttons = [..InternalButtons]; + MeetingButtons = [..InternalMeetingButtons]; + TargetedMeetingButtons = [..InternalTargetedMeetingButtons]; } internal List InternalPresets { get; } = []; @@ -72,12 +94,15 @@ public class MiraPluginInfo internal List InternalModifiers { get; } = []; + internal Dictionary GameModes { get; } = []; internal Dictionary InternalRoles { get; } = []; - internal Dictionary InternalGameModes { get; } = []; - internal List InternalButtons { get; } = []; + internal List InternalTargetedMeetingButtons { get; } = []; + + internal List InternalMeetingButtons { get; } = []; + /// /// Gets the plugin's ID, as defined in the plugin's BepInEx metadata. /// diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 21b9868..e2dbc91 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -7,20 +7,25 @@ using System.Reflection; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using Il2CppInterop.Runtime.Injection; using MiraAPI.Colors; using MiraAPI.Events; using MiraAPI.GameEnd; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; using MiraAPI.Keybinds; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; +using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Patches.Options; using MiraAPI.Presets; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; +using Reactor.Localization.Utilities; using Reactor.Networking; using Reactor.Utilities; @@ -33,18 +38,120 @@ public sealed class MiraPluginManager { private readonly Dictionary _registeredPlugins = []; - internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!; - internal MiraPluginInfo[] RegisteredPluginsWithOptions { get; private set; } = null!; + internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = []; + internal MiraPluginInfo[] RegisteredPluginsWithOptions { get; private set; } = []; + internal MiraPluginInfo[] PluginsWithOptionsOrGameModes { get; private set; } = []; internal Dictionary> QueuedRoleRegistrations { get; } = []; internal static MiraPluginManager Instance { get; private set; } = new(); + public static ConfigFile MiraApiConfig { get; private set; } = null!; + public static MiraPluginInfo MiraApiPluginInfo { get; private set; } = null!; - internal void Initialize() + internal void Initialize(BasePlugin miraApiPlugin, IMiraPlugin apiPlugin) { Instance = this; + CustomGameModeManager.RegisterDefaultMode(); + CustomGameModeManager.GetAndSetGameMode(); + if (IL2CPPChainloader.Instance.Plugins.TryGetValue(MiraApiPlugin.Id, out var newPlugin)) + { + var assembly = Assembly.GetCallingAssembly(); + var info = new MiraPluginInfo(apiPlugin, newPlugin); + var roles = new List(); + + var oldConfigSetting = info.PluginConfig.SaveOnConfigSet; + info.PluginConfig.SaveOnConfigSet = false; + + foreach (var type in assembly.GetTypes()) + { + if (type.IsAbstract || type.GetCustomAttribute() != null) + { + continue; + } + + foreach (var method in AccessTools.GetDeclaredMethods(type)) + { + var eventAttribute = method.GetCustomAttribute(); + if (eventAttribute == null) + { + continue; + } + + if (!method.IsStatic) + { + Error($"Event method {method.Name} in {type.Name} must be static."); + continue; + } + + var parameters = method.GetParameters(); + if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) + { + Error($"Invalid event registration method {method.Name} in {type.Name}"); + continue; + } + + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + } + + if (RegisterModifier(type, info)) + { + continue; + } + + if (RegisterOptions(type, info)) + { + continue; + } + + if (RegisterLocalTabs(type, miraApiPlugin)) + { + continue; + } + + if (RegisterRole(type, info, out var role)) + { + roles.Add(role); + continue; + } + + if (RegisterButton(type, info)) + { + continue; + } + + if (RegisterGameOver(type)) + { + continue; + } + + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + + RegisterColorClasses(type); + RegisterKeybinds(type, miraApiPlugin); + } + + info.PluginConfig.Save(); + info.PluginConfig.SaveOnConfigSet = oldConfigSetting; + MiraApiConfig = info.PluginConfig; + + info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + QueuedRoleRegistrations.Add(info, roles); + + _registeredPlugins.Add(assembly, info); + MiraApiPluginInfo = info; + + info.SavePublicCollections(); + PresetManager.CreateDefaultPreset(info); + PresetManager.LoadPresets(info); + Info($"Registering Mira API as mod."); + } + IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => { - if (plugin is not IMiraPlugin miraPlugin) + if (plugin is not IMiraPlugin miraPlugin || pluginInfo.Metadata.GUID == MiraApiPlugin.Id) { return; } @@ -118,6 +225,22 @@ public sealed class MiraPluginManager continue; } + + if (RegisterMeetingAbility(type, info)) + { + continue; + } + + if (RegisterTargetedMeetingAbility(type, info)) + { + continue; + } + + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + RegisterColorClasses(type); RegisterKeybinds(type, plugin); } @@ -146,6 +269,7 @@ public sealed class MiraPluginManager // Cache all the registered plugins into an array for easy access RegisteredPlugins = [.. _registeredPlugins.Values]; RegisteredPluginsWithOptions = [.. RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu)]; + PluginsWithOptionsOrGameModes = [..RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu || m.GameModes.Count > 0)]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); }; @@ -334,6 +458,20 @@ public sealed class MiraPluginManager return false; } + private static bool RegisterGameModeAttribute(Type type, MiraPluginInfo pluginInfo) + { + try + { + if (typeof(AbstractGameMode).IsAssignableFrom(type)) + return CustomGameModeManager.RegisterGameMode(type, pluginInfo); + } + catch (Exception e) + { + Error($"Failed to register gamemode {type.Name}: {e}"); + } + return false; + } + private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo) { try @@ -436,7 +574,7 @@ public sealed class MiraPluginManager KeybindManager.Keybinds.Add(keybind); if (source is IMiraPlugin miraPlugin) { - keybind.SourcePluginName = miraPlugin.OptionsTitleText; + keybind.SourcePluginName = miraPlugin.OptionsTitleText.Translate(); } else if (source is MiraApiPlugin) { @@ -454,4 +592,29 @@ public sealed class MiraPluginManager Error($"Failed to register keybind class {type.Name}: {e}"); } } + + private static bool RegisterMeetingAbility(Type type, MiraPluginInfo info) + { + try + { + return MeetingButtonManager.RegisterMeetingButton(type, info); + } + catch (Exception e) + { + Error($"Failed to register meeting ability {type.Name}: {e}"); + return false; + } + } + private static bool RegisterTargetedMeetingAbility(Type type, MiraPluginInfo info) + { + try + { + return MeetingButtonManager.RegisterTargetedMeetingButton(type, info); + } + catch (Exception e) + { + Error($"Failed to register targeted meeting ability {type.Name}: {e}"); + return false; + } + } } diff --git a/MiraAPI/Resources/ClassicGamemodeIcon.png b/MiraAPI/Resources/ClassicGamemodeIcon.png new file mode 100644 index 0000000..163ac1a Binary files /dev/null and b/MiraAPI/Resources/ClassicGamemodeIcon.png differ diff --git a/MiraAPI/Resources/HnSGamemodeIcon.png b/MiraAPI/Resources/HnSGamemodeIcon.png new file mode 100644 index 0000000..8b60c3d Binary files /dev/null and b/MiraAPI/Resources/HnSGamemodeIcon.png differ diff --git a/MiraAPI/Resources/Locale/cs_CZ.xml b/MiraAPI/Resources/Locale/cs_CZ.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/cs_CZ.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/de_DE.xml b/MiraAPI/Resources/Locale/de_DE.xml new file mode 100644 index 0000000..bf6add6 --- /dev/null +++ b/MiraAPI/Resources/Locale/de_DE.xml @@ -0,0 +1,14 @@ + + + Knopfskalierungsfaktor + Wiki Taste Unter Haupt UI + Rollen + Modifikator + Modifikatoren + Besatzung + Besatzung + Verräter + Verräter + Neutral + Neutral + diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml new file mode 100644 index 0000000..a00952f --- /dev/null +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -0,0 +1,43 @@ + + + Mira API + Visuals/UI + Accessibility + MiraAPI + Show Modifiers HUD on Left Side + Show Keybinds on Buttons + Button Scale Factor + Top-Right UI Scale Factor + Max FPS + Display Wiki on Bottom Row + Enable Sabotage Flashes + Enable Sabotage Blare + RETURN TO ROLE SETTINGS + Custom Category 1 + Custom Category 2 + Configure modifiers and their settings here! + Apply game settings for this mod! + Classic + The classic Among Us experience! +Find the Impostors or deceive the Crew. + Hide n Seek + You can run, but you can't hide! +Outlive the timer or eliminate the crew. + Click to close + Click to open + Select or create a options preset for this game + Save + Roles + Modifiers + Modifier + Crewmate + Crew + Crewmate Roles + Impostor + Imp + Impostor Roles + Neutral + Neut + Neutral Roles + Other + diff --git a/MiraAPI/Resources/Locale/es_419.xml b/MiraAPI/Resources/Locale/es_419.xml new file mode 100644 index 0000000..dea47cd --- /dev/null +++ b/MiraAPI/Resources/Locale/es_419.xml @@ -0,0 +1,43 @@ + + + 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 new file mode 100644 index 0000000..3240eed --- /dev/null +++ b/MiraAPI/Resources/Locale/es_ES.xml @@ -0,0 +1,15 @@ + + + Roles + Modificador + Modificadores + Tripulante + Tripulante + Impostor + Impostor + Neutro + Neutro + Mira API + Accesibilidad + MiraAPI + diff --git a/MiraAPI/Resources/Locale/fil_PH.xml b/MiraAPI/Resources/Locale/fil_PH.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/fil_PH.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/fr_FR.xml b/MiraAPI/Resources/Locale/fr_FR.xml new file mode 100644 index 0000000..12a1ded --- /dev/null +++ b/MiraAPI/Resources/Locale/fr_FR.xml @@ -0,0 +1,14 @@ + + + Échelle de tailles de boutons + Bouton pour le Wiki sous l'interface principale + Rôles + Modificateur + Modificateurs + Coéquipier + Équipage + Imposteur + Impo + Neutre + Neutre + diff --git a/MiraAPI/Resources/Locale/ga_IE.xml b/MiraAPI/Resources/Locale/ga_IE.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/ga_IE.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/it_IT.xml b/MiraAPI/Resources/Locale/it_IT.xml new file mode 100644 index 0000000..ed0cbed --- /dev/null +++ b/MiraAPI/Resources/Locale/it_IT.xml @@ -0,0 +1,13 @@ + + + Fattore Di Scala Dei Pulsanti + Ruoli + Modificatore + Modificatori + Astronauta + Astronauta + Impostore + Impo + Neutrale + Neutro + diff --git a/MiraAPI/Resources/Locale/ja_JP.xml b/MiraAPI/Resources/Locale/ja_JP.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/ja_JP.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/ko_KR.xml b/MiraAPI/Resources/Locale/ko_KR.xml new file mode 100644 index 0000000..545eafc --- /dev/null +++ b/MiraAPI/Resources/Locale/ko_KR.xml @@ -0,0 +1,13 @@ + + + 버튼 크기 + 역할 + 애드온 + 애드온들 + 크루원 + 크루 + 임포스터 + 임포스터 + 중립 + 중립 + diff --git a/MiraAPI/Resources/Locale/lt_LT.xml b/MiraAPI/Resources/Locale/lt_LT.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/lt_LT.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/nl_NL.xml b/MiraAPI/Resources/Locale/nl_NL.xml new file mode 100644 index 0000000..1b31126 --- /dev/null +++ b/MiraAPI/Resources/Locale/nl_NL.xml @@ -0,0 +1,13 @@ + + + Knoppenschaalfactor + Rollen + Modificator + Modificatoren + Bemanningslid + Bemanning + Bedrieger + Bedrieger + Neutraal + Neut + diff --git a/MiraAPI/Resources/Locale/pl_PL.xml b/MiraAPI/Resources/Locale/pl_PL.xml new file mode 100644 index 0000000..0142715 --- /dev/null +++ b/MiraAPI/Resources/Locale/pl_PL.xml @@ -0,0 +1,32 @@ + + + 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_BR.xml b/MiraAPI/Resources/Locale/pt_BR.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/pt_BR.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/pt_PT.xml b/MiraAPI/Resources/Locale/pt_PT.xml new file mode 100644 index 0000000..1cfec24 --- /dev/null +++ b/MiraAPI/Resources/Locale/pt_PT.xml @@ -0,0 +1,43 @@ + + + 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/Resources/Locale/ru_RU.xml b/MiraAPI/Resources/Locale/ru_RU.xml new file mode 100644 index 0000000..b45ced1 --- /dev/null +++ b/MiraAPI/Resources/Locale/ru_RU.xml @@ -0,0 +1,14 @@ + + + Множитель разрешения верх.-правого интерфейса + Кнопка вики под основным интерфейсом + Роли + Атрибут + Атрибуты + Член Экипажа + Экипаж + Предатель + Пред + Нейтрал + Нейт + diff --git a/MiraAPI/Resources/Locale/sv_SE.xml b/MiraAPI/Resources/Locale/sv_SE.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/sv_SE.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/tr_TR.xml b/MiraAPI/Resources/Locale/tr_TR.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/tr_TR.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/zh_CN.xml b/MiraAPI/Resources/Locale/zh_CN.xml new file mode 100644 index 0000000..445b2be --- /dev/null +++ b/MiraAPI/Resources/Locale/zh_CN.xml @@ -0,0 +1,32 @@ + + + Mira API + 界面/视觉 + 无障碍 + MiraAPI + 在左侧显示附加职业UI + 在按钮上显示快捷键 + 按钮缩放比例 + 最大帧数 + 启用破坏红色闪烁 + 启用破坏音效 + 返回职业设置 + 自定义类别 1 + 自定义类别 2 + 在此配置附加职业及其设置! + 为此模组应用游戏设置! + 默认 + 默认原版游戏模式 + 点击关闭 + 为本游戏选择或创建一个预设 + 保存 + 职业 + 附加职业 + 附加属性 + 船员 + 船员 + 伪装者 + 伪装者 + 中立 + 中立 + diff --git a/MiraAPI/Resources/Locale/zh_TW.xml b/MiraAPI/Resources/Locale/zh_TW.xml new file mode 100644 index 0000000..4438808 --- /dev/null +++ b/MiraAPI/Resources/Locale/zh_TW.xml @@ -0,0 +1,14 @@ + + + 右上角 UI 縮放比例 + 維基按鈕顯示在主界面下方 + 職業 + 附加屬性 + 附加屬性 + 船員 + 船員 + 偽裝者 + 偽裝者 + 中立 + 中立 + diff --git a/MiraAPI/Resources/NormalChatHover.png b/MiraAPI/Resources/NormalChatHover.png new file mode 100644 index 0000000..e6d7a50 Binary files /dev/null and b/MiraAPI/Resources/NormalChatHover.png differ diff --git a/MiraAPI/Resources/NormalChatIdle.png b/MiraAPI/Resources/NormalChatIdle.png new file mode 100644 index 0000000..42fc51b Binary files /dev/null and b/MiraAPI/Resources/NormalChatIdle.png differ diff --git a/MiraAPI/Resources/NormalChatOpen.png b/MiraAPI/Resources/NormalChatOpen.png new file mode 100644 index 0000000..62d7b26 Binary files /dev/null and b/MiraAPI/Resources/NormalChatOpen.png differ diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index f756fed..0829eaa 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,6 +1,7 @@ -using AmongUs.GameOptions; using System; +using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameModes; using MiraAPI.Patches.Freeplay; using MiraAPI.Utilities.Assets; using TMPro; @@ -100,6 +101,11 @@ public record struct CustomRoleConfiguration [HideFromIl2Cpp] public LoadableAsset? IntroSound { get; set; } = null; + /// + /// Gets or sets the associated game mode for this role. This is used to determine if the role should be available in a specific game mode. + /// + public Type AssociatedGameMode { get; set; } = typeof(ClassicMode); + /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. /// diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 0b2be26..bc93252 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -7,6 +7,7 @@ using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Networking; using MiraAPI.PluginLoading; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; @@ -131,9 +132,10 @@ public static class CustomRoleManager roleBehaviour.Role = (RoleTypes)roleId; roleBehaviour.TeamType = customRole.Team == ModdedRoleTeams.Custom ? RoleTeamTypes.Crewmate : (RoleTeamTypes)customRole.Team; roleBehaviour.NameColor = customRole.RoleColor; - roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName); - roleBehaviour.BlurbName = CustomStringName.CreateAndRegister(customRole.RoleDescription); - roleBehaviour.BlurbNameLong = CustomStringName.CreateAndRegister(customRole.RoleLongDescription); + roleBehaviour.StringName = MiraLocaleManager.GetOrCreateLocaleString(customRole.RoleNameLocale); + roleBehaviour.BlurbName = MiraLocaleManager.GetOrCreateLocaleString(customRole.RoleDescriptionLocale); + roleBehaviour.BlurbNameMed = MiraLocaleManager.GetOrCreateLocaleString(customRole.RoleMedDescriptionLocale); + roleBehaviour.BlurbNameLong = MiraLocaleManager.GetOrCreateLocaleString(customRole.RoleLongDescriptionLocale); roleBehaviour.AffectedByLightAffectors = customRole.Configuration.AffectedByLightOnAirship; roleBehaviour.CanBeKilled = customRole.Configuration.CanGetKilled; roleBehaviour.CanUseKillButton = customRole.Configuration.UseVanillaKillButton; diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index 08c5cd4..d43acbe 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -4,6 +4,9 @@ using System.Globalization; using System.Linq; using System.Text; using AmongUs.GameOptions; +using MiraAPI.Translation; +using MiraAPI.GameModes; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -23,14 +26,14 @@ public static class CustomRoleUtils { if (role is ICustomRole custom) { - return custom.CanSpawnOnCurrentMode(); + return custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode); } - if (GameManager.Instance.IsHideAndSeek()) + if (CustomGameModeManager.ActiveMode is HideAndSeekMode) { return role.Role is RoleTypes.Engineer || role.Role is RoleTypes.Impostor; } - return true; + return !Helpers.IsRoleBlacklisted(role); } public static List<(ushort RoleType, int Chance)> GetPossibleRoles( diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index c0c97be..50a9224 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,9 +1,11 @@ using System; using System.Text; using BepInEx.Configuration; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; +using MiraAPI.Translation; using UnityEngine; namespace MiraAPI.Roles; @@ -13,20 +15,76 @@ namespace MiraAPI.Roles; /// public interface ICustomRole : IOptionable { + /// + /// Gets the id part used to build the role's translation keys. It is recommended to simply call it what the role is, as other mods may utilize it. + /// + 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 + /// + string IdPrefix => GetType().Namespace!; + /// /// Gets the name of the role. /// - string RoleName { get; } + string RoleName => MiraLocaleManager.Get(RoleNameLocale); + + /// + /// Gets the role's name id for localization. + /// + string RoleNameLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart); /// /// Gets the description of the role. Used in the Intro Cutscene. /// - string RoleDescription { get; } + string RoleDescription => MiraLocaleManager.Get(RoleDescriptionLocale); /// - /// Gets the long description of the role. Used in the Role Tab and Role Options. + /// Gets the role's intro blurb id for localization. /// - string RoleLongDescription { get; } + string RoleDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "IntroBlurb"); + + /// + /// Gets the medium description of the role. Used in the role guide and options menu. + /// + string RoleMedDescription => MiraLocaleManager.Get(RoleMedDescriptionLocale); + + /// + /// Gets the role's medium description id for localization. + /// + string RoleMedDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "MedDescription"); + + /// + /// Gets the long description of the role. Used in the Role Tab. + /// + string RoleLongDescription => MiraLocaleManager.Get(RoleLongDescriptionLocale); + + /// + /// Gets the role's long description id for localization. + /// + string RoleLongDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "TabDescription"); + + /// + /// Gets the wiki description of the role. Used in the wiki, but not currently required. + /// + string RoleWikiDescription => MiraLocaleManager.Get(RoleLongDescriptionLocale); + + /// + /// Gets the role's wiki description id for localization. + /// + string RoleWikiDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "WikiDescription"); + + /// + /// Gets the faction / alignment of the role. Used in the role guide. + /// + string RoleFactionTitle => Team switch + { + ModdedRoleTeams.Crewmate => MiraLocaleManager.Get("MiraApi.RoleTeam.Crewmate"), + ModdedRoleTeams.Impostor => MiraLocaleManager.Get("MiraApi.RoleTeam.Impostor"), + ModdedRoleTeams.Custom => MiraLocaleManager.Get("MiraApi.RoleTeam.Neutral"), + _ => MiraLocaleManager.Get("MiraApi.RoleTeam.Other"), + }; /// /// Gets the of the role. @@ -244,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. diff --git a/MiraAPI/Roles/RoleOptionsGroup.cs b/MiraAPI/Roles/RoleOptionsGroup.cs index 46b6067..f8f25f5 100644 --- a/MiraAPI/Roles/RoleOptionsGroup.cs +++ b/MiraAPI/Roles/RoleOptionsGroup.cs @@ -13,15 +13,15 @@ public record struct RoleOptionsGroup(string Name, Color Color, int Priority = 0 /// /// Gets the default group for crewmates. /// - public static RoleOptionsGroup Crewmate { get; } = new("Crewmate", Palette.CrewmateBlue, -2); + public static RoleOptionsGroup Crewmate { get; } = new("MiraApi.RoleTeam.Crewmate.Group", Palette.CrewmateBlue, -2); /// /// Gets the default group for impostors. /// - public static RoleOptionsGroup Impostor { get; } = new("Impostor", Palette.ImpostorRed, -1); + public static RoleOptionsGroup Impostor { get; } = new("MiraApi.RoleTeam.Impostor.Group", Palette.ImpostorRed, -1); /// /// Gets the default group for neutral roles. /// - public static RoleOptionsGroup Neutral { get; } = new("Neutral Roles", Color.gray); + public static RoleOptionsGroup Neutral { get; } = new("MiraApi.RoleTeam.Neutral.Group", Color.gray); } diff --git a/MiraAPI/Translation/MiraLanguage.cs b/MiraAPI/Translation/MiraLanguage.cs new file mode 100644 index 0000000..d6d2624 --- /dev/null +++ b/MiraAPI/Translation/MiraLanguage.cs @@ -0,0 +1,30 @@ +namespace MiraAPI.Translation; + +/// +/// Supported languages for Mira translation system. +/// Maps to Among Us's SupportedLangs enum values. +/// +public enum MiraLanguage +{ + English, + Latam, + Brazilian, + Portuguese, + Korean, + Russian, + Dutch, + Filipino, + French, + German, + Italian, + Japanese, + Spanish, + SChinese, + TChinese, + Irish, + Polish, + Turkish, + Swedish, + Lithuanian, + Czech, +} diff --git a/MiraAPI/Translation/MiraLocaleManager.cs b/MiraAPI/Translation/MiraLocaleManager.cs new file mode 100644 index 0000000..90f0df4 --- /dev/null +++ b/MiraAPI/Translation/MiraLocaleManager.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; +using MiraAPI.Utilities; +using MonoMod.Utils; +using Reactor.Localization.Utilities; +using UnityEngine; + +namespace MiraAPI.Translation; + +public static class MiraLocaleManager +{ + private const string LangDirectory = "mira_languages"; + + // 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 = []; + + public static Dictionary LangList { get; } = new() + { + { MiraLanguage.English, "en_US" }, + { MiraLanguage.Latam, "es_419" }, + { MiraLanguage.Brazilian, "pt_BR" }, + { MiraLanguage.Portuguese, "pt_PT" }, + { MiraLanguage.Korean, "ko_KR" }, + { MiraLanguage.Russian, "ru_RU" }, + { MiraLanguage.Dutch, "nl_NL" }, + { MiraLanguage.Filipino, "fil_PH" }, + { MiraLanguage.French, "fr_FR" }, + { MiraLanguage.German, "de_DE" }, + { MiraLanguage.Italian, "it_IT" }, + { MiraLanguage.Japanese, "ja_JP" }, + { MiraLanguage.Spanish, "es_ES" }, + { MiraLanguage.SChinese, "zh_CN" }, + { MiraLanguage.TChinese, "zh_TW" }, + { MiraLanguage.Irish, "ga_IE" }, + { MiraLanguage.Polish, "pl_PL" }, // Custom + { MiraLanguage.Turkish, "tr_TR" }, // Custom + { MiraLanguage.Swedish, "sv_SE" }, // Custom + { MiraLanguage.Lithuanian, "lt_LT" }, // Custom + { MiraLanguage.Czech, "cs_CZ" }, // Custom + }; + public static Dictionary LangCultureList { get; } = new() + { + { MiraLanguage.English, "en-US" }, + { MiraLanguage.Latam, "es-419" }, + { MiraLanguage.Brazilian, "pt-BR" }, + { MiraLanguage.Portuguese, "pt-PT" }, + { MiraLanguage.Korean, "ko-KR" }, + { MiraLanguage.Russian, "ru-RU" }, + { MiraLanguage.Dutch, "nl-NL" }, + { MiraLanguage.Filipino, "fil-PH" }, + { MiraLanguage.French, "fr-FR" }, + { MiraLanguage.German, "de-DE" }, + { MiraLanguage.Italian, "it-IT" }, + { MiraLanguage.Japanese, "ja-JP" }, + { MiraLanguage.Spanish, "es-ES" }, + { MiraLanguage.SChinese, "zh-CN" }, + { MiraLanguage.TChinese, "zh-TW" }, + { MiraLanguage.Irish, "ga-IE" }, + { MiraLanguage.Polish, "pl-PL" }, // Custom + { MiraLanguage.Turkish, "tr-TR" }, // Custom + { MiraLanguage.Swedish, "sv-SE" }, // Custom + { MiraLanguage.Lithuanian, "lt-LT" }, // Custom + { MiraLanguage.Czech, "cs-CZ" }, // Custom + }; + + /// + /// Gets the current language from Among Us settings. + /// + public static MiraLanguage CurrentLanguage + { + get + { + try + { + var langName = AmongUs.Data.DataManager.Settings.Language.CurrentLanguage; + return (MiraLanguage)langName; + } + catch + { + return MiraLanguage.English; + } + } + } + + /// + /// 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(); + + LoadInternalStrings(callingAssembly, callingAssembly.GetName().Name!, modGuid); + } + + /// + /// Registers translations for a mod. + /// Call once per mod during plugin Load(). + /// + /// The mod GUID (e.g., "mira.example"). + /// The mod's root namespace (e.g., "mira.api"). + public static void Register(string modGuid, string internalName) + { + var callingAssembly = Assembly.GetCallingAssembly(); + + LoadInternalStrings(callingAssembly, internalName, modGuid); + } + + /// + /// Translates a key into the current language. + /// + /// 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 = "") + { + return Get(CurrentLanguage, key, fallback != string.Empty ? fallback : key); + } + + /// + /// 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)) + { + return translation; + } + + if (Locale.TryGetValue(MiraLanguage.English, out var translationsEng) && + translationsEng.TryGetValue(key, out var translationEng)) + { + return translationEng; + } + + return fallback; + } + + /// + /// 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 (language is not MiraLanguage.English && + Locale.TryGetValue(language, out var translations) && + translations.TryGetValue(key, out var translation)) + { + text = translation; + } + + 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) + { + return idPart.StartsWith('#') ? idPart[1..] : $"{modId}.{idPart}.{suffix}"; + } + + public static string BuildTranslationId(string modId, string idPart) + { + return idPart.StartsWith('#') ? idPart[1..] : $"{modId}.{idPart}"; + } + + private static string GetModLangDir(string modGuid) + { + return Path.Combine(Application.persistentDataPath, LangDirectory, modGuid); + } + + public static StringNames GetOrCreateLocaleString(string name) + { + if (RegisteredStringNames.TryGetValue(name, out var stringName)) + { + return stringName; + } + + var newString = CustomStringName.CreateAndRegister(name); + RegisteredStringNames.Add(name, newString); + StringNamesLookup.Add(newString, name); + return newString; + } + + 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); + + foreach (var filePath in Directory.GetFiles(dir, "*.xml")) + { + 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) + { + using var resourceStream = + assembly.GetManifestResourceStream(resourcePrefix + locale.Value + ".xml"); + if (resourceStream == null) + { + // Silently skipped + continue; + } + + using StreamReader reader = new(resourceStream); + string xmlContent = reader.ReadToEnd(); + try + { + Locale.TryAdd(locale.Key, []); + ParseXmlFile(xmlContent, locale.Key, true); + atLeastOneLoaded = true; + } + catch (Exception e) + { + Error($"Failed to load translation {resourcePrefix}{locale.Value}: {e.Message}"); + } + } + + if (!atLeastOneLoaded) + { + Error($"No internal strings were found for {internalName}!"); + } + } + + private static void ParseXmlFile(string xmlContent, MiraLanguage language, bool loadingInternal) + { + var dict = Locale[language]; + XmlDocument xmlDoc = new(); + try + { + xmlDoc.LoadXml(xmlContent); + XmlNodeList? stringNodes = xmlDoc.SelectNodes("/resources/string"); + + if (stringNodes != null) + { + var total = 0; + foreach (XmlNode node in stringNodes) + { + if (node.Attributes?["name"] != null) + { + string name = node.Attributes["name"]!.Value; + string value = node.InnerText; + + if (string.IsNullOrEmpty(name)) continue; + + if (value.Contains('[')) + { + value = value.Replace("[", "<"); + } + + if (value.Contains(']')) + { + value = value.Replace("]", ">"); + } + + 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}"); + } + } +} diff --git a/MiraAPI/Translation/MiraLocalizationProvider.cs b/MiraAPI/Translation/MiraLocalizationProvider.cs new file mode 100644 index 0000000..3ac1133 --- /dev/null +++ b/MiraAPI/Translation/MiraLocalizationProvider.cs @@ -0,0 +1,69 @@ +using System.Linq; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.LocalSettings; +using Reactor.Localization; +using Reactor.Localization.Providers; +using Reactor.Utilities; + +namespace MiraAPI.Translation; + +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) + { + if (MiraLocaleManager.StringNamesLookup.TryGetValue(stringName, out var key)) + { + result = MiraLocaleManager.Get(key); + return true; + } + result = null; + return false; + } + + public override bool TryGetTextFormatted(StringNames stringName, Il2CppReferenceArray parts, out string? result) + { + if (!TryGetText(stringName, out result)) return false; + + result = Il2CppSystem.String.Format(result, parts); + return true; + } + + public override void OnLanguageChanged(SupportedLangs newLanguage) + { + _reactorProvider ??= LocalizationManager.Providers.First(x => x is HardCodedLocalizationProvider); + + if (MiraLocaleManager.LangCultureList.TryGetValue((MiraLanguage)newLanguage, out var culture)) + { + MiraApiPlugin.Culture = new(culture); + } + if (!_loadedStrings) + { + MiraLocaleManager.LoadExternalLocale(); + _loadedStrings = true; + } + + foreach (var tab in LocalSettingsTab.TabGroups) + { + if (tab.Key == null) + { + break; + } + + tab.Key.text = $"{LocalSettingsTab.GetShortName(tab.Value.Translate())}"; + } + /*Warning($""); + Warning($""); + foreach (var stringName in TranslationController.Instance.currentLanguage.AllStrings) + { + var value = stringName.Value.Replace("\n", "\\%nl\\%"); + value = value.Replace("{", "["); + value = value.Replace("}", "]"); + Warning($"{value}"); + } + Warning($"");*/ + } +} diff --git a/MiraAPI/Translation/TranslationExtensions.cs b/MiraAPI/Translation/TranslationExtensions.cs new file mode 100644 index 0000000..5f0edbd --- /dev/null +++ b/MiraAPI/Translation/TranslationExtensions.cs @@ -0,0 +1,15 @@ +namespace MiraAPI.Translation; + +/// +/// Extension method for translating strings. +/// +public static class TranslationExtensions +{ + /// + /// Translates a key to the current language. + /// + public static string Translate(this string key) + { + return MiraLocaleManager.Get(key); + } +} diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 10bceda..bec994a 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -130,10 +130,35 @@ public static class MiraAssets /// public static LoadableResourceAsset ModifierFile { get; } = new("MiraAPI.Resources.ModifierFile.png"); + /// + /// Gets the sprite used for the Classic gamemode. + /// + public static LoadableResourceAsset ClassicGamemodeIcon { get; } = new("MiraAPI.Resources.ClassicGamemodeIcon.png"); + + /// + /// Gets the sprite used for the Hide n Seek gamemode. + /// + public static LoadableResourceAsset HnSGamemodeIcon { get; } = new("MiraAPI.Resources.HnSGamemodeIcon.png"); + /// /// Gets the sprite used for timed modifier file in TaskAdderGame. /// public static LoadableResourceAsset TimedModifierFile { get; } = new("MiraAPI.Resources.TimedModifierFile.png"); + /// + /// Gets the sprite used for the new chat button while hovering. + /// + public static LoadableResourceAsset ChatHoverSprite { get; } = new("MiraAPI.Resources.NormalChatHover.png"); + + /// + /// Gets the sprite used for the new chat button while hovering. + /// + public static LoadableResourceAsset ChatIdleSprite { get; } = new("MiraAPI.Resources.NormalChatIdle.png"); + + /// + /// Gets the sprite used for the new chat button while hovering. + /// + public static LoadableResourceAsset ChatOpenSprite { get; } = new("MiraAPI.Resources.NormalChatOpen.png"); + internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png"); } diff --git a/MiraAPI/Utilities/Components/CenteredGridArrange.cs b/MiraAPI/Utilities/Components/CenteredGridArrange.cs new file mode 100644 index 0000000..a8d4ccd --- /dev/null +++ b/MiraAPI/Utilities/Components/CenteredGridArrange.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.Utilities.Components; + +/// +/// Utility component for arranging objects in a centered layout, similar to . +/// +/// The for the component. +[RegisterInIl2Cpp] +public class CenteredGridArrange(IntPtr iPtr) : MonoBehaviour(iPtr) +{ + /// + /// Gets or sets the cell size, which is used for spacing. + /// + public Vector2 CellSize; + + /// + /// Gets or sets the maximum amount of columns. + /// + public int MaxColumns = 6; + + private List cells; + private static List currentChildren = new List(); + + private void Start() + { + cells = new List(); + GetChildsActive(); + CheckCurrentChildren(); + } + + private void FixedUpdate() => CheckCurrentChildren(); + + private void CheckCurrentChildren() + { + GetChildsActive(); + if (cells.SequenceEqual(currentChildren)) + return; + cells.Clear(); + foreach (Transform currentChild in currentChildren) + cells.Add(currentChild); + ArrangeChilds(); + } + + private void GetChildsActive() + { + currentChildren.Clear(); + foreach (var obj in transform) + { + var child = obj.TryCast(); + if (child == null) continue; + if (child.gameObject.activeSelf) + currentChildren.Add(child); + } + } + + private void ArrangeChilds() + { + if (cells.Count == 0) + return; + + int totalRows = Mathf.CeilToInt((float)cells.Count / MaxColumns); + float totalHeight = (totalRows - 1) * CellSize.y; + float startY = transform.position.y + totalHeight * 0.5f; + + for (int index = 0; index < cells.Count; ++index) + { + int row = index / MaxColumns; + int rowStartIndex = row * MaxColumns; + int itemsInRow = Mathf.Min(MaxColumns, cells.Count - rowStartIndex); + int col = index - rowStartIndex; + + float rowWidth = (itemsInRow - 1) * CellSize.x; + float startX = transform.position.x - rowWidth * 0.5f; + + float x = startX + col * CellSize.x; + float y = startY - row * CellSize.y; + + Transform cell = cells[index]; + cell.position = new Vector3(x, y, cell.position.z); + } + } +} diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index a1c2b5e..d99c24b 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; @@ -164,7 +165,7 @@ public static class Extensions /// /// The . /// The player's . - public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.TargetPlayerId)?.Object; + public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.PlayerId)?.Object; /// /// Gets an representing the amount of tasks a player has left. @@ -366,6 +367,71 @@ public static class Extensions GC.Collect(); } + /// + /// Gets all child objects in a parent object. + /// + /// An that contains s. + public static IEnumerable GetAllChildren(this GameObject go) + { + for (var i = 0; i < go.transform.childCount; i++) + { + yield return go.transform.GetChild(i).gameObject; + } + } + + /// + /// Gets all child objects in a parent Transform. + /// + /// An that contains s. + public static IEnumerable GetAllChildren(this Transform go) + { + for (var i = 0; i < go.transform.childCount; i++) + { + yield return go.transform.GetChild(i).gameObject; + } + } + + /// + /// Resizes a appropriate to the maximum pixel size without messing up ratios. + /// + /// The to adjust. + /// The scale for the sprite to be adjusted to. + public static void SetSizeLimit(this SpriteRenderer sprite, float pixelSize) + { + sprite.drawMode = SpriteDrawMode.Sliced; + if (!sprite.sprite) + { + return; + } + + float spriteWidth = sprite.sprite.rect.width; + float spriteHeight = sprite.sprite.rect.height; + + if (spriteWidth < spriteHeight) + { + sprite.size = new Vector2(pixelSize * spriteWidth / spriteHeight, pixelSize); + } + else + { + sprite.size = new Vector2(pixelSize, pixelSize * spriteHeight / spriteWidth); + } + } + + /// + /// Resizes a appropriate to the maximum pixel size without messing up ratios. + /// + /// The to adjust. + /// The scale for the sprite to be adjusted to. + public static void SetSizeLimit(this GameObject spriteObj, float pixelSize) + { + if (!spriteObj.TryGetComponent(out var sprite)) + { + return; + } + + sprite.SetSizeLimit(pixelSize); + } + /// /// Gets a cache of 's to improve performance. /// @@ -611,7 +677,8 @@ public static class Extensions public static bool IsCustom(this OptionBehaviour optionBehaviour) { return ModdedOptionsManager.ModdedOptions.Values.Any( - opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour); + opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour) + || optionBehaviour.Equals(GameModeOption.OptionBehaviour); } /// diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 9f66817..5a70c2c 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -5,6 +5,7 @@ using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; @@ -541,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). /// diff --git a/MiraAPI/VanillaEvents/JudgeEvents.cs b/MiraAPI/VanillaEvents/JudgeEvents.cs new file mode 100644 index 0000000..4a20c6e --- /dev/null +++ b/MiraAPI/VanillaEvents/JudgeEvents.cs @@ -0,0 +1,28 @@ +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Meeting.Voting; +using MiraAPI.Roles; + +namespace MiraAPI.VanillaEvents; + +public static class JudgeEvents +{ + public static void Initialize() + { + // This is required because MiraAPI isn't an IMiraPlugin (besides in the gamemodes branch) + MiraEventManager.RegisterEventHandler(@event => ProcessVotesEventHandler(@event), -1000); + } + + public static void ProcessVotesEventHandler(ProcessVotesEvent @event) + { + if (MeetingHud.Instance.TryGetWinningOverrule(out var judgeOverrule, out var networkedPlayerInfo, out var networkedPlayerInfo2)) + { + Error("Judge has overruled votes!"); + @event.OverruledVote = true; + @event.OverruledNonce = judgeOverrule.OverruleNonce; + + @event.ExiledPlayer = networkedPlayerInfo2.Role is ICustomRole { Team: not ModdedRoleTeams.Crewmate } || networkedPlayerInfo2.Role.TeamType == RoleTeamTypes.Impostor + ? GameData.Instance.GetPlayerById(judgeOverrule.OverruledPlayerId) + : networkedPlayerInfo; + } + } +} 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 fb117e4..a1f989d 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.Networking; @@ -41,9 +42,9 @@ public static class VotingUtils /// The player's . /// Who the player voted for. /// Whether you want the vote to commence or not. - private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, out bool cancelVote) + private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, bool byJudge, out bool cancelVote) { - var @event = new HandleVoteEvent(voteData, suspectIdx); + var @event = new HandleVoteEvent(voteData, suspectIdx, byJudge); MiraEventManager.InvokeEvent(@event); cancelVote = @event.PreventVote; @@ -60,7 +61,14 @@ public static class VotingUtils return; } - voteData.DecreaseRemainingVotes(1); + if (byJudge) + { + voteData.SetRemainingVotes(0); + } + else + { + voteData.DecreaseRemainingVotes(1); + } voteData.VoteForPlayer(suspectIdx); } @@ -75,14 +83,14 @@ public static class VotingUtils { if (!source.IsHost()) return; - MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == voterId).UnsetVote(); + MeetingHud.Instance.playerStates.First(state => state.PlayerId == voterId).UnsetVote(); if (PlayerControl.LocalPlayer.PlayerId != voterId) { return; } - MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == votedFor).ThumbsDown.enabled = false; + MeetingHud.Instance.playerStates.First(state => state.PlayerId == votedFor).ThumbsDown.enabled = false; if (!AmongUsClient.Instance.AmHost) { @@ -93,13 +101,74 @@ public static class VotingUtils foreach (var t in MeetingHud.Instance.playerStates) { - t.voteComplete = false; + t.VoteComplete = false; } - MeetingHud.Instance.SkipVoteButton.voteComplete = false; + MeetingHud.Instance.SkipVoteButton.VoteComplete = false; MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(true); } + /// + /// Networks the casting of a vote. We replace the vanilla solution with a custom version that works for Judge specifically. + /// + /// The who sent this RPC. + /// The id of the player who casted the vote. + /// The voted player's id. + /// Data that is checked by the Judge role to determine which Judge takes priority. + [MethodRpc((uint)MiraRpc.QueueOverruleVotes)] + public static void RpcQueueOverruleVotes(PlayerControl source, byte srcPlayerId, byte suspectPlayerId, ushort overruleNonce) + { + CustomCastJudgeVote(srcPlayerId, suspectPlayerId, overruleNonce); + } + + public static void CustomCastJudgeVote(PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) + { + var plr = GameData.Instance.GetPlayerById(judgePlayerId.Value); + if (!plr) return; + + var voteData = plr.Object.GetVoteData(); + if (!voteData) return; + + HandleVote(voteData, targetPlayerId.Value, true, out var cancelVote); + + // Handle local behaviour for the voter (for some reason checking AmOwner does not work for host) + if (PlayerControl.LocalPlayer.PlayerId == judgePlayerId.Value) + { + if (!cancelVote) SoundManager.Instance.PlaySound(MeetingHud.Instance.VoteLockinSound, false); + + foreach (var playerVoteArea in MeetingHud.Instance.playerStates) + { + playerVoteArea.ClearButtons(); + } + + MeetingHud.Instance.SkipVoteButton.ClearButtons(); + + var localVoteData = PlayerControl.LocalPlayer.GetVoteData(); + if (!localVoteData || localVoteData.VotesRemaining != 0) return; + + MeetingHud.Instance.SkipVoteButton.VoteComplete = true; + MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(false); + } + + if (cancelVote) return; + + // If player has no more votes, then make it show that the player has used all their votes. + if (voteData.VotesRemaining == 0) + { + MeetingHud.Instance.AddOrUpdateJudgeOverrule(judgePlayerId, targetPlayerId, overruleNonce); + MeetingHud.Instance.playerStates.First(x => x.PlayerId == judgePlayerId.Value).SetVote(targetPlayerId.Value); + } + + // If host, then check end voting, and set votes/send chat if applicable + if (!PlayerControl.LocalPlayer.IsHost()) return; + + MeetingHud.Instance.SetDirtyBit(1U); + MeetingHud.Instance.CheckForEndVoting(); + + if (voteData.VotesRemaining != 0) return; + PlayerControl.LocalPlayer.RpcSendChatNote(judgePlayerId.Value, ChatNoteTypes.DidVote); + } + /// /// Networks the casting of a vote. We replace the vanilla solution with a custom version that works for the use case. /// @@ -112,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; @@ -120,7 +189,7 @@ public static class VotingUtils var voteData = plr.Object.GetVoteData(); if (!voteData) return; - HandleVote(voteData, suspectPlayerId, out var cancelVote); + HandleVote(voteData, suspectPlayerId, false, out var cancelVote); // Handle local behaviour for the voter (for some reason checking AmOwner does not work for host) if (PlayerControl.LocalPlayer.PlayerId == srcPlayerId) @@ -137,7 +206,7 @@ public static class VotingUtils var localVoteData = PlayerControl.LocalPlayer.GetVoteData(); if (!localVoteData || localVoteData.VotesRemaining != 0) return; - MeetingHud.Instance.SkipVoteButton.voteComplete = true; + MeetingHud.Instance.SkipVoteButton.VoteComplete = true; MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(false); } @@ -146,7 +215,7 @@ public static class VotingUtils // If player has no more votes, then make it show that the player has used all their votes. if (voteData.VotesRemaining == 0) { - MeetingHud.Instance.playerStates.First(x => x.TargetPlayerId == srcPlayerId).SetVote(suspectPlayerId); + MeetingHud.Instance.playerStates.First(x => x.PlayerId == srcPlayerId).SetVote(suspectPlayerId); } // If host, then check end voting, and set votes/send chat if applicable @@ -233,7 +302,7 @@ public static class VotingUtils MeetingHud.Instance.BloopAVoteIcon(playerById, num, MeetingHud.Instance.SkippedVoting.transform); num++; } - else if (vote.Suspect == playerVoteArea.TargetPlayerId) + else if (vote.Suspect == playerVoteArea.PlayerId) { if (!delays.TryAdd(vote.Suspect, 0)) { diff --git a/README.md b/README.md index 6420a54..84823f4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ [![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](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://dcbadge.limes.pink/api/server/AEfHJGwggC)](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