// AntiTeamGuard v1.0.1 using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; // ____ _ _ _ // / ___|(_) __ _(_) | ___ // \___ \| |/ _` | | |/ _ \ // ___) | | (_| | | | (_) | // |____/|_|\__, |_|_|\___/ // |___/ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ // ✦ RUST PLUGINS ✦ // ✦ Sigilo.dev ✦ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ namespace Oxide.Plugins { [Info("AntiTeamGuard", "Sigilo", "1.0.1")] [Description("Detects teaming violations and applies progressive damage penalties")] public class AntiTeamGuard : RustPlugin { [PluginReference] private Plugin Clans; private static AntiTeamGuard _instance; private Configuration _config; private ProximityTracker _proximityTracker; private GameObject _proximityObject; private Dictionary> _pairData = new Dictionary>(); private Dictionary _penalties = new Dictionary(); private Dictionary> _voiceChatTicks = new Dictionary>(); private Dictionary _corpseLooting = new Dictionary(); private Dictionary _cooldowns = new Dictionary(); private Dictionary _scanCooldowns = new Dictionary(); private const string PERMISSION_ADMIN = "antiteamguard.admin"; private const string PERMISSION_BYPASS = "antiteamguard.bypass"; private const string PERMISSION_TEAMSTATUS = "antiteamguard.teamstatus"; private const string DATA_PAIR = "AntiTeamGuard_PairData"; private const string DATA_PENALTIES = "AntiTeamGuard_Penalties"; #region Initialization void Init() { _instance = this; permission.RegisterPermission(PERMISSION_ADMIN, this); permission.RegisterPermission(PERMISSION_BYPASS, this); permission.RegisterPermission(PERMISSION_TEAMSTATUS, this); } void OnServerInitialized(bool initial) { LoadData(); if (_config.Proximity.Enabled) { _proximityObject = new GameObject("AntiTeamGuard_ProximityTracker"); _proximityTracker = _proximityObject.AddComponent(); _proximityTracker.Initialize(this, _config.Proximity); } AddCovalenceCommand("atg.status", nameof(CommandStatus)); AddCovalenceCommand("teamstatus", nameof(CommandPlayerStatus)); AddCovalenceCommand("atg.check", nameof(CommandCheck)); AddCovalenceCommand("atg.clear", nameof(CommandClear)); AddCovalenceCommand("atg.penalty", nameof(CommandPenalty)); CleanupExpiredPenalties(); timer.Every(300f, CleanupExpiredPenalties); timer.Every(600f, SaveData); } void Unload() { SaveData(); if (_proximityTracker != null) _proximityTracker.Destroy(); if (_proximityObject != null) UnityEngine.Object.Destroy(_proximityObject); _instance = null; } void OnNewSave(string filename) { _pairData.Clear(); SaveData(); Puts("Wipe detected - cleared pair data (penalties preserved)"); } #endregion #region Configuration protected override void LoadDefaultConfig() => _config = Configuration.Default(); protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new Exception(); SaveConfig(); } catch { PrintError("Invalid configuration - loading defaults"); LoadDefaultConfig(); } } protected override void SaveConfig() => Config.WriteObject(_config); private class Configuration { [JsonProperty("Team Size Limit")] public int TeamLimit { get; set; } = 8; [JsonProperty("Ignore Admins")] public bool IgnoreAdmins { get; set; } = true; [JsonProperty("Server Name (for Discord)")] public string ServerName { get; set; } = ""; [JsonProperty("Discord Webhook URL")] public string DiscordWebhook { get; set; } = ""; [JsonProperty("Public Sanction Webhook URL")] public string PublicDiscordWebhook { get; set; } = ""; [JsonProperty("Detection Settings")] public DetectionSettings Detection { get; set; } = new DetectionSettings(); [JsonProperty("Proximity Tracking")] public ProximitySettings Proximity { get; set; } = new ProximitySettings(); [JsonProperty("Penalty Settings")] public PenaltySettings Penalties { get; set; } = new PenaltySettings(); [JsonProperty("Alert Weights")] public Dictionary Weights { get; set; } = new Dictionary { { "CodeLock", 0.4f }, { "Cupboard", 0.3f }, { "Turret", 0.3f }, { "SharedVehicle", 0.3f }, { "SleepingBag", 0.3f }, { "TeamKill", 0.5f }, { "Revive", 0.35f }, { "Looting", 0.25f }, { "VoiceChat", 0.2f }, { "ProximityTime", 0.3f }, { "ProximityKills", 0.35f } }; [JsonProperty("Penalty Threshold (total weight to trigger penalty)")] public float PenaltyThreshold { get; set; } = 1.0f; [JsonProperty("Offense Decay (Days per level)")] public int OffenseDecayDays { get; set; } = 7; public static Configuration Default() { var config = new Configuration(); config.Penalties.Tiers = new List { new OffenseTier { Offense = 1, DamageReduction = 10, DurationHours = 2, BanDurationMinutes = 10 }, new OffenseTier { Offense = 2, DamageReduction = 20, DurationHours = 4, BanDurationMinutes = 30 }, new OffenseTier { Offense = 3, DamageReduction = 30, DurationHours = 8, BanDurationMinutes = 60 }, new OffenseTier { Offense = 4, DamageReduction = 40, DurationHours = 12, BanDurationMinutes = 120 }, new OffenseTier { Offense = 5, DamageReduction = 50, DurationHours = 16, BanDurationMinutes = 180 }, new OffenseTier { Offense = 6, DamageReduction = 60, DurationHours = 24, BanDurationMinutes = 300 }, new OffenseTier { Offense = 7, DamageReduction = 70, DurationHours = 32, BanDurationMinutes = 480 }, new OffenseTier { Offense = 8, DamageReduction = 80, DurationHours = 40, BanDurationMinutes = 720 }, new OffenseTier { Offense = 9, DamageReduction = 90, DurationHours = 48, BanDurationMinutes = 1080 }, new OffenseTier { Offense = 10, DamageReduction = 95, DurationHours = 48, BanDurationMinutes = 1440 } }; config.Detection.VoiceChat = false; config.Proximity.TimeThreshold = 600f; config.Weights["SharedVehicle"] = 0.3f; return config; } } private class DetectionSettings { [JsonProperty("Code Lock Detection")] public bool CodeLock { get; set; } = true; [JsonProperty("Cupboard Detection")] public bool Cupboard { get; set; } = true; [JsonProperty("Turret Detection")] public bool Turret { get; set; } = true; [JsonProperty("Shared Vehicle Detection")] public bool SharedVehicle { get; set; } = true; [JsonProperty("Sleeping Bag Detection")] public bool SleepingBag { get; set; } = true; [JsonProperty("Team Kill Detection")] public bool TeamKill { get; set; } = true; [JsonProperty("Revive Detection")] public bool Revive { get; set; } = true; [JsonProperty("Shared Looting Detection")] public bool Looting { get; set; } = true; [JsonProperty("Voice Chat Detection")] public bool VoiceChat { get; set; } = true; [JsonProperty("Team Kill - Max Distance Between Players")] public float TeamKillMaxDistance { get; set; } = 8f; [JsonProperty("Team Kill - Min Distance To Target")] public float TeamKillMinTargetDistance { get; set; } = 15f; [JsonProperty("Voice Chat - Seconds To Trigger Alert")] public int VoiceChatSeconds { get; set; } = 60; [JsonProperty("Looting - Window Seconds")] public float LootingWindow { get; set; } = 5f; } private class ProximitySettings { [JsonProperty("Enabled")] public bool Enabled { get; set; } = true; [JsonProperty("Tracking Distance")] public float Distance { get; set; } = 25f; [JsonProperty("Check Visibility")] public bool CheckVisibility { get; set; } = true; [JsonProperty("Time Threshold (seconds)")] public float TimeThreshold { get; set; } = 900f; [JsonProperty("Kill Threshold")] public int KillThreshold { get; set; } = 3; } private class PenaltySettings { [JsonProperty("Enable Damage Penalties")] public bool Enabled { get; set; } = true; [JsonProperty("Enable Ban Penalties")] public bool BanEnabled { get; set; } = false; [JsonProperty("Offense Tiers")] public List Tiers { get; set; } = new List(); public OffenseTier GetTier(int offenseCount) { if (Tiers == null || Tiers.Count == 0) return null; var sorted = Tiers.OrderBy(t => t.Offense).ToList(); return sorted.LastOrDefault(t => t.Offense <= offenseCount) ?? sorted.Last(); } } private class OffenseTier { [JsonProperty("Offense Number")] public int Offense { get; set; } [JsonProperty("Damage Reduction Percent")] public int DamageReduction { get; set; } [JsonProperty("Duration (Hours)")] public int DurationHours { get; set; } [JsonProperty("Ban Duration (Minutes)")] public int BanDurationMinutes { get; set; } } #endregion #region Data Classes private class PlayerPairData { public List Alerts { get; set; } = new List(); public float AlertScore { get; set; } = 0f; public DateTime LastDecay { get; set; } = DateTime.UtcNow; public Dictionary AlertCooldowns { get; set; } = new Dictionary(); public float ProximityTime { get; set; } = 0f; public int ProximityKills { get; set; } = 0; public int SelfKills { get; set; } = 0; public bool Warned { get; set; } = false; public DateTime LastActivity { get; set; } = DateTime.UtcNow; } private class PlayerPenalty { public int OffenseCount { get; set; } = 0; public float CurrentReduction { get; set; } = 0f; public bool IsBan { get; set; } = false; public DateTime ExpiresAt { get; set; } = DateTime.MinValue; public DateTime LastOffenseDate { get; set; } = DateTime.MinValue; public bool PendingExpirationNotice { get; set; } = false; } #endregion #region Data Management void LoadData() { try { if (Interface.Oxide.DataFileSystem.ExistsDatafile(DATA_PAIR)) _pairData = Interface.Oxide.DataFileSystem.ReadObject>>(DATA_PAIR) ?? new Dictionary>(); if (Interface.Oxide.DataFileSystem.ExistsDatafile(DATA_PENALTIES)) _penalties = Interface.Oxide.DataFileSystem.ReadObject>(DATA_PENALTIES) ?? new Dictionary(); float loadMultiplier = _config.TeamLimit >= 2 ? 1.0f : Math.Max(1.0f, 2.0f / _config.TeamLimit); foreach (var outer in _pairData) { foreach (var inner in outer.Value) { if (inner.Value.AlertScore == 0f && inner.Value.Alerts.Count > 0) { foreach (var alert in inner.Value.Alerts) { if (_config.Weights.TryGetValue(alert, out var weight)) inner.Value.AlertScore += weight * loadMultiplier; } } } } } catch (Exception ex) { PrintError($"Error loading data: {ex.Message}"); _pairData = new Dictionary>(); _penalties = new Dictionary(); } } void SaveData() { try { Interface.Oxide.DataFileSystem.WriteObject(DATA_PAIR, _pairData); Interface.Oxide.DataFileSystem.WriteObject(DATA_PENALTIES, _penalties); } catch (Exception ex) { PrintError($"Error saving data: {ex.Message}"); } } void CleanupExpiredPenalties() { var now = DateTime.UtcNow; var expired = _penalties.Where(p => p.Value.ExpiresAt < now && (p.Value.CurrentReduction > 0 || p.Value.IsBan)).Select(p => p.Key).ToList(); bool changed = false; foreach (var id in expired) { _penalties[id].CurrentReduction = 0f; _penalties[id].IsBan = false; var player = BasePlayer.FindByID(id); if (player != null && player.IsConnected) { player.ChatMessage("Your teaming penalty has expired."); } else { _penalties[id].PendingExpirationNotice = true; } changed = true; } if (changed) SaveData(); } private void CheckOffenseDecay(ulong playerId) { if (!_penalties.TryGetValue(playerId, out var penalty)) return; if (penalty.OffenseCount <= 0) return; double daysSinceLast = (DateTime.UtcNow - penalty.LastOffenseDate).TotalDays; int decayAmount = (int)(daysSinceLast / _config.OffenseDecayDays); if (decayAmount > 0) { penalty.OffenseCount = Math.Max(0, penalty.OffenseCount - decayAmount); penalty.LastOffenseDate = penalty.LastOffenseDate.AddDays(decayAmount * _config.OffenseDecayDays); SaveData(); } } void OnPlayerConnected(BasePlayer player) { CheckOffenseDecay(player.userID); if (_penalties.TryGetValue(player.userID, out var penalty)) { if (penalty.PendingExpirationNotice) { penalty.PendingExpirationNotice = false; player.ChatMessage("Your teaming penalty has expired."); SaveData(); } else if (penalty.CurrentReduction > 0 && penalty.ExpiresAt > DateTime.UtcNow) { TimeSpan remaining = penalty.ExpiresAt - DateTime.UtcNow; string durationStr = remaining.TotalMinutes < 60 ? $"{Math.Ceiling(remaining.TotalMinutes)} minutes" : remaining.TotalHours < 24 ? $"{Math.Ceiling(remaining.TotalHours)} hours" : $"{remaining.Days} days"; string msg = $"\u26A0 ACTIVE TEAMING PENALTY \u26A0\n" + $"You have an active penalty for past teaming violations.\n" + $"Status: {penalty.CurrentReduction}% Damage Reduction\n" + $"Time Remaining: {durationStr}"; player.ChatMessage(msg); } } } #endregion #region Pair Data Access private (ulong, ulong) NormalizePair(ulong id1, ulong id2) { return id1 > id2 ? (id1, id2) : (id2, id1); } private PlayerPairData GetPairData(ulong id1, ulong id2) { var (primary, secondary) = NormalizePair(id1, id2); if (!_pairData.TryGetValue(primary, out var subDict)) { subDict = new Dictionary(); _pairData[primary] = subDict; } if (!subDict.TryGetValue(secondary, out var data)) { data = new PlayerPairData(); subDict[secondary] = data; } return data; } private float GetTeamLimitMultiplier() { if (_config.TeamLimit >= 2) return 1.0f; return Math.Max(1.0f, 2.0f / _config.TeamLimit); } public void AddAlert(ulong id1, ulong id2, string alertType, bool checkCooldown = true) { var data = GetPairData(id1, id2); ApplyDecay(data); if (checkCooldown && data.AlertCooldowns.TryGetValue(alertType, out var lastTime)) { if ((DateTime.UtcNow - lastTime).TotalMinutes < 15.0) return; } if (_config.Weights.TryGetValue(alertType, out var weight)) { float multiplier = GetTeamLimitMultiplier(); data.AlertScore += weight * multiplier; data.AlertCooldowns[alertType] = DateTime.UtcNow; data.Alerts.Add(alertType); if (data.Alerts.Count > 20) data.Alerts.RemoveAt(0); } data.LastActivity = DateTime.UtcNow; } #endregion #region Helpers private bool ShouldIgnore(BasePlayer player) { if (player == null) return true; if (!player.userID.IsSteamId()) return true; if (_config.IgnoreAdmins && player.IsAdmin) return true; if (permission.UserHasPermission(player.UserIDString, PERMISSION_BYPASS)) return true; return false; } private bool IsTeammate(ulong player1, ulong player2) { if (RelationshipManager.ServerInstance.playerToTeam.TryGetValue(player1, out var team)) { if (team.members.Contains(player2)) return true; } if (Clans != null) { var clanMembers = Clans.Call>("GetClanMembers", player1); if (clanMembers != null && clanMembers.Contains(player2.ToString())) return true; } return false; } private List GetTeammates(ulong playerId) { var teammates = new List(); if (RelationshipManager.ServerInstance.playerToTeam.TryGetValue(playerId, out var team)) teammates.AddRange(team.members); if (Clans != null) { var clanMembers = Clans.Call>("GetClanMembers", playerId); if (clanMembers != null) { foreach (var member in clanMembers) { if (ulong.TryParse(member, out var id) && !teammates.Contains(id)) teammates.Add(id); } } } return teammates; } private bool IsOnCooldown(ulong playerId) { if (_cooldowns.TryGetValue(playerId, out var cooldownEnd)) { if (Time.time < cooldownEnd) return true; _cooldowns.Remove(playerId); } return false; } private void SetCooldown(ulong playerId, float seconds) { _cooldowns[playerId] = Time.time + seconds; } private bool CanScan(ulong playerId, float interval = 1.0f) { if (_scanCooldowns.TryGetValue(playerId, out var nextScan)) { if (Time.time < nextScan) return false; } _scanCooldowns[playerId] = Time.time + interval; return true; } private static bool IsVisible(BasePlayer from, BasePlayer to) { if (to == null || from == null) return false; Vector3 viewPos = from.eyes.position; return to.IsVisible(viewPos, to.CenterPoint()) || to.IsVisible(viewPos, to.eyes.position); } #endregion #region Score Calculation private void ApplyDecay(PlayerPairData data) { double hours = (DateTime.UtcNow - data.LastDecay).TotalHours; if (hours >= 6.0) { int steps = (int)(hours / 6.0); float decay = steps * 0.2f; if (data.AlertScore > 0) { data.AlertScore = Math.Max(0f, data.AlertScore - decay); } data.LastDecay = data.LastDecay.AddHours(steps * 6.0); } } private float CalculateScore(ulong id1, ulong id2) { var data = GetPairData(id1, id2); ApplyDecay(data); return data.AlertScore; } private List GetTeamingPartners(ulong targetId, float threshold) { var partners = new List(); if (_pairData.TryGetValue(targetId, out var subDict)) { foreach (var kvp in subDict) { if (CalculateScore(targetId, kvp.Key) >= threshold) partners.Add(kvp.Key); } } foreach (var kvp in _pairData) { if (kvp.Key <= targetId) continue; if (kvp.Value.ContainsKey(targetId)) { if (CalculateScore(kvp.Key, targetId) >= threshold) partners.Add(kvp.Key); } } return partners; } private void CheckAndApplyPenalty(ulong id1, ulong id2, string alertType) { var cluster = new HashSet { id1, id2 }; cluster.UnionWith(GetTeammates(id1)); cluster.UnionWith(GetTeammates(id2)); if (cluster.Count <= _config.TeamLimit * 2) { var queue = new Queue(cluster); var processed = new HashSet(cluster); float linkThreshold = _config.PenaltyThreshold * 0.4f; int safetyBreak = 0; while (queue.Count > 0 && safetyBreak < 50) { var current = queue.Dequeue(); safetyBreak++; var partners = GetTeamingPartners(current, linkThreshold); foreach (var p in partners) { if (!processed.Contains(p)) { cluster.Add(p); processed.Add(p); var teammates = GetTeammates(p); foreach (var tm in teammates) { if (!processed.Contains(tm)) { cluster.Add(tm); processed.Add(tm); queue.Enqueue(tm); } } queue.Enqueue(p); } } } } if (cluster.Count <= _config.TeamLimit) return; float score = CalculateScore(id1, id2); var data = GetPairData(id1, id2); float warningThreshold = _config.PenaltyThreshold * 0.6f; if (score >= warningThreshold && score < _config.PenaltyThreshold && !data.Warned) { data.Warned = true; SendWarning(id1, id2, score, alertType, data); } if (score >= _config.PenaltyThreshold) { SendDiscordAlert(id1, id2, alertType, score, data, cluster.Count); ApplyPenalty(id1, id2, data); } } #endregion #region Penalty Application private void ApplyPenalty(ulong id1, ulong id2, PlayerPairData pairData) { if (!_config.Penalties.Enabled && !_config.Penalties.BanEnabled) return; var players = new List { id1, id2 }; players.AddRange(GetTeammates(id1)); players.AddRange(GetTeammates(id2)); players = players.Distinct().ToList(); foreach (var playerId in players) { if (!_penalties.TryGetValue(playerId, out var penalty)) { penalty = new PlayerPenalty(); _penalties[playerId] = penalty; } if ((DateTime.UtcNow - penalty.LastOffenseDate).TotalSeconds < 5.0) continue; penalty.OffenseCount++; penalty.LastOffenseDate = DateTime.UtcNow; var tier = _config.Penalties.GetTier(penalty.OffenseCount); if (tier != null) { if (_config.Penalties.BanEnabled) { penalty.IsBan = true; penalty.CurrentReduction = 0f; int durationMinutes = tier.BanDurationMinutes > 0 ? tier.BanDurationMinutes : 1; penalty.ExpiresAt = DateTime.UtcNow.AddMinutes(durationMinutes); string timeStr = durationMinutes >= 60 ? $"{durationMinutes / 60} hours" : $"{durationMinutes} minutes"; var player = BasePlayer.FindByID(playerId); string playerName = player?.displayName; if (player != null && player.IsConnected) { player.Kick($"Teaming Violation: Temp Ban for {timeStr}."); } SendPublicDiscordSanction(playerId, "Temporary Ban", penalty.ExpiresAt, playerName); } else if (_config.Penalties.Enabled) { penalty.IsBan = false; penalty.CurrentReduction = tier.DamageReduction; penalty.ExpiresAt = DateTime.UtcNow.AddHours(tier.DurationHours); var player = BasePlayer.FindByID(playerId); string playerName = player?.displayName; if (player != null && player.IsConnected) { TimeSpan remaining = penalty.ExpiresAt - DateTime.UtcNow; string durationStr = remaining.TotalMinutes < 60 ? $"{Math.Ceiling(remaining.TotalMinutes)} minutes" : remaining.TotalHours < 24 ? $"{Math.Ceiling(remaining.TotalHours)} hours" : $"{remaining.Days} days"; string msg = $"\u26A0 TEAMING VIOLATION \u26A0\n" + $"Offense #{penalty.OffenseCount}\n" + $"Penalty: {penalty.CurrentReduction}% damage reduction for {durationStr}"; player.ChatMessage(msg); } SendPublicDiscordSanction(playerId, $"{tier.DamageReduction}% Damage Reduction", penalty.ExpiresAt, playerName); } } } pairData.Alerts.Clear(); pairData.Warned = false; SaveData(); } private void SendWarning(ulong id1, ulong id2, float score, string alertType = "Warning", PlayerPairData data = null) { var player1 = BasePlayer.FindByID(id1); var player2 = BasePlayer.FindByID(id2); string riskMsg = _config.Penalties.BanEnabled ? "Continue at risk of BAN!" : "Continue at risk of damage penalty!"; string warning = $"\u26A0 TEAMING WARNING \u26A0\n" + $"Suspicious activity detected with players outside your team.\n" + $"Score: {score:F2}/{_config.PenaltyThreshold:F2}\n" + $"{riskMsg}"; if (player1?.IsConnected == true) player1.ChatMessage(warning); if (player2?.IsConnected == true) player2.ChatMessage(warning); if (!string.IsNullOrEmpty(_config.DiscordWebhook)) { string name1 = player1?.displayName ?? id1.ToString(); string name2 = player2?.displayName ?? id2.ToString(); string alertHistory = data != null && data.Alerts.Count > 0 ? string.Join(", ", data.Alerts) : "None"; var embed = new { title = "⚠️ Teaming Warning Issued", color = 0xFFAA00, description = string.IsNullOrEmpty(_config.ServerName) ? "" : _config.ServerName, fields = new[] { new { name = "Player 1", value = $"[{name1}](https://steamcommunity.com/profiles/{id1})", inline = true }, new { name = "Player 2", value = $"[{name2}](https://steamcommunity.com/profiles/{id2})", inline = true }, new { name = "Trigger", value = alertType, inline = true }, new { name = "Current Score", value = $"{score:F2}/{_config.PenaltyThreshold:F2}", inline = true }, new { name = "History", value = alertHistory, inline = false } }, footer = new { text = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC") } }; var payload = JsonConvert.SerializeObject(new { embeds = new[] { embed } }); webrequest.Enqueue(_config.DiscordWebhook, payload, (code, response) => {}, this, Core.Libraries.RequestMethod.POST, new Dictionary { { "Content-Type", "application/json" } }); } } #endregion #region Detection Hooks void OnCodeEntered(CodeLock codeLock, BasePlayer player, string code) { if (!_config.Detection.CodeLock) return; if (ShouldIgnore(player)) return; if (codeLock.code != code) return; var authList = codeLock.whitelistPlayers.Where(x => x.IsSteamId()).ToList(); if (!authList.Contains(player.userID)) authList.Add(player.userID); if (authList.Count > _config.TeamLimit) { foreach (var otherId in authList) { if (otherId == player.userID) continue; if (IsTeammate(player.userID, otherId)) continue; AddAlert(player.userID, otherId, "CodeLock"); CheckAndApplyPenalty(player.userID, otherId, "CodeLock"); break; } } } void OnCupboardAuthorize(BuildingPrivlidge privilege, BasePlayer player) { if (!_config.Detection.Cupboard) return; if (ShouldIgnore(player)) return; var authList = new List(); foreach (var entry in privilege.authorizedPlayers) { ulong id = (ulong)entry; if (id.IsSteamId()) authList.Add(id); } if (!authList.Contains(player.userID)) authList.Add(player.userID); if (privilege.GetProtectedMinutes() > 0 && authList.Count > _config.TeamLimit) { foreach (var otherId in authList) { if (otherId == player.userID) continue; if (IsTeammate(player.userID, otherId)) continue; AddAlert(player.userID, otherId, "Cupboard"); CheckAndApplyPenalty(player.userID, otherId, "Cupboard"); break; } } } void OnTurretAuthorize(AutoTurret turret, BasePlayer player) { if (!_config.Detection.Turret) return; if (ShouldIgnore(player)) return; var authList = new List(); foreach (var entry in turret.authorizedPlayers) { ulong id = (ulong)entry; if (id.IsSteamId()) authList.Add(id); } if (!authList.Contains(player.userID)) authList.Add(player.userID); var cupboard = turret.GetBuildingPrivilege(); if (cupboard?.GetProtectedMinutes() <= 0) return; if (authList.Count > _config.TeamLimit) { foreach (var otherId in authList) { if (otherId == player.userID) continue; if (IsTeammate(player.userID, otherId)) continue; AddAlert(player.userID, otherId, "Turret"); CheckAndApplyPenalty(player.userID, otherId, "Turret"); break; } } } void OnEntityMounted(BaseMountable entity, BasePlayer player) { if (!_config.Detection.SharedVehicle) return; if (ShouldIgnore(player)) return; if (entity.GetParentEntity() == null) return; if (player.InSafeZone()) return; if (entity.GetParentEntity() is BaseVehicle vehicle) { var mounted = new List(); foreach (var mount in vehicle.mountPoints) { var mountedPlayer = mount?.mountable?.GetMounted(); if (mountedPlayer != null && mountedPlayer.userID.IsSteamId()) { if (!(_config.IgnoreAdmins && mountedPlayer.IsAdmin)) mounted.Add(mountedPlayer.userID); } } if (!mounted.Contains(player.userID)) mounted.Add(player.userID); if (mounted.Count > _config.TeamLimit) { for (int i = 0; i < mounted.Count; i++) { for (int j = i + 1; j < mounted.Count; j++) { if (IsTeammate(mounted[i], mounted[j])) continue; AddAlert(mounted[i], mounted[j], "SharedVehicle"); CheckAndApplyPenalty(mounted[i], mounted[j], "SharedVehicle"); return; } } } } } void OnEntityBuilt(Planner plan, GameObject go) { if (!_config.Detection.SleepingBag) return; if (plan == null || go == null) return; var player = plan.GetOwnerPlayer(); var entity = go.ToBaseEntity(); if (entity == null || player == null) return; if (ShouldIgnore(player)) return; if (entity is SleepingBag bag) CheckBag(player, bag, player.userID); } object CanAssignBed(BasePlayer player, SleepingBag bag, ulong targetPlayerId) { if (_config.Detection.SleepingBag && player != null && !ShouldIgnore(player)) CheckBag(player, bag, targetPlayerId); return null; } void CheckBag(BasePlayer player, SleepingBag bag, ulong targetPlayerId) { if (player.userID == targetPlayerId) return; var bagsInBuilding = SleepingBag.sleepingBags .Where(x => x.buildingID == bag.buildingID && x != bag && x.deployerUserID.IsSteamId()) .Select(x => x.deployerUserID) .Distinct() .ToList(); if (targetPlayerId.IsSteamId() && !bagsInBuilding.Contains(targetPlayerId)) bagsInBuilding.Add(targetPlayerId); if (bagsInBuilding.Count > _config.TeamLimit) { foreach (var otherId in bagsInBuilding) { if (otherId == player.userID) continue; if (IsTeammate(player.userID, otherId)) continue; AddAlert(player.userID, otherId, "SleepingBag"); CheckAndApplyPenalty(player.userID, otherId, "SleepingBag"); break; } } } void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) { if (entity == null || info == null) return; var attacker = info.InitiatorPlayer; if (attacker == null || attacker.IsNpc) return; if (_config.Penalties.Enabled && _penalties.TryGetValue(attacker.userID, out var penalty)) { if (penalty.CurrentReduction > 0 && penalty.ExpiresAt > DateTime.UtcNow) { bool isPlayer = entity is BasePlayer p && !p.IsNpc && p != attacker; bool isStructure = entity is BuildingBlock || entity is Door || entity is SimpleBuildingBlock; if (isPlayer || isStructure) { float scale = 1f - (penalty.CurrentReduction / 100f); info.damageTypes.ScaleAll(scale); } } } var victim = entity as BasePlayer; if (victim == null || victim.IsNpc) return; if (attacker == victim) return; if (!IsTeammate(attacker.userID, victim.userID)) { var pairData = GetPairData(attacker.userID, victim.userID); pairData.ProximityTime = 0f; } if (!_config.Detection.TeamKill) return; if (ShouldIgnore(attacker)) return; if (info.ProjectileID == 0) return; if (IsOnCooldown(attacker.userID)) return; if (!CanScan(attacker.userID, 1.0f)) return; List nearbyPlayers = Facepunch.Pool.Get>(); try { Vis.Entities(attacker.transform.position, _config.Detection.TeamKillMaxDistance, nearbyPlayers); foreach (var nearby in nearbyPlayers) { if (nearby == attacker || nearby == victim) continue; if (ShouldIgnore(nearby)) continue; if (nearby.IsSleeping() || !nearby.IsConnected) continue; if (IsTeammate(attacker.userID, nearby.userID)) continue; var heldEntity = nearby.GetActiveItem()?.GetHeldEntity() as BaseProjectile; if (heldEntity == null) continue; float distanceToVictim = Vector3.Distance(nearby.transform.position, victim.transform.position); if (distanceToVictim < _config.Detection.TeamKillMinTargetDistance) continue; Vector3 lookDir = nearby.eyes.HeadForward(); Vector3 toVictim = (victim.transform.position - nearby.transform.position).normalized; float angle = Vector3.Angle(lookDir, toVictim); if (angle < 30f) { if (!IsVisible(attacker, nearby)) continue; AddAlert(attacker.userID, nearby.userID, "TeamKill"); CheckAndApplyPenalty(attacker.userID, nearby.userID, "TeamKill"); SetCooldown(attacker.userID, 60f); break; } } } finally { Facepunch.Pool.FreeUnmanaged(ref nearbyPlayers); } } void OnPlayerRecovered(BasePlayer player) { if (!_config.Detection.Revive) return; if (player.InSafeZone()) return; List nearbyPlayers = Facepunch.Pool.Get>(); try { Vis.Entities(player.transform.position, 3f, nearbyPlayers); foreach (var reviver in nearbyPlayers) { if (reviver == player) continue; if (ShouldIgnore(reviver)) continue; if (!reviver.serverInput.IsDown(BUTTON.USE)) continue; if (IsTeammate(player.userID, reviver.userID)) continue; timer.Once(3f, () => { if (player != null && !player.IsDead()) { AddAlert(player.userID, reviver.userID, "Revive"); CheckAndApplyPenalty(player.userID, reviver.userID, "Revive"); } }); break; } } finally { Facepunch.Pool.FreeUnmanaged(ref nearbyPlayers); } } void OnLootEntity(BasePlayer player, BaseEntity entity) { if (!_config.Detection.Looting) return; if (ShouldIgnore(player)) return; if (!(entity is PlayerCorpse corpse)) return; if (!corpse.playerSteamID.IsSteamId()) return; if (player.InSafeZone()) return; var netId = corpse.net.ID.Value; if (_corpseLooting.TryGetValue(netId, out var firstLooter)) { if (firstLooter != player.userID && !IsTeammate(player.userID, firstLooter)) { AddAlert(player.userID, firstLooter, "Looting"); CheckAndApplyPenalty(player.userID, firstLooter, "Looting"); } return; } _corpseLooting[netId] = player.userID; timer.Once(_config.Detection.LootingWindow, () => _corpseLooting.Remove(netId)); } void OnPlayerVoice(BasePlayer player, byte[] data) { if (!_config.Detection.VoiceChat) return; if (ShouldIgnore(player)) return; if (player.InSafeZone() || player.IsBuildingAuthed()) return; if (!CanScan(player.userID, 2.0f)) return; List nearbyPlayers = Facepunch.Pool.Get>(); try { Vis.Entities(player.transform.position, 10f, nearbyPlayers); foreach (var other in nearbyPlayers) { if (other == player) continue; if (other.IsSleeping() || !other.IsConnected || !other.IsAlive()) continue; if (ShouldIgnore(other)) continue; if (other.InSafeZone() || other.IsBuildingAuthed()) continue; if (IsTeammate(player.userID, other.userID)) continue; if (!IsVisible(player, other)) continue; IncrementVoiceChat(player.userID, other.userID); } } finally { Facepunch.Pool.FreeUnmanaged(ref nearbyPlayers); } } void IncrementVoiceChat(ulong id1, ulong id2) { var (primary, secondary) = NormalizePair(id1, id2); if (!_voiceChatTicks.TryGetValue(primary, out var subDict)) { subDict = new Dictionary(); _voiceChatTicks[primary] = subDict; } if (!subDict.TryGetValue(secondary, out var ticks)) ticks = 0; ticks++; subDict[secondary] = ticks; if (ticks == _config.Detection.VoiceChatSeconds * 10) { AddAlert(primary, secondary, "VoiceChat"); CheckAndApplyPenalty(primary, secondary, "VoiceChat"); } } void OnPlayerDeath(BasePlayer player, HitInfo info) { if (_proximityTracker == null) return; if (info?.InitiatorPlayer == null) return; if (info.InitiatorPlayer == player) return; if (!player.userID.IsSteamId()) return; if (!info.InitiatorPlayer.userID.IsSteamId()) return; var killer = info.InitiatorPlayer; if (ShouldIgnore(killer)) return; if (IsTeammate(player.userID, killer.userID)) return; var pairData = GetPairData(player.userID, killer.userID); pairData.SelfKills++; var nearbyPlayers = Facepunch.Pool.Get>(); try { Vis.Entities(killer.transform.position, _config.Proximity.Distance, nearbyPlayers); foreach (var nearby in nearbyPlayers) { if (nearby == player || nearby == killer) continue; if (ShouldIgnore(nearby)) continue; if (IsTeammate(killer.userID, nearby.userID)) continue; if (_config.Proximity.CheckVisibility && !IsVisible(killer, nearby)) continue; var nearbyPair = GetPairData(killer.userID, nearby.userID); nearbyPair.ProximityKills++; if (nearbyPair.ProximityKills >= _config.Proximity.KillThreshold) { AddAlert(killer.userID, nearby.userID, "ProximityKills", false); nearbyPair.ProximityKills = 0; CheckAndApplyPenalty(killer.userID, nearby.userID, "ProximityKills"); break; } } } finally { Facepunch.Pool.FreeUnmanaged(ref nearbyPlayers); } } object CanUserLogin(string name, string id, string ip) { if (!ulong.TryParse(id, out var userId)) return null; if (_penalties.TryGetValue(userId, out var penalty)) { if (penalty.IsBan && penalty.ExpiresAt > DateTime.UtcNow) { TimeSpan remaining = penalty.ExpiresAt - DateTime.UtcNow; string durationStr = remaining.TotalMinutes < 60 ? $"{Math.Ceiling(remaining.TotalMinutes)} minutes" : remaining.TotalHours < 24 ? $"{Math.Ceiling(remaining.TotalHours)} hours" : $"{remaining.Days} days"; return $"Teaming Violation: You are banned for {durationStr}."; } } return null; } #endregion #region Commands void CommandStatus(IPlayer iplayer, string command, string[] args) { if (!iplayer.IsAdmin && !iplayer.HasPermission(PERMISSION_ADMIN)) { iplayer.Reply("No permission."); return; } string status = $"AntiTeamGuard Status\n"; status += $"Active Pairs: {_pairData.Sum(x => x.Value.Count)}\n"; status += $"Players with Penalties: {_penalties.Count(p => p.Value.CurrentReduction > 0)}\n"; status += $"Team Limit: {_config.TeamLimit}"; iplayer.Reply(status); } void CommandCheck(IPlayer iplayer, string command, string[] args) { if (!iplayer.IsAdmin && !iplayer.HasPermission(PERMISSION_ADMIN)) { iplayer.Reply("No permission."); return; } if (args.Length < 1) { iplayer.Reply("Usage: atg.check "); return; } if (!ulong.TryParse(args[0], out var targetId)) { iplayer.Reply("Invalid SteamID"); return; } string result = $"Player {targetId}\n"; if (_penalties.TryGetValue(targetId, out var penalty)) { result += $"Offense Count: {penalty.OffenseCount}\n"; if (penalty.IsBan) result += $"Type: BAN\n"; else result += $"Current Reduction: {penalty.CurrentReduction}%\n"; if (penalty.ExpiresAt > DateTime.UtcNow) result += $"Expires: {penalty.ExpiresAt:yyyy-MM-dd HH:mm} UTC\n"; } else { result += "No penalties on record.\n"; } int pairCount = 0; foreach (var kvp in _pairData) { if (kvp.Key == targetId || kvp.Value.ContainsKey(targetId)) pairCount += kvp.Value.Count; } result += $"Active Pairs: {pairCount}"; iplayer.Reply(result); } void CommandClear(IPlayer iplayer, string command, string[] args) { if (!iplayer.IsAdmin && !iplayer.HasPermission(PERMISSION_ADMIN)) { iplayer.Reply("No permission."); return; } if (args.Length < 1) { iplayer.Reply("Usage: atg.clear "); return; } if (!ulong.TryParse(args[0], out var targetId)) { iplayer.Reply("Invalid SteamID"); return; } if (_penalties.ContainsKey(targetId)) { _penalties.Remove(targetId); SaveData(); iplayer.Reply($"Cleared penalties for {targetId}"); } else { iplayer.Reply($"No penalties found for {targetId}"); } } void CommandPenalty(IPlayer iplayer, string command, string[] args) { if (!iplayer.IsAdmin && !iplayer.HasPermission(PERMISSION_ADMIN)) { iplayer.Reply("No permission."); return; } if (args.Length < 3) { iplayer.Reply("Usage: atg.penalty "); return; } if (!ulong.TryParse(args[0], out var targetId)) { iplayer.Reply("Invalid SteamID"); return; } if (!float.TryParse(args[1], out var reduction) || !int.TryParse(args[2], out var days)) { iplayer.Reply("Invalid reduction or days value"); return; } if (!_penalties.TryGetValue(targetId, out var penalty)) { penalty = new PlayerPenalty(); _penalties[targetId] = penalty; } penalty.CurrentReduction = reduction; penalty.ExpiresAt = DateTime.UtcNow.AddDays(days); penalty.OffenseCount++; penalty.LastOffenseDate = DateTime.UtcNow; SaveData(); iplayer.Reply($"Applied {reduction}% damage reduction for {days} days to {targetId}"); var player = BasePlayer.FindByID(targetId); if (player?.IsConnected == true) { player.ChatMessage($"Admin applied {reduction}% damage penalty for {days} days."); } } void CommandPlayerStatus(IPlayer iplayer, string command, string[] args) { if (!iplayer.HasPermission(PERMISSION_TEAMSTATUS)) { iplayer.Reply("You do not have permission to use this command."); return; } if (!ulong.TryParse(iplayer.Id, out var playerId)) return; float maxScore = 0f; if (_pairData.TryGetValue(playerId, out var subDict)) { foreach (var targetId in subDict.Keys.ToList()) { float s = CalculateScore(playerId, targetId); if (s > maxScore) maxScore = s; } } foreach (var kvp in _pairData) { if (kvp.Key <= playerId) continue; if (kvp.Value.ContainsKey(playerId)) { float s = CalculateScore(kvp.Key, playerId); if (s > maxScore) maxScore = s; } } string msg = ""; if (_penalties.TryGetValue(playerId, out var penalty) && penalty.OffenseCount > 0) { DateTime nextDecay = penalty.LastOffenseDate.AddDays(_config.OffenseDecayDays); TimeSpan timeToDecay = nextDecay - DateTime.UtcNow; string dropStr = timeToDecay.TotalSeconds > 0 ? (timeToDecay.TotalHours < 24 ? $"{Math.Ceiling(timeToDecay.TotalHours)}h" : $"{timeToDecay.Days}d {timeToDecay.Hours}h") : "Soon"; msg += $"Offense Level: {penalty.OffenseCount} (Decays in {dropStr})\n"; if (penalty.CurrentReduction > 0 && penalty.ExpiresAt > DateTime.UtcNow) { TimeSpan remaining = penalty.ExpiresAt - DateTime.UtcNow; string remStr = remaining.TotalMinutes < 60 ? $"{Math.Ceiling(remaining.TotalMinutes)}m" : $"{Math.Ceiling(remaining.TotalHours)}h"; msg += $"Active Penalty: {penalty.CurrentReduction}% Damage Reduction (Expires in {remStr})\n"; } var nextTier = _config.Penalties.GetTier(penalty.OffenseCount + 1); if (nextTier != null) { string nextAction = _config.Penalties.BanEnabled ? $"Ban for {nextTier.BanDurationMinutes} minutes" : $"{nextTier.DamageReduction}% Damage Reduction x {nextTier.DurationHours} hours"; msg += $"Next Violation: {nextAction}\n"; } } else { msg += "You have a clean record. No active penalties.\n"; var tier1 = _config.Penalties.GetTier(1); if (tier1 != null) { string firstAction = _config.Penalties.BanEnabled ? $"Ban for {tier1.BanDurationMinutes} minutes" : $"{tier1.DamageReduction}% Damage Reduction x {tier1.DurationHours} hours"; msg += $"Penalty for 1st Violation: {firstAction}\n"; } } string riskColor = maxScore > 0 ? (maxScore >= _config.PenaltyThreshold * 0.6f ? "#ffaa00" : "#ffff00") : "#00ff00"; msg += $"Current Teaming Risk: {maxScore:F2} / {_config.PenaltyThreshold:F1}"; iplayer.Reply(msg); } #endregion #region Discord void SendDiscordAlert(ulong id1, ulong id2, string alertType, float score, PlayerPairData data, int clusterSize = 0) { if (string.IsNullOrEmpty(_config.DiscordWebhook)) return; var player1 = BasePlayer.FindByID(id1); var player2 = BasePlayer.FindByID(id2); string name1 = player1?.displayName ?? id1.ToString(); string name2 = player2?.displayName ?? id2.ToString(); string sizeInfo = clusterSize > 0 ? $" (Cluster: {clusterSize})" : ""; var embed = new { title = $"🚨 Teaming Detection: {alertType}", color = 0xFF4444, description = string.IsNullOrEmpty(_config.ServerName) ? "" : _config.ServerName, fields = new[] { new { name = "Player 1", value = $"[{name1}](https://steamcommunity.com/profiles/{id1})", inline = true }, new { name = "Player 2", value = $"[{name2}](https://steamcommunity.com/profiles/{id2})", inline = true }, new { name = "Alert Type", value = alertType + sizeInfo, inline = true }, new { name = "Total Score", value = $"{score:F2}/{_config.PenaltyThreshold:F2}", inline = true }, new { name = "All Alerts", value = data.Alerts.Count > 0 ? string.Join(", ", data.Alerts) : "None", inline = false } }, footer = new { text = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC") } }; var payload = JsonConvert.SerializeObject(new { embeds = new[] { embed } }); webrequest.Enqueue(_config.DiscordWebhook, payload, (code, response) => { if (code != 200 && code != 204) PrintWarning($"Discord webhook failed: {code}"); }, this, Core.Libraries.RequestMethod.POST, new Dictionary { { "Content-Type", "application/json" } }); } void SendPublicDiscordSanction(ulong playerId, string sanctionType, DateTime expiresAt, string knownName = null) { if (string.IsNullOrEmpty(_config.PublicDiscordWebhook)) return; string name = knownName; if (string.IsNullOrEmpty(name)) { var player = BasePlayer.FindByID(playerId) ?? BasePlayer.FindAwakeOrSleeping(playerId.ToString()); name = player?.displayName ?? playerId.ToString(); if (name == playerId.ToString() && covalence != null) { var iplayer = covalence.Players.FindPlayer(playerId.ToString()); if (iplayer != null) name = iplayer.Name; } } long unixTimestamp = (long)(expiresAt.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; var embed = new { title = "⚖️ Sanction Applied", color = 0xFF0000, description = (string.IsNullOrEmpty(_config.ServerName) ? "" : $"**{_config.ServerName}**\n") + $"A penalty has been applied to **[{name}](https://steamcommunity.com/profiles/{playerId.ToString()})** for violating team limits.", fields = new[] { new { name = "Sanction", value = sanctionType, inline = true }, new { name = "Expiration", value = $" ()", inline = true } }, footer = new { text = "AntiTeamGuard Automated System" } }; var payload = JsonConvert.SerializeObject(new { embeds = new[] { embed } }); webrequest.Enqueue(_config.PublicDiscordWebhook, payload, (code, response) => { if (code != 200 && code != 204) PrintWarning($"Public Discord webhook failed: {code}"); }, this, Core.Libraries.RequestMethod.POST, new Dictionary { { "Content-Type", "application/json" } }); } #endregion #region Proximity Tracker private class ProximityTracker : MonoBehaviour { private AntiTeamGuard _plugin; private ProximitySettings _settings; public void Initialize(AntiTeamGuard plugin, ProximitySettings settings) { _plugin = plugin; _settings = settings; InvokeRepeating(nameof(Track), 5f, 2f); } void Track() { if (_plugin == null) return; float timeDelta = 2.0f; foreach (var player in BasePlayer.activePlayerList) { if (_plugin.ShouldIgnore(player)) continue; if (player.IsSleeping() || player.IsDead()) continue; if (player.InSafeZone()) continue; List nearbyPlayers = Facepunch.Pool.Get>(); try { Vis.Entities(player.transform.position, _settings.Distance, nearbyPlayers); foreach (var other in nearbyPlayers) { if (other == player) continue; if (_plugin.ShouldIgnore(other)) continue; if (other.IsSleeping() || other.IsDead()) continue; if (_plugin.IsTeammate(player.userID, other.userID)) continue; if (_settings.CheckVisibility && !IsVisible(player, other)) continue; var data = _plugin.GetPairData(player.userID, other.userID); data.ProximityTime += timeDelta; if (data.ProximityTime >= _settings.TimeThreshold) { _plugin.AddAlert(player.userID, other.userID, "ProximityTime", false); data.ProximityTime = 0f; _plugin.CheckAndApplyPenalty(player.userID, other.userID, "ProximityTime"); } } } finally { Facepunch.Pool.FreeUnmanaged(ref nearbyPlayers); } } } public void Destroy() { CancelInvoke(nameof(Track)); } } #endregion } }