diff --git a/AmongUs.props b/AmongUs.props index afc343f..0819a88 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -2,7 +2,7 @@ - + @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/MiraAPI.Example/ExampleAssets.cs b/MiraAPI.Example/ExampleAssets.cs index 903afd8..b0ca484 100644 --- a/MiraAPI.Example/ExampleAssets.cs +++ b/MiraAPI.Example/ExampleAssets.cs @@ -9,5 +9,6 @@ public static class ExampleAssets // Credit to EpicHorrors for the teleport button asset. public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png"); + public static LoadableResourceAsset SharpshootButton { get; } = new("MiraAPI.Example.Resources.SharpshootButton.png"); public static LoadableResourceAsset Banner { get; } = new("MiraAPI.Example.Resources.FortniteBanner.jpeg"); } diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index 3ff65d6..c63fe39 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -4,9 +4,11 @@ using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Translation; +using MiraAPI.Utilities.Assets; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; +using UnityEngine; namespace MiraAPI.Example; @@ -23,6 +25,7 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin } public Harmony Harmony { get; } = new(Id); public string OptionsTitleText => "Mira API\nExample Mod"; + public string CustomOptionMenuNameTwo => "angxl's Options"; public ConfigFile GetConfigFile() => Config; public override void Load() { diff --git a/MiraAPI.Example/GameModes/ExampleMode.cs b/MiraAPI.Example/GameModes/ExampleMode.cs new file mode 100644 index 0000000..c15e1d5 --- /dev/null +++ b/MiraAPI.Example/GameModes/ExampleMode.cs @@ -0,0 +1,13 @@ +using MiraAPI.GameModes; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.GameModes; + +public class ExampleMode : AbstractGameMode +{ + public override string Name => "Example Mode"; + public override string Description => "An example gamemode."; + public override LoadableAsset? Icon => ExampleAssets.ExampleButton; + public override Color Color => Color.red; +} diff --git a/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs b/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs new file mode 100644 index 0000000..0d39027 --- /dev/null +++ b/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs @@ -0,0 +1,33 @@ +using MiraAPI.Example.Modifiers; +using MiraAPI.Example.Roles; +using MiraAPI.Hud; +using MiraAPI.Keybinds; +using MiraAPI.MeetingAbilities; +using MiraAPI.Modifiers; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.MeetingAbilities; + +public class EndMeetingButton : MeetingActionButton +{ + public override string Name => "End Meeting"; + + public override float Cooldown => 15; + + public override float InitialCooldown => 30; + + public override int MaxUses => 3; + + public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + + public override bool Enabled(RoleBehaviour? role) + { + return PlayerControl.LocalPlayer != null && role is MayorRole; + } + + protected override void OnClick() + { + MeetingHud.Instance.RpcClose(); + } +} diff --git a/MiraAPI.Example/MeetingAbilities/ShootButton.cs b/MiraAPI.Example/MeetingAbilities/ShootButton.cs new file mode 100644 index 0000000..ad5f336 --- /dev/null +++ b/MiraAPI.Example/MeetingAbilities/ShootButton.cs @@ -0,0 +1,34 @@ +using MiraAPI.MeetingAbilities; +using MiraAPI.Networking; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.MeetingAbilities; + +public class ShootButton : TargetedMeetingButton +{ + public override string Name => "Shoot"; + + public override int MaxUses => 3; + + public override float InitialCooldown => 5; + + public override float Cooldown => 3; + + public override LoadableAsset Sprite => ExampleAssets.SharpshootButton; + + public override Color OutlineColor => Color.red; + + public override bool Enabled(RoleBehaviour r) + { + return r.IsImpostor; + } + + protected override void OnClick(PlayerVoteArea playerVoteArea) + { + playerVoteArea.SetDisabled(); + var player = playerVoteArea.GetPlayer(); + PlayerControl.LocalPlayer.RpcCustomMurder(player); + } +} \ No newline at end of file diff --git a/MiraAPI.Example/MeetingAbilities/ShotgunButton.cs b/MiraAPI.Example/MeetingAbilities/ShotgunButton.cs new file mode 100644 index 0000000..bc9f20c --- /dev/null +++ b/MiraAPI.Example/MeetingAbilities/ShotgunButton.cs @@ -0,0 +1,51 @@ +using System; +using MiraAPI.MeetingAbilities; +using MiraAPI.Networking; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.MeetingAbilities; + +public class ShotgunButton : MultiTargetMeetingButton +{ + public override string Name => "Shotgun"; + + public override int MaxUses => 3; + + public override float InitialCooldown => 5; + + public override float Cooldown => 3; + + public override LoadableAsset Sprite => ExampleAssets.SharpshootButton; + + public override Color OutlineColor => Color.red; + + public override bool Enabled(RoleBehaviour r) + { + return r.IsImpostor; + } + + protected override void OnClick(PlayerVoteArea playerVoteArea) + { + } + + public override LoadableAsset SpriteActive => ExampleAssets.CallMeetingButton; + + public override int TargetsCount => 4; + + protected override void OnSelect(PlayerVoteArea playerVoteArea, bool toggle) + { + HudManager.Instance.PlayerCam.ShakeScreen(0.5f, 2); + } + + public override void OnFinish() + { + foreach (var playerVoteArea in Targets.Keys) + { + playerVoteArea.SetDisabled(); + var player = playerVoteArea.GetPlayer(); + PlayerControl.LocalPlayer.RpcCustomMurder(player); + } + } +} \ No newline at end of file diff --git a/MiraAPI.Example/Resources/SharpshootButton.png b/MiraAPI.Example/Resources/SharpshootButton.png new file mode 100644 index 0000000..f8ccae6 Binary files /dev/null and b/MiraAPI.Example/Resources/SharpshootButton.png differ diff --git a/MiraAPI/Events/Mira/MiraButtonClickEvent.cs b/MiraAPI/Events/Mira/MiraButtonClickEvent.cs index 401cc11..0b18028 100644 --- a/MiraAPI/Events/Mira/MiraButtonClickEvent.cs +++ b/MiraAPI/Events/Mira/MiraButtonClickEvent.cs @@ -6,7 +6,7 @@ namespace MiraAPI.Events.Mira; /// Button click event for s only. Do not use for vanilla s. /// /// The type. -public sealed class MiraButtonClickEvent : MiraCancelableEvent where T : CustomActionButton +public class MiraButtonClickEvent : MiraCancelableEvent where T : CustomActionButton { /// /// Gets the that was clicked. @@ -33,7 +33,7 @@ public sealed class MiraButtonClickEvent : MiraCancelableEvent where T : Cust /// /// Button click event for s only. Do not use for vanilla s. /// -public sealed class MiraButtonClickEvent : MiraCancelableEvent +public class MiraButtonClickEvent : MiraCancelableEvent { /// /// Gets the that was clicked. diff --git a/MiraAPI/Events/MiraCancelableEvent.cs b/MiraAPI/Events/MiraCancelableEvent.cs index b4d3f05..fe772a9 100644 --- a/MiraAPI/Events/MiraCancelableEvent.cs +++ b/MiraAPI/Events/MiraCancelableEvent.cs @@ -1,8 +1,11 @@ -namespace MiraAPI.Events; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Events; /// /// Abstract class for Mira Events that can be cancelled. /// +[MiraIgnore] public abstract class MiraCancelableEvent : MiraEvent { /// diff --git a/MiraAPI/Events/MiraEvent.cs b/MiraAPI/Events/MiraEvent.cs index f0f49bd..7fdca49 100644 --- a/MiraAPI/Events/MiraEvent.cs +++ b/MiraAPI/Events/MiraEvent.cs @@ -1,6 +1,9 @@ -namespace MiraAPI.Events; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Events; /// /// Abstract class for Mira Events. /// +[MiraIgnore] public abstract class MiraEvent; diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs index 2145b32..8f29544 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs @@ -34,6 +34,11 @@ public class HandleVoteEvent : MiraCancelableEvent /// public NetworkedPlayerInfo TargetPlayerInfo { get; } + /// + /// Gets or sets a value indicating whether the vote event is an Overrule caused by a Judge. + /// + public bool IsOverruling { get; set; } + /// /// Initializes a new instance of the class. /// @@ -47,4 +52,20 @@ public class HandleVoteEvent : MiraCancelableEvent PreventVote = false; TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId); } + + /// + /// Initializes a new instance of the class. + /// + /// The voter's . + /// The target's playerId. + /// Whether the vote is actually for a Judge. + public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId, bool isOverruling) + { + VoteData = playerVoteData; + Player = playerVoteData.Owner; + TargetId = targetId; + PreventVote = false; + IsOverruling = isOverruling; + TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId); + } } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs index a61cc51..8e4894a 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs @@ -33,6 +33,16 @@ public class ProcessVotesEvent(List votes, NetworkedPlayerInfo? exil /// public List Votes { get; } = votes; + /// + /// Gets or sets a value indicating whether an overruling was done. Normally set by judge but can be overwritten. + /// + public bool OverruledVote { get; set; } + + /// + /// Gets or sets a value specifically to determine which Judge (if any) caused the overruling. Normally set by judge but can be overwritten. + /// + public ushort OverruledNonce { get; set; } + /// /// Gets or sets the player to be exiled. Will be if no player is to be exiled. /// diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs new file mode 100644 index 0000000..6fa2d8c --- /dev/null +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -0,0 +1,296 @@ +using System.Collections; +using System.Collections.Generic; +using AmongUs.GameOptions; +using MiraAPI.GameOptions; +using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; + +namespace MiraAPI.GameModes; + +/// +/// Base class for custom gamemodes. +/// +[MiraIgnore] +public abstract class AbstractGameMode : IOptionable +{ + /// + /// Gets the game mode name. + /// + public abstract string Name { get; } + + /// + /// Gets a value indicating whether the mode is visible in the menu. Only checked at startup. + /// + public virtual bool HideMode => false; + + /// + /// Gets the game mode description. + /// + public abstract string Description { get; } + + /// + /// Gets the game mode description. + /// + public virtual Color Color { get; } = Color.white; + + /// + /// Gets the icon of the gamemode. Will be shown in options menu underneath the gamemode option. + /// + public virtual LoadableAsset? Icon => null; + + /// + /// Gets the icon of the gamemode for use on the options pop-up. + /// + public TMP_SpriteAsset TmpIcon { get; set; } = null!; + + /// + /// Gets the colored game mode name, using the color and name properties. + /// + public string ColoredName => $"{this.Name}"; + + /// + /// Gets the game mode id. + /// + public uint ID { get; internal set; } + + /// + /// Gets a value indicating whether a specific body type is used by the game mode. + /// + public virtual bool GameModeBodyTypeOverride => false; + + /// + /// Gets a value indicating whether the gamemode removes the normal game settings. + /// + public virtual bool ShowNormalGameSettings => true; + + /// + /// Gets a value indicating whether the gamemode removes the normal role settings. + /// + public virtual bool ShowNormalRoleSettings => true; + + /// + /// Gets a value indicating the default kill cooldown for impostors in this mode. + /// + public virtual float DefaultImpostorKillCooldown => GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown); + + /// + /// Called on GameManager.GetBodyType(). + /// + /// Player to get body type from. + /// Resulting body type. + public virtual PlayerBodyTypes GetBodyType(PlayerControl player) + { + if (AprilFoolsMode.ShouldHorseAround()) + { + return PlayerBodyTypes.Horse; + } + if (AprilFoolsMode.ShouldLongAround()) + { + return PlayerBodyTypes.Long; + } + return PlayerBodyTypes.Normal; + } + + /// + /// Gets a value indicating whether a custom intro sequence is implemented by the game mode. + /// + public virtual bool ShowGameModeIntroCutscene => false; + + /// + /// Called when Intro Cutscene is destroyed. + /// + public virtual void Initialize() + { + } + + /// + /// Called on HudManager.Start(). + /// + /// Instance of HudManager. + public virtual void HudStart(HudManager instance) + { + } + + /// + /// Called every frame on HudManager.Update(). + /// + /// Instance of HudManager. + public virtual void HudUpdate(HudManager instance) + { + } + + /// + /// Called when a player is killed. + /// + /// PlayerControl that was killed. + /// Determines whether to assign a ghost role on the player. + public virtual void OnPlayerDeath(PlayerControl player, bool assignGhostRole) + { + GameManager.Instance.LogicRoleSelection.OnPlayerDeath(player, assignGhostRole); + } + + /// + /// Calculate Game End logic. + /// + /// Should original game end logic be used. + /// Instance of LogicGameFlowNormal. + public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) + { + runOriginal = true; + } + + /// + /// Custom role assignment. + /// + /// Should original role assignment be used. + /// Instance of LogicRoleSelection. + public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) + { + runOriginal = true; + } + + /// + /// Custom role assignment to run after all roles are handed out. + /// + /// Instance of LogicRoleSelection. + public virtual void PostAssignRoles(LogicRoleSelectionNormal instance) + { + } + + /// + /// Custom murder logic. + /// + /// Should the original murder logic be used. + /// Result of custom murder logic. + /// Target player for murder. + public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target) + { + result = false; + runOriginal = true; + } + + /// + /// Custom winner selection. + /// + /// List of winners or null. + public virtual List? CalculateWinners() => null; + + /// + /// The IEnumerator that plays the intro cutscene for this gamemode. + /// + /// An instance of IntroCutscene. + /// An IEnumerator to run the intro cutscene instead of the base game one. + public virtual IEnumerator IntroCutscene(IntroCutscene __instance) + { + yield return new WaitForEndOfFrame(); + } + + /// + /// Can Admin be used in this gamemode. + /// + /// Admin Console. + /// True if Admin console is enabled. + public virtual bool CanUseMapConsole(MapConsole console) => true; + + /// + /// Can a body be reported in this gamemode. + /// + /// Target body for reporting. + /// True if dead bodies can be reported. + public virtual bool CanReport(DeadBody body) => true; + + /// + /// Can system consoles be used in this gamemode. + /// + /// System Console. + /// True if system consoles are enabled in this mode. + public virtual bool CanUseSystemConsole(SystemConsole console) => true; + + /// + /// Can tasks be interacted with in this gamemode. + /// + /// Task console. + /// True if tasks are enabled in this mode. + public virtual bool CanUseTasks(Console console) => true; + + /// + /// Should the sabotage map be used when attempting to open sabotage overlay. + /// + /// MapBehaviour object. + /// True if the sabotage map should be shown. + public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true; + + /// + /// Gets the to display on the map. + /// + /// The for the gamemode. + public virtual MapOptions GetMapOptions() + { + return new MapOptions + { + Mode = PlayerControl.LocalPlayer.Data.Role.IsImpostor && !MeetingHud.Instance + ? MapOptions.Modes.Sabotage + : MapOptions.Modes.Normal, + }; + } + + /// + /// Can a player vent in this gamemode. + /// + /// Target vent. + /// Player attempting to vent. + /// True if venting is enabled in this mode. + public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true; + + /// + /// Does the task bar appear in the gamemode. + /// + /// True if the task bar is enabled in this mode. + public virtual bool ShowTaskBar => true; + + public virtual void UpdateTaskPanel(TaskPanelBehaviour instance) + { + instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f) + ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f) + : Vector3.zero; + Vector3 vector = instance.background.sprite.bounds.extents; + vector.y = -vector.y; + vector = vector.Mul(instance.background.transform.localScale); + instance.background.transform.localPosition = vector; + Vector3 vector2 = instance.tab.sprite.bounds.extents; + vector2 = vector2.Mul(instance.tab.transform.localScale); + vector2.y = -vector2.y; + vector2.x += vector.x * 2f; + instance.tab.transform.localPosition = vector2; + if (GameManager.Instance == null) + { + return; + } + + var yPos = 0.6f; + var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x; + instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z); + instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z); + if (instance.open) + { + instance.timer = Mathf.Min(1f, instance.timer + Time.deltaTime / instance.animationTimeSeconds); + } + else + { + instance.timer = Mathf.Max(0f, instance.timer - Time.deltaTime / instance.animationTimeSeconds); + } + + Vector3 relativePos = new( + Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), + yPos, + instance.openPosition.z); + instance.transform.localPosition = + AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); + } + + /// + public override string ToString() => Name; +} diff --git a/MiraAPI/GameModes/ClassicMode.cs b/MiraAPI/GameModes/ClassicMode.cs new file mode 100644 index 0000000..44b43da --- /dev/null +++ b/MiraAPI/GameModes/ClassicMode.cs @@ -0,0 +1,20 @@ +using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.GameModes; + +/// +/// The classic game mode. +/// +[MiraIgnore] +public class ClassicMode : AbstractGameMode +{ + /// + public override string Name => "Classic"; + + /// + public override string Description => "The classic Among Us experience!\nFind the Impostors or deceive the Crew."; + + public override LoadableAsset? Icon => MiraAssets.ClassicGamemodeIcon; +} diff --git a/MiraAPI/GameModes/CustomGameMode.cs b/MiraAPI/GameModes/CustomGameMode.cs deleted file mode 100644 index f4dc43d..0000000 --- a/MiraAPI/GameModes/CustomGameMode.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Collections.Generic; - -namespace MiraAPI.GameModes; - -/// -/// Base class for custom gamemodes. -/// -public abstract class CustomGameMode -{ - /// - /// Gets the game mode name. - /// - public abstract string Name { get; } - - /// - /// Gets the game mode description. - /// - public abstract string Description { get; } - - /// - /// Gets the game mode ID. - /// - public abstract int Id { get; } - - /// - /// Called when is destroyed. - /// - public virtual void Initialize() - { - } - - /// - /// Called on . - /// - /// Instance of . - public virtual void HudStart(HudManager instance) - { - } - - /// - /// Called every frame on . - /// - /// Instance of . - public virtual void HudUpdate(HudManager instance) - { - } - - /// - /// Called when a player is killed. - /// - /// that was killed. - public virtual void OnDeath(PlayerControl player) - { - } - - /// - /// Calculate Game End logic. - /// - /// Should original game end logic be used. - /// Instance of . - public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) - { - runOriginal = true; - } - - /// - /// Custom role assignment. - /// - /// Should original role assignment be used. - /// Instance of . - public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) - { - runOriginal = true; - } - - /// - /// Custom murder logic. - /// - /// Should the original murder logic be used. - /// Result of custom murder logic. - /// Target for murder. - public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target) - { - result = false; - runOriginal = true; - } - - /// - /// Should Roles Settings be available when this gamemode is selected. - /// - /// if Role Settings are enabled in this game mode. - public virtual bool AreRoleSettingsEnabled() => true; - - /// - /// Should Game Settings be available when this gamemode is selected. - /// - /// if Game Settings are enabled in this mode. - public virtual bool AreGameSettingsEnabled() => true; - - /// - /// Custom winner selection. - /// - /// List of winners or . - public virtual List? CalculateWinners() => null; - - /// - /// Show gamemode in . - /// - /// if the game mode should be shown in the intro cutscene. - public virtual bool ShowGameModeIntroCutscene() => false; - - /// - /// Can Admin be used in this gamemode. - /// - /// Admin Console. - /// if Admin console is enabled. - public virtual bool CanUseMapConsole(MapConsole console) => true; - - /// - /// Can a be reported in this gamemode. - /// - /// Target for reporting. - /// if s can be reported. - public virtual bool CanReport(DeadBody body) => true; - - /// - /// Can s be used in this gamemode. - /// - /// System Console. - /// if s are enabled in this mode. - public virtual bool CanUseSystemConsole(SystemConsole console) => true; - - /// - /// Can tasks be interacted with in this gamemode. - /// - /// Task console. - /// if tasks are enabled in this mode. - public virtual bool CanUseTasks(Console console) => true; - - /// - /// Should the sabotage map be used when attempting to open sabotage overlay. - /// - /// object. - /// if the sabotage map should be shown. - public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true; - - /// - /// Can a player vent in this gamemode. - /// - /// Target vent. - /// Player attempting to vent. - /// if venting is enabled in this mode. - public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true; -} diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 57dca80..b47220a 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -2,71 +2,123 @@ using System.Collections.Generic; using System.Linq; using MiraAPI.Translation; +using MiraAPI.PluginLoading; using Reactor.Utilities; namespace MiraAPI.GameModes; /// -/// Manages s. +/// Manages custom gamemodes. /// public static class CustomGameModeManager { - /// - /// List of registered . - /// - internal static readonly Dictionary GameModes = []; + internal static readonly Dictionary IdToModeMap = []; - public static bool IsDefault() + private static uint GetNextId() => ++LastId; + + private static uint LastId { get; set; } + + /// + /// Register gamemode from type. + /// + /// Type of gamemode class, should inherit from . + /// The custom plugin info of the mod. + /// Whether the gamemode was successfully registered. + internal static bool RegisterGameMode(Type gameModeType, MiraPluginInfo pluginInfo) { - return ActiveMode?.Id == 0; - } - - /// - /// Gets the current . - /// - public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode(); - - /// - /// Set current . - /// - /// ID. - public static void SetGameMode(int id) - { - if (GameModes.TryGetValue(id, out var gameMode)) - { - ActiveMode = gameMode; - return; - } - - Error($"No gamemode with id {id} found!"); - } - - /// - /// Register from type. - /// - /// Type of , should inherit from . - internal static void RegisterGameMode(Type gameModeType) - { - if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType)) + if (!typeof(AbstractGameMode).IsAssignableFrom(gameModeType)) { Warning($"{gameModeType.Name} does not inherit CustomGameMode!"); - return; + return false; } - var modeObj = Activator.CreateInstance(gameModeType); + var instance = Activator.CreateInstance(gameModeType); - if (modeObj is not CustomGameMode gameMode) + if (instance is not AbstractGameMode mode) { Error($"Failed to create instance of {gameModeType.Name}"); - return; + return false; } - if (GameModes.Any(x => x.Key == gameMode.Id)) + if (!mode.HideMode) { - Error($"ID for gamemode {gameMode.Name.Translate()} already exists!"); + IdToModeMap.Add(GetNextId(), mode); + pluginInfo.GameModes.Add(LastId, mode); + mode.ID = LastId; + GameModeOption.AddOption(mode); + } + return true; + } + + /// + /// Checks to see if the current game mode is an instance of the classic mode. + /// + /// True if the classic mode is the current one. + public static bool IsClassic() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(ClassicMode)); + + /// + /// Checks to see if the current game mode is an instance of the HNS mode. + /// + /// True if the Hide & Seek mode is the current one. + public static bool IsHideNSeek() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(HideAndSeekMode)); + + /// + /// Checks if a provided GameMode is the current active one. + /// + /// The AbstractGameMode subclass being checked. + /// Whether the provided mode is the current active one. + public static bool IsActiveGameMode() where T : AbstractGameMode => ActiveMode is T; + + /// + /// Gets the current gamemode. + /// + public static AbstractGameMode? ActiveMode { get; private set; } + + /// + /// Gets a gamemode from an ID. + /// + /// The ID of the gamemode to fetch. + /// The gamemode matching that ID. + public static AbstractGameMode GetMode(uint id) => IdToModeMap[id]; + + /// + /// Finds the parent mod of a custom gamemode. + /// + /// The object. + /// A object representing the parent mod of the gamemode. + public static MiraPluginInfo? FindParentMod(AbstractGameMode gameMode) + { + return MiraPluginManager.Instance.RegisteredPlugins.FirstOrDefault(plugin => plugin.GameModes.ContainsValue(gameMode)); + } + + internal static void RegisterDefaultMode() + { + var defaultMode = new ClassicMode(); + IdToModeMap.Add(0, defaultMode); + // no need to add to game mode option as it already contains it + // because we cannot have the option be created with no values + defaultMode.ID = 0; + var hnsMode = new HideAndSeekMode(); + if (!hnsMode.HideMode) + { + IdToModeMap.Add(1, hnsMode); + hnsMode.ID = 1; + GameModeOption.AddOption(hnsMode); + LastId++; + } + } + + internal static void GetAndSetGameMode() + { + var id = (uint)GameModeOption.Value; + + if (IdToModeMap.TryGetValue(id, out var mode)) + { + ActiveMode = mode; return; } - GameModes.Add(gameMode.Id, gameMode); + ActiveMode = IdToModeMap[0]; + Logger.Warning($"Unable to find game mode of id {id}!"); } } diff --git a/MiraAPI/GameModes/DefaultMode.cs b/MiraAPI/GameModes/DefaultMode.cs deleted file mode 100644 index f431551..0000000 --- a/MiraAPI/GameModes/DefaultMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MiraAPI.Translation; - -namespace MiraAPI.GameModes; - -public class DefaultMode : CustomGameMode -{ - public override string Name => "gamemode.default.name"; - public override string Description => "gamemode.default.description".Translate(); - public override int Id => 0; -} diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs new file mode 100644 index 0000000..95acd9c --- /dev/null +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.GameOptions; +using MiraAPI.Networking; +using MiraAPI.Patches.GameModes; +using MiraAPI.Patches.Options; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using Reactor.Localization.Utilities; +using Reactor.Networking.Attributes; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.ProBuilder; +using Object = UnityEngine.Object; + +namespace MiraAPI.GameModes; + +/// +/// The game mode option. +/// +[HarmonyPatch] +public static class GameModeOption +{ + /// + /// Gets the current index of the Game Mode Option + /// For the value as an , see . + /// + public static int Value + { + get => + OptionBehaviour != null + ? OptionBehaviour.GetInt() + : _lastValue; + private set + { + _lastValue = value; + if (OptionBehaviour == null) + return; + OptionBehaviour.Value = value; + OptionBehaviour.UpdateValue(); + _lastValue = value; + } + } + internal static StringOption OptionBehaviour { get; set; } = null!; + + internal static int _lastValue; + internal static readonly StringNames GamemodeName = CustomStringName.CreateAndRegister("Gamemode"); + internal static readonly StringNames CustomName = CustomStringName.CreateAndRegister("Custom"); + internal static readonly Dictionary Values = new() + { + [0] = CustomStringName.CreateAndRegister("Classic"), + }; + + internal static void AddOption(AbstractGameMode mode) + { + if (!Values.ContainsKey(mode.ID)) + Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.ColoredName)); + } + /*[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] + [HarmonyPostfix] + private static void CreateSettingsPatch(GameOptionsMenu __instance) + { + if (MenuState.Instance.CurrentModIdx != 0 || GameManager.Instance.IsHideAndSeek() || CustomGameModeManager.ActiveMode == null) + { + return; + } + + var num = 0.713f; + + foreach (var category in __instance.settingsContainer.GetComponentsInChildren()) + { + if (category) + category.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); + } + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); + categoryHeaderMasked.SetHeader(CustomName, 20); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); + OptionBehaviour = Object.Instantiate( + __instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + __instance.settingsContainer); + num -= 0.63f; + OptionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + OptionBehaviour.SetClickMask(__instance.ButtonClickMask); + StringGameSetting setting = ScriptableObject.CreateInstance(); + setting.Type = OptionTypes.MultipleChoice; + setting.Title = GamemodeName; + setting.Index = _lastValue; + setting.Values = new Il2CppStructArray([Values[0]]); + OptionBehaviour.SetUpFromData(setting, 20); + Set(_lastValue); + OptionBehaviour.TitleText.fontSize = 3; + foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) + { + optionBehaviour.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); + } + __instance.Children.Add(OptionBehaviour); + for (var i = 1; i < Values.Count; i++) + OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); + __instance.scrollBar.SetYBoundsMax(__instance.scrollBar.GetYBounds().max + 1); + }*/ + + internal static void Set(int val) + { + Value = val; + var previousMode = CustomGameModeManager.ActiveMode; + CustomGameModeManager.GetAndSetGameMode(); + HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); + var gm = CustomGameModeManager.ActiveMode!; + if (gm != previousMode) + { + ModdedOptionsManager.AddSettingsChangeMessage( + HudManager.Instance.Notifier, + GamemodeName, + gm.ColoredName, + new Color(0.7333f, 0.7333f, 0.7333f, 1), + gm.TmpIcon, + true); + if (MenuState.Instance) + { + // should force reset roles for gamemodes properly + foreach (var roleMenu in MenuState.Instance.QueuedRoleMenuRefresh) + { + MenuState.Instance.QueuedRoleMenuRefresh[roleMenu.Key] = true; + } + } + } + // could make Values a dict of AbstractGameMode too + } + + [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] + [HarmonyPrefix] + private static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option) + { + if (OptionBehaviour.Equals(option)) + { + Info($"Game mode changed to {option.GetInt()}"); + RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt()); + if (GameSettingMenu.Instance && CustomGameModeManager.ActiveMode != null) + { + GameOptionsMenuPatch.ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance); + GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); + } + return false; + } + return true; + } + + [MethodRpc((uint) MiraRpc.SyncGamemodeOption)] + internal static void RpcSyncGamemode(PlayerControl host, int data) + { + Set(data); + } +} diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs new file mode 100644 index 0000000..d5c90e8 --- /dev/null +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -0,0 +1,534 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using AmongUs.Data; +using AmongUs.GameOptions; +using HarmonyLib; +using InnerNet; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented; +using MiraAPI.HnsReimplemented.Options; +using MiraAPI.PluginLoading; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using PowerTools; +using UnityEngine; + +namespace MiraAPI.GameModes; + +/// +/// The vanilla Hide and Seek game mode, ported to Mira. +/// +[MiraIgnore] +public class HideAndSeekMode : AbstractGameMode +{ + /// + public override string Name => "Hide n Seek"; + + /// + public override string Description => "You can run, but you can't hide!\nOutlive the timer or eliminate the crew."; + + // TODO: Once the Hide n Seek port is ready for release, we will remove this. + public override bool HideMode => !MiraApiPlugin.IsDevBuild; + + public override Color Color { get; } = new Color32(255, 88, 90, 255); + public override LoadableAsset? Icon => MiraAssets.HnSGamemodeIcon; + public override bool CanReport(DeadBody body) => false; + public override bool ShouldShowSabotageMap(MapBehaviour map) => false; + public override bool ShowGameModeIntroCutscene => true; + public override bool GameModeBodyTypeOverride => true; + public override bool ShowNormalGameSettings => false; + public override bool ShowNormalRoleSettings => false; + public override float DefaultImpostorKillCooldown => 1f; + + public override bool ShowTaskBar => false; + public static int ImpostorPlayerID() + { + return OptionGroupSingleton.Instance.SelectedSeeker.Value; + } + + public static bool HasImpostorPlayerID() + { + return ImpostorPlayerID() > -1; + } + + public static bool ValidateImpostorPlayerID(List players) + { + return HasImpostorPlayerID() && players.Find((NetworkedPlayerInfo p) => (int)p.PlayerId == ImpostorPlayerID()) != null; + } + + public override void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) + { + runOriginal = false; + Il2CppSystem.Collections.Generic.List list = new(); + AmongUsClient.Instance.GetAllClients(list); + List list2 = list.ToArray() + .Where(c => c.Character != null && c.Character.Data != null && !c.Character.Data.Disconnected && + !c.Character.Data.IsDead).OrderBy(c => c.Id).Select(c => c.Character.Data) + .ToList(); + + foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) + { + if (networkedPlayerInfo.Object != null && networkedPlayerInfo.Object.isDummy) + { + list2.Add(networkedPlayerInfo); + } + } + IGameOptions currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; + int adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count); + AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, Math.Max(adjustedNumImpostors, 1), RoleTypes.Impostor); + AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Engineer); + } + + public static void AssignRolesForTeam( + List players, + IGameOptions opts, + RoleTeamTypes team, + int teamMax, + RoleTypes defaultRole) + { + Error($"Hide And Seek Mode - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); + int num = 0; + IRoleOptionsCollection roleOptions = opts.RoleOptions; + var source = RoleManager.Instance.AllRoles.ToArray() + .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) && + CustomRoleUtils.CanSpawnOnCurrentMode(role)); + var assignmentData = source.Where(x => !x.IsDead).Select(role => + new RoleManager.RoleAssignmentData( + role, + roleOptions.GetNumPerGame(role.Role), + roleOptions.GetChancePerGame(role.Role))).ToList(); + var source2 = CustomRoleUtils.GetPossibleRoles(assignmentData, x => x.Chance == 100); + var guaranteedRoles = source.Where(x => source2.Contains(((ushort)x.Role, 100))); + List list = new List(); + if (team == RoleTeamTypes.Crewmate) + { + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) => + new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) + { + while (true) + { + RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; + int count = roleAssignmentData2.Count; + roleAssignmentData2.Count = count - 1; + if (count <= 0) + { + break; + } + + list.Add(roleAssignmentData.Role.Role); + } + } + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); + AssignRolesFromList(players, teamMax, list, ref num); + + var list2 = source.Where(x => !x.IsDead).Select(role => + new RoleManager.RoleAssignmentData( + role, + roleOptions.GetNumPerGame(role.Role), + roleOptions.GetChancePerGame(role.Role))).ToList(); + + list.Clear(); + foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2) + { + for (int i = 0; i < roleAssignmentData3.Count; i++) + { + if (HashRandom.Next(101) < roleAssignmentData3.Chance) + { + list.Add(roleAssignmentData3.Role.Role); + } + } + } + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Potential Assignment"); + + AssignRolesFromList(players, teamMax, list, ref num); + var basicRole = RoleTypes.Engineer; + while (list.Count < players.Count && list.Count + num < teamMax) + { + list.Add(basicRole); + } + + AssignRolesFromList(players, teamMax, list, ref num); + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Fallback Assignment"); + } + else if (team == RoleTeamTypes.Impostor) + { + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + var newImpostors = new List(); + // Specified Seeker + if (HasImpostorPlayerID() && + ValidateImpostorPlayerID(players) && + !AmongUsClient.Instance.IsGamePublic) + { + NetworkedPlayerInfo networkedPlayerInfo = players.ToArray() + .First(p => p.PlayerId == ImpostorPlayerID()); + players.Remove(networkedPlayerInfo); + newImpostors.Add(networkedPlayerInfo); + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); + } + // Random Seeker + else + { + int num2 = 0; + while (num2 < teamMax && players.Count > 0) + { + PseudoRandomList pseudoRandomList = new PseudoRandomList(AmongUsClient.Instance.GameId); + players.Do(x => pseudoRandomList.Add(x)); + for (int i = 0; i < GameData.RoundsPlayedInSession; i++) + { + pseudoRandomList.PickRandom(); + } + NetworkedPlayerInfo networkedPlayerInfo = pseudoRandomList.PickRandom(); + players.Remove(networkedPlayerInfo); + newImpostors.Add(networkedPlayerInfo); + num2++; + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); + } + } + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); + foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) => + new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) + { + while (true) + { + RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; + int count = roleAssignmentData2.Count; + roleAssignmentData2.Count = count - 1; + if (count <= 0) + { + break; + } + + list.Add(roleAssignmentData.Role.Role); + } + } + AssignRolesFromList(newImpostors, teamMax, list, ref num); + + var list2 = source.Where(x => !x.IsDead).Select(role => + new RoleManager.RoleAssignmentData( + role, + roleOptions.GetNumPerGame(role.Role), + roleOptions.GetChancePerGame(role.Role))).ToList(); + + list.Clear(); + foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2) + { + for (int i = 0; i < roleAssignmentData3.Count; i++) + { + if (HashRandom.Next(101) < roleAssignmentData3.Chance) + { + list.Add(roleAssignmentData3.Role.Role); + } + } + } + + AssignRolesFromList(newImpostors, teamMax, list, ref num); + var basicRole = RoleTypes.Impostor; + while (list.Count < newImpostors.Count && list.Count + num < teamMax) + { + list.Add(basicRole); + } + + AssignRolesFromList(newImpostors, teamMax, list, ref num); + } + } + + public static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned) + { + while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax) + { + int index = HashRandom.FastNext(roleList.Count); + RoleTypes roleType = roleList[index]; + roleList.RemoveAt(index); + int index2 = HashRandom.FastNext(players.Count); + players[index2].Object.RpcSetRole(roleType, false); + players.RemoveAt(index2); + rolesAssigned++; + } + } + + public override IEnumerator IntroCutscene(IntroCutscene __instance) + { + SoundManager.Instance.PlaySound(__instance.IntroStinger, false, 1f, null); + Logger.GlobalInstance.Info("IntroCutscene :: CoBegin() :: Game Mode: Hide and Seek (MiraAPI)", null); + __instance.LogPlayerRoleData(); + __instance.HideAndSeekPanels.SetActive(true); + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + __instance.CrewmateRules.SetActive(false); + __instance.ImpostorRules.SetActive(true); + } + else + { + __instance.CrewmateRules.SetActive(true); + __instance.ImpostorRules.SetActive(false); + } + + __instance.ImpostorName.gameObject.SetActive(true); + __instance.ImpostorTitle.gameObject.SetActive(true); + __instance.BackgroundBar.enabled = false; + __instance.TeamTitle.gameObject.SetActive(false); + var impostor = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(x => x.Data.Role.IsImpostor); + if (impostor == null) + { + Logger.GlobalInstance.Error("IntroCutscene :: CoBegin() :: impostor is NULL", null); + } + + GameManager.Instance.SetSpecialCosmetics(impostor); + if (impostor != null) + { + __instance.ImpostorName.text = impostor.Data.PlayerName; + } + else + { + __instance.ImpostorName.text = "???"; + } + + yield return new WaitForSecondsRealtime(0.1f); + if (impostor != null) + { + __instance.ImpostorTitle.text = impostor.Data.Role.GetRoleName(); + } + PoolablePlayer playerSlot = null; + if (impostor != null) + { + playerSlot = __instance.CreatePlayer(1, 1, impostor.Data, false); + playerSlot.SetBodyType(PlayerBodyTypes.Normal); + playerSlot.SetFlipX(false); + playerSlot.transform.localPosition = __instance.impostorPos; + playerSlot.transform.localScale = Vector3.one * __instance.impostorScale; + } + + yield return ShipStatus.Instance.CosmeticsCache.PopulateFromPlayers(); + yield return new WaitForSecondsRealtime(6f); + if (playerSlot != null) + { + playerSlot.gameObject.SetActive(false); + } + + __instance.HideAndSeekPanels.SetActive(false); + __instance.CrewmateRules.SetActive(false); + __instance.ImpostorRules.SetActive(false); + HnsMusicHandler.Instance.StartMusicWithIntro(); + var hideTimer = 10f; + + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + __instance.HideAndSeekTimerText.gameObject.SetActive(true); + PoolablePlayer poolablePlayer; + AnimationClip anim; + if (AprilFoolsMode.ShouldHorseAround()) + { + poolablePlayer = __instance.HorseWrangleVisualSuit; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); + anim = __instance.HnSSeekerSpawnHorseAnim; + __instance.HorseWrangleVisualPlayer.SetBodyType(PlayerBodyTypes.Normal); + __instance.HorseWrangleVisualPlayer.UpdateFromPlayerData( + PlayerControl.LocalPlayer.Data, + PlayerControl.LocalPlayer.CurrentOutfitType, + PlayerMaterial.MaskType.None, + false, + null, + false); + } + else if (AprilFoolsMode.ShouldLongAround()) + { + poolablePlayer = __instance.HideAndSeekPlayerVisual; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.LongSeeker); + anim = __instance.HnSSeekerSpawnLongAnim; + } + else + { + poolablePlayer = __instance.HideAndSeekPlayerVisual; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); + anim = __instance.HnSSeekerSpawnAnim; + } + + poolablePlayer.SetBodyCosmeticsVisible(false); + poolablePlayer.UpdateFromPlayerData( + PlayerControl.LocalPlayer.Data, + PlayerControl.LocalPlayer.CurrentOutfitType, + PlayerMaterial.MaskType.None, + false, + null, + false); + SpriteAnim component = poolablePlayer.GetComponent(); + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.ToggleName(false); + component.Play(anim, 1f); + while (hideTimer > 0f) + { + __instance.HideAndSeekTimerText.text = Mathf.RoundToInt(hideTimer).ToString(); + hideTimer -= Time.deltaTime; + yield return null; + } + } + else + { + HideAndSeekHudHelper.Instance.HideCountdown = hideTimer; + if (AprilFoolsMode.ShouldHorseAround()) + { + if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnHorseInGameAnim); + } + } + else if (AprilFoolsMode.ShouldLongAround()) + { + if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnLongInGameAnim); + } + } + else if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnAnim); + impostor.cosmetics.SetBodyCosmeticsVisible(false); + } + } + ShipStatus.Instance.StartSFX(); + HnsMusicHandler.Instance.OnGameStart(); + HnsDangerMeter.Instance.OnGameStart(); + UnityEngine.Object.Destroy(__instance.gameObject); + } + public override void Initialize() + { + deadPlayerCount = 0; + ShipStatus.Instance.BreakEmergencyButton(); + PlayerControl.LocalPlayer.SetKillTimer(0.01f); + if (HudManager.InstanceExists) + { + HudManager.Instance.gameObject.AddComponent(); + HudManager.Instance.gameObject.AddComponent(); + HudManager.Instance.gameObject.AddComponent(); + } + } + public override MapOptions GetMapOptions() + { + MapOptions mapOptions = new MapOptions + { + Mode = MapOptions.Modes.Normal, + }; + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && HideAndSeekHudHelper.Instance.SeekerAdminMapEnabled(PlayerControl.LocalPlayer)) + { + mapOptions.Mode = MapOptions.Modes.CountOverlay; + mapOptions.AllowMovementWhileMapOpen = true; + mapOptions.IncludeDeadBodies = false; + mapOptions.ShowLivePlayerPosition = false; + } + return mapOptions; + } + + public override void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) + { + runOriginal = false; + if (Helpers.GetAlivePlayers().Count(x => x.Data.Role.IsImpostor) == 0) + { + instance.Manager.RpcEndGame(GameOverReason.ImpostorDisconnect, !DataManager.Player.Ads.HasPurchasedAdRemoval); + } + if (Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor) != 0) + { + if (HideAndSeekHudHelper.Instance.AllTimersExpired()) + { + instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_CrewmatesByTimer, !DataManager.Player.Ads.HasPurchasedAdRemoval); + } + return; + } + instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_ImpostorsByKills, !DataManager.Player.Ads.HasPurchasedAdRemoval); + } + + public override PlayerBodyTypes GetBodyType(PlayerControl player) + { + if (player == null || player.Data == null || player.Data.Role == null) + { + if (AprilFoolsMode.ShouldHorseAround()) + { + return PlayerBodyTypes.Horse; + } + if (AprilFoolsMode.ShouldLongAround()) + { + return PlayerBodyTypes.Long; + } + return PlayerBodyTypes.Normal; + } + else if (AprilFoolsMode.ShouldHorseAround()) + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.Normal; + } + return PlayerBodyTypes.Horse; + } + else if (AprilFoolsMode.ShouldLongAround()) + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.LongSeeker; + } + return PlayerBodyTypes.Long; + } + else + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.Seeker; + } + return PlayerBodyTypes.Normal; + } + } + public override void UpdateTaskPanel(TaskPanelBehaviour instance) + { + instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f) + ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f) + : Vector3.zero; + Vector3 vector = instance.background.sprite.bounds.extents; + vector.y = -vector.y; + vector = vector.Mul(instance.background.transform.localScale); + instance.background.transform.localPosition = vector; + Vector3 vector2 = instance.tab.sprite.bounds.extents; + vector2 = vector2.Mul(instance.tab.transform.localScale); + vector2.y = -vector2.y; + vector2.x += vector.x * 2f; + instance.tab.transform.localPosition = vector2; + if (GameManager.Instance == null) + { + return; + } + + var yPos = 1.6f; + var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x; + instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z); + instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z); + if (instance.open) + { + instance.timer = Mathf.Min(1f, instance.timer + Time.deltaTime / instance.animationTimeSeconds); + } + else + { + instance.timer = Mathf.Max(0f, instance.timer - Time.deltaTime / instance.animationTimeSeconds); + } + + Vector3 relativePos = new( + Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), + yPos, + instance.openPosition.z); + instance.transform.localPosition = + AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); + } + private int deadPlayerCount; + public override void OnPlayerDeath(PlayerControl player, bool assignGhostRole) + { + base.OnPlayerDeath(player, assignGhostRole); + HudManager.Instance.NotifyOfDeath(); + var popup = GameManagerCreator.Instance.HideAndSeekManagerPrefab.DeathPopupPrefab; + deadPlayerCount++; + var item = UnityEngine.Object.Instantiate(popup, HudManager.Instance.transform.parent); + item.Show(player, deadPlayerCount); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index 02daa25..3d48453 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -8,8 +8,9 @@ namespace MiraAPI.GameOptions.Attributes; /// /// The option title. /// Optional parameter to specify a role Type. +/// Optional parameter to specify a game mode Type. [AttributeUsage(AttributeTargets.Property)] -public abstract class ModdedOptionAttribute(string title, Type? roleType = null) : Attribute +public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : Attribute { internal IModdedOption? HolderOption { get; set; } @@ -23,6 +24,11 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null) /// protected Type? RoleType { get; private set; } = roleType; + /// + /// Gets the game mode type of the option. + /// + protected Type? ModeType { get; private set; } = modeType; + /// /// Sets the value of the option. /// diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 7dd26ee..d91d079 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Reflection; using BepInEx.Configuration; using HarmonyLib; +using Il2CppInterop.Runtime.Injection; +using MiraAPI.GameModes; using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Networking; @@ -26,6 +28,8 @@ public static class ModdedOptionsManager private static readonly Dictionary TypeToGroup = []; internal static readonly Dictionary CreatedPlayerOptions = []; + internal static readonly Dictionary CreatedStringOptions = []; + internal static readonly Dictionary> GameModeOptionGroups = []; internal static readonly Dictionary ModdedOptions = []; internal static readonly List Groups = []; @@ -34,7 +38,7 @@ public static class ModdedOptionsManager public static void AddSettingsChangeMessage(NotificationPopper notif, StringNames key, string value, Color textColor, TMP_SpriteAsset? sprite, bool playSound = true) { - var item = string.Empty; + string item; var text = textColor.ToTextColor(); if (sprite != null) { @@ -62,6 +66,29 @@ public static class ModdedOptionsManager notif.SettingsChangeMessageLogic(key, item, playSound); } + internal static bool RegisterGroup(Type type) + { + if (Activator.CreateInstance(type) is not AbstractOptionGroup group) + { + return false; + } + + if (TypeToGroup.ContainsKey(type)) + { + Logger.Error($"Group {type.Name} already exists."); + return false; + } + + Groups.Add(group); + TypeToGroup.Add(type, group); + + typeof(OptionGroupSingleton<>).MakeGenericType(type) + .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! + .SetValue(null, group); + + return true; + } + internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo) { if (Activator.CreateInstance(type) is not AbstractOptionGroup group) @@ -77,6 +104,17 @@ public static class ModdedOptionsManager Groups.Add(group); TypeToGroup.Add(type, group); + if (group.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true) + { + if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList)) + { + oldList.Add(group); + } + else + { + GameModeOptionGroups.Add(group.OptionableType, new List() { group }); + } + } pluginInfo.InternalOptionGroups.Add(group); typeof(OptionGroupSingleton<>).MakeGenericType(type) diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs index 3db6566..6309649 100644 --- a/MiraAPI/GameOptions/ModifierOptionGroup.cs +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -1,8 +1,10 @@ using System.Linq; +using MiraAPI.PluginLoading; namespace MiraAPI.GameOptions; #pragma warning disable CA1852 +[MiraIgnore] internal class ModifierOptionGroup : AbstractOptionGroup #pragma warning restore CA1852 { diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 80e4363..6178eff 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -1,4 +1,4 @@ -using System; +using System; using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs new file mode 100644 index 0000000..62d07cf --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -0,0 +1,176 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.Networking; +using Reactor.Localization.Utilities; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.GameOptions.OptionTypes; + +/// +/// An option for selecting an enum value. +/// +public class ModdedStringOption : ModdedOption +{ + /// + /// Gets or sets the string values of the option. + /// + public string[] Values { get; set; } + + /// + /// Gets or sets the String Name values of the option. + /// + public StringNames[] StringNameValues { get; set; } + + /// + /// Gets the string's index. + /// + /// The option's string value. + /// The option's value in the index. + public int GetIndex(string value, int location) + { + if (Values.Contains(value)) + { + return Values.ToList().IndexOf(value); + } + Info($"Cannot find a valid index for {value} in {location}. Returning 0 instead. Values: {string.Join(", ", Values)}"); + return 0; + } + + /// + /// Adds a value to the option. + /// + /// The option's string value. + /// The additional value's index on the list. + public void RegisterExtraValue(string value, int index = -1) + { + if (!Values.Contains(value)) + { + if (index <= -1) + index = Values.Length; + + var newVals = Values.ToList(); + newVals.Insert(index, value); + Values = newVals.ToArray(); + var newStrVals = StringNameValues.ToList(); + newStrVals.Insert(index, CustomStringName.CreateAndRegister(value)); + StringNameValues = newStrVals.ToArray(); + } + + if (Data != null) + { + var data = (StringGameSetting)Data; + data.Values = StringNameValues; + + data.Index = GetIndex(Value, 1); + if (OptionBehaviour != null) + { + var strOpt = OptionBehaviour.TryCast(); + if (strOpt != null) + { + strOpt.Values = StringNameValues; + strOpt.Value = GetIndex(Value, 2); + Warning("StringOption is valid"); + } + else + { + Error("StringOption is null."); + } + Warning("OptionBehaviour is valid"); + } + else + { + Error("OptionBehaviour is null."); + } + Warning("Data is valid"); + } + else + { + Error("Data is null."); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// The default value as a string. + /// An option list of string values to use. + /// Whether to include this option in the preset or not. + public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset=true) : base(title, defaultValue, includeInPreset) + { + Values = values; + Data = ScriptableObject.CreateInstance(); + var data = (StringGameSetting)Data; + + data.Title = StringName; + data.Type = global::OptionTypes.String; + StringNameValues = values.Select(CustomStringName.CreateAndRegister).ToArray(); + data.Values = StringNameValues; + + data.Index = GetIndex(Value, 3); + } + + /// + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) + { + var stringOption = Object.Instantiate(stringOpt, container); + stringOption.name = + $"{ParentMod!.OptionsTitleText}.StringOption.{TranslationController.Instance.GetString(StringName)}"; + + stringOption.SetUpFromData(Data, 20); + stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + + // SetUpFromData method doesnt work correctly so we must set the values manually + stringOption.Title = StringName; + stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray(0); + stringOption.Value = GetIndex(Value, 4); + + OptionBehaviour = stringOption; + ModdedOptionsManager.CreatedStringOptions.TryAdd(stringOption, this); + + return stringOption; + } + + /// + public override float GetFloatData() + { + return Array.IndexOf(Values, Value); + } + + /// + public override NetData GetNetData() + { + return new NetData(Id, Encoding.Unicode.GetBytes(Convert.ToString(Value, NumberFormatInfo.InvariantInfo))); + } + + /// + public override void HandleNetData(byte[] data) + { + SetValue(Encoding.Unicode.GetString(data)); + } + + /// + public override string GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) + { + return Values[optionBehaviour.GetInt()]; + } + + /// + protected override void OnValueChanged(string newValue) + { + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(GetIndex(newValue, 5)), false); + if (!OptionBehaviour) + { + return; + } + + if (OptionBehaviour is StringOption opt) + { + opt.Value = GetIndex(newValue, 5); + } + } +} diff --git a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs new file mode 100644 index 0000000..d41b445 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs @@ -0,0 +1,285 @@ +using System.Collections; +using System.Linq; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HideAndSeekHudHelper Instance { get; private set; } + + public AudioClip FinalHideAlertSfx; + + public AudioClip FinalHideCountdownSfx; + + private AudioClip TaskFinishedSound; + + private const int SECONDS_TO_BEEP = 10; + + private const float SECONDS_TO_SET_DIRTY = 1f; + + private HideAndSeekTimerBar timerBar; + + private float totalHideTime = float.MaxValue; + + private float currentHideTime = float.MaxValue; + + private float totalFinalHideTime = float.MaxValue; + + private float currentFinalHideTime = float.MaxValue; + + private float secondsSinceLastSetDirty; + private Coroutine beepCoroutine; + + private void Awake() + { + Instance = this; + } + + public bool IsFinalCountdown + { + get { return currentHideTime <= 0f; } + } + + public bool SeekerAdminMapEnabled(PlayerControl player) + { + int item = Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor); + return !player.inVent && player.Data != null && player.Data.Role != null && + ((!player.inVent && player.Data.Role.IsImpostor && IsFinalCountdown && + OptionGroupSingleton.Instance.FinalHideSeekMap.Value) || + (player.Data.Role.IsImpostor && + item <= + (GameData.Instance.PlayerCount - 1) / 3)); + } + + public void OnTaskComplete(float timeDeduction) + { + if (timerBar != null) + { + timerBar.TaskComplete(); + } + + AdjustEscapeTimer(timeDeduction, true); + } + + public float GetTotalRoundTime() + { + float escapeTime = OptionGroupSingleton.Instance.HidingTime.Value; + float finalCountdownTime = OptionGroupSingleton.Instance.FinalHideTime.Value; + return escapeTime + finalCountdownTime; + } + + public float GetTotalTimeRemaining() + { + return currentHideTime + currentFinalHideTime; + } + + public float GetRoundTimeElapsed() + { + return GetTotalRoundTime() - GetTotalTimeRemaining(); + } + + public void Start() + { + totalHideTime = OptionGroupSingleton.Instance.HidingTime.Value; + currentHideTime = totalHideTime; + totalFinalHideTime = OptionGroupSingleton.Instance.FinalHideTime.Value; + currentFinalHideTime = totalFinalHideTime; + if (timerBar != null) + { + Object.Destroy(timerBar); + } + + FinalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX; + FinalHideCountdownSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideCountdownSFX; + TaskFinishedSound = GameManagerCreator.Instance.HideAndSeekManagerPrefab.TaskFinishedSound; + timerBar = Object.Instantiate( + GameManagerCreator.Instance.HideAndSeekManagerPrefab.TimerBarPrefab, + HudManager.Instance.transform.parent); + } + + public void OnGameEnd() + { + if (timerBar != null) + { + Object.Destroy(timerBar.gameObject); + } + + if (beepCoroutine != null) + { + StopCoroutine(beepCoroutine); + } + + beepCoroutine = null!; + } + private float taskDirtyTimer; + public float HideCountdown { get; set; } + + public void LateUpdate() + { + if (!HudManager.InstanceExists) + { + return; + } + if (HideCountdown > 0f && taskDirtyTimer > 0.25f) + { + float num = taskDirtyTimer; + taskDirtyTimer = 0f; + if (!PlayerControl.LocalPlayer) + { + HudManager.Instance.TaskPanel.SetTaskText(string.Empty); + return; + } + NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data; + if (data == null) + { + return; + } + bool flag = data.Role != null && data.Role.IsImpostor; + HudManager.Instance.tasksString.Clear(); + if (PlayerControl.LocalPlayer.myTasks == null || PlayerControl.LocalPlayer.myTasks.Count == 0) + { + HudManager.Instance.tasksString.Append("None"); + } + else + { + for (int i = 0; i < PlayerControl.LocalPlayer.myTasks.Count; i++) + { + PlayerTask playerTask = PlayerControl.LocalPlayer.myTasks[i]; + if (playerTask) + { + if (playerTask.TaskType == TaskTypes.FixComms && !flag) + { + HudManager.Instance.tasksString.Clear(); + playerTask.AppendTaskText(HudManager.Instance.tasksString); + break; + } + playerTask.AppendTaskText(HudManager.Instance.tasksString); + } + } + if (data.Role != null) + { + data.Role.AppendTaskHint(HudManager.Instance.tasksString); + } + if (HideCountdown > 0f) + { + HideCountdown -= num; + HudManager.Instance.tasksString.Append("\n\n" + ((int)HideCountdown)); + } + HudManager.Instance.tasksString.TrimEnd(); + } + HudManager.Instance.TaskPanel.SetTaskText(HudManager.Instance.tasksString.ToString()); + } + } + public void FixedUpdate() + { + secondsSinceLastSetDirty += Time.fixedDeltaTime; + if (IsFinalCountdown) + { + AdjustFinalEscapeTimer(Time.fixedDeltaTime); + return; + } + + AdjustEscapeTimer(Time.fixedDeltaTime, false); + } + + public void OnDestroy() + { + if (timerBar != null) + { + Object.Destroy(timerBar); + } + } + + private void OnFinalCountdownTriggered() + { + foreach (PlayerControl playerControl in PlayerControl.AllPlayerControls) + { + if (!playerControl.Data.Role.IsImpostor && !playerControl.Data.IsDead) + { + playerControl.ClearTasks(); + PlayerTask.GetOrCreateTask(playerControl, 0).Text = + DestroyableSingleton.Instance.GetString(StringNames.HideActionButton); + } + } + + if (!PlayerControl.LocalPlayer.Data.IsDead && Minigame.Instance != null) + { + Minigame instance = Minigame.Instance; + if (instance != null) + { + instance.ForceClose(); + } + } + + timerBar.StartFinalHide(); + SoundManager.Instance.PlaySound(FinalHideAlertSfx, false, 1f, null); + DestroyableSingleton.Instance.SetAlertOverlay(true); + } + + private void AdjustEscapeTimer(float timeDeduction, bool forceDirty) + { + float num = currentHideTime; + currentHideTime -= timeDeduction; + currentHideTime = Mathf.Max(currentHideTime, 0f); + if (currentHideTime <= 10f && beepCoroutine == null) + { + beepCoroutine = StartCoroutine(BeepAlmostEverySecond().WrapToIl2Cpp()); + } + + if (num > 0f && currentHideTime <= 0f) + { + OnFinalCountdownTriggered(); + } + + timerBar.UpdateTimer(currentHideTime, totalHideTime); + if (forceDirty || secondsSinceLastSetDirty > 1f) + { + secondsSinceLastSetDirty = 0f; + } + } + + private IEnumerator BeepAlmostEverySecond() + { + while (!IsFinalCountdown) + { + float num = currentHideTime / 10f; + float pitch = 1.5f - num / 2f; + SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch, null); + yield return new WaitForSeconds(1f); + } + + yield return Effects.Wait(currentFinalHideTime - 10f); + while (currentFinalHideTime > 0f) + { + float num2 = currentFinalHideTime / 10f; + float pitch2 = 1.5f - num2 / 2f; + SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch2, null); + yield return new WaitForSeconds(1f); + } + + yield break; + } + + private void AdjustFinalEscapeTimer(float timeDeduction) + { + currentFinalHideTime -= timeDeduction; + currentFinalHideTime = Mathf.Max(currentFinalHideTime, 0f); + timerBar.UpdateTimer(currentFinalHideTime, totalFinalHideTime); + if (secondsSinceLastSetDirty > 1f) + { + secondsSinceLastSetDirty = 0f; + } + } + + public bool AllTimersExpired() + { + return currentHideTime <= 0f && currentFinalHideTime <= 0f; + } +} diff --git a/MiraAPI/HnsReimplemented/HnsDangerMeter.cs b/MiraAPI/HnsReimplemented/HnsDangerMeter.cs new file mode 100644 index 0000000..d36c198 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HnsDangerMeter.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HnsDangerMeter Instance; + + private void Awake() + { + Instance = this; + } + + private DangerMeter dangerMeter; + + private List impostors = []; + + private float scaryMusicDistance; + + private float veryScaryMusicDistance; + + private float dangerLevel1; + + private float dangerLevel2; + + private bool firstMusicActivation; + + private float firstCrossfadeCountdown; + + public void FixedUpdate() + { + PlayerControl localPlayer = PlayerControl.LocalPlayer; + if (impostors == null || localPlayer == null) + { + return; + } + + if (impostors.Count <= 0) + { + return; + } + + float num = float.MaxValue; + foreach (PlayerControl playerControl in impostors) + { + if (!(playerControl == null)) + { + float sqrMagnitude = (playerControl.transform.position - localPlayer.transform.position).sqrMagnitude; + if (sqrMagnitude < scaryMusicDistance && num > sqrMagnitude) + { + num = sqrMagnitude; + } + } + } + + if (HideAndSeekHudHelper.Instance.HideCountdown > 0f) + { + dangerLevel1 = 0f; + dangerLevel2 = 0f; + } + else + { + if (firstMusicActivation) + { + firstMusicActivation = false; + firstCrossfadeCountdown = 3f; + HnsMusicHandler.Instance.SetMusicCrossfadeSpeed(0.6f); + } + + if (firstCrossfadeCountdown > 0f) + { + firstCrossfadeCountdown -= Time.deltaTime; + if (firstCrossfadeCountdown <= 0f) + { + HnsMusicHandler.Instance.SetMusicCrossfadeSpeed(5f); + } + } + + dangerLevel1 = Mathf.Clamp01( + (scaryMusicDistance - num) / (scaryMusicDistance - veryScaryMusicDistance)); + dangerLevel2 = Mathf.Clamp01((veryScaryMusicDistance - num) / veryScaryMusicDistance); + } + + UpdateDangerMeter(); + UpdateDangerMusic(); + } + + private void UpdateDangerMusic() + { + PlayerControl localPlayer = PlayerControl.LocalPlayer; + if (localPlayer != null && localPlayer.Data != null && localPlayer.Data.IsDead) + { + HnsMusicHandler.Instance.SetTaskState(false); + HnsMusicHandler.Instance.ResetMusic(); + return; + } + + HnsMusicHandler.Instance.SetMusicValues(dangerLevel1, dangerLevel2); + } + + private void UpdateDangerMeter() + { + if (dangerMeter == null) + { + return; + } + + dangerMeter.SetDangerValue(dangerLevel1, dangerLevel2); + } + + public void OnGameStart() + { + firstMusicActivation = true; + if (!PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + dangerMeter = HudManager.Instance.DangerMeter; + dangerMeter.gameObject.SetActive(true); + } + + impostors = Helpers.GetAlivePlayers().Where(x => x.Data.Role.IsImpostor).ToList(); + + var baseSpeed = OptionGroupSingleton.Instance.PlayerSpeed.Value; + scaryMusicDistance = 55f * + baseSpeed; + veryScaryMusicDistance = 15f * + baseSpeed; + if (scaryMusicDistance < veryScaryMusicDistance) + { + float num = veryScaryMusicDistance; + float num2 = scaryMusicDistance; + scaryMusicDistance = num; + veryScaryMusicDistance = num2; + } + } +} diff --git a/MiraAPI/HnsReimplemented/HnsMusicHandler.cs b/MiraAPI/HnsReimplemented/HnsMusicHandler.cs new file mode 100644 index 0000000..e3b7d68 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HnsMusicHandler.cs @@ -0,0 +1,237 @@ +using System.Collections.Generic; +using Amongus.GameModes.HideAndSeek; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HnsMusicHandler Instance; + + private void Awake() + { + Instance = this; + } + + private HideAndSeekMusicCollection musicCollection; + + private float lastMusicSyncTime; + + private bool isDoingTask; + + private float normalVolume; + + private float taskVolume; + + private float dangerLevel1Volume; + + private float dangerLevel2Volume; + + private AudioSource normalSource; + + private AudioSource taskSource; + + private AudioSource dangerLevel1Source; + + private AudioSource dangerLevel2Source; + + private float musicLerpSpeed = 5f; + + public void Start() + { + musicCollection = GameManagerCreator.Instance.HideAndSeekManagerPrefab.MusicCollection; + } + + public void OnGameStart() + { + InitMusic(); + ResetMusic(); + } + + public void OnDestroy() + { + ResetMusic(); + } + + private void InitMusic() + { + if (normalSource == null) + { + normalSource = + SoundManager.Instance.GetNamedSfxSource(musicNames[LogicHnSMusic.HideAndSeekMusicTrack.Normal]); + } + + normalSource.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + normalSource.clip = musicCollection.NormalMusic; + normalSource.loop = true; + if (taskSource == null) + { + taskSource = + SoundManager.Instance.GetNamedSfxSource(musicNames[LogicHnSMusic.HideAndSeekMusicTrack.Task]); + } + + taskSource.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + taskSource.volume = 0f; + taskSource.clip = musicCollection.TaskMusic; + taskSource.loop = true; + if (dangerLevel1Source == null) + { + dangerLevel1Source = + SoundManager.Instance.GetNamedSfxSource( + musicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1]); + } + + dangerLevel1Source.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + dangerLevel1Source.volume = 0f; + dangerLevel1Source.clip = musicCollection.DangerLevel1Music; + dangerLevel1Source.loop = true; + if (dangerLevel2Source == null) + { + dangerLevel2Source = + SoundManager.Instance.GetNamedSfxSource( + musicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2]); + } + + dangerLevel2Source.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + dangerLevel2Source.volume = 0f; + dangerLevel2Source.clip = musicCollection.DangerLevel2Music; + dangerLevel2Source.loop = true; + normalSource.Play(); + taskSource.Play(); + dangerLevel1Source.Play(); + dangerLevel2Source.Play(); + SyncMusic(); + } + + public void StartMusicWithIntro() + { + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + AudioClip clip = OptionGroupSingleton.Instance.HidingTime.Value <= 180f + ? musicCollection.ImpostorShortMusic + : musicCollection.ImpostorLongMusic; + if (AprilFoolsMode.ShouldHorseAround()) + { + clip = musicCollection.ImpostorRanchMusic; + } + + SoundManager.Instance.PlaySound(clip, true, 1f, SoundManager.Instance.MusicChannel); + } + } + + public void SetTaskState(bool isDoingTask) + { + isDoingTask = isDoingTask; + } + + public void FixedUpdate() + { + if (normalSource == null || taskSource == null || dangerLevel1Source == null || + dangerLevel2Source == null) + { + return; + } + + if (Time.unscaledTime > lastMusicSyncTime + 1f) + { + SyncMusic(); + } + + normalSource.volume = Mathf.Lerp( + normalSource.volume, + normalVolume, + Time.fixedDeltaTime * musicLerpSpeed); + taskSource.volume = Mathf.Lerp( + taskSource.volume, + taskVolume, + Time.fixedDeltaTime * musicLerpSpeed); + dangerLevel1Source.volume = Mathf.Lerp( + dangerLevel1Source.volume, + dangerLevel1Volume, + Time.fixedDeltaTime * musicLerpSpeed); + dangerLevel2Source.volume = Mathf.Lerp( + dangerLevel2Source.volume, + dangerLevel2Volume, + Time.fixedDeltaTime * musicLerpSpeed); + } + + private void SyncMusic() + { + taskSource.timeSamples = normalSource.timeSamples; + dangerLevel1Source.timeSamples = normalSource.timeSamples; + dangerLevel2Source.timeSamples = normalSource.timeSamples; + lastMusicSyncTime = Time.unscaledTime; + } + + public void ResetMusic() + { + SetMusicValues(0f, 0f); + } + + public void SetMusicCrossfadeSpeed(float lerpSpeed) + { + musicLerpSpeed = lerpSpeed; + } + + public void SetMusicValues(float dangerLevel1, float dangerLevel2) + { + if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + return; + } + + if (normalSource == null || taskSource == null || dangerLevel1Source == null || + dangerLevel2Source == null) + { + return; + } + + normalVolume = (isDoingTask ? 0f : 1f); + taskVolume = (isDoingTask ? 1f : 0f); + dangerLevel1Volume = 0f; + dangerLevel2Volume = 0f; + if (dangerLevel1 > 0f) + { + dangerLevel1Volume = dangerLevel1; + if (isDoingTask) + { + taskVolume = 1f - dangerLevel1; + } + else + { + normalVolume = 1f - dangerLevel1; + } + } + + if (dangerLevel2 > 0f) + { + dangerLevel2Volume = dangerLevel2; + dangerLevel1Volume = 1f - dangerLevel2; + } + } + + private readonly Dictionary musicNames = + new Dictionary + { + { + LogicHnSMusic.HideAndSeekMusicTrack.Normal, + "HnS_Music_Normal" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.Task, + "HnS_Music_Task" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1, + "HnS_Music_DangerLevel1" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2, + "HnS_Music_DangerLevel2" + }, + }; +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs new file mode 100644 index 0000000..cf1a7c6 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs @@ -0,0 +1,89 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsCrewmateOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Crewmates"; + + public ModdedNumberOption PlayerSpeed { get; set; } = new( + "Player Speed", + 1f, + 0.5f, + 3f, + 0.25f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption HidingTime { get; set; } = new( + "Hiding Time", + 200, + 160, + 300, + 20, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedNumberOption CrewmateVision { get; set; } = new( + "Crewmate Vision", + 0.6f, + 0.25f, + 1, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption MaxVentUses { get; set; } = new( + "Max Vent Uses", + 1, + 0, + 5, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption MaxTimeInVent { get; set; } = new( + "Max Time in Vent", + 3, + 1, + 10, + 1, + "#", + "#", + MiraNumberSuffixes.Seconds) + { + Visible = () => (int)OptionGroupSingleton.Instance.MaxVentUses.Value > 0, + }; + + public ModdedToggleOption FlashlightMode { get; set; } = new( + "Flashlight Mode", + true); + + public ModdedNumberOption CrewmateFlashlightSize { get; set; } = new( + "Crewmate Flashlight Size", + 0.35f, + 0.1f, + 0.5f, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier) + { + Visible = () => OptionGroupSingleton.Instance.FlashlightMode.Value, + }; + + public ModdedToggleOption ShowNames { get; set; } = new( + "Show Names", + false); +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs new file mode 100644 index 0000000..009b248 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs @@ -0,0 +1,52 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsFinalHideOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Final Hide"; + + public ModdedNumberOption FinalHideTime { get; set; } = new( + "Hiding Time", + 50, + 30, + 120, + 5, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedToggleOption FinalHidePings { get; set; } = new( + "Final Hide Pings", + true); + + public ModdedNumberOption FinalHideImpostorSpeed { get; set; } = new( + "Final Hide Impostor Speed", + 1.2f, + 1f, + 3, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption PingInterval { get; set; } = new( + "Ping Interval", + 6, + 3, + 10, + 5, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedToggleOption FinalHideSeekMap { get; set; } = new( + "Final Hide Seek Map", + true); +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs new file mode 100644 index 0000000..c0c61b5 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs @@ -0,0 +1,40 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsImpostorOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Impostors"; + + public ModdedPlayerOption SelectedSeeker { get; set; } = new( + "Forced Impostor"); + + public ModdedNumberOption ImpostorVision { get; set; } = new( + "Impostor Vision", + 0.6f, + 0.25f, + 1, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption ImpostorFlashlightSize { get; set; } = new( + "Impostor Flashlight Size", + 0.35f, + 0.1f, + 0.5f, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier) + { + Visible = () => OptionGroupSingleton.Instance.FlashlightMode.Value, + }; +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs new file mode 100644 index 0000000..7be2864 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs @@ -0,0 +1,43 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsTaskOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Tasks"; + + public ModdedNumberOption CommonTasks { get; set; } = new( + "# Common Tasks", + 1, + 0, + 4, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption LongTasks { get; set; } = new( + "# Long Tasks", + 1, + 0, + 4, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption ShortTasks { get; set; } = new( + "# Short Tasks", + 2, + 0, + 8, + 1, + "#", + "#", + MiraNumberSuffixes.None); +} diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs index 70ca14b..b1ddbce 100644 --- a/MiraAPI/Hud/CustomButtonManager.cs +++ b/MiraAPI/Hud/CustomButtonManager.cs @@ -17,6 +17,20 @@ public static class CustomButtonManager /// public static ReadOnlyCollection Buttons { get; internal set; } = new([]); + /// + /// Gets a list of all registered button-specific s. + /// + 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 + { }); + internal static readonly List CustomButtons = []; internal static readonly Dictionary ButtonEventTypes = []; internal static readonly Dictionary ButtonCancelledEventTypes = []; diff --git a/MiraAPI/Hud/MiraHudHelper.cs b/MiraAPI/Hud/MiraHudHelper.cs new file mode 100644 index 0000000..59a8857 --- /dev/null +++ b/MiraAPI/Hud/MiraHudHelper.cs @@ -0,0 +1,203 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using InnerNet; +using MiraAPI.LocalSettings; +using MiraAPI.Modifiers.ModifierDisplay; +using Reactor.Utilities.Attributes; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Hud; + +[RegisterInIl2Cpp] +public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static MiraHudHelper Instance { get; private set; } + public static GameObject ModifierDisplayObject; + public static GameObject VanillaMatchInfoButton; + public static bool ModifierDisplayOnRight; + public static GameObject ClonedChatButton; + public static GameObject ExtraUiTopRight; + public static GridArrange ExtraUiGrid; + public static AspectPosition ExtraUiAspectPos; + public static GameObject UiTopRight; + public static GridArrange UiGrid; + public static AspectPosition UiAspectPos; + public static GameObject SubmergedFloorButton; + public static SpriteRenderer SubmergedFloorButtonRenderer; + public static SpriteRenderer SubmergedFloorButtonRendererHover; + public void Awake() + { + Instance = this; + } + + public void Start() + { + MiraApiSettings.OldButtonScaleFactor = + LocalSettingsTabSingleton.Instance.TopRightButtonsFactorSlider.Value; + StartCoroutine(MiraApiSettings.CoResizeSettingsUI().WrapToIl2Cpp()); + } +#pragma warning disable S2325 + #pragma warning disable CA1822 + public void FixedUpdate() + { + if (!HudManager.InstanceExists || !PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data) + { + return; + } + + var instance = HudManager.Instance; + + CreateUiRow(instance); + CreateNewUiRow(instance); + AdjustModifierTab(); + + if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data || !PlayerControl.LocalPlayer.Data.Role || + !ShipStatus.Instance || + (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started && + !TutorialManager.InstanceExists)) + { + return; + } + UpdateSubmergedButtons(instance); + } + #pragma warning restore CA1822 + #pragma warning restore S2325 + + public static Vector3 BelowOptionPos = new(0.435f, 1.25f, 0f); + public static Vector3 FullTopPos = new(0.435f, 0.475f, 0f); + public static void CreateUiRow(HudManager instance) + { + if (!UiTopRight) + { + UiTopRight = instance.MapButton.transform.parent.gameObject; + + UiGrid = UiTopRight.AddComponent(); + UiAspectPos = UiTopRight.AddComponent(); + + UiGrid.Alignment = GridArrange.StartAlign.Left; + UiGrid.CellSize = new Vector2(0.85f, 0.85f); + UiGrid.MaxColumns = 6; + UiAspectPos.Alignment = AspectPosition.EdgeAlignments.RightTop; + UiGrid.Start(); + UiAspectPos.DistanceFromEdge = FullTopPos; + var mapButton = instance.MapButton.gameObject; + mapButton.GetComponent().Destroy(); + var settingsButton = instance.SettingsButton; + settingsButton.GetComponent().Destroy(); + VanillaMatchInfoButton = instance.MatchInfoButton.gameObject; + VanillaMatchInfoButton.GetComponent().Destroy(); + var oldPos = settingsButton.transform.localPosition; + settingsButton.transform.localPosition = new Vector3(oldPos.x, oldPos.y, -100); + var chatButton = instance.Chat.chatButton.gameObject; + ClonedChatButton = Object.Instantiate(chatButton, chatButton.transform.parent); + ClonedChatButton.SetActive(false); + instance.Chat.chatButtonAspectPosition = ClonedChatButton.GetComponent(); + chatButton.GetComponent().Destroy(); + var inactivePos = settingsButton.transform.GetChild(1).transform.localPosition; + var bg = settingsButton.transform.GetChild(2).gameObject; + var bgPos = bg.transform.localPosition; + var bgSprite = bg.GetComponent().sprite; + var activePos = settingsButton.transform.GetChild(3).transform.localPosition; + var selectedPos = settingsButton.transform.GetChild(4).transform.localPosition; + chatButton.transform.GetChild(2).transform.localPosition = inactivePos; + var chatBg = chatButton.transform.GetChild(3); + chatBg.transform.localPosition = bgPos; + chatBg.GetComponent().sprite = bgSprite; + chatButton.transform.GetChild(4).transform.localPosition = activePos; + chatButton.transform.GetChild(5).transform.localPosition = selectedPos; + var collider = chatButton.GetComponent(); + collider.size = new Vector2(0.4354f, 0.4003f); + collider.offset = new Vector2(0.0025f, 0.0254f); + if (FriendsListManager.InstanceExists && !TutorialManager.InstanceExists) + { + var listButton = FriendsListManager.Instance.FriendsListButton.transform.GetChild(0); + listButton.transform.SetParent(UiTopRight.transform, false); + FriendsListManager.Instance.FriendsListButton = listButton.GetComponent(); + listButton.GetComponent().Destroy(); + listButton.localPosition = new Vector3(0, 0, 0); + } + settingsButton.transform.SetAsLastSibling(); + chatButton.transform.SetParent(UiTopRight.transform, false); + instance.Chat.chatButton = chatButton.GetComponent(); + var iconContainer = new GameObject("iconContainer") + { + layer = LayerMask.NameToLayer("UI"), + }; + iconContainer.transform.SetParent(chatButton.transform, false); + iconContainer.transform.localPosition = new Vector3(0.1f, -0.1f, 0); + instance.Chat.chatNotifyDot.transform.SetParent(iconContainer.transform, false); + instance.Chat.chatNotifyDot = iconContainer.transform.GetChild(0).GetComponent(); + } + + if (UiTopRight && UiGrid) + { + var isChatButtonVisible = HudManager.Instance.Chat.isActiveAndEnabled; + instance.Chat.chatButton.gameObject.SetActive(isChatButtonVisible); + if (VanillaMatchInfoButton) + { + VanillaMatchInfoButton.SetActive(!GameSettingMenu.Instance && !Minigame.Instance); + } + } + } + public static void UpdateSubmergedButtons(HudManager instance) + { + if (ModCompatibility.IsSubmerged() && !SubmergedFloorButton && ExtraUiTopRight) + { + if (!SubmergedFloorButton && ExtraUiTopRight) + { + var transform = instance.MapButton.transform.parent.Find(instance.MapButton.name + "(Clone)"); + if (transform != null) + { + SubmergedFloorButton = transform.gameObject; + SubmergedFloorButton.transform.SetParent(ExtraUiTopRight.transform, false); + + SubmergedFloorButtonRenderer = + SubmergedFloorButton.transform.Find("Inactive").GetComponent(); + SubmergedFloorButtonRendererHover = + SubmergedFloorButton.transform.Find("Active").GetComponent(); + + MiraApiSettings.SetUpButtonPositions(); + } + } + } + } + + public static void AdjustModifierTab() + { + if (!ModifierDisplayObject && UiTopRight && ExtraUiTopRight && ModifierDisplayComponent.Instance) + { + ModifierDisplayObject = ModifierDisplayComponent.Instance?.gameObject ?? null!; + ModifierDisplayOnRight = !LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value; + if (ModifierDisplayOnRight) + { + ModifierDisplayObject.transform.SetParent(ExtraUiTopRight.transform, false); + ModifierDisplayObject.GetComponent().Destroy(); + var oldPos = ModifierDisplayObject.transform.GetChild(0).localPosition; + ModifierDisplayObject.transform.GetChild(0).localPosition = new Vector3(-1.1757f, -2.1633f, oldPos.z); + oldPos = ModifierDisplayObject.transform.GetChild(1).localPosition; + ModifierDisplayObject.transform.GetChild(1).localPosition = new Vector3(-0.45f, 0.3f, oldPos.z); + } + MiraApiSettings.SetUpButtonPositions(); + } + } + public static void CreateNewUiRow(HudManager instance) + { + if (!ExtraUiTopRight && UiTopRight) + { + ExtraUiTopRight = new GameObject("ExtraUiTopRight") + { + layer = UiTopRight.layer + }; + ExtraUiTopRight.transform.SetParent(instance.MapButton.transform.parent.parent, false); + + ExtraUiGrid = ExtraUiTopRight.AddComponent(); + ExtraUiAspectPos = ExtraUiTopRight.AddComponent(); + + ExtraUiGrid.Alignment = GridArrange.StartAlign.Left; + ExtraUiGrid.CellSize = new Vector2(0.85f, 0.85f); + ExtraUiAspectPos.Alignment = AspectPosition.EdgeAlignments.RightTop; + ExtraUiAspectPos.DistanceFromEdge = BelowOptionPos; + ExtraUiGrid.Start(); + } + } +} diff --git a/MiraAPI/Hud/MultiTargetButton.cs b/MiraAPI/Hud/MultiTargetMeetingButton.cs similarity index 96% rename from MiraAPI/Hud/MultiTargetButton.cs rename to MiraAPI/Hud/MultiTargetMeetingButton.cs index d52d845..2a36219 100644 --- a/MiraAPI/Hud/MultiTargetButton.cs +++ b/MiraAPI/Hud/MultiTargetMeetingButton.cs @@ -8,7 +8,7 @@ namespace MiraAPI.Hud; /// that has multiple target objects. /// /// The type of the target objects. -public abstract class MultiTargetButton : CustomActionButton where T : MonoBehaviour +public abstract class MultiTargetMeetingButton : CustomActionButton where T : MonoBehaviour { /// /// Gets or sets the list of targets of the button. diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index 52abe60..d7c2c64 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -1,11 +1,13 @@ using System; using System.Linq; using BepInEx.Configuration; +using MiraAPI.PluginLoading; using UnityEngine; namespace MiraAPI.LocalSettings.SettingTypes; /// +[MiraIgnore] public abstract class LocalSettingBase : ILocalSetting { /// diff --git a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs new file mode 100644 index 0000000..8474757 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs @@ -0,0 +1,120 @@ +using System; +using System.Globalization; +using Reactor.Utilities.Attributes; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; + +namespace MiraAPI.MeetingAbilities; + +/// +/// A component for handling targeted meeting buttons. +/// +[RegisterInIl2Cpp] +public class MeetingAbilityBehaviour : MonoBehaviour +{ + /// + /// Gets or sets whether the component has been initialized. + /// + private bool _init; + + /// + /// The attached to this component. + /// + private TargetedMeetingButton _button = null!; + + /// + /// The of this component. + /// + public PassiveButton Button; + + /// + /// The parent . + /// + public PlayerVoteArea VoteArea; + + /// + /// The button's component. + /// + public SpriteRenderer Renderer; + + /// + /// The label used for showing the ability's cooldown. + /// + public TextMeshPro CooldownText; + + /// + /// The label used for showing the ability's remaining uses. + /// + public TextMeshPro UsesText; + + /// + /// Initializes the various properties of this component. + /// + /// The to initialize this component for. + /// The parent . + public void Initialize(TargetedMeetingButton button, PlayerVoteArea voteArea) + { + if (_init) return; + VoteArea = voteArea; + _button = button; + Renderer = GetComponent(); + SetFillUp(1, 1); + if (!TryGetComponent(out Button)) + { + Error($"Could not initialize MeetingButtonBehaviour for {Button.GetType().Name}, Destroying..."); + Destroy(this); + } + _init = true; + CooldownText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, transform); + CooldownText.GetComponent().Destroy(); + CooldownText.transform.localPosition = new Vector3(0, 0, -10); + CooldownText.m_maxFontSize = 3f; + CooldownText.color = new Color(1, 1, 1, 0.7f); + UsesText = Instantiate(CooldownText, transform); + UsesText.transform.localPosition = new Vector3(0.25f, 0.25f, -10); + UsesText.transform.localScale = Vector3.one * 0.7f; + + CooldownText.gameObject.SetActive(true); + UsesText.gameObject.SetActive(true); + } + + private void OnEnable() + { + if (!_init) return; + Button.enabled = true; + Renderer.enabled = true; + UsesText.enabled = true; + CooldownText.enabled = true; + if (_button.Enabled(PlayerControl.LocalPlayer.Data.Role) && _button.IsTargetValid(VoteArea)) return; + Button.enabled = false; + Renderer.enabled = false; + UsesText.enabled = false; + CooldownText.enabled = false; + } + + private void Update() + { + if (!_init) return; + if (_button.Timer < 0.01f) + { + Renderer.material.SetFloat("_Desat", 0.0f); + CooldownText.text = string.Empty; + UsesText.text = _button.LimitedUses ? _button.UsesLeft.ToString(CultureInfo.InvariantCulture) : string.Empty; + } + else + { + Renderer.material.SetFloat("_Desat", 0.5f); + CooldownText.text = _button.Timer.ToString(_button.CooldownTimerFormatString, NumberFormatInfo.InvariantInfo); + UsesText.text = string.Empty; + } + SetFillUp(_button.Timer, _button.InitialCooldown); + } + + public void SetFillUp(float timer, float maxTimer) + { + float percentCool = Mathf.Clamp(timer / maxTimer, 0.001f, 1f); + if (percentCool <= 0) percentCool = 1f; + Renderer.material.SetFloat("_Percent", percentCool); + } +} diff --git a/MiraAPI/MeetingAbilities/MeetingActionButton.cs b/MiraAPI/MeetingAbilities/MeetingActionButton.cs new file mode 100644 index 0000000..88cc799 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingActionButton.cs @@ -0,0 +1,321 @@ +using System; +using MiraAPI.Events; +using MiraAPI.Events.Mira; +using MiraAPI.Hud; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; +using Object = UnityEngine.Object; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Class for making custom meeting action buttons. +/// +public abstract class MeetingActionButton +{ + /// + /// Gets the name and text of the button. + /// + public abstract string Name { get; } + + /// + /// Gets the initial cooldown duration in seconds. + /// + public virtual float InitialCooldown => Cooldown; + + /// + /// Gets the button's cooldown duration in seconds. + /// + public abstract float Cooldown { get; } + + /// + /// Gets the sprite of the button. Use to load a sprite from a resource path. Use to load a sprite from an asset bundle. + /// + public abstract LoadableAsset Sprite { get; } + + /// + /// Gets the format string for the cooldown timer. + /// + public virtual string CooldownTimerFormatString => "0"; + + /// + /// Gets a value indicating whether the button has limited uses. + /// + public bool LimitedUses => ZeroIsInfinite ? MaxUses > 0 : MaxUses >= 0; + + /// + /// Gets the maximum number of uses the button has. If the button has infinite uses, set to 0 or -1 based on what value that ZeroIsInfinite is set to. + /// + public virtual int MaxUses => ZeroIsInfinite ? 0 : -1; + + /// + /// Gets the value indicating uses mode. + /// + public virtual MeetingButtonUsesMode UsesMode => MeetingButtonUsesMode.PerGame; + + /// + /// Gets the button's text outline . + /// + public virtual Color TextOutlineColor => Color.clear; + + /// + /// Gets or sets a value indicating whether limited uses are determined via zero or a negative number of uses. + /// + public virtual bool ZeroIsInfinite { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the timer is currently active. + /// + public bool TimerPaused { get; set; } + + /// + /// Gets or sets the amount of uses left. + /// + public int UsesLeft { get; set; } + + /// + /// Gets or sets the timer variable to measure cooldowns and effects. + /// + public float Timer { get; set; } + + /// + /// Gets or sets the object in game. This is created by Mira API automatically. + /// + public ActionButton? Button { get; set; } + + /// + /// The method used to create the button. + /// + /// The parent of the button. + public virtual void CreateButton(Transform parent) + { + if (Button) + { + return; + } + + Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown; + TimerPaused = false; + + Button = Object.Instantiate(MeetingHud.Instance.MeetingAbilityButton, parent); + Button.name = Name + "Button"; + Button.OverrideText(Name.ToUpperInvariant()); + + Button.graphic.sprite = Sprite.LoadAsset(); + + Button.SetUsesRemaining(UsesLeft); + if (MaxUses <= 0) + { + Button.SetInfiniteUses(); + } + + if (TextOutlineColor != Color.clear) + { + SetTextOutline(TextOutlineColor); + } + + var pb = Button.GetComponent(); + pb.OnClick = new Button.ButtonClickedEvent(); + pb.OnClick.AddListener((UnityAction)(() => + { + ClickHandler(); + })); + Button.gameObject.SetActive(Enabled(PlayerControl.LocalPlayer.Data.Role)); + } + + /// + /// A utility function to change the outline of the button's text. + /// + /// The new . + public virtual void SetTextOutline(Color color) + { + Button?.buttonLabelText.SetOutlineColor(color); + } + + /// + /// A utility function to override the of the button. + /// + /// The new to override with. + public virtual void OverrideSprite(Sprite sprite) + { + if (Button != null) + { + Button.graphic.sprite = sprite; + } + } + + /// + /// A utility function to override the name of the button. + /// + /// The new name to override with. + public virtual void OverrideName(string name) + { + Button?.OverrideText(name); + } + + /// + /// Set the button's timer. + /// + /// The time you want to set to. + public virtual void SetTimer(float time) + { + Timer = Mathf.Clamp(time, -1, float.MaxValue); + } + + /// + /// Increase the button's timer. + /// + /// The amount you want to increase by. + public virtual void IncreaseTimer(float amount) + { + SetTimer(Timer + amount); + } + + /// + /// Decrease the button's timer. + /// + /// The amount you want to decrease by. + public virtual void DecreaseTimer(float amount) + { + SetTimer(Timer - amount); + } + + /// + /// Sets whether the timer is paused or not. + /// + /// Whether you want to pause/resume the timer. + public virtual void SetTimerPaused(bool val) + { + TimerPaused = val; + } + + /// + /// Set the amount of uses this button has left. + /// + /// The amount you want to set to. + public virtual void SetUses(int amount) + { + UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue); + Button?.SetUsesRemaining(UsesLeft); + + if (Button != null) + { + Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white; + } + } + + /// + /// Increase the amount of uses this button has left. + /// + /// The amount you want to increase by. Default: 1. + public virtual void IncreaseUses(int amount = 1) + { + SetUses(UsesLeft + amount); + } + + /// + /// Decrease the amount of uses this button has left. + /// + /// The amount you want to decrease by. Default: 1. + public virtual void DecreaseUses(int amount = 1) + { + SetUses(UsesLeft - amount); + } + + /// + /// A utility function that runs with the local player's if the button is enabled. + /// + /// the instance. + protected virtual void FixedUpdate(MeetingHud hud) + { + } + + /// + /// Callback method for the button click event. + /// + protected abstract void OnClick(); + + /// + /// This method determines if the button should be active or not. + /// means the button is active, means the button is disabled. + /// + /// The of the local player. + /// if the button is enabled, otherwise. + public abstract bool Enabled(RoleBehaviour? role); + + /// + /// When the button is usable, this method is called to determine if the button can be clicked. + /// By default, it takes into account the timer, effect, and uses. + /// You can override it to change the behavior. + /// + /// A value that represents whether the button can be clicked or not. + public virtual bool CanClick() + { + return Timer <= 0 && CanUse(); + } + + /// + /// Whether the button should light up or not. This is also the base for CanClick. + /// You can override it to change the behaviour. Do not include timer in here, that is for CanClick. + /// + /// A value that represents whether the button should light up or not. + public virtual bool CanUse() + { + return !LimitedUses || UsesLeft > 0; + } + + /// + /// This method handles the button click event. It is a wrapper for the method. + /// This method takes into account cooldowns, effects, and uses, before calling the method. + /// It can be overridden for custom behavior. + /// + public virtual void ClickHandler() + { + if (!CanClick()) + { + return; + } + + if (LimitedUses) + { + UsesLeft--; + Button?.SetUsesRemaining(UsesLeft); + } + + Timer = Cooldown; + OnClick(); + } + + /// + /// This method is called on the method. It is a wrapper for the method. + /// By default, it handles the cooldown and effect timers, and sets the button to enabled or disabled. + /// It can be overridden for custom behavior. + /// + /// The instance. + public virtual void UpdateHandler(MeetingHud hud) + { + if (Timer >= 0 && !TimerPaused) + { + Timer -= Time.deltaTime; + } + + if (Button) + { + if (CanUse()) + { + Button!.SetEnabled(); + } + else + { + Button!.SetDisabled(); + } + Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString); + Button.SetCoolDown(Timer, Cooldown); + } + + FixedUpdate(hud); + } +} diff --git a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs new file mode 100644 index 0000000..b45e00b --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; +using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; +using MiraAPI.Utilities.Components; +using UnityEngine; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Class for managing all registered meeting abilities. +/// +public static class MeetingButtonManager +{ + /// + /// Gets a list if==of all registered . + /// + public static List TargetedButtons { get; internal set; } = new(); + + /// + /// Gets a list if==of all registered . + /// + public static List UntargetedButtons { get; internal set; } = new(); + + internal static bool RegisterTargetedMeetingButton(Type type, MiraPluginInfo info) + { + if (!typeof(TargetedMeetingButton).IsAssignableFrom(type)) + { + return false; + } + + var button = Activator.CreateInstance(type) as TargetedMeetingButton; + info.InternalTargetedMeetingButtons.Add(button); + TargetedButtons.Add(button); + return true; + } + + internal static bool RegisterMeetingButton(Type type, MiraPluginInfo info) + { + if (!typeof(MeetingActionButton).IsAssignableFrom(type)) + { + return false; + } + + var button = Activator.CreateInstance(type) as MeetingActionButton; + info.InternalMeetingButtons.Add(button); + UntargetedButtons.Add(button); + return true; + } + + private static CenteredGridArrange? actionButtonsParent = null!; + + public static void OnGameStart() + { + foreach (var ability in TargetedButtons.Where(x => x.UsesMode == MeetingButtonUsesMode.PerGame)) + { + ability.UsesLeft = ability.MaxUses; + } + foreach (var ability in UntargetedButtons.Where(x => x.UsesMode == MeetingButtonUsesMode.PerGame)) + { + ability.UsesLeft = ability.MaxUses; + } + } + + /// + /// Called on . Handles button creation, and ability uses. + /// + /// The instance. + public static void OnMeetingStart(MeetingHud meetingHud) + { + // Untargeted buttons parent. + actionButtonsParent = new GameObject("ActionsButtonParent").AddComponent(); + actionButtonsParent.transform.SetParent(meetingHud.MeetingAbilityButton.transform.parent, meetingHud.MeetingAbilityButton.transform.parent); + actionButtonsParent.CellSize = new Vector2(0.7f, 0.7f); + actionButtonsParent.transform.localPosition = new Vector3(0, -2.1f, -10); + meetingHud.MeetingAbilityButton.transform.SetParent(actionButtonsParent.transform); + + // Untargeted buttons creation. + foreach (var ability in UntargetedButtons) + { + try + { + ability.CreateButton(actionButtonsParent.transform); + if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses; + ability.Timer = ability.InitialCooldown; + } + catch (Exception e) + { + Error($"Failed to create meeting button {ability.Name}: {e.Message}"); + } + } + + // Targeted buttons creation. + foreach (var ability in TargetedButtons) + { + try + { + foreach (var playerVoteArea in meetingHud.playerStates) + { + var btn = ability.CreateButton(playerVoteArea); + btn.transform.SetParent(playerVoteArea.Buttons.transform); + playerVoteArea.CancelButton.transform.SetAsFirstSibling(); + } + + if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses; + if (ability is MultiTargetMeetingButton multiAbility) multiAbility.Targets = new(); + } + catch (Exception e) + { + Error($"Failed to create targeted meeting button {ability.Name}: {e.Message}"); + } + } + } +} diff --git a/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs b/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs new file mode 100644 index 0000000..a4ddb40 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs @@ -0,0 +1,7 @@ +namespace MiraAPI.MeetingAbilities; + +public enum MeetingButtonUsesMode +{ + PerGame, + PerMeeting +} \ No newline at end of file diff --git a/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs new file mode 100644 index 0000000..b4dcffa --- /dev/null +++ b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.UI; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Abstract class for creating multi target meeting abilities +/// +public abstract class MultiTargetMeetingButton : TargetedMeetingButton +{ + /// + /// The used for when the button is on. + /// + public abstract LoadableAsset SpriteActive { get; } + + /// + /// Gets the required targets count. + /// + public abstract int TargetsCount { get; } + + /// + /// Gets or sets all selected targets. + /// + public Dictionary Targets { get; set; } = new(); + + /// + public override void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea) + { + if (UsesLeft <= 0 && LimitedUses) return; + if (Timer > 0) return; + var toggle = !Targets.Remove(playerVoteArea); + if (toggle) Targets.Add(playerVoteArea, button); + button.Renderer.sprite = toggle ? SpriteActive.LoadAsset() : Sprite.LoadAsset(); + if (Targets.Count >= TargetsCount) + { + playerVoteArea.Cancel(); + HandleFinish(); + return; + } + OnSelect(playerVoteArea, toggle); + } + + /// + /// Handles the finish logic, increasing timer, and decreasing uses. + /// + public virtual void HandleFinish() + { + Targets = Targets.Take(TargetsCount).ToDictionary(pair => pair.Key, pair => pair.Value); + foreach (var button in Targets.Values) + { + button.Renderer.sprite = Sprite.LoadAsset(); + } + Timer = Cooldown; + UsesLeft -= 1; + OnFinish(); + Targets = new(); + } + + /// + /// Callback method for when a player is selected. + /// + /// The target . + /// Whether the button was toggled on/off. + protected abstract void OnSelect(PlayerVoteArea playerVoteArea, bool toggle); + + /// + /// Callback method for the button click. + /// USE INSTEAD!!!. + /// + /// The target . + protected override void OnClick(PlayerVoteArea playerVoteArea) + { + } + + /// + /// Callback method for when enough targets are chosen. + /// + public abstract void OnFinish(); + + /// + public override MeetingAbilityBehaviour CreateButton(PlayerVoteArea playerVoteArea) + { + Targets = new(); + var button = base.CreateButton(playerVoteArea); + button.Button.OnClick = new Button.ButtonClickedEvent(); + button.Button.OnClick.AddListener(new System.Action(() => { ClickHandler(button, playerVoteArea); })); + return button; + } +} diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs new file mode 100644 index 0000000..324e7c7 --- /dev/null +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.UI; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Abstract class for creating targeted meeting abilities. +/// +public abstract class TargetedMeetingButton +{ + /// + /// Gets the name of the . + /// + public abstract string Name { get; } + + /// + /// Gets the Max Uses of the button. + /// Set it to 0 for Infinite uses. + /// + public abstract int MaxUses { get; } + + /// + /// Gets or sets the amount of uses left for this button. + /// + public int UsesLeft { get; set; } + + /// + /// Gets a value indicating whether the has limited uses. + /// + public bool LimitedUses => MaxUses != 0; + + /// + /// Gets the cooldown time for this button upon meeting start. + /// Defaults to unless overridden. + /// + public virtual float InitialCooldown => Cooldown; + + /// + /// Gets the cooldown time for this button after being used. + /// + public abstract float Cooldown { get; } + + /// + /// Gets or sets the current cooldown timer for this button. + /// + public float Timer { get; set; } + + /// + /// Gets the cooldown time for this button. + /// + public virtual string CooldownTimerFormatString => "0"; + + /// + /// Gets the sprite used for this button. + /// + public abstract LoadableAsset Sprite { get; } + + /// + /// Gets the button's outline color. + /// + public abstract Color OutlineColor { get; } + + /// + /// Gets the for this button. + /// + public virtual MeetingButtonUsesMode UsesMode { get; } = MeetingButtonUsesMode.PerGame; + + /// + /// Determines if the button is enabled. + /// + /// The local player's . + /// Whether the button is enabled or not. + public abstract bool Enabled(RoleBehaviour r); + + /// + /// Callback method for when the button is clicked. + /// + /// The target . + protected abstract void OnClick(PlayerVoteArea playerVoteArea); + + /// + /// Handles the button creation. + /// + /// The target . + /// The instance. + public virtual MeetingAbilityBehaviour CreateButton(PlayerVoteArea playerVoteArea) + { + if (MeetingHud.Instance == null) return null!; + + var btn = Object.Instantiate(MeetingHud.Instance.PlayerButtonPrefab.ConfirmButton).GetComponent(); + btn.gameObject.name = Name + "Button"; + + var spriteRenderer = btn.GetComponent(); + spriteRenderer.sprite = Sprite.LoadAsset(); + spriteRenderer.material = new Material(HudManager.Instance.KillButton.graphic.material); + spriteRenderer.SetCooldownNormalizedUvs(); + btn.gameObject.SetActive(true); + + btn.transform.GetChild(0).GetComponent().color = OutlineColor; + + btn.OnClick = new Button.ButtonClickedEvent(); + var abilityBehaviour = btn.gameObject.AddComponent(); + abilityBehaviour.Initialize(this, playerVoteArea); + btn.OnClick.AddListener(new System.Action(() => ClickHandler(abilityBehaviour, playerVoteArea))); + Timer = InitialCooldown; + + return abilityBehaviour; + } + + /// + /// Called every . + /// Handles the timer logic. + /// + public virtual void UpdateHandler() + { + Timer -= Time.deltaTime; + Timer = Mathf.Clamp(Timer, 0, int.MaxValue); + FixedUpdate(); + } + + /// + /// Callback method for handling logic every frame. + /// + public virtual void FixedUpdate() + { + } + + /// + /// Called when the button is clicked + /// Handles increasing cooldown and decreasing uses remaining. + /// + /// The instance. + /// The target . + public virtual void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea) + { + if (UsesLeft <= 0 && LimitedUses) return; + if (Timer > 0) return; + Timer = Cooldown; + UsesLeft--; + playerVoteArea.Cancel(); + OnClick(playerVoteArea); + } + + /// + /// Determines whether the target is valid. + /// + /// The target . + /// Whether the target is valid or not. + public virtual bool IsTargetValid(PlayerVoteArea playerVoteArea) + { + return playerVoteArea != null && playerVoteArea.PlayerId.Value != PlayerControl.LocalPlayer.Data.PlayerId; + } +} diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs new file mode 100644 index 0000000..47dc791 --- /dev/null +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace MiraAPI.MeetingAbilities; + +/// +/// A utility class to get the instance of a . +/// +/// The type of the button you are trying to access. +public static class TargetedMeetingButtonSingleton where T : TargetedMeetingButton +{ + private static T? _instance; + + /// + /// Gets the instance of the button. + /// + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a targeted meeting button.")] + public static T Instance => _instance ??= MeetingButtonManager.TargetedButtons.OfType().Single(); +} diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index ee3375d..c841fae 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,10 +1,12 @@ global using static Reactor.Utilities.Logger; using System; using BepInEx; +using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Translation; +using MiraAPI.VanillaEvents; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; @@ -19,10 +21,21 @@ namespace MiraAPI; [BepInAutoPlugin("mira.api", "MiraAPI")] [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] -[BepInDependency(ModCompatibility.SubmergedId, BepInDependency.DependencyFlags.SoftDependency)] [ReactorModFlags(ModFlags.RequireOnAllClients)] -public partial class MiraApiPlugin : BasePlugin +public partial class MiraApiPlugin : BasePlugin, IMiraPlugin { + /// + public ConfigFile GetConfigFile() + { + return Config; + } + + /// + public string OptionsTitleText => "MiraAPI"; + + /// + public bool DisplayOnOptionsMenu => false; + /// /// Gets a value indicating whether the current device is running Starlight (on mobile). /// @@ -50,12 +63,17 @@ public partial class MiraApiPlugin : BasePlugin public override void Load() { Harmony.PatchAll(); + JudgeEvents.Initialize(); ReactorCredits.Register("Mira API", Version, IsDevBuild, ReactorCredits.AlwaysShow); PluginManager = new MiraPluginManager(); - PluginManager.Initialize(); + PluginManager.Initialize(this, this); TranslationManager.Register("mira.api"); + + IL2CPPChainloader.Instance.Finished += + ModCompatibility + .Initialize; // Initialise AFTER the mods are loaded to ensure maximum parity (no need for the soft dependency either then) } } diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 64f3ca7..8259e7b 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -1,4 +1,6 @@ -using BepInEx.Configuration; +using System.Collections; +using BepInEx.Configuration; +using MiraAPI.Hud; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Patches; @@ -26,15 +28,21 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) }; /// - /// Gets or sets the value stored for scaling buttons properly. + /// Gets or sets the value stored for scaling ability buttons properly. /// public static float OldButtonScaleFactor { get; set; } + /// + /// Gets or sets the value stored for scaling UI buttons properly. + /// + public static float OldUiButtonScaleFactor { get; set; } + /// public override void Open() { base.Open(); OldButtonScaleFactor = ButtonUIFactorSlider.Value; + OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value; } /// @@ -50,12 +58,153 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) } OldButtonScaleFactor = ButtonUIFactorSlider.Value; } + else if (configEntry == TopRightButtonsFactorSlider) + { + if (MiraHudHelper.Instance) + { + ResizeUI(TopRightButtonsFactorSlider.Value); + } + OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value; + } + else if (configEntry == WikiOnBottomRow) + { + SetUpButtonPositions(); + } else if (configEntry == SetFpsSlider) { Application.targetFrameRate = (int)SetFpsSlider.Value; } } + public static void SetUpButtonPositions() + { + var topUi = MiraHudHelper.UiTopRight; + var extraTopUi = MiraHudHelper.ExtraUiTopRight; + var wikiButton = MiraHudHelper.VanillaMatchInfoButton; + var subButton = MiraHudHelper.SubmergedFloorButton; + var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; + ResetButtonPositions(); + if (topUi && extraTopUi) + { + var opts = LocalSettingsTabSingleton.Instance; + if (wikiButton) + { + wikiButton.transform.SetParent(opts.WikiOnBottomRow.Value ? extraTopUi.transform : topUi.transform); + } + if (subButton) + { + subButton.transform.SetParent(extraTopUi.transform); + } + if (modDisplay) + { + modDisplay.transform.SetParent(extraTopUi.transform); + } + MiraHudHelper.UiGrid.ArrangeChilds(); + MiraHudHelper.ExtraUiGrid.ArrangeChilds(); + } + } + + public static void ResetButtonPositions() + { + var topUi = MiraHudHelper.UiTopRight; + var extraTopUi = MiraHudHelper.ExtraUiTopRight; + var subButton = MiraHudHelper.SubmergedFloorButton; + var modDisplay = MiraHudHelper.ModifierDisplayOnRight ? MiraHudHelper.ModifierDisplayObject : null!; + if (topUi && extraTopUi) + { + var wikiButton = MiraHudHelper.VanillaMatchInfoButton; + if (wikiButton) + { + wikiButton.transform.SetParent(null); + } + if (subButton) + { + subButton.transform.SetParent(null); + } + if (modDisplay) + { + modDisplay.transform.SetParent(null); + } + } + } + + public static IEnumerator CoResizeSettingsUI() + { + while (!HudManager.Instance || !MiraHudHelper.UiGrid || !MiraHudHelper.ExtraUiGrid) + { + yield return null; + } + + yield return new WaitForSeconds(0.01f); + ResizeUI(LocalSettingsTabSingleton.Instance.TopRightButtonsFactorSlider.Value); + } + + public static void ResizeUI(float scaleFactor) + { + var alteredScale = scaleFactor * 0.85f; + var actualScaleVal = scaleFactor * 1.176470588235294f; + var actualScale = new Vector3(actualScaleVal, actualScaleVal, 1); + var baseAspect = MiraHudHelper.UiAspectPos; + var baseGrid = MiraHudHelper.UiGrid; + var baseUi = MiraHudHelper.UiTopRight; + if (baseUi && baseAspect && baseGrid) + { + baseAspect.DistanceFromEdge = new Vector2(0.435f * scaleFactor, 0.475f * scaleFactor); + + foreach (var button in baseUi.GetComponentsInChildren(true)) + { + if (button.gameObject == null) + { + continue; + } + if (button.transform.name.Contains("Friends List Button")) + { + button.gameObject.transform.localScale = new Vector3(0.2675f * actualScaleVal, 0.2675f * actualScaleVal, 1); + continue; + } + + button.gameObject.transform.localScale = actualScale; + } + + baseGrid.CellSize = new Vector2(alteredScale, alteredScale); + if (baseGrid.gameObject.transform.childCount != 0) + { + baseGrid.ArrangeChilds(); + } + baseAspect.AdjustPosition(); + } + + var extraAspect = MiraHudHelper.ExtraUiAspectPos; + var extraGrid = MiraHudHelper.ExtraUiGrid; + var extraUi = MiraHudHelper.ExtraUiTopRight; + if (extraUi && extraAspect && extraGrid) + { + extraAspect.DistanceFromEdge = new Vector3(0.435f * scaleFactor, 1.25f * scaleFactor, 0f); + + foreach (var button in extraUi.GetAllChildren()) + { + if (button.gameObject == null) + { + continue; + } + if (button.transform.name.Contains("Modifiers")) + { + button.gameObject.transform.localScale = new Vector3(0.65f * scaleFactor, 0.65f * scaleFactor, 1); + continue; + } + + button.gameObject.transform.localScale = actualScale; + } + + extraGrid.CellSize = new Vector2(alteredScale, alteredScale); + if (extraGrid.gameObject.transform.childCount != 0) + { + extraGrid.ArrangeChilds(); + } + extraAspect.AdjustPosition(); + } + } + /// /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. /// @@ -69,12 +218,19 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Visuals/UI", "Show Keybinds on Buttons", true); /// - /// Gets the scale of the buttons. + /// Gets the scale of the ability buttons. /// [LocalSliderSetting(min: 0.5f, max: 1.5f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)] public ConfigEntry ButtonUIFactorSlider { get; private set; } = config.Bind("Visuals/UI", "Button Scale Factor", 0.75f); + /// + /// 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("Visuals/UI", "Top-Right UI Scale Factor", 1f); + /// /// Gets the fps specified by the player. /// @@ -82,6 +238,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) public ConfigEntry SetFpsSlider { get; private set; } = config.Bind("Visuals/UI", "Max FPS", 120f); + [LocalToggleSetting] + public ConfigEntry WikiOnBottomRow { get; private set; } = + config.Bind("Visuals/UI", "Display Wiki on Bottom Row", true); + /// /// Gets whether to apply cosmetic changes to the TaskAdder. /// diff --git a/MiraAPI/ModCompatibility.cs b/MiraAPI/ModCompatibility.cs index 073453b..ebb1081 100644 --- a/MiraAPI/ModCompatibility.cs +++ b/MiraAPI/ModCompatibility.cs @@ -1,6 +1,6 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; +using System.Reflection; using BepInEx.Unity.IL2CPP; +using SemanticVersioning; namespace MiraAPI; @@ -14,15 +14,36 @@ public static class ModCompatibility /// public const string SubmergedId = "Submerged"; - /// - /// Checks if the Submerged mod is loaded. - /// - /// Submerged mod assembly if loaded, otherwise. - /// if the Submerged mod is loaded, otherwise. - public static bool SubmergedLoaded([NotNullWhen(true)] out Assembly? submergedAssembly) + public static Version SubVersion { get; private set; } + public static bool SubLoaded { get; private set; } + public static BasePlugin SubPlugin { get; private set; } + public static Assembly SubAssembly { get; private set; } + + public const ShipStatus.MapType SubmergedMapType = (ShipStatus.MapType)6; + + public static void Initialize() { - var result = IL2CPPChainloader.Instance.Plugins.TryGetValue(SubmergedId, out var plugin); - submergedAssembly = result ? plugin?.Instance.GetType().Assembly : null; - return result; + InitSubmerged(); + } + + public static void InitSubmerged() + { + if (!IL2CPPChainloader.Instance.Plugins.TryGetValue(SubmergedId, out var plugin)) + { + return; + } + + SubPlugin = (plugin!.Instance as BasePlugin)!; + SubVersion = plugin.Metadata.Version; + + SubAssembly = SubPlugin.GetType().Assembly; + + SubLoaded = true; + Message("Submerged was detected"); + } + + public static bool IsSubmerged() + { + return SubLoaded && ShipStatus.Instance && ShipStatus.Instance.Type == SubmergedMapType; } } diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index f5ae883..051ae07 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -1,8 +1,12 @@ -namespace MiraAPI.Modifiers.Types; +using MiraAPI.GameModes; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Modifiers.Types; /// /// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role. /// +[MiraIgnore] public abstract class GameModifier : BaseModifier { /// @@ -51,5 +55,5 @@ public abstract class GameModifier : BaseModifier /// Determines whether the modifier can spawn in general, accounting for gamemodes and everything else. /// /// if the modifier is able to spawn, otherwise . - public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek(); + public virtual bool CanSpawnOnCurrentMode() => CustomGameModeManager.IsClassic() && !GameManager.Instance.IsHideAndSeek(); } diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 472b3cf..cbcb13d 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -1,4 +1,6 @@ using System; +using MiraAPI.PluginLoading; +using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Modifiers.Types; @@ -6,6 +8,7 @@ namespace MiraAPI.Modifiers.Types; /// /// The base class for all timed modifiers. Timed modifiers have a duration and can be started and stopped. /// +[MiraIgnore] public abstract class TimedModifier : BaseModifier { /// diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 0622492..e840d00 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -7,6 +7,7 @@ using Assets.CoreScripts; using BepInEx.Unity.IL2CPP.Utils; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameModes; using MiraAPI.Utilities; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; @@ -629,7 +630,10 @@ public static class CustomMurderRpc if (resetKillTimer) { source.SetKillTimer( - GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown) / 2f); + CustomGameModeManager.ActiveMode != null + ? CustomGameModeManager.ActiveMode.DefaultImpostorKillCooldown / 2f + : GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown) / + 2f); } } else @@ -666,7 +670,9 @@ public static class CustomMurderRpc if (resetKillTimer) { - source.SetKillTimer(GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown)); + source.SetKillTimer(CustomGameModeManager.ActiveMode != null + ? CustomGameModeManager.ActiveMode.DefaultImpostorKillCooldown + : GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown)); } } diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index 25312e0..836e822 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -74,4 +74,14 @@ public enum MiraRpc : uint /// Custom RPC to replace Among Us CastVote. /// CastVote, + + /// + /// Custom RPC to replace Among Us QueueOverruleVotes. + /// + QueueOverruleVotes, + + /// + /// Custom RPC to sync the Gamemode option. + /// + SyncGamemodeOption, } diff --git a/MiraAPI/Patches/AmongUsClientSyncPatch.cs b/MiraAPI/Patches/AmongUsClientSyncPatch.cs index 6af91c1..a8a3dfe 100644 --- a/MiraAPI/Patches/AmongUsClientSyncPatch.cs +++ b/MiraAPI/Patches/AmongUsClientSyncPatch.cs @@ -2,6 +2,7 @@ using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Player; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Roles; @@ -32,6 +33,7 @@ internal static class AmongUsClientSyncPatch ModdedOptionsManager.SyncAllOptions(clientData.Id); CustomRoleManager.SyncAllRoleSettings(clientData.Id); + GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value); } [HarmonyPostfix] diff --git a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs index 3bda24d..0f582e1 100644 --- a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs +++ b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs @@ -1,7 +1,9 @@ -using System.Reflection; +using System.Linq; +using System.Reflection; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.MeetingAbilities; using MiraAPI.Utilities; namespace MiraAPI.Patches.Events; @@ -18,6 +20,7 @@ public static class FreeplayRoundStartPatch { if (!__result) { + MeetingButtonManager.OnGameStart(); MiraEventManager.InvokeEvent(new RoundStartEvent(true)); } } diff --git a/MiraAPI/Patches/GameModes/BodyTypePatch.cs b/MiraAPI/Patches/GameModes/BodyTypePatch.cs new file mode 100644 index 0000000..2aede92 --- /dev/null +++ b/MiraAPI/Patches/GameModes/BodyTypePatch.cs @@ -0,0 +1,20 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.CoBegin))] +internal static class BodyTypePatch +{ + public static bool Prefix(IntroCutscene __instance, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene == true) + { + __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/ConsolePatches.cs b/MiraAPI/Patches/GameModes/ConsolePatches.cs index e2f24b2..4427b16 100644 --- a/MiraAPI/Patches/GameModes/ConsolePatches.cs +++ b/MiraAPI/Patches/GameModes/ConsolePatches.cs @@ -1,14 +1,18 @@ -/* +using HarmonyLib; +using InnerNet; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch] -public static class ConsolePatches +internal static class ConsolePatches { - [HarmonyPrefix] - [HarmonyPatch(typeof(Console), nameof(Console.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(Console), nameof(Console.CanUse))] public static bool CanUsePatch(Console __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseConsole(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseTasks(__instance)) { var task = __instance.FindTask(pc.Object); @@ -31,15 +35,15 @@ public static class ConsolePatches return true; } - [HarmonyPrefix] - [HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.CanUse))] public static bool SystemCanUsePatch(SystemConsole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance)) { - return canUse = couldUse = true; + canUse = couldUse = true; + return true; } canUse = couldUse = false; @@ -50,15 +54,15 @@ public static class ConsolePatches return true; } - [HarmonyPrefix] - [HarmonyPatch(typeof(MapConsole), nameof(MapConsole.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(MapConsole), nameof(MapConsole.CanUse))] public static bool MapCanUsePatch(MapConsole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance)) { - return canUse = couldUse = true; + canUse = couldUse = true; + return true; } canUse = couldUse = false; @@ -68,4 +72,40 @@ public static class ConsolePatches canUse = couldUse = true; return true; } -}*/ + + [HarmonyPrefix, HarmonyPatch(typeof(MapConsole), nameof(MapConsole.Use))] + public static bool MapUsePatch(MapConsole __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance); + } + + return true; + } + + [HarmonyPrefix, HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.Use))] + public static bool SystemUsePatch(SystemConsole __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance); + } + + return true; + } + + [HarmonyPrefix, HarmonyPatch(typeof(Console), nameof(Console.Use))] + public static bool ConsoleUsePatch(Console __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode.CanUseTasks(__instance); + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs b/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs new file mode 100644 index 0000000..7e8144e --- /dev/null +++ b/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs @@ -0,0 +1,18 @@ +using HarmonyLib; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.SetGameMode))] +internal static class CreateOptionsPatch +{ + public static bool Prefix(CreateOptionsPicker __instance, AmongUs.GameOptions.GameModes mode) + { + if (mode <= AmongUs.GameOptions.GameModes.SeekFools) + { + return true; + } + + // TODO: finish this + return false; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs index 07a2899..fff7e40 100644 --- a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs +++ b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs @@ -1,11 +1,14 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch(typeof(DeadBody))] -public static class DeadBodyPatch +internal static class DeadBodyPatch { - [HarmonyPrefix] - [HarmonyPatch(nameof(DeadBody.OnClick))] + [HarmonyPrefix, HarmonyPatch(nameof(DeadBody.OnClick))] public static bool OnClickPatch(DeadBody __instance) { - return CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanReport(__instance); + return CustomGameModeManager.ActiveMode?.CanReport(__instance) == true; } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/GameLogicPatches.cs b/MiraAPI/Patches/GameModes/GameLogicPatches.cs index a62be83..742a56c 100644 --- a/MiraAPI/Patches/GameModes/GameLogicPatches.cs +++ b/MiraAPI/Patches/GameModes/GameLogicPatches.cs @@ -1,20 +1,78 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch] -public static class GameLogicPatches +internal static class GameLogicPatches { - [HarmonyPrefix, HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] - public static bool EndCritPatch(LogicGameFlowNormal __instance) + [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidHumansWin))] + public static bool DidHumansWin(GameOverReason reason, ref bool __result) { - var runOriginal = true; - CustomGameModeManager.ActiveMode?.CheckGameEnd(out runOriginal, __instance); - return runOriginal; + __result = reason == GameOverReason.CrewmatesByTask || reason == GameOverReason.CrewmatesByVote || + reason == GameOverReason.ImpostorDisconnect || reason == GameOverReason.HideAndSeek_CrewmatesByTimer; + return false; } - [HarmonyPrefix, HarmonyPatch(typeof(LogicRoleSelectionNormal), nameof(LogicRoleSelectionNormal.AssignRolesFromList))] - public static bool AssignRolesPatch(LogicRoleSelectionNormal __instance) + [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidImpostorsWin))] + public static bool DidImpostorsWin(GameOverReason reason, ref bool __result) { - var runOriginal = true; - CustomGameModeManager.ActiveMode?.AssignRoles(out runOriginal, __instance); - return runOriginal; + __result = reason == GameOverReason.ImpostorsByKill || reason == GameOverReason.ImpostorsBySabotage || + reason == GameOverReason.ImpostorsByVote || reason == GameOverReason.HideAndSeek_ImpostorsByKills || + reason == GameOverReason.CrewmateDisconnect; + return false; } -}*/ + + [HarmonyPrefix, HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetBodyType))] + public static bool BodyTypePatch(NormalGameManager __instance, PlayerControl player, ref PlayerBodyTypes __result) + { + if (CustomGameModeManager.ActiveMode == null || !CustomGameModeManager.ActiveMode.GameModeBodyTypeOverride) + { + return true; + } + __result = CustomGameModeManager.ActiveMode.GetBodyType(player); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] + public static bool EndGamePatch(LogicGameFlowNormal __instance) + { + if (TutorialManager.InstanceExists) + { + return true; + } + + if (!AmongUsClient.Instance.AmHost) + { + return false; + } + + if (!GameData.Instance) + { + return false; + } + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.CheckGameEnd(out var runOriginal, __instance); + + if (!runOriginal) + { + return false; + } + } + + return true; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.IsGameOverDueToDeath))] + public static void IsGameOverDueToDeathPatch(LogicGameFlowNormal __instance, ref bool __result) + { + if (CustomGameModeManager.IsActiveGameMode()) + { + __result = false; + } + } +} diff --git a/MiraAPI/Patches/GameModes/GameManagerPatches.cs b/MiraAPI/Patches/GameModes/GameManagerPatches.cs new file mode 100644 index 0000000..3f56e82 --- /dev/null +++ b/MiraAPI/Patches/GameModes/GameManagerPatches.cs @@ -0,0 +1,56 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(GameManager))] +internal static class GameManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(GameManager.OnPlayerDeath))] + public static bool OnDeathPrefix([HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] bool assignGhostRole) + { + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.OnPlayerDeath(player, assignGhostRole); + return false; + } + + return true; + } + [HarmonyPrefix, HarmonyPatch(nameof(GameManager.ShowCrewmatesKilled))] + public static bool ShowCrewmatesKilledPrefix(ref bool __result) + { + if (CustomGameModeManager.ActiveMode != null) + { + __result = CustomGameModeManager.ActiveMode is HideAndSeekMode; + return false; + } + + return true; + } + [HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetMapOptions))] + [HarmonyPrefix] + public static bool GetMapOptions(ref MapOptions __result) + { + if (CustomGameModeManager.ActiveMode == null) + { + return true; + } + + __result = CustomGameModeManager.ActiveMode.GetMapOptions(); + return false; + } +} + +[HarmonyPatch(typeof(GameStartManager))] +internal static class GameStartManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(GameStartManager.Start))] + public static void StartPatch(GameStartManager __instance) + { + if (MiraApiPlugin.IsDevBuild) + { + __instance.MinPlayers = 1; + } + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs new file mode 100644 index 0000000..735d198 --- /dev/null +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -0,0 +1,62 @@ +using System.Collections; +using HarmonyLib; +using Il2CppSystem; +using MiraAPI.GameModes; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(HudManager))] +internal static class HudPatches +{ + [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] + public static void HudUpdatePatch(HudManager __instance) + { + if (GameManager.Instance != null && GameManager.Instance.GameHasStarted && + CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.HudUpdate(__instance); + } + } + + + [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] + [HarmonyPostfix] + public static void PostHudStart(HudManager __instance) + { + Coroutines.Start(CoPostHudStart(__instance)); + } + + private static IEnumerator CoPostHudStart(HudManager instance) + { + yield return new WaitUntil((Func)(() => GameManager.Instance != null)); + if (GameManager.Instance.IsHideAndSeek()) + yield return null; + var infoPane = instance.gameObject.transform.FindChild("LobbyInfoPane"); + var aspect = infoPane.gameObject.transform.Find("AspectSize"); + var modeLabel = aspect!.Find("ModeLabel"); + var modeValue = aspect.Find("ModeValue"); + var modelText = modeLabel!.Find("Text_TMP").gameObject; + var modelTextClone = Object.Instantiate(modelText, modeLabel); + Object.Destroy(modelTextClone.GetComponent()); + modelTextClone.GetComponent().text = "Gamemode"; + var gmText = modeValue!.Find("GameModeText").gameObject; + var gmTextClone = Object.Instantiate(gmText, modeValue); + gmText.SetActive(false); + modelText.SetActive(false); + _text = gmTextClone.GetComponent(); + _text.text = CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic"; + CustomGameModeManager.ActiveMode?.HudStart(instance); + } + + private static TextMeshPro? _text; + internal static void SetGameModeText(string text) + { + if (_text != null) + _text.text = text; + } +} diff --git a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs index ad0d9e0..de9bbf1 100644 --- a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs +++ b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs @@ -1,9 +1,13 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + /// -/// Allow Impostors to kill each other if can kill is enabled in gamemode or friendly fire is toggled on +/// Allow Impostors to kill each other if can kill is enabled in gamemode or friendly fire is toggled on. /// [HarmonyPatch(typeof(ImpostorRole), "IsValidTarget")] -public static class ImpostorTargetPatch +internal static class ImpostorTargetPatch { public static bool Prefix(ImpostorRole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo target, ref bool __result) { @@ -16,9 +20,8 @@ public static class ImpostorTargetPatch } __result = target is { Disconnected: false, IsDead: false } && - target.PlayerId != __instance.Player.PlayerId && !(target.Role == null) && - !(target.Object == null) && !target.Object.inVent && !target.Object.inMovingPlat; + target.PlayerId != __instance.Player.PlayerId && target.Role != null && + target.Object != null && !target.Object.inVent && !target.Object.inMovingPlat; return false; - } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs b/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs new file mode 100644 index 0000000..5701014 --- /dev/null +++ b/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs @@ -0,0 +1,20 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.CoBegin))] +internal static class IntroCutscenePatch +{ + public static bool Prefix(IntroCutscene __instance, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene == true) + { + __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs index 9a4bd8b..06ea282 100644 --- a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs +++ b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs @@ -1,9 +1,12 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch(typeof(MapBehaviour))] -public static class MapBehaviourPatch +internal static class MapBehaviourPatch { - [HarmonyPrefix] - [HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))] + [HarmonyPrefix, HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))] public static bool ShowSabotagePatch(MapBehaviour __instance) { var shouldShow = CustomGameModeManager.ActiveMode?.ShouldShowSabotageMap(__instance); @@ -15,4 +18,4 @@ public static class MapBehaviourPatch return true; } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs new file mode 100644 index 0000000..e7c4f8b --- /dev/null +++ b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs @@ -0,0 +1,19 @@ +/* +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(NormalGameManager))] +internal static class NormalGameManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(NormalGameManager.IsNormal))] + public static void IsNormal(ref bool __result) + { + if (CustomGameModeManager.ActiveMode != null) + { + __result = !CustomGameModeManager.ActiveMode.IsHideAndSeek; + } + } +} +*/ diff --git a/MiraAPI/Patches/GameModes/OnDeathPatch.cs b/MiraAPI/Patches/GameModes/OnDeathPatch.cs deleted file mode 100644 index 99f9211..0000000 --- a/MiraAPI/Patches/GameModes/OnDeathPatch.cs +++ /dev/null @@ -1,11 +0,0 @@ -/* -[HarmonyPatch(typeof(KillAnimation))] -public static class OnDeathPatch -{ - [HarmonyPostfix] - [HarmonyPatch(nameof(KillAnimation.CoPerformKill))] - public static void OnDeathPostfix([HarmonyArgument(0)] PlayerControl source, [HarmonyArgument(1)] PlayerControl target) - { - CustomGameModeManager.ActiveMode?.OnDeath(target); - } -}*/ diff --git a/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs new file mode 100644 index 0000000..53620e0 --- /dev/null +++ b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; +[HarmonyPatch(typeof(ProgressTracker), nameof(ProgressTracker.Start))] +internal static class ProgressTrackerPatch +{ + public static bool Prefix(ProgressTracker __instance) + { + if (CustomGameModeManager.ActiveMode != null && !CustomGameModeManager.ActiveMode.ShowTaskBar) + { + __instance.gameObject.SetActive(false); + return false; + } + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/TaskPanelPatch.cs b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs new file mode 100644 index 0000000..30a30cc --- /dev/null +++ b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; +[HarmonyPatch(typeof(TaskPanelBehaviour), nameof(TaskPanelBehaviour.Update))] +internal static class TaskPanelPatch +{ + public static bool Prefix(TaskPanelBehaviour __instance) + { + if (__instance != Roles.HudManagerPatches.RoleTab && CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.UpdateTaskPanel(__instance); + return false; + } + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/VentPatches.cs b/MiraAPI/Patches/GameModes/VentPatches.cs deleted file mode 100644 index 597c40a..0000000 --- a/MiraAPI/Patches/GameModes/VentPatches.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* -[HarmonyPatch(typeof(Vent))] -public static class VentPatches -{ - [HarmonyPrefix] - [HarmonyPatch(nameof(Vent.CanUse))] - public static bool CanUseVentPatch(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse) - { - if (CustomGameModeManager.ActiveMode?.CanVent(__instance, pc) == false) - { - return couldUse = canUse = false; - } - - if (pc.Role is not ICustomRole role) - { - return couldUse = canUse = true; - } - - var num = float.MaxValue; - var @object = pc.Object; - var customRoleUsable = role.CanUseVent; - - canUse = couldUse = customRoleUsable && !pc.IsDead && (@object.CanMove || @object.inVent); - if (canUse) - { - var center = @object.Collider.bounds.center; - var position = __instance.transform.position; - num = Vector2.Distance(center, position); - canUse &= num <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(@object.Collider, center, position, Constants.ShipOnlyMask, false); - } - __result = num; - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(Vent.SetOutline))] - public static bool SetOutlinePatch(Vent __instance, [HarmonyArgument(0)] bool on, [HarmonyArgument(1)] bool mainTarget) - { - var color = PlayerControl.LocalPlayer.Data.Role is ICustomRole role - ? role.RoleColor : PlayerControl.LocalPlayer.Data.Role.IsImpostor ? Palette.ImpostorRed : Palette.CrewmateBlue; - __instance.myRend.material.SetFloat(ShaderID.Outline, on ? 1 : 0); - __instance.myRend.material.SetColor(ShaderID.OutlineColor, color); - __instance.myRend.material.SetColor(ShaderID.AddColor, mainTarget ? color : Color.clear); - - return false; - } -}*/ diff --git a/MiraAPI/Patches/GameModes/WinnersPatch.cs b/MiraAPI/Patches/GameModes/WinnersPatch.cs new file mode 100644 index 0000000..c7c9385 --- /dev/null +++ b/MiraAPI/Patches/GameModes/WinnersPatch.cs @@ -0,0 +1,27 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))] +internal static class WinnersPatch +{ + [HarmonyPostfix] + public static void ShowWinningScreen() + { + if (CustomGameModeManager.ActiveMode is null or ClassicMode) + return; + + var winners = CustomGameModeManager.ActiveMode.CalculateWinners(); + + if (winners is null) + return; + + EndGameResult.CachedWinners.Clear(); + + foreach (var data in winners) + { + EndGameResult.CachedWinners.Add(new(data)); + } + } +} diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 76adc2c..345c9a6 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,9 +1,11 @@ using System.Collections; using System.Collections.Generic; +using System.Globalization; using HarmonyLib; using MiraAPI.Hud; using MiraAPI.Keybinds; using MiraAPI.LocalSettings; +using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -123,6 +125,47 @@ public static class HudManagerPatches CustomGameModeManager.ActiveMode?.HudStart(__instance); }*/ + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(FriendsListManager), nameof(FriendsListManager.SetFriendButtonColor))] + public static bool SetFriendButtonColor(FriendsListManager __instance, bool isGrayedOut) + { + __instance.FriendsListButton?.SetGlyphColor(isGrayedOut); + return false; + } + + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Toggle))] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Close))] + public static void TogglePrefix(ChatController __instance) + { + if (!MiraHudHelper.ClonedChatButton) + { + return; + } + __instance.chatButton.transform.localPosition = MiraHudHelper.ClonedChatButton.transform.localPosition + new Vector3(-0.3f, 0); + } + + [HarmonyPostfix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Toggle))] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Close))] + public static void TogglePostfix(ChatController __instance) + { + MiraHudHelper.UiGrid.ArrangeChilds(); + } + + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChatNote))] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] + [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChatWarning))] + public static void ChatBubbleUpdatePrefix(ChatController __instance) + { + __instance.chatNotifyDot.transform.localPosition = new Vector3(-0.34f, 0.373f, -1f); + } + /// /// Create custom buttons and arrange them on the hud. /// @@ -131,6 +174,7 @@ public static class HudManagerPatches [HarmonyPatch(nameof(HudManager.Start))] public static void StartPostfix(HudManager __instance) { + __instance.gameObject.AddComponent(); if (Buttons == null) { Buttons = __instance.transform.Find("Buttons"); diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index c44675e..3a7f235 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -1,8 +1,11 @@ -using System.Reflection; +using System.Linq; +using System.Reflection; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.MeetingAbilities; +using MiraAPI.GameModes; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; @@ -30,6 +33,11 @@ public static class IntroCutscenePatches { var @event = new IntroBeginEvent(__instance); MiraEventManager.InvokeEvent(@event); + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.Initialize(); + } } [HarmonyPatch] @@ -121,9 +129,9 @@ public static class IntroCutscenePatches { introCutscene = __instance.Cast(); } - Info("IntroCutscene ended"); + MeetingButtonManager.OnGameStart(); MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene)); var @event = new BeforeRoundStartEvent(true); diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs index 6e96d5f..0092103 100644 --- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs +++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs @@ -7,6 +7,8 @@ using Reactor.Utilities.Extensions; using System; using System.Collections.Generic; using System.Linq; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using Object = UnityEngine.Object; diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 0aea27b..12f293c 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -3,6 +3,9 @@ using System.Collections; using System.Linq; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppSystem.Collections.Generic; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.Presets; @@ -14,6 +17,7 @@ using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using UnityEngine.ProBuilder; using UnityEngine.UI; using Object = UnityEngine.Object; @@ -22,6 +26,15 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(GameOptionsMenu))] internal static class GameOptionsMenuPatch { + private static List _vanillaHeaders = new(); + private static List _vanillaOptions = new(); + private static List _mainOptions = new(); + private static System.Collections.Generic.Dictionary> _gameModeGroups = new(); + private static System.Collections.Generic.Dictionary> _gameModeOptions = new(); + private static System.Collections.Generic.Dictionary> _gameModeHeaders = new(); + private static TextMeshPro _gamemodeDescription = null!; + private static SpriteRenderer _modIcon = null!; + [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] // ReSharper disable once InconsistentNaming @@ -42,15 +55,10 @@ internal static class GameOptionsMenuPatch var mapNameSetting = GameManager.Instance.GameSettingsList.MapNameSetting; __instance.MapPicker.SetUpFromData(mapNameSetting, 20); __instance.Children.Add(__instance.MapPicker); - __instance.CreateSettings(); - foreach (var optionBehaviour in __instance.Children) + CreateSettings(__instance, container); + if (CustomGameModeManager.ActiveMode != null) { - if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost) - { - optionBehaviour.SetAsPlayer(); - } - - optionBehaviour.OnValueChanged = new Action(__instance.ValueChanged); + ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance); } } else @@ -70,6 +78,314 @@ internal static class GameOptionsMenuPatch return false; } + public static float AdditionalVanillaScrollNum; + internal static void ToggleGamemodeOptions(AbstractGameMode gameMode, GameOptionsMenu instance) + { + float num = -1.217f; + instance.Children.Clear(); + foreach (var opt in _mainOptions) + { + instance.Children.Add(opt); + } + + var icon = gameMode.Icon; + if (icon != null) + { + _modIcon.gameObject.SetActive(true); + _gamemodeDescription.transform.localPosition = new Vector3(0.3f, 0, -0.01f); + _gamemodeDescription.horizontalAlignment = HorizontalAlignmentOptions.Left; + var texture = icon.LoadAsset().texture; + var scale = Mathf.Min(128f / texture.width, 128f / texture.height); + + var newSprite = Sprite.Create( + texture, + new Rect(0, 0, texture.width, texture.height), + Vector2.one / 2, + 100f / scale + ); + + _modIcon.sprite = newSprite; + _modIcon.drawMode = SpriteDrawMode.Simple; + } + else + { + _gamemodeDescription.transform.localPosition = new Vector3(0f, 0, -0.01f); + _gamemodeDescription.horizontalAlignment = HorizontalAlignmentOptions.Center; + _modIcon.gameObject.SetActive(false); + } + + _gamemodeDescription.text = gameMode.Description; + if (gameMode.ShowNormalGameSettings) + { + foreach (var opt in _vanillaHeaders) + { + num -= 0.63f; + opt.gameObject.SetActive(true); + } + foreach (var opt in _vanillaOptions) + { + opt.gameObject.SetActive(true); + instance.Children.Add(opt); + num -= 0.45f; + } + } + else + { + foreach (var opt in _vanillaHeaders) + { + opt.gameObject.SetActive(false); + } + foreach (var opt in _vanillaOptions) + { + opt.gameObject.SetActive(false); + } + } + + foreach (var pair in _gameModeGroups) + { + if (pair.Value.Count != 0) + { + if (gameMode == pair.Key) + { + foreach (var opt in _gameModeHeaders[gameMode]) + { + num -= 0.63f; + opt.gameObject.SetActive(true); + } + + foreach (var opt in _gameModeOptions[gameMode]) + { + opt.gameObject.SetActive(true); + instance.Children.Add(opt); + num -= 0.45f; + } + } + else + { + foreach (var opt in _gameModeHeaders[pair.Key]) + { + opt.gameObject.SetActive(false); + } + + foreach (var opt in _gameModeOptions[pair.Key]) + { + opt.gameObject.SetActive(false); + } + } + } + } + + instance.ControllerSelectable.Clear(); + foreach (var obj in instance.scrollBar.GetComponentsInChildren()) + { + if (obj.gameObject.activeSelf) + { + instance.ControllerSelectable.Add(obj); + } + } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } + + + private static void CreateSettings(GameOptionsMenu instance, Transform container) + { + float num = 0.713f; + _vanillaHeaders.Clear(); + _vanillaOptions.Clear(); + _mainOptions.Clear(); + _gameModeHeaders.Clear(); + _gameModeOptions.Clear(); + _gameModeGroups.Clear(); + + CategoryHeaderMasked gmCategory = Object.Instantiate(instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, container); + gmCategory.SetHeader(GameModeOption.CustomName, 20); + gmCategory.transform.localScale = Vector3.one * 0.63f; + gmCategory.transform.localPosition = new Vector3(-0.903f, num, -2f); + GameModeOption.OptionBehaviour = Object.Instantiate( + instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + num -= 0.63f; + GameModeOption.OptionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + GameModeOption.OptionBehaviour.SetClickMask(instance.ButtonClickMask); + StringGameSetting setting = ScriptableObject.CreateInstance(); + setting.Type = OptionTypes.MultipleChoice; + setting.Title = GameModeOption.GamemodeName; + setting.Index = GameModeOption._lastValue; + setting.Values = new Il2CppStructArray([GameModeOption.Values[0]]); + GameModeOption.OptionBehaviour.SetUpFromData(setting, 20); + GameModeOption.Set(GameModeOption._lastValue); + GameModeOption.OptionBehaviour.TitleText.fontSize = 3; + instance.Children.Add(GameModeOption.OptionBehaviour); + foreach (var optionBehaviour in instance.Children) + { + _mainOptions.Add(optionBehaviour); + } + for (var i = 1; i < GameModeOption.Values.Count; i++) + { + GameModeOption.OptionBehaviour.Values = + (Il2CppStructArray)GameModeOption.OptionBehaviour.Values.Add( + GameModeOption.Values.ElementAt(i).Value); + } + + var modeInfoHolder = new GameObject("GamemodeInfo") + { + transform = + { + parent = container, + localPosition = new Vector3(1.2f, -0.675f, 1), + localScale = Vector3.one, + }, + layer = container.gameObject.layer, + }.transform; + + var gamemodeBg = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(0).gameObject, Vector3.zero, Quaternion.identity, modeInfoHolder); + gamemodeBg.transform.localPosition = Vector3.zero; + gamemodeBg.transform.localScale = new Vector3(1.35f, 0.85f, 1); + gamemodeBg.name = "Background"; + + var modIconObj = new GameObject("ModIcon") + { + transform = + { + parent = modeInfoHolder, + localPosition = new Vector3(-1.75f, 0, -0.1f), + localScale = new Vector3(0.45f, 0.45f, 1), + }, + layer = gamemodeBg.layer, + }; + _modIcon = modIconObj.AddComponent(); + _modIcon.sprite = MiraAssets.BlankSprite.LoadAsset(); + _modIcon.material = gamemodeBg.gameObject.GetComponent().material; + + var gamemodeTextObj = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(1).gameObject, Vector3.zero, Quaternion.identity, modeInfoHolder); + gamemodeTextObj.transform.localPosition = new Vector3(0.3f, 0, -0.01f); + gamemodeTextObj.transform.localScale = new Vector3(2.12f, 2.12f, 1); + _gamemodeDescription = gamemodeTextObj.GetComponentInChildren(); + _gamemodeDescription.fontSizeMin = 0.65f; + _gamemodeDescription.fontSizeMax = 0.9f; + _gamemodeDescription.alignment = TextAlignmentOptions.Left; + + num -= 1.3f; + AdditionalVanillaScrollNum = 0f; + foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) + { + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate( + instance.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + container); + categoryHeaderMasked.SetHeader(rulesCategory.CategoryName, 20); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); + _vanillaHeaders.Add(categoryHeaderMasked); + num -= 0.63f; + AdditionalVanillaScrollNum -= 0.63f; + foreach (BaseGameSetting baseGameSetting in rulesCategory.AllGameSettings) + { + switch (baseGameSetting.Type) + { + case OptionTypes.Checkbox: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.checkboxOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.String: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.Float: + case OptionTypes.Int: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.numberOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.Player: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.playerOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + } + + num -= 0.45f; + AdditionalVanillaScrollNum -= 0.45f; + } + } + foreach (var optionBehaviour in _vanillaOptions) + { + if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost) + { + optionBehaviour.SetAsPlayer(); + } + instance.Children.Add(optionBehaviour); + } + + foreach (var optionBehaviour in instance.Children) + { + optionBehaviour.OnValueChanged = new Action(instance.ValueChanged); + } + + foreach (var mode in CustomGameModeManager.IdToModeMap.Values) + { + var filteredGroups = + ModdedOptionsManager.GameModeOptionGroups + .Where(x => x.Key == mode.GetType()).Select(y => y.Value).SelectMany(y => y) ?? []; + _gameModeGroups.Add(mode, filteredGroups.ToList()); + + var optionBehaviours = new System.Collections.Generic.List(); + var categoryHeaders = new System.Collections.Generic.List(); + var newNum = mode.ShowNormalGameSettings ? num : num - AdditionalVanillaScrollNum; + foreach (var group in filteredGroups) + { + CreateGroup(instance, group, container, ref newNum, ref optionBehaviours, ref categoryHeaders); + group.Ready = true; + } + _gameModeHeaders.Add(mode, categoryHeaders); + _gameModeOptions.Add(mode, optionBehaviours); + } + + instance.ControllerSelectable.Clear(); + foreach (var obj in instance.scrollBar.GetComponentsInChildren()) + { + instance.ControllerSelectable.Add(obj); + } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } + [HarmonyPostfix] [HarmonyPatch(nameof(GameOptionsMenu.OnEnable))] public static void OpenPatch(GameOptionsMenu __instance) @@ -89,11 +405,10 @@ internal static class GameOptionsMenuPatch // ReSharper disable once InconsistentNaming public static void UpdatePatch(GameOptionsMenu __instance) { - if (MenuState.Instance.CurrentModIdx == 0) + if (MenuState.Instance.CurrentModIdx == 0 && _gameModeGroups.Count <= 0) { return; } - var num = 2.1f; switch (MenuState.Instance.CurrentMenu) @@ -108,9 +423,15 @@ internal static class GameOptionsMenuPatch CustomMenuTwoUpdate(ref num); break; default: + if (MenuState.Instance.CurrentModIdx == 0) + { + num = -1.217f; + GamemodeOptionsUpdate(ref num); + break; + } var filteredGroups = MenuState.Instance.CurrentMod.InternalOptionGroups - .Where(x => x.OptionableType == null && + .Where(x => (x.OptionableType == null || CustomGameModeManager.ActiveMode!.GetType().IsAssignableTo(x.OptionableType)) && x.ParentMenu == MenuCategory.Game) ?? []; foreach (var group in filteredGroups) @@ -123,8 +444,100 @@ 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) + { + var categoryHeaderMasked = Object.Instantiate( + menu.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + container); + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num + 1, -2f); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaders.Add(categoryHeaderMasked); - private static void UpdateGroup(AbstractOptionGroup? group, ref float num) + num -= 0.58f; + + categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); + categoryHeaderMasked.Background.color = group.GroupColor; + categoryHeaderMasked.Divider.color = group.GroupColor; + categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + + categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); + categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear; + categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp; + + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0); + categoryHeaderMasked.Background.size = new Vector2( + categoryHeaderMasked.Background.size.x + 1.5f, + categoryHeaderMasked.Background.size.y); + + categoryHeaderMasked.gameObject.SetActive(true); + group.Header = categoryHeaderMasked; + categoryHeaderMasked.transform.localPosition += Vector3.down; + + var options = group.Options.Select(opt => opt.CreateOption( + menu.checkboxOrigin, + menu.numberOptionOrigin, + menu.stringOptionOrigin, + menu.playerOptionOrigin, + container)); + + foreach (var newOpt in options) + { + newOpt.SetClickMask(menu.ButtonClickMask); + SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); + foreach (var renderer in componentsInChildren) + { + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) + { + renderer.color = group.GroupColor.FindAlternateColor(); + if (renderer.transform.parent.TryGetComponent(out var btn)) + { + btn.interactableColor = group.GroupColor.FindAlternateColor(); + btn.interactableHoveredColor = Color.white; + } + } + + renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); + } + + foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) + { + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) + { + textMeshPro.color = group.GroupColor; + } + + textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); + } + + if (newOpt is ToggleOption toggle) + { + toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); + toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") + .GetComponent(); + rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); + rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + } + + optionBehaviours.Add(newOpt); + newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); + num -= 0.45f; + + newOpt.Initialize(); + newOpt.gameObject.SetActive(true); + } + } + + internal static void UpdateGroup(AbstractOptionGroup? group, ref float num) { if (group is null || !group.Ready || group.Options.Count == 0 || group.Header == null) { @@ -133,18 +546,24 @@ internal static class GameOptionsMenuPatch if (!group.GroupVisible.Invoke()) { - group.Header.gameObject.SetActive(false); + if (group.Header != null && group.Header.gameObject != null) + group.Header.gameObject.SetActive(false); foreach (var option in group.Options) { - option.OptionBehaviour?.gameObject.SetActive(false); + if (option.OptionBehaviour != null && option.OptionBehaviour.gameObject != null) + option.OptionBehaviour?.gameObject.SetActive(false); } return; } - group.Header.gameObject.SetActive(true); - group.Header.transform.localScale = Vector3.one * 0.63f; - group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + if (group.Header != null) + { + if (group.Header.gameObject != null) + group.Header.gameObject.SetActive(true); + group.Header.transform.localScale = Vector3.one * 0.63f; + group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + } num -= 0.58f; @@ -176,6 +595,26 @@ internal static class GameOptionsMenuPatch } } + private static void GamemodeOptionsUpdate(ref float num) + { + var mode = CustomGameModeManager.ActiveMode; + if (mode == null) return; + + if (mode.ShowNormalGameSettings) + { + num += AdditionalVanillaScrollNum; + } + + if (!_gameModeGroups.TryGetValue(mode, out var groups) || groups.Count == 0) + { + return; + } + + foreach (var group in groups) + { + UpdateGroup(group, ref num); + } + } private static void CustomMenuOneUpdate(ref float num) { var groups = MenuState.Instance.CurrentMod.InternalOptionGroups diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index c31b519..d4df499 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,17 +1,4 @@ -using System; -using System.Collections.Generic; -using HarmonyLib; -using MiraAPI.GameOptions; -using MiraAPI.Modifiers; -using MiraAPI.PluginLoading; -using MiraAPI.Presets; -using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; -using TMPro; -using UnityEngine; -using UnityEngine.Events; -using static UnityEngine.UI.Button; -using Object = UnityEngine.Object; +using HarmonyLib; namespace MiraAPI.Patches.Options; diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index dcc9b75..6ee00cd 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; @@ -24,14 +25,51 @@ public static class LobbyViewPanePatches { private static int SelectedModIdx { get; set; } + private static MiraPluginInfo[] Plugins => MiraPluginManager.Instance.RegisteredPluginsWithOptions; + private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0 ? null - : MiraPluginManager.Instance.RegisteredPluginsWithOptions[SelectedModIdx - 1]; + : Plugins[SelectedModIdx - 1]; private static PassiveButton? ModifiersTabButton { get; set; } private static StringNames ModifiersTabName { get; } = CustomStringName.CreateAndRegister("ModifiersTab"); + private static AbstractGameMode? GetCustomGamemode() + { + if (!CustomGameModeManager.IsClassic() && !CustomGameModeManager.IsHideNSeek()) + { + return CustomGameModeManager.ActiveMode; + } + + return null; + } + + private static IEnumerable? GetCustomGamemodeOptions() + { + if (CustomGameModeManager.ActiveMode == null) + { + return null; + } + + if (!CustomGameModeManager.IsClassic() && !CustomGameModeManager.IsHideNSeek()) + { + var plugin = CustomGameModeManager.FindParentMod(CustomGameModeManager.ActiveMode); + + return plugin?.InternalOptionGroups + .Where(x => x.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true && x.GroupVisible()); + } + + return null; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.OnEnable))] + public static void OnEnable(LobbyViewSettingsPane __instance) + { + Refresh(__instance); + } + [HarmonyPostfix] [HarmonyPatch(nameof(LobbyViewSettingsPane.Awake))] public static void AwakePatch(LobbyViewSettingsPane __instance) @@ -76,7 +114,7 @@ public static class LobbyViewPanePatches (UnityAction)(() => { SelectedModIdx += 1; - if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length) + if (SelectedModIdx > Plugins.Length) { SelectedModIdx = 0; } @@ -100,7 +138,7 @@ public static class LobbyViewPanePatches SelectedModIdx -= 1; if (SelectedModIdx < 0) { - SelectedModIdx = MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length; + SelectedModIdx = Plugins.Length; } Refresh(__instance); @@ -109,8 +147,25 @@ public static class LobbyViewPanePatches private static void Refresh(LobbyViewSettingsPane menu) { - ModifiersTabButton?.gameObject.SetActive(SelectedModIdx != 0); - menu.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Main"; + if (SelectedMod == null) + { + var cgm = GetCustomGamemode(); + ModifiersTabButton?.gameObject.SetActive(false); + menu.rolesTabButton.gameObject.SetActive(cgm is null or { ShowNormalRoleSettings: true }); + menu.gameModeText.text = cgm is null ? "Main" : cgm.ColoredName; + } + else + { + var showModifierButton = SelectedMod.InternalOptionGroups.Any(m => + m.ParentMenu is MenuCategory.Modifiers || m.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true); + var showRolesButton = SelectedMod.InternalOptionGroups.Any(r => + r.ParentMenu is MenuCategory.Roles || r.OptionableType?.IsAssignableTo(typeof(ICustomRole)) == true); + + ModifiersTabButton?.gameObject.SetActive(showModifierButton); + menu.rolesTabButton.gameObject.SetActive(showRolesButton); + menu.gameModeText.text = SelectedMod.PluginInfo.Metadata.Name; + } + menu.RefreshTab(); menu.scrollBar.ScrollToTop(); } @@ -142,8 +197,15 @@ public static class LobbyViewPanePatches [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))] public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance) { - if (SelectedModIdx == 0 || SelectedMod == null) + if (SelectedMod == null) { + var gamemodeOpts = GetCustomGamemodeOptions(); + if (gamemodeOpts != null) + { + DrawOptions(__instance, gamemodeOpts); + return false; + } + return true; } @@ -350,6 +412,13 @@ public static class LobbyViewPanePatches else { viewSettingsInfoPanel.SetInfo(data.Title, data.GetValueString(option.GetFloatData()), 61); + + if (viewSettingsInfoPanel.titleText.text.Contains("Game Mode")) + { + viewSettingsInfoPanel.titleText.text = TranslationController.Instance.GetString( + data.Title, CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic" + ); + } } menu.settingsInfo.Add(viewSettingsInfoPanel.gameObject); diff --git a/MiraAPI/Patches/Options/MenuState.cs b/MiraAPI/Patches/Options/MenuState.cs index 6c88089..d7b8e11 100644 --- a/MiraAPI/Patches/Options/MenuState.cs +++ b/MiraAPI/Patches/Options/MenuState.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Presets; using MiraAPI.Translation; +using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; @@ -61,6 +64,8 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] internal Dictionary FinishedRoleMenus { get; } = []; + [HideFromIl2Cpp] + internal Dictionary QueuedRoleMenuRefresh { get; } = []; public void Awake() { @@ -405,14 +410,16 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) if (CurrentModIdx == 0) { Gsm.GameSettingsButton.gameObject.SetActive(true); - Gsm.RoleSettingsButton.gameObject.SetActive(true); + Gsm.RoleSettingsButton.gameObject.SetActive( + CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); } else { var hasOptions = CurrentMod.InternalOptionGroups.Exists(g => g.OptionableType == null && g.ParentMenu == MenuCategory.Game); - var hasRoles = CurrentMod.InternalRoles.Count > 0; + var customRoles = CurrentMod.InternalRoles.Values.OfType(); + var hasRoles = customRoles.Any(x => x.VisibleInSettings.Invoke()); var hasModifiers = CurrentMod.InternalOptionGroups.Exists(g => g.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true || g.ParentMenu == MenuCategory.Modifiers); diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index 0ae7982..f6b873c 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -1,9 +1,11 @@ using HarmonyLib; using Il2CppSystem; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; using UnityEngine; using Object = Il2CppSystem.Object; @@ -200,8 +202,7 @@ 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; } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 174b6f8..d08961e 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -5,6 +5,7 @@ using System.Linq; using AmongUs.GameOptions; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; @@ -75,7 +76,24 @@ public static class RoleSettingMenuPatches dividerImage.gameObject.SetActive(false); } - if (MenuState.Instance.FinishedRoleMenus.TryGetValue(MenuState.Instance.CurrentModIdx, out var finished) && finished) + var queuedRefresh = false; + if (MenuState.Instance.QueuedRoleMenuRefresh.TryGetValue(MenuState.Instance.CurrentModIdx, out var queued) && queued) + { + queuedRefresh = true; + var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); + var headers = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); + foreach (var child in roleOptionSettings) + { + Object.Destroy(child.gameObject); + } + foreach (var child in headers) + { + Object.Destroy(child.gameObject); + } + yield return new WaitForEndOfFrame(); + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; + } + if (!queuedRefresh && MenuState.Instance.FinishedRoleMenus.TryGetValue(MenuState.Instance.CurrentModIdx, out var finished) && finished) { var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); foreach (var r in roleOptionSettings) @@ -154,6 +172,17 @@ public static class RoleSettingMenuPatches ScrollerNum = 0.522f; var num4 = 0; + var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(false); + var headers = roleMenu.scrollBar.Inner.GetComponentsInChildren(false); + foreach (var child in roleOptionSettings) + { + Object.Destroy(child.gameObject); + } + foreach (var child in headers) + { + Object.Destroy(child.gameObject); + } + yield return new WaitForEndOfFrame(); var roleGroups = MenuState.Instance.CurrentMod.InternalRoles.Values.OfType() .ToLookup(x => x.RoleOptionsGroup); @@ -177,7 +206,7 @@ public static class RoleSettingMenuPatches foreach (var grouping in sortedRoleGroups) { if (!grouping.Any() || - grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings())) + grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings() || !x.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode))) { continue; } @@ -309,6 +338,7 @@ public static class RoleSettingMenuPatches RoleGroupHidden[group] = !groupHidden; } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = false; foreach (var child in roleMenu.RoleChancesSettings.transform) @@ -328,6 +358,7 @@ public static class RoleSettingMenuPatches } } } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = true; } @@ -447,6 +478,10 @@ public static class RoleSettingMenuPatches if (role.Configuration.CanModifyChance) { role.SetChance(roleSetting.RoleChance); + if (roleSetting.RoleChance == 0 && role.Configuration.MaxRoleCount != 0) + { + role.SetCount(0); + } } } catch (Exception e) @@ -518,6 +553,26 @@ public static class RoleSettingMenuPatches CurrentRoleOptions = filteredOptions; } + [HarmonyPrefix] + [HarmonyPatch(nameof(RolesSettingsMenu.ChangeTab))] + public static void ChangeTabPatch(RolesSettingsMenu __instance) + { + var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); + categoryHeaderMasked.Title.text = TranslationController.Instance.GetString(StringNames.RoleSettingsLabel); + var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground"); + var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); + imgBg.gameObject.SetActive(true); + __instance.roleScreenshot.gameObject.SetActive(true); + __instance.roleDescriptionText.transform.parent.localPosition = new Vector3(2.5176f, -0.2731f, -1f); + __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.0675f, 0.1494f, 0.5687f); + labelBg.transform.localPosition = new Vector3(1.082f, 0.1054f, -2.5f); + __instance.roleScreenshot.drawMode = SpriteDrawMode.Simple; + foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren()) + { + optBehaviour.gameObject.Destroy(); + } + } + private static void ChangeTab(RoleBehaviour role, RolesSettingsMenu __instance) { if (role is not ICustomRole customRole) @@ -547,12 +602,8 @@ public static class RoleSettingMenuPatches __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.0675f, 0.1494f, 0.5687f); labelBg.transform.localPosition = new Vector3(1.082f, 0.1054f, -2.5f); - __instance.roleScreenshot.sprite = Sprite.Create( - role.RoleScreenshot.texture, - new Rect(0, 0, 370, 230), - Vector2.one / 2, - 100); - __instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced; + __instance.roleScreenshot.sprite = role.RoleScreenshot; + __instance.roleScreenshot.drawMode = SpriteDrawMode.Simple; } __instance.roleHeaderSprite.color = customRole.OptionsMenuColor; diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs new file mode 100644 index 0000000..ca7f2be --- /dev/null +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -0,0 +1,164 @@ +using System.Globalization; +using System.Linq; +using HarmonyLib; +using Innersloth.Assets; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Patches; + +[HarmonyPatch] +public static class RoleGuidePatches +{ + [HarmonyPostfix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreateNormalModeSettings))] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreateHnSModeSettings))] + public static void CreateNormalModeSettings(MatchInfoGuide __instance) + { + __instance.MatchInfoRoleMaskArea.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + + __instance.matchInfoPlayersMaskArea.transform.localPosition = + __instance.matchInfoSettingsMaskArea.transform.localPosition = new Vector3(1.22f, -0.335f, -0.1f); + + __instance.matchInfoPlayersMaskArea.size = + __instance.matchInfoSettingsMaskArea.size = + __instance.MatchInfoRoleMaskArea.size = new Vector2(-6, 1.8f); + + __instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren().First(x => x.name.Contains("BG_Gradient")) + .GetComponent() + .maskInteraction = + __instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).GetComponent() + .maskInteraction = + __instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).GetComponent() + .maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + } + + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] + public static void Awake(MatchInfoGuide __instance) + { + __instance.transitionOpen.targetSize = 1.3f; + } + + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Open))] + public static bool Open(MatchInfoGuide __instance) + { + if (HudManager.Instance.GameMenu.IsOpen || HudManager.Instance.Chat.IsOpenOrOpening) + { + return false; + } + + if (Minigame.Instance != null) + { + Minigame.Instance.Close(); + } + if (MapBehaviour.Instance) + { + MapBehaviour.Instance.Close(); + } + if (HudManager.InstanceExists) + { + ConsoleJoystick.SetMode_MenuAdditive(); + } + ControllerManager.Instance.OpenOverlayMenu("MatchInfoGuide", __instance.closeButton); + bool enabled = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Joystick; + __instance.glyphL.enabled = enabled; + __instance.glyphR.enabled = enabled; + if (GameManager.Instance.TryCast() != null) + { + if (__instance.NormalModeSettings.Count == 0) + { + __instance.numOfTabs = 3; + __instance.TabButtons[0].SelectButton(true); + __instance.CreateNormalModeSettings(); + } + } + else if (__instance.HnSModeSettings.Count == 0) + { + __instance.numOfTabs = 2; + __instance.TabButtons[0].SelectButton(true); + __instance.CreateHnSModeSettings(); + } + PlayerControl.LocalPlayer.NetTransform.Halt(); + __instance.MatchInfoParent.SetActive(true); + ControllerManager instance = ControllerManager.Instance; + ControllerUiElementsState currentUiState = ControllerManager.Instance.CurrentUiState; + Il2CppSystem.Collections.Generic.List controllerSelectable = __instance.ControllerSelectable; + instance.SetUpSelectables(currentUiState, controllerSelectable[controllerSelectable.Count - 1], __instance.ControllerSelectable); + ControllerManager instance2 = ControllerManager.Instance; + Il2CppSystem.Collections.Generic.List controllerSelectable2 = __instance.ControllerSelectable; + instance2.SetCurrentSelected(controllerSelectable2[controllerSelectable2.Count - 1]); + __instance.SetActiveTab(0); + return false; + } + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreatePlayerEntries))] + private static bool CreatePlayerEntries(MatchInfoGuide __instance) + { + __instance.PlayerPool.ReclaimAll(); + int num = 51; + foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) + { + PlayerIdentifierButton component = + __instance.PlayerPool.Get().GetComponent(); + component.transform.localPosition = new Vector3(0f, 0f, -1f); + component.Populate(networkedPlayerInfo); + __instance.ControllerSelectable.Add(component.Button); + component.SetTextStencil(num++); + component.PlatformIdentifier.transform.localPosition = new Vector3(0.314f, 0.088f, -2.78f); + component.NameText.transform.localPosition = new Vector3(0.3563f, 0, -2.98f); + component.NameText.text += $"\n{networkedPlayerInfo.GetPlayerColorString()}"; + var namePlate = HatManager.Instance.GetNamePlateById(networkedPlayerInfo.DefaultOutfit.NamePlateId); + var x = (NamePlateViewData viewdata) => + { + component.buttonSprite.sprite = viewdata?.Image; + component.buttonSprite.transform.localScale = new Vector3(0.7f, 1.075f, 1); + component.buttonSprite.transform.localPosition = new Vector3(-0.395f, 0, 0.1f); + }; + __instance.StartCoroutine( + AddressableAssetExtensions.CoLoadAssetAsync( + __instance, + namePlate.GetAssetReference(), + x)); + } + + return false; + } + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(MatchInfoRolePanel), nameof(MatchInfoRolePanel.SetPanel))] + public static bool SetPanel(MatchInfoRolePanel __instance, RoleBehaviour role, int numPerGame, int chancePerGame) + { + __instance.roleCount.text = string.Format(CultureInfo.InvariantCulture, "{0} at {1}%", numPerGame.ToString(CultureInfo.InvariantCulture), chancePerGame); + if (role is ICustomRole customRole) + { + __instance.roleName.text = customRole.RoleName; + __instance.roleDescription.text = $"{customRole.RoleFactionTitle}\n" + customRole.RoleMedDescription; + __instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset(); + __instance.roleCount.text += $" ({customRole.ParentMod.MiraPlugin.GetAbbreviatedModName()})"; + } + else + { + __instance.roleName.text = role.NiceName; + __instance.roleDescription.text = $"{TranslationController.Instance.GetString(role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor)}\n" + role.BlurbMed; + __instance.roleIcon.sprite = role.RoleIconColor; + __instance.roleCount.text += " (AU)"; + } + + __instance.roleIcon.SetSizeLimit(0.13f); + __instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); + __instance.roleName.fontMaterial.SetFloat(__instance.STENCIL_NAME, 50f); + __instance.roleDescription.fontMaterial.SetFloat(__instance.STENCIL_NAME, 50f); + __instance.roleCount.fontMaterial.SetFloat(__instance.STENCIL_NAME, 50f); + __instance.roleIcon.transform.localScale = new Vector3(4f, 4f, 1f); + return false; + } +} diff --git a/MiraAPI/Patches/Roles/GameStartupPatch.cs b/MiraAPI/Patches/Roles/GameStartupPatch.cs index 9f88bdd..94a7ec1 100644 --- a/MiraAPI/Patches/Roles/GameStartupPatch.cs +++ b/MiraAPI/Patches/Roles/GameStartupPatch.cs @@ -1,6 +1,8 @@ using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Utilities.Assets; namespace MiraAPI.Patches.Roles; @@ -22,6 +24,13 @@ public static class GameStartupPatch } _runOnce = true; + foreach (var mode in CustomGameModeManager.IdToModeMap.Values) + { + if (mode.Icon != null) + { + mode.TmpIcon = TmpSpriteUtils.CreateSpriteAsset(mode.Icon.LoadAsset(), $"MiraApi.Gamemode.{mode.Name.Replace(" ", string.Empty)}.png", 1.35f); + } + } if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) { diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index 8d13595..99359a6 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -3,6 +3,7 @@ using AmongUs.GameOptions; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameModes; using MiraAPI.Modifiers; using MiraAPI.Roles; using UnityEngine; @@ -65,14 +66,20 @@ public static class RoleManagerPatches [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static void ModifierSelectionPatches(RoleManager __instance) { - if (!AmongUsClient.Instance.AmHost || !ModifierManager.MiraAssignsModifiers) + if (AmongUsClient.Instance.AmHost && ModifierManager.MiraAssignsModifiers) + { + ModifierManager.AssignModifiers( + PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) + .ToList()); + } + + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (CustomGameModeManager.ActiveMode == null || roleSelection == null) { return; } - ModifierManager.AssignModifiers( - PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) - .ToList()); + CustomGameModeManager.ActiveMode.PostAssignRoles(roleSelection); } [HarmonyPrefix] diff --git a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs index d4d80d3..011fc28 100644 --- a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs +++ b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs @@ -8,17 +8,17 @@ namespace MiraAPI.Patches.Roles; /// /// Patches to return the correct role counts. /// -[HarmonyPatch(typeof(RoleOptionsCollectionV10))] +[HarmonyPatch(typeof(RoleOptionsCollectionV11))] public static class RoleOptionsCollectionPatch { /// - /// This patch fixes being inlined (2025.9.9) in the original code. + /// This patch fixes being inlined (2025.9.9) in the original code. /// [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV10.AnyRolesEnabled))] - public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV10 __instance, ref bool __result) + [HarmonyPatch(nameof(RoleOptionsCollectionV11.AnyRolesEnabled))] + public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV11 __instance, ref bool __result) { - foreach (KeyValuePair keyValuePair in __instance.roles) + foreach (KeyValuePair keyValuePair in __instance.roles) { if (__instance.GetNumPerGame(keyValuePair.Key) > 0) { @@ -33,7 +33,7 @@ public static class RoleOptionsCollectionPatch /// /// Return to skip original method, to not. [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV10.GetChancePerGame))] + [HarmonyPatch(nameof(RoleOptionsCollectionV11.GetChancePerGame))] public static bool GetChancePrefix(RoleTypes role, ref int __result) { if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null) @@ -63,7 +63,7 @@ public static class RoleOptionsCollectionPatch /// /// Return to skip original method, to not. [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV10.GetNumPerGame))] + [HarmonyPatch(nameof(RoleOptionsCollectionV11.GetNumPerGame))] public static bool GetNumPrefix(RoleTypes role, ref int __result) { if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null) diff --git a/MiraAPI/Patches/Roles/SelectRolesPatch.cs b/MiraAPI/Patches/Roles/SelectRolesPatch.cs index dc51f03..1d17765 100644 --- a/MiraAPI/Patches/Roles/SelectRolesPatch.cs +++ b/MiraAPI/Patches/Roles/SelectRolesPatch.cs @@ -3,6 +3,7 @@ using AmongUs.GameOptions; using HarmonyLib; using System.Linq; using InnerNet; +using MiraAPI.GameModes; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -15,6 +16,21 @@ public static class SelectRolesPatch [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static bool SelectRoles() { + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (!AmongUsClient.Instance.AmHost || roleSelection == null) + { + return true; + } + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.AssignRoles(out var runOriginal, roleSelection); + + if (!runOriginal) + { + return false; + } + } if (!ApiHandlesRoleSelect) { return true; diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 7a4338a..e907f34 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -1,13 +1,19 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Meeting; using MiraAPI.Events.Vanilla.Meeting.Voting; +using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Utilities; +using MiraAPI.Utilities.Components; using MiraAPI.Voting; +using UnityEngine; namespace MiraAPI.Patches.Voting; @@ -33,10 +39,46 @@ internal static class MeetingHudPatches return true; } + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.Select))] + public static bool VoteAreaSelectPatch(PlayerVoteArea __instance) + { + if (PlayerControl.LocalPlayer.Data.IsDead || __instance.AmDead || !(bool) (UnityEngine.Object) __instance.Parent) + return false; + JudgeRole? judgeRole = PlayerControl.LocalPlayer.Data.Role.TryCast(); + bool flag = judgeRole && PlayerControl.LocalPlayer.PlayerId != __instance.PlayerId; + __instance.JudgeOverruleButton?.gameObject.SetActive(flag); + if (__instance.VoteComplete || !__instance.Parent.Select((byte) __instance.PlayerId)) + return false; + __instance.Buttons.SetActive(true); + + float startPos = __instance.AnimateButtonsFromLeft ? 0.2f : 1.95f; + + Il2CppSystem.Collections.Generic.List selectableElements = new Il2CppSystem.Collections.Generic.List(); + foreach (var btn in __instance.Buttons.GetComponentsInChildren()) + { + selectableElements.Add(btn); + } + + for (int i = 0; i < selectableElements.Count; i++) + { + var button = selectableElements[i]; + float endPos = 1.3f - 0.65f * i; + float duration = 0.25f + 0.1f * i; + __instance.StartCoroutine(Effects.All(Effects.Lerp(duration, (Action) (t => + button.transform.localPosition = Vector2.Lerp(Vector2.right * startPos, Vector2.right * endPos, Effects.ExpOut(t)))))); + } + + ControllerManager.Instance.OpenOverlayMenu(__instance.name, __instance.CancelButton, selectableElements[1], selectableElements); + + return false; + } + [HarmonyPostfix] [HarmonyPatch(nameof(MeetingHud.Start))] public static void MeetingHudStartPatch(MeetingHud __instance) { + MeetingButtonManager.OnMeetingStart(__instance); foreach (var plr in PlayerControl.AllPlayerControls) { var voteData = plr.GetVoteData(); @@ -77,7 +119,17 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.Update))] public static void ForceSkipPatch(MeetingHud __instance) { - if (__instance.state is not (MeetingHud.VoteStates.NotVoted or MeetingHud.VoteStates.Voted)) + foreach (var targetedMeetingAbility in MeetingButtonManager.TargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role))) + { + targetedMeetingAbility.UpdateHandler(); + } + + foreach (var meetingAbility in MeetingButtonManager.UntargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role))) + { + meetingAbility.UpdateHandler(__instance); + } + + if (__instance.state is not (MeetingHud.MeetingStates.NotVoted or MeetingHud.MeetingStates.Voted)) { return; } @@ -105,7 +157,7 @@ internal static class MeetingHudPatches [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.Select))] - public static bool SelectPatch(MeetingHud __instance, int suspectStateIdx) + public static bool MeetingHudSelectPatch(MeetingHud __instance, int suspectStateIdx) { var voteData = PlayerControl.LocalPlayer.GetVoteData(); @@ -127,13 +179,13 @@ internal static class MeetingHudPatches return false; } - var playerVoteArea = __instance.playerStates.First(pv => pv.TargetPlayerId == pc.PlayerId); + var playerVoteArea = __instance.playerStates.First(pv => pv.PlayerId == pc.PlayerId); playerVoteArea.AmDead = true; playerVoteArea.Overlay.gameObject.SetActive(true); foreach (var player in Helpers.GetAlivePlayers()) { - var pva = __instance.playerStates.First(pv => pv.TargetPlayerId == player.PlayerId); + var pva = __instance.playerStates.First(pv => pv.PlayerId == player.PlayerId); var voteData = player.GetVoteData(); if (pva.AmDead || !voteData.VotedFor(pc.PlayerId)) @@ -150,7 +202,7 @@ internal static class MeetingHudPatches __instance.SetDirtyBit(1U); __instance.CheckForEndVoting(); - if (__instance.state == MeetingHud.VoteStates.Results) + if (__instance.state == MeetingHud.MeetingStates.Results) { __instance.SetupProceedButton(); } @@ -199,7 +251,7 @@ internal static class MeetingHudPatches }) ]); - __instance.RpcVotingComplete(voterStates, exiled, isTie); + __instance.RpcVotingComplete(voterStates, exiled, isTie, @event.OverruledVote, @event.OverruledNonce); return false; } @@ -223,6 +275,16 @@ internal static class MeetingHudPatches return false; } + // TODO: figure out a way to do host-authorization since right now any player can send RpcQueueOverruleVotes + [HarmonyPrefix] + [HarmonyPatch(nameof(MeetingHud.CmdQueueOverruleVotes))] + // Although this method is inlined in MeetingHud.Confirm, the next patch fixes that. + public static bool CmdQueueOverruleVotesPatch(MeetingHud __instance, PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) + { + VotingUtils.RpcQueueOverruleVotes(PlayerControl.LocalPlayer, judgePlayerId.Value, targetPlayerId.Value, overruleNonce); + return false; + } + [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.Confirm))] public static bool ConfirmPatch(MeetingHud __instance, [HarmonyArgument(0)] byte suspect) diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs index a29dd4c..6fb13ed 100644 --- a/MiraAPI/PluginLoading/IMiraPlugin.cs +++ b/MiraAPI/PluginLoading/IMiraPlugin.cs @@ -1,5 +1,7 @@ using System.Linq; using BepInEx.Configuration; +using MiraAPI.Utilities.Assets; +using UnityEngine; namespace MiraAPI.PluginLoading; @@ -17,7 +19,7 @@ public interface IMiraPlugin /// Gets the name to display on the options menu. /// string OptionsTitleText { get; } - + /// /// Gets the abbreviated name to display for other mods to pick up. /// diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index db030b0..874c32b 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -5,6 +5,7 @@ using BepInEx.Configuration; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Presets; @@ -44,11 +45,30 @@ public class MiraPluginInfo /// public ReadOnlyDictionary Roles { get; private set; } = null!; + /// + /// Gets a read only dictionary of Game Mode IDs and the AbstractGameMode object they are associated with. + /// + /// Read only dictionary of IDs and Game Modes. + public ReadOnlyDictionary GetModes() + { + return new ReadOnlyDictionary(GameModes); + } + /// /// Gets a of this 's . /// public IReadOnlyCollection Buttons { get; private set; } = null!; + /// + /// Gets a of this 's . + /// + public IReadOnlyCollection TargetedMeetingButtons { get; private set; } = null!; + + /// + /// Gets a of this 's . + /// + public IReadOnlyCollection MeetingButtons { get; private set; } = null!; + /// /// Gets a of this 's s. /// @@ -62,6 +82,8 @@ public class MiraPluginInfo Options = [..InternalOptions]; Roles = new ReadOnlyDictionary(InternalRoles); Buttons = [..InternalButtons]; + MeetingButtons = [..InternalMeetingButtons]; + TargetedMeetingButtons = [..InternalTargetedMeetingButtons]; } internal List InternalPresets { get; } = []; @@ -72,12 +94,15 @@ public class MiraPluginInfo internal List InternalModifiers { get; } = []; + internal Dictionary GameModes { get; } = []; internal Dictionary InternalRoles { get; } = []; - internal Dictionary InternalGameModes { get; } = []; - internal List InternalButtons { get; } = []; + internal List InternalTargetedMeetingButtons { get; } = []; + + internal List InternalMeetingButtons { get; } = []; + /// /// Gets the plugin's ID, as defined in the plugin's BepInEx metadata. /// diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index abeaf12..3b1ffa6 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -7,21 +7,25 @@ using System.Reflection; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using Il2CppInterop.Runtime.Injection; using MiraAPI.Colors; using MiraAPI.Events; using MiraAPI.GameEnd; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; using MiraAPI.Keybinds; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; +using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Patches.Options; using MiraAPI.Presets; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; +using Reactor.Localization.Utilities; using Reactor.Networking; using Reactor.Utilities; @@ -34,15 +38,117 @@ public sealed class MiraPluginManager { private readonly Dictionary _registeredPlugins = []; - internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!; - internal MiraPluginInfo[] RegisteredPluginsWithOptions { get; private set; } = null!; + internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = []; + internal MiraPluginInfo[] RegisteredPluginsWithOptions { get; private set; } = []; + internal MiraPluginInfo[] PluginsWithOptionsOrGameModes { get; private set; } = []; internal Dictionary> QueuedRoleRegistrations { get; } = []; internal static MiraPluginManager Instance { get; private set; } = new(); + public static ConfigFile MiraApiConfig { get; private set; } = null!; + public static MiraPluginInfo MiraApiPluginInfo { get; private set; } = null!; - internal void Initialize() + internal void Initialize(BasePlugin miraApiPlugin, IMiraPlugin apiPlugin) { Instance = this; + CustomGameModeManager.RegisterDefaultMode(); + CustomGameModeManager.GetAndSetGameMode(); + if (IL2CPPChainloader.Instance.Plugins.TryGetValue(MiraApiPlugin.Id, out var newPlugin)) + { + var assembly = Assembly.GetCallingAssembly(); + var info = new MiraPluginInfo(apiPlugin, newPlugin); + var roles = new List(); + + var oldConfigSetting = info.PluginConfig.SaveOnConfigSet; + info.PluginConfig.SaveOnConfigSet = false; + + foreach (var type in assembly.GetTypes()) + { + if (type.IsAbstract || type.GetCustomAttribute() != null) + { + continue; + } + + foreach (var method in AccessTools.GetDeclaredMethods(type)) + { + var eventAttribute = method.GetCustomAttribute(); + if (eventAttribute == null) + { + continue; + } + + if (!method.IsStatic) + { + Error($"Event method {method.Name} in {type.Name} must be static."); + continue; + } + + var parameters = method.GetParameters(); + if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) + { + Error($"Invalid event registration method {method.Name} in {type.Name}"); + continue; + } + + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + } + + if (RegisterModifier(type, info)) + { + continue; + } + + if (RegisterOptions(type, info)) + { + continue; + } + + if (RegisterLocalTabs(type, miraApiPlugin)) + { + continue; + } + + if (RegisterRole(type, info, out var role)) + { + roles.Add(role); + continue; + } + + if (RegisterButton(type, info)) + { + continue; + } + + if (RegisterGameOver(type)) + { + continue; + } + + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + + RegisterColorClasses(type); + RegisterKeybinds(type, miraApiPlugin); + } + + info.PluginConfig.Save(); + info.PluginConfig.SaveOnConfigSet = oldConfigSetting; + MiraApiConfig = info.PluginConfig; + + info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + QueuedRoleRegistrations.Add(info, roles); + + _registeredPlugins.Add(assembly, info); + MiraApiPluginInfo = info; + + info.SavePublicCollections(); + PresetManager.CreateDefaultPreset(info); + PresetManager.LoadPresets(info); + Info($"Registering Mira API as mod."); + } + IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => { if (plugin is not IMiraPlugin miraPlugin) @@ -119,6 +225,22 @@ public sealed class MiraPluginManager continue; } + + if (RegisterMeetingAbility(type, info)) + { + continue; + } + + if (RegisterTargetedMeetingAbility(type, info)) + { + continue; + } + + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + RegisterColorClasses(type); RegisterKeybinds(type, plugin); } @@ -141,12 +263,30 @@ public sealed class MiraPluginManager { // Save all buttons into a read-only collection for easy access CustomButtonManager.Buttons = new ReadOnlyCollection(CustomButtonManager.CustomButtons); + CustomButtonManager.EventTypes = new ReadOnlyDictionary(CustomButtonManager.ButtonEventTypes); + CustomButtonManager.CancelledEventTypes = new ReadOnlyDictionary(CustomButtonManager.ButtonCancelledEventTypes); // Cache all the registered plugins into an array for easy access RegisteredPlugins = [.. _registeredPlugins.Values]; RegisteredPluginsWithOptions = [.. RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu)]; + PluginsWithOptionsOrGameModes = [..RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu || m.GameModes.Count > 0)]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); + + var dict = new Dictionary(); + foreach (var (key, value) in CustomGameModeManager.IdToModeMap) + { + if (value is ClassicMode) + { + continue; + } + dict.Add(value.Name, key); + if (((AmongUs.GameOptions.GameModes) key) == AmongUs.GameOptions.GameModes.Normal) + return; + // 'Default' was still being registered twice and idk why + GameModesHelpers.ModeToName.Add((AmongUs.GameOptions.GameModes)key, CustomStringName.CreateAndRegister(value.Name)); + } + EnumInjector.InjectEnumValues(dict); }; RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); @@ -322,6 +462,20 @@ public sealed class MiraPluginManager return false; } + private static bool RegisterGameModeAttribute(Type type, MiraPluginInfo pluginInfo) + { + try + { + if (typeof(AbstractGameMode).IsAssignableFrom(type)) + return CustomGameModeManager.RegisterGameMode(type, pluginInfo); + } + catch (Exception e) + { + Error($"Failed to register gamemode {type.Name}: {e}"); + } + return false; + } + private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo) { try @@ -442,4 +596,29 @@ public sealed class MiraPluginManager Error($"Failed to register keybind class {type.Name}: {e}"); } } + + private static bool RegisterMeetingAbility(Type type, MiraPluginInfo info) + { + try + { + return MeetingButtonManager.RegisterMeetingButton(type, info); + } + catch (Exception e) + { + Error($"Failed to register meeting ability {type.Name}: {e}"); + return false; + } + } + private static bool RegisterTargetedMeetingAbility(Type type, MiraPluginInfo info) + { + try + { + return MeetingButtonManager.RegisterTargetedMeetingButton(type, info); + } + catch (Exception e) + { + Error($"Failed to register targeted meeting ability {type.Name}: {e}"); + return false; + } + } } diff --git a/MiraAPI/Resources/ClassicGamemodeIcon.png b/MiraAPI/Resources/ClassicGamemodeIcon.png new file mode 100644 index 0000000..163ac1a Binary files /dev/null and b/MiraAPI/Resources/ClassicGamemodeIcon.png differ diff --git a/MiraAPI/Resources/HnSGamemodeIcon.png b/MiraAPI/Resources/HnSGamemodeIcon.png new file mode 100644 index 0000000..8b60c3d Binary files /dev/null and b/MiraAPI/Resources/HnSGamemodeIcon.png differ diff --git a/MiraAPI/Resources/NormalChatHover.png b/MiraAPI/Resources/NormalChatHover.png new file mode 100644 index 0000000..e6d7a50 Binary files /dev/null and b/MiraAPI/Resources/NormalChatHover.png differ diff --git a/MiraAPI/Resources/NormalChatIdle.png b/MiraAPI/Resources/NormalChatIdle.png new file mode 100644 index 0000000..42fc51b Binary files /dev/null and b/MiraAPI/Resources/NormalChatIdle.png differ diff --git a/MiraAPI/Resources/NormalChatOpen.png b/MiraAPI/Resources/NormalChatOpen.png new file mode 100644 index 0000000..62d7b26 Binary files /dev/null and b/MiraAPI/Resources/NormalChatOpen.png differ diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index f756fed..0829eaa 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,6 +1,7 @@ -using AmongUs.GameOptions; using System; +using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameModes; using MiraAPI.Patches.Freeplay; using MiraAPI.Utilities.Assets; using TMPro; @@ -100,6 +101,11 @@ public record struct CustomRoleConfiguration [HideFromIl2Cpp] public LoadableAsset? IntroSound { get; set; } = null; + /// + /// Gets or sets the associated game mode for this role. This is used to determine if the role should be available in a specific game mode. + /// + public Type AssociatedGameMode { get; set; } = typeof(ClassicMode); + /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. /// diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index d02f50f..a54c439 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -142,11 +142,18 @@ public static class CustomRoleManager roleBehaviour.CanVent = customRole.Configuration.CanUseVent || customRole.Configuration.GetsVentData; roleBehaviour.DefaultGhostRole = customRole.Configuration.GhostRole; roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount; - roleBehaviour.RoleScreenshot = Sprite.Create( - customRole.Configuration.OptionsScreenshot?.LoadAsset().texture, - new Rect(0, 0, 370, 230), - Vector2.one / 2, - 100); + if (customRole.Configuration.OptionsScreenshot != null) + { + roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot.LoadAsset(); + } + else + { + roleBehaviour.RoleScreenshot = Sprite.Create( + null, + new Rect(0, 0, 370, 230), + Vector2.one / 2, + 100); + } _emptySettings ??= new(0); _emptyKillAnimations ??= new(0); diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index f0bfe4e..c418a30 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Text; using AmongUs.GameOptions; using MiraAPI.Translation; +using MiraAPI.GameModes; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -24,14 +26,14 @@ public static class CustomRoleUtils { if (role is ICustomRole custom) { - return custom.CanSpawnOnCurrentMode(); + return custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode); } - if (GameManager.Instance.IsHideAndSeek()) + if (CustomGameModeManager.ActiveMode is HideAndSeekMode) { return role.Role is RoleTypes.Engineer || role.Role is RoleTypes.Impostor; } - return true; + return !Helpers.IsRoleBlacklisted(role); } public static List<(ushort RoleType, int Chance)> GetPossibleRoles( diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 0b3f53b..e1b3b9f 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -29,11 +29,27 @@ public interface ICustomRole : IOptionable /// string RoleDescription => TranslationManager.BuildTranslationId(IdPart, "description"); + /// + /// Gets the medium description of the role. Used in the role guide. + /// + string RoleMedDescription => RoleDescription; + /// /// Gets the long description of the role. Used in the Role Tab and Role Options. /// string RoleLongDescription => TranslationManager.BuildTranslationId(IdPart, "description"); + /// + /// Gets the faction / alignment of the role. Used in the role guide. + /// + string RoleFactionTitle => Team switch + { + ModdedRoleTeams.Crewmate => TranslationController.Instance.GetString(StringNames.Crewmate), + ModdedRoleTeams.Impostor => TranslationController.Instance.GetString(StringNames.Impostor), + ModdedRoleTeams.Custom => "Neutral", + _ => "Other", + }; + /// /// Gets the of the role. /// @@ -118,6 +134,10 @@ public interface ICustomRole : IOptionable if (presetConfig.TryGetEntry(ChanceConfigDefinition, out ConfigEntry chanceEntry)) { SetChance(chanceEntry.Value); + if (chanceEntry.Value == 0) + { + SetCount(0); + } } } @@ -146,6 +166,10 @@ public interface ICustomRole : IOptionable /// The role count option. public virtual int? GetCount() { + if (ParentMod.PluginConfig.TryGetEntry(ChanceConfigDefinition, out ConfigEntry entry2) && entry2.Value == 0) + { + return 0; + } if (ParentMod.PluginConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry entry)) { return Mathf.Clamp(entry.Value, 0, Configuration.MaxRoleCount); diff --git a/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs b/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs new file mode 100644 index 0000000..ef7aea3 --- /dev/null +++ b/MiraAPI/Utilities/Assets/LoadableBundleSubAsset.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using UnityEngine; + +namespace MiraAPI.Utilities.Assets; + +/// +/// A utility class for loading multiple assets from an . +/// +/// The name of the asset. +/// The that contains the asset. +public class LoadableBundleSubAsset(string name, LoadableBundleSubAssetHolder assetHolder) : LoadableAsset +{ + /// + /// Loads the asset from the . + /// + /// The asset. + /// The asset did not load properly. + public override Sprite LoadAsset() + { + if (LoadedAsset != null) + { + return LoadedAsset; + } + + assetHolder.TryInit(); + + var loadedAsset = assetHolder.SubSprites.FirstOrDefault(x => x.name == name); + + if (loadedAsset == null) + { + throw new InvalidOperationException($"INVALID ASSETS: {name}"); + } + LoadedAsset = loadedAsset; + return LoadedAsset; + } +} diff --git a/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs new file mode 100644 index 0000000..3b84d8d --- /dev/null +++ b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using HarmonyLib; +using Il2CppInterop.Runtime; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Utilities.Assets; + +/// +/// A utility class for loading multiple assets from an . +/// +public class LoadableBundleSubAssetHolder +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the assets to pull from. + /// The that contains the assets. + public LoadableBundleSubAssetHolder(string[] names, AssetBundle bundle) + { + Bundle = bundle; + SpriteNames = names; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the asset. + /// The that contains the assets. + public LoadableBundleSubAssetHolder(string name, AssetBundle bundle) + { + Bundle = bundle; + SpriteNames = [name]; + } + internal string[] SpriteNames = []; + internal AssetBundle Bundle; + public Sprite[] SubSprites = []; + + public void TryInit() + { + if (SubSprites.Length == 0) + { + var newSprites = Array.Empty(); + foreach (var name in SpriteNames) + { + var loadedAssets = Bundle.LoadAssetWithSubAssets(name, Il2CppType.From(typeof(Sprite))).ToArray(); + + 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); + } + } + } + + SubSprites = newSprites; + } + } + + /// + /// Unloads an asset. + /// + /// True if the asset was unloaded, false otherwise. + public bool UnloadAsset() + { + if (SubSprites.Length == 0) + { + return false; + } + + foreach (var sprite in SubSprites) + { + sprite.DestroyImmediate(); + } + SubSprites = []; + return true; + } +} diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 10bceda..bec994a 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -130,10 +130,35 @@ public static class MiraAssets /// public static LoadableResourceAsset ModifierFile { get; } = new("MiraAPI.Resources.ModifierFile.png"); + /// + /// Gets the sprite used for the Classic gamemode. + /// + public static LoadableResourceAsset ClassicGamemodeIcon { get; } = new("MiraAPI.Resources.ClassicGamemodeIcon.png"); + + /// + /// Gets the sprite used for the Hide n Seek gamemode. + /// + public static LoadableResourceAsset HnSGamemodeIcon { get; } = new("MiraAPI.Resources.HnSGamemodeIcon.png"); + /// /// Gets the sprite used for timed modifier file in TaskAdderGame. /// public static LoadableResourceAsset TimedModifierFile { get; } = new("MiraAPI.Resources.TimedModifierFile.png"); + /// + /// Gets the sprite used for the new chat button while hovering. + /// + public static LoadableResourceAsset ChatHoverSprite { get; } = new("MiraAPI.Resources.NormalChatHover.png"); + + /// + /// Gets the sprite used for the new chat button while hovering. + /// + public static LoadableResourceAsset ChatIdleSprite { get; } = new("MiraAPI.Resources.NormalChatIdle.png"); + + /// + /// Gets the sprite used for the new chat button while hovering. + /// + public static LoadableResourceAsset ChatOpenSprite { get; } = new("MiraAPI.Resources.NormalChatOpen.png"); + internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png"); } diff --git a/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs b/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs index 518e4ac..6160a01 100644 --- a/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs +++ b/MiraAPI/Utilities/Assets/TmpSpriteUtils.cs @@ -71,8 +71,8 @@ public static class TmpSpriteUtils { name = spriteName, hashCode = TMP_TextUtilities.GetSimpleHashCode(spriteName), - x = 0, - y = 0, + x = rect.x, + y = rect.y, width = rect.width, height = rect.height, xOffset = -(rect.width / 3) + (scale - 1) * (rect.width / 2), diff --git a/MiraAPI/Utilities/Components/CenteredGridArrange.cs b/MiraAPI/Utilities/Components/CenteredGridArrange.cs new file mode 100644 index 0000000..ee32b26 --- /dev/null +++ b/MiraAPI/Utilities/Components/CenteredGridArrange.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.Utilities.Components; + +/// +/// Utility component for arranging objects in a centered layout, similar to . +/// +/// +[RegisterInIl2Cpp] +public class CenteredGridArrange(IntPtr ptr) : MonoBehaviour(ptr) +{ + /// + /// Gets or sets the cell size, which is used for spacing. + /// + public Vector2 CellSize; + + /// + /// Gets or sets the maximum amount of columns. + /// + public int MaxColumns = 6; + + private List cells; + private static List currentChildren = new List(); + + private void Start() + { + cells = new List(); + GetChildsActive(); + CheckCurrentChildren(); + } + + private void FixedUpdate() => CheckCurrentChildren(); + + private void CheckCurrentChildren() + { + GetChildsActive(); + if (cells.SequenceEqual(currentChildren)) + return; + cells.Clear(); + foreach (Transform currentChild in currentChildren) + cells.Add(currentChild); + ArrangeChilds(); + } + + private void GetChildsActive() + { + currentChildren.Clear(); + foreach (var obj in transform) + { + var child = obj.TryCast(); + if (child == null) continue; + if (child.gameObject.activeSelf) + currentChildren.Add(child); + } + } + + private void ArrangeChilds() + { + if (cells.Count == 0) + return; + + int totalRows = Mathf.CeilToInt((float)cells.Count / MaxColumns); + float totalHeight = (totalRows - 1) * CellSize.y; + float startY = transform.position.y + totalHeight * 0.5f; + + for (int index = 0; index < cells.Count; ++index) + { + int row = index / MaxColumns; + int rowStartIndex = row * MaxColumns; + int itemsInRow = Mathf.Min(MaxColumns, cells.Count - rowStartIndex); + int col = index - rowStartIndex; + + float rowWidth = (itemsInRow - 1) * CellSize.x; + float startX = transform.position.x - rowWidth * 0.5f; + + float x = startX + col * CellSize.x; + float y = startY - row * CellSize.y; + + Transform cell = cells[index]; + cell.position = new Vector3(x, y, cell.position.z); + } + } +} diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 53e395e..c31b9e5 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; @@ -164,7 +165,7 @@ public static class Extensions /// /// The . /// The player's . - public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.TargetPlayerId)?.Object; + public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.PlayerId)?.Object; /// /// Gets an representing the amount of tasks a player has left. @@ -366,6 +367,71 @@ public static class Extensions GC.Collect(); } + /// + /// Gets all child objects in a parent object. + /// + /// An that contains s. + public static IEnumerable GetAllChildren(this GameObject go) + { + for (var i = 0; i < go.transform.childCount; i++) + { + yield return go.transform.GetChild(i).gameObject; + } + } + + /// + /// Gets all child objects in a parent Transform. + /// + /// An that contains s. + public static IEnumerable GetAllChildren(this Transform go) + { + for (var i = 0; i < go.transform.childCount; i++) + { + yield return go.transform.GetChild(i).gameObject; + } + } + + /// + /// Resizes a appropriate to the maximum pixel size without messing up ratios. + /// + /// The to adjust. + /// The scale for the sprite to be adjusted to. + public static void SetSizeLimit(this SpriteRenderer sprite, float pixelSize) + { + sprite.drawMode = SpriteDrawMode.Sliced; + if (!sprite.sprite) + { + return; + } + + float spriteWidth = sprite.sprite.rect.width; + float spriteHeight = sprite.sprite.rect.height; + + if (spriteWidth < spriteHeight) + { + sprite.size = new Vector2(pixelSize * spriteWidth / spriteHeight, pixelSize); + } + else + { + sprite.size = new Vector2(pixelSize, pixelSize * spriteHeight / spriteWidth); + } + } + + /// + /// Resizes a appropriate to the maximum pixel size without messing up ratios. + /// + /// The to adjust. + /// The scale for the sprite to be adjusted to. + public static void SetSizeLimit(this GameObject spriteObj, float pixelSize) + { + if (!spriteObj.TryGetComponent(out var sprite)) + { + return; + } + + sprite.SetSizeLimit(pixelSize); + } + /// /// Gets a cache of 's to improve performance. /// @@ -611,7 +677,8 @@ public static class Extensions public static bool IsCustom(this OptionBehaviour optionBehaviour) { return ModdedOptionsManager.ModdedOptions.Values.Any( - opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour); + opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour) + || optionBehaviour.Equals(GameModeOption.OptionBehaviour); } /// diff --git a/MiraAPI/VanillaEvents/JudgeEvents.cs b/MiraAPI/VanillaEvents/JudgeEvents.cs new file mode 100644 index 0000000..4a20c6e --- /dev/null +++ b/MiraAPI/VanillaEvents/JudgeEvents.cs @@ -0,0 +1,28 @@ +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Meeting.Voting; +using MiraAPI.Roles; + +namespace MiraAPI.VanillaEvents; + +public static class JudgeEvents +{ + public static void Initialize() + { + // This is required because MiraAPI isn't an IMiraPlugin (besides in the gamemodes branch) + MiraEventManager.RegisterEventHandler(@event => ProcessVotesEventHandler(@event), -1000); + } + + public static void ProcessVotesEventHandler(ProcessVotesEvent @event) + { + if (MeetingHud.Instance.TryGetWinningOverrule(out var judgeOverrule, out var networkedPlayerInfo, out var networkedPlayerInfo2)) + { + Error("Judge has overruled votes!"); + @event.OverruledVote = true; + @event.OverruledNonce = judgeOverrule.OverruleNonce; + + @event.ExiledPlayer = networkedPlayerInfo2.Role is ICustomRole { Team: not ModdedRoleTeams.Crewmate } || networkedPlayerInfo2.Role.TeamType == RoleTeamTypes.Impostor + ? GameData.Instance.GetPlayerById(judgeOverrule.OverruledPlayerId) + : networkedPlayerInfo; + } + } +} diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index fb117e4..9744546 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.Networking; @@ -41,9 +42,9 @@ public static class VotingUtils /// The player's . /// Who the player voted for. /// Whether you want the vote to commence or not. - private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, out bool cancelVote) + private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, bool byJudge, out bool cancelVote) { - var @event = new HandleVoteEvent(voteData, suspectIdx); + var @event = new HandleVoteEvent(voteData, suspectIdx, byJudge); MiraEventManager.InvokeEvent(@event); cancelVote = @event.PreventVote; @@ -60,7 +61,14 @@ public static class VotingUtils return; } - voteData.DecreaseRemainingVotes(1); + if (byJudge) + { + voteData.SetRemainingVotes(0); + } + else + { + voteData.DecreaseRemainingVotes(1); + } voteData.VoteForPlayer(suspectIdx); } @@ -75,14 +83,14 @@ public static class VotingUtils { if (!source.IsHost()) return; - MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == voterId).UnsetVote(); + MeetingHud.Instance.playerStates.First(state => state.PlayerId == voterId).UnsetVote(); if (PlayerControl.LocalPlayer.PlayerId != voterId) { return; } - MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == votedFor).ThumbsDown.enabled = false; + MeetingHud.Instance.playerStates.First(state => state.PlayerId == votedFor).ThumbsDown.enabled = false; if (!AmongUsClient.Instance.AmHost) { @@ -93,13 +101,74 @@ public static class VotingUtils foreach (var t in MeetingHud.Instance.playerStates) { - t.voteComplete = false; + t.VoteComplete = false; } - MeetingHud.Instance.SkipVoteButton.voteComplete = false; + MeetingHud.Instance.SkipVoteButton.VoteComplete = false; MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(true); } + /// + /// Networks the casting of a vote. We replace the vanilla solution with a custom version that works for Judge specifically. + /// + /// The who sent this RPC. + /// The id of the player who casted the vote. + /// The voted player's id. + /// Data that is checked by the Judge role to determine which Judge takes priority. + [MethodRpc((uint)MiraRpc.QueueOverruleVotes)] + public static void RpcQueueOverruleVotes(PlayerControl source, byte srcPlayerId, byte suspectPlayerId, ushort overruleNonce) + { + CustomCastJudgeVote(srcPlayerId, suspectPlayerId, overruleNonce); + } + + private static void CustomCastJudgeVote(PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce) + { + var plr = GameData.Instance.GetPlayerById(judgePlayerId.Value); + if (!plr) return; + + var voteData = plr.Object.GetVoteData(); + if (!voteData) return; + + HandleVote(voteData, targetPlayerId.Value, true, out var cancelVote); + + // Handle local behaviour for the voter (for some reason checking AmOwner does not work for host) + if (PlayerControl.LocalPlayer.PlayerId == judgePlayerId.Value) + { + if (!cancelVote) SoundManager.Instance.PlaySound(MeetingHud.Instance.VoteLockinSound, false); + + foreach (var playerVoteArea in MeetingHud.Instance.playerStates) + { + playerVoteArea.ClearButtons(); + } + + MeetingHud.Instance.SkipVoteButton.ClearButtons(); + + var localVoteData = PlayerControl.LocalPlayer.GetVoteData(); + if (!localVoteData || localVoteData.VotesRemaining != 0) return; + + MeetingHud.Instance.SkipVoteButton.VoteComplete = true; + MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(false); + } + + if (cancelVote) return; + + // If player has no more votes, then make it show that the player has used all their votes. + if (voteData.VotesRemaining == 0) + { + MeetingHud.Instance.AddOrUpdateJudgeOverrule(judgePlayerId, targetPlayerId, overruleNonce); + MeetingHud.Instance.playerStates.First(x => x.PlayerId == judgePlayerId.Value).SetVote(targetPlayerId.Value); + } + + // If host, then check end voting, and set votes/send chat if applicable + if (!PlayerControl.LocalPlayer.IsHost()) return; + + MeetingHud.Instance.SetDirtyBit(1U); + MeetingHud.Instance.CheckForEndVoting(); + + if (voteData.VotesRemaining != 0) return; + PlayerControl.LocalPlayer.RpcSendChatNote(judgePlayerId.Value, ChatNoteTypes.DidVote); + } + /// /// Networks the casting of a vote. We replace the vanilla solution with a custom version that works for the use case. /// @@ -120,7 +189,7 @@ public static class VotingUtils var voteData = plr.Object.GetVoteData(); if (!voteData) return; - HandleVote(voteData, suspectPlayerId, out var cancelVote); + HandleVote(voteData, suspectPlayerId, false, out var cancelVote); // Handle local behaviour for the voter (for some reason checking AmOwner does not work for host) if (PlayerControl.LocalPlayer.PlayerId == srcPlayerId) @@ -137,7 +206,7 @@ public static class VotingUtils var localVoteData = PlayerControl.LocalPlayer.GetVoteData(); if (!localVoteData || localVoteData.VotesRemaining != 0) return; - MeetingHud.Instance.SkipVoteButton.voteComplete = true; + MeetingHud.Instance.SkipVoteButton.VoteComplete = true; MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(false); } @@ -146,7 +215,7 @@ public static class VotingUtils // If player has no more votes, then make it show that the player has used all their votes. if (voteData.VotesRemaining == 0) { - MeetingHud.Instance.playerStates.First(x => x.TargetPlayerId == srcPlayerId).SetVote(suspectPlayerId); + MeetingHud.Instance.playerStates.First(x => x.PlayerId == srcPlayerId).SetVote(suspectPlayerId); } // If host, then check end voting, and set votes/send chat if applicable @@ -233,7 +302,7 @@ public static class VotingUtils MeetingHud.Instance.BloopAVoteIcon(playerById, num, MeetingHud.Instance.SkippedVoting.transform); num++; } - else if (vote.Suspect == playerVoteArea.TargetPlayerId) + else if (vote.Suspect == playerVoteArea.PlayerId) { if (!delays.TryAdd(vote.Suspect, 0)) {