Repository: EnhancedNetwork/TownofHost-Enhanced
Pull request #1503: Recode "Remove Incompatible Add-ons"
Author: login: Tommy-XL
Labels: name: Fix bug
description: Something isn't working
Created: 2025-02-28T15:29:09Z | Merged: 2025-03-01T13:34:21Z

`RemoveIncompatibleAddOns()` remained unchanged for assigning roles as there should be no problems with it

`CheckAndAssignAddOn()` did exclusively for assign add-ons in mid-game and it is there that all the old add-ons will be checked with the new addon and ONLY the OLD add-ons will be removed

Did `RemoveIncompatibleAddOns()` And `CheckAndAssignAddOn()` as async to prevent strong lag

[Need recode RemoveIncompatibleAddOns()](https://github.com/EnhancedNetwork/TownofHost-Enhanced/issues/1498)


Diff:
diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs
index 3519b7bc9d..4b51c1a331 100644
--- a/Modules/CustomRolesHelper.cs
+++ b/Modules/CustomRolesHelper.cs
@@ -456,16 +456,19 @@ public static bool IsPlayerCovenTeam(this PlayerState player, bool onlyMainRole
 
         return player.MainRole.IsCoven();
     }
-    public static bool CheckAddonConfilct(CustomRoles role, PlayerControl pc, bool checkLimitAddons = true, bool checkSelfAddOn = true)
+    public static bool CheckAddonConfilct(CustomRoles role, PlayerControl pc, bool checkLimitAddons = true, bool checkConditions = true)
     {
         // Only add-ons
         if (!role.IsAdditionRole() || pc == null) return false;
 
-        if (Options.AddonCanBeSettings.TryGetValue(role, out var o) && ((!o.Imp.GetBool() && pc.GetCustomRole().IsImpostor()) || (!o.Neutral.GetBool() && pc.GetCustomRole().IsNeutral()) || (!o.Crew.GetBool() && pc.GetCustomRole().IsCrewmate()) || (!o.Coven.GetBool() && pc.GetCustomRole().IsCoven())))
-            return false;
+        if (checkConditions)
+        {
+            if (Options.AddonCanBeSettings.TryGetValue(role, out var o) && ((!o.Imp.GetBool() && pc.GetCustomRole().IsImpostor()) || (!o.Neutral.GetBool() && pc.GetCustomRole().IsNeutral()) || (!o.Crew.GetBool() && pc.GetCustomRole().IsCrewmate()) || (!o.Coven.GetBool() && pc.GetCustomRole().IsCoven())))
+                return false;
 
-        // if player already has this addon
-        else if (checkSelfAddOn && pc.Is(role)) return false;
+            // if player already has this addon
+            else if (pc.Is(role)) return false;
+        }
 
         // Checking Lovers and Romantics
         else if ((pc.Is(CustomRoles.RuthlessRomantic) || pc.Is(CustomRoles.Romantic) || pc.Is(CustomRoles.VengefulRomantic)) && role is CustomRoles.Lovers) return false;
@@ -480,7 +483,7 @@ public static bool CheckAddonConfilct(CustomRoles role, PlayerControl pc, bool c
         // Checking for conflicts with roles and other add-ons
         switch (role)
         {
-            case var Addon when (pc.IsAnySubRole(x => x.IsSpeedRole()) || pc.GetCustomRole().IsSpeedRole()) && Addon.IsSpeedRole():
+            case var Addon when checkConditions && (pc.IsAnySubRole(x => x.IsSpeedRole()) || pc.GetCustomRole().IsSpeedRole()) && Addon.IsSpeedRole():
                 return false;
 
             case CustomRoles.Autopsy:
diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs
index a9b91b245e..9e128d953c 100644
--- a/Modules/ExtendedPlayerControl.cs
+++ b/Modules/ExtendedPlayerControl.cs
@@ -4,6 +4,7 @@
 using InnerNet;
 using System;
 using System.Text;
+using UnityEngine;
 using TOHE.Modules;
 using TOHE.Patches;
 using TOHE.Roles.AddOns.Common;
@@ -13,7 +14,7 @@
 using TOHE.Roles.Crewmate;
 using TOHE.Roles.Impostor;
 using TOHE.Roles.Neutral;
-using UnityEngine;
+using BepInEx.Unity.IL2CPP.Utils.Collections;
 using static TOHE.Translator;
 
 namespace TOHE;
@@ -26,17 +27,26 @@ public static void RpcSetCustomRole(this PlayerControl player, CustomRoles role,
         if (role < CustomRoles.NotAssigned)
         {
             Main.PlayerStates[player.PlayerId].SetMainRole(role);
-            //  player.GetRoleClass()?.OnAdd(player.PlayerId);
-            // Remember to manually add OnAdd if you are setting role mid game
-            if (checkAddons && Options.RemoveIncompatibleAddOnsMidGame.GetBool()) player.RemoveIncompatibleAddOns();
+
+            // Remove incompatible Add-ons
+            if (checkAddons && Options.RemoveIncompatibleAddOnsMidGame.GetBool())
+            {
+                player.StartCoroutine(player.RemoveIncompatibleAddOnsAsync().WrapToIl2Cpp());
+            }
         }
         else if (role >= CustomRoles.NotAssigned)   //500:NoSubRole 501~:SubRole 
         {
             if (Cleanser.CantGetAddon() && player.Is(CustomRoles.Cleansed)) return;
 
-            Main.PlayerStates[player.PlayerId].SetSubRole(role, pc: player);
-
-            if (checkAAconflict && Options.RemoveIncompatibleAddOnsMidGame.GetBool()) player.RemoveIncompatibleAddOns();
+            // Check conflict
+            if (checkAAconflict && Options.RemoveIncompatibleAddOnsMidGame.GetBool())
+            {
+                player.StartCoroutine(player.CheckAndAssignAddOnAsync(role).WrapToIl2Cpp());
+            }
+            else
+            {
+                Main.PlayerStates[player.PlayerId].SetSubRole(role, pc: player);
+            }
         }
         if (AmongUsClient.Instance.AmHost)
         {
@@ -57,20 +67,56 @@ public static void RpcSetCustomRole(byte PlayerId, CustomRoles role)
             AmongUsClient.Instance.FinishRpcImmediately(writer);
         }
     }
-    public static void RemoveIncompatibleAddOns(this PlayerControl player)
+    public static void CheckConflictedAddOnsFromList(this PlayerControl player, ref List<CustomRoles> addOnList)
+    {
+        List<CustomRoles> conflictedAddOns = [];
+        foreach (var addon in addOnList)
+        {
+            if (!CustomRolesHelper.CheckAddonConfilct(addon, player, checkLimitAddons: false))
+            {
+                conflictedAddOns.Add(addon);
+            }
+        }
+        foreach (var removeAddOns in conflictedAddOns.ToArray())
+        {
+            Logger.Msg($"{removeAddOns} have conflict, remove from list", $"{player.GetCustomRole()}");
+            addOnList.Remove(removeAddOns);
+        }
+    }
+    private static System.Collections.IEnumerator RemoveIncompatibleAddOnsAsync(this PlayerControl player)
     {
-        var roles = player.GetCustomSubRoles().ToList();
-        roles = roles.Where(x => !x.IsAddonAssignedMidGame()).ToList();
-        roles.Shuffle();
+        var currentAddOns = player.GetCustomSubRoles().Where(x => !x.IsAddonAssignedMidGame()).ToArray();
+
+        yield return new WaitForSeconds(0.1f);
 
-        foreach (var addon in roles)
+        foreach (var addon in currentAddOns)
         {
-            if (!CustomRolesHelper.CheckAddonConfilct(addon, player, checkLimitAddons: false, checkSelfAddOn: false))
+            if (!CustomRolesHelper.CheckAddonConfilct(addon, player, checkLimitAddons: false, checkConditions: false))
             {
                 Main.PlayerStates[player.PlayerId].RemoveSubRole(addon);
-                Logger.Info($"{player.GetNameWithRole()} had incompatible addon {addon.ToString()}, removing addon", $"{player.GetCustomRole().ToString()}");
+                Logger.Msg($"{player.GetNameWithRole()} had incompatible addon {addon}, removing addon", $"RemoveIncompatibleAddOnsAsync: {player.GetCustomRole()}");
+            }
+            yield return new WaitForSeconds(0.2f);
+        }
+    }
+    private static System.Collections.IEnumerator CheckAndAssignAddOnAsync(this PlayerControl player, CustomRoles newAddOn)
+    {
+        var currentAddOns = player.GetCustomSubRoles().Where(x => !x.IsAddonAssignedMidGame()).ToArray();
+
+        Main.PlayerStates[player.PlayerId].SetSubRole(newAddOn, pc: player);
+
+        yield return new WaitForSeconds(0.1f);
+
+        foreach (var addOn in currentAddOns)
+        {
+            if (!CustomRolesHelper.CheckAddonConfilct(addOn, player, checkLimitAddons: false, checkConditions: false))
+            {
+                Main.PlayerStates[player.PlayerId].RemoveSubRole(addOn);
+                Logger.Msg($"{player.GetNameWithRole()} had incompatible addon {addOn}, removing addon", $"CheckAndAssignAddOnAsync: {player.GetCustomRole()}");
             }
+            yield return new WaitForSeconds(0.2f);
         }
+
     }
     public static void SetRole(this PlayerControl player, RoleTypes role, bool canOverride)
     {
@@ -107,7 +153,7 @@ public static void RpcRevive(this PlayerControl player)
         if (player.HasGhostRole())
         {
             player.GetRoleClass().OnRemove(player.PlayerId);
-            player.RpcSetCustomRole(player.GetRoleMap().CustomRole);
+            player.RpcSetCustomRole(player.GetRoleMap().CustomRole, false, false);
             player.GetRoleClass().OnAdd(player.PlayerId);
         }
 
@@ -1291,13 +1337,9 @@ CustomRoles.Enchanted and not
             CustomRoles.Contagious;
     }
 
-    public static void AddInSwitchAddons(this PlayerControl Killed, PlayerControl target, CustomRoles Addon = CustomRoles.NotAssigned, CustomRoles? IsAddon = CustomRoles.NotAssigned)
+    public static void AddInSwitchAddons(this PlayerControl Killed, PlayerControl target, CustomRoles addOn = CustomRoles.NotAssigned)
     {
-        if (Addon == CustomRoles.NotAssigned)
-        {
-            Addon = IsAddon ?? CustomRoles.NotAssigned;
-        }
-        if (CustomRoleManager.AddonClasses.TryGetValue(Addon, out var IAddon))
+        if (CustomRoleManager.AddonClasses.TryGetValue(addOn, out var IAddon))
         {
             IAddon?.Remove(Killed.PlayerId);
             IAddon?.Add(target.PlayerId, false);
diff --git a/Patches/TaskAdderPatch.cs b/Patches/TaskAdderPatch.cs
index 344a475f6c..8e974afd28 100644
--- a/Patches/TaskAdderPatch.cs
+++ b/Patches/TaskAdderPatch.cs
@@ -96,7 +96,7 @@ public static bool Prefix(TaskAddButton __instance)
             if ((int)__instance.Role.Role >= 1000)
             {
                 CustomRoles FileCustomRole = (CustomRoles)__instance.Role.Role - 1000;
-                PlayerControl.LocalPlayer.RpcSetCustomRole(FileCustomRole);
+                PlayerControl.LocalPlayer.RpcSetCustomRole(FileCustomRole, false, false);
                 PlayerControl.LocalPlayer.RpcSetRole(FileCustomRole.GetRoleTypes(), true);
                 return false;
             }
@@ -104,4 +104,4 @@ public static bool Prefix(TaskAddButton __instance)
         catch { }
         return true;
     }
-}
\ No newline at end of file
+}
diff --git a/Patches/onGameStartedPatch.cs b/Patches/onGameStartedPatch.cs
index 9a8d6c2b29..b5854df0a7 100644
--- a/Patches/onGameStartedPatch.cs
+++ b/Patches/onGameStartedPatch.cs
@@ -446,7 +446,7 @@ public static System.Collections.IEnumerator AssignRoles()
             {
                 foreach (var pair in RoleAssign.RoleResult)
                 {
-                    pair.Key.GetPlayer()?.RpcSetCustomRole(pair.Value, checkAddons: false);
+                    pair.Key.GetPlayer()?.RpcSetCustomRole(pair.Value, false, false);
                 }
                 goto EndOfSelectRolePatch;
             }
diff --git a/Roles/AddOns/Common/Oiiai.cs b/Roles/AddOns/Common/Oiiai.cs
index e77c4d9cf6..40ef780953 100644
--- a/Roles/AddOns/Common/Oiiai.cs
+++ b/Roles/AddOns/Common/Oiiai.cs
@@ -106,7 +106,6 @@ public static void OnMurderPlayer(PlayerControl killer, PlayerControl target)
             Main.DesyncPlayerList.Remove(killer.PlayerId);
             killer.GetRoleClass().OnAdd(killer.PlayerId);
             killer.RpcSetCustomRole(CustomRoles.Enchanted);
-            killer.AddInSwitchAddons(killer, CustomRoles.Enchanted);
             Logger.Info($"Oiiai {killer.GetNameWithRole().RemoveHtmlTags()} with Coven without Necronomicon.", "Oiiai");
         }
         else if (CovenManager.HasNecronomicon(killer))
@@ -122,7 +121,6 @@ public static void OnMurderPlayer(PlayerControl killer, PlayerControl target)
             Main.DesyncPlayerList.Remove(killer.PlayerId);
             killer.GetRoleClass().OnAdd(killer.PlayerId);
             killer.RpcSetCustomRole(CustomRoles.Madmate);
-            killer.AddInSwitchAddons(killer, CustomRoles.Madmate);
             Logger.Info($"Oiiai {killer.GetNameWithRole().RemoveHtmlTags()} with Madmates assign.", "Oiiai");
         }
         else if (killer.Is(CustomRoles.Sidekick))
@@ -133,7 +131,6 @@ public static void OnMurderPlayer(PlayerControl killer, PlayerControl target)
             Main.DesyncPlayerList.Remove(killer.PlayerId);
             killer.GetRoleClass().OnAdd(killer.PlayerId);
             killer.RpcSetCustomRole(CustomRoles.Recruit);
-            killer.AddInSwitchAddons(killer, CustomRoles.Recruit);
             Logger.Info($"Oiiai {killer.GetNameWithRole().RemoveHtmlTags()} with Sidekicks assign.", "Oiiai");
         }
         else if (!killerRole.IsNeutral())
