diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 71c0dc7..2456af0 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -179,6 +179,7 @@ public static class ModifierManager private static bool IsGameModifierValid(PlayerControl player, GameModifier modifier, uint modifierId) { return (player.Data.Role is not ICustomRole role || role.IsModifierApplicable(modifier)) && - modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifier(modifierId); + modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifier(modifierId) && + modifier.CanSpawnOnCurrentMode(); } } diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index 13f6754..144ff51 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -39,4 +39,10 @@ public abstract class GameModifier : BaseModifier /// The reason why the game ended. /// True if the player won, false if they lost. Return null to use the player's default win condition. public virtual bool? DidWin(GameOverReason reason) => null; + + /// + /// Determines whether the modifier can spawn in general, accounting for gamemodes and everything else. + /// + /// True if the modifier is able to spawn, otherwise false. + public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek(); } diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs new file mode 100644 index 0000000..7789180 --- /dev/null +++ b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs @@ -0,0 +1,179 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using Il2CppSystem.Collections.Generic; +using System.Linq; +using MiraAPI.Roles; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(LogicRoleSelectionHnS))] +public static class LogicRoleSelectionHnsPatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(LogicRoleSelectionHnS.AssignRolesForTeam))] + public static bool AssignRolesForTeam( + LogicRoleSelectionHnS __instance, + List players, + IGameOptions opts, + RoleTeamTypes team, + int teamMax, + Il2CppSystem.Nullable defaultRole) + { + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); + int num = 0; + IRoleOptionsCollection roleOptions = opts.RoleOptions; + 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))) + { + for (;;) + { + 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) + { + var newImpostors = new List(); + // Specified Seeker + if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() && + __instance.hnsManager.LogicOptionsHnS.ValidateImpostorPlayerID(players) && + !AmongUsClient.Instance.IsGamePublic) + { + NetworkedPlayerInfo networkedPlayerInfo = players.ToArray() + .First(p => p.PlayerId == __instance.hnsManager.LogicOptionsHnS.ImpostorPlayerID()); + players.Remove(networkedPlayerInfo); + newImpostors.Add(networkedPlayerInfo); + } + // Random Seeker + else + { + int num2 = 0; + while (num2 < teamMax && players.Count > 0) + { + PseudoRandomList pseudoRandomList = new PseudoRandomList(AmongUsClient.Instance.GameId); + players._items.Do(x => pseudoRandomList.Add(x)); + for (int i = 0; i < GameData.RoundsPlayedInSession; i++) + { + pseudoRandomList.PickRandom(); + } + NetworkedPlayerInfo networkedPlayerInfo = pseudoRandomList.PickRandom(); + players.Remove(networkedPlayerInfo); + newImpostors.Add(networkedPlayerInfo); + num2++; + } + } + foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) => + new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) + { + for (;;) + { + RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; + int count = roleAssignmentData2.Count; + roleAssignmentData2.Count = count - 1; + if (count <= 0) + { + break; + } + + list.Add(roleAssignmentData.Role.Role); + } + } + 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); + } + } + } + + AssignRolesFromList(players, teamMax, list, ref num); + var basicRole = RoleTypes.Impostor; + while (list.Count < players.Count && list.Count + num < teamMax) + { + list.Add(basicRole); + } + + AssignRolesFromList(players, teamMax, list, ref num); + } + return false; + } + + 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++; + } + } +} diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs new file mode 100644 index 0000000..d32f0dd --- /dev/null +++ b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs @@ -0,0 +1,86 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using Il2CppSystem.Collections.Generic; +using System.Linq; +using MiraAPI.Roles; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(LogicRoleSelectionNormal))] +public static class LogicRoleSelectionNormalPatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(LogicRoleSelectionNormal.AssignRolesForTeam))] + public static bool AssignRolesForTeam( + LogicRoleSelectionNormal __instance, + List players, + IGameOptions opts, + RoleTeamTypes team, + int teamMax, + Il2CppSystem.Nullable defaultRole) + { + int num = 0; + var source = RoleManager.Instance.AllRoles.ToArray() + .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) && + CustomRoleUtils.CanSpawnOnCurrentMode(role)); + List list = new List(); + IRoleOptionsCollection roleOptions = opts.RoleOptions; + + 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))); + foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) => + new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) + { + for (;;) + { + RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; + int count = roleAssignmentData2.Count; + roleAssignmentData2.Count = count - 1; + if (count <= 0) + { + break; + } + + list.Add(roleAssignmentData.Role.Role); + } + } + + __instance.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); + } + } + } + + __instance.AssignRolesFromList(players, teamMax, list, ref num); + if (defaultRole != null) + { + while (list.Count < players.Count && list.Count + num < teamMax) + { + list.Add(defaultRole.Value); + } + + __instance.AssignRolesFromList(players, teamMax, list, ref num); + } + + return false; + } +} diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index 8d13595..713ffdf 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -1,6 +1,7 @@ using System.Linq; using AmongUs.GameOptions; using HarmonyLib; +using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Modifiers; diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 891c3ba..7168a70 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -51,7 +51,7 @@ public class MiraPluginInfo public IReadOnlyCollection Buttons { get; private set; } = null!; /// - /// Gets a read only dictionary of this plugin's custom game modes. + /// Gets a read only dictionary of this plugin's custom presets. /// public IReadOnlyCollection Presets { get; internal set; } = null!; diff --git a/MiraAPI/Presets/PresetManager.cs b/MiraAPI/Presets/PresetManager.cs index 7fa83a2..9204e42 100644 --- a/MiraAPI/Presets/PresetManager.cs +++ b/MiraAPI/Presets/PresetManager.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Linq; using BepInEx.Configuration; using MiraAPI.PluginLoading; @@ -18,6 +19,13 @@ public static class PresetManager /// public static string PresetDirectory { get; } = Path.GetFullPath("mira_presets", Application.persistentDataPath); + internal static List InternalMasterPresets { get; } = []; + + /// + /// Gets a read only dictionary of this plugin's custom presets. + /// + public static IReadOnlyCollection MasterPresets { get; internal set; } = null!; + internal static void CreateDefaultPreset(MiraPluginInfo plugin) { var presetPath = Path.Combine(PresetDirectory, plugin.PluginId); @@ -93,4 +101,58 @@ public static class PresetManager } plugin.Presets = [..plugin.InternalPresets]; } + + /// + /// Loads the master presets by reading the configuration files from the preset directory. + /// + public static void LoadMasterPreset() + { + /*foreach (var btn in plugin.InternalPresets.Select(x=>x.PresetButton)) + { + if (btn != null) + { + Object.DestroyImmediate(btn); + } + } + + plugin.InternalPresets.Clear();*/ + if (!Directory.Exists(PresetDirectory)) + { + Directory.CreateDirectory(PresetDirectory); + } + + var pluginPresetPath = Path.Combine(PresetDirectory, "MasterPresets"); + if (!Directory.Exists(pluginPresetPath)) + { + Directory.CreateDirectory(pluginPresetPath); + } + + foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg")) + { + var fileName = Path.GetFileName(file); + Info($"Loading preset file {fileName}"); + var presetName = Path.GetFileNameWithoutExtension(file); + var presetConfig = new ConfigFile(file, false) + { + SaveOnConfigSet = false, + }; + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins) + { + foreach (var option in plugin.InternalOptions.Where(x=>x.IncludeInPreset)) + { + option.Bind(presetConfig); + } + + foreach (var role in plugin.InternalRoles.Values.OfType().Where(x=>!x.Configuration.HideSettings)) + { + role.BindConfig(presetConfig); + } + } + + presetConfig.Save(); + + // InternalMasterPresets.Add(new OptionPreset(presetName, plugin, presetConfig)); + } + MasterPresets = [..InternalMasterPresets]; + } } diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index cb4dbfb..fba91bf 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; @@ -13,6 +14,62 @@ namespace MiraAPI.Roles; /// public static class CustomRoleUtils { + /// + /// Determines whether the specified role can spawn in general, accounting for gamemodes and everything else. + /// + /// The role you would like to check for. + /// True if the role is able to spawn, otherwise false. + public static bool CanSpawnOnCurrentMode(RoleBehaviour role) + { + if (role is ICustomRole custom) + { + return custom.CanSpawnOnCurrentMode(); + } + + if (GameManager.Instance.IsHideAndSeek()) + { + return role.Role is RoleTypes.Engineer || role.Role is RoleTypes.Impostor; + } + return true; + } + + public static List<(ushort RoleType, int Chance)> GetPossibleRoles( + List assignmentData, + Func? predicate = null) + { + var roles = new List<(ushort, int)>(); + + assignmentData.Where(x => predicate == null || predicate(x)).ToList().ForEach(x => + { + for (var i = 0; i < x.Count; i++) + { + roles.Add(((ushort)x.Role.Role, x.Chance)); + } + }); + + return roles; + } + + public static RoleManager.RoleAssignmentData GetAssignData(RoleTypes roleType) + { + var currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; + var roleOptions = currentGameOptions.RoleOptions; + + var role = GetRegisteredRole(roleType); + var assignmentData = new RoleManager.RoleAssignmentData(role, roleOptions.GetNumPerGame(role!.Role), + roleOptions.GetChancePerGame(role.Role)); + + return assignmentData; + } + + public static RoleBehaviour? GetRegisteredRole(RoleTypes roleType) + { + // we want to prioritize the custom roles because the role has the right RoleColour/TeamColor + var role = CustomRoleManager.AllRoles.FirstOrDefault(x => x.Role == roleType); + + return role; + } + /// /// Gets all active in-game roles. /// diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index d0cfe90..9ed860d 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -239,4 +239,10 @@ public interface ICustomRole : IOptionable { return true; } + + /// + /// Determines whether the role can spawn in general, accounting for gamemodes and everything else. + /// + /// True if the role is able to spawn, otherwise false. + public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek(); }