C#:
// 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<ulong, Dictionary<ulong, PlayerPairData>> _pairData = new Dictionary<ulong, Dictionary<ulong, PlayerPairData>>();
private Dictionary<ulong, PlayerPenalty> _penalties = new Dictionary<ulong, PlayerPenalty>();
private Dictionary<ulong, Dictionary<ulong, int>> _voiceChatTicks = new Dictionary<ulong, Dictionary<ulong, int>>();
private Dictionary<ulong, ulong> _corpseLooting = new Dictionary<ulong, ulong>();
private Dictionary<ulong, float> _cooldowns = new Dictionary<ulong, float>();
private Dictionary<ulong, float> _scanCooldowns = new Dictionary<ulong, float>();
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>();
_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<Configuration>();
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<string, float> Weights { get; set; } = new Dictionary<string, float>
{
{ "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<OffenseTier>
{
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<OffenseTier> Tiers { get; set; } = new List<OffenseTier>();
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<string> Alerts { get; set; } = new List<string>();
public float AlertScore { get; set; } = 0f;
public DateTime LastDecay { get; set; } = DateTime.UtcNow;
public Dictionary<string, DateTime> AlertCooldowns { get; set; } = new Dictionary<string, DateTime>();
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<Dictionary<ulong, Dictionary<ulong, PlayerPairData>>>(DATA_PAIR)
?? new Dictionary<ulong, Dictionary<ulong, PlayerPairData>>();
if (Interface.Oxide.DataFileSystem.ExistsDatafile(DATA_PENALTIES))
_penalties = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, PlayerPenalty>>(DATA_PENALTIES)
?? new Dictionary<ulong, PlayerPenalty>();
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<ulong, Dictionary<ulong, PlayerPairData>>();
_penalties = new Dictionary<ulong, PlayerPenalty>();
}
}
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("<color=#00ff00>Your teaming penalty has expired.</color>");
}
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("<color=#00ff00>Your teaming penalty has expired.</color>");
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 = $"<color=#ff4444>⚠ ACTIVE TEAMING PENALTY ⚠</color>\n" +
$"<color=#ffffff>You have an active penalty for past teaming violations.</color>\n" +
$"<color=#ffaa00>Status: {penalty.CurrentReduction}% Damage Reduction</color>\n" +
$"<color=#ffaa00>Time Remaining: {durationStr}</color>";
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<ulong, PlayerPairData>();
_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<List<string>>("GetClanMembers", player1);
if (clanMembers != null && clanMembers.Contains(player2.ToString()))
return true;
}
return false;
}
private List<ulong> GetTeammates(ulong playerId)
{
var teammates = new List<ulong>();
if (RelationshipManager.ServerInstance.playerToTeam.TryGetValue(playerId, out var team))
teammates.AddRange(team.members);
if (Clans != null)
{
var clanMembers = Clans.Call<List<string>>("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<ulong> GetTeamingPartners(ulong targetId, float threshold)
{
var partners = new List<ulong>();
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<ulong> { id1, id2 };
cluster.UnionWith(GetTeammates(id1));
cluster.UnionWith(GetTeammates(id2));
if (cluster.Count <= _config.TeamLimit * 2)
{
var queue = new Queue<ulong>(cluster);
var processed = new HashSet<ulong>(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<ulong> { 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 = $"<color=#ff4444>⚠ TEAMING VIOLATION ⚠</color>\n" +
$"<color=#ffffff>Offense #{penalty.OffenseCount}</color>\n" +
$"<color=#ffaa00>Penalty: {penalty.CurrentReduction}% damage reduction for {durationStr}</color>";
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 = $"<color=#ff6b35>⚠ TEAMING WARNING ⚠</color>\n" +
$"<color=#ffffff>Suspicious activity detected with players outside your team.</color>\n" +
$"<color=#ffaa00>Score: {score:F2}/{_config.PenaltyThreshold:F2}</color>\n" +
$"<color=#ff4444>{riskMsg}</color>";
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<string, string> { { "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<ulong>();
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<ulong>();
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<ulong>();
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<BasePlayer> nearbyPlayers = Facepunch.Pool.Get<List<BasePlayer>>();
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<BasePlayer> nearbyPlayers = Facepunch.Pool.Get<List<BasePlayer>>();
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<BasePlayer> nearbyPlayers = Facepunch.Pool.Get<List<BasePlayer>>();
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<ulong, int>();
_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.Initiato
// ... [Code preview truncated due to length. Full source in attached file] ...