C#:
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Core.Libraries.Covalence;
namespace Oxide.Plugins;
// Changelog
// minor - updated as the Death is still recorded efter the player reset is completed in Hardcore Mode.
// minor - Death now resets player stats completely without storing the Death/Suicide.
// minor - Added Hooks to check for Expansions.
[Info("PlayerStatTags", "CupsOhJoe", "1.3.6")]
[Description("Tracks player stats, assigns a single chat tag (by priority) to top players")]
internal class PlayerStatTags : RustPlugin
{
#region Plugin References
[PluginReference] private Plugin HarvestTagPack, BetterChat, Economics, ServerRewards;
#endregion
#region Configuration
private const int currentConfigVersion = 5;
private ConfigData config;
private class ConfigData
{
[JsonProperty("Tags (Change Tags between [] only and set their priority as needed)")]
public Dictionary<string, TagInfo> ChatTags = new()
{
{
"PVPKills",
new TagInfo
{
Tag = "[<color=#71201b>Killer</color>]", Priority = 1,
Description = "Top PVP Kills: Awarded to the player with the highest number of PVP kills."
}
},
{
"PVPDistance",
new TagInfo
{
Tag = "[<color=#ff2323>Sharp Shooter</color>]", Priority = 2,
Description = "Top PVP Distance: Awarded to the player with the longest PVP engagement distance."
}
},
{
"PVEKills",
new TagInfo
{
Tag = "[<color=#525600>Hunter</color>]", Priority = 3,
Description = "Top PVE Kills: Awarded to the player with the highest number of PVE kills."
}
},
{
"PVEDistance",
new TagInfo
{
Tag = "[<color=#d4af37>Snipe-Hunter</color>]", Priority = 4,
Description = "Top PVE Distance: Awarded to the player with the longest PVE engagement distance."
}
},
{
"NPCKills",
new TagInfo
{
Tag = "[<color=#3364b4>Bot Killer</color>]", Priority = 5,
Description = "Top NPC Kills: Awarded to the player with the most NPC kills."
}
},
{
"AnimalKills",
new TagInfo
{
Tag = "[<color=#d4af37>Animal Hunter</color>]", Priority = 6,
Description = "Top Animal Kills: Awarded to the player with the most animal kills."
}
},
{
"NPCDistance",
new TagInfo
{
Tag = "[<color=#00abff>Bot Sniper</color>]", Priority = 7,
Description = "Top NPC Distance: Awarded to the player with the longest distance in NPC kills."
}
},
{
"SleepersKilled",
new TagInfo
{
Tag = "[<color=#df7676>NoMercy</color>]", Priority = 8,
Description = "Top Sleeper Kills: Awarded to the player with the highest number of sleeper kills."
}
},
{
"HeadShots",
new TagInfo
{
Tag = "[<color=#7e3b3b>Head Shotter</color>]", Priority = 9,
Description = "Top Headshots: Awarded to the player with the most headshots."
}
},
{
"Deaths",
new TagInfo
{
Tag = "[<color=#505050>Death</color>]", Priority = 10,
Description = "Deaths: Tracked for ratios; not typically used as a positive tag."
}
},
{
"Suicides",
new TagInfo
{
Tag = "[<color=#aeaeae>Respawn</color>]", Priority = 11,
Description = "Suicides: Tracked for ratios; not typically used as a positive tag."
}
},
{
"SkullsCrushed",
new TagInfo
{
Tag = "[<color=#bebebe>Skull Crusher</color>]", Priority = 12,
Description = "Skulls Crushed: Awarded to the player with the most skulls crushed."
}
},
{
"TimesWounded",
new TagInfo
{
Tag = "[<color=#923a3a>Always Downed</color>]", Priority = 13,
Description = "Times Wounded: Awarded to the player who was wounded the most."
}
},
{
"TimesHealed",
new TagInfo
{
Tag = "[<color=#00ff12>Medic</color>]", Priority = 14,
Description = "Times Healed: Awarded to the player who was healed the most (or healed others the most)."
}
},
{
"HeliHits",
new TagInfo
{
Tag = "[<color=#00296a>Heli Hitter</color>]", Priority = 15,
Description = "Helicopter Hits: Awarded to the player with the most helicopter hits."
}
},
{
"HeliKills",
new TagInfo
{
Tag = "[<color=#3362ab>Heli Killer</color>]", Priority = 16,
Description = "Helicopter Kills: Awarded to the player with the most helicopter kills."
}
},
{
"APCHits",
new TagInfo
{
Tag = "[<color=#8e4d9d>Bradley Hitter</color>]", Priority = 17,
Description = "APC Hits: Awarded to the player with the most APC hits."
}
},
{
"APCKills",
new TagInfo
{
Tag = "[<color=#66007d>Bradley Killer</color>]", Priority = 18,
Description = "APC Kills: Awarded to the player with the most APC kills."
}
},
{
"BarrelsDestroyed",
new TagInfo
{
Tag = "[<color=#e3d984>Barrel Farmer</color>]", Priority = 19,
Description = "Barrels Destroyed: Awarded to the player who destroyed the most barrels."
}
},
{
"ExplosivesThrown",
new TagInfo
{
Tag = "[<color=#ee7d00>Ok Boomer</color>]", Priority = 20,
Description = "Explosives Thrown: Awarded to the player who threw the most explosives."
}
},
{
"ArrowsFired",
new TagInfo
{
Tag = "[<color=#9a5100>Archer</color>]", Priority = 21,
Description = "Arrows Fired: Awarded to the player who fired the most arrows."
}
},
{
"BulletsFired",
new TagInfo
{
Tag = "[<color=#7e00ed>Sprayer</color>]", Priority = 22,
Description = "Bullets Fired: Awarded to the player who fired the most bullets."
}
},
{
"RocketsLaunched",
new TagInfo
{
Tag = "[<color=#c68cf8>Rocket Man</color>]", Priority = 23,
Description = "Rockets Launched: Awarded to the player who launched the most rockets."
}
},
{
"MLRSFired",
new TagInfo
{
Tag = "[<color=#b3102e>Airstrike</color>]", Priority = 24,
Description = "MLRS Fired: Awarded to the player who fired the most MLRS."
}
},
{
"WeaponTrapsDestroyed",
new TagInfo
{
Tag = "[<color=#0040c0>Trap Trapper</color>]", Priority = 25,
Description = "Weapon Traps Destroyed: Awarded to the player who destroyed the most weapon traps."
}
},
{
"DropsLooted",
new TagInfo
{
Tag = "[<color=#c724ff>Supply Hunter</color>]", Priority = 26,
Description = "Drops Looted: Awarded to the player who looted the most airdrops."
}
},
{
"FishCaught",
new TagInfo
{
Tag = "[<color=#7d9eff>Fisherman</color>]", Priority = 27,
Description = "Fish Caught: Awarded to the player who caught the most fish."
}
},
{
"Economics",
new TagInfo
{
Tag = "[<color=#836b27>Earner</color>]", Priority = 28,
Description = "Economics: Awarded to the player with the highest in-game economic balance."
}
},
{
"ServerRewards",
new TagInfo
{
Tag = "[<color=#836b27>Reward Earner</color>]", Priority = 29,
Description = "Server Rewards: Awarded to the player with the most server rewards points."
}
},
{
"StructuresBuilt",
new TagInfo
{
Tag = "[<color=#0041ff>Architect</color>]", Priority = 30,
Description = "Structures Built: Awarded to the player who built the most structures."
}
},
{
"StructuresDemolished",
new TagInfo
{
Tag = "[<color=#836b27>Demolition Man</color>]", Priority = 31,
Description = "Structures Demolished: Awarded to the player who demolished the most structures."
}
},
{
"ItemsDeployed",
new TagInfo
{
Tag = "[<color=#598f2c>Deployer</color>]", Priority = 32,
Description = "Items Deployed: Awarded to the player who deployed the most items."
}
},
{
"ItemsCrafted",
new TagInfo
{
Tag = "[<color=#374b6a>Maker</color>]", Priority = 33,
Description = "Items Crafted: Awarded to the player who crafted the most items."
}
},
{
"EntitiesRepaired",
new TagInfo
{
Tag = "[<color=#048c8d>Fixer</color>]", Priority = 34,
Description = "Entities Repaired: Awarded to the player who repaired the most entities."
}
},
{
"ResourcesGathered",
new TagInfo
{
Tag = "[<color=#7200a6>Hoarder</color>]", Priority = 35,
Description = "Resources Gathered: Awarded to the player who gathered the most resources."
}
},
{
"PlantsGathered",
new TagInfo
{
Tag = "[<color=#048d0a>Farmer</color>]", Priority = 36,
Description = "Plants Gathered: Awarded to the player who gathered the most plants."
}
},
{
"StructuresUpgraded",
new TagInfo
{
Tag = "[<color=#769225>Home Improver</color>]", Priority = 37,
Description = "Structures Upgraded: Awarded to the player who upgraded the most structures."
}
},
{
"HeliCrashes",
new TagInfo
{
Tag = "[<color=#c8c0af>Crash Dummy</color>]", Priority = 38,
Description = "Heli Crashes: Awarded to the player when they crash their own helicopter."
}
},
{
"CarCrashes",
new TagInfo
{
Tag = "[<color=#8d8d8d>Road Rash</color>]", Priority = 39,
Description = "Car Crashes: Awarded to the player with the most car crashes."
}
},
{
"VehicleKills",
new TagInfo
{
Tag = "[<color=#5a5a5a>Destroyer</color>]", Priority = 40,
Description = "Vehicle Kills: Awarded to the player who destroyed the most vehicles."
}
}
};
[JsonProperty("Remove Data on MAP Wipe")]
public bool DeleteDataOnMapWipe = false;
[JsonProperty("How often to Update Data (sec)")]
public float UpdateInterval = 15f;
[JsonProperty("How Often to Save Data to file (sec)")]
public float SaveInterval = 60f;
[JsonProperty("Enable Hardcore Mode (Reset stats on death)")]
public bool EnableHardcoreMode = false;
[JsonProperty("Hardcore Mode Notification Message")]
public string HardcoreModeMessage = "Your stats have been reset due to hardcore mode!";
[JsonProperty("Broadcast Hardcore Mode Resets")]
public bool BroadcastHardcoreResets = true;
[JsonProperty("How long to keep Ownership of Vehicles (sec)")]
public float VehicleOwnershipClearInterval = 60f;
[JsonProperty("Use BetterChat Integration")]
public bool EnableBetterChatIntegration = false;
[JsonProperty("Enable SteamID Exclusion")]
public bool EnableSteamIDExclusion = false;
[JsonProperty("Excluded SteamIDs (Players who will be excluded from stat tracking)")]
public List<ulong> ExcludedSteamIDs = new List<ulong>();
[JsonProperty("Config Version (DO NOT CHANGE)")]
public int ConfigVersion = currentConfigVersion;
}
public class TagInfo
{
public string Tag { get; set; } = "";
public int Priority { get; set; }
public string Description { get; set; } = "";
}
protected override void LoadDefaultConfig()
{
Puts("Creating new default config file.");
config = new ConfigData();
SaveConfig();
}
private void LoadConfigData()
{
try
{
config = Config.ReadObject<ConfigData>();
if (config == null)
{
LoadDefaultConfig();
return;
}
if (config.ConfigVersion < currentConfigVersion)
{
Puts("Config file is outdated; updating with new keys.");
config.ConfigVersion = currentConfigVersion;
SaveConfig();
}
}
catch
{
Puts("Could not load config, creating new default config.");
LoadDefaultConfig();
}
}
private void UpdateConfig()
{
var defaultConfig = new ConfigData();
foreach (var kvp in defaultConfig.ChatTags)
if (!config.ChatTags.ContainsKey(kvp.Key))
{
config.ChatTags[kvp.Key] = kvp.Value;
}
else
{
if (string.IsNullOrEmpty(config.ChatTags[kvp.Key].Description))
config.ChatTags[kvp.Key].Description = kvp.Value.Description;
}
// Let's add the Hardcode mode Thanks Rust STunts
if (!config.GetType().GetFields().Any(f => f.Name == "EnableHardcoreMode"))
{
config.EnableHardcoreMode = defaultConfig.EnableHardcoreMode;
config.HardcoreModeMessage = defaultConfig.HardcoreModeMessage;
config.BroadcastHardcoreResets = defaultConfig.BroadcastHardcoreResets;
}
config.UpdateInterval = defaultConfig.UpdateInterval;
config.SaveInterval = defaultConfig.SaveInterval;
config.ConfigVersion = currentConfigVersion;
}
private void SaveConfig()
{
Config.WriteObject(config, true);
}
#endregion
#region Data Storage
private DataStore storedData;
private class DataStore
{
public Dictionary<ulong, Dictionary<string, double>> playerStats = new();
}
private Dictionary<ulong, Dictionary<string, double>> playerStats => storedData.playerStats;
private void LoadData()
{
try
{
storedData = Interface.Oxide.DataFileSystem.ReadObject<DataStore>("PlayerStatTagsData");
if (storedData == null)
storedData = new DataStore();
CorrectDataFormat();
if (config.EnableSteamIDExclusion)
{
foreach (var excludedId in config.ExcludedSteamIDs)
{
if (storedData.playerStats.ContainsKey(excludedId))
{
DebugLog($"[Debug] Removing stats for excluded player {excludedId}");
storedData.playerStats.Remove(excludedId);
}
}
}
}
catch
{
storedData = new DataStore();
}
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject("PlayerStatTagsData", storedData);
}
private void CorrectDataFormat()
{
foreach (var playerEntry in storedData.playerStats.ToList())
{
var playerId = playerEntry.Key;
var stats = playerEntry.Value;
foreach (var key in stats.Keys.ToList())
if (!config.ChatTags.ContainsKey(key))
{
DebugLog($"[Debug] Removing obsolete stat '{key}' for player {playerId}");
stats.Remove(key);
}
}
}
private void OnNewSave(string filename)
{
if (config.DeleteDataOnMapWipe)
{
var dataFile = Interface.Oxide.DataFileSystem.GetFile("PlayerStatTagsData");
dataFile.Clear();
dataFile.Save();
Puts("Map wipe detected: PlayerStatTags data file deleted and new data store initialized.");
}
}
private void ResetPlayerStats(ulong playerId)
{
if (playerStats.ContainsKey(playerId))
{
// Create a backup of stats for debugging
var statsBackup = new Dictionary<string, double>(playerStats[playerId]);
DebugLog($"[Debug] Resetting stats for player {playerId}. Previous stats: {string.Join(", ", statsBackup.Select(kvp => $"{kvp.Key}:{kvp.Value}"))}");
// Remove player from top players list
foreach (var category in config.ChatTags.Keys)
{
if (topPlayers.TryGetValue(category, out var topId) && topId == playerId)
{
// Find new top player for this category
ulong newTopId = 0;
double newTopValue = 0;
foreach (var kvp in playerStats)
{
if (kvp.Key != playerId &&
kvp.Value.TryGetValue(category, out var value) &&
value > newTopValue)
{
newTopValue = value;
newTopId = kvp.Key;
}
}
topPlayers[category] = newTopId;
DebugLog($"[Debug] Top player for {category} changed from {playerId} to {newTopId} with value {newTopValue}");
}
}
playerStats[playerId] = new Dictionary<string, double>();
SaveData();
UpdateTopPlayers();
DebugLog($"[Debug] Force-updated top players list after resetting player {playerId}'s stats");
}
}
#endregion
#region Stat Tracking
private bool harvestTrackingEnabled = true;
private bool travelTrackingEnabled = true;
private void OnItemDeployed(Deployer deployer, BaseEntity entity)
{
if (deployer == null) return;
var player = deployer.GetOwnerPlayer();
if (player == null || player.IsNpc || IsHumanNPC(player)) return;
UpdateStat(player.userID, "ItemsDeployed", 1, player);
if (playerStats[player.userID]["ItemsDeployed"] > GetTopValue("ItemsDeployed"))
topPlayers["ItemsDeployed"] = player.userID;
DebugLog($"[Debug] Player {player.userID} deployed an item");
}
private void ClearExpiredVehicleOwnerships()
{
var currentTime = Time.realtimeSinceStartup;
// gto through the copy of dictionary
foreach (var kvp in vehicleOwnership.ToList())
{
var vehicleId = kvp.Key;
var info = kvp.Value;
if (info.DismountTime > 0 && currentTime - info.DismountTime >= config.VehicleOwnershipClearInterval)
{
DebugLog(
$"[Debug] Vehicle {vehicleId} ownership expired after {config.VehicleOwnershipClearInterval} seconds, clearing owner.");
// remove the owner after X amounts of seconds, I guees
info.OwnerId = 0;
info.DismountTime = 0f;
}
}
}
private Dictionary<string, ulong> topPlayers = new();
private List<ulong> airdrops = new List<ulong>();
private void UpdateStat(ulong id, string category, double amount, BasePlayer player)
{
if (config.EnableSteamIDExclusion && config.ExcludedSteamIDs.Contains(id))
{
DebugLog($"[Debug] Player {id} is in the exclusion list and will not be tracked for stats.");
return;
}
if (player == null || player.net?.connection == null)
return;
if (!playerStats.ContainsKey(id))
playerStats[id] = new Dictionary<string, double>();
if (!playerStats[id].ContainsKey(category))
playerStats[id][category] = 0;
playerStats[id][category] += amount;
DebugLog($"Updated stat '{category}' for player {id} by {amount}. New value: {playerStats[id][category]}");
}
private double GetTopValue(string category)
{
ulong topId;
if (topPlayers.TryGetValue(category, out topId))
if (playerStats.ContainsKey(topId) && playerStats[topId].ContainsKey(category))
return playerStats[topId][category];
return 0;
}
private void ComputeRatios()
{
foreach (var kvp in playerStats)
{
var stats = kvp.Value;
if (stats.ContainsKey("PVPKills") && stats.ContainsKey("Deaths"))
{
var deaths = stats["Deaths"];
var pvpkills = stats["PVPKills"];
stats["KDR"] = deaths > 0 ? pvpkills / deaths : pvpkills;
var suicides = stats.ContainsKey("Suicides") ? stats["Suicides"] : 0;
stats["SDR"] = deaths > 0 ? suicides / deaths : suicides;
}
}
}
private void UpdateTopPlayers()
{
ComputeRatios();
foreach (var category in config.ChatTags.Keys)
{
ulong currentTop = 0;
double currentTopValue = 0;
foreach (var kvp in playerStats)
{
if (config.EnableSteamIDExclusion && config.ExcludedSteamIDs.Contains(kvp.Key))
continue;
double statValue = 0;
if (kvp.Value.TryGetValue(category, out statValue))
if (statValue > currentTopValue)
{
currentTopValue = statValue;
currentTop = kvp.Key;
}
}
topPlayers[category] = currentTop;
DebugLog($"Top for '{category}' is {currentTop} with value {currentTopValue}");
}
}
#region Vehicle Pilot Tracking
private class HeliPilotInfo
{
public ulong PilotId { get; set; }
public float ExitTime { get; set; }
public bool HasExited { get; set; }
}
private class VehicleOwnershipInfo
{
public ulong OwnerId { get; set; }
public float DismountTime { get; set; }
}
private Dictionary<uint, VehicleOwnershipInfo> vehicleOwnership = new();
//I might need this for the Heli Tracking
//private Dictionary<uint, HeliPilotInfo> vehiclePilots = new Dictionary<uint, HeliPilotInfo>();
// Change who the heck owns the mount/vehicle. Maybe
private void ChangeVehicleOwnership(BaseCombatEntity vehicle, ulong userId)
{
var vehicleId = vehicle != null && vehicle.net != null ? vehicle.net.ID.Value : 0;
DebugLog($"DEBUG: Changing vehicle ownership for vehicle {vehicleId} to user {userId}.");
vehicle.OwnerID = userId;
Interface.CallHook("OnVehicleOwnershipChanged", vehicle);
DebugLog($"DEBUG: Vehicle {vehicleId} ownership updated to {vehicle.OwnerID}.");
}
private void OnEntityMounted(BaseMountable entity, BasePlayer player)
{
if (entity == null || player == null)
return;
// Check if the player is an NPC
if (player.IsNpc || IsHumanNPC(player))
{
DebugLog($"[Debug] NPC {player.userID} mounting vehicle, skipping ownership tracking");
return;
}
var vehicle = entity.GetComponentInParent<BaseVehicle>();
uint entityId = 0;
if (vehicle != null)
entityId = (uint)vehicle.net.ID.Value;
else
entityId = (uint)entity.net.ID.Value;
// Existing ownership tracking code
vehicleOwnership[entityId] = new VehicleOwnershipInfo { OwnerId = player.userID, DismountTime = 0f };
DebugLog($"[Debug] OnEntityMounted: Stored ownership with entity ID {entityId} for player {player.userID}.");
if (vehicle != null)
{
ChangeVehicleOwnership(vehicle, player.userID);
DebugLog($"[Debug] OnEntityMounted: Vehicle claimed by {player.userID}.");
}
else
{
DebugLog($"[Debug] OnEntityMounted: No parent vehicle found, using entity net ID {entityId} for ownership.");
}
}
public bool IsHumanNPC(BasePlayer player)
{
foreach (var comp in player?.GetComponents<Component>())
if (comp?.GetType()?.Name == "HumanPlayer")
return true;
return false;
}
#endregion
private void PurgeNonPlayerData()
{
foreach (var entry in storedData.playerStats.Keys.ToList())
if (!entry.ToString().StartsWith("7656119"))
{
DebugLog($"[Debug] Removing non-player data for SteamID: {entry}");
storedData.playerStats.Remove(entry);
}
}
#endregion
#region Debug Logging
private bool debugEnabled = false;
private void DebugLog(string message)
{
if (debugEnabled)
Puts("[Debug] " + message);
}
[ChatCommand("playerstattag")]
private void PlayerStatTagCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, "playerstattags.admin"))
{
player.ChatMessage("You do not have permission to use this command.");
return;
}
if (args.Length < 2)
{
player.ChatMessage("Usage: /playerstattag debug on/off");
return;
}
if (args[0].ToLower() == "debug")
{
if (args[1].ToLower() == "on")
{
debugEnabled = true;
player.ChatMessage("PlayerStatTags debugging is now ON.");
Puts("PlayerStatTags debugging enabled by admin.");
}
else if (args[1].ToLower() == "off")
{
debugEnabled = false;
player.ChatMessage("PlayerStatTags debugging is now OFF.");
Puts("PlayerStatTags debugging disabled by admin.");
}
else
{
player.ChatMessage("Usage: /playerstattag debug on/off");
}
}
else
{
player.ChatMessage("Unknown subcommand. Usage: /playerstattag debug on/off");
}
}
[ChatCommand("purgedata")]
private void PurgeDataCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, "playerstattags.admin"))
{
player.ChatMessage("You do not have permission to run this command.");
return;
}
PurgeNonPlayerData();
player.ChatMessage("Purged non-player data from stat records.");
}
[ChatCommand("clearstats")]
private void ClearStatsCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, "playerstattags.admin"))
{
player.ChatMessage("You do not have permission to use this command.");
return;
}
if (args.Length == 0)
{
player.ChatMessage("Usage: /clearstats all OR /clearstats <steamid>");
return;
}
if (args[0].ToLower() == "all")
{
storedData.playerStats.Clear();
SaveData();
player.ChatMessage("All stats have been cleared.");
Puts($"Admin {player.displayName} cleared all stats.");
}
else
{
ulong targetId;
if (!ulong.TryParse(args[0], out targetId))
{
player.ChatMessage("Invalid SteamID.");
return;
}
if (storedData.playerStats.ContainsKey(targetId))
{
storedData.playerStats.Remove(targetId);
SaveData();
player.ChatMessage($"Stats for {args[0]} have been cleared.");
Puts($"Admin {player.displayName} cleared stats for SteamID {args[0]}.");
}
else
{
player.ChatMessage("No stats found for that SteamID.");
}
}
}
[ConsoleCommand("clearstats")]
private void ClearStatsConsoleCommand(ConsoleSystem.Arg arg)
{
var args = arg.Args;
if (args == null || args.Length == 0)
{
Puts("Usage: clearstats all OR clearstats <steamid>");
return;
}
if (args[0].ToString().ToLower() == "all")
{
storedData.playerStats.Clear();
SaveData();
Puts("All stats have been cleared via console.");
}
else
{
ulong targetId;
if (!ulong.TryParse(args[0], out targetId))
{
Puts("Invalid SteamID.");
return;
}
if (storedData.playerStats.ContainsKey(targetId))
{
storedData.playerStats.Remove(targetId);
SaveData();
Puts($"Stats for SteamID {args[0]} have been cleared via console.");
}
else
{
Puts("No stats found for that SteamID.");
}
}
}
#endregion
#region Hooks – Tracking Events
private void OnServerInitialized()
{
Puts("PlayerStatTags initialized.");
LoadConfigData();
LoadData();
CheckForBetterChat();
CheckForServerRewards();
// Check for expansion packs
if (plugins.Exists("HarvestTagPack"))
{
Puts("HarvestTagPack detected - disabling redundant resource tracking in PlayerStatTags");
harvestTrackingEnabled = false;
}
if (plugins.Exists("TravelTagPack"))
{
Puts("TravelTagPack detected - disabling redundant travel tracking in PlayerStatTags");
travelTrackingEnabled = false;
}
}
private void OnPlayerConnected(BasePlayer player)
{
// Inform player about hardcore mode if enabled
if (config.EnableHardcoreMode && player != null)
{
timer.Once(5f, () => {
if (player != null && player.IsConnected)
{
player.ChatMessage("<color=red>WARNING: Hardcore Mode is enabled!</color> Your stats will be reset if you die.");
}
});
}
if (ServerRewards == null || player == null) return;
timer.Once(1f, () => {
if (ServerRewards != null)
{
DebugLog($"Updating ServerRewards data for player {player.displayName} on connect");
UpdatePlayerServerRewardsData(player.userID);
}
});
}
private void OnPlayerDisconnected(BasePlayer player)
{
if (ServerRewards == null || player == null) return;
DebugLog($"Updating ServerRewards data for player {player.displayName} on disconnect");
UpdatePlayerServerRewardsData(player.userID);
}
private void CheckForServerRewards()
{
if (ServerRewards != null)
{
try
{
Puts($"ServerRewards plugin detected (Version: {ServerRewards.Version})");
Puts("Setting up polling for ServerRewards points");
timer.Once(5f, () => {
if (ServerRewards != null)
{
Puts("Performing initial ServerRewards data update...");
UpdateAllServerRewardsData();
}
});
updateTimer = timer.Every(30f, () => {
if (ServerRewards != null)
{
DebugLog("Running scheduled ServerRewards data update...");
UpdateAllServerRewardsData();
}
else
{
DebugLog("ServerRewards is no longer loaded - canceling timer");
updateTimer.Destroy();
updateTimer = null;
}
});
Subscribe(nameof(OnPlayerConnected));
Subscribe(nameof(OnPlayerDisconnected));
}
catch (Exception ex)
{
Puts($"Error while integrating with ServerRewards: {ex.Message}");
Puts($"Stack Trace: {ex.StackTrace}");
}
}
else
{
Puts("ServerRewards plugin not installed or not loaded. ServerRewards integration disabled.");
}
}
private Timer updateTimer;
private void UpdatePlayerServerRewardsData(ulong playerId)
{
if (ServerRewards == null || playerId == 0) return;
try
{
object pointsObj = ServerRewards.Call("CheckPoints", playerId);
if (pointsObj == null) return;
int rewardPoints;
if (pointsObj is int)
{
rewardPoints = (int)pointsObj;
}
else if (int.TryParse(pointsObj.ToString(), out int parsedPoints))
{
rewardPoints = parsedPoints;
}
else
{
DebugLog($"Could not parse points data for player {playerId}: {pointsObj}");
return;
}
UpdatePlayerServerRewardsPoints(playerId, rewardPoints);
}
catch (Exception ex)
{
DebugLog($"Error updating ServerRewards data for player {playerId}: {ex.Message}");
}
}
private void Init()
{
// Permissions, need to add more maybe?
permission.RegisterPermission("playerstattags.admin", this);
permission.RegisterPermission("playerstattags.test", this);
LoadConfigData();
LoadData();
foreach (var category in config.ChatTags.Keys)
topPlayers[category] = 0;
UpdateTopPlayers();
// Let mAke sure we continue to track the players
timer.Every(config.UpdateInterval, UpdateTopPlayers);
timer.Every(config.SaveInterval, SaveData);
//Use the time from the config to automatically remove owners, hopefully just the dictionary :)
timer.Every(config.VehicleOwnershipClearInterval, ClearExpiredVehicleOwnerships);
}
private void OnExplosiveThrown(BasePlayer player, BaseEntity explosive, ThrownWeapon item)
{
if (player == null) return;
UpdateStat(player.userID, "ExplosivesThrown", 1, player);
if (playerStats[player.userID]["ExplosivesThrown"] > GetTopValue("ExplosivesThrown"))
topPlayers["ExplosivesThrown"] = player.userID;
}
private void OnEntityBuilt(Planner planner, GameObject gameObject)
{
if (planner == null) return;
var player = planner.GetOwnerPlayer();
if (player == null) return;
UpdateStat(player.userID, "StructuresBuilt", 1, player);
if (playerStats[player.userID]["StructuresBuilt"] > GetTopValue("StructuresBuilt"))
topPlayers["StructuresBuilt"] = player.userID;
}
private void OnWeaponFired(BaseProjectile projectile, BasePlayer player, ItemModProjectile mod)
{
if (player == null || mod == null) return;
var modStr = mod.ToString().ToLower();
if (modStr.Contains("arrow"))
{
UpdateStat(player.userID, "ArrowsFired", 1, player);
if (playerStats[player.userID]["ArrowsFired"] > GetTopValue("ArrowsFired"))
topPlayers["ArrowsFired"] = player.userID;
}
if (modStr.Contains("ammo"))
{
UpdateStat(player.userID, "BulletsFired", 1, player);
if (playerStats[player.userID]["BulletsFired"] > GetTopValue("BulletsFired"))
topPlayers["BulletsFired"] = player.userID;
}
if (modStr.Contains("rocket") || (projectile?.ShortPrefabName?.Contains("rocket") ?? false))
{
UpdateStat(player.userID, "RocketsLaunched", 1, player);
if (playerStats[player.userID]["RocketsLaunched"] > GetTopValue("RocketsLaunched"))
topPlayers["RocketsLaunched"] = player.userID;
DebugLog($"[Debug] Player {player.userID} launched a rocket");
}
}
void OnEntitySpawned(SupplyDrop entity)
{
NextTick(() =>
{
if (entity != null)
airdrops.Add(entity.net.ID.Value);
});
}
void OnLootEntity(BasePlayer player, SupplyDrop entity)
{
if (player == null || entity?.net?.ID == null)
return;
if (airdrops.Contains(entity.net.ID.Value))
{
airdrops.Remove(entity.net.ID.Value);
UpdateStat(player.userID, "DropsLooted", 1, player);
DebugLog($"[Debug] Player {player.userID} looted airdrop. Current count: {playerStats[player.userID]["DropsLooted"]}");
}
}
private void OnPlayerWounded(BasePlayer player)
{
if (player == null || player.IsNpc || IsHumanNPC(player)) return;
UpdateStat(player.userID, "TimesWounded", 1, player);
if (playerStats[player.userID]["TimesWounded"] > GetTopValue("TimesWounded"))
topPlayers["TimesWounded"] = player.userID;
DebugLog($"[Debug] Player {player.userID} was wounded. Current count: {playerStats[player.userID]["TimesWounded"]}");
}
private void OnMedicalItemUsed(MedicalTool tool, BasePlayer player)
{
if (player == null || player.IsNpc || IsHumanNPC(player)) return;
UpdateStat(player.userID, "TimesHealed", 1, player);
if (playerStats[player.userID]["TimesHealed"] > GetTopValue("TimesHealed"))
topPlayers["TimesHealed"] = player.userID;
DebugLog($"[Debug] Player {player.userID} used a medical item");
}
private void OnPlayerRecovered(BasePlayer player, BasePlayer healer)
{
if (healer == null || healer.IsNpc || IsHumanNPC(healer)) return;
UpdateStat(healer.userID, "TimesHealed", 1, healer);
if (playerStats[healer.userID]["TimesHealed"] > GetTopValue("TimesHealed"))
topPlayers["TimesHealed"] = healer.userID;
DebugLog($"[Debug] Player {healer.userID} healed another player. Current count: {playerStats[healer.userID]["TimesHealed"]}");
}
#endregion
#region Server Rewards tracking
private void UpdatePlayerServerRewardsPoints(ulong playerId, int points)
{
if (playerId == 0 || (config.EnableSteamIDExclusion && config.ExcludedSteamIDs.Contains(playerId))) return;
bool pointsChanged = false;
if (!playerStats.ContainsKey(playerId))
{
playerStats[playerId] = new Dictionary<string, double>();
pointsChanged = true;
}
if (!playerStats[playerId].ContainsKey("ServerRewards") ||
Math.Abs(playerStats[playerId]["ServerRewards"] - points) > 0.01)
{
playerStats[playerId]["ServerRewards"] = points;
pointsChanged = true;
DebugLog($"ServerRewards points updated for player {playerId}: {points}");
}
if (pointsChanged)
{
double topPoints = 0;
if (topPlayers.TryGetValue("ServerRewards", out var currentTopPlayer) &&
playerStats.TryGetValue(currentTopPlayer, out var topPlayerStats) &&
topPlayerStats.TryGetValue("ServerRewards", out topPoints))
{
if (points > topPoints)
{
topPlayers["ServerRewards"] = playerId;
DebugLog($"New top ServerRewards player: {playerId} with {points} points");
SaveData();
}
}
else if (points > 0)
{
topPlayers["ServerRewards"] = playerId;
DebugLog($"First top ServerRewards player: {playerId} with {points} points");
SaveData();
}
}
}
private void UpdateAllServerRewardsData()
{
if (ServerRewards == null)
{
DebugLog("ServerRewards plugin is null, cannot update data.");
return;
}
try
{
int changedPlayers = 0;
bool topPlayerChanged = false;
ulong oldTopPlayer = 0;
if (topPlayers.TryGetValue("ServerRewards", out var currentTop))
oldTopPlayer = currentTop;
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || player.userID == 0) continue;
object pointsObj = ServerRewards.Call("CheckPoints", player.userID);
if (pointsObj != null)
{
int rewardPoints;
if (pointsObj is int)
{
rewardPoints = (int)pointsObj;
}
else if (int.TryParse(pointsObj.ToString(), out int parsedPoints))
{
rewardPoints = parsedPoints;
}
else
{
DebugLog($"Could not parse points data for player {player.displayName}: {pointsObj}");
continue;
}
bool pointsChanged = false;
if (!playerStats.ContainsKey(player.userID))
{
playerStats[player.userID] = new Dictionary<string, double>();
pointsChanged = true;
}
if (!playerStats[player.userID].ContainsKey("ServerRewards") ||
Math.Abs(playerStats[player.userID]["ServerRewards"] - rewardPoints) > 0.01)
{
playerStats[player.userID]["ServerRewards"] = rewardPoints;
pointsChanged = true;
}
if (pointsChanged)
changedPlayers++;
if (rewardPoints > 0)
{
double topPoints = 0;
if (topPlayers.TryGetValue("ServerRewards", out var topPlayerId) &&
playerStats.TryGetValue(topPlayerId, out var topPlayerStats) &&
topPlayerStats.TryGetValue("ServerRewards", out topPoints))
{
if (rewardPoints > topPoints)
{
topPlayers["ServerRewards"] = player.userID;
topPlayerChanged = true;
DebugLog($"New top ServerRewards player: {player.displayName} with {rewardPoints} points");
}
}
else
{
topPlayers["ServerRewards"] = player.userID;
topPlayerChanged = true;
DebugLog($"First top ServerRewards player: {player.displayName} with {rewardPoints} points");
}
}
}
}
if (changedPlayers > 0 || topPlayerChanged)
{
SaveData();
if (topPlayerChanged)
{
ulong newTopPlayer = topPlayers["ServerRewards"];
if (newTopPlayer != oldTopPlayer)
{
string oldName = BasePlayer.FindByID(oldTopPlayer)?.displayName ?? oldTopPlayer.ToString();
string newName = BasePlayer.FindByID(newTopPlayer)?.displayName ?? newTopPlayer.ToString();
DebugLog($"Top ServerRewards player changed from {oldName} to {newName}");
}
}
DebugLog($"Updated ServerRewards data for {changedPlayers} players");
}
}
catch (Exception ex)
{
Puts($"Error updating ServerRewards data: {ex.Message}");
}
}
#endregion
#region Updated OnEntityDeath for Vehicles
private float crashThreshold = 10f;
private string[] heliVehicleNames = {
"minicopter",
"attackhelicopter",
"transporthelicopter",
"scrapped_helicopter"
};
private void OnEntityDeath(BaseEntity entity, HitInfo hitinfo)
{
if (entity == null)
{
Puts("[Debug] OnEntityDeath: entity is null");
return;
}
DebugLog($"[Debug] OnEntityDeath: Entity type: {entity.GetType().Name}, name: {entity.name}");
var victim
// ... [Code preview truncated due to length. Full source in attached file] ...