diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index c80aec444..941ee90cc 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -17,7 +17,7 @@ namespace EHR { public class CustomNetObject { - public static readonly List AllObjects = []; + public static List AllObjects = []; private static int MaxId = -1; private readonly HashSet HiddenList = []; protected int Id; @@ -98,15 +98,6 @@ namespace EHR } catch (Exception e) { - if (CustomGameMode.NaturalDisasters.IsActiveOrIntegrated()) - { - if (HiddenList.Contains(PlayerControl.LocalPlayer.PlayerId)) return; - - Logger.Warn("Error during despawn, hiding instead", "CNO.Despawn"); - Main.AllPlayerControls.Do(Hide); - return; - } - Utils.ThrowException(e); } } @@ -335,6 +326,20 @@ namespace EHR } catch (Exception e) { Utils.ThrowException(e); } } + + private static List TempDespawnedObjects = []; + + public static void OnMeeting() + { + TempDespawnedObjects = AllObjects.ToList(); + Reset(); + } + + public static void AfterMeeting() + { + TempDespawnedObjects.ForEach(x => x.CreateNetObject(x.Sprite, x.Position)); + TempDespawnedObjects.Clear(); + } } /* diff --git a/Modules/RPC.cs b/Modules/RPC.cs index 3517f4fec..3dee547ef 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -168,6 +168,7 @@ public enum Sounds internal static class RPCHandlerPatch { public static readonly Dictionary ReportDeadBodyRPCs = []; + public static readonly Dictionary NumRPCsThisSecond = []; private static bool TrustedRpc(byte id) { @@ -182,67 +183,58 @@ internal static class RPCHandlerPatch Logger.Info($"From ID: {__instance?.Data?.PlayerId} ({(__instance?.Data?.PlayerId == 0 ? "Host" : __instance?.Data?.PlayerName)}) : {callId} ({RPC.GetRpcName(callId)})", "ReceiveRPC"); - if (callId == 11 && __instance != null) + if (__instance != null) { - if (!ReportDeadBodyRPCs.ContainsKey(__instance.PlayerId)) ReportDeadBodyRPCs.TryAdd(__instance.PlayerId, 0); + if (NumRPCsThisSecond.TryAdd(__instance.PlayerId, 0)) LateTask.New(() => NumRPCsThisSecond.Remove(__instance.PlayerId), 1f, log: false); + NumRPCsThisSecond[__instance.PlayerId] += rpcType is RpcCalls.CheckMurder or RpcCalls.CheckProtect ? 1 : 2; - ReportDeadBodyRPCs[__instance.PlayerId]++; - Logger.Info($"ReportDeadBody RPC count: {ReportDeadBodyRPCs[__instance.PlayerId]}, from {__instance.Data?.PlayerName}", "EAC"); - } + switch (rpcType) + { + case RpcCalls.SetName: + subReader.ReadUInt32(); + string name = subReader.ReadString(); + if (subReader.BytesRemaining > 0 && subReader.ReadBoolean()) return false; + Logger.Info($"RPC Set Name For Player: {__instance.GetNameWithRole().RemoveHtmlTags()} => {name}", "SetName"); + break; + case RpcCalls.SetRole: + var role = (RoleTypes)subReader.ReadUInt16(); + bool canOverriddenRole = subReader.ReadBoolean(); + Logger.Info($"RPC Set Role For Player: {__instance.GetRealName()} => {role} CanOverrideRole: {canOverriddenRole}", "SetRole"); + break; + case RpcCalls.SendChat: + string text = subReader.ReadString(); + Logger.Info($"{__instance.GetNameWithRole().RemoveHtmlTags()}:{text}", "ReceiveChat"); + ChatCommands.OnReceiveChat(__instance, text, out bool canceled); + if (canceled) return false; + break; + case RpcCalls.StartMeeting: + PlayerControl p = Utils.GetPlayerById(subReader.ReadByte()); + Logger.Info($"{__instance.GetNameWithRole().RemoveHtmlTags()} => {p?.GetNameWithRole() ?? "null"}", "StartMeeting"); + break; + case RpcCalls.Pet: + Logger.Info($"{__instance.GetNameWithRole().RemoveHtmlTags()} petted their pet", "RpcHandlerPatch"); + break; + } - switch (rpcType) - { - case RpcCalls.SetName: - subReader.ReadUInt32(); - string name = subReader.ReadString(); - if (subReader.BytesRemaining > 0 && subReader.ReadBoolean()) return false; + if (!__instance.IsHost() && Enum.IsDefined(typeof(CustomRPC), (int)callId) && !TrustedRpc(callId)) + { + Logger.Warn($"{__instance.Data?.PlayerName}:{callId}({RPC.GetRpcName(callId)}) canceled because it was sent by someone other than the host.", "CustomRPC"); + if (!AmongUsClient.Instance.AmHost) return false; - Logger.Info($"RPC Set Name For Player: {__instance.GetNameWithRole().RemoveHtmlTags()} => {name}", "SetName"); - break; - case RpcCalls.SetRole: - var role = (RoleTypes)subReader.ReadUInt16(); - bool canOverriddenRole = subReader.ReadBoolean(); - Logger.Info($"RPC Set Role For Player: {__instance.GetRealName()} => {role} CanOverrideRole: {canOverriddenRole}", "SetRole"); - break; - case RpcCalls.SendChat: - string text = subReader.ReadString(); - Logger.Info($"{__instance.GetNameWithRole().RemoveHtmlTags()}:{text}", "ReceiveChat"); - ChatCommands.OnReceiveChat(__instance, text, out bool canceled); - if (canceled) return false; + if (!EAC.ReceiveInvalidRpc(__instance, callId)) return false; - break; - case RpcCalls.StartMeeting: - PlayerControl p = Utils.GetPlayerById(subReader.ReadByte()); - Logger.Info($"{__instance.GetNameWithRole().RemoveHtmlTags()} => {p?.GetNameWithRole() ?? "null"}", "StartMeeting"); - break; - case RpcCalls.Pet: - Logger.Info($"{__instance.GetNameWithRole().RemoveHtmlTags()} petted their pet", "RpcHandlerPatch"); - break; - case RpcCalls.SetScanner when Main.HasJustStarted && AmongUsClient.Instance.AmHost: - Logger.Fatal($"{__instance.GetNameWithRole().RemoveHtmlTags()} triggered this bs ---- revive was attempted", "RpcHandlerPatch"); - __instance?.Revive(); + AmongUsClient.Instance.KickPlayer(__instance.GetClientId(), false); + Logger.Warn($"The RPC received from {__instance.Data?.PlayerName} is not trusted, so they were kicked.", "Kick"); + Logger.SendInGame(string.Format(GetString("Warning.InvalidRpc"), __instance.Data?.PlayerName)); return false; - } + } - if (__instance != null && !__instance.IsHost() && Enum.IsDefined(typeof(CustomRPC), (int)callId) && !TrustedRpc(callId)) - { - Logger.Warn($"{__instance.Data?.PlayerName}:{callId}({RPC.GetRpcName(callId)}) canceled because it was sent by someone other than the host.", "CustomRPC"); - if (!AmongUsClient.Instance.AmHost) return false; - - if (!EAC.ReceiveInvalidRpc(__instance, callId)) return false; - - AmongUsClient.Instance.KickPlayer(__instance.GetClientId(), false); - Logger.Warn($"The RPC received from {__instance.Data?.PlayerName} is not trusted, so they were kicked.", "Kick"); - Logger.SendInGame(string.Format(GetString("Warning.InvalidRpc"), __instance.Data?.PlayerName)); - return false; - } - - if (__instance != null && ReportDeadBodyRPCs.TryGetValue(__instance.PlayerId, out int times) && times > 4) - { - AmongUsClient.Instance.KickPlayer(__instance.GetClientId(), true); - Logger.Warn($"{__instance.Data?.PlayerName} has sent 5 or more ReportDeadBody RPCs in the last 1 second, they were banned for hacking.", "EAC"); - Logger.SendInGame(string.Format(GetString("Warning.ReportDeadBodyHack"), __instance.Data?.PlayerName)); - return false; + if (NumRPCsThisSecond.TryGetValue(__instance.PlayerId, out int times) && times > (GameStates.IsLobby ? 24 : 40)) + { + AmongUsClient.Instance.KickPlayer(__instance.GetClientId(), false); + Logger.SendInGame(string.Format(GetString("Warning.TooManyRPCs"), __instance.Data?.PlayerName)); + return false; + } } return true; diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 5022bd50d..a0f2adbf7 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -508,7 +508,7 @@ public static class Utils { foreach (CustomRoles subRole in targetSubRoles) { - if (subRole is not CustomRoles.LastImpostor and not CustomRoles.Madmate and not CustomRoles.Charmed and not CustomRoles.Recruit and not CustomRoles.Lovers and not CustomRoles.Contagious and not CustomRoles.Bloodlust and not CustomRoles.Entranced) + if (subRole is not CustomRoles.LastImpostor and not CustomRoles.Madmate and not CustomRoles.Charmed and not CustomRoles.Recruit and not CustomRoles.Lovers and not CustomRoles.Contagious and not CustomRoles.Bloodlust and not CustomRoles.Entranced and not CustomRoles.Egoist) { string str = GetString("Prefix." + subRole); if (!subRole.IsAdditionRole()) str = GetString(subRole.ToString()); @@ -3188,6 +3188,8 @@ public static class Utils LateTask.New(() => Asthmatic.RunChecks = true, 2f, log: false); EAC.InvalidReports.Clear(); + + CustomNetObject.AfterMeeting(); } public static void AfterPlayerDeathTasks(PlayerControl target, bool onMeeting = false, bool disconnect = false) @@ -3532,17 +3534,17 @@ public static class Utils if (!impShow && !nkShow && !covenShow) return string.Empty; - foreach (PlayerControl pc in Main.AllAlivePlayerControls) + foreach (PlayerControl pc in Main.AllPlayerControls) { if (excludeId != byte.MaxValue && pc.PlayerId == excludeId) continue; - if (Forger.Forges.TryGetValue(pc.PlayerId, out var forgedRole)) + if (Forger.Forges.TryGetValue(pc.PlayerId, out var forgedRole) && (ExileController.Instance || !pc.IsAlive() || (GameStates.IsMeeting && MeetingHud.Instance.state is MeetingHud.VoteStates.Results or MeetingHud.VoteStates.Proceeding or MeetingHud.VoteStates.Voted or MeetingHud.VoteStates.NotVoted)) && !GameStates.IsEnded) { - if (impShow && forgedRole.Is(Team.Impostor)) impnum++; - else if (nkShow && forgedRole.IsNK()) neutralnum++; - else if (covenShow && forgedRole.Is(Team.Coven)) covenNum++; + if (impShow && forgedRole.Is(Team.Impostor)) impnum--; + else if (nkShow && forgedRole.IsNK()) neutralnum--; + else if (covenShow && forgedRole.Is(Team.Coven)) covenNum--; } - else + else if (pc.IsAlive()) { if (impShow && pc.Is(Team.Impostor)) impnum++; else if (nkShow && pc.IsNeutralKiller()) neutralnum++; diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 0efa81688..d69c5b4e2 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -109,6 +109,7 @@ internal static class ChatCommands public static readonly HashSet Spectators = []; public static readonly HashSet LastSpectators = []; + public static readonly HashSet ForcedSpectators = []; private static HashSet ReadyPlayers = []; @@ -187,7 +188,7 @@ internal static class ChatCommands new(["achievements", "достижения", "成就", "conquistas"], "", GetString("CommandDescription.Achievements"), Command.UsageLevels.Modded, Command.UsageTimes.Always, AchievementsCommand, true, false), new(["dn", "deathnote", "заметкамертвого", "死亡笔记"], "{name}", GetString("CommandDescription.DeathNote"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, DeathNoteCommand, true, true, [GetString("CommandArgs.DeathNote.Name")]), new(["w", "whisper", "шепот", "ш", "私聊", "sussurrar"], "{id} {message}", GetString("CommandDescription.Whisper"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, WhisperCommand, true, true, [GetString("CommandArgs.Whisper.Id"), GetString("CommandArgs.Whisper.Message")]), - new(["spectate", "спектейт", "观战", "espectar"], "", GetString("CommandDescription.Spectate"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, SpectateCommand, false, false), + new(["spectate", "спектейт", "观战", "espectar"], "[id]", GetString("CommandDescription.Spectate"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, SpectateCommand, false, false, [GetString("CommandArgs.Spectate.Id")]), new(["anagram", "анаграмма"], "", GetString("CommandDescription.Anagram"), Command.UsageLevels.Everyone, Command.UsageTimes.Always, AnagramCommand, true, false), new(["rl", "rolelist", "роли"], "", GetString("CommandDescription.RoleList"), Command.UsageLevels.Everyone, Command.UsageTimes.Always, RoleListCommand, true, false), new(["jt", "jailtalk", "тюремныйразговор", "监狱谈话"], "{message}", GetString("CommandDescription.JailTalk"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, JailTalkCommand, true, true, [GetString("CommandArgs.JailTalk.Message")]), @@ -718,6 +719,19 @@ internal static class ChatCommands RequestCommandProcessingFromHost(nameof(SpectateCommand), text); return; } + + if (player.IsHost() && args.Length > 1 && byte.TryParse(args[1], out byte targetId)) + { + PlayerControl pc = targetId.GetPlayer(); + if (pc == null) return; + + if (ForcedSpectators.Remove(targetId)) + Utils.SendMessage("\n", player.PlayerId, string.Format(GetString("SpectateCommand.RemovedForcedSpectator"), targetId.ColoredPlayerName())); + + if (ForcedSpectators.Add(targetId)) + Utils.SendMessage("\n", player.PlayerId, string.Format(GetString("SpectateCommand.ForcedSpectator"), targetId.ColoredPlayerName())); + return; + } if (Options.DisableSpectateCommand.GetBool()) { diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index ed455f9d3..c0c434486 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -247,6 +247,8 @@ internal static class ChangeRoleSettings Options.UsedButtonCount = 0; + ChatCommands.Spectators.UnionWith(ChatCommands.ForcedSpectators); + ChatCommands.ForcedSpectators.Clear(); ChatCommands.LastSpectators.Clear(); ChatCommands.LastSpectators.UnionWith(ChatCommands.Spectators); diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index fe957f5d4..a900a5283 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -1087,6 +1087,8 @@ internal static class ReportDeadBodyPatch //============================================================================================= // Hereinafter, it is confirmed that the meeting is allowed, and the meeting will start. //============================================================================================= + + CustomNetObject.OnMeeting(); Asthmatic.RunChecks = false; diff --git a/Patches/TextBoxPatch.cs b/Patches/TextBoxPatch.cs index 6998bfe36..4836fb332 100644 --- a/Patches/TextBoxPatch.cs +++ b/Patches/TextBoxPatch.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using AmongUs.Data; using HarmonyLib; +using InnerNet; using TMPro; using UnityEngine; @@ -257,7 +259,13 @@ internal static class TextBoxTMPSetTextPatch { if (PlaceHolderText != null) PlaceHolderText.enabled = false; if (CommandInfoText != null) CommandInfoText.enabled = false; - if (AdditionalInfoText != null) AdditionalInfoText.enabled = false; + + if (AdditionalInfoText != null) + { + bool showLobbyCode = FastDestroyableSingleton.Instance?.Chat?.IsOpenOrOpening == true && GameStates.IsLobby && Options.GetSuffixMode() == SuffixModes.Streaming && !Options.HideGameSettings.GetBool() && !DataManager.Settings.Gameplay.StreamerMode; + AdditionalInfoText.enabled = showLobbyCode; + if (showLobbyCode) AdditionalInfoText.text = $"\n\n{Translator.GetString("LobbyCode")}:\n{GameCode.IntToGameName(AmongUsClient.Instance.GameId)}"; + } } } @@ -276,7 +284,7 @@ internal static class TextBoxTMPSetTextPatch { try { - bool open = HudManager.Instance?.Chat?.IsOpenOrOpening ?? false; + bool open = FastDestroyableSingleton.Instance?.Chat?.IsOpenOrOpening ?? false; PlaceHolderText?.gameObject.SetActive(open); CommandInfoText?.gameObject.SetActive(open); AdditionalInfoText?.gameObject.SetActive(open); diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index c2e981c72..086b6e9af 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -2338,7 +2338,7 @@ "Message.YTPlanCanNotFindRoleThePlayerEnter": "Could not find the role you searching\\nUse command /r to show role list", "Message.YTPlanNotice": "Note: The [YouTuber Plan] is enabled in this lobby, which means the Host can specify their role in the next game to make it easier to get content. In case the Host abuses this feature, please exit the game or report it.\\nCurrent Creator Credentials:", "Warning.InvalidRpc": "Kicked {0} because an invalid RPC was received.\\nPlease check that no mods other than EHR are installed.", - "Warning.ReportDeadBodyHack": "Banned hacker {0} for sending too many ReportDeadBody RPCs.\\nUse SHIFT + L + ENTER to end the game or SHIFT + M + ENTER to end the meeting.\\nSorry for the inconvenience.", + "Warning.TooManyRPCs": "Kicked {0} because too many RPCs were received from them.", "Warning.NoModHost": "EHR is not installed on the host", "Warning.MismatchedVersion": "{0} has a different version than {1}", "Warning.AutoExitAtMismatchedVersion": "The host has no or a different version of {0}\\nYou will be kicked in {1}", @@ -2511,6 +2511,7 @@ "CommandArgs.DeathNote.Name": "The real name to guess. If it's correct, you kill the player with this name.", "CommandArgs.Whisper.Id": "The player's ID to whisper to", "CommandArgs.Whisper.Message": "The message to whisper", + "CommandArgs.Spectate.Id": "The player's ID who will be forced to spectate (leave blank to use it on yourself)", "CommandArgs.JailTalk.Message": "The message to send to the Jailor/jailed player", "CommandArgs.EightBall.Question": "The question to ask", "CommandArgs.AddTag.Id": "The player's ID to add a tag to", @@ -2831,6 +2832,8 @@ "SpectateCommand.WasSpectatingLastRound": "<#ff0000>X You were spectating last round. You can't spectate again.", "SpectateCommand.Removed": "<#ffab1b>- You are no longer spectating.", "SpectateCommand.Success": "<#00ff00>✓ You are now spectating for the next game.", + "SpectateCommand.ForcedSpectator": "<#00ff00>✓ {0} will spectate for the next game.", + "SpectateCommand.RemovedForcedSpectator": "<#ffab1b>- {0} is no longer a forced spectator.", "ReadyClientsInfo": "\\nReady clients: {0} (Timeout in {1}s)", "MessageTooLong": "Failed to send chat message. The size of the RPC would've exceeded the limit specified in System Settings.", "Vision": "Vision", @@ -9279,7 +9282,8 @@ "EnchanterInfoLong": "(Coven):\\nEnchant a player, making them appear as a Coven member to crew investigators. When an enchanted player is killed, their death reason will be random. When enhanced by the Necronomicon, you gain the ability to kill (by double clicking with your kill button on someone).", "Siren": "Siren", "SirenInfo": "Sing to convert with stages", - "SirenInfoLong": "(Coven):\\nThe Siren has an entrancing voice. Use the Sing ability (via the Vanish button) to affect nearby players.\\n- On the first hearing, non-Coven players are blinded and slowed for a short time.\\n- On the second, they are again blinded and slowed, and cannot report bodies temporarily.\\n- On the third, they become Entranced, converting to the Coven.\\nWith the Necronomicon, the Siren gains the ability to kill normally, and her Sing is boosted by two stages:\\n- Players who've heard no songs are treated as if they've heard two.\\n- Players who've heard one song are treated as if they've heard three and become Entranced.\\nThe Siren sees a tracker near each player's name showing their current stage (0/3, 1/3, 2/3, Entranced).", + "SirenInfoLong": "(Coven):\\nThe Siren has an entrancing voice. Use the Sing ability (via the Vanish button) to affect nearby players.\\n- On the first hearing, non-Coven players are blinded and slowed for a short time.\\n- On the second, they are again blinded and slowed, and cannot report bodies temporarily.\\n- On the third, they become Entranced, converting to the Coven.\\n\\nWith the Necronomicon, the Siren gains the ability to kill normally, and her Sing is boosted by two stages:\\n- Players who've heard no songs are treated as if they've heard two.\\n- Players who've heard one song are treated as if they've heard three and become Entranced.\\nThe Siren sees a tracker near each player's name showing their current stage (0/3, 1/3, 2/3, Entranced).", + "Siren.SingRange": "Sing Affect Range", "Siren.Stage1EffectsLength": "Length of Stage 1 Effects", "Siren.Stage2EffectsLength": "Length of Stage 2 Effects", "Siren.ReducedVision": "Reduced Vision", @@ -9301,7 +9305,7 @@ "Entranced-": "Entranced ", "Wyrd": "Wyrd", "WyrdInfo": "Mark players; doom triggers on action.", - "WyrdInfoLong": "(Coven):\\nThe Wyrd manipulates destiny by marking players with Fate’s Countdown. Marked players share a hidden countdown that begins when the first player is marked. Once the countdown expires, any marked player who tries to complete a task, report a body, or press the meeting button will instantly die.\\nThe Wyrd can only mark a certain number of players at a time. To mark someone, shapeshift into them.\\nWhen 2 or more players are marked, they can see the countdown.\\n\\nWith the Necronomicon, the Wyrd can kill directly using the kill button.\\n\\nNotes:\\nAll marked players share the same countdown.\\nA small bonus to he countdown is added for each additional marked player.\\nThe countdown pauses during meetings.", + "WyrdInfoLong": "(Coven):\\nThe Wyrd manipulates destiny by marking players with Fate’s Countdown. Marked players share a hidden countdown that begins when the first player is marked. Once the countdown expires, any marked player who tries to complete a task, report a body, or press the meeting button will instantly die.\\n\\nThe Wyrd can only mark a certain number of players at a time. To mark someone, shapeshift into them.\\nWhen 2 or more players are marked, they can see the countdown.\\n\\nWith the Necronomicon, the Wyrd can kill directly using the kill button.\\n\\nNotes:\\nAll marked players share the same countdown.\\nA small bonus to he countdown is added for each additional marked player.\\nThe countdown pauses during meetings.", "Wyrd.MaxMarkedPlayersAtOnce": "Max Marked Players At Once", "Wyrd.CountdownTime": "Countdown Time", "Wyrd.TimeAdditionOnMoreMark": "Time Addition On More Marks", @@ -9425,6 +9429,7 @@ "DiscoChangeInterval": "Disco Change Color Interval", "Modifier": "\nModifier: ", "Modifiers": "\nModifiers: ", + "LobbyCode": "Lobby Code", "RoleSelected": "{0}'s role in the next game will be {1}", "UpReplacedMessage": "/up was replaced by /setrole which allows you to set anyone's role for any game by typing '/setrole (ID) (ROLE)'. You can view ID's with /id.\nTo set your own role like with /up, you need to do '/setrole 0 (ROLE)'.", "PingText": "Ping", diff --git a/Roles/Coven/Siren.cs b/Roles/Coven/Siren.cs index 764c29560..c912baf92 100644 --- a/Roles/Coven/Siren.cs +++ b/Roles/Coven/Siren.cs @@ -10,20 +10,20 @@ public class Siren : Coven public static bool On; private static List Instances = []; - public static OptionItem AbilityCooldown; - public static OptionItem SingRange; - public static OptionItem Stage1EffectsLength; - public static OptionItem Stage2EffectsLength; - public static OptionItem ReducedVision; - public static OptionItem ReducedSpeed; + private static OptionItem AbilityCooldown; + private static OptionItem SingRange; + private static OptionItem Stage1EffectsLength; + private static OptionItem Stage2EffectsLength; + private static OptionItem ReducedVision; + private static OptionItem ReducedSpeed; public static OptionItem EntrancedCanKillCoven; public static OptionItem CovenKnowEntranced; public static OptionItem EntrancedKnowEntranced; public static OptionItem CovenCanKillEntranced; public static OptionItem EntrancedCountMode; public static OptionItem EntrancedKnowCoven; - public static OptionItem CanVentBeforeNecronomicon; - public static OptionItem CanVentAfterNecronomicon; + private static OptionItem CanVentBeforeNecronomicon; + private static OptionItem CanVentAfterNecronomicon; private static readonly string[] CovenKnowEntrancedOptions = [