diff --git a/Roles/AddOns/Crewmate/Workhorse.cs b/Roles/AddOns/Crewmate/Workhorse.cs
index fa9b81f6fb..3d09e0193e 100644
--- a/Roles/AddOns/Crewmate/Workhorse.cs
+++ b/Roles/AddOns/Crewmate/Workhorse.cs
@@ -79,7 +79,7 @@ public static bool OnAddTask(PlayerControl pc)
         if (!CustomRoles.Workhorse.IsEnable() || playerIdList.Count >= CustomRoles.Workhorse.GetCount()) return true;
         if (!IsAssignTarget(pc)) return true;
 
-        pc.RpcSetCustomRole(CustomRoles.Workhorse);
+        pc.RpcSetCustomRole(CustomRoles.Workhorse, false, false);
         var taskState = pc.GetPlayerTaskState();
         taskState.AllTasksCount += NumLongTasks + NumShortTasks;
         //taskState.CompletedTasksCount++; //Addition for this completion
diff --git a/Roles/Coven/MoonDancer.cs b/Roles/Coven/MoonDancer.cs
index 755e940934..951ad8f287 100644
--- a/Roles/Coven/MoonDancer.cs
+++ b/Roles/Coven/MoonDancer.cs
@@ -228,9 +228,11 @@ private static void DistributeAddOns(PlayerControl md)
                 continue;
             }
 
