From 2a4d16e65e7e4d90dce9206f8f8327cf20a601d4 Mon Sep 17 00:00:00 2001 From: yanpla Date: Sun, 9 Aug 2026 01:50:29 +0200 Subject: [PATCH 1/3] simplify and fix role assignment patch --- .../Roles/LogicRoleSelectionNormalPatch.cs | 63 +++++++------------ 1 file changed, 22 insertions(+), 41 deletions(-) diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs index b0540bb..e74d7ab 100644 --- a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs +++ b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs @@ -26,68 +26,49 @@ public static class LogicRoleSelectionNormalPatch 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))) + // Assign guaranteed roles first, just like the vanilla selector. This is + // important because the list of players is shared by both team passes. + foreach (var role in source.Where(x => roleOptions.GetChancePerGame(x.Role) == 100)) { - while (true) + for (var i = 0; i < roleOptions.GetNumPerGame(role.Role); i++) { - RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; - int count = roleAssignmentData2.Count; - roleAssignmentData2.Count = count - 1; - if (count <= 0) - { - break; - } - - list.Add(roleAssignmentData.Role.Role); + list.Add(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(); - + // A 100% role was already assigned above. Including it here can consume + // another player and, more importantly, leaves the fallback count wrong. list.Clear(); - foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2) + foreach (var role in source.Where(x => + roleOptions.GetChancePerGame(x.Role) > 0 && + roleOptions.GetChancePerGame(x.Role) < 100)) { - for (int i = 0; i < roleAssignmentData3.Count; i++) + for (var i = 0; i < roleOptions.GetNumPerGame(role.Role); i++) { - if (HashRandom.Next(101) < roleAssignmentData3.Chance) + if (HashRandom.Next(101) < roleOptions.GetChancePerGame(role.Role)) { - list.Add(roleAssignmentData3.Role.Role); + list.Add(role.Role); } } } __instance.AssignRolesFromList(players, teamMax, list, ref num); - var defaultRole2 = team is RoleTeamTypes.Crewmate ? RoleTypes.Crewmate : RoleTypes.Impostor; - try - { - defaultRole2 = defaultRole.Value; - } - catch - { - // Ignored - } - while (list.Count < players.Count && list.Count + num < teamMax) + // Do not read defaultRole here. The nullable argument is not marshalled + // reliably by IL2CPP and can return Crewmate for the impostor pass. + var defaultRole2 = team is RoleTeamTypes.Crewmate ? RoleTypes.Crewmate : RoleTypes.Impostor; + + // Assign the remaining players up to the requested team limit. The + // vanilla list has been consumed by AssignRolesFromList at this point, + // so checking list.Count can leave players unassigned. + while (players.Count > 0 && num < teamMax) { list.Add(defaultRole2); + __instance.AssignRolesFromList(players, teamMax, list, ref num); } - __instance.AssignRolesFromList(players, teamMax, list, ref num); - return false; } } From df7c182abb34380f6a72482fe1e5a9815f779dbe Mon Sep 17 00:00:00 2001 From: yanpla Date: Sun, 9 Aug 2026 01:52:33 +0200 Subject: [PATCH 2/3] use .select() in loop --- .../Roles/LogicRoleSelectionNormalPatch.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs index e74d7ab..0dc9474 100644 --- a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs +++ b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs @@ -28,11 +28,12 @@ public static class LogicRoleSelectionNormalPatch // Assign guaranteed roles first, just like the vanilla selector. This is // important because the list of players is shared by both team passes. - foreach (var role in source.Where(x => roleOptions.GetChancePerGame(x.Role) == 100)) + foreach (var role in source.Where(x => roleOptions.GetChancePerGame(x.Role) == 100) + .Select(role => role.Role)) { - for (var i = 0; i < roleOptions.GetNumPerGame(role.Role); i++) + for (var i = 0; i < roleOptions.GetNumPerGame(role); i++) { - list.Add(role.Role); + list.Add(role); } } @@ -43,13 +44,14 @@ public static class LogicRoleSelectionNormalPatch list.Clear(); foreach (var role in source.Where(x => roleOptions.GetChancePerGame(x.Role) > 0 && - roleOptions.GetChancePerGame(x.Role) < 100)) + roleOptions.GetChancePerGame(x.Role) < 100) + .Select(role => role.Role)) { - for (var i = 0; i < roleOptions.GetNumPerGame(role.Role); i++) + for (var i = 0; i < roleOptions.GetNumPerGame(role); i++) { - if (HashRandom.Next(101) < roleOptions.GetChancePerGame(role.Role)) + if (HashRandom.Next(101) < roleOptions.GetChancePerGame(role)) { - list.Add(role.Role); + list.Add(role); } } } From 043845ee11bdc3f6c8d8c9f4dd627c340198ca8d Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:57:47 -0500 Subject: [PATCH 3/3] Fix crashing on Epic and MS Store --- .../Roles/LogicRoleSelectionNormalPatch.cs | 76 ------------ MiraAPI/Patches/Roles/SelectRolesPatch.cs | 111 ++++++++++++++++++ 2 files changed, 111 insertions(+), 76 deletions(-) delete mode 100644 MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs create mode 100644 MiraAPI/Patches/Roles/SelectRolesPatch.cs diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs deleted file mode 100644 index 0dc9474..0000000 --- a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs +++ /dev/null @@ -1,76 +0,0 @@ -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; - - // Assign guaranteed roles first, just like the vanilla selector. This is - // important because the list of players is shared by both team passes. - foreach (var role in source.Where(x => roleOptions.GetChancePerGame(x.Role) == 100) - .Select(role => role.Role)) - { - for (var i = 0; i < roleOptions.GetNumPerGame(role); i++) - { - list.Add(role); - } - } - - __instance.AssignRolesFromList(players, teamMax, list, ref num); - - // A 100% role was already assigned above. Including it here can consume - // another player and, more importantly, leaves the fallback count wrong. - list.Clear(); - foreach (var role in source.Where(x => - roleOptions.GetChancePerGame(x.Role) > 0 && - roleOptions.GetChancePerGame(x.Role) < 100) - .Select(role => role.Role)) - { - for (var i = 0; i < roleOptions.GetNumPerGame(role); i++) - { - if (HashRandom.Next(101) < roleOptions.GetChancePerGame(role)) - { - list.Add(role); - } - } - } - - __instance.AssignRolesFromList(players, teamMax, list, ref num); - - // Do not read defaultRole here. The nullable argument is not marshalled - // reliably by IL2CPP and can return Crewmate for the impostor pass. - var defaultRole2 = team is RoleTeamTypes.Crewmate ? RoleTypes.Crewmate : RoleTypes.Impostor; - - // Assign the remaining players up to the requested team limit. The - // vanilla list has been consumed by AssignRolesFromList at this point, - // so checking list.Count can leave players unassigned. - while (players.Count > 0 && num < teamMax) - { - list.Add(defaultRole2); - __instance.AssignRolesFromList(players, teamMax, list, ref num); - } - - return false; - } -} diff --git a/MiraAPI/Patches/Roles/SelectRolesPatch.cs b/MiraAPI/Patches/Roles/SelectRolesPatch.cs new file mode 100644 index 0000000..dc51f03 --- /dev/null +++ b/MiraAPI/Patches/Roles/SelectRolesPatch.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using AmongUs.GameOptions; +using HarmonyLib; +using System.Linq; +using InnerNet; +using MiraAPI.Roles; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(RoleManager))] +public static class SelectRolesPatch +{ + public static bool ApiHandlesRoleSelect = true; + [HarmonyPrefix] + [HarmonyPatch(nameof(RoleManager.SelectRoles))] + public static bool SelectRoles() + { + if (!ApiHandlesRoleSelect) + { + return true; + } + Il2CppSystem.Collections.Generic.List list = new(); + AmongUsClient.Instance.GetAllClients(list); + List list2 = list.ToArray() + .Where(c => c.Character != null && c.Character.Data != null && !c.Character.Data.Disconnected && + !c.Character.Data.IsDead).OrderBy(c => c.Id).Select(c => c.Character.Data) + .ToList(); + + foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) + { + 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, adjustedNumImpostors, RoleTypes.Impostor); + AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Crewmate); + return false; + } + + public static void AssignRolesForTeam( + List players, + IGameOptions opts, + RoleTeamTypes team, + int teamMax, + RoleTypes 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; + + // Assign guaranteed roles first, just like the vanilla selector. This is + // important because the list of players is shared by both team passes. + foreach (var role in source.Where(x => roleOptions.GetChancePerGame(x.Role) == 100) + .Select(role => role.Role)) + { + for (var i = 0; i < roleOptions.GetNumPerGame(role); i++) + { + list.Add(role); + } + } + + AssignRolesFromList(players, teamMax, list, ref num); + + // A 100% role was already assigned above. Including it here can consume + // another player and, more importantly, leaves the fallback count wrong. + list.Clear(); + foreach (var role in source.Where(x => + roleOptions.GetChancePerGame(x.Role) > 0 && + roleOptions.GetChancePerGame(x.Role) < 100) + .Select(role => role.Role)) + { + for (var i = 0; i < roleOptions.GetNumPerGame(role); i++) + { + if (HashRandom.Next(101) < roleOptions.GetChancePerGame(role)) + { + list.Add(role); + } + } + } + + AssignRolesFromList(players, teamMax, list, ref num); + + // Assign the remaining players up to the requested team limit. The + // vanilla list has been consumed by AssignRolesFromList at this point, + // so checking list.Count can leave players unassigned. + while (players.Count > 0 && num < teamMax) + { + list.Add(defaultRole); + AssignRolesFromList(players, teamMax, list, ref num); + } + } + private static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned) + { + while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax) + { + int index = HashRandom.FastNext(roleList.Count); + RoleTypes roleType = roleList[index]; + roleList.RemoveAt(index); + int index2 = HashRandom.FastNext(players.Count); + players[index2].Object.RpcSetRole(roleType, false); + players.RemoveAt(index2); + rolesAssigned++; + } + } +}