diff --git a/src/Managers/KickCooldownManager.cs b/src/Managers/KickCooldownManager.cs new file mode 100644 index 0000000..5092a35 --- /dev/null +++ b/src/Managers/KickCooldownManager.cs @@ -0,0 +1,36 @@ +using System.Collections; +using BepInEx.Unity.IL2CPP.Utils; +using BetterAmongUs.Patches.Gameplay.UI.Settings; +using UnityEngine; + +namespace BetterAmongUs.Managers; + +/// +/// Manages the cooldown period between player kick actions. +/// This cooldown is necessary to avoid bans by Innersloth anti-cheat. +/// +internal static class KickCooldownManager +{ + private static float CooldownSeconds => BetterGameSettings.KickCooldown.GetFloat(); + + private static float _lastTriggerTime; + + internal static bool IsReady() => Time.time - _lastTriggerTime > CooldownSeconds; + internal static void Trigger() => _lastTriggerTime = Time.time; + private static float TimeToNextAvailableTrigger() => CooldownSeconds - (Time.time - _lastTriggerTime); + + internal static void ScheduleAction(Action action) + { + AmongUsClient.Instance.StartCoroutine(RunScheduledAction(action)); + } + + private static IEnumerator RunScheduledAction(Action action) + { + while (!IsReady()) + { + yield return new WaitForSeconds(TimeToNextAvailableTrigger()); + } + Trigger(); + action(); + } +} \ No newline at end of file diff --git a/src/Modules/AntiCheat/BetterAntiCheat.cs b/src/Modules/AntiCheat/BetterAntiCheat.cs index 92bb9cd..374619e 100644 --- a/src/Modules/AntiCheat/BetterAntiCheat.cs +++ b/src/Modules/AntiCheat/BetterAntiCheat.cs @@ -51,31 +51,31 @@ internal static class BetterAntiCheat { string reason = TranslationStrings.AntiCheat_Reason_SickoMenuUser.LocalizedString; string kickMessage = TranslationStrings.AntiCheat_KickMessage.Format(TranslationStrings.AntiCheat_ByAntiCheat, reason); - player.Kick(true, kickMessage, true); + player.TryKick(true, kickMessage, true); } else if (BetterDataManager.Files.BetterDataFile.AUMData.Any(info => info.CheckPlayerData(player.Data))) { string reason = TranslationStrings.AntiCheat_Reason_AUMUser.LocalizedString; string kickMessage = TranslationStrings.AntiCheat_KickMessage.Format(TranslationStrings.AntiCheat_ByAntiCheat, reason); - player.Kick(true, kickMessage, true); + player.TryKick(true, kickMessage, true); } else if (BetterDataManager.Files.BetterDataFile.KNData.Any(info => info.CheckPlayerData(player.Data))) { string reason = TranslationStrings.AntiCheat_Reason_KNUser.LocalizedString; string kickMessage = TranslationStrings.AntiCheat_KickMessage.Format(TranslationStrings.AntiCheat_ByAntiCheat, reason); - player.Kick(true, kickMessage, true); + player.TryKick(true, kickMessage, true); } else if (BetterDataManager.Files.BetterDataFile.MMCData.Any(info => info.CheckPlayerData(player.Data))) { string reason = TranslationStrings.AntiCheat_Reason_MMCUser.LocalizedString; string kickMessage = TranslationStrings.AntiCheat_KickMessage.Format(TranslationStrings.AntiCheat_ByAntiCheat, reason); - player.Kick(true, kickMessage, true); + player.TryKick(true, kickMessage, true); } else if (BetterDataManager.Files.BetterDataFile.CheatData.Any(info => info.CheckPlayerData(player.Data))) { string reason = TranslationStrings.AntiCheat_Reason_KnownCheater.LocalizedString; string kickMessage = TranslationStrings.AntiCheat_KickMessage.Format(TranslationStrings.AntiCheat_ByAntiCheat, reason); - player.Kick(true, kickMessage, true); + player.TryKick(true, kickMessage, true); } } } diff --git a/src/MonoScripts/PlayerInfoDisplay.cs b/src/MonoScripts/PlayerInfoDisplay.cs index 4432f7d..9c33aac 100644 --- a/src/MonoScripts/PlayerInfoDisplay.cs +++ b/src/MonoScripts/PlayerInfoDisplay.cs @@ -277,7 +277,7 @@ internal class PlayerInfoDisplay : MonoBehaviour if (GameState.IsHost && BetterGameSettings.InvalidFriendCode.GetBool()) { string kickMessage = TranslationStrings.AntiCheat_KickMessage.Format(TranslationStrings.AntiCheat_ByAntiCheat, TranslationStrings.AntiCheat_Reason_InvalidFriendCode); - _player.Kick(true, kickMessage, true); + _player.TryKick(true, kickMessage, true); } } diff --git a/src/Patches/Gameplay/Anticheat/CheckPlayerLevelPatch.cs b/src/Patches/Gameplay/Anticheat/CheckPlayerLevelPatch.cs index 42d55a5..e18f9d2 100644 --- a/src/Patches/Gameplay/Anticheat/CheckPlayerLevelPatch.cs +++ b/src/Patches/Gameplay/Anticheat/CheckPlayerLevelPatch.cs @@ -1,6 +1,6 @@ -using BetterAmongUs.Modules; +using BetterAmongUs.Utilities; +using BetterAmongUs.Modules; using BetterAmongUs.Patches.Gameplay.UI.Settings; -using BetterAmongUs.Utilities; using HarmonyLib; namespace BetterAmongUs.Patches.Gameplay.Anticheat; @@ -20,7 +20,7 @@ internal static class CheckPlayerLevelPatch // Kick players below minimum level if (!__instance.IsLocalPlayer() && BetterGameSettings.KickLevel.GetBool() && __instance.Data.PlayerLevel < BetterGameSettings.KickLevelBelow.GetInt()) { - __instance.Kick(setReasonInfo: $" is level {__instance.Data.PlayerLevel}, level must be equal or above {BetterGameSettings.KickLevelBelow.GetInt()} to join"); + __instance.TryKick(setReasonInfo: $" is level {__instance.Data.PlayerLevel}, level must be equal or above {BetterGameSettings.KickLevelBelow.GetInt()} to join"); } } } diff --git a/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs b/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs index 99b5da5..e3bf117 100644 --- a/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs +++ b/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs @@ -1,7 +1,9 @@ +using System.Collections; using BepInEx.Unity.IL2CPP.Utils; using BetterAmongUs.Data; using BetterAmongUs.Data.Config; using BetterAmongUs.Generated; +using BetterAmongUs.Managers; using BetterAmongUs.Modules; using BetterAmongUs.Modules.Support; using BetterAmongUs.MonoScripts.Extended; @@ -10,7 +12,6 @@ using BetterAmongUs.Patches.Gameplay.UI.Settings; using BetterAmongUs.Utilities; using HarmonyLib; using InnerNet; -using System.Collections; namespace BetterAmongUs.Patches.Gameplay.Player; @@ -38,6 +39,7 @@ internal static class PlayerJoinAndLeftPatch { if (GameState.IsHost) { + KickCooldownManager.Trigger(); __instance.StartCoroutine(CoCheckPlayerOnJoin(data)); } } @@ -59,8 +61,7 @@ internal static class PlayerJoinAndLeftPatch if (player != null) { if (TextFileHandler.CompareStringMatch(BetterDataManager.Files.banPlayerListFilePath, - BAUPlugin.AllPlayerControls.Select(player => player.Data.FriendCode) - .Concat(BAUPlugin.AllPlayerControls.Select(player => player.GetHashPuid())).ToArray())) + [player.Data.FriendCode, player.GetHashPuid()])) { player.Kick(true, TranslationStrings.AntiCheat_BanPlayerListMessage.LocalizedString, bypassDataCheck: true); yield break; @@ -75,7 +76,7 @@ internal static class PlayerJoinAndLeftPatch { if (TextFileHandler.CompareStringFilters(BetterDataManager.Files.banNameListFilePath, [player.Data.PlayerName])) { - player?.Kick(true, TranslationStrings.AntiCheat_BanPlayerListMessage.LocalizedString, bypassDataCheck: true); + player.Kick(true, TranslationStrings.AntiCheat_BanNameListMessage.LocalizedString, bypassDataCheck: true); } } } @@ -86,6 +87,8 @@ internal static class PlayerJoinAndLeftPatch [HarmonyPostfix] private static void AmongUsClient_OnPlayerLeft_Postfix(ClientData data, DisconnectReasons reason) { + KickCooldownManager.Trigger(); + // Reclaim favorite color when player leaves in lobby if (GameState.IsLobby) { diff --git a/src/Patches/Gameplay/UI/Settings/GameSettingsPatch.cs b/src/Patches/Gameplay/UI/Settings/GameSettingsPatch.cs index 05417a2..ab230c1 100644 --- a/src/Patches/Gameplay/UI/Settings/GameSettingsPatch.cs +++ b/src/Patches/Gameplay/UI/Settings/GameSettingsPatch.cs @@ -11,6 +11,7 @@ namespace BetterAmongUs.Patches.Gameplay.UI.Settings; // Custom game settings definitions internal sealed class BetterGameSettings { + internal static OptionFloatItem? KickCooldown; internal static OptionStringItem? WhenCheating; internal static OptionCheckboxItem? InvalidFriendCode; internal static OptionCheckboxItem? UseBanPlayerList; @@ -67,6 +68,7 @@ internal static class GameSettingsPatch if (IsPreload || GameState.IsHost) { OptionTitleItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_TextHeader_HostOnly); + BetterGameSettings.KickCooldown = OptionFloatItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_KickCooldown, (0f, 3f, 0.1f), 2f, ("", "s")); BetterGameSettings.WhenCheating = OptionStringItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_WhenCheating, [TranslationStrings.BetterSetting_Setting_WhenCheating_Notify, TranslationStrings.BetterSetting_Setting_WhenCheating_Kick, TranslationStrings.BetterSetting_Setting_WhenCheating_Ban], 2); BetterGameSettings.InvalidFriendCode = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_InvalidFriendCode, true); diff --git a/src/Resources/Lang/en_US.json b/src/Resources/Lang/en_US.json index 3042bc2..ec8ec52 100644 --- a/src/Resources/Lang/en_US.json +++ b/src/Resources/Lang/en_US.json @@ -79,6 +79,7 @@ "BetterSetting.TextHeader.HostOnly": "Host Only", "BetterSetting.TextHeader.Detections": "Detections", + "BetterSetting.Setting.KickCooldown": "Kick cooldown", "BetterSetting.Setting.WhenCheating": "When a player is caught cheating", "BetterSetting.Setting.WhenCheating.Notify": "Notify", "BetterSetting.Setting.WhenCheating.Kick": "Kick", diff --git a/src/Utilities/PlayerControlUtils.cs b/src/Utilities/PlayerControlUtils.cs index 0fc610c..128d3b9 100644 --- a/src/Utilities/PlayerControlUtils.cs +++ b/src/Utilities/PlayerControlUtils.cs @@ -1,6 +1,7 @@ using AmongUs.GameOptions; using BetterAmongUs.Data; using BetterAmongUs.Generated; +using BetterAmongUs.Managers; using BetterAmongUs.Modules; using BetterAmongUs.MonoScripts.Extended; using BetterAmongUs.Patches.Gameplay.Player; @@ -127,42 +128,113 @@ internal static class PlayerControlUtils return true; } + /// + /// Kicks a player from the game as soon as it is safe to do so. + /// + /// The player to kick. + /// Whether to ban the player. + /// Custom reason message for the kick. + /// Whether this is an anti-cheat related kick. + /// Whether to bypass the data collection check. + /// Whether to force a ban regardless of settings. + internal static void Kick(this PlayerControl player, bool ban = false, string setReasonInfo = "", bool antiCheatBan = false, bool bypassDataCheck = false, bool forceBan = false) + { + if (!player.CanKick(ban, antiCheatBan, bypassDataCheck, forceBan, out var shouldBan)) return; + KickCooldownManager.ScheduleAction(() => { player.PerformKick(shouldBan, setReasonInfo, antiCheatBan); }); + } + + /// + /// Kicks a player from the game immediately if it is safe to do so, otherwise does nothing. + /// This should be used instead of in places where this method is repeatedly called, like Update methods for example. + /// + /// The player to kick. + /// Whether to ban the player. + /// Custom reason message for the kick. + /// Whether this is an anti-cheat related kick. + /// Whether to bypass the data collection check. + /// Whether to force a ban regardless of settings. + internal static void TryKick(this PlayerControl player, bool ban = false, string setReasonInfo = "", bool antiCheatBan = false, bool bypassDataCheck = false, bool forceBan = false) + { + if (!KickCooldownManager.IsReady()) return; + if (!player.CanKick(ban, antiCheatBan, bypassDataCheck, forceBan, out var shouldBan)) return; + KickCooldownManager.Trigger(); + player.PerformKick(shouldBan, setReasonInfo, antiCheatBan); + } + + /// + /// Kicks a player from the game immediately, regardless of cooldown. + /// Only use this if the target player is extremely dangerous and must be removed immediately. + /// + /// The player to kick. + /// Whether to ban the player. + /// Custom reason message for the kick. + /// Whether this is an anti-cheat related kick. + /// Whether to bypass the data collection check. + /// Whether to force a ban regardless of settings. + internal static void KickImmediate(this PlayerControl player, bool ban = false, string setReasonInfo = "", bool antiCheatBan = false, bool bypassDataCheck = false, bool forceBan = false) + { + if (!player.CanKick(ban, antiCheatBan, bypassDataCheck, forceBan, out var shouldBan)) return; + player.PerformKick(shouldBan, setReasonInfo, antiCheatBan); + } + + /// + /// Determines whether the specified player can be kicked from the game and sets the outcome. + /// + /// The player to evaluate. + /// Whether to ban the player. + /// Whether this is an anti-cheat related kick. + /// Whether to bypass the data collection check. + /// Whether to force a ban regardless of settings. + /// + /// When the method returns, this value indicates whether the player should be banned + /// based on the provided flags and internal checks. + /// + /// true if the player can be kicked/banned, false otherwise. + private static bool CanKick(this PlayerControl player, bool ban, bool antiCheatBan, bool bypassDataCheck, + bool forceBan, out bool shouldBan) + { + shouldBan = ban || forceBan; + + if (!GameState.IsHost || player.IsLocalPlayer() || (!player.DataIsCollected() && !bypassDataCheck) || player.IsHost() || player.isDummy) + return false; + + if (forceBan || !antiCheatBan) return true; + + return HandleAntiCheatBanCheck(shouldBan, out shouldBan); + } + + /// + /// Determines whether a player should be banned when an anti‑cheat check is performed. + /// + /// Whether the player has already been flagged for banning before this method is called. + /// The output value that indicates whether the player should ultimately be banned. + /// + /// true if the player can be kicked or banned.
+ /// false if is set to 0 (Notify). + ///
+ private static bool HandleAntiCheatBanCheck(bool ban, out bool shouldBan) + { + shouldBan = ban && BetterGameSettings.WhenCheating.GetStringIndex() == 2; + return BetterGameSettings.WhenCheating.GetStringIndex() != 0; + } + /// /// Kicks a player from the game with optional ban and anti-cheat features. /// /// The player to kick. /// Whether to ban the player. /// Custom reason message for the kick. - /// Whether this is an anti-cheat related kick. - /// Whether to bypass the data collection check. - /// Whether to force a ban regardless of settings. - internal static void Kick(this PlayerControl player, bool ban = false, string setReasonInfo = "", bool AntiCheatBan = false, bool bypassDataCheck = false, bool forceBan = false) + /// Whether this is an anti-cheat related kick. + private static void PerformKick(this PlayerControl player, bool ban = false, string setReasonInfo = "", bool antiCheatBan = false) { - var Ban = ban || forceBan; - - if (!GameState.IsHost || player.IsLocalPlayer() || (!player.DataIsCollected() && !bypassDataCheck) || player.IsHost() || player.isDummy) - { - return; - } - - if (AntiCheatBan) - { - if (BetterGameSettings.WhenCheating.GetStringIndex() == 0 && !forceBan) - { - return; - } - - Ban = (Ban && BetterGameSettings.WhenCheating.GetStringIndex() == 2) || forceBan; - } - if (setReasonInfo != "") { - PlayerJoinAndLeftPatch.BetterShowNotification(player.Data, forceReasonText: string.Format(setReasonInfo, Ban ? TranslationStrings.AntiCheat_Ban.LocalizedString.ToLower() : TranslationStrings.AntiCheat_Kick.LocalizedString.ToLower())); + PlayerJoinAndLeftPatch.BetterShowNotification(player.Data, forceReasonText: string.Format(setReasonInfo, ban ? TranslationStrings.AntiCheat_Ban.LocalizedString.ToLower() : TranslationStrings.AntiCheat_Kick.LocalizedString.ToLower())); } - AmongUsClient.Instance.KickPlayer(player.GetClientId(), Ban); + AmongUsClient.Instance.KickPlayer(player.GetClientId(), ban); - player.ExtendedData().AntiCheatInfo.BannedByAntiCheat = AntiCheatBan; + player.ExtendedData().AntiCheatInfo.BannedByAntiCheat = antiCheatBan; } /// diff --git a/src/build/Generated/TranslationStrings.cs b/src/build/Generated/TranslationStrings.cs index 9878aeb..2cba8ab 100644 --- a/src/build/Generated/TranslationStrings.cs +++ b/src/build/Generated/TranslationStrings.cs @@ -388,6 +388,11 @@ public static class TranslationStrings /// public static readonly TranslationString BetterSetting_TextHeader_Detections = new("BetterSetting.TextHeader.Detections"); + /// + /// Base Translation: Kick cooldown + /// + public static readonly TranslationString BetterSetting_Setting_KickCooldown = new("BetterSetting.Setting.KickCooldown"); + /// /// Base Translation: When a player is caught cheating ///