+            player.CheckConflictedAddOnsFromList(ref addons);
+
             var addon = addons.RandomElement();
-            var helpful = GroupedAddons[AddonTypes.Helpful].Where(x => addons.Contains(x)).ToList();
-            var harmful = GroupedAddons[AddonTypes.Harmful].Where(x => addons.Contains(x)).ToList();
+            var helpful = GroupedAddons[AddonTypes.Helpful].Where(addons.Contains).ToList();
+            var harmful = GroupedAddons[AddonTypes.Harmful].Where(addons.Contains).ToList();
             if (player.GetCustomRole().IsCovenTeam() || (player.Is(CustomRoles.Lovers) && md.Is(CustomRoles.Lovers)))
             {
                 if (helpful.Count <= 0)
@@ -251,9 +253,12 @@ private static void DistributeAddOns(PlayerControl md)
                 }
                 addon = harmful.RandomElement();
             }
-            player.RpcSetCustomRole(addon);
-            player.AddInSwitchAddons(player, addon);
-            Logger.Info("Addon Passed.", "MoonDancer");
+
+            if (addon != 0) // not default value
+            {
+                player.RpcSetCustomRole(addon, false, false);
+                Logger.Info("Addon Passed.", "MoonDancer");
+            }
         }
         BatonPassList[md.PlayerId].Clear();
     }
