diff --git a/.editorconfig b/.editorconfig index 02994df..83c451b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,3 +17,6 @@ dotnet_diagnostic.CA1805.severity = none # S3358: Ternary operators should not be nested dotnet_diagnostic.S3358.severity = none + +# IDE1006: Naming Styles +dotnet_diagnostic.IDE1006.severity = none 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/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 650bdac..bda713b 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; public override int MaxUses => 3; 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 4d277ef..7e9392b 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 85c9487..989e14c 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,14 @@ 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 { get; } = "Mira API\nExample Mod"; + public string CustomOptionMenuNameTwo { get; } = "Example Options"; public ConfigFile GetConfigFile() { 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 60753ad..4f67086 100644 --- a/MiraAPI.Example/MiraAPI.Example.csproj +++ b/MiraAPI.Example/MiraAPI.Example.csproj @@ -1,18 +1,18 @@ - + 1.0.0 Example mod for Mira API - Angxl, XtraCube + Author Name $(NoWarn);EnableGenerateDocumentationFile - + - - - + + + 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 53bc9eb..aff6ccf 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -7,19 +7,19 @@ 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; - [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; } diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs index 0f17078..6acba04 100644 --- a/MiraAPI.Example/Options/ExampleOptions2.cs +++ b/MiraAPI.Example/Options/ExampleOptions2.cs @@ -6,22 +6,22 @@ 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 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 089a33e..72542af 100644 --- a/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs +++ b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs @@ -5,13 +5,13 @@ 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 d38d406..4cd04c5 100644 --- a/MiraAPI.Example/Roles/ChameleonRole.cs +++ b/MiraAPI.Example/Roles/ChameleonRole.cs @@ -10,9 +10,8 @@ namespace MiraAPI.Example.Roles; public class ChameleonRole : CrewmateRole, ICustomRole { - public string RoleName => "Chameleon"; - public string RoleLongDescription => "Stay invisible while not moving."; - public string RoleDescription => RoleLongDescription; + public string IdPart => "Chameleon"; + public string IdPrefix => "ApiExample.Role.Crewmate"; public Color RoleColor => Palette.AcceptedGreen; public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; @@ -22,7 +21,9 @@ public class ChameleonRole : 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 ChameleonRole : 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 ChameleonRole : 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 d3a15ac..cf3fade 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(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 d6a954d..72307b0 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/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 7e14a17..f8f0e9a 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs @@ -11,7 +11,8 @@ namespace MiraAPI.Events.Vanilla.Meeting.Voting; /// /// The voter's . /// The target's playerId. -public class HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId) : MiraCancelableEvent +/// Whether the vote is actually for a Judge. +public class HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId, bool isOverruling) : MiraCancelableEvent { /// /// Gets the instance of the voter's . @@ -37,4 +38,18 @@ public class HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId) : Mir /// Gets the of the target. /// public NetworkedPlayerInfo TargetPlayerInfo { get; } = GameData.Instance.GetPlayerById(targetId); + + /// + /// Gets or sets a value indicating whether the vote event is an Overrule caused by a Judge. + /// + public bool IsOverruling { get; set; } = isOverruling; + + /// + /// Initializes a new instance of the class. + /// + /// The voter's . + /// The target's playerId. + public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId) : this(playerVoteData, targetId, false) + { + } } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs index f31a34f..94464b8 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs @@ -30,6 +30,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..32339cf --- /dev/null +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -0,0 +1,321 @@ +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() + { + return 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) + { + return 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) + { + return 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) + { + return 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) + { + return 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) + { + return 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) + { + return 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() + { + return 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 f44d006..0000000 --- a/MiraAPI/GameModes/CustomGameMode.cs +++ /dev/null @@ -1,184 +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() - { - return true; - } - - /// - /// Should Game Settings be available when this gamemode is selected. - /// - /// if Game Settings are enabled in this mode. - public virtual bool AreGameSettingsEnabled() - { - return true; - } - - /// - /// Custom winner selection. - /// - /// List of winners or . - public virtual List? CalculateWinners() - { - return null; - } - - /// - /// Show gamemode in . - /// - /// if the game mode should be shown in the intro cutscene. - public virtual bool ShowGameModeIntroCutscene() - { - return false; - } - - /// - /// Can Admin be used in this gamemode. - /// - /// Admin Console. - /// if Admin console is enabled. - public virtual bool CanUseMapConsole(MapConsole console) - { - return true; - } - - /// - /// Can a be reported in this gamemode. - /// - /// Target for reporting. - /// if s can be reported. - public virtual bool CanReport(DeadBody body) - { - return true; - } - - /// - /// Can s be used in this gamemode. - /// - /// System Console. - /// if s are enabled in this mode. - public virtual bool CanUseSystemConsole(SystemConsole console) - { - return true; - } - - /// - /// Can tasks be interacted with in this gamemode. - /// - /// Task console. - /// if tasks are enabled in this mode. - public virtual bool CanUseTasks(Console console) - { - return 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) - { - return 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) - { - return true; - } -} diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 92eec65..2d3f071 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,74 +1,140 @@ using System; using System.Collections.Generic; using System.Linq; +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 = []; - /// - /// Checks to see if the currently selected gamemode is vanilla. - /// - /// true if there is no custom gamemode selected. - public static bool IsDefault() + private static uint GetNextId() { - return ActiveMode?.Id == 0; + return ++LastId; } - /// - /// Gets the current . - /// - public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode(); + private static uint LastId { get; set; } /// - /// Set current . + /// Register gamemode from type. /// - /// ID. - public static void SetGameMode(int id) + /// 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) { - 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() + { + return 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() + { + return 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 + { + return 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) + { + return 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) + return; + + 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 47c6c6b..0000000 --- a/MiraAPI/GameModes/DefaultMode.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MiraAPI.GameModes; - -/// -/// The default gamemode of Among Us. -/// -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..a4f8fdb --- /dev/null +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using MiraAPI.GameOptions; +using MiraAPI.Networking; +using MiraAPI.Patches.GameModes; +using MiraAPI.Patches.Options; +using MiraAPI.Translation; +using Reactor.Networking.Attributes; +using UnityEngine; + +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..705b29b --- /dev/null +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -0,0 +1,542 @@ +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) + { + return false; + } + + public override bool ShouldShowSabotageMap(MapBehaviour map) + { + return 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 5c937b6..8dc4b26 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 6ffe75e..d0b95d5 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 02daa25..3d48453 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) : Attribute +public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : Attribute { internal IModdedOption? HolderOption { get; set; } @@ -23,6 +24,11 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null) /// protected Type? RoleType { get; private set; } = roleType; + /// + /// Gets the game mode type of the option. + /// + protected Type? ModeType { get; private set; } = modeType; + /// /// Sets the value of the option. /// diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs index 8f570e6..825915d 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 d686f50..00af52b 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using BepInEx.Configuration; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Networking; @@ -26,6 +27,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 = []; @@ -66,11 +69,23 @@ public static class ModdedOptionsManager notif.SettingsChangeMessageLogic(key, item, playSound); } - [SuppressMessage( - "Major Code Smell", - "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", - Justification = "Dynamic singleton initialization requires reflection to bypass the private field access modifier because the type is only known at runtime." - )] + 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; + } + + RegisterGroupInternal(group, type); + return true; + } + internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo) { if (Activator.CreateInstance(type) is not AbstractOptionGroup group) @@ -84,15 +99,35 @@ public static class ModdedOptionsManager return false; } - Groups.Add(group); - TypeToGroup.Add(type, group); + if (typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType)) + { + if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList)) + { + oldList.Add(group); + } + else + { + GameModeOptionGroups.Add(group.OptionableType, [group]); + } + } pluginInfo.InternalOptionGroups.Add(group); + RegisterGroupInternal(group, type); + return true; + } + + [SuppressMessage( + "Major Code Smell", + "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", + Justification = "Dynamic singleton initialization requires reflection to bypass the private field access modifier because the type is only known at runtime." + )] + private static void RegisterGroupInternal(AbstractOptionGroup group, Type type) + { + Groups.Add(group); + TypeToGroup.Add(type, group); typeof(OptionGroupSingleton<>).MakeGenericType(type) .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! // Message suppression points to here .SetValue(null, group); - - return true; } internal static void RegisterPropertyOption(Type type, PropertyInfo property, MiraPluginInfo pluginInfo) diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs index fa5ed94..19b1a20 100644 --- a/MiraAPI/GameOptions/ModifierOptionGroup.cs +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -1,7 +1,9 @@ using System.Linq; +using MiraAPI.PluginLoading; namespace MiraAPI.GameOptions; +[MiraIgnore] internal sealed class ModifierOptionGroup : AbstractOptionGroup { public override string GroupName { get; } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index aba6228..c6a1ada 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Networking; -using Reactor.Localization.Utilities; +using MiraAPI.Translation; using UnityEngine; using Object = UnityEngine.Object; @@ -35,7 +35,7 @@ public class ModdedEnumOption : ModdedOption var data = ScriptableObject.CreateInstance(); data.Title = StringName; data.Type = global::OptionTypes.String; - data.Values = Values.Select(CustomStringName.CreateAndRegister).ToArray(); + data.Values = Values.Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray(); data.Index = Value; Data = data; @@ -132,7 +132,7 @@ public class ModdedEnumOption : ModdedOption where T : Enum var data = ScriptableObject.CreateInstance(); data.Title = StringName; data.Type = global::OptionTypes.String; - data.Values = Values.Select(CustomStringName.CreateAndRegister).ToArray(); + data.Values = Values.Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray(); data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo); Data = data; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 34811a6..7246465 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -1,8 +1,8 @@ -using System; +using System; using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; -using Reactor.Localization.Utilities; +using MiraAPI.Translation; using Reactor.Networking.Rpc; using UnityEngine; @@ -78,10 +78,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 654fc79..674868a 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..a578ac1 --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -0,0 +1,176 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.Networking; +using MiraAPI.Translation; +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 27d15a1..8b15a31 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..d41b445 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs @@ -0,0 +1,285 @@ +using System.Collections; +using System.Linq; +using BepInEx.Unity.IL2CPP.Utils.Collections; +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; + } + } + + 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 6e74b0b..aa7ee61 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..59a8857 --- /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/Hud/MultiTargetButton.cs b/MiraAPI/Hud/MultiTargetMeetingButton.cs similarity index 96% rename from MiraAPI/Hud/MultiTargetButton.cs rename to MiraAPI/Hud/MultiTargetMeetingButton.cs index 6e4c4b8..436d5a2 100644 --- a/MiraAPI/Hud/MultiTargetButton.cs +++ b/MiraAPI/Hud/MultiTargetMeetingButton.cs @@ -8,7 +8,7 @@ namespace MiraAPI.Hud; /// that has multiple target objects. /// /// The type of the target objects. -public abstract class MultiTargetButton : CustomActionButton where T : MonoBehaviour +public abstract class MultiTargetMeetingButton : CustomActionButton where T : MonoBehaviour { /// /// Gets or sets the list of targets of the button. 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 41ccc5e..5cfd25f 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using BepInEx.Configuration; using HarmonyLib; using MiraAPI.Patches.LocalSettings; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; using TMPro; @@ -47,11 +48,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 +84,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 +240,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 +269,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); })); @@ -268,13 +293,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); @@ -282,7 +307,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(); @@ -292,7 +318,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 9d4fac4..231ba86 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; @@ -36,15 +37,20 @@ public class LocalEnumSetting( /// Gets the values. /// public string[] Values { get; } = values ?? [.. Enum - .GetValues(configEntry.SettingType) - .Cast() - .Select(x => x.ToDisplayString())]; + .GetValues(configEntry.SettingType) + .Cast() + .Select(x => x.ToDisplayString())]; + + private SpriteRenderer _highlight { get; set; } + + private TextMeshPro _btnText { get; set; } /// public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) { 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); @@ -54,6 +60,7 @@ public class LocalEnumSetting( var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); if (highlight != null) { + _highlight = highlight; highlight.color = Tab!.TabAppearance.EnumHoverColor; highlight.gameObject.SetActive(false); } @@ -116,9 +123,19 @@ public class LocalEnumSetting( 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 3f3626f..24bffae 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; @@ -50,11 +51,16 @@ public class LocalNumberSetting( /// public MiraNumberSuffixes SuffixType { get; } = suffixType ?? MiraNumberSuffixes.None; + private SpriteRenderer _highlight { get; set; } + + private TextMeshPro _btnText { get; set; } + /// public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) { 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); @@ -64,6 +70,7 @@ public class LocalNumberSetting( var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); if (highlight != null) { + _highlight = highlight; highlight.color = Tab!.TabAppearance.NumberHoverColor; highlight.gameObject.SetActive(false); } @@ -126,11 +133,21 @@ public class LocalNumberSetting( return button.gameObject; } + /// + public override void RefreshOption() + { + _btnText.text = GetValueText(); + if (_highlight) + { + _highlight.gameObject.SetActive(false); + } + } + /// protected override string GetValueText() { var value = GetValue(); var formatted = Helpers.FormatValue(value, SuffixType, FormatString); - return $"{Name}: {formatted}"; + return $"{Name.Translate()}: {formatted}"; } } 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 f90b6ec..63d17e2 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; @@ -57,10 +58,13 @@ public class LocalSliderSetting( /// public MiraNumberSuffixes SuffixType { get; } = suffixType ?? MiraNumberSuffixes.None; + private SlideBar _slider { get; set; } + /// 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(); @@ -95,6 +99,12 @@ public class LocalSliderSetting( return newSlider.gameObject; } + /// + public override void RefreshOption() + { + _slider.Title.text = GetValueText(); + } + /// protected override string GetValueText() { @@ -103,9 +113,9 @@ public class LocalSliderSetting( var value = GetValue(); var formatted = Helpers.FormatValue(value, SuffixType, FormatString); var maxFormatted = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString); - return $"{Name}: {formatted} / {maxFormatted}"; + return $"{Name.Translate()}: {formatted} / {maxFormatted}"; } - return $"{Name}"; + return $"{Name.Translate()}"; } } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs index 7561b6b..ad7a868 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs @@ -1,7 +1,7 @@ using System; using BepInEx.Configuration; +using MiraAPI.Translation; using MiraAPI.Utilities; -using Reactor.Localization.Utilities; using TMPro; using UnityEngine; using UnityEngine.Events; @@ -23,10 +23,13 @@ public class LocalToggleSetting( string? description = null ) : LocalSettingBase(tab, configEntry, name, description) { + private ToggleButtonBehaviour _toggle { get; set; } + /// 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(); @@ -42,9 +45,9 @@ public class LocalToggleSetting( 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; @@ -81,4 +84,11 @@ public class LocalToggleSetting( 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..3af8c65 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs @@ -0,0 +1,119 @@ +using System.Globalization; +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 . + 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..6844b32 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingActionButton.cs @@ -0,0 +1,317 @@ +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 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); + 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 !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(); + } + + /// + /// 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..6bd50f4 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.PluginLoading; +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); + playerVoteArea.CancelButton.transform.SetAsFirstSibling(); + } + + 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}"); + } + } + } +} 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..7dcec24 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs @@ -0,0 +1,92 @@ +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..b1141ae --- /dev/null +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs @@ -0,0 +1,154 @@ +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; + } +} 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 4dc6457..6ad0e47 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,11 +1,16 @@ 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; @@ -19,10 +24,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). /// @@ -50,10 +71,18 @@ public partial class MiraApiPlugin : BasePlugin public override void Load() { Harmony.PatchAll(); + 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 42d6d39..9e8b1bf 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -1,4 +1,6 @@ -using BepInEx.Configuration; +using System.Collections; +using BepInEx.Configuration; +using MiraAPI.Hud; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Patches; @@ -14,7 +16,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 +27,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,6 +57,18 @@ 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; @@ -56,44 +76,193 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) } /// - /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. + /// Sets up the button positions in the UI. /// - [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; + var wikiButton = MiraHudHelper.VanillaMatchInfoButton; + var subButton = MiraHudHelper.SubmergedFloorButton; + var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; + ResetButtonPositions(); + if (topUi && extraTopUi) + { + var opts = LocalSettingsTabSingleton.Instance; + if (wikiButton) + { + wikiButton.transform.SetParent(opts.WikiOnBottomRow.Value ? extraTopUi.transform : topUi.transform); + } + if (subButton) + { + subButton.transform.SetParent(extraTopUi.transform); + } + if (modDisplay) + { + modDisplay.transform.SetParent(extraTopUi.transform); + } + MiraHudHelper.UiGrid.ArrangeChilds(); + MiraHudHelper.ExtraUiGrid.ArrangeChilds(); + } + } /// - /// Gets whether to show keybinds on buttons. + /// Resets the button positions in the UI. /// - [LocalToggleSetting] - public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Visuals/UI", "Show Keybinds on Buttons", true); + public static void ResetButtonPositions() + { + var topUi = MiraHudHelper.UiTopRight; + var extraTopUi = MiraHudHelper.ExtraUiTopRight; + var subButton = MiraHudHelper.SubmergedFloorButton; + var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; + if (topUi && extraTopUi) + { + var wikiButton = MiraHudHelper.VanillaMatchInfoButton; + if (wikiButton) + { + wikiButton.transform.SetParent(null); + } + if (subButton) + { + subButton.transform.SetParent(null); + } + if (modDisplay) + { + modDisplay.transform.SetParent(null); + } + } + } /// - /// Gets the scale of the buttons. + /// Resizes the UI as a coroutine. + /// + /// The coroutine that resizes the UI. + 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); + } + + /// + /// Resizes the UI based on the provided scaling factor. + /// + /// The factor by which the UI needs to be scaled with. + 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 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); + public ConfigEntry ButtonUIFactorSlider { get; private set; } = 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); + public ConfigEntry SetFpsSlider { get; private set; } = 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); + + /// + /// Gets the flag indicating whether the wiki is on the bottom row. + /// + [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/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs index 25a6651..9af6b53 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -5,6 +5,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; @@ -155,7 +156,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 d767e50..8061250 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Il2CppInterop.Runtime.Attributes; using Reactor.Utilities.Attributes; using TMPro; diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 801a22a..dc1a250 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; @@ -133,7 +134,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; } @@ -158,7 +159,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; @@ -169,7 +170,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 1082f8f..f574fd3 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 { /// @@ -65,6 +69,6 @@ public abstract class GameModifier : BaseModifier /// if the modifier is able to spawn, otherwise . public virtual bool CanSpawnOnCurrentMode() { - return !GameManager.Instance.IsHideAndSeek(); + return CustomGameModeManager.IsClassic() && !GameManager.Instance.IsHideAndSeek(); } } diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 472b3cf..526a452 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -1,4 +1,5 @@ using System; +using MiraAPI.PluginLoading; using UnityEngine; namespace MiraAPI.Modifiers.Types; @@ -6,6 +7,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 2ac485c..c0b4dd0 100644 --- a/MiraAPI/Patches/DetectiveNotesMinigamePatch.cs +++ b/MiraAPI/Patches/DetectiveNotesMinigamePatch.cs @@ -1,13 +1,6 @@ -using System.Linq; -using AmongUs.GameOptions; -using HarmonyLib; -using Il2CppSystem.Collections.Generic; -using MiraAPI.Roles; -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 +32,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..0cc6cb8 100644 --- a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs +++ b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs @@ -2,6 +2,7 @@ using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.MeetingAbilities; using MiraAPI.Utilities; namespace MiraAPI.Patches.Events; @@ -18,6 +19,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 9342dce..2a58937 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; @@ -266,13 +266,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(); @@ -356,15 +352,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) @@ -411,19 +404,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) { @@ -460,7 +450,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..dd235ed --- /dev/null +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -0,0 +1,62 @@ +using System.Collections; +using HarmonyLib; +using Il2CppSystem; +using MiraAPI.GameModes; +using MiraAPI.Translation; +using Reactor.Utilities; +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 207aa1d..327fff3 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -43,12 +43,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) { @@ -83,9 +87,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) { @@ -131,6 +135,47 @@ public static class HudManagerPatches */ #pragma warning restore S125 // Sections of code should not be commented out + [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() + { + 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. /// @@ -139,6 +184,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"); @@ -245,6 +291,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..c42ce9f 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -3,7 +3,10 @@ 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 +32,11 @@ public static class IntroCutscenePatches { var @event = new IntroBeginEvent(__instance); MiraEventManager.InvokeEvent(@event); + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.Initialize(); + } } [HarmonyPatch] @@ -78,8 +86,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 +128,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 b998adb..9e9eeb8 100644 --- a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -72,6 +72,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); @@ -216,6 +217,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 8a49e72..0f526d2 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -3,16 +3,20 @@ 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; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using UnityEngine.ProBuilder; using UnityEngine.UI; using Object = UnityEngine.Object; @@ -21,6 +25,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 +54,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 +77,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() @@ -88,11 +404,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 +422,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 +443,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 +594,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 +696,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 +715,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 +732,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 f4508e3..54af816 100644 --- a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs +++ b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs @@ -6,6 +6,7 @@ using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Presets; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; @@ -44,7 +45,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 { @@ -183,7 +184,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/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index e7f4773..d802921 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -1,15 +1,16 @@ -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; using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; @@ -23,13 +24,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 +82,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 +112,7 @@ public static class LobbyViewPanePatches (UnityAction)(() => { SelectedModIdx += 1; - if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length) + if (SelectedModIdx > Plugins.Length) { SelectedModIdx = 0; } @@ -99,7 +136,7 @@ public static class LobbyViewPanePatches SelectedModIdx -= 1; if (SelectedModIdx < 0) { - SelectedModIdx = MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length; + SelectedModIdx = Plugins.Length; } Refresh(__instance); @@ -108,8 +145,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 +195,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; } @@ -299,7 +360,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); @@ -348,6 +409,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); @@ -384,7 +452,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) { @@ -395,12 +463,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 a0c46eb..cdecfc5 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..b6725f9 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -2,6 +2,7 @@ using Il2CppSystem; using MiraAPI.GameOptions; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using UnityEngine; using Object = Il2CppSystem.Object; @@ -162,7 +163,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 +200,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 +307,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 b564f81..b91e792 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -5,12 +5,13 @@ 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; using Reactor.Networking.Rpc; using Reactor.Utilities.Extensions; using TMPro; @@ -74,7 +75,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 +171,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 +195,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 +205,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 +214,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 +288,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 +331,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 +351,7 @@ public static class RoleSettingMenuPatches } } } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = true; } @@ -516,6 +542,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 +570,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 +605,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..ca7f2be --- /dev/null +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -0,0 +1,164 @@ +using System.Globalization; +using System.Linq; +using HarmonyLib; +using Innersloth.Assets; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using UnityEngine; + +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); + __instance.CreateNormalModeSettings(); + } + } + 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; + } + [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 00857a5..a77d3cf 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,12 +66,19 @@ public static class RoleManagerPatches [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static void ModifierSelectionPatches() { - 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)]); + } + + 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)]); + CustomGameModeManager.ActiveMode.PostAssignRoles(roleSelection); } [HarmonyPrefix] diff --git a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs index 45550ef..a1c7a6e 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) + [HarmonyPatch(nameof(RoleOptionsCollectionV11.AnyRolesEnabled))] + public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV11 __instance) { - 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 8def5a9..71c18f5 100644 --- a/MiraAPI/Patches/Roles/SelectRolesPatch.cs +++ b/MiraAPI/Patches/Roles/SelectRolesPatch.cs @@ -4,6 +4,7 @@ using System.Linq; using AmongUs.GameOptions; using HarmonyLib; using InnerNet; +using MiraAPI.GameModes; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -21,6 +22,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 b848555..90eac2e 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -1,13 +1,17 @@ -using System.Diagnostics.CodeAnalysis; +using System; +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.Voting; +using UnityEngine; namespace MiraAPI.Patches.Voting; @@ -33,10 +37,49 @@ 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 || !__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(); + 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(); @@ -77,7 +120,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 +158,7 @@ internal static class MeetingHudPatches [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.Select))] - public static bool SelectPatch(int suspectStateIdx) + public static bool MeetingHudSelectPatch(int suspectStateIdx) { var voteData = PlayerControl.LocalPlayer.GetVoteData(); @@ -127,13 +180,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 +203,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 +252,7 @@ internal static class MeetingHudPatches }) ]); - __instance.RpcVotingComplete(voterStates, exiled, isTie); + __instance.RpcVotingComplete(voterStates, exiled, isTie, @event.OverruledVote, @event.OverruledNonce); return false; } @@ -223,6 +276,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(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 14f58b7..5cb3b99 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 /// 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 4c170f9..d1b5d51 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 1194533..a56523f 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -10,15 +10,18 @@ using HarmonyLib; 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.Presets; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; @@ -32,18 +35,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; } @@ -117,6 +222,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); } @@ -145,6 +266,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); }; @@ -322,6 +444,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 @@ -424,7 +560,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) { @@ -442,4 +578,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..d6e11d8 --- /dev/null +++ b/MiraAPI/Resources/Locale/es_419.xml @@ -0,0 +1,14 @@ + + + Escala De Botón + Botón de Wiki debajo de la IU principal + Roles + Modificador + Modificadores + Tripulante + Trip + Impostor + Imp + Neutral + Neut + diff --git a/MiraAPI/Resources/Locale/es_ES.xml b/MiraAPI/Resources/Locale/es_ES.xml new file mode 100644 index 0000000..f0e084c --- /dev/null +++ b/MiraAPI/Resources/Locale/es_ES.xml @@ -0,0 +1,12 @@ + + + Roles + Modificador + Modificadores + Tripulante + Tripulante + Impostor + Impostor + Neutro + Neutro + 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..e59c8b8 --- /dev/null +++ b/MiraAPI/Resources/Locale/pl_PL.xml @@ -0,0 +1,14 @@ + + + Skala Przycisków + Przycisk Wiki Poniżej Głównego UI + Role + Modyfikator + Modyfikatory + Załogant + Zał + Intruz + Int + Neutralny + Neut + 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..4bfa806 --- /dev/null +++ b/MiraAPI/Resources/Locale/pt_PT.xml @@ -0,0 +1,14 @@ + + + Fator do Tamanho do Botão + Botão Debaixo do Interface Principal + Funções + Modificador + Modificadores + Tripulante + Tripulante + Impostor + Impostor + Neutro + Neutro + 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 1fb8312..3615ea2 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; @@ -101,6 +102,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 763f5d0..64606c2 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -7,9 +7,9 @@ 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; using Reactor.Networking.Rpc; using Reactor.Utilities.Extensions; using TMPro; @@ -131,9 +131,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 6d09171..3c690ac 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Linq; using System.Text; using AmongUs.GameOptions; +using MiraAPI.GameModes; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -22,8 +24,10 @@ public static class CustomRoleUtils public static bool CanSpawnOnCurrentMode(RoleBehaviour role) { return role is ICustomRole custom - ? custom.CanSpawnOnCurrentMode() - : !GameManager.Instance.IsHideAndSeek() || role.Role is RoleTypes.Engineer or RoleTypes.Impostor; + ? custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode) + : (CustomGameModeManager.ActiveMode is HideAndSeekMode + ? role.Role is RoleTypes.Engineer or RoleTypes.Impostor + : !Helpers.IsRoleBlacklisted(role)); } /// diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index a789838..c7a3cb8 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -4,6 +4,7 @@ using BepInEx.Configuration; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; +using MiraAPI.Translation; using UnityEngine; namespace MiraAPI.Roles; @@ -13,20 +14,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. 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..d0a642a --- /dev/null +++ b/MiraAPI/Translation/MiraLocaleManager.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; +using MiraAPI.Utilities; +using Reactor.Localization.Utilities; +using UnityEngine; + +namespace MiraAPI.Translation; + +public static class MiraLocaleManager +{ + private const string LangDirectory = "mira_languages"; + + public static readonly Dictionary>> Locale = []; + + 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. Copies embedded XML to mira_languages/{modGuid}/ on first run, + /// then loads all XML files from that directory into memory. + /// Call once per mod during plugin Load(). + /// + /// The mod GUID (e.g., "mira.example"). + public static void Register(string modGuid) + { + var callingAssembly = Assembly.GetCallingAssembly(); + var dir = GetModLangDir(modGuid); + + LoadInternalStrings(callingAssembly, callingAssembly.GetName().Name!, modGuid, dir); + } + + /// + /// Registers translations for a mod. Copies embedded XML to mira_languages/{modGuid}/ on first run, + /// then loads all XML files from that directory into memory. + /// 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(); + var dir = GetModLangDir(modGuid); + + LoadInternalStrings(callingAssembly, internalName, modGuid, dir); + } + + /// + /// Translates a key into the current language. + /// Searches all mods, with reverse lookup fallback. + /// + /// The string id to find. + /// Fallback string to use if no translation is found. + /// A based on the key provided. + public static string Get(string key, string fallback = "") + { + var currentLang = CurrentLanguage; + + foreach (var (_, modLocale) in Locale) + { + if (modLocale.TryGetValue(currentLang, out var langDict) && + langDict.TryGetValue(key, out var translated)) + { + return translated; + } + } + + foreach (var (_, modLocale) in Locale) + { + if (modLocale.TryGetValue(MiraLanguage.English, out var engDict) && + engDict.TryGetValue(key, out var english)) + { + return english; + } + } + + string? bestStructuredKey = null; + foreach (var (_, modLocale) in Locale) + { + if (modLocale.TryGetValue(MiraLanguage.English, out var engDict)) + { + foreach (var (structuredKey, englishValue) in engDict) + { + if (englishValue == key) + { + bestStructuredKey = structuredKey; + break; + } + } + } + + if (bestStructuredKey != null) break; + } + + if (bestStructuredKey != null) + { + foreach (var (_, modLocale) in Locale) + { + if (modLocale.TryGetValue(currentLang, out var langDict) && + langDict.TryGetValue(bestStructuredKey, out var revTranslated)) + { + return revTranslated; + } + } + + foreach (var (_, modLocale) in Locale) + { + if (modLocale.TryGetValue(MiraLanguage.English, out var engDict) && + engDict.TryGetValue(bestStructuredKey, out var revEnglish)) + { + return revEnglish; + } + } + } + + return fallback != string.Empty ? fallback : key; + } + + 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; + } + + private static void LoadInternalStrings(Assembly assembly, string internalName, string modGuid, string dir) + { + Directory.CreateDirectory(dir); + + if (!Locale.ContainsKey(modGuid)) + { + Locale[modGuid] = []; + } + + var resourcePrefix = $"{internalName}.Resources.Locale."; + + 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 + { + var dict = ParseXmlFile(xmlContent); + Locale[modGuid][locale.Key] = dict; + Info($"Loaded {locale.Key.ToDisplayString()} translation for mod {modGuid} ({dict.Count} keys)"); + 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}!"); + } + + if (!Locale.TryGetValue(modGuid, out _)) + { + Locale[modGuid] = []; + } + + foreach (var filePath in Directory.GetFiles(dir, "*")) + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + + var lang = LangList.ContainsValue(fileName) + ? LangList.First(x => x.Value == fileName).Key + : (MiraLanguage)(-1); + if ((int)lang == -1) continue; + + try + { + var dict = ParseXmlFile(filePath); + Locale[modGuid][lang] = dict; + Info($"Loaded external {lang.ToDisplayString()} translation for mod {modGuid} ({dict.Count} keys)"); + } + catch (Exception e) + { + Error($"Failed to load external translation {filePath}: {e.Message}"); + } + } + } + + private static Dictionary ParseXmlFile(string xmlContent) + { + var dict = new Dictionary(); + XmlDocument xmlDoc = new(); + try + { + xmlDoc.LoadXml(xmlContent); + XmlNodeList? stringNodes = xmlDoc.SelectNodes("/resources/string"); + + if (stringNodes != null) + { + 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("", "&"); + + dict[name] = value; + } + } + } + } + catch (XmlException ex) + { + Error($"XML parsing error: {ex.Message}"); + } + + return dict; + } +} diff --git a/MiraAPI/Translation/MiraLocalizationProvider.cs b/MiraAPI/Translation/MiraLocalizationProvider.cs new file mode 100644 index 0000000..47cf747 --- /dev/null +++ b/MiraAPI/Translation/MiraLocalizationProvider.cs @@ -0,0 +1,63 @@ +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; + + 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); + } + + 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..ef71fef --- /dev/null +++ b/MiraAPI/Utilities/Components/CenteredGridArrange.cs @@ -0,0 +1,90 @@ +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 666dbdf..e1fd75d 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -8,6 +8,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; @@ -151,7 +152,7 @@ public static class Extensions /// The player's . public static PlayerControl? GetPlayer(this PlayerVoteArea state) { - return GameData.Instance.GetPlayerById(state.TargetPlayerId)?.Object; + return GameData.Instance.GetPlayerById(state.PlayerId)?.Object; } /// @@ -264,7 +265,7 @@ public static class Extensions } } - // Left intentionally, did you guys mean to use it in DeepDestroy? + // Left as is intentionally, did you guys mean to use it in DeepDestroy? [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Not known until intent it clear.")] [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unclear, but please shut up.")] private static IEnumerator Nuke(GameObject? go, bool clearGc) @@ -365,6 +366,68 @@ public static class Extensions GC.Collect(); } + /// + /// Gets all child objects in a parent object. + /// + /// The object to get all children of. + /// 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. + /// + /// The object to get all children of. + /// 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; + + sprite.size = spriteWidth < spriteHeight + ? new Vector2(pixelSize * spriteWidth / spriteHeight, pixelSize) + : 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. /// @@ -610,7 +673,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/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/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index b463ca2..1b2cb27 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.Networking; @@ -42,9 +43,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; @@ -61,7 +62,14 @@ public static class VotingUtils return; } - voteData.DecreaseRemainingVotes(1); + if (byJudge) + { + voteData.SetRemainingVotes(0); + } + else + { + voteData.DecreaseRemainingVotes(1); + } voteData.VoteForPlayer(suspectIdx); } @@ -76,14 +84,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) { @@ -94,13 +102,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); + } + + private 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. /// @@ -126,7 +195,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) @@ -143,7 +212,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); } @@ -152,7 +221,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 @@ -239,7 +308,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)) {