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();
}