diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs index c0281de3b..d0a822af1 100644 --- a/Modules/AntiBlackout.cs +++ b/Modules/AntiBlackout.cs @@ -4,13 +4,16 @@ using System.Linq; using AmongUs.GameOptions; using EHR.Modules; using EHR.Patches; +using EHR.Roles; using Hazel; +using UnityEngine; namespace EHR; public static class AntiBlackout { public static bool SkipTasks; + public static bool AllowSyncSettings; private static Dictionary<(byte SeerID, byte TargetID), (RoleTypes RoleType, CustomRoles CustomRole)> CachedRoleMap = []; // Optimally, there's 1 living impostor and at least 2 living crewmates in everyone's POV. @@ -128,35 +131,102 @@ public static class AntiBlackout LateTask.New(() => { + List rpcGuardAndKill = []; var elapsedSeconds = (int)ExileControllerWrapUpPatch.Stopwatch.Elapsed.TotalSeconds; - var sender = CustomRpcSender.Create("Exile Dead Players After Meeting", SendOption.Reliable); - var hasValue = false; + var senderPacked = CustomRpcSender.Create("AntiBlackout Packed Sender", SendOption.Reliable).StartPackedMessage(); + var senderToAll = CustomRpcSender.Create("Exile Dead Players After Meeting", SendOption.Reliable); + var hasValuePacked = false; + var hasValueToAll = false; foreach (PlayerControl pc in Main.EnumeratePlayerControls()) { try { + if (pc.OwnerId < 0) continue; + if (pc.IsAlive()) { // Due to the role base change, we need to reset the cooldowns for abilities. if (!Utils.ShouldNotApplyAbilityCooldownAfterMeeting(pc)) - pc.RpcResetAbilityCooldown(); + { + if (senderPacked.RpcResetAbilityCooldown(pc)) + hasValuePacked = true; + } + + float time = -1f; if (Main.AllPlayerKillCooldown.TryGetValue(pc.PlayerId, out float kcd)) { - float time = kcd - elapsedSeconds; - if (time > 0) pc.SetKillCooldown(time); + time = kcd - elapsedSeconds; + if (time <= 0) continue; + } + + if (!Mathf.Approximately(time, -1f) && Committed.ReduceKCD != null && Committed.ReduceKCD.TryGetValue(pc.PlayerId, out float reduction)) + { + time -= reduction; + if (time <= 0) continue; + } + + Logger.Info($"{pc.GetNameWithRole()}'s KCD set to {(time < 0f ? Main.AllPlayerKillCooldown[pc.PlayerId] : time)}s", "SetKCD"); + + if (pc.GetCustomRole().UsesPetInsteadOfKill()) + { + if (time < 0f) + pc.AddKCDAsAbilityCD(); + else + pc.AddAbilityCD((int)Math.Round(time)); + + if (pc.GetCustomRole() is not CustomRoles.Necromancer and not CustomRoles.Deathknight and not CustomRoles.Renegade and not CustomRoles.Sidekick) continue; + } + + pc.AddKillTimerToDict(cd: time); + + if (time >= 0f) + Main.AllPlayerKillCooldown[pc.PlayerId] = time * 2; + else + Main.AllPlayerKillCooldown[pc.PlayerId] *= 2; + + if (pc.Is(CustomRoles.Glitch) && Main.PlayerStates[pc.PlayerId].Role is Glitch gc) + { + gc.LastKill = Utils.TimeStamp + ((int)(time / 2) - Glitch.KillCooldown.GetInt()); + gc.KCDTimer = (int)(time / 2); + } + else if (!pc.IsModdedClient() || !Options.DisableShieldAnimations.GetBool()) + { + pc.MarkDirtySettings(); + rpcGuardAndKill.Add(pc); } else - pc.SetKillCooldown(); + { + time = Main.AllPlayerKillCooldown[pc.PlayerId] / 2; + + if (pc.AmOwner) + PlayerControl.LocalPlayer.SetKillTimer(time); + else + { + senderPacked.AutoStartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetKillTimer, pc.OwnerId); + senderPacked.Write(time); + senderPacked.EndRpc(); + hasValuePacked = true; + } + } + + if (pc.GetCustomRole() is not CustomRoles.Inhibitor and not CustomRoles.Saboteur) + { + LateTask.New(() => + { + pc.ResetKillCooldown(sync: false); + pc.MarkDirtySettings(); + }, 0.3f, log: false); + } } else { if (pc.AmOwner && Utils.TempReviveHostRunning) continue; // Ensure that the players who are considered dead by the mod are actually dead in the game. - sender.RpcExiled(pc); - hasValue = true; + senderToAll.RpcExiled(pc); + hasValueToAll = true; if (GhostRolesManager.AssignedGhostRoles.TryGetValue(pc.PlayerId, out var ghostRole) && ghostRole.Instance.RoleTypes == RoleTypes.GuardianAngel) pc.AddAbilityCD(ghostRole.Instance.Cooldown); @@ -164,8 +234,16 @@ public static class AntiBlackout } catch (Exception e) { Utils.ThrowException(e); } } + + AllowSyncSettings = true; + PlayerGameOptionsSender.SendAllImmediately(); + AllowSyncSettings = false; - sender.SendMessage(dispose: !hasValue); + rpcGuardAndKill.ForEach(pc => hasValuePacked |= senderPacked.RpcGuardAndKill(pc, fromSetKCD: true)); + + LateTask.New(() => senderPacked.SendMessage(dispose: !hasValuePacked), 0.1f); + + senderToAll.SendMessage(dispose: !hasValueToAll); // Only execute AfterMeetingTasks after everything is reset. LateTask.New(() => diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index 5e489c149..72969755f 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -349,8 +349,8 @@ namespace EHR if (PlayerControl.AllPlayerControls.Contains(playerControl)) PlayerControl.AllPlayerControls.Remove(playerControl); - playerControl.cosmetics.currentBodySprite.BodySprite.color = Color.clear; - playerControl.cosmetics.colorBlindText.color = Color.clear; + try { playerControl.cosmetics.currentBodySprite.BodySprite.color = Color.clear; } catch { } + try { playerControl.cosmetics.colorBlindText.color = Color.clear; } catch { } Position = position; Sprite = sprite; ++MaxId; diff --git a/Modules/CustomRpcSender.cs b/Modules/CustomRpcSender.cs index add65d191..889d2063a 100644 --- a/Modules/CustomRpcSender.cs +++ b/Modules/CustomRpcSender.cs @@ -707,6 +707,9 @@ public static class CustomRpcSenderExtensions { if (!player) return false; + if (!Mathf.Approximately(time, -1f) && Committed.ReduceKCD != null && Committed.ReduceKCD.TryGetValue(player.PlayerId, out float reduction)) + time = Math.Max(time - reduction, 0.01f); + Logger.Info($"{player.GetNameWithRole()}'s KCD set to {(Math.Abs(time - -1f) < 0.5f ? Main.AllPlayerKillCooldown[player.PlayerId] : time)}s", "SetKCD"); if (player.GetCustomRole().UsesPetInsteadOfKill()) diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 98ea724be..ae44e2bbf 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -1135,14 +1135,16 @@ internal static class ExtendedPlayerControl { try { - if (NameWithRoleCache.TryGetValue(player?.PlayerId ?? 250, out string cache)) return cache; + if (!player) return "Unknown Player"; + if (NameWithRoleCache.TryGetValue(player.PlayerId, out string cache)) return cache; + if (!player.Data) return "Unknown Player"; bool addRoleName = GameStates.IsInGame && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.StopAndGo and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters and not CustomGameMode.RoomRush and not CustomGameMode.Quiz and not CustomGameMode.TheMindGame and not CustomGameMode.BedWars and not CustomGameMode.Deathrace and not CustomGameMode.Mingle and not CustomGameMode.Snowdown; - return NameWithRoleCache[player?.PlayerId ?? 250] = $"{player?.Data?.PlayerName}" + (addRoleName ? $" ({player?.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty); + return NameWithRoleCache[player.PlayerId] = $"{player.Data.PlayerName}" + (addRoleName ? $" ({player.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty); } catch (Exception e) { ThrowException(e); - return !player || !player.Data ? "Unknown Player" : player.Data.PlayerName; + return "Unknown Player"; } } diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index 0c1bef03e..a79dbe022 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -55,7 +55,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS public static DataFlagRateLimiter.QueuedAction SendAllImmediately() { - if (AntiBlackout.SkipTasks) return null; + if (AntiBlackout.SkipTasks && !AntiBlackout.AllowSyncSettings) return null; ForceWaitFrame = true; @@ -264,7 +264,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS protected override void SendOptionsArray(Il2CppStructArray optionArray, byte logicOptionsIndex) { - if (AntiBlackout.SkipTasks) return; + if (AntiBlackout.SkipTasks && !AntiBlackout.AllowSyncSettings) return; if (PackedWriter == null) // Single write { diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 6e8f56351..d1a3a76ab 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -1110,7 +1110,7 @@ internal static class ChatCommands { GMPollGameModes = Main.CustomGameModeValues[..^1].Where(x => Options.GMPollGameModesSettings[x].GetBool()).ToList(); if (GMPollGameModes.Contains(CustomGameMode.HideAndSeek)) GMPollGameModes.Add((CustomGameMode)100); - string gmNames = GMPollGameModes.Join(' ', x => GetString((int)x == 100 ? "HNS.ShiftAndSeek" : x.ToString().Replace(' ', '_'))); + string gmNames = GMPollGameModes.Join(' ', x => GetString((int)x == 100 ? "HNS.ShiftAndSeek" : x.ToString().Replace(' ', '-'))); var msg = $"/poll {GetString("GameModePoll.Question").TrimEnd('?')}? {gmNames}"; PollCommand(player, msg, msg.Split(' ')); } @@ -1118,7 +1118,7 @@ internal static class ChatCommands public static void MapPollCommand(PlayerControl player, string text, string[] args) { MPollMaps = Main.MapNamesValues.Where(x => Options.MPollMapsSettings[x].GetBool()).ToList(); - string mNames = MPollMaps.Join(' ', x => GetString(x.ToString().Replace(' ', '_'))); + string mNames = MPollMaps.Join(' ', x => GetString(x.ToString().Replace(' ', '-'))); var msg = $"/poll {GetString("MapPoll.Question").TrimEnd('?')}? {mNames}"; PollCommand(player, msg, msg.Split(' ')); } @@ -1126,7 +1126,7 @@ internal static class ChatCommands public static void PresetPollCommand(PlayerControl player, string text, string[] args) { var presetConfigs = new[] { Main.Preset1, Main.Preset2, Main.Preset3, Main.Preset4, Main.Preset5, Main.Preset6, Main.Preset7, Main.Preset8, Main.Preset9, Main.Preset10, Main.Preset11, Main.Preset12, Main.Preset13, Main.Preset14, Main.Preset15, Main.Preset16, Main.Preset17, Main.Preset18, Main.Preset19, Main.Preset20 }; - string presetNames = string.Join(' ', presetConfigs.Select((cfg, i) => (cfg.Value == (string)cfg.DefaultValue ? GetString($"Preset_{i + 1}") : cfg.Value).Replace(' ', '_'))); + string presetNames = string.Join(' ', presetConfigs.Select((cfg, i) => (cfg.Value == (string)cfg.DefaultValue ? GetString($"Preset_{i + 1}") : cfg.Value).Replace(' ', '-'))); var msg = $"/poll {GetString("PresetPoll.Question").TrimEnd('?')}? {presetNames}"; PollCommand(player, msg, msg.Split(' ')); diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 04d93fc85..2572b475c 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -426,17 +426,7 @@ internal static class GameEndChecker void SetGhostRole(bool toGhostImpostor) { if (!pc.Data.IsDead) playersToRevive.Add(pc.PlayerId); - - if (toGhostImpostor) - { - Logger.Info($"{pc.GetNameWithRole()}: changed to ImpostorGhost", "ResetRoleAndEndGame"); - pc.RpcSetRole(RoleTypes.ImpostorGhost); - } - else - { - Logger.Info($"{pc.GetNameWithRole()}: changed to CrewmateGhost", "ResetRoleAndEndGame"); - pc.RpcSetRole(RoleTypes.CrewmateGhost); - } + pc.RpcSetRole(toGhostImpostor ? RoleTypes.ImpostorGhost : RoleTypes.CrewmateGhost); } } diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index 21a1964a5..33218e9e7 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -494,6 +494,8 @@ internal static class StartGameHostPatch } catch (Exception e) { Utils.ThrowException(e); } + yield return null; + try { GC.Collect(); diff --git a/Roles/Standard/Crewmate/Killing/Sheriff.cs b/Roles/Standard/Crewmate/Killing/Sheriff.cs index 4ee8a9255..1670257bf 100644 --- a/Roles/Standard/Crewmate/Killing/Sheriff.cs +++ b/Roles/Standard/Crewmate/Killing/Sheriff.cs @@ -118,9 +118,9 @@ public class Sheriff : RoleBase public override bool CanUseKillButton(PlayerControl pc) { - return !Main.PlayerStates[pc.PlayerId].IsDead - && (CanKillAllAlive.GetBool() || GameStates.AlreadyDied) - && pc.GetAbilityUseLimit() is float.NaN or > 0; + if (!CanKillAllAlive.GetBool() && !GameStates.AlreadyDied) return false; + float abilityUseLimit = pc.GetAbilityUseLimit(); + return float.IsNaN(abilityUseLimit) || abilityUseLimit > 0 || pc.Is(CustomRoles.Bloodlust); } public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) @@ -139,6 +139,9 @@ public class Sheriff : RoleBase { killer.RpcRemoveAbilityUse(); Logger.Info($"{killer.GetNameWithRole()} : Number of kills left: {killer.GetAbilityUseLimit()}", "Sheriff"); + + if (killer.Is(CustomRoles.Bloodlust)) return; + if (killer.PlayerId != target.PlayerId) Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Shot; diff --git a/Roles/Standard/Crewmate/Support/TimeMaster.cs b/Roles/Standard/Crewmate/Support/TimeMaster.cs index c80336a2b..be7f5c5c3 100644 --- a/Roles/Standard/Crewmate/Support/TimeMaster.cs +++ b/Roles/Standard/Crewmate/Support/TimeMaster.cs @@ -108,7 +108,7 @@ internal class TimeMaster : RoleBase public override void OnPet(PlayerControl pc) { - if (pc.GetAbilityUseLimit() < 1) return; + if (Rewinding || pc.GetAbilityUseLimit() < 1) return; pc.RpcRemoveAbilityUse(notify: false); Main.Instance.StartCoroutine(Rewind()); @@ -120,7 +120,7 @@ internal class TimeMaster : RoleBase public override void OnEnterVent(PlayerControl pc, Vent vent) { if (UsePets.GetBool()) return; - if (pc.GetAbilityUseLimit() < 1) return; + if (Rewinding || pc.GetAbilityUseLimit() < 1) return; pc.RpcRemoveAbilityUse(notify: false); Main.Instance.StartCoroutine(Rewind()); diff --git a/Roles/Standard/Impostor/Concealing/Penguin.cs b/Roles/Standard/Impostor/Concealing/Penguin.cs index 7faddcdbb..c3252faad 100644 --- a/Roles/Standard/Impostor/Concealing/Penguin.cs +++ b/Roles/Standard/Impostor/Concealing/Penguin.cs @@ -305,7 +305,7 @@ public class Penguin : RoleBase if (AbductVictim) { - if (!Penguin_.IsAlive() || Pelican.IsEaten(PenguinId) || !AbductVictim.IsAlive()) + if (!Penguin_ || !Penguin_.IsAlive() || Pelican.IsEaten(PenguinId) || !AbductVictim.IsAlive()) { RemoveVictim(); return; diff --git a/Roles/Standard/Neutral/Killing/Duality.cs b/Roles/Standard/Neutral/Killing/Duality.cs index c63dc1d83..67c4dffbf 100644 --- a/Roles/Standard/Neutral/Killing/Duality.cs +++ b/Roles/Standard/Neutral/Killing/Duality.cs @@ -1,4 +1,5 @@ -using AmongUs.GameOptions; +using System.Collections.Generic; +using AmongUs.GameOptions; using EHR.Modules; using EHR.Modules.Extensions; using Hazel; @@ -69,6 +70,8 @@ public class Duality : RoleBase public override void OnMurder(PlayerControl killer, PlayerControl target) { + if (!killer.IsAlive()) return; + LateTask.New(() => { KillingPhase = false; @@ -80,8 +83,13 @@ public class Duality : RoleBase public override void OnTaskComplete(PlayerControl pc, int completedTaskCount, int totalTaskCount) { + if (!pc.IsAlive()) return; + KillingPhase = true; - pc.RpcSetRoleDesync(RoleTypes.Impostor, pc.OwnerId, setRoleMap: true); + var sender = CustomRpcSender.Create($"Duality to killing phase ({Main.AllPlayerNames.GetValueOrDefault(pc.PlayerId, "Someone")})", SendOption.Reliable); + sender.RpcSetRole(pc, RoleTypes.Impostor, pc.OwnerId); + if (!pc.IsImpostor()) Main.CachedAlivePlayerControls().DoIf(x => x.IsImpostor(), x => sender.RpcSetRole(x, RoleTypes.Crewmate, pc.OwnerId)); + sender.SendMessage(); ResetTimer(); } @@ -93,17 +101,17 @@ public class Duality : RoleBase void ResetTimer(int add = 0) { var pc = DualityId.GetPlayer(); - if (pc == null || !pc.IsAlive()) return; + if (!pc || !pc.IsAlive()) return; Timer?.Dispose(); Timer = new CountdownTimer(Time.GetInt() + add, () => { - if (pc == null || !pc.IsAlive()) return; + if (!pc || !pc.IsAlive()) return; pc.Suicide(); if (pc.AmOwner) Achievements.Type.OutOfTime.Complete(); Timer = null; }, onTick: () => { - if (pc == null || !pc.IsAlive()) + if (!pc || !pc.IsAlive()) { Timer.Dispose(); return;