From 692075d29673a482aa9496dd0e36cf54c5154302 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Sun, 7 Jul 2024 21:45:36 +0200 Subject: [PATCH] massive fixes, AFK detection, cooler ID showing in meetings --- EHR.sln.DotSettings | 2 + Modules/AFKDetector.cs | 140 +++++++++++++ Modules/CustomNetObject.cs | 8 +- Modules/CustomRolesHelper.cs | 16 +- Modules/ExtendedPlayerControl.cs | 19 +- Modules/Extensions/CollectionExtensions.cs | 6 +- .../PlayerGameOptionsSender.cs | 4 + Modules/GameState.cs | 1 + Modules/GhostRolesManager.cs | 18 +- Modules/GuessManager.cs | 30 ++- Modules/RPC.cs | 8 +- Modules/Utils.cs | 22 ++- Patches/ChatCommandPatch.cs | 13 +- Patches/CreateGameOptionsPatches.cs | 153 -------------- Patches/Crowded.cs | 187 ++++++++++++++++++ Patches/GameOptionsMenuPatch.cs | 103 +++++----- Patches/GameStartManagerPatch.cs | 2 +- Patches/IntroPatch.cs | 45 +++-- Patches/MeetingHudPatch.cs | 26 --- Patches/OnGameStartedPatch.cs | 70 ++++--- Patches/OutroPatch.cs | 3 +- Patches/PetActionsPatch.cs | 4 +- Patches/PlayerControlPatch.cs | 33 +++- Patches/PlayerJoinAndLeftPatch.cs | 49 ++++- Resources/Lang/en_US.json | 56 ++++-- Roles/AddOns/GhostRoles/Bloodmoon.cs | 1 + Roles/AddOns/GhostRoles/GA.cs | 1 + Roles/AddOns/GhostRoles/Haunter.cs | 1 + Roles/AddOns/GhostRoles/IGhostRole.cs | 1 + Roles/AddOns/GhostRoles/Minion.cs | 1 + Roles/AddOns/GhostRoles/Specter.cs | 1 + Roles/AddOns/GhostRoles/Warden.cs | 1 + Roles/Crewmate/Aid.cs | 3 +- Roles/Crewmate/Divinator.cs | 7 +- Roles/Crewmate/Goose.cs | 4 + Roles/Crewmate/Mediumshiper.cs | 2 +- Roles/Impostor/Penguin.cs | 26 ++- Roles/Impostor/Stealth.cs | 86 ++++---- Roles/Impostor/Swooper.cs | 8 +- Roles/Neutral/Glitch.cs | 2 +- Roles/Neutral/Vulture.cs | 2 +- main.cs | 4 +- 42 files changed, 757 insertions(+), 412 deletions(-) create mode 100644 Modules/AFKDetector.cs delete mode 100644 Patches/CreateGameOptionsPatches.cs create mode 100644 Patches/Crowded.cs diff --git a/EHR.sln.DotSettings b/EHR.sln.DotSettings index e377f4788..929413dec 100644 --- a/EHR.sln.DotSettings +++ b/EHR.sln.DotSettings @@ -2,6 +2,7 @@ xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> SOLUTION + AFK ATK AU CD @@ -12,6 +13,7 @@ FFA FFF FPS + GA GM HP HUD diff --git a/Modules/AFKDetector.cs b/Modules/AFKDetector.cs new file mode 100644 index 000000000..5c49633b3 --- /dev/null +++ b/Modules/AFKDetector.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace EHR.Modules +{ + public static class AFKDetector + { + private static OptionItem EnableDetector; + private static OptionItem ConsequenceOption; + + private static readonly Dictionary PlayerData = []; + public static readonly HashSet ExemptedPlayers = []; + public static readonly HashSet ShieldedPlayers = []; + public static int NumAFK; + + public static void SetupCustomOption() + { + new TextOptionItem(100080, "MenuTitle.AFKDetection", TabGroup.GameSettings) + .SetColor(new Color32(0, 255, 160, 255)) + .SetHeader(true); + EnableDetector = new BooleanOptionItem(90, "EnableAFKDetector", true, TabGroup.GameSettings) + .SetColor(new Color32(0, 255, 160, 255)); + ConsequenceOption = new StringOptionItem(91, "AFKConsequence", Enum.GetNames().Select(x => $"AFKConsequence.{x}").ToArray(), 0, TabGroup.GameSettings) + .SetParent(EnableDetector) + .SetColor(new Color32(0, 255, 160, 255)); + } + + public static void RecordPosition(PlayerControl pc) + { + if (!EnableDetector.GetBool() || ExemptedPlayers.Contains(pc.PlayerId)) return; + + PlayerData[pc.PlayerId] = new() + { + LastPosition = pc.Pos(), + Timer = 10f + (pc.Is(CustomRoles.Truant) ? Options.TruantWaitingTime.GetFloat() : 0f) + }; + } + + public static void OnFixedUpdate(PlayerControl pc) + { + if (!EnableDetector.GetBool() || !PlayerData.TryGetValue(pc.PlayerId, out var data)) return; + + if (Vector2.Distance(pc.Pos(), data.LastPosition) > 0.1f) + { + PlayerData.Remove(pc.PlayerId); + ShieldedPlayers.Remove(pc.PlayerId); + return; + } + + int lastTimer = (int)Math.Round(data.Timer); + data.Timer -= Time.fixedDeltaTime; + int currentTimer = (int)Math.Round(data.Timer); + + if (data.Timer <= 0f) + { + switch (data.CurrentPhase) + { + case Data.Phase.Detection: + if (!pc.IsModClient()) NumAFK++; + data.CurrentPhase = Data.Phase.Warning; + data.Timer = 15f; + break; + case Data.Phase.Warning: + data.CurrentPhase = Data.Phase.Consequence; + HandleConsequence(pc, (Consequence)ConsequenceOption.GetInt()); + break; + } + + if (Main.AllAlivePlayerControls.Length / 2 <= NumAFK) + { + Logger.SendInGame(Translator.GetString("AFKTooMany")); + PlayerData.Clear(); + } + } + + if (data.CurrentPhase == Data.Phase.Warning && lastTimer != currentTimer) + { + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); + } + } + + public static string GetSuffix(PlayerControl seer, PlayerControl target) + { + if (seer.IsAlive() && PlayerData.TryGetValue(seer.PlayerId, out var seerData) && seerData.CurrentPhase > Data.Phase.Detection) + return seerData.CurrentPhase == Data.Phase.Warning ? string.Format(Translator.GetString("AFKWarning"), (int)Math.Round(seerData.Timer)) : Translator.GetString("AFKSuffix"); + + if (target.IsAlive() && PlayerData.TryGetValue(target.PlayerId, out var targetData) && targetData.CurrentPhase > Data.Phase.Detection) + return Translator.GetString("AFKSuffix"); + + return string.Empty; + } + + private static void HandleConsequence(PlayerControl pc, Consequence consequence) + { + switch (consequence) + { + case Consequence.Shield: + ShieldedPlayers.Add(pc.PlayerId); + Utils.NotifyRoles(SpecifyTarget: pc); + break; + case Consequence.Kick: + AmongUsClient.Instance.KickPlayer(pc.GetClientId(), false); + Logger.SendInGame(string.Format(Translator.GetString("AFKKick"), pc.PlayerId.ColoredPlayerName())); + break; + case Consequence.Suicide: + pc.Suicide(PlayerState.DeathReason.AFK); + break; + case Consequence.Nothing: + Utils.NotifyRoles(SpecifyTarget: pc); + break; + } + + PlayerData.Remove(pc.PlayerId); + } + + enum Consequence + { + Nothing, + Shield, + Suicide, + Kick + } + + class Data + { + public enum Phase + { + Detection, + Warning, + Consequence + } + + public Vector2 LastPosition { get; init; } + public float Timer { get; set; } + public Phase CurrentPhase { get; set; } + } + } +} \ No newline at end of file diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index 79cad3818..78f8290c9 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -348,7 +348,7 @@ namespace EHR } } - public static void FixedUpdate() => AllObjects.ForEach(x => x.OnFixedUpdate()); + public static void FixedUpdate() => AllObjects.ToArray().Do(x => x.OnFixedUpdate()); public static CustomNetObject Get(int id) => AllObjects.FirstOrDefault(x => x.Id == id); public static void Reset() @@ -395,6 +395,8 @@ namespace EHR protected override void OnFixedUpdate() { + base.OnFixedUpdate(); + Timer += Time.deltaTime; if (Timer >= Duration / 5f && Frame == 0) { @@ -448,6 +450,8 @@ namespace EHR protected override void OnFixedUpdate() { + base.OnFixedUpdate(); + Timer += Time.deltaTime; if (Timer >= WaitDuration * 0.75f && State == 0) { @@ -477,6 +481,8 @@ namespace EHR protected override void OnFixedUpdate() { + base.OnFixedUpdate(); + try { if (Gone) return; diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 84e1a9950..b9178947c 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -785,17 +785,7 @@ internal static class CustomRolesHelper CustomRoles.Undead or CustomRoles.Egoist; - public static bool IsImpOnlyAddon(this CustomRoles role) => role is - CustomRoles.Mare or - CustomRoles.LastImpostor or - CustomRoles.Mare or - CustomRoles.Mimic or - CustomRoles.Damocles or - CustomRoles.DeadlyQuota or - CustomRoles.Circumvent or - CustomRoles.TicketsStealer or - CustomRoles.Taskcounter or - CustomRoles.Swift; + public static bool IsImpOnlyAddon(this CustomRoles role) => Options.GroupedAddons[AddonTypes.ImpOnly].Contains(role); public static bool IsTaskBasedCrewmate(this CustomRoles role) => role is CustomRoles.Snitch or @@ -825,7 +815,7 @@ internal static class CustomRolesHelper public static bool IsNoAnimationShifter(this CustomRoles role) => role is CustomRoles.Echo; - public static bool CheckAddonConflict(CustomRoles role, PlayerControl pc) => role.IsAdditionRole() && (!Main.NeverSpawnTogetherCombos.TryGetValue(OptionItem.CurrentPreset, out var neverList) || !neverList.TryGetValue(pc.GetCustomRole(), out var bannedAddonList) || !bannedAddonList.Contains(role)) && pc.GetCustomRole() is not CustomRoles.GuardianAngelEHR and not CustomRoles.God && !pc.Is(CustomRoles.Madmate) && !pc.Is(CustomRoles.GM) && role is not CustomRoles.Lovers && !pc.Is(CustomRoles.Needy) && (!pc.HasSubRole() || pc.GetCustomSubRoles().Count < Options.NoLimitAddonsNumMax.GetInt()) && (!Options.AddonCanBeSettings.TryGetValue(role, out var o) || ((o.Imp.GetBool() || !pc.GetCustomRole().IsImpostor()) && (o.Neutral.GetBool() || !pc.GetCustomRole().IsNeutral()) && (o.Crew.GetBool() || !pc.IsCrewmate()))) && role switch + public static bool CheckAddonConflict(CustomRoles role, PlayerControl pc) => role.IsAdditionRole() && (!Main.NeverSpawnTogetherCombos.TryGetValue(OptionItem.CurrentPreset, out var neverList) || !neverList.TryGetValue(pc.GetCustomRole(), out var bannedAddonList) || !bannedAddonList.Contains(role)) && pc.GetCustomRole() is not CustomRoles.GuardianAngelEHR and not CustomRoles.God && !pc.Is(CustomRoles.Madmate) && !pc.Is(CustomRoles.GM) && role is not CustomRoles.Lovers && !pc.Is(CustomRoles.Needy) && (!pc.HasSubRole() || pc.GetCustomSubRoles().Count < Options.NoLimitAddonsNumMax.GetInt()) && (!Options.AddonCanBeSettings.TryGetValue(role, out var o) || ((o.Imp.GetBool() || !pc.GetCustomRole().IsImpostor()) && (o.Neutral.GetBool() || !pc.GetCustomRole().IsNeutral()) && (o.Crew.GetBool() || !pc.IsCrewmate()))) && (!role.IsImpOnlyAddon() || pc.IsImpostor()) && role switch { CustomRoles.Rookie when !pc.CanUseKillButton() => false, CustomRoles.Energetic when !Options.UsePets.GetBool() => false, @@ -840,7 +830,6 @@ internal static class CustomRolesHelper CustomRoles.Stressed when !pc.IsCrewmate() || pc.GetCustomRole().IsTasklessCrewmate() => false, CustomRoles.Lazy when pc.GetCustomRole().IsNeutral() || pc.IsImpostor() || (pc.GetCustomRole().IsTasklessCrewmate() && !Options.TasklessCrewCanBeLazy.GetBool()) || (pc.GetCustomRole().IsTaskBasedCrewmate() && !Options.TaskBasedCrewCanBeLazy.GetBool()) => false, CustomRoles.TicketsStealer or CustomRoles.Swift or CustomRoles.DeadlyQuota or CustomRoles.Damocles or CustomRoles.Mare when pc.Is(CustomRoles.Bomber) || pc.Is(CustomRoles.Nuker) || pc.Is(CustomRoles.BoobyTrap) || pc.Is(CustomRoles.Capitalism) => false, - CustomRoles.TicketsStealer or CustomRoles.Mimic or CustomRoles.Swift or CustomRoles.DeadlyQuota or CustomRoles.Circumvent or CustomRoles.Damocles or CustomRoles.Taskcounter or CustomRoles.Mare when !pc.IsImpostor() => false, CustomRoles.Torch when !pc.IsCrewmate() || pc.Is(CustomRoles.Bewilder) || pc.Is(CustomRoles.Sunglasses) || pc.Is(CustomRoles.GuardianAngelEHR) => false, CustomRoles.Bewilder when pc.Is(CustomRoles.Torch) || pc.Is(CustomRoles.GuardianAngelEHR) || pc.Is(CustomRoles.Sunglasses) => false, CustomRoles.Sunglasses when pc.Is(CustomRoles.Torch) || pc.Is(CustomRoles.GuardianAngelEHR) || pc.Is(CustomRoles.Bewilder) => false, @@ -968,7 +957,6 @@ internal static class CustomRolesHelper CustomRoles.Charmed or CustomRoles.Recruit or CustomRoles.Contagious or - CustomRoles.Sidekick or CustomRoles.Undead or CustomRoles.Lovers; diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index cb7ee1261..9c6887686 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -153,17 +153,16 @@ static class ExtendedPlayerControl Main.LastNotifyNames[(player.PlayerId, seer.PlayerId)] = name; Logger.Info($"Set:{player.Data?.PlayerName}:{name} for {seer.GetNameWithRole().RemoveHtmlTags()}", "RpcSetNamePrivate"); + if (seer == null || player == null) return; + var clientId = seer.GetClientId(); - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.SetName, SendOption.Reliable, clientId); - try - { - writer.Write(player.Data.NetId); - writer.Write(name); - } - finally - { - AmongUsClient.Instance.FinishRpcImmediately(writer); - } + + var sender = CustomRpcSender.Create(name: "SetNamePrivate"); + sender.AutoStartRpc(player.NetId, (byte)RpcCalls.SetName, clientId) + .Write(seer.Data.NetId) + .Write(name) + .EndRpc(); + sender.SendMessage(); } public static void RpcSetRoleDesync(this PlayerControl player, RoleTypes role, bool canOverride, int clientId) diff --git a/Modules/Extensions/CollectionExtensions.cs b/Modules/Extensions/CollectionExtensions.cs index d6c199bf0..f28ec355f 100644 --- a/Modules/Extensions/CollectionExtensions.cs +++ b/Modules/Extensions/CollectionExtensions.cs @@ -60,7 +60,7 @@ namespace EHR /// The collection to iterate over /// The action to execute for each element /// The type of the elements in the collection - public static IEnumerable Do(this IEnumerable collection, Action action) + public static void Do(this IEnumerable collection, Action action) { if (collection is List list) { @@ -69,15 +69,13 @@ namespace EHR action(list[i]); } - return collection; + return; } foreach (T element in collection) { action(element); } - - return collection; } /// diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index bce6ea830..6232d998a 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -214,6 +214,10 @@ public class PlayerGameOptionsSender(PlayerControl player) : GameOptionsSender break; } + // When impostor alert is off, and the player is a desync crewmate, set impostor alert as true + if (role.IsDesyncRole() && role.IsCrewmate() && !CrewmateVanillaRoles.NoiseMakerImpostorAlert.GetBool()) AURoleOptions.NoisemakerImpostorAlert = true; + else AURoleOptions.NoisemakerImpostorAlert = CrewmateVanillaRoles.NoiseMakerImpostorAlert.GetBool(); + if (Shifter.WasShifter.Contains(player.PlayerId) && role.IsImpostor()) opt.SetVision(true); Main.PlayerStates[player.PlayerId].Role.ApplyGameOptions(opt, player.PlayerId); diff --git a/Modules/GameState.cs b/Modules/GameState.cs index 74f691cb9..93c7037b7 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -32,6 +32,7 @@ public class PlayerState(byte playerId) Execution, Disconnected, Fall, + AFK, // EHR Gambled, diff --git a/Modules/GhostRolesManager.cs b/Modules/GhostRolesManager.cs index 5236bf1e3..3eaad0886 100644 --- a/Modules/GhostRolesManager.cs +++ b/Modules/GhostRolesManager.cs @@ -31,15 +31,16 @@ namespace EHR.Modules var suitableRole = GetSuitableGhostRole(pc); Logger.Warn($"Assigning Ghost Role: {pc.GetNameWithRole()} => {suitableRole}", "GhostRolesManager"); - pc.RpcSetCustomRole(suitableRole); - pc.RpcSetRole(RoleTypes.GuardianAngel); IGhostRole instance = CreateGhostRoleInstance(suitableRole); + pc.RpcSetCustomRole(suitableRole); + if (instance.ChangeToGA) pc.RpcSetRole(RoleTypes.GuardianAngel); instance.OnAssign(pc); + Main.ResetCamPlayerList.Add(pc.PlayerId); AssignedGhostRoles[pc.PlayerId] = (suitableRole, instance); if (suitableRole == CustomRoles.Haunter) GhostRoles.Remove(suitableRole); - NotifyAboutGhostRole(pc); + NotifyAboutGhostRole(pc, true); } public static void SpecificAssignGhostRole(byte id, CustomRoles role, bool set) @@ -47,18 +48,23 @@ namespace EHR.Modules if (AssignedGhostRoles.Any(x => x.Key == id || x.Value.Role == role)) return; var pc = Utils.GetPlayerById(id); - if (set) pc.RpcSetRole(RoleTypes.GuardianAngel); IGhostRole instance = CreateGhostRoleInstance(role); + if (set && instance.ChangeToGA) pc.RpcSetRole(RoleTypes.GuardianAngel); instance.OnAssign(pc); + Main.ResetCamPlayerList.Add(pc.PlayerId); AssignedGhostRoles[id] = (role, instance); } - public static void NotifyAboutGhostRole(PlayerControl pc) + public static void NotifyAboutGhostRole(PlayerControl pc, bool first = false) { if (!AssignedGhostRoles.TryGetValue(pc.PlayerId, out var ghostRole)) return; + if (!first && pc.IsModClient()) return; + CustomRoles role = ghostRole.Role; - pc.Notify($"{Translator.GetString("GotGhostRoleNotify")}\n{GetMessage(Translator.GetString($"{role}InfoLong").Split("\n")[1..].Join(delimiter: "\n"))}", 300f); + var text = $"{Translator.GetString("GotGhostRoleNotify")}\n{GetMessage(Translator.GetString($"{role}InfoLong").Split("\n")[1..].Join(delimiter: "\n"))}"; + Utils.SendMessage(title: text, sendTo: pc.PlayerId, text: "\n"); + pc.Notify(text, 5 * text.Count(x => x == '\n')); return; string GetMessage(string baseMessage) diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index 76ca54da4..e7a37dfec 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -578,8 +578,6 @@ public static class GuessManager return true; } - private static TextMeshPro NameText(this PlayerControl p) => p.cosmetics.nameText; - public static void RpcGuesserMurderPlayer(this PlayerControl pc /*, float delay = 0f*/) { // DEATH STUFF // @@ -597,7 +595,7 @@ public static class GuessManager if (amOwner) { hudManager.ShadowQuad.gameObject.SetActive(false); - pc.NameText().GetComponent().material.SetInt(Mask, 0); + pc.cosmetics.nameText.GetComponent().material.SetInt(Mask, 0); pc.RpcSetScanner(false); ImportantTextTask importantTextTask = new GameObject("_Player").AddComponent(); importantTextTask.transform.SetParent(AmongUsClient.Instance.transform, false); @@ -642,7 +640,7 @@ public static class GuessManager if (amOwner) { hudManager.ShadowQuad.gameObject.SetActive(false); - pc.NameText().GetComponent().material.SetInt(Mask, 0); + pc.cosmetics.nameText.GetComponent().material.SetInt(Mask, 0); pc.RpcSetScanner(false); ImportantTextTask importantTextTask = new GameObject("_Player").AddComponent(); importantTextTask.transform.SetParent(AmongUsClient.Instance.transform, false); @@ -733,6 +731,28 @@ public static class GuessManager } } + private static void CreateIDLabels(MeetingHud __instance) + { + foreach (var pva in __instance.playerStates) + { + var levelDisplay = pva.transform.FindChild("PlayerLevel").gameObject; + var panel = Object.Instantiate(levelDisplay, pva.transform, true); + var panelTransform = panel.transform; + var background = panel.GetComponent(); + background.color = Palette.Purple; + background.sortingOrder = 99; + panelTransform.SetAsFirstSibling(); + panelTransform.localPosition = new(-1.2f, -0.6f, 0f); + var levelLabel = panelTransform.FindChild("LevelLabel").GetComponents()[0]; + levelLabel.DestroyTranslator(); + levelLabel.text = "ID"; + levelLabel.sortingOrder = 100; + var levelNumber = panelTransform.FindChild("LevelNumber").GetComponent(); + levelNumber.text = pva.TargetPlayerId.ToString(); + levelNumber.sortingOrder = 100; + } + } + static void GuesserSelectRole(CustomRoleTypes Role, bool SetPage = true) { CurrentTeamType = Role; @@ -1154,6 +1174,8 @@ public static class GuessManager if (alive && lp.Is(CustomRoles.Guesser)) CreateGuesserButton(__instance); } + + CreateIDLabels(__instance); } } diff --git a/Modules/RPC.cs b/Modules/RPC.cs index 2e335b5ae..0e4ee3704 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -270,6 +270,12 @@ internal class RPCHandlerPatch Version version = Version.Parse(reader.ReadString()); string tag = reader.ReadString(); string forkId = reader.ReadString(); + + if (!Main.PlayerVersion.ContainsKey(__instance.PlayerId)) + { + RPC.RpcVersionCheck(); + } + Main.PlayerVersion[__instance.PlayerId] = new(version, tag, forkId); if (Main.VersionCheat.Value && __instance.IsHost()) RPC.RpcVersionCheck(); @@ -278,7 +284,7 @@ internal class RPCHandlerPatch Main.PlayerVersion[__instance.PlayerId] = Main.PlayerVersion[0]; // Kick Unmached Player Start - if (AmongUsClient.Instance.AmHost && tag != $"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})" && forkId != Main.ForkId) + if (AmongUsClient.Instance.AmHost && tag != $"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})" && forkId != Main.ForkId && !Main.VersionCheat.Value) { LateTask.New(() => { diff --git a/Modules/Utils.cs b/Modules/Utils.cs index e6c04b25d..141217167 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -317,7 +317,7 @@ public static class Utils { try { - var data = new Il2CppSystem.Collections.Generic.Dictionary>>(); + var data = new Il2CppSystem.Collections.Generic.Dictionary>>(); var dict = path.Contains("Always") ? Main.AlwaysSpawnTogetherCombos : Main.NeverSpawnTogetherCombos; dict.Do(kvp => { @@ -326,7 +326,7 @@ public static class Utils { var key = pair.Key.ToString(); data[kvp.Key][key] = new(); - pair.Value.Do(x => data[kvp.Key][key].Add((int)x)); + pair.Value.Do(x => data[kvp.Key][key].Add(x.ToString())); }); }); File.WriteAllText(path, JsonConvert.SerializeObject(data, Formatting.Indented)); @@ -350,7 +350,7 @@ public static class Utils try { if (!File.Exists(path)) return; - var data = JsonConvert.DeserializeObject>>>(File.ReadAllText(path)); + var data = JsonConvert.DeserializeObject>>>(File.ReadAllText(path)); var dict = path.Contains("Always") ? Main.AlwaysSpawnTogetherCombos : Main.NeverSpawnTogetherCombos; dict.Clear(); foreach (var kvp in data) @@ -362,7 +362,7 @@ public static class Utils dict[kvp.Key][key] = []; foreach (var n in pair.Value) { - dict[kvp.Key][key].Add((CustomRoles)n); + dict[kvp.Key][key].Add(Enum.Parse(n)); } } } @@ -733,6 +733,7 @@ public static class Utils case CustomRoles.Gaulois when !Options.UsePets.GetBool() || !Gaulois.UsePet.GetBool(): case CustomRoles.Analyst when !Options.UsePets.GetBool() || !Analyst.UsePet.GetBool(): case CustomRoles.Witness when !Options.UsePets.GetBool() || !Options.WitnessUsePet.GetBool(): + case CustomRoles.Goose: case CustomRoles.Pursuer: case CustomRoles.Spiritcaller: case CustomRoles.PlagueBearer: @@ -1905,6 +1906,8 @@ public static class Utils return add; } + public static string ColoredPlayerName(this byte id) => ColorString(Main.PlayerColors.GetValueOrDefault(id, Color.white), Main.AllPlayerNames.GetValueOrDefault(id, $"Someone (ID {id})")); + public static PlayerControl GetPlayerById(int PlayerId, bool fast = true) { if (PlayerId is > byte.MaxValue or < byte.MinValue) return null; @@ -1919,7 +1922,7 @@ public static class Utils //if (Options.DeepLowLoad.GetBool()) await Task.Run(() => { DoNotifyRoles(isForMeeting, SpecifySeer, NoCache, ForceLoop, CamouflageIsForMeeting, GuesserIsForMeeting); }); /*else */ - if (!SetUpRoleTextPatch.IsInIntro && ((SpecifySeer != null && SpecifySeer.IsModClient()) || !AmongUsClient.Instance.AmHost || Main.AllPlayerControls == null || GameStates.IsMeeting || GameStates.IsLobby)) return; + if (!SetUpRoleTextPatch.IsInIntro && ((SpecifySeer != null && SpecifySeer.IsModClient()) || !AmongUsClient.Instance.AmHost || Main.AllPlayerControls == null || (GameStates.IsMeeting && !isForMeeting) || GameStates.IsLobby)) return; await DoNotifyRoles(isForMeeting, SpecifySeer, SpecifyTarget, NoCache, ForceLoop, CamouflageIsForMeeting, GuesserIsForMeeting, MushroomMixup); } @@ -2423,6 +2426,8 @@ public static class Utils if (MeetingStates.FirstMeeting && Main.FirstDied != string.Empty && Main.FirstDied == target.FriendCode && Main.ShieldPlayer != string.Empty && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.SoloKombat or CustomGameMode.FFA) TargetSuffix.Append(GetString("DiedR1Warning")); + + TargetSuffix.Append(AFKDetector.GetSuffix(seer, target)); } string TargetDeathReason = string.Empty; @@ -2548,8 +2553,8 @@ public static class Utils } Lovers.IsChatActivated = false; - Main.ProcessShapeshifts = true; + AFKDetector.NumAFK = 0; foreach (var pc in Main.AllPlayerControls) { @@ -2571,11 +2576,14 @@ public static class Utils if (Options.UsePets.GetBool()) pc.AddAbilityCD(includeDuration: false); + AFKDetector.RecordPosition(pc); + Main.PlayerStates[pc.PlayerId].Role.AfterMeetingTasks(); } else { - if (pc.IsCrewmate() && !pc.GetTaskState().IsTaskFinished) + TaskState taskState = pc.GetTaskState(); + if (pc.IsCrewmate() && !taskState.IsTaskFinished && taskState.hasTasks) pc.Notify(GetString("DoYourTasksPlease"), 10f); GhostRolesManager.NotifyAboutGhostRole(pc); diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 2c1dfd61f..0742f55c4 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -778,6 +778,13 @@ internal class ChatCommands break; + case "/afkexempt": + canceled = true; + if (args.Length < 2 || !byte.TryParse(args[1], out var afkId)) break; + AFKDetector.ExemptedPlayers.Add(afkId); + Utils.SendMessage("\n", localPlayerId, string.Format(GetString("PlayerExemptedFromAFK"), afkId.ColoredPlayerName())); + break; + case "/m": case "/myrole": canceled = true; @@ -806,7 +813,7 @@ internal class ChatCommands sb.Replace(subRole.ToString().ToLower(), subRole.ToColoredString()); } - Utils.SendMessage("\n", localPlayerId, settings.ToString()); + if (settings.Length > 0) Utils.SendMessage("\n", localPlayerId, settings.ToString()); Utils.SendMessage(sb.Append("").ToString(), localPlayerId, titleSb.ToString()); } else Utils.SendMessage((lp.FriendCode.GetDevUser().HasTag() ? "\n" : string.Empty) + GetString("Message.CanNotUseInLobby"), localPlayerId); @@ -1440,7 +1447,7 @@ internal class ChatCommands sb.Clear().Append(txt); if (rl.PetActivatedAbility()) sb.Append($"{GetString("SupportsPetMessage")}"); - Utils.SendMessage(text: "\n", sendTo: playerId, title: settings.ToString()); + if (settings.Length > 0) Utils.SendMessage(text: "\n", sendTo: playerId, title: settings.ToString()); Utils.SendMessage(text: sb.ToString(), sendTo: playerId, title: title); return; @@ -1557,7 +1564,7 @@ internal class ChatCommands sb.Replace(subRole.ToString().ToLower(), subRole.ToColoredString()); } - Utils.SendMessage("\n", player.PlayerId, settings.ToString()); + if (settings.Length > 0) Utils.SendMessage("\n", player.PlayerId, settings.ToString()); Utils.SendMessage(sb.Append("").ToString(), player.PlayerId, titleSb.ToString()); } else Utils.SendMessage(GetString("Message.CanNotUseInLobby"), player.PlayerId); diff --git a/Patches/CreateGameOptionsPatches.cs b/Patches/CreateGameOptionsPatches.cs deleted file mode 100644 index 29c719773..000000000 --- a/Patches/CreateGameOptionsPatches.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using AmongUs.GameOptions; -using HarmonyLib; -using TMPro; -using UnityEngine; - -// All errors in this file are fake, the build succeeds -// https://github.com/CrowdedMods/CrowdedMod/blob/master/src/CrowdedMod/Patches/CreateGameOptionsPatches.cs -namespace EHR.Patches -{ - internal static class CreateGameOptionsPatches - { - [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.Awake))] - public static class CreateOptionsPickerAwakePatch - { - public static void Postfix(CreateOptionsPicker __instance) - { - if (__instance.mode != SettingsMode.Host) return; - - var firstButtonRenderer = __instance.MaxPlayerButtons[0]; - firstButtonRenderer.GetComponentInChildren().text = "-"; - firstButtonRenderer.enabled = false; - - var firstButtonButton = firstButtonRenderer.GetComponent(); - firstButtonButton.OnClick.RemoveAllListeners(); - firstButtonButton.OnClick.AddListener((Action)(() => - { - for (var i = 1; i < 11; i++) - { - var playerButton = __instance.MaxPlayerButtons[i]; - var tmp = playerButton.GetComponentInChildren(); - var newValue = Mathf.Max(byte.Parse(tmp.text) - 10, byte.Parse(playerButton.name) - 2); - tmp.text = newValue.ToString(); - } - - __instance.UpdateMaxPlayersButtons(__instance.GetTargetOptions()); - })); - - var lastButtonRenderer = __instance.MaxPlayerButtons[^1]; - lastButtonRenderer.GetComponentInChildren().text = "+"; - lastButtonRenderer.enabled = false; - - var lastButtonButton = lastButtonRenderer.GetComponent(); - lastButtonButton.OnClick.RemoveAllListeners(); - lastButtonButton.OnClick.AddListener((Action)(() => - { - for (var i = 1; i < 11; i++) - { - var playerButton = __instance.MaxPlayerButtons[i]; - var tmp = playerButton.GetComponentInChildren(); - var newValue = Mathf.Min(byte.Parse(tmp.text) + 10, - 127 - 14 + byte.Parse(playerButton.name)); - tmp.text = newValue.ToString(); - } - - __instance.UpdateMaxPlayersButtons(__instance.GetTargetOptions()); - })); - - for (var i = 1; i < 11; i++) - { - var playerButton = __instance.MaxPlayerButtons[i].GetComponent(); - var text = playerButton.GetComponentInChildren(); - - playerButton.OnClick.RemoveAllListeners(); - playerButton.OnClick.AddListener((Action)(() => - { - var maxPlayers = byte.Parse(text.text); - var maxImp = Mathf.Min(__instance.GetTargetOptions().NumImpostors, maxPlayers / 2); - __instance.GetTargetOptions().SetInt(Int32OptionNames.NumImpostors, maxImp); - __instance.ImpostorButtons[1].TextMesh.text = maxImp.ToString(); - __instance.SetMaxPlayersButtons(maxPlayers); - })); - } - - foreach (var button in __instance.MaxPlayerButtons) - { - button.enabled = button.GetComponentInChildren().text == __instance.GetTargetOptions().MaxPlayers.ToString(); - } - - { - var secondButton = __instance.ImpostorButtons[1]; - secondButton.SpriteRenderer.enabled = false; - - var secondButtonText = secondButton.TextMesh; - secondButtonText.text = __instance.GetTargetOptions().NumImpostors.ToString(); - - var firstButton = __instance.ImpostorButtons[0]; - firstButton.SpriteRenderer.enabled = false; - firstButton.TextMesh.text = "-"; - - var firstPassiveButton = firstButton.PassiveButton; - firstPassiveButton.OnClick.RemoveAllListeners(); - firstPassiveButton.OnClick.AddListener((Action)(() => - { - var newVal = Mathf.Clamp( - byte.Parse(secondButtonText.text) - 1, - 1, - __instance.GetTargetOptions().MaxPlayers / 2 - ); - __instance.SetImpostorButtons(newVal); - secondButtonText.text = newVal.ToString(); - })); - - var thirdButton = __instance.ImpostorButtons[2]; - thirdButton.SpriteRenderer.enabled = false; - thirdButton.TextMesh.text = "+"; - - var thirdPassiveButton = thirdButton.PassiveButton; - thirdPassiveButton.OnClick.RemoveAllListeners(); - thirdPassiveButton.OnClick.AddListener((Action)(() => - { - var newVal = Mathf.Clamp( - byte.Parse(secondButtonText.text) + 1, - 1, - __instance.GetTargetOptions().MaxPlayers / 2 - ); - __instance.SetImpostorButtons(newVal); - secondButtonText.text = newVal.ToString(); - })); - } - } - } - - [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.UpdateMaxPlayersButtons))] - public static class CreateOptionsPickerUpdateMaxPlayersButtonsPatch - { - public static bool Prefix(CreateOptionsPicker __instance, [HarmonyArgument(0)] IGameOptions opts) - { - if (__instance.CrewArea) - { - __instance.CrewArea.SetCrewSize(opts.MaxPlayers, opts.NumImpostors); - } - - var selectedAsString = opts.MaxPlayers.ToString(); - for (var i = 1; i < __instance.MaxPlayerButtons.Count - 1; i++) - { - __instance.MaxPlayerButtons[i].enabled = __instance.MaxPlayerButtons[i].GetComponentInChildren().text == selectedAsString; - } - - return false; - } - } - - [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.UpdateImpostorsButtons))] - public static class CreateOptionsPickerUpdateImpostorsButtonsPatch - { - public static bool Prefix() - { - return false; - } - } - } -} \ No newline at end of file diff --git a/Patches/Crowded.cs b/Patches/Crowded.cs new file mode 100644 index 000000000..546e24633 --- /dev/null +++ b/Patches/Crowded.cs @@ -0,0 +1,187 @@ +using System; +using AmongUs.GameOptions; +using HarmonyLib; +using TMPro; +using UnityEngine; + +// All errors in this file are fake, the build succeeds +// https://github.com/CrowdedMods/CrowdedMod/blob/master/src/CrowdedMod/Patches/CreateGameOptionsPatches.cs +namespace EHR.Patches +{ + internal static class Crowded + { + public const int MaxPlayers = 127; + public const int MaxImpostors = 127 / 2; + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.Awake))] + public static class CreateOptionsPickerAwake + { + public static void Postfix(CreateOptionsPicker __instance) + { + if (__instance.mode != SettingsMode.Host) return; + + { + var firstButtonRenderer = __instance.MaxPlayerButtons[0]; + firstButtonRenderer.GetComponentInChildren().text = "-"; + firstButtonRenderer.enabled = false; + + var firstButtonButton = firstButtonRenderer.GetComponent(); + firstButtonButton.OnClick.RemoveAllListeners(); + firstButtonButton.OnClick.AddListener((Action)(() => + { + for (var i = 1; i < 11; i++) + { + var playerButton = __instance.MaxPlayerButtons[i]; + + var tmp = playerButton.GetComponentInChildren(); + var newValue = Mathf.Max(byte.Parse(tmp.text) - 10, byte.Parse(playerButton.name) - 2); + tmp.text = newValue.ToString(); + } + + __instance.UpdateMaxPlayersButtons(__instance.GetTargetOptions()); + })); + Object.Destroy(firstButtonRenderer); + + var lastButtonRenderer = __instance.MaxPlayerButtons[^1]; + lastButtonRenderer.GetComponentInChildren().text = "+"; + lastButtonRenderer.enabled = false; + + var lastButtonButton = lastButtonRenderer.GetComponent(); + lastButtonButton.OnClick.RemoveAllListeners(); + lastButtonButton.OnClick.AddListener((Action)(() => + { + for (var i = 1; i < 11; i++) + { + var playerButton = __instance.MaxPlayerButtons[i]; + + var tmp = playerButton.GetComponentInChildren(); + var newValue = Mathf.Min(byte.Parse(tmp.text) + 10, + MaxPlayers - 14 + byte.Parse(playerButton.name)); + tmp.text = newValue.ToString(); + } + + __instance.UpdateMaxPlayersButtons(__instance.GetTargetOptions()); + })); + Object.Destroy(lastButtonRenderer); + + for (var i = 1; i < 11; i++) + { + var playerButton = __instance.MaxPlayerButtons[i].GetComponent(); + var text = playerButton.GetComponentInChildren(); + + playerButton.OnClick.RemoveAllListeners(); + playerButton.OnClick.AddListener((Action)(() => + { + var maxPlayers = byte.Parse(text.text); + var maxImp = Mathf.Min(__instance.GetTargetOptions().NumImpostors, maxPlayers / 2); + __instance.GetTargetOptions().SetInt(Int32OptionNames.NumImpostors, maxImp); + __instance.ImpostorButtons[1].TextMesh.text = maxImp.ToString(); + __instance.SetMaxPlayersButtons(maxPlayers); + })); + } + + foreach (var button in __instance.MaxPlayerButtons) + { + button.enabled = button.GetComponentInChildren().text == __instance.GetTargetOptions().MaxPlayers.ToString(); + } + } + + { + var secondButton = __instance.ImpostorButtons[1]; + secondButton.SpriteRenderer.enabled = false; + Object.Destroy(secondButton.transform.FindChild("ConsoleHighlight").gameObject); + Object.Destroy(secondButton.PassiveButton); + Object.Destroy(secondButton.BoxCollider); + + var secondButtonText = secondButton.TextMesh; + secondButtonText.text = __instance.GetTargetOptions().NumImpostors.ToString(); + + var firstButton = __instance.ImpostorButtons[0]; + firstButton.SpriteRenderer.enabled = false; + firstButton.TextMesh.text = "-"; + + var firstPassiveButton = firstButton.PassiveButton; + firstPassiveButton.OnClick.RemoveAllListeners(); + firstPassiveButton.OnClick.AddListener((Action)(() => + { + var newVal = Mathf.Clamp( + byte.Parse(secondButtonText.text) - 1, + 1, + __instance.GetTargetOptions().MaxPlayers / 2 + ); + __instance.SetImpostorButtons(newVal); + secondButtonText.text = newVal.ToString(); + })); + + var thirdButton = __instance.ImpostorButtons[2]; + thirdButton.SpriteRenderer.enabled = false; + thirdButton.TextMesh.text = "+"; + + var thirdPassiveButton = thirdButton.PassiveButton; + thirdPassiveButton.OnClick.RemoveAllListeners(); + thirdPassiveButton.OnClick.AddListener((Action)(() => + { + var newVal = Mathf.Clamp( + byte.Parse(secondButtonText.text) + 1, + 1, + __instance.GetTargetOptions().MaxPlayers / 2 + ); + __instance.SetImpostorButtons(newVal); + secondButtonText.text = newVal.ToString(); + })); + } + } + } + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.UpdateMaxPlayersButtons))] + public static class CreateOptionsPickerUpdateMaxPlayersButtons + { + public static bool Prefix(CreateOptionsPicker __instance, [HarmonyArgument(0)] IGameOptions opts) + { + if (__instance.CrewArea) + { + __instance.CrewArea.SetCrewSize(opts.MaxPlayers, opts.NumImpostors); + } + + var selectedAsString = opts.MaxPlayers.ToString(); + for (var i = 1; i < __instance.MaxPlayerButtons.Count - 1; i++) + { + __instance.MaxPlayerButtons[i].enabled = __instance.MaxPlayerButtons[i].GetComponentInChildren().text == selectedAsString; + } + + return false; + } + } + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.UpdateImpostorsButtons))] + public static class CreateOptionsPickerUpdateImpostorsButtons + { + public static bool Prefix() + { + return false; + } + } + + [HarmonyPatch(typeof(GameOptionsData), nameof(GameOptionsData.AreInvalid))] + public static class InvalidOptionsPatches + { + public static bool Prefix(GameOptionsData __instance, [HarmonyArgument(0)] int maxExpectedPlayers) + { + return __instance.MaxPlayers > maxExpectedPlayers || + __instance.NumImpostors < 1 || + __instance.NumImpostors + 1 > maxExpectedPlayers / 2 || + __instance.KillDistance is < 0 or > 2 || + __instance.PlayerSpeedMod is <= 0f or > 3f; + } + } + + [HarmonyPatch(typeof(SecurityLogger), nameof(SecurityLogger.Awake))] + public static class SecurityLoggerPatch + { + public static void Postfix(ref SecurityLogger __instance) + { + __instance.Timers = new float[127]; + } + } + } +} \ No newline at end of file diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs index 8a7ae2741..e6272d6b2 100644 --- a/Patches/GameOptionsMenuPatch.cs +++ b/Patches/GameOptionsMenuPatch.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using BepInEx.Unity.IL2CPP.Utils.Collections; +using EHR.Patches; using HarmonyLib; using Il2CppSystem.Collections.Generic; using TMPro; @@ -300,52 +301,58 @@ public static class GameOptionsMenuPatch private static BaseGameSetting GetSetting(OptionItem item) { - // ReSharper disable Unity.IncorrectScriptableObjectInstantiation - BaseGameSetting baseGameSetting = item switch + BaseGameSetting baseGameSetting; + switch (item) { - BooleanOptionItem => new CheckboxGameSetting - { - Type = OptionTypes.Checkbox - }, - IntegerOptionItem integerOptionItem => new IntGameSetting - { - Type = OptionTypes.Int, - Value = integerOptionItem.GetInt(), - Increment = integerOptionItem.Rule.Step, - ValidRange = new(integerOptionItem.Rule.MinValue, integerOptionItem.Rule.MaxValue), - ZeroIsInfinity = false, - SuffixType = NumberSuffixes.Multiplier, - FormatString = string.Empty - }, - FloatOptionItem floatOptionItem => new FloatGameSetting - { - Type = OptionTypes.Float, - Value = floatOptionItem.GetFloat(), - Increment = floatOptionItem.Rule.Step, - ValidRange = new(floatOptionItem.Rule.MinValue, floatOptionItem.Rule.MaxValue), - ZeroIsInfinity = false, - SuffixType = NumberSuffixes.Multiplier, - FormatString = string.Empty - }, - StringOptionItem stringOptionItem => new StringGameSetting - { - Type = OptionTypes.String, - Values = new StringNames[stringOptionItem.Selections.Count], - Index = stringOptionItem.GetInt() - }, - PresetOptionItem presetOptionItem => new IntGameSetting - { - Type = OptionTypes.Int, - Value = presetOptionItem.GetInt(), - Increment = presetOptionItem.Rule.Step, - ValidRange = new(presetOptionItem.Rule.MinValue, presetOptionItem.Rule.MaxValue), - ZeroIsInfinity = false, - SuffixType = NumberSuffixes.Multiplier, - FormatString = string.Empty - }, - _ => null - }; - // ReSharper restore Unity.IncorrectScriptableObjectInstantiation + case BooleanOptionItem: + var checkboxGameSetting = ScriptableObject.CreateInstance(); + checkboxGameSetting.Type = OptionTypes.Checkbox; + baseGameSetting = checkboxGameSetting; + break; + case IntegerOptionItem integerOptionItem: + var intGameSetting = ScriptableObject.CreateInstance(); + intGameSetting.Type = OptionTypes.Int; + intGameSetting.Value = integerOptionItem.GetInt(); + intGameSetting.Increment = integerOptionItem.Rule.Step; + intGameSetting.ValidRange = new(integerOptionItem.Rule.MinValue, integerOptionItem.Rule.MaxValue); + intGameSetting.ZeroIsInfinity = false; + intGameSetting.SuffixType = NumberSuffixes.Multiplier; + intGameSetting.FormatString = string.Empty; + baseGameSetting = intGameSetting; + break; + case FloatOptionItem floatOptionItem: + var floatGameSetting = ScriptableObject.CreateInstance(); + floatGameSetting.Type = OptionTypes.Float; + floatGameSetting.Value = floatOptionItem.GetFloat(); + floatGameSetting.Increment = floatOptionItem.Rule.Step; + floatGameSetting.ValidRange = new(floatOptionItem.Rule.MinValue, floatOptionItem.Rule.MaxValue); + floatGameSetting.ZeroIsInfinity = false; + floatGameSetting.SuffixType = NumberSuffixes.Multiplier; + floatGameSetting.FormatString = string.Empty; + baseGameSetting = floatGameSetting; + break; + case StringOptionItem stringOptionItem: + var stringGameSetting = ScriptableObject.CreateInstance(); + stringGameSetting.Type = OptionTypes.String; + stringGameSetting.Values = new StringNames[stringOptionItem.Selections.Count]; + stringGameSetting.Index = stringOptionItem.GetInt(); + baseGameSetting = stringGameSetting; + break; + case PresetOptionItem presetOptionItem: + var presetIntGameSetting = ScriptableObject.CreateInstance(); + presetIntGameSetting.Type = OptionTypes.Int; + presetIntGameSetting.Value = presetOptionItem.GetInt(); + presetIntGameSetting.Increment = presetOptionItem.Rule.Step; + presetIntGameSetting.ValidRange = new(presetOptionItem.Rule.MinValue, presetOptionItem.Rule.MaxValue); + presetIntGameSetting.ZeroIsInfinity = false; + presetIntGameSetting.SuffixType = NumberSuffixes.Multiplier; + presetIntGameSetting.FormatString = string.Empty; + baseGameSetting = presetIntGameSetting; + break; + default: + baseGameSetting = null; + break; + } if (baseGameSetting != null) { @@ -426,8 +433,10 @@ public static class NumberOptionPatch __instance.Increment = 0.05f; __instance.Value = (float)Math.Round(__instance.Value, 2); break; - case StringNames.GameNumImpostors when DebugModeManager.IsDebugMode: - __instance.ValidRange.min = 0; + case StringNames.GameNumImpostors: + __instance.ValidRange = new(1, Crowded.MaxImpostors); + __instance.Value = (float)Math.Round(__instance.Value, 2); + if (DebugModeManager.AmDebugger) __instance.ValidRange.min = 0; break; } diff --git a/Patches/GameStartManagerPatch.cs b/Patches/GameStartManagerPatch.cs index e069e2ede..e5424a693 100644 --- a/Patches/GameStartManagerPatch.cs +++ b/Patches/GameStartManagerPatch.cs @@ -136,7 +136,7 @@ public class GameStartManagerPatch } } - if (AmongUsClient.Instance == null || GameData.Instance == null || !AmongUsClient.Instance.AmHost || !GameData.Instance) return false; + if (AmongUsClient.Instance == null || GameData.Instance == null || !AmongUsClient.Instance.AmHost || !GameData.Instance) return true; if (Main.AutoStart != null && Main.AutoStart.Value) { diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 6c69a11a8..7acb6bbc8 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -418,24 +418,28 @@ class BeginCrewmatePatch CustomRoles.Disperser => ShipStatus.Instance.VentMoveSounds.FirstOrDefault(), - _ => PlayerControl.LocalPlayer.GetCustomRoleTypes() switch - { - CustomRoleTypes.Impostor => GetIntroSound(RoleTypes.Impostor), - CustomRoleTypes.Crewmate => GetIntroSound(RoleTypes.Crewmate), - CustomRoleTypes.Neutral => GetIntroSound(RoleTypes.Shapeshifter), - _ => GetIntroSound(RoleTypes.Crewmate) - } + CustomRoles.Tracker + or CustomRoles.EvilTracker + => GetIntroSound(RoleTypes.Tracker), + + CustomRoles.Noisemaker + or CustomRoles.NoisemakerEHR + => GetIntroSound(RoleTypes.Noisemaker), + + CustomRoles.Phantom + or CustomRoles.PhantomEHR + => GetIntroSound(RoleTypes.Phantom), + + CustomRoles.Shapeshifter + or CustomRoles.ShapeshifterEHR + => GetIntroSound(RoleTypes.Shapeshifter), + + _ => GetAudioClipFromCustomRoleType() }; } catch (Exception ex) { - PlayerControl.LocalPlayer.Data.Role.IntroSound = PlayerControl.LocalPlayer.GetCustomRoleTypes() switch - { - CustomRoleTypes.Impostor => GetIntroSound(RoleTypes.Impostor), - CustomRoleTypes.Crewmate => GetIntroSound(RoleTypes.Crewmate), - CustomRoleTypes.Neutral => GetIntroSound(RoleTypes.Shapeshifter), - _ => GetIntroSound(RoleTypes.Crewmate) - }; + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetAudioClipFromCustomRoleType(); Logger.Warn($"Could not set intro sound\n{ex}", "IntroSound"); } @@ -555,6 +559,17 @@ class BeginCrewmatePatch __instance.TeamTitle.color = Color.magenta; StartFadeIntro(__instance, Color.magenta, Color.magenta); } + + AudioClip GetAudioClipFromCustomRoleType() + { + return PlayerControl.LocalPlayer.GetCustomRoleTypes() switch + { + CustomRoleTypes.Impostor => GetIntroSound(RoleTypes.Impostor), + CustomRoleTypes.Crewmate => GetIntroSound(RoleTypes.Crewmate), + CustomRoleTypes.Neutral => GetIntroSound(RoleTypes.Shapeshifter), + _ => GetIntroSound(RoleTypes.Crewmate) + }; + } } public static AudioClip GetIntroSound(RoleTypes roleType) @@ -743,7 +758,7 @@ class IntroCutsceneDestroyPatch 5 => new RandomSpawn.FungleSpawnMap(), _ => null }; - if (map != null) Main.AllAlivePlayerControls.Do(map.RandomTeleport); + if (map != null && AmongUsClient.Instance.AmHost) Main.AllAlivePlayerControls.Do(map.RandomTeleport); } if (Main.ResetCamPlayerList.Contains(PlayerControl.LocalPlayer.PlayerId)) diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index c5b4df97d..70951d24e 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -892,34 +892,8 @@ class MeetingHudStartPatch // Name Color Manager pva.NameText.text = pva.NameText.text.ApplyNameColorData(seer, target, true); - var seerRole = seer.GetCustomRole(); - // Guesser Mode // - if (((Options.GuesserMode.GetBool() && !seer.Data.IsDead && !target.Data.IsDead && - ((Options.CrewmatesCanGuess.GetBool() && seer.IsCrewmate() && !seer.Is(CustomRoles.Judge) && !seer.Is(CustomRoles.NiceSwapper) && !seer.Is(CustomRoles.Lookout) && !seer.Is(CustomRoles.ParityCop)) || - (Options.ImpostorsCanGuess.GetBool() && seerRole.IsImpostor() && !seer.Is(CustomRoles.Councillor)) || - (Options.NeutralKillersCanGuess.GetBool() && seer.IsNeutralKiller()) || - (Options.PassiveNeutralsCanGuess.GetBool() && seerRole.IsNonNK() && !seer.Is(CustomRoles.Doomsayer)))) || - - // Other Roles and Add-ons that can see player IDs // - (seer.Data.IsDead && !target.Data.IsDead && seerRole is CustomRoles.Mafia) || - (!seer.Data.IsDead && !target.Data.IsDead && - (seer.Is(CustomRoles.Guesser) || - seerRole is - CustomRoles.Judge or - CustomRoles.NiceSwapper or - CustomRoles.Lookout or - CustomRoles.ParityCop or - CustomRoles.Councillor or - CustomRoles.Doomsayer or - CustomRoles.EvilGuesser or - CustomRoles.NiceGuesser - )))) - { - pva.NameText.text = $"{Utils.ColorString(Utils.GetRoleColor(seerRole), target.PlayerId.ToString())} {pva.NameText.text}"; - } - if (seer.KnowDeathReason(target)) sb.Append($"({Utils.ColorString(Utils.GetRoleColor(CustomRoles.Doctor), Utils.GetVitalText(target.PlayerId))})"); diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index 5da8e3057..1c36552e7 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -209,8 +209,9 @@ internal class ChangeRoleSettings Provocateur.Provoked = []; Crusader.ForCrusade = []; Godfather.GodfatherTarget = byte.MaxValue; - ChatManager.ResetHistory(); - CustomNetObject.Reset(); + Crewpostor.TasksDone = []; + Express.SpeedNormal = []; + Express.SpeedUp = []; ReportDeadBodyPatch.CanReport = []; SabotageMapPatch.TimerTexts = []; @@ -225,6 +226,8 @@ internal class ChangeRoleSettings RandomSpawn.CustomNetworkTransformPatch.NumOfTP = []; + AFKDetector.ShieldedPlayers.Clear(); + MeetingTimeManager.Init(); Main.DefaultCrewmateVision = Main.RealOptionsData.GetFloat(FloatOptionNames.CrewLightMod); Main.DefaultImpostorVision = Main.RealOptionsData.GetFloat(FloatOptionNames.ImpostorLightMod); @@ -305,10 +308,6 @@ internal class ChangeRoleSettings Logger.Exception(ex, "Init Roles"); } - Crewpostor.TasksDone = []; - Express.SpeedNormal = []; - Express.SpeedUp = []; - Main.ChangedRole = false; try @@ -332,6 +331,8 @@ internal class ChangeRoleSettings DoorsReset.Initialize(); GhostRolesManager.Initialize(); RoleBlockManager.Reset(); + ChatManager.ResetHistory(); + CustomNetObject.Reset(); IRandom.SetInstanceById(Options.RoleAssigningAlgorithm.GetValue()); @@ -406,18 +407,26 @@ internal class SelectRolesPatch } - var rd = IRandom.Instance; - BasisChangingAddons.Remove(CustomRoles.Bloodlust); - bool bloodlustSpawn = rd.Next(1, 100) <= (Options.CustomAdtRoleSpawnRate.TryGetValue(CustomRoles.Bloodlust, out var option3) ? option3.GetFloat() : 0) && CustomRoles.Bloodlust.IsEnable(); - HashSet bloodlustList = RoleResult.Where(x => x.Value.IsCrewmate() && !x.Value.IsTaskBasedCrewmate()).Select(x => x.Key.PlayerId).ToHashSet(); - if (bloodlustList.Count == 0) bloodlustSpawn = false; - if (Main.SetAddOns.Values.Any(x => x.Contains(CustomRoles.Bloodlust))) - { - bloodlustSpawn = true; - bloodlustList = Main.SetAddOns.Where(x => x.Value.Contains(CustomRoles.Bloodlust)).Select(x => x.Key).ToHashSet(); - } + BasisChangingAddons.Clear(); - if (bloodlustSpawn) BasisChangingAddons[CustomRoles.Bloodlust] = bloodlustList.Shuffle().Take(CustomRoles.Bloodlust.GetCount()).ToList(); + try + { + var rd = IRandom.Instance; + bool bloodlustSpawn = rd.Next(1, 100) <= (Options.CustomAdtRoleSpawnRate.TryGetValue(CustomRoles.Bloodlust, out var option3) ? option3.GetFloat() : 0) && CustomRoles.Bloodlust.IsEnable(); + HashSet bloodlustList = RoleResult.Where(x => x.Value.IsCrewmate() && !x.Value.IsTaskBasedCrewmate() && (Options.UsePets.GetBool() || x.Value.GetRoleTypes() != RoleTypes.Impostor)).Select(x => x.Key.PlayerId).ToHashSet(); + if (bloodlustList.Count == 0) bloodlustSpawn = false; + if (Main.SetAddOns.Values.Any(x => x.Contains(CustomRoles.Bloodlust))) + { + bloodlustSpawn = true; + bloodlustList = Main.SetAddOns.Where(x => x.Value.Contains(CustomRoles.Bloodlust)).Select(x => x.Key).ToHashSet(); + } + + if (bloodlustSpawn) BasisChangingAddons[CustomRoles.Bloodlust] = bloodlustList.Shuffle().Take(CustomRoles.Bloodlust.GetCount()).ToList(); + } + catch (Exception e) + { + Utils.ThrowException(e); + } Dictionary<(byte, byte), RoleTypes> rolesMap = []; @@ -450,8 +459,6 @@ internal class SelectRolesPatch { var rd = IRandom.Instance; - new[] { CustomRoles.Nimble, CustomRoles.Physicist, CustomRoles.Finder, CustomRoles.Noisy }.Do(x => BasisChangingAddons.Remove(x)); - bool physicistSpawn = rd.Next(100) < (Options.CustomAdtRoleSpawnRate.TryGetValue(CustomRoles.Physicist, out var option1) ? option1.GetFloat() : 0) && CustomRoles.Physicist.IsEnable(); bool nimbleSpawn = rd.Next(100) < (Options.CustomAdtRoleSpawnRate.TryGetValue(CustomRoles.Nimble, out var option2) ? option2.GetFloat() : 0) && CustomRoles.Nimble.IsEnable(); bool finderSpawn = rd.Next(100) < (Options.CustomAdtRoleSpawnRate.TryGetValue(CustomRoles.Finder, out var option3) ? option3.GetFloat() : 0) && CustomRoles.Finder.IsEnable(); @@ -664,19 +671,19 @@ internal class SelectRolesPatch var aapc = Main.AllAlivePlayerControls; var addonNum = aapc.ToDictionary(x => x, _ => 0); AddonRolesList + .Except(BasisChangingAddons.Keys) .Where(x => x.IsEnable()) .SelectMany(x => Enumerable.Repeat(x, Math.Clamp(x.GetCount(), 0, aapc.Length))) .Where(x => IRandom.Instance.Next(1, 100) <= (Options.CustomAdtRoleSpawnRate.TryGetValue(x, out var sc) ? sc.GetFloat() : 0)) .Shuffle() - .Chunk(aapc.Length) - .SelectMany(a => a.Select(x => + .Select(x => { var suitablePlayer = aapc .OrderBy(p => addonNum[p]) .FirstOrDefault(p => CustomRolesHelper.CheckAddonConflict(x, p)); if (suitablePlayer != null) addonNum[suitablePlayer]++; return (Role: x, SuitablePlayer: suitablePlayer); - })) + }) .DoIf(x => x.SuitablePlayer != null, x => Main.PlayerStates[x.SuitablePlayer.PlayerId].SetSubRole(x.Role)); @@ -863,12 +870,25 @@ internal class SelectRolesPatch { foreach (PlayerControl seer in Main.AllPlayerControls) { - var sender = senders[seer.PlayerId]; foreach (PlayerControl target in Main.AllPlayerControls) { - if (rolesMap.TryGetValue((seer.PlayerId, target.PlayerId), out var role)) + try + { + if (rolesMap.TryGetValue((seer.PlayerId, target.PlayerId), out var role)) + { + // Change Scientist to Noisemaker when the role is desync and target have the Noisemaker role << Thanks: TommyXL + if (role is RoleTypes.Scientist && RoleResult.Any(x => x.Key.PlayerId == seer.PlayerId && x.Value is CustomRoles.NoisemakerEHR or CustomRoles.Noisemaker)) + { + Logger.Info($"seer: {seer.PlayerId}, target: {target.PlayerId}, {role} => {RoleTypes.Noisemaker}", "OverrideRoleForDesync"); + role = RoleTypes.Noisemaker; + } + + var sender = senders[seer.PlayerId]; + sender.RpcSetRole(seer, role, target.GetClientId()); + } + } + catch { - sender.RpcSetRole(seer, role, target.GetClientId()); } } } diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index 1e3a2cf70..5f31dd520 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -48,7 +48,7 @@ class EndGamePatch SummaryText[id] = Utils.SummaryTexts(id, disableColor: false); if (state.SubRoles.Count == 0) continue; - Main.LastAddOns[id] = $"{Utils.ColorString(Main.PlayerColors.GetValueOrDefault(id, Color.white), Main.AllPlayerNames.GetValueOrDefault(id, $"ID {id}"))}: {state.SubRoles.Join(x => x.ToColoredString())}"; + Main.LastAddOns[id] = $"{id.ColoredPlayerName()}: {state.SubRoles.Join(x => x.ToColoredString())}"; } if (Options.DumpLogAfterGameEnd.GetBool()) @@ -110,6 +110,7 @@ class EndGamePatch CustomNetObject.Reset(); Main.LoversPlayers.Clear(); Bloodmoon.OnMeetingStart(); + AFKDetector.ExemptedPlayers.Clear(); foreach (var state in Main.PlayerStates.Values) { diff --git a/Patches/PetActionsPatch.cs b/Patches/PetActionsPatch.cs index f14c2572e..15986359b 100644 --- a/Patches/PetActionsPatch.cs +++ b/Patches/PetActionsPatch.cs @@ -75,7 +75,7 @@ class ExternalRpcPetPatch Logger.Info($"Player {pc.GetNameWithRole().RemoveHtmlTags()} petted their pet", "PetActionTrigger"); - LateTask.New(() => { OnPetUse(pc); }, 0.2f, $"OnPetUse: {pc.GetNameWithRole().RemoveHtmlTags()}", false); + LateTask.New(() => OnPetUse(pc), 0.2f, $"OnPetUse: {pc.GetNameWithRole().RemoveHtmlTags()}", false); } public static void OnPetUse(PlayerControl pc) @@ -109,7 +109,7 @@ class ExternalRpcPetPatch if (target != null) hasKillTarget = true; if (!pc.CanUseKillButton()) hasKillTarget = false; - if (pc.GetCustomRole().UsesPetInsteadOfKill() && hasKillTarget && pc.Data.RoleType != RoleTypes.Impostor) + if (pc.GetCustomRole().UsesPetInsteadOfKill() && hasKillTarget && (pc.Data.RoleType != RoleTypes.Impostor || pc.GetCustomRole() is CustomRoles.Necromancer or CustomRoles.Deathknight)) { pc.AddKCDAsAbilityCD(); if (Main.PlayerStates[pc.PlayerId].Role.OnCheckMurder(pc, target)) diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 0b3da15d2..668125b76 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -150,7 +150,6 @@ class CheckMurderPatch if (target.Data == null || target.inVent - || target.walkingToVent || target.inMovingPlat || target.MyPhysics.Animations.IsPlayingEnterVentAnimation() || target.MyPhysics.Animations.IsPlayingAnyLadderAnimation() @@ -312,6 +311,12 @@ class CheckMurderPatch return false; } + if (AFKDetector.ShieldedPlayers.Contains(target.PlayerId)) + { + Notify("AFKShielded"); + return false; + } + if ((killer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Sidekick) && !Options.JackalCanKillSidekick.GetBool()) || (killer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Jackal) && !Options.SidekickCanKillJackal.GetBool()) || (killer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Recruit) && !Options.JackalCanKillSidekick.GetBool()) || @@ -1040,7 +1045,7 @@ class ReportDeadBodyPatch Camouflage.RpcSetSkin(pc, RevertToDefault: true); } - if (Main.CurrentMap == MapNames.Fungle && pc.IsMushroomMixupActive()) + if (Main.CurrentMap == MapNames.Fungle && (pc.IsMushroomMixupActive() || IsActive(SystemTypes.MushroomMixupSabotage))) { pc.FixMixedUpOutfit(); } @@ -1159,7 +1164,12 @@ class FixedUpdatePatch NameNotifyManager.OnFixedUpdate(player); TargetArrow.OnFixedUpdate(player); LocateArrow.OnFixedUpdate(player); - Camouflage.OnFixedUpdate(player); + + if (AmongUsClient.Instance.AmHost) + { + Camouflage.OnFixedUpdate(player); + AFKDetector.OnFixedUpdate(player); + } if (RPCHandlerPatch.ReportDeadBodyRPCs.Remove(playerId)) Logger.Info($"Cleared ReportDeadBodyRPC Count for {player.GetRealName().RemoveHtmlTags()}", "FixedUpdatePatch"); @@ -1428,6 +1438,8 @@ class FixedUpdatePatch if (self) Suffix.Append(CustomTeamManager.GetSuffix(seer)); + Suffix.Append(AFKDetector.GetSuffix(seer, target)); + switch (target.GetCustomRole()) { case CustomRoles.Snitch when seer.IsImpostor() && target.Is(CustomRoles.Madmate) && target.GetTaskState().IsTaskFinished: @@ -2141,4 +2153,19 @@ class CmdCheckAppearPatch return false; } +} + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.AssertWithTimeout))] +class AssertWithTimeoutPatch +{ + public static bool Prefix() => false; +} + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckName))] +class CmdCheckNameVersionCheckPatch +{ + public static void Postfix(PlayerControl __instance) + { + RPC.RpcVersionCheck(); + } } \ No newline at end of file diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs index 96ebf88f4..1bdb5c704 100644 --- a/Patches/PlayerJoinAndLeftPatch.cs +++ b/Patches/PlayerJoinAndLeftPatch.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using AmongUs.Data; @@ -7,6 +8,7 @@ using EHR.Crewmate; using EHR.Modules; using EHR.Neutral; using HarmonyLib; +using Hazel; using InnerNet; using static EHR.Translator; @@ -84,6 +86,33 @@ class OnPlayerJoinedPatch public static void Postfix( /*AmongUsClient __instance,*/ [HarmonyArgument(0)] ClientData client) { Logger.Info($"{client.PlayerName} (ClientID: {client.Id} / FriendCode: {client.FriendCode}) joined the lobby", "Session"); + + LateTask.New(() => + { + try + { + if (AmongUsClient.Instance.AmHost) + { + if (!client.Character.Data.Disconnected && client.Character.Data.IsIncomplete) + { + Logger.SendInGame(GetString("Error.InvalidColor") + $" {client.Id}/{client.PlayerName}"); + AmongUsClient.Instance.KickPlayer(client.Id, false); + Logger.Info($"Kicked client {client.Id}/{client.PlayerName} because PlayerControl is not spawned in time.", "OnPlayerJoinedPatchPostfix"); + return; + } + + if (!Main.PlayerVersion.TryGetValue(client.Character.PlayerId, out _)) + { + var retry = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.RequestRetryVersionCheck, SendOption.None, client.Id); + AmongUsClient.Instance.FinishRpcImmediately(retry); + } + } + } + catch + { + } + }, 3f, "green bean kick late task", false); + if (AmongUsClient.Instance.AmHost && client.FriendCode == "" && Options.KickPlayerFriendCodeNotExist.GetBool() && !GameStates.IsLocalGame) { if (!BanManager.TempBanWhiteList.Contains(client.GetHashedPuid())) @@ -101,7 +130,7 @@ class OnPlayerJoinedPatch Logger.Info(msg, "Android Kick"); } - if (AmongUsClient.Instance.AmHost && client.PlayerName.Contains("Silasticm", StringComparison.OrdinalIgnoreCase)) + if (AmongUsClient.Instance.AmHost && (client.PlayerName.EndsWith("cm", StringComparison.OrdinalIgnoreCase) || client.PlayerName.EndsWith("sm", StringComparison.OrdinalIgnoreCase)) && (client.PlayerName.Length == 4 || client.PlayerName.Count(x => x is 'i' or 'I') >= 2)) { AmongUsClient.Instance.KickPlayer(client.Id, false); Logger.SendInGame("They were probably hacking tbh"); @@ -176,7 +205,7 @@ class OnPlayerLeftPatch if (Main.HostClientId == __instance.ClientId) { - var clientId = -1; + const int clientId = -1; var player = PlayerControl.LocalPlayer; var title = "" + GetString("DefaultSystemMessageTitle") + ""; var name = player?.Data?.PlayerName; @@ -238,6 +267,22 @@ class OnPlayerLeftPatch Main.SayStartTimes.Remove(__instance.ClientId); Main.SayBanwordsTimes.Remove(__instance.ClientId); Main.PlayerVersion.Remove(data?.Character?.PlayerId ?? byte.MaxValue); + Main.MessagesToSend.RemoveAll(x => x.RECEIVER_ID == data?.Character.PlayerId); + + if (data != null && data.Character != null) + { + var netid = data.Character.NetId; + LateTask.New(() => + { + if (GameStates.IsOnlineGame) + { + MessageWriter messageWriter = AmongUsClient.Instance.Streams[1]; + messageWriter.StartMessage(5); + messageWriter.WritePacked(netid); + messageWriter.EndMessage(); + } + }, 2.5f, "Repeat Despawn", false); + } } Utils.CountAlivePlayers(true); diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 262234a42..d70951468 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1143,6 +1143,7 @@ "MenuTitle.Guessers": "★ Guesser Mode ★", "MenuTitle.GuesserModeRoles": "★ Roles and Add-ons for Guesser Mode ★", "MenuTitle.CTA": "★ Custom Team Assignment ★", + "MenuTitle.AFKDetection": "★ AFK Detection ★", "ShieldPersonDiedFirst": "Shield the person who got killed first in the last game", "SidekickKnowOtherSidekick": "Sidekicks know each other", "SidekickKnowOtherSidekickRole": "Sidekicks know the roles of other Sidekicks", @@ -1639,6 +1640,7 @@ "DoppelgangerWarning": "<#ffff00>The Doppelganger swapped skins with you!", "CheckMurderFail": "<#ff0000>The murder was canceled due to the following reason:\\n", "SameCTATeam": "The player you attacked is in the same custom team as you", + "AFKShielded": "According to the Host's settings, AFK players cannot be killed", "JackalSidekick": "This Jackal/Sidekick/Recruit interaction is not allowed", "ContagiousPlayers": "Contagious players cannot kill each other", "MadmateKillImpostor": "Madmates cannot kill Impostors", @@ -1680,6 +1682,14 @@ "KickCommandKicked": "was kicked from the game.", "KickCommandKickedRole": "Their role was", "ModeratorTag": "<#8000ff>Moderator", + "EnableAFKDetector": "Enable AFK Detector", + "AFKWarning": "<#ffff00>Please move to avoid being marked as AFK <#ffffff>({0})", + "AFKSuffix": "<#ffa500>(AFK)", + "AFKConsequence": "Consequence for AFK players", + "AFKConsequence.Nothing": "Nothing", + "AFKConsequence.Shield": "Shield", + "AFKConsequence.Suicide": "Suicide", + "AFKConsequence.Kick": "Kick", "DeathReason.Kill": "Kill", "DeathReason.Vote": "Ejected", "DeathReason.Suicide": "Suicide", @@ -1697,6 +1707,7 @@ "DeathReason.Execution": "Executed", "DeathReason.Disconnected": "Disconnected", "DeathReason.Fall": "Fall", + "DeathReason.AFK": "AFK", "DeathReason.Revenge": "Revenge", "DeathReason.Eaten": "Eaten", "DeathReason.Sacrifice": "Victim", @@ -2226,27 +2237,27 @@ "RoleRateNoColor": "Random", "RoleOnNoColor": "Always", "RoleOffNoColor": "Disabled", - "Rate0": "<#444444>0%", - "Rate5": "<#808F8F>5%", - "Rate10": "<#799595>10%", - "Rate15": "<#719C9C>15%", - "Rate20": "<#6AA2A2>20%", - "Rate25": "<#62A9A9>25%", - "Rate30": "<#5BB0B0>30%", - "Rate35": "<#53B6B6>35%", - "Rate40": "<#4CBDBD>40%", - "Rate45": "<#44C4C4>45%", - "Rate50": "<#3CCACA>50%", - "Rate55": "<#35D1D1>55%", - "Rate60": "<#2DD7D7>60%", - "Rate65": "<#26DEDE>65%", - "Rate70": "<#1EE5E5>70%", - "Rate75": "<#17EBEB>75%", - "Rate80": "<#0FF2F2>80%", - "Rate85": "<#08F8F8>85%", - "Rate90": "<#07F9F9>90%", - "Rate95": "<#00FFFF>95%", - "Rate100": "<#00FFA5>100%", + "Rate0": "<#440000>0%", + "Rate5": "<#00ffff>5%", + "Rate10": "<#00fffb>10%", + "Rate15": "<#00fff8>15%", + "Rate20": "<#00fff4>20%", + "Rate25": "<#00fff0>25%", + "Rate30": "<#00ffec>30%", + "Rate35": "<#00ffe7>35%", + "Rate40": "<#00ffe3>40%", + "Rate45": "<#00ffde>45%", + "Rate50": "<#00ffd9>50%", + "Rate55": "<#00ffd4>55%", + "Rate60": "<#00ffcf>60%", + "Rate65": "<#00ffc9>65%", + "Rate70": "<#00ffc4>70%", + "Rate75": "<#00ffbe>75%", + "Rate80": "<#00ffb8>80%", + "Rate85": "<#00ffb2>85%", + "Rate90": "<#00ffac>90%", + "Rate95": "<#00ffa6>95%", + "Rate100": "<#ffff44>100%", "Preset": "Preset", "Preset_1": "Preset 1", "Preset_2": "Preset 2", @@ -2395,6 +2406,8 @@ "KickXboxPlayer": "{0} was kicked due to playing on Xbox.", "KickPlayStationPlayer": "{0} was kicked due to playing on PlayStation.", "KickNintendoPlayer": "{0} was kicked due to playing on Nintendo Switch.", + "AFKKick": "{0} was kicked because they went AFK", + "AFKTooMany": "AFK detection was disabled for this round since there are too many AFK players", "KBDisplayScore": "Rank: {0} Kills: {1}", "KBTimeRemain": "Time Remaining: {0}s", "ResetDoorsEveryTurns": "Reset All Doors After Meetings", @@ -2547,6 +2560,7 @@ "pet_UFO": "UFO", "pet_D2WormPet": "Worm", "pet_RANDOM_FOR_EVERYONE": "Random", + "PlayerExemptedFromAFK": "{0} was exempted from AFK detection for 1 game", "Swapster": "Swapster", "SwapsterInfo": "Swap Players", "SwapsterInfoLong": "(Impostors):\\nAs the Swapster, you can select 2 players using your shapeshift menu to swap them.", diff --git a/Roles/AddOns/GhostRoles/Bloodmoon.cs b/Roles/AddOns/GhostRoles/Bloodmoon.cs index b6a635006..ff50bb56a 100644 --- a/Roles/AddOns/GhostRoles/Bloodmoon.cs +++ b/Roles/AddOns/GhostRoles/Bloodmoon.cs @@ -11,6 +11,7 @@ namespace EHR.AddOns.GhostRoles private static readonly Dictionary ScheduledDeaths = []; public Team Team => Team.Impostor | Team.Neutral; public int Cooldown => Duration.GetInt() + CD.GetInt(); + public bool ChangeToGA => true; public void OnAssign(PlayerControl pc) { diff --git a/Roles/AddOns/GhostRoles/GA.cs b/Roles/AddOns/GhostRoles/GA.cs index c42d6807d..dfa73bc90 100644 --- a/Roles/AddOns/GhostRoles/GA.cs +++ b/Roles/AddOns/GhostRoles/GA.cs @@ -11,6 +11,7 @@ namespace EHR.AddOns.GhostRoles public Team Team => Team.Crewmate; public int Cooldown => CD.GetInt(); + public bool ChangeToGA => true; public void OnProtect(PlayerControl pc, PlayerControl target) { diff --git a/Roles/AddOns/GhostRoles/Haunter.cs b/Roles/AddOns/GhostRoles/Haunter.cs index 191c13cc3..44a691890 100644 --- a/Roles/AddOns/GhostRoles/Haunter.cs +++ b/Roles/AddOns/GhostRoles/Haunter.cs @@ -21,6 +21,7 @@ namespace EHR.AddOns.GhostRoles private long WarnTimeStamp = 0; public Team Team => Team.Crewmate | Team.Neutral; public int Cooldown => 900; + public bool ChangeToGA => false; public void OnProtect(PlayerControl pc, PlayerControl target) { diff --git a/Roles/AddOns/GhostRoles/IGhostRole.cs b/Roles/AddOns/GhostRoles/IGhostRole.cs index 87818b0c6..37e459e07 100644 --- a/Roles/AddOns/GhostRoles/IGhostRole.cs +++ b/Roles/AddOns/GhostRoles/IGhostRole.cs @@ -4,6 +4,7 @@ { public Team Team { get; } public int Cooldown { get; } + public bool ChangeToGA { get; } public void OnProtect(PlayerControl pc, PlayerControl target); public void OnAssign(PlayerControl pc); } diff --git a/Roles/AddOns/GhostRoles/Minion.cs b/Roles/AddOns/GhostRoles/Minion.cs index 51169c644..d463ea75f 100644 --- a/Roles/AddOns/GhostRoles/Minion.cs +++ b/Roles/AddOns/GhostRoles/Minion.cs @@ -11,6 +11,7 @@ namespace EHR.AddOns.GhostRoles public Team Team => Team.Impostor; public int Cooldown => CD.GetInt(); + public bool ChangeToGA => true; public void OnProtect(PlayerControl pc, PlayerControl target) { diff --git a/Roles/AddOns/GhostRoles/Specter.cs b/Roles/AddOns/GhostRoles/Specter.cs index 1960a32d2..341980566 100644 --- a/Roles/AddOns/GhostRoles/Specter.cs +++ b/Roles/AddOns/GhostRoles/Specter.cs @@ -11,6 +11,7 @@ namespace EHR.AddOns.GhostRoles public bool IsWon; public Team Team => Team.Neutral; public int Cooldown => 900; + public bool ChangeToGA => false; public void OnAssign(PlayerControl pc) { diff --git a/Roles/AddOns/GhostRoles/Warden.cs b/Roles/AddOns/GhostRoles/Warden.cs index 72533e23d..0af66a112 100644 --- a/Roles/AddOns/GhostRoles/Warden.cs +++ b/Roles/AddOns/GhostRoles/Warden.cs @@ -14,6 +14,7 @@ namespace EHR.AddOns.GhostRoles public Team Team => Team.Crewmate; public int Cooldown => CD.GetInt(); + public bool ChangeToGA => true; public void OnProtect(PlayerControl pc, PlayerControl target) { diff --git a/Roles/Crewmate/Aid.cs b/Roles/Crewmate/Aid.cs index e137c7c22..b6d423691 100644 --- a/Roles/Crewmate/Aid.cs +++ b/Roles/Crewmate/Aid.cs @@ -3,7 +3,6 @@ using System.Linq; using AmongUs.GameOptions; using EHR.Modules; using Hazel; -using UnityEngine; namespace EHR.Crewmate { @@ -132,7 +131,7 @@ namespace EHR.Crewmate { var duration = AidDur.GetInt(); var now = Utils.TimeStamp; - var formatted = ShieldedPlayers.Select(x => string.Format(Translator.GetString("AidCounterTarget"), Utils.ColorString(Main.PlayerColors.GetValueOrDefault(x.Key, Color.white), Main.AllPlayerNames.GetValueOrDefault(x.Key, "Someone")), duration - (now - x.Value))); + var formatted = ShieldedPlayers.Select(x => string.Format(Translator.GetString("AidCounterTarget"), x.Key.ColoredPlayerName(), duration - (now - x.Value))); return string.Join("\n", formatted); } diff --git a/Roles/Crewmate/Divinator.cs b/Roles/Crewmate/Divinator.cs index 2920c848c..f6ca18b54 100644 --- a/Roles/Crewmate/Divinator.cs +++ b/Roles/Crewmate/Divinator.cs @@ -61,14 +61,15 @@ public class Divinator : RoleBase var players = Main.AllAlivePlayerControls; int rolesNeeded = players.Length * (RolesPerCategory - 1); - AllPlayerRoleList = Enum.GetValues() + var roleList = Enum.GetValues() .Where(x => !x.IsVanilla() && !x.IsAdditionRole() && x is not CustomRoles.GM and not CustomRoles.Convict && !x.IsForOtherGameMode()) .OrderBy(x => x.IsEnable() ? IRandom.Instance.Next(10) : IRandom.Instance.Next(10, 100)) .Take(rolesNeeded) .Chunk(RolesPerCategory - 1) .Zip(players, (Array, Player) => (RoleList: Array.ToList(), Player)) - .Do(x => x.RoleList.Insert(IRandom.Instance.Next(x.RoleList.Count), x.Player.GetCustomRole())) - .ToDictionary(x => x.Player.PlayerId, x => x.RoleList); + .ToArray(); + roleList.Do(x => x.RoleList.Insert(IRandom.Instance.Next(x.RoleList.Count), x.Player.GetCustomRole())); + AllPlayerRoleList = roleList.ToDictionary(x => x.Player.PlayerId, x => x.RoleList); Logger.Info(string.Join(" ---- ", AllPlayerRoleList.Select(x => $"ID {x.Key}: {string.Join(", ", x.Value)}")), "Divinator Roles"); } diff --git a/Roles/Crewmate/Goose.cs b/Roles/Crewmate/Goose.cs index 0cf571ac0..ae9b5641b 100644 --- a/Roles/Crewmate/Goose.cs +++ b/Roles/Crewmate/Goose.cs @@ -8,6 +8,7 @@ public static OptionItem OptionSpeedDuringDrag; public static OptionItem OptionVictimCanUseAbilities; public static OptionItem CanBeGuessed; + public static OptionItem Cooldown; public void SetupCustomOption() { @@ -22,6 +23,9 @@ .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Goose]); CanBeGuessed = new BooleanOptionItem(Id + 15, "CanBeGuessed", true, TabGroup.CrewmateRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Goose]); + Cooldown = new FloatOptionItem(Id + 16, "AbilityCooldown", new(0f, 180f, 0.5f), 15f, TabGroup.CrewmateRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Goose]) + .SetValueFormat(OptionFormat.Seconds); } } } \ No newline at end of file diff --git a/Roles/Crewmate/Mediumshiper.cs b/Roles/Crewmate/Mediumshiper.cs index f0dd503a8..cb8b105ee 100644 --- a/Roles/Crewmate/Mediumshiper.cs +++ b/Roles/Crewmate/Mediumshiper.cs @@ -47,7 +47,7 @@ public class Mediumshiper : RoleBase public static void OnReportDeadBody(NetworkedPlayerInfo target) { ContactPlayer = []; - if (target == null) return; + if (target == null || target.Object == null) return; foreach (var pc in Main.AllAlivePlayerControls.Where(x => playerIdList.Contains(x.PlayerId) && x.PlayerId != target.PlayerId).ToArray()) { if (pc.GetAbilityUseLimit() < 1) continue; diff --git a/Roles/Impostor/Penguin.cs b/Roles/Impostor/Penguin.cs index 56689c7f5..940ed1a35 100644 --- a/Roles/Impostor/Penguin.cs +++ b/Roles/Impostor/Penguin.cs @@ -1,4 +1,5 @@ -using EHR.Crewmate; +using AmongUs.GameOptions; +using EHR.Crewmate; using EHR.Modules; using Hazel; using UnityEngine; @@ -14,21 +15,24 @@ namespace EHR.Impostor private static OptionItem OptionMeetingKill; private static OptionItem OptionSpeedDuringDrag; private static OptionItem OptionVictimCanUseAbilities; + private float AbductTimer; private float AbductTimerLimit; private PlayerControl AbductVictim; + private float Cooldown; - int Count = 0; + private int Count; private float DefaultSpeed; private bool IsGoose; private long LastNotify; private bool MeetingKill; - private PlayerControl Penguin_; + private PlayerControl Penguin_; private byte PenguinId = byte.MaxValue; private float SpeedDuringDrag; + private bool stopCount; private bool VictimCanUseAbilities; @@ -77,6 +81,7 @@ namespace EHR.Impostor MeetingKill = OptionMeetingKill.GetBool(); SpeedDuringDrag = OptionSpeedDuringDrag.GetFloat(); VictimCanUseAbilities = OptionVictimCanUseAbilities.GetBool(); + Cooldown = Options.DefaultKillCooldown; } else { @@ -84,9 +89,10 @@ namespace EHR.Impostor MeetingKill = false; SpeedDuringDrag = Goose.OptionSpeedDuringDrag.GetFloat(); VictimCanUseAbilities = Goose.OptionVictimCanUseAbilities.GetBool(); + Cooldown = Goose.Cooldown.GetFloat(); } - LateTask.New(() => { DefaultSpeed = Main.AllPlayerSpeed[playerId]; }, 9f, log: false); + LateTask.New(() => DefaultSpeed = Main.AllPlayerSpeed[playerId], 9f, log: false); PenguinId = playerId; Penguin_ = Utils.GetPlayerById(playerId); @@ -96,9 +102,15 @@ namespace EHR.Impostor LastNotify = 0; } - public override void SetKillCooldown(byte id) => Main.AllPlayerKillCooldown[id] = Options.DefaultKillCooldown; - public override bool CanUseImpostorVentButton(PlayerControl pc) => AbductVictim == null; + public override void SetKillCooldown(byte id) => Main.AllPlayerKillCooldown[id] = Cooldown; + public override bool CanUseImpostorVentButton(PlayerControl pc) => AbductVictim == null && !IsGoose; public override bool CanUseKillButton(PlayerControl pc) => pc.IsAlive(); + public override bool CanUseSabotage(PlayerControl pc) => !IsGoose && pc.IsAlive(); + + public override void ApplyGameOptions(IGameOptions opt, byte playerId) + { + if (IsGoose) opt.SetVision(false); + } void SendRPC() { @@ -190,7 +202,7 @@ namespace EHR.Impostor } else { - if (!killer.RpcCheckAndMurder(target, check: true)) return false; + if (!IsGoose && !killer.RpcCheckAndMurder(target, check: true)) return false; doKill = false; AddVictim(target); } diff --git a/Roles/Impostor/Stealth.cs b/Roles/Impostor/Stealth.cs index abfc03b15..cc293907b 100644 --- a/Roles/Impostor/Stealth.cs +++ b/Roles/Impostor/Stealth.cs @@ -9,41 +9,44 @@ namespace EHR.Impostor public class Stealth : RoleBase { private const int Id = 641900; - private static List playerIdList = []; + private static List PlayerIdList = []; - private static OptionItem optionExcludeImpostors; - private static OptionItem optionDarkenDuration; + private static OptionItem OptionExcludeImpostors; + private static OptionItem OptionDarkenDuration; - private static bool excludeImpostors; - private static float darkenDuration; - private static float darkenTimer; - private static PlayerControl[] darkenedPlayers = []; - private static SystemTypes? darkenedRoom; + private static bool ExcludeImpostors; + private static float DarkenDuration; + private PlayerControl[] DarkenedPlayers = []; + private SystemTypes? DarkenedRoom; + private float DarkenTimer; - public override bool IsEnable => playerIdList.Count > 0; + private PlayerControl StealthPC; + + public override bool IsEnable => PlayerIdList.Count > 0; public static void SetupCustomOption() { Options.SetupSingleRoleOptions(Id, TabGroup.ImpostorRoles, CustomRoles.Stealth); - optionExcludeImpostors = new BooleanOptionItem(Id + 10, "StealthExcludeImpostors", false, TabGroup.ImpostorRoles) + OptionExcludeImpostors = new BooleanOptionItem(Id + 10, "StealthExcludeImpostors", false, TabGroup.ImpostorRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Stealth]); - optionDarkenDuration = new FloatOptionItem(Id + 20, "StealthDarkenDuration", new(0.5f, 10f, 0.5f), 3f, TabGroup.ImpostorRoles) + OptionDarkenDuration = new FloatOptionItem(Id + 20, "StealthDarkenDuration", new(0.5f, 10f, 0.5f), 3f, TabGroup.ImpostorRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Stealth]) .SetValueFormat(OptionFormat.Seconds); } public override void Init() { - playerIdList = []; + PlayerIdList = []; + ExcludeImpostors = OptionExcludeImpostors.GetBool(); + DarkenDuration = DarkenTimer = OptionDarkenDuration.GetFloat(); } public override void Add(byte playerId) { - excludeImpostors = optionExcludeImpostors.GetBool(); - darkenDuration = darkenTimer = optionDarkenDuration.GetFloat(); - darkenedPlayers = null; + StealthPC = Utils.GetPlayerById(playerId); + DarkenedPlayers = null; - playerIdList.Add(playerId); + PlayerIdList.Add(playerId); } public override void SetKillCooldown(byte id) => Main.AllPlayerKillCooldown[id] = Options.DefaultKillCooldown; @@ -51,10 +54,6 @@ namespace EHR.Impostor public override void OnMurder(PlayerControl killer, PlayerControl target) { if (!IsEnable) return; - if (!killer.CanUseKillButton() || killer == null || target == null) - { - return; - } var playersToDarken = FindPlayersInSameRoom(target); if (playersToDarken == null) @@ -63,7 +62,7 @@ namespace EHR.Impostor return; } - if (excludeImpostors) + if (ExcludeImpostors) { playersToDarken = playersToDarken.Where(player => !player.Is(CustomRoleTypes.Impostor)).ToArray(); } @@ -75,21 +74,16 @@ namespace EHR.Impostor PlayerControl[] FindPlayersInSameRoom(PlayerControl killedPlayer) { var room = killedPlayer.GetPlainShipRoom(); - if (room == null) - { - return null; - } + if (room == null) return null; - var roomArea = room.roomArea; - var roomName = room.RoomId; - RpcDarken(roomName); - return Main.AllAlivePlayerControls.Where(player => player != Utils.GetPlayerById(playerIdList[0]) && player.Collider.IsTouching(roomArea)).ToArray(); + RpcDarken(room.RoomId); + return Main.AllAlivePlayerControls.Where(p => p.PlayerId != StealthPC.PlayerId && p.GetPlainShipRoom() == room).ToArray(); } - /// Give the given player zero visibility for seconds. - static void DarkenPlayers(PlayerControl[] playersToDarken) + /// Give the given player zero visibility for seconds. + void DarkenPlayers(PlayerControl[] playersToDarken) { - darkenedPlayers = [.. playersToDarken]; + DarkenedPlayers = [.. playersToDarken]; foreach (PlayerControl player in playersToDarken) { Main.PlayerStates[player.PlayerId].IsBlackOut = true; @@ -106,12 +100,12 @@ namespace EHR.Impostor } // when you're darkening someone - if (darkenedPlayers != null) + if (DarkenedPlayers != null) { // reduce timer - darkenTimer -= Time.fixedDeltaTime; + DarkenTimer -= Time.fixedDeltaTime; // When the timer reaches 0, return everyone's vision and reset the timer and darkening player. - if (darkenTimer <= 0) + if (DarkenTimer <= 0) { ResetDarkenState(); } @@ -131,14 +125,15 @@ namespace EHR.Impostor { if (!IsEnable) return; Logger.Info($"Set the darkened room to {roomType?.ToString() ?? "null"}", "Stealth"); - darkenedRoom = roomType; + DarkenedRoom = roomType; SendRPC(roomType); } void SendRPC(SystemTypes? roomType) { if (!IsEnable) return; - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PenguinSync, SendOption.Reliable); + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.StealthDarken, SendOption.Reliable); + writer.Write(StealthPC.PlayerId); writer.Write((byte?)roomType ?? byte.MaxValue); AmongUsClient.Instance.FinishRpcImmediately(writer); } @@ -146,27 +141,28 @@ namespace EHR.Impostor public static void ReceiveRPC(MessageReader reader) { var roomId = reader.ReadByte(); - darkenedRoom = roomId == byte.MaxValue ? null : (SystemTypes)roomId; + var stealthId = reader.ReadByte(); + ((Stealth)Main.PlayerStates[stealthId].Role).DarkenedRoom = roomId == byte.MaxValue ? null : (SystemTypes)roomId; } /// Removes the darkening effect that has occurred. void ResetDarkenState() { if (!IsEnable) return; - if (darkenedPlayers != null) + if (DarkenedPlayers != null) { - foreach (PlayerControl player in darkenedPlayers) + foreach (PlayerControl player in DarkenedPlayers) { Main.PlayerStates[player.PlayerId].IsBlackOut = false; player.MarkDirtySettings(); } - darkenedPlayers = null; + DarkenedPlayers = null; } - darkenTimer = darkenDuration; + DarkenTimer = DarkenDuration; RpcDarken(null); - Utils.NotifyRoles(SpecifySeer: Utils.GetPlayerById(playerIdList[0]), SpecifyTarget: Utils.GetPlayerById(playerIdList[0])); + Utils.NotifyRoles(SpecifySeer: StealthPC, SpecifyTarget: StealthPC); } public override string GetSuffix(PlayerControl seer, PlayerControl seen, bool hud = false, bool meeting = false) @@ -174,9 +170,9 @@ namespace EHR.Impostor if (Main.PlayerStates[seer.PlayerId].Role is not Stealth { IsEnable: true }) return string.Empty; seen ??= seer; // During the meeting, unless it's my suffix, or it's dark everywhere, I won't show anything. - return meeting || seen != seer || !darkenedRoom.HasValue || (seer.IsModClient() && !hud) + return meeting || seen != seer || !DarkenedRoom.HasValue || (seer.IsModClient() && !hud) ? string.Empty - : string.Format(Translator.GetString("StealthDarkened"), DestroyableSingleton.Instance.GetString(darkenedRoom.Value)); + : string.Format(Translator.GetString("StealthDarkened"), DestroyableSingleton.Instance.GetString(DarkenedRoom.Value)); } } } \ No newline at end of file diff --git a/Roles/Impostor/Swooper.cs b/Roles/Impostor/Swooper.cs index e07a3068b..86298fceb 100644 --- a/Roles/Impostor/Swooper.cs +++ b/Roles/Impostor/Swooper.cs @@ -205,8 +205,8 @@ public class Swooper : RoleBase LateTask.New(() => { float limit = pc.GetAbilityUseLimit(); - bool naN = float.IsNaN(limit); - if (CanGoInvis && (naN || limit >= 1)) + bool wraith = UsedRole == CustomRoles.Wraith; + if (CanGoInvis && (wraith || limit >= 1)) { ventedId = ventId; @@ -215,7 +215,7 @@ public class Swooper : RoleBase AmongUsClient.Instance.FinishRpcImmediately(writer); InvisTime = Utils.TimeStamp; - if (!naN) pc.RpcRemoveAbilityUse(); + if (!wraith) pc.RpcRemoveAbilityUse(); SendRPC(); pc.Notify(GetString("SwooperInvisState"), Duration); } @@ -229,7 +229,7 @@ public class Swooper : RoleBase public override void OnEnterVent(PlayerControl pc, Vent vent) { - if (!IsInvis) return; + if (!IsInvis || InvisTime == Utils.TimeStamp) return; InvisTime = -10; lastTime = Utils.TimeStamp; diff --git a/Roles/Neutral/Glitch.cs b/Roles/Neutral/Glitch.cs index fb57f36f1..a1037d9b7 100644 --- a/Roles/Neutral/Glitch.cs +++ b/Roles/Neutral/Glitch.cs @@ -86,7 +86,7 @@ public class Glitch : RoleBase public override void SetButtonTexts(HudManager hud, byte id) { - hud.SabotageButton?.ToggleVisible(true); + hud.SabotageButton?.ToggleVisible(!Main.PlayerStates[id].IsDead); hud.SabotageButton?.OverrideText(Translator.GetString("HackButtonText")); } diff --git a/Roles/Neutral/Vulture.cs b/Roles/Neutral/Vulture.cs index a1075a426..1aed9be13 100644 --- a/Roles/Neutral/Vulture.cs +++ b/Roles/Neutral/Vulture.cs @@ -173,7 +173,7 @@ public class Vulture : RoleBase return; } - if (ChangeRoleWhenCantWin.GetBool() && Main.AllAlivePlayerControls.Length <= (NumberOfReportsToWin.GetInt() - BodyReportCount)) + if (ChangeRoleWhenCantWin.GetBool() && Main.AllAlivePlayerControls.Length - 1 <= (NumberOfReportsToWin.GetInt() - BodyReportCount)) { var role = ChangeRoles[ChangeRole.GetValue()]; pc.RpcSetCustomRole(role); diff --git a/main.cs b/main.cs index a3a48fab1..1e21b6ee9 100644 --- a/main.cs +++ b/main.cs @@ -29,8 +29,8 @@ public class Main : BasePlugin private const string DebugKeyHash = "c0fd562955ba56af3ae20d7ec9e64c664f0facecef4b3e366e109306adeae29d"; private const string DebugKeySalt = "59687b"; private const string PluginGuid = "com.gurge44.endlesshostroles"; - public const string PluginVersion = "4.0.0"; - public const string PluginDisplayVersion = "4.0.0"; + public const string PluginVersion = "4.1.0"; + public const string PluginDisplayVersion = "4.1.0"; public const string NeutralColor = "#ffab1b"; public const string ImpostorColor = "#ff1919"; public const string CrewmateColor = "#8cffff";