diff --git a/Roles/Coven/Necromancer.cs b/Roles/Coven/Necromancer.cs
index 0e19b20c5c..d6dad902e7 100644
--- a/Roles/Coven/Necromancer.cs
+++ b/Roles/Coven/Necromancer.cs
@@ -143,7 +143,7 @@ public override void UnShapeShiftButton(PlayerControl nm)
         }
         var role = deadRoles.RandomElement();
         nm.RpcChangeRoleBasis(role);
-        nm.RpcSetCustomRole(role, checkAddons: false);
+        nm.RpcSetCustomRole(role, false, false);
         nm.GetRoleClass()?.OnAdd(nm.PlayerId);
         nm.SyncSettings();
         Dictionary<byte, List<CustomRoles>> CurrentAddons = new();
@@ -179,10 +179,10 @@ private static void RevertRole(PlayerControl nm, CustomRoles role)
         }
         if (nm.IsAlive())
             nm.RpcChangeRoleBasis(CustomRoles.Necromancer);
-        nm.RpcSetCustomRole(CustomRoles.Necromancer, checkAddons: false);
+        nm.RpcSetCustomRole(CustomRoles.Necromancer, false, false);
         foreach (var addon in OldAddons[nm.PlayerId])
         {
-            nm.RpcSetCustomRole(addon, checkAddons: false);
+            nm.RpcSetCustomRole(addon, false, false);
         }
         OldAddons[nm.PlayerId].Clear();
         nm.ResetKillCooldown();
