diff --git a/.editorconfig b/.editorconfig index e091cf9..6daa6a7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1 +1,106 @@ -[*.{cs,vb}] \ No newline at end of file +[*.{cs,vb}] + +# IDE0008: Use explicit type +dotnet_diagnostic.IDE0008.severity = none + +# IDE0160: Convert to block scoped namespace +dotnet_diagnostic.IDE0160.severity = none + +# IDE0011: Add braces +dotnet_diagnostic.IDE0011.severity = none + +# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. +dotnet_diagnostic.CS8618.severity = none + +# CA1805: Do not initialize unnecessarily +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 + +# S3267: Use LINQ methods to simply foreach loops +dotnet_diagnostic.S3267.severity = none + +# S125: Sections of code should not be commented out +dotnet_diagnostic.S125.severity = none + +# SA1512: Single-line comments should not be followed by blank line +dotnet_diagnostic.SA1512.severity = none + +# S2696: Instance members should not write to "static" fields +dotnet_diagnostic.S2696.severity = none + +# S2245: Pseudorandom number generators (PRNGs) should not be used in security contexts +dotnet_diagnostic.S2245.severity = none + +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = none + +# SA1633: File should have header +dotnet_diagnostic.SA1633.severity = none + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none + +# SA1516: Elements should be separated by blank line +dotnet_diagnostic.SA1516.severity = none + +# SA1503: Braces should not be omitted +dotnet_diagnostic.SA1503.severity = none + +# SA1201: Elements should appear in the correct order +dotnet_diagnostic.SA1201.severity = none + +# SA1402: File may only contain a single type +dotnet_diagnostic.SA1402.severity = none + +# SA1204: Static elements should appear before instance elements +dotnet_diagnostic.SA1204.severity = none + +# SA1202: Elements should be ordered by access +dotnet_diagnostic.SA1202.severity = none + +# SA1649: File name should match first type name +dotnet_diagnostic.SA1649.severity = none + +# SA1313: Parameter names should begin with lower-case letter +dotnet_diagnostic.SA1313.severity = none + +# SA1636: The file header copyright text should match the copyright text from the settings +dotnet_diagnostic.SA1636.severity = none + +# SA1309: Field names should not begin with underscore +dotnet_diagnostic.SA1309.severity = none + +# SA1407: Arithmetic expressions should declare precedence +dotnet_diagnostic.SA1407.severity = none + +# SA1133: Each attribute should be placed in its own set of square brackets +dotnet_diagnostic.SA1133.severity = none + +# IDE0001: Simplify Names +dotnet_diagnostic.IDE0001.severity = warning + +# IDE0079: Remove unnecessary suppression +dotnet_diagnostic.IDE0079.severity = none + +# S3903: Types should be defined in named namespaces +dotnet_diagnostic.S3903.severity = none + +# IDE0045: Convert to conditional expression +dotnet_diagnostic.IDE0045.severity = none + +# IDE0046: Convert to conditional expression +dotnet_diagnostic.IDE0046.severity = none + +# S1244: Floating point numbers should not be tested for equality +dotnet_diagnostic.S1244.severity = none + +# S2325: Methods and properties that don't access instance data should be static +dotnet_diagnostic.S2325.severity = none diff --git a/.gitignore b/.gitignore index aa43b10..b8ff29a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ .git/ .idea/ -.vs/ +.vs*/ obj/ bin/ .qodo/ *.user -*.lock.json \ No newline at end of file +*.lock.json diff --git a/AmongUs.props b/AmongUs.props index 0819a88..b00f7ff 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -2,7 +2,7 @@ - + diff --git a/Directory.Build.props b/Directory.Build.props index 462fbe9..4bdcab7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,18 +28,15 @@ latest recommended true + $(NoWarn);SA0001 + + + + - - - - all - runtime; build; native; contentfiles; analyzers - + - - $(MSBuildThisFileDirectory)\stylecop.ruleset - diff --git a/ExternalAnnotations/0Harmony.xml b/ExternalAnnotations/0Harmony.xml new file mode 100755 index 0000000..0cb768a --- /dev/null +++ b/ExternalAnnotations/0Harmony.xml @@ -0,0 +1,29 @@ + + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiraAPI.Example/.editorconfig b/MiraAPI.Example/.editorconfig new file mode 100644 index 0000000..7a18861 --- /dev/null +++ b/MiraAPI.Example/.editorconfig @@ -0,0 +1,7 @@ +[*.{cs,vb}] + +# SA1602: Enumeration items should be documented. +dotnet_diagnostic.SA1602.severity = none + +# SA1601: Partial elements should be documented +dotnet_diagnostic.SA1601.severity = none diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index d43bda6..bda713b 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -10,9 +10,7 @@ namespace MiraAPI.Example.Buttons; public class MeetingButton : CustomActionButton { public override string Name => "button.callMeeting"; - public override float Cooldown => 15; - public override int MaxUses => 3; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; @@ -30,10 +28,10 @@ public class MeetingButton : CustomActionButton PlayerControl.LocalPlayer.NetTransform.Halt(); var minigame = Object.Instantiate(bt.MinigamePrefab, Camera.main!.transform, false); - var taskAdderGame = minigame as TaskAdderGame; - if (taskAdderGame != null) + var taskAdderGame = minigame.TryCast(); + if (taskAdderGame) { - taskAdderGame.SafePositionWorld = bt.SafePositionLocal + (Vector2)bt.transform.position; + taskAdderGame!.SafePositionWorld = bt.SafePositionLocal + (Vector2)bt.transform.position; } minigame.transform.localPosition = new Vector3(0f, 0f, -50f); diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index 8dead81..cf963fe 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -61,11 +61,14 @@ public class TeleportButton : CustomActionButton { Camera.main.orthographicSize = MeetingHud.Instance ? 3f : ft; ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); - foreach (var cam in Camera.allCameras) cam.orthographicSize = Camera.main.orthographicSize; + foreach (var cam in Camera.allCameras) + cam.orthographicSize = Camera.main.orthographicSize; + yield return null; } - foreach (var cam in Camera.allCameras) cam.orthographicSize = zoomDistance; + foreach (var cam in Camera.allCameras) + cam.orthographicSize = zoomDistance; ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); } @@ -75,12 +78,14 @@ public class TeleportButton : CustomActionButton { Camera.main.orthographicSize = MeetingHud.Instance ? 3f : ft; ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); - foreach (var cam in Camera.allCameras) cam.orthographicSize = Camera.main.orthographicSize; + foreach (var cam in Camera.allCameras) + cam.orthographicSize = Camera.main.orthographicSize; yield return null; } - foreach (var cam in Camera.allCameras) cam.orthographicSize = 3f; + foreach (var cam in Camera.allCameras) + cam.orthographicSize = 3f; HudManager.Instance.ShadowQuad.gameObject.SetActive(true); IsZoom = false; diff --git a/MiraAPI.Example/Buttons/ThinkButton.cs b/MiraAPI.Example/Buttons/ThinkButton.cs index 71013e9..6497784 100644 --- a/MiraAPI.Example/Buttons/ThinkButton.cs +++ b/MiraAPI.Example/Buttons/ThinkButton.cs @@ -1,4 +1,5 @@ using MiraAPI.Hud; +using MiraAPI.Keybinds; using MiraAPI.Translation; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -13,6 +14,8 @@ public class ThinkButton : CustomActionButton public override int MaxUses => 1; public override ButtonUsesMode UsesMode => ButtonUsesMode.PerRound; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility; + protected override void OnClick() { Button!.OverrideText("button.think.thinking".Translate()); diff --git a/MiraAPI.Example/ExampleColors.cs b/MiraAPI.Example/ExampleColors.cs index cde5aae..297a370 100644 --- a/MiraAPI.Example/ExampleColors.cs +++ b/MiraAPI.Example/ExampleColors.cs @@ -1,4 +1,6 @@ using MiraAPI.Colors; +using MiraAPI.Colors.ColorSpaces; +using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Example; @@ -14,4 +16,9 @@ public static class ExampleColors public static CustomColor Rose { get; } = new("color.rose", new Color(0.98f, 0.26f, 0.62f)); public static CustomColor Gold { get; } = new("color.gold", new Color(1.0f, 0.84f, 0.0f)); + + public static VariableColor Rainbow { get; } = new("color.rainbow", t => new HsbColor(MathUtilities.PingPong(t, 1f, 0.3f), 1f, 1f)) + { + ColorBrightness = CustomColorBrightness.Lighter, + }; } diff --git a/MiraAPI.Example/ExampleKeybinds.cs b/MiraAPI.Example/ExampleKeybinds.cs index dba6b67..4c8f04f 100644 --- a/MiraAPI.Example/ExampleKeybinds.cs +++ b/MiraAPI.Example/ExampleKeybinds.cs @@ -1,4 +1,4 @@ -using MiraAPI.Keybinds; +using MiraAPI.Keybinds; using Rewired; namespace MiraAPI.Example; @@ -6,5 +6,5 @@ namespace MiraAPI.Example; [RegisterCustomKeybinds] public static class ExampleKeybinds { - public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, [ModifierKey.Shift]); + public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, ControllerButton.RightStick, [ModifierKey.Shift]); } diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index 319e2cf..7e9392b 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -33,7 +33,7 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) public ConfigEntry ExampleNumber { get; private set; } = config.Bind("General", "Example Number", 4f); [LocalNumberSetting(min: 0.1f, max: 2.5f, increment: 0.1f, formatString: "0.0", suffixType: MiraNumberSuffixes.Multiplier)] - public ConfigEntry ExampleFloatNumber { get; private set; } = config.Bind("General", "Example Multplier", 1f); + public ConfigEntry ExampleFloatNumber { get; private set; } = config.Bind("General", "Example Multiplier", 1f); [LocalEnumSetting] public ConfigEntry ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries); diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index 4ff4c8e..989e14c 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -21,10 +21,16 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin { MiraLocaleManager.Register("mira.example"); } + public Harmony Harmony { get; } = new(Id); - public string OptionsTitleText => "Mira API\nExample Mod"; - public string CustomOptionMenuNameTwo => "angxl's Options"; - public ConfigFile GetConfigFile() => Config; + public string OptionsTitleText { get; } = "Mira API\nExample Mod"; + public string CustomOptionMenuNameTwo { get; } = "Example Options"; + + public ConfigFile GetConfigFile() + { + return Config; + } + public override void Load() { ExampleEventHandlers.Initialize(); diff --git a/MiraAPI.Example/MiraAPI.Example.csproj b/MiraAPI.Example/MiraAPI.Example.csproj index 675a84e..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 - true + Author Name + $(NoWarn);EnableGenerateDocumentationFile - + - - - + + + diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index 8cd0139..6a7f3a0 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -1,5 +1,4 @@ -using Il2CppSystem; -using MiraAPI.Example.Roles; +using MiraAPI.Example.Roles; using MiraAPI.Modifiers.Types; using MiraAPI.Translation; using UnityEngine; @@ -28,7 +27,7 @@ public class FreezeModifier : TimedModifier if (Player.AmOwner || PlayerControl.LocalPlayer.Data.Role is FreezerRole) { - Player.cosmetics.SetOutline(true, new Nullable(Palette.LightBlue)); + Player.cosmetics.SetOutline(true, new Il2CppSystem.Nullable(Palette.LightBlue)); } } diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index aff6ccf..9b424c0 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -1,31 +1,88 @@ -using MiraAPI.GameOptions; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Example.Options; +[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Demo.")] +[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Read above.")] +[SuppressMessage("Minor Code Smell", "S3400:Methods should not return constants", Justification = "Read above.")] public class ExampleOptions : AbstractOptionGroup { public override string GroupName => "options.exampleOptions1"; public override Color GroupColor => Color.green; + public override bool ShowInMatchInfoGuide => true; [ModdedToggleOption("options.exampleOptions1.toggleOpt1")] - public bool ToggleOpt { get; set; } = false; + [ModdedOptionVisibility] // Automatically resolves to using ToggleOpt1Visibility, can also target toggle options + public bool ToggleOpt1 { get; set; } = false; - [ModdedToggleOption("options.exampleOptions1.toggleOpt1")] + [ModdedToggleOption("options.exampleOptions1.toggleOpt2")] + [ModdedOptionVisibility(nameof(ToggleOpt1))] // ToggleOpt2 will be visible only when ToggleOpt1 is true public bool ToggleOpt2 { get; set; } = true; [ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)] + [ModdedOptionVisibility(nameof(NumberOptVisibility))] // Can also target static methods public float NumberOpt { get; set; } = 4f; - [ModdedEnumOption("options.exampleOptions1.bestApi", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])] - public BestApi Opt { get; set; } = BestApi.MiraAPI; + [ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Distance)] + public int NumberOpt2 { get; set; } = 4; // Can also be int! + + [ModdedEnumOption("options.exampleOptions1.bestApi", typeof(Api), ["Mira API", "Mitochondria", "Reactor"])] + public Api BestApi { get; set; } = Api.MiraAPI; + + [ModdedEnumOption("options.exampleOptions1.worstApi", ["Mira API", "Mitochondria", "Reactor"])] + + // These three can target both instance and static methods + [ModdedOptionGetter(nameof(GetApi))] // Can also automatically resolve to {Option Name}Getter + [ModdedOptionSetter(nameof(SetApi))] // Can also automatically resolve to {Option Name}Setter + [ModdedOptionChanged(nameof(ApiChanged))] // Can also automatically resolve to {Option Name}Changed + public Api WorstApi { get; set; } = Api.MiraAPI; + + [ModdedEnumOptionList("Value State {0}", ["None", "Primary", "Secondary"])] + public ModdedArray ValueStates { get; set; } = [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None]; + + [ModdedPlayerOption("options.exampleOptions1.players")] + public NetworkedPlayerInfo PlayerSelected { get; set; } + + public bool ToggleOpt1Visibility() + { + return true; + } + + public static bool NumberOptVisibility() + { + return true; + } + + public static Api GetApi(Api currentValue) + { + return currentValue; + } + + public static Api SetApi(Api newValue, Api oldValue) + { + return default; + } + + public static void ApiChanged(Api currentValue) + { + // Something + } } -public enum BestApi +public enum Api { MiraAPI, Mitochondria, Reactor, } + +public enum ValueState +{ + None, + Primary, + Secondary, +} diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs index 6acba04..e0d4aaa 100644 --- a/MiraAPI.Example/Options/ExampleOptions2.cs +++ b/MiraAPI.Example/Options/ExampleOptions2.cs @@ -7,15 +7,17 @@ namespace MiraAPI.Example.Options; public class ExampleOptions2 : AbstractOptionGroup { 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 override bool ShowInMatchInfoGuide => true; public ModdedPlayerOption PlayerOption2 { get; } = new("options.exampleOptions2.youMustChoseAPlayer", false); public ModdedPlayerOption PlayerOption { get; } = new("options.exampleOptions2.ehWhatever"); public ModdedToggleOption ToggleOpt1 { get; } = new("options.exampleOptions2.toggleOption1", false); + public ModdedOptionList PlayerNums { get; } = new(5, i => + new($"PlayerNum{i}", 2, 0, 5, 1, Utilities.MiraNumberSuffixes.None)); + public ModdedToggleOption ToggleOpt2 { get; } = new("options.exampleOptions2.toggleOption2", false) { Visible = () => OptionGroupSingleton.Instance.ToggleOpt1, // implicit cast from ModdedToggleOption to bool @@ -24,6 +26,8 @@ public class ExampleOptions2 : AbstractOptionGroup public ModdedEnumOption EnumOpt { get; } = new("options.exampleOptions2.enumOpt", 0) { ChangedEvent = x => Logger.Info($"changed Enum Opt to {x}"), + Getter = currentValue => currentValue, + Setter = (newValue, oldValue) => newValue, }; } diff --git a/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs index 8d0f82d..4d2bd70 100644 --- a/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs +++ b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs @@ -6,14 +6,12 @@ namespace MiraAPI.Example.Options.Modifiers; public class GeneralModifierOptions : AbstractOptionGroup { public override string GroupName => "options.generalModifier"; - public override uint GroupPriority => 0; - public override MenuCategory ParentMenu => MenuCategory.Modifiers; [ModdedToggleOption("options.generalModifier.someBooleanOption")] public bool SomeBooleanOption { get; set; } = true; - [ModdedNumberOption("options.generalModifier.numberOption", 0, 10, 1)] + [ModdedNumberOption("options.generalModifier.numberOption", 0, 10)] public float NumberOption { get; set; } = 5; } diff --git a/MiraAPI.Example/Resources/Locale/en_US.xml b/MiraAPI.Example/Resources/Locale/en_US.xml index e370dc3..b37cad6 100644 --- a/MiraAPI.Example/Resources/Locale/en_US.xml +++ b/MiraAPI.Example/Resources/Locale/en_US.xml @@ -16,11 +16,11 @@ 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. + Chameleon + Become one with the environment. + Fade out of everyone's vision by not moving. + Stay invisible while not moving. + The Chameleon is a Crewmate role that slowly becomes invisible while standing still. Outcast Killer Kill everyone else. Be the last outcast standing. @@ -47,6 +47,8 @@ Eliminate every other faction. Toggle Opt 2 Number Opt Best API + Worst API + Choose A Player Example Options 2 YOU MUST CHOSE A PLAYER Eh whatever @@ -73,11 +75,12 @@ Eliminate every other faction. Example Bool 2 Slider Example Number - Example Multplier + Example Multiplier Example Enum Example Button Outcast Killer Wins! Cerulean Rose Gold + Rainbow diff --git a/MiraAPI.Example/Roles/ChameleonRole.cs b/MiraAPI.Example/Roles/ChameleonRole.cs index c1d9be2..e6e3adf 100644 --- a/MiraAPI.Example/Roles/ChameleonRole.cs +++ b/MiraAPI.Example/Roles/ChameleonRole.cs @@ -3,20 +3,19 @@ using MiraAPI.Patches.Stubs; using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using Reactor.Utilities; -using TMPro; using UnityEngine; namespace MiraAPI.Example.Roles; -public class ChameloenRole : CrewmateRole, ICustomRole +public class ChameleonRole : CrewmateRole, ICustomRole { - public string IdPart => "Chamelon"; + private bool shouldHide; + + public string IdPart => "Chameleon"; public string IdPrefix => "ApiExample.Role.Crewmate"; public Color RoleColor => Palette.AcceptedGreen; public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; - private bool _shouldHide; - public CustomRoleConfiguration Configuration => new(this) { OptionsScreenshot = ExampleAssets.Banner, @@ -29,14 +28,14 @@ public class ChameloenRole : CrewmateRole, ICustomRole public override void Initialize(PlayerControl player) { - Logger.Info("Initializing ChamelonRole for player: " + player.PlayerId); + Logger.Info("Initializing ChameleonRole for player: " + player.PlayerId); RoleBehaviourStubs.Initialize(this, player); - _shouldHide = true; + shouldHide = true; } public void FixedUpdate() { - if (!Player || !_shouldHide) + if (!Player || !shouldHide) { return; } @@ -55,8 +54,8 @@ public class ChameloenRole : CrewmateRole, ICustomRole } else { - SpriteRenderer rend = Player.cosmetics.currentBodySprite.BodySprite; - TextMeshPro tmp = Player.cosmetics.nameText; + var rend = Player.cosmetics.currentBodySprite.BodySprite; + var 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), @@ -75,9 +74,9 @@ public class ChameloenRole : CrewmateRole, ICustomRole public override void Deinitialize(PlayerControl targetPlayer) { - Logger.Info("Deinitializing ChamelonRole for player: " + targetPlayer.PlayerId); + Logger.Info("Deinitializing ChameleonRole for player: " + targetPlayer.PlayerId); RoleBehaviourStubs.Deinitialize(this, targetPlayer); - _shouldHide = false; + shouldHide = false; foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren(true)) { cosmetic.color = Color.white; diff --git a/MiraAPI.Example/Roles/FreezerRole.cs b/MiraAPI.Example/Roles/FreezerRole.cs index 775b373..cf3fade 100644 --- a/MiraAPI.Example/Roles/FreezerRole.cs +++ b/MiraAPI.Example/Roles/FreezerRole.cs @@ -11,7 +11,7 @@ public class FreezerRole : ImpostorRole, ICustomRole public Color RoleColor => Palette.Blue; public ModdedRoleTeams Team => ModdedRoleTeams.Impostor; - public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this) + public CustomRoleConfiguration Configuration => new(this) { Icon = MiraAssets.ImpostorFile, IconTmp = TmpSpriteUtils.CreateSpriteAsset(MiraAssets.ImpostorFile.LoadAsset(), "ApiExample.Role.Impostor.Freezer"), diff --git a/MiraAPI.Example/Roles/TeleporterRole.cs b/MiraAPI.Example/Roles/TeleporterRole.cs index 95b8b6c..72307b0 100644 --- a/MiraAPI.Example/Roles/TeleporterRole.cs +++ b/MiraAPI.Example/Roles/TeleporterRole.cs @@ -10,7 +10,7 @@ public class TeleporterRole : CrewmateRole, ICustomRole public Color RoleColor => new Color32(221, 176, 152, 255); public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; - public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this) + public CustomRoleConfiguration Configuration => new(this) { OptionsScreenshot = ExampleAssets.Banner, CanModifyChance = false, diff --git a/MiraAPI/Colors/ColorSpaces/CmykColor.cs b/MiraAPI/Colors/ColorSpaces/CmykColor.cs new file mode 100644 index 0000000..11ec563 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/CmykColor.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Colors.ColorSpaces; + +/// +/// A struct that denotes the CMYK (Cyan, Magenta, Yellow, Key) color space. +/// +/// The cyan component. +/// The magenta component. +/// The yellow component. +/// The key component. +/// The alpha component. +[Serializable] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")] +public record struct CmykColor(float c, float m, float y, float k, float a = 1f) +{ + /// + /// Implicitly converts a to a Unity . + /// + /// The color being converted. + public static implicit operator Color(CmykColor c) + { + return ToColor(c); + } + + /// + /// Implicitly converts a Unity to a . + /// + /// The color being converted. + public static implicit operator CmykColor(Color c) + { + return FromColor(c); + } + + /// + /// Converts a to a standard Unity RGB . + /// + /// The CMYK color to convert. + /// A Unity representing the same visual color. + public static Color ToColor(CmykColor cmyk) + { + var key = 1f - cmyk.k; + + var r = (1f - cmyk.c) * key; + var g = (1f - cmyk.m) * key; + var b = (1f - cmyk.y) * key; + + return new Color(MathUtilities.Clamp01(r), MathUtilities.Clamp01(g), MathUtilities.Clamp01(b), cmyk.a); + } + + /// + /// Converts a standard Unity RGB to a . + /// + /// The Unity Color to convert. + /// A representing the same visual color. + public static CmykColor FromColor(Color color) + { + var r = color.r; + var g = color.g; + var b = color.b; + + var k = 1f - MathF.Max(r, MathF.Max(g, b)); + + if (k >= 1f) + return new CmykColor(0f, 0f, 0f, 1f, color.a); + + var invK = 1f / (1f - k); + + var c = (1f - r - k) * invK; + var m = (1f - g - k) * invK; + var y = (1f - b - k) * invK; + + return new CmykColor(MathUtilities.Clamp01(c), MathUtilities.Clamp01(m), MathUtilities.Clamp01(y), MathUtilities.Clamp01(k), color.a); + } + + /// + /// Linearly interpolates between two values. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors, clamped to a 0 to 1 range. + /// The interpolated . + public static CmykColor Lerp(CmykColor a, CmykColor b, float t) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors. + /// The interpolated . + public static CmykColor LerpUnclamped(CmykColor a, CmykColor b, float t) + { + return new CmykColor( + MathUtilities.LerpUnclamped(a.c, b.c, t), + MathUtilities.LerpUnclamped(a.m, b.m, t), + MathUtilities.LerpUnclamped(a.y, b.y, t), + MathUtilities.LerpUnclamped(a.k, b.k, t), + MathUtilities.LerpUnclamped(a.a, b.a, t)); + } +} diff --git a/MiraAPI/Colors/ColorSpaces/HsbColor.cs b/MiraAPI/Colors/ColorSpaces/HsbColor.cs new file mode 100644 index 0000000..b566333 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/HsbColor.cs @@ -0,0 +1,136 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Colors.ColorSpaces; + +/// +/// A struct that denotes the HSB (Hue, Saturation, Brightness) color space. +/// +/// The hue component. +/// The saturation component. +/// The brightness component. +/// The alpha component. +[Serializable] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")] +public record struct HsbColor(float h, float s, float b, float a = 1f) +{ + /// + /// Implicitly converts an to a Unity . + /// + /// The color being converted. + public static implicit operator Color(HsbColor c) + { + return ToColor(c); + } + + /// + /// Implicitly converts a Unity to an . + /// + /// The color being converted. + public static implicit operator HsbColor(Color c) + { + return FromColor(c); + } + + /// + /// Converts an to a standard Unity RGB . + /// + /// The HSB color to convert. + /// A Unity representing the same visual color. + public static Color ToColor(HsbColor hsb) + { + if (hsb.s <= float.Epsilon) + return new Color(MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), hsb.a); + + var c = hsb.b * hsb.s; + var hPrime = hsb.h * 6f; + var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f)); + var m = hsb.b - c; + + var (r, g, b) = hPrime switch + { + < 1f => (c, x, 0f), + < 2f => (x, c, 0f), + < 3f => (0f, c, x), + < 4f => (0f, x, c), + < 5f => (x, 0f, c), + <= 6f => (c, 0f, x), + _ => (0f, 0f, 0f), + }; + + return new Color( + MathUtilities.Clamp01(r + m), + MathUtilities.Clamp01(g + m), + MathUtilities.Clamp01(b + m), + hsb.a); + } + + /// + /// Converts a standard Unity RGB to an . + /// + /// The Unity Color to convert. + /// An representing the same visual color. + public static HsbColor FromColor(Color color) + { + var max = MathF.Max(color.r, MathF.Max(color.g, color.b)); + var min = MathF.Min(color.r, MathF.Min(color.g, color.b)); + var delta = max - min; + + var h = 0f; + + if (delta > 0f) + { + if (max == color.r) + h = (color.g - color.b) / delta % 6f; + else if (max == color.g) + h = (color.b - color.r) / delta + 2f; + else + h = (color.r - color.g) / delta + 4f; + + h /= 6f; + + if (h < 0f) + h += 1f; + } + + var s = max == 0f ? 0f : delta / max; + var b = max; + + return new HsbColor(h, s, b, color.a); + } + + /// + /// Linearly interpolates between two values, using the shortest path for the hue. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors, clamped to a 0 to 1 range. + /// The interpolated . + public static HsbColor Lerp(HsbColor a, HsbColor b, float t) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors. + /// The interpolated . + public static HsbColor LerpUnclamped(HsbColor a, HsbColor b, float t) + { + var hueDifference = MathUtilities.Repeat(b.h - a.h, 1f); + + if (hueDifference > 0.5f) + hueDifference -= 1f; + + return new HsbColor( + MathUtilities.Repeat(a.h + (hueDifference * t), 1f), + MathUtilities.LerpUnclamped(a.s, b.s, t), + MathUtilities.LerpUnclamped(a.b, b.b, t), + MathUtilities.LerpUnclamped(a.a, b.a, t)); + } +} diff --git a/MiraAPI/Colors/ColorSpaces/HslColor.cs b/MiraAPI/Colors/ColorSpaces/HslColor.cs new file mode 100644 index 0000000..466f159 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/HslColor.cs @@ -0,0 +1,132 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Colors.ColorSpaces; + +/// +/// A struct that denotes the HSL (Hue, Saturation, Lightness) color space. +/// +/// The hue component. +/// The saturation component. +/// The lightness component. +/// The alpha component. +[Serializable] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")] +public record struct HslColor(float h, float s, float l, float a = 1f) +{ + /// + /// Implicitly converts an to a Unity . + /// + /// The color being converted. + public static implicit operator Color(HslColor c) + { + return ToColor(c); + } + + /// + /// Implicitly converts a Unity to an . + /// + /// The color being converted. + public static implicit operator HslColor(Color c) + { + return FromColor(c); + } + + /// + /// Converts an to a standard Unity RGB . + /// + /// The HSL color to convert. + /// A Unity representing the same visual color. + public static Color ToColor(HslColor hsl) + { + if (hsl.s <= float.Epsilon) + return new Color(MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), hsl.a); + + var c = (1f - MathF.Abs(2f * hsl.l - 1f)) * hsl.s; + var hPrime = hsl.h * 6f; + var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f)); + var m = hsl.l - c / 2f; + + var (r, g, b) = hPrime switch + { + < 1f => (c, x, 0f), + < 2f => (x, c, 0f), + < 3f => (0f, c, x), + < 4f => (0f, x, c), + < 5f => (x, 0f, c), + <= 6f => (c, 0f, x), + _ => (0f, 0f, 0f), + }; + + return new Color(MathUtilities.Clamp01(r + m), MathUtilities.Clamp01(g + m), MathUtilities.Clamp01(b + m), hsl.a); + } + + /// + /// Converts a standard Unity RGB to an . + /// + /// The Unity Color to convert. + /// An representing the same visual color. + public static HslColor FromColor(Color color) + { + var max = MathF.Max(color.r, MathF.Max(color.g, color.b)); + var min = MathF.Min(color.r, MathF.Min(color.g, color.b)); + var l = (max + min) / 2f; + var delta = max - min; + + var s = delta == 0f ? 0f : delta / (1f - MathF.Abs((2f * l) - 1f)); + + var h = 0f; + + if (delta > 0f) + { + if (max == color.r) + h = (color.g - color.b) / delta % 6f; + else if (max == color.g) + h = (color.b - color.r) / delta + 2f; + else + h = (color.r - color.g) / delta + 4f; + + h /= 6f; + + if (h < 0f) + h += 1f; + } + + return new HslColor(h, s, l, color.a); + } + + /// + /// Linearly interpolates between two values, using the shortest path for the hue. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors, clamped to a 0 to 1 range. + /// The interpolated . + public static HslColor Lerp(HslColor a, HslColor b, float t) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors. + /// The interpolated . + public static HslColor LerpUnclamped(HslColor a, HslColor b, float t) + { + var hueDifference = MathUtilities.Repeat(b.h - a.h, 1f); + + if (hueDifference > 0.5f) + hueDifference -= 1f; + + return new HslColor( + MathUtilities.Repeat(a.h + (hueDifference * t), 1f), + MathUtilities.LerpUnclamped(a.s, b.s, t), + MathUtilities.LerpUnclamped(a.l, b.l, t), + MathUtilities.LerpUnclamped(a.a, b.a, t)); + } +} diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs index f1b5c49..db91356 100644 --- a/MiraAPI/Colors/CustomColor.cs +++ b/MiraAPI/Colors/CustomColor.cs @@ -1,4 +1,5 @@ -using MiraAPI.Translation; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Translation; using MiraAPI.Utilities; using UnityEngine; @@ -10,17 +11,37 @@ namespace MiraAPI.Colors; /// The name of the option. /// The main color. /// The shadow color. -public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor) +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal code.")] +[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "Looks weird.")] +public class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor) { /// /// Gets or sets the main color. /// - public Color32 MainColor { get; set; } = mainColor; + public Color32 MainColor + { + get; + set + { + field = value; + InternalMainColor = value; + } + } + = mainColor; /// /// Gets or sets the shadow color. /// - public Color32 ShadowColor { get; set; } = shadowColor; + public Color32 ShadowColor + { + get; + set + { + field = value; + InternalShadowColor = value; + } + } + = shadowColor; /// /// Gets or sets the name of the color. @@ -32,18 +53,24 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha /// public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker; - /// - public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60)) + internal Color InternalMainColor = mainColor; + internal Color InternalShadowColor = shadowColor; + + /// + public CustomColor(StringNames name, Color32 mainColor) + : this(name, mainColor, mainColor.GetShadowColor(60)) { } - /// - public CustomColor(string name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60)) + /// + public CustomColor(string name, Color32 mainColor) + : this(name, mainColor, mainColor.GetShadowColor(60)) { } - /// - public CustomColor(string name, Color32 mainColor, Color32 shadowColor) : this(MiraLocaleManager.GetOrCreateLocaleString(name), mainColor, shadowColor) + /// + public CustomColor(string name, Color32 mainColor, Color32 shadowColor) + : this(MiraLocaleManager.GetOrCreateLocaleString(name), mainColor, shadowColor) { } } @@ -53,6 +80,11 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha /// public enum CustomColorBrightness { + /// + /// An invalid color. + /// + Invalid = -1, + /// /// A lighter color. /// diff --git a/MiraAPI/Colors/PaletteManager.cs b/MiraAPI/Colors/PaletteManager.cs index 3d25b33..3b9cd4a 100644 --- a/MiraAPI/Colors/PaletteManager.cs +++ b/MiraAPI/Colors/PaletteManager.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; +using UnityEngine; namespace MiraAPI.Colors; @@ -15,13 +17,39 @@ public static class PaletteManager public static CustomColor[] RegisteredColors => [.. CustomColors]; internal static readonly List CustomColors = []; + internal static readonly HashSet VariableColorNames = []; + + private static readonly CustomColorBrightness[] ColorBrightnessMap = + [ + CustomColorBrightness.Darker, // Red + CustomColorBrightness.Darker, // Blue + CustomColorBrightness.Darker, // Green + CustomColorBrightness.Lighter, // Pink + CustomColorBrightness.Lighter, // Orange + CustomColorBrightness.Lighter, // Yellow + CustomColorBrightness.Darker, // Black + CustomColorBrightness.Lighter, // White + CustomColorBrightness.Darker, // Purple + CustomColorBrightness.Darker, // Brown + CustomColorBrightness.Lighter, // Cyan + CustomColorBrightness.Lighter, // Lime + CustomColorBrightness.Darker, // Maroon + CustomColorBrightness.Lighter, // Rose + CustomColorBrightness.Lighter, // Banana + CustomColorBrightness.Darker, // Gray + CustomColorBrightness.Darker, // Tan + CustomColorBrightness.Lighter, // Coral + ]; + + private static int originalLength; internal static void RegisterAllColors() { + originalLength = Palette.PlayerColors.Length; + 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); @@ -30,4 +58,90 @@ public static class PaletteManager Palette.TextColors = Palette.TextColors.ToArray().AddRangeToArray(colors); Palette.TextOutlineColors = Palette.TextOutlineColors.ToArray().AddRangeToArray(shadowColors); } + + /// + /// Checks to see if the provided color ID belongs to a variable color. + /// + /// The id of the color. + /// true if the color is a variable color; false otherwise. + public static bool IsVariable(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return false; + + return VariableColorNames.Contains(Palette.ColorNames[colorId]); + } + + /// + /// Checks to see if the provided color ID belongs to a custom color. + /// + /// The ID of the color. + /// true if the color is a custom color; false otherwise. + public static bool IsCustomColor(int colorId) + { + return (uint)(colorId - originalLength) < (uint)CustomColors.Count; + } + + /// + /// Gets the main color of the provided color ID. + /// + /// The id of the color. + /// The main color associated with the provided ID. + public static Color GetMainColor(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return Color.white; + + return !TryGetCustomColor(colorId, out var custom) + ? Palette.PlayerColors[colorId] + : (custom is VariableColor variable + ? variable.EvaluateMainColor(Time.time) + : custom.InternalMainColor); + } + + /// + /// Gets the shadow color of the provided color ID. + /// + /// The ID of the color. + /// The shadow color associated with the provided ID. + public static Color GetShadowColor(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return Color.white; + + return !TryGetCustomColor(colorId, out var custom) + ? Palette.ShadowColors[colorId] + : (custom is VariableColor variable + ? variable.EvaluateShadowColor(Time.time) + : custom.InternalShadowColor); + } + + /// + /// Gets the brightness type of the color associated with the provided ID. + /// + /// The ID of the color. + /// The brightness type of the provided ID. + public static CustomColorBrightness GetBrightness(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return CustomColorBrightness.Invalid; + + return TryGetCustomColor(colorId, out var custom) + ? custom.ColorBrightness + : ColorBrightnessMap[colorId]; + } + + internal static bool TryGetCustomColor(int colorId, [NotNullWhen(true)] out CustomColor? custom) + { + var index = colorId - originalLength; + + if ((uint)index >= (uint)CustomColors.Count) + { + custom = null; + return false; + } + + custom = CustomColors[index]; + return true; + } } diff --git a/MiraAPI/Colors/VariableColor.cs b/MiraAPI/Colors/VariableColor.cs new file mode 100644 index 0000000..bfb9717 --- /dev/null +++ b/MiraAPI/Colors/VariableColor.cs @@ -0,0 +1,72 @@ +using System; +using MiraAPI.LocalSettings; +using MiraAPI.Translation; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Colors; + +/// +/// Represents a dynamic custom color that changes over time. +/// +/// The name of the color. +/// The function to evaluate the dynamic main color. +/// The function to evaluate the dynamic shadow color. +/// The static main color used when dynamic colors are disabled. +/// The static shadow color used when dynamic colors are disabled. +/// The default time that is used to automatically fill in the static colors when the latter is not provided. +public sealed class VariableColor(StringNames name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f) + : CustomColor(name, mainColor ?? mainEvaluator(defaultTime), shadowColor ?? shadowEvaluator?.Invoke(defaultTime) ?? (mainColor ?? mainEvaluator(defaultTime)).GetShadowColor(60)) +{ + /// + /// Gets or sets a delegate that takes in a time value and returns the dynamic main color. + /// + public Func MainEvaluator { get; set; } = mainEvaluator; + + /// + /// Gets or sets a delegate that takes in a time value and returns the dynamic shadow color. + /// + public Func ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).GetShadowColor(0.24f)); + + /// + public VariableColor(StringNames name, Func mainEvaluator, Color32? mainColor = null, float defaultTime = 0f) + : this(name, mainEvaluator, null, mainColor, null, defaultTime) + { + } + + /// + public VariableColor(string name, Func mainEvaluator, Color32? mainColor = null, float defaultTime = 0f) + : this(name, mainEvaluator, null, mainColor, null, defaultTime) + { + } + + /// + public VariableColor(string name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f) + : this(MiraLocaleManager.GetOrCreateLocaleString(name), mainEvaluator, shadowEvaluator, mainColor, shadowColor, defaultTime) + { + } + + /// + /// Evaluates the dynamic main color based on the provided time value. + /// + /// The time to evaluate the color against. + /// The dynamically evaluated color, or the static if static variable colors are enabled in the user's settings. + public Color EvaluateMainColor(float time) + { + return LocalSettingsTabSingleton.Instance.EnableVariableColors.Value + ? MainEvaluator(time) + : InternalMainColor; + } + + /// + /// Evaluates the dynamic shadow color based on the provided time value. + /// + /// The time to evaluate the color against. + /// The dynamically evaluated color, or the static if static variable colors are enabled in the user's settings. + public Color EvaluateShadowColor(float time) + { + return LocalSettingsTabSingleton.Instance.EnableVariableColors.Value + ? ShadowEvaluator(time) + : InternalShadowColor; + } +} diff --git a/MiraAPI/Colors/VariableColorBehaviour.cs b/MiraAPI/Colors/VariableColorBehaviour.cs new file mode 100644 index 0000000..e0d4cea --- /dev/null +++ b/MiraAPI/Colors/VariableColorBehaviour.cs @@ -0,0 +1,102 @@ +using System.Diagnostics.CodeAnalysis; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Colors; +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI; + +/// +/// The base class for variable color behaviour on s. +/// +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")] +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity fields.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +public abstract class BaseVariableColorBehaviour : MonoBehaviour +{ + /// + /// The to apply. + /// + protected VariableColor? color; + + /// + /// Sets the current variable color. + /// + /// The new . + [HideFromIl2Cpp] + public void SetColor(VariableColor? newColor) + { + color = newColor; + } +} + +/// +/// A mono script that adjusts the material colors of a component that it's attached to based on the provided variable color. +/// +[RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")] +public sealed class VariableColorBehaviour : BaseVariableColorBehaviour +{ + private Renderer? rend; + + private static readonly Color VisorColor = Palette.VisorColor; + + /// + /// Sets the new target. + /// + /// The new that the script must target. + public void SetRend(Renderer? newRend) + { + rend = newRend; + } + + private void Awake() + { + rend = GetComponent(); + } + + private void Update() + { + if (color == null || rend == null) + return; + + var time = Time.time; + var material = rend.material; + + material.SetColor(ShaderID.VisorColor, VisorColor); + + material.SetColor(ShaderID.BodyColor, color.EvaluateMainColor(time)); + material.SetColor(ShaderID.BackColor, color.EvaluateShadowColor(time)); + } +} + +/// +/// A mono script that adjusts the graphic colors of a component that it's attached to based on the provided variable color. +/// +[RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")] +public sealed class BasicVariableColorBehaviour : BaseVariableColorBehaviour +{ + private SpriteRenderer? rend; + + /// + /// Sets the new target. + /// + /// The new that the script must target. + public void SetRend(SpriteRenderer? newRend) + { + rend = newRend; + } + + private void Awake() + { + rend = GetComponent(); + } + + private void Update() + { + if (color != null && rend != null) + rend.color = color.EvaluateMainColor(Time.time); + } +} diff --git a/MiraAPI/Events/Mira/MiraButtonCancelledEvent.cs b/MiraAPI/Events/Mira/MiraButtonCancelledEvent.cs index e6c7c15..6973c4f 100644 --- a/MiraAPI/Events/Mira/MiraButtonCancelledEvent.cs +++ b/MiraAPI/Events/Mira/MiraButtonCancelledEvent.cs @@ -6,39 +6,24 @@ namespace MiraAPI.Events.Mira; /// Invoked when a click is cancelled. Do not use for vanilla s. /// /// The type. -public sealed class MiraButtonCancelledEvent : MiraEvent where T : CustomActionButton +/// The whose click was cancelled. +public sealed class MiraButtonCancelledEvent(T button) : MiraEvent + where T : CustomActionButton { /// /// Gets the whose click was cancelled. /// - public T Button { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The whose click was cancelled. - public MiraButtonCancelledEvent(T button) - { - Button = button; - } + public T Button { get; } = button; } /// /// Invoked when a click is cancelled. Do not use for vanilla s. /// -public sealed class MiraButtonCancelledEvent : MiraEvent +/// The whose click was cancelled. +public sealed class MiraButtonCancelledEvent(CustomActionButton button) : MiraEvent { /// /// Gets the whose click was cancelled. /// - public CustomActionButton Button { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The whose click was cancelled. - public MiraButtonCancelledEvent(CustomActionButton button) - { - Button = button; - } + public CustomActionButton Button { get; } = button; } diff --git a/MiraAPI/Events/Mira/MiraButtonClickEvent.cs b/MiraAPI/Events/Mira/MiraButtonClickEvent.cs index 0b18028..72237d2 100644 --- a/MiraAPI/Events/Mira/MiraButtonClickEvent.cs +++ b/MiraAPI/Events/Mira/MiraButtonClickEvent.cs @@ -6,46 +6,30 @@ namespace MiraAPI.Events.Mira; /// Button click event for s only. Do not use for vanilla s. /// /// The type. -public class MiraButtonClickEvent : MiraCancelableEvent where T : CustomActionButton +/// The that was clicked. +/// The generic invoked before button-specific events. +public class MiraButtonClickEvent(T button, MiraButtonClickEvent genericClickEvent) : MiraCancelableEvent + where T : CustomActionButton { /// /// Gets the that was clicked. /// - public T Button { get; } + public T Button { get; } = button; /// /// Gets the generic that is invoked before this button-specific event. /// - public MiraButtonClickEvent GenericClickEvent { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The that was clicked. - /// The generic invoked before button-specific events. - public MiraButtonClickEvent(T button, MiraButtonClickEvent genericClickEvent) - { - GenericClickEvent = genericClickEvent; - Button = button; - } + public MiraButtonClickEvent GenericClickEvent { get; } = genericClickEvent; } /// /// Button click event for s only. Do not use for vanilla s. /// -public class MiraButtonClickEvent : MiraCancelableEvent +/// The that was clicked. +public class MiraButtonClickEvent(CustomActionButton button) : MiraCancelableEvent { /// /// Gets the that was clicked. /// - public CustomActionButton Button { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The that was clicked. - public MiraButtonClickEvent(CustomActionButton button) - { - Button = button; - } + public CustomActionButton Button { get; } = button; } diff --git a/MiraAPI/Events/Mira/UiButtonPostResetEvent.cs b/MiraAPI/Events/Mira/UiButtonPostResetEvent.cs index e7221ae..38c1755 100644 --- a/MiraAPI/Events/Mira/UiButtonPostResetEvent.cs +++ b/MiraAPI/Events/Mira/UiButtonPostResetEvent.cs @@ -5,26 +5,17 @@ namespace MiraAPI.Events.Mira; /// /// Event fired by Mira API to adjust the UI buttons at the top right of the screen. /// -public class UiButtonPostResetEvent : MiraEvent +/// The for the top UI row. +/// The for the second UI row. +public class UiButtonPostResetEvent(GameObject topUiRow, GameObject secondUiRow) : MiraEvent { /// /// Gets the parent for the very top UI row. /// - public GameObject MainTopUiRow { get; } + public GameObject MainTopUiRow { get; } = topUiRow; /// /// Gets the parent for the second top UI row. /// - public GameObject SecondTopUiRow { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The for the top UI row. - /// The for the second UI row. - public UiButtonPostResetEvent(GameObject topUiRow, GameObject secondUiRow) - { - MainTopUiRow = topUiRow; - SecondTopUiRow = secondUiRow; - } + public GameObject SecondTopUiRow { get; } = secondUiRow; } diff --git a/MiraAPI/Events/MiraEventManager.cs b/MiraAPI/Events/MiraEventManager.cs index 30ffb74..066ef74 100644 --- a/MiraAPI/Events/MiraEventManager.cs +++ b/MiraAPI/Events/MiraEventManager.cs @@ -17,9 +17,11 @@ public static class MiraEventManager /// The instance. /// Type of Event. /// If there was an event handler invoked for this event, return . Otherwise, return . - public static bool InvokeEvent(T eventInstance) where T : MiraEvent + public static bool InvokeEvent(this T eventInstance) + where T : MiraEvent { EventWrappers.TryGetValue(typeof(T), out var handlers); + if (handlers == null || handlers.Count == 0) { return false; @@ -46,9 +48,10 @@ public static class MiraEventManager /// The instance. /// The type to use for handler lookup. /// If there was an event handler invoked for this event, return . Otherwise, return . - public static bool InvokeEvent(MiraEvent eventInstance, Type type) + public static bool InvokeEvent(this MiraEvent eventInstance, Type type) { EventWrappers.TryGetValue(type, out var handlers); + if (handlers == null || handlers.Count == 0) { return false; @@ -107,7 +110,8 @@ public static class MiraEventManager /// The priority of the event handler. Lower values are called first. /// Type of event. /// A to use when unregistering the event. - public static MiraEventHandle RegisterEventHandler(Action handler, int priority = 0) where T : MiraEvent + public static MiraEventHandle RegisterEventHandler(Action handler, int priority = 0) + where T : MiraEvent { EventWrappers.TryAdd(typeof(T), []); @@ -146,6 +150,7 @@ public static class MiraEventManager { EventWrappers.Remove(eventHandle.EventType); } + return true; } } diff --git a/MiraAPI/Events/MiraEventWrapper.cs b/MiraAPI/Events/MiraEventWrapper.cs index 3784688..2a2c410 100644 --- a/MiraAPI/Events/MiraEventWrapper.cs +++ b/MiraAPI/Events/MiraEventWrapper.cs @@ -5,26 +5,17 @@ namespace MiraAPI.Events; /// /// Wrapper for handlers. /// -public class MiraEventWrapper +/// The action for the event handler. +/// The priority of the handler. +public class MiraEventWrapper(Delegate eventHandler, int priority) { /// /// Gets the event handler delegate. /// - public Delegate EventHandler { get; } + public Delegate EventHandler { get; } = eventHandler; /// /// Gets the priority of the handler. Lower values are called first. /// - public int Priority { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The action for the event handler. - /// The priority of the handler. - public MiraEventWrapper(Delegate eventHandler, int priority) - { - EventHandler = eventHandler; - Priority = priority; - } + public int Priority { get; } = priority; } diff --git a/MiraAPI/Events/RegisterEventAttribute.cs b/MiraAPI/Events/RegisterEventAttribute.cs index 2fee367..062f30e 100644 --- a/MiraAPI/Events/RegisterEventAttribute.cs +++ b/MiraAPI/Events/RegisterEventAttribute.cs @@ -5,20 +5,12 @@ namespace MiraAPI.Events; /// /// Register a handler. /// +/// The priority of the event. Lower values are called first. [AttributeUsage(AttributeTargets.Method)] -public class RegisterEventAttribute : Attribute +public class RegisterEventAttribute(int priority = 0) : Attribute { /// /// Gets the priority of the event. Lower values are called first. /// - public int Priority { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The priority of the event. Lower values are called first. - public RegisterEventAttribute(int priority = 0) - { - Priority = priority; - } + public int Priority { get; } = priority; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/AfterMurderEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/AfterMurderEvent.cs index 8db74b9..1ca09d6 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/AfterMurderEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/AfterMurderEvent.cs @@ -3,38 +3,44 @@ /// /// Event that is invoked after a player is murdered. Only called after a successful murder. This event is not cancelable. /// -public class AfterMurderEvent : MiraEvent +/// The that killed the . +/// The that was killed. +/// The that was framed. +/// The 's , if it exists. +/// Whether the murder was caused indirectly. +/// Whether the murder ignored protection. +public class AfterMurderEvent(PlayerControl source, PlayerControl target, PlayerControl? framed, DeadBody? deadBody, bool isIndirect = true, bool ignoreDefense = false) : MiraEvent { /// /// Gets the that killed the . /// - public PlayerControl Source { get; } + public PlayerControl Source { get; } = source; /// /// Gets the that was killed. /// - public PlayerControl Target { get; } + public PlayerControl Target { get; } = target; /// /// Gets the that was framed, if any. /// - public PlayerControl? Framed { get; } + public PlayerControl? Framed { get; } = framed; /// /// Gets a value indicating whether the murder was done indirectly. /// This is commonly used to prevent the from being killed. /// - public bool IsIndirectAttack { get; } + public bool IsIndirectAttack { get; } = isIndirect; /// /// Gets a value indicating whether the murder was able to bypass protective roles or general defense, such as . /// - public bool IgnoreDefense { get; } + public bool IgnoreDefense { get; } = ignoreDefense; /// /// Gets the 's , if it exists. /// - public DeadBody? DeadBody { get; } + public DeadBody? DeadBody { get; } = deadBody; /// /// Initializes a new instance of the class. @@ -45,32 +51,8 @@ public class AfterMurderEvent : MiraEvent /// Whether the murder was caused indirectly. /// Whether the murder ignored protection. public AfterMurderEvent(PlayerControl source, PlayerControl target, DeadBody? deadBody, bool isIndirect, bool ignoreDefense = false) + : this(source, target, null, deadBody, isIndirect, ignoreDefense) { - Source = source; - Target = target; - Framed = null; - DeadBody = deadBody; - IsIndirectAttack = isIndirect; - IgnoreDefense = ignoreDefense; - } - - /// - /// Initializes a new instance of the class. - /// - /// The that killed the . - /// The that was killed. - /// The that was framed. - /// The 's , if it exists. - /// Whether the murder was caused indirectly. - /// Whether the murder ignored protection. - public AfterMurderEvent(PlayerControl source, PlayerControl target, PlayerControl framed, DeadBody? deadBody, bool isIndirect = true, bool ignoreDefense = false) - { - Source = source; - Target = target; - Framed = framed; - DeadBody = deadBody; - IsIndirectAttack = isIndirect; - IgnoreDefense = ignoreDefense; } /// @@ -80,12 +62,7 @@ public class AfterMurderEvent : MiraEvent /// The that was killed. /// The 's , if it exists. public AfterMurderEvent(PlayerControl source, PlayerControl target, DeadBody? deadBody) + : this(source, target, null, deadBody) { - Source = source; - Target = target; - Framed = null; - DeadBody = deadBody; - IsIndirectAttack = false; - IgnoreDefense = false; } } diff --git a/MiraAPI/Events/Vanilla/Gameplay/BeforeGameEndEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/BeforeGameEndEvent.cs index 9703bd5..0b96741 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/BeforeGameEndEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/BeforeGameEndEvent.cs @@ -3,19 +3,11 @@ /// /// Invoked before is called, allowing cancellation of the game end entirely. /// -public class BeforeGameEndEvent : MiraCancelableEvent +/// The reason for the game end. +public class BeforeGameEndEvent(GameOverReason reason) : MiraCancelableEvent { /// /// Gets the reason for the game end. /// - public GameOverReason Reason { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The reason for the game end. - public BeforeGameEndEvent(GameOverReason reason) - { - Reason = reason; - } + public GameOverReason Reason { get; } = reason; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/BeforeMurderEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/BeforeMurderEvent.cs index 00a977e..4441391 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/BeforeMurderEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/BeforeMurderEvent.cs @@ -5,38 +5,56 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// /// Event that is invoked before a player is murdered. This event is cancelable. /// -public sealed class BeforeMurderEvent : MiraCancelableEvent +/// The that is killing the . +/// The that is being killed. +/// The that is being framed. +/// Whether the murder is caused indirectly. +/// Whether the murder should ignore protection. +/// Whether the murder is intended to be triggered in a meeting. +public sealed class BeforeMurderEvent(PlayerControl source, PlayerControl target, PlayerControl? framed, bool isIndirect = true, bool ignoreDefense = false, MeetingCheck inMeeting = MeetingCheck.Ignore) + : MiraCancelableEvent { /// /// Gets the that is killing the . /// - public PlayerControl Source { get; } + public PlayerControl Source { get; } = source; /// /// Gets the that is being killed. /// - public PlayerControl Target { get; } + public PlayerControl Target { get; } = target; /// /// Gets the that is being framed, if any. /// - public PlayerControl? Framed { get; } + public PlayerControl? Framed { get; } = framed; /// /// Gets or sets a value indicating whether the murder is done indirectly. /// This is commonly used to prevent the from being killed. /// - public bool IsIndirectAttack { get; set; } + public bool IsIndirectAttack { get; set; } = isIndirect; /// /// Gets or sets a value indicating whether the murder should bypass protective roles or general defense, such as . /// - public bool IgnoreDefense { get; set; } + public bool IgnoreDefense { get; set; } = ignoreDefense; /// /// Gets whether the murder was meant to be done in a meeting, via an enum. /// - public MeetingCheck InMeeting { get; } + public MeetingCheck InMeeting { get; } = inMeeting; + + /// + /// Initializes a new instance of the class. + /// + /// The that is killing the . + /// The that is being killed. + /// Whether the murder is intended to be triggered in a meeting. + public BeforeMurderEvent(PlayerControl source, PlayerControl target, MeetingCheck inMeeting = MeetingCheck.Ignore) + : this(source, target, false, false, inMeeting) + { + } /// /// Initializes a new instance of the class. @@ -47,62 +65,7 @@ public sealed class BeforeMurderEvent : MiraCancelableEvent /// Whether the murder should ignore protection. /// Whether the murder is intended to be triggered in a meeting. public BeforeMurderEvent(PlayerControl source, PlayerControl target, bool isIndirect, bool ignoreDefense = false, MeetingCheck inMeeting = MeetingCheck.Ignore) + : this(source, target, null, isIndirect, ignoreDefense, inMeeting) { - Source = source; - Target = target; - Framed = null; - IsIndirectAttack = isIndirect; - IgnoreDefense = ignoreDefense; - InMeeting = inMeeting; - } - - /// - /// Initializes a new instance of the class. - /// - /// The that is killing the . - /// The that is being killed. - /// The that is being framed. - /// Whether the murder is caused indirectly. - /// Whether the murder should ignore protection. - /// Whether the murder is intended to be triggered in a meeting. - public BeforeMurderEvent(PlayerControl source, PlayerControl target, PlayerControl framed, bool isIndirect = true, bool ignoreDefense = false, MeetingCheck inMeeting = MeetingCheck.Ignore) - { - Source = source; - Target = target; - Framed = framed; - IsIndirectAttack = isIndirect; - IgnoreDefense = ignoreDefense; - InMeeting = inMeeting; - } - - /// - /// Initializes a new instance of the class. - /// - /// The that is killing the . - /// The that is being killed. - /// Whether the murder is intended to be triggered in a meeting. - public BeforeMurderEvent(PlayerControl source, PlayerControl target, MeetingCheck inMeeting) - { - Source = source; - Target = target; - Framed = null; - IsIndirectAttack = false; - IgnoreDefense = false; - InMeeting = inMeeting; - } - - /// - /// Initializes a new instance of the class. - /// - /// The that is killing the . - /// The that is being killed. - public BeforeMurderEvent(PlayerControl source, PlayerControl target) - { - Source = source; - Target = target; - Framed = null; - IsIndirectAttack = false; - IgnoreDefense = false; - InMeeting = MeetingCheck.Ignore; } } diff --git a/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs index 77422af..841f24d 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs @@ -1,22 +1,13 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// -/// Event invoked on and -/// to determine if it should run the event or not. +/// Event invoked on and to determine if it should run the event or not. /// -public class BeforeRoundStartEvent : MiraCancelableEvent +/// Whether the event was triggered by the intro or not. +public class BeforeRoundStartEvent(bool triggeredByIntro) : MiraCancelableEvent { /// /// Gets a value indicating whether the event was triggered by the or . /// - public bool TriggeredByIntro { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Whether the event was triggered by the intro or not. - public BeforeRoundStartEvent(bool triggeredByIntro) - { - TriggeredByIntro = triggeredByIntro; - } + public bool TriggeredByIntro { get; } = triggeredByIntro; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/GameEndEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/GameEndEvent.cs index d44dae5..5d94b4b 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/GameEndEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/GameEndEvent.cs @@ -1,21 +1,13 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// -/// The event that is invoked when the end game screen is shown. Non cancelable. +/// The event that is invoked when the end game screen is shown. Non-cancelable. /// -public class GameEndEvent : MiraEvent +/// The instance. +public class GameEndEvent(EndGameManager manager) : MiraEvent { /// /// Gets the instance. /// - public EndGameManager EndGameManager { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The instance. - public GameEndEvent(EndGameManager manager) - { - EndGameManager = manager; - } + public EndGameManager EndGameManager { get; } = manager; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/IntroBeginEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/IntroBeginEvent.cs index b08ec56..b381390 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/IntroBeginEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/IntroBeginEvent.cs @@ -1,21 +1,13 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// -/// The event that is invoked when the intro cutscene is shown. Non cancelable. +/// The event that is invoked when the intro cutscene is shown. Non-cancelable. /// -public class IntroBeginEvent : MiraEvent +/// The intro cutscene. +public class IntroBeginEvent(IntroCutscene introCutscene) : MiraEvent { /// /// Gets the instance. /// - public IntroCutscene IntroCutscene { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The intro cutscene. - public IntroBeginEvent(IntroCutscene introCutscene) - { - IntroCutscene = introCutscene; - } + public IntroCutscene IntroCutscene { get; } = introCutscene; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/IntroEndEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/IntroEndEvent.cs index a23b960..bdd64bd 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/IntroEndEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/IntroEndEvent.cs @@ -3,6 +3,7 @@ /// /// The event that is invoked when the intro cutscene has finished playing. /// +/// The intro cutscene. public class IntroEndEvent(IntroCutscene introCutscene) : MiraEvent { /// diff --git a/MiraAPI/Events/Vanilla/Gameplay/IntroRoleRevealEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/IntroRoleRevealEvent.cs index 1ffdf67..417e96b 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/IntroRoleRevealEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/IntroRoleRevealEvent.cs @@ -1,21 +1,13 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// -/// The event that is invoked when the player's role is shown on the intro cutscene. Non cancelable. +/// The event that is invoked when the player's role is shown on the intro cutscene. Non-cancelable. /// -public class IntroRoleRevealEvent : MiraEvent +/// The intro cutscene. +public class IntroRoleRevealEvent(IntroCutscene introCutscene) : MiraEvent { /// /// Gets the instance. /// - public IntroCutscene IntroCutscene { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The intro cutscene. - public IntroRoleRevealEvent(IntroCutscene introCutscene) - { - IntroCutscene = introCutscene; - } + public IntroCutscene IntroCutscene { get; } = introCutscene; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs index 93a7bfd..e9356ed 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs @@ -4,19 +4,11 @@ /// Start Round event, invoked on and /// if the isn't cancelled. /// -public class RoundStartEvent : MiraEvent +/// Whether the event was triggered by the intro or not. +public class RoundStartEvent(bool triggeredByIntro) : MiraEvent { /// /// Gets a value indicating whether the event was triggered by the or . /// - public bool TriggeredByIntro { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Whether the event was triggered by the intro or not. - public RoundStartEvent(bool triggeredByIntro) - { - TriggeredByIntro = triggeredByIntro; - } + public bool TriggeredByIntro { get; } = triggeredByIntro; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/SetRoleEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/SetRoleEvent.cs index 1ebfd78..f20996a 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/SetRoleEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/SetRoleEvent.cs @@ -5,26 +5,17 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// /// Event that is invoked after a player's role is set. This event is not cancelable. /// -public class SetRoleEvent : MiraEvent +/// The player. +/// The new role. +public class SetRoleEvent(PlayerControl player, RoleTypes role) : MiraEvent { /// /// Gets the whose role was changed. /// - public PlayerControl Player { get; } + public PlayerControl Player { get; } = player; /// /// Gets the role that the player was set to. /// - public RoleTypes Role { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The player. - /// The new role. - public SetRoleEvent(PlayerControl player, RoleTypes role) - { - Player = player; - Role = role; - } + public RoleTypes Role { get; } = role; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/VanillaButtonCancelledEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/VanillaButtonCancelledEvent.cs index 9e334b8..7320080 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/VanillaButtonCancelledEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/VanillaButtonCancelledEvent.cs @@ -6,39 +6,24 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// Invoked when a Vanilla click is cancelled. Do not use for . /// /// The vanilla type. -public sealed class VanillaButtonCancelledEvent : MiraEvent where T : AbilityButton +/// The vanilla whose click was cancelled. +public sealed class VanillaButtonCancelledEvent(T button) : MiraEvent + where T : AbilityButton { /// /// Gets the Vanilla whose click was cancelled. /// - public T Button { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The vanilla whose click was cancelled. - public VanillaButtonCancelledEvent(T button) - { - Button = button; - } + public T Button { get; } = button; } /// /// Invoked when a Vanilla click is cancelled. Do not use for s. /// -public sealed class VanillaButtonCancelledEvent : MiraEvent +/// The vanilla whose click was cancelled. +public sealed class VanillaButtonCancelledEvent(AbilityButton button) : MiraEvent { /// /// Gets the Vanilla whose click was cancelled. /// - public AbilityButton Button { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The vanilla whose click was cancelled. - public VanillaButtonCancelledEvent(AbilityButton button) - { - Button = button; - } + public AbilityButton Button { get; } = button; } diff --git a/MiraAPI/Events/Vanilla/Gameplay/VanillaButtonClickEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/VanillaButtonClickEvent.cs index 563cf66..cd7028a 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/VanillaButtonClickEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/VanillaButtonClickEvent.cs @@ -6,60 +6,42 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// Button click event for Vanilla s only. Do not use for s. /// /// The vanilla type. -public sealed class VanillaButtonClickEvent : MiraCancelableEvent where T : AbilityButton +/// The vanilla that was clicked. +/// The generic invoked before button-specific events. +public sealed class VanillaButtonClickEvent(T button, VanillaButtonClickEvent genericClickEvent) : MiraCancelableEvent + where T : AbilityButton { /// /// Gets the vanilla that was clicked. /// - public T Button { get; } + public T Button { get; } = button; /// /// Gets the generic that is invoked before this button-specific event. /// - public VanillaButtonClickEvent GenericClickEvent { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The vanilla that was clicked. - /// The generic invoked before button-specific events. - public VanillaButtonClickEvent(T button, VanillaButtonClickEvent genericClickEvent) - { - GenericClickEvent = genericClickEvent; - Button = button; - } + public VanillaButtonClickEvent GenericClickEvent { get; } = genericClickEvent; } /// /// Button click event for Vanilla only. Do not use for s. /// -public sealed class VanillaButtonClickEvent : MiraCancelableEvent +/// The Vanilla that was clicked. +/// The target, if any. +/// The target, if any. +public sealed class VanillaButtonClickEvent(AbilityButton button, PlayerControl? playerTarget = null, Vent? ventTarget = null) : MiraCancelableEvent { /// /// Gets the Vanilla that was clicked. /// - public AbilityButton Button { get; } + public AbilityButton Button { get; } = button; /// /// Gets the target , if any, of the Vanilla that was clicked. /// - public PlayerControl? PlayerTarget { get; } + public PlayerControl? PlayerTarget { get; } = playerTarget; /// /// Gets the target , if any, of the Vanilla that was clicked. /// - public Vent? VentTarget { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The Vanilla that was clicked. - /// The target, if any. - /// The target, if any. - public VanillaButtonClickEvent(AbilityButton button, PlayerControl? playerTarget = null, Vent? ventTarget = null) - { - Button = button; - PlayerTarget = playerTarget; - VentTarget = ventTarget; - } + public Vent? VentTarget { get; } = ventTarget; } diff --git a/MiraAPI/Events/Vanilla/Map/CloseDoorsEvent.cs b/MiraAPI/Events/Vanilla/Map/CloseDoorsEvent.cs index 86f2bfd..d0ce1cc 100644 --- a/MiraAPI/Events/Vanilla/Map/CloseDoorsEvent.cs +++ b/MiraAPI/Events/Vanilla/Map/CloseDoorsEvent.cs @@ -3,19 +3,11 @@ /// /// Event fired when a player closes the doors in a room. /// -public class CloseDoorsEvent : MiraCancelableEvent +/// The room that was closed. +public class CloseDoorsEvent(SystemTypes room) : MiraCancelableEvent { /// /// Gets the room that the doors were closed in. /// - public SystemTypes Room { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The room that was closed. - public CloseDoorsEvent(SystemTypes room) - { - Room = room; - } + public SystemTypes Room { get; } = room; } diff --git a/MiraAPI/Events/Vanilla/Map/PlayerOpenSabotageEvent.cs b/MiraAPI/Events/Vanilla/Map/PlayerOpenSabotageEvent.cs index 6415d70..4e9327f 100644 --- a/MiraAPI/Events/Vanilla/Map/PlayerOpenSabotageEvent.cs +++ b/MiraAPI/Events/Vanilla/Map/PlayerOpenSabotageEvent.cs @@ -3,19 +3,11 @@ /// /// Sabotage from Vanilla Among Us. /// -public class PlayerOpenSabotageEvent : MiraCancelableEvent +/// The . +public class PlayerOpenSabotageEvent(MapBehaviour mapBehaviour) : MiraCancelableEvent { /// /// Gets the . /// - public MapBehaviour MapBehaviour { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The . - public PlayerOpenSabotageEvent(MapBehaviour mapBehaviour) - { - MapBehaviour = mapBehaviour; - } + public MapBehaviour MapBehaviour { get; } = mapBehaviour; } diff --git a/MiraAPI/Events/Vanilla/Map/UpdateSystemEvent.cs b/MiraAPI/Events/Vanilla/Map/UpdateSystemEvent.cs index 5e2219c..b1382e5 100644 --- a/MiraAPI/Events/Vanilla/Map/UpdateSystemEvent.cs +++ b/MiraAPI/Events/Vanilla/Map/UpdateSystemEvent.cs @@ -3,33 +3,23 @@ /// /// that is invoked before a system is updated. Usually used for sabotage events. /// -public class UpdateSystemEvent : MiraCancelableEvent +/// The being updated. +/// The that is updating the system. +/// Amount to update System to. +public class UpdateSystemEvent(SystemTypes systemType, PlayerControl player, byte amount) : MiraCancelableEvent { /// /// Gets the type of system that will be updated. /// - public SystemTypes SystemType { get; } + public SystemTypes SystemType { get; } = systemType; /// /// Gets the that is updating the system. /// - public PlayerControl Player { get; } + public PlayerControl Player { get; } = player; /// /// Gets the byte amount the system is being updated by. /// - public byte Amount { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The being updated. - /// The that is updating the system. - /// Amount to update System to. - public UpdateSystemEvent(SystemTypes systemType, PlayerControl player, byte amount) - { - SystemType = systemType; - Player = player; - Amount = amount; - } + public byte Amount { get; } = amount; } diff --git a/MiraAPI/Events/Vanilla/Meeting/EjectionEvent.cs b/MiraAPI/Events/Vanilla/Meeting/EjectionEvent.cs index 054335c..5c0eaef 100644 --- a/MiraAPI/Events/Vanilla/Meeting/EjectionEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/EjectionEvent.cs @@ -3,19 +3,11 @@ /// /// The event that is invoked when a player is ejected. Non-cancelable. /// -public class EjectionEvent : MiraEvent +/// The instance. +public class EjectionEvent(ExileController controller) : MiraEvent { /// /// Gets the instance. /// - public ExileController ExileController { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The instance. - public EjectionEvent(ExileController controller) - { - ExileController = controller; - } + public ExileController ExileController { get; } = controller; } diff --git a/MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs b/MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs index fb085b6..139aa77 100644 --- a/MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs @@ -1,36 +1,26 @@ namespace MiraAPI.Events.Vanilla.Meeting; /// -/// The event that is invoked when a body is reported. +/// The event that is invoked when a body is reported.
/// This is invoked in . ///
-public class ReportBodyEvent : MiraCancelableEvent +/// The player who reported the body. +/// The player being reported. +/// The being reported. +public class ReportBodyEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body) : MiraCancelableEvent { /// /// Gets the player that is reporting the body. /// - public PlayerControl Reporter { get; } + public PlayerControl Reporter { get; } = reporter; /// /// Gets the player that is being reported. Will be for emergency meeting. /// - public NetworkedPlayerInfo? Target { get; } + public NetworkedPlayerInfo? Target { get; } = target; /// /// Gets the that is being reported. Will be for emergency meeting. /// - public DeadBody? Body { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The player who reported the body. - /// The player being reported. - /// The being reported. - public ReportBodyEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body) - { - Reporter = reporter; - Target = target; - Body = body; - } + public DeadBody? Body { get; } = body; } diff --git a/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs b/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs index 0a15d99..a38b3c6 100644 --- a/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs @@ -4,19 +4,11 @@ /// The event that is invoked when a meeting is called. This event is not cancelable. /// This event is called after Mira resets votes, so if you plan on adding votes to a specific player, do it with this event. ///
-public class StartMeetingEvent : MiraEvent +/// The instance. +public class StartMeetingEvent(MeetingHud meetingHud) : MiraEvent { /// /// Gets the instance. /// - public MeetingHud MeetingHud { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The instance. - public StartMeetingEvent(MeetingHud meetingHud) - { - MeetingHud = meetingHud; - } + public MeetingHud MeetingHud { get; } = meetingHud; } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/AfterVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/AfterVoteEvent.cs index 60a3d36..34df3f9 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/AfterVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/AfterVoteEvent.cs @@ -3,26 +3,17 @@ /// /// Event that is invoked after the local player successfully votes a player or skips. This event is not cancelable. /// -public class AfterVoteEvent : MiraEvent +/// The that was voted on. +/// The player who voted. +public class AfterVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter) : MiraEvent { /// /// Gets the instance of the voter's . /// - public PlayerVoteArea VoteArea { get; } + public PlayerVoteArea VoteArea { get; } = playerVoteArea; /// /// Gets the player who voted. /// - public PlayerControl Player { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The that was voted on. - /// The player who voted. - public AfterVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter) - { - VoteArea = playerVoteArea; - Player = voter; - } + public PlayerControl Player { get; } = voter; } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/BeforeVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/BeforeVoteEvent.cs index e6b28f9..9d21b71 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/BeforeVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/BeforeVoteEvent.cs @@ -3,26 +3,17 @@ /// /// Event that is invoked before the local player confirms a vote on a player or when skipping. This event is cancelable. /// -public class BeforeVoteEvent : MiraCancelableEvent +/// The that was voted on. +/// The player who voted. +public class BeforeVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter) : MiraCancelableEvent { /// /// Gets the instance of the voter's . /// - public PlayerVoteArea VoteArea { get; } + public PlayerVoteArea VoteArea { get; } = playerVoteArea; /// /// Gets the player who voted. /// - public PlayerControl Player { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The that was voted on. - /// The player who voted. - public BeforeVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter) - { - VoteArea = playerVoteArea; - Player = voter; - } + public PlayerControl Player { get; } = voter; } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/CheckForEndVotingEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/CheckForEndVotingEvent.cs index 988cdc4..952b298 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/CheckForEndVotingEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/CheckForEndVotingEvent.cs @@ -3,24 +3,16 @@ /// /// Checks if voting is complete. If canceled, the default end voting logic will be skipped. /// -public class CheckForEndVotingEvent : MiraCancelableEvent +/// if voting is complete, otherwise. +public class CheckForEndVotingEvent(bool isVotingComplete) : MiraCancelableEvent { /// /// Gets a value indicating whether default voting logic determines that voting is complete. /// - public bool IsVotingComplete { get; } + public bool IsVotingComplete { get; } = isVotingComplete; /// /// Gets or sets a value indicating whether the voting should be forced to end, regardless of the default logic. /// public bool ForceEndVoting { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// if voting is complete, otherwise. - public CheckForEndVotingEvent(bool isVotingComplete) - { - IsVotingComplete = isVotingComplete; - } } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs index ea3aa69..1bb0bc8 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs @@ -7,38 +7,27 @@ namespace MiraAPI.Events.Vanilla.Meeting.Voting; /// /// Invoked before the dummy selects their vote. If canceled, the dummy won't vote. /// -public class DummyVoteEvent : MiraCancelableEvent +/// The voting dummy. +public class DummyVoteEvent(DummyBehaviour dummy) : MiraCancelableEvent { /// /// Gets the dummy that needs to vote. /// - public DummyBehaviour Dummy { get; } + public DummyBehaviour Dummy { get; } = dummy; /// /// Gets the instance of the dummy's . /// - public PlayerVoteData VoteData { get; } + public PlayerVoteData VoteData { get; } = dummy.myPlayer.GetVoteData(); /// /// Gets or sets a value indicating whether the dummy can vote to skip the meeting. /// - public bool CanSkip { get; set; } + public bool CanSkip { get; set; } = true; /// /// Gets or sets a predicate for validating whether the dummy can vote on a player. /// By default, excludes dead and disconnected players. /// - public Predicate PlayerIsValid { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The voting dummy. - public DummyVoteEvent(DummyBehaviour dummy) - { - Dummy = dummy; - VoteData = dummy.myPlayer.GetVoteData(); - CanSkip = true; - PlayerIsValid = x => !x.Data.IsDead && !x.Data.Disconnected; - } + public Predicate PlayerIsValid { get; set; } = x => !x.Data.IsDead && !x.Data.Disconnected; } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs index 8f29544..0c67e88 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs @@ -4,68 +4,43 @@ namespace MiraAPI.Events.Vanilla.Meeting.Voting; /// /// The event that is invoked when Mira handles a vote. This is invoked before Mira's behaviour, so be cautious. -/// If you intend on adding custom vote behaviour, cancel the event and do so. -/// If you are NOT canceling the event, keep in mind that Mira automatically removes a vote and adds the suspect to the list after this event is invoked. /// -public class HandleVoteEvent : MiraCancelableEvent +/// +/// If you intend on adding custom vote behaviour, cancel the event and do so.
+/// If you are NOT canceling the event, keep in mind that Mira automatically removes a vote and adds the suspect to the list after this event is invoked. +///
+/// The voter's . +/// The target's playerId. +/// Whether the vote is actually for a Judge. +public class HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId, bool isOverruling = false) : MiraCancelableEvent { /// /// Gets the instance of the voter's . /// - public PlayerVoteData VoteData { get; } + public PlayerVoteData VoteData { get; } = playerVoteData; /// /// Gets the player who voted. /// - public PlayerControl Player { get; } + public PlayerControl Player { get; } = playerVoteData.Owner; /// /// Gets the player id of the target. /// - public byte TargetId { get; } + public byte TargetId { get; } = targetId; /// /// Gets or sets a value indicating whether to prevent the vote from commencing. /// - public bool PreventVote { get; set; } + public bool PreventVote { get; set; } = false; /// /// Gets the of the target. /// - public NetworkedPlayerInfo TargetPlayerInfo { get; } + 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; } - - /// - /// Initializes a new instance of the class. - /// - /// The voter's . - /// The target's playerId. - public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId) - { - VoteData = playerVoteData; - Player = playerVoteData.Owner; - TargetId = targetId; - PreventVote = false; - TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId); - } - - /// - /// Initializes a new instance of the class. - /// - /// The voter's . - /// The target's playerId. - /// Whether the vote is actually for a Judge. - public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId, bool isOverruling) - { - VoteData = playerVoteData; - Player = playerVoteData.Owner; - TargetId = targetId; - PreventVote = false; - IsOverruling = isOverruling; - TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId); - } + public bool IsOverruling { get; set; } = isOverruling; } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs index 0c3f6d5..aff24f7 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs @@ -5,39 +5,28 @@ namespace MiraAPI.Events.Vanilla.Meeting.Voting; /// /// The event that is invoked when the player tries to select another player to vote. /// -public class MeetingSelectEvent : MiraEvent +/// The voter's . +/// The target's playerId. +/// Whether the player is allowed to select the target. +public class MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId, bool allowSelect) : MiraEvent { /// /// Gets the instance of the voter's . /// - public PlayerVoteData VoteData { get; } + public PlayerVoteData VoteData { get; } = playerVoteData; /// /// Gets the player id of the target. /// - public int TargetId { get; } + public int TargetId { get; } = targetId; /// /// Gets the of the target. /// - public NetworkedPlayerInfo TargetPlayerInfo { get; } + public NetworkedPlayerInfo TargetPlayerInfo { get; } = GameData.Instance.GetPlayerById((byte)targetId); /// /// Gets or sets a value indicating whether the player is allowed to select the target. /// - public bool AllowSelect { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The voter's . - /// The target's playerId. - /// Whether the player is allowed to select the target. - public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId, bool allowSelect) - { - VoteData = playerVoteData; - TargetId = targetId; - TargetPlayerInfo = GameData.Instance.GetPlayerById((byte)targetId); - AllowSelect = allowSelect; - } + public bool AllowSelect { get; set; } = allowSelect; } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/PopulateResultsEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/PopulateResultsEvent.cs index f5e4001..3914722 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/PopulateResultsEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/PopulateResultsEvent.cs @@ -5,21 +5,15 @@ namespace MiraAPI.Events.Vanilla.Meeting.Voting; /// /// Called in . -/// Cancelling is NOT advised but if you do plan on cancelling, please ensure you account for displaying ALL player's votes. /// -public class PopulateResultsEvent : MiraCancelableEvent +/// +/// Cancelling is NOT advised but if you do plan on cancelling, please ensure you account for displaying ALL player's votes. +/// +/// The list of s. +public class PopulateResultsEvent(List votes) : MiraCancelableEvent { /// /// Gets a list of networked s. /// - public List Votes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The list of s. - public PopulateResultsEvent(List votes) - { - Votes = votes; - } + public List Votes { get; } = votes; } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs index 8e4894a..043ec56 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using MiraAPI.Voting; @@ -7,16 +8,11 @@ namespace MiraAPI.Events.Vanilla.Meeting.Voting; /// /// Ran after calculating votes and before displaying the results. Only ran on the host. /// -/// -/// Initializes a new instance of the class. -/// /// The list of s that are being processed. /// The player to be exiled. Will be if no player is to be exiled. public class ProcessVotesEvent(List votes, NetworkedPlayerInfo? exiledPlayer = null) : MiraEvent { - private readonly List _originalVotes = [.. votes]; - - private NetworkedPlayerInfo? _exiledPlayer = exiledPlayer; + private readonly List originalVotes = [.. votes]; /// /// Gets a value indicating whether the exiled player has been modified by the event. @@ -26,7 +22,7 @@ public class ProcessVotesEvent(List votes, NetworkedPlayerInfo? exil /// /// Gets a value indicating whether the votes have been modified by the event. /// - public bool VotesModified => !Votes.SequenceEqual(_originalVotes); + public bool VotesModified => !Votes.SequenceEqual(originalVotes); /// /// Gets the list of s that are being processed. @@ -46,13 +42,15 @@ public class ProcessVotesEvent(List votes, NetworkedPlayerInfo? exil /// /// Gets or sets the player to be exiled. Will be if no player is to be exiled. /// + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "Looks weird.")] public NetworkedPlayerInfo? ExiledPlayer { - get => _exiledPlayer; + get; set { - _exiledPlayer = value; + field = value; ExiledPlayerModified = true; } } + = exiledPlayer; } diff --git a/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs b/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs index 5ac7190..7528fc1 100644 --- a/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs +++ b/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs @@ -3,33 +3,23 @@ /// /// The event that is invoked when a player completes a task within Hide and Seek. Non-cancelable. /// -public class CompleteHnsTaskEvent : MiraEvent +/// The currently active. +/// The player who completed the task. +/// The task that the player completed. +public class CompleteHnsTaskEvent(HideAndSeekManager hnsManager, PlayerControl player, PlayerTask task) : MiraEvent { /// /// Gets the instance of the . /// - public PlayerControl Player { get; } + public PlayerControl Player { get; } = player; /// /// Gets the instance of the that the player completed. /// - public PlayerTask Task { get; } + public PlayerTask Task { get; } = task; /// /// Gets the instance of the attached to the game. /// - public HideAndSeekManager HnsManager { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The currently active. - /// The player who completed the task. - /// The task that the player completed. - public CompleteHnsTaskEvent(HideAndSeekManager hnsManager, PlayerControl player, PlayerTask task) - { - HnsManager = hnsManager; - Player = player; - Task = task; - } + public HideAndSeekManager HnsManager { get; } = hnsManager; } diff --git a/MiraAPI/Events/Vanilla/Player/CompleteTaskEvent.cs b/MiraAPI/Events/Vanilla/Player/CompleteTaskEvent.cs index 31fd9d4..95b2430 100644 --- a/MiraAPI/Events/Vanilla/Player/CompleteTaskEvent.cs +++ b/MiraAPI/Events/Vanilla/Player/CompleteTaskEvent.cs @@ -1,28 +1,19 @@ namespace MiraAPI.Events.Vanilla.Player; /// -/// The event that is invoked when a player completes a task. Non cancelable. +/// The event that is invoked when a player completes a task. Non-cancelable. /// -public class CompleteTaskEvent : MiraEvent +/// The player who completed the task. +/// The task that the player completed. +public class CompleteTaskEvent(PlayerControl player, PlayerTask task) : MiraEvent { /// /// Gets the instance of the . /// - public PlayerControl Player { get; } + public PlayerControl Player { get; } = player; /// /// Gets the instance of the that the player completed. /// - public PlayerTask Task { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The player who completed the task. - /// The task that the player completed. - public CompleteTaskEvent(PlayerControl player, PlayerTask task) - { - Player = player; - Task = task; - } + public PlayerTask Task { get; } = task; } diff --git a/MiraAPI/Events/Vanilla/Player/PlayerDeathEvent.cs b/MiraAPI/Events/Vanilla/Player/PlayerDeathEvent.cs index 3821af4..b03d2aa 100644 --- a/MiraAPI/Events/Vanilla/Player/PlayerDeathEvent.cs +++ b/MiraAPI/Events/Vanilla/Player/PlayerDeathEvent.cs @@ -1,35 +1,25 @@ namespace MiraAPI.Events.Vanilla.Player; /// -/// Event that is invoked when a player dies. Non cancelable. +/// Event that is invoked when a player dies. Non-cancelable. /// -public class PlayerDeathEvent : MiraEvent +/// The player who died. +/// The reason the player died. +/// The player's , if it exists. +public class PlayerDeathEvent(PlayerControl player, DeathReason reason, DeadBody? deadBody) : MiraEvent { /// /// Gets the player who died. /// - public PlayerControl Player { get; } + public PlayerControl Player { get; } = player; /// /// Gets the reason the player died. /// - public DeathReason DeathReason { get; } + public DeathReason DeathReason { get; } = reason; /// /// Gets the associated with the player, if any. /// - public DeadBody? DeadBody { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The player who died. - /// The reason the player died. - /// The player's , if it exists. - public PlayerDeathEvent(PlayerControl player, DeathReason reason, DeadBody? deadBody) - { - Player = player; - DeathReason = reason; - DeadBody = deadBody; - } + public DeadBody? DeadBody { get; } = deadBody; } diff --git a/MiraAPI/Events/Vanilla/Player/PlayerJoinEvent.cs b/MiraAPI/Events/Vanilla/Player/PlayerJoinEvent.cs index ba9bfb5..5f66472 100644 --- a/MiraAPI/Events/Vanilla/Player/PlayerJoinEvent.cs +++ b/MiraAPI/Events/Vanilla/Player/PlayerJoinEvent.cs @@ -3,21 +3,13 @@ namespace MiraAPI.Events.Vanilla.Player; /// -/// Event that is invoked when a player joins the lobby. Non cancelable. +/// Event that is invoked when a player joins the lobby. Non-cancelable. /// -public class PlayerJoinEvent : MiraEvent +/// The data of the player who joined. +public class PlayerJoinEvent(ClientData data) : MiraEvent { /// /// Gets the player who joined. /// - public ClientData ClientData { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The data of the player who joined. - public PlayerJoinEvent(ClientData data) - { - ClientData = data; - } + public ClientData ClientData { get; } = data; } diff --git a/MiraAPI/Events/Vanilla/Player/PlayerLeaveEvent.cs b/MiraAPI/Events/Vanilla/Player/PlayerLeaveEvent.cs index 4607305..1af8bde 100644 --- a/MiraAPI/Events/Vanilla/Player/PlayerLeaveEvent.cs +++ b/MiraAPI/Events/Vanilla/Player/PlayerLeaveEvent.cs @@ -3,28 +3,19 @@ namespace MiraAPI.Events.Vanilla.Player; /// -/// Event that is invoked when a player leaves the game. Non cancelable. +/// Event that is invoked when a player leaves the game. Non-cancelable. /// -public class PlayerLeaveEvent : MiraEvent +/// The data of the player who left. +/// The reason why the player left. +public class PlayerLeaveEvent(ClientData data, DisconnectReasons reason) : MiraEvent { /// /// Gets the player who left. /// - public ClientData ClientData { get; } + public ClientData ClientData { get; } = data; /// /// Gets the reason why the player left. /// - public DisconnectReasons Reason { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The data of the player who left. - /// The reason why the player left. - public PlayerLeaveEvent(ClientData data, DisconnectReasons reason) - { - ClientData = data; - Reason = reason; - } + public DisconnectReasons Reason { get; } = reason; } diff --git a/MiraAPI/Events/Vanilla/Usables/EnterVentEvent.cs b/MiraAPI/Events/Vanilla/Usables/EnterVentEvent.cs index 233e6ee..4d6eaaa 100644 --- a/MiraAPI/Events/Vanilla/Usables/EnterVentEvent.cs +++ b/MiraAPI/Events/Vanilla/Usables/EnterVentEvent.cs @@ -3,26 +3,17 @@ /// /// Event that is invoked when a player enters a vent. This event is cancelable. /// -public class EnterVentEvent : MiraCancelableEvent +/// The that is entering the . +/// The being entered. +public class EnterVentEvent(PlayerControl player, Vent? vent) : MiraCancelableEvent { /// /// Gets the that is entering the . /// - public PlayerControl Player { get; } + public PlayerControl Player { get; } = player; /// /// Gets the that the is entering. /// - public Vent? Vent { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The that is entering the . - /// The being entered. - public EnterVentEvent(PlayerControl player, Vent? vent) - { - Player = player; - Vent = vent; - } + public Vent? Vent { get; } = vent; } diff --git a/MiraAPI/Events/Vanilla/Usables/ExitVentEvent.cs b/MiraAPI/Events/Vanilla/Usables/ExitVentEvent.cs index 6194905..3c0782e 100644 --- a/MiraAPI/Events/Vanilla/Usables/ExitVentEvent.cs +++ b/MiraAPI/Events/Vanilla/Usables/ExitVentEvent.cs @@ -3,26 +3,17 @@ /// /// Event that is invoked when a player exits a vent. This event is cancelable. /// -public class ExitVentEvent : MiraCancelableEvent +/// The who is exiting the . +/// The being exited from. +public class ExitVentEvent(PlayerControl player, Vent? vent) : MiraCancelableEvent { /// /// Gets the that is exiting the . /// - public PlayerControl Player { get; } + public PlayerControl Player { get; } = player; /// /// Gets the that the is exiting. /// - public Vent? Vent { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The who is exiting the . - /// The being exited from. - public ExitVentEvent(PlayerControl player, Vent? vent) - { - Player = player; - Vent = vent; - } + public Vent? Vent { get; } = vent; } diff --git a/MiraAPI/Events/Vanilla/Usables/PlayerCanUseEvent.cs b/MiraAPI/Events/Vanilla/Usables/PlayerCanUseEvent.cs index 9dced79..4f547c6 100644 --- a/MiraAPI/Events/Vanilla/Usables/PlayerCanUseEvent.cs +++ b/MiraAPI/Events/Vanilla/Usables/PlayerCanUseEvent.cs @@ -1,15 +1,7 @@ namespace MiraAPI.Events.Vanilla.Usables; /// -/// Event for if a can use an from Vanilla Among Us. Will always be ran locally. +/// Event for if a can use an from Vanilla Among Us. Will always be run locally. /// -public class PlayerCanUseEvent : PlayerUseEvent -{ - /// - /// Initializes a new instance of the class. - /// - /// The . - public PlayerCanUseEvent(IUsable usable) : base(usable) - { - } -} +/// The . +public class PlayerCanUseEvent(IUsable usable) : PlayerUseEvent(usable); diff --git a/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs b/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs index 7fc5cd5..a23ce0e 100644 --- a/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs +++ b/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs @@ -1,34 +1,23 @@ namespace MiraAPI.Events.Vanilla.Usables; /// -/// Event for using a from Vanilla Among Us. Will always be ran locally. +/// Event for using a from Vanilla Among Us. Will always be run locally. /// -public class PlayerUseEvent : MiraCancelableEvent +/// The . +public class PlayerUseEvent(IUsable usable) : MiraCancelableEvent { /// /// Gets the instance of that was used. /// - public IUsable Usable { get; } + public IUsable Usable { get; } = usable; /// /// Gets a value indicating whether the is a , , or . /// - public bool IsPrimaryConsole { get; } + public bool IsPrimaryConsole { get; } = usable.TryCast() || usable.TryCast() || usable.TryCast(); /// /// Gets a value indicating whether the is a . /// - public bool IsVent { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The . - public PlayerUseEvent(IUsable usable) - { - Usable = usable; - - IsPrimaryConsole = usable.TryCast() || usable.TryCast() || usable.TryCast(); - IsVent = usable.TryCast(); - } + public bool IsVent { get; } = usable.TryCast(); } diff --git a/MiraAPI/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs index bb9e23b..88bb903 100644 --- a/MiraAPI/GameEnd/CustomGameOver.cs +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -48,7 +48,8 @@ public abstract class CustomGameOver /// /// Type of the custom game over. /// The associated with the custom game over. - public static GameOverReason GameOverReason() where T : CustomGameOver + public static GameOverReason GameOverReason() + where T : CustomGameOver { return (GameOverReason)GameOverManager.GetGameOverId(); } @@ -58,7 +59,8 @@ public abstract class CustomGameOver /// /// A collection of winners. /// Type of the custom game over. - public static void Trigger(IEnumerable winners) where T : CustomGameOver + public static void Trigger(IEnumerable winners) + where T : CustomGameOver { var reason = GameOverManager.GetGameOverId(); var data = new GameOverData(reason, [.. winners]); diff --git a/MiraAPI/GameEnd/GameOverManager.cs b/MiraAPI/GameEnd/GameOverManager.cs index 28d2f48..bde0aa8 100644 --- a/MiraAPI/GameEnd/GameOverManager.cs +++ b/MiraAPI/GameEnd/GameOverManager.cs @@ -12,7 +12,7 @@ public static class GameOverManager private static readonly Dictionary GameOverIds = []; private static readonly Dictionary GameOverTypes = []; - private static int _nextId = Enum.GetNames().Length; + private static int nextId = Enum.GetNames().Length; /// /// Register a . @@ -39,9 +39,9 @@ public static class GameOverManager return false; } - GameOverIds.Add(gameOverType, _nextId); - GameOverTypes.Add(_nextId, gameOverType); - _nextId++; + GameOverIds.Add(gameOverType, nextId); + GameOverTypes.Add(nextId, gameOverType); + nextId++; return true; } @@ -68,7 +68,8 @@ public static class GameOverManager /// /// Type of the . /// The ID of the . - public static int GetGameOverId() where T : CustomGameOver + public static int GetGameOverId() + where T : CustomGameOver { return GetGameOverId(typeof(T)); } @@ -81,9 +82,8 @@ public static class GameOverManager /// Thrown when the type is not registered. public static int GetGameOverId(Type gameOverType) { - if (GameOverIds.TryGetValue(gameOverType, out var id)) - return id; - - throw new ArgumentException($"{gameOverType.FullName} is not a registered custom game over!"); + return GameOverIds.TryGetValue(gameOverType, out var id) + ? id + : throw new ArgumentException($"{gameOverType.FullName} is not a registered custom game over!"); } } diff --git a/MiraAPI/GameModes/.editorconfig b/MiraAPI/GameModes/.editorconfig deleted file mode 100644 index 948715b..0000000 --- a/MiraAPI/GameModes/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*.cs] -dotnet_diagnostic.CS1591.severity = none \ No newline at end of file diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 10dd377..8344839 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -84,15 +84,11 @@ public abstract class AbstractGameMode : IOptionable /// Resulting body type. public virtual PlayerBodyTypes GetBodyType(PlayerControl player) { - if (AprilFoolsMode.ShouldHorseAround()) - { - return PlayerBodyTypes.Horse; - } - if (AprilFoolsMode.ShouldLongAround()) - { - return PlayerBodyTypes.Long; - } - return PlayerBodyTypes.Normal; + return AprilFoolsMode.ShouldHorseAround() + ? PlayerBodyTypes.Horse + : (AprilFoolsMode.ShouldLongAround() + ? PlayerBodyTypes.Long + : PlayerBodyTypes.Normal); } /// @@ -177,14 +173,17 @@ public abstract class AbstractGameMode : IOptionable /// Custom winner selection. /// /// List of winners or null. - public virtual List? CalculateWinners() => null; + public virtual List? CalculateWinners() + { + return null; + } /// /// The IEnumerator that plays the intro cutscene for this gamemode. /// - /// An instance of IntroCutscene. + /// An instance of IntroCutscene. /// An IEnumerator to run the intro cutscene instead of the base game one. - public virtual IEnumerator IntroCutscene(IntroCutscene __instance) + public virtual IEnumerator IntroCutscene(IntroCutscene introCutscene) { yield return new WaitForEndOfFrame(); } @@ -194,35 +193,50 @@ public abstract class AbstractGameMode : IOptionable /// /// Admin Console. /// True if Admin console is enabled. - public virtual bool CanUseMapConsole(MapConsole console) => true; + 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) => true; + 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) => true; + 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) => true; + 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) => true; + public virtual bool ShouldShowSabotageMap(MapBehaviour map) + { + return true; + } /// /// Gets the to display on the map. @@ -244,28 +258,36 @@ public abstract class AbstractGameMode : IOptionable /// Target vent. /// Player attempting to vent. /// True if venting is enabled in this mode. - public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true; + public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) + { + return true; + } /// - /// Does the task bar appear in the gamemode. + /// Gets a value indicating whether the task bar appears in the gamemode. /// /// True if the task bar is enabled in this mode. public virtual bool ShowTaskBar => true; + /// + /// Updates the task panel. + /// + /// The task panel to update. public virtual void UpdateTaskPanel(TaskPanelBehaviour instance) { - instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f) + 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; + var 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; + var 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; @@ -275,14 +297,9 @@ public abstract class AbstractGameMode : IOptionable 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); - } + instance.timer = instance.open + ? Mathf.Min(1f, instance.timer + (Time.deltaTime / instance.animationTimeSeconds)) + : Mathf.Max(0f, instance.timer - (Time.deltaTime / instance.animationTimeSeconds)); Vector3 relativePos = new( Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), @@ -293,5 +310,8 @@ public abstract class AbstractGameMode : IOptionable } /// - public override string ToString() => Name; + public override string ToString() + { + return Name; + } } diff --git a/MiraAPI/GameModes/ClassicMode.cs b/MiraAPI/GameModes/ClassicMode.cs index 3da9a23..75499fa 100644 --- a/MiraAPI/GameModes/ClassicMode.cs +++ b/MiraAPI/GameModes/ClassicMode.cs @@ -16,5 +16,6 @@ public class ClassicMode : AbstractGameMode /// public override string Description => "MiraApi.Gamemode.Classic.Description"; - public override LoadableAsset? Icon => MiraAssets.ClassicGamemodeIcon; + /// + public override LoadableAsset Icon => MiraAssets.ClassicGamemodeIcon; } diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 254bf6d..f43fc0d 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MiraAPI.Translation; using MiraAPI.PluginLoading; -using Reactor.Utilities; namespace MiraAPI.GameModes; @@ -14,7 +12,10 @@ public static class CustomGameModeManager { internal static readonly Dictionary IdToModeMap = []; - private static uint GetNextId() => ++LastId; + private static uint GetNextId() + { + return ++LastId; + } private static uint LastId { get; set; } @@ -40,13 +41,13 @@ public static class CustomGameModeManager return false; } - if (!mode.HideMode) - { - IdToModeMap.Add(GetNextId(), mode); - pluginInfo.GameModes.Add(LastId, mode); - mode.ID = LastId; - GameModeOption.AddOption(mode); - } + if (mode.HideMode) + return true; + + IdToModeMap.Add(GetNextId(), mode); + pluginInfo.GameModes.Add(LastId, mode); + mode.ID = LastId; + GameModeOption.AddOption(mode); return true; } @@ -54,20 +55,30 @@ public static class CustomGameModeManager /// Checks to see if the current game mode is an instance of the classic mode. /// /// True if the classic mode is the current one. - public static bool IsClassic() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(ClassicMode)); + 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() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(HideAndSeekMode)); + 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 => ActiveMode is T; + public static bool IsActiveGameMode() + where T : AbstractGameMode + { + return ActiveMode is T; + } /// /// Gets the current gamemode. @@ -79,7 +90,10 @@ public static class CustomGameModeManager /// /// The ID of the gamemode to fetch. /// The gamemode matching that ID. - public static AbstractGameMode GetMode(uint id) => IdToModeMap[id]; + public static AbstractGameMode GetMode(uint id) + { + return IdToModeMap[id]; + } /// /// Finds the parent mod of a custom gamemode. @@ -95,17 +109,19 @@ public static class CustomGameModeManager { var defaultMode = new ClassicMode(); IdToModeMap.Add(0, defaultMode); + // no need to add to game mode option as it already contains it // because we cannot have the option be created with no values defaultMode.ID = 0; var hnsMode = new HideAndSeekMode(); - if (!hnsMode.HideMode) - { - IdToModeMap.Add(1, hnsMode); - hnsMode.ID = 1; - GameModeOption.AddOption(hnsMode); - LastId++; - } + + if (hnsMode.HideMode) + return; + + IdToModeMap.Add(1, hnsMode); + hnsMode.ID = 1; + GameModeOption.AddOption(hnsMode); + LastId++; } internal static void GetAndSetGameMode() @@ -119,6 +135,6 @@ public static class CustomGameModeManager } ActiveMode = IdToModeMap[0]; - Logger.Warning($"Unable to find game mode of id {id}!"); + Warning($"Unable to find game mode of id {id}!"); } } diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 3e0d2f7..56b63ac 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -1,22 +1,15 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; -using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.GameOptions; using MiraAPI.Networking; +using MiraAPI.Patches; using MiraAPI.Patches.GameModes; using MiraAPI.Patches.Options; using MiraAPI.Translation; -using MiraAPI.Utilities; -using MiraAPI.Utilities.Assets; -using Reactor.Localization.Utilities; using Reactor.Networking.Attributes; -using Reactor.Utilities.Extensions; -using TMPro; using UnityEngine; -using UnityEngine.Events; -using UnityEngine.ProBuilder; -using Object = UnityEngine.Object; namespace MiraAPI.GameModes; @@ -27,7 +20,7 @@ namespace MiraAPI.GameModes; public static class GameModeOption { /// - /// Gets the current index of the Game Mode Option + /// Gets the current index of the Game Mode Option.
/// For the value as an , see . ///
public static int Value @@ -35,32 +28,37 @@ public static class GameModeOption get => OptionBehaviour != null ? OptionBehaviour.GetInt() - : _lastValue; + : LastValue; private set { - _lastValue = value; + LastValue = value; if (OptionBehaviour == null) return; OptionBehaviour.Value = value; OptionBehaviour.UpdateValue(); - _lastValue = value; + 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 GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("MiraApi.Gamemode"); internal static readonly StringNames CustomName = MiraLocaleManager.GetOrCreateLocaleString("Custom"); internal static readonly Dictionary Values = new() { [0] = MiraLocaleManager.GetOrCreateLocaleString("MiraApi.Gamemode.Classic"), }; + internal static StringOption OptionBehaviour { get; set; } = null!; + + [SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "Internal behaviour that does not need property-level validation.")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] // why so many warnings??? + internal static int LastValue; + 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) @@ -112,8 +110,9 @@ public static class GameModeOption Value = val; var previousMode = CustomGameModeManager.ActiveMode; CustomGameModeManager.GetAndSetGameMode(); - HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); + HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(LastValue).Key).ColoredName); var gm = CustomGameModeManager.ActiveMode!; + if (gm != previousMode) { ModdedOptionsManager.AddSettingsChangeMessage( @@ -121,8 +120,7 @@ public static class GameModeOption GamemodeName, gm.ColoredName, new Color(0.7333f, 0.7333f, 0.7333f, 1), - gm.TmpIcon, - true); + gm.TmpIcon); if (MenuState.Instance) { // should force reset roles for gamemodes properly @@ -131,29 +129,29 @@ public static class GameModeOption MenuState.Instance.QueuedRoleMenuRefresh[roleMenu.Key] = true; } } + + GameOptionsMenuPatch.RefreshForGameMode(); + RoleGuidePatches.RefreshSettingsForGameMode(); } - // could make Values a dict of AbstractGameMode too + + // TODO: could make Values a dict of AbstractGameMode too } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] [HarmonyPrefix] - private static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option) + private static bool ValueChanged(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; + if (!OptionBehaviour.Equals(option)) + return true; + + Info($"Game mode changed to {option.GetInt()}"); + RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt()); + + return false; } - [MethodRpc((uint) MiraRpc.SyncGamemodeOption)] + [MethodRpc((uint)MiraRpc.SyncGamemodeOption)] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Intentionally unused because the main intent is for it to be used by dependent mods to add to a blacklist.")] internal static void RpcSyncGamemode(PlayerControl host, int data) { Set(data); diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index cd99eae..ec99ce9 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; using AmongUs.Data; using AmongUs.GameOptions; @@ -30,276 +31,287 @@ public class HideAndSeekMode : AbstractGameMode /// public override string Description => "MiraApi.Gamemode.HideAndSeek.Description"; + /// // TODO: Once the Hide n Seek port is ready for release, we will remove this. public override bool HideMode => !MiraApiPlugin.IsDevBuild; + /// public override Color Color { get; } = new Color32(255, 88, 90, 255); - public override LoadableAsset? Icon => MiraAssets.HnSGamemodeIcon; - public override bool CanReport(DeadBody body) => false; - public override bool ShouldShowSabotageMap(MapBehaviour map) => false; + + /// + public override LoadableAsset Icon => MiraAssets.HnSGamemodeIcon; + + /// 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() + + /// + public override bool CanReport(DeadBody body) + { + return false; + } + + /// + public override bool ShouldShowSabotageMap(MapBehaviour map) + { + return false; + } + + private static int ImpostorPlayerID() { return OptionGroupSingleton.Instance.SelectedSeeker.Value; } - public static bool HasImpostorPlayerID() + private static bool HasImpostorPlayerID() { return ImpostorPlayerID() > -1; } - public static bool ValidateImpostorPlayerID(List players) + private static bool ValidateImpostorPlayerID(List players) { - return HasImpostorPlayerID() && players.Find((NetworkedPlayerInfo p) => (int)p.PlayerId == ImpostorPlayerID()) != null; + return HasImpostorPlayerID() && players.Find(p => p.PlayerId == ImpostorPlayerID()) != null; } + /// public override void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) { runOriginal = false; - Il2CppSystem.Collections.Generic.List list = new(); + CppCollections.List list = new(); AmongUsClient.Instance.GetAllClients(list); - List list2 = list.ToArray() + 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(); + !c.Character.Data.IsDead).OrderBy(c => c.Id).Select(c => c.Character.Data)]; - foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) + foreach (var 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); + + var currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; + var 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( + private static void AddGuaranteedRoles(IEnumerable guaranteedRoles, IGameOptions opts, List list) + { + var roleOptions = opts.RoleOptions; + foreach (var roleAssignmentData in guaranteedRoles.Select(x => + new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) + { + while (true) + { + var count = roleAssignmentData.Count; + roleAssignmentData.Count = count - 1; + if (count <= 0) break; + + list.Add(roleAssignmentData.Role.Role); + } + } + } + + private static void AddPotentialRoles(IEnumerable source, IGameOptions opts, List list) + { + var roleOptions = opts.RoleOptions; + var potentialRoles = source.Where(x => !x.IsDead).Select(role => + new RoleManager.RoleAssignmentData( + role, + roleOptions.GetNumPerGame(role.Role), + roleOptions.GetChancePerGame(role.Role))).ToList(); + + list.Clear(); + foreach (var roleData in potentialRoles) + { + for (var i = 0; i < roleData.Count; i++) + { + if (HashRandom.Next(101) < roleData.Chance) + { + list.Add(roleData.Role.Role); + } + } + } + } + + private static void AddFallbackRoles(List list, int targetPlayerCount, int teamMax, int rolesAssigned, RoleTypes basicRole) + { + while (list.Count < targetPlayerCount && list.Count + rolesAssigned < teamMax) + { + list.Add(basicRole); + } + } + + private 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; + Info($"Hide And Seek Mode - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); + var num = 0; + var roleOptions = opts.RoleOptions; var source = RoleManager.Instance.AllRoles.ToArray() .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) && - CustomRoleUtils.CanSpawnOnCurrentMode(role)); + CustomRoleUtils.CanSpawnOnCurrentMode(role)) + .ToList(); + 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) + var guaranteedRoles = source.Where(x => source2.Contains(((ushort)x.Role, 100))).ToList(); + List list = []; + + switch (team) { - 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) + case RoleTeamTypes.Crewmate: + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + AddGuaranteedRoles(guaranteedRoles, opts, list); + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); + AssignRolesFromList(players, teamMax, list, ref num); + + AddPotentialRoles(source, opts, list); + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Potential Assignment"); + AssignRolesFromList(players, teamMax, list, ref num); + + const RoleTypes basicCrewRole = RoleTypes.Engineer; + AddFallbackRoles(list, players.Count, teamMax, num, basicCrewRole); + AssignRolesFromList(players, teamMax, list, ref num); + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Fallback Assignment"); + break; + + case RoleTeamTypes.Impostor: + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + var newImpostors = new List(); + + if (HasImpostorPlayerID() && + ValidateImpostorPlayerID(players) && + !AmongUsClient.Instance.IsGamePublic) { - 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(); + // Specified Seeker + var networkedPlayerInfo = players.First(p => p.PlayerId == ImpostorPlayerID()); players.Remove(networkedPlayerInfo); newImpostors.Add(networkedPlayerInfo); - num2++; - Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); + Info($"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) + else { - RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; - int count = roleAssignmentData2.Count; - roleAssignmentData2.Count = count - 1; - if (count <= 0) + // Random Seeker + var num2 = 0; + while (num2 < teamMax && players.Count > 0) { - break; - } + PseudoRandomList pseudoRandomList = new(AmongUsClient.Instance.GameId); + players.Do(pseudoRandomList.Add); + for (var i = 0; i < GameData.RoundsPlayedInSession; i++) + { + pseudoRandomList.PickRandom(); + } - 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); + var networkedPlayerInfo = pseudoRandomList.PickRandom(); + players.Remove(networkedPlayerInfo); + newImpostors.Add(networkedPlayerInfo); + num2++; + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); } } - } - AssignRolesFromList(newImpostors, teamMax, list, ref num); - var basicRole = RoleTypes.Impostor; - while (list.Count < newImpostors.Count && list.Count + num < teamMax) - { - list.Add(basicRole); - } + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); - AssignRolesFromList(newImpostors, teamMax, list, ref num); + AddGuaranteedRoles(guaranteedRoles, opts, list); + AssignRolesFromList(newImpostors, teamMax, list, ref num); + + AddPotentialRoles(source, opts, list); + AssignRolesFromList(newImpostors, teamMax, list, ref num); + + const RoleTypes basicImpRole = RoleTypes.Impostor; + AddFallbackRoles(list, newImpostors.Count, teamMax, num, basicImpRole); + AssignRolesFromList(newImpostors, teamMax, list, ref num); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(team), team, null); } } - public static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned) + private 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]; + var index = HashRandom.FastNext(roleList.Count); + var roleType = roleList[index]; roleList.RemoveAt(index); - int index2 = HashRandom.FastNext(players.Count); - players[index2].Object.RpcSetRole(roleType, false); + var index2 = HashRandom.FastNext(players.Count); + players[index2].Object.RpcSetRole(roleType); players.RemoveAt(index2); rolesAssigned++; } } - public override IEnumerator IntroCutscene(IntroCutscene __instance) + /// + public override IEnumerator IntroCutscene(IntroCutscene introCutscene) { - 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); + SoundManager.Instance.PlaySound(introCutscene.IntroStinger, false); + Logger.GlobalInstance.Info("IntroCutscene :: CoBegin() :: Game Mode: Hide and Seek (MiraAPI)"); + introCutscene.LogPlayerRoleData(); + introCutscene.HideAndSeekPanels.SetActive(true); if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) { - __instance.CrewmateRules.SetActive(false); - __instance.ImpostorRules.SetActive(true); + introCutscene.CrewmateRules.SetActive(false); + introCutscene.ImpostorRules.SetActive(true); } else { - __instance.CrewmateRules.SetActive(true); - __instance.ImpostorRules.SetActive(false); + introCutscene.CrewmateRules.SetActive(true); + introCutscene.ImpostorRules.SetActive(false); } - __instance.ImpostorName.gameObject.SetActive(true); - __instance.ImpostorTitle.gameObject.SetActive(true); - __instance.BackgroundBar.enabled = false; - __instance.TeamTitle.gameObject.SetActive(false); + introCutscene.ImpostorName.gameObject.SetActive(true); + introCutscene.ImpostorTitle.gameObject.SetActive(true); + introCutscene.BackgroundBar.enabled = false; + introCutscene.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); + Logger.GlobalInstance.Error("IntroCutscene :: CoBegin() :: impostor is NULL"); } GameManager.Instance.SetSpecialCosmetics(impostor); - if (impostor != null) - { - __instance.ImpostorName.text = impostor.Data.PlayerName; - } - else - { - __instance.ImpostorName.text = "???"; - } + introCutscene.ImpostorName.text = impostor != null ? impostor.Data.PlayerName : "???"; yield return new WaitForSecondsRealtime(0.1f); if (impostor != null) { - __instance.ImpostorTitle.text = impostor.Data.Role.GetRoleName(); + introCutscene.ImpostorTitle.text = impostor.Data.Role.GetRoleName(); } - PoolablePlayer playerSlot = null; + + PoolablePlayer? playerSlot = null; if (impostor != null) { - playerSlot = __instance.CreatePlayer(1, 1, impostor.Data, false); + playerSlot = introCutscene.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; + playerSlot.transform.localPosition = introCutscene.impostorPos; + playerSlot.transform.localScale = Vector3.one * introCutscene.impostorScale; } yield return ShipStatus.Instance.CosmeticsCache.PopulateFromPlayers(); @@ -309,45 +321,43 @@ public class HideAndSeekMode : AbstractGameMode playerSlot.gameObject.SetActive(false); } - __instance.HideAndSeekPanels.SetActive(false); - __instance.CrewmateRules.SetActive(false); - __instance.ImpostorRules.SetActive(false); + introCutscene.HideAndSeekPanels.SetActive(false); + introCutscene.CrewmateRules.SetActive(false); + introCutscene.ImpostorRules.SetActive(false); HnsMusicHandler.Instance.StartMusicWithIntro(); var hideTimer = 10f; if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) { - __instance.HideAndSeekTimerText.gameObject.SetActive(true); + introCutscene.HideAndSeekTimerText.gameObject.SetActive(true); PoolablePlayer poolablePlayer; AnimationClip anim; if (AprilFoolsMode.ShouldHorseAround()) { - poolablePlayer = __instance.HorseWrangleVisualSuit; + poolablePlayer = introCutscene.HorseWrangleVisualSuit; poolablePlayer.gameObject.SetActive(true); poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); - anim = __instance.HnSSeekerSpawnHorseAnim; - __instance.HorseWrangleVisualPlayer.SetBodyType(PlayerBodyTypes.Normal); - __instance.HorseWrangleVisualPlayer.UpdateFromPlayerData( + anim = introCutscene.HnSSeekerSpawnHorseAnim; + introCutscene.HorseWrangleVisualPlayer.SetBodyType(PlayerBodyTypes.Normal); + introCutscene.HorseWrangleVisualPlayer.UpdateFromPlayerData( PlayerControl.LocalPlayer.Data, PlayerControl.LocalPlayer.CurrentOutfitType, PlayerMaterial.MaskType.None, - false, - null, false); } else if (AprilFoolsMode.ShouldLongAround()) { - poolablePlayer = __instance.HideAndSeekPlayerVisual; + poolablePlayer = introCutscene.HideAndSeekPlayerVisual; poolablePlayer.gameObject.SetActive(true); poolablePlayer.SetBodyType(PlayerBodyTypes.LongSeeker); - anim = __instance.HnSSeekerSpawnLongAnim; + anim = introCutscene.HnSSeekerSpawnLongAnim; } else { - poolablePlayer = __instance.HideAndSeekPlayerVisual; + poolablePlayer = introCutscene.HideAndSeekPlayerVisual; poolablePlayer.gameObject.SetActive(true); poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); - anim = __instance.HnSSeekerSpawnAnim; + anim = introCutscene.HnSSeekerSpawnAnim; } poolablePlayer.SetBodyCosmeticsVisible(false); @@ -355,16 +365,14 @@ public class HideAndSeekMode : AbstractGameMode PlayerControl.LocalPlayer.Data, PlayerControl.LocalPlayer.CurrentOutfitType, PlayerMaterial.MaskType.None, - false, - null, false); - SpriteAnim component = poolablePlayer.GetComponent(); + var component = poolablePlayer.GetComponent(); poolablePlayer.gameObject.SetActive(true); poolablePlayer.ToggleName(false); - component.Play(anim, 1f); + component.Play(anim); while (hideTimer > 0f) { - __instance.HideAndSeekTimerText.text = Mathf.RoundToInt(hideTimer).ToString(); + introCutscene.HideAndSeekTimerText.text = Mathf.RoundToInt(hideTimer).ToString(CultureInfo.InvariantCulture); hideTimer -= Time.deltaTime; yield return null; } @@ -376,42 +384,45 @@ public class HideAndSeekMode : AbstractGameMode { if (impostor != null) { - impostor.AnimateCustom(__instance.HnSSeekerSpawnHorseInGameAnim); + impostor.AnimateCustom(introCutscene.HnSSeekerSpawnHorseInGameAnim); } } else if (AprilFoolsMode.ShouldLongAround()) { if (impostor != null) { - impostor.AnimateCustom(__instance.HnSSeekerSpawnLongInGameAnim); + impostor.AnimateCustom(introCutscene.HnSSeekerSpawnLongInGameAnim); } } else if (impostor != null) { - impostor.AnimateCustom(__instance.HnSSeekerSpawnAnim); + impostor.AnimateCustom(introCutscene.HnSSeekerSpawnAnim); impostor.cosmetics.SetBodyCosmeticsVisible(false); } } + ShipStatus.Instance.StartSFX(); HnsMusicHandler.Instance.OnGameStart(); HnsDangerMeter.Instance.OnGameStart(); - UnityEngine.Object.Destroy(__instance.gameObject); + UnityEngine.Object.Destroy(introCutscene.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(); - } + if (!HudManager.InstanceExists) return; + HudManager.Instance.gameObject.AddComponent(); + HudManager.Instance.gameObject.AddComponent(); + HudManager.Instance.gameObject.AddComponent(); } + + /// public override MapOptions GetMapOptions() { - MapOptions mapOptions = new MapOptions + MapOptions mapOptions = new() { Mode = MapOptions.Modes.Normal, }; @@ -422,27 +433,34 @@ public class HideAndSeekMode : AbstractGameMode 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) + var players = Helpers.GetAlivePlayers(); + if (!players.Any(x => x.Data.Role.IsImpostor)) { instance.Manager.RpcEndGame(GameOverReason.ImpostorDisconnect, !DataManager.Player.Ads.HasPurchasedAdRemoval); } - if (Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor) != 0) + + if (players.Any(x => !x.Data.Role.IsImpostor)) { 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) @@ -451,47 +469,39 @@ public class HideAndSeekMode : AbstractGameMode { return PlayerBodyTypes.Horse; } + if (AprilFoolsMode.ShouldLongAround()) { return PlayerBodyTypes.Long; } + return PlayerBodyTypes.Normal; } - else if (AprilFoolsMode.ShouldHorseAround()) + + if (AprilFoolsMode.ShouldHorseAround()) { - if (player.Data.Role.IsImpostor) - { - return PlayerBodyTypes.Normal; - } - return PlayerBodyTypes.Horse; + return player.Data.Role.IsImpostor ? PlayerBodyTypes.Normal : PlayerBodyTypes.Horse; } - else if (AprilFoolsMode.ShouldLongAround()) + + 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; + return player.Data.Role.IsImpostor ? PlayerBodyTypes.LongSeeker : PlayerBodyTypes.Long; } + + return player.Data.Role.IsImpostor ? PlayerBodyTypes.Seeker : 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; + var 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; + var vector2 = instance.tab.sprite.bounds.extents; vector2 = vector2.Mul(instance.tab.transform.localScale); vector2.y = -vector2.y; vector2.x += vector.x * 2f; @@ -505,14 +515,9 @@ public class HideAndSeekMode : AbstractGameMode 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); - } + instance.timer = instance.open + ? Mathf.Min(1f, instance.timer + (Time.deltaTime / instance.animationTimeSeconds)) + : Mathf.Max(0f, instance.timer - (Time.deltaTime / instance.animationTimeSeconds)); Vector3 relativePos = new( Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), @@ -521,7 +526,10 @@ public class HideAndSeekMode : AbstractGameMode instance.transform.localPosition = AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); } + private int deadPlayerCount; + + /// public override void OnPlayerDeath(PlayerControl player, bool assignGhostRole) { base.OnPlayerDeath(player, assignGhostRole); diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 01c9e09..589f705 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.Modifiers; using MiraAPI.Roles; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; using UnityEngine; namespace MiraAPI.GameOptions; @@ -25,6 +29,12 @@ public abstract class AbstractOptionGroup ///
public abstract string GroupName { get; } + /// + /// Gets the sprite of the group that is displayed in the match info guide. + /// + [HideFromIl2Cpp] + public virtual LoadableAsset Sprite => MiraAssets.IconSettings; + /// /// Gets the Optionable type of the group. /// @@ -33,10 +43,9 @@ public abstract class AbstractOptionGroup /// /// Gets a value indicating whether the group should be shown in the modifiers menu. /// - // TODO: make this not a boolean -#pragma warning disable S1133 - [Obsolete("Use ParentMenu instead.")] -#pragma warning restore S1133 + // Completed: make this not a boolean + [Obsolete("Use ParentMenu instead.", true)] + [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Retained.")] public virtual bool ShowInModifiersMenu => false; /// @@ -47,7 +56,43 @@ public abstract class AbstractOptionGroup /// /// Gets the function that determines whether the group should be visible or not. /// - public virtual Func GroupVisible => () => true; + public virtual Func GroupVisible => DelegateUtilities.AlwaysTrue; + + /// + /// Gets a value indicating whether this group's options are shown in the Match Info Guide. + /// + public virtual bool ShowInMatchInfoGuide => ParentMenu is MenuCategory.Game; + + /// + /// Gets the name title of the group when it is in the match info guide. + /// + public virtual string RoleGuideTitle => GroupName; + + /// + /// Gets the key used to combine this group with other option groups on one Match Info Guide page. + /// Groups are combined only with groups from the same plugin that use the same non-empty key. + /// + public virtual string? MatchInfoGuidePageKey => null; + + /// + /// Gets the title of the Match Info Guide page. For groups sharing a page key, the first + /// group in menu order supplies the page title. + /// + public virtual string MatchInfoGuidePageTitle => RoleGuideTitle; + + /// + /// Gets the ordering priority of this Match Info Guide page. Lower values appear first + /// among pages with the same menu category and group priority. Groups sharing a page key + /// use the lowest page-order value among those groups. + /// + public virtual int MatchInfoGuidePageOrder => int.MaxValue; + + /// + /// Gets the number of columns used to lay out options on this Match Info Guide page. + /// For a combined page, groups are placed into these columns. Values are limited to 1-6; + /// for groups sharing a page key, the first group in menu order supplies this value. + /// + public virtual int MatchInfoGuidePageColumns => 2; /// /// Gets the group . This is used to color the group in the options menu. @@ -76,7 +121,8 @@ public abstract class AbstractOptionGroup /// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. /// /// The type of the optionable that this group contains. -public abstract class AbstractOptionGroup : AbstractOptionGroup where T : IOptionable +public abstract class AbstractOptionGroup : AbstractOptionGroup + where T : IOptionable { /// public override Type OptionableType => typeof(T); @@ -87,14 +133,15 @@ public abstract class AbstractOptionGroup : AbstractOptionGroup where T : IOp get { var type = typeof(T); - if (type == typeof(BaseModifier)) + if (typeof(BaseModifier).IsAssignableFrom(type)) { return MenuCategory.Modifiers; } - else if (type == typeof(RoleBehaviour)) + else if (typeof(RoleBehaviour).IsAssignableFrom(type)) { return MenuCategory.Roles; } + return MenuCategory.Game; } } @@ -104,7 +151,8 @@ public abstract class AbstractOptionGroup : AbstractOptionGroup where T : IOp /// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. /// /// The custom role that the group is for. -public abstract class AbstractRoleOptionGroup() : AbstractOptionGroup where T : ICustomRole +public abstract class AbstractRoleOptionGroup : AbstractOptionGroup + where T : ICustomRole { /// public override Type OptionableType => typeof(T); @@ -118,11 +166,9 @@ public abstract class AbstractRoleOptionGroup() : AbstractOptionGroup wher get { var role = CustomRoleManager.CustomMiraRoles.FirstOrDefault(x => x.GetType() == OptionableType); - if (role == null) - { - return new(new Color(0.7333f, 0.7333f, 0.7333f, 1)); - } - return new(role.RoleColor, role.Configuration.IconTmp); + return role == null + ? new(new Color(0.7333f, 0.7333f, 0.7333f, 1)) + : new(role.RoleColor, role.Configuration.IconTmp); } } } @@ -133,7 +179,7 @@ public abstract class AbstractRoleOptionGroup() : AbstractOptionGroup wher public enum MenuCategory { /// - /// Placeholder for indexing purposes. Options don't exist in the presset tab. + /// Placeholder for indexing purposes. Options don't exist in the preset tab. /// Preset, diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs new file mode 100644 index 0000000..e98d010 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute to set the delegate in a or . +/// +/// The method to get the changed event from. +/// The type the method is in. +[AttributeUsage(AttributeTargets.Property)] +public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type? holderType = null) + : ModdedOptionMemberAttribute(methodName, holderType, "Changed") +{ + internal Delegate? GetChanged(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) + { + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + + return null; + } + + var valueType = option.GetValueType(); + + if (!ValidateSignature(method, typeof(void), valueType)) + return null; + + var delegateType = typeof(Action<>).MakeGenericType(valueType); + return CompileDynamicDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "Changed"); + } + + internal Func? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType) + { + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + + return null; + } + + if (!ValidateListSignature(method, typeof(void), out var hasIndex, valueType)) + return null; + + var delegateType = typeof(Action<>).MakeGenericType(valueType); + return CompileDynamicListDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "ListChanged", hasIndex); + } +} diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs new file mode 100644 index 0000000..90e7009 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute to set the delegate in a or . +/// +/// The method to get the getter from. +/// The type the method is in. +[AttributeUsage(AttributeTargets.Property)] +public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type? holderType = null) + : ModdedOptionMemberAttribute(methodName, holderType, "Getter") +{ + internal Delegate? GetGetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) + { + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + + return null; + } + + var valueType = option.GetValueType(); + + if (!ValidateSignature(method, valueType, valueType)) + return null; + + var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType); + return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "Getter"); + } + + internal Func? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) + { + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + + return null; + } + + if (!ValidateListSignature(method, valueType, out var hasIndex, valueType)) + return null; + + var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType); + return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "ListGetter", hasIndex); + } +} diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs new file mode 100644 index 0000000..f98fc0e --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using MiraAPI.Utilities; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Base attribute for resolving and compiling option delegate members (, , , and ). +/// +/// The name of the target member, or to use the property name with . +/// The type containing the target member, or to use the declaring type. +/// The default suffix appended to the property name when is not specified. +[AttributeUsage(AttributeTargets.Property)] +public abstract class ModdedOptionMemberAttribute(string? memberName, Type? holderType, string defaultSuffix) : Attribute +{ + private static readonly Dictionary DelegateCache = []; + + /// + /// A helper class to bind a specific list index to a dynamically compiled delegate. + /// + /// The index to bind. + public sealed class DelegateIndexClosure(int index) + { + /// + /// The bound index. + /// + [SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "No need to be private.")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] + public readonly int Index = index; + } + + /// + /// Exposes the index field so child attributes can emit instructions to read it. + /// + protected static readonly FieldInfo IndexClosureField = typeof(DelegateIndexClosure).GetField(nameof(DelegateIndexClosure.Index))!; + + /// + /// Gets the resolved name of the target member. + /// + protected string? MemberName { get; private set; } = memberName; + + /// + /// Validates that a method matches the expected return type and parameter types. + /// + /// The method to validate. + /// The expected return type of the method. + /// The expected types of the parameters of the method. + /// true if the method is valid; false otherwise. + protected bool ValidateSignature(MethodInfo method, Type expectedReturnType, params Type[] expectedParamTypes) + { + if (method.ReturnType != expectedReturnType) + { + Error($"{MemberName}'s return type must be {expectedReturnType.Name}."); + return false; + } + + var parameters = method.GetParameters(); + + if (parameters.Length != expectedParamTypes.Length) + { + Error($"{MemberName} must have exactly {expectedParamTypes.Length} parameter(s)."); + return false; + } + + for (var i = 0; i < expectedParamTypes.Length; i++) + { + if (parameters[i].ParameterType == expectedParamTypes[i]) + continue; + + Error($"{MemberName}'s parameter at index {i} must be {expectedParamTypes[i].Name}."); + return false; + } + + return true; + } + + /// + /// Validates that a list method matches the expected return type, base parameter types, and an optional integer index. + /// + /// The method to validate. + /// The expected return type of the method. + /// When the method returns, a flag that indicates if the method has an index parameter. + /// The expected types of the parameters of the method. + /// true if the method is valid; false otherwise. + protected bool ValidateListSignature(MethodInfo method, Type expectedReturnType, out bool hasIndex, params Type[] expectedParamTypes) + { + hasIndex = false; + + if (method.ReturnType != expectedReturnType) + { + Error($"{MemberName}'s return type must be {expectedReturnType.Name}."); + return false; + } + + var parameters = method.GetParameters(); + + if (parameters.Length < expectedParamTypes.Length || parameters.Length > expectedParamTypes.Length + 1) + { + Error($"{MemberName} must have {expectedParamTypes.Length} parameter(s), optionally with an extra trailing int parameter."); + return false; + } + + for (var i = 0; i < expectedParamTypes.Length; i++) + { + if (parameters[i].ParameterType == expectedParamTypes[i]) + continue; + + Error($"{MemberName}'s parameter at index {i} must be {expectedParamTypes[i].Name}."); + return false; + } + + if (parameters.Length == expectedParamTypes.Length + 1) + { + if (parameters[^1].ParameterType != typeof(int)) + { + Error($"{MemberName}'s final parameter must be an int (the list index)."); + return false; + } + + hasIndex = true; + } + + return true; + } + + /// + /// Resolves the target (property, field or method) and its associated singleton instance getter, if applicable. + /// + /// The option group instance the property belongs to. + /// The option property decorated with this attribute. + /// When this method returns, contains the resolved expected to hold the target member. + /// + /// When this method returns, contains the getter + /// if the resolved member is an instance member on an ; otherwise, . + /// + /// When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too. + /// The resolved if found; otherwise, . + [SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")] + protected MemberInfo? ResolveMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound) + { + var groupType = group.GetType(); + var flags = BindingFlags.Static | BindingFlags.Public; + + MemberName ??= property.Name + defaultSuffix; + + type = holderType ?? groupType; + instanceGetter = null; + + if (type.IsAssignableTo(typeof(AbstractOptionGroup))) + { + flags |= BindingFlags.Instance; + + if (type == groupType) + flags |= BindingFlags.NonPublic; + + instanceGetter = typeof(OptionGroupSingleton<>) + .MakeGenericType(type) + .GetProperty(nameof(OptionGroupSingleton<>.Instance))! + .GetGetMethod(); + } + else if (!type.IsStatic()) + { + flags |= BindingFlags.Instance; + + var canSearchPrivates = type.GetCustomAttribute() != null; + + if (canSearchPrivates) + flags |= BindingFlags.NonPublic; + + var getterFlags = BindingFlags.Static | BindingFlags.Public | (canSearchPrivates ? BindingFlags.NonPublic : BindingFlags.Default); + + foreach (var method in type.GetMethods(getterFlags)) + { + if (method.GetParameters().Length > 0 || (method.GetCustomAttribute() == null && method.Name != "GetInstance")) + continue; + + instanceGetter = method; + break; + } + + if (instanceGetter == null) + { + foreach (var prop in type.GetProperties(getterFlags)) + { + if (prop.GetCustomAttribute() == null && prop.Name != "Instance") + continue; + + var getMethod = prop.GetGetMethod(canSearchPrivates); + + if (getMethod == null) + continue; + + instanceGetter = getMethod; + break; + } + } + + if (instanceGetter == null) + { + Error($"No valid static instance getter (method GetInstance or property Instance) found in instanced type {type.Name}. Try renaming the method/property, or decorate it with InstanceGetterAttribute."); + notFound = false; + return null; + } + + if (!instanceGetter.ReturnType.IsAssignableTo(type)) + { + Error($"Found instance getter {instanceGetter.Name}, but the return type did not match what was expected ({type.Name})."); + notFound = false; + return null; + } + } + else if (type.GetCustomAttribute() != null) + { + flags |= BindingFlags.NonPublic; + } + + var member = type.GetMember(MemberName, flags).FirstOrDefault(); + notFound = true; + + if (member == null) + return null; + + notFound = false; + + if (instanceGetter == null) + return member; + + bool isStatic; + + if (member is FieldInfo field) + { + isStatic = field.IsStatic; + } + else + { + var method = (member as PropertyInfo)?.GetGetMethod(true) ?? member as MethodInfo; + isStatic = method is { IsStatic: true }; + } + + if (isStatic) + instanceGetter = null; + + return member; + } + + /// + /// Resolves the target and its associated singleton instance getter, if applicable. + /// + /// The option group instance the property belongs to. + /// The option property decorated with this attribute. + /// When this method returns, contains the resolved expected to hold the target method. + /// + /// When this method returns, contains the getter + /// if the resolved method is an instance method on an ; otherwise, . + /// + /// When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too. + /// The resolved if found; otherwise, . + protected MethodInfo? ResolveMethod(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound) + { + return ResolveMember(group, property, out type, out instanceGetter, out notFound) as MethodInfo; + } + + /// + /// Compiles a using IL generation to invoke the resolved static or instance . + /// + /// The that owns the generated and holds the target method. + /// The target to invoke. + /// + /// The static getter used to push the target instance onto the evaluation stack for instance methods, + /// or if is static. + /// + /// The return of the generated delegate (or for actions). + /// The parameter types accepted by the generated delegate. + /// The concrete delegate to create (e.g., or ). + /// A descriptive suffix appended to the generated name for debugging and stack traces. + /// Whether to push the delegate parameters onto the stack before invoking . + /// An optional callback to emit additional IL instructions before returning. + /// A compiled of type bound to the generated IL method. + protected static Delegate CompileDynamicDelegate( + Type ownerType, + MethodInfo method, + MethodInfo? instanceGetter, + Type returnType, + Type[] paramTypes, + Type delegateType, + string suffix, + bool forwardParameters = true, + Action? postProcessCall = null) + { + if (postProcessCall == null && DelegateCache.TryGetValue(method, out var cachedDelegate)) + return cachedDelegate; + + var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}", returnType, paramTypes, ownerType, true); + var il = dm.GetILGenerator(); + + if (!method.IsStatic && instanceGetter != null) + il.Emit(OpCodes.Call, instanceGetter); + + if (forwardParameters) + { + for (byte i = 0; i < paramTypes.Length; i++) + { + var loadOp = i switch + { + 0 => OpCodes.Ldarg_0, + 1 => OpCodes.Ldarg_1, + 2 => OpCodes.Ldarg_2, + 3 => OpCodes.Ldarg_3, + _ => OpCodes.Ldarg_S, + }; + + if (loadOp == OpCodes.Ldarg_S) + il.Emit(loadOp, i); + else + il.Emit(loadOp); + } + } + + var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt; + il.Emit(callOp, method); + + postProcessCall?.Invoke(il); + + il.Emit(OpCodes.Ret); + + var compiledDelegate = dm.CreateDelegate(delegateType); + + if (postProcessCall == null) + DelegateCache[method] = compiledDelegate; + + return compiledDelegate; + } + + /// + /// Compiles a factory function that provides a for each index in an option list. + /// If is and is , + /// a single is compiled once and reused for all indices. + /// + /// The that owns the generated and holds the target method. + /// The target to invoke. + /// + /// The static getter used to push the target instance onto the evaluation stack for instance methods, + /// or if is static. + /// + /// The return of the generated delegate (or for actions). + /// The parameter types accepted by the generated delegate. + /// The concrete delegate to create. + /// A descriptive suffix appended to the generated name for debugging and stack traces. + /// Whether the target takes the list index as its final parameter. + /// An optional index-independent callback to emit additional IL instructions after is called. + /// An optional index-dependent callback to emit additional IL instructions after is called. + /// A factory function that returns a compiled of type for a given list index. + protected static Func CompileDynamicListDelegate( + Type ownerType, + MethodInfo method, + MethodInfo? instanceGetter, + Type returnType, + Type[] baseParamTypes, + Type delegateType, + string suffix, + bool hasIndexParameter, + Action? postProcessCall = null, + Action? postProcessListCall = null) + { + if (!hasIndexParameter && postProcessListCall is null) + { + var sharedDelegate = CompileDynamicDelegate( + ownerType, + method, + instanceGetter, + returnType, + baseParamTypes, + delegateType, + suffix, + baseParamTypes.Length > 0, + postProcessCall); + + return _ => sharedDelegate; + } + + var fullParamTypes = new Type[baseParamTypes.Length + 1]; + fullParamTypes[0] = typeof(DelegateIndexClosure); + Array.Copy(baseParamTypes, 0, fullParamTypes, 1, baseParamTypes.Length); + + var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}_List", returnType, fullParamTypes, ownerType, true); + var il = dm.GetILGenerator(); + + if (!method.IsStatic && instanceGetter != null) + il.Emit(OpCodes.Call, instanceGetter); + + for (var i = 0; i < baseParamTypes.Length; i++) + { + var argPos = (byte)(i + 1); + var loadOp = argPos switch + { + 1 => OpCodes.Ldarg_1, + 2 => OpCodes.Ldarg_2, + 3 => OpCodes.Ldarg_3, + _ => OpCodes.Ldarg_S, + }; + + if (loadOp == OpCodes.Ldarg_S) + il.Emit(loadOp, argPos); + else + il.Emit(loadOp); + } + + if (hasIndexParameter) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, IndexClosureField); + } + + var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt; + il.Emit(callOp, method); + + postProcessCall?.Invoke(il); + postProcessListCall?.Invoke(il); + + il.Emit(OpCodes.Ret); + + return index => dm.CreateDelegate(delegateType, new DelegateIndexClosure(index)); + } +} diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs new file mode 100644 index 0000000..9e085ba --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute to set the delegate in a or . +/// +/// The method to get the setter from. +/// The type the method is in. +[AttributeUsage(AttributeTargets.Property)] +public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type? holderType = null) + : ModdedOptionMemberAttribute(methodName, holderType, "Setter") +{ + internal Delegate? GetSetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) + { + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + + return null; + } + + var valueType = option.GetValueType(); + + if (!ValidateSignature(method, valueType, valueType, valueType)) + return null; + + var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType); + return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "Setter"); + } + + internal Func? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) + { + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + + return null; + } + + if (!ValidateListSignature(method, valueType, out var hasIndex, valueType, valueType)) + return null; + + var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType); + return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "ListSetter", hasIndex); + } +} diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs new file mode 100644 index 0000000..232ffee --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using MiraAPI.GameOptions.OptionTypes; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute to set the delegate in a or . +/// +/// The member to get the visibility function from. +/// The type the member is in. +[AttributeUsage(AttributeTargets.Property)] +public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, Type? holderType = null) + : ModdedOptionMemberAttribute(memberName, holderType, "Visibility") +{ + private static readonly MethodInfo ModdedOptionBoolValueGetter = typeof(ModdedOption).GetProperty(nameof(ModdedOption<>.Value))!.GetGetMethod()!; + private static readonly MethodInfo ListBoolIndexerGetter = typeof(IList).GetProperty("Item")!.GetGetMethod()!; + + internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property) + { + if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member) + { + if (notFound) + Error($"Member {MemberName} does not exist in {type.FullName}"); + + return null; + } + + MethodInfo? targetMethod; + Action? postProcess = null; + + if (member is PropertyInfo vProperty) + { + if (vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption))) + { + postProcess = il => il.Emit(OpCodes.Callvirt, ModdedOptionBoolValueGetter); + } + else if (!vProperty.PropertyType.IsAssignableTo(typeof(bool))) + { + Error($"Property {MemberName} is not a bool or a bool ModdedOption."); + return null; + } + + if (vProperty.GetIndexParameters().Length > 0) + { + Error($"Property {MemberName} cannot be an indexer."); + return null; + } + + targetMethod = vProperty.GetGetMethod(true); + + if (targetMethod is null) + { + Error($"Property {MemberName} does not have a getter."); + return null; + } + } + else if (member is MethodInfo vMethod) + { + if (vMethod.ReturnType != typeof(bool)) + { + Error($"Method {MemberName} does not return a bool."); + return null; + } + + if (vMethod.GetParameters().Length > 0) + { + Error($"Method {MemberName} has too many parameters."); + return null; + } + + targetMethod = vMethod; + } + else if (member is FieldInfo field) + { + if (field.FieldType != typeof(bool)) + { + Error($"Field {MemberName} is not a bool."); + return null; + } + + var dynamicMethod = new DynamicMethod($"GetVisibility_{type.Name}_{field.Name}", typeof(bool), Type.EmptyTypes, type, true); + var il = dynamicMethod.GetILGenerator(); + + if (instanceGetter != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, instanceGetter); + il.Emit(OpCodes.Ldfld, field); + } + else + { + il.Emit(OpCodes.Ldsfld, field); + } + + il.Emit(OpCodes.Ret); + return dynamicMethod.CreateDelegate>(); + } + else + { + Error($"Member {MemberName} of {type.FullName} is not valid for Visible function."); + return null; + } + + return (Func)CompileDynamicDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func), "Visibility", false, postProcess); + } + + internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) + { + if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member) + { + if (notFound) + Error($"Member {MemberName} does not exist in {type.FullName}"); + + return null; + } + + MethodInfo? targetMethod; + Action? postProcess = null; + Action? postProcessList = null; + + var hasIndexParam = false; + + if (member is PropertyInfo vProperty) + { + var isListBool = false; + + if (vProperty.PropertyType.IsAssignableTo(typeof(IList))) + { + isListBool = true; + postProcessList = il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, IndexClosureField); + il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter); + }; + } + else if (vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption))) + { + postProcess = il => il.Emit(OpCodes.Callvirt, ModdedOptionBoolValueGetter); + } + else if (!vProperty.PropertyType.IsAssignableTo(typeof(bool))) + { + Error($"Property {MemberName} is not a bool value or indexer, a list of bools, or a bool ModdedOption."); + return null; + } + + var indexParams = vProperty.GetIndexParameters(); + + if (indexParams.Length > 1) + { + Error($"Indexer {MemberName} has too many parameters."); + return null; + } + + if (isListBool && indexParams.Length != 0) + { + Error($"Property {MemberName} cannot be an indexer of lists of bools."); + return null; + } + + if (indexParams.Length == 1) + { + if (indexParams[0].ParameterType != typeof(int)) + { + Error($"Indexer {MemberName}'s parameter is not an int."); + return null; + } + + hasIndexParam = true; + } + + targetMethod = vProperty.GetGetMethod(true); + + if (targetMethod is null) + { + Error($"Property {MemberName} does not have a getter."); + return null; + } + } + else if (member is MethodInfo vMethod) + { + if (vMethod.ReturnType != typeof(bool)) + { + Error($"Method {MemberName} does not return a bool."); + return null; + } + + var paramList = vMethod.GetParameters(); + + if (paramList.Length > 1) + { + Error($"Method {MemberName} has too many parameters."); + return null; + } + + if (paramList.Length == 1) + { + if (paramList[0].ParameterType != typeof(int)) + { + Error($"Method {MemberName}'s parameter is not an int."); + return null; + } + + hasIndexParam = true; + } + + targetMethod = vMethod; + } + else if (member is FieldInfo field) + { + if (field.FieldType.IsAssignableTo(typeof(IList))) + { + postProcess = il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter); + }; + } + else if (field.FieldType != typeof(bool)) + { + Error($"Field {MemberName} is not a bool or a list of bools."); + return null; + } + + var dynamicMethod = new DynamicMethod($"GetListVisibility_{type.Name}_{field.Name}", typeof(bool), [typeof(int)], type, true); + var il = dynamicMethod.GetILGenerator(); + + if (instanceGetter != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, instanceGetter); + il.Emit(OpCodes.Ldfld, field); + } + else + { + il.Emit(OpCodes.Ldsfld, field); + } + + postProcess?.Invoke(il); + + il.Emit(OpCodes.Ret); + + var func = dynamicMethod.CreateDelegate>(); + return index => () => func(index); + } + else + { + Error($"Member {MemberName} of {type.FullName} is not valid for Visibility function."); + return null; + } + + return CompileDynamicListDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func), "ListVisibility", hasIndexParam, postProcess, postProcessList); + } +} diff --git a/MiraAPI/GameOptions/Attributes/Delegate/SearchAttributes.cs b/MiraAPI/GameOptions/Attributes/Delegate/SearchAttributes.cs new file mode 100644 index 0000000..c6d4261 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/Delegate/SearchAttributes.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// An attribute used to mark a method or property as one that returns an instance of the class it's declared in. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public sealed class InstanceGetterAttribute : Attribute; + +/// +/// An attribute used to mark a class as one whose reflection search should include private members. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class SearchPrivateAttribute : Attribute; diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index 8dc4b26..2754920 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -1,20 +1,22 @@ using System; using System.Reflection; using MiraAPI.GameOptions.OptionTypes; -using MiraAPI.Translation; namespace MiraAPI.GameOptions.Attributes; /// /// Attribute for creating a . /// +/// The title of the option. +/// The type. +/// An option list of string values to use in place of the name. [AttributeUsage(AttributeTargets.Property)] public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null) : ModdedOptionAttribute(title) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var opt = new ModdedEnumOption(Title.Translate(), (int)(value ?? 0), enumType, values); + var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values); return opt; } @@ -30,6 +32,39 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va { return HolderOption is ModdedEnumOption opt ? Enum.ToObject(enumType, opt.Value) - : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); + : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {enumType.FullName} is not a ModdedEnumOption"); + } +} + +/// +/// Attribute for creating an enum option. +/// +/// The enum type. +/// The title of the option. +/// An option list of string values to use in place of the name. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionAttribute(string title, string[]? values = null) + : ModdedOptionAttribute(title) + where T : struct, Enum +{ + internal override IModdedOption CreateOption(object? value, PropertyInfo property) + { + var opt = new ModdedEnumOption(Title, (T)(value ?? 0), values); + return opt; + } + + /// + public override void SetValue(object value) + { + var opt = HolderOption as ModdedEnumOption; + opt?.SetValue((T)value); + } + + /// + public override object GetValue() + { + return HolderOption is ModdedEnumOption opt + ? opt.Value + : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).Name}>"); } } diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index 5a30db0..f43773e 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -1,7 +1,6 @@ using System; using System.Reflection; using MiraAPI.GameOptions.OptionTypes; -using MiraAPI.Translation; using MiraAPI.Utilities; namespace MiraAPI.GameOptions.Attributes; @@ -9,44 +8,97 @@ namespace MiraAPI.GameOptions.Attributes; /// /// Attribute for a . /// +/// The option title. +/// The minimum value. +/// The maximum value. +/// The increment. +/// Determines what is shown for zero. +/// Determines what is shown for negative 1. +/// The suffix type. +/// Optional format string for the option screen. +/// Whether increments can be split in half (using Shift). [AttributeUsage(AttributeTargets.Property)] public class ModdedNumberOptionAttribute( string title, float min, float max, - float increment = 1, + float increment, + string zeroBehavior, + string negativeBehavior, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, string? formatString = null, - bool zeroInfinity = false) + bool halfIncrements = false) : ModdedOptionAttribute(title) { + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// The minimum value. + /// The maximum value. + /// The increment. + /// The suffix type. + /// Whether zero is infinity or not. + /// Optional format string for the option screen. + public ModdedNumberOptionAttribute( + string title, + float min, + float max, + float increment = 1, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null, + bool zeroInfinity = false) + : this(title, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f) + { + } + internal override IModdedOption CreateOption(object? value, PropertyInfo property) { + var final = value switch + { + null => min + increment, + int integer => integer, + _ => (float)value, + }; + return new ModdedNumberOption( - Title.Translate(), - (float)(value ?? min + increment), + Title, + final, min, max, increment, + zeroBehavior, + negativeBehavior, suffixType, formatString, - zeroInfinity); + halfIncrements); } /// public override void SetValue(object value) { - var opt = HolderOption as ModdedNumberOption; - opt?.SetValue((float)value); + if (HolderOption is not ModdedNumberOption opt) + return; + + var final = value switch + { + int integer => integer, + _ => (float)value, + }; + opt.SetValue(final); } /// public override object GetValue() { - if (HolderOption is ModdedNumberOption opt) - { - return opt.Value; - } - throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption"); + if (HolderOption is not ModdedNumberOption opt) + throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption"); + + var value = opt.Value; + + if (ValueType == typeof(int)) + return (int)Math.Round(value); + + return value; } } diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index 3d48453..91b7856 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -10,14 +10,15 @@ namespace MiraAPI.GameOptions.Attributes; /// 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, Type? modeType = null) : Attribute +public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : PropertyOptionAttribute { internal IModdedOption? HolderOption { get; set; } + internal Type ValueType { get; set; } /// /// Gets the title of the option. /// - public string Title { get; private set; } = title; + public string Title => title; /// /// Gets the role type of the option. @@ -29,17 +30,5 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null, /// protected Type? ModeType { get; private set; } = modeType; - /// - /// Sets the value of the option. - /// - /// The new value as an . - public abstract void SetValue(object value); - - /// - /// Gets the value of the option. - /// - /// The value of the option as an . - public abstract object GetValue(); - internal abstract IModdedOption? CreateOption(object? value, PropertyInfo property); } diff --git a/MiraAPI/GameOptions/Attributes/ModdedPlayerOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedPlayerOptionAttribute.cs new file mode 100644 index 0000000..610d0c8 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedPlayerOptionAttribute.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for a . +/// +/// The title of the option. +/// Whether the option includes a none option. If None is selected, value will return -1. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedPlayerOptionAttribute( + string title, + bool allowNone = true) + : ModdedOptionAttribute(title) +{ + internal override IModdedOption? CreateOption(object? value, PropertyInfo property) + { + var option = new ModdedPlayerOption(Title, allowNone); + return option; + } + + /// + public override object GetValue() + { + if (HolderOption is not ModdedPlayerOption opt) + throw new InvalidOperationException($"Holder option for {Title} is not a ModdedPlayerOption."); + + if (ValueType == typeof(int)) + return opt.Value; + + return opt.SelectedPlayer!; + } + + /// + public override void SetValue(object value) + { + if (HolderOption is not ModdedPlayerOption opt) + return; + + var final = value switch + { + NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0), + _ => (int)value, + }; + opt?.SetValue(final); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs new file mode 100644 index 0000000..9086b1f --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for a . +/// +/// The title of the option. +/// An option list of string value. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedStringOptionAttribute( + string title, + string[] values) + : ModdedOptionAttribute(title) +{ + internal override IModdedOption? CreateOption(object? value, PropertyInfo property) + { + var option = new ModdedStringOption(Title, (string)(value ?? string.Empty), values); + return option; + } + + /// + public override object GetValue() + { + return HolderOption is ModdedStringOption opt + ? opt.Value + : throw new InvalidOperationException($"Holder option for {Title} is not a ModdedStringOption."); + } + + /// + public override void SetValue(object value) + { + var opt = HolderOption as ModdedStringOption; + opt?.SetValue((string)value); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs index 838f8b6..042c1f3 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs @@ -1,7 +1,6 @@ using System; using System.Reflection; using MiraAPI.GameOptions.OptionTypes; -using MiraAPI.Translation; namespace MiraAPI.GameOptions.Attributes; @@ -9,12 +8,27 @@ namespace MiraAPI.GameOptions.Attributes; /// Attribute for a . /// /// The option title. +/// What the option displays when toggled on. +/// What the option displays when toggled off. [AttributeUsage(AttributeTargets.Property)] -public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(title) +public class ModdedToggleOptionAttribute( + string title, + string on, + string off) + : ModdedOptionAttribute(title) { + /// + /// Initializes a new instance of the class. + /// + /// The option title. + public ModdedToggleOptionAttribute(string title) + : this(title, "On", "Off") + { + } + internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var toggleOpt = new ModdedToggleOption(Title.Translate(), (bool)(value ?? false)); + var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), on, off); return toggleOpt; } @@ -28,10 +42,8 @@ public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(t /// public override object GetValue() { - if (HolderOption is ModdedToggleOption opt) - { - return opt.Value; - } - throw new InvalidOperationException($"Holder option for {Title} is not a ModdedToggleOption."); + return HolderOption is ModdedToggleOption opt + ? opt.Value + : throw new InvalidOperationException($"Holder option for {Title} is not a ModdedToggleOption."); } } diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs new file mode 100644 index 0000000..bf2fdf0 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for creating a list of enum options. +/// +/// The title of the option. +/// The type. +/// An option list of string values to use in place of the name. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]? values = null) + : ModdedOptionListAttribute(title) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList( + value.Count, idx => new(GetFormattedTitle(idx), (int)(value[idx] ?? 0), enumType, values)); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedEnumOption; + opt?.SetValue((int)value); + } + + /// + public override object GetValue(int idx) + { + return HolderOptionList?[idx] is ModdedEnumOption opt + ? Enum.ToObject(enumType, opt.Value) + : throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {enumType.FullName} is not a ModdedEnumOption"); + } +} + +/// +/// Attribute for creating a list of enum options. +/// +/// The enum type. +/// The option's title. +/// The option's possible values. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionListAttribute(string title, string[]? values = null) + : ModdedOptionListAttribute(title) + where T : struct, Enum +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList>( + value.Count, idx => new(GetFormattedTitle(idx), (T)(value[idx] ?? 0), values)); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedEnumOption; + opt?.SetValue((T)value); + } + + /// + public override object GetValue(int idx) + { + return HolderOptionList?[idx] is ModdedEnumOption opt + ? opt.Value + : throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).FullName}>"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs new file mode 100644 index 0000000..c393db3 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections; +using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for creating a list of number options. +/// +/// The option title. +/// The minimum value. +/// The maximum value. +/// The increment. +/// Determines what is shown for zero. +/// Determines what is shown for negative 1. +/// The suffix type. +/// Optional format string for the option screen. +/// Whether increments can be split in half (using Shift). +[AttributeUsage(AttributeTargets.Property)] +public class ModdedNumberOptionListAttribute( + string title, + float min, + float max, + float increment, + string zeroBehavior, + string negativeBehavior, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null, + bool halfIncrements = false) + : ModdedOptionListAttribute(title) +{ + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// The minimum value. + /// The maximum value. + /// The increment. + /// The suffix type. + /// Whether zero is infinity or not. + /// Optional format string for the option screen. + public ModdedNumberOptionListAttribute( + string title, + float min, + float max, + float increment = 1, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null, + bool zeroInfinity = false) + : this(title, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f) + { + } + + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + return new ModdedOptionList(value.Count, idx => + { + var final = value[idx] switch + { + null => min + increment, + int integer => integer, + _ => (float)value[idx]!, + }; + + return new( + GetFormattedTitle(idx), + final, + min, + max, + increment, + zeroBehavior, + negativeBehavior, + suffixType, + formatString, + halfIncrements); + }); + } + + /// + public override void SetValue(int idx, object value) + { + if (HolderOptionList?[idx] is not ModdedNumberOption opt) + return; + + var final = value switch + { + int integer => integer, + _ => (float)value, + }; + opt.SetValue(final); + } + + /// + public override object GetValue(int idx) + { + if (HolderOptionList?[idx] is not ModdedNumberOption opt) + throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption"); + + var value = opt.Value; + + if (ValueType == typeof(int)) + return (int)Math.Round(value); + + return value; + } +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs new file mode 100644 index 0000000..d7ef6e6 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.RegularExpressions; +using MiraAPI.Translation; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Represents an attribute that is used to define a list of modded options. +/// +/// The title of the options. +[AttributeUsage(AttributeTargets.Property)] +public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAttribute +{ + internal IModdedOptionList? HolderOptionList { get; set; } + internal IList? Value { get; set; } + internal Type ValueType { get; set; } + + /// + /// Gets the title of the options. + /// If the string contains a format parameter, e.g., {0}, then the option's index will be given to it. + /// The string cannot contain more than one parameter. + /// If no parameter is given, the title will simply append the index to the end of it. + /// + public string Title => title; + + /// + /// Sets the value of all the options. + /// + /// The new values as an object. + public override void SetValue(object value) + { + Value = (IList)value; + + if (Value.Count != HolderOptionList!.Count) + { + throw new InvalidOperationException($"Value set to option list cannot change the list's length."); + } + + for (int i = 0; i < Value.Count; i++) + { + SetValue(i, Value[i]!); + } + } + + /// + /// Sets the value of the specific option. + /// + /// The option to set. + /// The new value as an object. + public abstract void SetValue(int idx, object value); + + /// + /// Gets the value of all the options. + /// + /// The value of the options as an object. + public override object GetValue() + { + return Value!; + } + + /// + /// Gets the value of the specific option. + /// + /// The option to set. + /// The value of the option as an object. + public abstract object GetValue(int idx); + + internal abstract IModdedOptionList? CreateOptionList(IList value, PropertyInfo property); + + /// + /// Gets the formatted with the option's index. + /// + /// The option's index. + /// The formatted option's title. + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "I don't even know if this is worth it.")] + protected string GetFormattedTitle(int index) + { + var match = Regex.Match(Title.Translate(), @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout); + + if (!match.Success) + { + return Title + index; + } + + if (match.Value == "{0}" && !match.NextMatch().Success) + { + return string.Format(Title, index); + } + + Error($"ModdedOptionList Title of {Title} cannot contain more than the first parameter for formatting."); + return Title + index; + } +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedPlayerOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedPlayerOptionListAttribute.cs new file mode 100644 index 0000000..533492b --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedPlayerOptionListAttribute.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections; +using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for creating a list of player options. +/// +/// The option title. +/// Whether the option includes a none option. If None is selected, value will return -1. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedPlayerOptionListAttribute( + string title, + bool allowNone = true) + : ModdedOptionListAttribute(title) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + return new ModdedOptionList(value.Count, idx => + { + return new(GetFormattedTitle(idx), allowNone); + }); + } + + /// + public override void SetValue(int idx, object value) + { + if (HolderOptionList?[idx] is not ModdedPlayerOption opt) + return; + + var final = value switch + { + NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0), + _ => (int)value, + }; + opt?.SetValue(final); + } + + /// + public override object GetValue(int idx) + { + if (HolderOptionList?[idx] is not ModdedPlayerOption opt) + throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedPlayerOption"); + + if (ValueType == typeof(int)) + return opt.Value; + + return opt.SelectedPlayer!; + } +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedStringOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedStringOptionListAttribute.cs new file mode 100644 index 0000000..78c4b66 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedStringOptionListAttribute.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections; +using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Translation; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for creating a list of string options. +/// +/// The title of the option. +/// An option list of string value. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedStringOptionListAttribute( + string title, + string[] values) + : ModdedOptionListAttribute(title) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + return new ModdedOptionList(value.Count, idx => + { + return new ModdedStringOption(Title.Translate(), (string)(value[idx] ?? string.Empty), values); + }); + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedStringOption; + opt?.SetValue((string)value); + } + + /// + public override object GetValue(int idx) + { + return HolderOptionList?[idx] is ModdedStringOption opt + ? opt.Value + : throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedStringOption"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs new file mode 100644 index 0000000..9dab9a4 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections; +using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for a list of toggle options. +/// +/// The option's title. +/// What the option displays when toggled on. +/// What the option displays when toggled off. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedToggleOptionListAttribute( + string title, + string on, + string off) + : ModdedOptionListAttribute(title) +{ + /// + /// Initializes a new instance of the class. + /// + /// The option's title. + public ModdedToggleOptionListAttribute(string title) + : this(title, "On", "Off") + { + } + + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList( + value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false), on, off)); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedToggleOption; + opt?.SetValue((bool)value); + } + + /// + public override object GetValue(int idx) + { + return HolderOptionList?[idx] is ModdedToggleOption opt + ? opt.Value + : throw new InvalidOperationException($"Holder option for {GetFormattedTitle(idx)} is not a ModdedToggleOption."); + } +} diff --git a/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs new file mode 100644 index 0000000..643d7b7 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Represents an attribute that is used to intercept a property's getter and setter. +/// +[AttributeUsage(AttributeTargets.Property)] +public abstract class PropertyOptionAttribute : Attribute +{ + /// + /// Sets the value of the option. + /// + /// The new value as an object. + public abstract void SetValue(object value); + + /// + /// Gets the value of the option. + /// + /// The value of the option as an object. + public abstract object GetValue(); +} diff --git a/MiraAPI/GameOptions/CollectionBuilder.cs b/MiraAPI/GameOptions/CollectionBuilder.cs new file mode 100644 index 0000000..99d42d9 --- /dev/null +++ b/MiraAPI/GameOptions/CollectionBuilder.cs @@ -0,0 +1,8 @@ +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)] +internal sealed class CollectionBuilderAttribute(Type builderType, string methodName) : Attribute +{ + public Type BuilderType { get; } = builderType; + public string MethodName { get; } = methodName; +} diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 646c22b..315fe2e 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; @@ -56,6 +57,21 @@ public interface IModdedOption /// Func Visible { get; set; } + /// + /// Gets or sets a getter event for the option, allowing for dynamic values based on external circumstances. + /// + Delegate? Getter { get; set; } + + /// + /// Gets or sets a setter event for the option, allowing for dynamic value changes based on external circumstances. + /// + Delegate? Setter { get; set; } + + /// + /// Gets or sets the event that is invoked when the value of the option changes. + /// + Delegate? ChangedEvent { get; set; } + /// /// Gets or sets a value indicating whether the option should be included with presets. /// @@ -113,4 +129,37 @@ public interface IModdedOption /// /// The representing the preset configuration. void LoadFromPreset(ConfigFile presetConfig); + + /// + /// Gets the type of the value that the option represents. + /// + /// The type of the option value. + Type GetValueType(); +} + +/// +/// Interface for list of modded options. +/// +public interface IModdedOptionList : IReadOnlyList +{ + /// + /// Gets the function to name the keys of the individual options in the configuration file. + /// + /// Return to use the default of the property's name with its index appended. + /// + Func? ConfigName { get; init; } +} + +/// +/// Interface for list of options. +/// +/// The type of . +public interface IModdedOptionList : IModdedOptionList + where T : IModdedOption +{ + /// + new T this[int index] { get; } + + /// + new IEnumerator GetEnumerator(); } diff --git a/MiraAPI/GameOptions/ISummarizedOptions.cs b/MiraAPI/GameOptions/ISummarizedOptions.cs new file mode 100644 index 0000000..7873902 --- /dev/null +++ b/MiraAPI/GameOptions/ISummarizedOptions.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace MiraAPI.GameOptions; + +/// +/// Allows an option group to compress noisy option blocks in the wiki "Options" section. +/// Implementers can hide specific option keys and replace them with one or more summary lines. +/// +public interface ISummarizedOptions +{ + /// + /// Gets the option string keys (StringNames) to omit from the wiki options list (e.g., a large set of related options). + /// + IReadOnlySet WikiHiddenOptionKeys { get; } + + /// + /// Gets summary lines to insert when the first hidden option would have appeared. + /// Lines should already be formatted as "Title: Value". + /// + IEnumerable GetWikiOptionSummaryLines(); +} diff --git a/MiraAPI/GameOptions/ModdedArray.cs b/MiraAPI/GameOptions/ModdedArray.cs new file mode 100644 index 0000000..65f60ef --- /dev/null +++ b/MiraAPI/GameOptions/ModdedArray.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace MiraAPI.GameOptions; + +/// +/// A fixed-size array created specifically for modded options that automatically synchronizes with option attributes. +/// +/// The type of the values that the array holds. +[CollectionBuilder(typeof(ModdedArrayBuilder), nameof(ModdedArrayBuilder.Create))] +public sealed class ModdedArray : IList, IReadOnlyList, IList +{ + private readonly T[] items; + private int version; + + /// + public int Count => items.Length; + + /// + public bool IsReadOnly => false; + + /// + bool IList.IsFixedSize => true; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => items.SyncRoot; + + /// + /// Initializes a new instance of the class with the provided capacity. + /// + /// The capacity of the list. + public ModdedArray(int capacity) + { + items = new T[capacity]; + } + + /// + /// Initializes a new instance of the class with initial values. + /// + /// The initial collection of values to initialise with. + public ModdedArray(IEnumerable collection) + { + items = [.. collection]; + } + + /// + /// Initializes a new instance of the class with initial values. + /// + /// The initial span of values to initialise with. + public ModdedArray(Span span) + { + items = [.. span]; + } + + /// + /// Initializes a new instance of the class with initial values. + /// + /// The initial span of values to initialise with. + public ModdedArray(ReadOnlySpan span) + { + items = [.. span]; + } + + /// + /// Gets or sets the element at the specified index, syncing with the attribute if registered. + /// + /// The index of the value. + public T this[int index] + { + get => ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute) + ? (T)attribute.GetValue(index) + : items[index]; + set + { + if (ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute)) + attribute.SetValue(index, value!); + else + items[index] = value; + + version++; + } + } + + /// + object? IList.this[int index] + { + get => this[index]; + set => this[index] = (T)value!; + } + + /// + public bool Contains(T item) + { + return Array.IndexOf(items, item) >= 0; + } + + /// + public void CopyTo(T[] array, int arrayIndex) + { + items.CopyTo(array, arrayIndex); + } + + /// + public int IndexOf(T item) + { + return Array.IndexOf(items, item); + } + + /// + public Enumerator GetEnumerator() + { + return new(this); + } + + /// + bool IList.Contains(object? value) + { + return ((IList)items).Contains(value); + } + + /// + int IList.IndexOf(object? value) + { + return ((IList)items).IndexOf(value); + } + + /// + void ICollection.CopyTo(Array array, int index) + { + items.CopyTo(array, index); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + int IList.Add(object? value) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + void IList.Clear() + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + void IList.Insert(int index, object? value) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + void IList.Remove(object? value) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + void IList.RemoveAt(int index) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public void Insert(int index, T item) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public void RemoveAt(int index) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public void Add(T item) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public void Clear() + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public bool Remove(T item) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator : IEnumerator + { + private readonly ModdedArray array; + private readonly int version; + + private int index; + private T? current; + + internal Enumerator(ModdedArray moddedArray) + { + array = moddedArray; + version = moddedArray.version; + index = 0; + current = default; + } + + /// + public readonly T Current => current!; + + /// + readonly object? IEnumerator.Current + { + get + { + if (index == 0 || index == array.Count + 1) + throw new InvalidOperationException("Enumeration has not started or has already finished."); + + return Current; + } + } + + /// + public readonly void Dispose() + { + } + + /// + public bool MoveNext() + { + if (version != array.version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + + if ((uint)index < (uint)array.Count) + { + current = array[index]; + index++; + return true; + } + + index = array.Count + 1; + current = default; + return false; + } + + /// + public void Reset() + { + if (version != array.version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + + index = 0; + current = default; + } + } +} + +/// +/// Provides a builder for to support C# 12 collection expressions. +/// +public static class ModdedArrayBuilder +{ + /// + /// Creates a new from a span of values. + /// + /// The the type of the values. + /// The values to initialise with. + /// The instance initialised with the provided values. + public static ModdedArray Create(ReadOnlySpan values) + { + return new(values); + } +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index d91d079..3e11b3d 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -1,11 +1,12 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using BepInEx.Configuration; using HarmonyLib; -using Il2CppInterop.Runtime.Injection; using MiraAPI.GameModes; using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.OptionTypes; @@ -24,9 +25,15 @@ namespace MiraAPI.GameOptions; /// public static class ModdedOptionsManager { - private static readonly Dictionary OptionAttributes = []; + private static readonly Dictionary GetterLookups = []; + private static readonly Dictionary SetterLookups = []; private static readonly Dictionary TypeToGroup = []; + private static readonly HashSet OptionAttributes = []; + private static readonly HarmonyMethod CachedGetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyGetterPatch)); + private static readonly HarmonyMethod CachedSetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertySetterPatch)); + + internal static readonly ConditionalWeakTable ListInstanceCache = []; internal static readonly Dictionary CreatedPlayerOptions = []; internal static readonly Dictionary CreatedStringOptions = []; internal static readonly Dictionary> GameModeOptionGroups = []; @@ -36,75 +43,60 @@ public static class ModdedOptionsManager internal static uint NextId => _nextId++; private static uint _nextId = 1; + /// + /// Pops a message notification of a setting being changed. + /// + /// The game's notification popper. + /// The key of the option that changed. + /// The new value of the option. + /// The color of the notification. + /// The optional sprite of the notification. + /// A flag that indicates if a notification sound should be played. public static void AddSettingsChangeMessage(NotificationPopper notif, StringNames key, string value, Color textColor, TMP_SpriteAsset? sprite, bool playSound = true) { - string item; var text = textColor.ToTextColor(); - if (sprite != null) - { - item = TranslationController.Instance.GetString( - StringNames.LobbyChangeSettingNotification, - string.Concat( - "", - text, - TranslationController.Instance.GetString(key), - "
"), - "" + value + "" - ); - } - else - { - item = TranslationController.Instance.GetString( - StringNames.LobbyChangeSettingNotification, - "" + - text + - TranslationController.Instance.GetString(key) + "
", - "" + value + ""); - } + var translatedKey = TranslationController.Instance.GetString(key); + var fontTag = ""; + var spritePrefix = sprite != null ? $"" : string.Empty; + + var item = TranslationController.Instance.GetString( + StringNames.LobbyChangeSettingNotification, + $"{spritePrefix}{fontTag}{text}{translatedKey}", + $"{fontTag}{value}"); notif.SettingsChangeMessageLogic(key, item, playSound); } internal static bool RegisterGroup(Type type) { - if (Activator.CreateInstance(type) is not AbstractOptionGroup group) - { - return false; - } - - if (TypeToGroup.ContainsKey(type)) - { - Logger.Error($"Group {type.Name} already exists."); - return false; - } - - Groups.Add(group); - TypeToGroup.Add(type, group); - - typeof(OptionGroupSingleton<>).MakeGenericType(type) - .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! - .SetValue(null, group); - - return true; - } - - internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo) - { - if (Activator.CreateInstance(type) is not AbstractOptionGroup group) - { - return false; - } - if (TypeToGroup.ContainsKey(type)) { Error($"Group {type.Name} already exists."); return false; } - Groups.Add(group); - TypeToGroup.Add(type, group); - if (group.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true) + if (Activator.CreateInstance(type) is not AbstractOptionGroup group) + { + return false; + } + + RegisterGroupInternal(group, type); + return true; + } + + internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo) + { + if (TypeToGroup.ContainsKey(type)) + { + Error($"Group {type.Name} already exists."); + return false; + } + + if (Activator.CreateInstance(type) is not AbstractOptionGroup group) + { + return false; + } + + if (typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType)) { if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList)) { @@ -112,20 +104,29 @@ public static class ModdedOptionsManager } else { - GameModeOptionGroups.Add(group.OptionableType, new List() { group }); + GameModeOptionGroups.Add(group.OptionableType, [group]); } } + pluginInfo.InternalOptionGroups.Add(group); - typeof(OptionGroupSingleton<>).MakeGenericType(type) -#pragma warning disable S3011 - .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! -#pragma warning restore S3011 - .SetValue(null, 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)! + .SetValue(null, group); + } + internal static void RegisterPropertyOption(Type type, PropertyInfo property, MiraPluginInfo pluginInfo) { if (!TypeToGroup.TryGetValue(type, out var group)) @@ -149,7 +150,7 @@ public static class ModdedOptionsManager PropertyInfo property, MiraPluginInfo pluginInfo) { - if (OptionAttributes.ContainsKey(property)) + if (!OptionAttributes.Add(property)) { Error($"Property {property.Name} already has an attribute registered."); return; @@ -169,20 +170,178 @@ public static class ModdedOptionsManager return; } - var setterOriginal = property.GetSetMethod(); - var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); - PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); + var getterOriginal = property.GetGetMethod(true)!; + PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch); + GetterLookups[getterOriginal] = attribute; - var getterOriginal = property.GetGetMethod(); - var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); - PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); + var setterOriginal = property.GetSetMethod(true); + + if (setterOriginal != null) + { + PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch); + SetterLookups[setterOriginal] = attribute; + } - OptionAttributes.Add(property, attribute); attribute.HolderOption = option; + attribute.ValueType = property.PropertyType; + + var customAttributes = property.GetCustomAttributes(false); + + foreach (var attr in customAttributes) + { + switch (attr) + { + case ModdedOptionVisibilityAttribute visibilityAttr: + option.Visible = visibilityAttr.GetVisibility(group, property) ?? DelegateUtilities.AlwaysTrue; + break; + case ModdedOptionGetterAttribute getterAttr: + option.Getter = getterAttr.GetGetter(group, property, option); + break; + case ModdedOptionSetterAttribute setterAttr: + option.Setter = setterAttr.GetSetter(group, property, option); + break; + case ModdedOptionChangedAttribute changedAttr: + option.ChangedEvent = changedAttr.GetChanged(group, property, option); + break; + } + } RegisterOption(option, group, property.Name, pluginInfo); } + internal static void RegisterPropertyOptionList(Type type, PropertyInfo property, MiraPluginInfo pluginInfo) + { + if (!TypeToGroup.TryGetValue(type, out var group)) + { + Error($"Failed to get group for {type.Name}"); + return; + } + + if (property.GetValue(group) is not IModdedOptionList optionList) + { + Error($"Failed to get option list for {property.Name}"); + return; + } + + var configName = optionList.ConfigName ?? (i => property.Name + (i + 1)); + + for (var i = 0; i < optionList.Count; i++) + { + RegisterOption(optionList[i], group, configName(i), pluginInfo); + } + } + + internal static void RegisterAttributeOptionList( + Type type, + ModdedOptionListAttribute attribute, + PropertyInfo property, + MiraPluginInfo pluginInfo) + { + if (!OptionAttributes.Add(property)) + { + Error($"Property {property.Name} already has an attribute registered."); + return; + } + + if (!TypeToGroup.TryGetValue(type, out var group)) + { + Error($"Failed to get group for {type.Name}"); + return; + } + + var propertyVal = property.GetValue(group); + + if (propertyVal == null) + { + Error("Cannot initialize option list with null value."); + return; + } + + var propertyList = (IList)propertyVal; + var optionList = attribute.CreateOptionList(propertyList, property); + + if (optionList == null) + { + Error($"Failed to get option for {property.Name}"); + return; + } + + if (propertyList.Count != optionList.Count) + { + Error("Mismatch in count between values and created options."); + return; + } + + var listType = propertyList.GetType(); + var valueType = listType.GetGenericArguments()[0]; + + if (!listType.IsGenericType || listType.GetGenericTypeDefinition() != typeof(ModdedArray<>)) + { + Error($"Please use ModdedList<{valueType.Name}> for modded option lists."); + return; + } + + var getterOriginal = property.GetGetMethod(true)!; + PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch); + GetterLookups[getterOriginal] = attribute; + + var setterOriginal = property.GetSetMethod(true); + + if (setterOriginal != null) + { + PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch); + SetterLookups[setterOriginal] = attribute; + } + + attribute.HolderOptionList = optionList; + attribute.Value = propertyList; + attribute.ValueType = valueType; + + ListInstanceCache.Add(propertyVal, attribute); + + var customAttributes = property.GetCustomAttributes(false); + + Func? visibilityFactory = null, getterFactory = null, setterFactory = null, changedFactory = null; + + foreach (var attr in customAttributes) + { + switch (attr) + { + case ModdedOptionVisibilityAttribute visibilityAttr: + visibilityFactory = visibilityAttr.GetListVisibility(group, property); + break; + case ModdedOptionGetterAttribute getterAttr: + getterFactory = getterAttr.GetListGetter(group, property, valueType); + break; + case ModdedOptionSetterAttribute setterAttr: + setterFactory = setterAttr.GetListSetter(group, property, valueType); + break; + case ModdedOptionChangedAttribute changedAttr: + changedFactory = changedAttr.GetListChanged(group, property, valueType); + break; + } + } + + for (var i = 0; i < optionList.Count; i++) + { + var option = optionList[i]; + + if (visibilityFactory != null) + option.Visible = (Func)(visibilityFactory(i) ?? DelegateUtilities.AlwaysTrue); + + if (getterFactory != null) + option.Getter = getterFactory(i); + + if (setterFactory != null) + option.Setter = setterFactory(i); + + if (changedFactory != null) + option.ChangedEvent = changedFactory(i); + + RegisterOption(option, group, property.Name + (i + 1), pluginInfo); + } + } + internal static void RegisterOption( IModdedOption option, AbstractOptionGroup group, @@ -213,7 +372,7 @@ public static class ModdedOptionsManager internal static void HandleSyncOptions(NetData[] data) { // necessary to disable then re-enable this setting - // we dont know how other plugins handle their configs + // we don't know how other plugins handle their configs // this way, all the options are saved at once, instead of one by one var oldConfigSetting = new Dictionary(); foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins) @@ -249,13 +408,17 @@ public static class ModdedOptionsManager /// /// The original setter method. /// The new object value. -#pragma warning disable CA1707 - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention.")] + [SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")] public static void PropertySetterPatch(MethodBase __originalMethod, object value) -#pragma warning restore CA1707 { - var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value; + if (!SetterLookups.TryGetValue(__originalMethod, out var attribute)) + return; + attribute.SetValue(value); + + if (attribute is ModdedOptionListAttribute listAttribute && value != null) + ListInstanceCache.AddOrUpdate(value, listAttribute); } /// @@ -264,12 +427,13 @@ public static class ModdedOptionsManager /// The original getter method. /// The result of the property getter. /// so the original getter gets skipped. -#pragma warning disable CA1707 [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] + [SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")] public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result) -#pragma warning restore CA1707 { - var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value; + if (!GetterLookups.TryGetValue(__originalMethod, out var attribute)) + return true; + __result = attribute.GetValue(); return false; } diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs index 6309649..19b1a20 100644 --- a/MiraAPI/GameOptions/ModifierOptionGroup.cs +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -3,10 +3,8 @@ using MiraAPI.PluginLoading; namespace MiraAPI.GameOptions; -#pragma warning disable CA1852 [MiraIgnore] -internal class ModifierOptionGroup : AbstractOptionGroup -#pragma warning restore CA1852 +internal sealed class ModifierOptionGroup : AbstractOptionGroup { public override string GroupName { get; } @@ -14,6 +12,6 @@ internal class ModifierOptionGroup : AbstractOptionGroup { GroupName = name; Options.AddRange(options); - Options.AddRange(groups.SelectMany(x=>x.Options)); + Options.AddRange(groups.SelectMany(x => x.Options)); } } diff --git a/MiraAPI/GameOptions/OptionGroupSingleton.cs b/MiraAPI/GameOptions/OptionGroupSingleton.cs index a521d7d..1957b0b 100644 --- a/MiraAPI/GameOptions/OptionGroupSingleton.cs +++ b/MiraAPI/GameOptions/OptionGroupSingleton.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace MiraAPI.GameOptions; @@ -6,14 +7,14 @@ namespace MiraAPI.GameOptions; /// Singleton for s. /// /// The option group type. -public static class OptionGroupSingleton where T : AbstractOptionGroup +public static class OptionGroupSingleton + where T : AbstractOptionGroup { private static T? _instance; /// /// Gets the instance of the group. /// -#pragma warning disable CA1000 + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a custom option group.")] public static T Instance => _instance ??= ModdedOptionsManager.Groups.OfType().Single(); -#pragma warning restore CA1000 } diff --git a/MiraAPI/GameOptions/OptionNotifConfiguration.cs b/MiraAPI/GameOptions/OptionNotifConfiguration.cs index e16106f..aa02128 100644 --- a/MiraAPI/GameOptions/OptionNotifConfiguration.cs +++ b/MiraAPI/GameOptions/OptionNotifConfiguration.cs @@ -1,7 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using Il2CppInterop.Runtime.Attributes; using TMPro; using UnityEngine; + namespace MiraAPI.GameOptions; /// @@ -9,14 +11,13 @@ namespace MiraAPI.GameOptions; /// public record struct OptionNotifConfiguration { -#pragma warning disable S1133 - [Obsolete("Default constructor is not supported")] -#pragma warning restore S1133 -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + [Obsolete("Default constructor is not supported. Please use the constructor that takes an AbstractOptionGroup parameter.", true)] + [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Will not be removed, as this throws instead.")] +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Read above.) public OptionNotifConfiguration() #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member { - throw new NotImplementedException("Default constructor is not supported."); + throw new NotImplementedException("Default constructor is not supported. Please use the constructor that takes an AbstractOptionGroup parameter."); } /// diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 4e512af..7931d02 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -5,7 +5,6 @@ using System.Text; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Networking; using MiraAPI.Translation; -using Reactor.Localization.Utilities; using UnityEngine; using Object = UnityEngine.Object; @@ -29,19 +28,18 @@ public class ModdedEnumOption : ModdedOption /// The type. /// An option list of string values to use in place of the name. /// Whether to include this option in the preset or not. - public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset) + public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, bool includeInPreset = true) + : base(title, defaultValue, includeInPreset) { Values = values ?? Enum.GetNames(enumType); - Data = ScriptableObject.CreateInstance(); - var data = (StringGameSetting)Data; + var data = ScriptableObject.CreateInstance(); data.Title = StringName; data.Type = global::OptionTypes.String; - data.Values = values is null ? - Enum.GetNames(enumType).Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray() - : [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)]; - + data.Values = Values.Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray(); data.Index = Value; + + Data = data; } /// @@ -52,9 +50,13 @@ public class ModdedEnumOption : ModdedOption $"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}"; stringOption.SetUpFromData(Data, 20); - stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + stringOption.OnValueChanged = (Action)ValueChanged; - // SetUpFromData method doesnt work correctly so we must set the values manually + // Changing the button text to make them more distinct from number options + stringOption.MinusBtn.ChangeButtonText("<"); + stringOption.PlusBtn.ChangeButtonText(">"); + + // SetUpFromData method doesn't work correctly so we must set the values manually stringOption.Title = StringName; stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray(0); stringOption.Value = Value; @@ -114,7 +116,8 @@ public class ModdedEnumOption : ModdedOption /// An option for selecting an value. /// /// The type. -public class ModdedEnumOption : ModdedOption where T : Enum +public class ModdedEnumOption : ModdedOption + where T : struct, Enum { /// /// Gets the string values of the . @@ -128,19 +131,18 @@ public class ModdedEnumOption : ModdedOption where T : Enum /// The default value as an . /// An option list of string values to use in place of the name. /// Whether to include this option in the preset or not. - public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset) + public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset = true) + : base(title, defaultValue, includeInPreset) { - Values = values ?? Enum.GetNames(typeof(T)); - Data = ScriptableObject.CreateInstance(); - var data = (StringGameSetting)Data; + Values = values ?? Enum.GetNames(); + var data = ScriptableObject.CreateInstance(); data.Title = StringName; data.Type = global::OptionTypes.String; - data.Values = values is null ? - Enum.GetNames(typeof(T)).Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray() - : [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)]; - + data.Values = Values.Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray(); data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo); + + Data = data; } /// @@ -151,9 +153,13 @@ public class ModdedEnumOption : ModdedOption where T : Enum $"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}"; stringOption.SetUpFromData(Data, 20); - stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + stringOption.OnValueChanged = (Action)ValueChanged; - // SetUpFromData method doesnt work correctly so we must set the values manually + // Changing the button text to make them more distinct from number options + stringOption.MinusBtn.ChangeButtonText("<"); + stringOption.PlusBtn.ChangeButtonText(">"); + + // SetUpFromData method doesn't work correctly so we must set the values manually stringOption.Title = StringName; stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray(0); stringOption.Value = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo); @@ -178,13 +184,13 @@ public class ModdedEnumOption : ModdedOption where T : Enum /// public override void HandleNetData(byte[] data) { - SetValue((T)Enum.Parse(typeof(T), Encoding.Unicode.GetString(data))); + SetValue(Enum.Parse(Encoding.Unicode.GetString(data))); } /// public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) { - return (T)Enum.Parse(typeof(T), optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo)); + return Enum.Parse(optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo)); } /// diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index 87f4ca9..f27ed13 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -61,10 +61,10 @@ public class ModdedNumberOption : ModdedOption /// The maximum value. /// The increment. /// The suffix type. - /// Optional format string for the option screen. - /// Determines what is shown for zero. Options: ∞, #, or a word value (such as Off). - /// Determines what is shown for negative 1. Options: ∞, #, or a word value (such as Off). + /// Determines what is shown for zero. + /// Determines what is shown for negative 1. /// Whether increments can be split in half. + /// Optional format string for the option screen. /// Whether to include this option in the preset or not. public ModdedNumberOption( string title, @@ -77,21 +77,20 @@ public class ModdedNumberOption : ModdedOption MiraNumberSuffixes suffixType, string? formatString = null, bool halfIncrements = false, - bool includeInPreset = true) : base(title, defaultValue, includeInPreset) + bool includeInPreset = true) + : base(title, defaultValue, includeInPreset) { Min = min; Max = max; Increment = increment; SuffixType = suffixType; ZeroWordValue = zeroBehavior; - NegativeWordValue = negativeBehavior; ShiftIncrement = halfIncrements; + NegativeWordValue = negativeBehavior; Value = Mathf.Clamp(defaultValue, min, max); - Data = ScriptableObject.CreateInstance(); - - var data = (FloatGameSetting)Data; + var data = ScriptableObject.CreateInstance(); data.Type = global::OptionTypes.Float; data.Title = StringName; data.Value = Value; @@ -107,6 +106,8 @@ public class ModdedNumberOption : ModdedOption data.ZeroIsInfinity = ZeroInfinity; data.SuffixType = (NumberSuffixes)SuffixType; data.OptionName = FloatOptionNames.Invalid; + + Data = data; } /// @@ -118,8 +119,8 @@ public class ModdedNumberOption : ModdedOption /// The maximum value. /// The increment. /// The suffix type. - /// Optional format string for the option screen. /// Whether zero is infinity or not. + /// Optional format string for the option screen. /// Whether to include this option in the preset or not. public ModdedNumberOption( string title, @@ -130,36 +131,9 @@ public class ModdedNumberOption : ModdedOption MiraNumberSuffixes suffixType, string? formatString = null, bool zeroInfinity = false, - bool includeInPreset = true) : base(title, defaultValue, includeInPreset) + bool includeInPreset = true) + : this(title, defaultValue, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f, includeInPreset) { - Min = min; - Max = max; - Increment = increment; - SuffixType = suffixType; - ZeroWordValue = zeroInfinity ? "∞" : "#"; - NegativeWordValue = "#"; - ShiftIncrement = increment != 1f; - - Value = Mathf.Clamp(defaultValue, min, max); - - Data = ScriptableObject.CreateInstance(); - - var data = (FloatGameSetting)Data; - data.Type = global::OptionTypes.Float; - data.Title = StringName; - data.Value = Value; - data.Increment = Increment; - data.ValidRange = new FloatRange(Min, Max); - data.FormatString = formatString ?? - (defaultValue.IsInteger() && - Increment.IsInteger() && - Value.IsInteger() && - Min.IsInteger() && - Max.IsInteger() ? "0" : "0.0"); - - data.ZeroIsInfinity = ZeroInfinity; - data.SuffixType = (NumberSuffixes)SuffixType; - data.OptionName = FloatOptionNames.Invalid; } /// diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 5fa0ceb..e2486ae 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -1,9 +1,10 @@ using System; +using System.Collections.Generic; using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; using MiraAPI.Translation; -using Reactor.Localization.Utilities; +using MiraAPI.Utilities; using Reactor.Networking.Rpc; using UnityEngine; @@ -15,8 +16,6 @@ namespace MiraAPI.GameOptions.OptionTypes; /// The value type. public abstract class ModdedOption : IModdedOption { - private IMiraPlugin? _parentMod; - /// public uint Id { get; } @@ -32,13 +31,13 @@ public abstract class ModdedOption : IModdedOption /// public IMiraPlugin? ParentMod { - get => _parentMod; + get; set { - if (_parentMod != null || value == null) return; - _parentMod = value; + if (field != null || value == null) return; + field = value; - var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue); + var entry = field.GetConfigFile().Bind(ConfigDefinition, DefaultValue); Value = entry.Value; } } @@ -46,16 +45,32 @@ public abstract class ModdedOption : IModdedOption /// /// Gets or sets the value of the option. /// - public T Value { get; protected set; } + public T Value + { + get => Getter != null ? Getter(_value) : _value; + protected set + { + if (Setter != null) + value = Setter(value, _value); + + _value = value; + } + } + + private T _value; /// /// Gets the default value of the option. /// public T DefaultValue { get; } - /// - /// Gets or sets the event that is invoked when the value of the option changes. - /// + /// + public Func? Getter { get; set; } + + /// + public Func? Setter { get; set; } + + /// public Action? ChangedEvent { get; set; } /// @@ -70,6 +85,33 @@ public abstract class ModdedOption : IModdedOption /// public ConfigDefinition? ConfigDefinition { get; set; } + /// + public AbstractOptionGroup ParentGroup { get; set; } + + /// + public virtual OptionNotifConfiguration Configuration => new(ParentGroup); + + /// + Delegate? IModdedOption.Getter + { + get => Getter; + set => Getter = value as Func; + } + + /// + Delegate? IModdedOption.Setter + { + get => Setter; + set => Setter = value as Func; + } + + /// + Delegate? IModdedOption.ChangedEvent + { + get => ChangedEvent; + set => ChangedEvent = value as Action; + } + /// /// Initializes a new instance of the class. /// @@ -83,7 +125,7 @@ public abstract class ModdedOption : IModdedOption DefaultValue = defaultValue; Value = defaultValue; StringName = MiraLocaleManager.GetOrCreateLocaleString(Title); - Visible = () => true; + Visible = DelegateUtilities.AlwaysTrue; IncludeInPreset = includeInPreset; } @@ -99,35 +141,36 @@ public abstract class ModdedOption : IModdedOption /// Whether to send the value to other players. public void SetValue(T newValue, bool sendRpc = true) { - var oldVal = Value; + var oldVal = _value; Value = newValue; - if (Value?.Equals(oldVal) == false) + if (!EqualityComparer.Default.Equals(_value, oldVal)) { - ChangedEvent?.Invoke(Value); + ChangedEvent?.Invoke(_value); } if (sendRpc && AmongUsClient.Instance.AmHost) { if (ParentMod?.GetConfigFile().TryGetEntry(ConfigDefinition, out var entry) == true) { - entry.Value = Value; + entry.Value = _value; } Rpc.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true); } - OnValueChanged(newValue); + OnValueChanged(_value); } /// - public void SaveToPreset(ConfigFile presetConfig, bool saveDefault=false) + public void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) { if (ConfigDefinition is null) { Error($"Attempted to save {Title} to preset, but ConfigDefinition is null."); return; } + Bind(presetConfig); presetConfig[ConfigDefinition].BoxedValue = saveDefault ? DefaultValue : Value; } @@ -151,6 +194,12 @@ public abstract class ModdedOption : IModdedOption } } + /// + public Type GetValueType() + { + return typeof(T); + } + /// public abstract float GetFloatData(); @@ -190,10 +239,4 @@ public abstract class ModdedOption : IModdedOption { return option.Value; } - - /// - public AbstractOptionGroup ParentGroup { get; set; } - - /// - public virtual OptionNotifConfiguration Configuration => new(ParentGroup); } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs new file mode 100644 index 0000000..6e9e9ad --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace MiraAPI.GameOptions.OptionTypes; + +/// +/// Represents a modded option list. +/// +/// The type of options. +public class ModdedOptionList : IModdedOptionList + where T : IModdedOption +{ + /// + public int Count { get; } + + /// + /// Gets the list of options. + /// + public IReadOnlyList Options { get; } + + /// + public Func? ConfigName { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The option list's length. + /// The option factory to instantiate the options from. + public ModdedOptionList(int count, Func optionFactory) + { + Count = count; + Options = [.. Enumerable.Range(0, Count).Select(optionFactory)]; + } + + /// + /// Initializes a new instance of the class. + /// + /// The options list. + public ModdedOptionList(IEnumerable options) + { + Count = options.Count(); + Options = [.. options]; + } + + /// + /// Indexes the option of type . + /// + /// The option's index. + /// option of type . + public T this[int idx] => Options[idx]; + + /// + public IEnumerator GetEnumerator() + { + return Options.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return Options.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)Options).GetEnumerator(); + } + + /// + IModdedOption IReadOnlyList.this[int index] => this[index]; +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs index 789368f..513aad0 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs @@ -1,6 +1,6 @@ using System; +using System.Collections.Generic; using System.Linq; -using Il2CppSystem.Collections.Generic; using MiraAPI.Networking; using MiraAPI.Translation; using MiraAPI.Utilities; @@ -10,7 +10,7 @@ using Object = UnityEngine.Object; namespace MiraAPI.GameOptions.OptionTypes; /// -/// An option for selecting an ingame player. It returns an index of the values list, NOT A PLAYER ID. To get a player out of this, index the list. +/// An option for selecting an in-game player. It returns an index of the values list, NOT A PLAYER ID. To get a player out of this, index the list. /// public class ModdedPlayerOption : ModdedOption { @@ -29,22 +29,29 @@ public class ModdedPlayerOption : ModdedOption /// public bool AllowNone { get; set; } + /// + /// Gets the player selected by this option. May be null if allowed to have None as an option. + /// + public NetworkedPlayerInfo? SelectedPlayer => Value == -1 ? null : Values[Value]; + /// /// Initializes a new instance of the class. /// /// The title of the option. /// Whether the option includes a none option. If None is selected, value will return -1. - public ModdedPlayerOption(string title, bool allowNone = true) : base(title, allowNone ? -1 : 0, false) + public ModdedPlayerOption(string title, bool allowNone = true) + : base(title, allowNone ? -1 : 0, false) { - Data = ScriptableObject.CreateInstance(); - var data = (PlayerSelectionGameSetting)Data; + Values = []; + AllowNone = allowNone; + Value = DefaultValue; + + var data = ScriptableObject.CreateInstance(); data.Title = StringName; data.Type = global::OptionTypes.Player; - Values = new List(); - AllowNone = allowNone; - Value = DefaultValue; + Data = data; } /// @@ -59,17 +66,16 @@ public class ModdedPlayerOption : ModdedOption playerOption.name = $"{ParentMod!.OptionsTitleText}.PlayerOption.{TranslationController.Instance.GetString(StringName)}"; - var filteredList = GetFilteredPlayers(); - Values = filteredList.ToIl2CppList(); + Values = GetFilteredPlayers(); playerOption.SetUpFromData(Data, 20); - playerOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + playerOption.OnValueChanged = (Action)ValueChanged; playerOption.Title = StringName; playerOption.TitleText.text = Title.Translate(); - playerOption.Values = Values; + playerOption.Values = Values.ToIl2CppList(); playerOption.Value = Value; - playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value); + playerOption.playerIndex = Values.FindIndex(p => p.PlayerId == Value); OptionBehaviour = playerOption; ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this); @@ -84,11 +90,16 @@ public class ModdedPlayerOption : ModdedOption /// Returns a player ran through the filter. /// /// A list of filtered players. - public System.Collections.Generic.List GetFilteredPlayers() + public List GetFilteredPlayers() { - return GameData.Instance.AllPlayers.ToArray() - .Where(x => PlayerFilter?.Invoke(x) ?? true) - .ToList(); + var players = GameData.Instance.AllPlayers.ToArray(); + + IEnumerable filtered = players; + + if (PlayerFilter != null) + filtered = players.Where(PlayerFilter); + + return [.. filtered.OrderBy(x => x.PlayerId)]; } /// diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs index 630a9ec..b1c9cc1 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -5,7 +5,6 @@ using System.Text; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Networking; using MiraAPI.Translation; -using Reactor.Localization.Utilities; using UnityEngine; using Object = UnityEngine.Object; @@ -26,19 +25,42 @@ public class ModdedStringOption : ModdedOption /// public StringNames[] StringNameValues { get; set; } + /// + /// 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; + StringNameValues = [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)]; + + var data = ScriptableObject.CreateInstance(); + data.Title = StringName; + data.Type = global::OptionTypes.String; + data.Values = StringNameValues; + data.Index = GetIndex(Value); + + Data = data; + } + /// /// Gets the string's index. /// /// The option's string value. /// The option's value in the index. - public int GetIndex(string value, int location) + public int GetIndex(string value) { if (Values.Contains(value)) { - return Values.ToList().IndexOf(value); + return Array.IndexOf(Values, value); } - Info($"Cannot find a valid index for {value} in {location}. Returning 0 instead. Values: {string.Join(", ", Values)}"); - return 0; + + Info($"Cannot find a valid index for {value}. Returning -1 instead. Values: {string.Join(", ", Values)}"); + return -1; } /// @@ -55,64 +77,43 @@ public class ModdedStringOption : ModdedOption var newVals = Values.ToList(); newVals.Insert(index, value); - Values = newVals.ToArray(); + Values = [.. newVals]; var newStrVals = StringNameValues.ToList(); newStrVals.Insert(index, MiraLocaleManager.GetOrCreateLocaleString(value)); - StringNameValues = newStrVals.ToArray(); + StringNameValues = [.. newStrVals]; } - 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 + if (Data == null) { Error("Data is null."); + return; } - } - /// - /// 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); - data.Index = GetIndex(Value, 3); + Warning("Data is valid"); + + if (OptionBehaviour == null) + { + Error("OptionBehaviour is null."); + return; + } + + var strOpt = OptionBehaviour.TryCast(); + + Warning("OptionBehaviour is valid"); + + if (strOpt == null) + { + Error("StringOption is null."); + return; + } + + strOpt.Values = StringNameValues; + strOpt.Value = GetIndex(Value); + Warning("StringOption is valid"); } /// @@ -123,12 +124,16 @@ public class ModdedStringOption : ModdedOption $"{ParentMod!.OptionsTitleText}.StringOption.{TranslationController.Instance.GetString(StringName)}"; stringOption.SetUpFromData(Data, 20); - stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + stringOption.OnValueChanged = (Action)ValueChanged; - // SetUpFromData method doesnt work correctly so we must set the values manually + // Changing the button text to make them more distinct from number options + stringOption.MinusBtn.ChangeButtonText("<"); + stringOption.PlusBtn.ChangeButtonText(">"); + + // SetUpFromData method doesn't 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); + stringOption.Value = GetIndex(Value); OptionBehaviour = stringOption; ModdedOptionsManager.CreatedStringOptions.TryAdd(stringOption, this); @@ -163,7 +168,7 @@ public class ModdedStringOption : ModdedOption /// protected override void OnValueChanged(string newValue) { - HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(GetIndex(newValue, 5)), false); + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(GetIndex(newValue)), false); if (!OptionBehaviour) { return; @@ -171,7 +176,7 @@ public class ModdedStringOption : ModdedOption if (OptionBehaviour is StringOption opt) { - opt.Value = GetIndex(newValue, 5); + opt.Value = GetIndex(newValue); } } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 348f853..a0759c6 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -11,19 +11,46 @@ namespace MiraAPI.GameOptions.OptionTypes; /// public class ModdedToggleOption : ModdedOption { + /// + /// Gets or sets the display value when the option is toggled on. + /// + public string OnValue { get; set; } + + /// + /// Gets or sets the display value when the option is toggled off. + /// + public string OffValue { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The option title. + /// The default value. + /// What the option displays when toggled on. + /// What the option displays when toggled off. + /// Whether to include this option in the preset or not. + public ModdedToggleOption(string title, bool defaultValue, string on, string off, bool includeInPreset = true) + : base(title, defaultValue, includeInPreset) + { + OnValue = on; + OffValue = off; + + var data = ScriptableObject.CreateInstance(); + data.Title = StringName; + data.Type = global::OptionTypes.Checkbox; + + Data = data; + } + /// /// Initializes a new instance of the class. /// /// The option title. /// The default value. /// Whether to include this option in the preset or not. - public ModdedToggleOption(string title, bool defaultValue, bool includeInPreset=true) : base(title, defaultValue, includeInPreset) + public ModdedToggleOption(string title, bool defaultValue, bool includeInPreset = true) + : this(title, defaultValue, "On", "Off", includeInPreset) { - Data = ScriptableObject.CreateInstance(); - - var data = (CheckboxGameSetting)Data; - data.Title = StringName; - data.Type = global::OptionTypes.Checkbox; } /// @@ -38,7 +65,7 @@ public class ModdedToggleOption : ModdedOption toggleOption.Title = StringName; toggleOption.TitleText.text = Title.Translate(); toggleOption.CheckMark.enabled = Value; - toggleOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + toggleOption.OnValueChanged = (Action)ValueChanged; OptionBehaviour = toggleOption; @@ -75,7 +102,7 @@ public class ModdedToggleOption : ModdedOption ModdedOptionsManager.AddSettingsChangeMessage( HudManager.Instance.Notifier, StringName, - newValue ? "On" : "Off", + (newValue ? OnValue : OffValue).Translate(), Configuration.PopUpTextColor, Configuration.PopUpIconTmp, false); diff --git a/MiraAPI/GlobalUsings.cs b/MiraAPI/GlobalUsings.cs new file mode 100644 index 0000000..6a7d395 --- /dev/null +++ b/MiraAPI/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using static Reactor.Utilities.Logger; +global using CppCollections = Il2CppSystem.Collections.Generic; +global using CppIEnumerator = Il2CppSystem.Collections.IEnumerator; diff --git a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs index b708148..89347e0 100644 --- a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs +++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.Linq; using BepInEx.Unity.IL2CPP.Utils.Collections; using Il2CppInterop.Runtime.Attributes; @@ -10,47 +11,62 @@ using UnityEngine; namespace MiraAPI.HnsReimplemented; +/// +/// A Unity script designed to help manage the HUD during HnS mode. +/// +/// The pointer of this instance's equivalent in the Il2Cpp domain. [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity Convention.")] public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) { + /// + /// Gets the instance of the HnS hud helper. + /// public static HideAndSeekHudHelper Instance { get; private set; } - public AudioClip FinalHideAlertSfx; + private AudioClip finalHideAlertSfx; + private AudioClip finalHideCountdownSfx; - public AudioClip FinalHideCountdownSfx; + // ReSharper disable once NotAccessedField.Local (Justification: Read above.) + [SuppressMessage("Style", "IDE0052:Remove unread private members", Justification = "Ignore for now; tentative code.")] + private AudioClip taskFinishedSound; - private AudioClip TaskFinishedSound; - - private const int SECONDS_TO_BEEP = 10; - - private const float SECONDS_TO_SET_DIRTY = 1f; - - private HideAndSeekTimerBar timerBar; + // private const int SECONDS_TO_BEEP = 10; + // private const float SECONDS_TO_SET_DIRTY = 1f; 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 Coroutine? beepCoroutine; + private HideAndSeekTimerBar timerBar; + private float taskDirtyTimer; + + /// + /// Gets or sets the hide countdown timer. + /// + public float HideCountdown { get; set; } + + /// + /// Gets a value indicating whether the game has reached the final phase. + /// + public bool IsFinalCountdown => currentHideTime <= 0f; private void Awake() { Instance = this; } - public bool IsFinalCountdown - { - get { return currentHideTime <= 0f; } - } - + /// + /// Checks to see if the player can use the Seeker admin. + /// + /// The player to check. + /// true if the player can use the admin ability. public bool SeekerAdminMapEnabled(PlayerControl player) { - int item = Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor); + var 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) || @@ -59,6 +75,10 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) (GameData.Instance.PlayerCount - 1) / 3)); } + /// + /// An event that is called when a task is completed. + /// + /// The amount of time to deduct from the timer. public void OnTaskComplete(float timeDeduction) { if (timerBar != null) @@ -69,24 +89,36 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) AdjustEscapeTimer(timeDeduction, true); } - public float GetTotalRoundTime() + /// + /// Gets the match duration. + /// + /// The match duration in seconds. + public static float GetTotalRoundTime() { - float escapeTime = OptionGroupSingleton.Instance.HidingTime.Value; - float finalCountdownTime = OptionGroupSingleton.Instance.FinalHideTime.Value; + var escapeTime = OptionGroupSingleton.Instance.HidingTime.Value; + var finalCountdownTime = OptionGroupSingleton.Instance.FinalHideTime.Value; return escapeTime + finalCountdownTime; } + /// + /// Gets the amount of total time remaining in the current round. + /// + /// The remaining time in seconds. public float GetTotalTimeRemaining() { return currentHideTime + currentFinalHideTime; } + /// + /// Gets how much time has passed since the start of the round. + /// + /// The elapsed time in seconds. public float GetRoundTimeElapsed() { return GetTotalRoundTime() - GetTotalTimeRemaining(); } - public void Start() + private void Start() { totalHideTime = OptionGroupSingleton.Instance.HidingTime.Value; currentHideTime = totalHideTime; @@ -94,22 +126,22 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) currentFinalHideTime = totalFinalHideTime; if (timerBar != null) { - Object.Destroy(timerBar); + Destroy(timerBar); } - FinalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX; - FinalHideCountdownSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideCountdownSFX; - TaskFinishedSound = GameManagerCreator.Instance.HideAndSeekManagerPrefab.TaskFinishedSound; - timerBar = Object.Instantiate( + finalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX; + finalHideCountdownSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideCountdownSFX; + taskFinishedSound = GameManagerCreator.Instance.HideAndSeekManagerPrefab.TaskFinishedSound; + timerBar = Instantiate( GameManagerCreator.Instance.HideAndSeekManagerPrefab.TimerBarPrefab, HudManager.Instance.transform.parent); } - public void OnGameEnd() + private void OnGameEnd() { if (timerBar != null) { - Object.Destroy(timerBar.gameObject); + Destroy(timerBar.gameObject); } if (beepCoroutine != null) @@ -119,68 +151,74 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) beepCoroutine = null!; } - private float taskDirtyTimer; - public float HideCountdown { get; set; } - public void LateUpdate() + private void LateUpdate() { if (!HudManager.InstanceExists) { return; } - if (HideCountdown > 0f && taskDirtyTimer > 0.25f) + + if (!(HideCountdown > 0f) || !(taskDirtyTimer > 0.25f)) return; + var num = taskDirtyTimer; + taskDirtyTimer = 0f; + if (!PlayerControl.LocalPlayer) { - 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()); + HudManager.Instance.TaskPanel.SetTaskText(string.Empty); + return; } + + var data = PlayerControl.LocalPlayer.Data; + if (data == null) + { + return; + } + + var 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 (var i = 0; i < PlayerControl.LocalPlayer.myTasks.Count; i++) + { + var 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() + + private void FixedUpdate() { secondsSinceLastSetDirty += Time.fixedDeltaTime; + if (IsFinalCountdown) { AdjustFinalEscapeTimer(Time.fixedDeltaTime); @@ -190,29 +228,29 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) AdjustEscapeTimer(Time.fixedDeltaTime, false); } - public void OnDestroy() + private void OnDestroy() { if (timerBar != null) { - Object.Destroy(timerBar); + Destroy(timerBar); } } private void OnFinalCountdownTriggered() { - foreach (PlayerControl playerControl in PlayerControl.AllPlayerControls) + foreach (var playerControl in PlayerControl.AllPlayerControls) { if (!playerControl.Data.Role.IsImpostor && !playerControl.Data.IsDead) { playerControl.ClearTasks(); - PlayerTask.GetOrCreateTask(playerControl, 0).Text = + PlayerTask.GetOrCreateTask(playerControl).Text = DestroyableSingleton.Instance.GetString(StringNames.HideActionButton); } } if (!PlayerControl.LocalPlayer.Data.IsDead && Minigame.Instance != null) { - Minigame instance = Minigame.Instance; + var instance = Minigame.Instance; if (instance != null) { instance.ForceClose(); @@ -220,13 +258,13 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) } timerBar.StartFinalHide(); - SoundManager.Instance.PlaySound(FinalHideAlertSfx, false, 1f, null); + SoundManager.Instance.PlaySound(finalHideAlertSfx, false); DestroyableSingleton.Instance.SetAlertOverlay(true); } private void AdjustEscapeTimer(float timeDeduction, bool forceDirty) { - float num = currentHideTime; + var num = currentHideTime; currentHideTime -= timeDeduction; currentHideTime = Mathf.Max(currentHideTime, 0f); if (currentHideTime <= 10f && beepCoroutine == null) @@ -251,22 +289,20 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) { while (!IsFinalCountdown) { - float num = currentHideTime / 10f; - float pitch = 1.5f - num / 2f; - SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch, null); + var num = currentHideTime / 10f; + var pitch = 1.5f - num / 2f; + SoundManager.Instance.PlaySoundImmediate(finalHideCountdownSfx, false, 1f, pitch); 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); + var num2 = currentFinalHideTime / 10f; + var pitch2 = 1.5f - num2 / 2f; + SoundManager.Instance.PlaySoundImmediate(finalHideCountdownSfx, false, 1f, pitch2); yield return new WaitForSeconds(1f); } - - yield break; } private void AdjustFinalEscapeTimer(float timeDeduction) @@ -280,6 +316,10 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) } } + /// + /// Checks to see if all timers have expired. + /// + /// true if all times have expired. public bool AllTimersExpired() { return currentHideTime <= 0f && currentFinalHideTime <= 0f; diff --git a/MiraAPI/HnsReimplemented/HnsDangerMeter.cs b/MiraAPI/HnsReimplemented/HnsDangerMeter.cs index d36c198..0309e16 100644 --- a/MiraAPI/HnsReimplemented/HnsDangerMeter.cs +++ b/MiraAPI/HnsReimplemented/HnsDangerMeter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using MiraAPI.GameOptions; using MiraAPI.HnsReimplemented.Options; @@ -8,19 +9,22 @@ using UnityEngine; namespace MiraAPI.HnsReimplemented; +/// +/// A Unity script designed to mimic the base game's player danger meter during Hide and Seek. +/// +/// The pointer of this instance's equivalent in the Il2Cpp domain. [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity Convention.")] public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) { - public static HnsDangerMeter Instance; - - private void Awake() - { - Instance = this; - } + /// + /// Gets the instance of the danger meter. + /// + public static HnsDangerMeter Instance { get; private set; } private DangerMeter dangerMeter; - private List impostors = []; + private List? impostors = []; private float scaryMusicDistance; @@ -34,9 +38,14 @@ public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) private float firstCrossfadeCountdown; - public void FixedUpdate() + private void Awake() { - PlayerControl localPlayer = PlayerControl.LocalPlayer; + Instance = this; + } + + private void FixedUpdate() + { + var localPlayer = PlayerControl.LocalPlayer; if (impostors == null || localPlayer == null) { return; @@ -47,16 +56,15 @@ public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) return; } - float num = float.MaxValue; - foreach (PlayerControl playerControl in impostors) + var num = float.MaxValue; + foreach (var playerControl in impostors) { - if (!(playerControl == null)) + if (playerControl == null) + continue; + var sqrMagnitude = (playerControl.transform.position - localPlayer.transform.position).sqrMagnitude; + if (sqrMagnitude < scaryMusicDistance && num > sqrMagnitude) { - float sqrMagnitude = (playerControl.transform.position - localPlayer.transform.position).sqrMagnitude; - if (sqrMagnitude < scaryMusicDistance && num > sqrMagnitude) - { - num = sqrMagnitude; - } + num = sqrMagnitude; } } @@ -71,7 +79,7 @@ public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) { firstMusicActivation = false; firstCrossfadeCountdown = 3f; - HnsMusicHandler.Instance.SetMusicCrossfadeSpeed(0.6f); + HnsMusicHandler.Instance.SetMusicCrossFadeSpeed(0.6f); } if (firstCrossfadeCountdown > 0f) @@ -79,7 +87,7 @@ public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) firstCrossfadeCountdown -= Time.deltaTime; if (firstCrossfadeCountdown <= 0f) { - HnsMusicHandler.Instance.SetMusicCrossfadeSpeed(5f); + HnsMusicHandler.Instance.SetMusicCrossFadeSpeed(5f); } } @@ -94,7 +102,7 @@ public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) private void UpdateDangerMusic() { - PlayerControl localPlayer = PlayerControl.LocalPlayer; + var localPlayer = PlayerControl.LocalPlayer; if (localPlayer != null && localPlayer.Data != null && localPlayer.Data.IsDead) { HnsMusicHandler.Instance.SetTaskState(false); @@ -115,6 +123,9 @@ public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) dangerMeter.SetDangerValue(dangerLevel1, dangerLevel2); } + /// + /// An event that is executed when the game starts. + /// public void OnGameStart() { firstMusicActivation = true; @@ -124,19 +135,15 @@ public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) dangerMeter.gameObject.SetActive(true); } - impostors = Helpers.GetAlivePlayers().Where(x => x.Data.Role.IsImpostor).ToList(); + impostors = [.. Helpers.GetAlivePlayers().Where(x => x.Data.Role.IsImpostor)]; 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; - } + scaryMusicDistance = 55f * baseSpeed; + veryScaryMusicDistance = 15f * baseSpeed; + if (scaryMusicDistance >= veryScaryMusicDistance) return; + var num = veryScaryMusicDistance; + var num2 = scaryMusicDistance; + scaryMusicDistance = num; + veryScaryMusicDistance = num2; } } diff --git a/MiraAPI/HnsReimplemented/HnsMusicHandler.cs b/MiraAPI/HnsReimplemented/HnsMusicHandler.cs index e3b7d68..76c4a25 100644 --- a/MiraAPI/HnsReimplemented/HnsMusicHandler.cs +++ b/MiraAPI/HnsReimplemented/HnsMusicHandler.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Amongus.GameModes.HideAndSeek; using MiraAPI.GameOptions; using MiraAPI.HnsReimplemented.Options; @@ -7,15 +8,38 @@ using UnityEngine; namespace MiraAPI.HnsReimplemented; +/// +/// A Unity script designed to handle the music during Hide and Seek. +/// +/// The pointer of this instance's equivalent in the Il2Cpp domain. [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) { - public static HnsMusicHandler Instance; + /// + /// Gets the instance of the music handler. + /// + public static HnsMusicHandler Instance { get; private set; } - private void Awake() + private static readonly Dictionary MusicNames = new() { - Instance = this; - } + { + LogicHnSMusic.HideAndSeekMusicTrack.Normal, + "HnS_Music_Normal" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.Task, + "HnS_Music_Task" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1, + "HnS_Music_DangerLevel1" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2, + "HnS_Music_DangerLevel2" + }, + }; private HideAndSeekMusicCollection musicCollection; @@ -41,18 +65,26 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) private float musicLerpSpeed = 5f; - public void Start() + private void Awake() + { + Instance = this; + } + + private void Start() { musicCollection = GameManagerCreator.Instance.HideAndSeekManagerPrefab.MusicCollection; } + /// + /// An event that is executed when the round starts. + /// public void OnGameStart() { InitMusic(); ResetMusic(); } - public void OnDestroy() + private void OnDestroy() { ResetMusic(); } @@ -62,7 +94,7 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) if (normalSource == null) { normalSource = - SoundManager.Instance.GetNamedSfxSource(musicNames[LogicHnSMusic.HideAndSeekMusicTrack.Normal]); + SoundManager.Instance.GetNamedSfxSource(MusicNames[LogicHnSMusic.HideAndSeekMusicTrack.Normal]); } normalSource.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; @@ -71,7 +103,7 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) if (taskSource == null) { taskSource = - SoundManager.Instance.GetNamedSfxSource(musicNames[LogicHnSMusic.HideAndSeekMusicTrack.Task]); + SoundManager.Instance.GetNamedSfxSource(MusicNames[LogicHnSMusic.HideAndSeekMusicTrack.Task]); } taskSource.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; @@ -82,7 +114,7 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) { dangerLevel1Source = SoundManager.Instance.GetNamedSfxSource( - musicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1]); + MusicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1]); } dangerLevel1Source.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; @@ -93,7 +125,7 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) { dangerLevel2Source = SoundManager.Instance.GetNamedSfxSource( - musicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2]); + MusicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2]); } dangerLevel2Source.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; @@ -107,28 +139,33 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) SyncMusic(); } + /// + /// Gets the music with the intro. + /// public void StartMusicWithIntro() { - if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + if (!PlayerControl.LocalPlayer.Data.Role.IsImpostor) return; + var clip = OptionGroupSingleton.Instance.HidingTime.Value <= 180f + ? musicCollection.ImpostorShortMusic + : musicCollection.ImpostorLongMusic; + if (AprilFoolsMode.ShouldHorseAround()) { - 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); + clip = musicCollection.ImpostorRanchMusic; } + + SoundManager.Instance.PlaySound(clip, true, 1f, SoundManager.Instance.MusicChannel); } - public void SetTaskState(bool isDoingTask) + /// + /// Sets the current task state to alter music behaviour. + /// + /// The new value of the state. + public void SetTaskState(bool taskState) { - isDoingTask = isDoingTask; + isDoingTask = taskState; } - public void FixedUpdate() + private void FixedUpdate() { if (normalSource == null || taskSource == null || dangerLevel1Source == null || dangerLevel2Source == null) @@ -167,16 +204,28 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) lastMusicSyncTime = Time.unscaledTime; } + /// + /// Resets the music currently playing. + /// public void ResetMusic() { SetMusicValues(0f, 0f); } - public void SetMusicCrossfadeSpeed(float lerpSpeed) + /// + /// Sets the music crossfade speed. + /// + /// The new speed. + public void SetMusicCrossFadeSpeed(float lerpSpeed) { musicLerpSpeed = lerpSpeed; } + /// + /// Sets the current music value based on the danger level. + /// + /// The first danger level. + /// The second danger level. public void SetMusicValues(float dangerLevel1, float dangerLevel2) { if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.Data.Role.IsImpostor) @@ -190,8 +239,8 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) return; } - normalVolume = (isDoingTask ? 0f : 1f); - taskVolume = (isDoingTask ? 1f : 0f); + normalVolume = isDoingTask ? 0f : 1f; + taskVolume = isDoingTask ? 1f : 0f; dangerLevel1Volume = 0f; dangerLevel2Volume = 0f; if (dangerLevel1 > 0f) @@ -213,25 +262,4 @@ public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) 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 index cf1a7c6..949cf54 100644 --- a/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs +++ b/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs @@ -5,12 +5,18 @@ using MiraAPI.Utilities; namespace MiraAPI.HnsReimplemented.Options; -/// +/// +/// Gets or sets the crewmate options for Hide and Seek. +/// +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated via Activator.CreateInstance.) public class HnsCrewmateOptions : AbstractOptionGroup { /// public override string GroupName => "Crewmates"; + /// + /// Gets or sets the player speed. + /// public ModdedNumberOption PlayerSpeed { get; set; } = new( "Player Speed", 1f, @@ -22,6 +28,9 @@ public class HnsCrewmateOptions : AbstractOptionGroup MiraNumberSuffixes.Multiplier, "0.00"); + /// + /// Gets or sets the hiding time. + /// public ModdedNumberOption HidingTime { get; set; } = new( "Hiding Time", 200, @@ -32,6 +41,9 @@ public class HnsCrewmateOptions : AbstractOptionGroup "#", MiraNumberSuffixes.Seconds); + /// + /// Gets or sets the limits of a crewmate's vision. + /// public ModdedNumberOption CrewmateVision { get; set; } = new( "Crewmate Vision", 0.6f, @@ -43,33 +55,16 @@ public class HnsCrewmateOptions : AbstractOptionGroup 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, - }; - + /// + /// Gets or sets a flag that toggles flashlight vision. + /// public ModdedToggleOption FlashlightMode { get; set; } = new( "Flashlight Mode", true); + /// + /// Gets or sets the range of a crewmate's flashlight. + /// public ModdedNumberOption CrewmateFlashlightSize { get; set; } = new( "Crewmate Flashlight Size", 0.35f, @@ -83,6 +78,38 @@ public class HnsCrewmateOptions : AbstractOptionGroup Visible = () => OptionGroupSingleton.Instance.FlashlightMode.Value, }; + /// + /// Gets or sets the maximum number of times a crewmate can vent. + /// + public ModdedNumberOption MaxVentUses { get; set; } = new( + "Max Vent Uses", + 1, + 0, + 5, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + /// + /// Gets or sets the maximum amount of time a crewmate can spend inside a vent. + /// + public ModdedNumberOption MaxTimeInVent { get; set; } = new( + "Max Time in Vent", + 3, + 1, + 10, + 1, + "#", + "#", + MiraNumberSuffixes.Seconds) + { + Visible = () => (int)OptionGroupSingleton.Instance.MaxVentUses.Value > 0, + }; + + /// + /// Gets or sets the visibility of player names. + /// public ModdedToggleOption ShowNames { get; set; } = new( "Show Names", false); diff --git a/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs index 009b248..ab6d393 100644 --- a/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs +++ b/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs @@ -5,12 +5,18 @@ using MiraAPI.Utilities; namespace MiraAPI.HnsReimplemented.Options; -/// +/// +/// Gets or sets the Final Hide options for Hide and Seek. +/// +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated via Activator.CreateInstance.) public class HnsFinalHideOptions : AbstractOptionGroup { /// public override string GroupName => "Final Hide"; + /// + /// Gets or sets the duration of the Final Hide phase. + /// public ModdedNumberOption FinalHideTime { get; set; } = new( "Hiding Time", 50, @@ -21,10 +27,9 @@ public class HnsFinalHideOptions : AbstractOptionGroup "#", MiraNumberSuffixes.Seconds); - public ModdedToggleOption FinalHidePings { get; set; } = new( - "Final Hide Pings", - true); - + /// + /// Gets or sets the Seeker's speed when the Final Hide phase begins. + /// public ModdedNumberOption FinalHideImpostorSpeed { get; set; } = new( "Final Hide Impostor Speed", 1.2f, @@ -36,6 +41,16 @@ public class HnsFinalHideOptions : AbstractOptionGroup MiraNumberSuffixes.Multiplier, "0.00"); + /// + /// Gets or sets whether the Seeker sees the Final hide player pings. + /// + public ModdedToggleOption FinalHidePings { get; set; } = new( + "Final Hide Pings", + true); + + /// + /// Gets or sets the interval between each ping. + /// public ModdedNumberOption PingInterval { get; set; } = new( "Ping Interval", 6, @@ -46,6 +61,9 @@ public class HnsFinalHideOptions : AbstractOptionGroup "#", MiraNumberSuffixes.Seconds); + /// + /// Gets or sets whether the Seeker can see players on an admin map. + /// 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 index c0c61b5..15da3ff 100644 --- a/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs +++ b/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs @@ -5,15 +5,24 @@ using MiraAPI.Utilities; namespace MiraAPI.HnsReimplemented.Options; -/// +/// +/// Gets or sets the seeker settings on Hide and Seek. +/// +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated via Activator.CreateInstance.) public class HnsImpostorOptions : AbstractOptionGroup { /// public override string GroupName => "Impostors"; + /// + /// Gets or sets the player who will be the seeker. + /// public ModdedPlayerOption SelectedSeeker { get; set; } = new( "Forced Impostor"); + /// + /// Gets or sets the range of a seeker's vision. + /// public ModdedNumberOption ImpostorVision { get; set; } = new( "Impostor Vision", 0.6f, @@ -25,6 +34,9 @@ public class HnsImpostorOptions : AbstractOptionGroup MiraNumberSuffixes.Multiplier, "0.00"); + /// + /// Gets or sets the range of a seeker's flashlight. + /// public ModdedNumberOption ImpostorFlashlightSize { get; set; } = new( "Impostor Flashlight Size", 0.35f, diff --git a/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs index 7be2864..dddd704 100644 --- a/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs +++ b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs @@ -5,12 +5,18 @@ using MiraAPI.Utilities; namespace MiraAPI.HnsReimplemented.Options; -/// +/// +/// Gets or sets the task options for Hide and Seek. +/// +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated via Activator.CreateInstance.) public class HnsTaskOptions : AbstractOptionGroup { /// public override string GroupName => "Tasks"; + /// + /// Gets or sets the umber of common tasks. + /// public ModdedNumberOption CommonTasks { get; set; } = new( "# Common Tasks", 1, @@ -21,6 +27,9 @@ public class HnsTaskOptions : AbstractOptionGroup "#", MiraNumberSuffixes.None); + /// + /// Gets or sets the number of long tasks. + /// public ModdedNumberOption LongTasks { get; set; } = new( "# Long Tasks", 1, @@ -31,6 +40,9 @@ public class HnsTaskOptions : AbstractOptionGroup "#", MiraNumberSuffixes.None); + /// + /// Gets or sets the number of short tasks. + /// public ModdedNumberOption ShortTasks { get; set; } = new( "# Short Tasks", 2, diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index d5d8956..ce91ad5 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -139,10 +139,7 @@ public abstract class CustomActionButton /// The parent of the button. public virtual void CreateButton(Transform parent) { - if (Button) - { - return; - } + if (Button) return; UsesLeft = MaxUses; Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown; @@ -152,19 +149,15 @@ public abstract class CustomActionButton Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent); Button.name = Name + "Button"; Button.OverrideText(Name.Translate().ToUpperInvariant()); + Object.Destroy(Button.glyph.GetComponent()); + Object.Destroy(Button.glyph.GetComponent()); Button.graphic.sprite = Sprite.LoadAsset(); Button.SetUsesRemaining(MaxUses); - if (MaxUses <= 0) - { - Button.SetInfiniteUses(); - } + if (MaxUses <= 0) Button.SetInfiniteUses(); - if (TextOutlineColor != Color.clear) - { - SetTextOutline(TextOutlineColor); - } + if (TextOutlineColor != Color.clear) SetTextOutline(TextOutlineColor); var pb = Button.GetComponent(); pb.OnClick = new Button.ButtonClickedEvent(); @@ -173,10 +166,7 @@ public abstract class CustomActionButton // Invoke the generic button click event. var genericEvent = new MiraButtonClickEvent(this); MiraEventManager.InvokeEvent(genericEvent); - if (genericEvent.IsCancelled) - { - MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this)); - } + if (genericEvent.IsCancelled) MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this)); // Invoke the button click event for specific button. var eventType = CustomButtonManager.ButtonEventTypes[GetType()]; @@ -191,72 +181,47 @@ public abstract class CustomActionButton if (specificInvoked) { - if (!@event.IsCancelled) - { - ClickHandler(); - } + if (!@event.IsCancelled) ClickHandler(); } else { - if (!genericEvent.IsCancelled) - { - ClickHandler(); - } + if (!genericEvent.IsCancelled) ClickHandler(); } })); - if (Keybind != null) + if (Keybind == null) return; + Keybind.RemoveOnActivate(OnKeybind); + Keybind.OnActivate(OnKeybind); + + KeybindIcon = Helpers.CreateKeybindIcon(Button.gameObject, Keybind.CurrentKey, new Vector3(0.4f, 0.45f, -9f)); + KeybindText = KeybindIcon.transform.GetChild(0).GetComponent(); + KeybindText.fontSizeMax = KeybindText.fontSize; + KeybindText.fontSizeMin = KeybindText.fontSize * 0.5f; + KeybindText.enableAutoSizing = true; + Button.usesRemainingSprite.transform.localPosition = new Vector3(-0.341f, 0.45f, -0.1f); + UpdateKeybindIcon(); + } + + private void OnKeybind() + { + if (Button == null || !Enabled(PlayerControl.LocalPlayer.Data.Role)) return; + + Button.GetComponent().OnClick.Invoke(); + } + + internal void UpdateKeybindIcon() + { + if (Keybind == null || KeybindText == null) return; + + var text = ActiveInputManager.currentControlType switch { - Keybind.OnActivate(() => - { - if (Enabled(PlayerControl.LocalPlayer.Data.Role)) - { - // Invoke the generic button click event. - var genericEvent = new MiraButtonClickEvent(this); - MiraEventManager.InvokeEvent(genericEvent); - if (genericEvent.IsCancelled) - { - MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this)); - } + ActiveInputManager.InputType.Keyboard when Keybind.CurrentKey != KeyboardKeyCode.None => Keybind.CurrentKey.ToString(), + ActiveInputManager.InputType.Joystick => Keybind.CurrentControllerButton, + _ => string.Empty, + }; - // Invoke the button click event for specific button. - var eventType = CustomButtonManager.ButtonEventTypes[GetType()]; - var @event = (MiraCancelableEvent)Activator.CreateInstance(eventType, this, genericEvent)!; - var specificInvoked = MiraEventManager.InvokeEvent(@event, eventType); - if (@event.IsCancelled) - { - var cancelEventType = CustomButtonManager.ButtonCancelledEventTypes[GetType()]; - var cancelEvent = (MiraEvent)Activator.CreateInstance(cancelEventType, this)!; - MiraEventManager.InvokeEvent(cancelEvent, cancelEventType); - } - - if (specificInvoked) - { - if (!@event.IsCancelled) - { - ClickHandler(); - } - } - else - { - if (!genericEvent.IsCancelled) - { - ClickHandler(); - } - } - } - }); - - KeybindIcon = - Helpers.CreateKeybindIcon( - Button.gameObject, - Keybind.CurrentKey, - new Vector3(0.4f, 0.45f, -9f) - ); - KeybindText = KeybindIcon.transform.GetChild(0).GetComponent(); - HudManagerPatches.ModdedKeybindIcons.Add(KeybindText); - Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f); - } + KeybindText.text = text; + KeybindIcon?.SetActive(text.Length > 0 && LocalSettingsTabSingleton.Instance.ShowKeybinds.Value); } /// @@ -266,22 +231,13 @@ public abstract class CustomActionButton /// Whether the button's position should change in-game. public virtual void SetButtonLocation(ButtonLocation location, bool moveButton = true) { - if (!HudManager.InstanceExists || Button == null) - { - return; - } + if (!HudManager.InstanceExists || Button == null) return; Location = location; - if (!moveButton) - { - return; - } + if (!moveButton) return; - if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null) - { - return; - } + if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null) return; switch (location) { @@ -298,6 +254,8 @@ public abstract class CustomActionButton case ButtonLocation.BottomRight: Button.transform.SetParent(HudManagerPatches.BottomRight); break; + default: + throw new ArgumentOutOfRangeException(nameof(location), location, null); } } @@ -307,10 +265,7 @@ public abstract class CustomActionButton public virtual void ResetCooldownAndOrEffect() { Timer = Cooldown; - if (EffectActive) - { - OnEffectEnd(); - } + if (EffectActive) OnEffectEnd(); EffectActive = false; } @@ -330,10 +285,7 @@ public abstract class CustomActionButton /// The new to override with. public virtual void OverrideSprite(Sprite sprite) { - if (Button != null) - { - Button.graphic.sprite = sprite; - } + if (Button != null) Button.graphic.sprite = sprite; } /// @@ -390,10 +342,7 @@ public abstract class CustomActionButton UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue); Button?.SetUsesRemaining(UsesLeft); - if (Button != null) - { - Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white; - } + if (Button != null) Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white; } /// @@ -448,7 +397,10 @@ public abstract class CustomActionButton /// Always by default. /// /// Can the effect be canceled. - public virtual bool IsEffectCancellable() => false; + public virtual bool IsEffectCancellable() + { + return false; + } /// /// When the button is usable, this method is called to determine if the button can be clicked. @@ -468,8 +420,7 @@ public abstract class CustomActionButton /// A value that represents whether the button should light up or not. public virtual bool CanUse() { - return PlayerControl.LocalPlayer.moveable && - ((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0))); + return PlayerControl.LocalPlayer.moveable && ((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0))); } /// @@ -497,10 +448,7 @@ public abstract class CustomActionButton return; } - if (!CanClick()) - { - return; - } + if (!CanClick()) return; if (LimitedUses) { @@ -529,20 +477,9 @@ public abstract class CustomActionButton /// The local PlayerControl. public virtual void FixedUpdateHandler(PlayerControl playerControl) { - if (Keybind != null && KeybindText != null) - { - KeybindText.text = Keybind.CurrentKey.ToString(); - KeybindIcon?.SetActive(ActiveInputManager.currentControlType is ActiveInputManager.InputType.Keyboard && - LocalSettingsTabSingleton.Instance.ShowKeybinds.Value && - Keybind.CurrentKey != KeyboardKeyCode.None); - } - if (Timer >= 0) { - if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent)) - { - Timer -= Time.deltaTime; - } + if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent)) Timer -= Time.deltaTime; } else if (HasEffect && EffectActive && EffectDuration > 0) { @@ -554,13 +491,9 @@ public abstract class CustomActionButton if (Button) { if (CanUse()) - { Button!.SetEnabled(); - } else - { Button!.SetDisabled(); - } if (EffectActive && EffectDuration > 0) { @@ -583,7 +516,8 @@ public abstract class CustomActionButton /// Custom action button that has a target . /// /// The type of the target object. -public abstract class CustomActionButton : CustomActionButton where T : MonoBehaviour +public abstract class CustomActionButton : CustomActionButton + where T : MonoBehaviour { /// /// Gets or sets the target of the button. @@ -621,10 +555,7 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB public override bool CanUse() { var newTarget = GetTarget(); - if (newTarget != Target) - { - SetOutline(false); - } + if (newTarget != Target) SetOutline(false); Target = IsTargetValid(newTarget) ? newTarget : null; SetOutline(true); @@ -643,7 +574,7 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB /// public virtual void ResetTarget() { - Target = null; SetOutline(false); + Target = null; } } diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs index b1ddbce..a532f8e 100644 --- a/MiraAPI/Hud/CustomButtonManager.cs +++ b/MiraAPI/Hud/CustomButtonManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using MiraAPI.Events.Mira; using MiraAPI.PluginLoading; @@ -20,21 +21,21 @@ public static class CustomButtonManager /// /// Gets a list of all registered button-specific s. /// - public static ReadOnlyDictionary EventTypes { get; internal set; } = new( - new Dictionary - { }); + public static ReadOnlyDictionary EventTypes { get; internal set; } = new(new Dictionary()); /// /// Gets a list of all registered button-specific s. /// - public static ReadOnlyDictionary CancelledEventTypes { get; internal set; } = new( - new Dictionary - { }); + public static ReadOnlyDictionary CancelledEventTypes { get; internal set; } = new(new Dictionary()); internal static readonly List CustomButtons = []; internal static readonly Dictionary ButtonEventTypes = []; internal static readonly Dictionary ButtonCancelledEventTypes = []; + [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 RegisterButton(Type buttonType, MiraPluginInfo pluginInfo) { if (!buttonType.IsAssignableTo(typeof(CustomActionButton)) || Activator.CreateInstance(buttonType) is not CustomActionButton button) @@ -45,9 +46,7 @@ public static class CustomButtonManager CustomButtons.Add(button); pluginInfo.InternalButtons.Add(button); typeof(CustomButtonSingleton<>).MakeGenericType(buttonType) -#pragma warning disable S3011 - .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! -#pragma warning restore S3011 + .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! // Suppression message refers to this .SetValue(null, button); ButtonEventTypes.Add(buttonType, typeof(MiraButtonClickEvent<>).MakeGenericType(buttonType)); diff --git a/MiraAPI/Hud/CustomButtonSingleton.cs b/MiraAPI/Hud/CustomButtonSingleton.cs index 957445b..d3861da 100644 --- a/MiraAPI/Hud/CustomButtonSingleton.cs +++ b/MiraAPI/Hud/CustomButtonSingleton.cs @@ -7,7 +7,8 @@ namespace MiraAPI.Hud; /// A utility class to get the instance of a . /// /// The type of the button you are trying to access. -public static class CustomButtonSingleton where T : CustomActionButton +public static class CustomButtonSingleton + where T : CustomActionButton { private static T? _instance; diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs new file mode 100644 index 0000000..50e2ff5 --- /dev/null +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using MiraAPI.Patches.Stubs; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.Hud; + +/// +/// Multi-select using the as a base. +/// +/// The type of object each entry represents. +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity convention.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")] +public abstract class CustomMultiSelectMenu : CustomPhoneMenu.MenuEntry> + where TEntry : class +{ + private readonly List selectedEntries = []; + private int totalSelections; + private bool shouldConfirm; + private bool canRepeat; + + private LoadableAsset? hoverSelectSprite; + private LoadableAsset? hoverDeselectSprite; + private Color? activeColor; + private Color? hoverSelectColor; + private Color? hoverDeselectColor; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + [SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "Unity convention.")] + public UiElement confirmButton; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + private Action>? onSelection; + + /// + /// Menu Entry used when multiple entries are at play. + /// + /// The panel of the entry. + /// The entry itself. + public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry; + + /// + /// Creates a . + /// + /// The type of . + /// The to use when an entry is selected but not hovered over. + /// The to use when an entry is hovered over while not selected. + /// The to use when an entry is hovered over while not selected. + /// The to use when an entry is hovered over while selected. + /// The to use when an entry is hovered over while selected. + /// Function that can optionally be run when the mouse is moved outside a menu panel. + /// Function that can optionally be run when the mouse is moved over a menu panel. + /// New object. + protected static TMenu Create( + Color? activeColor = null, + LoadableAsset? hoverSelectSprite = null, + Color? hoverSelectColor = null, + LoadableAsset? hoverDeselectSprite = null, + Color? hoverDeselectColor = null, + PanelButtonOnMouse? onMouseOut = null, + PanelButtonOnMouse? onMouseOver = null) + where TMenu : CustomMultiSelectMenu, new() + { + TMenu customMenu = Create(onMouseOut, onMouseOver); + + customMenu.confirmButton = null!; // TODO: create/add confirm button + + /* + var button = customMenu.confirmButton.GetComponent(); + button.OnClick.RemoveAllListeners(); + button.OnClick.AddListener((UnityAction)customMenu.OnCompleteSelection);*/ + + customMenu.activeColor = activeColor; + + customMenu.hoverSelectSprite = hoverSelectSprite; + customMenu.hoverSelectColor = hoverSelectColor; + + customMenu.hoverDeselectSprite = hoverDeselectSprite ?? hoverSelectSprite; + customMenu.hoverDeselectColor = hoverDeselectColor ?? hoverSelectColor; + + return customMenu; + } + + /// + /// Setup the given once created, given its index, entry, and onClick action. + /// + /// The panel to configure. + /// The 's index. + /// The 's entry. + /// Action to perform when the is clicked on. + protected abstract void SetupPanelEntry(ShapeshifterPanel panel, int i, TEntry entry, Action onClick); + + /// + /// Begins/opens the custom multi-select menu. + /// + /// All entries to give the custom menu. + /// Function called when all selections are made/confirmed. + /// The number of selections required. + /// Whether the entire selection should be confirmed manually. + /// If the same entry can be selected multiple times, else unselect entry on click. + protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm, bool canRepeat = false) + { + MinigameStubs.Begin(Component, null); + + this.totalSelections = totalSelections; + this.shouldConfirm = shouldConfirm; + this.canRepeat = canRepeat; + this.onSelection = onClick; + + var back = backButton.GetComponent(); + back.OnClick.AddListener((UnityAction)(() => + { + selectedEntries.Clear(); + onClick(null); + })); + + DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, Component.TaskType); + var list2 = new CppCollections.List(); + RegisterPanels( + entries, + (shapeshifterPanel, i, entry) => + { + var num = i % 3; + var num2 = i / 3; + shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); + SetupPanelEntry(shapeshifterPanel, i, entry, () => OnEntryClick(entry)); + list2.Add(shapeshifterPanel.Button); + }, + (p, e) => new MenuEntry(p, e)); + ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); + } + + /// + /// Begins/opens the custom menu, only requiring a single selection. + /// + /// All entries to give the custom menu. + /// Function called when the selection is made. + /// Whether the set of both selections should be confirmed manually. + protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm) + { + Begin(entries, list => onClick(list?[0]), 1, shouldConfirm); + } + + /// + /// Begins/opens the custom menu, requiring two selections. + /// + /// All entries to give the custom menu. + /// Function called when both selections are made. + /// Whether the set of both selections should be confirmed manually. + /// If the same entry can be selected both times, else unselect entry on click. + protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm, bool canRepeat = false) + { + Begin(entries, list => onClick(list?[0], list?[1]), 2, shouldConfirm, canRepeat); + } + + private void OnEntryClick(TEntry entry) + { + MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry); + + // Unselect previous choice + if (!canRepeat && selectedEntries.Remove(menuEntry)) + { + // TODO: if confirm button is enabled, disable it + SetNameplateAppearance(menuEntry, false); + return; + } + + // Do not go past total selections + if (selectedEntries.Count >= totalSelections) + { + return; + } + + selectedEntries.Add(menuEntry); + + // Add choice to list of selections + if (selectedEntries.Count <= totalSelections) + { + SetNameplateAppearance(menuEntry, true); + } + + if (selectedEntries.Count < totalSelections) + { + return; + } + + // Total selections reached + if (shouldConfirm) + { + // TODO: enable confirm button + return; + } + + OnCompleteSelection(); + selectedEntries.Clear(); + } + + private void OnCompleteSelection() + { + onSelection!([.. selectedEntries.Select(se => se.Entry)]); + } + + /// + protected override bool IsEntrySelected(IMenuEntry entry) + { + return selectedEntries.Any(e => e.Panel == entry.Panel); + } + + /// + /// + /// Whether the menu entry is actively selected. + protected void SetNameplateAppearance(IMenuEntry menuEntry, bool isSelected) + { + SetNameplateAppearance( + menuEntry, + isSelected ? hoverDeselectSprite : hoverSelectSprite, + isSelected ? hoverDeselectColor : hoverSelectColor, + isSelected ? activeColor : Color.clear); + } +} diff --git a/MiraAPI/Hud/CustomPaginableMenu.cs b/MiraAPI/Hud/CustomPaginableMenu.cs new file mode 100644 index 0000000..14f573b --- /dev/null +++ b/MiraAPI/Hud/CustomPaginableMenu.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using MiraAPI.Patches.Stubs; +using MiraAPI.Translation; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; +using Object = UnityEngine.Object; + +namespace MiraAPI.Hud; + +/// +/// Paginable using the as a base. +/// +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity convention.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")] +public abstract class CustomPaginableMenu : CustomPhoneMenu +{ + private const int ItemsPerPage = 15; + + /// + /// Menu Entry used for when multiple pages are needed. + /// + /// The panel. + /// The key by which the panels are sorted. + public record MenuEntry(ShapeshifterPanel Panel, string SortKey) : IMenuEntry; + + /// + /// Gets the name of the menu. + /// + protected abstract string Name { get; } + + /// + /// Gets the prefab used to generate the search field. + /// + protected abstract TextBoxTMP? PrefabTextbox { get; } + + /// + protected override float MenuDepth => -60f; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + protected int currentPage; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + private TextBoxTMP? searchTextbox; + private string searchText = string.Empty; + private TextMeshPro? noResultsText; + + /// + /// Creates a . + /// + /// The type of . + /// Function that can optionally be run when the mouse is moved outside a menu panel. + /// Function that can optionally be run when the mouse is moved over a menu panel. + /// New object. + protected static new TMenu Create(PanelButtonOnMouse? onMouseOut = null, PanelButtonOnMouse? onMouseOver = null) + where TMenu : CustomPaginableMenu, new() + { + TMenu customMenu = CustomPhoneMenu.Create(onMouseOut, onMouseOver); + + var nextButton = Object.Instantiate(customMenu.backButton, customMenu.transform).gameObject; + nextButton.transform.localPosition = new Vector3(1.85f, -2.185f, customMenu.MenuDepth); + nextButton.transform.localScale = new Vector3(0.65f, 0.65f, 1); + nextButton.name = "RightArrowButton"; + nextButton.GetComponent().sprite = MiraAssets.NextButton.LoadAsset(); + nextButton.gameObject.GetComponent().DestroyImmediate(); + + var passiveButton = nextButton.gameObject.GetComponent(); + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.OnClick.AddListener((UnityAction)customMenu.NextPage); + + var backButton = Object.Instantiate(nextButton, customMenu.transform).gameObject; + backButton.transform.localPosition = new Vector3(-1.85f, -2.185f, customMenu.MenuDepth); + backButton.name = "LeftArrowButton"; + backButton.gameObject.GetComponent().Destroy(); + backButton.GetComponent().flipX = true; + var prevPassive = backButton.gameObject.GetComponent(); + prevPassive.OnClick.AddListener((UnityAction)customMenu.PreviousPage); + + customMenu.PhoneUI.GetChild(0).GetComponent().material = + PlayerControl.LocalPlayer.cosmetics.currentBodySprite.BodySprite.material; + customMenu.PhoneUI.GetChild(1).GetComponent().material = + PlayerControl.LocalPlayer.cosmetics.currentBodySprite.BodySprite.material; + + return customMenu; + } + + private static string NormalizeForSearch(string? text) + { + return string.IsNullOrWhiteSpace(text) + ? string.Empty + : text.Trim().ToLowerInvariant(); + } + + private List GetFilteredEntries() + { + var query = NormalizeForSearch(searchText); + + if (string.IsNullOrEmpty(query)) + return MenuEntries; + + return [.. MenuEntries + .Where(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(e => e.SortKey.Equals(query, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(e => e.SortKey.StartsWith(query, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase)) + .ThenBy(e => e.SortKey, StringComparer.OrdinalIgnoreCase)]; + } + + private static int GetTotalPages(int itemCount) + { + return Mathf.Max(1, Mathf.CeilToInt(itemCount / (float)ItemsPerPage)); + } + + private void RefreshControllerOverlay(CppCollections.List list) + { + if (ControllerManager.Instance && backButton != null) + { + ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list); + } + } + + private IEnumerator CoRestoreFocus() + { + yield return null; + searchTextbox?.GiveFocus(); + } + + private void NextPage() + { + var filtered = GetFilteredEntries(); + var pages = GetTotalPages(filtered.Count); + currentPage = (currentPage + 1) % pages; + var list = ShowPage(); + RefreshControllerOverlay(list); + } + + private void PreviousPage() + { + var filtered = GetFilteredEntries(); + var pages = GetTotalPages(filtered.Count); + currentPage = (currentPage - 1 + pages) % pages; + var list = ShowPage(); + RefreshControllerOverlay(list); + } + + /// + /// Shows the current page. + /// + /// The Il2CPP list of ui elements displayed on the page. + public CppCollections.List ShowPage() + { + foreach (var entry in MenuEntries) + { + entry.Panel.gameObject.SetActive(false); + } + + var filtered = GetFilteredEntries(); + noResultsText?.gameObject.SetActive(filtered.Count == 0 && !string.IsNullOrWhiteSpace(searchText)); + var totalPages = GetTotalPages(filtered.Count); + currentPage = Mathf.Clamp(currentPage, 0, totalPages - 1); + + var list = filtered.Skip(currentPage * ItemsPerPage).Take(ItemsPerPage).ToList(); + var list2 = new CppCollections.List(); + + for (var i = 0; i < list.Count; i++) + { + var entry = list[i]; + var num = i % 3; + var num2 = i / 3 % 5; + entry.Panel.transform.localPosition = + new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); + entry.Panel.gameObject.SetActive(true); + list2.Add(entry.Panel.Button); + } + + return list2; + } + + /// + /// Ensures that the search UI exists. + /// + protected void EnsureSearchUi() + { + if (searchTextbox != null) + { + return; + } + + var gridCenterX = xStart + xOffset; + var desiredSearchBarCenterY = yStart + 0.55f; + + if (PrefabTextbox == null) + { + return; + } + + var searchRoot = PrefabTextbox.transform.parent != null ? PrefabTextbox.transform.parent.gameObject : PrefabTextbox.gameObject; + var searchObj = Object.Instantiate(searchRoot, transform); + searchObj.name = $"{Name}SearchBar"; + + foreach (var aspect in searchObj.GetComponentsInChildren(true)) + { + aspect.DestroyImmediate(); + } + + searchTextbox = searchObj.GetComponentInChildren(true); + if (searchTextbox == null) + { + return; + } + + try + { + var placeholder = searchTextbox.transform.parent.GetChild(2).GetComponent(); + placeholder?.gameObject.SetActive(false); + } + catch + { + foreach (var tmp in searchObj.GetComponentsInChildren(true)) + { + if (tmp != searchTextbox.outputText && + (tmp.text.Contains("Search", StringComparison.OrdinalIgnoreCase) || + tmp.text.Contains("Here", StringComparison.OrdinalIgnoreCase))) + { + tmp.gameObject.SetActive(false); + } + } + } + + searchObj.transform.localScale = new Vector3(0.7f, 0.7f, 1f); + searchObj.transform.localPosition = new Vector3(gridCenterX, desiredSearchBarCenterY, -1f); + + var searchBounds = CalcSpriteBoundsInParentSpace(transform, searchObj); + var deltaX = gridCenterX - searchBounds.center.x; + var deltaY = desiredSearchBarCenterY - searchBounds.center.y; + searchObj.transform.localPosition += new Vector3(deltaX, deltaY, 0f); + searchBounds = CalcSpriteBoundsInParentSpace(transform, searchObj); + + var wikiClickSound = HudManager.Instance?.MapButton?.ClickSound; + + var searchFocusButton = searchTextbox.gameObject.GetOrAddComponent(); + if (wikiClickSound != null) + { + searchFocusButton.ClickSound = wikiClickSound; + } + + searchFocusButton.OnClick.RemoveAllListeners(); + searchFocusButton.OnClick.AddListener((UnityAction)(Action)(() => + { + searchTextbox.GiveFocus(); + })); + + searchTextbox.SetText(string.Empty); + searchTextbox.OnChange.RemoveAllListeners(); + searchTextbox.OnChange.AddListener((UnityAction)(Action)(() => + { + searchText = searchTextbox.outputText.text ?? string.Empty; + currentPage = 0; + var list = ShowPage(); + RefreshControllerOverlay(list); + + if (searchTextbox != null) + { + Coroutines.Start(CoRestoreFocus()); + } + })); + + var label = Object.Instantiate(HudManager.Instance?.TaskPanel.taskText, transform); + if (label != null) + { + label.name = $"{Name}SearchLabel"; + label.text = MiraLocaleManager.Get("Search", "Search"); + label.alignment = TextAlignmentOptions.Center; + label.fontSize = label.fontSizeMin = label.fontSizeMax = 2.1f; + label.color = Color.white; + label.transform.localPosition = new Vector3(gridCenterX, searchBounds.max.y + 0.18f, -1f); + + if (searchTextbox.outputText != null) + { + label.font = searchTextbox.outputText.font; + label.fontMaterial = searchTextbox.outputText.fontMaterial; + } + } + + noResultsText = Object.Instantiate(HudManager.Instance?.TaskPanel.taskText, transform); + if (noResultsText != null) + { + noResultsText.name = $"{Name}NoResultsText"; + noResultsText.text = MiraLocaleManager.Get($"{Name}NoResults", "No results"); + noResultsText.alignment = TextAlignmentOptions.Center; + noResultsText.fontSize = noResultsText.fontSizeMin = noResultsText.fontSizeMax = 2.25f; + noResultsText.color = Color.white; + noResultsText.transform.localPosition = new Vector3(gridCenterX, yStart + 0.1f, -1f); + noResultsText.gameObject.SetActive(false); + } + + if (backButton == null || searchTextbox == null) + return; + + var clearButtonX = searchBounds.max.x + 0.2f; + var clearButtonY = searchBounds.center.y; + + var clearObj = Object.Instantiate(backButton.gameObject, transform); + clearObj.name = "ClearSearchButton"; + clearObj.transform.localScale = new Vector3(0.35f, 0.35f, 1f); + clearObj.transform.localPosition = new Vector3(clearButtonX, clearButtonY, -1f); + + clearObj.GetComponent()?.DestroyImmediate(); + clearObj.GetComponent()?.DestroyImmediate(); + + var clearSearchButton = clearObj.GetComponent(); + if (clearSearchButton == null) + { + return; + } + + if (wikiClickSound != null) + { + clearSearchButton.ClickSound = wikiClickSound; + } + + clearSearchButton.OnClick.RemoveAllListeners(); + clearSearchButton.OnClick = new Button.ButtonClickedEvent(); + clearSearchButton.OnClick.AddListener((UnityAction)(() => + { + if (searchTextbox == null) + { + return; + } + + searchTextbox.SetText(string.Empty); + searchText = string.Empty; + currentPage = 0; + var list = ShowPage(); + RefreshControllerOverlay(list); + })); + } + + private static Bounds CalcSpriteBoundsInParentSpace(Transform parent, GameObject root) + { + var first = true; + var bounds = default(Bounds); + + foreach (var r in root.GetComponentsInChildren(true)) + { + if (r == null || r.sprite == null) + continue; + + var b = r.bounds; + var c = b.center; + var e = b.extents; + + var p1 = parent.InverseTransformPoint(new Vector3(c.x - e.x, c.y - e.y, c.z)); + var p2 = parent.InverseTransformPoint(new Vector3(c.x - e.x, c.y + e.y, c.z)); + var p3 = parent.InverseTransformPoint(new Vector3(c.x + e.x, c.y - e.y, c.z)); + var p4 = parent.InverseTransformPoint(new Vector3(c.x + e.x, c.y + e.y, c.z)); + + if (first) + { + bounds = new Bounds(p1, Vector3.zero); + first = false; + } + + bounds.Encapsulate(p1); + bounds.Encapsulate(p2); + bounds.Encapsulate(p3); + bounds.Encapsulate(p4); + } + + return bounds; + } + + /// + /// Begins/opens the custom player menu. After registering panels, it will prepare the search, pages, and open the menu. + /// + /// Function where all panels should be registered. + protected void Begin(Action registerEntryPanels) + { + MinigameStubs.Begin(Component, null); + + searchText = string.Empty; + currentPage = 0; + + registerEntryPanels(); + + EnsureSearchUi(); + + var list2 = ShowPage(); + + ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); + } +} diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs new file mode 100644 index 0000000..79f1f7c --- /dev/null +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AmongUs.GameOptions; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Attributes; +using Reactor.Utilities.Extensions; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.Hud; + +/// +/// Component registered in Il2Cpp domain to handle Unity events for custom menus. +/// +[RegisterInIl2Cpp] +public class CustomPhoneMenuComponent(IntPtr cppPtr) : Minigame(cppPtr) +{ + [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] + private void OnDisable() + { + if (ControllerManager.Instance) + { + ControllerManager.Instance.CloseOverlayMenu(name); + } + } + + [Obsolete("Should not be called, see the respective Begin methods instead.", true)] + [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Retained.")] +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member (Justification: Intentional to avoid mods calling Begin when they are not supposed to.) +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Base game code.) + public override void Begin(PlayerTask task) +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + { + throw new NotSupportedException("Should not be called, see the respective Begin methods instead."); + } +} + +/// +/// Defines an entry in a with a Panel. +/// +public interface IMenuEntry +{ + /// + /// Gets the base Panel of the menu entry. + /// + ShapeshifterPanel Panel { get; } +} + +/// +/// Defines a custom menu with a list of entries. +/// +public interface ICustomMenu +{ + /// + /// Gets all registered s. + /// + List MenuEntries { get; } +} + +/// +/// Defines a custom menu with a list of only entries. +/// +/// Only implement this if you want to define a single new type of menu entries explicitly. +/// Must reference the member of the superclass that is being hidden by this one to work. +/// +/// The type of menu entry. +public interface ICustomMenu : ICustomMenu + where TMenu : IMenuEntry +{ + /// + /// Gets all registered s. + /// + new List MenuEntries { get; } +} + +/// +/// Custom Phone Menu logic using the as a base. +/// +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")] +[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Proxy for Unity properties.")] +public abstract class CustomPhoneMenu : ICustomMenu +{ + /// + /// Menu Entry used when specifically only the Panel itself is required. + /// + /// The instance. + protected sealed record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry + { + /// + /// Implicitly converts to . + /// + /// The entry that contains the reference to the panel. + public static implicit operator ShapeshifterPanel(BasicEntry entry) + { + return entry.Panel; + } + + /// + /// Implicitly converts to an instance of . + /// + /// The panel instance to make an entry off of. + public static implicit operator BasicEntry(ShapeshifterPanel panel) + { + return new(panel); + } + } + + /// + /// A delegate that is invoked when the mouse interacts with a panel. + /// + /// The panel's highlight that the delegate modifies. + /// The panel's icon that the delegate modifies. + /// A flag that indicates if the current panel is selected. + public delegate void PanelButtonOnMouse(SpriteRenderer highlight, SpriteRenderer icon, bool isSelected); + + /// + /// Gets the wrapped component that the menu modifies. + /// + public CustomPhoneMenuComponent Component { get; internal set; } = null!; + + /// + public List MenuEntries { get; protected set; } = []; + + /// + /// Gets all of the panels of the menu. + /// + public List EntryPanels => [.. MenuEntries.Select(e => e.Panel)]; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Unity convention.) +#pragma warning disable S1104 // Fields should not have public accessibility (Justification: Read above.) +#pragma warning disable SA1300 // Element should begin with upper-case letter (Justification: Read above.) + public Transform transform => Component.transform; + public GameObject gameObject => Component.gameObject; + public string name => Component.name; +#pragma warning restore SA1300 // Element should begin with upper-case letter + + public float xStart = -0.8f; + public float yStart = 2.15f; + public float xOffset = 1.95f; + public float yOffset = -0.65f; + + public ShapeshifterPanel panelPrefab; + public UiElement backButton; + public UiElement defaultButtonSelected; + + protected PanelButtonOnMouse? onMouseOverAction; + protected PanelButtonOnMouse? onMouseOutAction; +#pragma warning restore S1104 // Fields should not have public accessibility +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + /// + /// Gets the transform of the menu's Phone UI. + /// + public Transform PhoneUI => transform.FindChild("PhoneUI"); + + /// + /// Gets the z-axis depth that the menu is instantiated at. + /// + protected virtual float MenuDepth => -50f; + + /// + /// Closes the menu. + /// + public void Close() + { + Component.Close(); + } + + /// + /// Forcibly closes the menu. + /// + public void ForceClose() + { + Component.ForceClose(); + } + + /// + /// Creates a . + /// + /// The type of . + /// Function that can optionally be run when the mouse is moved outside a menu panel. + /// Function that can optionally be run when the mouse is moved over a menu panel. + /// New object. + protected static TMenu Create(PanelButtonOnMouse? onMouseOut = null, PanelButtonOnMouse? onMouseOver = null) + where TMenu : CustomPhoneMenu, new() + { + var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter); + + var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu; + var newMenu = Object.Instantiate(ogMenu); + var component = newMenu.gameObject.AddComponent(); + var customMenu = new TMenu + { + Component = component, + panelPrefab = newMenu.PanelPrefab, + xStart = newMenu.XStart, + yStart = newMenu.YStart, + xOffset = newMenu.XOffset, + yOffset = newMenu.YOffset, + defaultButtonSelected = newMenu.DefaultButtonSelected, + backButton = newMenu.BackButton, + }; + + var back = customMenu.backButton.GetComponent(); + back.OnClick.RemoveAllListeners(); + back.OnClick.AddListener((UnityAction)customMenu.Close); + + component.CloseSound = newMenu.CloseSound; + component.logger = newMenu.logger; + component.OpenSound = newMenu.OpenSound; + + newMenu.DestroyImmediate(); + + customMenu.transform.SetParent(Camera.main!.transform, false); + customMenu.transform.localPosition = new Vector3(0f, 0f, customMenu.MenuDepth); + + customMenu.onMouseOverAction = onMouseOver; + customMenu.onMouseOutAction = onMouseOut; + + return customMenu; + } + + /// + /// Register new menu panels given a set of entries. + /// + /// The type of the entry. + /// The entries to create panels for. + /// Function to configure the menu's panel once created, given its index and entry. + /// Function to create a for a given panel and it's entry. + /// If , it will create s, acting as wrappers for the s. + protected void RegisterPanels( + IEnumerable entries, + Action entryPanelConfig, + Func? menuEntryMaker = null) + { + int currentEntries = MenuEntries.Count; + + var list = entries.ToList(); + + for (var i = 0; i < list.Count; i++) + { + var index = currentEntries + i; + var entry = list[i]; + + var shapeshifterPanel = Object.Instantiate(panelPrefab, transform); + shapeshifterPanel.transform.localPosition = new Vector3(0f, 0f, -1f); + entryPanelConfig(shapeshifterPanel, index, entry); + + menuEntryMaker ??= (s, _) => new BasicEntry(s); + var menuEntry = menuEntryMaker(shapeshifterPanel, entry); + MenuEntries.Add(menuEntry); + + var button = shapeshifterPanel.Button; + var nameplate = shapeshifterPanel.gameObject.transform.FindChild("Nameplate"); + var highlight = nameplate.FindChild("Highlight").GetComponent(); + var icon = highlight.transform.GetChild(0).GetComponent(); + + if (onMouseOverAction != null) + { + button.OnMouseOver.RemoveAllListeners(); + button.OnMouseOver = new UnityEvent(); + button.OnMouseOver.AddListener((UnityAction)(() => onMouseOverAction(highlight, icon, IsEntrySelected(menuEntry)))); + } + + if (onMouseOutAction != null) + { + button.OnMouseOut.RemoveAllListeners(); + button.OnMouseOut = new UnityEvent(); + button.OnMouseOut.AddListener((UnityAction)(() => onMouseOutAction(highlight, icon, IsEntrySelected(menuEntry)))); + } + } + } + + /// + /// + /// Action to perform when a given entry is clicked on. + /// Argument will be if the back button was clicked instead. + /// Function to configure the menu's panel once created, given its index, entry, and onClick action. + /// + protected void RegisterPanels( + IEnumerable entries, + Action onEntryClick, + Action entryPanelActionConfig, + Func? menuEntryMaker = null) + { + RegisterPanels(entries, (p, i, e) => entryPanelActionConfig(p, i, e, () => onEntryClick(e)), menuEntryMaker); + } + + /// + /// Determines if a given entry is currently selected, not just hovered over. + /// + /// The to check. + /// if the is selected, else . + protected virtual bool IsEntrySelected(IMenuEntry entry) + { + return false; + } + + /// + /// Set the icon, over color, and unselected color for a given entry. + /// + /// The menu entry. + /// The to use as the icon, if any. + /// The to use when hovering over an entry. + /// The to use when not hovering over an entry. + protected static void SetNameplateAppearance( + IMenuEntry menuEntry, LoadableAsset? sprite, Color? overColor, Color? unselectedColor) + { + ShapeshifterPanel panel = menuEntry.Panel; + + var nameplate = panel.gameObject.transform.FindChild("Nameplate"); + var highlight = nameplate.FindChild("Highlight").GetComponent(); + var icon = highlight.transform.GetChild(0).GetComponent(); + if (sprite != null) + { + icon.sprite = sprite.LoadAsset(); + } + + var button = nameplate.GetComponent(); + if (overColor is { } oColor) + { + button.OverColor = oColor; + } + + if (unselectedColor is { } uColor) + { + button.UnselectedColor = uColor; + } + } + + /// + /// Implicitly converts to the the wraps. + /// + /// The menu instance. + public static implicit operator Minigame(CustomPhoneMenu menu) + { + return menu.Component; + } +} + +/// +/// The type of menu entries. +public abstract class CustomPhoneMenu : CustomPhoneMenu, ICustomMenu + where TMenu : IMenuEntry +{ + /// + public new List MenuEntries + { + get => [.. base.MenuEntries.Cast()]; + protected set => base.MenuEntries = [.. value.Cast()]; + } +} diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index fb27f14..26c3b94 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -2,36 +2,23 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using AmongUs.GameOptions; -using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Patches.Stubs; -using Reactor.Utilities.Attributes; -using Reactor.Utilities.Extensions; +using MiraAPI.Utilities.Assets; using UnityEngine; -using UnityEngine.Events; -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. namespace MiraAPI.Hud; /// /// Custom Player Menu using the as a base. /// -/// Used by Il2Cpp. Do not use constructor, this is a . -[RegisterInIl2Cpp] -[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")] -[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")] -[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")] -public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")] +public class CustomPlayerMenu : CustomMultiSelectMenu { - public ShapeshifterPanel panelPrefab; - public float xStart = -0.8f; - public float yStart = 2.15f; - public float xOffset = 1.95f; - public float yOffset = -0.65f; - public UiElement backButton; - public UiElement defaultButtonSelected; + [SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "Read above.")] +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public List potentialVictims; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// /// Creates a . @@ -39,45 +26,37 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) /// New object. public static CustomPlayerMenu Create() { - var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter); - - var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu; - var newMenu = Instantiate(ogMenu); - var customMenu = newMenu.gameObject.AddComponent(); - - customMenu.panelPrefab = newMenu.PanelPrefab; - customMenu.xStart = newMenu.XStart; - customMenu.yStart = newMenu.YStart; - customMenu.xOffset = newMenu.XOffset; - customMenu.yOffset = newMenu.YOffset; - customMenu.backButton = newMenu.BackButton; - var back = customMenu.backButton.GetComponent(); - back.OnClick.RemoveAllListeners(); - back.OnClick.AddListener((UnityAction)(() => - { - Instance.Close(); - })); - - customMenu.CloseSound = newMenu.CloseSound; - customMenu.logger = newMenu.logger; - customMenu.OpenSound = newMenu.OpenSound; - - newMenu.DestroyImmediate(); - - customMenu.transform.SetParent(Camera.main!.transform, false); - customMenu.transform.localPosition = new Vector3(0f, 0f, -50f); - return customMenu; + return Create(); } - private void OnDisable() + /// + /// Creates a . + /// + /// New object. + /// The to use when an entry is selected but not hovered over. + /// The to use when an entry is hovered over while not selected. + /// The to use when an entry is hovered over while not selected. + /// The to use when an entry is hovered over while selected. + /// The to use when an entry is hovered over while selected. + /// Function that can optionally be run when the mouse is moved outside a menu panel. + /// Function that can optionally be run when the mouse is moved over a menu panel. + public static CustomPlayerMenu Create( + Color? activeColor, + LoadableAsset? hoverSelectSprite = null, + Color? hoverSelectColor = null, + LoadableAsset? hoverDeselectSprite = null, + Color? hoverDeselectColor = null, + PanelButtonOnMouse? onMouseOut = null, + PanelButtonOnMouse? onMouseOver = null) { - ControllerManager.Instance.CloseOverlayMenu(name); - } - - /// - public override void Begin(PlayerTask task) - { - throw new NotImplementedException("Use the other Begin method."); + return Create( + activeColor, + hoverSelectSprite, + hoverSelectColor, + hoverDeselectSprite, + hoverDeselectColor, + onMouseOut, + onMouseOver); } /// @@ -85,35 +64,31 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) /// /// Function to determine if player should show in the custom menu. /// action for player. - [HideFromIl2Cpp] - public void Begin(Func playerMatch, Action onClick) + /// Whether the set of both selections should be confirmed manually. + public void Begin(Func playerMatch, Action onClick, bool shouldConfirm = false) { - MinigameStubs.Begin(this, null); + Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick, shouldConfirm); + potentialVictims = EntryPanels; + } - var back = backButton.GetComponent(); - back.OnClick.AddListener((UnityAction)(() => - { - onClick(null); - })); + /// + /// Begins/opens the custom player menu. + /// + /// Function to determine if player should show in the custom menu. + /// action for the two-player selection. + /// Whether the set of both selections should be confirmed manually. + /// If the same entry can be selected both times, else unselect entry on click. + public void Begin(Func playerMatch, Action onClick, bool shouldConfirm = false, bool canRepeat = false) + { + Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick, shouldConfirm, canRepeat); + potentialVictims = EntryPanels; + } - DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType); - var list = PlayerControl.AllPlayerControls.ToArray().Where(playerMatch).ToList(); - potentialVictims = []; - var list2 = new Il2CppSystem.Collections.Generic.List(); - - for (var i = 0; i < list.Count; i++) - { - var player = list[i]; - var num = i % 3; - var num2 = i / 3; - var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; - var shapeshifterPanel = Instantiate(panelPrefab, transform); - shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); - shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); })); - shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white; - potentialVictims.Add(shapeshifterPanel); - list2.Add(shapeshifterPanel.Button); - } - ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); + /// + protected override void SetupPanelEntry(ShapeshifterPanel panel, int i, PlayerControl entry, Action onClick) + { + var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == entry.Data.Role.NameColor; + panel.SetPlayer(i, entry.Data, (Il2CppSystem.Action)onClick); + panel.NameText.color = flag ? entry.Data.Role.NameColor : Color.white; } } diff --git a/MiraAPI/Hud/MiraHudHelper.cs b/MiraAPI/Hud/MiraHudHelper.cs index b072dc8..27c1968 100644 --- a/MiraAPI/Hud/MiraHudHelper.cs +++ b/MiraAPI/Hud/MiraHudHelper.cs @@ -1,4 +1,5 @@ -using BepInEx.Unity.IL2CPP.Utils.Collections; +using System.Diagnostics.CodeAnalysis; +using BepInEx.Unity.IL2CPP.Utils.Collections; using InnerNet; using MiraAPI.LocalSettings; using MiraAPI.Modifiers.ModifierDisplay; @@ -8,37 +9,101 @@ using UnityEngine; namespace MiraAPI.Hud; +/// +/// A mono script to handle hud behaviour. +/// +/// The Il2Cpp pointer to the script's object. [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity Convention.")] +[SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "Is internal code. Property-level protections are not required.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) { + /// + /// Gets the static instance of . + /// 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() + + /// + /// Gets or sets the vanilla match info button. + /// + public static GameObject VanillaMatchInfoButton { get; set; } + + /// + /// Gets or sets the modifier display menu. + /// + public static GameObject ModifierDisplayObject { get; set; } + + /// + /// Gets the cloned chat button. + /// + public static GameObject ClonedChatButton { get; private set; } + + /// + /// Gets the object for extra top right buttons. + /// + public static GameObject ExtraUiTopRight { get; private set; } + + /// + /// Gets the arranger object for the extra ui grid. + /// + public static GridArrange ExtraUiGrid { get; private set; } + + /// + /// Gets the aspect position for the extra ui grid. + /// + public static AspectPosition ExtraUiAspectPos { get; private set; } + + /// + /// Gets the top right ui object. + /// + public static GameObject UiTopRight { get; private set; } + + /// + /// Gets the arranger object for the ui grid. + /// + public static GridArrange UiGrid { get; private set; } + + /// + /// Gets the aspect position for the ui grid. + /// + public static AspectPosition UiAspectPos { get; private set; } + + /// + /// Gets the game object of the Submerged map floor change button. + /// + public static GameObject SubmergedFloorButton { get; private set; } + + /// + /// Gets the normal sprite renderer of the Submerged floor change button. + /// + public static SpriteRenderer SubmergedFloorButtonRenderer { get; private set; } + + /// + /// Gets the hover sprite renderer of the Submerged floor change button. + /// + public static SpriteRenderer SubmergedFloorButtonRendererHover { get; private set; } + + internal static bool ModifierDisplayOnRight; + + private static readonly Vector3 BelowOptionPos = new(0.435f, 1.25f, 0f); + private static readonly Vector3 FullTopPos = new(0.435f, 0.475f, 0f); + + private void Awake() { Instance = this; } - public void Start() + private void Start() { MiraApiSettings.OldButtonScaleFactor = LocalSettingsTabSingleton.Instance.TopRightButtonsFactorSlider.Value; StartCoroutine(MiraApiSettings.CoResizeSettingsUI().WrapToIl2Cpp()); } -#pragma warning disable S2325 - #pragma warning disable CA1822 - public void FixedUpdate() + + [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Unity script method.")] + [SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "Read above.")] + private void FixedUpdate() { if (!HudManager.InstanceExists || !PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data) { @@ -58,14 +123,11 @@ public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) { 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) + private static void CreateUiRow(HudManager instance) { if (!UiTopRight) { @@ -89,7 +151,7 @@ public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) 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 = Instantiate(chatButton, chatButton.transform.parent); ClonedChatButton.SetActive(false); instance.Chat.chatButtonAspectPosition = ClonedChatButton.GetComponent(); chatButton.GetComponent().Destroy(); @@ -116,6 +178,7 @@ public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) listButton.GetComponent().Destroy(); listButton.localPosition = new Vector3(0, 0, 0); } + settingsButton.transform.SetAsLastSibling(); chatButton.transform.SetParent(UiTopRight.transform, false); instance.Chat.chatButton = chatButton.GetComponent(); @@ -129,75 +192,75 @@ public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) 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); + if (!UiTopRight || !UiGrid) + return; - SubmergedFloorButtonRenderer = - SubmergedFloorButton.transform.Find("Inactive").GetComponent(); - SubmergedFloorButtonRendererHover = - SubmergedFloorButton.transform.Find("Active").GetComponent(); - - MiraApiSettings.SetUpButtonPositions(); - } - } + var isChatButtonVisible = HudManager.Instance.Chat.isActiveAndEnabled; + instance.Chat.chatButton.gameObject.SetActive(isChatButtonVisible); + if (VanillaMatchInfoButton) + { + VanillaMatchInfoButton.SetActive(!GameSettingMenu.Instance && !Minigame.Instance); } } - public static void AdjustModifierTab() + private static void UpdateSubmergedButtons(HudManager instance) { - 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(); - } + if (!ModCompatibility.IsSubmerged() || SubmergedFloorButton || !ExtraUiTopRight || SubmergedFloorButton || !ExtraUiTopRight) + return; + + var transform = instance.MapButton.transform.parent.Find(instance.MapButton.name + "(Clone)"); + if (transform == null) + return; + + 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 CreateNewUiRow(HudManager instance) + + private static void AdjustModifierTab() { - if (!ExtraUiTopRight && UiTopRight) + if (ModifierDisplayObject || !UiTopRight || !ExtraUiTopRight || !ModifierDisplayComponent.Instance) + return; + + ModifierDisplayObject = ModifierDisplayComponent.Instance?.gameObject ?? null!; + ModifierDisplayOnRight = !LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value; + if (ModifierDisplayOnRight) { - 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(); + 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(); + } + + private static void CreateNewUiRow(HudManager instance) + { + if (ExtraUiTopRight || !UiTopRight) + return; + + 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/MultiTargetButton.cs index d52d845..9d44b4d 100644 --- a/MiraAPI/Hud/MultiTargetButton.cs +++ b/MiraAPI/Hud/MultiTargetButton.cs @@ -8,7 +8,8 @@ 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 MultiTargetButton : CustomActionButton + where T : MonoBehaviour { /// /// Gets or sets the list of targets of the button. @@ -38,7 +39,7 @@ public abstract class MultiTargetButton : CustomActionButton where T : MonoBe /// /// Sets the outline of the target . /// - /// The target to set the oultine. + /// The target to set the outline. /// Should the outline be active. public abstract void SetOutline(T target, bool active); @@ -54,10 +55,9 @@ public abstract class MultiTargetButton : CustomActionButton where T : MonoBe } } - Targets = newTargets + Targets = [.. newTargets .Where(IsTargetValid) - .Take(MaxTargets) - .ToArray(); + .Take(MaxTargets)]; Targets.Do(t => SetOutline(t, true)); return base.CanUse() && Targets.Length > 0; diff --git a/MiraAPI/Keybinds/BaseKeybind.cs b/MiraAPI/Keybinds/BaseKeybind.cs index e9407ed..9944964 100644 --- a/MiraAPI/Keybinds/BaseKeybind.cs +++ b/MiraAPI/Keybinds/BaseKeybind.cs @@ -1,4 +1,4 @@ -using System; +using System; using Rewired; namespace MiraAPI.Keybinds; @@ -9,7 +9,7 @@ namespace MiraAPI.Keybinds; public class BaseKeybind { /// - /// Gets the Rewired assinged for this keybind. + /// Gets the Rewired assigned for this keybind. /// public InputAction? RewiredInputAction { get; internal set; } @@ -23,6 +23,11 @@ public class BaseKeybind /// public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this); + /// + /// Gets the display label of the currently assigned button on the active controller. + /// + public string CurrentControllerButton => KeybindUtils.GetControllerButtonByKeybind(this); + /// /// Gets or sets the handler of the keybind. Invoked when the keybind is activated. /// @@ -38,6 +43,9 @@ public class BaseKeybind Handler = () => { }; } + /// + /// Initializes a new instance of the class. + /// public BaseKeybind() { Handler = () => { }; @@ -67,6 +75,8 @@ public class BaseKeybind /// The to remove. public void RemoveOnActivate(Action action) { +#pragma warning disable CS8601 // Possible null reference assignment (Justification: Shut up.) Handler -= action; +#pragma warning restore CS8601 // Possible null reference assignment } } diff --git a/MiraAPI/Keybinds/ControllerButton.cs b/MiraAPI/Keybinds/ControllerButton.cs new file mode 100644 index 0000000..c00eb4b --- /dev/null +++ b/MiraAPI/Keybinds/ControllerButton.cs @@ -0,0 +1,87 @@ +namespace MiraAPI.Keybinds; + +/// +/// Controller buttons used for default keybinds. +/// +public enum ControllerButton +{ + /// + /// The bottom face button (Cross on PlayStation, A on Xbox). + /// + South = 4, + + /// + /// The right face button (Circle on PlayStation, B on Xbox). + /// + East = 5, + + /// + /// The left face button (Square on PlayStation, X on Xbox). + /// + West = 7, + + /// + /// The top face button (Triangle on PlayStation, Y on Xbox). + /// + North = 8, + + /// + /// The left bumper (L1 on PlayStation, LB on Xbox). + /// + LeftBumper = 10, + + /// + /// The right bumper (R1 on PlayStation, RB on Xbox). + /// + RightBumper = 12, + + /// + /// The left trigger (L2 on PlayStation, LT on Xbox). + /// + LeftTrigger = 11, + + /// + /// The right trigger (R2 on PlayStation, RT on Xbox). + /// + RightTrigger = 13, + + /// + /// The left stick button (L3 on PlayStation). + /// + LeftStick = 17, + + /// + /// The right stick button (R3 on PlayStation). + /// + RightStick = 18, + + /// + /// The back button (Create on PlayStation, View on Xbox). + /// + Back = 14, + + /// + /// The start button (Options on PlayStation, Menu on Xbox). + /// + Start = 15, + + /// + /// The D-pad up button. + /// + DPadUp = 19, + + /// + /// The D-pad right button. + /// + DPadRight = 20, + + /// + /// The D-pad down button. + /// + DPadDown = 21, + + /// + /// The D-pad left button. + /// + DPadLeft = 22, +} diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs index 042322a..143863e 100644 --- a/MiraAPI/Keybinds/KeybindManager.cs +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -15,7 +15,7 @@ public static class KeybindManager /// /// Gets a list of all registered s. /// - public static List Keybinds { get; } = new(); + public static List Keybinds { get; } = []; [HideFromIl2Cpp] internal static Dictionary VanillaKeybinds { get; set; } = []; @@ -33,12 +33,40 @@ public static class KeybindManager continue; } - keybind.RewiredInputAction = instance.userData.RegisterModBind( - keybind.Id, - keybind.Name, - keybind.SourcePluginName, - keybind.DefaultKey, - modifiers: keybind.ModifierKeys); + keybind.RewiredInputAction = instance.userData.RegisterModBind(keybind.Id, keybind.Name, keybind.SourcePluginName, keybind.DefaultKey, modifiers: keybind.ModifierKeys); + + if (keybind.DefaultControllerButton == null) continue; + + var button = (int)keybind.DefaultControllerButton.Value; + var template = instance.dataFiles.GetJoystickTemplate(GamepadTemplate.typeGuid); + foreach (var map in instance.userData.joystickMaps) + { + if (map.categoryId != 1) continue; + + var elementId = button; + var elementType = keybind.DefaultControllerButton is ControllerButton.LeftTrigger or ControllerButton.RightTrigger ? ControllerElementType.Axis : ControllerElementType.Button; + + if (map.hardwareGuid != GamepadTemplate.typeGuid) + { + var controller = template?.joysticks.ToArray().FirstOrDefault(x => x.JoystickGuid == map.hardwareGuid); + elementId = controller?.GetJoystickElementId(button) ?? -1; + if (elementId < 0) continue; + + var element = instance.dataFiles.GetHardwareJoystickMap(map.hardwareGuid)?.GetElementIdentifier(elementId); + if (element == null) continue; + + elementType = element.elementType; + } + + map.actionElementMaps.Add(new ActionElementMap + { + _actionId = keybind.RewiredInputAction.id, + _elementIdentifierId = elementId, + _elementType = elementType, + _axisRange = AxisRange.Positive, + _axisContribution = Pole.Positive, + }); + } } } catch (Exception e) @@ -50,24 +78,66 @@ public static class KeybindManager /// /// Returns all conflicts where s use the same key. /// - /// A list of pairs of keybinds that conflict. + /// The conflicting keybinds grouped by keycode. public static Dictionary> GetConflicts() { var conflicts = new Dictionary>(); foreach (var keybind in Keybinds) { - var all = GetKeybindsForKey(keybind.CurrentKey, true); + var key = keybind.CurrentKey; + if (key == KeyboardKeyCode.None) continue; + + var all = GetKeybindsForKey(key, true); if (all.Length > 1) { - if (!conflicts.TryGetValue(keybind.CurrentKey, out var group)) + if (!conflicts.TryGetValue(key, out var group)) { - conflicts.Add(keybind.CurrentKey, new List()); - group = conflicts[keybind.CurrentKey]; + group = []; + conflicts.Add(key, group); } - if (!group.Contains(keybind)) + if (!group.Contains(keybind)) group.Add(keybind); + } + } + + return conflicts; + } + + /// + /// Returns controller bindings shared by exclusive custom keybinds. + /// + /// Whether to check only active controller maps and available HUD buttons. + /// The conflicting custom bindings. + public static List GetControllerConflicts(bool enabledOnly = false) + { + var conflicts = new List(); + var actions = Keybinds.Where(x => x.Exclusive && x.RewiredInputAction != null && (!enabledOnly || KeybindUtils.IsControllerActionActive(x.RewiredInputAction.id))).Select(x => x.RewiredInputAction!.id).ToHashSet(); + var player = ReInput.players.GetPlayer(0); + for (var i = 0; i < player.controllers.joystickCount; i++) + { + var controller = player.controllers.Joysticks[i]; + var maps = new CppCollections.List(player.controllers.maps.GetMaps(controller).Cast>()).ToArray(); + foreach (var map in maps) + { + if (enabledOnly && !map.enabled) continue; + + foreach (var binding in map.GetElementMaps()) { - group.Add(keybind); + if (!binding.enabled || !actions.Contains(binding.actionId)) continue; + + foreach (var otherMap in maps) + { + if (enabledOnly && !otherMap.enabled) continue; + if (!enabledOnly && map.categoryId != otherMap.categoryId && map.categoryId != 0 && otherMap.categoryId != 0) continue; + + foreach (var other in otherMap.GetElementMaps()) + { + if (!other.enabled || binding.actionId == other.actionId || !actions.Contains(other.actionId) || !binding.CheckForAssignmentConflict(other)) continue; + + if (!conflicts.Contains(binding)) conflicts.Add(binding); + if (!conflicts.Contains(other)) conflicts.Add(other); + } + } } } } @@ -79,7 +149,7 @@ public static class KeybindManager /// Returns all s for a specified keycode. /// /// The key to look for. - /// /// Should only exlusive keybinds be present. + /// Whether to return only exclusive keybinds. /// The list of s. public static MiraKeybind[] GetKeybindsForKey(KeyboardKeyCode keyCode, bool exclusiveCheck = false) { @@ -87,11 +157,9 @@ public static class KeybindManager foreach (var keybind in Keybinds) { if (keybind.CurrentKey == keyCode && (keybind.Exclusive || !exclusiveCheck)) - { all.Add(keybind); - } } - return all.ToArray(); + return [.. all]; } } diff --git a/MiraAPI/Keybinds/KeybindUtils.cs b/MiraAPI/Keybinds/KeybindUtils.cs index 7a316be..26bcf55 100644 --- a/MiraAPI/Keybinds/KeybindUtils.cs +++ b/MiraAPI/Keybinds/KeybindUtils.cs @@ -1,10 +1,12 @@ using System; +using System.Linq; +using MiraAPI.Hud; using Rewired; namespace MiraAPI.Keybinds; /// -/// Collection of keybind-related utility classes. +/// Collection of keybind-related utilities. /// public static class KeybindUtils { @@ -20,12 +22,26 @@ public static class KeybindUtils /// The currently assigned keycode. public static KeyboardKeyCode GetKeycodeByKeybind(BaseKeybind keybind) { - if (keybind.RewiredInputAction == null) - { - return KeyboardKeyCode.None; - } + return keybind.RewiredInputAction == null ? KeyboardKeyCode.None : GetKeycodeByActionId(keybind.RewiredInputAction.id); + } - return GetKeycodeByActionId(keybind.RewiredInputAction.id); + /// + /// Returns the display label for the currently assigned controller button of a . + /// + /// The keybind to get the controller button from. + /// The button label, or an empty string if no controller binding is available. + public static string GetControllerButtonByKeybind(BaseKeybind keybind) + { + var map = keybind.RewiredInputAction == null ? null : GetControllerElementMap(keybind.RewiredInputAction.id); + var name = map?.elementIdentifierName ?? string.Empty; + return name.ToLowerInvariant() switch + { + "cross" => "X", + "square" => "□", + "circle" => "○", + "triangle" => "△", + _ => name, + }; } /// @@ -42,23 +58,40 @@ public static class KeybindUtils /// Gets a by id with . /// /// The action ID. - /// The keyboard keycode. + /// The keyboard binding, or null if the action is unassigned. public static ActionElementMap? GetActionElementMap(int actionId) { var player = ReInput.players.GetPlayer(0); return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false); } + /// + /// Gets the binding for an action on the active controller. + /// + /// The action ID. + /// The controller binding, or null if no active controller or binding is available. + public static ActionElementMap? GetControllerElementMap(int actionId) + { + var player = ReInput.players.GetPlayer(0); + var controller = player.controllers.GetLastActiveController(ControllerType.Joystick); + return controller == null ? null : player.controllers.maps.GetFirstElementMapWithAction(controller, actionId, false); + } + /// /// Gets an by id. /// - /// The action identifier name. - /// The keyboard keycode. + /// The action ID. + /// The input action, or null if it is unavailable. public static InputAction? GetInputActionById(int id) { return RewiredInputManager?.userData?.GetActionById(id); } + internal static bool IsControllerActionActive(int actionId) + { + return CustomButtonManager.Buttons.Any(x => x.Keybind?.RewiredInputAction?.id == actionId && x.Button != null && x.Button.isActiveAndEnabled && x.Enabled(PlayerControl.LocalPlayer?.Data?.Role)); + } + /// /// Finds and returns an unused KeyCode that is not equal to the excluded key. /// @@ -69,12 +102,13 @@ public static class KeybindUtils /// public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude) { - foreach (KeyboardKeyCode key in Enum.GetValues()) + foreach (var key in Enum.GetValues()) { if (key == exclude) continue; - bool used = KeybindManager.Keybinds.Exists(e => e.DefaultKey == key); + var used = KeybindManager.Keybinds.Exists(e => e.DefaultKey == key); if (!used) return key; } + return KeyboardKeyCode.None; } } diff --git a/MiraAPI/Keybinds/MiraGlobalKeybinds.cs b/MiraAPI/Keybinds/MiraGlobalKeybinds.cs index f945ca9..5ef5380 100644 --- a/MiraAPI/Keybinds/MiraGlobalKeybinds.cs +++ b/MiraAPI/Keybinds/MiraGlobalKeybinds.cs @@ -1,10 +1,11 @@ -using Rewired; +using Rewired; namespace MiraAPI.Keybinds; /// /// Class containing all default s. /// Recommended for cross-mod compatibility. +/// Controller bindings are unassigned by default. /// [RegisterCustomKeybinds] public static class MiraGlobalKeybinds @@ -20,7 +21,7 @@ public static class MiraGlobalKeybinds public static MiraKeybind SecondaryAbility { get; } = new("Secondary Ability", KeyboardKeyCode.Y); /// - /// Gets the keybind for Tetiary abilities. + /// Gets the keybind for Tertiary abilities. /// public static MiraKeybind TertiaryAbility { get; } = new("Tertiary Ability", KeyboardKeyCode.U); @@ -35,7 +36,7 @@ public static class MiraGlobalKeybinds public static MiraKeybind ModifierSecondaryAbility { get; } = new("Modifier Secondary Ability", KeyboardKeyCode.O); /// - /// Gets the keybind for Tetiary modifier abilities. + /// Gets the keybind for Tertiary modifier abilities. /// public static MiraKeybind ModifierTertiaryAbility { get; } = new("Modifier Tertiary Ability", KeyboardKeyCode.P); } diff --git a/MiraAPI/Keybinds/MiraKeybind.cs b/MiraAPI/Keybinds/MiraKeybind.cs index d2b5847..ef466b5 100644 --- a/MiraAPI/Keybinds/MiraKeybind.cs +++ b/MiraAPI/Keybinds/MiraKeybind.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using Rewired; namespace MiraAPI.Keybinds; @@ -6,48 +6,53 @@ namespace MiraAPI.Keybinds; /// /// Represents a registered keybind. /// -public class MiraKeybind : BaseKeybind +/// The name of the keybind. +/// The default keycode. +/// Up to 3 optional s. +/// Is exclusive. +public class MiraKeybind(string name, KeyboardKeyCode? defaultKeycode, ModifierKey[]? modifierKeys = null, bool exclusive = true) : BaseKeybind(name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_')) { /// /// Gets name of the keybind. /// - public string Name { get; } + public string Name { get; } = name; /// /// Gets the default keycode of this keybind. /// - public KeyboardKeyCode DefaultKey { get; } + public KeyboardKeyCode DefaultKey { get; } = defaultKeycode ?? KeyboardKeyCode.None; + + /// + /// Gets the default controller button. If , the controller binding is left unassigned. + /// + public ControllerButton? DefaultControllerButton { get; } /// /// Gets the s assigned to this keybind. /// Due to Rewired limitations, there can only be 3 s. /// - public ModifierKey[] ModifierKeys { get; } + public ModifierKey[] ModifierKeys { get; } = modifierKeys ?? []; /// /// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key. + /// Active exclusive controller bindings take priority over vanilla actions during gameplay. /// - public bool Exclusive { get; } + public bool Exclusive { get; } = exclusive; internal string? SourcePluginName { get; set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with a controller binding. /// /// The name of the keybind. /// The default keycode. - /// Up to 3 optional s. + /// The default controller button. + /// Up to 3 optional s for the keyboard binding. /// Is exclusive. - public MiraKeybind( - string name, - KeyboardKeyCode? defaultKeycode, - ModifierKey[]? modifierKeys = null, - bool exclusive = true) : base(name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_')) + public MiraKeybind(string name, KeyboardKeyCode? defaultKeycode, ControllerButton defaultControllerButton, ModifierKey[]? modifierKeys = null, bool exclusive = true) + : this(name, defaultKeycode, modifierKeys, exclusive) { - Name = name; - DefaultKey = defaultKeycode ?? KeyboardKeyCode.None; - ModifierKeys = modifierKeys ?? []; - Exclusive = exclusive; + DefaultControllerButton = defaultControllerButton; } /// diff --git a/MiraAPI/Keybinds/RegisterCustomKeybindsAttribute.cs b/MiraAPI/Keybinds/RegisterCustomKeybindsAttribute.cs index 088f158..50f8039 100644 --- a/MiraAPI/Keybinds/RegisterCustomKeybindsAttribute.cs +++ b/MiraAPI/Keybinds/RegisterCustomKeybindsAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace MiraAPI.Keybinds; diff --git a/MiraAPI/Keybinds/VanillaKeybind.cs b/MiraAPI/Keybinds/VanillaKeybind.cs index 5f34288..01d37b3 100644 --- a/MiraAPI/Keybinds/VanillaKeybind.cs +++ b/MiraAPI/Keybinds/VanillaKeybind.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace MiraAPI.Keybinds; @@ -8,7 +8,7 @@ namespace MiraAPI.Keybinds; public class VanillaKeybind : BaseKeybind { /// - /// Gets the this keybind is binded to. + /// Gets the this keybind is bound to. /// public ActionButton? Button { get; internal set; } diff --git a/MiraAPI/Keybinds/VanillaKeybindSingleton.cs b/MiraAPI/Keybinds/VanillaKeybindSingleton.cs index 5876fcb..d4e3fd1 100644 --- a/MiraAPI/Keybinds/VanillaKeybindSingleton.cs +++ b/MiraAPI/Keybinds/VanillaKeybindSingleton.cs @@ -1,17 +1,20 @@ -namespace MiraAPI.Keybinds; +using System.Diagnostics.CodeAnalysis; + +namespace MiraAPI.Keybinds; /// /// Singleton for getting a by . /// -/// type. -public static class VanillaKeybinding where T : ActionButton +/// type. +public static class VanillaKeybinding + where T : ActionButton { + [SuppressMessage("Major Code Smell", "S2743:Static fields should not be used in generic types", Justification = "The instance holds a reference to the button that defines the class's type parameter.")] private static VanillaKeybind? _instance; /// /// Gets the instance of the . /// -#pragma warning disable CA1000 + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a vanilla keybind.")] public static VanillaKeybind Instance => _instance ??= KeybindManager.VanillaKeybinds[typeof(T)]; -#pragma warning restore CA1000 } diff --git a/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs index c0608ff..6932f4f 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs @@ -7,14 +7,15 @@ namespace MiraAPI.LocalSettings.Attributes; /// /// Creates a for the . /// +/// The name of the setting. +/// The description of the setting. /// Optional custom names. -/// [AttributeUsage(AttributeTargets.Property)] public class LocalEnumSettingAttribute( string? name = null, string? description = null, - string[]? names = null - ) : LocalSettingAttribute(name, description) + string[]? names = null) + : LocalSettingAttribute(name, description) { private readonly string? _name = name; private readonly string? _description = description; diff --git a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs index b5d681c..56c0f8c 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs @@ -8,11 +8,13 @@ namespace MiraAPI.LocalSettings.Attributes; /// /// Creates a for the . /// +/// The name of the setting. +/// The description of the setting. /// Minimum range. /// Maximum range. /// Increment per use. /// Suffix for the value. -/// Format string used when formating. +/// Format string used when formatting. /// [AttributeUsage(AttributeTargets.Property)] public class LocalNumberSettingAttribute( @@ -22,8 +24,8 @@ public class LocalNumberSettingAttribute( float max = 5, float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, - string? formatString = null - ) : LocalSettingAttribute(name, description) + string? formatString = null) + : LocalSettingAttribute(name, description) { private readonly string? _name = name; private readonly string? _description = description; diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs index 5aaef81..62e49cc 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -7,12 +7,14 @@ namespace MiraAPI.LocalSettings.Attributes; /// Base class for all local settings attributes. /// /// The name of the setting. Defaults to entry key. -/// The description of the setting. Defalts to entry description. +/// The description of the setting. Defaults to entry description. [AttributeUsage(AttributeTargets.Property)] public abstract class LocalSettingAttribute( +#pragma warning disable CS9113 // Parameter is unread (Justification: No idea why they exist, but they probably do for a reason unknown to me.) string? name = null, - string? description = null - ) : Attribute + string? description = null) +#pragma warning restore CS9113 // Parameter is unread + : Attribute { /// /// Returns the created object. diff --git a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs index 9e14a8e..d63334d 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs @@ -9,10 +9,14 @@ namespace MiraAPI.LocalSettings.Attributes; /// Creates a for the . /// /// +/// The name of the setting. +/// The description of the setting. /// Minimum range. /// Maximum range. /// Should the value be rounded. /// Suffix for the value. +/// Should display the value next to the name. +/// The format string used for formatting. [AttributeUsage(AttributeTargets.Property)] public class LocalSliderSettingAttribute( string? name = null, @@ -22,8 +26,8 @@ public class LocalSliderSettingAttribute( bool displayValue = false, string? formatString = null, bool roundValue = false, - MiraNumberSuffixes suffixType = MiraNumberSuffixes.None - ) : LocalSettingAttribute(name, description) + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None) + : LocalSettingAttribute(name, description) { private readonly string? _name = name; private readonly string? _description = description; diff --git a/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs index 22ca7aa..8b7abe8 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs @@ -7,12 +7,13 @@ namespace MiraAPI.LocalSettings.Attributes; /// /// Creates a for the . /// -/// +/// The name of the setting. +/// The description of the setting. [AttributeUsage(AttributeTargets.Property)] public class LocalToggleSettingAttribute( string? name = null, - string? description = null - ) : LocalSettingAttribute(name, description) + string? description = null) + : LocalSettingAttribute(name, description) { private readonly string? _name = name; private readonly string? _description = description; diff --git a/MiraAPI/LocalSettings/LocalSettingsButton.cs b/MiraAPI/LocalSettings/LocalSettingsButton.cs index 747b1ce..e35617f 100644 --- a/MiraAPI/LocalSettings/LocalSettingsButton.cs +++ b/MiraAPI/LocalSettings/LocalSettingsButton.cs @@ -29,7 +29,7 @@ public class LocalSettingsButton(string text, Action onClick) /// public LocalSettingsTab? Tab { get; internal set; } - private TextMeshPro _btnText { get; set; } + private TextMeshPro _btnText; internal GameObject CreateButton(ToggleButtonBehaviour toggle, Transform parent, ref float offset, ref int order, bool last) { @@ -59,9 +59,9 @@ public class LocalSettingsButton(string text, Action onClick) button.name = Text; button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); rollover.OutColor = Tab!.TabAppearance.ButtonColor; - rollover.OverColor = Tab!.TabAppearance.ButtonHoverColor; + rollover.OverColor = Tab.TabAppearance.ButtonHoverColor; rollover.Target = background; - background.color = Tab!.TabAppearance.ButtonColor; + background.color = Tab.TabAppearance.ButtonColor; button.OnClick.AddListener((UnityAction)OnClick); background.transform.localScale = new Vector3(1, 1.25f, 1); @@ -73,12 +73,16 @@ public class LocalSettingsButton(string text, Action onClick) offset += 0.5f; order = 1; } + if (last) offset += 0.6f; return button.gameObject; } + /// + /// Refreshes the button. + /// public void RefreshButton() { _btnText.text = Text.Translate(); diff --git a/MiraAPI/LocalSettings/LocalSettingsManager.cs b/MiraAPI/LocalSettings/LocalSettingsManager.cs index 874e258..4907d8c 100644 --- a/MiraAPI/LocalSettings/LocalSettingsManager.cs +++ b/MiraAPI/LocalSettings/LocalSettingsManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using BepInEx.Unity.IL2CPP; @@ -14,6 +15,10 @@ public static class LocalSettingsManager internal static readonly List Tabs = []; internal static readonly List AvailableTabs = []; + [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 RegisterTab(Type type, BasePlugin pluginInfo) { if (Activator.CreateInstance(type, pluginInfo.Config) is not LocalSettingsTab tab) @@ -32,12 +37,11 @@ public static class LocalSettingsManager { AvailableTabs.Add(tab); } + TypeToTab.Add(type, tab); typeof(LocalSettingsTabSingleton<>).MakeGenericType(type) -#pragma warning disable S3011 - .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! -#pragma warning restore S3011 + .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! // Suppression message refers to this .SetValue(null, tab); return true; diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index eace08f..ebc5c03 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using BepInEx.Configuration; using HarmonyLib; -using MiraAPI.LocalSettings.SettingTypes; using MiraAPI.Patches.LocalSettings; using MiraAPI.Translation; using MiraAPI.Utilities; @@ -94,6 +93,7 @@ public abstract class LocalSettingsTab(ConfigFile config) { setting.RefreshOption(); } + foreach (var button in Buttons) { button.RefreshButton(); @@ -109,115 +109,115 @@ public abstract class LocalSettingsTab(ConfigFile config) /// /// Creates the tab and it's content. /// - /// The instace. + /// The instance. /// The created tab . public virtual GameObject CreateTab(OptionsMenuBehaviour instance) { - var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); - tab.name = $"{TabName}Tab"; - tab.transform.DestroyChildren(); - tab.gameObject.SetActive(false); + var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); + tab.name = $"{TabName}Tab"; + tab.transform.DestroyChildren(); + tab.gameObject.SetActive(false); - if (Scroller == null) - { - Scroller = Helpers.CreateScroller(tab.transform, OptionsMenuPatches.MaskCollider); - } + if (Scroller == null) + { + Scroller = Helpers.CreateScroller(tab.transform, OptionsMenuPatches.MaskCollider); + } - if (TabButton != null) - { - TabButton.Content = tab.gameObject; - } + if (TabButton != null) + { + TabButton.Content = tab.gameObject; + } - var generalLabel = instance.transform.FindChild("GeneralTab").FindChild("ControlGroup") - .FindChild("ControlText_TMP").gameObject; - var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent(); - var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent(); + var generalLabel = instance.transform.FindChild("GeneralTab").FindChild("ControlGroup") + .FindChild("ControlText_TMP").gameObject; + var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent(); + var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent(); - Dictionary> entriesByGroup = new(); - foreach (var entry in Settings) - { - var group = entry.ConfigEntry.Definition.Section; - if (!entriesByGroup.ContainsKey(group)) - entriesByGroup.Add(group, []); + Dictionary> entriesByGroup = []; + foreach (var entry in Settings) + { + var group = entry.ConfigEntry.Definition.Section; + if (!entriesByGroup.ContainsKey(group)) + entriesByGroup.Add(group, []); - entriesByGroup[group].Add(entry); - } + entriesByGroup[group].Add(entry); + } - float contentOffset = 0; - var contentOrder = 1; - var contentIndex = 1; + float contentOffset = 0; + var contentOrder = 1; + var contentIndex = 1; - foreach (var pair in entriesByGroup) - { - if (ShouldCreateLabels) - { - CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset); - contentOrder = 1; - contentIndex = 1; - } + foreach (var pair in entriesByGroup) + { + if (ShouldCreateLabels) + { + CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset); + contentOrder = 1; + contentIndex = 1; + } - foreach (var setting in pair.Value) - { - var obj = setting.CreateOption( - toggle, - slider, - Scroller.Inner, - ref contentOffset, - ref contentOrder, - contentIndex == pair.Value.Count); + foreach (var setting in pair.Value) + { + var obj = setting.CreateOption( + toggle, + slider, + Scroller.Inner, + ref contentOffset, + ref contentOrder, + contentIndex == pair.Value.Count); - obj!.GetComponentsInChildren(true).Do(x => - { - x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - x.sortingOrder = 150; - x.sortingLayerName = "Default"; - }); + obj!.GetComponentsInChildren(true).Do(x => + { + x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + x.sortingOrder = 150; + x.sortingLayerName = "Default"; + }); - obj.GetComponentsInChildren(true).Do(x => - { - x.sortingOrder = 151; - x.sortingLayerName = "Default"; - }); + obj.GetComponentsInChildren(true).Do(x => + { + x.sortingOrder = 151; + x.sortingLayerName = "Default"; + }); - obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); - contentIndex++; - } + contentIndex++; + } - contentOrder = 1; - } + contentOrder = 1; + } - contentIndex = 1; - contentOrder = 1; - foreach (var button in Buttons) - { - var obj = button.CreateButton( - toggle, - Scroller.Inner, - ref contentOffset, - ref contentOrder, - contentIndex == Buttons.Count); + contentIndex = 1; + contentOrder = 1; + foreach (var button in Buttons) + { + var obj = button.CreateButton( + toggle, + Scroller.Inner, + ref contentOffset, + ref contentOrder, + contentIndex == Buttons.Count); - obj!.GetComponentsInChildren(true).Do(x => - { - x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - x.sortingOrder = 150; - x.sortingLayerName = "Default"; - }); + obj.GetComponentsInChildren(true).Do(x => + { + x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + x.sortingOrder = 150; + x.sortingLayerName = "Default"; + }); - obj.GetComponentsInChildren(true).Do(x => - { - x.sortingOrder = 151; - x.sortingLayerName = "Default"; - }); + obj.GetComponentsInChildren(true).Do(x => + { + x.sortingOrder = 151; + x.sortingLayerName = "Default"; + }); - obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); - contentIndex++; - } + contentIndex++; + } - Scroller.SetBounds(new FloatRange(0, (Settings.Count + Buttons.Count) * 0.5f - 5f), new FloatRange(0, 0)); - return tab; + Scroller.SetBounds(new FloatRange(0, (Settings.Count + Buttons.Count) * 0.5f - 5f), new FloatRange(0, 0)); + return tab; } /// @@ -278,8 +278,8 @@ public abstract class LocalSettingsTab(ConfigFile config) { if (!tabButtonObject.Content.gameObject.activeSelf) { - //tabButtonObject.Button.color = TabColor; - //tabButtonObject.Rollover.OutColor = TabColor; + // tabButtonObject.Button.color = TabColor; + // tabButtonObject.Rollover.OutColor = TabColor; } tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f); diff --git a/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs index a435fa4..0b840f2 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace MiraAPI.LocalSettings; @@ -6,14 +7,14 @@ namespace MiraAPI.LocalSettings; /// Singleton for s. /// /// The settings tab type. -public static class LocalSettingsTabSingleton where T : LocalSettingsTab +public static class LocalSettingsTabSingleton + where T : LocalSettingsTab { private static T? _instance; /// /// Gets the instance of the setting tab. /// -#pragma warning disable CA1000 + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a local settings tab.")] public static T Instance => _instance ??= LocalSettingsManager.Tabs.OfType().Single(); -#pragma warning restore CA1000 } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs index e1e0c7b..7eeeecc 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs @@ -14,44 +14,35 @@ namespace MiraAPI.LocalSettings.SettingTypes; /// /// Local setting class for s. /// -public class LocalEnumSetting : LocalSettingBase +/// The tab to create the setting in. +/// The config entry. +/// The type. +/// The name of the setting. +/// The description of the setting. +/// The optional values array to replace the names. +public class LocalEnumSetting( + Type tab, + ConfigEntryBase configEntry, + Type enumType, + string? name = null, + string? description = null, + string[]? values = null) : LocalSettingBase(tab, configEntry, name, description) { /// /// Gets the type of the setting. /// - public Type EnumType { get; } + public Type EnumType { get; } = enumType; /// /// Gets the values. /// - public string[] Values { get; } + public string[] Values { get; } = values ?? [.. Enum + .GetValues(configEntry.SettingType) + .Cast() + .Select(x => x.ToDisplayString())]; - private SpriteRenderer _highlight { get; set; } - - private TextMeshPro _btnText { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The type. - /// The optional values array to replace the names. - public LocalEnumSetting( - Type tab, - ConfigEntryBase configEntry, - Type enumType, - string? name = null, - string? description = null, - string[]? values = null) - : base(tab, configEntry, name, description) - { - EnumType = enumType; - Values = values ?? Enum - .GetValues(configEntry.SettingType) - .Cast() - .Select(x => x.ToDisplayString()) - .ToArray(); - } + private SpriteRenderer _highlight; + private TextMeshPro _btnText; /// public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) @@ -68,10 +59,11 @@ public class LocalEnumSetting : LocalSettingBase var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); if (highlight != null) { - _highlight = highlight; + this._highlight = highlight; highlight.color = Tab!.TabAppearance.EnumHoverColor; highlight.gameObject.SetActive(false); } + toggleComp.Destroy(); if (last && order == 1) @@ -88,12 +80,12 @@ public class LocalEnumSetting : LocalSettingBase button.name = Name; button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); rollover.OutColor = Tab!.TabAppearance.EnumColor; - rollover.OverColor = Tab!.TabAppearance.EnumHoverColor; - background.color = Tab!.TabAppearance.EnumColor; + rollover.OverColor = Tab.TabAppearance.EnumHoverColor; + background.color = Tab.TabAppearance.EnumColor; button.OnClick.AddListener((UnityAction)(() => { - int value = GetValue(); + var value = GetValue(); value++; if (value >= Values.Length) { @@ -109,6 +101,7 @@ public class LocalEnumSetting : LocalSettingBase { tmp.text = Description; } + highlight?.gameObject.SetActive(true); })); button.OnMouseOut.AddListener((UnityAction)(() => @@ -125,6 +118,7 @@ public class LocalEnumSetting : LocalSettingBase offset += 0.5f; order = 1; } + if (last) offset += 0.6f; diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs index f37ea80..83e96dc 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs @@ -13,56 +13,46 @@ namespace MiraAPI.LocalSettings.SettingTypes; /// /// Local setting class for numbers. /// -public class LocalNumberSetting : LocalSettingBase +/// The tab to create the setting in. +/// The config entry. +/// The value range. +/// The name of the setting. +/// The description of the setting. +/// The increment per click. +/// The suffix used for formatting. +/// The format string used for formatting. +public class LocalNumberSetting( + Type tab, + ConfigEntryBase configEntry, + string? name = null, + string? description = null, + FloatRange? numberRange = null, + float? increment = null, + MiraNumberSuffixes? suffixType = null, + string? formatString = null) : LocalSettingBase(tab, configEntry, name, description) { /// /// Gets the range of the button. /// - public FloatRange NumberRange { get; } + public FloatRange NumberRange { get; } = numberRange ?? new FloatRange(1, 5); /// /// Gets the increment of the value when button is pressed. /// - public float Increment { get; } + public float Increment { get; } = increment ?? 1; /// /// Gets a format for the text to use to format the number. /// - public string FormatString { get; } + public string FormatString { get; } = formatString ?? "0"; /// /// Gets the suffix for the number value. /// - public MiraNumberSuffixes SuffixType { get; } + public MiraNumberSuffixes SuffixType { get; } = suffixType ?? MiraNumberSuffixes.None; - private SpriteRenderer _highlight { get; set; } - - private TextMeshPro _btnText { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The value range. - /// The increment per click. - /// The suffix used for formating. - /// The format string used for formating. - public LocalNumberSetting( - Type tab, - ConfigEntryBase configEntry, - string? name = null, - string? description = null, - FloatRange? numberRange = null, - float? increment = null, - MiraNumberSuffixes? suffixType = null, - string? formatString = null) - : base(tab, configEntry, name, description) - { - SuffixType = suffixType ?? MiraNumberSuffixes.None; - NumberRange = numberRange ?? new FloatRange(1, 5); - Increment = increment ?? 1; - FormatString = formatString ?? "0"; - } + private SpriteRenderer _highlight; + private TextMeshPro _btnText; /// public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) @@ -83,6 +73,7 @@ public class LocalNumberSetting : LocalSettingBase highlight.color = Tab!.TabAppearance.NumberHoverColor; highlight.gameObject.SetActive(false); } + toggleComp.Destroy(); if (last && order == 1) @@ -99,12 +90,12 @@ public class LocalNumberSetting : LocalSettingBase button.name = Name; button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); rollover.OutColor = Tab!.TabAppearance.NumberColor; - rollover.OverColor = Tab!.TabAppearance.NumberHoverColor; - background.color = Tab!.TabAppearance.NumberColor; + rollover.OverColor = Tab.TabAppearance.NumberHoverColor; + background.color = Tab.TabAppearance.NumberColor; button.OnClick.AddListener((UnityAction)(() => { - float value = GetValue(); + var value = GetValue(); value += Increment; if (value > NumberRange.max) { @@ -120,6 +111,7 @@ public class LocalNumberSetting : LocalSettingBase { tmp.text = Description; } + highlight?.gameObject.SetActive(true); })); button.OnMouseOut.AddListener((UnityAction)(() => @@ -136,6 +128,7 @@ public class LocalNumberSetting : LocalSettingBase offset += 0.5f; order = 1; } + if (last) offset += 0.6f; @@ -156,7 +149,7 @@ public class LocalNumberSetting : LocalSettingBase protected override string GetValueText() { var value = GetValue(); - var formated = Helpers.FormatValue(value, SuffixType, FormatString); - return $"{Name.Translate()}: {formated}"; + var formatted = Helpers.FormatValue(value, SuffixType, FormatString); + return $"{Name.Translate()}: {formatted}"; } } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index e26f6ce..8e9517a 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -58,7 +58,7 @@ public abstract class LocalSettingBase : ILocalSetting public abstract void RefreshOption(); /// - /// Returns the formated string to use in the text of the setting. + /// Returns the formatted string to use in the text of the setting. /// /// The value text. protected virtual string GetValueText() diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs index 2a90bfd..a84b95b 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs @@ -13,61 +13,52 @@ namespace MiraAPI.LocalSettings.SettingTypes; /// /// Local setting class for sliders. /// -public class LocalSliderSetting : LocalSettingBase +/// The tab to create the setting in. +/// The config entry. +/// The name of the setting. +/// The description of the setting. +/// The value range. +/// The suffix used for formatting. +/// The format string used for formatting. +/// Should the value be rounded. +/// Should display the value next to the name. +public class LocalSliderSetting( + Type tab, + ConfigEntryBase configEntry, + string? name = null, + string? description = null, + FloatRange? sliderRange = null, + bool displayValue = false, + MiraNumberSuffixes? suffixType = null, + string? formatString = null, + bool roundValue = false) : LocalSettingBase(tab, configEntry, name, description) { /// /// Gets the range of the slider. /// - public FloatRange SliderRange { get; } + public FloatRange SliderRange { get; } = sliderRange ?? new FloatRange(0, 100); /// /// Gets a value indicating whether the value should be displayed next to name. /// - public bool DisplayValue { get; } + public bool DisplayValue { get; } = displayValue; /// /// Gets a format for the text to use to format the number. /// - public string FormatString { get; } + public string FormatString { get; } = formatString ?? "0.0"; /// /// Gets a value indicating whether the value should be rounded. /// - public bool RoundValue { get; } + public bool RoundValue { get; } = roundValue; /// /// Gets the suffix for the number value. /// - public MiraNumberSuffixes SuffixType { get; } + public MiraNumberSuffixes SuffixType { get; } = suffixType ?? MiraNumberSuffixes.None; - private SlideBar _slider { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The value range. - /// The suffix used for formating. - /// The format string used for formating. - /// Should the value be rounded. - public LocalSliderSetting( - Type tab, - ConfigEntryBase configEntry, - string? name = null, - string? description = null, - FloatRange? sliderRange = null, - bool displayValue = false, - MiraNumberSuffixes? suffixType = null, - string? formatString = null, - bool roundValue = false) - : base(tab, configEntry, name, description) - { - SliderRange = sliderRange ?? new FloatRange(0, 100); - DisplayValue = displayValue; - SuffixType = suffixType ?? MiraNumberSuffixes.None; - FormatString = formatString ?? "0.0"; - RoundValue = roundValue; - } + private SlideBar _slider; /// public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) @@ -88,8 +79,8 @@ public class LocalSliderSetting : LocalSettingBase newSlider.name = Name; newSlider.Range = new FloatRange(-1.5f, 1.5f); newSlider.SetValue(Mathf.InverseLerp(SliderRange.min, SliderRange.max, GetValue())); - rollover.OutColor = Tab!.TabAppearance.SliderColor; - rollover.OverColor = Tab!.TabAppearance.SliderHoverColor; + rollover.OutColor = Tab.TabAppearance.SliderColor; + rollover.OverColor = Tab.TabAppearance.SliderHoverColor; newSlider.Title.transform.localPosition = new Vector3(0.5f, 0, -1f); newSlider.Title.horizontalAlignment = DisplayValue ? HorizontalAlignmentOptions.Left : HorizontalAlignmentOptions.Center; newSlider.Title.text = GetValueText(); @@ -120,9 +111,9 @@ public class LocalSliderSetting : LocalSettingBase if (DisplayValue) { var value = GetValue(); - var formated = Helpers.FormatValue(value, SuffixType, FormatString); - var maxFormated = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString); - return $"{Name.Translate()}: {formated} / {maxFormated}"; + var formatted = Helpers.FormatValue(value, SuffixType, FormatString); + var maxFormatted = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString); + return $"{Name.Translate()}: {formatted} / {maxFormatted}"; } return $"{Name.Translate()}"; diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs index f28b6db..af76e24 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs @@ -2,7 +2,6 @@ using BepInEx.Configuration; using MiraAPI.Translation; using MiraAPI.Utilities; -using Reactor.Localization.Utilities; using TMPro; using UnityEngine; using UnityEngine.Events; @@ -13,22 +12,18 @@ namespace MiraAPI.LocalSettings.SettingTypes; /// /// Local setting class for toggles. /// -public class LocalToggleSetting : LocalSettingBase +/// The tab to create the setting in. +/// The config entry. +/// The name of the setting. +/// The description of the setting. +public class LocalToggleSetting( + Type tab, + ConfigEntryBase configEntry, + string? name = null, + string? description = null) + : LocalSettingBase(tab, configEntry, name, description) { - private ToggleButtonBehaviour _toggle { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// - public LocalToggleSetting( - Type tab, - ConfigEntryBase configEntry, - string? name = null, - string? description = null - ) : base(tab, configEntry, name, description) - { - } + private ToggleButtonBehaviour _toggle; /// public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) @@ -55,13 +50,13 @@ public class LocalToggleSetting : LocalSettingBase 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; + rollover.OverColor = Tab.TabAppearance.ToggleHoverColor; passiveButton.OnClick.AddListener((UnityAction)(() => { SetValue(!GetValue()); toggleObject.UpdateText(GetValue()); - toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; + toggleObject.Background.color = GetValue() ? Tab.TabAppearance.ToggleActiveColor : Tab.TabAppearance.ToggleInactiveColor; })); passiveButton.OnMouseOver.AddListener((UnityAction)(() => { @@ -73,7 +68,7 @@ public class LocalToggleSetting : LocalSettingBase passiveButton.OnMouseOut.AddListener((UnityAction)(() => { toggleObject.UpdateText(GetValue()); - toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; + toggleObject.Background.color = GetValue() ? Tab.TabAppearance.ToggleActiveColor : Tab.TabAppearance.ToggleInactiveColor; })); Helpers.DivideSize(toggleObject.gameObject, 1.1f); @@ -84,6 +79,7 @@ public class LocalToggleSetting : LocalSettingBase offset += 0.5f; order = 1; } + if (last) offset += 0.6f; diff --git a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs index 44ba56f..1f0f7f1 100644 --- a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs +++ b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs @@ -1,6 +1,7 @@ -using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Utilities; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using TMPro; @@ -12,6 +13,7 @@ namespace MiraAPI.MeetingAbilities; /// A component for handling targeted meeting buttons. /// [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] public class MeetingAbilityBehaviour : MonoBehaviour { /// @@ -25,29 +27,29 @@ public class MeetingAbilityBehaviour : MonoBehaviour private TargetedMeetingButton _button = null!; /// - /// The of this component. + /// Gets he of this component. /// - public PassiveButton Button; + public PassiveButton Button { get; internal set; } /// - /// The parent . + /// Gets the parent . /// - public PlayerVoteArea VoteArea; + public PlayerVoteArea VoteArea { get; internal set; } /// - /// The button's component. + /// Gets the button's component. /// - public SpriteRenderer Renderer; + public SpriteRenderer Renderer { get; internal set; } /// - /// The label used for showing the ability's cooldown. + /// Gets he label used for showing the ability's cooldown. /// - public TextMeshPro CooldownText; + public TextMeshPro CooldownText { get; internal set; } /// - /// The label used for showing the ability's remaining uses. + /// Gets the label used for showing the ability's remaining uses. /// - public TextMeshPro UsesText; + public TextMeshPro UsesText { get; internal set; } /// /// Initializes the various properties of this component. @@ -62,12 +64,14 @@ public class MeetingAbilityBehaviour : MonoBehaviour _button = button; Renderer = GetComponent(); SetFillUp(1, 1); - if (!TryGetComponent(out Button)) + if (!TryGetComponent(out var but)) { Error($"Could not initialize MeetingButtonBehaviour for {Button.GetType().Name}, Destroying..."); Destroy(this); } + _init = true; + Button = but; CooldownText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, transform); CooldownText.GetComponent().Destroy(); CooldownText.transform.localPosition = new Vector3(0, 0, -10); @@ -100,23 +104,29 @@ public class MeetingAbilityBehaviour : MonoBehaviour if (!_init) return; if (_button.Timer < 0.01f) { - Renderer.material.SetFloat("_Desat", 0.0f); + Renderer.material.SetFloat(ShaderID.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); + Renderer.material.SetFloat(ShaderID.Desat, 0.5f); CooldownText.text = _button.Timer.ToString(_button.CooldownTimerFormatString, NumberFormatInfo.InvariantInfo); UsesText.text = string.Empty; } + SetFillUp(_button.Timer, _button.InitialCooldown); } + /// + /// Sets the button fill up coloring based on the ratio between the time remaining and the maximum time of the button. + /// + /// The remaining timer. + /// The maximum timer. public void SetFillUp(float timer, float maxTimer) { - float percentCool = Mathf.Clamp(timer / maxTimer, 0.001f, 1f); + var percentCool = Mathf.Clamp(timer / maxTimer, 0.001f, 1f); if (percentCool <= 0) percentCool = 1f; - Renderer.material.SetFloat("_Percent", percentCool); + Renderer.material.SetFloat(ShaderID.Percent, percentCool); } } diff --git a/MiraAPI/MeetingAbilities/MeetingActionButton.cs b/MiraAPI/MeetingAbilities/MeetingActionButton.cs index dd733af..290419b 100644 --- a/MiraAPI/MeetingAbilities/MeetingActionButton.cs +++ b/MiraAPI/MeetingAbilities/MeetingActionButton.cs @@ -1,7 +1,3 @@ -using System; -using MiraAPI.Events; -using MiraAPI.Events.Mira; -using MiraAPI.Hud; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -77,17 +73,17 @@ public abstract class MeetingActionButton public int UsesSoFar { get; set; } /// - /// Gets a value determining if the abiity is disabled upon being used. + /// Gets a value indicating whether the ability is disabled upon being used. /// public virtual bool DisableUponUse => false; /// - /// Gets a value determining if the abiity is disabled upon voting. + /// Gets a value indicating whether the ability is disabled upon voting. /// public virtual bool DisableUponVoting => false; /// - /// Gets a value determining if the abiity is hidden when votes wrapping up. + /// Gets a value indicating whether the ability is hidden when votes wrapping up. /// public virtual bool HideUponWrapUp => false; @@ -340,6 +336,7 @@ public abstract class MeetingActionButton { Button!.SetDisabled(); } + Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString); Button.SetCoolDown(Timer, Cooldown); } diff --git a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs index 9f44fcf..5ca75d8 100644 --- a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs +++ b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs @@ -1,10 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; -using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Components; using UnityEngine; @@ -18,12 +15,12 @@ public static class MeetingButtonManager /// /// Gets a list if==of all registered . /// - public static List TargetedButtons { get; internal set; } = new(); + public static List TargetedButtons { get; internal set; } = []; /// /// Gets a list if==of all registered . /// - public static List UntargetedButtons { get; internal set; } = new(); + public static List UntargetedButtons { get; internal set; } = []; internal static bool RegisterTargetedMeetingButton(Type type, MiraPluginInfo info) { @@ -32,7 +29,7 @@ public static class MeetingButtonManager return false; } - var button = Activator.CreateInstance(type) as TargetedMeetingButton; + var button = (Activator.CreateInstance(type) as TargetedMeetingButton)!; info.InternalTargetedMeetingButtons.Add(button); TargetedButtons.Add(button); return true; @@ -45,20 +42,22 @@ public static class MeetingButtonManager return false; } - var button = Activator.CreateInstance(type) as MeetingActionButton; + var button = (Activator.CreateInstance(type) as MeetingActionButton)!; info.InternalMeetingButtons.Add(button); UntargetedButtons.Add(button); return true; } - private static CenteredGridArrange? actionButtonsParent = null!; - + /// + /// Called on and /. + /// 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; @@ -72,7 +71,7 @@ public static class MeetingButtonManager public static void OnMeetingStart(MeetingHud meetingHud) { // Untargeted buttons parent. - actionButtonsParent = new GameObject("ActionsButtonParent").AddComponent(); + var 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); @@ -103,21 +102,24 @@ public static class MeetingButtonManager var btn = ability.CreateButton(playerVoteArea); btn.transform.SetParent(playerVoteArea.Buttons.transform); } + var btn2 = ability.CreateButton(meetingHud.SkipVoteButton); btn2.transform.SetParent(meetingHud.SkipVoteButton.Buttons.transform); if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses; - if (ability is MultiTargetMeetingButton multiAbility) multiAbility.Targets = new(); + if (ability is MultiTargetMeetingButton multiAbility) multiAbility.Targets = []; } catch (Exception e) { Error($"Failed to create targeted meeting button {ability.Name}: {e.Message}"); } } + foreach (var playerVoteArea in meetingHud.playerStates) { playerVoteArea.CancelButton.transform.SetAsFirstSibling(); } + meetingHud.SkipVoteButton.CancelButton.transform.SetAsFirstSibling(); } } diff --git a/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs b/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs index a4ddb40..4ba87b7 100644 --- a/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs +++ b/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs @@ -1,7 +1,17 @@ namespace MiraAPI.MeetingAbilities; +/// +/// The use mode of the meeting button. +/// public enum MeetingButtonUsesMode { + /// + /// Sets the uses to be limited per game. + /// PerGame, - PerMeeting -} \ No newline at end of file + + /// + /// Sets the uses to be limited per meeting. + /// + PerMeeting, +} diff --git a/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs index b4dcffa..6fb4bca 100644 --- a/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs +++ b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using MiraAPI.Utilities.Assets; @@ -8,12 +7,12 @@ using UnityEngine.UI; namespace MiraAPI.MeetingAbilities; /// -/// Abstract class for creating multi target meeting abilities +/// Abstract class for creating multi target meeting abilities. /// public abstract class MultiTargetMeetingButton : TargetedMeetingButton { /// - /// The used for when the button is on. + /// Gets he used for when the button is on. /// public abstract LoadableAsset SpriteActive { get; } @@ -25,7 +24,7 @@ public abstract class MultiTargetMeetingButton : TargetedMeetingButton /// /// Gets or sets all selected targets. /// - public Dictionary Targets { get; set; } = new(); + public Dictionary Targets { get; set; } = []; /// public override void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea) @@ -41,6 +40,7 @@ public abstract class MultiTargetMeetingButton : TargetedMeetingButton HandleFinish(); return; } + OnSelect(playerVoteArea, toggle); } @@ -54,10 +54,11 @@ public abstract class MultiTargetMeetingButton : TargetedMeetingButton { button.Renderer.sprite = Sprite.LoadAsset(); } + Timer = Cooldown; UsesLeft -= 1; OnFinish(); - Targets = new(); + Targets = []; } /// @@ -84,7 +85,7 @@ public abstract class MultiTargetMeetingButton : TargetedMeetingButton /// public override MeetingAbilityBehaviour CreateButton(PlayerVoteArea playerVoteArea) { - Targets = new(); + Targets = []; var button = base.CreateButton(playerVoteArea); button.Button.OnClick = new Button.ButtonClickedEvent(); button.Button.OnClick.AddListener(new System.Action(() => { ClickHandler(button, playerVoteArea); })); diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs index d1d6643..2d4766e 100644 --- a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using MiraAPI.Utilities.Assets; using UnityEngine; using UnityEngine.UI; diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs index 47dc791..3f20b61 100644 --- a/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs @@ -7,7 +7,8 @@ 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 +public static class TargetedMeetingButtonSingleton + where T : TargetedMeetingButton { private static T? _instance; diff --git a/MiraAPI/MiraAPI.csproj b/MiraAPI/MiraAPI.csproj index ec090f0..b6bcb5d 100644 --- a/MiraAPI/MiraAPI.csproj +++ b/MiraAPI/MiraAPI.csproj @@ -1,20 +1,26 @@  - latest + preview Simple and easy to use Among Us modding API. AllOfUs.MiraAPI true true + IDE0031;IDE0032 - + - + - - + + + + + + + diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 0ea0b20..3aa3bb5 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,10 +1,11 @@ -global using static Reactor.Utilities.Logger; -using System; +using System; using System.Globalization; using BepInEx; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.Patches; +using MiraAPI.Patches.Roles; using MiraAPI.PluginLoading; using MiraAPI.Translation; using MiraAPI.VanillaEvents; @@ -24,16 +25,13 @@ namespace MiraAPI; [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] [ReactorModFlags(ModFlags.RequireOnAllClients)] +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.) +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by BepInEx.) public partial class MiraApiPlugin : BasePlugin, IMiraPlugin +#pragma warning restore SA1515 // Single-line comment should be preceded by blank line { - /// - public ConfigFile GetConfigFile() - { - return Config; - } - /// - /// Gets the specified Culture for string manipulations. + /// Gets the specified Culture for string manipulations. /// public static CultureInfo Culture { get; internal set; } = new("en-US"); @@ -66,10 +64,36 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); + internal static ConfigEntry WikiSortingGroup { get; private set; } + internal static ConfigEntry WikiSortingMethod { get; private set; } + internal static ConfigEntry WikiSortingFilter { get; private set; } + internal static ConfigEntry DistanceSuffix { get; private set; } + /// public override void Load() { + WikiSortingGroup = Config.Bind( + "Wiki", + "WikiSortingGroup", + SortingGroups.Ungrouped, + "Determines how clusters of roles / modifiers are grouped together, if at all."); + WikiSortingMethod = Config.Bind( + "Wiki", + "WikiSortingMethod", + SortingMethod.Alphabetical, + "Determines if the list of roles / modifiers are organized in ascending or descending order."); + WikiSortingFilter = Config.Bind( + "Wiki", + "WikiSortingFilter", + SortingFilter.AllRoles, + "Determines what roles / modifiers are displayed at once."); + DistanceSuffix = Config.Bind( + "Display", + "Distance", + "unit(s)", + "Dictates the unit of distance used in numeric option displays representing a distance."); Harmony.PatchAll(); + RoleOptionsCollectionPatch.PatchRoleMethods(Harmony); UiResetEvents.Initialize(); JudgeEvents.Initialize(); @@ -85,4 +109,10 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin ModCompatibility .Initialize; // Initialise AFTER the mods are loaded to ensure maximum parity (no need for the soft dependency either then) } + + /// + public ConfigFile GetConfigFile() + { + return Config; + } } diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 3d74768..1afca88 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -6,7 +6,6 @@ using MiraAPI.Hud; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Patches; -using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -58,6 +57,7 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) HudManagerPatches.ResizeUI(1f / OldButtonScaleFactor); HudManagerPatches.ResizeUI(ButtonUIFactorSlider.Value); } + OldButtonScaleFactor = ButtonUIFactorSlider.Value; } else if (configEntry == TopRightButtonsFactorSlider) @@ -66,8 +66,13 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) { ResizeUI(TopRightButtonsFactorSlider.Value); } + OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value; } + else if (configEntry == MatchInfoGuideScaleFactor && MatchInfoGuide.Instance) + { + RoleGuidePatches.ApplyGuideScale(MatchInfoGuide.Instance, MatchInfoGuideScaleFactor.Value); + } else if (configEntry == WikiOnBottomRow) { SetUpButtonPositions(); @@ -78,22 +83,30 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) } } + /// + /// Sets up the button positions in the UI. + /// public static void SetUpButtonPositions() { var topUi = MiraHudHelper.UiTopRight; var extraTopUi = MiraHudHelper.ExtraUiTopRight; - if (topUi && extraTopUi) - { - var genericEvent = new UiButtonResetEvent(); - MiraEventManager.InvokeEvent(genericEvent); - var genericEvent2 = new UiButtonPostResetEvent(topUi, extraTopUi); - MiraEventManager.InvokeEvent(genericEvent2); - MiraHudHelper.UiGrid.ArrangeChilds(); - MiraHudHelper.ExtraUiGrid.ArrangeChilds(); - } + if (!topUi || !extraTopUi) + return; + + var genericEvent = new UiButtonResetEvent(); + MiraEventManager.InvokeEvent(genericEvent); + + var genericEvent2 = new UiButtonPostResetEvent(topUi, extraTopUi); + MiraEventManager.InvokeEvent(genericEvent2); + MiraHudHelper.UiGrid.ArrangeChilds(); + MiraHudHelper.ExtraUiGrid.ArrangeChilds(); } + /// + /// Resizes the UI as a coroutine. + /// + /// The coroutine that resizes the UI. public static IEnumerator CoResizeSettingsUI() { while (!HudManager.Instance || !MiraHudHelper.UiGrid || !MiraHudHelper.ExtraUiGrid) @@ -105,6 +118,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) 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; @@ -123,6 +140,7 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) { continue; } + if (button.transform.name.Contains("Friends List Button")) { button.gameObject.transform.localScale = new Vector3(0.2675f * actualScaleVal, 0.2675f * actualScaleVal, 1); @@ -137,60 +155,58 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) { baseGrid.ArrangeChilds(); } + baseAspect.AdjustPosition(); } var extraAspect = MiraHudHelper.ExtraUiAspectPos; var extraGrid = MiraHudHelper.ExtraUiGrid; var extraUi = MiraHudHelper.ExtraUiTopRight; - if (extraUi && extraAspect && extraGrid) + if (!extraUi || !extraAspect || !extraGrid) return; + extraAspect.DistanceFromEdge = new Vector3(0.435f * scaleFactor, 1.25f * scaleFactor, 0f); + + foreach (var button in extraUi.GetAllChildren()) { - extraAspect.DistanceFromEdge = new Vector3(0.435f * scaleFactor, 1.25f * scaleFactor, 0f); - - foreach (var button in extraUi.GetAllChildren()) + if (button.gameObject == null) { - 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; + continue; } - extraGrid.CellSize = new Vector2(alteredScale, alteredScale); - if (extraGrid.gameObject.transform.childCount != 0) + if (button.transform.name.Contains("Modifiers")) { - extraGrid.ArrangeChilds(); + button.gameObject.transform.localScale = new Vector3(0.65f * scaleFactor, 0.65f * scaleFactor, 1); + continue; } - extraAspect.AdjustPosition(); + + 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("MiraApi.VisualsUi", "MiraApi.ButtonScaleFactor", 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); + 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("MiraApi.VisualsUi", "Max FPS", 120f); + public ConfigEntry SetFpsSlider { get; private set; } = config.Bind("MiraApi.VisualsUi", "Max FPS", 120f); /// /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. @@ -204,9 +220,24 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) [LocalToggleSetting] public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.ShowKeybindsOnButtons", true); + /// + /// Gets the scale of the match info guide. + /// + [LocalSliderSetting(min: 0.5f, max: 1f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)] + public ConfigEntry MatchInfoGuideScaleFactor { get; private set; } = config.Bind("MiraApi.MatchInfoGuide", "MiraApi.MatchInfoGuideScaleFactor", 1f); + + /// + /// 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); + public ConfigEntry WikiOnBottomRow { get; private set; } = config.Bind("MiraApi.MatchInfoGuide", "MiraApi.WikiOnBottomRow", true); + + /// + /// Gets the flag indicating the position for the wiki's tabs. + /// + [LocalEnumSetting(names: ["MiraApi.WikiTabPosition.LeftSide", "MiraApi.WikiTabPosition.RightSide", "MiraApi.WikiTabPosition.Below"])] + public ConfigEntry WikiTabPosition { get; private set; } = + config.Bind("MiraApi.MatchInfoGuide", "MiraApi.WikiTabPosition", MiraApiPlugin.IsMobile ? MatchInfoTabPos.RightSide : MatchInfoTabPos.LeftSide); /// /// Gets whether to show the red flash from sabotages. @@ -219,4 +250,31 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) /// [LocalToggleSetting] public ConfigEntry EnableSabotageBlares { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableSabotageBlare", true); + + /// + /// Gets whether variable colors will remain static or not. + /// + [LocalToggleSetting] + public ConfigEntry EnableVariableColors { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableVariableColors", true); +} + +/// +/// An enum that dictates the location of the match info tab buttons. +/// +public enum MatchInfoTabPos +{ + /// + /// The left side. + /// + LeftSide, + + /// + /// The right side. + /// + RightSide, + + /// + /// Below the menu. + /// + Below, } diff --git a/MiraAPI/ModCompatibility.cs b/MiraAPI/ModCompatibility.cs index ebb1081..870c1fe 100644 --- a/MiraAPI/ModCompatibility.cs +++ b/MiraAPI/ModCompatibility.cs @@ -14,26 +14,44 @@ public static class ModCompatibility /// public const string SubmergedId = "Submerged"; + /// + /// Gets the of the Submerged mod. + /// public static Version SubVersion { get; private set; } + + /// + /// Gets a value indicating whether Submerged is loaded. + /// public static bool SubLoaded { get; private set; } + + /// + /// Gets the of the Submerged mod. + /// public static BasePlugin SubPlugin { get; private set; } + + /// + /// Gets the of the Submerged mod. + /// public static Assembly SubAssembly { get; private set; } + /// + /// The map ID of Submerged. + /// public const ShipStatus.MapType SubmergedMapType = (ShipStatus.MapType)6; - public static void Initialize() + internal static void Initialize() { InitSubmerged(); } - public static void InitSubmerged() + private static void InitSubmerged() { if (!IL2CPPChainloader.Instance.Plugins.TryGetValue(SubmergedId, out var plugin)) { return; } - SubPlugin = (plugin!.Instance as BasePlugin)!; + SubPlugin = (plugin.Instance as BasePlugin)!; SubVersion = plugin.Metadata.Version; SubAssembly = SubPlugin.GetType().Assembly; @@ -42,6 +60,10 @@ public static class ModCompatibility Message("Submerged was detected"); } + /// + /// Checks to see if the current map is the Submerged map. + /// + /// true if the current map is Submerged. public static bool IsSubmerged() { return SubLoaded && ShipStatus.Instance && ShipStatus.Instance.Type == SubmergedMapType; diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 930cc29..3daeb56 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,7 +1,13 @@ using System; +using System.Collections.Generic; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameOptions; using MiraAPI.PluginLoading; +using MiraAPI.Roles; +using MiraAPI.Translation; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using TMPro; using UnityEngine; namespace MiraAPI.Modifiers; @@ -41,19 +47,101 @@ public abstract class BaseModifier : IOptionable /// public MiraPluginInfo ParentMod => Array.Find( MiraPluginManager.Instance.RegisteredPlugins, - x => x.InternalModifiers.Exists(y => y.TypeId == TypeId) - ) ?? throw new InvalidOperationException("Modifier is not registered."); + x => x.InternalModifiers.Exists(y => y.TypeId == TypeId)) + ?? throw new InvalidOperationException("Modifier is not registered."); + + /// + /// Gets the id part used to build the modifier's translation keys. It is recommended to simply call it what the modifier is, as other mods may utilize it. + /// + public virtual string IdPart => GetType().Name; + + /// + /// Gets the id part used to build the modifier's translation keys. It is recommended to call it ModGuid.Modifier.Type. + /// + public virtual string IdPrefix => GetType().Namespace!; /// /// Gets the modifier name. /// - public abstract string ModifierName { get; } + public virtual string ModifierName => MiraLocaleManager.Get(ModifierNameLocale); + + /// + /// Gets the modifier's name id for localization. + /// + public virtual string ModifierNameLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart); + + /// + /// Gets the medium description of the modifier. Used in the modifier guide and options menu. + /// + public virtual string ModifierMedDescription => MiraLocaleManager.Get(ModifierMedDescriptionLocale, GetDescription()); + + /// + /// Gets the modifier's medium description id for localization. + /// + public virtual string ModifierMedDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "MedDescription"); + + /// + /// Gets the wiki description of the modifier. Used in the wiki and normally appends the options text as well. + /// + public virtual string ModifierWikiDescription => MiraLocaleManager.Get(ModifierWikiDescriptionLocale, GetDescription()) + + Helpers.GetOptionsText(GetType()); + + /// + /// Gets the modifier's wiki description id for localization. + /// + public virtual string ModifierWikiDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "WikiDescription"); + + /// + /// Gets the category of the modifier. Used in the wiki. + /// + public virtual string ModifierCategoryTitle => MiraLocaleManager.Get("Modifier"); + + /// + /// Gets whether the modifier is forcibly shown or disabled in the wiki screen. + /// + /// if the modifier is always displayed, otherwise if it is never displayable, or if it is dictated by amount and chance. + public virtual bool? ForceShowModifierOnWiki => false; + + /// + /// Gets the information to display in the wiki. + /// + /// The guide object. + /// The title text object. + /// The title's icon. + /// The scroller parent. + /// The of the wiki. + public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent) + { + var abilities = WikiAbilities; + return Helpers.CreateAdvancedWikiPage( + guide, + titleText, + titleIcon, + parent, + ModifierNameLocale, + $"{ModifierName} ({ModifierCategoryTitle})".TrimStart(), + ModifierIcon ?? MiraAssets.IconNeutral.LoadAsset(), + ModifierWikiDescription, + abilities); + } + + /// + /// Gets the list of abilities or other similar information to display below the text of a modifier's wiki page. + /// + [HideFromIl2Cpp] + public virtual List WikiAbilities => []; /// /// Gets the modifier icon. Useless if is . /// public virtual LoadableAsset? ModifierIcon => null; + /// + /// Gets the for the Modifier Icon. + /// + [HideFromIl2Cpp] + public virtual TMP_SpriteAsset IconTmp => null!; + /// /// Gets a value indicating whether the modifier is hidden on the UI. Will be hidden either way if no description is provided. /// @@ -69,6 +157,11 @@ public abstract class BaseModifier : IOptionable /// public virtual Color FreeplayFileColor => Color.gray; + /// + /// Gets the of the modifier, used in the wiki and other UI outside freeplay. + /// + public virtual Color GeneralColor => FreeplayFileColor; + /// /// Gets a value indicating whether the modifier is unique. If , the player can only have one instance of this modifier. /// @@ -78,7 +171,10 @@ public abstract class BaseModifier : IOptionable /// Gets the HUD description for this modifier. Does nothing if is . Required to be visible on UI. /// /// The description string for the HUD. - public virtual string GetDescription() => string.Empty; + public virtual string GetDescription() + { + return string.Empty; + } /// /// Called when the modifier is activated. @@ -127,5 +223,16 @@ public abstract class BaseModifier : IOptionable /// Determines whether the player can vent. /// /// if the player can vent, otherwise. for no effect. - public virtual bool? CanVent() => null; + public virtual bool? CanVent() + { + return null; + } + + /// + /// Removes this modifier instance from the player. + /// + public void RemoveSelf() + { + ModifierComponent?.RemoveModifier(this); + } } diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 1d7e5d7..aeebfa2 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -8,8 +8,6 @@ using MiraAPI.LocalSettings; using MiraAPI.Modifiers.ModifierDisplay; using MiraAPI.Modifiers.Types; using MiraAPI.Patches.Roles; -using MiraAPI.Translation; -using Reactor.Utilities; using Reactor.Utilities.Attributes; using UnityEngine; @@ -19,6 +17,7 @@ namespace MiraAPI.Modifiers; /// The component for handling s. /// [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity Convention.")] public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) { /// @@ -27,6 +26,10 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public ImmutableList ActiveModifiers { get; private set; } = ImmutableList.Empty; + private readonly List _toRemove = []; + + private readonly List _toAdd = []; + private ModifierDisplayComponent? ModifierDisplay { get; set; } [HideFromIl2Cpp] @@ -34,10 +37,6 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) private PlayerControl _player = null!; - private readonly List _toRemove = []; - - private readonly List _toAdd = []; - internal void ClearModifiers() { _toRemove.AddRange(Modifiers); @@ -172,7 +171,8 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The Type of the e. /// An of s. [HideFromIl2Cpp] - public IEnumerable GetModifiers(Func? predicate=null) where T : BaseModifier + public IEnumerable GetModifiers(Func? predicate = null) + where T : BaseModifier { return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x)); } @@ -184,7 +184,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the by. /// An of s. [HideFromIl2Cpp] - public IEnumerable GetModifiers(Type type, Func? predicate=null) + public IEnumerable GetModifiers(Type type, Func? predicate = null) { return ActiveModifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))); } @@ -196,7 +196,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the by. /// An of s. [HideFromIl2Cpp] - public IEnumerable GetModifiers(uint id, Func? predicate=null) + public IEnumerable GetModifiers(uint id, Func? predicate = null) { var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( $"Cannot get modifier with id {id} because it is not registered."); @@ -204,6 +204,19 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return GetModifiers(type, predicate); } + /// + /// Gets a collection of s by type, if the type is an interface. + /// + /// The predicate to check the by. + /// The Type of the interface of the . + /// An of s of type . + [HideFromIl2Cpp] + public IEnumerable GetModifiersOfType(Func? predicate = null) + where T : class + { + return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x)); + } + /// /// Tries to get a . /// @@ -212,7 +225,8 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The Type of the . /// if the was found, otherwise. [HideFromIl2Cpp] - public bool TryGetModifier([NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : BaseModifier + public bool TryGetModifier([NotNullWhen(true)] out T? modifier, Func? predicate = null) + where T : BaseModifier { modifier = GetModifier(predicate); return modifier != null; @@ -259,6 +273,21 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return modifier != null; } + /// + /// Tries to get a by its type, if the type is an interface. + /// + /// The of type or null. + /// The predicate to check the by. + /// The Type of the interface of the . + /// if the of type was found, otherwise. + [HideFromIl2Cpp] + public bool TryGetModifierOfType([NotNullWhen(true)] out T? modifier, Func? predicate = null) + where T : class + { + modifier = GetModifierOfType(predicate); + return modifier != null; + } + /// /// Gets a . /// @@ -266,7 +295,8 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The Type of the . /// The if it is found, otherwise. [HideFromIl2Cpp] - public T? GetModifier(Func? predicate = null) where T : BaseModifier + public T? GetModifier(Func? predicate = null) + where T : BaseModifier { return GetModifiers(predicate).FirstOrDefault(); } @@ -309,13 +339,27 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return ActiveModifiers.Find(x => x.UniqueId == modifierGuid); } + /// + /// Gets a by its type, if the type is an interface. + /// + /// The predicate to check the by. + /// The Type of the interface of the . + /// The of type if it is found, otherwise. + [HideFromIl2Cpp] + public T? GetModifierOfType(Func? predicate = null) + where T : class + { + return GetModifiersOfType(predicate).FirstOrDefault(); + } + /// /// Removes a from the player. /// /// The type. /// The predicate to check the by. [HideFromIl2Cpp] - public void RemoveModifier(Func? predicate = null) where T : BaseModifier + public void RemoveModifier(Func? predicate = null) + where T : BaseModifier { RemoveModifier(typeof(T), x => predicate == null || predicate((T)x)); } @@ -390,6 +434,78 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) RemoveModifier(modifier); } + /// + /// Tries to remove a from the player. + /// + /// The type. + /// The predicate to check the by. + /// if the is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Func? predicate = null) + where T : BaseModifier + { + return TryGetModifier(out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a from the player. + /// + /// The type. + /// The predicate to check the modifier by. + /// if the is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Type type, Func? predicate = null) + { + return TryGetModifier(type, out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a from the player. + /// + /// The object. + /// if the is not active on this player, else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(BaseModifier modifier) + { + if (!ActiveModifiers.Contains(modifier)) + { + return false; + } + + _toRemove.Add(modifier); + return true; + } + + /// + /// Tries to remove a from the player. + /// + /// The 's type ID. + /// The predicate to check the by. + /// if the is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(uint typeId, Func? predicate = null) + { + return TryGetModifier(typeId, out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a from the player. + /// + /// The 's unique ID. + /// if the is not active on this player, else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Guid uniqueId) + { + return TryGetModifier(uniqueId, out var modifier) && + TryRemoveModifier(modifier); + } + /// /// Adds a to the player. /// @@ -397,7 +513,8 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The Type of the . /// The new . [HideFromIl2Cpp] - public T? AddModifier(params object[] args) where T : BaseModifier + public T? AddModifier(params object[] args) + where T : BaseModifier { return AddModifier(typeof(T), args) as T; } @@ -436,6 +553,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) { modifier.UniqueId = Guid.NewGuid(); } + return modifier; } @@ -448,20 +566,10 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? AddModifier(Type type, params object[] args) { - BaseModifier? modifier; - if (args.Length > 0) - { - modifier = ModifierFactory.CreateInstance(type, args); - } - else - { - modifier = Activator.CreateInstance(type) as BaseModifier; - if (modifier == null) - { - throw new InvalidOperationException($"Cannot add modifier {type.Name} because it is not a valid modifier."); - } - } - + var modifier = args.Length > 0 + ? ModifierFactory.CreateInstance(type, args) + : Activator.CreateInstance(type) as BaseModifier + ?? throw new InvalidOperationException($"Cannot add modifier {type.Name} because it is not a valid modifier."); return AddModifier(modifier); } @@ -487,7 +595,8 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The Type of the . /// if the is present, otherwise. [HideFromIl2Cpp] - public bool HasModifier(Func? predicate=null) where T : BaseModifier + public bool HasModifier(Func? predicate = null) + where T : BaseModifier { return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); } @@ -499,7 +608,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the . /// if the is present, otherwise. [HideFromIl2Cpp] - public bool HasModifier(Type type, Func? predicate=null) + public bool HasModifier(Type type, Func? predicate = null) { return ActiveModifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x))); } @@ -511,7 +620,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the . /// if the is present, otherwise. [HideFromIl2Cpp] - public bool HasModifier(uint id, Func? predicate=null) + public bool HasModifier(uint id, Func? predicate = null) { var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( $"Cannot get modifier with id {id} because it is not registered."); @@ -538,10 +647,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The Type of the . /// if the is present, otherwise. [HideFromIl2Cpp] - public bool HasModifier(bool checkInactive, Func? predicate=null) where T : BaseModifier + public bool HasModifier(bool checkInactive, Func? predicate = null) + where T : BaseModifier { return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr)); - bool MatchExpr(BaseModifier bm) => bm is T modifier && (predicate == null || predicate(modifier)); + bool MatchExpr(BaseModifier bm) + { + return bm is T modifier && (predicate == null || predicate(modifier)); + } } /// @@ -552,10 +665,13 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the . /// if the is present, otherwise. [HideFromIl2Cpp] - public bool HasModifier(Type type, bool checkInactive, Func? predicate=null) + public bool HasModifier(Type type, bool checkInactive, Func? predicate = null) { return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr)); - bool MatchExpr(BaseModifier bm) => bm.GetType() == type && (predicate == null || predicate(bm)); + bool MatchExpr(BaseModifier bm) + { + return bm.GetType() == type && (predicate == null || predicate(bm)); + } } /// @@ -566,7 +682,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the . /// if the is present, otherwise. [HideFromIl2Cpp] - public bool HasModifier(uint id, bool checkInactive, Func? predicate=null) + public bool HasModifier(uint id, bool checkInactive, Func? predicate = null) { var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( $"Cannot get modifier with id {id} because it is not registered."); @@ -584,6 +700,40 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) public bool HasModifier(Guid id, bool checkInactive) { return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr)); - bool MatchExpr(BaseModifier bm) => bm.UniqueId == id; + bool MatchExpr(BaseModifier bm) + { + return bm.UniqueId == id; + } + } + + /// + /// Checks if a player has an active modifier by its type, if the type is an interface. + /// + /// The predicate to check the modifier. + /// The Type of the interface of the Modifier. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifierOfType(Func? predicate = null) + where T : class + { + return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); + } + + /// + /// Checks if a player has an active modifier by its type, if the type is an interface. + /// + /// Whether to check inactive modifiers (those pending to be added). + /// The predicate to check the modifier. + /// The Type of the interface of the Modifier. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifierOfType(bool checkInactive, Func? predicate = null) + where T : class + { + return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr)); + bool MatchExpr(BaseModifier bm) + { + return bm is T modifier && (predicate == null || predicate(modifier)); + } } } diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs index 73922f3..84defb8 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.Attributes; @@ -19,13 +20,18 @@ namespace MiraAPI.Modifiers.ModifierDisplay; /// The code used to display s. /// [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity Convention.")] public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) { + private const int ItemsPerPage = 3; + /// /// Gets the instance of the . /// public static ModifierDisplayComponent? Instance { get; private set; } + private readonly Dictionary _modifiers = []; + private RectTransform _children = null!; private GameObject _modTemplate = null!; private PassiveButton _toggleButton = null!; @@ -37,9 +43,6 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) private PassiveButton _backButton = null!; private int _currentPage; - private const int ItemsPerPage = 3; - - private readonly Dictionary _modifiers = []; /// /// Gets a of the created s. @@ -97,15 +100,13 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) IsOpen = false; _children.gameObject.SetActive(false); - if (LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value) - { - var aspect = GetComponent(); - _toggleButton.transform.localPosition = new Vector3(-1f, 2.7f, 0f); - _children.transform.localPosition = new Vector3(-0.2f, -0.05f, 0f); - aspect.Alignment = AspectPosition.EdgeAlignments.LeftTop; - aspect.DistanceFromEdge = new Vector3(1.8f, 2.55f, -20f); - aspect.AdjustPosition(); - } + if (!LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value) return; + var aspect = GetComponent(); + _toggleButton.transform.localPosition = new Vector3(-1f, 2.7f, 0f); + _children.transform.localPosition = new Vector3(-0.2f, -0.05f, 0f); + aspect.Alignment = AspectPosition.EdgeAlignments.LeftTop; + aspect.DistanceFromEdge = new Vector3(1.8f, 2.55f, -20f); + aspect.AdjustPosition(); } /// @@ -175,7 +176,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) _pagination.gameObject.SetActive(true); if (_pageText != null) { - _pageText.text = $"Page {_currentPage + 1}/{Mathf.Max(totalPages, 1)}"; + _pageText.text = $"{MiraLocaleManager.Get("MiraApi.ModifierDisplay.Page", "Page")} {_currentPage + 1}/{Mathf.Max(totalPages, 1)}"; } _nextButton.gameObject.SetActive(true); @@ -212,7 +213,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) _modifiers.Remove(mod.Key); } - _toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})"; + _toggleBtnText.text = $"{MiraLocaleManager.Get("Modifiers", "Modifiers")} ({filteredModifiers.Count})"; _toggleButton.gameObject.SetActive(filteredModifiers.Count != 0); diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs index cafbd1b..cfe6319 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs @@ -1,5 +1,5 @@ -using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Translation; +using System.Diagnostics.CodeAnalysis; +using Il2CppInterop.Runtime.Attributes; using Reactor.Utilities.Attributes; using TMPro; using UnityEngine; @@ -10,6 +10,7 @@ namespace MiraAPI.Modifiers.ModifierDisplay; /// The code placed on every HUD object. Used to handle updating. /// [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity Convention.")] public class ModifierUiComponent(nint cppPtr) : MonoBehaviour(cppPtr) { /// @@ -24,6 +25,7 @@ public class ModifierUiComponent(nint cppPtr) : MonoBehaviour(cppPtr) private SpriteRenderer icon = null!; private TextMeshPro nameText = null!; private RectTransform descRect = null!; + // ReSharper restore InconsistentNaming private void Awake() { diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index e70a26a..47875a2 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -52,7 +52,8 @@ public static class ModifierExtensions /// The to add the to. /// The arguments to initialize the with. /// The Type. - public static void RpcAddModifier(this PlayerControl player, params object[] args) where T : BaseModifier + public static void RpcAddModifier(this PlayerControl player, params object[] args) + where T : BaseModifier { player.RpcAddModifier(typeof(T), args); } @@ -147,7 +148,8 @@ public static class ModifierExtensions /// The instance. /// Optional predicate to filter the s. /// if the has the , otherwise. - public static bool HasModifier(this PlayerControl player, Func? predicate = null) where T : BaseModifier + public static bool HasModifier(this PlayerControl player, Func? predicate = null) + where T : BaseModifier { return player.GetModifierComponent().HasModifier(predicate); } @@ -190,6 +192,19 @@ public static class ModifierExtensions return player.GetModifierComponent().HasModifier(uniqueId); } + /// + /// Checks if the has a specific , if the type is an interface. + /// + /// The type of the interface of the . + /// The instance. + /// Optional predicate to filter the s. + /// if the has the of type , otherwise. + public static bool HasModifierOfType(this PlayerControl player, Func? predicate = null) + where T : class + { + return player.GetModifierComponent().HasModifierOfType(predicate); + } + /// /// Clears all s from the . /// @@ -222,7 +237,8 @@ public static class ModifierExtensions /// The predicate to check the by. /// The Type of the . /// if the was found, otherwise. - public static bool TryGetModifier(this PlayerControl player, [NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : BaseModifier + public static bool TryGetModifier(this PlayerControl player, [NotNullWhen(true)] out T? modifier, Func? predicate = null) + where T : BaseModifier { return player.GetModifierComponent().TryGetModifier(out modifier, predicate); } @@ -265,6 +281,20 @@ public static class ModifierExtensions return player.GetModifierComponent().TryGetModifier(modifierGuid, out modifier); } + /// + /// Tries to get a by its type, if the type is an interface. + /// + /// The instance. + /// The of type or null. + /// The predicate to check the of type by. + /// The Type of the interface of the . + /// if the of type was found, otherwise. + public static bool TryGetModifierOfType(this PlayerControl player, [NotNullWhen(true)] out T? modifier, Func? predicate = null) + where T : class + { + return player.GetModifierComponent().TryGetModifierOfType(out modifier, predicate); + } + /// /// Gets a specific from the . /// @@ -272,7 +302,8 @@ public static class ModifierExtensions /// The instance. /// Optional predicate to filter the s. /// The if found, otherwise. - public static T? GetModifier(this PlayerControl player, Func? predicate = null) where T : BaseModifier + public static T? GetModifier(this PlayerControl player, Func? predicate = null) + where T : BaseModifier { return player.GetModifierComponent().GetModifier(predicate); } @@ -320,6 +351,19 @@ public static class ModifierExtensions return player.GetModifierComponent().GetModifier(uniqueId); } + /// + /// Gets a specific of type from the , if the type is an interface. + /// + /// The type of the interface of the modifier. + /// The instance. + /// Optional predicate to filter the s. + /// The of type if found, otherwise. + public static T? GetModifierOfType(this PlayerControl player, Func? predicate = null) + where T : class + { + return player.GetModifierComponent().GetModifierOfType(predicate); + } + /// /// Gets all s from the . /// @@ -363,6 +407,19 @@ public static class ModifierExtensions return player.GetModifierComponent().GetModifiers(typeId, predicate); } + /// + /// Gets all s of a specific type from the , if the type is an interface. + /// + /// The type of the interface of the s. + /// The PlayerControl instance. + /// Optional predicate to filter the s. + /// An of s of type . + public static IEnumerable GetModifiersOfType(this PlayerControl player, Func? predicate = null) + where T : class + { + return player.GetModifierComponent().GetModifiersOfType(predicate); + } + /// /// Removes a specific from the . /// @@ -422,6 +479,73 @@ public static class ModifierExtensions player.GetModifierComponent().RemoveModifier(uniqueId); } + /// + /// Tries to remove a specific from the . + /// + /// The instance. + /// The to remove. + /// if the is not active on this , else . + public static bool TryRemoveModifier(this PlayerControl player, BaseModifier modifier) + { + return player.GetModifierComponent().TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a specific from the . + /// + /// The type of the . + /// The instance. + /// Optional predicate to filter the s. + /// if the is not active on this , or there are multiple instances; + /// else . + public static bool TryRemoveModifier(this PlayerControl player, Func? predicate = null) + where T : BaseModifier + { + return player.GetModifierComponent().TryRemoveModifier(predicate); + } + + /// + /// Tries to remove a specific from the by type. + /// + /// The instance. + /// The type of the . + /// Optional predicate to filter the s. + /// if the is not active on this , or there are multiple instances; + /// else . + public static bool TryRemoveModifier(this PlayerControl player, Type type, Func? predicate = null) + { + return player.GetModifierComponent().TryRemoveModifier(type, predicate); + } + + /// + /// Tries to remove a specific from the by its type ID. + /// + /// The instance. + /// The type ID of the . + /// Optional predicate to filter the s. + /// if the is not active on this , or there are multiple instances; + /// else . + public static bool TryRemoveModifier( + this PlayerControl player, + uint typeId, + Func? predicate = null) + { + return player.GetModifierComponent().TryRemoveModifier(typeId, predicate); + } + + /// + /// Tries to remove a specific from the by its GUID. + /// + /// The instance. + /// The GUID of the . + /// if the is not active on this , else . + public static bool TryRemoveModifier( + this PlayerControl player, + Guid uniqueId) + { + return player.GetModifierComponent().TryRemoveModifier(uniqueId); + } + /// /// Adds a specific to the . /// @@ -429,7 +553,8 @@ public static class ModifierExtensions /// The instance. /// The arguments to initialize the with. /// The added . - public static T? AddModifier(this PlayerControl player, params object[] args) where T : BaseModifier + public static T? AddModifier(this PlayerControl player, params object[] args) + where T : BaseModifier { return player.GetModifierComponent().AddModifier(args); } diff --git a/MiraAPI/Modifiers/ModifierFactory.cs b/MiraAPI/Modifiers/ModifierFactory.cs index cd3c9be..780eb0c 100644 --- a/MiraAPI/Modifiers/ModifierFactory.cs +++ b/MiraAPI/Modifiers/ModifierFactory.cs @@ -63,7 +63,8 @@ public static class ModifierFactory /// Factory for creating instances of . More efficient than using reflection. /// /// The type. -public static class ModifierFactory where T : BaseModifier +public static class ModifierFactory + where T : BaseModifier { private static readonly Func Constructor = CreateConstructor(); diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index ee0fb5e..935c82e 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Runtime.Serialization; using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; -using MiraAPI.Translation; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; using Random = System.Random; @@ -58,12 +58,7 @@ public static class ModifierManager /// The ID of the modifier. public static uint? GetModifierTypeId(Type type) { - if (!TypeToIdModifierMap.TryGetValue(type, out var id)) - { - return null; - } - - return id; + return !TypeToIdModifierMap.TryGetValue(type, out var id) ? null : id; } internal static bool RegisterModifier(Type modifierType, MiraPluginInfo info) @@ -122,7 +117,7 @@ public static class ModifierManager // Filter and sort modifiers by descending priority. var modifiers = IdToTypeModifierMap - .Where(x=>x.Value.IsAssignableTo(typeof(GameModifier))) + .Where(x => x.Value.IsAssignableTo(typeof(GameModifier))) .Select(x => Activator.CreateInstance(x.Value) as GameModifier) .OfType() .Where(x => x.GetAmountPerGame() > 0 && x.GetAssignmentChance() > 0) @@ -141,6 +136,7 @@ public static class ModifierManager { Warning($"No valid players for modifier {modifier.ModifierName.Translate()}"); } + continue; } @@ -193,6 +189,7 @@ public static class ModifierManager !player.HasModifier(modifierId) && modifier.IsModifierValidOn(player.Data.Role) && modifier.CanSpawnOnCurrentMode(); } + private static bool IsGameModifierPostCheck(PlayerControl player, GameModifier modifier, uint modifierId) { return (player.Data.Role is not ICustomRole role || role.IsModifierApplicable(modifier)) && diff --git a/MiraAPI/Modifiers/ModifierUtils.cs b/MiraAPI/Modifiers/ModifierUtils.cs index f536489..4d499c9 100644 --- a/MiraAPI/Modifiers/ModifierUtils.cs +++ b/MiraAPI/Modifiers/ModifierUtils.cs @@ -15,9 +15,10 @@ public static class ModifierUtils /// Select if is valid to be added to list. /// The type. /// An of s. - public static IEnumerable GetActiveModifiers(Func? predicate = null) where T : BaseModifier + public static IEnumerable GetActiveModifiers(Func? predicate = null) + where T : BaseModifier { - return PlayerControl.AllPlayerControls.ToArray().SelectMany(x => x.GetModifiers(predicate)).OfType(); + return PlayerControl.AllPlayerControls.ToArray().SelectMany(x => x.GetModifiers(predicate)); } /// @@ -26,8 +27,9 @@ public static class ModifierUtils /// Select if is valid. /// The type. /// An of s with that . - public static IEnumerable GetPlayersWithModifier(Func? predicate = null) where T : BaseModifier + public static IEnumerable GetPlayersWithModifier(Func? predicate = null) + where T : BaseModifier { - return PlayerControl.AllPlayerControls.ToArray().Where(x => x.HasModifier(predicate)); + return PlayerControl.AllPlayerControls.ToArray().Where(x => x.HasModifier(predicate)); } } diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index 051ae07..b432920 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -9,6 +9,9 @@ namespace MiraAPI.Modifiers.Types; [MiraIgnore] public abstract class GameModifier : BaseModifier { + /// + public override bool? ForceShowModifierOnWiki => null; + /// public override bool ShowInFreeplay => true; @@ -28,32 +31,47 @@ public abstract class GameModifier : BaseModifier /// Gets the priority at which the modifier will spawn. The higher the value, the higher up on the assignment list. /// /// An value greater than or equal to -1. - public virtual int Priority() => -1; + public virtual int Priority() + { + return -1; + } /// /// Determines whether the modifier is valid on a role. /// /// The role to be checked. /// if the modifier is valid on the role, otherwise . - public virtual bool IsModifierValidOn(RoleBehaviour role) => true; + public virtual bool IsModifierValidOn(RoleBehaviour role) + { + return true; + } /// /// Determines whether the modifier is valid on a role, which runs after players are determined but before the modifiers are given. /// /// The role to be checked. /// if the modifier is valid on the role, otherwise . - public virtual bool IsModifierValidOnPostCheck(RoleBehaviour role) => IsModifierValidOn(role); + public virtual bool IsModifierValidOnPostCheck(RoleBehaviour role) + { + return IsModifierValidOn(role); + } /// /// Determines whether the player won the game with this modifier. The modifier with the highest priority will override other modifiers. /// /// The reason why the game ended. /// if the player won, if they lost. Return to use the player's default win condition. - public virtual bool? DidWin(GameOverReason reason) => null; + public virtual bool? DidWin(GameOverReason reason) + { + return null; + } /// /// Determines whether the modifier can spawn in general, accounting for gamemodes and everything else. /// /// if the modifier is able to spawn, otherwise . - public virtual bool CanSpawnOnCurrentMode() => CustomGameModeManager.IsClassic() && !GameManager.Instance.IsHideAndSeek(); + public virtual bool CanSpawnOnCurrentMode() + { + return CustomGameModeManager.IsClassic() && !GameManager.Instance.IsHideAndSeek(); + } } diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index cbcb13d..526a452 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -1,6 +1,5 @@ using System; using MiraAPI.PluginLoading; -using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Modifiers.Types; diff --git a/MiraAPI/Networking/CustomGameOverRpc.cs b/MiraAPI/Networking/CustomGameOverRpc.cs index 6475d6a..f6b77e8 100644 --- a/MiraAPI/Networking/CustomGameOverRpc.cs +++ b/MiraAPI/Networking/CustomGameOverRpc.cs @@ -12,7 +12,10 @@ namespace MiraAPI.Networking; /// /// [RegisterCustomRpc((uint)MiraRpc.CustomGameOver)] +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.) +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by Reactor.) public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +#pragma warning restore SA1515 // Single-line comment should be preceded by blank line { /// public override RpcLocalHandling LocalHandling => RpcLocalHandling.After; diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index e840d00..4be8723 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -81,6 +81,7 @@ public static class CustomMurderRpc { return; } + var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError; var beforeMurderEvent = new BeforeMurderEvent(source, target, inMeeting); @@ -222,6 +223,7 @@ public static class CustomMurderRpc { return; } + var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError; var beforeMurderEvent = new BeforeMurderEvent(source, target, isIndirect, ignoreDefense, inMeeting); @@ -351,6 +353,7 @@ public static class CustomMurderRpc { return; } + var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError; var beforeMurderEvent = new BeforeMurderEvent(source, target, framed, isIndirect, ignoreDefense, inMeeting); @@ -745,6 +748,7 @@ public static class CustomMurderRpc { yield break; } + var cam = Camera.main?.GetComponent(); var playerToLunge = framed ?? source; var isParticipant = playerToLunge.AmOwner || target.AmOwner; diff --git a/MiraAPI/Networking/Modifiers/AddModifierRpc.cs b/MiraAPI/Networking/Modifiers/AddModifierRpc.cs index d3bcaee..5b29282 100644 --- a/MiraAPI/Networking/Modifiers/AddModifierRpc.cs +++ b/MiraAPI/Networking/Modifiers/AddModifierRpc.cs @@ -13,7 +13,10 @@ namespace MiraAPI.Networking.Modifiers; /// Mira plugin. /// RPC ID. [RegisterCustomRpc((uint)MiraRpc.AddModifier)] +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.) +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by Reactor.) public class AddModifierRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +#pragma warning restore SA1515 // Single-line comment should be preceded by blank line { /// public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before; @@ -57,19 +60,10 @@ public class AddModifierRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc 0) - { - modifier = ModifierFactory.CreateInstance(type, data.Args); - } - else - { - modifier = Activator.CreateInstance(type) as BaseModifier; - if (modifier == null) - { - throw new InvalidOperationException($"Cannot add modifier {type.Name} because it is not a valid modifier."); - } - } + var modifier = data.Args.Length > 0 + ? ModifierFactory.CreateInstance(type, data.Args) + : Activator.CreateInstance(type) as BaseModifier + ?? throw new InvalidOperationException($"Cannot add modifier {type.Name} because it is not a valid modifier."); modifier.UniqueId = data.UniqueId; player.AddModifier(modifier); } diff --git a/MiraAPI/Networking/NetData.cs b/MiraAPI/Networking/NetData.cs index ce5de46..aa9a52e 100644 --- a/MiraAPI/Networking/NetData.cs +++ b/MiraAPI/Networking/NetData.cs @@ -21,5 +21,8 @@ public readonly struct NetData(uint id, byte[] data) /// Gets the length of the data in bytes. /// /// An int representing the number of bytes this NetData takes up. - public int GetLength() => 4 + Data.Length; + public int GetLength() + { + return 4 + Data.Length; + } } diff --git a/MiraAPI/Networking/SyncOptionsRpc.cs b/MiraAPI/Networking/SyncOptionsRpc.cs index a80ace3..6de1933 100644 --- a/MiraAPI/Networking/SyncOptionsRpc.cs +++ b/MiraAPI/Networking/SyncOptionsRpc.cs @@ -5,9 +5,12 @@ using Reactor.Networking.Rpc; namespace MiraAPI.Networking; -// METHOD RPC DOESNT WORK WITH THE ARRAYS AND STUFF SO THIS IS HOW WE WILL DO IT FOR NOW +// METHOD RPC DOESN'T WORK WITH THE ARRAYS AND STUFF SO THIS IS HOW WE WILL DO IT FOR NOW [RegisterCustomRpc((uint)MiraRpc.SyncGameOptions)] +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.) +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by Reactor.) internal sealed class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +#pragma warning restore SA1515 // Single-line comment should be preceded by blank line { public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; diff --git a/MiraAPI/Networking/SyncRoleOptionsRpc.cs b/MiraAPI/Networking/SyncRoleOptionsRpc.cs index adfe537..fb0f2af 100644 --- a/MiraAPI/Networking/SyncRoleOptionsRpc.cs +++ b/MiraAPI/Networking/SyncRoleOptionsRpc.cs @@ -6,7 +6,10 @@ using Reactor.Networking.Rpc; namespace MiraAPI.Networking; [RegisterCustomRpc((uint)MiraRpc.SyncRoleOptions)] +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.) +// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by Reactor.) internal sealed class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +#pragma warning restore SA1515 // Single-line comment should be preceded by blank line { public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; diff --git a/MiraAPI/Patches/.editorconfig b/MiraAPI/Patches/.editorconfig index 1afcd96..3b3da0b 100644 --- a/MiraAPI/Patches/.editorconfig +++ b/MiraAPI/Patches/.editorconfig @@ -1,5 +1,16 @@ [*.{cs,vb}] + +# CA1707: Identifiers should not contain underscores. dotnet_diagnostic.CA1707.severity = none + +# CS1591: Missing XML comment for publicly visible type or member. dotnet_diagnostic.CS1591.severity = none + +# SA1611: Element parameters should be documented. dotnet_diagnostic.SA1611.severity = none -dotnet_diagnostic.SA1615.severity = none \ No newline at end of file + +# SA1615: Element return value should be documented. +dotnet_diagnostic.SA1615.severity = none + +# SA1602: Enumeration items should be documented. +dotnet_diagnostic.SA1602.severity = none diff --git a/MiraAPI/Patches/Accessibility/HudManagerFlashPatches.cs b/MiraAPI/Patches/Accessibility/HudManagerFlashPatches.cs index 2fda4c5..76dc0f4 100644 --- a/MiraAPI/Patches/Accessibility/HudManagerFlashPatches.cs +++ b/MiraAPI/Patches/Accessibility/HudManagerFlashPatches.cs @@ -1,12 +1,18 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; using MiraAPI.LocalSettings; using UnityEngine; namespace MiraAPI.Patches.Accessibility; + [HarmonyPatch] +[SuppressMessage("Style", "IDE0074:Use compound assignment", Justification = "Using compound assignment bypasses Unity lifetime checks.")] +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.) +// ReSharper disable ConvertIfStatementToNullCoalescingAssignment (Justification: Read above.) public static class HudManagerFlashPatches +#pragma warning restore SA1515 // Single-line comment should be preceded by blank line { [HarmonyPatch(typeof(HudManager), nameof(HudManager.StartReactorFlash))] [HarmonyPrefix] @@ -19,6 +25,7 @@ public static class HudManagerFlashPatches return false; } + [HarmonyPatch(typeof(HudManager), nameof(HudManager.StartOxyFlash))] [HarmonyPrefix] public static bool OxygenFlashPrefix(HudManager __instance) @@ -30,25 +37,28 @@ public static class HudManagerFlashPatches return false; } - public static IEnumerator CoReactorFlash() + + private static IEnumerator CoReactorFlash() { if (!HudManager.InstanceExists) { yield break; } + var hudManager = HudManager.Instance; - WaitForSeconds wait = new WaitForSeconds(1f); - bool light = false; + var wait = new WaitForSeconds(1f); + var light = false; hudManager.FullScreen.color = new Color(1f, 0f, 0f, 0.37254903f); while (true) { - var settins = LocalSettingsTabSingleton.Instance; - hudManager.FullScreen.gameObject.SetActive(settins.EnableSabotageFlashes.Value && !hudManager.FullScreen.gameObject.activeSelf); - if (settins.EnableSabotageBlares.Value) + var settings = LocalSettingsTabSingleton.Instance; + hudManager.FullScreen.gameObject.SetActive(settings.EnableSabotageFlashes.Value && !hudManager.FullScreen.gameObject.activeSelf); + if (settings.EnableSabotageBlares.Value) { - SoundManager.Instance.PlaySound(ShipStatus.Instance.SabotageSound, false, 1f, null); + SoundManager.Instance.PlaySound(ShipStatus.Instance.SabotageSound, false); } + light = !light; /*if (!MiraApiPlugin.IsMobile) diff --git a/MiraAPI/Patches/AmongUsClientSyncPatch.cs b/MiraAPI/Patches/AmongUsClientSyncPatch.cs index a8a3dfe..926004d 100644 --- a/MiraAPI/Patches/AmongUsClientSyncPatch.cs +++ b/MiraAPI/Patches/AmongUsClientSyncPatch.cs @@ -36,7 +36,7 @@ internal static class AmongUsClientSyncPatch GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value); } - [HarmonyPostfix] + [HarmonyPrefix] [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerLeft))] public static void PlayerLeftPatch(ClientData data, DisconnectReasons reason) { diff --git a/MiraAPI/Patches/ChatControllerPatch.cs b/MiraAPI/Patches/ChatControllerPatch.cs index c9a6565..95c388b 100644 --- a/MiraAPI/Patches/ChatControllerPatch.cs +++ b/MiraAPI/Patches/ChatControllerPatch.cs @@ -3,12 +3,10 @@ using UnityEngine; namespace MiraAPI.Patches; -#pragma warning disable SA1629 /// -/// Allows players to paste text into chat. -/// Source: +/// Allows players to paste text into chat.
+/// Source: . ///
-#pragma warning restore SA1629 [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] public static class ChatControllerPatch { diff --git a/MiraAPI/Patches/Colors/VariableColorPatches.cs b/MiraAPI/Patches/Colors/VariableColorPatches.cs new file mode 100644 index 0000000..98bed64 --- /dev/null +++ b/MiraAPI/Patches/Colors/VariableColorPatches.cs @@ -0,0 +1,110 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using AmongUs.Data; +using HarmonyLib; +using MiraAPI.Colors; +using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Patches.Colors; + +[HarmonyPatch(typeof(PlayerMaterial))] +internal static class SetPlayerMaterialPatch +{ + [HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(int), typeof(Renderer))] + public static bool Prefix(int colorId, Renderer rend) + { + rend.gameObject.GetOrAddComponent().SetColor( + PaletteManager.TryGetCustomColor(colorId, out var color) && color is VariableColor variable + ? variable + : null); + return !PaletteManager.IsVariable(colorId); + } + + [HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(Color), typeof(Renderer))] + public static void Prefix(Renderer rend) + { + rend.gameObject.GetOrAddComponent().SetColor(null); + } +} + +[HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.Update))] +internal static class PlayerTabPatch +{ + public static void Postfix(PlayerTab __instance) + { + for (var i = 0; i < __instance.ColorChips.Count; i++) + { + __instance.ColorChips[i].Inner.SpriteColor = PaletteManager.GetMainColor(i); + } + } +} + +[HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.Update))] +internal static class ChatNotifRainbowPatch +{ + private static readonly Regex RichTags = new(@"<[^>]*>", default, Regex.InfiniteMatchTimeout); + + public static void Postfix(ChatNotification __instance) + { + if (!__instance.gameObject.active) + return; + + var colorId = __instance.player.cosmetics.ColorId; + + if (!PaletteManager.IsVariable(colorId)) + return; + + var color = PaletteManager.GetMainColor(colorId); + var str = color.ToHtmlStringRGBA(); + var nameText = __instance.playerNameText; + nameText.text = "" + RichTags.Replace(nameText.text, string.Empty); + nameText.outlineColor = color.GetShadowColor(0.5f); + } +} + +[HarmonyPatch(typeof(HostInfoPanel), nameof(HostInfoPanel.Update))] +internal static class RainbowLobbyInfoPanePatch +{ + [SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Operator becomes too large.")] + public static void Postfix(HostInfoPanel __instance) + { + if (!__instance.gameObject.activeInHierarchy) + return; + + var colorId = __instance.player.cosmetics.ColorId; + + if (!PaletteManager.IsVariable(colorId)) + return; + + var host = GameData.Instance.GetHost(); + + if (!host) + return; + + var text = PaletteManager.GetMainColor(colorId).ToHtmlStringRGBA(); + var label = __instance.hostLabel; + label.text = TranslationController.Instance.GetString(StringNames.HostNounLabel); + + if (__instance.ShouldBoldenHostLabel(DataManager.Settings.Language.CurrentLanguage)) + { + label.text = "" + label.text + ""; + } + + if (AmongUsClient.Instance.AmHost) + { + __instance.playerName.text = (string.IsNullOrEmpty(host.PlayerName) + ? "..." + : $"{host.PlayerName}") + + " " + + TranslationController.Instance.GetString(StringNames.HostYouLabel); + } + else + { + __instance.playerName.text = + (string.IsNullOrEmpty(host.PlayerName) ? "..." : $"{host.PlayerName}") + + " (" + __instance.player.ColorBlindName + ")"; + } + } +} diff --git a/MiraAPI/Patches/DetectiveNotesMinigamePatch.cs b/MiraAPI/Patches/DetectiveNotesMinigamePatch.cs index 5f67033..5986675 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; -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) @@ -30,7 +23,7 @@ public static class DetectiveNotesMinigamePatch var customRoles = CustomRoleManager.CustomRoleBehaviours; foreach (var role in allRoles) { - if (role.TeamType != RoleTeamTypes.Impostor || role.Role == RoleTypes.ImpostorGhost || customRoles.Contains(role) ) + if (role.TeamType != RoleTeamTypes.Impostor || role.Role == RoleTypes.ImpostorGhost || customRoles.Contains(role)) continue; var gameObject = Object.Instantiate(__instance.impostorTypePrefab, __instance.impostorTypeParent); __instance.impostorButton.Add(gameObject); diff --git a/MiraAPI/Patches/Events/CanUsePatches.cs b/MiraAPI/Patches/Events/CanUsePatches.cs index 6206591..7af9acd 100644 --- a/MiraAPI/Patches/Events/CanUsePatches.cs +++ b/MiraAPI/Patches/Events/CanUsePatches.cs @@ -21,24 +21,21 @@ public static class CanUsePatches yield return AccessTools.Method(typeof(PlatformConsole), nameof(PlatformConsole.CanUse)); yield return AccessTools.Method(typeof(SystemConsole), nameof(SystemConsole.CanUse)); yield return AccessTools.Method(typeof(ZiplineConsole), nameof(ZiplineConsole.CanUse)); + // vent is not necessary as it has its own patch } [HarmonyPriority(Priority.Last)] [HarmonyPrefix] - public static bool CanUsePatch(Il2CppSystem.Object __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) + public static bool CanUsePatch(Il2CppSystem.Object __instance, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { canUse = couldUse = false; - IUsable usable = __instance.Cast(); - if (usable != null) - { - var @event = new PlayerCanUseEvent(__instance.Cast()); - MiraEventManager.InvokeEvent(@event); + var usable = __instance.TryCast(); + if (usable == null) return true; + var @event = new PlayerCanUseEvent(usable); + MiraEventManager.InvokeEvent(@event); - return !@event.IsCancelled; - } - - return true; + return !@event.IsCancelled; } } diff --git a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs index 0f582e1..8427679 100644 --- a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs +++ b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Reflection; +using System.Reflection; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; @@ -18,10 +17,8 @@ public static class FreeplayRoundStartPatch public static void Postfix(ref bool __result) { - if (!__result) - { - MeetingButtonManager.OnGameStart(); - MiraEventManager.InvokeEvent(new RoundStartEvent(true)); - } + if (__result) return; + MeetingButtonManager.OnGameStart(); + MiraEventManager.InvokeEvent(new RoundStartEvent(true)); } } diff --git a/MiraAPI/Patches/Events/SabotageEventPatches.cs b/MiraAPI/Patches/Events/SabotageEventPatches.cs index 8df8750..0717d6f 100644 --- a/MiraAPI/Patches/Events/SabotageEventPatches.cs +++ b/MiraAPI/Patches/Events/SabotageEventPatches.cs @@ -12,7 +12,7 @@ public static class SabotageEventPatches { [HarmonyPrefix] [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.RpcUpdateSystem), typeof(SystemTypes), typeof(byte))] - public static bool ShipStatusUpdateSystemPrefix(ShipStatus __instance, SystemTypes systemType, byte amount) + public static bool ShipStatusUpdateSystemPrefix(SystemTypes systemType, byte amount) { var @event = new UpdateSystemEvent(systemType, PlayerControl.LocalPlayer, amount); MiraEventManager.InvokeEvent(@event); @@ -21,7 +21,7 @@ public static class SabotageEventPatches [HarmonyPrefix] [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.RpcCloseDoorsOfType))] - public static bool ShipStatusCloseDoorsOfTypePrefix(ShipStatus __instance, SystemTypes type) + public static bool ShipStatusCloseDoorsOfTypePrefix(SystemTypes type) { var @event = new CloseDoorsEvent(type); MiraEventManager.InvokeEvent(@event); diff --git a/MiraAPI/Patches/Events/VanillaButtonPatches.cs b/MiraAPI/Patches/Events/VanillaButtonPatches.cs index 71df73f..70a218b 100644 --- a/MiraAPI/Patches/Events/VanillaButtonPatches.cs +++ b/MiraAPI/Patches/Events/VanillaButtonPatches.cs @@ -23,14 +23,23 @@ public static class VanillaButtonPatches { playerTarget = role.Cast().currentTarget; } + else if (role.Role is RoleTypes.Detective) + { + playerTarget = role.Cast().currentTarget; + } else if (role.Role is RoleTypes.GuardianAngel) { playerTarget = role.Cast().currentTarget; } + else if (role.Role is RoleTypes.SpiritGuide) + { + playerTarget = role.Cast().currentTarget; + } else if (role.Role is RoleTypes.Engineer) { ventTarget = role.Cast().currentTarget; } + var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget, ventTarget); MiraEventManager.InvokeEvent(genericEvent); if (genericEvent.IsCancelled) @@ -38,13 +47,9 @@ public static class VanillaButtonPatches MiraEventManager.InvokeEvent(new VanillaButtonCancelledEvent(__instance)); } - if (!genericEvent.IsCancelled) - { - return true; - } - - return false; + return !genericEvent.IsCancelled; } + [HarmonyPrefix] [HarmonyPatch(typeof(SecondaryAbilityButton), nameof(SecondaryAbilityButton.DoClick))] public static bool SecondaryDoClickPrefix(AbilityButton __instance) @@ -56,6 +61,11 @@ public static class VanillaButtonPatches { playerTarget = role.Cast().currentTarget; } + else if (role.Role is RoleTypes.SpiritGuide) + { + playerTarget = role.Cast().currentTarget; + } + var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget); MiraEventManager.InvokeEvent(genericEvent); if (genericEvent.IsCancelled) @@ -63,11 +73,6 @@ public static class VanillaButtonPatches MiraEventManager.InvokeEvent(new VanillaButtonCancelledEvent(__instance)); } - if (!genericEvent.IsCancelled) - { - return true; - } - - return false; + return !genericEvent.IsCancelled; } } diff --git a/MiraAPI/Patches/Events/VentEventPatches.cs b/MiraAPI/Patches/Events/VentEventPatches.cs index 9d4ad9f..737d221 100644 --- a/MiraAPI/Patches/Events/VentEventPatches.cs +++ b/MiraAPI/Patches/Events/VentEventPatches.cs @@ -11,7 +11,7 @@ namespace MiraAPI.Patches.Events; [HarmonyPatch] public static class VentEventPatches { - // necessary because Vent.Use is inlined in il2cpp. + // necessary because Vent.Use is inlined in IL2Cpp. private static bool _showButtons; [HarmonyPostfix] @@ -35,6 +35,7 @@ public static class VentEventPatches { _showButtons = !@event.IsCancelled; } + return !@event.IsCancelled; } @@ -52,6 +53,7 @@ public static class VentEventPatches { _showButtons = @event.IsCancelled; } + return !@event.IsCancelled; } } diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 3e4053e..f9e657d 100644 --- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -11,13 +11,11 @@ internal static class TaskAddButtonPatches public static bool StartPrefix(TaskAddButton __instance) { // if this becomes problematic in the future, find a new method. - if (uint.TryParse(__instance.name, out var result)) - { - __instance.Overlay.sprite = __instance.CheckImage; - __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifier(result); - return false; - } + if (!uint.TryParse(__instance.name, out var result)) + return true; - return true; + __instance.Overlay.sprite = __instance.CheckImage; + __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifier(result); + return false; } } diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 71fa411..7db4211 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; @@ -23,8 +24,9 @@ namespace MiraAPI.Patches.Freeplay; [HarmonyPatch(typeof(TaskAdderGame))] public static class TaskAdderPatches { + private static readonly Dictionary Folders = []; private static Scroller _scroller = null!; - private static Dictionary folders = new(); + public static string CrewmateName => TranslationController.Instance.GetString(StringNames.Crewmate); public static string ImpostorName => TranslationController.Instance.GetString(StringNames.Impostor); public static string NeutralName => "Neutral"; @@ -32,6 +34,7 @@ public static class TaskAdderPatches [HarmonyPostfix] [HarmonyPatch(nameof(TaskAdderGame.Begin))] + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "Don't know, but seems important enough to keep.")] public static void AddRolesFolder(TaskAdderGame __instance) { GameObject inner = new("Inner"); @@ -72,16 +75,16 @@ public static class TaskAdderPatches __instance.TaskParent = inner.transform; var crewmateFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == CrewmateName)!; var impostorFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == ImpostorName)!; - // var neutralFolder = __instance.CreateFolder("Neutral", __instance.Root, 2, Color.gray); + // var neutralFolder = __instance.CreateFolder(NeutralName, __instance.Root, 2, Color.gray); var modifiersFolder = __instance.CreateFolder(ModifiersName, __instance.Root, 0, Color.blue); - folders.Clear(); - folders.Add(crewmateFolder.FolderName, crewmateFolder); - folders.Add(impostorFolder.FolderName, impostorFolder); - // folders.Add("Neutrals", neutralFolder); - folders.Add(ModifiersName, modifiersFolder); + Folders.Clear(); + Folders.Add(crewmateFolder.FolderName, crewmateFolder); + Folders.Add(impostorFolder.FolderName, impostorFolder); + // Folders.Add(NeutralName, neutralFolder); + Folders.Add(ModifiersName, modifiersFolder); - int folderIdx = 2; + var folderIdx = 2; foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins) { var pluginFolders = new Dictionary(); @@ -92,10 +95,10 @@ public static class TaskAdderPatches { var customRole = role as ICustomRole; var folderName = customRole!.Configuration.FreeplayFolder; - if (!folders.TryGetValue(folderName, out var teamFolder)) + if (!Folders.TryGetValue(folderName, out var teamFolder)) { teamFolder = __instance.CreateFolder(folderName, __instance.Root, folderIdx++, customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray); - folders.Add(folderName, teamFolder); + Folders.Add(folderName, teamFolder); } if (!pluginFolders.TryGetValue(folderName, out var pluginFolder)) @@ -121,7 +124,7 @@ public static class TaskAdderPatches [HarmonyPrefix] [HarmonyPatch(nameof(TaskAdderGame.PopulateRoot))] - private static bool PopulateRootPrefix(TaskAdderGame __instance, TaskAdderGame.FolderType folderType, TaskFolder rootFolder, Il2CppSystem.Collections.Generic.Dictionary folders, Il2CppReferenceArray taskList) + private static bool PopulateRootPrefix(TaskAdderGame __instance, TaskAdderGame.FolderType folderType, TaskFolder rootFolder, CppCollections.Dictionary folders, Il2CppReferenceArray taskList) { if (folderType != TaskAdderGame.FolderType.Tasks) { @@ -130,11 +133,11 @@ public static class TaskAdderPatches return false; } - TaskFolder crewFolder = folders["Crewmates"] = + var crewFolder = folders["Crewmates"] = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform); crewFolder.gameObject.SetActive(false); crewFolder.SetFolderColor(TaskFolder.FolderColor.Blue); - TaskFolder impFolder = folders["Impostors"] = + var impFolder = folders["Impostors"] = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform); impFolder.gameObject.SetActive(false); impFolder.SetFolderColor(TaskFolder.FolderColor.Red); @@ -142,8 +145,8 @@ public static class TaskAdderPatches impFolder.FolderName = ImpostorName; rootFolder.SubFolders.Insert(0, crewFolder); rootFolder.SubFolders.Insert(0, impFolder); - Il2CppSystem.Collections.Generic.List impRoles = new(); - Il2CppSystem.Collections.Generic.List crewRoles = new(); + CppCollections.List impRoles = new(); + CppCollections.List crewRoles = new(); foreach (var role in RoleManager.Instance.AllRoles) { if (role.Role != RoleTypes.ImpostorGhost && role.Role != RoleTypes.CrewmateGhost && !role.IsCustomRole()) @@ -164,14 +167,15 @@ public static class TaskAdderPatches return false; } + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract (Justification: Nullable annotations of reference types do not survive compilation.) if (taskList == null) { return false; } - foreach (NormalPlayerTask normalPlayerTask in taskList) + foreach (var normalPlayerTask in taskList) { - SystemTypes systemTypes = normalPlayerTask.StartAt; + var systemTypes = normalPlayerTask.StartAt; if (normalPlayerTask is DivertPowerTask task) { systemTypes = task.TargetSystem; @@ -187,14 +191,9 @@ public static class TaskAdderPatches taskFolder3 = folders[systemTypes.ToString()] = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform); taskFolder3.SetFolderColor(TaskFolder.FolderColor.Tan); taskFolder3.gameObject.SetActive(false); - if (systemTypes == SystemTypes.UpperEngine) - { - taskFolder3.FolderName = TranslationController.Instance.GetString(StringNames.Engines); - } - else - { - taskFolder3.FolderName = TranslationController.Instance.GetString(systemTypes); - } + taskFolder3.FolderName = systemTypes == SystemTypes.UpperEngine + ? TranslationController.Instance.GetString(StringNames.Engines) + : TranslationController.Instance.GetString(systemTypes); rootFolder.SubFolders.Add(taskFolder3); } @@ -205,7 +204,6 @@ public static class TaskAdderPatches return false; } - private static TaskFolder CreateFolder(this TaskAdderGame instance, string name, TaskFolder parent, int idx = -1, Color? color = null) { var folder = Object.Instantiate(instance.RootFolderPrefab, instance.transform); @@ -250,23 +248,27 @@ public static class TaskAdderPatches instance.ActiveItems.Add(item.transform); } - private static bool IsChildOf(this TaskFolder child, TaskFolder parent) => parent.SubFolders - .ToArray() - .Any(x => x.FolderName == child.FolderName); + private static bool IsChildOf(this TaskFolder child, TaskFolder parent) + { + return parent.SubFolders + .ToArray() + .Any(x => x.FolderName == child.FolderName); + } - // yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry: + // yes it might be crazy patching the entire method, but I tried so many other methods and only this works :cry: // true -chip [HarmonyPrefix] [HarmonyPatch(nameof(TaskAdderGame.ShowFolder))] public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder) { - StringBuilder stringBuilder = new StringBuilder(64); + var stringBuilder = new StringBuilder(64); __instance.Hierarchy.Add(taskFolder); foreach (var t in __instance.Hierarchy) { stringBuilder.Append(t.FolderName); stringBuilder.Append('\\'); } + __instance.PathText.text = stringBuilder.ToString(); __instance.PathText.fontSizeMin = 3; __instance.PathText.fontSizeMax = 3; @@ -275,12 +277,12 @@ public static class TaskAdderPatches __instance.ActiveItems.ToArray().Do(x => x.gameObject.DeepDestroy(false)); __instance.ActiveItems.Clear(); - float num = 0f; - float num2 = 0f; - float num3 = 0f; + var num = 0f; + var num2 = 0f; + var num3 = 0f; foreach (var t in taskFolder.SubFolders) { - TaskFolder taskFolder2 = Object.Instantiate(t, __instance.TaskParent); + var taskFolder2 = Object.Instantiate(t, __instance.TaskParent); taskFolder2.gameObject.SetActive(true); taskFolder2.Parent = __instance; taskFolder2.transform.localPosition = new Vector3(num, num2, 0f); @@ -307,20 +309,17 @@ public static class TaskAdderPatches .ToArray(); foreach (var task in list) { - TaskAddButton taskAddButton = Object.Instantiate(__instance.TaskPrefab); + var taskAddButton = Object.Instantiate(__instance.TaskPrefab); taskAddButton.MyTask = task; switch (task.TaskType) { case TaskTypes.DivertPower: - { var targetSystem = task.Cast().TargetSystem; taskAddButton.Text.text = TranslationController.Instance.GetString( StringNames.DivertPowerTo, TranslationController.Instance.GetString(targetSystem)); break; - } case TaskTypes.FixWeatherNode: - { var nodeId = task.Cast().NodeId; taskAddButton.Text.text = TranslationController.Instance.GetString( @@ -328,7 +327,6 @@ public static class TaskAdderPatches TranslationController.Instance.GetString( WeatherSwitchGame.ControlNames[nodeId]); break; - } default: taskAddButton.Text.text = TranslationController.Instance.GetString(task.TaskType); @@ -338,7 +336,7 @@ public static class TaskAdderPatches __instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); if (taskAddButton != null && taskAddButton.Button != null) { - ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button, false); + ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); } } @@ -348,7 +346,7 @@ public static class TaskAdderPatches .ToArray(); foreach (var role in roleChildren) { - TaskAddButton roleAddButton = Object.Instantiate(__instance.RoleButton); + var roleAddButton = Object.Instantiate(__instance.RoleButton); roleAddButton.SafePositionWorld = __instance.SafePositionWorld; roleAddButton.Text.text = role.GetRoleName(); roleAddButton.Role = role; @@ -373,7 +371,8 @@ public static class TaskAdderPatches { // I hate you } - if (split is ["Roles", _, _] && folders.Any(x => taskFolder.IsChildOf(x.Value))) + + if (split is ["Roles", _, _] && Folders.Any(x => taskFolder.IsChildOf(x.Value))) { var plugin = MiraPluginManager.GetPluginByGuid(split[2]); if (plugin != null) @@ -386,6 +385,7 @@ public static class TaskAdderPatches { continue; } + var crewLocale = TranslationController.Instance.GetString(StringNames.Crewmate); var impLocale = TranslationController.Instance.GetString(StringNames.Impostor); @@ -394,6 +394,7 @@ public static class TaskAdderPatches { teamFolder = cs.Configuration.FreeplayFolder; } + if (split[1] != teamFolder) { continue; @@ -421,6 +422,7 @@ public static class TaskAdderPatches { customColor = Palette.ImpostorRed; } + roleAddButton.FileImage.color = customColor; roleAddButton.RolloverHandler.OutColor = customColor; } @@ -431,7 +433,7 @@ public static class TaskAdderPatches } } - if (folders.TryGetValue(ModifiersName, out var modifiersFolder) && taskFolder.IsChildOf(modifiersFolder)) + if (Folders.TryGetValue(ModifiersName, out var modifiersFolder) && taskFolder.IsChildOf(modifiersFolder)) { var plugin = MiraPluginManager.GetPluginByGuid(taskFolder.name.Replace("(Clone)", string.Empty)); if (plugin != null) diff --git a/MiraAPI/Patches/GameModes/.editorconfig b/MiraAPI/Patches/GameModes/.editorconfig deleted file mode 100644 index d825cc9..0000000 --- a/MiraAPI/Patches/GameModes/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*.cs] -dotnet_diagnostic.S125.severity = none \ No newline at end of file diff --git a/MiraAPI/Patches/GameModes/BodyTypePatch.cs b/MiraAPI/Patches/GameModes/BodyTypePatch.cs index 2aede92..71a0b53 100644 --- a/MiraAPI/Patches/GameModes/BodyTypePatch.cs +++ b/MiraAPI/Patches/GameModes/BodyTypePatch.cs @@ -7,14 +7,10 @@ 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) + public static bool Prefix(IntroCutscene __instance, ref CppIEnumerator __result) { - if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene == true) - { - __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); - return false; - } - - return true; + if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene != true) return true; + __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); + return false; } } diff --git a/MiraAPI/Patches/GameModes/ConsolePatches.cs b/MiraAPI/Patches/GameModes/ConsolePatches.cs index 4427b16..ecfdf70 100644 --- a/MiraAPI/Patches/GameModes/ConsolePatches.cs +++ b/MiraAPI/Patches/GameModes/ConsolePatches.cs @@ -10,102 +10,87 @@ internal static class ConsolePatches [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 (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started || !ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseTasks(__instance)) - { - var task = __instance.FindTask(pc.Object); - - if (task && task.GetComponent()) - { - canUse = couldUse = true; - return true; - } - - canUse = false; - couldUse = false; - return true; - } + canUse = couldUse = true; + return true; + } + if (CustomGameModeManager.ActiveMode != null && !CustomGameModeManager.ActiveMode.CanUseTasks(__instance)) + { canUse = couldUse = false; return false; } - canUse = couldUse = true; + var task = __instance.FindTask(pc.Object); + + if (task && task.GetComponent()) + { + canUse = couldUse = true; + return true; + } + + canUse = false; + couldUse = false; return true; } [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) + public static bool SystemCanUsePatch(SystemConsole __instance, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { - if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + if (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started || !ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance)) - { - canUse = couldUse = true; - return true; - } - - canUse = couldUse = false; - return false; + canUse = couldUse = true; + return true; } - canUse = couldUse = true; - return true; + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance)) + { + canUse = couldUse = true; + return true; + } + + canUse = couldUse = false; + return false; } [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) + public static bool MapCanUsePatch(MapConsole __instance, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { - if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + if (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started || !ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance)) - { - canUse = couldUse = true; - return true; - } - - canUse = couldUse = false; - return false; + canUse = couldUse = true; + return true; } - canUse = couldUse = true; - return true; + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance)) + { + canUse = couldUse = true; + return true; + } + + canUse = couldUse = false; + return false; } [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 || + return AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started || !ShipStatus.Instance || 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 || + return AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started || !ShipStatus.Instance || 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 || + return AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started || !ShipStatus.Instance || CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseTasks(__instance); - } - - return true; } } diff --git a/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs b/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs index 7e8144e..b33c1fd 100644 --- a/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs +++ b/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs @@ -1,13 +1,14 @@ using HarmonyLib; +using GameModesEnum = AmongUs.GameOptions.GameModes; namespace MiraAPI.Patches.GameModes; [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.SetGameMode))] internal static class CreateOptionsPatch { - public static bool Prefix(CreateOptionsPicker __instance, AmongUs.GameOptions.GameModes mode) + public static bool Prefix(CreateOptionsPicker __instance, GameModesEnum mode) { - if (mode <= AmongUs.GameOptions.GameModes.SeekFools) + if (mode <= GameModesEnum.SeekFools) { return true; } @@ -15,4 +16,4 @@ internal static class CreateOptionsPatch // TODO: finish this return false; } -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/GameModes/GameLogicPatches.cs b/MiraAPI/Patches/GameModes/GameLogicPatches.cs index 742a56c..719fe05 100644 --- a/MiraAPI/Patches/GameModes/GameLogicPatches.cs +++ b/MiraAPI/Patches/GameModes/GameLogicPatches.cs @@ -9,27 +9,28 @@ internal static class GameLogicPatches [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidHumansWin))] public static bool DidHumansWin(GameOverReason reason, ref bool __result) { - __result = reason == GameOverReason.CrewmatesByTask || reason == GameOverReason.CrewmatesByVote || - reason == GameOverReason.ImpostorDisconnect || reason == GameOverReason.HideAndSeek_CrewmatesByTimer; + __result = reason is GameOverReason.CrewmatesByTask or GameOverReason.CrewmatesByVote or + GameOverReason.ImpostorDisconnect or GameOverReason.HideAndSeek_CrewmatesByTimer; return false; } [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidImpostorsWin))] public static bool DidImpostorsWin(GameOverReason reason, ref bool __result) { - __result = reason == GameOverReason.ImpostorsByKill || reason == GameOverReason.ImpostorsBySabotage || - reason == GameOverReason.ImpostorsByVote || reason == GameOverReason.HideAndSeek_ImpostorsByKills || - reason == GameOverReason.CrewmateDisconnect; + __result = reason is GameOverReason.ImpostorsByKill or GameOverReason.ImpostorsBySabotage or + GameOverReason.ImpostorsByVote or GameOverReason.HideAndSeek_ImpostorsByKills or + GameOverReason.CrewmateDisconnect; return false; } [HarmonyPrefix, HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetBodyType))] - public static bool BodyTypePatch(NormalGameManager __instance, PlayerControl player, ref PlayerBodyTypes __result) + public static bool BodyTypePatch(PlayerControl player, ref PlayerBodyTypes __result) { if (CustomGameModeManager.ActiveMode == null || !CustomGameModeManager.ActiveMode.GameModeBodyTypeOverride) { return true; } + __result = CustomGameModeManager.ActiveMode.GetBodyType(player); return false; } @@ -68,7 +69,7 @@ internal static class GameLogicPatches [HarmonyPostfix] [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.IsGameOverDueToDeath))] - public static void IsGameOverDueToDeathPatch(LogicGameFlowNormal __instance, ref bool __result) + public static void IsGameOverDueToDeathPatch(ref bool __result) { if (CustomGameModeManager.IsActiveGameMode()) { diff --git a/MiraAPI/Patches/GameModes/GameManagerPatches.cs b/MiraAPI/Patches/GameModes/GameManagerPatches.cs index 3f56e82..72b53d6 100644 --- a/MiraAPI/Patches/GameModes/GameManagerPatches.cs +++ b/MiraAPI/Patches/GameModes/GameManagerPatches.cs @@ -17,6 +17,7 @@ internal static class GameManagerPatches return true; } + [HarmonyPrefix, HarmonyPatch(nameof(GameManager.ShowCrewmatesKilled))] public static bool ShowCrewmatesKilledPrefix(ref bool __result) { @@ -28,6 +29,7 @@ internal static class GameManagerPatches return true; } + [HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetMapOptions))] [HarmonyPrefix] public static bool GetMapOptions(ref MapOptions __result) @@ -53,4 +55,4 @@ internal static class GameStartManagerPatches __instance.MinPlayers = 1; } } -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index 4426e0a..3c2d815 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -1,10 +1,8 @@ using System.Collections; using HarmonyLib; -using Il2CppSystem; using MiraAPI.GameModes; using MiraAPI.Translation; using Reactor.Utilities; -using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using Object = UnityEngine.Object; @@ -17,14 +15,13 @@ internal static class HudPatches [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] public static void HudUpdatePatch(HudManager __instance) { - if (GameManager.Instance != null && GameManager.Instance.GameHasStarted && + if (GameManager.Instance && GameManager.Instance.GameHasStarted && CustomGameModeManager.ActiveMode != null) { CustomGameModeManager.ActiveMode.HudUpdate(__instance); } } - [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] [HarmonyPostfix] public static void PostHudStart(HudManager __instance) @@ -34,7 +31,7 @@ internal static class HudPatches private static IEnumerator CoPostHudStart(HudManager instance) { - yield return new WaitUntil((Func)(() => GameManager.Instance != null)); + yield return new WaitUntil((Il2CppSystem.Func)(() => GameManager.Instance != null)); if (GameManager.Instance.IsHideAndSeek()) yield return null; var infoPane = instance.gameObject.transform.FindChild("LobbyInfoPane"); @@ -44,7 +41,7 @@ internal static class HudPatches var modelText = modeLabel!.Find("Text_TMP").gameObject; var modelTextClone = Object.Instantiate(modelText, modeLabel); Object.Destroy(modelTextClone.GetComponent()); - modelTextClone.GetComponent().text = "Gamemode"; + modelTextClone.GetComponent().text = MiraLocaleManager.Get("MiraApi.Gamemode", "Gamemode"); var gmText = modeValue!.Find("GameModeText").gameObject; var gmTextClone = Object.Instantiate(gmText, modeValue); gmText.SetActive(false); diff --git a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs index de9bbf1..10db226 100644 --- a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs +++ b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs @@ -4,7 +4,7 @@ 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 CanKill is enabled in gamemode or friendly fire is toggled on. /// [HarmonyPatch(typeof(ImpostorRole), "IsValidTarget")] internal static class ImpostorTargetPatch diff --git a/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs b/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs index 5701014..5b51373 100644 --- a/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs +++ b/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs @@ -7,14 +7,10 @@ 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) + public static bool Prefix(IntroCutscene __instance, ref CppIEnumerator __result) { - if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene == true) - { - __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); - return false; - } - - return true; + if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene != true) return true; + __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); + return false; } } diff --git a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs index 06ea282..b5ddb46 100644 --- a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs +++ b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs @@ -10,12 +10,8 @@ internal static class MapBehaviourPatch public static bool ShowSabotagePatch(MapBehaviour __instance) { var shouldShow = CustomGameModeManager.ActiveMode?.ShouldShowSabotageMap(__instance); - if (shouldShow.HasValue && !shouldShow.Value) - { - __instance.ShowNormalMap(); - return false; - } - - return true; + if (!shouldShow.HasValue || shouldShow.Value) return true; + __instance.ShowNormalMap(); + return false; } } diff --git a/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs index 53620e0..6c172ba 100644 --- a/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs +++ b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs @@ -2,16 +2,14 @@ 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; + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.ShowTaskBar) return true; + __instance.gameObject.SetActive(false); + return false; } } diff --git a/MiraAPI/Patches/GameModes/TaskPanelPatch.cs b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs index 30a30cc..c7d8589 100644 --- a/MiraAPI/Patches/GameModes/TaskPanelPatch.cs +++ b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs @@ -2,16 +2,14 @@ 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; + if (__instance == Roles.HudManagerPatches.RoleTab || CustomGameModeManager.ActiveMode == null) return true; + CustomGameModeManager.ActiveMode.UpdateTaskPanel(__instance); + return false; } } diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index f8835a8..ad12c68 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,11 +1,9 @@ using System.Collections; using System.Collections.Generic; -using System.Globalization; using HarmonyLib; using MiraAPI.Hud; using MiraAPI.Keybinds; using MiraAPI.LocalSettings; -using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -29,44 +27,29 @@ public static class HudManagerPatches private static Dictionary vanillaKeybindIcons = []; - internal static List ModdedKeybindIcons = []; - public static IEnumerator CoResizeUI() { - while (!HudManager.Instance) - { - yield return null; - } + while (!HudManager.Instance) yield return null; yield return new WaitForSeconds(0.01f); ResizeUI(LocalSettingsTabSingleton.Instance.ButtonUIFactorSlider.Value); } private static Transform _storedButtonsParent; + public static void ResizeUI(float scaleFactor) { - if (!_storedButtonsParent) - { - _storedButtonsParent = HudManager.Instance.transform.FindChild("Buttons"); - } + if (!_storedButtonsParent) _storedButtonsParent = HudManager.Instance.transform.FindChild("Buttons"); + if (_storedButtonsParent) { foreach (var aspect in _storedButtonsParent.GetComponentsInChildren(true)) { - if (!aspect.gameObject) - { - continue; - } + if (!aspect.gameObject) continue; - if (aspect.gameObject.transform.parent.name == "TopRight") - { - continue; - } + if (aspect.gameObject.transform.parent.name == "TopRight") continue; - if (aspect.gameObject.name.Contains("TopRight")) - { - continue; - } + if (aspect.gameObject.name.Contains("TopRight")) continue; aspect.gameObject.SetActive(!aspect.isActiveAndEnabled); aspect.DistanceFromEdge *= new Vector2(scaleFactor, scaleFactor); @@ -76,58 +59,46 @@ public static class HudManagerPatches foreach (var button in HudManager.Instance.GetComponentsInChildren(true)) { - if (!button.gameObject) - { - continue; - } + if (!button.gameObject) continue; button.gameObject.SetActive(!button.isActiveAndEnabled); button.gameObject.transform.localScale *= scaleFactor; button.gameObject.SetActive(!button.isActiveAndEnabled); } - if (_storedButtonsParent) + if (!_storedButtonsParent) return; + foreach (var arrange in _storedButtonsParent.GetComponentsInChildren(true)) { - foreach (var arrange in _storedButtonsParent.GetComponentsInChildren(true)) + if (!arrange.gameObject || !arrange.transform) continue; + + if (arrange.gameObject.name.Contains("TopRight")) continue; + + arrange.gameObject.SetActive(!arrange.isActiveAndEnabled); + arrange.CellSize = new Vector2(scaleFactor, scaleFactor); + arrange.gameObject.SetActive(!arrange.isActiveAndEnabled); + if (!arrange.isActiveAndEnabled || arrange.gameObject.transform.childCount == 0) continue; + try { - if (!arrange.gameObject || !arrange.transform) - { - continue; - } - - if (arrange.gameObject.name.Contains("TopRight")) - { - continue; - } - - arrange.gameObject.SetActive(!arrange.isActiveAndEnabled); - arrange.CellSize = new Vector2(scaleFactor, scaleFactor); - arrange.gameObject.SetActive(!arrange.isActiveAndEnabled); - if (arrange.isActiveAndEnabled && arrange.gameObject.transform.childCount != 0) - { - try - { - arrange.ArrangeChilds(); - } - catch - { - // Error($"Error arranging child objects in GridArrange: {e}"); - } - } + arrange.ArrangeChilds(); + } + catch + { + // Error($"Error arranging child objects in GridArrange: {e}"); } } } /* /// - /// Trigger hudstart on current custom gamemode + /// Trigger HudStart on current custom gamemode /// [HarmonyPostfix] [HarmonyPatch(nameof(HudManager.OnGameStart))] public static void GameStartPatch(HudManager __instance) { CustomGameModeManager.ActiveMode?.HudStart(__instance); - }*/ + } + */ [HarmonyPrefix] [HarmonyPriority(Priority.First)] @@ -144,10 +115,8 @@ public static class HudManagerPatches [HarmonyPatch(typeof(ChatController), nameof(ChatController.Close))] public static void TogglePrefix(ChatController __instance) { - if (!MiraHudHelper.ClonedChatButton) - { - return; - } + if (!MiraHudHelper.ClonedChatButton) return; + __instance.chatButton.transform.localPosition = MiraHudHelper.ClonedChatButton.transform.localPosition + new Vector3(-0.3f, 0); } @@ -155,7 +124,7 @@ public static class HudManagerPatches [HarmonyPriority(Priority.First)] [HarmonyPatch(typeof(ChatController), nameof(ChatController.Toggle))] [HarmonyPatch(typeof(ChatController), nameof(ChatController.Close))] - public static void TogglePostfix(ChatController __instance) + public static void TogglePostfix() { MiraHudHelper.UiGrid.ArrangeChilds(); } @@ -179,25 +148,13 @@ public static class HudManagerPatches public static void StartPostfix(HudManager __instance) { __instance.gameObject.AddComponent(); - if (Buttons == null) - { - Buttons = __instance.transform.Find("Buttons"); - } + if (Buttons == null) Buttons = __instance.transform.Find("Buttons"); - if (BottomRight == null) - { - BottomRight = Buttons.Find("BottomRight"); - } + if (BottomRight == null) BottomRight = Buttons.Find("BottomRight"); - if (BottomLeft == null) - { - BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons); - } + if (BottomLeft == null) BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons); - foreach (var t in BottomLeft.GetComponentsInChildren(true)) - { - t.gameObject.Destroy(); - } + foreach (var t in BottomLeft.GetComponentsInChildren(true)) t.gameObject.Destroy(); var gridArrange = BottomLeft.GetComponent(); var aspectPosition = BottomLeft.GetComponent(); @@ -209,10 +166,7 @@ public static class HudManagerPatches if (Constants.GetPlatformType() is Platforms.Android or Platforms.IPhone) { var fakeButton = Object.Instantiate(__instance.AbilityButton, BottomLeft.transform); - foreach (var renderer in fakeButton.GetComponentsInChildren()) - { - renderer.enabled = false; - } + foreach (var renderer in fakeButton.GetComponentsInChildren()) renderer.enabled = false; fakeButton.buttonLabelText.Destroy(); fakeButton.cooldownTimerText.Destroy(); @@ -221,7 +175,6 @@ public static class HudManagerPatches fakeButton.ToggleVisible(true); fakeButton.Destroy(); } - ModdedKeybindIcons = []; foreach (var button in CustomButtonManager.CustomButtons) { @@ -232,10 +185,7 @@ public static class HudManagerPatches _ => null, }; - if (location is null) - { - continue; - } + if (location is null) continue; try { @@ -246,6 +196,7 @@ public static class HudManagerPatches Error($"Failed to create custom button {button.GetType().Name}: {e}"); } } + __instance.ImpostorVentButton.transform.SetParent(null); __instance.ImpostorVentButton.transform.SetParent(BottomRight.transform); @@ -270,23 +221,21 @@ public static class HudManagerPatches var buttonObj = kvp.Key; var actionId = kvp.Value; - var key = KeybindUtils.GetKeycodeByActionId(actionId); - if (key == KeyboardKeyCode.None) - { - continue; - } - var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); - vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); if (!MiraApiPlugin.IsMobile) { var comp = buttonObj.GetComponent(); KeybindManager.VanillaKeybinds[comp.GetType()].Button = comp; } + + var key = KeybindUtils.GetKeycodeByActionId(actionId); + if (key == KeyboardKeyCode.None) continue; + + var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); + vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); } - MiraApiSettings.OldButtonScaleFactor = - LocalSettingsTabSingleton.Instance.ButtonUIFactorSlider.Value; - MiraApiSettings.OldUiButtonScaleFactor = - LocalSettingsTabSingleton.Instance.TopRightButtonsFactorSlider.Value; + + MiraApiSettings.OldButtonScaleFactor = LocalSettingsTabSingleton.Instance.ButtonUIFactorSlider.Value; + MiraApiSettings.OldUiButtonScaleFactor = LocalSettingsTabSingleton.Instance.TopRightButtonsFactorSlider.Value; Coroutines.Start(CoResizeUI()); } @@ -302,10 +251,7 @@ public static class HudManagerPatches public static void SetHudActivePostfix(HudManager __instance, PlayerControl localPlayer, RoleBehaviour role, bool isActive) { __instance.AdminButton.ToggleVisible(isActive && role.IsImpostor && GameOptionsManager.Instance.CurrentGameOptions.GameMode == AmongUs.GameOptions.GameModes.HideNSeek); - if (localPlayer.Data == null) - { - return; - } + if (localPlayer.Data == null) return; foreach (var button in CustomButtonManager.CustomButtons) { @@ -324,8 +270,7 @@ public static class HudManagerPatches [HarmonyPostfix] public static void UpdatePostfix() { - var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard && - LocalSettingsTabSingleton.Instance.ShowKeybinds.Value; + var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard && LocalSettingsTabSingleton.Instance.ShowKeybinds.Value; foreach (var btnIcon in vanillaKeybindIcons) { @@ -333,47 +278,19 @@ public static class HudManagerPatches btnIcon.Key.transform.parent.gameObject.SetActive(canSeeBinds); } - foreach (var btnIcon in ModdedKeybindIcons) - { - btnIcon.transform.parent.gameObject.SetActive(canSeeBinds); - } + foreach (var button in CustomButtonManager.CustomButtons) button.UpdateKeybindIcon(); var player = ReInput.players.GetPlayer(0); - var keyboard = player.controllers.Keyboard; foreach (var entry in KeybindManager.Keybinds) { - var modKeys = entry.ModifierKeys; - bool modifierKeysPressed; - if (modKeys.Length == 0) - { - modifierKeysPressed = true; - } - else - { - modifierKeysPressed = true; - foreach (var key in modKeys) - { - if (keyboard.GetModifierKey(key)) continue; - modifierKeysPressed = false; - break; - } - } - - if (player.GetButtonDown(entry.Id) && modifierKeysPressed) - { + if (entry.RewiredInputAction != null && player.GetButtonDown(entry.RewiredInputAction.id)) entry.Invoke(); - } } - // suppressed warning because we want minimum allocations here -#pragma warning disable S3267 foreach (var entry in KeybindManager.VanillaKeybinds.Values) -#pragma warning restore S3267 { if (player.GetButtonDown(entry.Id)) - { entry.Invoke(); - } } } } diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index 3a7f235..8abbefb 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -1,11 +1,10 @@ -using System.Linq; -using System.Reflection; +using System.Reflection; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; -using MiraAPI.MeetingAbilities; using MiraAPI.GameModes; +using MiraAPI.MeetingAbilities; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; @@ -51,6 +50,7 @@ public static class IntroCutscenePatches public static void Postfix(Il2CppObjectBase __instance) { var wrapper = new StateMachineWrapper(__instance); + // run before the first yield if (wrapper.GetState() != 1) { @@ -68,7 +68,7 @@ public static class IntroCutscenePatches [HarmonyPrefix] [HarmonyPatch(nameof(IntroCutscene.BeginImpostor))] [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] - public static bool BeginPrefix(IntroCutscene __instance, [HarmonyArgument(0)] ref Il2CppSystem.Collections.Generic.List yourTeam) + public static bool BeginPrefix(IntroCutscene __instance, [HarmonyArgument(0)] ref CppCollections.List yourTeam) { return PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole || customRole.SetupIntroTeam(__instance, ref yourTeam); } @@ -118,17 +118,20 @@ public static class IntroCutscenePatches if (_usedFallback) { var wrapper = new StateMachineWrapper(__instance); + // run after the final yield if (wrapper.GetState() != -1) { return; } + introCutscene = wrapper.Instance; } else { introCutscene = __instance.Cast(); } + Info("IntroCutscene ended"); MeetingButtonManager.OnGameStart(); diff --git a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs index 6666a2c..df2179a 100644 --- a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs +++ b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs @@ -1,6 +1,7 @@ -using System.Linq; +using System.Linq; using HarmonyLib; using MiraAPI.Keybinds; +using Rewired; using Rewired.UI.ControlMapper; using UnityEngine; using UnityEngine.UI; @@ -10,53 +11,71 @@ namespace MiraAPI.Patches.Keybinds; [HarmonyPatch(typeof(ControlMapper))] public static class ControlMapperPatches { - // Patching this crashes for Epic Games users - /*[HarmonyPrefix] - [HarmonyPatch(nameof(ControlMapper.Start))] - private static void StartPrefix(ControlMapper __instance) + [HarmonyPrefix] + [HarmonyPatch(nameof(ControlMapper.Initialize))] + private static void InitializePrefix(ControlMapper __instance) { - var doneButton = GameObject.Find("DoneButton")?.GetComponent(); - if (doneButton == null) - { - return; - } + __instance.showControllers = true; + __instance.showGlyphs = false; + } - var toggle = Object.Instantiate(doneButton, doneButton.transform.parent); - var text = toggle.GetComponentInChildren(); - var entry = LocalSettingsTabSingleton.Instance.ShowKeybinds; - toggle.gameObject.name = "KeybindsVisibleToggle"; - text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}"; - toggle.onClick.RemoveAllListeners(); - toggle.GetComponent().identifier = "KeybindsVisibleToggle"; - toggle.onClick.AddListener((UnityAction)(() => - { - entry.Value = !entry.Value; - text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}"; - })); - }*/ + [HarmonyPostfix] + [HarmonyPatch(nameof(ControlMapper.CreateLayout))] + private static void CreateLayoutPostfix(ControlMapper __instance) + { + __instance.references.controllerGroup.gameObject.SetActive(false); + __instance.references.assignedControllersGroup.gameObject.SetActive(false); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ControlMapper.GetControllerMap))] + private static bool GetControllerMapPrefix(ControlMapper __instance, ControllerType type, ref ControllerMap? __result) + { + if (type != ControllerType.Joystick) return true; + + __result = __instance.currentPlayer?.controllers.maps.GetFirstMapInCategory(type, __instance.currentJoystickId, 1); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ControlMapper.HasElementAssignmentConflicts))] + private static bool HasElementAssignmentConflictsPrefix(ControlMapper.InputMapping mapping, ref bool __result) + { + if (mapping == null || mapping.controllerType != ControllerType.Joystick || !KeybindManager.Keybinds.Any(x => x.RewiredInputAction?.id == mapping.fieldInfo.actionId)) return true; + + __result = false; + return false; + } [HarmonyPrefix] [HarmonyPatch(nameof(ControlMapper.Update))] private static void UpdatePrefix(ControlMapper __instance) { + var conflicts = KeybindManager.GetConflicts(); + var controllerConflicts = KeybindManager.GetControllerConflicts(); foreach (var element in __instance.themedElements) { var info = element.GetComponent(); - if (info == null) - { - continue; - } + if (info == null) continue; - var key = info.glyphOrText?.actionElementMap?.keyboardKeyCode; - if (key == null) - { - continue; - } + var map = ReInput.mapping.GetActionElementMap(info.actionElementMapId); + var image = element.GetComponent(); + if (image == null) continue; - var conflicts = KeybindManager.GetConflicts(); - if (conflicts.Any(x => x.Key.ToString() == key.ToString())) + var hasConflict = map?.controllerMap.controllerType switch { - element.GetComponent().color = Color.red; + ControllerType.Keyboard => conflicts.ContainsKey(map.keyboardKeyCode), + ControllerType.Joystick => controllerConflicts.Any(x => x.controllerMap.id == map.controllerMap.id && x.id == map.id), + _ => false, + }; + if (hasConflict) + { + image.color = Color.red; + } + else if (image.color == Color.red) + { + image.color = Color.white; + element.ApplyTheme(); } } } diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs index 9c1034f..e7acf89 100644 --- a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs +++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using HarmonyLib; using MiraAPI.Keybinds; using Rewired; @@ -10,19 +12,18 @@ public static class KeybindMenuPatch private static bool _registered; [HarmonyPrefix] + + // ReSharper disable once InconsistentNaming private static void AwakePrefix(InputManager_Base __instance) { - if (_registered) - { - return; - } + if (_registered) return; KeybindUtils.RewiredInputManager = __instance; KeybindManager.RewiredInit(); _registered = true; // Set these here right after input manager initializes - KeybindManager.VanillaKeybinds = new() + KeybindManager.VanillaKeybinds = new Dictionary { { typeof(KillButton), new VanillaKeybind(8) }, { typeof(UseButton), new VanillaKeybind(6) }, diff --git a/MiraAPI/Patches/Keybinds/OptionsMenuBehaviourPatch.cs b/MiraAPI/Patches/Keybinds/OptionsMenuBehaviourPatch.cs index 816ebf4..ddd7a62 100644 --- a/MiraAPI/Patches/Keybinds/OptionsMenuBehaviourPatch.cs +++ b/MiraAPI/Patches/Keybinds/OptionsMenuBehaviourPatch.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using System.Linq; +using HarmonyLib; using MiraAPI.Keybinds; using UnityEngine; @@ -7,41 +8,45 @@ namespace MiraAPI.Patches.Keybinds; [HarmonyPatch(typeof(OptionsMenuBehaviour))] public static class OptionsMenuBehaviourPatch { - private static ButtonRolloverHandler? _remap_rollover; - private static SpriteRenderer? _remap_background; - private static bool _conflicts => KeybindManager.GetConflicts().Count > 0; + private static ButtonRolloverHandler? _remapRollover; + private static SpriteRenderer? _remapBackground; + + private static bool Conflicts => KeybindManager.GetConflicts().Count > 0 || KeybindManager.GetControllerConflicts().Count > 0; [HarmonyPostfix] [HarmonyPatch(nameof(OptionsMenuBehaviour.Open))] private static void OpenPostfix(OptionsMenuBehaviour __instance) { - _remap_rollover = GameObject.Find("Remap Controls")?.GetComponent()!; - try - { - _remap_background = _remap_rollover.transform.FindChild("Background").GetComponent(); - _remap_background.color = _conflicts ? Color.red : Color.white; - } - catch - { - // ignored, this normally breaks when on mobile since there's no remap button screen. - } + _remapRollover = __instance.GetComponentsInChildren(true).FirstOrDefault(x => x.name == "Remap Controls"); + _remapBackground = _remapRollover?.transform.FindChild("Background")?.GetComponent(); + if (_remapBackground != null) _remapBackground.color = Conflicts ? Color.red : Color.white; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Update))] + private static bool UpdatePrefix(OptionsMenuBehaviour __instance) + { + if (MiraApiPlugin.IsMobile || ActiveInputManager.currentControlType != ActiveInputManager.InputType.Joystick) return true; + + if (Input.GetKeyUp(KeyCode.Escape) && !__instance.KeyboardOptions.activeSelf) __instance.Close(); + + __instance.KeyboardOptions.SetActive(true); + __instance.MouseAndKeyboardOptions.SetActive(true); + return false; } [HarmonyPostfix] [HarmonyPatch(nameof(OptionsMenuBehaviour.Update))] - private static void UpdatePostfix(OptionsMenuBehaviour __instance) + private static void UpdatePostfix() { - if (_remap_rollover == null) - { - return; - } - if (_remap_background == null) - { - return; - } + if (_remapRollover == null) return; - _remap_rollover.OutColor = _conflicts ? Color.red : Color.white; - _remap_rollover.UnselectedColor = _conflicts ? Color.red : Color.white; - _remap_rollover.OverColor = _conflicts ? new Color32(255, 55, 55, 255) : Palette.AcceptedGreen; + if (_remapBackground == null) return; + + var conflicts = Conflicts; + _remapRollover.OutColor = conflicts ? Color.red : Color.white; + _remapRollover.UnselectedColor = conflicts ? Color.red : Color.white; + _remapRollover.OverColor = conflicts ? new Color32(255, 55, 55, 255) : Palette.AcceptedGreen; + if (!conflicts && _remapBackground.color == Color.red) _remapBackground.color = Color.white; } } diff --git a/MiraAPI/Patches/Keybinds/PlayerKeybindPatches.cs b/MiraAPI/Patches/Keybinds/PlayerKeybindPatches.cs new file mode 100644 index 0000000..bc9b8c4 --- /dev/null +++ b/MiraAPI/Patches/Keybinds/PlayerKeybindPatches.cs @@ -0,0 +1,64 @@ +using System.Linq; +using HarmonyLib; +using MiraAPI.Keybinds; +using Rewired; + +namespace MiraAPI.Patches.Keybinds; + +[HarmonyPatch(typeof(Player))] +public static class PlayerKeybindPatches +{ + [HarmonyPostfix] + [HarmonyPatch(nameof(Player.GetButtonDown), typeof(int))] + private static void GetButtonPostfix(Player __instance, int __0, ref bool __result) + { + if (!__result || __instance.id != 0 || PlayerControl.LocalPlayer == null || ConsoleJoystick.inputState != ConsoleJoystick.ConsoleInputState.Gameplay || ControllerManager.Instance.IsUiControllerActive) return; + + var keybind = KeybindManager.Keybinds.FirstOrDefault(x => x.RewiredInputAction?.id == __0); + if (keybind is { Exclusive: false }) return; + + var sources = new CppCollections.List(__instance.GetCurrentInputSources(__0).Cast>()).ToArray(); + if (sources.Length == 0 || sources.Any(x => x.controllerType != ControllerType.Joystick)) return; + + foreach (var source in sources) + { + var claimed = false; + foreach (var custom in KeybindManager.Keybinds) + { + if (custom == keybind) break; + if (!custom.Exclusive || custom.RewiredInputAction == null || !KeybindUtils.IsControllerActionActive(custom.RewiredInputAction.id)) continue; + + var inputs = new CppCollections.List(__instance.GetCurrentInputSources(custom.RewiredInputAction.id).Cast>()).ToArray(); + if (!inputs.Any(x => x.controllerType == ControllerType.Joystick && x.controller.id == source.controller.id && x.actionElementMap.CheckForAssignmentConflict(source.actionElementMap))) continue; + + claimed = true; + break; + } + + if (!claimed) return; + } + + __result = false; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Player.GetButton), typeof(int))] + private static void GetButtonHeldPostfix(Player __instance, int __0, ref bool __result) + { + GetButtonPostfix(__instance, __0, ref __result); + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Player.GetButtonDown), typeof(string))] + private static void GetButtonByNamePostfix(Player __instance, string __0, ref bool __result) + { + if (__result) GetButtonPostfix(__instance, ReInput.mapping.GetActionId(__0), ref __result); + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Player.GetButton), typeof(string))] + private static void GetButtonHeldByNamePostfix(Player __instance, string __0, ref bool __result) + { + if (__result) GetButtonPostfix(__instance, ReInput.mapping.GetActionId(__0), ref __result); + } +} diff --git a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs index 5b89fa8..6b64159 100644 --- a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.LocalSettings; using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -13,12 +14,22 @@ namespace MiraAPI.Patches.LocalSettings; [HarmonyPatch(typeof(OptionsMenuBehaviour))] public static class OptionsMenuPatches { - internal static OptionsMenuBehaviour? Instance { get; private set; } - internal static BoxCollider2D MaskCollider = null!; - private static SpriteRenderer? background; + private static readonly Dictionary> TabButtons = []; + private static int _currentPage = 1; - private static int currentPage = 1; - private static Dictionary> tabButtons = new(); + internal static OptionsMenuBehaviour? Instance { get; private set; } + + [SuppressMessage( + "Critical Code Smell", + "S2223:Non-constant static fields should not be visible", + Justification = "Internal behaviour that does not need property-level validation.")] + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Read above.")] + internal static BoxCollider2D MaskCollider = null!; + + private static SpriteRenderer? background; /// /// Creates the s and their content. @@ -28,6 +39,7 @@ public static class OptionsMenuPatches public static void StartPostfix(OptionsMenuBehaviour __instance) { Instance = __instance; + // Fix for tabs not being clickable in the main menu if (!AmongUsClient.Instance.IsInGame) { @@ -63,12 +75,12 @@ public static class OptionsMenuPatches MaskCollider.isTrigger = true; MaskCollider.enabled = true; - currentPage = 1; - tabButtons.Clear(); + _currentPage = 1; + TabButtons.Clear(); float yOffset = 0; - int i = 0; - int tabIdx = 0; - int page = 1; + var i = 0; + var tabIdx = 0; + var page = 1; LocalSettingsTab.TabGroups.Clear(); foreach (var settings in LocalSettingsManager.AvailableTabs) { @@ -77,18 +89,20 @@ public static class OptionsMenuPatches { var button = settings.CreateTabButton(__instance, ref tabIdx, ref yOffset); button.GetComponent().Content = tab; - button.SetActive(page == currentPage); + button.SetActive(page == _currentPage); - if (!tabButtons.TryGetValue(page, out var list)) + if (!TabButtons.TryGetValue(page, out var list)) { - tabButtons.Add(page, new()); - list = tabButtons[page]; + TabButtons.Add(page, []); + list = TabButtons[page]; } + list.Add(button); yOffset += 0.6f; tabIdx++; i++; } + if (i > 7) { i = 0; @@ -97,64 +111,63 @@ public static class OptionsMenuPatches } } - void ChangePage(int increase) - { - currentPage += increase; - if (currentPage > tabButtons.Count) - { - currentPage = 1; - } - if (currentPage < 1) - { - currentPage = tabButtons.Count; - } - } - // Create page buttons // TODO: In-game menu has no close button. I tried making one from scratch but failed. Add one later plz - if (tabButtons.Count <= 1) + if (TabButtons.Count <= 1) { return; } - var nextButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent(); - nextButton.transform.localPosition = new Vector3(3.2f, 2.6f, -10); - nextButton.transform.localScale = new Vector2(0.25f, 0.25f); - var nextRend = nextButton.GetComponent(); - nextRend.sprite = MiraAssets.NextButton.LoadAsset(); - nextButton.gameObject.GetComponent().DestroyImmediate(); + CreatePageButton(__instance, 3.2f, false, 1); + CreatePageButton(__instance, 2.9f, true, -1); + } - var nextRollover = nextButton.gameObject.AddComponent(); - nextRollover.Target = nextRend; - nextRollover.OverColor = Palette.AcceptedGreen; + private static void CreatePageButton(OptionsMenuBehaviour menu, float x, bool flip, int direction) + { + var obj = new GameObject("PageButton") { layer = 5 }; + obj.transform.SetParent(menu.transform); + obj.transform.localPosition = new Vector3(x, 2.6f, -10); + obj.transform.localScale = new Vector3(0.25f, 0.25f, 1); - nextButton.OnClick = new Button.ButtonClickedEvent(); - nextButton.OnClick.AddListener( + var rend = obj.AddComponent(); + rend.sprite = MiraAssets.NextButton.LoadAsset(); + rend.flipX = flip; + + var col = obj.AddComponent(); + col.size = rend.sprite.bounds.size; + + var rollover = obj.AddComponent(); + rollover.Target = rend; + rollover.OverColor = Palette.AcceptedGreen; + rollover.OutColor = Color.white; + + var button = obj.AddComponent(); + button.Colliders = new Il2CppReferenceArray([col]); + button.OnClick = new Button.ButtonClickedEvent(); + button.OnClick.AddListener( (UnityAction)(() => { - ChangePage(1); + ChangePage(direction); UpdatePages(); })); + button.OnMouseOver = new UnityEvent(); + button.OnMouseOver.AddListener((UnityAction)(() => rollover.DoMouseOver())); + button.OnMouseOut = new UnityEvent(); + button.OnMouseOut.AddListener((UnityAction)(() => rollover.DoMouseOut())); + } - var backButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent(); - backButton.transform.localPosition = new Vector3(2.9f, 2.6f, -10); - backButton.transform.localScale = new Vector2(0.25f, 0.25f); - var backRend = backButton.GetComponent(); - backRend.sprite = MiraAssets.NextButton.LoadAsset(); - backRend.flipX = true; - backButton.gameObject.GetComponent().DestroyImmediate(); + private static void ChangePage(int increase) + { + _currentPage += increase; + if (_currentPage > TabButtons.Count) + { + _currentPage = 1; + } - var backRollover = backButton.gameObject.AddComponent(); - backRollover.Target = backRend; - backRollover.OverColor = Palette.AcceptedGreen; - - backButton.OnClick = new Button.ButtonClickedEvent(); - backButton.OnClick.AddListener( - (UnityAction)(() => - { - ChangePage(-1); - UpdatePages(); - })); + if (_currentPage < 1) + { + _currentPage = TabButtons.Count; + } } /// @@ -182,22 +195,20 @@ public static class OptionsMenuPatches /// [HarmonyPostfix] [HarmonyPatch(nameof(OptionsMenuBehaviour.Open))] - public static void OpenPostfix(OptionsMenuBehaviour __instance) + public static void OpenPostfix() { LocalSettingsManager.AvailableTabs.ForEach(CustomClose); - currentPage = 1; + _currentPage = 1; UpdatePages(); } private static void UpdatePages() { - foreach (var pages in tabButtons) + foreach (var pages in TabButtons) { pages.Value? .Where(x => x != null) - .Do( - x => x?.SetActive( - pages.Key == currentPage)); + .Do(x => x?.SetActive(pages.Key == _currentPage)); } } @@ -217,6 +228,7 @@ public static class OptionsMenuPatches tab.Open(); } + private static void CustomClose(LocalSettingsTab tab) { if (tab.TabButton?.Button) diff --git a/MiraAPI/Patches/Menu/HatsTabPatches.cs b/MiraAPI/Patches/Menu/HatsTabPatches.cs deleted file mode 100644 index 43d96c3..0000000 --- a/MiraAPI/Patches/Menu/HatsTabPatches.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using AmongUs.Data; -using HarmonyLib; -using MiraAPI.Utilities; -using MiraAPI.Utilities.Assets; -using Reactor.Utilities; -using Reactor.Utilities.Extensions; -using TMPro; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace MiraAPI.Patches.Menu; - -[HarmonyPatch(typeof(HatsTab))] -public static class HatsTabPatches -{ - private static SortedList> sortedHats = []; - private static Dictionary storeNames = []; - private static int currentPage; - - private static void PreviousPage(HatsTab hatsTab) - { - currentPage--; - currentPage = currentPage < 0 ? sortedHats.Count - 1 : currentPage; - GenerateHats(hatsTab, currentPage); - } - - private static void NextPage(HatsTab hatsTab) - { - currentPage++; - currentPage = currentPage > sortedHats.Count - 1 ? 0 : currentPage; - GenerateHats(hatsTab, currentPage); - } - - [HarmonyPatch(nameof(HatsTab.OnEnable))] - [HarmonyPrefix] - public static bool OnEnablePrefix(HatsTab __instance) - { - if (!AddressablesLoader.AddressableHatsExist) - { - return true; - } - __instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat); - var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList(); - - if (sortedHats.Count == 0) - { - var num = 0; - var comparer = new ControllableComparer(["vanilla"], [], StringComparer.InvariantCulture); - sortedHats = new SortedList>(comparer); - foreach (var hat in allHats) - { - if (!sortedHats.ContainsKey(hat.StoreName)) sortedHats[hat.StoreName] = []; - sortedHats[hat.StoreName].Add(hat); - - if (!storeNames.ContainsValue(hat.StoreName)) - { - storeNames.Add(num, hat.StoreName); - num++; - } - } - } - - InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage); - - GenerateHats(__instance, currentPage); - - return false; - } - - [HarmonyPatch(typeof(HatsTab), nameof(HatsTab.Update))] - [HarmonyPrefix] - public static void UpdatePrefix(HatsTab __instance) - { - if (!AddressablesLoader.AddressableHatsExist) - { - return; - } - if (sortedHats.Count == 0) return; - - if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow)) - { - PreviousPage(__instance); - } - else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow)) - { - NextPage(__instance); - } - } - - private static IEnumerator? loadRoutine; - private static int hatIndex; - - private static void GenerateHats(HatsTab __instance, int page) - { - if (loadRoutine != null) Coroutines.Stop(loadRoutine); - - hatIndex = 0; - foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy(); - __instance.ColorChips.Clear(); - __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.DeepDestroy(false)); - - var groupNameText = __instance.GetComponentInChildren(false); - var group = sortedHats.Where(x => x.Key == storeNames[page]); - - foreach ((string groupName, List hats) in group) - { - var text = Object.Instantiate(groupNameText, __instance.scroller.Inner); - text.enabled = true; - text.gameObject.transform.localScale = Vector3.one; - text.GetComponent().Destroy(); - text.EnableStencilMasking(); - text.text = $"{groupName} ({currentPage + 1}/{sortedHats.Count})"; - text.alignment = TextAlignmentOptions.Center; - text.fontSize = 5f; - text.fontSizeMax = 5f; - text.fontSizeMin = 0f; - var xLerp = __instance.XRange.Lerp(0.5f); - var yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset; - text.transform.localPosition = new Vector3(xLerp, yLerp, -1f); - - hatIndex += 5; - loadRoutine = Coroutines.Start(CoGenerateChips(__instance, hats)); - } - __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f; - __instance.currentHatIsEquipped = true; - } - - private static IEnumerator CoGenerateChips(HatsTab __instance, List hats) - { - __instance.scroller.ScrollToTop(); - var batchSize = 5; - for (var i = 0; i < hats.Count; i += batchSize) - { - var batch = hats.Skip(i).Take(batchSize).ToList(); - - foreach (var hat in batch.OrderBy(HatManager.Instance.allHats.IndexOf)) - { - var hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f)); - var hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset; - GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), hat); - hatIndex += 1; - yield return null; - } - - __instance.SetScrollerBounds(); - yield return new WaitForSeconds(0.01f); - } - __instance.currentHatIsEquipped = true; - loadRoutine = null; - } - - private static void GenerateColorChip(HatsTab __instance, Vector2 position, HatData hat) - { - var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner); - colorChip.gameObject.name = hat.ProductId; - if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard) - { - colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip); - colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat))); - colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat)))); - } - else - { - colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectHat(hat))); - } - - colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color); - colorChip.Button.ClickMask = __instance.scroller.Hitbox; - colorChip.SelectionHighlight.gameObject.SetActive(false); - __instance.UpdateMaterials(colorChip.Inner.FrontLayer, hat); - colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.SimpleUI); - colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f); - colorChip.Inner.transform.localPosition = hat.ChipOffset + new Vector2(0f, -0.3f); - colorChip.Tag = hat; - __instance.ColorChips.Add(colorChip); - } -} diff --git a/MiraAPI/Patches/Menu/InventoryUtility.cs b/MiraAPI/Patches/Menu/InventoryUtility.cs deleted file mode 100644 index 95004d0..0000000 --- a/MiraAPI/Patches/Menu/InventoryUtility.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using MiraAPI.Utilities.Assets; -using TMPro; -using UnityEngine; -using UnityEngine.Events; -using UnityEngine.UI; -using Object = UnityEngine.Object; - -namespace MiraAPI.Patches.Menu; - -public static class InventoryUtility -{ - public static void CreateNextBackButtons(T tab, Action previousPage, Action nextPage) where T : InventoryTab - { - var title = tab.transform.FindChild("Text"); - title.localPosition = new Vector3(.9123f, -.23f, -55); - title.GetComponent().alignment = TextAlignmentOptions.Center; - - tab.scroller.Hitbox.transform.localScale -= new Vector3(0, 0.1f, 0); - tab.scroller.Hitbox.transform.localPosition -= new Vector3(0, 0.1f, 0); - - var leftButtonElement = Object.Instantiate(PlayerCustomizationMenu.Instance.BackButton.gameObject, tab.transform); - var leftSpriteRenderer = leftButtonElement.GetComponent(); - var leftButton = leftButtonElement.GetComponent(); - - leftButtonElement.transform.localPosition = new Vector3(-1.1f, -0.2f, -55); - leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset(); - leftSpriteRenderer.flipX = true; - - leftButton.OnMouseOver = new UnityEvent(); - leftButton.OnMouseOver.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset())); - leftButton.OnMouseOut = new UnityEvent(); - leftButton.OnMouseOut.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset())); - - leftButton.OnClick = new Button.ButtonClickedEvent(); - leftButton.OnClick.AddListener((Action)(() => previousPage(tab))); - - var rightButtonElement = Object.Instantiate(leftButtonElement, tab.transform); - var rightSpriteRenderer = rightButtonElement.GetComponent(); - var rightButton = rightButtonElement.GetComponent(); - - rightButtonElement.transform.localPosition = new Vector3(2.8f, -0.2f, -55); - rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset(); - rightSpriteRenderer.flipX = false; - - rightButton.OnMouseOver = new UnityEvent(); - rightButton.OnMouseOver.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset())); - rightButton.OnMouseOut = new UnityEvent(); - rightButton.OnMouseOut.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset())); - - rightButton.OnClick = new Button.ButtonClickedEvent(); - rightButton.OnClick.AddListener((Action)(() => nextPage(tab))); - } -} diff --git a/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs b/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs index f69e374..aff0906 100644 --- a/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs +++ b/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs @@ -2,7 +2,6 @@ using System.Collections; using HarmonyLib; using MiraAPI.LocalSettings; -using MiraAPI.Utilities.Assets; using Reactor.Utilities; using UnityEngine; @@ -17,16 +16,15 @@ public static class MainMenuManagerPatches internal static bool NeedsDeepDestroy { get; private set; } /// - /// A postifix on to load all the addressables registered. + /// A postfix on to load all the addressables registered. /// [HarmonyPatch(nameof(MainMenuManager.Awake))] [HarmonyPostfix] public static void AwakePostfix() { - var requiredVersion = new Version(2026, 6, 5); + var requiredVersion = new Version(2026, 9, 29); var version = Version.Parse(Application.version); NeedsDeepDestroy = version >= requiredVersion; - AddressablesLoader.LoadAll(); Coroutines.Start(SetFps()); } diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs deleted file mode 100644 index 6e96d5f..0000000 --- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs +++ /dev/null @@ -1,151 +0,0 @@ -using AmongUs.Data; -using HarmonyLib; -using Innersloth.Assets; -using MiraAPI.Utilities; -using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; -using TMPro; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace MiraAPI.Patches.Menu; - -[HarmonyPatch(typeof(NameplatesTab))] -public static class NameplatesTabPatches -{ - private static readonly SortedList> SortedNameplates = new(new ControllableComparer(["vanilla"], [], StringComparer.InvariantCulture)); - private static int currentPage; - internal static void AddRange(IEnumerable<(string Key, NamePlateData Visor)> data) - { - foreach (var item in data) - { - if (!SortedNameplates.ContainsKey(item.Key)) SortedNameplates.Add(item.Key, []); - SortedNameplates[item.Key].Add(item.Visor); - } - } - - private static void PreviousPage(NameplatesTab tab) - { - currentPage--; - currentPage = currentPage < 0 ? SortedNameplates.Count - 1 : currentPage; - GenerateHats(tab, currentPage); - } - - private static void NextPage(NameplatesTab tab) - { - currentPage++; - currentPage = currentPage > SortedNameplates.Count - 1 ? 0 : currentPage; - GenerateHats(tab, currentPage); - } - - [HarmonyPatch(nameof(NameplatesTab.OnEnable))] - [HarmonyPrefix] - public static bool OnEnablePrefix(NameplatesTab __instance) - { - if (!AddressablesLoader.AddressableNameplatesExist) - { - return true; - } - __instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId; - - if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(HatManager.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x))); - - InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage); - - GenerateHats(__instance, currentPage); - - return false; - } - - [HarmonyPatch(typeof(NameplatesTab), nameof(NameplatesTab.Update))] - [HarmonyPrefix] - - public static void UpdatePrefix(NameplatesTab __instance) - { - if (!AddressablesLoader.AddressableNameplatesExist) - { - return; - } - if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow)) - { - PreviousPage(__instance); - } - else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow)) - { - NextPage(__instance); - } - } - - private static void GenerateHats(NameplatesTab __instance, int page) - { - foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy(); - __instance.ColorChips.Clear(); - __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.DeepDestroy(false)); - - var groupNameText = __instance.GetComponentInChildren(false); - - int hatIndex = 0; - - var (groupName, nameplates) = SortedNameplates.ToArray()[page]; - var text = Object.Instantiate(groupNameText, __instance.scroller.Inner); - text.enabled = true; - text.gameObject.transform.localScale = Vector3.one; - text.GetComponent().Destroy(); - text.EnableStencilMasking(); - text.text = $"{groupName} ({currentPage + 1}/{SortedNameplates.Count})"; - text.alignment = TextAlignmentOptions.Center; - text.fontSize = 5f; - text.fontSizeMax = 5f; - text.fontSizeMin = 0f; - float xLerp = __instance.XRange.Lerp(0.5f); - float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset; - text.transform.localPosition = new Vector3(xLerp, yLerp, -1f); - - hatIndex += 2; - foreach (var visor in nameplates.OrderBy(HatManager.Instance.allNamePlates.IndexOf)) - { - float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f)); - float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset; - GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor); - hatIndex += 1; - } - - __instance.SetScrollerBounds(); - __instance.currentNameplateIsEquipped = true; - } - - private static void GenerateColorChip(NameplatesTab __instance, Vector2 position, NamePlateData namePlate) - { - var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner); - colorChip.gameObject.name = namePlate.ProductId; - - if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard) - { - colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip); - colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate))); - colorChip.Button.OnMouseOut.AddListener( - (Action)(() => __instance.SelectNameplate( - HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate)))); - } - else - { - colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectNameplate(namePlate))); - } - - colorChip.Button.ClickMask = __instance.scroller.Hitbox; - colorChip.ProductId = namePlate.ProdId; - - var x = (NamePlateViewData viewdata) => - { - colorChip.transform.GetChild(1).GetComponent().sprite = viewdata?.Image; - // (colorChip as NameplateChip).image.sprite = viewdata?.Image; - }; - __instance.StartCoroutine(AddressableAssetExtensions.CoLoadAssetAsync(__instance, namePlate.GetAssetReference(), x)); - colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f); - colorChip.Tag = namePlate; - __instance.ColorChips.Add(colorChip); - } -} diff --git a/MiraAPI/Patches/Menu/SkinsTabPatches.cs b/MiraAPI/Patches/Menu/SkinsTabPatches.cs deleted file mode 100644 index e1c97a5..0000000 --- a/MiraAPI/Patches/Menu/SkinsTabPatches.cs +++ /dev/null @@ -1,148 +0,0 @@ -using AmongUs.Data; -using HarmonyLib; -using MiraAPI.Utilities; -using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using TMPro; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace MiraAPI.Patches.Menu; - -[HarmonyPatch(typeof(SkinsTab))] -public static class SkinsTabPatches -{ - private static SortedList> sortedSkins = []; - private static int currentPage; - - private static void PreviousPage(SkinsTab tab) - { - currentPage--; - currentPage = currentPage < 0 ? sortedSkins.Count - 1 : currentPage; - GenerateHats(tab, currentPage); - } - - private static void NextPage(SkinsTab tab) - { - currentPage++; - currentPage = currentPage > sortedSkins.Count - 1 ? 0 : currentPage; - GenerateHats(tab, currentPage); - } - - [HarmonyPatch(nameof(SkinsTab.OnEnable))] - [HarmonyPrefix] - public static bool OnEnablePrefix(SkinsTab __instance) - { - if (!AddressablesLoader.AddressableSkinsExist) - { - return true; - } - __instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId; - var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList(); - - if (sortedSkins.Count == 0) - { - var comparer = new ControllableComparer(["vanilla"], [], StringComparer.InvariantCulture); - sortedSkins = new SortedList>(comparer); - foreach (var skin in allSkins) - { - if (!sortedSkins.ContainsKey(skin.StoreName)) sortedSkins[skin.StoreName] = []; - sortedSkins[skin.StoreName].Add(skin); - } - } - - InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage); - - GenerateHats(__instance, currentPage); - - return false; - } - - [HarmonyPatch(typeof(SkinsTab), nameof(SkinsTab.Update))] - [HarmonyPrefix] - - public static void UpdatePrefix(SkinsTab __instance) - { - if (!AddressablesLoader.AddressableSkinsExist) - { - return; - } - if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow)) - { - PreviousPage(__instance); - } - else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow)) - { - NextPage(__instance); - } - } - - private static void GenerateHats(SkinsTab __instance, int page) - { - foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy(); - __instance.ColorChips.Clear(); - __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.DeepDestroy(false)); - - var groupNameText = __instance.GetComponentInChildren(false); - - int hatIndex = 0; - - var (groupName, skins) = sortedSkins.ToArray()[page]; - var text = Object.Instantiate(groupNameText, __instance.scroller.Inner); - text.enabled = true; - text.gameObject.transform.localScale = Vector3.one; - text.GetComponent().Destroy(); - text.EnableStencilMasking(); - text.text = $"{groupName} ({currentPage + 1}/{sortedSkins.Count})"; - text.alignment = TextAlignmentOptions.Center; - text.fontSize = 5f; - text.fontSizeMax = 5f; - text.fontSizeMin = 0f; - float xLerp = __instance.XRange.Lerp(0.5f); - float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset; - text.transform.localPosition = new Vector3(xLerp, yLerp, -1f); - - hatIndex += 5; - foreach (var skin in skins.OrderBy(HatManager.Instance.allSkins.IndexOf)) - { - float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f)); - float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset; - GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), skin); - hatIndex += 1; - } - - __instance.SetScrollerBounds(); - __instance.currentSkinIsEquipped = true; - } - - private static void GenerateColorChip(SkinsTab __instance, Vector2 position, SkinData skin) - { - var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner); - colorChip.gameObject.name = skin.ProductId; - if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard) - { - colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip); - colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin))); - colorChip.Button.OnMouseOut.AddListener( - (Action)(() => - __instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin)))); - } - else - { - colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectSkin(skin))); - } - colorChip.Button.ClickMask = __instance.scroller.Hitbox; - colorChip.ProductId = skin.ProductId; - colorChip.SelectionHighlight.gameObject.SetActive(false); - __instance.UpdateMaterials(colorChip.Inner.FrontLayer, skin); - skin.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color); - colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f); - colorChip.Inner.transform.localPosition = skin.ChipOffset + new Vector2(0f, -0.3f); - colorChip.Tag = skin; - __instance.ColorChips.Add(colorChip); - } -} diff --git a/MiraAPI/Patches/Menu/VisorsTabPatches.cs b/MiraAPI/Patches/Menu/VisorsTabPatches.cs deleted file mode 100644 index a1627cf..0000000 --- a/MiraAPI/Patches/Menu/VisorsTabPatches.cs +++ /dev/null @@ -1,147 +0,0 @@ -using AmongUs.Data; -using HarmonyLib; -using MiraAPI.Utilities; -using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; -using TMPro; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace MiraAPI.Patches.Menu; - -[HarmonyPatch(typeof(VisorsTab))] -public static class VisorsTabPatches -{ - private static readonly SortedList> SortedVisors = new(new ControllableComparer(["vanilla"], [], StringComparer.InvariantCulture)); - private static int currentPage; - internal static void AddRange(IEnumerable<(string Key, VisorData Visor)> data) - { - foreach (var item in data) - { - if (!SortedVisors.ContainsKey(item.Key)) SortedVisors.Add(item.Key, []); - SortedVisors[item.Key].Add(item.Visor); - } - } - - private static void PreviousPage(VisorsTab tab) - { - currentPage--; - currentPage = currentPage < 0 ? SortedVisors.Count - 1 : currentPage; - GenerateHats(tab, currentPage); - } - - private static void NextPage(VisorsTab tab) - { - currentPage++; - currentPage = currentPage > SortedVisors.Count - 1 ? 0 : currentPage; - GenerateHats(tab, currentPage); - } - - [HarmonyPatch(nameof(VisorsTab.OnEnable))] - [HarmonyPrefix] - public static bool OnEnablePrefix(VisorsTab __instance) - { - if (!AddressablesLoader.AddressableVisorsExist) - { - return true; - } - __instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId; - - if (!SortedVisors.ContainsKey("Vanilla")) AddRange(HatManager.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x))); - - InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage); - - GenerateHats(__instance, currentPage); - - return false; - } - - [HarmonyPatch(typeof(VisorsTab), nameof(VisorsTab.Update))] - [HarmonyPrefix] - - public static void UpdatePrefix(VisorsTab __instance) - { - if (!AddressablesLoader.AddressableVisorsExist) - { - return; - } - if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow)) - { - PreviousPage(__instance); - } - else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow)) - { - NextPage(__instance); - } - } - - private static void GenerateHats(VisorsTab __instance, int page) - { - foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy(); - __instance.ColorChips.Clear(); - __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.DeepDestroy(false)); - - var groupNameText = __instance.GetComponentInChildren(false); - - int hatIndex = 0; - - var (groupName, visors) = SortedVisors.ToArray()[page]; - var text = Object.Instantiate(groupNameText, __instance.scroller.Inner); - text.enabled = true; - text.gameObject.transform.localScale = Vector3.one; - text.GetComponent().Destroy(); - text.EnableStencilMasking(); - text.text = $"{groupName} ({currentPage + 1}/{SortedVisors.Count})"; - text.alignment = TextAlignmentOptions.Center; - text.fontSize = 5f; - text.fontSizeMax = 5f; - text.fontSizeMin = 0f; - float xLerp = __instance.XRange.Lerp(0.5f); - float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset; - text.transform.localPosition = new Vector3(xLerp, yLerp, -1f); - - hatIndex += 5; - foreach (var visor in visors.OrderBy(HatManager.Instance.allVisors.IndexOf)) - { - float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f)); - float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset; - GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor); - hatIndex += 1; - } - - __instance.SetScrollerBounds(); - __instance.currentVisorIsEquipped = true; - } - - private static void GenerateColorChip(VisorsTab __instance, Vector2 position, VisorData visor) - { - var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner); - colorChip.gameObject.name = visor.ProductId; - - if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard) - { - colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip); - colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor))); - colorChip.Button.OnMouseOut.AddListener( - (Action)(() => - __instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor)))); - } - else - { - colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectVisor(visor))); - } - - colorChip.Button.ClickMask = __instance.scroller.Hitbox; - colorChip.ProductId = visor.ProductId; - colorChip.SelectionHighlight.gameObject.SetActive(false); - __instance.UpdateMaterials(colorChip.Inner.FrontLayer, visor); - visor.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color); - colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f); - colorChip.Inner.transform.localPosition = visor.ChipOffset + new Vector2(0f, -0.3f); - colorChip.Tag = visor; - __instance.ColorChips.Add(colorChip); - } -} diff --git a/MiraAPI/Patches/MinigameBeginErrorPatch.cs b/MiraAPI/Patches/MinigameBeginErrorPatch.cs index da7de72..71695ff 100644 --- a/MiraAPI/Patches/MinigameBeginErrorPatch.cs +++ b/MiraAPI/Patches/MinigameBeginErrorPatch.cs @@ -8,12 +8,12 @@ namespace MiraAPI.Patches; [HarmonyPatch] public static class MinigameBeginErrorPatch { - private static readonly Logger _logger = new(Logger.Category.Gameplay, "Minigame"); + private static readonly Logger Logger = new(Logger.Category.Gameplay, "Minigame"); [HarmonyPatch(typeof(Minigame), nameof(Minigame.Begin))] [HarmonyPrefix] public static void Prefix(Minigame __instance) { - __instance.logger ??= _logger; + __instance.logger ??= Logger; } } diff --git a/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs index eee933c..d14e17f 100644 --- a/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs +++ b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs @@ -33,6 +33,7 @@ internal static class EndGameDidWinPatch { CachedWinners.Add(new CachedPlayerData(networkedPlayerInfo)); } + continue; } @@ -52,7 +53,7 @@ internal static class EndGameDidWinPatch } } - public static void Postfix(EndGameResult endGameResult) + public static void Postfix() { EndGameResult.CachedWinners.Clear(); foreach (var winner in CachedWinners) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 46108fc..9dacf03 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,10 +1,10 @@ using System; using System.Collections; +using System.Collections.Generic; 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; @@ -12,7 +12,6 @@ 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; @@ -26,21 +25,21 @@ 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 readonly List VanillaHeaders = []; + private static readonly List VanillaOptions = []; + private static readonly List MainOptions = []; + private static readonly Dictionary> GameModeGroups = []; + private static readonly Dictionary> GameModeOptions = []; + private static readonly Dictionary> GameModeHeaders = []; private static TextMeshPro _gamemodeDescription = null!; private static SpriteRenderer _modIcon = null!; [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] - // ReSharper disable once InconsistentNaming public static bool InitPatch(GameOptionsMenu __instance) { - __instance.Children ??= new Il2CppSystem.Collections.Generic.List(); + Helpers.CacheAdvancedSettingsRowPrefab(__instance.numberOptionOrigin); + __instance.Children ??= new(); __instance.Children.Clear(); if (MenuState.Instance.CurrentModIdx == 0) @@ -78,12 +77,13 @@ internal static class GameOptionsMenuPatch return false; } - public static float AdditionalVanillaScrollNum; + private static float additionalVanillaScrollNum; + internal static void ToggleGamemodeOptions(AbstractGameMode gameMode, GameOptionsMenu instance) { - float num = -1.217f; + var num = -1.217f; instance.Children.Clear(); - foreach (var opt in _mainOptions) + foreach (var opt in MainOptions) { instance.Children.Add(opt); } @@ -101,8 +101,7 @@ internal static class GameOptionsMenuPatch texture, new Rect(0, 0, texture.width, texture.height), Vector2.one / 2, - 100f / scale - ); + 100f / scale); _modIcon.sprite = newSprite; _modIcon.drawMode = SpriteDrawMode.Simple; @@ -117,12 +116,13 @@ internal static class GameOptionsMenuPatch _gamemodeDescription.text = MiraLocaleManager.Get(gameMode.Description); if (gameMode.ShowNormalGameSettings) { - foreach (var opt in _vanillaHeaders) + foreach (var opt in VanillaHeaders) { num -= 0.63f; opt.gameObject.SetActive(true); } - foreach (var opt in _vanillaOptions) + + foreach (var opt in VanillaOptions) { opt.gameObject.SetActive(true); instance.Children.Add(opt); @@ -131,29 +131,30 @@ internal static class GameOptionsMenuPatch } else { - foreach (var opt in _vanillaHeaders) + foreach (var opt in VanillaHeaders) { opt.gameObject.SetActive(false); } - foreach (var opt in _vanillaOptions) + + foreach (var opt in VanillaOptions) { opt.gameObject.SetActive(false); } } - foreach (var pair in _gameModeGroups) + foreach (var pair in GameModeGroups) { if (pair.Value.Count != 0) { if (gameMode == pair.Key) { - foreach (var opt in _gameModeHeaders[gameMode]) + foreach (var opt in GameModeHeaders[gameMode]) { num -= 0.63f; opt.gameObject.SetActive(true); } - foreach (var opt in _gameModeOptions[gameMode]) + foreach (var opt in GameModeOptions[gameMode]) { opt.gameObject.SetActive(true); instance.Children.Add(opt); @@ -162,12 +163,12 @@ internal static class GameOptionsMenuPatch } else { - foreach (var opt in _gameModeHeaders[pair.Key]) + foreach (var opt in GameModeHeaders[pair.Key]) { opt.gameObject.SetActive(false); } - foreach (var opt in _gameModeOptions[pair.Key]) + foreach (var opt in GameModeOptions[pair.Key]) { opt.gameObject.SetActive(false); } @@ -183,45 +184,102 @@ internal static class GameOptionsMenuPatch instance.ControllerSelectable.Add(obj); } } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); } - + + internal static void RefreshForGameMode() + { + if (CustomGameModeManager.ActiveMode == null || + !GameSettingMenu.Instance || + !GameSettingMenu.Instance.GameSettingsTab || + !MenuState.Instance || + MenuState.Instance.CurrentModIdx != 0 || + GameModeGroups.Count == 0) + { + return; + } + + ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, GameSettingMenu.Instance.GameSettingsTab); + GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive( + CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); + } + + internal static List> GetActiveGameSettingsRows(Func formatBool) + { + var menu = GameSettingMenu.Instance ? GameSettingMenu.Instance.GameSettingsTab : null; + if (!menu || menu!.Children == null) + { + return []; + } + + var rows = new List>(); + foreach (var option in menu.Children) + { + if (!option || !option.gameObject.activeInHierarchy || + option == menu.MapPicker || + (GameModeOption.OptionBehaviour && option == GameModeOption.OptionBehaviour) || + !option.Data) + { + continue; + } + + var title = option switch + { + ToggleOption toggle => toggle.TitleText.text, + NumberOption number => number.TitleText.text, + StringOption str => str.TitleText.text, + PlayerOption player => player.TitleText.text, + _ => TranslationController.Instance.GetString(option.Data.Title), + }; + + var value = option switch + { + ToggleOption toggle => formatBool(toggle.GetBool()), + NumberOption number => number.ValueText.text, + StringOption str => str.ValueText.text, + PlayerOption player => player.ValueText.text, + _ => option.Data.GetValueString(option.GetFloat()), + }; + + rows.Add(new KeyValuePair(title, value)); + } + + return rows; + } private static void CreateSettings(GameOptionsMenu instance, Transform container) { - float num = 0.713f; - _vanillaHeaders.Clear(); - _vanillaOptions.Clear(); - _mainOptions.Clear(); - _gameModeHeaders.Clear(); - _gameModeOptions.Clear(); - _gameModeGroups.Clear(); + var 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); + var 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); + GameModeOption.OptionBehaviour = Object.Instantiate(instance.stringOptionOrigin, container); num -= 0.63f; GameModeOption.OptionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); GameModeOption.OptionBehaviour.SetClickMask(instance.ButtonClickMask); - StringGameSetting setting = ScriptableObject.CreateInstance(); + var setting = ScriptableObject.CreateInstance(); setting.Type = OptionTypes.MultipleChoice; setting.Title = GameModeOption.GamemodeName; - setting.Index = GameModeOption._lastValue; + setting.Index = GameModeOption.LastValue; setting.Values = new Il2CppStructArray([GameModeOption.Values[0]]); GameModeOption.OptionBehaviour.SetUpFromData(setting, 20); - GameModeOption.Set(GameModeOption._lastValue); + GameModeOption.Set(GameModeOption.LastValue); GameModeOption.OptionBehaviour.TitleText.fontSize = 3; instance.Children.Add(GameModeOption.OptionBehaviour); foreach (var optionBehaviour in instance.Children) { - _mainOptions.Add(optionBehaviour); + MainOptions.Add(optionBehaviour); } + for (var i = 1; i < GameModeOption.Values.Count; i++) { GameModeOption.OptionBehaviour.Values = @@ -268,89 +326,47 @@ internal static class GameOptionsMenuPatch _gamemodeDescription.alignment = TextAlignmentOptions.Left; num -= 1.3f; - AdditionalVanillaScrollNum = 0f; - foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) + additionalVanillaScrollNum = 0f; + foreach (var rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) { - CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate( - instance.categoryHeaderOrigin, - Vector3.zero, - Quaternion.identity, - container); + var categoryHeaderMasked = Object.Instantiate(instance.categoryHeaderOrigin, 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); + VanillaHeaders.Add(categoryHeaderMasked); num -= 0.63f; - AdditionalVanillaScrollNum -= 0.63f; - foreach (BaseGameSetting baseGameSetting in rulesCategory.AllGameSettings) + additionalVanillaScrollNum -= 0.63f; + foreach (var baseGameSetting in rulesCategory.AllGameSettings) { - switch (baseGameSetting.Type) + OptionBehaviour? optionBehaviour = baseGameSetting.Type switch { - 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; - } - } + OptionTypes.Checkbox => Object.Instantiate(instance.checkboxOrigin, container), + OptionTypes.String => Object.Instantiate(instance.stringOptionOrigin, container), + OptionTypes.Float or OptionTypes.Int => Object.Instantiate(instance.numberOptionOrigin, container), + OptionTypes.Player => Object.Instantiate(instance.playerOptionOrigin, container), + _ => null, + }; + + if (!optionBehaviour) + continue; + + optionBehaviour!.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + VanillaOptions.Add(optionBehaviour); num -= 0.45f; - AdditionalVanillaScrollNum -= 0.45f; + additionalVanillaScrollNum -= 0.45f; } } - foreach (var optionBehaviour in _vanillaOptions) + + foreach (var optionBehaviour in VanillaOptions) { if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost) { optionBehaviour.SetAsPlayer(); } + instance.Children.Add(optionBehaviour); } @@ -363,19 +379,23 @@ internal static class GameOptionsMenuPatch { var filteredGroups = ModdedOptionsManager.GameModeOptionGroups - .Where(x => x.Key == mode.GetType()).Select(y => y.Value).SelectMany(y => y) ?? []; - _gameModeGroups.Add(mode, filteredGroups.ToList()); + .Where(x => x.Key == mode.GetType()) + .Select(y => y.Value) + .SelectMany(y => y) + .ToList(); + GameModeGroups.Add(mode, [.. filteredGroups]); - var optionBehaviours = new System.Collections.Generic.List(); - var categoryHeaders = new System.Collections.Generic.List(); - var newNum = mode.ShowNormalGameSettings ? num : num - AdditionalVanillaScrollNum; + var optionBehaviours = new List(); + var categoryHeaders = new 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); + + GameModeHeaders.Add(mode, categoryHeaders); + GameModeOptions.Add(mode, optionBehaviours); } instance.ControllerSelectable.Clear(); @@ -383,32 +403,33 @@ internal static class GameOptionsMenuPatch { instance.ControllerSelectable.Add(obj); } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); } [HarmonyPostfix] [HarmonyPatch(nameof(GameOptionsMenu.OnEnable))] - public static void OpenPatch(GameOptionsMenu __instance) + public static void OpenPatch() { HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = false; } [HarmonyPostfix] [HarmonyPatch(nameof(GameOptionsMenu.OnDisable))] - public static void ClosePatch(GameOptionsMenu __instance) + public static void ClosePatch() { HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = true; } [HarmonyPostfix] [HarmonyPatch(nameof(GameOptionsMenu.Update))] - // ReSharper disable once InconsistentNaming public static void UpdatePatch(GameOptionsMenu __instance) { - if (MenuState.Instance.CurrentModIdx == 0 && _gameModeGroups.Count <= 0) + if (MenuState.Instance.CurrentModIdx == 0 && GameModeGroups.Count <= 0) { return; } + var num = 2.1f; switch (MenuState.Instance.CurrentMenu) @@ -429,10 +450,11 @@ internal static class GameOptionsMenuPatch GamemodeOptionsUpdate(ref num); break; } + var filteredGroups = MenuState.Instance.CurrentMod.InternalOptionGroups .Where(x => (x.OptionableType == null || CustomGameModeManager.ActiveMode!.GetType().IsAssignableTo(x.OptionableType)) && - x.ParentMenu == MenuCategory.Game) ?? []; + x.ParentMenu == MenuCategory.Game); foreach (var group in filteredGroups) { @@ -444,7 +466,8 @@ internal static class GameOptionsMenuPatch __instance.scrollBar.SetYBoundsMax(-num - 1.65f); } - 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) + + private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref List optionBehaviours, ref List categoryHeaders) { var categoryHeaderMasked = Object.Instantiate( menu.categoryHeaderOrigin, @@ -551,7 +574,7 @@ internal static class GameOptionsMenuPatch foreach (var option in group.Options) { if (option.OptionBehaviour != null && option.OptionBehaviour.gameObject != null) - option.OptionBehaviour?.gameObject.SetActive(false); + option.OptionBehaviour.gameObject.SetActive(false); } return; @@ -602,10 +625,10 @@ internal static class GameOptionsMenuPatch if (mode.ShowNormalGameSettings) { - num += AdditionalVanillaScrollNum; + num += additionalVanillaScrollNum; } - if (!_gameModeGroups.TryGetValue(mode, out var groups) || groups.Count == 0) + if (!GameModeGroups.TryGetValue(mode, out var groups) || groups.Count == 0) { return; } @@ -615,6 +638,7 @@ internal static class GameOptionsMenuPatch UpdateGroup(group, ref num); } } + private static void CustomMenuOneUpdate(ref float num) { var groups = MenuState.Instance.CurrentMod.InternalOptionGroups @@ -655,6 +679,8 @@ internal static class GameOptionsMenuPatch { menu.MapPicker.gameObject.SetActive(false); var mod = MenuState.Instance.CurrentMod; + var container = MenuState.Instance.CurrentContainer.transform; + PresetManager.DefaultPresets.TryGetValue(mod, out var defaultPreset); var filteredGroups = MenuState.Instance.CurrentMenu switch { @@ -674,20 +700,19 @@ internal static class GameOptionsMenuPatch } _creationCoroutine = GameSettingMenu.Instance.StartCoroutine(CreateGroups().WrapToIl2Cpp()); + return; IEnumerator CreateGroups() { foreach (var group in filteredGroups) { - yield return CoCreateGroup(menu, group); + yield return CoCreateGroup(menu, group, container, defaultPreset); } } } - private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group) + private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, OptionPreset? defaultPreset) { - var container = MenuState.Instance.CurrentContainer.transform; - group.Ready = false; if (group.Header == null || !group.Header) { @@ -741,20 +766,13 @@ internal static class GameOptionsMenuPatch group.Header = categoryHeaderMasked; } - var options = group.Options.Where(opt => opt.OptionBehaviour == null || !opt.OptionBehaviour - ).Select(opt => opt.CreateOption( - menu.checkboxOrigin, - menu.numberOptionOrigin, - menu.stringOptionOrigin, - menu.playerOptionOrigin, - container) - ); - - OptionPreset? defaultPreset = null; - if (PresetManager.DefaultPresets.TryGetValue(MenuState.Instance.CurrentMod, out var preset)) - { - defaultPreset = preset; - } + var options = group.Options.Where(opt => opt.OptionBehaviour == null || !opt.OptionBehaviour) + .Select(opt => opt.CreateOption( + menu.checkboxOrigin, + menu.numberOptionOrigin, + menu.stringOptionOrigin, + menu.playerOptionOrigin, + container)); yield return options.CoLoopWithBudget(newOpt => { @@ -802,68 +820,53 @@ internal static class GameOptionsMenuPatch } menu.Children.Add(newOpt); - var resetBtn = new GameObject("ResetOption"); - resetBtn.transform.parent = newOpt.transform; - resetBtn.transform.localScale = new(.5f, .5f, 1); - resetBtn.layer = LayerMask.NameToLayer("UI"); - resetBtn.transform.localPosition = new Vector3(-3.1f, 0f, -2f); - var resetRend = resetBtn.AddComponent(); - resetRend.sprite = MiraAssets.ResetButton.LoadAsset(); - resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) - ? Color.white - : group.GroupColor.FindAlternateColor(); - var resetBoxCol = resetBtn.gameObject.AddComponent(); - resetBoxCol.size = new Vector2(1f, 1f); - resetBoxCol.offset = new Vector2(0, 0); - var passiveButton = resetBtn.AddComponent(); - passiveButton.OnClick = new Button.ButtonClickedEvent(); - passiveButton.ClickSound = menu.BackButton.GetComponent().ClickSound; - passiveButton.OnMouseOver = new UnityEvent(); - passiveButton.OnMouseOver.AddListener( - (UnityAction)(() => + if (defaultPreset != null && defaultPreset.IsOptionInPreset(newOpt) && + newOpt is ToggleOption or NumberOption or StringOption) + { + var resetBtn = new GameObject("ResetOption") { - resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor - ? group.GroupColor - : MiraAssets.AcceptedTeal; - })); - passiveButton.OnMouseOut = new UnityEvent(); - passiveButton.OnMouseOut.AddListener( - (UnityAction)(() => - { - resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) - ? Color.white - : group.GroupColor.FindAlternateColor(); - })); - if (newOpt is ToggleOption toggleOpt) - { + transform = + { + parent = newOpt.transform, + localScale = new(.5f, .5f, 1), + localPosition = new Vector3(-3.1f, 0f, -2f), + }, + layer = LayerMask.NameToLayer("UI"), + }; + var resetRend = resetBtn.AddComponent(); + resetRend.sprite = MiraAssets.ResetButton.LoadAsset(); + resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + var resetBoxCol = resetBtn.gameObject.AddComponent(); + resetBoxCol.size = new Vector2(1f, 1f); + resetBoxCol.offset = new Vector2(0, 0); + var passiveButton = resetBtn.AddComponent(); + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.ClickSound = menu.BackButton.GetComponent().ClickSound; + passiveButton.OnMouseOver = new UnityEvent(); + passiveButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + })); + passiveButton.OnMouseOut = new UnityEvent(); + passiveButton.OnMouseOut.AddListener( + (UnityAction)(() => + { + resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + })); passiveButton.OnClick.AddListener( (UnityAction)(() => { - defaultPreset!.ResetOption(toggleOpt); + defaultPreset.ResetOption(newOpt); })); } - else if (newOpt is NumberOption numOpt) - { - passiveButton.OnClick.AddListener( - (UnityAction)(() => - { - defaultPreset!.ResetOption(numOpt); - })); - } - else if (newOpt is StringOption strOpt) - { - passiveButton.OnClick.AddListener( - (UnityAction)(() => - { - defaultPreset!.ResetOption(strOpt); - })); - } - - if (!defaultPreset!.IsOptionInPreset(newOpt)) - { - resetBtn.Destroy(); - } newOpt.Initialize(); newOpt.gameObject.SetActive(false); diff --git a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs index c1db00b..4bf0cf8 100644 --- a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs +++ b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs @@ -3,7 +3,6 @@ using System.IO; using System.Linq; using BepInEx.Configuration; using HarmonyLib; -using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using MiraAPI.Presets; using MiraAPI.Roles; @@ -21,13 +20,13 @@ namespace MiraAPI.Patches.Options; internal static class GamePresetsTabPatches { + private static readonly object Lock = new(); + private static GameObject _saveButton = null!; private static GameObject _refreshButton = null!; private static GameObject _folderButton = null!; private static GameObject _newDivider = null!; - private static readonly object Lock = new(); - [HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnEnable))] public static class GamePresetsOnEnablePatch { @@ -58,14 +57,17 @@ internal static class GamePresetsTabPatches { _saveButton.SetActive(MenuState.Instance.CurrentModIdx != 0); } + if (_refreshButton) { _refreshButton.SetActive(MenuState.Instance.CurrentModIdx != 0); } + if (_folderButton) { _folderButton.SetActive(MenuState.Instance.CurrentModIdx != 0); } + if (_newDivider) { _newDivider.SetActive(MenuState.Instance.CurrentModIdx != 0); @@ -124,10 +126,12 @@ internal static class GamePresetsTabPatches { _saveButton.SetActive(false); } + if (_refreshButton) { _refreshButton.SetActive(false); } + if (_folderButton) { _folderButton.SetActive(false); @@ -232,7 +236,7 @@ internal static class GamePresetsTabPatches option.SaveToPreset(presetFile); } - foreach (var role in MenuState.Instance.CurrentMod.InternalRoles.Values.OfType().Where(x=>!x.Configuration.HideSettings)) + foreach (var role in MenuState.Instance.CurrentMod.InternalRoles.Values.OfType().Where(x => !x.Configuration.HideSettings)) { role.SaveToPreset(presetFile); } @@ -268,6 +272,7 @@ internal static class GamePresetsTabPatches refreshButton.OnClick = new Button.ButtonClickedEvent(); refreshButton.OnClick.AddListener((UnityAction)Refresh); } + refreshButton = _refreshButton.GetComponent(); if (!_folderButton) @@ -318,6 +323,7 @@ internal static class GamePresetsTabPatches { PresetManager.LoadPresets(MenuState.Instance.CurrentMod); } + GameSettingMenu.Instance.ChangeTab(0, false); // refresh the tab } } diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index d4df499..6c1ede7 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using MiraAPI.Utilities; namespace MiraAPI.Patches.Options; @@ -13,6 +14,11 @@ internal static class GameSettingMenuPatches [HarmonyPatch(nameof(GameSettingMenu.OnEnable))] public static void OnEnablePrefix(GameSettingMenu __instance) { + if (__instance.GameSettingsTab && __instance.GameSettingsTab.numberOptionOrigin) + { + Helpers.CacheAdvancedSettingsRowPrefab(__instance.GameSettingsTab.numberOptionOrigin); + } + // Add Menu State object if (!__instance.gameObject.GetComponent()) { @@ -37,6 +43,7 @@ internal static class GameSettingMenuPatches { Error("MenuState instance is null. Cannot change tab."); } + return false; } } diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 7c96f7d..5f5ca9b 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -11,7 +11,6 @@ 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; @@ -36,12 +35,7 @@ public static class LobbyViewPanePatches private static AbstractGameMode? GetCustomGamemode() { - if (!CustomGameModeManager.IsClassic() && !CustomGameModeManager.IsHideNSeek()) - { - return CustomGameModeManager.ActiveMode; - } - - return null; + return !CustomGameModeManager.IsClassic() && !CustomGameModeManager.IsHideNSeek() ? CustomGameModeManager.ActiveMode : null; } private static IEnumerable? GetCustomGamemodeOptions() @@ -90,7 +84,7 @@ public static class LobbyViewPanePatches { __instance.ChangeTab(ModifiersTabName); })); - ModifiersTabButton.gameObject.SetActive(SelectedModIdx!=0); + ModifiersTabButton.gameObject.SetActive(SelectedModIdx != 0); // Create the next button var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject; @@ -169,10 +163,10 @@ public static class LobbyViewPanePatches menu.scrollBar.ScrollToTop(); } + // CHANGED BECAUSE OF INLINING [HarmonyPostfix] [HarmonyPatch(nameof(LobbyViewSettingsPane.ChangeTab))] [HarmonyPatch(nameof(LobbyViewSettingsPane.RefreshTab))] - // CHANGED BECAUSE OF INLINING public static void SetTabPatch(LobbyViewSettingsPane __instance) { if (__instance.currentTab != ModifiersTabName || SelectedMod == null) @@ -226,16 +220,16 @@ public static class LobbyViewPanePatches { if (SelectedModIdx == 0) { - float num = 0.95f; - float num2 = -6.53f; - CategoryHeaderMasked categoryHeaderMasked = + var num = 0.95f; + var num2 = -6.53f; + var categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, __instance.settingsContainer); categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61); categoryHeaderMasked.transform.localScale = Vector3.one; categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, 1.26f, -2f); __instance.settingsInfo.Add(categoryHeaderMasked.gameObject); var list = new List(); - for (int i = 0; i < 2; i++) + for (var i = 0; i < 2; i++) { var categoryHeaderRoleVariant = Object.Instantiate(__instance.categoryHeaderRoleOrigin, __instance.settingsContainer); @@ -251,16 +245,16 @@ public static class LobbyViewPanePatches x.Role != RoleTypes.Impostor && ((i == 0 && x.TeamType is RoleTeamTypes.Crewmate) || (i == 1 && x.TeamType is RoleTeamTypes.Impostor)) && x.Role != RoleTypes.CrewmateGhost && x.Role != RoleTypes.ImpostorGhost).ToList(); - for (int j = 0; j < roles.Count; j++) + for (var j = 0; j < roles.Count; j++) { var roleBehaviour = roles[j]; - int chancePerGame = + var chancePerGame = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame( roleBehaviour.Role); - int numPerGame = + var numPerGame = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions .GetNumPerGame(roleBehaviour.Role); - bool flag = numPerGame == 0; + var flag = numPerGame == 0; var viewSettingsInfoPanelRoleVariant = Object.Instantiate( __instance.infoPanelRoleOrigin, @@ -273,8 +267,7 @@ public static class LobbyViewPanePatches } var color = (i == 0) ? Palette.CrewmateRoleBlue : Palette.ImpostorRoleRed; - if (roleBehaviour is ICustomRole custom && (custom.Team is not ModdedRoleTeams.Crewmate && - custom.Team is not ModdedRoleTeams.Impostor)) + if (roleBehaviour is ICustomRole { Team: not (ModdedRoleTeams.Crewmate or ModdedRoleTeams.Impostor) }) { color = Color.grey; } @@ -302,8 +295,8 @@ public static class LobbyViewPanePatches categoryHeaderMasked2.transform.localPosition = new Vector3(-9.77f, num, -2f); __instance.settingsInfo.Add(categoryHeaderMasked2.gameObject); num -= 2.1f; - float num3 = 0f; - for (int k = 0; k < list.Count; k++) + var num3 = 0f; + for (var k = 0; k < list.Count; k++) { float num4; if (k % 2 == 0) @@ -324,7 +317,7 @@ public static class LobbyViewPanePatches Object.Instantiate(__instance.advancedRolePanelOrigin, __instance.settingsContainer); advancedRoleViewPanel.transform.localScale = Vector3.one; advancedRoleViewPanel.transform.localPosition = new Vector3(num4, num, -2f); - float num5 = advancedRoleViewPanel.SetUp(list[k], 0.85f, 61); + var num5 = advancedRoleViewPanel.SetUp(list[k], 0.85f, 61); if (num5 > num3) { num3 = num5; @@ -415,8 +408,7 @@ public static class LobbyViewPanePatches if (viewSettingsInfoPanel.titleText.text.Contains("Game Mode")) { viewSettingsInfoPanel.titleText.text = TranslationController.Instance.GetString( - data.Title, CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic" - ); + data.Title, CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic"); } } @@ -438,7 +430,7 @@ public static class LobbyViewPanePatches } var num = 0.95f; - var num2 = -6.53f; + const float num2 = -6.53f; var categoryHeaderMasked = Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true); categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61); @@ -458,7 +450,7 @@ public static class LobbyViewPanePatches foreach (var grouping in sortedRoleGroups) { - if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings())) + if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings() || !x.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode))) { continue; } @@ -484,7 +476,7 @@ public static class LobbyViewPanePatches foreach (var customRole in grouping) { - if (customRole.Configuration.HideSettings || !customRole.VisibleInSettings()) + if (customRole.Configuration.HideSettings || !customRole.VisibleInSettings() || !customRole.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode)) { continue; } @@ -605,6 +597,7 @@ public static class LobbyViewPanePatches header.Divider.color = Color.grey; header.Title.color = new Color32(50, 50, 50, 255); } + if (roleIcon != null && header.icon != null) { header.icon.material.SetInt(PlayerMaterial.MaskLayer, maskLayer); @@ -648,7 +641,7 @@ public static class LobbyViewPanePatches var filteredOptions = SelectedMod.InternalOptionGroups .Where(x => x.OptionableType == roleType) - .SelectMany(x=>x.Options) + .SelectMany(x => x.Options) .ToList(); for (var i = 0; i < filteredOptions.Count; i++) diff --git a/MiraAPI/Patches/Options/MenuState.cs b/MiraAPI/Patches/Options/MenuState.cs index 44be9a5..6687699 100644 --- a/MiraAPI/Patches/Options/MenuState.cs +++ b/MiraAPI/Patches/Options/MenuState.cs @@ -7,8 +7,8 @@ using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Presets; -using MiraAPI.Translation; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; @@ -262,7 +262,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) var smC1Btn = Instantiate(smModBtn, smModBtn.transform.parent); _smallButtons[MenuButton.CustomOne] = smC1Btn; smC1Btn.name = "SmallCustomOneButton"; - smC1Btn.buttonText.text = "Custom 1"; + smC1Btn.buttonText.text = "MiraApi.CustomCategory1".Translate(); smC1Btn.OnClick = new Button.ButtonClickedEvent(); smC1Btn.OnClick.AddListener( (UnityAction)(() => @@ -280,7 +280,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) var smC2Btn = Instantiate(smModBtn, smModBtn.transform.parent); _smallButtons[MenuButton.CustomTwo] = smC2Btn; smC2Btn.name = "SmallCustomTwoButton"; - smC2Btn.buttonText.text = "Custom 2"; + smC2Btn.buttonText.text = "MiraApi.CustomCategory2".Translate(); smC2Btn.OnClick = new Button.ButtonClickedEvent(); smC2Btn.OnClick.AddListener( (UnityAction)(() => @@ -368,7 +368,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) // Game / Modifier / Custom Settings tab // GST.settingsContainer is same as GST.scrollBar.Inner - if (CurrentMenu != MenuCategory.Preset && CurrentMenu != MenuCategory.Roles) + if (CurrentMenu is not (MenuCategory.Preset or MenuCategory.Roles)) { Gsm.GameSettingsTab.settingsContainer = Gsm.GameSettingsTab.scrollBar.Inner = CurrentContainer.transform; CurrentContainer.SetActive(true); @@ -382,15 +382,17 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) private void UpdateUi() { + var pageText = "MiraApi.Page".Translate(); + // Update mod text if (CurrentModIdx == 0) { - _text.text = $"(Page 1/{ModCount + 1})\nMain"; + _text.text = $"({pageText} 1/{ModCount + 1})\n{"MiraApi.Main".Translate()}"; } else { var modName = CurrentMod.MiraPlugin.OptionsTitleText.Translate(); - _text.text = $"(Page {CurrentModIdx + 1}/{ModCount + 1})\n" + + _text.text = $"({pageText} {CurrentModIdx + 1}/{ModCount + 1})\n" + modName[..Math.Min(modName.Length, 25)]; } @@ -672,6 +674,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) public void NextMod() { CurrentModIdx++; + // The existence of vanilla settings offsets the off-by-one errors. if (CurrentModIdx > ModCount) CurrentModIdx = 0; CurrentMenu = GetNewPageIfNeeded(); @@ -682,6 +685,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) public void PreviousMod() { CurrentModIdx--; + // The existence of vanilla settings offsets the off-by-one errors. if (CurrentModIdx < 0) CurrentModIdx = ModCount; CurrentMenu = GetNewPageIfNeeded(); diff --git a/MiraAPI/Patches/Options/NotificationPopperPatch.cs b/MiraAPI/Patches/Options/NotificationPopperPatch.cs index 8024ea4..f0b472e 100644 --- a/MiraAPI/Patches/Options/NotificationPopperPatch.cs +++ b/MiraAPI/Patches/Options/NotificationPopperPatch.cs @@ -14,10 +14,9 @@ public static class NotificationPopperPatch [HarmonyArgument(0)] StringNames key, [HarmonyArgument(1)] int roleCount, [HarmonyArgument(2)] int roleChance, - [HarmonyArgument(3)] RoleTeamTypes teamType, [HarmonyArgument(4)] bool playSound) { - if (CustomRoleManager.CustomRoles.Values.FirstOrDefault(x=>x.StringName==key) is not ICustomRole customRole) + if (CustomRoleManager.CustomRoles.Values.FirstOrDefault(x => x.StringName == key) is not ICustomRole customRole) { return true; } @@ -31,12 +30,10 @@ public static class NotificationPopperPatch icon, "", textColor, - TranslationController.Instance.GetString(key), - "" - ), + customRole.RoleName, + ""), "" + roleCount + "", - "" + roleChance + "%" - ); + "" + roleChance + "%"); __instance.SettingsChangeMessageLogic(key, item, playSound); return false; diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index f6b873c..670a782 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -1,13 +1,9 @@ using HarmonyLib; -using Il2CppSystem; -using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; -using Reactor.Utilities.Extensions; using UnityEngine; -using Object = Il2CppSystem.Object; namespace MiraAPI.Patches.Options; @@ -36,6 +32,11 @@ public static class OptionsPatches __instance.roleChance = 0; } + if (__instance.roleChance == 0) + { + __instance.roleMaxCount = 0; + } + __instance.OnValueChanged.Invoke(__instance); return false; } @@ -62,6 +63,11 @@ public static class OptionsPatches __instance.roleChance = 100; } + if (__instance.roleChance == 0) + { + __instance.roleMaxCount = 0; + } + __instance.OnValueChanged.Invoke(__instance); return false; } @@ -86,6 +92,11 @@ public static class OptionsPatches __instance.roleMaxCount = 0; } + if (__instance.roleMaxCount == 0) + { + __instance.roleChance = 0; + } + __instance.OnValueChanged.Invoke(__instance); return false; } @@ -110,6 +121,11 @@ public static class OptionsPatches __instance.roleMaxCount = __instance.role.MaxCount; } + if (__instance.roleMaxCount == 0) + { + __instance.roleChance = 0; + } + __instance.OnValueChanged.Invoke(__instance); return false; } @@ -129,11 +145,13 @@ public static class OptionsPatches numberOption.MinusBtn.SetInteractable(true); numberOption.PlusBtn.SetInteractable(true); } + if (__instance.TryCast() is { } stringOption) { stringOption.MinusBtn.SetInteractable(true); stringOption.PlusBtn.SetInteractable(true); } + if (__instance.TryCast() is { } playerOption) { playerOption.MinusBtn.SetInteractable(true); @@ -165,14 +183,14 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()).Translate(); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate(); return false; } + // UpdateValue was inlined. [HarmonyPrefix] [HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.Toggle))] - // UpdateValue was inlined. public static bool ToggleUpdate(ToggleOption __instance) { if (!__instance.IsCustom()) @@ -202,6 +220,7 @@ public static class OptionsPatches { return true; } + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate(); return false; } @@ -229,6 +248,7 @@ public static class OptionsPatches { __instance.Value = __instance.ValidRange.min; } + __instance.OnValueChanged.Invoke(__instance); __instance.AdjustButtonsActiveState(); return false; @@ -257,6 +277,7 @@ public static class OptionsPatches { __instance.Value = __instance.ValidRange.max; } + __instance.OnValueChanged.Invoke(__instance); __instance.AdjustButtonsActiveState(); return false; @@ -276,6 +297,7 @@ public static class OptionsPatches { __instance.Value = 0; } + __instance.OnValueChanged.Invoke(__instance); __instance.AdjustButtonsActiveState(); return false; @@ -295,6 +317,7 @@ public static class OptionsPatches { __instance.Value = __instance.Values.Length - 1; } + __instance.OnValueChanged.Invoke(__instance); __instance.AdjustButtonsActiveState(); return false; @@ -309,8 +332,8 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()).Translate(); - __instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty()); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate(); + __instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value]); return false; } diff --git a/MiraAPI/Patches/Options/PlayerOptionPatches.cs b/MiraAPI/Patches/Options/PlayerOptionPatches.cs index 9cf0134..ab19fce 100644 --- a/MiraAPI/Patches/Options/PlayerOptionPatches.cs +++ b/MiraAPI/Patches/Options/PlayerOptionPatches.cs @@ -26,8 +26,8 @@ public static class PlayerOptionPatches { filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId)); + moddedPlrOpt.Values = filteredPlayers; __instance.Values = filteredPlayers.ToIl2CppList(); - moddedPlrOpt.Values = __instance.Values; __instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value); __instance.SetValueText(); } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 8694bb0..c05018c 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -12,7 +12,6 @@ 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; @@ -35,7 +34,7 @@ public static class RoleSettingMenuPatches [HarmonyPostfix] [HarmonyPatch(nameof(RolesSettingsMenu.OnEnable))] - public static void OpenPatch(RolesSettingsMenu __instance) + public static void OpenPatch() { HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = false; } @@ -86,20 +85,23 @@ public static class RoleSettingMenuPatches { 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) { roleMenu.roleChances.Add(r); - roleMenu.QuotaTabSelectables.AddRange(new Il2CppSystem.Collections.Generic.IEnumerable(r.ControllerSelectable.Pointer)); + roleMenu.QuotaTabSelectables.AddRange(new CppCollections.IEnumerable(r.ControllerSelectable.Pointer)); } Info($"Already created role options for {MenuState.Instance.CurrentModIdx}"); @@ -178,10 +180,12 @@ public static class RoleSettingMenuPatches { 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() @@ -206,7 +210,7 @@ public static class RoleSettingMenuPatches foreach (var grouping in sortedRoleGroups) { if (!grouping.Any() || - grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings() || !x.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode))) + grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings() || !x.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode))) { continue; } @@ -352,6 +356,7 @@ public static class RoleSettingMenuPatches } } } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = true; } @@ -388,7 +393,7 @@ public static class RoleSettingMenuPatches if (controllerSelected) { __instance.ControllerSelectable.AddRange( - new Il2CppSystem.Collections.Generic.IEnumerable( + new CppCollections.IEnumerable( __instance.QuotaTabSelectables.Pointer)); } @@ -400,11 +405,13 @@ public static class RoleSettingMenuPatches ControllerManager.Instance.SetDefaultSelection(__instance.ControllerSelectable[0]); } } + var passiveButton = __instance.currentTabButton; if (passiveButton) { passiveButton.SelectButton(false); } + __instance.AllButton.SelectButton(true); __instance.currentTabButton = __instance.AllButton; } @@ -424,8 +431,8 @@ public static class RoleSettingMenuPatches scroller.SetYBoundsMax(-ScrollerNum - 2); } - [HarmonyPrefix] // TODO: turn this into a fixed update + [HarmonyPrefix] [HarmonyPatch(nameof(RolesSettingsMenu.Update))] public static bool UpdatePatch(RolesSettingsMenu __instance) { @@ -550,6 +557,8 @@ public static class RoleSettingMenuPatches var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); categoryHeaderMasked.Title.text = TranslationController.Instance.GetString(StringNames.RoleSettingsLabel); var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground"); + + // ReSharper disable once StringLiteralTypo (Justification: Actual name in-game.) var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); imgBg.gameObject.SetActive(true); __instance.roleScreenshot.gameObject.SetActive(true); @@ -574,6 +583,7 @@ public static class RoleSettingMenuPatches __instance.roleDescriptionText.text = customRole.RoleMedDescription; __instance.roleTitleText.text = role.GetRoleName(); + // ReSharper disable once StringLiteralTypo (Justification: Actual name in-game.) var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground"); if (role.RoleScreenshot == null) @@ -627,7 +637,7 @@ public static class RoleSettingMenuPatches IEnumerator CoReturnToRoleSettings() { - // set gameobjects + // set game objects __instance.RoleChancesSettings.SetActive(true); __instance.AdvancedRolesSettings.SetActive(false); @@ -707,11 +717,16 @@ public static class RoleSettingMenuPatches if (customRole.Configuration.Icon != null) { - var roleIcon = new GameObject("RoleIcon"); - roleIcon.transform.parent = roleOptionSetting.transform; - roleIcon.transform.localScale = new(.25f, .25f, 1); - roleIcon.layer = LayerMask.NameToLayer("UI"); - roleIcon.transform.localPosition = new Vector3(-1.3f, -0.3f, -2f); + var roleIcon = new GameObject("RoleIcon") + { + transform = + { + parent = roleOptionSetting.transform, + localScale = new(.25f, .25f, 1), + localPosition = new Vector3(-1.3f, -0.3f, -2f), + }, + layer = LayerMask.NameToLayer("UI"), + }; var rend = roleIcon.AddComponent(); rend.sprite = customRole.Configuration.Icon.LoadAsset(); diff --git a/MiraAPI/Patches/Options/SettingPatches.cs b/MiraAPI/Patches/Options/SettingPatches.cs index 59b956c..1ef854b 100644 --- a/MiraAPI/Patches/Options/SettingPatches.cs +++ b/MiraAPI/Patches/Options/SettingPatches.cs @@ -46,14 +46,14 @@ public static class SettingPatches } else { + var part = value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo); result = suffix switch { - MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo), - MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x", - MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%", - _ => TranslationController.Instance.GetString( - StringNames.GameSecondsAbbrev, - (Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]), + MiraNumberSuffixes.None => part, + MiraNumberSuffixes.Multiplier => part + "x", + MiraNumberSuffixes.Percent => part + "%", + MiraNumberSuffixes.Distance => string.Concat(part, " ", MiraApiPlugin.DistanceSuffix.Value), + _ => TranslationController.Instance.GetString(StringNames.GameSecondsAbbrev, part), }; } } @@ -63,14 +63,14 @@ public static class SettingPatches } else { + var part = value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo); result = suffix switch { - MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo), - MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x", - MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%", - _ => TranslationController.Instance.GetString( - StringNames.GameSecondsAbbrev, - (Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]), + MiraNumberSuffixes.None => part, + MiraNumberSuffixes.Multiplier => part + "x", + MiraNumberSuffixes.Percent => part + "%", + MiraNumberSuffixes.Distance => string.Concat(part, " ", MiraApiPlugin.DistanceSuffix.Value), + _ => TranslationController.Instance.GetString(StringNames.GameSecondsAbbrev, part), }; } diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index 77b83c2..791334d 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -20,7 +20,6 @@ internal static class PlayerControlPatches { [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.Start))] - // ReSharper disable once InconsistentNaming public static void PlayerControlStartPostfix(PlayerControl __instance) { if (__instance.gameObject.TryGetComponent(out var modifierComp)) @@ -39,7 +38,6 @@ internal static class PlayerControlPatches [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.Die))] - // ReSharper disable once InconsistentNaming public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason) { var deathEvent = new PlayerDeathEvent(__instance, reason, Helpers.GetBodyById(__instance.PlayerId)); @@ -55,7 +53,6 @@ internal static class PlayerControlPatches [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.CompleteTask))] - // ReSharper disable once InconsistentNaming public static void PlayerCompleteTaskPostfix(PlayerControl __instance, uint idx) { var playerTask = __instance.myTasks.ToArray().First(playerTask => playerTask.Id == idx); @@ -82,13 +79,13 @@ internal static class PlayerControlPatches if (!target || __instance.Data.IsDead || !__instance.Data.Role.IsImpostor || __instance.Data.Disconnected) { - int num = target ? target!.PlayerId : -1; + var num = target ? target!.PlayerId : -1; __instance.logger.Warning($"Bad kill from {__instance.PlayerId} to {num}"); __instance.RpcMurderPlayer(target, false); return; } - NetworkedPlayerInfo data = target!.Data; + var data = target!.Data; if (data == null || data.IsDead || target.inVent || target.MyPhysics.Animations.IsPlayingEnterVentAnimation() || target.MyPhysics.Animations.IsPlayingAnyLadderAnimation() || target.inMovingPlat) { @@ -119,20 +116,13 @@ internal static class PlayerControlPatches [HarmonyPrefix] [HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))] [HarmonyPatch(nameof(PlayerControl.MurderPlayer))] - // ReSharper disable once InconsistentNaming - public static bool MurderPlayerPrefix(PlayerControl __instance) + public static bool MurderPlayerPrefix() { - if (LobbyBehaviour.Instance) - { - return false; - } - - return true; + return !LobbyBehaviour.Instance; } [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.FixedUpdate))] - // ReSharper disable once InconsistentNaming public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance) { if (!__instance.AmOwner) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index a4878e4..5354c04 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1,13 +1,27 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using AmongUs.GameOptions; using HarmonyLib; using Innersloth.Assets; +using MiraAPI.GameModes; +using MiraAPI.LocalSettings; +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; +using MiraAPI.Patches.Options; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; using Object = UnityEngine.Object; namespace MiraAPI.Patches; @@ -15,30 +29,98 @@ namespace MiraAPI.Patches; [HarmonyPatch] public static class RoleGuidePatches { + private const int SettingsTabIndex = 1; + private const int RolesTabIndex = 2; + private const int ModifiersTabIndex = 3; + private const float MinGuideScaleFactor = 0.5f; + private const float MaxGuideScaleFactor = 1f; + private const float SearchDebounceSeconds = 0.05f; + + private static readonly List RoleEntries = []; + private static readonly List ModifierEntries = []; + private static readonly List UntrackedRolePanelDiagnostics = []; + + private static GridLayoutGroup tabButtonContainer; + private static SpriteRenderer tabContainerRend; + private static GameObject searchComponentContainer; + private static SpriteRenderer _searchIconIdle; + private static SpriteRenderer _searchIconHover; + private static PassiveButton _searchSortingOrderButton; + private static SpriteRenderer _searchSortingOrderIdle; + private static SpriteRenderer _searchSortingOrderHover; + private static TextMeshPro groupSortingTmp; + private static PassiveButton _searchSortingGroupButton; + private static SpriteRenderer _searchSortingGroupIdle; + private static SpriteRenderer _searchSortingGroupHover; + private static TextMeshPro filterSortingTmp; + private static PassiveButton _searchSortingFilterButton; + private static SpriteRenderer _searchSortingFilterIdle; + private static SpriteRenderer _searchSortingFilterHover; + private static Scroller _rolesScroller; + private static Scroller _modifiersScroller; + private static GameObject _modifierNoResultsMessage; + private static Scroller _wikiScroller; + private static TextBoxTMP _searchBoxTmp; + private static GameObject _currentWikiPage; + private static TextMeshPro _titleText; + private static SpriteRenderer _titleIcon; + private static bool _suppressSearchRefresh; + private static IEnumerator? _searchDebounce; + private static string _appliedSearch = string.Empty; + private static float _openedAt; + private static bool _uiInitialized; + private static MatchInfoGuide? _guide; + private static int _activeGuideTabIndex = -1; + private static IEnumerator? _populate; + private static bool _loading; + private static Type? _roleEntriesMode; + private static bool _roleEntriesInitialized; + private static bool _rolePanelsCleaned; + private static LoadingRing? _loadingSpinner; + private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; + private static SortingMethod _sortMethod = SortingMethod.Alphabetical; + private static SortingGroups _sortGrouping = SortingGroups.Ungrouped; + + private static int ActiveGuideTabIndex => + _guide && MatchInfoGuide.Instance == _guide ? MatchInfoGuide.Instance.activeTabIndex : _activeGuideTabIndex; + + private static bool IsModifiersTabActive => ActiveGuideTabIndex == ModifiersTabIndex; + [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); + ApplyPanelLayout(__instance); + } - __instance.matchInfoPlayersMaskArea.transform.localPosition = - __instance.matchInfoSettingsMaskArea.transform.localPosition = new Vector3(1.22f, -0.335f, -0.1f); + private static void ApplyPanelLayout(MatchInfoGuide instance) + { + instance.MatchInfoRoleMaskArea.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); - __instance.matchInfoPlayersMaskArea.size = - __instance.matchInfoSettingsMaskArea.size = - __instance.MatchInfoRoleMaskArea.size = new Vector2(-6, 1.8f); + instance.matchInfoPlayersMaskArea.transform.localPosition = + instance.matchInfoSettingsMaskArea.transform.localPosition = new Vector3(1.22f, -0.335f, -0.1f); - __instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren().First(x => x.name.Contains("BG_Gradient")) + instance.matchInfoPlayersMaskArea.size = + instance.matchInfoSettingsMaskArea.size = + instance.MatchInfoRoleMaskArea.size = new Vector2(-6, 1.8f); + + var playerMenuGradient = instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")); + var settingsMenuGradient = instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")); + var roleMenuGradient = instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")); + playerMenuGradient .GetComponent() .maskInteraction = - __instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).GetComponent() + settingsMenuGradient.GetComponent() .maskInteraction = - __instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).GetComponent() + roleMenuGradient.GetComponent() .maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + roleMenuGradient.transform.localScale = new Vector3(0.5297f, 0.1565f, 1); + roleMenuGradient.transform.localPosition = new Vector3(0, -0.62f, -5); } [HarmonyPrefix] @@ -46,7 +128,127 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] public static void Awake(MatchInfoGuide __instance) { - __instance.transitionOpen.targetSize = 1.3f; + __instance.transitionOpen.targetSize = Helpers.MatchInfoGuideBaseScale; + if (_guide != __instance) + { + ResetState(); + } + } + + public static void ApplyGuideScale(MatchInfoGuide instance, float scaleFactor) + { + if (!instance || !instance.transitionOpen) + { + return; + } + + var targetSize = Helpers.MatchInfoGuideBaseScale * + Mathf.Clamp(scaleFactor, MinGuideScaleFactor, MaxGuideScaleFactor); + instance.transitionOpen.targetSize = targetSize; + if (instance.MatchInfoParent.activeSelf) + { + instance.transitionOpen.transform.localScale = Vector3.one * targetSize; + } + } + + /// + /// Clears all cached state tied to a previous instance. + /// Every game spawns a fresh guide, so anything we cached from the old one is a destroyed Unity object. + /// + private static void ResetState() + { + if (_populate != null) + { + Coroutines.Stop(_populate); + _populate = null; + } + + if (_searchDebounce != null) + { + Coroutines.Stop(_searchDebounce); + _searchDebounce = null; + } + + RoleEntries.Clear(); + ModifierEntries.Clear(); + UntrackedRolePanelDiagnostics.Clear(); + _roleEntriesMode = null; + _roleEntriesInitialized = false; + _rolePanelsCleaned = false; + _guide = null; + _activeGuideTabIndex = -1; + _uiInitialized = false; + _loading = false; + _appliedSearch = string.Empty; + _suppressSearchRefresh = false; + _titleText = null!; + _titleIcon = null!; + _searchBoxTmp = null!; + _rolesScroller = null!; + _modifiersScroller = null!; + _modifierNoResultsMessage = null!; + _wikiScroller = null!; + _currentWikiPage = null!; + _loadingSpinner = null; + ResetSettingsState(); + _searchSortingOrderButton = null!; + _searchSortingGroupButton = null!; + _searchSortingFilterButton = null!; + _searchIconIdle = null!; + _searchIconHover = null!; + _searchSortingOrderIdle = null!; + _searchSortingOrderHover = null!; + _searchSortingGroupIdle = null!; + _searchSortingGroupHover = null!; + _searchSortingFilterIdle = null!; + _searchSortingFilterHover = null!; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] + private static void AwakePostfix(MatchInfoGuide __instance) + { + ApplyGuideScale(__instance, LocalSettingsTabSingleton.Instance.MatchInfoGuideScaleFactor.Value); + Coroutines.Start(CoPreload(__instance)); + } + + public static void TriggerPreload(MatchInfoGuide instance) + { + if (!instance || _populate != null) + { + return; + } + + Coroutines.Start(CoPreload(instance)); + } + + private static IEnumerator CoPreload(MatchInfoGuide instance) + { + while (instance && (!HudManager.InstanceExists || !GameManager.Instance)) + { + yield return null; + } + + if (!instance || GameManager.Instance.TryCast() == null) + { + yield break; + } + + _guide = instance; + BuildEntries(); + if (_populate != null) + { + yield break; + } + + _populate = Coroutines.Start(CoPreloadPanels()); + } + + private static IEnumerator CoPreloadPanels() + { + yield return CoForEachBudgeted(RoleEntries, PreloadPanel); + yield return CoForEachBudgeted(ModifierEntries, PreloadPanel); + _populate = null; } [HarmonyPrefix] @@ -54,57 +256,107 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Open))] public static bool Open(MatchInfoGuide __instance) { + if (!HudManager.InstanceExists || !GameManager.Instance || !PlayerControl.LocalPlayer) + { + return false; + } + if (HudManager.Instance.GameMenu.IsOpen || HudManager.Instance.Chat.IsOpenOrOpening) { return false; } + ApplyGuideScale(__instance, LocalSettingsTabSingleton.Instance.MatchInfoGuideScaleFactor.Value); + 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; + var enabled = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Joystick; __instance.glyphL.enabled = enabled; __instance.glyphR.enabled = enabled; - if (GameManager.Instance.TryCast() != null) + if (!_titleText) { + _titleText = __instance.transitionOpen.transform.FindChild("Text_Title")?.GetComponent()!; + if (_titleText) + { + _titleText.transform.localPosition += new Vector3(0.5f, 0); + _titleText.transform.GetComponent().Destroy(); + } + } + + if (!_titleIcon) + { + var titleIconObj = new GameObject("TitleIcon"); + _titleIcon = titleIconObj.AddComponent(); + _titleIcon.transform.SetParent(_titleText.transform.parent); + _titleIcon.transform.localPosition = new Vector3(-2.75f, 1.4f, 0); + titleIconObj.layer = _titleText.gameObject.layer; + } + + if (_titleText) + { + _titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + } + + var regGame = GameManager.Instance.TryCast() != null; + if (regGame) + { + if (!_uiInitialized) + { + Initialize(__instance); + } + + SetUpTabGrids(LocalSettingsTabSingleton.Instance.WikiTabPosition.Value); + + if (RoleEntries.Count == 0) + { + BuildEntries(); + } + if (__instance.NormalModeSettings.Count == 0) { - __instance.numOfTabs = 3; - __instance.TabButtons[0].SelectButton(true); - DisplayNormalRoleSettings(__instance, true); - __instance.MatchInfoRoleMaskArea.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); - - __instance.matchInfoPlayersMaskArea.transform.localPosition = - __instance.matchInfoSettingsMaskArea.transform.localPosition = new Vector3(1.22f, -0.335f, -0.1f); - - __instance.matchInfoPlayersMaskArea.size = - __instance.matchInfoSettingsMaskArea.size = - __instance.MatchInfoRoleMaskArea.size = new Vector2(-6, 1.8f); - - __instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren().First(x => x.name.Contains("BG_Gradient")) - .GetComponent() - .maskInteraction = - __instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).GetComponent() - .maskInteraction = - __instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).GetComponent() - .maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - } - else - { - DisplayNormalRoleSettings(__instance, false); + __instance.CreateSettingsEntry( + StringNames.GameNumImpostors, + GameManager.Instance.AllGameSettingData[StringNames.GameNumImpostors] + .GetValueString(GameManager.Instance.LogicOptions.NumImpostors)); + __instance.CreateSettingsEntry( + StringNames.GameKillCooldown, + GameManager.Instance.AllGameSettingData[StringNames.GameKillCooldown] + .GetValueString(GameManager.Instance.LogicOptions.GetKillCooldown())); + __instance.CreateSettingsEntry( + StringNames.GameEmergencyCooldown, + GameManager.Instance.AllGameSettingData[StringNames.GameEmergencyCooldown] + .GetValueString(GameManager.Instance.LogicOptions.GetEmergencyCooldown())); + __instance.CreateSettingsEntry( + StringNames.GameVisualTasks, + __instance.GetBoolString(GameManager.Instance.LogicOptions.GetVisualTasks())); + __instance.CreateSettingsEntry( + StringNames.GameAnonymousVotes, + __instance.GetBoolString(GameManager.Instance.LogicOptions.GetAnonymousVotes())); + __instance.CreateSettingsEntry( + StringNames.GameConfirmImpostor, + __instance.GetBoolString(GameManager.Instance.LogicOptions.GetConfirmImpostor())); + __instance.CreateSettingsEntry( + StringNames.GameTaskBarMode, + GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); + __instance.CreatePlayerEntries(); + __instance.rolesEnabledMessage.SetActive(false); } + + ResetSearchBox(); } else if (__instance.HnSModeSettings.Count == 0) { @@ -112,100 +364,1175 @@ public static class RoleGuidePatches __instance.TabButtons[0].SelectButton(true); __instance.CreateHnSModeSettings(); } + PlayerControl.LocalPlayer.NetTransform.Halt(); + HidePanels(RoleEntries); + HidePanels(ModifierEntries); __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); + _openedAt = Time.time; + if (__instance.ControllerSelectable.Count > 0) + { + var controllerManager = ControllerManager.Instance; + controllerManager.SetUpSelectables( + controllerManager.CurrentUiState, + __instance.ControllerSelectable[^1], + __instance.ControllerSelectable); + controllerManager.SetCurrentSelected(__instance.ControllerSelectable[^1]); + } + + __instance.SetActiveTab(regGame ? RolesTabIndex : 0); + if (regGame) + { + OpenTab(RolesTabIndex, __instance.transitionOpen.duration); + } + return false; } - private static Dictionary _rolePanels = []; - - public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) + private static void SetUpTabGrids(MatchInfoTabPos position) { - if (reset) + tabButtonContainer.startCorner = GridLayoutGroup.Corner.UpperLeft; + tabButtonContainer.startAxis = GridLayoutGroup.Axis.Horizontal; + tabButtonContainer.constraint = GridLayoutGroup.Constraint.FixedColumnCount; + tabContainerRend.transform.localScale = new Vector3(0.4f, 0.4125f, 1); + tabContainerRend.transform.localRotation = Quaternion.Euler(new Vector3(0, 0, 0)); + switch (position) { - _rolePanels.Clear(); - instance.CreateSettingsEntry( - StringNames.GameNumImpostors, - GameManager.Instance.AllGameSettingData[StringNames.GameNumImpostors] - .GetValueString(GameManager.Instance.LogicOptions.NumImpostors)); - instance.CreateSettingsEntry( - StringNames.GameKillCooldown, - GameManager.Instance.AllGameSettingData[StringNames.GameKillCooldown] - .GetValueString(GameManager.Instance.LogicOptions.GetKillCooldown())); - instance.CreateSettingsEntry( - StringNames.GameEmergencyCooldown, - GameManager.Instance.AllGameSettingData[StringNames.GameEmergencyCooldown] - .GetValueString((float)GameManager.Instance.LogicOptions.GetEmergencyCooldown())); - instance.CreateSettingsEntry( - StringNames.GameVisualTasks, - instance.GetBoolString(GameManager.Instance.LogicOptions.GetVisualTasks())); - instance.CreateSettingsEntry( - StringNames.GameAnonymousVotes, - instance.GetBoolString(GameManager.Instance.LogicOptions.GetAnonymousVotes())); - instance.CreateSettingsEntry( - StringNames.GameConfirmImpostor, - instance.GetBoolString(GameManager.Instance.LogicOptions.GetConfirmImpostor())); - instance.CreateSettingsEntry( - StringNames.GameTaskBarMode, - GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); - foreach (RoleBehaviour roleBehaviour in RoleManager.Instance.AllRoles) + case MatchInfoTabPos.LeftSide: + tabButtonContainer.childAlignment = TextAnchor.MiddleLeft; + tabButtonContainer.transform.localPosition = new Vector3(-3.95f, 0, -0.2f); + tabContainerRend.transform.localPosition = new Vector3(0.325f, 0, 0); + break; + case MatchInfoTabPos.RightSide: + tabButtonContainer.childAlignment = TextAnchor.MiddleRight; + tabButtonContainer.transform.localPosition = new Vector3(3.95f, 0, -0.2f); + tabContainerRend.transform.localPosition = new Vector3(-0.325f, 0, 0); + break; + default: + tabButtonContainer.startCorner = GridLayoutGroup.Corner.LowerLeft; + tabButtonContainer.childAlignment = TextAnchor.LowerCenter; + tabButtonContainer.startAxis = GridLayoutGroup.Axis.Vertical; + tabButtonContainer.constraint = GridLayoutGroup.Constraint.FixedRowCount; + tabButtonContainer.transform.localPosition = new Vector3(0, -2.15f, -0.2f); + tabContainerRend.transform.localPosition = new Vector3(-0.025f, 0.3f, 0); + tabContainerRend.transform.localRotation = Quaternion.Euler(new Vector3(0, 0, 270)); + tabContainerRend.transform.localScale = new Vector3(0.4f, 0.475f, 1); + break; + } + + tabButtonContainer.constraintCount = 1; + } + + private static void Initialize(MatchInfoGuide instance) + { + if (_uiInitialized) + { + return; + } + + _sortFilter = MiraApiPlugin.WikiSortingFilter.Value; + _sortMethod = MiraApiPlugin.WikiSortingMethod.Value; + _sortGrouping = MiraApiPlugin.WikiSortingGroup.Value; + + SetNoResultsText(instance.rolesEnabledMessage); + + if (_populate != null) + { + Coroutines.Stop(_populate); + _populate = null; + } + + _guide = instance; + _loading = false; + searchComponentContainer = new GameObject(); + searchComponentContainer.transform.SetParent(instance.MatchInfoParent.transform); + searchComponentContainer.transform.localPosition = Vector3.zero; + instance.MatchInfoParent.transform.GetChild(1).transform.SetParent(searchComponentContainer.transform); + + var newContainer = new GameObject("TabButtonContainer"); + newContainer.transform.SetParent(instance.MatchInfoParent.transform); + tabButtonContainer = newContainer.AddComponent(); + tabButtonContainer.cellSize = new Vector2(0.65f, 0.55f); + tabButtonContainer.rectTransform.offsetMax = tabButtonContainer.rectTransform.offsetMin = Vector2.zero; + tabContainerRend = new GameObject("BackRenderer").AddComponent(); + tabContainerRend.transform.SetParent(newContainer.transform); + tabContainerRend.sprite = MiraAssets.TabButtonContainer.LoadAsset(); + tabContainerRend.transform.localPosition = Vector3.zero; + tabContainerRend.gameObject.layer = 5; + + var sortingOrderButton = Object.Instantiate( + HudManager.Instance.SettingsButton, + searchComponentContainer.transform); + sortingOrderButton.gameObject.SetActive(true); + _searchSortingOrderButton = sortingOrderButton.GetComponent(); + SetupSearchButton( + _searchSortingOrderButton, + "SearchSortingOrderButton", + SwitchMethod, + GetSortMethodSprite(_sortMethod, false), + GetSortMethodSprite(_sortMethod, true), + new Vector3(2.4f, 0.765f, -0.1f), + out _searchSortingOrderIdle, + out _searchSortingOrderHover); + + var sortingGroupButton = Object.Instantiate( + sortingOrderButton, + searchComponentContainer.transform); + sortingGroupButton.gameObject.SetActive(true); + _searchSortingGroupButton = sortingGroupButton.GetComponent(); + SetupSearchButton( + _searchSortingGroupButton, + "SearchSortingGroupButton", + SwitchGrouping, + GetSortGroupSprite(_sortGrouping, false), + GetSortGroupSprite(_sortGrouping, true), + new Vector3(1.7f, 0.765f, -0.1f), + out _searchSortingGroupIdle, + out _searchSortingGroupHover); + + var sortingFilterButton = Object.Instantiate( + sortingOrderButton, + searchComponentContainer.transform); + sortingFilterButton.gameObject.SetActive(true); + _searchSortingFilterButton = sortingFilterButton.GetComponent(); + SetupSearchButton( + _searchSortingFilterButton, + "SearchSortingFilterButton", + SwitchFilter, + GetSortFilterSprite(_sortFilter, false), + GetSortFilterSprite(_sortFilter, true), + new Vector3(1f, 0.765f, -0.1f), + out _searchSortingFilterIdle, + out _searchSortingFilterHover); + + instance.numOfTabs = 4; + instance.TabButtons[2].SelectButton(true); + ApplyPanelLayout(instance); + instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); + instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); + + var modifiersTab = Object.Instantiate( + instance.settingsTabs[RolesTabIndex], + instance.settingsTabs[RolesTabIndex].transform.parent); + modifiersTab.name = "ModifiersPanel"; + _modifierNoResultsMessage = Object.Instantiate( + instance.rolesEnabledMessage, + modifiersTab.transform, + true); + _modifierNoResultsMessage.name = "NoResultsFound"; + _modifierNoResultsMessage.SetActive(false); + SetNoResultsText(_modifierNoResultsMessage); + modifiersTab.transform.FindChild("MaskArea")?.transform.localPosition = + new Vector3(-0.0184f, 0.15f, -0.1f); + modifiersTab.transform.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); + _modifiersScroller = modifiersTab.GetComponent(); + instance.settingsTabs.Add(modifiersTab); + InitializeSettingsPanel(instance); + + var wikiTab = Object.Instantiate( + instance.settingsTabs[RolesTabIndex], + instance.settingsTabs[RolesTabIndex].transform.parent); + wikiTab.name = "WikiPanels"; + var wikiMask = wikiTab.transform.FindChild("MaskArea"); + wikiMask.transform.localPosition = new Vector3(-0.0184f, 0.425f, -0.1f); + wikiMask.transform.localScale = new Vector3(1, 1.3f, 1); + wikiTab.transform.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); + _wikiScroller = wikiTab.GetComponent(); + var wikiScrollHitbox = wikiTab.transform.FindChild("Hitbox"); + _wikiScroller.ScrollbarYBounds.max = 1.2f; + wikiScrollHitbox.GetComponent().size = new Vector2(5.75f, 2.4f); + wikiScrollHitbox.transform.localPosition += new Vector3(0, 0.3f); + _wikiScroller.ScrollbarY.trackGraphic.transform.localPosition = new Vector3(2.8364f, 0.3649f, -0.2f); + _wikiScroller.ScrollbarY.trackGraphic.transform.localScale = new Vector3(0.0234f, 2.068f, 0.5598f); + + var spinnerParent = instance.settingsTabs[RolesTabIndex].transform.parent; + var spinnerPos = spinnerParent.InverseTransformPoint(instance.MatchInfoRoleMaskArea.transform.position); + spinnerPos.z = -5f; + _loadingSpinner = LoadingRing.Create(spinnerParent, spinnerPos, 0.6f); + _loadingSpinner.GetComponent().color = MiraAssets.AcceptedTeal; + + var searchIconButton = Object.Instantiate( + sortingOrderButton, + sortingOrderButton.transform.parent); + searchIconButton.gameObject.SetActive(true); + SetupSearchButton( + searchIconButton.GetComponent(), + "SearchIconButton", + ClearSearchText, + MiraAssets.SearchIconIdleSprite, + MiraAssets.SearchIconHoverSprite, + new Vector3(-2.79f, 0.765f, -0.1f), + out _searchIconIdle, + out _searchIconHover); + + var searchBox = Object.Instantiate( + instance.TabButtons[2].gameObject, + searchComponentContainer.transform); + var searchButton = searchBox.GetComponent(); + var tmpText = searchButton.transform.GetChild(0).GetComponent(); + tmpText.GetComponent().Destroy(); + tmpText.color = new Color(0.75f, 0.75f, 0.75f); + tmpText.text = string.Empty; + tmpText.fontSizeMax = 4; + tmpText.overflowMode = TextOverflowModes.Ellipsis; + tmpText.alignment = TextAlignmentOptions.Left; + tmpText.horizontalAlignment = HorizontalAlignmentOptions.Left; + tmpText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); + tmpText.rectTransform.sizeDelta = new Vector2(3, 1); + tmpText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); + var inactive = searchButton.inactiveSprites; + var selected = searchButton.selectedSprites; + var highlight = searchButton.activeSprites; + var disabledSprite = searchButton.disabledSprites; + disabledSprite.GetComponent().size = new Vector2(3, 0.6f); + searchButton.Destroy(); + inactive.gameObject.SetActive(false); + selected.gameObject.SetActive(false); + highlight.gameObject.SetActive(false); + disabledSprite.gameObject.SetActive(true); + var button = searchBox.AddComponent(); + button.OnUp = true; + var template = HudManager.Instance.Chat.freeChatField.textArea; + _searchBoxTmp = Object.Instantiate(template, searchBox.transform); + _searchBoxTmp.gameObject.SetActive(true); + _searchBoxTmp.name = "SearchField"; + _searchBoxTmp.transform.localPosition = Vector3.zero; + _searchBoxTmp.OnEnter = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnChange = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnFocus = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnFocusLost = new Button.ButtonClickedEvent(); + _searchBoxTmp.sendButtonGlyph = null; + _searchBoxTmp.SendOnFullChars = false; + _searchBoxTmp.ClearOnFocus = false; + _searchBoxTmp.characterLimit = 40; + _searchBoxTmp.AllowSymbols = true; + _searchBoxTmp.allowAllCharacters = true; + if (_searchBoxTmp.Background) + { + _searchBoxTmp.Background.enabled = false; + } + + var boxCollider = _searchBoxTmp.GetComponent(); + if (boxCollider) + { + boxCollider.size = new Vector2(3f, 0.6f); + boxCollider.offset = Vector2.zero; + } + + var outputText = _searchBoxTmp.outputText; + var textParent = tmpText.transform.parent; + outputText.transform.SetParent(textParent, false); + outputText.GetComponent()?.Destroy(); + CopyTextLayout(tmpText, outputText); + outputText.color = new Color(0.75f, 0.75f, 0.75f); + outputText.text = string.Empty; + + var placeholderText = _searchBoxTmp.placeholderText; + if (placeholderText) + { + placeholderText.transform.SetParent(textParent, false); + placeholderText.GetComponent()?.Destroy(); + CopyTextLayout(tmpText, placeholderText); + placeholderText.color = new Color(0.55f, 0.55f, 0.55f); + placeholderText.text = MiraLocaleManager.Get("MiraApi.Wiki.SearchPlaceholder"); + placeholderText.gameObject.SetActive(true); + } + + if (_searchBoxTmp.Pipe) + { + _searchBoxTmp.Pipe.transform.SetParent(textParent, false); + _searchBoxTmp.Pipe.transform.localPosition = new Vector3( + outputText.transform.localPosition.x - + (outputText.rectTransform.sizeDelta.x * outputText.rectTransform.pivot.x), + outputText.transform.localPosition.y + _searchBoxTmp.caretYOffset, + -0.3f); + } + + tmpText.gameObject.Destroy(); + + button.OnClick.AddListener((UnityAction)(() => { _searchBoxTmp.GiveFocus(); })); + button.OnMouseOver = new UnityEvent(); + button.OnMouseOut = new UnityEvent(); + _rolesScroller = instance.settingsTabs[RolesTabIndex].GetComponent(); + _searchBoxTmp.OnChange.AddListener( + (UnityAction)(() => { - if (roleBehaviour.Role != RoleTypes.Crewmate && roleBehaviour.Role != RoleTypes.Impostor && - roleBehaviour.Role is not RoleTypes.CrewmateGhost && - roleBehaviour.Role is not RoleTypes.ImpostorGhost) + if (GetSearchText() != string.Empty) { - var panel = Object.Instantiate( - instance.MatchInfoRolePanelPrefab, - instance.settingsTabs[2].GetComponent().Inner); - _rolePanels.Add(roleBehaviour, panel); + _searchIconIdle.sprite = MiraAssets.SearchClearIdleSprite; + _searchIconHover.sprite = MiraAssets.SearchClearHoverSprite; + } + else + { + _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; + _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; + } + + if (_suppressSearchRefresh) + { + return; + } + + QueueSearchRefresh(); + })); + searchBox.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); + + var playerButton = instance.TabButtons[0]; + SetupTabButton( + playerButton, + MiraAssets.WikiPlayersButtonIdleSprite, + MiraAssets.WikiPlayersButtonOpenSprite, + MiraAssets.WikiPlayersButtonHoverSprite); + + var settingButton = instance.TabButtons[1]; + SetupTabButton( + settingButton, + MiraAssets.WikiSettingsButtonIdleSprite, + MiraAssets.WikiSettingsButtonOpenSprite, + MiraAssets.WikiSettingsButtonHoverSprite); + + var rolesButton = instance.TabButtons[2]; + SetupTabButton( + rolesButton, + MiraAssets.WikiRolesButtonIdleSprite, + MiraAssets.WikiRolesButtonOpenSprite, + MiraAssets.WikiRolesButtonHoverSprite); + rolesButton.OnClick = new Button.ButtonClickedEvent(); + rolesButton.OnClick.AddListener((Action)(() => OpenRolesTab())); + + var modifiersButton = Object.Instantiate( + instance.TabButtons[2], + instance.TabButtons[2].transform.parent); + SetupTabButton( + modifiersButton, + MiraAssets.WikiModifiersButtonIdleSprite, + MiraAssets.WikiModifiersButtonOpenSprite, + MiraAssets.WikiModifiersButtonHoverSprite); + modifiersButton.name = "ModifiersButton"; + modifiersButton.OnClick = new Button.ButtonClickedEvent(); + modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); + instance.TabButtons.Add(modifiersButton); + + var orderSortingTmp = Object.Instantiate( + _titleText, + sortingOrderButton.transform); + orderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.OrderLabel"); + orderSortingTmp.fontSize = orderSortingTmp.fontSizeMax = 3f; + orderSortingTmp.alignment = TextAlignmentOptions.Center; + orderSortingTmp.horizontalAlignment = HorizontalAlignmentOptions.Center; + orderSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + + groupSortingTmp = Object.Instantiate( + orderSortingTmp, + sortingGroupButton.transform); + groupSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel"); + groupSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + + filterSortingTmp = Object.Instantiate( + orderSortingTmp, + sortingFilterButton.transform); + filterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.FilterLabel"); + filterSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + + playerButton.transform.localPosition = new Vector3(-3.6f, 0.656f, -0.2f); + settingButton.transform.localPosition = new Vector3(-3.6f, 0.056f, -0.2f); + rolesButton.transform.localPosition = new Vector3(-3.6f, -0.544f, -0.2f); + modifiersButton.transform.localPosition = new Vector3(-3.6f, -1.144f, -0.2f); + _uiInitialized = true; + } + + private static void BuildEntries() + { + if (ModifierEntries.Count == 0) + { + foreach (var modifier in ModifierManager.Modifiers.OfType()) + { + ModifierEntries.Add(new DetailedPanel( + modifier.ModifierName, + modifier.ModifierCategoryTitle, + modifier.ParentMod.MiraPlugin.GetAbbreviatedModName(), + null, + modifier)); + } + } + + var activeMode = CustomGameModeManager.ActiveMode; + var activeModeType = activeMode?.GetType(); + if (_roleEntriesInitialized && _roleEntriesMode == activeModeType) + { + return; + } + + foreach (var entry in RoleEntries) + { + if (entry.Panel) + { + entry.Panel!.gameObject.Destroy(); + } + } + + RoleEntries.Clear(); + _rolePanelsCleaned = false; + foreach (var roleBehaviour in CustomRoleManager.AllStoredRoleBehaviours.Where(x => + x.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and + not RoleTypes.CrewmateGhost and not RoleTypes.ImpostorGhost && + CustomRoleUtils.CanSpawnOnCurrentMode(x)) + .GroupBy(x => x.Role) + .Select(group => group.First())) + { + var modId = roleBehaviour is ICustomRole custom + ? custom.ParentMod.MiraPlugin.GetAbbreviatedModName() + : "AU"; + RoleEntries.Add(new DetailedPanel( + roleBehaviour.GetRoleName(), + roleBehaviour.GetCategoryTitle(), + modId, + roleBehaviour, + null)); + } + + _roleEntriesMode = activeModeType; + _roleEntriesInitialized = true; + } + + private static void SetupTabButton(MatchInfoGuideTabButton button, Sprite inactive, Sprite selected, Sprite hover) + { + var buttonCollider = button.GetComponent(); + button.transform.GetChild(0).gameObject.SetActive(false); + var settingBtnInactive = button.inactiveSprites.GetComponent(); + settingBtnInactive.sprite = inactive; + settingBtnInactive.transform.GetChild(0).gameObject.SetActive(false); + var settingBtnSelected = button.selectedSprites.GetComponent(); + settingBtnSelected.sprite = selected; + settingBtnSelected.transform.GetChild(0).gameObject.SetActive(false); + var settingBtnHighlight = button.activeSprites.GetComponent(); + settingBtnHighlight.sprite = hover; + settingBtnInactive.drawMode = settingBtnSelected.drawMode = settingBtnHighlight.drawMode = SpriteDrawMode.Sliced; + settingBtnInactive.size = settingBtnSelected.size = settingBtnHighlight.size = Vector2.one; + settingBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); + buttonCollider.size = new Vector2(0.8f, 0.74f); + buttonCollider.offset = Vector2.zero; + button.transform.localScale = new Vector3(0.7f, 0.7f, 1); + button.transform.SetParent(tabButtonContainer.transform); + button.gameObject.AddComponent(); + } + + private static void SetupSearchButton(PassiveButton button, string objName, Action onClick, Sprite inactive, Sprite hover, Vector3 pos, out SpriteRenderer inactiveSprite, out SpriteRenderer hoverSprite) + { + button.name = objName; + inactiveSprite = button.inactiveSprites.GetComponent(); + inactiveSprite.sprite = inactive; + hoverSprite = button.activeSprites.GetComponent(); + hoverSprite.sprite = hover; + button.OnClick = new Button.ButtonClickedEvent(); + button.OnClick.AddListener(onClick); + + button.transform.localPosition = pos; + button.transform.localScale = Vector3.one; + button.transform.GetChild(2).gameObject.SetActive(false); + } + + private static void OpenTab(int tabIndex, float delay = 0f) + { + _wikiScroller?.gameObject.SetActive(false); + _activeGuideTabIndex = tabIndex; + MatchInfoGuide.Instance.SetActiveTab(tabIndex); + if (tabIndex == RolesTabIndex) + { + BuildEntries(); + if (!_rolePanelsCleaned) + { + _rolePanelsCleaned = RemoveUntrackedRolePanels(); + } + } + + HidePanels(tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries); + StartPopulate(delay); + } + + private static bool RemoveUntrackedRolePanels() + { + if (_rolesScroller?.Inner == null) + { + return false; + } + + var trackedPanels = RoleEntries + .Where(entry => entry.Panel) + .Select(entry => entry.Panel!) + .ToHashSet(); + foreach (var panel in _rolesScroller.Inner.GetComponentsInChildren(true)) + { + if (!trackedPanels.Contains(panel)) + { + var panelName = panel.roleName ? panel.roleName.text : panel.name; + UntrackedRolePanelDiagnostics.Add(panelName); + panel.gameObject.Destroy(); + } + } + + if (UntrackedRolePanelDiagnostics.Count > 0) + { + var panelNames = string.Join(", ", UntrackedRolePanelDiagnostics.Take(10)); + var additionalCount = UntrackedRolePanelDiagnostics.Count - 10; + Logger.Warning( + $"Removed or blocked {UntrackedRolePanelDiagnostics.Count} untracked Match Info role panel(s) in " + + $"{CustomGameModeManager.ActiveMode?.GetType().Name ?? "vanilla"} mode: {panelNames}" + + (additionalCount > 0 ? $", and {additionalCount} more" : string.Empty)); + UntrackedRolePanelDiagnostics.Clear(); + } + + return true; + } + + private static void HidePanels(List entries) + { + foreach (var entry in entries) + { + if (entry.Panel && entry.Panel!.gameObject.activeSelf) + { + entry.Panel.gameObject.SetActive(false); + } + } + } + + public static void OpenModifiersTab() + { + OpenTab(ModifiersTabIndex); + } + + public static void OpenRolesTab() + { + OpenTab(RolesTabIndex); + } + + private static void SetNoResultsText(GameObject message) + { + var text = message.GetComponentInChildren(true); + if (!text) + { + return; + } + + text.GetComponent()?.Destroy(); + text.text = MiraLocaleManager.Get("MiraApi.Wiki.NoResultsFound", "No Results Found"); + } + + private static void SetNoResultsVisible(GameObject? message, bool visible) + { + if (message != null && message) + { + message.SetActive(visible); + } + } + + private sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier) + { + public MatchInfoRolePanel? Panel { get; set; } + public string Title { get; } = title; + public string Category { get; } = category; + public string ModId { get; } = modId; + public RoleBehaviour? Role { get; } = role; + public GameModifier? Modifier { get; } = modifier; + public int Amount { get; set; } + public int Chance { get; set; } + public float Likelihood { get; set; } + public bool CanShow { get; set; } + public bool CountsReady { get; set; } + public bool Visible { get; set; } + + public string GetSortKey(SortingGroups grouping) + { + return grouping switch + { + SortingGroups.Faction => + $"{Category} ({Title}) {Likelihood.ToString("0000.0", CultureInfo.InvariantCulture)}", + SortingGroups.AmountChance => + $"{Likelihood.ToString("0000.0", CultureInfo.InvariantCulture)} {Title} ({Category})", + _ => + $"{Title} ({Category}) {Likelihood.ToString("0000.0", CultureInfo.InvariantCulture)}", + }; + } + } + + private static string GetSearchText() + { + return _searchBoxTmp && _searchBoxTmp.outputText ? _searchBoxTmp.outputText.text : string.Empty; + } + + private static bool ShouldShowPanel(bool? forceShow, bool isDisabled, bool isHidden) + { + return forceShow ?? + !(isHidden || (isDisabled && _sortFilter is SortingFilter.EnabledOnly) || + (!isDisabled && _sortFilter is SortingFilter.DisabledOnly)); + } + + private static void UpdateSpinner() + { + if (_loadingSpinner != null) + { + _loadingSpinner.gameObject.SetActive( + _loading && ActiveGuideTabIndex is RolesTabIndex or ModifiersTabIndex && + !(_wikiScroller && _wikiScroller.gameObject.activeSelf)); + } + } + + private static float GetGridScrollBounds(int count) + { + return Mathf.Clamp(Mathf.Ceil(count / 2f) * 1.3f - 1.5f, 0f, 999f); + } + + private static void StartPopulate(float delay = 0f) + { + if (_populate != null) + { + Coroutines.Stop(_populate); + } + + _loading = true; + UpdateSpinner(); + _populate = Coroutines.Start(CoPopulate(delay)); + } + + private static IEnumerator CoPopulate(float delay) + { + yield return delay > 0f ? new WaitForSeconds(delay) : null; + + if (!_guide) + { + yield break; + } + + var modifiers = IsModifiersTabActive; + var entries = modifiers ? ModifierEntries : RoleEntries; + var shown = UpdateEntries(entries, modifiers); + yield return CoForEachBudgeted(shown, entry => + { + EnsurePanel(entry); + entry.Panel!.transform.SetAsLastSibling(); + if (!entry.Panel.gameObject.activeSelf) + { + entry.Panel.gameObject.SetActive(true); + } + }); + _loading = false; + UpdateSpinner(); + if (!modifiers) + { + RemoveUntrackedRolePanels(); + yield return null; + if (_guide) + { + RemoveUntrackedRolePanels(); + } + } + + _populate = null; + } + + private static IEnumerator CoForEachBudgeted(IEnumerable collection, Action action) + { + var fps = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60; + var budget = Math.Max(1L, 1000L / (fps * 4)); + var timer = new Stopwatch(); + timer.Start(); + foreach (var item in collection) + { + action(item); + if (timer.ElapsedMilliseconds > budget) + { + timer.Restart(); + yield return null; + if (!_guide) + { + yield break; + } + } + } + } + + private static void EnsurePanel(DetailedPanel entry) + { + if (entry.Panel) + { + SetupPanelButton(entry.Panel!, () => OpenAdvancedWiki(entry)); + return; + } + + if (!_guide || !_guide!.MatchInfoRolePanelPrefab) + { + return; + } + + var parent = entry.Modifier != null ? _modifiersScroller?.Inner : _rolesScroller?.Inner; + if (parent == null) + { + return; + } + + var panel = Object.Instantiate(_guide.MatchInfoRolePanelPrefab, parent); + if (entry.Modifier != null) + { + panel.SetModifierPanel(entry.Modifier, entry.Amount, entry.Chance); + } + else + { + panel.SetRolePanel(entry.Role!, entry.Amount, entry.Chance); + } + + SetupPanelButton(panel, () => OpenAdvancedWiki(entry)); + panel.gameObject.SetActive(false); + entry.Panel = panel; + } + + private static void OpenAdvancedWiki(DetailedPanel entry) + { + if (entry.Modifier != null) + { + DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Modifier); + } + else if (entry.Role != null) + { + DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Role); + } + } + + private static void PreloadPanel(DetailedPanel entry) + { + if (!entry.Panel) + { + ComputeCounts(entry); + EnsurePanel(entry); + } + } + + private static void ComputeCounts(DetailedPanel entry) + { + int amount; + int chance; + bool? forceShow = null; + bool isHidden; + if (entry.Modifier is { } modifier) + { + amount = modifier.GetAmountPerGame(); + chance = modifier.GetAssignmentChance(); + forceShow = modifier.ForceShowModifierOnWiki; + isHidden = !modifier.CanSpawnOnCurrentMode() || modifier.GetDescription() == string.Empty; + } + else + { + var role = entry.Role!; + if (role is ICustomRole custom) + { + amount = custom.GetCount().GetValueOrDefault(0); + chance = custom.GetChance().GetValueOrDefault(0); + forceShow = custom.ForceShowRoleOnWiki; + } + else + { + amount = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role.Role); + chance = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(role.Role); + } + + isHidden = (role is ICustomRole custom2 && !custom2.CanSpawnOnCurrentMode()) || + (Enum.IsDefined(role.Role) && role.IsRoleBlacklisted()); + } + + entry.Amount = amount; + entry.Chance = chance; + entry.Likelihood = amount * chance; + entry.CanShow = ShouldShowPanel(forceShow, amount == 0 || chance == 0, isHidden); + entry.CountsReady = true; + entry.Visible = entry.CanShow; + } + + private static List UpdateEntries(List entries, bool modifiers) + { + var searchText = GetSearchText(); + foreach (var entry in entries) + { + ComputeCounts(entry); + entry.Visible = entry.CanShow && MatchesSearch(entry, searchText); + if (entry.Panel) + { + SetPanelCount(entry.Panel!, entry.Amount, entry.Chance, entry.ModId); + if (!entry.Visible && entry.Panel!.gameObject.activeSelf) + { + entry.Panel.gameObject.SetActive(false); } } } - int num = 0; - foreach (var pair in _rolePanels) + var shown = SortEntries(entries, searchText); + + var instance = MatchInfoGuide.Instance; + if (modifiers) { - var role = pair.Key; - var panel = pair.Value; - var amount = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role.Role); - var chance = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(role.Role); - var forciblyShow = role is ICustomRole custom ? custom.ForceShowRoleOnWiki : null; - if (amount == 0 || chance == 0 || (Enum.IsDefined(role.Role) && role.IsRoleBlacklisted()) || - (role is ICustomRole custom2 && ((!custom2.CanSpawnOnCurrentMode() && forciblyShow == null) || - (forciblyShow.HasValue && !forciblyShow.Value)))) + _modifiersScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); + SetNoResultsVisible(_modifierNoResultsMessage, shown.Count == 0); + } + else + { + instance.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); + SetNoResultsVisible(instance.rolesEnabledMessage, shown.Count == 0); + } + + return shown; + } + + private static List SortEntries(List entries, string searchText) + { + var sorted = entries.Where(p => p.Visible) + .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(p => + p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase)); + return [.. _sortMethod is SortingMethod.Alphabetical + ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping)) + : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))]; + } + + private static bool MatchesSearch(DetailedPanel entry, string searchText) + { + return searchText.Length == 0 || + entry.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + entry.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase); + } + + private static void RefreshActiveTab(bool scrollToTop) + { + if (IsSettingsTabActive) + { + RefreshSettingsTab(scrollToTop); + return; + } + + StartPopulate(); + if (scrollToTop) + { + (IsModifiersTabActive ? _modifiersScroller : _rolesScroller).ScrollToTop(); + } + } + + private static void QueueSearchRefresh() + { + if (_searchDebounce != null) + { + Coroutines.Stop(_searchDebounce); + } + + _searchDebounce = Coroutines.Start(CoSearchRefresh()); + } + + private static IEnumerator CoSearchRefresh() + { + yield return new WaitForSeconds(SearchDebounceSeconds); + _searchDebounce = null; + var text = GetSearchText(); + if (text == _appliedSearch) + { + yield break; + } + + _appliedSearch = text; + if (IsSettingsTabActive) + { + RefreshSettingsTab(true); + yield break; + } + + if (TryRefreshSearch(text)) + { + yield break; + } + + RefreshActiveTab(true); + } + + private static bool TryRefreshSearch(string searchText) + { + var guide = MatchInfoGuide.Instance; + if (!_guide || guide != _guide) + { + return false; + } + + var activeTabIndex = ActiveGuideTabIndex; + var entries = activeTabIndex switch + { + RolesTabIndex => RoleEntries, + ModifiersTabIndex => ModifierEntries, + _ => null, + }; + if (entries == null || _populate != null || !ArePanelsReady(entries)) + { + return false; + } + + foreach (var entry in entries) + { + entry.Visible = entry.CanShow && MatchesSearch(entry, searchText); + if (entry.Panel!.gameObject.activeSelf != entry.Visible) { - panel.gameObject.SetActive(false); - continue; + entry.Panel.gameObject.SetActive(entry.Visible); } - - panel.gameObject.SetActive(true); - panel.SetPanel( - role, - amount, - chance); - num++; } - if (num == 0) + var shown = SortEntries(entries, searchText); + foreach (var entry in shown) { - instance.rolesEnabledMessage.SetActive(true); + entry.Panel!.transform.SetAsLastSibling(); } - instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil((float)num / 2f) * 1.3f - 1.5f, 0f, 999f)); - instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); - instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); - if (reset) + var scroller = activeTabIndex == ModifiersTabIndex + ? _modifiersScroller + : guide.MatchInfoRoleScroller; + scroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); + var noResultsMessage = activeTabIndex == ModifiersTabIndex + ? _modifierNoResultsMessage + : guide.rolesEnabledMessage; + SetNoResultsVisible(noResultsMessage, shown.Count == 0); + scroller.ScrollToTop(); + return true; + } + + private static bool ArePanelsReady(List entries) + { + if (entries.Count == 0) { - instance.CreatePlayerEntries(); + return false; } + + foreach (var entry in entries) + { + if (!entry.CountsReady || !entry.Panel) + { + return false; + } + } + + return true; + } + + private static void ResetSearchBox() + { + if (_searchDebounce != null) + { + Coroutines.Stop(_searchDebounce); + _searchDebounce = null; + } + + _appliedSearch = string.Empty; + if (_searchIconIdle) + { + _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; + } + + if (_searchIconHover) + { + _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; + } + + if (_searchBoxTmp && GetSearchText().Length > 0) + { + _suppressSearchRefresh = true; + _searchBoxTmp.Clear(); + _suppressSearchRefresh = false; + } + } + + public static void ClearSearchText() + { + ResetSearchBox(); + RefreshActiveTab(true); + } + + public static void SwitchFilter() + { + if (IsSettingsTabActive) + { + ChangeSettingsPage(-1); + return; + } + + var stepUp = (SortingFilter)((int)_sortFilter + 1); + + _sortFilter = Enum.IsDefined(stepUp) ? stepUp : SortingFilter.EnabledOnly; + + MiraApiPlugin.WikiSortingFilter.Value = _sortFilter; + + _searchSortingFilterIdle.sprite = GetSortFilterSprite(_sortFilter, false); + _searchSortingFilterHover.sprite = GetSortFilterSprite(_sortFilter, true); + + RefreshActiveTab(false); + } + + private static Sprite GetSortFilterSprite(SortingFilter value, bool hover) + { + if (hover) + { + switch (value) + { + case SortingFilter.AllRoles: + return MiraAssets.VisibleAllHoverSprite; + case SortingFilter.DisabledOnly: + return MiraAssets.VisibleOffHoverSprite; + default: + return MiraAssets.VisibleOnHoverSprite; + } + } + + switch (value) + { + case SortingFilter.AllRoles: + return MiraAssets.VisibleAllIdleSprite; + case SortingFilter.DisabledOnly: + return MiraAssets.VisibleOffIdleSprite; + default: + return MiraAssets.VisibleOnIdleSprite; + } + } + + public static void SwitchMethod() + { + if (IsSettingsTabActive) + { + return; + } + + var stepUp = (SortingMethod)((int)_sortMethod + 1); + + _sortMethod = Enum.IsDefined(stepUp) ? stepUp : SortingMethod.Alphabetical; + + MiraApiPlugin.WikiSortingMethod.Value = _sortMethod; + + _searchSortingOrderIdle.sprite = GetSortMethodSprite(_sortMethod, false); + _searchSortingOrderHover.sprite = GetSortMethodSprite(_sortMethod, true); + + RefreshActiveTab(false); + } + + private static Sprite GetSortMethodSprite(SortingMethod value, bool hover) + { + if (hover) + { + return value is SortingMethod.Alphabetical + ? MiraAssets.SortingAzHoverSprite + : MiraAssets.SortingZaHoverSprite; + } + + return _sortMethod is SortingMethod.Alphabetical + ? MiraAssets.SortingAzIdleSprite + : MiraAssets.SortingZaIdleSprite; + } + + public static void SwitchGrouping() + { + if (IsSettingsTabActive) + { + ChangeSettingsPage(1); + return; + } + + var stepUp = (SortingGroups)((int)_sortGrouping + 1); + if (Enum.IsDefined(stepUp)) + { + _sortGrouping = stepUp; + } + else + { + _sortGrouping = SortingGroups.Ungrouped; + } + + MiraApiPlugin.WikiSortingGroup.Value = _sortGrouping; + + _searchSortingGroupIdle.sprite = GetSortGroupSprite(_sortGrouping, false); + _searchSortingGroupHover.sprite = GetSortGroupSprite(_sortGrouping, true); + + RefreshActiveTab(false); + } + + private static Sprite GetSortGroupSprite(SortingGroups value, bool hover) + { + if (hover) + { + switch (value) + { + case SortingGroups.Faction: + return MiraAssets.GroupFactionHoverSprite; + case SortingGroups.AmountChance: + return MiraAssets.GroupCountHoverSprite; + default: + return MiraAssets.GroupNoneHoverSprite; + } + } + + switch (value) + { + case SortingGroups.Faction: + return MiraAssets.GroupFactionIdleSprite; + case SortingGroups.AmountChance: + return MiraAssets.GroupCountIdleSprite; + default: + return MiraAssets.GroupNoneIdleSprite; + } + } + + private static void CopyTextLayout(TextMeshPro source, TextMeshPro target) + { + target.font = source.font; + target.fontSharedMaterial = source.fontSharedMaterial; + target.fontSize = source.fontSize; + target.fontSizeMin = source.fontSizeMin; + target.fontSizeMax = source.fontSizeMax; + target.enableAutoSizing = source.enableAutoSizing; + target.overflowMode = source.overflowMode; + target.alignment = source.alignment; + target.horizontalAlignment = source.horizontalAlignment; + target.verticalAlignment = source.verticalAlignment; + target.margin = source.margin; + target.enableWordWrapping = source.enableWordWrapping; + var sourceRect = source.rectTransform; + var targetRect = target.rectTransform; + targetRect.anchorMin = sourceRect.anchorMin; + targetRect.anchorMax = sourceRect.anchorMax; + targetRect.pivot = sourceRect.pivot; + targetRect.sizeDelta = sourceRect.sizeDelta; + targetRect.anchoredPosition = sourceRect.anchoredPosition; + targetRect.localScale = sourceRect.localScale; + targetRect.localRotation = sourceRect.localRotation; + targetRect.localPosition = sourceRect.localPosition; + } + + private static void SetupPanelButton(MatchInfoRolePanel panel, Action onClick) + { + var collider = panel.roleIcon.gameObject.GetComponent(); + if (!collider) + { + collider = panel.roleIcon.gameObject.AddComponent(); + } + + collider.size = new Vector2(0.13f, 0.13f); + collider.offset = new Vector2(0, 0); + collider.enabled = true; + var passiveButton = panel.roleIcon.gameObject.GetComponent(); + if (!passiveButton) + { + passiveButton = panel.roleIcon.gameObject.AddComponent(); + } + + passiveButton.enabled = true; + passiveButton.ClickSound = HudManager.Instance.MapButton.ClickSound; + passiveButton.OnMouseOver = new UnityEvent(); + passiveButton.OnMouseOver.AddListener( + (UnityAction)(() => { panel.roleIcon.color = new Color32(255, 255, 255, 150); })); + passiveButton.OnMouseOut = new UnityEvent(); + passiveButton.OnMouseOut.AddListener((UnityAction)(() => { panel.roleIcon.color = Color.white; })); + + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.OnClick.AddListener(onClick); } [HarmonyPrefix] @@ -214,10 +1541,10 @@ public static class RoleGuidePatches private static bool CreatePlayerEntries(MatchInfoGuide __instance) { __instance.PlayerPool.ReclaimAll(); - int num = 51; - foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) + var num = 51; + foreach (var networkedPlayerInfo in GameData.Instance.AllPlayers) { - PlayerIdentifierButton component = + var component = __instance.PlayerPool.Get().GetComponent(); component.transform.localPosition = new Vector3(0f, 0f, -1f); component.Populate(networkedPlayerInfo); @@ -227,38 +1554,262 @@ public static class RoleGuidePatches 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) => + + if (namePlate != null) { - component.buttonSprite.sprite = viewdata?.Image; + __instance.StartCoroutine( + __instance.CoLoadAssetAsync( + namePlate.GetAssetReference(), + (Action?)LoadNameplate)); + } + + void LoadNameplate(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; } + + [HarmonyPostfix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.SetActiveTab))] + private static void SetActiveTab(int tabIndex) + { + _activeGuideTabIndex = tabIndex; + HideNoResultsMessages(); + + if (_titleText && _titleIcon) + { + switch (tabIndex) + { + case 0: + _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab"); + _titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset(); + break; + case SettingsTabIndex: + _titleText.text = GetSettingsTitle(); + _titleIcon.sprite = GetSettingsIcon(); + break; + case RolesTabIndex: + _titleText.text = MiraLocaleManager.Get("Roles"); + _titleIcon.sprite = GetRandomRoleIcon(); + break; + case ModifiersTabIndex: + _titleText.text = MiraLocaleManager.Get("Modifiers"); + _titleIcon.sprite = GetRandomModifierIcon(); + break; + default: + _titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + _titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset(); + break; + } + + _titleIcon.SetSizeLimit(0.55f); + } + + _wikiScroller?.gameObject.SetActive(false); + searchComponentContainer?.SetActive(true); + if (tabIndex == SettingsTabIndex) + { + EnterSettingsTab(); + } + else + { + LeaveSettingsTab(); + } + + UpdateSpinner(); + } + + private static void HideNoResultsMessages() + { + var guide = _guide; + if (guide != null && guide) + { + SetNoResultsVisible(guide.rolesEnabledMessage, false); + } + + SetNoResultsVisible(_modifierNoResultsMessage, false); + SetNoResultsVisible(_settingsNoResults, false); + } + + public static Sprite GetRandomModifierIcon() + { + var activeMods = PlayerControl.LocalPlayer.GetModifiers() + .Where(x => !x.HideOnUi && x.ModifierIcon != null).ToList(); + if (activeMods.Count > 0) + { + return activeMods.Random()!.ModifierIcon!.LoadAsset(); + } + + return GetRandomFactionIcon(); + } + + public static Sprite GetRandomRoleIcon() + { + var aliveRole = PlayerControl.LocalPlayer.Data?.Role; + if (aliveRole != null && aliveRole.RoleIconSolid != null) + { + return aliveRole.RoleIconSolid; + } + + return GetRandomFactionIcon(); + } + + public static Sprite GetRandomFactionIcon() + { + var random = UnityEngine.Random.Range(1, 4); + if (random == 1) + { + return MiraAssets.IconCrewmate.LoadAsset(); + } + + if (random == 2) + { + return MiraAssets.IconImpostor.LoadAsset(); + } + + return MiraAssets.IconNeutral.LoadAsset(); + } + + private static bool TryPrepareWikiPage(MatchInfoGuide instance) + { + instance.settingsTabs[RolesTabIndex].SetActive(false); + _modifiersScroller?.gameObject.SetActive(false); + LeaveSettingsTab(); + if (_currentWikiPage) + { + _currentWikiPage.SetActive(false); + _currentWikiPage.Destroy(); + } + + if (searchComponentContainer) + { + searchComponentContainer.SetActive(false); + } + + if (_wikiScroller == null) + { + Warning("Wiki tab is null."); + return false; + } + + _wikiScroller.gameObject.SetActive(true); + UpdateSpinner(); + return true; + } + + private static IEnumerator CoDisplayWikiPageLater(float delay, Action display) + { + yield return new WaitForSeconds(delay); + if (_guide && MatchInfoGuide.Instance && MatchInfoGuide.Instance.MatchInfoParent.activeSelf) + { + display(); + } + } + + public static void DisplayAdvancedWiki(MatchInfoGuide instance, BaseModifier modifier) + { + var remaining = _openedAt + instance.transitionOpen.duration - Time.time; + if (remaining > 0f) + { + Coroutines.Start(CoDisplayWikiPageLater(remaining, () => DisplayAdvancedWiki(instance, modifier))); + return; + } + + var name = modifier.ModifierName; + Info($"Opening wiki page for {name}."); + if (!TryPrepareWikiPage(instance)) + { + return; + } + + _currentWikiPage = modifier.GetAdvancedWiki(instance, _titleText, _titleIcon, _wikiScroller); + } + + public static void DisplayAdvancedWiki(MatchInfoGuide instance, RoleBehaviour role) + { + var remaining = _openedAt + instance.transitionOpen.duration - Time.time; + if (remaining > 0f) + { + Coroutines.Start(CoDisplayWikiPageLater(remaining, () => DisplayAdvancedWiki(instance, role))); + return; + } + + Info($"Opening wiki page for {role.GetRoleName()}."); + if (!TryPrepareWikiPage(instance)) + { + return; + } + + if (role is ICustomRole custom) + { + _currentWikiPage = custom.GetAdvancedWiki(instance, _titleText, _titleIcon, _wikiScroller); + } + else + { + var titleTxt = $"{role.GetRoleName()}" + + $" ({TranslationController.Instance.GetString(role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor)})"; + var description = TranslationController.Instance.GetString(role.BlurbNameLong); + if (description.Contains("STRMISS")) + { + var baseName = $"{role.StringName}".Replace("Role", string.Empty); + if (Enum.TryParse($"RolesHelp_{baseName}_01", out var helpName)) + { + description = TranslationController.Instance.GetString(helpName); + } + } + + _currentWikiPage = Helpers.CreateAdvancedWikiPage( + instance, + _titleText, + _titleIcon, + _wikiScroller, + role.Role.ToString(), + titleTxt, + role.RoleIconColor, + description, + []); + } + } + [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); + var rolesInner = _rolesScroller ? _rolesScroller.Inner : null; + if (rolesInner && + __instance.transform.IsChildOf(rolesInner) && + !RoleEntries.Any(entry => entry.Panel == __instance)) + { + UntrackedRolePanelDiagnostics.Add( + role is ICustomRole untrackedRole ? untrackedRole.RoleName : role.NiceName); + __instance.gameObject.Destroy(); + return false; + } + + __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.roleDescription.text = + $"{role.GetCategoryTitle()}\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.roleDescription.text = $"{role.GetCategoryTitle()}\n" + role.BlurbMed; __instance.roleIcon.sprite = role.RoleIconColor; __instance.roleCount.text += " (AU)"; } @@ -271,4 +1822,484 @@ public static class RoleGuidePatches __instance.roleIcon.transform.localScale = new Vector3(4f, 4f, 1f); return false; } + + private static void SetPanelCount(MatchInfoRolePanel panel, int amount, int chance, string modId) + { + panel.roleCount.text = string.Format( + CultureInfo.InvariantCulture, + "{0} at {1}% ({2})", + amount.ToString(CultureInfo.InvariantCulture), + chance, + modId); + } + + public static void SetRolePanel( + this MatchInfoRolePanel instance, + RoleBehaviour role, + int numPerGame, + int chancePerGame) + { + if (role is ICustomRole customRole) + { + instance.roleName.text = customRole.RoleName; + instance.roleDescription.text = + $"{role.GetCategoryTitle()}\n" + customRole.RoleMedDescription; + instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset(); + SetPanelCount(instance, numPerGame, chancePerGame, customRole.ParentMod.MiraPlugin.GetAbbreviatedModName()); + } + else + { + instance.roleName.text = role.NiceName; + instance.roleDescription.text = $"{role.GetCategoryTitle()}\n" + role.BlurbMed; + instance.roleIcon.sprite = role.RoleIconColor; + SetPanelCount(instance, numPerGame, chancePerGame, "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); + } + + public static void SetModifierPanel( + this MatchInfoRolePanel instance, + GameModifier modifier, + int numPerGame, + int chancePerGame) + { + instance.roleName.text = modifier.ModifierName; + instance.roleDescription.text = + $"{modifier.ModifierCategoryTitle}\n" + modifier.ModifierMedDescription; + instance.roleIcon.sprite = modifier.ModifierIcon?.LoadAsset(); + SetPanelCount(instance, numPerGame, chancePerGame, modifier.ParentMod.MiraPlugin.GetAbbreviatedModName()); + + 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); + } + + public static string GetCategoryTitle(this RoleBehaviour role) + { + if (role is ICustomRole customRole) + { + return customRole.GetCategoryTitle(); + } + + return TranslationController.Instance.GetString( + role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor); + } + + public static string GetCategoryTitle(this ICustomRole customRole) + { + return customRole.RoleFactionTitle; + } + + // Settings page state. + private static Scroller? _settingsScroller; + private static GameObject? _settingsNoResults; + private static GameObject? _settingsPageObject; + private static List _settingsPages = []; + private static int _settingsPage; // 0 = vanilla settings, 1..N = _settingsPages[page - 1] + private static bool _settingsSearching; + + // Save the sort-button appearance while they are used for page navigation. + private static bool _sortButtonsRepurposed; + private static string _savedFilterLabel = string.Empty; + private static string _savedGroupLabel = string.Empty; + private static Vector3 _savedFilterIconScale = Vector3.one; + private static Vector3 _savedGroupIconScale = Vector3.one; + private static Vector3 _savedFilterLabelPosition = Vector3.zero; + private static Vector3 _savedGroupLabelPosition = Vector3.zero; + + private static bool IsSettingsTabActive => + _uiInitialized && MatchInfoGuide.Instance && ActiveGuideTabIndex == SettingsTabIndex; + + private static void ResetSettingsState() + { + _settingsScroller = null; + _settingsNoResults = null; + _settingsPageObject = null; + _settingsPages = []; + _settingsPage = 0; + _settingsSearching = false; + _sortButtonsRepurposed = false; + } + + private static void InitializeSettingsPanel(MatchInfoGuide instance) + { + var rolesTab = instance.settingsTabs[RolesTabIndex]; + var settingsTab = Object.Instantiate(rolesTab, rolesTab.transform.parent); + settingsTab.name = "SettingsOptionsPanel"; + settingsTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + settingsTab.transform.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); + _settingsScroller = settingsTab.GetComponent(); + settingsTab.SetActive(false); + + _settingsNoResults = Object.Instantiate( + instance.rolesEnabledMessage, + settingsTab.transform, + true); + _settingsNoResults.name = "SettingsNoResultsFound"; + _settingsNoResults.SetActive(false); + SetNoResultsText(_settingsNoResults); + } + + private static void EnterSettingsTab() + { + if (!_uiInitialized) + { + return; + } + + RefreshSettingsTab(true); + } + + private static void LeaveSettingsTab() + { + if (_settingsScroller != null) + { + _settingsScroller.gameObject.SetActive(false); + } + + if (_settingsNoResults) + { + _settingsNoResults!.SetActive(false); + } + + RestoreSortButtons(); + } + + private static void ChangeSettingsPage(int delta) + { + var total = _settingsPages.Count + 1; + if (total <= 1) + { + return; + } + + _settingsPage = (_settingsPage + delta + total) % total; + RefreshSettingsTab(true); + } + + /// + /// Updates the Settings tab with the current option values. + /// + private static void RefreshSettingsTab(bool scrollToTop) + { + var guide = MatchInfoGuide.Instance; + if (!_uiInitialized || !guide) + { + return; + } + + var vanillaTab = guide.settingsTabs[SettingsTabIndex]; + vanillaTab.SetActive(false); + var vanillaEntries = GameOptionsMenuPatch.GetActiveGameSettingsRows(guide.GetBoolString); + if (vanillaEntries.Count == 0) + { + vanillaEntries = BuildVanillaSettingsEntries(guide); + } + + _settingsPages = GuideOptionPages.BuildPages(guide.GetBoolString); + if (_settingsPage > _settingsPages.Count) + { + _settingsPage = 0; + } + + var searchText = GetSearchText().Trim(); + _settingsSearching = searchText.Length > 0; + + if (_settingsSearching) + { + ShowSettingsEntries(GuideOptionPages.Search(_settingsPages, searchText), true, scrollToTop); + vanillaTab.SetActive(false); + RepurposeSortButtons(false); + } + else if (_settingsPage == 0) + { + ShowSettingsRows(vanillaEntries, scrollToTop); + RepurposeSortButtons(true); + } + else + { + var page = _settingsPages[_settingsPage - 1]; + ShowSettingsEntries( + page.Entries, + page.Groups.Count > 1, + scrollToTop, + page.Columns, + page.Groups.Count > 1); + vanillaTab.SetActive(false); + RepurposeSortButtons(true); + } + + UpdateSettingsTitle(); + } + + internal static void RefreshSettingsForGameMode() + { + if (IsSettingsTabActive) + { + RefreshSettingsTab(false); + } + } + + private static List> BuildVanillaSettingsEntries(MatchInfoGuide guide) + { + foreach (var setting in guide.NormalModeSettings.ToArray()) + { + if (setting) + { + setting.Destroy(); + } + } + + guide.NormalModeSettings.Clear(); + guide.CreateNormalModeSettings(); + + var entries = new List>(guide.NormalModeSettings.Count); + foreach (var setting in guide.NormalModeSettings) + { + if (!setting) + { + continue; + } + + var texts = setting.GetComponentsInChildren(true); + if (texts.Length < 2) + { + Warning($"Could not read vanilla settings row '{setting.name}'."); + continue; + } + + entries.Add(new KeyValuePair(texts[0].text, texts[1].text)); + } + + return entries; + } + + private static void ShowSettingsEntries( + List entries, + bool showGroup, + bool scrollToTop, + int columns = 2, + bool groupsAsColumns = false) + { + var orderedEntries = showGroup + ? entries.GroupBy(entry => entry.Group).SelectMany(group => group) + : entries; + var rows = orderedEntries + .Select(entry => new AdvancedSettingsRow(entry.Title, entry.Value, showGroup ? entry.Group : null)) + .ToList(); + ShowSettingsRows(rows, scrollToTop, columns, groupsAsColumns); + _settingsNoResults?.SetActive(_settingsSearching && entries.Count == 0); + } + + private static void ShowSettingsRows(IReadOnlyList> rows, bool scrollToTop) + { + var advancedRows = rows + .Select(row => new AdvancedSettingsRow(row.Key, row.Value, null)) + .ToList(); + ShowSettingsRows(advancedRows, scrollToTop); + } + + private static void ShowSettingsRows( + List rows, + bool scrollToTop, + int columns = 2, + bool groupsAsColumns = false) + { + var guide = MatchInfoGuide.Instance; + if (_settingsScroller == null || !guide || !guide.MatchInfoRolePanelPrefab) + { + // Use the vanilla settings tab if the custom panel is unavailable. + guide?.settingsTabs[SettingsTabIndex].SetActive(true); + return; + } + + if (_settingsPageObject) + { + _settingsPageObject!.SetActive(false); + _settingsPageObject.Destroy(); + _settingsPageObject = null; + } + + _settingsScroller.gameObject.SetActive(true); + _settingsPageObject = Helpers.CreateAdvancedSettingsPage( + _settingsScroller, + rows, + columns, + groupsAsColumns); + _settingsNoResults?.SetActive(false); + if (scrollToTop) + { + _settingsScroller.ScrollToTop(); + } + } + + private static string GetSettingsTitle() + { + var baseTitle = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); + if (!_uiInitialized) + { + return baseTitle; + } + + if (_settingsSearching) + { + return $"{baseTitle}: {MiraLocaleManager.Get("MiraApi.Wiki.SearchResults", "Search Results")}"; + } + + if (_settingsPage == 0 || _settingsPages.Count == 0) + { + var vanillaTitle = $"{MiraLocaleManager.Get("MiraApi.Wiki.VanillaSettings", "Vanilla")} Settings"; + return $"{vanillaTitle} ({_settingsPage + 1}/{_settingsPages.Count + 1})"; + } + + var counter = $"({_settingsPage + 1}/{_settingsPages.Count + 1})"; + var pageTitle = _settingsPages[_settingsPage - 1].Title; + var titleAlreadyDescribesSettings = pageTitle.EndsWith("settings", StringComparison.OrdinalIgnoreCase) || + pageTitle.EndsWith("options", StringComparison.OrdinalIgnoreCase); + return titleAlreadyDescribesSettings + ? $"{pageTitle} {counter}" + : $"{pageTitle} Settings {counter}"; + } + + private static Sprite GetSettingsIcon() + { + if (!_uiInitialized || _settingsSearching || _settingsPage == 0 || _settingsPages.Count == 0) + { + return MiraAssets.IconSettings; + } + + return _settingsPages[_settingsPage - 1].Sprite; + } + + private static void UpdateSettingsTitle() + { + if (_titleText && IsSettingsTabActive) + { + _titleText.text = GetSettingsTitle(); + if (_titleIcon) + { + _titleIcon.sprite = GetSettingsIcon(); + + _titleIcon.SetSizeLimit(0.55f); + } + } + } + + /// + /// Turns the Filter button into "previous page" and the Grouping button into "next page", + /// and hides the Order button (it only sorts roles/modifiers). + /// + /// False while searching, since search results are a single page. + private static void RepurposeSortButtons(bool paginationVisible) + { + if (!_searchSortingFilterButton || !_searchSortingGroupButton || !_searchSortingOrderButton) + { + return; + } + + if (!_sortButtonsRepurposed) + { + _savedFilterLabel = filterSortingTmp.text; + _savedGroupLabel = groupSortingTmp.text; + _savedFilterIconScale = _searchSortingFilterIdle.transform.localScale; + _savedGroupIconScale = _searchSortingGroupIdle.transform.localScale; + _savedFilterLabelPosition = filterSortingTmp.transform.localPosition; + _savedGroupLabelPosition = groupSortingTmp.transform.localPosition; + + SetPaginationSprites(_searchSortingFilterIdle, _searchSortingFilterHover, true); + SetPaginationSprites(_searchSortingGroupIdle, _searchSortingGroupHover, false); + filterSortingTmp.transform.localPosition = new Vector3( + -1.25f, + 0f, + _savedFilterLabelPosition.z); + groupSortingTmp.transform.localPosition = new Vector3( + 0f, + 0f, + _savedGroupLabelPosition.z); + filterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.PrevPage", "Prev"); + groupSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.NextPage", "Next"); + _sortButtonsRepurposed = true; + } + + _searchSortingOrderButton.gameObject.SetActive(false); + _searchSortingFilterButton.gameObject.SetActive(paginationVisible); + _searchSortingGroupButton.gameObject.SetActive(paginationVisible); + } + + private static void SetPaginationSprites(SpriteRenderer idle, SpriteRenderer hover, bool isLeft) + { + var idleSprite = isLeft ? MiraAssets.PageLeftIdle.LoadAsset() : MiraAssets.PageRightIdle.LoadAsset(); + var hoverSprite = isLeft ? MiraAssets.PageLeftHover.LoadAsset() : MiraAssets.PageRightHover.LoadAsset(); + + // Match the on-screen size of the sort icon being replaced, since the arrow art differs in size. + var currentHeight = idle.sprite ? idle.sprite.bounds.size.y * idle.transform.localScale.y : 0f; + var newHeight = idleSprite.bounds.size.y; + var scale = currentHeight > 0f && newHeight > 0f ? currentHeight / newHeight : 1f; + + idle.sprite = idleSprite; + hover.sprite = hoverSprite; + idle.transform.localScale = hover.transform.localScale = new Vector3(scale, scale, 1f); + } + + private static void RestoreSortButtons() + { + if (!_sortButtonsRepurposed) + { + return; + } + + _sortButtonsRepurposed = false; + if (!_searchSortingFilterButton || !_searchSortingGroupButton || !_searchSortingOrderButton) + { + return; + } + + _searchSortingFilterIdle.sprite = GetSortFilterSprite(_sortFilter, false); + _searchSortingFilterHover.sprite = GetSortFilterSprite(_sortFilter, true); + _searchSortingGroupIdle.sprite = GetSortGroupSprite(_sortGrouping, false); + _searchSortingGroupHover.sprite = GetSortGroupSprite(_sortGrouping, true); + _searchSortingFilterIdle.flipX = _searchSortingFilterHover.flipX = false; + _searchSortingGroupIdle.flipX = _searchSortingGroupHover.flipX = false; + _searchSortingFilterIdle.transform.localScale = + _searchSortingFilterHover.transform.localScale = _savedFilterIconScale; + _searchSortingGroupIdle.transform.localScale = + _searchSortingGroupHover.transform.localScale = _savedGroupIconScale; + filterSortingTmp.text = _savedFilterLabel; + groupSortingTmp.text = _savedGroupLabel; + filterSortingTmp.transform.localPosition = _savedFilterLabelPosition; + groupSortingTmp.transform.localPosition = _savedGroupLabelPosition; + _searchSortingOrderButton.gameObject.SetActive(true); + _searchSortingFilterButton.gameObject.SetActive(true); + _searchSortingGroupButton.gameObject.SetActive(true); + } +} + +internal enum SortingGroups +{ + Ungrouped, + Faction, + AmountChance, +} + +internal enum SortingMethod +{ + Alphabetical, + AlphabeticalDescending, +} + +internal enum SortingFilter +{ + EnabledOnly, + DisabledOnly, + AllRoles, } diff --git a/MiraAPI/Patches/Roles/HauntMenuPatch.cs b/MiraAPI/Patches/Roles/HauntMenuPatch.cs index 851ddaa..2c873bf 100644 --- a/MiraAPI/Patches/Roles/HauntMenuPatch.cs +++ b/MiraAPI/Patches/Roles/HauntMenuPatch.cs @@ -8,8 +8,8 @@ namespace MiraAPI.Patches.Roles; /// /// Patches the to show the actual role name rather than team. /// -[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetHauntTarget))] // SetFilterText was INLINED +[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetHauntTarget))] public static class HauntMenuMinigamePatch { public static void Postfix(HauntMenuMinigame __instance) diff --git a/MiraAPI/Patches/Roles/HowToPlayPatch.cs b/MiraAPI/Patches/Roles/HowToPlayPatch.cs index c7cc2e5..b2fd4e2 100644 --- a/MiraAPI/Patches/Roles/HowToPlayPatch.cs +++ b/MiraAPI/Patches/Roles/HowToPlayPatch.cs @@ -16,6 +16,7 @@ internal static class HowToPlayPatch { return; } + __instance.sceneIndex = 0; __instance.category = HowToPlayScene.HowToPlayCategory.RolesSelection; __instance.startPage.SetActive(false); @@ -25,8 +26,8 @@ internal static class HowToPlayPatch { if (!role.IsSimpleRole && role.Role != RoleTypes.CrewmateGhost && role.Role != RoleTypes.ImpostorGhost) { - HowToPlayRoleButton component = Object.Instantiate(__instance.roleButtonPrefab, __instance.roleButtonsParent).GetComponent(); - Sprite roleIcon = __instance.rolesScenes.ToArray().First(r => r.role == role.Role).roleIcon; + var component = Object.Instantiate(__instance.roleButtonPrefab, __instance.roleButtonsParent).GetComponent(); + var roleIcon = __instance.rolesScenes.ToArray().First(r => r.role == role.Role).roleIcon; component.SetRoleInfo(role, roleIcon); component.SetButtonAction((Il2CppSystem.Action)(() => { @@ -35,16 +36,20 @@ internal static class HowToPlayPatch __instance.controllerSelectables.Add(component.GetComponent()); } } - foreach (UiElement uiElement in __instance.controllerSelectables) + + foreach (var uiElement in __instance.controllerSelectables) { uiElement.ReceiveMouseOut(); } - ControllerManager.Instance.NewScene(__instance.name, __instance.closeButton, __instance.defaultButtonSelected, __instance.controllerSelectables, false); + + ControllerManager.Instance.NewScene(__instance.name, __instance.closeButton, __instance.defaultButtonSelected, __instance.controllerSelectables); } + __instance.DisableAllScenes(); __instance.roleSelectionScene.SetActive(true); - ControllerManager.Instance.SetDefaultSelection(__instance.defaultButtonSelected, null); + ControllerManager.Instance.SetDefaultSelection(__instance.defaultButtonSelected); } + public static void OpenRolePage(HowToPlayScene instance, RoleTypes roleType) { instance.category = HowToPlayScene.HowToPlayCategory.Roles; @@ -61,6 +66,7 @@ internal static class HowToPlayPatch } } } + instance.SetupDots(instance.rolesScenes[instance.sceneIndex].rolePages.Count); instance.ChangeScene(0); } diff --git a/MiraAPI/Patches/Roles/HudManagerPatches.cs b/MiraAPI/Patches/Roles/HudManagerPatches.cs index 3de9b71..12e1952 100644 --- a/MiraAPI/Patches/Roles/HudManagerPatches.cs +++ b/MiraAPI/Patches/Roles/HudManagerPatches.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using System.Diagnostics.CodeAnalysis; +using HarmonyLib; using InnerNet; using MiraAPI.Roles; using Reactor.Utilities.Extensions; @@ -12,6 +13,10 @@ namespace MiraAPI.Patches.Roles; public static class HudManagerPatches { // Custom role tab. + [SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "Internal behaviour that does not need property-level validation.")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] + [SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Read above.")] + [SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "Read above.")] // why so many warnings??? public static TaskPanelBehaviour? RoleTab; /// @@ -49,7 +54,7 @@ public static class HudManagerPatches /// [HarmonyPostfix] [HarmonyPatch(nameof(HudManager.Update))] - public static void UpdatePostfix(HudManager __instance) + public static void UpdatePostfix() { var local = PlayerControl.LocalPlayer; diff --git a/MiraAPI/Patches/Roles/KillButtonPatches.cs b/MiraAPI/Patches/Roles/KillButtonPatches.cs index 9a00422..9693346 100644 --- a/MiraAPI/Patches/Roles/KillButtonPatches.cs +++ b/MiraAPI/Patches/Roles/KillButtonPatches.cs @@ -1,5 +1,4 @@ using HarmonyLib; -using Il2CppSystem; using MiraAPI.Networking; using MiraAPI.Roles; using UnityEngine; @@ -7,7 +6,7 @@ using UnityEngine; namespace MiraAPI.Patches.Roles; /// -/// Fix issues for neutral killers. +/// Fixes issues for neutral killers. /// [HarmonyPatch(typeof(KillButton))] public static class KillButtonPatches @@ -31,22 +30,24 @@ public static class KillButtonPatches if (__instance.currentTarget && __instance.currentTarget != target) { - __instance.currentTarget.cosmetics.SetOutline(false, new Nullable(Color.clear)); + __instance.currentTarget.cosmetics.SetOutline(false, new Il2CppSystem.Nullable(Color.clear)); } + __instance.currentTarget = target; if (__instance.currentTarget) { - __instance.currentTarget.cosmetics.SetOutline(true, new Nullable(customRole.Configuration.KillButtonOutlineColor)); + __instance.currentTarget.cosmetics.SetOutline(true, new Il2CppSystem.Nullable(customRole.Configuration.KillButtonOutlineColor)); __instance.SetEnabled(); return false; } + __instance.SetDisabled(); return false; } /// - /// Use + /// Use /// instead of from vanilla. /// [HarmonyPrefix] diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs index c3ed277..c1e9da7 100644 --- a/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs +++ b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs @@ -1,7 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; using AmongUs.GameOptions; using HarmonyLib; -using Il2CppSystem.Collections.Generic; -using System.Linq; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -13,171 +14,164 @@ public static class LogicRoleSelectionHnsPatch [HarmonyPatch(nameof(LogicRoleSelectionHnS.AssignRolesForTeam))] public static bool AssignRolesForTeam( LogicRoleSelectionHnS __instance, - List players, + Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, int teamMax, Il2CppSystem.Nullable defaultRole) { - Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); - int num = 0; - IRoleOptionsCollection roleOptions = opts.RoleOptions; + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); + var num = 0; + var roleOptions = opts.RoleOptions; + var source = RoleManager.Instance.AllRoles.ToArray() .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) && - CustomRoleUtils.CanSpawnOnCurrentMode(role)); + CustomRoleUtils.CanSpawnOnCurrentMode(role)).ToArray(); + 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) + var list = new List(); + + switch (team) { - 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) + case RoleTeamTypes.Crewmate: + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + AddGuaranteedRoles(guaranteedRoles, opts, list); + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); + AssignRolesFromList(players, teamMax, list, ref num); + + AddPotentialRoles(source, opts, list); + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Potential Assignment"); + AssignRolesFromList(players, teamMax, list, ref num); + + const RoleTypes basicCrewRole = RoleTypes.Engineer; + AddFallbackRoles(list, players.Count, teamMax, num, basicCrewRole); + AssignRolesFromList(players, teamMax, list, ref num); + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Fallback Assignment"); + break; + + case RoleTeamTypes.Impostor: + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + var newImpostors = new Il2CppSystem.Collections.Generic.List(); + + // Specified Seeker + if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() && + __instance.hnsManager.LogicOptionsHnS.ValidateImpostorPlayerID(players) && + !AmongUsClient.Instance.IsGamePublic) { - 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 (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() && - __instance.hnsManager.LogicOptionsHnS.ValidateImpostorPlayerID(players) && - !AmongUsClient.Instance.IsGamePublic) - { - NetworkedPlayerInfo networkedPlayerInfo = players.ToArray() - .First(p => p.PlayerId == __instance.hnsManager.LogicOptionsHnS.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._items.Do(x => pseudoRandomList.Add(x)); - for (int i = 0; i < GameData.RoundsPlayedInSession; i++) - { - pseudoRandomList.PickRandom(); - } - NetworkedPlayerInfo networkedPlayerInfo = pseudoRandomList.PickRandom(); + var networkedPlayerInfo = players.ToArray() + .First(p => p.PlayerId == __instance.hnsManager.LogicOptionsHnS.ImpostorPlayerID()); players.Remove(networkedPlayerInfo); newImpostors.Add(networkedPlayerInfo); - num2++; - Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); + Info($"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) + + // Random Seeker + else { - RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; - int count = roleAssignmentData2.Count; - roleAssignmentData2.Count = count - 1; - if (count <= 0) + var num2 = 0; + while (num2 < teamMax && players.Count > 0) { - break; - } + var pseudoRandomList = new PseudoRandomList(AmongUsClient.Instance.GameId); + players._items.Do(pseudoRandomList.Add); + for (var i = 0; i < GameData.RoundsPlayedInSession; i++) + { + pseudoRandomList.PickRandom(); + } - 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); + var networkedPlayerInfo = pseudoRandomList.PickRandom(); + players.Remove(networkedPlayerInfo); + newImpostors.Add(networkedPlayerInfo); + num2++; + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); } } - } - AssignRolesFromList(newImpostors, teamMax, list, ref num); - var basicRole = RoleTypes.Impostor; - while (list.Count < newImpostors.Count && list.Count + num < teamMax) - { - list.Add(basicRole); - } + Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); - AssignRolesFromList(newImpostors, teamMax, list, ref num); + AddGuaranteedRoles(guaranteedRoles, opts, list); + AssignRolesFromList(newImpostors, teamMax, list, ref num); + + AddPotentialRoles(source, opts, list); + AssignRolesFromList(newImpostors, teamMax, list, ref num); + + const RoleTypes basicImpRole = RoleTypes.Impostor; + AddFallbackRoles(list, newImpostors.Count, teamMax, num, basicImpRole); + AssignRolesFromList(newImpostors, teamMax, list, ref num); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(team), team, null); } + return false; } - public static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned) + public static void AssignRolesFromList(Il2CppSystem.Collections.Generic.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]; + var index = HashRandom.FastNext(roleList.Count); + var roleType = roleList[index]; roleList.RemoveAt(index); - int index2 = HashRandom.FastNext(players.Count); - players[index2].Object.RpcSetRole(roleType, false); + var index2 = HashRandom.FastNext(players.Count); + players[index2].Object.RpcSetRole(roleType); players.RemoveAt(index2); rolesAssigned++; } } + + private static void AddGuaranteedRoles( + IEnumerable guaranteedRoles, + IGameOptions opts, + List list) + { + var roleOptions = opts.RoleOptions; + foreach (var roleAssignmentData in guaranteedRoles.Select(x => + new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) + { + while (true) + { + var count = roleAssignmentData.Count; + roleAssignmentData.Count = count - 1; + if (count <= 0) break; + list.Add(roleAssignmentData.Role.Role); + } + } + } + + private static void AddPotentialRoles(IEnumerable source, IGameOptions opts, List list) + { + var roleOptions = opts.RoleOptions; + var potentialRoles = source.Where(x => !x.IsDead).Select(role => new RoleManager.RoleAssignmentData( + role, + roleOptions.GetNumPerGame(role.Role), + roleOptions.GetChancePerGame(role.Role))).ToList(); + + list.Clear(); + foreach (var roleData in potentialRoles) + { + for (var i = 0; i < roleData.Count; i++) + { + if (HashRandom.Next(101) < roleData.Chance) + { + list.Add(roleData.Role.Role); + } + } + } + } + + private static void AddFallbackRoles(List list, int targetPlayerCount, int teamMax, int rolesAssigned, RoleTypes basicRole) + { + while (list.Count < targetPlayerCount && list.Count + rolesAssigned < teamMax) + { + list.Add(basicRole); + } + } } diff --git a/MiraAPI/Patches/Roles/NameTagPatch.cs b/MiraAPI/Patches/Roles/NameTagPatch.cs index f373bdf..2eef3a2 100644 --- a/MiraAPI/Patches/Roles/NameTagPatch.cs +++ b/MiraAPI/Patches/Roles/NameTagPatch.cs @@ -33,10 +33,10 @@ public static class NameTagPatch if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data || !PlayerControl.LocalPlayer.Data?.Role || !otherPlayerRole) { __result = Color.white; + return false; } __result = PlayerControl.LocalPlayer.Data?.Role == otherPlayerRole ? otherPlayerRole.NameColor : Color.white; - return false; } } diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index 99359a6..82e937f 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -13,27 +13,30 @@ namespace MiraAPI.Patches.Roles; [HarmonyPatch(typeof(RoleManager))] public static class RoleManagerPatches { - [HarmonyPrefix] - [HarmonyPatch(nameof(RoleManager.SetRole))] // NOTE: As of 2025.5.20 update, this is INLINED in ONE place: GameManager::ReviveEveryoneFreeplay(void). // Should not affect normal gameplay, but may cause issues in Freeplay. + [HarmonyPrefix] + [HarmonyPatch(nameof(RoleManager.SetRole))] public static bool SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType) { if (!targetPlayer) { return false; } + var data = targetPlayer.Data; if (data == null) { Error("It shouldn't be possible, but " + targetPlayer.name + " still doesn't have PlayerData during role selection."); return false; } + if (data.Role) { data.Role.Deinitialize(targetPlayer); Object.Destroy(data.Role.gameObject); } + var roleBehaviour = Object.Instantiate(__instance.AllRoles.ToArray().First(r => r.Role == roleType), data.gameObject.transform); roleBehaviour.Initialize(targetPlayer); targetPlayer.Data.Role = roleBehaviour; @@ -45,6 +48,7 @@ public static class RoleManagerPatches { targetPlayer.Data.RoleWhenAlive = new Il2CppSystem.Nullable(roleType); } + roleBehaviour.AdjustTasks(targetPlayer); switch (roleBehaviour.IsDead) { @@ -64,13 +68,12 @@ public static class RoleManagerPatches [HarmonyPostfix] [HarmonyPatch(nameof(RoleManager.SelectRoles))] - public static void ModifierSelectionPatches(RoleManager __instance) + public static void ModifierSelectionPatches() { if (AmongUsClient.Instance.AmHost && ModifierManager.MiraAssignsModifiers) { ModifierManager.AssignModifiers( - PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) - .ToList()); + [.. PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected)]); } var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); @@ -84,7 +87,7 @@ public static class RoleManagerPatches [HarmonyPrefix] [HarmonyPatch(nameof(RoleManager.AssignRoleOnDeath))] - public static bool AssignRoleOnDeath(RoleManager __instance, [HarmonyArgument(0)] PlayerControl plr) + public static bool AssignRoleOnDeath([HarmonyArgument(0)] PlayerControl plr) { if (!plr || !plr.Data.IsDead) { diff --git a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs index 011fc28..0e856ef 100644 --- a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs +++ b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs @@ -1,6 +1,8 @@ -using AmongUs.GameOptions; +using System; +using System.Linq; +using System.Reflection; +using AmongUs.GameOptions; using HarmonyLib; -using Il2CppSystem.Collections.Generic; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -8,23 +10,76 @@ namespace MiraAPI.Patches.Roles; /// /// Patches to return the correct role counts. /// -[HarmonyPatch(typeof(RoleOptionsCollectionV11))] public static class RoleOptionsCollectionPatch { + internal static Assembly BaseAssembly { get; private set; } + internal static Type[] BaseTypes { get; private set; } + internal static Type CollectionsType { get; private set; } + public static void PatchRoleMethods(Harmony harmony) + { + var assembly = Array.Find( + AppDomain.CurrentDomain.GetAssemblies(), + ass => ass.GetName().Name == "Assembly-CSharp"); + + if (assembly == null) + return; + + var compatType = typeof(RoleOptionsCollectionPatch); + BaseAssembly = assembly; + BaseTypes = AccessTools.GetTypesFromAssembly(assembly); + var collections = BaseTypes.Where(x => x.Name.Contains("RoleOptionsCollectionV")).ToArray(); + var pairings = new System.Collections.Generic.Dictionary(); + var newestId = 0; + foreach (var collection in collections) + { + var remainder = collection.Name.Replace("RoleOptionsCollectionV", string.Empty); + if (!int.TryParse(remainder, out var id)) + { + continue; + } + + pairings.Add(id, collection); + if (newestId < id) + { + newestId = id; + } + } + + if (pairings.TryGetValue(newestId, out var typeToGet)) + { + CollectionsType = typeToGet; + } + + var anyRolesEnabledMethod = AccessTools.Method(CollectionsType, "AnyRolesEnabled"); + harmony.Patch( + anyRolesEnabledMethod, + new HarmonyMethod(AccessTools.Method(compatType, nameof(AnyRolesEnabledPrefix)))); + + var chancePerGameMethod = AccessTools.Method(CollectionsType, "GetChancePerGame"); + harmony.Patch( + chancePerGameMethod, + new HarmonyMethod(AccessTools.Method(compatType, nameof(GetChancePrefix)))); + + var numPerGameMethod = AccessTools.Method(CollectionsType, "GetNumPerGame"); + harmony.Patch( + numPerGameMethod, + new HarmonyMethod(AccessTools.Method(compatType, nameof(GetNumPrefix)))); + Info($"Patched methods for RoleOptionsCollectionV{newestId}"); + } + /// /// This patch fixes being inlined (2025.9.9) in the original code. /// - [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV11.AnyRolesEnabled))] - public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV11 __instance, ref bool __result) + public static bool AnyRolesEnabledPrefix(dynamic __instance) { - foreach (KeyValuePair keyValuePair in __instance.roles) + foreach (var keyValuePair in __instance.roles) { if (__instance.GetNumPerGame(keyValuePair.Key) > 0) { return true; } } + return false; } @@ -32,8 +87,6 @@ public static class RoleOptionsCollectionPatch /// Set the role chance for custom Launchpad roles based on config. /// /// Return to skip original method, to not. - [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV11.GetChancePerGame))] public static bool GetChancePrefix(RoleTypes role, ref int __result) { if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null) @@ -62,8 +115,6 @@ public static class RoleOptionsCollectionPatch /// Set the amount for custom Launchpad roles based on config. /// /// Return to skip original method, to not. - [HarmonyPrefix] - [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/SabotageButtonPatch.cs b/MiraAPI/Patches/Roles/SabotageButtonPatch.cs index 50c6133..8114bfa 100644 --- a/MiraAPI/Patches/Roles/SabotageButtonPatch.cs +++ b/MiraAPI/Patches/Roles/SabotageButtonPatch.cs @@ -11,7 +11,7 @@ public static class SabotageButtonPatch /// [HarmonyPatch(nameof(SabotageButton.DoClick))] [HarmonyPrefix] - public static bool DoClickPrefix(SabotageButton __instance) + public static bool DoClickPrefix() { var player = PlayerControl.LocalPlayer; @@ -56,6 +56,7 @@ public static class SabotageButtonPatch __instance.SetDisabled(); return false; } + __instance.SetEnabled(); return false; } diff --git a/MiraAPI/Patches/Roles/SelectRolesPatch.cs b/MiraAPI/Patches/Roles/SelectRolesPatch.cs index 1d17765..993454a 100644 --- a/MiraAPI/Patches/Roles/SelectRolesPatch.cs +++ b/MiraAPI/Patches/Roles/SelectRolesPatch.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using AmongUs.GameOptions; using HarmonyLib; -using System.Linq; using InnerNet; using MiraAPI.GameModes; using MiraAPI.Roles; @@ -11,7 +12,12 @@ namespace MiraAPI.Patches.Roles; [HarmonyPatch(typeof(RoleManager))] public static class SelectRolesPatch { + [SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "Internal behaviour that does not need property-level validation.")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] + [SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Read above.")] + [SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "Read above.")] // why so many warnings??? public static bool ApiHandlesRoleSelect = true; + [HarmonyPrefix] [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static bool SelectRoles() @@ -31,26 +37,28 @@ public static class SelectRolesPatch return false; } } + if (!ApiHandlesRoleSelect) { return true; } - 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) + CppCollections.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)]; + + foreach (var 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); + + var currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; + var adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count); AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, adjustedNumImpostors, RoleTypes.Impostor); AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Crewmate); return false; @@ -63,12 +71,13 @@ public static class SelectRolesPatch int teamMax, RoleTypes defaultRole) { - int num = 0; + var num = 0; var source = RoleManager.Instance.AllRoles.ToArray() .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) && - CustomRoleUtils.CanSpawnOnCurrentMode(role)); - List list = new List(); - IRoleOptionsCollection roleOptions = opts.RoleOptions; + CustomRoleUtils.CanSpawnOnCurrentMode(role)) + .ToArray(); + var list = new List(); + var roleOptions = opts.RoleOptions; // Assign guaranteed roles first, just like the vanilla selector. This is // important because the list of players is shared by both team passes. @@ -87,8 +96,7 @@ public static class SelectRolesPatch // another player and, more importantly, leaves the fallback count wrong. list.Clear(); foreach (var role in source.Where(x => - roleOptions.GetChancePerGame(x.Role) > 0 && - roleOptions.GetChancePerGame(x.Role) < 100) + roleOptions.GetChancePerGame(x.Role) is > 0 and < 100) .Select(role => role.Role)) { for (var i = 0; i < roleOptions.GetNumPerGame(role); i++) @@ -111,15 +119,16 @@ public static class SelectRolesPatch AssignRolesFromList(players, teamMax, list, ref num); } } + private 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]; + var index = HashRandom.FastNext(roleList.Count); + var roleType = roleList[index]; roleList.RemoveAt(index); - int index2 = HashRandom.FastNext(players.Count); - players[index2].Object.RpcSetRole(roleType, false); + var index2 = HashRandom.FastNext(players.Count); + players[index2].Object.RpcSetRole(roleType); players.RemoveAt(index2); rolesAssigned++; } diff --git a/MiraAPI/Patches/Roles/TaskPanelPatch.cs b/MiraAPI/Patches/Roles/TaskPanelPatch.cs index a08445a..c858d9f 100644 --- a/MiraAPI/Patches/Roles/TaskPanelPatch.cs +++ b/MiraAPI/Patches/Roles/TaskPanelPatch.cs @@ -8,8 +8,8 @@ internal static class TaskPanelPatch { /// /// This patch is to override the automatic updating of the y position on the tab (which is in base game) - /// because I can't change the custom tab y pos if it's being overriden every frame. - /// Im sure there is an easier/better way, but this is the fix that worked for me. + /// because I can't change the custom tab y pos if it's being overridden every frame. + /// I'm sure there is an easier/better way, but this is the fix that worked for me. /// [HarmonyPrefix] [HarmonyPatch(nameof(TaskPanelBehaviour.Update))] diff --git a/MiraAPI/Patches/ServerDropdownPatch.cs b/MiraAPI/Patches/ServerDropdownPatch.cs index 7749c9b..46a91e5 100644 --- a/MiraAPI/Patches/ServerDropdownPatch.cs +++ b/MiraAPI/Patches/ServerDropdownPatch.cs @@ -15,7 +15,7 @@ public static class ServerDropdownPatch region.TranslateName == other.TranslateName && region.PingServer == other.PingServer && region.TargetServer == other.TargetServer && - region.Servers.All(s=>other.Servers.Any(x=>x.Equals(s))); + region.Servers.All(s => other.Servers.Any(x => x.Equals(s))); } public static bool Prefix(ServerDropdown __instance) @@ -44,6 +44,7 @@ public static class ServerDropdownPatch { x += 2; } + var y = -0.55f * (num / 2f); serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f); serverListButton.transform.localScale = Vector3.one; diff --git a/MiraAPI/Patches/Stubs/MinigameStubs.cs b/MiraAPI/Patches/Stubs/MinigameStubs.cs index 4c2628f..64668ea 100644 --- a/MiraAPI/Patches/Stubs/MinigameStubs.cs +++ b/MiraAPI/Patches/Stubs/MinigameStubs.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using HarmonyLib; namespace MiraAPI.Patches.Stubs; @@ -7,6 +8,7 @@ namespace MiraAPI.Patches.Stubs; /// Reverse patches for s. /// [HarmonyPatch] +[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Stub parameters.")] public static class MinigameStubs { /// @@ -29,6 +31,7 @@ public static class MinigameStubs [HarmonyReversePatch] [HarmonyPatch(typeof(Minigame), nameof(Minigame.Close), [])] [MethodImpl(MethodImplOptions.NoInlining)] + [SuppressMessage("Minor Code Smell", "S3878:Arrays should not be created for params parameters", Justification = "Prevents ambiguity errors.")] public static void Close(Minigame instance) { // nothing needed diff --git a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs index 8743f3b..b856d22 100644 --- a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs +++ b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs @@ -1,7 +1,7 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using HarmonyLib; using Hazel; -using Il2CppSystem.Text; namespace MiraAPI.Patches.Stubs; @@ -9,6 +9,7 @@ namespace MiraAPI.Patches.Stubs; /// Stub methods for the class. Needed because of Il2Cpp limitations on injected classes. /// [HarmonyPatch] +[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Stub parameters.")] public static class RoleBehaviourStubs { /// @@ -38,14 +39,14 @@ public static class RoleBehaviourStubs } /// - /// Stub method for . + /// Stub method for . /// /// The object. - /// The to append the task hint to. + /// The to append the task hint to. [HarmonyReversePatch] - [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(StringBuilder))] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(Il2CppSystem.Text.StringBuilder))] [MethodImpl(MethodImplOptions.NoInlining)] - public static void AppendTaskHint(RoleBehaviour instance, StringBuilder taskStringBuilder) + public static void AppendTaskHint(RoleBehaviour instance, Il2CppSystem.Text.StringBuilder taskStringBuilder) { // nothing needed } @@ -283,6 +284,8 @@ public static class RoleBehaviourStubs /// Stub method for . /// /// The object. + /// The rpc message id. + /// The used to parse the message. [HarmonyReversePatch] [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.HandleRoleRpc))] [MethodImpl(MethodImplOptions.NoInlining)] @@ -295,6 +298,9 @@ public static class RoleBehaviourStubs /// Stub method for . /// /// The object. + /// The to apply the special setup to. + /// The killer. + /// The victim. [HarmonyReversePatch] [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.KillAnimSpecialSetup))] [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/MiraAPI/Patches/TmpAwakePatch.cs b/MiraAPI/Patches/TmpAwakePatch.cs index 9405e01..cb3f223 100644 --- a/MiraAPI/Patches/TmpAwakePatch.cs +++ b/MiraAPI/Patches/TmpAwakePatch.cs @@ -3,6 +3,7 @@ using MiraAPI.Utilities.Assets; using TMPro; namespace MiraAPI.Patches; + [HarmonyPatch] public static class TmpAwakePatch { @@ -21,6 +22,7 @@ public static class TmpAwakePatch __instance.UpdateMeshPadding(); return; } + __instance.m_spriteAsset.fallbackSpriteAssets.Add(TmpSpriteUtils.AssetHolder); __instance.UpdateMeshPadding(); } diff --git a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs index 1d8c1e9..a893d51 100644 --- a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs +++ b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs @@ -19,7 +19,7 @@ internal static class DummyBehaviourPatches [HarmonyPatch(nameof(DummyBehaviour.Update))] public static bool DummyUpdatePatch(DummyBehaviour __instance) { - NetworkedPlayerInfo data = __instance.myPlayer.Data; + var data = __instance.myPlayer.Data; if (data == null || data.IsDead) return false; if (MeetingHud.Instance) @@ -51,19 +51,21 @@ internal static class DummyBehaviourPatches yield break; } - List potentialSuspects = new(); - potentialSuspects.AddRange(PlayerControl.AllPlayerControls - .ToArray() - .Where(p => p != dummy.myPlayer) - .Where(p => dummyVoteEvent.PlayerIsValid(p)) - .Select(p => p.PlayerId)); + List potentialSuspects = + [ + .. PlayerControl.AllPlayerControls + .ToArray() + .Where(p => p != dummy.myPlayer) + .Where(p => dummyVoteEvent.PlayerIsValid(p)) + .Select(p => p.PlayerId), + ]; if (dummyVoteEvent.CanSkip) { potentialSuspects.Add(253); } - if (CanVote(dummy)) + if (dummy.CanVote()) { VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, dummy.myPlayer.PlayerId, potentialSuspects.Random()); } diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 80ab9d6..1239ac7 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; @@ -11,14 +10,13 @@ using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Utilities; -using MiraAPI.Utilities.Components; using MiraAPI.Voting; using UnityEngine; namespace MiraAPI.Patches.Voting; [HarmonyPatch(typeof(MeetingHud))] -[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony Convention")] +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony convention.")] internal static class MeetingHudPatches { [HarmonyPrefix] @@ -43,30 +41,34 @@ internal static class MeetingHudPatches [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.Select))] public static bool VoteAreaSelectPatch(PlayerVoteArea __instance) { - if (PlayerControl.LocalPlayer.Data.IsDead || __instance.AmDead || !(bool) (UnityEngine.Object) __instance.Parent) + 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; + + var judgeRole = PlayerControl.LocalPlayer.Data.Role.TryCast(); + var flag = judgeRole && PlayerControl.LocalPlayer.PlayerId != __instance.PlayerId; __instance.JudgeOverruleButton?.gameObject.SetActive(flag); - if (__instance.VoteComplete || !__instance.Parent.Select((byte) __instance.PlayerId)) + + if (__instance.VoteComplete || !__instance.Parent.Select((byte)__instance.PlayerId)) return false; + __instance.Buttons.SetActive(true); - float startPos = __instance.AnimateButtonsFromLeft ? 0.2f : 1.95f; + var startPos = __instance.AnimateButtonsFromLeft ? 0.2f : 1.95f; - Il2CppSystem.Collections.Generic.List selectableElements = new Il2CppSystem.Collections.Generic.List(); + CppCollections.List selectableElements = new(); foreach (var btn in __instance.Buttons.GetComponentsInChildren()) { selectableElements.Add(btn); } - for (int i = 0; i < selectableElements.Count; i++) + for (var 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)))))); + var endPos = 1.3f - 0.65f * i; + var 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); @@ -120,6 +122,7 @@ internal static class MeetingHudPatches meetingAbility.Button!.SetDisabled(); } } + MiraEventManager.InvokeEvent(new VotingCompleteEvent(__instance)); } @@ -166,7 +169,7 @@ internal static class MeetingHudPatches [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.Select))] - public static bool MeetingHudSelectPatch(MeetingHud __instance, int suspectStateIdx) + public static bool MeetingHudSelectPatch(int suspectStateIdx) { var voteData = PlayerControl.LocalPlayer.GetVoteData(); @@ -202,7 +205,7 @@ internal static class MeetingHudPatches continue; } - voteData.Votes.RemoveAll(x=>x.Suspect==pc.PlayerId); + voteData.Votes.RemoveAll(x => x.Suspect == pc.PlayerId); voteData.VotesRemaining += 1; VotingUtils.RpcRemoveVote(PlayerControl.LocalPlayer, player.PlayerId, pc.PlayerId); @@ -253,7 +256,7 @@ internal static class MeetingHudPatches var voterStates = new Il2CppStructArray([ .. votes.Select( - v=> new MeetingHud.VoterState + v => new MeetingHud.VoterState { VoterId = v.Voter, VotedForId = v.Suspect, @@ -266,29 +269,29 @@ internal static class MeetingHudPatches [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.PopulateResults))] - public static bool PopulateResultsPatch(MeetingHud __instance, ref Il2CppStructArray states) + public static bool PopulateResultsPatch(ref Il2CppStructArray states) { - var votes = states.Select(x=> new CustomVote(x.VoterId, x.VotedForId)).ToList(); + var votes = states.Select(x => new CustomVote(x.VoterId, x.VotedForId)).ToList(); VotingUtils.HandlePopulateResults(votes); return false; } // TODO: figure out a way to do host-authorization since right now any player can send RpcCastVote + // Although this method is inlined in MeetingHud.Confirm, the next patch fixes that. [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.CmdCastVote))] - // Although this method is inlined in MeetingHud.Confirm, the next patch fixes that. - public static bool CmdCastVoteOverridePatch(MeetingHud __instance, byte playerId, byte suspectIdx) + public static bool CmdCastVoteOverridePatch(byte playerId, byte suspectIdx) { VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, playerId, suspectIdx); return false; } // TODO: figure out a way to do host-authorization since right now any player can send RpcQueueOverruleVotes + // Although this method is inlined in MeetingHud.Confirm, the next patch fixes that. [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.CmdQueueOverruleVotes))] - // Although this method is inlined in MeetingHud.Confirm, the next patch fixes that. - public static bool CmdQueueOverruleVotesPatch(MeetingHud __instance, PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) + public static bool CmdQueueOverruleVotesPatch(PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) { VotingUtils.RpcQueueOverruleVotes(PlayerControl.LocalPlayer, judgePlayerId.Value, targetPlayerId.Value, overruleNonce); return false; diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs index 21cd9d9..c77a97e 100644 --- a/MiraAPI/PluginLoading/IMiraPlugin.cs +++ b/MiraAPI/PluginLoading/IMiraPlugin.cs @@ -10,14 +10,14 @@ namespace MiraAPI.PluginLoading; public interface IMiraPlugin { /// - /// Gets or sets a value indicating whether to display the plugin in the options menu. + /// Gets a value indicating whether to display the plugin in the options menu. /// - public virtual bool DisplayOnOptionsMenu => true; + bool DisplayOnOptionsMenu => true; /// - /// Gets or sets a value for the localization id to use for anything registered under this mod. + /// Gets the localization id to use for anything registered under this mod. /// - public virtual string ParentLocaleId => GetType().Name.ToLower(CultureInfo.InvariantCulture); + string ParentLocaleId => GetType().Name.ToLower(CultureInfo.InvariantCulture); /// /// Gets the name to display on the options menu. @@ -28,39 +28,39 @@ public interface IMiraPlugin /// Gets the abbreviated name to display for other mods to pick up. /// /// The text for the plugin. - public virtual string GetAbbreviatedModName() + string GetAbbreviatedModName() { - return new(OptionsTitleText.Where(c => !char.IsLower(c) && !char.IsWhiteSpace(c)).ToArray()); + return new([.. OptionsTitleText.Where(c => !char.IsLower(c) && !char.IsWhiteSpace(c))]); } /// /// Gets the name for the first custom category in the game options menu, if any. /// - public virtual string CustomOptionMenuNameOne => "Custom Category 1"; + string CustomOptionMenuNameOne => "Custom Category 1"; /// /// Gets the name for the second custom category in the game options menu, if any. /// - public virtual string CustomOptionMenuNameTwo => "Custom Category 2"; + string CustomOptionMenuNameTwo => "Custom Category 2"; /// /// Gets the description for the second custom category in the game options menu, if any. /// - public virtual string ModifierMenuDescription => "Configure modifiers and their settings here!"; + string ModifierMenuDescription => "Configure modifiers and their settings here!"; /// /// Gets the description for the first custom category in the game options menu, if any. /// - public virtual string CustomOptionMenuOneDescription => "Apply game settings for this mod!"; + string CustomOptionMenuOneDescription => "Apply game settings for this mod!"; /// /// Gets the description for the second custom category in the game options menu, if any. /// - public virtual string CustomOptionMenuTwoDescription => "Apply game settings for this mod!"; + string CustomOptionMenuTwoDescription => "Apply game settings for this mod!"; /// /// Gets the for the plugin. /// /// The for the plugin. - public ConfigFile GetConfigFile(); + ConfigFile GetConfigFile(); } diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 874c32b..d1b5d51 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -76,14 +76,14 @@ public class MiraPluginInfo internal void SavePublicCollections() { - Presets = [..InternalPresets]; - Modifiers = [..InternalModifiers]; - OptionGroups = [..InternalOptionGroups]; - Options = [..InternalOptions]; + Presets = [.. InternalPresets]; + Modifiers = [.. InternalModifiers]; + OptionGroups = [.. InternalOptionGroups]; + Options = [.. InternalOptions]; Roles = new ReadOnlyDictionary(InternalRoles); - Buttons = [..InternalButtons]; - MeetingButtons = [..InternalMeetingButtons]; - TargetedMeetingButtons = [..InternalTargetedMeetingButtons]; + Buttons = [.. InternalButtons]; + MeetingButtons = [.. InternalMeetingButtons]; + TargetedMeetingButtons = [.. InternalTargetedMeetingButtons]; } internal List InternalPresets { get; } = []; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 1a3fe4b..f021c3f 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -7,7 +7,6 @@ using System.Reflection; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; -using Il2CppInterop.Runtime.Injection; using MiraAPI.Colors; using MiraAPI.Events; using MiraAPI.GameEnd; @@ -20,12 +19,10 @@ using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; -using MiraAPI.Patches.Options; using MiraAPI.Presets; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; -using Reactor.Localization.Utilities; using Reactor.Networking; using Reactor.Utilities; @@ -44,8 +41,8 @@ public sealed class MiraPluginManager 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 static ConfigFile MiraApiConfig { get; private set; } = null!; + internal static MiraPluginInfo MiraApiPluginInfo { get; private set; } = null!; internal void Initialize(BasePlugin miraApiPlugin, IMiraPlugin apiPlugin) { @@ -225,7 +222,6 @@ public sealed class MiraPluginManager continue; } - if (RegisterMeetingAbility(type, info)) { continue; @@ -269,7 +265,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)]; + PluginsWithOptionsOrGameModes = [.. RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu || m.GameModes.Count > 0)]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); }; @@ -317,9 +313,12 @@ public sealed class MiraPluginManager foreach (var property in type.GetProperties()) { + if (property.GetCustomAttribute() != null) + continue; + if (property.GetMethod?.IsStatic == true) { - Error($"Option property {property.Name} in {type.Name} must not be static."); + Error($"Non-ignored option property {property.Name} in {type.Name} must not be static."); continue; } @@ -329,16 +328,28 @@ public sealed class MiraPluginManager continue; } - var attribute = property.GetCustomAttribute(); - if (attribute == null) + if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList))) { + ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo); continue; } - ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo); + var attribute = property.GetCustomAttribute(); + if (attribute != null) + { + ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo); + continue; + } + + var listAttr = property.GetCustomAttribute(); + if (listAttr != null) + { + ModdedOptionsManager.RegisterAttributeOptionList(type, listAttr, property, pluginInfo); + } } - foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption)))) + foreach (var field in type.GetFields() + .Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption)) || f.FieldType.IsAssignableTo(typeof(IModdedOptionList)))) { Error($"{field.Name} is a field, not a property. Use properties for options."); } @@ -349,6 +360,7 @@ public sealed class MiraPluginManager { Error($"Failed to register options for {type.Name}: {e.ToString()}"); } + return false; } @@ -375,6 +387,7 @@ public sealed class MiraPluginManager { Error($"Failed to register role for {type.Name}: {e}"); } + return false; } @@ -395,7 +408,7 @@ public sealed class MiraPluginManager foreach (var property in type.GetProperties()) { - if (property.PropertyType != typeof(CustomColor)) + if (!property.PropertyType.IsAssignableTo(typeof(CustomColor))) { continue; } @@ -407,6 +420,9 @@ public sealed class MiraPluginManager } PaletteManager.CustomColors.Add(color); + + if (color is VariableColor) + PaletteManager.VariableColorNames.Add(color.Name); } foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor)))) @@ -458,6 +474,7 @@ public sealed class MiraPluginManager { Error($"Failed to register gamemode {type.Name}: {e}"); } + return false; } @@ -594,6 +611,7 @@ public sealed class MiraPluginManager return false; } } + private static bool RegisterTargetedMeetingAbility(Type type, MiraPluginInfo info) { try diff --git a/MiraAPI/Presets/OptionPreset.cs b/MiraAPI/Presets/OptionPreset.cs index 11b6d75..6ecab1f 100644 --- a/MiraAPI/Presets/OptionPreset.cs +++ b/MiraAPI/Presets/OptionPreset.cs @@ -10,47 +10,36 @@ namespace MiraAPI.Presets; /// /// Represents a preset of game options that can be applied to the game. /// -public class OptionPreset +/// The name of the preset. +/// The plugin associated with the preset. +/// The configuration file for the preset. +public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetConfig) { /// /// Gets the name of the preset. /// - public string Name { get; } + public string Name { get; } = name; /// /// Gets the plugin associated with the preset. /// - public MiraPluginInfo Plugin { get; } + public MiraPluginInfo Plugin { get; } = plugin; /// /// Gets the of the plugin associated with the preset. /// - public ConfigFile PluginConfig { get; } + public ConfigFile PluginConfig { get; } = plugin.PluginConfig; /// /// Gets the for the preset. /// - public ConfigFile PresetConfig { get; } + public ConfigFile PresetConfig { get; } = presetConfig; /// /// Gets or sets the button associated with the preset in the UI. /// public GameObject? PresetButton { get; set; } - /// - /// Initializes a new instance of the class with the specified name and configuration file. - /// - /// The name of the preset. - /// The plugin associated with the preset. - /// The configuration file for the preset. - public OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetConfig) - { - Name = name; - Plugin = plugin; - PluginConfig = plugin.PluginConfig; - PresetConfig = presetConfig; - } - /// /// Loads the preset by applying the values from the preset configuration to the plugin configuration. /// @@ -78,7 +67,12 @@ public class OptionPreset /// The option to reset. public void ResetOption(OptionBehaviour baseOption) { - var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption); + var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption); + if (selectedOpt == null) + { + return; + } + selectedOpt.LoadFromPreset(PresetConfig); /*ModdedOptionsManager.SyncAllOptions(); @@ -92,7 +86,7 @@ public class OptionPreset /// The value of whether the option is in the preset. public bool IsOptionInPreset(OptionBehaviour baseOption) { - var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption); - return selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition); + var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption); + return selectedOpt != null && selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition); } } diff --git a/MiraAPI/Presets/PresetManager.cs b/MiraAPI/Presets/PresetManager.cs index f753db5..42f0a07 100644 --- a/MiraAPI/Presets/PresetManager.cs +++ b/MiraAPI/Presets/PresetManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using BepInEx.Configuration; @@ -55,9 +56,10 @@ public static class PresetManager /// Loads the s for the specified plugin by reading the s from the preset directory. /// /// The plugin for which the s should be loaded. + [SuppressMessage("Style", "IDE0031:Use null propagation", Justification = "Using null propagation bypasses Unity's lifetime checks.")] public static void LoadPresets(MiraPluginInfo plugin) { - foreach (var btn in plugin.InternalPresets.Select(x=>x.PresetButton)) + foreach (var btn in plugin.InternalPresets.Select(x => x.PresetButton)) { if (btn != null) { @@ -87,12 +89,12 @@ public static class PresetManager SaveOnConfigSet = false, }; - foreach (var option in plugin.InternalOptions.Where(x=>x.IncludeInPreset)) + foreach (var option in plugin.InternalOptions.Where(x => x.IncludeInPreset)) { option.Bind(presetConfig); } - foreach (var role in plugin.InternalRoles.Values.OfType().Where(x=>!x.Configuration.HideSettings)) + foreach (var role in plugin.InternalRoles.Values.OfType().Where(x => !x.Configuration.HideSettings)) { role.BindConfig(presetConfig); } @@ -101,7 +103,8 @@ public static class PresetManager plugin.InternalPresets.Add(new OptionPreset(presetName, plugin, presetConfig)); } - plugin.Presets = [..plugin.InternalPresets]; + + plugin.Presets = [.. plugin.InternalPresets]; } /// @@ -109,7 +112,7 @@ public static class PresetManager /// public static void LoadMasterPreset() { - /*foreach (var btn in plugin.InternalPresets.Select(x=>x.PresetButton)) + /*foreach (var btn in plugin.InternalPresets.Select(x => x.PresetButton)) { if (btn != null) { @@ -118,6 +121,7 @@ public static class PresetManager } plugin.InternalPresets.Clear();*/ + if (!Directory.Exists(PresetDirectory)) { Directory.CreateDirectory(PresetDirectory); @@ -133,6 +137,7 @@ public static class PresetManager { var fileName = Path.GetFileName(file); Info($"Loading preset file {fileName}"); + // var presetName = Path.GetFileNameWithoutExtension(file); var presetConfig = new ConfigFile(file, false) { @@ -140,12 +145,12 @@ public static class PresetManager }; foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins) { - foreach (var option in plugin.InternalOptions.Where(x=>x.IncludeInPreset)) + foreach (var option in plugin.InternalOptions.Where(x => x.IncludeInPreset)) { option.Bind(presetConfig); } - foreach (var role in plugin.InternalRoles.Values.OfType().Where(x=>!x.Configuration.HideSettings)) + foreach (var role in plugin.InternalRoles.Values.OfType().Where(x => !x.Configuration.HideSettings)) { role.BindConfig(presetConfig); } @@ -155,6 +160,7 @@ public static class PresetManager // InternalMasterPresets.Add(new OptionPreset(presetName, plugin, presetConfig)); } - MasterPresets = [..InternalMasterPresets]; + + MasterPresets = [.. InternalMasterPresets]; } } diff --git a/MiraAPI/Presets/SavePresetPopup.cs b/MiraAPI/Presets/SavePresetPopup.cs index 87959e7..9405e83 100644 --- a/MiraAPI/Presets/SavePresetPopup.cs +++ b/MiraAPI/Presets/SavePresetPopup.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Attributes; @@ -7,24 +8,25 @@ using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +// TODO: for the love of fucking god please add a namespace on the asset bundle AND here - Atony. +/* +namespace MiraAPI.Presets; +*/ [RegisterInIl2Cpp] -#pragma warning disable S3903 -#pragma warning disable CA1050 +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] +[SuppressMessage("Design", "CA1050:Declare types in namespaces", Justification = "Unity script.")] +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Unity fields; ignore.) public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr) -#pragma warning restore CA1050 -#pragma warning restore S3903 { -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. private TextMeshPro textBoxText; private TextBoxTMP textBox; private Action onSave; -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. // Cleanup holder object - public void OnDestroy() + private void OnDestroy() { transform.parent.gameObject.Destroy(); } @@ -80,7 +82,7 @@ public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr) public static void CreatePopup(Action saveAction) { - // Because innerscuff doesnt account for Z value in Close animation + // Because innerscuff doesn't account for Z value in Close animation var holder = new GameObject("PopupHolder") { transform = diff --git a/MiraAPI/Resources/Locale/arb.xml b/MiraAPI/Resources/Locale/arb.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/arb.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/de_DE.xml b/MiraAPI/Resources/Locale/de_DE.xml index bf6add6..08f818f 100644 --- a/MiraAPI/Resources/Locale/de_DE.xml +++ b/MiraAPI/Resources/Locale/de_DE.xml @@ -1,14 +1,43 @@ - Knopfskalierungsfaktor - Wiki Taste Unter Haupt UI - Rollen - Modifikator - Modifikatoren - Besatzung - Besatzung - Verräter - Verräter - Neutral - Neutral + Knopfskalierungsfaktor + Wiki Taste auf der Hauptseite + Rollen + Modifikator + Modifikatoren + Besatzung + Besatzung + Verräter + Verräter + Neutral + Neutral + Mira API + Effekte/Benutzeroberfläche + Bedienungshilfen + MiraAPI + Modifikatoren Oberfläche links zeigen + Tastenbelegung auf Knöpfen zeigen + Knopf Skallierung + Maximale FPS + Sabortageblitze aktivieren + Sabortage Geräusche aktivieren + Zu Rolleneinstellungen zurückkehren + Benutzerdefinierte Kategorie 1 + Benutzerdefinierte Kategorie 2 + Bearbeite Modifikatoren und deren Einstellungen hier! + Spieleinstellungen für die Mod speicher! + Klassik + Die klassische Among Us erfahrung +Finde den Verrähter oder täusche die Besatzung. + Verstecken + Du kannst rennen, aber dich nicht Verstecken +Überlebe die Zeit oder eliminiere die Besatzung. + Zum schließen klicken + Zum öffnen klicken + Wähle oder erstelle Voreinstellungen für dieses Spiel + Speichern + Besatzung Rollen + Verräter Rollen + Neutrale Rollen + Andere diff --git a/MiraAPI/Resources/Locale/el_GR.xml b/MiraAPI/Resources/Locale/el_GR.xml new file mode 100644 index 0000000..30f2574 --- /dev/null +++ b/MiraAPI/Resources/Locale/el_GR.xml @@ -0,0 +1,4 @@ + + + + diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index a00952f..9073a6f 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -2,21 +2,29 @@ Mira API Visuals/UI + Match Info Guide Accessibility MiraAPI Show Modifiers HUD on Left Side Show Keybinds on Buttons Button Scale Factor Top-Right UI Scale Factor + Match Info Guide Scale Factor + Tab Buttons Grid Placement + Left-Side + Right-Side + Below UI Max FPS - Display Wiki on Bottom Row + Display Button on Bottom Row Enable Sabotage Flashes Enable Sabotage Blare + Enable Variable Color Changes RETURN TO ROLE SETTINGS Custom Category 1 Custom Category 2 Configure modifiers and their settings here! Apply game settings for this mod! + Gamemode Classic The classic Among Us experience! Find the Impostors or deceive the Crew. @@ -40,4 +48,35 @@ Outlive the timer or eliminate the crew. Neut Neutral Roles Other + Page + Page + Main + Ability + Indirect Ability + Interaction Ability + Radius Ability + Menu Ability + Meeting Ability + Selective Meeting Ability + Meeting Menu Ability + Options + Player List + Game Settings + Search... + Filter + Grouping + Order + Vanilla + Search Results + Previous + Next + No Results Found + On + Off + Enabled + Disabled + Active + Inactive + Activated + Deactivated diff --git a/MiraAPI/Resources/Locale/es_ES.xml b/MiraAPI/Resources/Locale/es_ES.xml index 3240eed..d1fd7aa 100644 --- a/MiraAPI/Resources/Locale/es_ES.xml +++ b/MiraAPI/Resources/Locale/es_ES.xml @@ -12,4 +12,5 @@ Mira API Accesibilidad MiraAPI + Visuales/IU diff --git a/MiraAPI/Resources/Locale/heb.xml b/MiraAPI/Resources/Locale/heb.xml new file mode 100644 index 0000000..1573833 --- /dev/null +++ b/MiraAPI/Resources/Locale/heb.xml @@ -0,0 +1,43 @@ + + + ויזואליה/ממשק משתמש + נגישות + ממשק התכנות של מירה + הצג מקשי קיצור על הכפתורים + הצג שינויים HUD מופעל בצד שמאל + מקדם גודל הכפתורים + ‫מקדם גודל ממשק המשתמש בפינה הימנית העליונה + מקסימום פריימים לשנייה + הצג את הוויקי בשורה התחתונה + הפעל הבהובים של מצבי חירום + הפעל צלילים של מצבי חירום + חזור להגדרות התפקיד + קטגוריה מותאמת אישית 1 + קטגוריה מותאמת אישית 2 + חבר צוות + צוות + תפקידי חברי הצוות + מתחזה + מתחזה + תפקידי המתחזים + אחר + מחבואים + אתה יכול לרוץ, אבל לא להתחבא! +לשרוד עד שהזמן אוזל או להשמיד את חברי הצוות. + לחץ כדי לסגור + לחץ כדי לפתוח + בחר או צור ערכת הגדרות למשחק + שמירה + תפקידים + נטרלי + ניוט + תפקידים נטרלים + התכנות של מירה + הגדר כאן את המאפיינים הנוספים ואת ההגדרות שלהם! + החל הגדרות משחק עבור מוד זה! + קלאסי + חווית אמונג אס הקלאסית. +מצאו את המתחזים או הטעו את הצוות. + תוספים + תוסף + diff --git a/MiraAPI/Resources/Locale/lzh_CN.xml b/MiraAPI/Resources/Locale/lzh_CN.xml new file mode 100644 index 0000000..38f23cc --- /dev/null +++ b/MiraAPI/Resources/Locale/lzh_CN.xml @@ -0,0 +1,24 @@ + + + Mira API + 視/介 + 無礙 + MiraAPI + 於左示附職之界 + 於鈕上示熱鍵 + 鈕縮之比 + 右上角界縮之比 + 幀之極 + 維基之鈕示於底 + 啟破壞赤爍 + 啟破壞樂 + 反職之設 + 自訂類者一 + 自訂類者二 + 於此定附職及其設! + 為此模組施以戲之設 + 典 + 爾虞我詐典式之體驗哉! +求奸,或欺瞞良眾。 + 藏矇 + diff --git a/MiraAPI/Resources/Locale/pl_PL.xml b/MiraAPI/Resources/Locale/pl_PL.xml index 0142715..bb46467 100644 --- a/MiraAPI/Resources/Locale/pl_PL.xml +++ b/MiraAPI/Resources/Locale/pl_PL.xml @@ -1,7 +1,7 @@ Skala Przycisków w Prawym Górnym - Przycisk Wiki Poniżej Głównego UI + Pokaż Wiki w Dolnym Rzędzie Role Modyfikator Modyfikatory @@ -29,4 +29,15 @@ Znajdź Intruzów, bądź oszukaj Załogę. Chowany Możesz uciekać, ale nie możesz się schować! Przeżyj przez wymagany czas, albo zabij chowających. + Zastosuj ustawienia gry dla tego moda! + Naciśnij aby zamknąć + Naciśnij aby otworzyć + Stwórz lub wybierz preset ustawień na tą grę + Zapisz + Mira API + MiraAPI + Role Załogi + Role Intruzów + Role Neutralne + Inne diff --git a/MiraAPI/Resources/Locale/pt_PT.xml b/MiraAPI/Resources/Locale/pt_PT.xml index 1cfec24..254fe03 100644 --- a/MiraAPI/Resources/Locale/pt_PT.xml +++ b/MiraAPI/Resources/Locale/pt_PT.xml @@ -29,7 +29,7 @@ Clássico A experiencia clássica do Among Us! Encontra os Impostores ou engana a Tripulação. - Escondidinhas + Escondidas Podes fugir, mas não podes esconder! Vive até o tempo acabar ou elimina a tripulação. Clica para fechar diff --git a/MiraAPI/Resources/Locale/tr_TR.xml b/MiraAPI/Resources/Locale/tr_TR.xml index 30f2574..ecbb7e7 100644 --- a/MiraAPI/Resources/Locale/tr_TR.xml +++ b/MiraAPI/Resources/Locale/tr_TR.xml @@ -1,4 +1,43 @@ - + Mira API + Görseller/Arayüz + Erişilebilirlik + MiraAPI + Değiştiriciler HUD'ını Sol Tarafta Göster + Düğmelerde Tuş Atamalarını Göster + Düğme Ölçek Katsayısı + Sağ Üst Arayüz Ölçek Katsayısı + Maks FPS + Wiki'yi Alt Satırda Göster + Sabotaj Flaşlarını Etkinleştir + Sabotaj Sirenini Etkinleştir + ROL AYARLARINA DÖN + Özel Kategori 1 + Özel Kategori 2 + Değiştiricileri ve ayarlarını buradan yapılandırın! + Bu mod için oyun ayarlarını uygula! + Klasik + Klasik Among Us deneyimi! +Sahtekârları bulun veya Mürettebatı kandırın. + Saklambaç + Kaçabilirsin ama saklanamazsın! +Süre dolana kadar hayatta kal veya Mürettebatı ortadan kaldır. + Kapatmak için tıklayın + Açmak için tıklayın + Bu oyun için bir seçenek profili seçin veya oluşturun + Kaydet + Roller + Değiştiriciler + Değiştirici + Mürettebat + Mürettebat + Mürettebat Rolleri + Sahtekâr + Sahtekâr + Sahtekâr Rolleri + Tarafsız + Tarafsız + Tarafsız Roller + Diğer diff --git a/MiraAPI/Resources/Locale/zh_CN.xml b/MiraAPI/Resources/Locale/zh_CN.xml index 445b2be..7359c06 100644 --- a/MiraAPI/Resources/Locale/zh_CN.xml +++ b/MiraAPI/Resources/Locale/zh_CN.xml @@ -22,11 +22,24 @@ 保存 职业 附加职业 - 附加属性 - 船员 - 船员 - 伪装者 - 伪装者 - 中立 - 中立 + 附加属性 + 船员 + 船员 + 伪装者 + 伪装者 + 中立 + 中立 + 右上角缩放比例 + 维基按钮显示在底部 + 点击开启 + 船员职业 + 伪装者职业 + 中立职业 + 其他职业 + 经典 + 经典的 Among Us 模式体验! +找出伪装者,或者瞒过船员。 + 捉迷藏 + 你可以逃,但你躲不掉! +撑过倒计时,或消灭所有船员。 diff --git a/MiraAPI/Resources/Locale/zh_TW.xml b/MiraAPI/Resources/Locale/zh_TW.xml index 4438808..03db07e 100644 --- a/MiraAPI/Resources/Locale/zh_TW.xml +++ b/MiraAPI/Resources/Locale/zh_TW.xml @@ -1,14 +1,43 @@ - 右上角 UI 縮放比例 - 維基按鈕顯示在主界面下方 - 職業 - 附加屬性 - 附加屬性 - 船員 - 船員 - 偽裝者 - 偽裝者 - 中立 - 中立 + 右上角 UI 縮放比例 + 維基按鈕顯示在底部 + 職業 + 附加屬性 + 附加屬性 + 船員 + 船員 + 偽裝者 + 偽裝者 + 中立 + 中立 + Mira API + 視覺/介面 + 無障礙 + MiraAPI + 在左側顯示附加職業UI + 在按鈕上顯示快捷鍵 + 按鈕縮放比例 + 最大幀數 + 啟用破壞紅色閃爍 + 啟用破壞音效 + 返回職業設定 + 自定義類別 1 + 自定義類別 2 + 在此配置附加職業及其設定! + 為此模組套用遊戲設定! + 點擊關閉 + 點擊開啟 + 保存 + 為本遊戲選擇或創建一個預設 + 船員職業 + 偽裝者職業 + 中立職業 + 其他職業 + 經典 + 經典的 Among Us 模式體驗! +找出偽裝者, 或者瞞過船員。 + 捉迷藏 + 你可以逃, 但你躲不掉! +撐過倒計時, 或消滅所有船員。 diff --git a/MiraAPI/Resources/Wiki/GroupCountHover.png b/MiraAPI/Resources/Wiki/GroupCountHover.png new file mode 100644 index 0000000..0e8ec76 Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupCountHover.png differ diff --git a/MiraAPI/Resources/Wiki/GroupCountIdle.png b/MiraAPI/Resources/Wiki/GroupCountIdle.png new file mode 100644 index 0000000..5bf5f98 Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupCountIdle.png differ diff --git a/MiraAPI/Resources/Wiki/GroupFactionHover.png b/MiraAPI/Resources/Wiki/GroupFactionHover.png new file mode 100644 index 0000000..864e94d Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupFactionHover.png differ diff --git a/MiraAPI/Resources/Wiki/GroupFactionIdle.png b/MiraAPI/Resources/Wiki/GroupFactionIdle.png new file mode 100644 index 0000000..1d50880 Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupFactionIdle.png differ diff --git a/MiraAPI/Resources/Wiki/GroupNoneHover.png b/MiraAPI/Resources/Wiki/GroupNoneHover.png new file mode 100644 index 0000000..dd303b2 Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupNoneHover.png differ diff --git a/MiraAPI/Resources/Wiki/GroupNoneIdle.png b/MiraAPI/Resources/Wiki/GroupNoneIdle.png new file mode 100644 index 0000000..c215f2f Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupNoneIdle.png differ diff --git a/MiraAPI/Resources/Wiki/IconCrewmate.png b/MiraAPI/Resources/Wiki/IconCrewmate.png new file mode 100644 index 0000000..61f706f Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconCrewmate.png differ diff --git a/MiraAPI/Resources/Wiki/IconImpostor.png b/MiraAPI/Resources/Wiki/IconImpostor.png new file mode 100644 index 0000000..6100cd1 Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconImpostor.png differ diff --git a/MiraAPI/Resources/Wiki/IconNeutral.png b/MiraAPI/Resources/Wiki/IconNeutral.png new file mode 100644 index 0000000..984041c Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconNeutral.png differ diff --git a/MiraAPI/Resources/Wiki/IconPlayerList.png b/MiraAPI/Resources/Wiki/IconPlayerList.png new file mode 100644 index 0000000..234baa0 Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconPlayerList.png differ diff --git a/MiraAPI/Resources/Wiki/IconSettings.png b/MiraAPI/Resources/Wiki/IconSettings.png new file mode 100644 index 0000000..50452dc Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconSettings.png differ diff --git a/MiraAPI/Resources/Wiki/IconTerminology.png b/MiraAPI/Resources/Wiki/IconTerminology.png new file mode 100644 index 0000000..8b95d62 Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconTerminology.png differ diff --git a/MiraAPI/Resources/Wiki/PageLeftHover.png b/MiraAPI/Resources/Wiki/PageLeftHover.png new file mode 100644 index 0000000..7fa3b15 Binary files /dev/null and b/MiraAPI/Resources/Wiki/PageLeftHover.png differ diff --git a/MiraAPI/Resources/Wiki/PageLeftIdle.png b/MiraAPI/Resources/Wiki/PageLeftIdle.png new file mode 100644 index 0000000..cbedc93 Binary files /dev/null and b/MiraAPI/Resources/Wiki/PageLeftIdle.png differ diff --git a/MiraAPI/Resources/Wiki/PageRightHover.png b/MiraAPI/Resources/Wiki/PageRightHover.png new file mode 100644 index 0000000..8ffb435 Binary files /dev/null and b/MiraAPI/Resources/Wiki/PageRightHover.png differ diff --git a/MiraAPI/Resources/Wiki/PageRightIdle.png b/MiraAPI/Resources/Wiki/PageRightIdle.png new file mode 100644 index 0000000..0cb09f6 Binary files /dev/null and b/MiraAPI/Resources/Wiki/PageRightIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SearchClearHover.png b/MiraAPI/Resources/Wiki/SearchClearHover.png new file mode 100644 index 0000000..a23a1da Binary files /dev/null and b/MiraAPI/Resources/Wiki/SearchClearHover.png differ diff --git a/MiraAPI/Resources/Wiki/SearchClearIdle.png b/MiraAPI/Resources/Wiki/SearchClearIdle.png new file mode 100644 index 0000000..7f50a9b Binary files /dev/null and b/MiraAPI/Resources/Wiki/SearchClearIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SearchIconHover.png b/MiraAPI/Resources/Wiki/SearchIconHover.png new file mode 100644 index 0000000..d64f9af Binary files /dev/null and b/MiraAPI/Resources/Wiki/SearchIconHover.png differ diff --git a/MiraAPI/Resources/Wiki/SearchIconIdle.png b/MiraAPI/Resources/Wiki/SearchIconIdle.png new file mode 100644 index 0000000..d16f7f9 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SearchIconIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarModifiersButtonHover.png b/MiraAPI/Resources/Wiki/SidebarModifiersButtonHover.png new file mode 100644 index 0000000..090abc5 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarModifiersButtonHover.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarModifiersButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarModifiersButtonIdle.png new file mode 100644 index 0000000..2712441 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarModifiersButtonIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png new file mode 100644 index 0000000..1127956 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarPlayersButtonHover.png b/MiraAPI/Resources/Wiki/SidebarPlayersButtonHover.png new file mode 100644 index 0000000..c4bd871 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarPlayersButtonHover.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarPlayersButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarPlayersButtonIdle.png new file mode 100644 index 0000000..6c991be Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarPlayersButtonIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarPlayersButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarPlayersButtonOpen.png new file mode 100644 index 0000000..6e6ccbf Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarPlayersButtonOpen.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarRolesButtonHover.png b/MiraAPI/Resources/Wiki/SidebarRolesButtonHover.png new file mode 100644 index 0000000..8bfb8d1 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarRolesButtonHover.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarRolesButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarRolesButtonIdle.png new file mode 100644 index 0000000..c75d077 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarRolesButtonIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarRolesButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarRolesButtonOpen.png new file mode 100644 index 0000000..162bf07 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarRolesButtonOpen.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png new file mode 100644 index 0000000..b4e53a9 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png new file mode 100644 index 0000000..2d040a6 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png new file mode 100644 index 0000000..e750bac Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png differ diff --git a/MiraAPI/Resources/Wiki/SortingAzHover.png b/MiraAPI/Resources/Wiki/SortingAzHover.png new file mode 100644 index 0000000..d9779c6 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SortingAzHover.png differ diff --git a/MiraAPI/Resources/Wiki/SortingAzIdle.png b/MiraAPI/Resources/Wiki/SortingAzIdle.png new file mode 100644 index 0000000..5efb21b Binary files /dev/null and b/MiraAPI/Resources/Wiki/SortingAzIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SortingZaHover.png b/MiraAPI/Resources/Wiki/SortingZaHover.png new file mode 100644 index 0000000..c9d817a Binary files /dev/null and b/MiraAPI/Resources/Wiki/SortingZaHover.png differ diff --git a/MiraAPI/Resources/Wiki/SortingZaIdle.png b/MiraAPI/Resources/Wiki/SortingZaIdle.png new file mode 100644 index 0000000..199071a Binary files /dev/null and b/MiraAPI/Resources/Wiki/SortingZaIdle.png differ diff --git a/MiraAPI/Resources/Wiki/TabButtonContainer.png b/MiraAPI/Resources/Wiki/TabButtonContainer.png new file mode 100644 index 0000000..7aa62a3 Binary files /dev/null and b/MiraAPI/Resources/Wiki/TabButtonContainer.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleAllHover.png b/MiraAPI/Resources/Wiki/VisibleAllHover.png new file mode 100644 index 0000000..9380f83 Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleAllHover.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleAllIdle.png b/MiraAPI/Resources/Wiki/VisibleAllIdle.png new file mode 100644 index 0000000..9ddcaee Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleAllIdle.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleOffHover.png b/MiraAPI/Resources/Wiki/VisibleOffHover.png new file mode 100644 index 0000000..ecfaefe Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleOffHover.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleOffIdle.png b/MiraAPI/Resources/Wiki/VisibleOffIdle.png new file mode 100644 index 0000000..a0c8ea9 Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleOffIdle.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleOnHover.png b/MiraAPI/Resources/Wiki/VisibleOnHover.png new file mode 100644 index 0000000..a4e5be8 Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleOnHover.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleOnIdle.png b/MiraAPI/Resources/Wiki/VisibleOnIdle.png new file mode 100644 index 0000000..30f1067 Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleOnIdle.png differ diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 0829eaa..46fc3bd 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameModes; @@ -6,6 +7,7 @@ using MiraAPI.Patches.Freeplay; using MiraAPI.Utilities.Assets; using TMPro; using UnityEngine; + namespace MiraAPI.Roles; /// @@ -13,10 +15,9 @@ namespace MiraAPI.Roles; /// public record struct CustomRoleConfiguration { -#pragma warning disable S1133 - [Obsolete("Default constructor is not supported. Please use the constructor that takes an ICustomRole parameter.")] -#pragma warning restore S1133 -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + [Obsolete("Default constructor is not supported. Please use the constructor that takes an ICustomRole parameter.", true)] + [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Will not be removed, as this throws instead.")] +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Read above.) public CustomRoleConfiguration() #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member { diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index bc93252..0548eaa 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -10,7 +10,6 @@ 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; @@ -46,6 +45,11 @@ public static class CustomRoleManager /// public static IReadOnlyList CustomRoleBehaviours { get; private set; } = []; + /// + /// Gets the of all roles as objects. + /// + public static IReadOnlyList AllStoredRoleBehaviours { get; private set; } = []; + /// /// Gets the of custom roles as objects. /// @@ -53,8 +57,9 @@ public static class CustomRoleManager internal static readonly Dictionary CustomRoles = []; internal static readonly Dictionary RoleIds = []; + internal static readonly Dictionary RoleParentMods = []; - private static Il2CppSystem.Collections.Generic.List? _emptySettings; + private static CppCollections.List? _emptySettings; private static Il2CppReferenceArray? _emptyKillAnimations; private static ushort _roleId = 100; @@ -76,8 +81,9 @@ public static class CustomRoleManager RoleManager.GhostRoles.Add(role.Role); } - CustomRoleBehaviours = CustomRoles.Values.ToList(); - CustomMiraRoles = CustomRoles.Values.OfType().ToList(); + CustomRoleBehaviours = [.. CustomRoles.Values]; + CustomMiraRoles = [.. CustomRoles.Values.OfType()]; + AllStoredRoleBehaviours = [.. RoleManager.Instance.AllRoles]; } internal static void RegisterRoleTypes(List roles, MiraPluginInfo pluginInfo) @@ -100,6 +106,7 @@ public static class CustomRoleManager } pluginInfo.InternalRoles.Add((ushort)role.Role, role); + RoleParentMods[roleType] = pluginInfo; } catch (Exception ex) { @@ -143,18 +150,13 @@ public static class CustomRoleManager roleBehaviour.CanVent = customRole.Configuration.CanUseVent || customRole.Configuration.GetsVentData; roleBehaviour.DefaultGhostRole = customRole.Configuration.GhostRole; roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount; - if (customRole.Configuration.OptionsScreenshot != null) - { - roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot.LoadAsset(); - } - else - { - roleBehaviour.RoleScreenshot = Sprite.Create( + roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot != null + ? customRole.Configuration.OptionsScreenshot.LoadAsset() + : Sprite.Create( null, new Rect(0, 0, 370, 230), Vector2.one / 2, 100); - } _emptySettings ??= new(0); _emptyKillAnimations ??= new(0); @@ -211,7 +213,9 @@ public static class CustomRoleManager /// A object representing the parent mod of the role. public static MiraPluginInfo FindParentMod(ICustomRole role) { - return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException())); + return RoleParentMods.TryGetValue(role.GetType(), out var mod) + ? mod + : MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException())); } /// @@ -283,7 +287,7 @@ public static class CustomRoleManager internal static void HandleSyncRoleOptions(NetData[] data) { // necessary to disable then re-enable this setting - // we dont know how other plugins handle their configs + // we don't know how other plugins handle their configs // this way, all the options are saved at once, instead of one by one var oldConfigSetting = new Dictionary(); foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins) diff --git a/MiraAPI/Roles/CustomRoleSingleton.cs b/MiraAPI/Roles/CustomRoleSingleton.cs index fe69eb0..0addc6f 100644 --- a/MiraAPI/Roles/CustomRoleSingleton.cs +++ b/MiraAPI/Roles/CustomRoleSingleton.cs @@ -7,7 +7,8 @@ namespace MiraAPI.Roles; /// A utility class to get the instance of a custom role. /// /// The you are trying to access. -public static class CustomRoleSingleton where T : ICustomRole +public static class CustomRoleSingleton + where T : ICustomRole { private static T? _instance; diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index d43acbe..c5b1feb 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.Linq; using System.Text; using AmongUs.GameOptions; -using MiraAPI.Translation; using MiraAPI.GameModes; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; @@ -24,18 +23,19 @@ public static class CustomRoleUtils /// if the is able to spawn, otherwise . public static bool CanSpawnOnCurrentMode(RoleBehaviour role) { - if (role is ICustomRole custom) - { - return custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode); - } - - if (CustomGameModeManager.ActiveMode is HideAndSeekMode) - { - return role.Role is RoleTypes.Engineer || role.Role is RoleTypes.Impostor; - } - return !Helpers.IsRoleBlacklisted(role); + return role is ICustomRole custom + ? custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode) + : (CustomGameModeManager.ActiveMode is HideAndSeekMode + ? role.Role is RoleTypes.Engineer or RoleTypes.Impostor + : !role.IsRoleBlacklisted()); } + /// + /// Retrieves a flattened list of possible roles and their selection chances based on the provided assignment data. + /// + /// The list of role assignment configurations to process. + /// An optional filter to apply to the assignment data. If null, all roles in the list are processed. + /// A list of tuples containing the role type and its corresponding chance, duplicated according to each role's configured count. public static List<(ushort RoleType, int Chance)> GetPossibleRoles( List assignmentData, Func? predicate = null) @@ -53,6 +53,11 @@ public static class CustomRoleUtils return roles; } + /// + /// Generates the role assignment data for a specific role type using the current game options. + /// + /// The type of the role to retrieve assignment data for. + /// A instance containing the role behaviour, its maximum count per game, and its selection chance. public static RoleManager.RoleAssignmentData GetAssignData(RoleTypes roleType) { var currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; @@ -67,12 +72,15 @@ public static class CustomRoleUtils return assignmentData; } + /// + /// Retrieves the registered behaviour for a given role type. + /// + /// The type of the role to find. + /// The matching if found; otherwise, null. public static RoleBehaviour? GetRegisteredRole(RoleTypes roleType) { // we want to prioritize the custom roles because the role has the right RoleColour/TeamColor - var role = CustomRoleManager.AllRoles.FirstOrDefault(x => x.Role == roleType); - - return role; + return CustomRoleManager.AllRoles.FirstOrDefault(x => x.Role == roleType); } /// @@ -80,21 +88,31 @@ public static class CustomRoleUtils /// /// An of s. // ReSharper disable once MemberCanBePrivate.Global - public static IEnumerable GetActiveRoles() => PlayerControl.AllPlayerControls.ToArray().Select(x => x.Data.Role); + public static IEnumerable GetActiveRoles() + { + return PlayerControl.AllPlayerControls.ToArray().Select(x => x.Data.Role); + } /// /// Gets all active in-game roles in a certain team. /// /// The team you would like to check for. /// A list of roles with the team. - public static IEnumerable GetActiveRolesOfTeam(ModdedRoleTeams team) => GetActiveRoles().Where(x => x is ICustomRole customRole && customRole.Team == team); + public static IEnumerable GetActiveRolesOfTeam(ModdedRoleTeams team) + { + return GetActiveRoles().Where(x => x is ICustomRole customRole && customRole.Team == team); + } /// /// Gets all active in-game roles. /// /// The you would like to check for. /// An of s. - public static IEnumerable GetActiveRolesOfType() where T : RoleBehaviour => GetActiveRoles().OfType(); + public static IEnumerable GetActiveRolesOfType() + where T : RoleBehaviour + { + return GetActiveRoles().OfType(); + } /// /// Creates a for the Role Tab. @@ -118,12 +136,7 @@ public static class CustomRoleUtils public static LoadableAsset? GetIntroSound(RoleTypes roleType) { var role = CustomRoleManager.AllRoles.FirstOrDefault(role => role.Role == roleType); - if (role is ICustomRole customRole) - { - return customRole.Configuration.IntroSound; - } - - return new PreloadedAsset(role!.IntroSound); + return role is ICustomRole customRole ? customRole.Configuration.IntroSound : new PreloadedAsset(role!.IntroSound); } /// diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 50a9224..26dd0cf 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,11 +1,16 @@ using System; +using System.Collections.Generic; using System.Text; using BepInEx.Configuration; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Translation; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using TMPro; using UnityEngine; namespace MiraAPI.Roles; @@ -21,7 +26,7 @@ public interface ICustomRole : IOptionable string IdPart => GetType().Name; /// - /// Gets the id part used to build the role's translation keys. It is recommended to call it ModGuid.Role.Faction + /// Gets the id part used to build the role's translation keys. It is recommended to call it ModGuid.Role.Faction. /// string IdPrefix => GetType().Namespace!; @@ -48,7 +53,7 @@ public interface ICustomRole : IOptionable /// /// Gets the medium description of the role. Used in the role guide and options menu. /// - string RoleMedDescription => MiraLocaleManager.Get(RoleMedDescriptionLocale); + string RoleMedDescription => MiraLocaleManager.Get(RoleMedDescriptionLocale, RoleLongDescription); /// /// Gets the role's medium description id for localization. @@ -66,9 +71,10 @@ public interface ICustomRole : IOptionable string RoleLongDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "TabDescription"); /// - /// Gets the wiki description of the role. Used in the wiki, but not currently required. + /// Gets the wiki description of the role. Used in the wiki and normally appends the options text as well. /// - string RoleWikiDescription => MiraLocaleManager.Get(RoleLongDescriptionLocale); + string RoleWikiDescription => MiraLocaleManager.Get(RoleWikiDescriptionLocale, RoleLongDescription) + + Helpers.GetOptionsText(GetType()); /// /// Gets the role's wiki description id for localization. @@ -138,7 +144,7 @@ public interface ICustomRole : IOptionable /// Binds the configuration options for this role to the provided ConfigFile. /// /// The ConfigFile to bind the options to. - public virtual void BindConfig(ConfigFile config) + void BindConfig(ConfigFile config) { config.Bind(NumConfigDefinition, Configuration.DefaultRoleCount); config.Bind(ChanceConfigDefinition, Configuration.DefaultChance); @@ -149,7 +155,7 @@ public interface ICustomRole : IOptionable /// /// The to save the preset configuration to. /// Whether to use the default values for the configuration. - public virtual void SaveToPreset(ConfigFile presetConfig, bool useDefault=false) + void SaveToPreset(ConfigFile presetConfig, bool useDefault = false) { BindConfig(presetConfig); presetConfig[NumConfigDefinition].BoxedValue = useDefault ? Configuration.DefaultRoleCount : GetCount(); @@ -160,7 +166,7 @@ public interface ICustomRole : IOptionable /// Loads the role's configuration from a preset . /// /// The containing the preset configuration. - public virtual void LoadFromPreset(ConfigFile presetConfig) + void LoadFromPreset(ConfigFile presetConfig) { if (presetConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry numEntry)) { @@ -177,40 +183,31 @@ public interface ICustomRole : IOptionable /// Gets the role chance option. /// /// The role chance option. - public virtual int? GetChance() + int? GetChance() { - if (!Configuration.CanModifyChance) - { - return Configuration.DefaultChance; - } - - if (ParentMod.PluginConfig.TryGetEntry(ChanceConfigDefinition, out ConfigEntry entry)) - { - return Mathf.Clamp(entry.Value, 0, 100); - } - - return null; + return !Configuration.CanModifyChance + ? Configuration.DefaultChance + : ParentMod.PluginConfig.TryGetEntry(ChanceConfigDefinition, out ConfigEntry entry) + ? Mathf.Clamp(entry.Value, 0, 100) + : null; } /// /// Gets the role count option. /// /// The role count option. - public virtual int? GetCount() + int? GetCount() { - if (ParentMod.PluginConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry entry)) - { - return Mathf.Clamp(entry.Value, 0, Configuration.MaxRoleCount); - } - - return null; + return ParentMod.PluginConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry entry) + ? Mathf.Clamp(entry.Value, 0, Configuration.MaxRoleCount) + : null; } /// /// Sets the role chance option. /// /// The chance between 0 and 100. - public virtual void SetChance(int chance) + void SetChance(int chance) { if (!Configuration.CanModifyChance) { @@ -231,7 +228,7 @@ public interface ICustomRole : IOptionable /// Sets the role count option. /// /// The amount of this role between zero and its MaxRoleCount in the Configuration. - public virtual void SetCount(int count) + void SetCount(int count) { if (ParentMod.PluginConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry entry)) { @@ -247,7 +244,7 @@ public interface ICustomRole : IOptionable /// /// The player with the role. /// Whether they can see the role (name color) or not. - public virtual bool CanLocalPlayerSeeRole(PlayerControl player) + bool CanLocalPlayerSeeRole(PlayerControl player) { return (PlayerControl.LocalPlayer.Data.Role.IsImpostor && player.Data.Role.IsImpostor) || PlayerControl.LocalPlayer.Data.IsDead; } @@ -258,11 +255,11 @@ public interface ICustomRole : IOptionable /// The intro cutscene instance. /// The reference to the list of player in the team. /// to use the original team intro code, to skip. - public virtual bool SetupIntroTeam(IntroCutscene instance, ref Il2CppSystem.Collections.Generic.List yourTeam) + bool SetupIntroTeam(IntroCutscene instance, ref CppCollections.List yourTeam) { if (Team == ModdedRoleTeams.Custom) { - var team = new Il2CppSystem.Collections.Generic.List(); + var team = new CppCollections.List(); team.Add(PlayerControl.LocalPlayer); @@ -286,7 +283,10 @@ public interface ICustomRole : IOptionable /// Get the custom Role Tab text for this role. /// /// A with the role tab text. - StringBuilder SetTabText() => CustomRoleUtils.CreateForRole(this); + StringBuilder SetTabText() + { + return CustomRoleUtils.CreateForRole(this); + } /// /// Determine whether a given can be applied to this role. @@ -302,16 +302,57 @@ public interface ICustomRole : IOptionable /// Determines whether the role can spawn in general, accounting for gamemodes and everything else. /// /// if the role is able to spawn, otherwise . - public virtual bool CanSpawnOnCurrentMode() => Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode); + bool CanSpawnOnCurrentMode() + { + return Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode); + } /// - /// Determines whether the role is forcibly shown or disabled in the wiki screen. + /// Gets whether the role is forcibly shown or disabled in the wiki screen. /// /// if the role is always displayed, otherwise if it is never displayable, or if it is dictated by amount and chance. - public virtual bool? ForceShowRoleOnWiki => null; + bool? ForceShowRoleOnWiki => null; + + /// + /// Gets the information to display in the wiki. + /// + /// The guide object. + /// The title text object. + /// The title's icon. + /// The scroller parent. + /// The of the wiki. + GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent) + { + var abilities = WikiAbilities; + return Helpers.CreateAdvancedWikiPage( + guide, + titleText, + titleIcon, + parent, + RoleNameLocale, + $"{RoleName} ({RoleFactionTitle})", + Configuration.Icon ?? MiraAssets.IconNeutral.LoadAsset(), + RoleWikiDescription, + abilities); + } /// /// Gets the function that determines whether the role should be toggled on or off in the game settings. /// - public virtual Func VisibleInSettings => () => true; + Func VisibleInSettings => DelegateUtilities.AlwaysTrue; + + /// + /// Gets the list of abilities or other similar information to display below the text of a role's wiki page. + /// + [HideFromIl2Cpp] + List WikiAbilities => []; } + +/// +/// A data holder of an ability's wiki entry. +/// +/// The name of the ability. +/// The type of the ability. +/// The description of the ability. +/// The icon sprite of the ability. +public record struct AdvancedWikiAbilityDescription(string Name, string AbilityType, string Description, LoadableAsset Icon); diff --git a/MiraAPI/Roles/RoleId.cs b/MiraAPI/Roles/RoleId.cs index 189682b..3dd06b5 100644 --- a/MiraAPI/Roles/RoleId.cs +++ b/MiraAPI/Roles/RoleId.cs @@ -1,4 +1,5 @@ using System; +using AmongUs.GameOptions; namespace MiraAPI.Roles; @@ -13,14 +14,12 @@ public static class RoleId /// The type of the role. /// The role ID as a ushort. /// Thrown when the role is not registered. - public static ushort Get() where T : ICustomRole + public static ushort Get() + where T : ICustomRole { - if (!CustomRoleManager.RoleIds.TryGetValue(typeof(T), out var roleId)) - { - throw new InvalidOperationException($"Role {typeof(T)} is not registered"); - } - - return roleId; + return !CustomRoleManager.RoleIds.TryGetValue(typeof(T), out var roleId) + ? throw new InvalidOperationException($"Role {typeof(T)} is not registered") + : roleId; } /// @@ -31,11 +30,65 @@ public static class RoleId /// Thrown when the role is not registered. public static ushort Get(Type type) { - if (!CustomRoleManager.RoleIds.TryGetValue(type, out var roleId)) - { - throw new InvalidOperationException($"Role {type} is not registered"); - } + return !CustomRoleManager.RoleIds.TryGetValue(type, out var roleId) + ? throw new InvalidOperationException($"Role {type} is not registered") + : roleId; + } - return roleId; + /// + /// Gets the ID of a role. + /// + /// The role. + /// The ID as a ushort. + /// Thrown when the role is not registered. + public static ushort Get(ICustomRole role) + { + return Get(role.GetType()); + } + + /// + /// Gets the ID of an . + /// + /// The type of the role. + /// The role ID as a . + /// Thrown when the role is not registered. + public static RoleTypes GetType() + where T : ICustomRole + { + return (RoleTypes)Get(); + } + + /// + /// Gets the ID of a role. + /// + /// The type of the role. + /// The ID as a . + /// Thrown when the role is not registered. + public static RoleTypes GetType(Type type) + { + return (RoleTypes)Get(type); + } + + /// + /// Gets the role. + /// + /// The type of the role. + /// The object. + /// Thrown when the role is not registered. + public static RoleBehaviour GetRole() + where T : ICustomRole + { + return RoleManager.Instance.GetRole(GetType()); + } + + /// + /// Gets the role from its type. + /// + /// The type of the role. + /// The object. + /// Thrown when the role is not registered. + public static RoleBehaviour GetRole(Type type) + { + return RoleManager.Instance.GetRole(GetType(type)); } } diff --git a/MiraAPI/Translation/MiraLanguage.cs b/MiraAPI/Translation/MiraLanguage.cs index d6d2624..afa3b7b 100644 --- a/MiraAPI/Translation/MiraLanguage.cs +++ b/MiraAPI/Translation/MiraLanguage.cs @@ -1,30 +1,134 @@ namespace MiraAPI.Translation; /// -/// Supported languages for Mira translation system. -/// Maps to Among Us's SupportedLangs enum values. +/// Supported languages for Mira translation system.
+/// Maps to Among Us's enum values. ///
public enum MiraLanguage { + /// + /// English. + /// English, + + /// + /// Latam. + /// Latam, + + /// + /// Brazilian. + /// Brazilian, + + /// + /// Portuguese. + /// Portuguese, + + /// + /// Korean. + /// Korean, + + /// + /// Russian. + /// Russian, + + /// + /// Dutch. + /// Dutch, + + /// + /// Filipino. + /// Filipino, + + /// + /// French. + /// French, + + /// + /// German. + /// German, + + /// + /// Italian. + /// Italian, + + /// + /// Japanese. + /// Japanese, + + /// + /// Spanish. + /// Spanish, + + /// + /// Simplified Chinese. + /// SChinese, + + /// + /// Traditional Chinese. + /// + // ReSharper disable once InconsistentNaming (Justification: Language name.) TChinese, + + /// + /// Irish. + /// Irish, + + /// + /// Polish. + /// Polish, + + /// + /// Turkish. + /// Turkish, + + /// + /// Swedish. + /// Swedish, + + /// + /// Lithuanian. + /// Lithuanian, + + /// + /// Czech. + /// Czech, + + /// + /// Literary Chinese. + /// + LChinese, + + /// + /// Greek. + /// + Greek, + + /// + /// Arabic. + /// + Arabic, + + /// + /// Hebrew. + /// + Hebrew, } diff --git a/MiraAPI/Translation/MiraLocaleManager.cs b/MiraAPI/Translation/MiraLocaleManager.cs index 90f0df4..0bce8ca 100644 --- a/MiraAPI/Translation/MiraLocaleManager.cs +++ b/MiraAPI/Translation/MiraLocaleManager.cs @@ -5,23 +5,39 @@ using System.Linq; using System.Reflection; using System.Xml; using MiraAPI.Utilities; -using MonoMod.Utils; using Reactor.Localization.Utilities; using UnityEngine; namespace MiraAPI.Translation; +/// +/// The locale manager to handle translations and more. +/// public static class MiraLocaleManager { private const string LangDirectory = "mira_languages"; - // Language, Xml Name, then Value + // Language, XML Name, then Value + + /// + /// Gets the dictionary containing all loaded localization strings, organized by language and XML name. + /// public static Dictionary> Locale { get; } = []; + + /// + /// Gets the list of mod GUIDs that have been registered with the localization manager. + /// public static readonly List RegisteredModIds = []; + /// + /// Gets a dictionary mapping registered string keys to their corresponding enum values. + /// public static readonly Dictionary RegisteredStringNames = []; internal static readonly Dictionary StringNamesLookup = []; + /// + /// Gets a dictionary mapping values to their standard language codes. + /// public static Dictionary LangList { get; } = new() { { MiraLanguage.English, "en_US" }, @@ -45,7 +61,15 @@ public static class MiraLocaleManager { MiraLanguage.Swedish, "sv_SE" }, // Custom { MiraLanguage.Lithuanian, "lt_LT" }, // Custom { MiraLanguage.Czech, "cs_CZ" }, // Custom + { MiraLanguage.LChinese, "lzh_CN" }, // Custom + { MiraLanguage.Greek, "el_GR" }, // Custom + { MiraLanguage.Arabic, "arb" }, // Custom + { MiraLanguage.Hebrew, "heb" }, // Custom }; + + /// + /// Gets a dictionary mapping values to their standard language codes. + /// public static Dictionary LangCultureList { get; } = new() { { MiraLanguage.English, "en-US" }, @@ -69,6 +93,10 @@ public static class MiraLocaleManager { MiraLanguage.Swedish, "sv-SE" }, // Custom { MiraLanguage.Lithuanian, "lt-LT" }, // Custom { MiraLanguage.Czech, "cs-CZ" }, // Custom + { MiraLanguage.LChinese, "lzh-CN" }, // Custom + { MiraLanguage.Greek, "el-GR" }, // Custom + { MiraLanguage.Arabic, "arb" }, // Custom + { MiraLanguage.Hebrew, "heb" }, // Custom }; /// @@ -202,11 +230,24 @@ public static class MiraLocaleManager return text; } + /// + /// Builds a formatted translation ID string using a mod ID, a specific ID part, and a suffix. + /// + /// The mod GUID. + /// The core part of the ID. If it starts with '#', the character is stripped. + /// The suffix to append to the ID. + /// A formatted translation ID . public static string BuildTranslationId(string modId, string idPart, string suffix) { return idPart.StartsWith('#') ? idPart[1..] : $"{modId}.{idPart}.{suffix}"; } + /// + /// Builds a formatted translation ID string using a mod ID and a specific ID part. + /// + /// The mod GUID. + /// The core part of the ID. If it starts with '#', the character is stripped. + /// A formatted translation ID . public static string BuildTranslationId(string modId, string idPart) { return idPart.StartsWith('#') ? idPart[1..] : $"{modId}.{idPart}"; @@ -217,6 +258,11 @@ public static class MiraLocaleManager return Path.Combine(Application.persistentDataPath, LangDirectory, modGuid); } + /// + /// Retrieves the value for a given locale string name, creating and registering a new one if it does not already exist. + /// + /// The name of the locale string. + /// The corresponding value. public static StringNames GetOrCreateLocaleString(string name) { if (RegisteredStringNames.TryGetValue(name, out var stringName)) @@ -230,6 +276,9 @@ public static class MiraLocaleManager return newString; } + /// + /// Loads external locale XML files by checking predefined directories, including the plugin path, BepInEx root, game root, and registered mod directories. + /// public static void LoadExternalLocale() { CheckExternalDirectory("mira.api", BepInEx.Paths.PluginPath); @@ -241,6 +290,11 @@ public static class MiraLocaleManager } } + /// + /// Checks a specific directory for XML language files and parses their contents into the dictionary. + /// + /// The mod GUID associated with the directory. + /// The directory path to search for XML files. public static void CheckExternalDirectory(string modGuid, string dir) { Directory.CreateDirectory(dir); @@ -262,7 +316,7 @@ public static class MiraLocaleManager } catch (Exception e) { - Error($"Failed to load external translation {filePath}: {e.Message}"); + Error($"Failed to load external translation {filePath} for mod {modGuid}: {e.Message}"); } } } @@ -285,7 +339,7 @@ public static class MiraLocaleManager } using StreamReader reader = new(resourceStream); - string xmlContent = reader.ReadToEnd(); + var xmlContent = reader.ReadToEnd(); try { Locale.TryAdd(locale.Key, []); @@ -304,49 +358,62 @@ public static class MiraLocaleManager } } - private static void ParseXmlFile(string xmlContent, MiraLanguage language, bool loadingInternal) + /// + /// Parses the provided xml file into translation keys and values and registers them to the locale manager. + /// + /// The xml file being parsed. + /// The language that the xml file corresponds to. + /// Flag that indicates if the xml being parsed is internal or not. + public static void ParseXmlFile(string xmlContent, MiraLanguage language, bool loadingInternal) { var dict = Locale[language]; XmlDocument xmlDoc = new(); try { xmlDoc.LoadXml(xmlContent); - XmlNodeList? stringNodes = xmlDoc.SelectNodes("/resources/string"); + var stringNodes = xmlDoc.SelectNodes("/resources/string"); - if (stringNodes != null) + if (stringNodes == null) { - var total = 0; - foreach (XmlNode node in stringNodes) - { - if (node.Attributes?["name"] != null) - { - string name = node.Attributes["name"]!.Value; - string value = node.InnerText; - - if (string.IsNullOrEmpty(name)) continue; - - if (value.Contains('[')) - { - value = value.Replace("[", "<"); - } - - if (value.Contains(']')) - { - value = value.Replace("]", ">"); - } - - value = value.Replace("", "\n").Replace("", "&"); - - if (loadingInternal && Locale[language].ContainsKey(name)) - { - Error($"String for \"{name}\" in {language} was overwritten by duplicate!"); - } - dict[name] = value; - total++; - } - } - Info($"Loaded {language.ToDisplayString()} translation with ({total} keys)"); + return; } + + var total = 0; + + foreach (XmlNode node in stringNodes) + { + if (node.Attributes?["name"] == null) + { + continue; + } + + var name = node.Attributes["name"]!.Value; + var value = node.InnerText; + + if (string.IsNullOrEmpty(name)) continue; + + if (value.Contains('[')) + { + value = value.Replace('[', '<'); + } + + if (value.Contains(']')) + { + value = value.Replace(']', '>'); + } + + value = value.Replace("", "\n").Replace("", "&"); + + if (loadingInternal && Locale[language].ContainsKey(name)) + { + Error($"String for \"{name}\" in {language} was overwritten by duplicate!"); + } + + dict[name] = value; + total++; + } + + Info($"Loaded {language.ToDisplayString()} translation with ({total} keys)"); } catch (XmlException ex) { diff --git a/MiraAPI/Translation/MiraLocalizationProvider.cs b/MiraAPI/Translation/MiraLocalizationProvider.cs index 3ac1133..4e757a0 100644 --- a/MiraAPI/Translation/MiraLocalizationProvider.cs +++ b/MiraAPI/Translation/MiraLocalizationProvider.cs @@ -7,12 +7,17 @@ using Reactor.Utilities; namespace MiraAPI.Translation; +/// +/// Provides customized localization support, integrating MiraAPI translations into Reactor's localization system. +/// public class MiraLocalizationProvider : LocalizationProvider { + /// public override int Priority => ReactorPriority.Normal; private static LocalizationProvider? _reactorProvider; private static bool _loadedStrings; + /// public override bool TryGetText(StringNames stringName, out string? result) { if (MiraLocaleManager.StringNamesLookup.TryGetValue(stringName, out var key)) @@ -20,10 +25,12 @@ public class MiraLocalizationProvider : LocalizationProvider 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; @@ -32,6 +39,7 @@ public class MiraLocalizationProvider : LocalizationProvider return true; } + /// public override void OnLanguageChanged(SupportedLangs newLanguage) { _reactorProvider ??= LocalizationManager.Providers.First(x => x is HardCodedLocalizationProvider); @@ -40,6 +48,7 @@ public class MiraLocalizationProvider : LocalizationProvider { MiraApiPlugin.Culture = new(culture); } + if (!_loadedStrings) { MiraLocaleManager.LoadExternalLocale(); @@ -55,6 +64,7 @@ public class MiraLocalizationProvider : LocalizationProvider tab.Key.text = $"{LocalSettingsTab.GetShortName(tab.Value.Translate())}"; } + /*Warning($""); Warning($""); foreach (var stringName in TranslationController.Instance.currentLanguage.AllStrings) diff --git a/MiraAPI/Translation/TranslationExtensions.cs b/MiraAPI/Translation/TranslationExtensions.cs index 5f0edbd..8b45b48 100644 --- a/MiraAPI/Translation/TranslationExtensions.cs +++ b/MiraAPI/Translation/TranslationExtensions.cs @@ -8,6 +8,8 @@ public static class TranslationExtensions /// /// Translates a key to the current language. /// + /// The translation key. + /// The corresponding translation. public static string Translate(this string key) { return MiraLocaleManager.Get(key); diff --git a/MiraAPI/Utilities/Assets/AddressablesLoader.cs b/MiraAPI/Utilities/Assets/AddressablesLoader.cs index 7acf73d..fee0a72 100644 --- a/MiraAPI/Utilities/Assets/AddressablesLoader.cs +++ b/MiraAPI/Utilities/Assets/AddressablesLoader.cs @@ -1,48 +1,39 @@ -using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Patches.Menu; -using Reactor.Utilities; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using UnityEngine.AddressableAssets; -using UnityEngine.ResourceManagement.AsyncOperations; +using System; +using System.Diagnostics.CodeAnalysis; namespace MiraAPI.Utilities.Assets; /// /// The component for loading Addressables, and cosmetics. /// +[Obsolete("This system has been deprecated in favour of CorsacCosmetics. This will be removed in a future version.", true)] +[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Part of obsolete code.")] public static class AddressablesLoader { - private static bool _isInitialized; - - private static readonly List<(string Location, string ProviderSuffix)> CatalogLocations = []; - private static readonly List LoadedLocations = []; - - private static readonly List RegisteredHatKeys = []; - private static readonly List<(string, string)> RegisteredVisorKeys = []; - private static readonly List<(string, string)> RegisteredNameplateKeys = []; - private static readonly List RegisteredSkinKeys = []; - /// /// Gets a value indicating whether hats have been loaded by the addressables system. /// - public static bool AddressableHatsExist { get; private set; } + public static bool AddressableHatsExist => false; /// /// Gets a value indicating whether visors have been loaded by the addressables system. /// - public static bool AddressableVisorsExist { get; private set; } + public static bool AddressableVisorsExist => false; /// /// Gets a value indicating whether nameplates have been loaded by the addressables system. /// - public static bool AddressableNameplatesExist { get; private set; } + public static bool AddressableNameplatesExist => false; /// /// Gets a value indicating whether skins have been loaded by the addressables system. /// - public static bool AddressableSkinsExist { get; private set; } + public static bool AddressableSkinsExist => false; + + /// + /// Gets a value indicating whether catalogs have been loaded by the addressables system. + /// + public static bool CatalogsExist => false; /// /// Registers a specific addressables package to load asynchronously at the start of the game, when possible. @@ -51,13 +42,7 @@ public static class AddressablesLoader /// The suffix of the provider for an addressables package. public static void RegisterCatalog(string location, string providerSuffix = "") { - if (_isInitialized) - { - Error("AddressablesLoader has already been initialized, cannot register more catalogs."); - return; - } - - CatalogLocations.Add((location, providerSuffix)); + Warning("The addressables system has been deprecated. None of the registered assets will be added."); } /// @@ -66,14 +51,7 @@ public static class AddressablesLoader /// The key/label/group for a List . public static void RegisterHats(string addressablesKey) { - if (_isInitialized) - { - Error("AddressablesLoader has already been initialized, cannot register more keys for hats."); - return; - } - - AddressableHatsExist = true; - RegisteredHatKeys.Add(addressablesKey); + Warning("The addressables system has been deprecated. None of the registered assets will be added."); } /// @@ -82,14 +60,7 @@ public static class AddressablesLoader /// The key/label/group for a List . public static void RegisterSkins(string addressablesKey) { - if (_isInitialized) - { - Error("AddressablesLoader has already been initialized, cannot register more keys for skins."); - return; - } - - AddressableSkinsExist = true; - RegisteredSkinKeys.Add(addressablesKey); + Warning("The addressables system has been deprecated. None of the registered assets will be added."); } /// @@ -99,14 +70,7 @@ public static class AddressablesLoader /// /// The title of the group for visors. public static void RegisterNameplates(string addressablesKey, string groupTitle) { - if (_isInitialized) - { - Error("AddressablesLoader has already been initialized, cannot register more keys for nameplates."); - return; - } - - AddressableNameplatesExist = true; - RegisteredNameplateKeys.Add((addressablesKey, groupTitle)); + Warning("The addressables system has been deprecated. None of the registered assets will be added."); } /// @@ -116,135 +80,6 @@ public static class AddressablesLoader /// The title of the group for visors. public static void RegisterVisors(string addressablesKey, string groupTitle) { - if (_isInitialized) - { - Error("AddressablesLoader has already been initialized, cannot register more keys for visors."); - return; - } - - AddressableVisorsExist = true; - RegisteredVisorKeys.Add((addressablesKey, groupTitle)); - } - - internal static void LoadAll() - { - if (_isInitialized) - { - Error("AddressablesLoader has already been initialized."); - return; - } - - _isInitialized = true; - foreach (var (location, providerSuffix) in CatalogLocations) - { - Coroutines.Start(CoLoadAddressables(location, providerSuffix)); - } - - Coroutines.Start(LoadCosmetics()); - } - - internal static IEnumerator CoLoadAddressables(string location, string suffix = "") - { - while (!AmongUsClient.Instance) yield return null; - // Load the local/remote content catalog - var catalogOperation = Addressables.LoadContentCatalog(location, suffix); - yield return catalogOperation; - - // Check for errors - if (catalogOperation.Status != AsyncOperationStatus.Succeeded) - { - Error($"Failed to load catalog {location}."); - } - - Info($"Loaded addressables {location}."); - LoadedLocations.Add(location); - } - - [HideFromIl2Cpp] - internal static IEnumerator LoadCosmetics() - { - while (!AmongUsClient.Instance || CatalogLocations.Select(x=>x.Location).Any(x=>!LoadedLocations.Contains(x))) yield return null; - - var hatBehaviours = DiscoverData(RegisteredHatKeys); - hatBehaviours = hatBehaviours.OrderBy(x => x.StoreName).ToList(); - var skinBehaviours = DiscoverData(RegisteredSkinKeys); - skinBehaviours = skinBehaviours.OrderBy(x => x.StoreName).ToList(); - var namePlateBehaviours = DiscoverAndReportData(RegisteredNameplateKeys); - namePlateBehaviours = namePlateBehaviours.OrderBy(x => x.Category).ToList(); - var visorBehaviours = DiscoverAndReportData(RegisteredVisorKeys); - visorBehaviours = visorBehaviours.OrderBy(x => x.Category).ToList(); - - var hatData = new List(); - hatData.AddRange(HatManager.Instance.allHats); - hatData.ForEach(x => x.StoreName = "Vanilla"); - HatManager.Instance.allHats = PrepareArray(hatData, hatBehaviours); - - var skinData = new List(); - skinData.AddRange(HatManager.Instance.allSkins); - skinData.ForEach(x => x.StoreName = "Vanilla"); - HatManager.Instance.allSkins = PrepareArray(skinData, skinBehaviours); - - var visorData = new List(); - visorData.AddRange(HatManager.Instance.allVisors); - VisorsTabPatches.AddRange(visorBehaviours); - HatManager.Instance.allVisors = PrepareArray(visorData, visorBehaviours.Select(x=>x.Data).ToList()); - - var namePlateData = new List(); - namePlateData.AddRange(HatManager.Instance.allNamePlates); - NameplatesTabPatches.AddRange(namePlateBehaviours); - HatManager.Instance.allNamePlates = PrepareArray(namePlateData, namePlateBehaviours.Select(x => x.Data).ToList()); - } - - private static T[] PrepareArray(List data, List behaviours) where T : CosmeticData - { - var count = data.Count; - for (int i = 0; i < behaviours.Count; i++) - { - behaviours[i].displayOrder = count + i; - data.Add(behaviours[i]); - } - return data.ToArray(); - } - - private static List DiscoverData(List tags) - { - var behaviours = new List(); - - foreach (var tag in tags) - { - try - { - var allLocations = Addressables.LoadResourceLocationsAsync(tag).WaitForCompletion(); - var assets = Addressables.LoadAssetsAsync(allLocations, null, false).WaitForCompletion(); - var array = new Il2CppSystem.Collections.Generic.List(assets.Pointer); - behaviours.AddRange(array.ToArray()); - } - catch - { - Error($"Failed to find tag {tag}"); - } - } - return behaviours; - } - - private static List<(string Category, T Data)> DiscoverAndReportData(List<(string Tag, string Category)> tags) - { - var behaviours = new List<(string, T)>(); - - foreach (var tag in tags) - { - try - { - var allLocations = Addressables.LoadResourceLocationsAsync(tag.Tag).WaitForCompletion(); - var assets = Addressables.LoadAssetsAsync(allLocations, null, false).WaitForCompletion(); - var array = new Il2CppSystem.Collections.Generic.List(assets.Pointer); - behaviours.AddRange(array.ToArray().Select(x => (tag.Category, x))); - } - catch - { - Error($"Failed to find tag {tag}"); - } - } - return behaviours; + Warning("The addressables system has been deprecated. None of the registered assets will be added."); } } diff --git a/MiraAPI/Utilities/Assets/AudioTools.cs b/MiraAPI/Utilities/Assets/AudioTools.cs new file mode 100644 index 0000000..075b65c --- /dev/null +++ b/MiraAPI/Utilities/Assets/AudioTools.cs @@ -0,0 +1,159 @@ +using System; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; +using System.Text; +using UnityEngine; + +namespace MiraAPI.Utilities.Assets; + +/// +/// A utility class for various audio-related operations. +/// +public static class AudioTools +{ + /// + /// Loads and returns an audio clip from a resource path using the specified . + /// + /// The path to the resource within the . + /// The from which to load the resource. + /// A object loaded from the specified resource path. + /// Attempted to load an Audio file in non WAV format. + /// Stream failed to load. Check if the name of your asset was correct. + public static AudioClip LoadAudioFromResources(string resourcePath, Assembly assembly) + { + using var assetStream = assembly.GetManifestResourceStream(resourcePath) + ?? throw new MissingManifestResourceException($"Stream failed to load. Check if the asset name is correct: {resourcePath}"); + + using var reader = new BinaryReader(assetStream); + + // Read RIFF header + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF") + { + throw new NotSupportedException("Invalid WAV file (missing RIFF header)."); + } + + reader.ReadInt32(); // File size (ignore) + + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE") + { + throw new NotSupportedException("Invalid WAV file (missing WAVE header)."); + } + + // Read chunks + int channels = 0, sampleRate = 0, bitsPerSample = 0; + byte[]? audioData = null; + + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4)); + var chunkSize = reader.ReadInt32(); + + // Format chunk + if (chunkId == "fmt ") + { + int audioFormat = reader.ReadInt16(); + channels = reader.ReadInt16(); + sampleRate = reader.ReadInt32(); + reader.ReadInt32(); // Byte rate (ignore) + reader.ReadInt16(); // Block align (ignore) + bitsPerSample = reader.ReadInt16(); + + if (audioFormat != 1) // Only PCM supported + throw new NotSupportedException("Only PCM WAV files are supported."); + } + + // Data chunk + else if (chunkId == "data") + { + audioData = reader.ReadBytes(chunkSize); + break; // No need to read further + } + else + { + reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); // Skip unknown chunks + } + } + + if (audioData == null) + { + throw new InvalidOperationException("WAV file does not contain audio data."); + } + + if (channels == 0 || sampleRate == 0 || bitsPerSample == 0) + { + throw new InvalidOperationException("WAV file does not contain valid format information."); + } + + // Convert PCM data to float array + var samples = ConvertPcmToFloat(audioData, bitsPerSample); + + // Create the AudioClip + var audioClip = AudioClip.Create(resourcePath, samples.Length / channels, channels, sampleRate, false); + audioClip.SetData(samples, 0); + + return audioClip; + } + + // This is somewhat magic to me, I just copied it from elsewhere. + // Could have been written better, so here - Az + private static float[] ConvertPcmToFloat(byte[] pcmData, int bitsPerSample) + { + var sampleCount = pcmData.Length / (bitsPerSample / 8); + var floatData = new float[sampleCount]; + + ReadOnlySpan source = pcmData; + Span destination = floatData; + + switch (bitsPerSample) + { + case 8: + for (var i = 0; i < sampleCount; i++) + { + destination[i] = (source[i] - 128) / 128f; // 8-bit PCM is unsigned + } + + break; + + case 16: + var shortSource = MemoryMarshal.Cast(source); + + for (var i = 0; i < sampleCount; i++) + { + destination[i] = shortSource[i] / 32768f; + } + + break; + + case 24: + for (int i = 0, byteIndex = 0; i < sampleCount; i++, byteIndex += 3) + { + var sample24 = source[byteIndex] | (source[byteIndex + 1] << 8) | (source[byteIndex + 2] << 16); + + // Convert from unsigned to signed + if (sample24 > 0x7FFFFF) + sample24 -= 0x1000000; + + destination[i] = sample24 / 8388608f; + } + + break; + + case 32: + var intSource = MemoryMarshal.Cast(source); + + for (var i = 0; i < sampleCount; i++) + { + destination[i] = intSource[i] / 2147483648f; + } + + break; + + default: + throw new NotSupportedException($"Unsupported bit depth: {bitsPerSample}"); + } + + return floatData; + } +} diff --git a/MiraAPI/Utilities/Assets/LoadableAddressableAsset.cs b/MiraAPI/Utilities/Assets/LoadableAddressableAsset.cs index 144f608..d72e77b 100644 --- a/MiraAPI/Utilities/Assets/LoadableAddressableAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableAddressableAsset.cs @@ -8,7 +8,8 @@ namespace MiraAPI.Utilities.Assets; /// /// The guid of the asset. /// The type of the asset to be loaded. -public class LoadableAddressableAsset(string uid) : LoadableAsset where T : UnityEngine.Object +public class LoadableAddressableAsset(string uid) : LoadableAsset + where T : UnityEngine.Object { private readonly Action>? _gcAction; @@ -18,7 +19,8 @@ public class LoadableAddressableAsset(string uid) : LoadableAsset where T /// /// The guid of the asset. /// A lambda that allows GCHandle.Alloc calls without garbage collection interferences. - public LoadableAddressableAsset(string uid, Action> garbageCollection) : this(uid) + public LoadableAddressableAsset(string uid, Action> garbageCollection) + : this(uid) { _gcAction = garbageCollection; } diff --git a/MiraAPI/Utilities/Assets/LoadableAddressableAssets.cs b/MiraAPI/Utilities/Assets/LoadableAddressableAssets.cs index c70100a..9e131bf 100644 --- a/MiraAPI/Utilities/Assets/LoadableAddressableAssets.cs +++ b/MiraAPI/Utilities/Assets/LoadableAddressableAssets.cs @@ -10,7 +10,8 @@ namespace MiraAPI.Utilities.Assets; /// A utility class for loading groups of assets from ALL addressable locations. /// /// The type of the asset to be loaded. -public class LoadableAddressableAssets(string key) where T : UnityEngine.Object +public class LoadableAddressableAssets(string key) + where T : UnityEngine.Object { private readonly Action>? _gcAction; @@ -25,7 +26,8 @@ public class LoadableAddressableAssets(string key) where T : UnityEngine.Obje /// /// The key of the assets. /// A lambda that allows GCHandle.Alloc calls without garbage collection interferences. - public LoadableAddressableAssets(string key, Action> garbageCollection) : this(key) + public LoadableAddressableAssets(string key, Action> garbageCollection) + : this(key) { _gcAction = garbageCollection; } @@ -53,7 +55,7 @@ public class LoadableAddressableAssets(string key) where T : UnityEngine.Obje throw new InvalidOperationException($"INVALID ASSET/s: {key}"); } - var assetsList = new Il2CppSystem.Collections.Generic.List(assetsIList.Pointer); + var assetsList = new CppCollections.List(assetsIList.Pointer); LoadedAssets = assetsList.ToArray().ToList(); _gcAction?.Invoke(this); diff --git a/MiraAPI/Utilities/Assets/LoadableAsset.cs b/MiraAPI/Utilities/Assets/LoadableAsset.cs index 7b0c032..28c7709 100644 --- a/MiraAPI/Utilities/Assets/LoadableAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableAsset.cs @@ -8,7 +8,8 @@ namespace MiraAPI.Utilities.Assets; /// You can create your own implementation of this class to load assets in different ways. /// /// The type of the asset to be loaded. -public abstract class LoadableAsset where T : UnityEngine.Object +public abstract class LoadableAsset + where T : UnityEngine.Object { /// /// Gets or sets reference to the loaded asset. Intended to be used for caching purposes. @@ -21,6 +22,15 @@ public abstract class LoadableAsset where T : UnityEngine.Object /// The loaded asset. public abstract T LoadAsset(); + /// + /// Converts the into its by loading the asset. + /// + /// The to get the asset from. + public static implicit operator T(LoadableAsset loadable) + { + return loadable.LoadAsset(); + } + /// /// Unloads an asset. /// diff --git a/MiraAPI/Utilities/Assets/LoadableAssetWrapper.cs b/MiraAPI/Utilities/Assets/LoadableAssetWrapper.cs index a368492..af73288 100644 --- a/MiraAPI/Utilities/Assets/LoadableAssetWrapper.cs +++ b/MiraAPI/Utilities/Assets/LoadableAssetWrapper.cs @@ -6,7 +6,8 @@ namespace MiraAPI.Utilities.Assets; /// Wrapper class to turn any into a . /// /// The type of the asset to be loaded. Must be a subclass of . -public class LoadableAssetWrapper : LoadableAsset where T : UnityEngine.Object +public class LoadableAssetWrapper : LoadableAsset + where T : UnityEngine.Object { /// /// Initializes a new instance of the class. diff --git a/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs b/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs index a9b8eb1..49529a5 100644 --- a/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs @@ -1,8 +1,4 @@ -using System; -using System.IO; using System.Reflection; -using System.Resources; -using System.Text; using UnityEngine; namespace MiraAPI.Utilities.Assets; @@ -15,117 +11,14 @@ public class LoadableAudioResourceAsset(string path) : LoadableAsset { private readonly Assembly _assembly = Assembly.GetCallingAssembly(); - /// - /// Loads the asset from embedded resources. - /// - /// The asset to load. - /// Attempted to load an Audio file in non WAV format. - /// Stream failed to load. Check if the name of your asset was correct. + /// public override AudioClip LoadAsset() { - using var assetStream = _assembly.GetManifestResourceStream(path) - ?? throw new MissingManifestResourceException($"Stream failed to load. Check if the asset name is correct: {path}"); - - using var reader = new BinaryReader(assetStream); - - // Read RIFF header - if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF") + if (!LoadedAsset) { - throw new NotSupportedException("Invalid WAV file (missing RIFF header)."); + LoadedAsset = AudioTools.LoadAudioFromResources(path, _assembly); } - reader.ReadInt32(); // File size (ignore) - - if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE") - { - throw new NotSupportedException("Invalid WAV file (missing WAVE header)."); - } - - // Read chunks - int channels = 0, sampleRate = 0, bitsPerSample = 0; - byte[]? audioData = null; - - while (reader.BaseStream.Position < reader.BaseStream.Length) - { - var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4)); - var chunkSize = reader.ReadInt32(); - - // Format chunk - if (chunkId == "fmt ") - { - int audioFormat = reader.ReadInt16(); - channels = reader.ReadInt16(); - sampleRate = reader.ReadInt32(); - reader.ReadInt32(); // Byte rate (ignore) - reader.ReadInt16(); // Block align (ignore) - bitsPerSample = reader.ReadInt16(); - - if (audioFormat != 1) // Only PCM supported - throw new NotSupportedException("Only PCM WAV files are supported."); - } - // Data chunk - else if (chunkId == "data") - { - audioData = reader.ReadBytes(chunkSize); - break; // No need to read further - } - else - { - reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); // Skip unknown chunks - } - } - - if (audioData == null) - { - throw new InvalidOperationException("WAV file does not contain audio data."); - } - - if (channels == 0 || sampleRate == 0 || bitsPerSample == 0) - { - throw new InvalidOperationException("WAV file does not contain valid format information."); - } - - // Convert PCM data to float array - var samples = ConvertPcmToFloat(audioData, bitsPerSample); - - // Create the AudioClip - var audioClip = AudioClip.Create(path, samples.Length / channels, channels, sampleRate, false); - audioClip.SetData(samples, 0); - - return audioClip; - } - - // This is somewhat magic to me, I just copied it from elsewhere. - private static float[] ConvertPcmToFloat(byte[] pcmData, int bitsPerSample) - { - var sampleCount = pcmData.Length / (bitsPerSample / 8); - var floatData = new float[sampleCount]; - - for (var i = 0; i < sampleCount; i++) - { - var byteIndex = i * (bitsPerSample / 8); - - switch (bitsPerSample) - { - case 8: - floatData[i] = (pcmData[byteIndex] - 128) / 128f; // 8-bit PCM is unsigned - break; - case 16: - floatData[i] = BitConverter.ToInt16(pcmData, byteIndex) / 32768f; - break; - case 24: - var sample24 = pcmData[byteIndex] | (pcmData[byteIndex + 1] << 8) | (pcmData[byteIndex + 2] << 16); - if (sample24 > 0x7FFFFF) sample24 -= 0x1000000; // Convert from unsigned to signed - floatData[i] = sample24 / 8388608f; - break; - case 32: - floatData[i] = BitConverter.ToInt32(pcmData, byteIndex) / 2147483648f; - break; - default: - throw new NotSupportedException($"Unsupported bit depth: {bitsPerSample}"); - } - } - - return floatData; + return LoadedAsset!; } } diff --git a/MiraAPI/Utilities/Assets/LoadableBundleAsset.cs b/MiraAPI/Utilities/Assets/LoadableBundleAsset.cs index 3476a9a..49aaf0d 100644 --- a/MiraAPI/Utilities/Assets/LoadableBundleAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableBundleAsset.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Reactor.Utilities.Extensions; using UnityEngine; @@ -10,13 +11,15 @@ namespace MiraAPI.Utilities.Assets; /// The name of the asset. /// The that contains the asset. /// The type of the asset to be loaded. -public class LoadableBundleAsset(string name, AssetBundle bundle) : LoadableAsset where T : UnityEngine.Object +public class LoadableBundleAsset(string name, AssetBundle bundle) : LoadableAsset + where T : UnityEngine.Object { /// /// Loads the asset from the . /// /// The asset. /// The asset did not load properly. + [SuppressMessage("Style", "IDE0029:Use coalesce expression", Justification = "Null coalescing bypasses Unity lifetime checks.")] public override T LoadAsset() { if (LoadedAsset != null) @@ -26,11 +29,6 @@ public class LoadableBundleAsset(string name, AssetBundle bundle) : LoadableA LoadedAsset = bundle.LoadAsset(name); - if (LoadedAsset == null) - { - throw new InvalidOperationException($"INVALID ASSET: {name}"); - } - - return LoadedAsset; + return LoadedAsset == null ? throw new InvalidOperationException($"INVALID ASSET: {name}") : LoadedAsset; } } diff --git a/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs b/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs index ef7aea3..1ff0558 100644 --- a/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs @@ -27,11 +27,12 @@ public class LoadableBundleSubAsset(string name, LoadableBundleSubAssetHolder as var loadedAsset = assetHolder.SubSprites.FirstOrDefault(x => x.name == name); - if (loadedAsset == null) + if (!loadedAsset) { throw new InvalidOperationException($"INVALID ASSETS: {name}"); } - LoadedAsset = loadedAsset; + + LoadedAsset = loadedAsset!; return LoadedAsset; } } diff --git a/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs index 3b84d8d..1507c42 100644 --- a/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs +++ b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs @@ -1,6 +1,5 @@ using System; -using System.Linq; -using HarmonyLib; +using System.Collections.Generic; using Il2CppInterop.Runtime; using Reactor.Utilities.Extensions; using UnityEngine; @@ -10,18 +9,14 @@ namespace MiraAPI.Utilities.Assets; /// /// A utility class for loading multiple assets from an . /// -public class LoadableBundleSubAssetHolder +/// The name of the assets to pull from. +/// The that contains the assets. +public class LoadableBundleSubAssetHolder(string[] names, AssetBundle bundle) { /// - /// Initializes a new instance of the class. + /// Gets the sprites contained within the asset. /// - /// The name of the assets to pull from. - /// The that contains the assets. - public LoadableBundleSubAssetHolder(string[] names, AssetBundle bundle) - { - Bundle = bundle; - SpriteNames = names; - } + public Sprite[] SubSprites { get; private set; } = []; /// /// Initializes a new instance of the class. @@ -29,40 +24,40 @@ public class LoadableBundleSubAssetHolder /// The name of the asset. /// The that contains the assets. public LoadableBundleSubAssetHolder(string name, AssetBundle bundle) + : this([name], bundle) { - Bundle = bundle; - SpriteNames = [name]; } - internal string[] SpriteNames = []; - internal AssetBundle Bundle; - public Sprite[] SubSprites = []; + /// + /// Attempts to load all of the sprite assets within the sprite sheet. + /// + /// Thrown if the asset is not actually a sprite sheet. public void TryInit() { - if (SubSprites.Length == 0) + if (SubSprites.Length != 0) + return; + + var newSprites = new List(); + var type = Il2CppType.From(typeof(Sprite)); + + foreach (var name in names) { - var newSprites = Array.Empty(); - foreach (var name in SpriteNames) + var loadedAssets = bundle.LoadAssetWithSubAssets(name, type) + ?? throw new InvalidOperationException($"INVALID ASSETS: {name}"); + + foreach (var obj in loadedAssets) { - var loadedAssets = Bundle.LoadAssetWithSubAssets(name, Il2CppType.From(typeof(Sprite))).ToArray(); + var img = obj.TryCast(); - if (loadedAssets == null) - { - throw new InvalidOperationException($"INVALID ASSETS: {name}"); - } - foreach (var obj in loadedAssets) - { - var img = obj.TryCast(); - if (img != null) - { - img.DontDestroy().DontUnload(); - newSprites = newSprites.AddToArray(img); - } - } + if (img == null) + continue; + + img.DontDestroy().DontUnload(); + newSprites.Add(img); } - - SubSprites = newSprites; } + + SubSprites = [.. newSprites]; } /// @@ -80,6 +75,7 @@ public class LoadableBundleSubAssetHolder { sprite.DestroyImmediate(); } + SubSprites = []; return true; } diff --git a/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs b/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs index 5678997..4fffd2f 100644 --- a/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs @@ -8,18 +8,18 @@ namespace MiraAPI.Utilities.Assets; /// /// The path to the embedded resource. /// The pixels per unit for the loaded sprite. -public class LoadableResourceAsset(string path, float pixelsPerUnit=100f) : LoadableAsset +public class LoadableResourceAsset(string path, float pixelsPerUnit = 100f) : LoadableAsset { private readonly Assembly _assembly = Assembly.GetCallingAssembly(); /// public override Sprite LoadAsset() { - if (LoadedAsset) + if (!LoadedAsset) { - return LoadedAsset!; + LoadedAsset = SpriteTools.LoadSpriteFromPath(path, _assembly, pixelsPerUnit); } - return LoadedAsset = SpriteTools.LoadSpriteFromPath(path, _assembly, pixelsPerUnit); + return LoadedAsset!; } } diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index bec994a..96ac078 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -19,7 +19,7 @@ public static class MiraAssets new Vector2(0.5f, 0.5f), 100f, 0U, - SpriteMeshType.Tight, + SpriteMeshType.FullRect, new Vector4(20, 20, 20, 20)); RoundedBox = new LoadableAssetWrapper(boxSprite); @@ -160,5 +160,220 @@ public static class MiraAssets /// public static LoadableResourceAsset ChatOpenSprite { get; } = new("MiraAPI.Resources.NormalChatOpen.png"); + /// + /// Gets the sprite used for players button while hovering. + /// + public static LoadableResourceAsset WikiPlayersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonHover.png"); + + /// + /// Gets the sprite used for the players button while hovering. + /// + public static LoadableResourceAsset WikiPlayersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonIdle.png"); + + /// + /// Gets the sprite used for the players button while hovering. + /// + public static LoadableResourceAsset WikiPlayersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonOpen.png"); + + /// + /// Gets the sprite used for the settings button while hovering. + /// + public static LoadableResourceAsset WikiSettingsButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonHover.png"); + + /// + /// Gets the sprite used for the settings button while hovering. + /// + public static LoadableResourceAsset WikiSettingsButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonIdle.png"); + + /// + /// Gets the sprite used for the settings button while hovering. + /// + public static LoadableResourceAsset WikiSettingsButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonOpen.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset WikiRolesButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset WikiRolesButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset WikiRolesButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonOpen.png"); + + /// + /// Gets the sprite used for the modifiers button while hovering. + /// + public static LoadableResourceAsset WikiModifiersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonHover.png"); + + /// + /// Gets the sprite used for the modifiers button while hovering. + /// + public static LoadableResourceAsset WikiModifiersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonIdle.png"); + + /// + /// Gets the sprite used for the modifiers button while hovering. + /// + public static LoadableResourceAsset WikiModifiersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonOpen.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupCountHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupCountIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupFactionHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupFactionIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupNoneHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupNoneIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SearchClearHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SearchClearIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SearchIconHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SearchIconIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SortingAzHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SortingAzIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SortingZaHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SortingZaIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleAllHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleAllIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleOffHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleOffIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleOnHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleOnIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnIdle.png"); + + /// + /// Gets the sprite used for crewmates. + /// + public static LoadableResourceAsset IconCrewmate { get; } = new("MiraAPI.Resources.Wiki.IconCrewmate.png"); + + /// + /// Gets the sprite used for impostors. + /// + public static LoadableResourceAsset IconImpostor { get; } = new("MiraAPI.Resources.Wiki.IconImpostor.png"); + + /// + /// Gets the sprite used for neutrals. + /// + public static LoadableResourceAsset IconNeutral { get; } = new("MiraAPI.Resources.Wiki.IconNeutral.png"); + + /// + /// Gets the sprite used for the player list. + /// + public static LoadableResourceAsset IconPlayerList { get; } = new("MiraAPI.Resources.Wiki.IconPlayerList.png"); + + /// + /// Gets the sprite used for settings. + /// + public static LoadableResourceAsset IconSettings { get; } = new("MiraAPI.Resources.Wiki.IconSettings.png"); + + /// + /// Gets the sprite used for terminology. + /// + public static LoadableResourceAsset IconTerminology { get; } = new("MiraAPI.Resources.Wiki.IconTerminology.png"); + + /// + /// Gets the Page switching sprite. + /// + public static LoadableResourceAsset PageLeftHover { get; } = new("MiraAPI.Resources.Wiki.PageLeftHover.png"); + + /// + /// Gets the Page switching sprite. + /// + public static LoadableResourceAsset PageLeftIdle { get; } = new("MiraAPI.Resources.Wiki.PageLeftIdle.png"); + + /// + /// Gets the Page switching sprite. + /// + public static LoadableResourceAsset PageRightHover { get; } = new("MiraAPI.Resources.Wiki.PageRightHover.png"); + + /// + /// Gets the Page switching sprite. + /// + public static LoadableResourceAsset PageRightIdle { get; } = new("MiraAPI.Resources.Wiki.PageRightIdle.png"); + + /// + /// Gets the sprite used below the tab icons in the wiki. + /// + public static LoadableResourceAsset TabButtonContainer { get; } = new("MiraAPI.Resources.Wiki.TabButtonContainer.png"); + internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png"); } diff --git a/MiraAPI/Utilities/Assets/PreloadedAsset.cs b/MiraAPI/Utilities/Assets/PreloadedAsset.cs index cf4ef7c..fe380f8 100644 --- a/MiraAPI/Utilities/Assets/PreloadedAsset.cs +++ b/MiraAPI/Utilities/Assets/PreloadedAsset.cs @@ -7,7 +7,8 @@ namespace MiraAPI.Utilities.Assets; /// /// /// -public class PreloadedAsset(T asset) : LoadableAsset where T : UnityEngine.Object +public class PreloadedAsset(T asset) : LoadableAsset + where T : UnityEngine.Object { private readonly T _loadedAsset = asset ?? throw new ArgumentNullException(nameof(asset)); diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs index b0d4a1e..05ebcfc 100644 --- a/MiraAPI/Utilities/Assets/SpriteTools.cs +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -48,7 +48,13 @@ public static class SpriteTools public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit) { var tex = LoadTextureFromResourcePath(resourcePath, assembly); - var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + var sprite = Sprite.Create( + tex, + new Rect(0, 0, tex.width, tex.height), + new Vector2(0.5f, 0.5f), + pixelsPerUnit, + 0, + SpriteMeshType.FullRect); sprite.name = resourcePath; return sprite; } diff --git a/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs b/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs index 6160a01..f1f3dfb 100644 --- a/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs +++ b/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs @@ -1,18 +1,43 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; namespace MiraAPI.Utilities.Assets; + +/// +/// Utility class for dynamically generating and managing TextMeshPro sprite assets. +/// public static class TmpSpriteUtils { + private static readonly Dictionary LoadedSprites = []; private static Shader _spriteShader; - private static Dictionary LoadedSprites = []; - public static TMP_SpriteAsset AssetHolder; + + [SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "Internal behaviour that does not need property-level validation.")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] + internal static TMP_SpriteAsset AssetHolder; + + /// + /// Creates and caches a from a given Unity . + /// + /// The Unity sprite to convert into a TMP sprite asset. + /// The unique name for the sprite asset, used for caching and inline rich text tags. + /// The scale applied to the sprite. Defaults to 1. + /// The generated or previously cached . public static TMP_SpriteAsset CreateSpriteAsset(Sprite sprite, string assetName, float scale = 1) { return CreateSpriteAsset(sprite.texture, sprite.rect, assetName, scale); } + + /// + /// Creates and caches a from a source texture and a specific region. + /// + /// The source containing the sprite. + /// The specific region of the texture to use. + /// The unique name for the sprite asset, used for caching and inline rich text tags. + /// The scale applied to the sprite. Defaults to 1. + /// The generated or previously cached . public static TMP_SpriteAsset CreateSpriteAsset(Texture2D sourceTexture, Rect rect, string assetName, float scale = 1) { if (LoadedSprites.TryGetValue(assetName, out var existingAsset)) @@ -26,12 +51,18 @@ public static class TmpSpriteUtils AssetHolder.DontUnload().DontDestroy(); AssetHolder.fallbackSpriteAssets = new(); } - TMP_SpriteAsset spriteAsset = ScriptableObject.CreateInstance(); + + var spriteAsset = ScriptableObject.CreateInstance(); spriteAsset.name = assetName; spriteAsset.spriteSheet = sourceTexture; + var spriteAssetMasked = ScriptableObject.CreateInstance(); + spriteAssetMasked.name = assetName + ".Masked"; + spriteAssetMasked.spriteSheet = sourceTexture; + if (!_spriteShader) { + // ReSharper disable once ShaderLabShaderReferenceNotResolved (Justification: Unnecessary.) _spriteShader = Shader.Find("TextMeshPro/Sprite"); } @@ -40,7 +71,6 @@ public static class TmpSpriteUtils name = assetName + " Material", }; material.SetTexture(ShaderUtilities.ID_MainTex, sourceTexture); - // TODO: Using these values, the icons will NOT clip through chatboxes. However, this breaks quite literally every other tmp text that isn't masked. Please fix this if a good solution is found. /*material.SetFloat(ShaderUtilities.ID_StencilComp, 4); material.SetFloat(ShaderUtilities.ID_StencilID, 1);*/ material.SetFloat(ShaderUtilities.ID_StencilComp, 0); @@ -51,23 +81,51 @@ public static class TmpSpriteUtils material.SetFloat(ShaderUtilities.ShaderTag_CullMode, 0); spriteAsset.material = material; - spriteAsset.spriteInfoList = new Il2CppSystem.Collections.Generic.List(); + spriteAsset.spriteInfoList = new CppCollections.List(); + + var maskedMaterial = new Material(_spriteShader) + { + name = assetName + " Masked Material", + }; + maskedMaterial.SetTexture(ShaderUtilities.ID_MainTex, sourceTexture); + maskedMaterial.SetFloat(ShaderUtilities.ID_StencilComp, 4); + maskedMaterial.SetFloat(ShaderUtilities.ID_StencilID, 1); + maskedMaterial.SetFloat(ShaderUtilities.ID_StencilOp, 0); + maskedMaterial.SetFloat(ShaderUtilities.ID_StencilWriteMask, 255); + maskedMaterial.SetFloat(ShaderUtilities.ID_StencilReadMask, 255); + maskedMaterial.SetFloat(ShaderUtilities.ShaderTag_CullMode, 0); + spriteAssetMasked.material = maskedMaterial; + + spriteAssetMasked.spriteInfoList = new CppCollections.List(); AddSpriteToAsset(spriteAsset, rect, assetName, scale); + AddSpriteToAsset(spriteAssetMasked, rect, assetName + ".Masked", scale); spriteAsset.DontUnload().DontDestroy(); spriteAsset.UpdateLookupTables(); spriteAsset.fallbackSpriteAssets = new(); + spriteAssetMasked.DontUnload().DontDestroy(); + spriteAssetMasked.UpdateLookupTables(); + spriteAssetMasked.fallbackSpriteAssets = new(); AssetHolder.fallbackSpriteAssets.Add(spriteAsset); + AssetHolder.fallbackSpriteAssets.Add(spriteAssetMasked); AssetHolder.UpdateLookupTables(); LoadedSprites.Add(assetName, spriteAsset); + LoadedSprites.Add(assetName + ".Masked", spriteAssetMasked); return spriteAsset; } + /// + /// Constructs a with calculated offsets and adds it to the provided . + /// + /// The target sprite asset to add the sprite information to. + /// The texture rect dimensions used to calculate width, height, and offsets. + /// The name assigned to the sprite, used to generate its hash code. + /// The visual scale applied to the sprite rendering. private static void AddSpriteToAsset(TMP_SpriteAsset spriteAsset, Rect rect, string spriteName, float scale) { - TMP_Sprite newSprite = new TMP_Sprite + var newSprite = new TMP_Sprite { name = spriteName, hashCode = TMP_TextUtilities.GetSimpleHashCode(spriteName), diff --git a/MiraAPI/Utilities/ColorExtensions.cs b/MiraAPI/Utilities/ColorExtensions.cs index 0bf4ebf..b077770 100644 --- a/MiraAPI/Utilities/ColorExtensions.cs +++ b/MiraAPI/Utilities/ColorExtensions.cs @@ -14,10 +14,19 @@ public static class ColorExtensions /// Luminance value between 0 and 1. public static float GetRelativeLuminance(this Color color) { - var r = color.r <= 0.03928f ? color.r / 12.92f : Mathf.Pow((color.r + 0.055f) / 1.055f, 2.4f); - var g = color.g <= 0.03928f ? color.g / 12.92f : Mathf.Pow((color.g + 0.055f) / 1.055f, 2.4f); - var b = color.b <= 0.03928f ? color.b / 12.92f : Mathf.Pow((color.b + 0.055f) / 1.055f, 2.4f); - return (0.2126f * r) + (0.7152f * g) + (0.0722f * b); + return (0.2126f * Linearize(color.r)) + + (0.7152f * Linearize(color.g)) + + (0.0722f * Linearize(color.b)); + } + + /// + /// Linearizes an sRGB component into linear space. + /// + /// The component to linearize. + /// The linearized result. + public static float Linearize(float c) + { + return c <= 0.03928f ? c / 12.92f : Mathf.Pow((c + 0.055f) / 1.055f, 2.4f); } /// @@ -84,6 +93,7 @@ public static class ColorExtensions low = t; } } + return bestColor; } } diff --git a/MiraAPI/Utilities/Components/CenteredGridArrange.cs b/MiraAPI/Utilities/Components/CenteredGridArrange.cs index a8d4ccd..6035f1c 100644 --- a/MiraAPI/Utilities/Components/CenteredGridArrange.cs +++ b/MiraAPI/Utilities/Components/CenteredGridArrange.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Reactor.Utilities.Attributes; using UnityEngine; @@ -11,76 +12,80 @@ namespace MiraAPI.Utilities.Components; /// /// The for the component. [RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity Convention.")] public class CenteredGridArrange(IntPtr iPtr) : MonoBehaviour(iPtr) { /// /// Gets or sets the cell size, which is used for spacing. /// - public Vector2 CellSize; + public Vector2 CellSize { get; set; } /// /// Gets or sets the maximum amount of columns. /// - public int MaxColumns = 6; + public int MaxColumns { get; set; } = 6; private List cells; - private static List currentChildren = new List(); + private static readonly List CurrentChildren = []; private void Start() { - cells = new List(); - GetChildsActive(); + cells = []; + GetChildrenActive(); CheckCurrentChildren(); } - private void FixedUpdate() => CheckCurrentChildren(); + private void FixedUpdate() + { + CheckCurrentChildren(); + } private void CheckCurrentChildren() { - GetChildsActive(); - if (cells.SequenceEqual(currentChildren)) + GetChildrenActive(); + if (cells.SequenceEqual(CurrentChildren)) return; cells.Clear(); - foreach (Transform currentChild in currentChildren) + foreach (var currentChild in CurrentChildren) cells.Add(currentChild); - ArrangeChilds(); + ArrangeChildren(); } - private void GetChildsActive() + private void GetChildrenActive() { - currentChildren.Clear(); + CurrentChildren.Clear(); foreach (var obj in transform) { var child = obj.TryCast(); if (child == null) continue; if (child.gameObject.activeSelf) - currentChildren.Add(child); + CurrentChildren.Add(child); } } - private void ArrangeChilds() + private void ArrangeChildren() { 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; + var totalRows = Mathf.CeilToInt((float)cells.Count / MaxColumns); + var totalHeight = (totalRows - 1) * CellSize.y; + var startY = transform.position.y + totalHeight * 0.5f; - for (int index = 0; index < cells.Count; ++index) + for (var 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; + var row = index / MaxColumns; + var rowStartIndex = row * MaxColumns; + var itemsInRow = Mathf.Min(MaxColumns, cells.Count - rowStartIndex); + var col = index - rowStartIndex; - float rowWidth = (itemsInRow - 1) * CellSize.x; - float startX = transform.position.x - rowWidth * 0.5f; + var rowWidth = (itemsInRow - 1) * CellSize.x; + var startX = transform.position.x - rowWidth * 0.5f; - float x = startX + col * CellSize.x; - float y = startY - row * CellSize.y; + var x = startX + col * CellSize.x; + var y = startY - row * CellSize.y; - Transform cell = cells[index]; + var cell = cells[index]; cell.position = new Vector3(x, y, cell.position.z); } } diff --git a/MiraAPI/Utilities/ControllableComparer.cs b/MiraAPI/Utilities/ControllableComparer.cs index 8c7806c..5e7ac70 100644 --- a/MiraAPI/Utilities/ControllableComparer.cs +++ b/MiraAPI/Utilities/ControllableComparer.cs @@ -4,34 +4,27 @@ using System.Linq; namespace MiraAPI.Utilities; -internal sealed class ControllableComparer : IComparer where T : IComparable +internal sealed class ControllableComparer(T[] forcedToBottom, T[] forcedToTop, IComparer fallbackComparer) : IComparer + where T : IComparable { - private readonly T[] _forcedToBottom; - private readonly T[] _forcedToTop; - private readonly IComparer _fallbackComparer; - public ControllableComparer(T[] forcedToBottom, T[] forcedToTop, IComparer fallbackComparer) - { - _forcedToBottom = forcedToBottom; - _forcedToTop = forcedToTop; - _fallbackComparer = fallbackComparer; - } - /// public int Compare(T? x, T? y) { - if ((_forcedToBottom.Contains(x) && _forcedToBottom.Contains(y)) || (_forcedToTop.Contains(x) && _forcedToTop.Contains(y))) - return _fallbackComparer.Compare(x, y); + if ((forcedToBottom.Contains(x) && forcedToBottom.Contains(y)) || (forcedToTop.Contains(x) && forcedToTop.Contains(y))) + return fallbackComparer.Compare(x, y); - if (_forcedToBottom.Contains(x)) - return 1; - if (_forcedToBottom.Contains(y)) - return -1; - - if (_forcedToTop.Contains(x)) - return -1; - if (_forcedToTop.Contains(y)) + if (forcedToBottom.Contains(x)) return 1; - return _fallbackComparer.Compare(x, y); + if (forcedToBottom.Contains(y)) + return -1; + + if (forcedToTop.Contains(x)) + return -1; + + if (forcedToTop.Contains(y)) + return 1; + + return fallbackComparer.Compare(x, y); } } diff --git a/MiraAPI/Utilities/DelegateUtilities.cs b/MiraAPI/Utilities/DelegateUtilities.cs new file mode 100644 index 0000000..2ac4acf --- /dev/null +++ b/MiraAPI/Utilities/DelegateUtilities.cs @@ -0,0 +1,19 @@ +using System; + +namespace MiraAPI.Utilities; + +/// +/// A utility class for using delegates. +/// +public static class DelegateUtilities +{ + /// + /// Gets a Func<bool> that always returns true. + /// + public static Func AlwaysTrue { get; } = () => true; + + /// + /// Gets a Func<bool> that always returns false. + /// + public static Func AlwaysFalse { get; } = () => false; +} diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index c31b9e5..21112f6 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -42,9 +43,7 @@ public static class Extensions chance = 0; } - return new NetData( - RoleId.Get(role.GetType()), - BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); + return new NetData(RoleId.Get(role), BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } /// @@ -53,39 +52,26 @@ public static class Extensions /// The . public static void BaseClose(this Minigame self) { - bool isComplete; if (self.amClosing == Minigame.CloseState.Closing) { self.gameObject.Destroy(); return; } - if (self.CloseSound && Constants.ShouldPlaySfx()) - { - SoundManager.Instance.PlaySound(self.CloseSound, false, 1f, null); - } - if (PlayerControl.LocalPlayer.Data.Role.TeamType == RoleTeamTypes.Crewmate) - { - GameManager.Instance.LogicMinigame.OnMinigameClose(); - } - if (PlayerControl.LocalPlayer) - { - PlayerControl.HideCursorTemporarily(); - } + + if (self.CloseSound && Constants.ShouldPlaySfx()) SoundManager.Instance.PlaySound(self.CloseSound, false); + + if (PlayerControl.LocalPlayer.Data.Role.TeamType == RoleTeamTypes.Crewmate) GameManager.Instance.LogicMinigame.OnMinigameClose(); + + if (PlayerControl.LocalPlayer) PlayerControl.HideCursorTemporarily(); + self.amClosing = Minigame.CloseState.Closing; self.logger.Info(string.Concat("Closing minigame ", self.GetType().Name)); - IAnalyticsReporter analytics = DebugAnalytics.Instance.Analytics; - NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data; - TaskTypes taskType = self.TaskType; - float realtimeSinceStartup = Time.realtimeSinceStartup - self.timeOpened; - PlayerTask myTask = self.MyTask; - if (myTask != null) - { - isComplete = myTask.IsComplete; - } - else - { - isComplete = false; - } + var analytics = DebugAnalytics.Instance.Analytics; + var data = PlayerControl.LocalPlayer.Data; + var taskType = self.TaskType; + var realtimeSinceStartup = Time.realtimeSinceStartup - self.timeOpened; + var myTask = self.MyTask; + var isComplete = myTask != null && myTask.IsComplete; analytics.MinigameClosed(data, taskType, realtimeSinceStartup, isComplete); self.StartCoroutine(self.CoDestroySelf()); } @@ -108,6 +94,7 @@ public static class Extensions button.cooldownTimerText.gameObject.SetActive(true); return; } + button.cooldownTimerText.gameObject.SetActive(false); } @@ -132,32 +119,22 @@ public static class Extensions { button.graphic.transform.localPosition = button.position; } + button.SetCooldownFill(num); } /// /// Returns a random element from the specified sequence. /// - /// - /// The sequence to select an element from. - /// - /// - /// The type of elements in the sequence. - /// - /// - /// A randomly selected element from . - /// - /// - /// Thrown when is empty. - /// + /// The sequence to select an element from. + /// The type of elements in the sequence. + /// A randomly selected element from . + /// Thrown when is empty. public static T RandomSnapshot(this IEnumerable input) { var list = input.ToList(); - if (list.Count == 0) - throw new InvalidOperationException("Cannot get random element from an empty collection."); - - return list[UnityEngine.Random.Range(0, list.Count)]; + return list.Count == 0 ? throw new InvalidOperationException("Cannot get random element from an empty collection.") : list[UnityEngine.Random.Range(0, list.Count)]; } /// @@ -165,14 +142,20 @@ public static class Extensions /// /// The . /// The player's . - public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.PlayerId)?.Object; + public static PlayerControl? GetPlayer(this PlayerVoteArea state) + { + return GameData.Instance.GetPlayerById(state.PlayerId)?.Object; + } /// /// Gets an representing the amount of tasks a player has left. /// /// The player. /// A count of how many tasks the player has left. - public static int GetTasksLeft(this PlayerControl player) => player.Data.Tasks.ToArray().Count(x => !x.Complete); + public static int GetTasksLeft(this PlayerControl player) + { + return player.Data.Tasks.ToArray().Count(x => !x.Complete); + } /// /// Checks if a is the game's host. @@ -195,19 +178,17 @@ public static class Extensions } /// - /// Used to convert a to . + /// Used to convert a to . /// /// The . /// The type in the list. /// The converted list. - public static Il2CppSystem.Collections.Generic.List ToIl2CppList(this List systemList) + public static CppCollections.List ToIl2CppList(this List systemList) { - var il2cppList = new Il2CppSystem.Collections.Generic.List(); + // ReSharper disable once InconsistentNaming (Justification: Acronym.) + var il2cppList = new CppCollections.List(); - foreach (var item in systemList) - { - il2cppList.Add(item); - } + foreach (var item in systemList) il2cppList.Add(item); return il2cppList; } @@ -227,9 +208,12 @@ public static class Extensions /// /// The to destroy. /// Whether to run the garbage collector immediately. + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Unclear, but please shut up.")] public static void DeepDestroy(this GameObject obj, bool clearGc = true) { obj.Destroy(); + + // Nuke(obj, clearGc); } /// @@ -242,7 +226,7 @@ public static class Extensions public static IEnumerator CoLoopWithBudget(this IEnumerable collection, Action action) { var fps = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60; - long budget = 1 / (fps * 2); // default is half of current frame time. + var budget = 1000L / (fps * 2); // default is half of current frame time. yield return collection.CoLoopWithBudget(budget, action); } @@ -270,7 +254,14 @@ public static class Extensions } } + // 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.")] +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppressions.) + // ReSharper disable once UnusedParameter.Local (Justification: Read above.) + // ReSharper disable once UnusedMember.Local (Justification: Read above.) private static IEnumerator Nuke(GameObject? go, bool clearGc) +#pragma warning restore SA1515 // Single-line comment should be preceded by blank line { if (go == null) yield break; @@ -360,35 +351,32 @@ public static class Extensions /// /// Clears up the Garbage Collector manually if necessary. /// + [SuppressMessage("Critical Code Smell", "S1215:\"GC.Collect\" should not be called", Justification = "Usage of GC.Collect is intentionally brought on by the user.")] public static void ClearGarbageCollector() { Resources.UnloadUnusedAssets(); - Il2CppSystem.GC.Collect(); - GC.Collect(); + /*Il2CppSystem.GC.Collect(); + 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; - } + 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; - } + for (var i = 0; i < go.transform.childCount; i++) yield return go.transform.GetChild(i).gameObject; } /// @@ -399,22 +387,12 @@ public static class Extensions public static void SetSizeLimit(this SpriteRenderer sprite, float pixelSize) { sprite.drawMode = SpriteDrawMode.Sliced; - if (!sprite.sprite) - { - return; - } + if (!sprite.sprite) return; - float spriteWidth = sprite.sprite.rect.width; - float spriteHeight = sprite.sprite.rect.height; + var spriteWidth = sprite.sprite.rect.width; + var spriteHeight = sprite.sprite.rect.height; - if (spriteWidth < spriteHeight) - { - sprite.size = new Vector2(pixelSize * spriteWidth / spriteHeight, pixelSize); - } - else - { - sprite.size = new Vector2(pixelSize, pixelSize * spriteHeight / spriteWidth); - } + sprite.size = spriteWidth < spriteHeight ? new Vector2(pixelSize * spriteWidth / spriteHeight, pixelSize) : new Vector2(pixelSize, pixelSize * spriteHeight / spriteWidth); } /// @@ -424,10 +402,7 @@ public static class Extensions /// 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; - } + if (!spriteObj.TryGetComponent(out var sprite)) return; sprite.SetSizeLimit(pixelSize); } @@ -444,16 +419,10 @@ public static class Extensions /// A if there is one, otherwise. public static PlayerVoteData GetVoteData(this PlayerControl player) { - if (VoteDataComponents.TryGetValue(player, out var component)) - { - return component; - } + if (VoteDataComponents.TryGetValue(player, out var component)) return component; component = player.GetComponent(); - if (!component) - { - throw new InvalidOperationException("PlayerVoteData is not attached to the player."); - } + if (!component) throw new InvalidOperationException("PlayerVoteData is not attached to the player."); VoteDataComponents[player] = component; return component; @@ -481,6 +450,7 @@ public static class Extensions tie = true; } } + return result; } @@ -506,6 +476,7 @@ public static class Extensions tie = true; } } + return result; } @@ -517,12 +488,7 @@ public static class Extensions /// The best constructor. public static ConstructorInfo? GetBestConstructor(this Type type, params object[] args) { - return type.GetValidConstructors(args) - .OrderBy( - ctor => ctor.GetParameters() - .Select((p, i) => GetInheritanceDistance(args[i].GetType(), p.ParameterType)) - .Sum()) - .FirstOrDefault(); + return type.GetValidConstructors(args).OrderBy(ctor => ctor.GetParameters().Select((p, i) => GetInheritanceDistance(args[i].GetType(), p.ParameterType)).Sum()).FirstOrDefault(); } /// @@ -532,7 +498,7 @@ public static class Extensions /// A proper string for the . public static string ToDisplayString(this Enum @enum) { - var regex = new Regex(@"([^\^])([A-Z][a-z$])"); + var regex = new Regex(@"([^\^])([A-Z][a-z$])", default, Regex.InfiniteMatchTimeout); return regex.Replace(@enum.ToString(), m => $"{m.Groups[1].Value} {m.Groups[2].Value}"); } @@ -544,14 +510,11 @@ public static class Extensions /// A collection of valid constructors. public static IEnumerable GetValidConstructors(this Type type, params object[] args) { - return type.GetConstructors().Where( - x => - { - var parameters = x.GetParameters(); - return parameters.Length == args.Length && Array.TrueForAll( - parameters, - t => t.ParameterType.IsInstanceOfType(args[t.Position])); - }); + return type.GetConstructors().Where(x => + { + var parameters = x.GetParameters(); + return parameters.Length == args.Length && Array.TrueForAll(parameters, t => t.ParameterType.IsInstanceOfType(args[t.Position])); + }); } /// @@ -563,10 +526,7 @@ public static class Extensions /// The distance between the types. public static int GetInheritanceDistance(Type from, Type to) { - if (!from.IsAssignableFrom(to)) - { - return int.MaxValue; - } + if (!from.IsAssignableFrom(to)) return int.MaxValue; var type = from; var distance = 0; @@ -575,6 +535,7 @@ public static class Extensions type = type.BaseType; distance++; } + return type == to ? distance : int.MaxValue; } @@ -611,7 +572,23 @@ public static class Extensions (byte)Mathf.Clamp(color.r - darknessAmount, 0, 255), (byte)Mathf.Clamp(color.g - darknessAmount, 0, 255), (byte)Mathf.Clamp(color.b - darknessAmount, 0, 255), - byte.MaxValue); + color.a); + } + + /// + /// Gets a darkened version of a . + /// + /// The original . + /// A darkness amount between 0 and 1. + /// The darkened . + public static Color GetShadowColor(this Color color, float darknessAmount) + { + return + new Color( + MathUtilities.Clamp01(color.r - darknessAmount), + MathUtilities.Clamp01(color.g - darknessAmount), + MathUtilities.Clamp01(color.b - darknessAmount), + color.a); } /// @@ -623,9 +600,7 @@ public static class Extensions /// A truncated string of maxLength with the attached suffix. public static string? Truncate(this string? value, int maxLength, string truncationSuffix = "…") { - return value?.Length > maxLength - ? value[..maxLength] + truncationSuffix - : value; + return value?.Length > maxLength ? value[..maxLength] + truncationSuffix : value; } /// @@ -661,10 +636,7 @@ public static class Extensions count += length; } - if (current.Count > 0) - { - chunks.Enqueue([.. current]); - } + if (current.Count > 0) chunks.Enqueue([.. current]); return chunks; } @@ -676,9 +648,7 @@ public static class Extensions /// if the is for a custom options, otherwise. public static bool IsCustom(this OptionBehaviour optionBehaviour) { - return ModdedOptionsManager.ModdedOptions.Values.Any( - opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour) - || optionBehaviour.Equals(GameModeOption.OptionBehaviour); + return ModdedOptionsManager.ModdedOptions.Values.Any(opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour) || optionBehaviour.Equals(GameModeOption.OptionBehaviour); } /// @@ -731,9 +701,7 @@ public static class Extensions /// The if it is found, or there is none within the radius. public static DeadBody? GetNearestDeadBody(this PlayerControl playerControl, float radius) { - return Helpers - .GetNearestDeadBodies(playerControl.GetTruePosition(), radius, Helpers.CreateFilter(Constants.NotShipMask)) - .Find(component => component && !component.Reported); + return Helpers.GetNearestDeadBodies(playerControl.GetTruePosition(), radius, Helpers.CreateFilter(Constants.NotShipMask)).Find(component => component && !component.Reported); } /// @@ -746,15 +714,10 @@ public static class Extensions /// Optional predicate to test if the is valid. /// The type of the object. /// The if it was found, or if there is none within the radius. - public static T? GetNearestObjectOfType( - this PlayerControl playerControl, - float radius, - ContactFilter2D filter, - string? colliderTag = null, - Predicate? predicate = null) where T : Component + public static T? GetNearestObjectOfType(this PlayerControl playerControl, float radius, ContactFilter2D filter, string? colliderTag = null, Predicate? predicate = null) + where T : Component { - return Helpers.GetNearestObjectsOfType(playerControl.GetTruePosition(), radius, filter, colliderTag) - .Find(predicate ?? (component => component)); + return Helpers.GetNearestObjectsOfType(playerControl.GetTruePosition(), radius, filter, colliderTag).Find(predicate ?? (component => component)); } /// @@ -767,26 +730,14 @@ public static class Extensions /// Determines if Ghosts are included. /// Optional predicate to test if the is valid. /// The closest if there is one, otherwise. - public static PlayerControl? GetClosestPlayer( - this PlayerControl playerControl, - bool includeImpostors, - float distance, - bool ignoreColliders = false, - bool includeGhosts = false, - Predicate? predicate = null) + public static PlayerControl? GetClosestPlayer(this PlayerControl playerControl, bool includeImpostors, float distance, bool ignoreColliders = false, bool includeGhosts = false, Predicate? predicate = null) { - var filteredPlayers = Helpers.GetClosestPlayers(playerControl, distance, ignoreColliders) - .Where( - playerInfo => !playerInfo.Data.Disconnected && - playerInfo.PlayerId != playerControl.PlayerId && - (includeGhosts || !playerInfo.Data.IsDead) && - (includeImpostors || !playerInfo.Data.Role.IsImpostor)) - .ToList(); + var filteredPlayers = Helpers.GetClosestPlayers(playerControl, distance, ignoreColliders).Where(playerInfo => !playerInfo.Data.Disconnected && playerInfo.PlayerId != playerControl.PlayerId && (includeGhosts || !playerInfo.Data.IsDead) && (includeImpostors || !playerInfo.Data.Role.IsImpostor)).ToList(); return predicate != null ? filteredPlayers.Find(predicate) : filteredPlayers.FirstOrDefault(); } /// - /// Fixed version of . + /// Fixed version of . /// /// The you want to update the outline for. /// The outline . @@ -818,9 +769,7 @@ public static class Extensions var action = userData.GetAction(userData.actions.Count - 1)!; action.name = id; - action.descriptiveName = group != null - ? $"{Palette.CrewmateRoleHeaderDarkBlue.ToTextColor()}{group.ReplaceLineEndings(" ")}\n{name}" - : name; + action.descriptiveName = group != null ? $"{Palette.CrewmateRoleHeaderDarkBlue.ToTextColor()}{group.ReplaceLineEndings(" ")}\n{name}" : name; action.categoryId = category; action.type = type; action.userAssignable = true; @@ -842,7 +791,19 @@ public static class Extensions } userData.keyboardMaps[0].actionElementMaps.Add(map); - userData.joystickMaps[0].actionElementMaps.Add(map); return action; } + + /// + /// Gets a component that is attached to the provided , attaching and returning the component if it wasn't found. + /// + /// The component type. + /// The to get the component from. + /// The component. + public static T GetOrAddComponent(this GameObject go) + where T : Component + { + var comp = go.GetComponent(); + return comp ? comp : go.AddComponent(); + } } diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs new file mode 100644 index 0000000..e33f601 --- /dev/null +++ b/MiraAPI/Utilities/GuideOptionsPatches.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.PluginLoading; +using MiraAPI.Translation; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities; +using UnityEngine; + +namespace MiraAPI.Utilities; + +internal sealed record GuideOptionEntry( + string Title, + string Value, + AbstractOptionGroup Group, + string GroupName, + string ModAbbreviation, + IReadOnlyList ModSearchTerms); + +internal sealed class GuideOptionPage( + AbstractOptionGroup group, + string title, + int order, + int menuOrder, + uint groupPriority, + int columns, + string modAbbreviation, + IReadOnlyList modSearchTerms, + List entries) +{ + public AbstractOptionGroup Group { get; } = group; + + public List Groups { get; } = [group]; + + public string Title { get; } = title; + + public LoadableAsset Sprite { get; } = group.Sprite; + + public int Order { get; set; } = order; + + public int MenuOrder { get; set; } = menuOrder; + + public uint GroupPriority { get; set; } = groupPriority; + + public int Columns { get; } = columns; + + public string ModAbbreviation { get; } = modAbbreviation; + + public HashSet ModSearchTerms { get; } = new(modSearchTerms, StringComparer.OrdinalIgnoreCase); + + public List Entries { get; } = entries; +} + +internal static class GuideOptionPages +{ + private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + + public static List BuildPages(Func formatBool) + { + var pages = new List(); + var combinedPages = new Dictionary(StringComparer.OrdinalIgnoreCase); + var mode = CustomGameModeManager.ActiveMode; + + var groups = MiraPluginManager.Instance.PluginsWithOptionsOrGameModes + .SelectMany(plugin => plugin.InternalOptionGroups + .Where(group => group.ShowInMatchInfoGuide && IsSettingsGroup(group, mode) && SafeGroupVisible(group)) + .Select(group => ( + Plugin: plugin, + Group: group, + ModAbbreviation: plugin.MiraPlugin.GetAbbreviatedModName()))) + .OrderBy(item => GetMenuOrder(item.Group.ParentMenu)) + .ThenBy(item => item.Group.GroupPriority) + .ThenBy(item => item.Group.GroupName, StringComparer.OrdinalIgnoreCase) + .ThenBy(item => item.ModAbbreviation, StringComparer.OrdinalIgnoreCase); + + foreach (var item in groups) + { + var groupName = item.Group.GroupName.Translate(); + var modSearchTerms = GetModSearchTerms(item.Plugin, item.Group); + var entries = new List(); + + foreach (var option in item.Group.Options) + { + if (!SafeOptionVisible(option)) + { + continue; + } + + var entry = TryCreateEntry( + option, + item.Group, + groupName, + item.ModAbbreviation, + modSearchTerms, + formatBool); + if (entry != null) + { + entries.Add(entry); + } + } + + if (entries.Count > 0) + { + var pageKey = item.Group.MatchInfoGuidePageKey?.Trim(); + var combinedKey = string.IsNullOrEmpty(pageKey) ? null : $"{item.Plugin.PluginId}\0{pageKey}"; + if (combinedKey != null && combinedPages.TryGetValue(combinedKey, out var combinedPage)) + { + combinedPage.Groups.Add(item.Group); + combinedPage.Entries.AddRange(entries); + combinedPage.ModSearchTerms.UnionWith(modSearchTerms); + combinedPage.Order = Math.Min(combinedPage.Order, item.Group.MatchInfoGuidePageOrder); + combinedPage.MenuOrder = Math.Min( + combinedPage.MenuOrder, + GetMenuOrder(item.Group.ParentMenu)); + combinedPage.GroupPriority = Math.Min( + combinedPage.GroupPriority, + item.Group.GroupPriority); + } + else + { + var page = new GuideOptionPage( + item.Group, + item.Group.MatchInfoGuidePageTitle, + item.Group.MatchInfoGuidePageOrder, + GetMenuOrder(item.Group.ParentMenu), + item.Group.GroupPriority, + Mathf.Clamp(item.Group.MatchInfoGuidePageColumns, 1, 6), + item.ModAbbreviation, + modSearchTerms, + entries); + pages.Add(page); + if (combinedKey != null) + { + combinedPages.Add(combinedKey, page); + } + } + } + } + + return [.. pages + .OrderBy(page => page.MenuOrder) + .ThenBy(page => page.GroupPriority) + .ThenBy(page => page.Order) + .ThenBy(page => page.Group.GroupName, StringComparer.OrdinalIgnoreCase) + .ThenBy(page => page.ModAbbreviation, StringComparer.OrdinalIgnoreCase)]; + } + + private static int GetMenuOrder(MenuCategory menu) + { + return menu switch + { + MenuCategory.Game => 0, + MenuCategory.CustomOne => 1, + MenuCategory.CustomTwo => 2, + _ => int.MaxValue, + }; + } + + public static List Search(IEnumerable pages, string query) + { + query = query.Trim(); + if (query.Length == 0) + { + return []; + } + + const StringComparison cmp = StringComparison.OrdinalIgnoreCase; + var pageList = pages.ToList(); + var normalizedQuery = NormalizeSearchText(query); + var matchingModPages = normalizedQuery.Length < 3 + ? [] + : pageList + .Where(page => page.ModSearchTerms.Any(term => + NormalizeSearchText(term).Contains(normalizedQuery, StringComparison.Ordinal))) + .ToArray(); + + if (matchingModPages.Length > 0) + { + return [.. matchingModPages.SelectMany(page => page.Entries)]; + } + + return [.. pageList + .SelectMany(page => page.Entries + .Select(entry => (Entry: entry, Rank: Rank(entry, query, cmp))) + .Where(result => result.Rank < int.MaxValue) + .OrderBy(result => result.Rank) + .Select(result => result.Entry))]; + } + + private static string[] GetModSearchTerms(MiraPluginInfo plugin, AbstractOptionGroup group) + { + var terms = new HashSet(StringComparer.OrdinalIgnoreCase) + { + plugin.PluginId, + plugin.PluginInfo.Metadata.Name, + plugin.MiraPlugin.OptionsTitleText, + plugin.MiraPlugin.ParentLocaleId, + plugin.MiraPlugin.GetAbbreviatedModName(), + }; + + AddNamespace(terms, plugin.MiraPlugin.GetType()); + AddNamespace(terms, group.GetType()); + foreach (var option in group.Options) + { + AddNamespace(terms, option.GetType()); + } + + return [.. terms.Where(term => !string.IsNullOrWhiteSpace(term))]; + } + + private static void AddNamespace(HashSet terms, Type type) + { + if (!string.IsNullOrWhiteSpace(type.Namespace)) + { + terms.Add(type.Namespace); + } + } + + private static string NormalizeSearchText(string value) + { + return new string([.. value.Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant)]); + } + + private static int Rank(GuideOptionEntry entry, string query, StringComparison cmp) + { + if (entry.Title.Equals(query, cmp)) + { + return 0; + } + + if (entry.Title.Contains(query, cmp)) + { + return 1; + } + + if (entry.GroupName.Contains(query, cmp) || entry.ModAbbreviation.Contains(query, cmp)) + { + return 2; + } + + return entry.Value.Contains(query, cmp) ? 3 : int.MaxValue; + } + + private static bool IsSettingsGroup(AbstractOptionGroup group, AbstractGameMode? mode) + { + if (group.ParentMenu is not (MenuCategory.Game or MenuCategory.CustomOne or MenuCategory.CustomTwo)) + { + return false; + } + + if (group.OptionableType == null) + { + return true; + } + + return mode != null && typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType) && + mode.GetType().IsAssignableTo(group.OptionableType); + } + + private static bool SafeGroupVisible(AbstractOptionGroup group) + { + try + { + return group.GroupVisible(); + } + catch (Exception e) + { + Logger.Warning($"GroupVisible threw for {group.GetType().Name}: {e.Message}"); + return false; + } + } + + private static bool SafeOptionVisible(IModdedOption option) + { + try + { + return option.Visible(); + } + catch (Exception e) + { + Logger.Warning($"Visible threw for option '{option.Title}': {e.Message}"); + return false; + } + } + + private static GuideOptionEntry? TryCreateEntry( + IModdedOption option, + AbstractOptionGroup group, + string groupName, + string modAbbreviation, + IReadOnlyList modSearchTerms, + Func formatBool) + { + try + { + var title = TranslationController.Instance.GetString(option.StringName); + var value = FormatValue(option, formatBool); + return new GuideOptionEntry(title, value, group, groupName, modAbbreviation, modSearchTerms); + } + catch (Exception e) + { + Logger.Warning($"Could not format option '{option.Title}' for the guide: {e.Message}"); + return null; + } + } + + private static string FormatValue(IModdedOption option, Func formatBool) + { + switch (option) + { + case ModdedToggleOption toggle: + return formatBool(toggle.Value); + + case ModdedEnumOption enumOption: + return MiraLocaleManager.Get( + enumOption.Values[enumOption.Value], + enumOption.Values[enumOption.Value]); + + case ModdedNumberOption number: + if (number is { NegativeWordValue: not "#", Value: -1 }) + { + return number.NegativeWordValue; + } + + if (number is { ZeroWordValue: not "#", Value: 0 }) + { + return number.ZeroWordValue; + } + + return TrailingZeroRegex.Replace(number.Data.GetValueString(number.Value), string.Empty); + + default: + return option.Data.Type == OptionTypes.Checkbox + ? formatBool(Math.Abs(option.GetFloatData() - 1f) < 0.0001f) + : option.Data.GetValueString(option.GetFloatData()); + } + } +} diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 5a70c2c..94fea9f 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,25 +1,169 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Text; +using System.Text.RegularExpressions; using HarmonyLib; +using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Modifiers; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; using UnityEngine; +using UnityEngine.UI; using MethodBase = System.Reflection.MethodBase; using Object = UnityEngine.Object; +using Random = UnityEngine.Random; namespace MiraAPI.Utilities; +internal readonly record struct AdvancedSettingsRow(string Title, string Value, AbstractOptionGroup? Group); + /// /// A class that contains helper methods. /// public static class Helpers { + internal const float MatchInfoGuideBaseScale = 1.3f; + private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + private static NumberOption? _advancedSettingsRowPrefab; + + /// + /// Gets all of the options stored within the provided type. + /// + /// The type to get the options of. + /// A readonly list of options from the provided type. + public static ReadOnlyCollection? GetModdedOptionsForType(Type classType) + { + var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType); + + return group?.Children; + } + + /// + /// Gets the option text of the provided type. + /// + /// The type to get the text for. + /// The options text of the provided type. + public static string GetOptionsText(Type classType) + { + var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType); + var options = group?.Children; + var summaryProvider = group as ISummarizedOptions; + var hiddenKeys = summaryProvider?.WikiHiddenOptionKeys; + + if (options == null) + { + return string.Empty; + } + + var lines = new List(); + var insertedSummary = false; + foreach (var option in options) + { + StringNames? key = option switch + { + ModdedToggleOption t => t.StringName, + ModdedEnumOption e => e.StringName, + ModdedNumberOption n => n.StringName, + _ => null, + }; + + if (!insertedSummary && summaryProvider != null && hiddenKeys != null && + key.HasValue && hiddenKeys.Contains(key.Value)) + { + foreach (var line in summaryProvider.GetWikiOptionSummaryLines()) + { + if (!string.IsNullOrWhiteSpace(line)) + { + lines.Add(line); + } + } + + insertedSummary = true; + } + + if (!option.Visible()) + { + continue; + } + + if (key.HasValue && hiddenKeys?.Contains(key.Value) == true) + { + continue; + } + + switch (option) + { + case ModdedToggleOption toggleOption: + lines.Add( + TranslationController.Instance.GetString(toggleOption.StringName) + ": " + + toggleOption.Value); + break; + /*case ModdedMultiSelectOption enumOption: + if (!enumOption.Visible()) + { + continue; + } + + builder.AppendLine(enumOption.Title + ": " + enumOption.Values[enumOption.Value]); + break;*/ + case ModdedEnumOption enumOption: + lines.Add( + TranslationController.Instance.GetString(enumOption.StringName) + ": " + + MiraLocaleManager.Get( + enumOption.Values[enumOption.Value], + enumOption.Values[enumOption.Value])); + break; + case ModdedNumberOption numberOption: + var optionStr = TrailingZeroRegex.Replace( + numberOption.Data.GetValueString(numberOption.Value), + string.Empty); + + var title = TranslationController.Instance.GetString(numberOption.StringName); + if (numberOption is { NegativeWordValue: not "#", Value: -1 }) + { + lines.Add(title + $": {numberOption.NegativeWordValue}"); + } + else if (numberOption is { ZeroWordValue: not "#", Value: 0 }) + { + lines.Add(title + $": {numberOption.ZeroWordValue}"); + } + else + { + lines.Add(title + ": " + optionStr); + } + + break; + } + } + + if (lines.Count == 0) + { + return string.Empty; + } + + var builder = new StringBuilder(); + builder.AppendLine( + MiraApiPlugin.Culture, + $"\n \n{MiraLocaleManager.Get("Options")}"); + foreach (var line in lines) + { + builder.AppendLine(line); + } + + return builder.ToString(); + } + /// /// Get all living players. /// @@ -43,6 +187,7 @@ public static class Helpers /// Gets the move next of an on . /// /// Where the should be parented to. + /// The name of the method being searched for. /// THe corresponding method to patch. public static MethodBase? GetStateMachineMoveNext(string methodName) { @@ -50,7 +195,7 @@ public static class Helpers var showRoleStateMachine = typeof(T) .GetNestedTypes() - .FirstOrDefault(x=>x.Name.Contains(methodName)); + .FirstOrDefault(x => x.Name.Contains(methodName)); if (showRoleStateMachine == null) { @@ -136,18 +281,12 @@ public static class Helpers /// if the number falls in the range, if not. public static bool CheckChance(int probability) { - switch (probability) + return probability switch { - case 0: - return false; - case 100: - return true; - default: - { - var num = Random.RandomRangeInt(1, 101); - return num <= probability; - } - } + 0 => false, + 100 => true, + _ => Random.RandomRangeInt(1, 100) <= probability, + }; } /// @@ -184,7 +323,8 @@ public static class Helpers /// The position of where you want to check from. For example: PlayerControl.LocalPlayer.transform.position. /// The object type. /// The closest object. - public static T? FindClosestObjectOfType(List objectList, Vector3 position) where T : MonoBehaviour + public static T? FindClosestObjectOfType(List objectList, Vector3 position) + where T : MonoBehaviour { T? closest = null; var closestDistanceSqr = Mathf.Infinity; @@ -197,11 +337,9 @@ public static class Helpers } var sqrDistance = (obj.transform.position - position).sqrMagnitude; - if (sqrDistance < closestDistanceSqr) - { - closestDistanceSqr = sqrDistance; - closest = obj; - } + if (!(sqrDistance < closestDistanceSqr)) continue; + closestDistanceSqr = sqrDistance; + closest = obj; } return closest; @@ -234,7 +372,7 @@ public static class Helpers var popper = HudManager.Instance.Notifier; var newMessage = Object.Instantiate(popper.notificationMessageOrigin, Vector3.zero, Quaternion.identity, popper.transform); newMessage.transform.localPosition = localPos; - newMessage.SetUp(text, spr ?? null, color, new System.Action(() => popper.OnMessageDestroy(newMessage))); + newMessage.SetUp(text, spr ?? null, color, new Action(() => popper.OnMessageDestroy(newMessage))); popper.lastMessageKey = -1; popper.ShiftMessages(); popper.AddMessageToQueue(newMessage); @@ -284,11 +422,14 @@ public static class Helpers /// A of s. public static List GetNearestDeadBodies(Vector2 source, float radius, ContactFilter2D filter) { - var results = new Il2CppSystem.Collections.Generic.List(); + var results = new CppCollections.List(); Physics2D.OverlapCircle(source, radius, filter, results); - return results.ToArray() - .Where(collider2D => collider2D.CompareTag("DeadBody")) - .Select(collider2D => collider2D.GetComponent()).ToList(); + return + [ + .. results.ToArray() + .Where(collider2D => collider2D.CompareTag("DeadBody")) + .Select(collider2D => collider2D.GetComponent()) + ]; } /// @@ -303,11 +444,14 @@ public static class Helpers public static List GetNearestObjectsOfType(Vector2 source, float radius, ContactFilter2D filter, string? colliderTag = null) where T : Component { - var results = new Il2CppSystem.Collections.Generic.List(); + var results = new CppCollections.List(); Physics2D.OverlapCircle(source, radius, filter, results); - return results.ToArray() - .Where(collider2D => colliderTag == null || collider2D.CompareTag(colliderTag)) - .Select(collider2D => collider2D.GetComponent()).ToList(); + return + [ + .. results.ToArray() + .Where(collider2D => colliderTag == null || collider2D.CompareTag(colliderTag)) + .Select(collider2D => collider2D.GetComponent()) + ]; } /// @@ -324,12 +468,11 @@ public static class Helpers { var newList = GetNearestObjectsOfType(source, radius, CreateFilter(Constants.NotShipMask)); - if (!ignoreColliders) - { - return newList; - } - - return (from player in newList + return !ignoreColliders + ? newList + : + [ + .. from player in newList let vector = player.GetTruePosition() - source let magnitude = vector.magnitude where !PhysicsHelpers.AnyNonTriggersBetween( @@ -337,7 +480,8 @@ public static class Helpers vector.normalized, magnitude, Constants.ShipAndObjectsMask) - select player).ToList(); + select player + ]; } /// @@ -362,7 +506,7 @@ public static class Helpers var myPos = source.GetTruePosition(); var players = GetClosestPlayers(myPos, distance, ignoreColliders); - return ignoreSource ? players.Where(plr => plr.PlayerId != source.PlayerId).ToList() : players; + return ignoreSource ? [.. players.Where(plr => plr.PlayerId != source.PlayerId)] : players; } /// @@ -398,23 +542,16 @@ public static class Helpers Constants.ShipAndObjectsMask)) select playerControl); - outputList.Sort( - delegate (PlayerControl a, PlayerControl b) - { - var magnitude2 = (a.GetTruePosition() - source).magnitude; - var magnitude3 = (b.GetTruePosition() - source).magnitude; - if (magnitude2 > magnitude3) - { - return 1; - } - - if (magnitude2 < magnitude3) - { - return -1; - } - - return 0; - }); + outputList.Sort((a, b) => + { + var magnitude2 = (a.GetTruePosition() - source).magnitude; + var magnitude3 = (b.GetTruePosition() - source).magnitude; + return magnitude2 > magnitude3 + ? 1 + : magnitude2 < magnitude3 + ? -1 + : 0; + }); return outputList; } @@ -463,7 +600,7 @@ public static class Helpers /// Gets a by its parent ID. /// /// The player ID. - /// A or if its not found. + /// A or if it's not found. public static DeadBody? GetBodyById(byte id) { return Object.FindObjectsOfType().FirstOrDefault(body => body.ParentId == id); @@ -504,17 +641,16 @@ public static class Helpers /// The random string. public static string RandomString(int length, string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") { - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[Random.RandomRangeInt(0, s.Length)]).ToArray()); + return new string([.. Enumerable.Repeat(chars, length).Select(s => s[Random.RandomRangeInt(0, s.Length)])]); } /// - /// Returns the formated value using the specified suffix and format string. + /// Returns the formatted value using the specified suffix and format string. /// /// The value to format. /// The suffix to add. /// The format string to use to format. - /// The formated value. + /// The formatted value. public static string FormatValue(float value, MiraNumberSuffixes suffix = MiraNumberSuffixes.None, string formatString = "0.0") { return suffix switch @@ -535,11 +671,7 @@ public static class Helpers /// The 's name. public static string GetRoleName(this RoleBehaviour role) { - if (role is ICustomRole custom) - { - return custom.RoleName; - } - return role.NiceName; + return role is ICustomRole custom ? custom.RoleName : role.NiceName; } /// @@ -549,11 +681,7 @@ public static class Helpers /// The 's intro blurb. public static string GetRoleIntroBlurb(this RoleBehaviour role) { - if (role is ICustomRole custom) - { - return custom.RoleDescription; - } - return role.Blurb; + return role is ICustomRole custom ? custom.RoleDescription : role.Blurb; } /// @@ -563,11 +691,7 @@ public static class Helpers /// The 's medium description. public static string GetRoleMedDescription(this RoleBehaviour role) { - if (role is ICustomRole custom) - { - return custom.RoleDescription; - } - return role.BlurbMed; + return role is ICustomRole custom ? custom.RoleDescription : role.BlurbMed; } /// @@ -577,11 +701,41 @@ public static class Helpers /// The 's long description. public static string GetRoleLongDescription(this RoleBehaviour role) { - if (role is ICustomRole custom) - { - return custom.RoleLongDescription; - } - return role.BlurbLong; + return role is ICustomRole custom ? custom.RoleLongDescription : role.BlurbLong; + } + + /// + /// Returns the TextMeshPro sprite tag for a role, matching how role names are shown in-game. + /// Uses the role's if set, otherwise falls back to the faction icon. + /// + /// The . + /// A <sprite> rich text tag. + public static string GetTmpIcon(this RoleBehaviour role) + { + return role is ICustomRole custom ? custom.GetTmpIcon() : $""; + } + + /// + /// Returns the TextMeshPro sprite tag for a custom role, matching how role names are shown in-game. + /// Uses the role's if set, otherwise falls back to the faction icon. + /// + /// The . + /// A <sprite> rich text tag. + public static string GetTmpIcon(this ICustomRole role) + { + return role.Configuration.IconTmp + ? $"" + : $""; + } + + /// + /// Returns the TextMeshPro sprite tag for a modifier, or an empty string if it has no . + /// + /// The . + /// A <sprite> rich text tag, or . + public static string GetTmpIcon(this BaseModifier modifier) + { + return modifier.IconTmp ? $"" : string.Empty; } /// @@ -589,9 +743,578 @@ public static class Helpers /// /// The to check. /// The 's blacklist status. + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Intentionally unused because the main intent is for it to be used by dependent mods to add to a blacklist.")] public static bool IsRoleBlacklisted(this RoleBehaviour role) { // This should be patchable by mods when a vanilla role is meant to be replaced by a custom role. return false; } + + /// + /// Creates a tab in the advanced wiki. + /// + /// The guide panel to attach the tab to. + /// The name of the tab. + /// The title of the tab. + /// The icon of the page. + /// The description of the tab. + /// The instance of the title. + /// The instance for the title. + /// The instance of the description. + /// The of the tab. + public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, Sprite icon, string description, TextMeshPro titleTmp, SpriteRenderer titleIcon, out TextMeshPro descriptionTmp) + { + var obj = new GameObject(objName); + titleTmp.richText = true; + titleTmp.text = title; + titleIcon.sprite = icon; + titleIcon.SetSizeLimit(0.55f); + descriptionTmp = Object.Instantiate(guide.MatchInfoRolePanelPrefab.roleCount, obj.transform); + descriptionTmp.fontSizeMin = descriptionTmp.fontSizeMax = descriptionTmp.fontSize = 2f; + var sourceTmp = HudManager.Instance.TaskPanel.taskText; + descriptionTmp.font = sourceTmp.font; + descriptionTmp.fontMaterial = sourceTmp.fontMaterial; + descriptionTmp.EnableStencilMasking(); + descriptionTmp.text = description; + descriptionTmp.rectTransform.sizeDelta = new Vector2(7.5f, 0.3f); + descriptionTmp.alignment = TextAlignmentOptions.TopLeft; + descriptionTmp.transform.localPosition = new Vector3(0, 1.125f, 0); + descriptionTmp.ForceMeshUpdate(); + return obj; + } + + /// + /// Creates a full advanced wiki page with a title, a description, and an optional grid of ability panels. + /// + /// The guide object. + /// The instance of the title. + /// The instance for the title. + /// The the page should be parented to. + /// The name of the page object. + /// The title of the page. + /// The icon of the page. + /// The description of the page. + /// The abilities to display in a grid below the description. + /// Whether to use variable text sizing for ability descriptions (allows different heights). + /// The of the wiki page. + public static GameObject CreateAdvancedWikiPage( + MatchInfoGuide guide, + TextMeshPro titleTmp, + SpriteRenderer titleIcon, + Scroller parent, + string objName, + string title, + Sprite icon, + string description, + IReadOnlyCollection abilities, + bool variableTextSizing = false) + { + parent.ScrollToTop(); + var obj = CreateAdvancedWikiTab( + guide, + objName, + title, + icon, + description, + titleTmp, + titleIcon, + out var desc); + var num = 0; + var maxTextSize = 0f; + if (abilities.Count != 0) + { + var grid = Object.Instantiate(parent.Inner, obj.transform); + var layoutGroup = grid.GetComponent(); + layoutGroup.startAxis = GridLayoutGroup.Axis.Vertical; + layoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount; + layoutGroup.spacing = new Vector2(0.75f, 0.4f); + grid.DestroyChildren(); + foreach (var ability in abilities) + { + num++; + var panel = Object.Instantiate(guide.MatchInfoRolePanelPrefab, grid); + SetupAbilityPanel(panel, ability, variableTextSizing); + if (variableTextSizing) + { + var descSize = panel.roleDescription.textBounds.size.y; + if (maxTextSize < descSize) + { + maxTextSize = descSize; + } + } + } + + grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f); + grid.localScale = new Vector3(1.3f, 1.3f, 1); + } + + obj.transform.SetParent(parent.Inner.transform, false); + obj.transform.localScale = Vector3.one / MatchInfoGuideBaseScale; + obj.transform.localPosition = new Vector3(0f, 0.5f, 0f); + var scrollHeight = variableTextSizing + ? desc.textBounds.size.y - 2.5f + maxTextSize + 0.475f + : desc.textBounds.size.y - 2.5f + Mathf.Ceil(num / 2f) * 1.45f; + parent.SetYBoundsMax(Mathf.Clamp(scrollHeight, 0f, 999f)); + return obj; + } + + /// + /// Caches a persistent clone of the vanilla settings row for use in the Match Info Guide. + /// + /// The vanilla settings row prefab. + internal static void CacheAdvancedSettingsRowPrefab(NumberOption? rowPrefab) + { + if (!rowPrefab) + { + foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of())) + { + var menu = candidate.TryCast(); + var settingsTab = menu ? menu!.GameSettingsTab : null; + if (settingsTab && settingsTab!.numberOptionOrigin) + { + rowPrefab = settingsTab.numberOptionOrigin; + break; + } + } + } + + if (!rowPrefab) + { + foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of())) + { + var option = candidate.TryCast(); + if (!option) + { + continue; + } + + if (option!.TitleText && option.ValueText && option.PlusBtn && option.MinusBtn) + { + rowPrefab = option; + break; + } + } + } + + if (_advancedSettingsRowPrefab || !rowPrefab) + { + return; + } + + var cachedPrefab = Object.Instantiate(rowPrefab!.gameObject); + cachedPrefab.name = "MiraAPIAdvancedSettingsRowPrefab"; + cachedPrefab.transform.SetParent(null, false); + cachedPrefab.transform.localPosition = Vector3.zero; + cachedPrefab.transform.localRotation = Quaternion.identity; + cachedPrefab.SetActive(false); + Object.DontDestroyOnLoad(cachedPrefab); + _advancedSettingsRowPrefab = cachedPrefab.GetComponent(); + } + + /// + /// Creates a page of vanilla settings rows inside a roles-style scroller. + /// + /// The the page is parented to. + /// The rows to show: key is the label, value is the displayed value. + /// The that holds all the rows. Destroy it to remove the page. + public static GameObject CreateAdvancedSettingsPage( + Scroller parent, + IReadOnlyList> settings) + { + return CreateAdvancedSettingsPage( + parent, + [.. settings.Select(setting => new AdvancedSettingsRow(setting.Key, setting.Value, null))], + 2, + false); + } + + internal static GameObject CreateAdvancedSettingsPage( + Scroller parent, + List settings, + int columns = 2, + bool groupsAsColumns = false) + { + const float contentTop = 0.95f; + const float horizontalOffset = -0.2f; + const float headerContentGap = 0.2f; + const int contentSortingOrder = 151; + + columns = Mathf.Clamp(columns, 1, 6); + var inner = parent.Inner; + var layout = inner.GetComponent(); + if (layout) + { + layout.enabled = false; + } + + var columnGap = layout ? Mathf.Clamp(layout.spacing.x, 0.1f, 0.6f) : 0.3f; + const int baseColumnCount = 2; + var baseColumnWidth = layout ? Mathf.Clamp(layout.cellSize.x, 2f, 3.2f) : 2.7f; + var availableWidth = baseColumnWidth * baseColumnCount + columnGap * (baseColumnCount - 1); + var columnWidth = Mathf.Clamp( + (availableWidth - (columns - 1) * columnGap) / columns, + 1f, + 3.2f); + var layer = inner.gameObject.layer; + var page = new GameObject("AdvancedSettingsPage") { layer = layer }; + page.transform.SetParent(inner, false); + page.transform.localPosition = Vector3.zero; + page.SetActive(false); + + var gameSettingsMenu = GameSettingMenu.Instance; + CacheAdvancedSettingsRowPrefab(gameSettingsMenu ? gameSettingsMenu.GameSettingsTab.numberOptionOrigin : null); + var rowPrefab = _advancedSettingsRowPrefab; + if (!rowPrefab) + { + Error("Could not render MiraAPI settings rows because the vanilla NumberOption prefab was not cached."); + return page; + } + + var spriteBounds = rowPrefab!.GetComponentsInChildren(true) + .Where(renderer => + renderer.enabled && + renderer.sprite && + renderer.transform != rowPrefab.PlusBtn.transform && + !renderer.transform.IsChildOf(rowPrefab.PlusBtn.transform) && + renderer.transform != rowPrefab.MinusBtn.transform && + !renderer.transform.IsChildOf(rowPrefab.MinusBtn.transform)) + .Select(renderer => renderer.bounds) + .ToArray(); + if (spriteBounds.Length == 0) + { + Error("Could not render MiraAPI settings rows because the vanilla NumberOption prefab has no sprites."); + return page; + } + + var prefabBounds = spriteBounds.Skip(1).Aggregate( + spriteBounds[0], + (bounds, next) => + { + bounds.Encapsulate(next); + return bounds; + }); + var rowScale = columnWidth / prefabBounds.size.x * 1.22f; + var rowHeight = prefabBounds.size.y * rowScale; + var rowPitch = 0.45f * rowScale; + var groupedSettings = groupsAsColumns + ? settings + .Where(setting => setting.Group != null) + .GroupBy(setting => setting.Group!) + .ToList() + : []; + groupsAsColumns = groupedSettings.Count > 1; + if (groupsAsColumns) + { + rowScale = columnWidth / prefabBounds.size.x * (columns > 2 ? 1.15f : 1f); + rowHeight = prefabBounds.size.y * rowScale; + rowPitch = 0.45f * rowScale; + } + + var groupColumns = new Dictionary(); + var groupTopOffsets = new Dictionary(); + var groupRows = new Dictionary(); + if (groupsAsColumns) + { + var bandCount = Mathf.CeilToInt(groupedSettings.Count / (float)columns); + var bandHeights = new float[bandCount]; + for (var i = 0; i < groupedSettings.Count; i++) + { + var group = groupedSettings[i].Key; + var band = i / columns; + groupColumns[group] = i % columns; + bandHeights[band] = Mathf.Max( + bandHeights[band], + rowPitch * (groupedSettings[i].Count() + 1) + headerContentGap + 0.25f); + } + + var bandOffset = 0f; + for (var band = 0; band < bandCount; band++) + { + for (var i = band * columns; i < Mathf.Min((band + 1) * columns, groupedSettings.Count); i++) + { + groupTopOffsets[groupedSettings[i].Key] = bandOffset; + groupRows[groupedSettings[i].Key] = 0; + } + + bandOffset += bandHeights[band]; + } + } + + var contentBottom = float.PositiveInfinity; + var previousGroup = (AbstractOptionGroup?)null; + var rowIndex = 0; + var column = 0; + var groupSpacing = 0f; + + for (var i = 0; i < settings.Count; i++) + { + if (settings[i].Group != previousGroup && settings[i].Group is { } group) + { + if (groupsAsColumns) + { + column = groupColumns[group]; + } + else + { + if (previousGroup != null) + { + groupSpacing += 0.2f; + } + + if (column != 0) + { + rowIndex++; + column = 0; + } + } + + var headerSource = group.Header; + if (!headerSource && gameSettingsMenu && gameSettingsMenu.GameSettingsTab) + { + headerSource = gameSettingsMenu.GameSettingsTab.categoryHeaderOrigin; + } + + if (!headerSource) + { + foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of())) + { + var candidateHeader = candidate.TryCast(); + if (candidateHeader && candidateHeader!.Background && candidateHeader.Title) + { + headerSource = candidateHeader; + break; + } + } + } + + if (headerSource) + { + const float headerScale = 0.46f; + var headerWidth = groupsAsColumns + ? columnWidth / headerScale + : (columns * columnWidth + (columns - 1) * columnGap) / headerScale; + var header = Object.Instantiate(headerSource!, page.transform, false); + header.transform.localScale = Vector3.one * headerScale; + if (groupsAsColumns) + { + var totalWidth = columns * columnWidth + (columns - 1) * columnGap; + var groupColumn = groupColumns[group]; + var groupLeft = -totalWidth / 2f + horizontalOffset + + groupColumn * (columnWidth + columnGap); + header.transform.localPosition = new Vector3( + groupLeft + (columnWidth / 2f) - 0.25f, + contentTop - groupTopOffsets[group], + -1f); + } + else + { + header.transform.localPosition = new Vector3( + horizontalOffset - 0.5f, + contentTop - (rowIndex * rowPitch) - groupSpacing, + -1f); + } + + header.SetHeader(MiraLocaleManager.GetOrCreateLocaleString(group.GroupName), 20); + if (header.Title) + { + var titleTranslator = header.Title.GetComponent(); + if (titleTranslator) + { + Object.Destroy(titleTranslator); + } + + header.Title.text = group.GroupName.Translate(); + header.Title.color = Color.white; + header.Title.horizontalAlignment = HorizontalAlignmentOptions.Left; + var titleTransform = header.Title.rectTransform; + var titlePosition = titleTransform.localPosition; + titlePosition.x = 0.5f; + titleTransform.localPosition = titlePosition; + titleTransform.SetSizeWithCurrentAnchors( + RectTransform.Axis.Horizontal, + headerWidth - 0.3f); + } + + if (header.Background) + { + header.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); + header.Background.drawMode = SpriteDrawMode.Sliced; + header.Background.color = group.GroupColor; + header.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0f); + header.Background.size = new Vector2(headerWidth, header.Background.size.y); + } + + if (header.Divider) + { + header.Divider.color = group.GroupColor; + } + + header.gameObject.SetActive(true); + foreach (var renderer in header.GetComponentsInChildren(true)) + { + renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + renderer.material.SetInt(PlayerMaterial.MaskLayer, 50); + renderer.sortingOrder = contentSortingOrder; + if (renderer.enabled && renderer.sprite) + { + contentBottom = Mathf.Min( + contentBottom, + inner.InverseTransformPoint(renderer.bounds.min).y); + } + } + + foreach (var text in header.GetComponentsInChildren(true)) + { + text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + text.fontMaterial.SetFloat(ShaderID.Stencil, 50f); + text.renderer.sortingOrder = contentSortingOrder; + if (text.enabled) + { + text.ForceMeshUpdate(); + contentBottom = Mathf.Min( + contentBottom, + inner.InverseTransformPoint(text.bounds.min).y); + } + } + + if (!groupsAsColumns) + { + rowIndex++; + } + } + + previousGroup = group; + } + + var originX = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f + + (column * (columnWidth + columnGap)) + + horizontalOffset; + var centerY = groupsAsColumns && settings[i].Group is { } rowGroup + ? contentTop - groupTopOffsets[rowGroup] - + (rowPitch * (groupRows[rowGroup] + 1)) - headerContentGap - (rowHeight / 2f) + : contentTop - (rowHeight / 2f) - (rowIndex * rowPitch) - groupSpacing; + var row = Object.Instantiate(rowPrefab, page.transform); + row.enabled = false; + row.PlusBtn.gameObject.SetActive(false); + row.MinusBtn.gameObject.SetActive(false); + + foreach (var button in row.GetComponentsInChildren(true)) + { + button.enabled = false; + } + + foreach (var collider in row.GetComponentsInChildren(true)) + { + collider.enabled = false; + } + + row.transform.localScale = rowPrefab.transform.localScale * rowScale; + row.transform.localPosition = new Vector3( + originX + (columnWidth / 2f) - (prefabBounds.center.x * rowScale), + centerY - (prefabBounds.center.y * rowScale), + 0f); + row.TitleText.text = settings[i].Title; + row.ValueText.text = settings[i].Value; + + foreach (var text in row.GetComponentsInChildren(true)) + { + if (text.TryGetComponent(out var translator)) + { + translator.enabled = false; + Object.Destroy(translator); + } + + text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + text.fontMaterial.SetFloat(ShaderID.Stencil, 50f); + text.renderer.sortingOrder = contentSortingOrder; + if (text.enabled) + { + text.ForceMeshUpdate(); + } + } + + foreach (var renderer in row.GetComponentsInChildren(true)) + { + renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + renderer.material.SetInt(PlayerMaterial.MaskLayer, 50); + renderer.sortingOrder = contentSortingOrder; + if (renderer.enabled && + renderer.sprite && + renderer.transform != row.PlusBtn.transform && + !renderer.transform.IsChildOf(row.PlusBtn.transform) && + renderer.transform != row.MinusBtn.transform && + !renderer.transform.IsChildOf(row.MinusBtn.transform)) + { + contentBottom = Mathf.Min( + contentBottom, + inner.InverseTransformPoint(renderer.bounds.min).y); + } + } + + foreach (var text in row.GetComponentsInChildren(true)) + { + if (text.enabled) + { + contentBottom = Mathf.Min( + contentBottom, + inner.InverseTransformPoint(text.bounds.min).y); + } + } + + row.gameObject.SetActive(true); + if (groupsAsColumns && settings[i].Group is { } renderedGroup) + { + groupRows[renderedGroup]++; + } + else + { + column++; + if (column == columns) + { + column = 0; + rowIndex++; + } + } + } + + var maskArea = parent.transform.FindChild("MaskArea")?.GetComponent(); + var maskBottom = maskArea + ? inner.InverseTransformPoint(maskArea!.bounds.min).y + : -0.75f; + var contentOverflow = float.IsPositiveInfinity(contentBottom) + ? 0f + : maskBottom - contentBottom; + var scrollRange = contentOverflow > 0.05f + ? contentOverflow + 0.05f + : 0f; + parent.SetYBoundsMax(Mathf.Clamp(scrollRange, 0f, 999f)); + page.SetActive(true); + return page; + } + + private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability, bool variableTextSizing = false) + { + panel.roleCount.text = ability.AbilityType; + panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0); + panel.roleName.text = ability.Name; + panel.roleName.transform.localPosition += new Vector3(-0.04f, 0); + panel.roleDescription.text = ability.Description; + panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f); + panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f); + if (variableTextSizing) + { + panel.roleDescription.alignment = TextAlignmentOptions.Top; + panel.roleDescription.fontSizeMin = 1.5f; + panel.roleDescription.ForceMeshUpdate(); + } + + panel.roleIcon.sprite = ability.Icon.LoadAsset(); + panel.roleIcon.SetSizeLimit(0.13f); + + panel.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); + panel.roleName.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); + panel.roleDescription.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); + panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); + panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f); + } } diff --git a/MiraAPI/Utilities/LoadingRing.cs b/MiraAPI/Utilities/LoadingRing.cs new file mode 100644 index 0000000..3f8af87 --- /dev/null +++ b/MiraAPI/Utilities/LoadingRing.cs @@ -0,0 +1,88 @@ +using System; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.Utilities; + +/// +/// A spinning ring used to indicate that content is loading. +/// +[RegisterInIl2Cpp] +public class LoadingRing(IntPtr cppPtr) : MonoBehaviour(cppPtr) +{ + private static Sprite? _ringSprite; + + /// + /// Gets or sets the rotation speed in degrees per second. + /// + public float Speed { get; set; } = 270f; + + /// + /// Creates a on the UI layer. + /// + /// The transform to parent the spinner to. + /// The local position of the spinner. + /// The uniform scale of the spinner. + /// The created . + public static LoadingRing Create(Transform parent, Vector3 localPosition, float scale) + { + if (!_ringSprite) + { + _ringSprite = CreateRingSprite(); + } + + var obj = new GameObject("LoadingRing") + { + transform = + { + parent = parent, + localPosition = localPosition, + localScale = Vector3.one * scale, + }, + layer = LayerMask.NameToLayer("UI"), + }; + var renderer = obj.AddComponent(); + renderer.sprite = _ringSprite; + return obj.AddComponent(); + } + + /// + /// Rotates the ring. + /// + public void Update() + { + transform.Rotate(0f, 0f, -Speed * Time.deltaTime); + } + + private static Sprite CreateRingSprite() + { + const int size = 128; + const float innerRadius = 46f; + const float outerRadius = 60f; + var texture = new Texture2D(size, size, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Bilinear, + wrapMode = TextureWrapMode.Clamp, + }; + var pixels = new Color[size * size]; + for (var y = 0; y < size; y++) + { + for (var x = 0; x < size; x++) + { + var dx = x + 0.5f - (size / 2f); + var dy = y + 0.5f - (size / 2f); + var radius = Mathf.Sqrt((dx * dx) + (dy * dy)); + var ring = Mathf.Clamp01(radius - innerRadius) * Mathf.Clamp01(outerRadius - radius); + var angle = (Mathf.Atan2(dy, dx) + Mathf.PI) / (2f * Mathf.PI); + pixels[(y * size) + x] = new Color(1f, 1f, 1f, ring * Mathf.Lerp(1f, 0.1f, angle)); + } + } + + texture.SetPixels(pixels); + texture.Apply(); + texture.hideFlags = HideFlags.HideAndDontSave; + var sprite = Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f); + sprite.hideFlags = HideFlags.HideAndDontSave; + return sprite; + } +} diff --git a/MiraAPI/Utilities/MathUtilities.cs b/MiraAPI/Utilities/MathUtilities.cs new file mode 100644 index 0000000..a505a36 --- /dev/null +++ b/MiraAPI/Utilities/MathUtilities.cs @@ -0,0 +1,82 @@ +using System; + +namespace MiraAPI.Utilities; + +/// +/// Provides mathematical utility methods. +/// +public static class MathUtilities +{ + /// + /// Linearly interpolates between two values, clamping the interpolant between 0 and 1. + /// + /// The starting value. + /// The ending value. + /// The interpolation value, automatically clamped to a 0 to 1 range. + /// The interpolated float. + public static float Lerp(float a, float b, float t) + { + return a + ((b - a) * Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting value. + /// The ending value. + /// The interpolation value. + /// The interpolated float. + public static float LerpUnclamped(float a, float b, float t) + { + return a + ((b - a) * t); + } + + /// + /// Clamps a floating-point value strictly between 0 and 1. + /// + /// The floating-point value to clamp. + /// The clamped value. + public static float Clamp01(float value) + { + return Math.Clamp(value, 0f, 1f); + } + + /// + /// Ping-pongs a value back and forth between a minimum and maximum value. + /// + /// The input time or interpolant. + /// The minimum boundary of the ping-pong wave. + /// The maximum boundary of the ping-pong wave. + /// The multiplier for the speed of the ping-pong effect. + /// A value oscillating continuously between and . + public static float PingPong(float t, float min, float max, float speed = 1f) + { + return min + PingPong(t, max - min, speed); + } + + /// + /// Ping-pongs a value back and forth between 0 and a specified length. + /// + /// The input time or interpolant. + /// The maximum value to bounce against, starting from 0. + /// The multiplier for the speed of the ping-pong effect. + /// A value oscillating continuously between 0 and . + public static float PingPong(float t, float length, float speed = 1f) + { + var dx = speed * t; + var floor = (int)MathF.Floor(dx); + var phase = floor % 2; // 0 = forward, 1 = backward + return Clamp01(((dx - floor) * ((2 * phase) - 1)) + 1 - phase) * length; + } + + /// + /// Loops a value strictly between 0 and a maximum length, behaving like a standard float modulo. + /// + /// The input value to loop. + /// The maximum length of the repeating sequence. + /// The wrapped value smoothly looping back to 0 upon reaching . + public static float Repeat(float t, float length) + { + return Math.Clamp(t - (MathF.Floor(t / length) * length), 0f, length); + } +} diff --git a/MiraAPI/Utilities/MiraNumberSuffixes.cs b/MiraAPI/Utilities/MiraNumberSuffixes.cs index 2f55226..d434814 100644 --- a/MiraAPI/Utilities/MiraNumberSuffixes.cs +++ b/MiraAPI/Utilities/MiraNumberSuffixes.cs @@ -24,4 +24,9 @@ public enum MiraNumberSuffixes /// Percentage suffix (%). /// Percent, + + /// + /// Distance suffix (u). + /// + Distance, } diff --git a/MiraAPI/Utilities/ShaderID.cs b/MiraAPI/Utilities/ShaderID.cs index 3f2855a..7c3ec5c 100644 --- a/MiraAPI/Utilities/ShaderID.cs +++ b/MiraAPI/Utilities/ShaderID.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using UnityEngine; -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace MiraAPI.Utilities; @@ -11,6 +10,11 @@ public static class ShaderID { private static readonly Dictionary Cache = []; + /// + /// Retrieves the cached shader property ID for the specified name. If it is not cached, it registers and caches the ID before returning it. + /// + /// The name of the shader property to look up. + /// The integer ID of the shader property. public static int Get(string name) { if (Cache.TryGetValue(name, out var id)) @@ -23,81 +27,233 @@ public static class ShaderID return id; } - // For player shader + // For player shaders + + /// + /// The shader property ID for "_BodyColor". + /// public static readonly int BodyColor = Get("_BodyColor"); + + /// + /// The shader property ID for "_BackColor". + /// public static readonly int BackColor = Get("_BackColor"); + + /// + /// The shader property ID for "_VisorColor". + /// public static readonly int VisorColor = Get("_VisorColor"); // Main texture, very obviously used in any shader with a texture + + /// + /// The shader property ID for "_MainTex". + /// public static readonly int MainTex = Get("_MainTex"); // Any masking stuff, like MeetingHud bubbles + + /// + /// The shader property ID for "_Mask". + /// public static readonly int Mask = Get("_Mask"); + + /// + /// The shader property ID for "_MaskComp". + /// public static readonly int MaskComp = Get("_MaskComp"); + + /// + /// The shader property ID for "_MaskLayer". + /// public static readonly int MaskLayer = Get("_MaskLayer"); + + /// + /// The shader property ID for "_Stencil". + /// public static readonly int Stencil = Get("_Stencil"); + + /// + /// The shader property ID for "_StencilComp". + /// public static readonly int StencilComp = Get("_StencilComp"); // Used in many tasks + + /// + /// The shader property ID for "_Color". + /// public static readonly int Color = Get("_Color"); // Has 2 uses in the game, provided for convenience + + /// + /// The shader property ID for "_Opacity". + /// public static readonly int Opacity = Get("_Opacity"); // Used once in CooldownHelpers, once in PowerBarMining + + /// + /// The shader property ID for "_NormalizedUvs". + /// public static readonly int NormalizedUvs = Get("_NormalizedUvs"); // Used in many consoles + + /// + /// The shader property ID for "_Outline". + /// public static readonly int Outline = Get("_Outline"); + + /// + /// The shader property ID for "_OutlineColor". + /// public static readonly int OutlineColor = Get("_OutlineColor"); // Used in some consoles + + /// + /// The shader property ID for "_AddColor". + /// public static readonly int AddColor = Get("_AddColor"); // Has some uses in various locations + + /// + /// The shader property ID for "_Percent". + /// public static readonly int Percent = Get("_Percent"); + + /// + /// The shader property ID for "_PercentY". + /// public static readonly int PercentY = Get("_PercentY"); + + /// + /// The shader property ID for "_Desat". + /// public static readonly int Desat = Get("_Desat"); // Used in LightSource + + /// + /// The shader property ID for "_PlayerRadius". + /// public static readonly int PlayerRadius = Get("_PlayerRadius"); + + /// + /// The shader property ID for "_LightRadius". + /// public static readonly int LightRadius = Get("_LightRadius"); + + /// + /// The shader property ID for "_LightOffset". + /// public static readonly int LightOffset = Get("_LightOffset"); + + /// + /// The shader property ID for "_FlashlightSize". + /// public static readonly int FlashlightSize = Get("_FlashlightSize"); + + /// + /// The shader property ID for "_FlashlightAngle". + /// public static readonly int FlashlightAngle = Get("_FlashlightAngle"); // Used once for LightSourceGpuRenderer + + /// + /// The shader property ID for "_DepthCompressionValue". + /// public static readonly int DepthCompressionValue = Get("_DepthCompressionValue"); // Used in ProgressTracker + + /// + /// The shader property ID for "_Buckets". + /// public static readonly int Buckets = Get("_Buckets"); + + /// + /// The shader property ID for "_FullBuckets". + /// public static readonly int FullBuckets = Get("_FullBuckets"); // Used once in IntroCutscene and EndGameManager + + /// + /// The shader property ID for "_Rad". + /// public static readonly int Rad = Get("_Rad"); // Used only once for Quick Chat + + /// + /// The shader property ID for "_FaceColor". + /// public static readonly int FaceColor = Get("_FaceColor"); // Used for NavigationMinigame + + /// + /// The shader property ID for "_CrossHair". + /// public static readonly int CrossHair = Get("_CrossHair"); + + /// + /// The shader property ID for "_CrossColor". + /// public static readonly int CrossColor = Get("_CrossColor"); // Used for both SurveillanceMinigame (Planet and Normal) + + /// + /// The shader property ID for "_Center". + /// public static readonly int Center = Get("_Center"); + + /// + /// The shader property ID for "_Color2". + /// public static readonly int Color2 = Get("_Color2"); // Used in ReactorShipRoom + + /// + /// The shader property ID for "_Speed". + /// public static readonly int Speed = Get("_Speed"); // Used only once in CourseMinigame + + /// + /// The shader property ID for "_AltTex". + /// public static readonly int AltTex = Get("_AltTex"); + + /// + /// The shader property ID for "_Perc". + /// public static readonly int Perc = Get("_Perc"); // Used once in TextMarquee + + /// + /// The shader property ID for "_VertexOffsetX". + /// public static readonly int VertexOffsetX = Get("_VertexOffsetX"); + + /// + /// The shader property ID for "_VertexOffsetY". + /// public static readonly int VertexOffsetY = Get("_VertexOffsetY"); // Used in VertLineBehaviour + + /// + /// The shader property ID for "_Fade". + /// public static readonly int Fade = Get("_Fade"); } diff --git a/MiraAPI/Utilities/StateMachineWrapper.cs b/MiraAPI/Utilities/StateMachineWrapper.cs index 3c05334..6b9d683 100644 --- a/MiraAPI/Utilities/StateMachineWrapper.cs +++ b/MiraAPI/Utilities/StateMachineWrapper.cs @@ -11,7 +11,8 @@ namespace MiraAPI.Utilities; /// A wrapper for state machine objects to access their parent instance and state. /// /// The type of the parent class that owns the state machine. -public class StateMachineWrapper where T : Il2CppObjectBase +public class StateMachineWrapper + where T : Il2CppObjectBase { private readonly Il2CppObjectBase _stateMachine; @@ -53,19 +54,28 @@ public class StateMachineWrapper where T : Il2CppObjectBase /// Gets the current state of the state machine. /// /// The current state as an integer. - public int GetState() => (int)_stateProperty.GetValue(_stateMachine)!; + public int GetState() + { + return (int)_stateProperty.GetValue(_stateMachine)!; + } /// /// Sets the current state of the state machine. /// /// The new state to use. - public void SetState(int newState) => _stateProperty.SetValue(_stateMachine, newState); + public void SetState(int newState) + { + _stateProperty.SetValue(_stateMachine, newState); + } /// /// Sets the newest yield return of the state machine. /// /// The new return to use. - public void SetRecentReturn(Object newReturn) => _currentProperty.SetValue(_stateMachine, newReturn); + public void SetRecentReturn(Object newReturn) + { + _currentProperty.SetValue(_stateMachine, newReturn); + } /// /// Gets a parameter of type from the state machine by its name. diff --git a/MiraAPI/VanillaEvents/JudgeEvents.cs b/MiraAPI/VanillaEvents/JudgeEvents.cs index 4a20c6e..6cfd526 100644 --- a/MiraAPI/VanillaEvents/JudgeEvents.cs +++ b/MiraAPI/VanillaEvents/JudgeEvents.cs @@ -4,25 +4,26 @@ using MiraAPI.Roles; namespace MiraAPI.VanillaEvents; -public static class JudgeEvents +internal 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); + MiraEventManager.RegisterEventHandler(ProcessVotesEventHandler, -1000); } - public static void ProcessVotesEventHandler(ProcessVotesEvent @event) + private 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; + if (!MeetingHud.Instance.TryGetWinningOverrule( + out var judgeOverrule, + out var networkedPlayerInfo, + out var networkedPlayerInfo2)) return; + 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; - } + @event.ExiledPlayer = networkedPlayerInfo2.Role is ICustomRole { Team: not ModdedRoleTeams.Crewmate } || networkedPlayerInfo2.Role.TeamType == RoleTeamTypes.Impostor + ? GameData.Instance.GetPlayerById(judgeOverrule.OverruledPlayerId) + : networkedPlayerInfo; } } diff --git a/MiraAPI/VanillaEvents/UiResetEvents.cs b/MiraAPI/VanillaEvents/UiResetEvents.cs index fe0e6e5..84d0022 100644 --- a/MiraAPI/VanillaEvents/UiResetEvents.cs +++ b/MiraAPI/VanillaEvents/UiResetEvents.cs @@ -5,17 +5,17 @@ using MiraAPI.LocalSettings; namespace MiraAPI.VanillaEvents; -public static class UiResetEvents +internal static class UiResetEvents { public static void Initialize() { - MiraEventManager.RegisterEventHandler(@event => ResetButtonParents(@event)); - MiraEventManager.RegisterEventHandler(@event => PlaceWikiButton(@event), -900); - MiraEventManager.RegisterEventHandler(@event => PlaceSubmergedButton(@event), -800); - MiraEventManager.RegisterEventHandler(@event => PlaceModifierUi(@event), -700); + MiraEventManager.RegisterEventHandler(ResetButtonParents); + MiraEventManager.RegisterEventHandler(PlaceWikiButton, -900); + MiraEventManager.RegisterEventHandler(PlaceSubmergedButton, -800); + MiraEventManager.RegisterEventHandler(PlaceModifierUi, -700); } - public static void ResetButtonParents(UiButtonResetEvent @event) + private static void ResetButtonParents(UiButtonResetEvent _) { var wikiButton = MiraHudHelper.VanillaMatchInfoButton; var subButton = MiraHudHelper.SubmergedFloorButton; @@ -24,47 +24,52 @@ public static class UiResetEvents { wikiButton.transform.SetParent(null); } + if (subButton) { subButton.transform.SetParent(null); } + if (modDisplay) { modDisplay.transform.SetParent(null); } } - public static void PlaceWikiButton(UiButtonPostResetEvent @event) + private static void PlaceWikiButton(UiButtonPostResetEvent @event) { var wikiButton = MiraHudHelper.VanillaMatchInfoButton; if (!wikiButton) { return; } + var firstRow = @event.MainTopUiRow; var secondRow = @event.SecondTopUiRow; var opts = LocalSettingsTabSingleton.Instance; wikiButton.transform.SetParent(opts.WikiOnBottomRow.Value ? secondRow.transform : firstRow.transform); } - public static void PlaceSubmergedButton(UiButtonPostResetEvent @event) + private static void PlaceSubmergedButton(UiButtonPostResetEvent @event) { var subButton = MiraHudHelper.SubmergedFloorButton; if (!subButton) { return; } + var secondRow = @event.SecondTopUiRow; subButton.transform.SetParent(secondRow.transform); } - public static void PlaceModifierUi(UiButtonPostResetEvent @event) + private static void PlaceModifierUi(UiButtonPostResetEvent @event) { var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; if (!modDisplay) { return; } + var secondRow = @event.SecondTopUiRow; modDisplay.transform.SetParent(secondRow.transform); } diff --git a/MiraAPI/Voting/PlayerVoteData.cs b/MiraAPI/Voting/PlayerVoteData.cs index 0e52078..45ab8b7 100644 --- a/MiraAPI/Voting/PlayerVoteData.cs +++ b/MiraAPI/Voting/PlayerVoteData.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Il2CppInterop.Runtime.Attributes; using Reactor.Utilities.Attributes; using UnityEngine; @@ -34,6 +35,7 @@ public class PlayerVoteData(nint cppPtr) : MonoBehaviour(cppPtr) private int _votesRemaining = 1; + [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] private void Awake() { Owner = GetComponent(); @@ -78,7 +80,7 @@ public class PlayerVoteData(nint cppPtr) : MonoBehaviour(cppPtr) /// The target's playerId. public void RemovePlayerVote(byte playerId) { - Votes.Remove(Votes.Find(x=>x.Suspect==playerId)); + Votes.Remove(Votes.Find(x => x.Suspect == playerId)); } /// diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index a1f989d..ad9e32c 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using InnerNet; using MiraAPI.Events; @@ -41,6 +42,7 @@ public static class VotingUtils /// /// The player's . /// Who the player voted for. + /// Flag that indicates that the vote was done by a Judge. /// Whether you want the vote to commence or not. private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, bool byJudge, out bool cancelVote) { @@ -69,6 +71,7 @@ public static class VotingUtils { voteData.DecreaseRemainingVotes(1); } + voteData.VoteForPlayer(suspectIdx); } @@ -112,15 +115,25 @@ public static class VotingUtils /// 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 id of the player who cast 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)] + [SuppressMessage( + "Style", + "IDE0060:Remove unused parameter", + Justification = "Required parameter: The MethodRpc system mandates an InnerNetObject (or its derived class) as the first parameter for sender context, even if unused in the method body.")] public static void RpcQueueOverruleVotes(PlayerControl source, byte srcPlayerId, byte suspectPlayerId, ushort overruleNonce) { CustomCastJudgeVote(srcPlayerId, suspectPlayerId, overruleNonce); } + /// + /// Casts a custom Judge vote. + /// + /// The Judge's player ID. + /// The unfortunate soul's player ID. + /// Overruled. public static void CustomCastJudgeVote(PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) { var plr = GameData.Instance.GetPlayerById(judgePlayerId.Value); @@ -173,14 +186,23 @@ public static class VotingUtils /// Networks the casting of a vote. We replace the vanilla solution with a custom version that works for the use case. /// /// The who sent this RPC. - /// The id of the player who casted the vote. + /// The id of the player who cast the vote. /// The voted player's id. [MethodRpc((uint)MiraRpc.CastVote)] + [SuppressMessage( + "Style", + "IDE0060:Remove unused parameter", + Justification = "Required parameter: The MethodRpc system mandates an InnerNetObject (or its derived class) as the first parameter for sender context, even if unused in the method body.")] public static void RpcCastVote(PlayerControl source, byte srcPlayerId, byte suspectPlayerId) { CustomCastVote(srcPlayerId, suspectPlayerId); } + /// + /// Casts a custom vote. + /// + /// The casting player's ID. + /// The voted player's ID. public static void CustomCastVote(byte srcPlayerId, byte suspectPlayerId) { var plr = GameData.Instance.GetPlayerById(srcPlayerId); @@ -237,7 +259,7 @@ public static class VotingUtils { var dictionary = new Dictionary(); - foreach (var vote in votes.Select(v=>v.Suspect)) + foreach (var vote in votes.Select(v => v.Suspect)) { if (!dictionary.TryAdd(vote, 1)) { @@ -267,7 +289,7 @@ public static class VotingUtils /// The list of s. public static void HandlePopulateResults(List votes) { - PopulateResultsEvent @event = new PopulateResultsEvent(votes); + var @event = new PopulateResultsEvent(votes); MiraEventManager.InvokeEvent(@event); if (@event.IsCancelled) @@ -278,10 +300,7 @@ public static class VotingUtils // If modified, these will visually change. votes = @event.Votes; - MeetingHud.Instance.TitleText.text = - TranslationController.Instance.GetString( - StringNames.MeetingVotingResults, - Il2CppSystem.Array.Empty()); + MeetingHud.Instance.TitleText.text = TranslationController.Instance.GetString(StringNames.MeetingVotingResults); var delays = new Dictionary(); var num = 0; diff --git a/packages/AmongUs.GameLibs.Steam.2025.10.14.nupkg b/packages/AmongUs.GameLibs.Steam.2025.10.14.nupkg deleted file mode 100644 index f693dda..0000000 Binary files a/packages/AmongUs.GameLibs.Steam.2025.10.14.nupkg and /dev/null differ diff --git a/stylecop.json b/stylecop.json index 21a7e94..4cb4b89 100644 --- a/stylecop.json +++ b/stylecop.json @@ -13,4 +13,4 @@ "documentInterfaces": false } } -} \ No newline at end of file +} diff --git a/stylecop.ruleset b/stylecop.ruleset deleted file mode 100644 index 73b4f35..0000000 --- a/stylecop.ruleset +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file