using System; using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; using Hazel; using UnityEngine; namespace EHR.Modules; internal static class CustomTeamManager { public static HashSet CustomTeams = []; public static HashSet EnabledCustomTeams = []; public static CustomTeam WinnerTeam; public static Dictionary> CustomTeamPlayerIds = []; public static List CustomTeamOptions = []; public static Dictionary> CustomTeamOptionsCache = []; public static void LoadCustomTeams() { try { if (!File.Exists($"{Main.DataPath}/EHR_DATA/CTA_Data.txt")) return; CustomTeams = File.ReadAllLines($"{Main.DataPath}/EHR_DATA/CTA_Data.txt").Select(x => new CustomTeam(x)).ToHashSet(); RefreshCustomOptions(); } catch (Exception e) { Utils.ThrowException(e); } } private static void RefreshCustomOptions() { CustomTeamOptions.Clear(); EnabledCustomTeams.Clear(); const int startId = 660000; const TabGroup tab = TabGroup.GameSettings; CustomTeamOptions = CustomTeams.Select((x, i) => CreateSetting(x, startId + 20 * i)).ToList(); UpdateEnabledTeams(); return; static CustomTeamOptionGroup CreateSetting(CustomTeam team, int id) { var enabled = new BooleanOptionItem(id++, "CTA.FLAG" + team.TeamName, true, tab); var knowRoles = new BooleanOptionItem(id++, "CTA.KnowRoles", true, tab); var winWithOriginalTeam = new BooleanOptionItem(id++, "CTA.WinWithOriginalTeam", false, tab); var originalWinCondition = new BooleanOptionItem(id++, "CTA.OriginalWinCondition", true, tab); var killEachOther = new BooleanOptionItem(id++, "CTA.KillEachOther", false, tab); var guessEachOther = new BooleanOptionItem(id++, "CTA.GuessEachOther", false, tab); var arrows = new BooleanOptionItem(id++, "CTA.Arrows", true, tab); var maxPlayers = new IntegerOptionItem(id++, "CTA.MaxPlayersAssignedToTeam", new(0, 15, 1), 15, tab); var teamPlayerCounts = new Dictionary(); foreach (Team teamType in EnumHelper.GetValues()[1..]) { var min = new IntegerOptionItem(id++, "CTA.MinPlayers." + teamType, new(0, 15, 1), 1, tab); var max = new IntegerOptionItem(id++, "CTA.MaxPlayers." + teamType, new(0, 15, 1), 15, tab); teamPlayerCounts[teamType] = [min, max]; } CustomTeamOptionGroup group = new(team, enabled, knowRoles, winWithOriginalTeam, originalWinCondition, killEachOther, guessEachOther, arrows, maxPlayers, teamPlayerCounts); group.AllOptions.Skip(1).Do(x => x.SetParent(enabled)); group.AllOptions.ForEach(x => x.SetColor(new Color32(215, 227, 84, byte.MaxValue))); group.AllOptions.ForEach(x => x.SetGameMode(CustomGameMode.Standard)); if (ColorUtility.TryParseHtmlString(team.RoleRevealScreenBackgroundColor, out Color color)) enabled.SetColor(color); enabled.RegisterUpdateValueEvent((_, _, _) => UpdateEnabledTeams()); return group; } } private static void UpdateEnabledTeams() { EnabledCustomTeams.Clear(); CustomTeamOptionsCache.Clear(); foreach (CustomTeamOptionGroup customTeamOptionGroup in CustomTeamOptions) { if (customTeamOptionGroup.Enabled.GetBool()) EnabledCustomTeams.Add(customTeamOptionGroup.Team); CustomTeamOptionsCache[customTeamOptionGroup.Team] = customTeamOptionGroup.AllOptions.ConvertAll(x => x.GetBool()); } } public static void InitializeCustomTeamPlayers() { WinnerTeam = null; CustomTeamPlayerIds.Clear(); UpdateEnabledTeams(); if (EnabledCustomTeams.Count == 0) return; var aapc = Main.CachedAlivePlayerControls(); CustomTeamPlayerIds = Main.PlayerStates .IntersectBy(aapc.Select(x => x.PlayerId), x => x.Key) .GroupBy(x => EnabledCustomTeams.FirstOrDefault(t => t.TeamMembers.Contains(x.Value.MainRole)), x => x.Key) .Where(x => x.Key != null) .ToDictionary(x => x.Key, x => TakeAsManyAsSet(x, x.Key)); foreach ((CustomTeam team, HashSet players) in CustomTeamPlayerIds) { if (!IsSettingEnabledForTeam(team, CTAOption.WinWithOriginalTeam)) { foreach (byte id in players) { if (Main.PlayerStates.TryGetValue(id, out PlayerState ps)) ps.countTypes = CountTypes.CustomTeam; } } if (!IsSettingEnabledForTeam(team, CTAOption.Arrows)) continue; foreach (byte player in players) { foreach (byte target in players) { if (player == target) continue; TargetArrow.Add(player, target); } } } var imps = aapc.Where(x => x.IsImpostor() && !CustomTeamPlayerIds.Values.Any(l => l.Contains(x.PlayerId))).ToList(); if (imps.Count > 0) { CustomTeamPlayerIds.Values.Flatten().ToValidPlayers().DoIf(x => x != null && x.IsImpostor() && !IsSettingEnabledForPlayerTeam(x.PlayerId, CTAOption.WinWithOriginalTeam), ctp => { imps.ForEach(imp => ctp.RpcSetRoleDesync(RoleTypes.Crewmate, imp.OwnerId, setRoleMap: true)); var sender = CustomRpcSender.Create("CustomTeamManager_SetImpostorDesync", SendOption.Reliable); imps.ForEach(imp => sender.RpcSetRole(imp, RoleTypes.Crewmate, ctp.OwnerId, changeRoleMap: true)); sender.SendMessage(); }); } if (Utils.DoRPC) { var msg = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.CTA, SendOption.Reliable); msg.WritePacked(1); msg.WritePacked(EnabledCustomTeams.Count); EnabledCustomTeams.Do(x => x.Serialize(msg)); AmongUsClient.Instance.FinishRpcImmediately(msg); msg = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.CTA, SendOption.Reliable); msg.WritePacked(2); msg.WritePacked(CustomTeamPlayerIds.Count); foreach ((CustomTeam customTeam, HashSet ids) in CustomTeamPlayerIds) { msg.WritePacked(ids.Count); ids.Do(x => msg.Write(x)); msg.Write(customTeam.TeamName); } AmongUsClient.Instance.FinishRpcImmediately(msg); msg = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.CTA, SendOption.Reliable); msg.WritePacked(3); msg.WritePacked(CustomTeamOptionsCache.Count); foreach ((CustomTeam customTeam, List bools) in CustomTeamOptionsCache) { msg.WritePacked(bools.Count); bools.ForEach(x => msg.Write(x)); msg.Write(customTeam.TeamName); } AmongUsClient.Instance.FinishRpcImmediately(msg); } return; static HashSet TakeAsManyAsSet(IEnumerable playerIds, CustomTeam customTeam) { try { CustomTeamOptionGroup options = CustomTeamOptions.First(x => x.Team.Equals(customTeam)); Dictionary> grouped = playerIds.ToValidPlayers().GroupBy(x => x.GetTeam()).ToDictionary(x => x.Key, x => x.Shuffle()); foreach ((Team team, List players) in grouped) { try { if (!options.TeamPlayerCounts.TryGetValue(team, out IntegerOptionItem[] counts) || counts.Length < 2) continue; int num = IRandom.Instance.Next(counts[0].GetInt(), counts[1].GetInt() + 1); if (players.Count <= num) continue; List selected = players.Take(num).Shuffle(); players.Clear(); players.AddRange(selected); } catch (Exception e) { Utils.ThrowException(e); } } return grouped.Values.Flatten().Select(x => x.PlayerId).Take(options.MaxPlayers.GetInt()).ToHashSet(); } catch (Exception e) { Utils.ThrowException(e); return []; } } } public static string GetSuffix(PlayerControl seer) { if (EnabledCustomTeams.Count == 0 || Main.HasJustStarted || !IsSettingEnabledForPlayerTeam(seer.PlayerId, CTAOption.Arrows)) return string.Empty; return CustomTeamPlayerIds[GetCustomTeam(seer.PlayerId)].Aggregate(string.Empty, (s, id) => s + Utils.ColorString(Main.PlayerColors.GetValueOrDefault(id, Color.white), TargetArrow.GetArrows(seer, id))); } public static void ReceiveRPC(MessageReader reader) { switch (reader.ReadPackedInt32()) { case 1: { EnabledCustomTeams.Clear(); Loop.Times(reader.ReadPackedInt32(), _ => EnabledCustomTeams.Add(new CustomTeam(reader.ReadString()))); break; } case 2: { CustomTeamPlayerIds.Clear(); Loop.Times(reader.ReadPackedInt32(), _ => { HashSet ids = []; Loop.Times(reader.ReadPackedInt32(), _ => ids.Add(reader.ReadByte())); string teamName = reader.ReadString(); CustomTeamPlayerIds[EnabledCustomTeams.First(x => x.TeamName == teamName)] = ids; }); break; } case 3: { CustomTeamOptionsCache.Clear(); Loop.Times(reader.ReadPackedInt32(), _ => { List bools = []; Loop.Times(reader.ReadPackedInt32(), _ => bools.Add(reader.ReadBoolean())); string teamName = reader.ReadString(); CustomTeamOptionsCache[EnabledCustomTeams.First(x => x.TeamName == teamName)] = bools; }); break; } } } private static readonly List AliveTeamPlayers = []; private static readonly List ToRemove = []; public static bool CheckCustomTeamGameEnd() { if (EnabledCustomTeams.Count == 0 || CustomTeamPlayerIds.Count == 0) return false; try { var aapc = Main.CachedAlivePlayerControls(); int aliveCount = aapc.Count; if (aliveCount == 1) { PlayerControl lastPlayer = aapc[0]; CustomTeam lastTeam = GetCustomTeam(lastPlayer.PlayerId); WinnerTeam = lastTeam; CustomWinnerHolder.SetWinnerOrAdditonalWinner(CustomWinner.CustomTeam); CustomWinnerHolder.WinnerIds = [lastPlayer.PlayerId]; return true; } foreach (var kvp in CustomTeamPlayerIds) { var originalSet = kvp.Value; ToRemove.Clear(); foreach (var id in originalSet) { PlayerControl pc = Utils.GetPlayerById(id); if (!pc || !pc.Data || pc.Data.Disconnected) ToRemove.Add(id); } for (int i = 0; i < ToRemove.Count; i++) originalSet.Remove(ToRemove[i]); } AliveTeamPlayers.Clear(); foreach ((CustomTeam teamKey, HashSet originalSet) in CustomTeamPlayerIds) { foreach (var id in originalSet) { var pc = Utils.GetPlayerById(id); if (pc && pc.IsAlive()) { AliveTeamPlayers.Add(teamKey); break; } } } if (AliveTeamPlayers.Count == 1) { var onlyTeam = AliveTeamPlayers[0]; for (int i = 0; i < aapc.Count; i++) { PlayerControl player = aapc[i]; CustomTeam playerTeam = GetCustomTeam(player.PlayerId); if (playerTeam == null || !Equals(playerTeam, onlyTeam)) return false; } WinnerTeam = onlyTeam; CustomWinnerHolder.SetWinnerOrAdditonalWinner(CustomWinner.CustomTeam); CustomWinnerHolder.WinnerIds = CustomTeamPlayerIds[onlyTeam]; return true; } } catch (Exception e) { Logger.Error($"Error in CheckCustomTeamGameEnd: {e}", "CustomTeamManager"); } return false; } public static CustomTeam GetCustomTeam(byte id) { foreach ((CustomTeam team, HashSet ids) in CustomTeamPlayerIds) { if (ids.Contains(id)) return team; } return null; } public static bool ArentInCustomTeam(params List ids) { if (EnabledCustomTeams.Count == 0 || CustomTeamPlayerIds.Count == 0) return true; return ids.TrueForAll(x => GetCustomTeam(x) == null); } public static bool AreInSameCustomTeam(byte id1, byte id2) { if (EnabledCustomTeams.Count == 0) return false; CustomTeam team1 = GetCustomTeam(id1); CustomTeam team2 = GetCustomTeam(id2); return team1 != null && team2 != null && team1.Equals(team2); } public static bool IsSettingEnabledForPlayerTeam(byte id, CTAOption setting) { CustomTeam team = GetCustomTeam(id); return team != null && IsSettingEnabledForTeam(team, setting); } public static bool IsSettingEnabledForTeam(CustomTeam team, CTAOption setting) { return CustomTeamOptionsCache[team][(int)setting]; } internal class CustomTeam { public readonly string RoleRevealScreenBackgroundColor; public readonly string RoleRevealScreenSubtitle; public readonly string RoleRevealScreenTitle; public readonly string TeamName; public CustomTeam(string line) { try { string[] parts = line.Split(';'); TeamName = parts[0]; RoleRevealScreenTitle = parts[1]; RoleRevealScreenSubtitle = parts[2]; RoleRevealScreenBackgroundColor = parts[3]; TeamMembers = parts[4].Split(',').Select(x => Enum.Parse(x, true)).ToList(); CustomTeams.Add(this); } catch (Exception e) { Utils.ThrowException(e); } } public List TeamMembers { get; } = []; public override bool Equals(object obj) { if (obj is not CustomTeam team) return false; return TeamName == team.TeamName; } public override int GetHashCode() { return TeamName.GetHashCode(); } public void Serialize(MessageWriter writer) { StringBuilder sb = new(); sb.Append(TeamName); sb.Append(';'); sb.Append(RoleRevealScreenTitle); sb.Append(';'); sb.Append(RoleRevealScreenSubtitle); sb.Append(';'); sb.Append(RoleRevealScreenBackgroundColor); sb.Append(';'); for (var index = 0; index < TeamMembers.Count; index++) { sb.Append(TeamMembers[index].ToString()); sb.Append(','); } writer.Write(sb.ToString()); } } internal class CustomTeamOptionGroup { public readonly List AllOptions; public CustomTeamOptionGroup(CustomTeam team, BooleanOptionItem enabled, BooleanOptionItem knowRoles, BooleanOptionItem winWithOriginalTeam, BooleanOptionItem originalWinCondition, BooleanOptionItem killEachOther, BooleanOptionItem guessEachOther, BooleanOptionItem arrows, IntegerOptionItem maxPlayers, Dictionary teamPlayerCounts) { AllOptions = [enabled, knowRoles, winWithOriginalTeam, originalWinCondition, killEachOther, guessEachOther, arrows, maxPlayers]; AllOptions.AddRange(teamPlayerCounts.Values.Flatten()); Team = team; Enabled = enabled; MaxPlayers = maxPlayers; TeamPlayerCounts = teamPlayerCounts; } public CustomTeam Team { get; } public BooleanOptionItem Enabled { get; } public IntegerOptionItem MaxPlayers { get; } public Dictionary TeamPlayerCounts { get; } } } public enum CTAOption { KnowRoles = 1, WinWithOriginalTeam, OriginalWinCondition, KillEachOther, GuessEachOther, Arrows }