diff --git a/Roles/Coven/Sacrifist.cs b/Roles/Coven/Sacrifist.cs
index f09b257814..8f779bab50 100644
--- a/Roles/Coven/Sacrifist.cs
+++ b/Roles/Coven/Sacrifist.cs
@@ -174,7 +174,7 @@ public override void UnShapeShiftButton(PlayerControl pc)
                     break;
                 // Cant Fix Sabotage (not coding allat, just give them Fool)
                 case 3:
-                    GetPlayerById(sacrifist).RpcSetCustomRole(CustomRoles.Fool);
+                    pc.RpcSetCustomRole(CustomRoles.Fool);
                     randPlayerPC.RpcSetCustomRole(CustomRoles.Fool);
                     Logger.Info($"{pc.GetRealName()} Gave Fool to {randPlayerPC.GetRealName()} and self", "Sacrifist");
                     pc.Notify(GetString("SacrifistFoolDebuff"), 5f);
diff --git a/Roles/Crewmate/Cleanser.cs b/Roles/Crewmate/Cleanser.cs
index 4d70fd6967..b7276c27e8 100644
--- a/Roles/Crewmate/Cleanser.cs
+++ b/Roles/Crewmate/Cleanser.cs
@@ -101,17 +101,15 @@ public override void AfterMeetingTasks()
         foreach (var pid in CleanserTarget.Keys.ToArray())
         {
             if (pid == byte.MaxValue) continue;
+            
             var targetid = CleanserTarget[pid];
             if (targetid == byte.MaxValue) continue;
-            var targetpc = Utils.GetPlayerById(targetid);
+            
+            var targetpc = targetid.GetPlayer();
             if (targetpc == null) continue;
-            //var allAddons = targetpc.GetCustomSubRoles();
-            targetpc.RpcSetCustomRole(CustomRoles.Cleansed);
+
+            targetpc.RpcSetCustomRole(CustomRoles.Cleansed, false, false);
             Logger.Info($"Removed all the add ons of {targetpc.GetNameWithRole()}", "Cleanser");
-            //foreach (var role in allAddons)
-            //{
-            //    Main.PlayerStates[targetid].RemoveSubRole(role);
-            //}
         }
         Utils.MarkEveryoneDirtySettings();
     }
diff --git a/Roles/Crewmate/CopyCat.cs b/Roles/Crewmate/CopyCat.cs
index fe6350e817..ab13e6c732 100644
--- a/Roles/Crewmate/CopyCat.cs
+++ b/Roles/Crewmate/CopyCat.cs
@@ -69,7 +69,7 @@ public static void UnAfterMeetingTasks()
             {
                 if (!pc.HasGhostRole())
                 {
-                    pc.RpcSetCustomRole(CustomRoles.CopyCat);
+                    pc.RpcSetCustomRole(CustomRoles.CopyCat, false, false);
                 }
                 continue;
             }
@@ -82,10 +82,10 @@ public static void UnAfterMeetingTasks()
                 {
                     pc.GetRoleClass()?.OnRemove(pc.PlayerId);
                     pc.RpcChangeRoleBasis(CustomRoles.CopyCat);
-                    pc.RpcSetCustomRole(CustomRoles.CopyCat, checkAddons: false);
+                    pc.RpcSetCustomRole(CustomRoles.CopyCat, false, false);
                     foreach (var addon in OldAddons[pc.PlayerId])
                     {
-                        pc.RpcSetCustomRole(addon, checkAddons: false);
+                        pc.RpcSetCustomRole(addon, false, false);
                     }
                 }
             }
@@ -158,7 +158,7 @@ CustomRoles.PotionMaster when PotionMaster.CurrentPotion() is 1 => CustomRoles.M
             if (role != CustomRoles.CopyCat)
             {
                 killer.RpcChangeRoleBasis(role);
-                killer.RpcSetCustomRole(role, checkAddons: false);
+                killer.RpcSetCustomRole(role, false, false);
                 killer.GetRoleClass()?.OnAdd(killer.PlayerId);
                 killer.SyncSettings();
                 Dictionary<byte, List<CustomRoles>> CurrentAddons = new();
diff --git a/Roles/Crewmate/Merchant.cs b/Roles/Crewmate/Merchant.cs
index 0f6934a7e8..e207f46828 100644
--- a/Roles/Crewmate/Merchant.cs
+++ b/Roles/Crewmate/Merchant.cs
@@ -169,12 +169,10 @@ public override bool OnTaskComplete(PlayerControl player, int completedTaskCount
 
             PlayerControl target = AllAlivePlayer.RandomElement();
 
-            target.RpcSetCustomRole(addon);
+            target.RpcSetCustomRole(addon, false, false);
             target.Notify(Utils.ColorString(Utils.GetRoleColor(CustomRoles.Merchant), GetString("MerchantAddonSell")));
             player.Notify(Utils.ColorString(Utils.GetRoleColor(CustomRoles.Merchant), GetString("MerchantAddonDelivered")));
 
-            target.AddInSwitchAddons(target, addon);
-
             addonsSold[player.PlayerId] += 1;
         }
         else
diff --git a/Roles/Crewmate/TaskManager.cs b/Roles/Crewmate/TaskManager.cs
index 8f6c49d6c3..70b1c25a47 100644
--- a/Roles/Crewmate/TaskManager.cs
+++ b/Roles/Crewmate/TaskManager.cs
@@ -108,15 +108,9 @@ public override void OnOthersTaskComplete(PlayerControl player, PlayerTask task,
         if (realPlayer.PlayerId == _Player.PlayerId || !realPlayer.GetPlayerTaskState().IsTaskFinished || abilityLimit < 1) return;
 
         var taskManager = _Player;
-        Addons.RemoveAll(taskManager.Is);
 
-        foreach (var addOn in Addons)
-        {
-            if (!CustomRolesHelper.CheckAddonConfilct(addOn, taskManager, checkLimitAddons: false, checkSelfAddOn: false))
-            {
-                Addons.Remove(addOn);
-            }
-        }
+        Addons.RemoveAll(taskManager.Is);
+        taskManager.CheckConflictedAddOnsFromList(ref Addons);
 
         if (Addons.Count == 0)
         {
@@ -128,7 +122,7 @@ public override void OnOthersTaskComplete(PlayerControl player, PlayerTask task,
             taskManager.RpcRemoveAbilityUse();
             var randomAddOn = Addons.RandomElement();
 
-            taskManager.RpcSetCustomRole(randomAddOn, checkAAconflict: false);
+            taskManager.RpcSetCustomRole(randomAddOn, false, false);
             taskManager.Notify(string.Format(GetString("TaskManager_YouGetAddon"), abilityLimit), time: 10);
         }
     }
diff --git a/Roles/Neutral/Bandit.cs b/Roles/Neutral/Bandit.cs
index 685dc70ffc..928a5d070d 100644
--- a/Roles/Neutral/Bandit.cs
+++ b/Roles/Neutral/Bandit.cs
@@ -68,22 +68,24 @@ public override void SetKillCooldown(byte id)
     public override bool CanUseSabotage(PlayerControl pc) => CanUsesSabotage.GetBool();
     public override bool CanUseKillButton(PlayerControl pc) => true;
 
-    private static CustomRoles? SelectRandomAddon(PlayerControl Target)
+    private static CustomRoles? SelectRandomAddon(PlayerControl killer, PlayerControl Target)
     {
         if (!AmongUsClient.Instance.AmHost) return null;
-        var AllSubRoles = Main.PlayerStates[Target.PlayerId].SubRoles.ToList();
-        for (int i = AllSubRoles.Count - 1; i >= 0; i--)
+
+        var AllSubRoles = Target.GetCustomSubRoles().ToList();
+        killer.CheckConflictedAddOnsFromList(ref AllSubRoles);
+
+        foreach (var subRole in AllSubRoles)
         {
-            var role = AllSubRoles[i];
-            if (role == CustomRoles.Cleansed || // making Bandit unable to steal Cleansed for obvious reasons. Although it can still be cleansed by cleanser.
-                role == CustomRoles.LastImpostor ||
-                role == CustomRoles.Lovers || // Causes issues involving Lovers Suicide
-                (role.IsImpOnlyAddon() && !CanStealImpOnlyAddon.GetBool()) ||
-                (role == CustomRoles.Nimble && CanVent.GetBool()) ||
-                ((role.IsBetrayalAddon() || role is CustomRoles.Lovers) && !CanStealBetrayalAddon.GetBool()))
+            if (subRole is CustomRoles.Cleansed or // making Bandit unable to steal Cleansed for obvious reasons. Although it can still be cleansed by cleanser.
+                CustomRoles.LastImpostor or
+                CustomRoles.Lovers or // Causes issues involving Lovers Suicide
+                CustomRoles.Nimble && CanVent.GetBool()
+                || (subRole.IsImpOnlyAddon() && !CanStealImpOnlyAddon.GetBool())
+                || ((subRole.IsBetrayalAddon() || subRole is CustomRoles.Lovers) && !CanStealBetrayalAddon.GetBool()))
             {
-                Logger.Info($"Removed {role} from list of stealable addons", "Bandit");
-                AllSubRoles.Remove(role);
+                Logger.Info($"Removed {subRole} from list of stealable addons", "Bandit");
+                AllSubRoles.Remove(subRole);
             }
         }
 
@@ -120,14 +122,14 @@ public override void ReceiveRPC(MessageReader reader, PlayerControl pc)
     }
     private void StealAddon(PlayerControl killer, PlayerControl target, CustomRoles? SelectedAddOn)
     {
-        target.AddInSwitchAddons(killer, IsAddon: SelectedAddOn);
+        target.AddInSwitchAddons(killer, SelectedAddOn ?? CustomRoles.NotAssigned);
 
         if (StealMode.GetValue() == 1)
         {
             Main.PlayerStates[target.PlayerId].RemoveSubRole((CustomRoles)SelectedAddOn);
             Logger.Info($"Successfully removed {SelectedAddOn} addon from {target.GetNameWithRole()}", "Bandit");
 
-            killer.RpcSetCustomRole((CustomRoles)SelectedAddOn);
+            killer.RpcSetCustomRole((CustomRoles)SelectedAddOn, false, false);
             Logger.Info($"Successfully Added {SelectedAddOn} addon to {killer.GetNameWithRole()}", "Bandit");
         }
         else
@@ -155,7 +157,7 @@ public override bool OnCheckMurderAsKiller(PlayerControl killer, PlayerControl t
         bool flag = false;
         if (!target.HasSubRole() || target.Is(CustomRoles.Stubborn)) flag = true;
 
-        var SelectedAddOn = SelectRandomAddon(target);
+        var SelectedAddOn = SelectRandomAddon(killer, target);
         if (SelectedAddOn == null || flag) // no stealable addons found on the target.
         {
             killer.Notify(Translator.GetString("Bandit_NoStealableAddons"));
@@ -203,7 +205,7 @@ public override void OnReportDeadBody(PlayerControl reportash, NetworkedPlayerIn
             Logger.Info($"Successfully removed {role} addon from {target.GetNameWithRole()}", "Bandit");
             SendRPC(targetId, role, true);
 
-            _Player.RpcSetCustomRole(role);
+            _Player.RpcSetCustomRole(role, false, false);
             Logger.Info($"Successfully Added {role} addon to {_Player?.GetNameWithRole()}", "Bandit");
         }
     }


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## PR Overview

This PR recodes the removal of incompatible add-ons by introducing asynchronous methods to remove and check add-ons in mid-game while refactoring related RPC and add-on handling calls. Key changes include:
- Replacing the synchronous RemoveIncompatibleAddOns method with two async coroutines: RemoveIncompatibleAddOnsAsync and CheckAndAssignAddOnAsync.
- Updating several RpcSetCustomRole calls to include additional boolean parameters.
- Adjusting the AddInSwitchAddons method signature for clearer parameter naming.

### Reviewed Changes

| File | Description |
| ---- | ----------- |
| Modules/ExtendedPlayerControl.cs | Introduces async methods for removing incompatible add-ons and refactors role assignment by separating add-on conflict checks. |
| Roles/Coven/MoonDancer.cs | Updates add-on distribution to use the new RpcSetCustomRole signature and streamlines LINQ usage. |
| Roles/Neutral/Bandit.cs | Adds a killer parameter to select a random add-on and updates corresponding Rpc calls. |
| Roles/Crewmate/TaskManager.cs | Replaces inline role assignment calls with the updated RpcSetCustomRole signature and adds an add-on conflict check. |
| Modules/CustomRolesHelper.cs | Modifies the CheckAddonConfilct function to use a checkConditions parameter instead of checkSelfAddOn for consistency. |
| Other Files (Cleanser.cs, Oiiai.cs, onGameStartedPatch.cs, CopyCat.cs, Necromancer.cs, Merchant.cs, TaskAdderPatch.cs, Workhorse.cs, Sacrifist.cs) | Updates RpcSetCustomRole and related calls to conform to the new method signature. |

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-01T09:41:32Z
user: login: Tommy-XL
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-01T09:42:09Z
user: login: NikoCat233
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-01T13:29:07Z

Review comments:
diff_hunk: @@ -57,20 +67,56 @@ public static void RpcSetCustomRole(byte PlayerId, CustomRoles role)
             AmongUsClient.Instance.FinishRpcImmediately(writer);
         }
     }
-    public static void RemoveIncompatibleAddOns(this PlayerControl player)
+    public static void CheckConflictedAddOnsFromList(this PlayerControl player, ref List<CustomRoles> addOnList)
+    {
+        List<CustomRoles> confilctedAddOns = [];
path: Modules/ExtendedPlayerControl.cs
user: login: Copilot
body: The variable 'confilctedAddOns' is misspelled. Consider renaming it to 'conflictedAddOns' for clarity and consistency.
```suggestion
        List<CustomRoles> conflictedAddOns = [];
```
created_at: 2025-03-01T09:41:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,20 +67,56 @@ public static void RpcSetCustomRole(byte PlayerId, CustomRoles role)
             AmongUsClient.Instance.FinishRpcImmediately(writer);
         }
     }
-    public static void RemoveIncompatibleAddOns(this PlayerControl player)
+    public static void CheckConflictedAddOnsFromList(this PlayerControl player, ref List<CustomRoles> addOnList)
+    {
+        List<CustomRoles> confilctedAddOns = [];
+        foreach (var addon in addOnList)
+        {
+            if (!CustomRolesHelper.CheckAddonConfilct(addon, player, checkLimitAddons: false))
+            {
+                confilctedAddOns.Add(addon);
+            }
+        }
+        foreach (var removeAddOns in confilctedAddOns.ToArray())
+        {
+            Logger.Msg($"{removeAddOns} have conflict, remove from list", $"{player.GetCustomRole()}");
+            addOnList.Remove(removeAddOns);
+        }
+    }
+    private static System.Collections.IEnumerator RemoveIncompatibleAddOnsAsync(this PlayerControl player)
     {
-        var roles = player.GetCustomSubRoles().ToList();
-        roles = roles.Where(x => !x.IsAddonAssignedMidGame()).ToList();
-        roles.Shuffle();
+        var currentAddOns = player.GetCustomSubRoles().Where(x => !x.IsAddonAssignedMidGame()).ToArray();
+
+        yield return new WaitForSeconds(0.1f);
 
path: Modules/ExtendedPlayerControl.cs
user: login: Copilot
body: [nitpick] Magic numbers (0.1f and 0.2f in the async methods) are hard-coded. It is recommended to extract these values into named constants to improve maintainability.
```suggestion
        yield return new WaitForSeconds(WAIT_TIME_SHORT);
```
created_at: 2025-03-01T09:41:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,20 +67,56 @@ public static void RpcSetCustomRole(byte PlayerId, CustomRoles role)
             AmongUsClient.Instance.FinishRpcImmediately(writer);
         }
     }
-    public static void RemoveIncompatibleAddOns(this PlayerControl player)
+    public static void CheckConflictedAddOnsFromList(this PlayerControl player, ref List<CustomRoles> addOnList)
+    {
+        List<CustomRoles> confilctedAddOns = [];
path: Modules/ExtendedPlayerControl.cs
user: login: Tommy-XL
body: lol
created_at: 2025-03-01T09:42:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: Tommy-XL
created_at: 2025-02-28T15:30:11Z
body: Also did `CheckConflictedAddOnsFromList` for remove conflicted add-ons from list
author_association: CONTRIBUTOR
user: login: Tommy-XL
created_at: 2025-02-28T15:50:46Z
body: Although, I thought it was better to add this for Alpha 2 rather than make 2.2.1 and get unexpected problems
author_association: CONTRIBUTOR
user: login: Tommy-XL
created_at: 2025-03-01T09:39:35Z
body: @NikoCat233 Check this PR with Copilot
author_association: CONTRIBUTOR
user: login: NikoCat233
created_at: 2025-03-01T09:42:23Z
body: Copilot can't do everything lol
author_association: COLLABORATOR