C#:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using Rust;
using UnityEngine;
namespace Oxide.Plugins {
[Info("RustStormRank", "Milestorme", "0.5.14")]
[Description("Low-overhead leaderboard and ranking core for RustStorm with current wipe, lifetime, team scopes, and polished rebuilt clean UI from stable core.")]
public class RustStormRank : RustPlugin {
[PluginReference]
private Plugin Clans;
#region Fields
private PluginConfig _config;
private StoredData _data;
private readonly HashSet<ulong> _dirtyPlayers = new HashSet<ulong>();
private readonly HashSet<ulong> _dirtyTeams = new HashSet<ulong>();
private readonly Dictionary<string, List<LeaderboardEntry>> _leaderboardCache = new Dictionary<string, List<LeaderboardEntry>>();
private readonly Dictionary<ulong, RuntimePlayerState> _runtimeStates = new Dictionary<ulong, RuntimePlayerState>();
private readonly Dictionary<ulong, Vector3> _lastPositionByPlayer = new Dictionary<ulong, Vector3>();
private readonly Dictionary<ulong, int> _playersPageByViewer = new Dictionary<ulong, int>();
private bool _leaderboardsDirty;
private Timer _saveTimer;
private Timer _recalcTimer;
private Timer _activityTimer;
private Timer _cleanupTimer;
private Timer _dailyDiscordTimer;
private DateTime _lastDailyDiscordPostUtc = DateTime.MinValue;
private string _lastDailyDiscordPostSlotKey = string.Empty;
private const string DataFileName = "RustStormRank_Data";
private const string ArchiveFilePrefix = "RustStormRank_Archive_";
private const string DefaultAdminPermission = "ruststormrank.admin";
private const string UiRoot = "RustStormRank.UI";
private const string UiBgMain = "0.025 0.030 0.040 0.985";
private const string UiBgHeader = "0.055 0.070 0.095 0.985";
private const string UiBgPanel = "0.070 0.085 0.110 0.955";
private const string UiBgCard = "0.090 0.110 0.145 0.965";
private const string UiBgStat = "0.145 0.165 0.205 0.940";
private const string UiBgStatSoft = "0.115 0.135 0.170 0.930";
private const string UiBgRow = "0.105 0.125 0.160 0.950";
private const string UiBgRowAlt = "0.090 0.108 0.140 0.950";
private const string UiBgRowTop = "0.135 0.125 0.095 0.955";
private const string UiBgRowSelf = "0.180 0.255 0.340 0.975";
private const string UiTextPrimary = "0.930 0.950 0.985 1.000";
private const string UiTextMuted = "0.660 0.710 0.780 1.000";
private const string UiTextSoft = "0.780 0.820 0.880 1.000";
private const string UiAccentBlue = "0.360 0.770 1.000 1.000";
private const string UiAccentGold = "0.930 0.720 0.370 1.000";
#endregion
#region Lifecycle
private void Init() {
LoadConfig();
LoadData();
permission.RegisterPermission(_config.General.AdminPermission, this);
AddCovalenceCommand(_config.General.ChatCommand, nameof(CommandRank));
AddCovalenceCommand("rankadmin", nameof(CommandRankAdmin));
}
private void OnServerInitialized() {
EnsureDataContainers();
DetectAndHandleWipe();
PrimeOnlinePlayers();
RemoveNpcRecords();
StartTimers();
RebuildAllCaches();
Puts($"[{Name}] Ready. Players={_data.Players.Count}, Teams={_data.Teams.Count}");
if (_config.Teams.UseClanTagsInTeamNames && Clans == null) PrintWarning("Team clan tags are enabled, but the Clans plugin was not found. Team names will be shown without clan tags.");
}
private void Unload() {
DestroyTimers();
var now = DateTime.UtcNow;
foreach (var player in BasePlayer.activePlayerList) {
if (IsValidPlayer(player)) {
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
RuntimePlayerState state;
if (_runtimeStates.TryGetValue(player.userID, out state)) AccumulatePlayedTime(record, state, now);
}
DestroyUi(player);
}
SaveData();
}
private void OnServerSave() {
SaveData();
}
private void OnNewSave(string filename) {
if (!_config.Wipe.AutoDetectMapWipe) return;
DetectAndHandleWipe();
RebuildAllCaches();
}
#endregion
#region Player Hooks
private void OnPlayerConnected(BasePlayer player) {
if (!IsValidPlayer(player)) return;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.LastKnownName = player.displayName;
record.IsNpc = player.IsNpc;
record.LastSeenUtc = DateTime.UtcNow;
EnsureRuntimeState(player, record);
_lastPositionByPlayer[player.userID] = player.transform.position;
MarkPlayerDirty(player.userID);
}
private void OnPlayerDisconnected(BasePlayer player, string reason) {
if (!IsValidPlayer(player)) return;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.LastKnownName = player.displayName;
record.IsNpc = player.IsNpc;
record.LastSeenUtc = DateTime.UtcNow;
RuntimePlayerState state;
if (_runtimeStates.TryGetValue(player.userID, out state)) AccumulatePlayedTime(record, state, DateTime.UtcNow);
MarkPlayerDirty(player.userID);
_lastPositionByPlayer.Remove(player.userID);
_runtimeStates.Remove(player.userID);
DestroyUi(player);
}
private void OnPlayerRespawned(BasePlayer player) {
if (!IsValidPlayer(player)) return;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.LastKnownName = player.displayName;
record.IsNpc = player.IsNpc;
record.LastSeenUtc = DateTime.UtcNow;
var now = DateTime.UtcNow;
record.CurrentLifeStartUtc = now;
var state = EnsureRuntimeState(player, record);
state.CurrentLifeStartUtc = now;
_lastPositionByPlayer[player.userID] = player.transform.position;
MarkPlayerDirty(player.userID);
}
#endregion
#region Combat / Resource / Build Hooks
private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) {
try {
if (entity == null || info == null) return;
var victimEntity = entity;
var victimPlayer = victimEntity as BasePlayer;
var attackerEntity = info.Initiator as BaseCombatEntity;
var attackerPlayer = info.InitiatorPlayer;
if (attackerEntity == null) return;
var totalDamage = info.damageTypes != null ? info.damageTypes.Total() : 0f;
if (totalDamage <= 0f) return;
if (victimPlayer != null && attackerPlayer != null && attackerPlayer == victimPlayer) return;
var victimIsPlayer = IsValidPlayer(victimPlayer);
var attackerIsPlayer = IsValidPlayer(attackerPlayer);
var victimIsNpc = victimPlayer != null && victimPlayer.IsNpc;
var attackerIsNpc = attackerPlayer != null && attackerPlayer.IsNpc;
var victimIsAnimal = IsTrackedAnimal(victimEntity);
var attackerIsAnimal = IsTrackedAnimal(attackerEntity);
if (victimIsPlayer && attackerIsPlayer) {
var victimRecord = GetOrCreatePlayerRecord(victimPlayer.userID, victimPlayer.displayName);
victimRecord.IsNpc = false;
victimRecord.CurrentWipe.PvP.DamageTaken += totalDamage;
victimRecord.Lifetime.PvP.DamageTaken += totalDamage;
MarkPlayerDirty(victimPlayer.userID);
var attackerRecord = GetOrCreatePlayerRecord(attackerPlayer.userID, attackerPlayer.displayName);
attackerRecord.IsNpc = false;
attackerRecord.CurrentWipe.PvP.DamageDealt += totalDamage;
attackerRecord.Lifetime.PvP.DamageDealt += totalDamage;
MarkPlayerDirty(attackerPlayer.userID);
return;
}
if (attackerIsPlayer && victimIsNpc) {
var attackerRecord = GetOrCreatePlayerRecord(attackerPlayer.userID, attackerPlayer.displayName);
attackerRecord.IsNpc = false;
attackerRecord.CurrentWipe.Survival.NpcDamageDone += totalDamage;
attackerRecord.Lifetime.Survival.NpcDamageDone += totalDamage;
MarkPlayerDirty(attackerPlayer.userID);
return;
}
if (attackerIsNpc && victimIsPlayer) {
var victimRecord = GetOrCreatePlayerRecord(victimPlayer.userID, victimPlayer.displayName);
victimRecord.IsNpc = false;
victimRecord.CurrentWipe.Survival.NpcDamageTaken += totalDamage;
victimRecord.Lifetime.Survival.NpcDamageTaken += totalDamage;
MarkPlayerDirty(victimPlayer.userID);
return;
}
if (attackerIsPlayer && victimIsAnimal) {
var attackerRecord = GetOrCreatePlayerRecord(attackerPlayer.userID, attackerPlayer.displayName);
attackerRecord.IsNpc = false;
attackerRecord.CurrentWipe.Survival.AnimalDamageDone += totalDamage;
attackerRecord.Lifetime.Survival.AnimalDamageDone += totalDamage;
MarkPlayerDirty(attackerPlayer.userID);
return;
}
if (attackerIsAnimal && victimIsPlayer) {
var victimRecord = GetOrCreatePlayerRecord(victimPlayer.userID, victimPlayer.displayName);
victimRecord.IsNpc = false;
victimRecord.CurrentWipe.Survival.AnimalDamageTaken += totalDamage;
victimRecord.Lifetime.Survival.AnimalDamageTaken += totalDamage;
MarkPlayerDirty(victimPlayer.userID);
}
}
catch (Exception ex) {
PrintError($"OnEntityTakeDamage error: {ex}");
}
}
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info) {
try {
if (entity == null) return;
var victimEntity = entity;
var victimPlayer = victimEntity as BasePlayer;
var attackerPlayer = info?.InitiatorPlayer;
var isTruePvPDeath = IsValidPlayer(victimPlayer) && IsValidPlayer(attackerPlayer) && attackerPlayer.userID != victimPlayer.userID;
var isPlayerKilledNpc = victimPlayer != null && victimPlayer.IsNpc && IsValidPlayer(attackerPlayer) && attackerPlayer.userID != victimPlayer.userID;
var isPlayerKilledAnimal = IsTrackedAnimal(victimEntity) && IsValidPlayer(attackerPlayer);
if (IsValidPlayer(victimPlayer)) {
var victimRecord = GetOrCreatePlayerRecord(victimPlayer.userID, victimPlayer.displayName);
victimRecord.IsNpc = false;
victimRecord.CurrentWipe.Survival.Respawns++;
victimRecord.Lifetime.Survival.Respawns++;
if (isTruePvPDeath) {
victimRecord.CurrentWipe.PvP.Deaths++;
victimRecord.Lifetime.PvP.Deaths++;
victimRecord.CurrentWipe.PvP.KillStreakCurrent = 0;
victimRecord.Lifetime.PvP.KillStreakCurrent = 0;
}
FinalizeLife(victimPlayer, victimRecord);
MarkPlayerDirty(victimPlayer.userID);
}
if (isTruePvPDeath) {
var attackerRecord = GetOrCreatePlayerRecord(attackerPlayer.userID, attackerPlayer.displayName);
attackerRecord.IsNpc = false;
attackerRecord.CurrentWipe.PvP.Kills++;
attackerRecord.Lifetime.PvP.Kills++;
if (info != null && info.isHeadshot) {
attackerRecord.CurrentWipe.PvP.Headshots++;
attackerRecord.Lifetime.PvP.Headshots++;
}
attackerRecord.CurrentWipe.PvP.KillStreakCurrent++;
attackerRecord.Lifetime.PvP.KillStreakCurrent++;
if (attackerRecord.CurrentWipe.PvP.KillStreakCurrent > attackerRecord.CurrentWipe.PvP.KillStreakBest) attackerRecord.CurrentWipe.PvP.KillStreakBest = attackerRecord.CurrentWipe.PvP.KillStreakCurrent;
if (attackerRecord.Lifetime.PvP.KillStreakCurrent > attackerRecord.Lifetime.PvP.KillStreakBest) attackerRecord.Lifetime.PvP.KillStreakBest = attackerRecord.Lifetime.PvP.KillStreakCurrent;
MarkPlayerDirty(attackerPlayer.userID);
return;
}
if (isPlayerKilledNpc) {
var attackerRecord = GetOrCreatePlayerRecord(attackerPlayer.userID, attackerPlayer.displayName);
attackerRecord.IsNpc = false;
attackerRecord.CurrentWipe.Survival.NpcsKilled++;
attackerRecord.Lifetime.Survival.NpcsKilled++;
MarkPlayerDirty(attackerPlayer.userID);
return;
}
if (isPlayerKilledAnimal) {
var attackerRecord = GetOrCreatePlayerRecord(attackerPlayer.userID, attackerPlayer.displayName);
attackerRecord.IsNpc = false;
attackerRecord.CurrentWipe.Survival.AnimalsKilled++;
attackerRecord.Lifetime.Survival.AnimalsKilled++;
MarkPlayerDirty(attackerPlayer.userID);
}
}
catch (Exception ex) {
PrintError($"OnEntityDeath error: {ex}");
}
}
private void OnDispenserGather(ResourceDispenser dispenser, BasePlayer player, Item item) {
try {
if (!IsValidPlayer(player) || item == null || item.info == null) return;
if (item.amount <= 0) return;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.IsNpc = player.IsNpc;
ApplyGather(record.CurrentWipe.Farm, item.info.shortname, item.amount);
ApplyGather(record.Lifetime.Farm, item.info.shortname, item.amount);
MarkPlayerDirty(player.userID);
}
catch (Exception ex) {
PrintError($"OnDispenserGather error: {ex}");
}
}
private void OnCollectiblePickup(Item item, BasePlayer player, CollectibleEntity collectible) {
try {
if (!IsValidPlayer(player) || item == null || item.info == null) return;
if (item.amount <= 0) return;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.IsNpc = player.IsNpc;
ApplyGather(record.CurrentWipe.Farm, item.info.shortname, item.amount);
ApplyGather(record.Lifetime.Farm, item.info.shortname, item.amount);
MarkPlayerDirty(player.userID);
}
catch (Exception ex) {
PrintError($"OnCollectiblePickup error: {ex}");
}
}
private void OnEntityBuilt(Planner planner, GameObject gameObject) {
try {
var player = planner?.GetOwnerPlayer();
if (!IsValidPlayer(player) || gameObject == null) return;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.IsNpc = player.IsNpc;
record.CurrentWipe.Build.StructuresBuilt++;
record.Lifetime.Build.StructuresBuilt++;
MarkPlayerDirty(player.userID);
}
catch (Exception ex) {
PrintError($"OnEntityBuilt error: {ex}");
}
}
private void OnStructureUpgrade(BuildingBlock block, BasePlayer player, BuildingGrade.Enum grade) {
try {
if (!IsValidPlayer(player) || block == null) return;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.IsNpc = player.IsNpc;
record.CurrentWipe.Build.StructuresUpgraded++;
record.Lifetime.Build.StructuresUpgraded++;
MarkPlayerDirty(player.userID);
}
catch (Exception ex) {
PrintError($"OnStructureUpgrade error: {ex}");
}
}
private void OnStructureRepair(BaseCombatEntity entity, BasePlayer player) {
try {
if (!IsValidPlayer(player) || entity == null) return;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.IsNpc = player.IsNpc;
record.CurrentWipe.Build.RepairsPerformed++;
record.Lifetime.Build.RepairsPerformed++;
MarkPlayerDirty(player.userID);
}
catch (Exception ex) {
PrintError($"OnStructureRepair error: {ex}");
}
}
#endregion
#region Activity Tracking
private void PrimeOnlinePlayers() {
foreach (var player in BasePlayer.activePlayerList) {
if (!IsValidPlayer(player)) continue;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
EnsureRuntimeState(player, record);
_lastPositionByPlayer[player.userID] = player.transform.position;
}
}
private void TickPlayerActivity() {
try {
var now = DateTime.UtcNow;
foreach (var player in BasePlayer.activePlayerList) {
if (!IsValidPlayer(player)) continue;
var record = GetOrCreatePlayerRecord(player.userID, player.displayName);
record.IsNpc = player.IsNpc;
var state = EnsureRuntimeState(player, record);
AccumulatePlayedTime(record, state, now);
var currentPos = player.transform.position;
Vector3 lastPos;
if (_lastPositionByPlayer.TryGetValue(player.userID, out lastPos)) {
var moved = Vector3.Distance(lastPos, currentPos);
if (!float.IsNaN(moved) && !float.IsInfinity(moved) && moved > 0f && moved < 500f) {
record.CurrentWipe.Survival.DistanceTraveled += moved;
record.Lifetime.Survival.DistanceTraveled += moved;
}
}
_lastPositionByPlayer[player.userID] = currentPos;
var lifeSeconds = (float)(now - state.CurrentLifeStartUtc).TotalSeconds;
if (lifeSeconds > record.CurrentWipe.Survival.LongestLifeSeconds) record.CurrentWipe.Survival.LongestLifeSeconds = lifeSeconds;
if (lifeSeconds > record.Lifetime.Survival.LongestLifeSeconds) record.Lifetime.Survival.LongestLifeSeconds = lifeSeconds;
UpdateTeamStateForPlayer(player);
MarkPlayerDirty(player.userID);
}
}
catch (Exception ex) {
PrintError($"TickPlayerActivity error: {ex}");
}
}
private void AccumulatePlayedTime(PlayerRecord record, RuntimePlayerState state, DateTime now) {
if (record == null || state == null) return;
if (state.LastActivityUpdateUtc <= DateTime.MinValue) {
state.LastActivityUpdateUtc = now;
return;
}
var elapsedSeconds = (float)(now - state.LastActivityUpdateUtc).TotalSeconds;
if (elapsedSeconds <= 0f) {
state.LastActivityUpdateUtc = now;
return;
}
if (elapsedSeconds > 86400f) elapsedSeconds = 86400f;
record.CurrentWipe.Survival.SecondsPlayed += elapsedSeconds;
record.Lifetime.Survival.SecondsPlayed += elapsedSeconds;
state.LastActivityUpdateUtc = now;
}
private RuntimePlayerState EnsureRuntimeState(BasePlayer player, PlayerRecord record = null) {
RuntimePlayerState state;
if (!_runtimeStates.TryGetValue(player.userID, out state)) {
var lifeStartUtc = record != null && record.CurrentLifeStartUtc > DateTime.MinValue ? record.CurrentLifeStartUtc : DateTime.UtcNow;
state = new RuntimePlayerState {
CurrentLifeStartUtc = lifeStartUtc, LastActivityUpdateUtc = DateTime.UtcNow, LastKnownTeamId = player.currentTeam
}
;
_runtimeStates[player.userID] = state;
}
if (record != null) {
if (record.CurrentLifeStartUtc <= DateTime.MinValue) record.CurrentLifeStartUtc = state.CurrentLifeStartUtc;
else if (state.CurrentLifeStartUtc != record.CurrentLifeStartUtc) state.CurrentLifeStartUtc = record.CurrentLifeStartUtc;
}
return state;
}
private void FinalizeLife(BasePlayer player, PlayerRecord record) {
var state = EnsureRuntimeState(player, record);
AccumulatePlayedTime(record, state, DateTime.UtcNow);
var lifeSeconds = (float)(DateTime.UtcNow - state.CurrentLifeStartUtc).TotalSeconds;
if (lifeSeconds > record.CurrentWipe.Survival.LongestLifeSeconds) record.CurrentWipe.Survival.LongestLifeSeconds = lifeSeconds;
if (lifeSeconds > record.Lifetime.Survival.LongestLifeSeconds) record.Lifetime.Survival.LongestLifeSeconds = lifeSeconds;
var now = DateTime.UtcNow;
state.CurrentLifeStartUtc = now;
record.CurrentLifeStartUtc = now;
}
private void UpdateTeamStateForPlayer(BasePlayer player) {
var state = EnsureRuntimeState(player);
var currentTeam = player.currentTeam;
if (state.LastKnownTeamId == currentTeam) return;
if (state.LastKnownTeamId != 0UL) MarkTeamDirty(state.LastKnownTeamId);
if (currentTeam != 0UL) MarkTeamDirty(currentTeam);
state.LastKnownTeamId = currentTeam;
}
#endregion
#region Commands
private void CommandRank(IPlayer iplayer, string command, string[] args) {
var player = iplayer?.Object as BasePlayer;
if (!IsValidPlayer(player)) {
iplayer?.Reply("This command can only be used by an in-game player.");
return;
}
var view = args != null && args.Length > 0 ? args[0].ToLowerInvariant() : "overview";
var scope = ResolveScope(args);
switch (view) {
case "top" : OpenRankUi(player, scope == RankScope.Team ? RankScope.Team : scope, "top", player.userID);
return;
case "pvp" : case "farm" : case "build" : case "survival" : case "overview" : OpenRankUi(player, scope, view, player.userID);
return;
case "players" : OpenRankUi(player, scope == RankScope.Team ? RankScope.CurrentWipe : scope, "players", player.userID);
return;
case "team" : OpenRankUi(player, RankScope.Team, "top", player.userID);
return;
case "lifetime" : OpenRankUi(player, RankScope.Lifetime, "overview", player.userID);
return;
default : OpenRankUi(player, scope, "overview");
return;
}
}
private void CommandRankAdmin(IPlayer iplayer, string command, string[] args) {
if (iplayer == null || !iplayer.HasPermission(_config.General.AdminPermission)) {
iplayer?.Reply("You do not have permission to use this command.");
return;
}
if (args == null || args.Length == 0) {
iplayer.Reply("Usage: /rankadmin rebuild | wipe | webhook <overall|pvp|farm|build|survival|team>");
return;
}
switch (args[0].ToLowerInvariant()) {
case "rebuild" : RebuildAllCaches();
iplayer.Reply("Rank caches rebuilt.");
return;
case "wipe" : ForceWipeRollover();
iplayer.Reply("Current wipe data rolled over.");
return;
case "webhook" : if (args.Length < 2) {
iplayer.Reply("Usage: /rankadmin webhook <overall|pvp|farm|build|survival|team>");
return;
}
var scope = args[1].Equals("team", StringComparison.OrdinalIgnoreCase) ? RankScope.Team : RankScope.CurrentWipe;
PostDiscordSummary(args[1].ToLowerInvariant(), scope);
iplayer.Reply($"Webhook attempted for {args[1]}.");
return;
default : iplayer.Reply("Unknown subcommand.");
return;
}
}
[ConsoleCommand("ruststormrank.ui")]
private void ConsoleCommandRankUi(ConsoleSystem.Arg arg) {
var player = arg?.Player();
if (!IsValidPlayer(player)) return;
var args = arg.Args;
var page = args != null && args.Length > 0 ? args[0].ToLowerInvariant() : "overview";
var scope = args != null && args.Length > 1 ? ParseScope(args[1]) : RankScope.CurrentWipe;
var targetUserId = player.userID;
if (args != null && args.Length > 2) {
ulong parsedTarget;
if (ulong.TryParse(args[2], out parsedTarget) && parsedTarget != 0UL) targetUserId = parsedTarget;
}
OpenRankUi(player, scope, page, targetUserId);
}
[ConsoleCommand("ruststormrank.playerspage")]
private void ConsoleCommandPlayersPage(ConsoleSystem.Arg arg) {
var player = arg?.Player();
if (!IsValidPlayer(player)) return;
var args = arg.Args;
var requestedPage = 0;
if (args != null && args.Length > 0) int.TryParse(args[0], out requestedPage);
if (requestedPage < 0) requestedPage = 0;
_playersPageByViewer[player.userID] = requestedPage;
var scope = args != null && args.Length > 1 ? ParseScope(args[1]) : RankScope.CurrentWipe;
var targetUserId = player.userID;
if (args != null && args.Length > 2) {
ulong parsedTarget;
if (ulong.TryParse(args[2], out parsedTarget) && parsedTarget != 0UL) targetUserId = parsedTarget;
}
OpenRankUi(player, scope, "players", targetUserId);
}
[ConsoleCommand("ruststormrank.close")]
private void ConsoleCommandCloseUi(ConsoleSystem.Arg arg) {
var player = arg?.Player();
if (!IsValidPlayer(player)) return;
DestroyUi(player);
}
#endregion
#region UI
private void OpenRankUi(BasePlayer player, RankScope scope, string page, ulong targetUserId = 0UL) {
DestroyUi(player);
var teamRecord = scope == RankScope.Team ? GetDisplayTeamRecord(player, targetUserId) : null;
var teamId = teamRecord != null ? teamRecord.TeamId : 0UL;
var record = GetDisplayPlayerRecord(player, scope == RankScope.Team ? 0UL : targetUserId);
if (record == null) return;
var normalizedTargetUserId = record.UserId;
var stats = scope == RankScope.Team ? GetScopeStats(teamRecord, ResolveTeamDataScope(page)) : GetScopeStats(record, scope);
var container = new CuiElementContainer();
var panel = container.Add(new CuiPanel {
Image = {
Color = UiBgMain
}
, RectTransform = {
AnchorMin = "0.17 0.13", AnchorMax = "0.83 0.87"
}
, CursorEnabled = true
}
, "Overlay", UiRoot);
container.Add(new CuiPanel {
Image = {
Color = UiBgHeader
}
, RectTransform = {
AnchorMin = "0 0.90", AnchorMax = "1 1"
}
}
, panel, UiRoot + ".Header");
AddLabel(container, panel, _config.General.UiTitle, 22, "0.03 0.925", "0.35 0.985", UiAccentBlue, TextAnchor.MiddleLeft);
AddLabel(container, panel, $"{GetScopeTitle(scope)}", 18, "0.36 0.925", "0.65 0.985", UiTextPrimary, TextAnchor.MiddleLeft);
var tabTargetId = scope == RankScope.Team ? teamId : normalizedTargetUserId;
AddButton(container, panel, "Overview", BuildUiCommand("overview", scope, player.userID, tabTargetId), "0.03 0.84", "0.155 0.89", false, page == "overview");
AddButton(container, panel, "PvP", BuildUiCommand("pvp", scope, player.userID, tabTargetId), "0.165 0.84", "0.255 0.89", false, page == "pvp");
AddButton(container, panel, "Farm", BuildUiCommand("farm", scope, player.userID, tabTargetId), "0.265 0.84", "0.355 0.89", false, page == "farm");
AddButton(container, panel, "Build", BuildUiCommand("build", scope, player.userID, tabTargetId), "0.365 0.84", "0.455 0.89", false, page == "build");
AddButton(container, panel, "Survival", BuildUiCommand("survival", scope, player.userID, tabTargetId), "0.465 0.84", "0.59 0.89", false, page == "survival");
AddButton(container, panel, "Top", BuildUiCommand("top", scope, player.userID, tabTargetId), "0.60 0.84", "0.69 0.89", false, page == "top");
AddButton(container, panel, "Lifetime", scope == RankScope.Lifetime ? BuildUiCommand("overview", RankScope.CurrentWipe, player.userID, normalizedTargetUserId) : BuildUiCommand("overview", RankScope.Lifetime, player.userID, normalizedTargetUserId), "0.70 0.84", "0.79 0.89", false, scope == RankScope.Lifetime);
if (scope == RankScope.Team) {
if (teamId != 0UL && HasValidTeamMembers(teamId)) AddButton(container, panel, "Members", "ruststormrank.playerspage 0 " + ScopeToArg(scope) + " " + teamId, "0.80 0.84", "0.89 0.89", false, page == "players");
}
else {
AddButton(container, panel, "Players", "ruststormrank.playerspage 0 " + ScopeToArg(scope) + " " + normalizedTargetUserId, "0.80 0.84", "0.89 0.89", false, page == "players");
}
AddButton(container, panel, "Teams", scope == RankScope.Team ? "ruststormrank.playerspage 0 " + ScopeToArg(RankScope.CurrentWipe) + " " + player.userID : BuildUiCommand("top", RankScope.Team, player.userID, 0UL), "0.90 0.84", "0.98 0.89", false, scope == RankScope.Team );
AddButton(container, panel, "Close", "ruststormrank.close", "0.89 0.925", "0.97 0.975", true);
AddLabel(container, panel, "Players Online: " + BasePlayer.activePlayerList.Count, 14, "0.68 0.925", "0.84 0.985", UiTextSoft, TextAnchor.MiddleRight);
if (scope == RankScope.Team) {
var teamName = teamRecord != null ? GetTeamDisplayName(teamRecord.TeamId) : "No Team";
AddLabel(container, panel, "Viewing: " + teamName, 13, "0.03 0.805", "0.30 0.845", UiAccentGold, TextAnchor.MiddleLeft);
}
else if (normalizedTargetUserId != player.userID) {
AddLabel(container, panel, "Viewing: " + record.LastKnownName, 13, "0.03 0.805", "0.30 0.845", UiAccentGold, TextAnchor.MiddleLeft);
}
AddSummaryCards(container, panel, stats, record, scope, teamRecord);
if (page == "top") AddLeaderboardSection(container, panel, player, scope, scope == RankScope.Team ? teamId : normalizedTargetUserId);
else if (page == "players") {
if (scope == RankScope.Team) AddTeamMembersSection(container, panel, player, teamId);
else AddPlayersSection(container, panel, player, scope, normalizedTargetUserId);
}
else AddStatsSection(container, panel, stats, scope, page, scope == RankScope.Team ? teamId : record.UserId);
CuiHelper.AddUi(player, container);
}
private void AddSummaryCards(CuiElementContainer container, string parent, ScopeStats stats, PlayerRecord record, RankScope scope, TeamRecord teamRecord = null) {
if (scope == RankScope.Team) {
if (teamRecord == null || !HasValidTeamMembers(teamRecord.TeamId)) {
AddCard(container, parent, "Team Score", "-", "0.03 0.68", "0.20 0.81", "0.91 0.70 0.35 1.00");
AddCard(container, parent, "PvP", "-", "0.215 0.68", "0.385 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Farm", "-", "0.40 0.68", "0.57 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Build", "-", "0.585 0.68", "0.755 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Survival", "-", "0.77 0.68", "0.94 0.81", "0.33 0.76 1.00 1.00");
AddLabel(container, parent, "Leader: -", 15, "0.04 0.63", "0.34 0.67", "0.85 0.92 1.00 1.00", TextAnchor.MiddleLeft);
AddLabel(container, parent, "Rank: -", 15, "0.35 0.63", "0.50 0.67", UiTextSoft, TextAnchor.MiddleLeft);
AddLabel(container, parent, "Top: -", 15, "0.51 0.63", "0.72 0.67", UiAccentBlue, TextAnchor.MiddleLeft);
AddLabel(container, parent, "Played: -", 15, "0.73 0.63", "0.94 0.67", UiAccentGold, TextAnchor.MiddleRight);
return;
}
AddCard(container, parent, "Team Score", stats.ScoreCache.TeamScore.ToString("F1"), "0.03 0.68", "0.20 0.81", "0.91 0.70 0.35 1.00");
AddCard(container, parent, "PvP", stats.ScoreCache.PvPScore.ToString("F1"), "0.215 0.68", "0.385 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Farm", stats.ScoreCache.FarmScore.ToString("F1"), "0.40 0.68", "0.57 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Build", stats.ScoreCache.BuildScore.ToString("F1"), "0.585 0.68", "0.755 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Survival", stats.ScoreCache.SurvivalScore.ToString("F1"), "0.77 0.68", "0.94 0.81", "0.33 0.76 1.00 1.00");
var teamRank = GetTeamRank(teamRecord.TeamId, RankScope.CurrentWipe);
var teamRankColor = GetRankDisplayColor(teamRank);
var leaderName = GetTeamLeaderName(teamRecord.TeamId);
var topPlayerName = GetTopPlayerNameInTeam(teamRecord.TeamId, RankScope.CurrentWipe);
AddLabel(container, parent, $"Leader: {leaderName}", 15, "0.04 0.63", "0.34 0.67", "0.85 0.92 1.00 1.00", TextAnchor.MiddleLeft);
AddLabel(container, parent, teamRank > 0 ? $"Rank: #{teamRank}" : "Rank: -", 15, "0.35 0.63", "0.50 0.67", teamRank > 0 ? teamRankColor : UiTextSoft, TextAnchor.MiddleLeft);
AddLabel(container, parent, $"Top: {topPlayerName}", 15, "0.51 0.63", "0.72 0.67", UiAccentBlue, TextAnchor.MiddleLeft);
AddLabel(container, parent, $"Played: {FormatDuration(stats.Survival.SecondsPlayed)}", 15, "0.73 0.63", "0.94 0.67", UiAccentGold, TextAnchor.MiddleRight);
return;
}
AddCard(container, parent, "Overall", stats.ScoreCache.OverallScore.ToString("F1"), "0.03 0.68", "0.20 0.81", "0.91 0.70 0.35 1.00");
AddCard(container, parent, "PvP", stats.ScoreCache.PvPScore.ToString("F1"), "0.215 0.68", "0.385 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Farm", stats.ScoreCache.FarmScore.ToString("F1"), "0.40 0.68", "0.57 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Build", stats.ScoreCache.BuildScore.ToString("F1"), "0.585 0.68", "0.755 0.81", "0.33 0.76 1.00 1.00");
AddCard(container, parent, "Survival", stats.ScoreCache.SurvivalScore.ToString("F1"), "0.77 0.68", "0.94 0.81", "0.33 0.76 1.00 1.00");
var playerRank = GetPlayerRank(record.UserId, scope);
var rankColor = GetRankDisplayColor(playerRank);
AddLabel(container, parent, $"Player: {record.LastKnownName}", 15, "0.04 0.63", "0.34 0.67", "0.85 0.92 1.00 1.00", TextAnchor.MiddleLeft);
AddLabel(container, parent, $"Rank: #{playerRank}", 15, "0.35 0.63", "0.50 0.67", rankColor, TextAnchor.MiddleLeft);
AddLabel(container, parent, $"KDR: {GetKdr(stats.PvP):F2}", 15, "0.51 0.63", "0.63 0.67", UiAccentBlue, TextAnchor.MiddleLeft);
AddLabel(container, parent, $"Played: {FormatDuration(stats.Survival.SecondsPlayed)}", 15, "0.64 0.63", "0.94 0.67", UiAccentGold, TextAnchor.MiddleRight);
}
private void AddStatsSection(CuiElementContainer container, string parent, ScopeStats stats, RankScope scope, string page, ulong recordUserId) {
var body = container.Add(new CuiPanel {
Image = {
Color = UiBgPanel
}
, RectTransform = {
AnchorMin = "0.03 0.05", AnchorMax = "0.97 0.62"
}
}
, parent);
AddLabel(container, body, GetPageTitle(page, scope), 19, "0.03 0.90", "0.97 0.98", UiTextPrimary, TextAnchor.MiddleLeft);
switch (page) {
case "pvp" : AddStatRow(container, body, 0.78f, "Kills", stats.PvP.Kills.ToString(), "Deaths", stats.PvP.Deaths.ToString(), "Headshot Kills", stats.PvP.Headshots.ToString());
AddStatRow(container, body, 0.62f, "KDR", GetKdr(stats.PvP).ToString("F2"), "Damage Dealt", stats.PvP.DamageDealt.ToString("F0"), "Damage Taken", stats.PvP.DamageTaken.ToString("F0"));
AddStatRow(container, body, 0.46f, "Best Streak", stats.PvP.KillStreakBest.ToString(), "Current Streak", stats.PvP.KillStreakCurrent.ToString(), "PvP Score", stats.ScoreCache.PvPScore.ToString("F1"));
break;
case "farm" : AddStatRow(container, body, 0.78f, "Wood", stats.Farm.WoodGathered.ToString(), "Stone", stats.Farm.StoneGathered.ToString(), "Metal", stats.Farm.MetalGathered.ToString());
AddStatRow(container, body, 0.62f, "Sulfur", stats.Farm.SulfurGathered.ToString(), "Nodes", stats.Farm.NodesHarvested.ToString(), "Farm Score", stats.ScoreCache.FarmScore.ToString("F1"));
AddStatRow(container, body, 0.46f, "Gather / Hour", CalculateGatherPerHour(stats).ToString("F0"), "Played", FormatDuration(stats.Survival.SecondsPlayed), "Scope", GetScopeTitle(scope));
break;
case "build" : AddStatRow(container, body, 0.78f, "Built", stats.Build.StructuresBuilt.ToString(), "Upgraded", stats.Build.StructuresUpgraded.ToString(), "Repaired", stats.Build.RepairsPerformed.ToString());
AddStatRow(container, body, 0.62f, "Build / Hour", CalculateBuildPerHour(stats).ToString("F1"), "Build Score", stats.ScoreCache.BuildScore.ToString("F1"), "Scope", GetScopeTitle(scope));
break;
case "survival" : AddStatRow(container, body, 0.78f, "Played", FormatDuration(stats.Survival.SecondsPlayed), "Longest Life", FormatDuration(stats.Survival.LongestLifeSeconds), "Respawns", stats.Survival.Respawns.ToString());
AddStatRow(container, body, 0.62f, "Distance", stats.Survival.DistanceTraveled.ToString("F0") + "m", "Survival Score", stats.ScoreCache.SurvivalScore.ToString("F1"), "Scope", GetScopeTitle(scope));
AddStatRow(container, body, 0.46f, "NPCs Killed", stats.Survival.NpcsKilled.ToString(), "NPC Damage Done", stats.Survival.NpcDamageDone.ToString("F0"), "NPC Damage Taken", stats.Survival.NpcDamageTaken.ToString("F0"));
AddStatRow(container, body, 0.30f, "Animals Killed", stats.Survival.AnimalsKilled.ToString(), "Animal Damage Done", stats.Survival.AnimalDamageDone.ToString("F0"), "Animal Damage Taken", stats.Survival.AnimalDamageTaken.ToString("F0"));
break;
default : if (scope == RankScope.Team) {
AddTeamOverviewSection(container, body, stats, recordUserId);
break;
}
AddLabel(container, body, "Score Summary", 16, "0.04 0.82", "0.30 0.88", UiAccentBlue, TextAnchor.MiddleLeft);
AddDivider(container, body, "0.04 0.795", "0.96 0.799");
AddMiniStat(container, body, "Overall Score", stats.ScoreCache.OverallScore.ToString("F1"), "0.04 0.71", "0.31 0.78");
AddMiniStat(container, body, "PvP Score", stats.ScoreCache.PvPScore.ToString("F1"), "0.365 0.71", "0.635 0.78");
AddMiniStat(container, body, "Farm Score", stats.ScoreCache.FarmScore.ToString("F1"), "0.69 0.71", "0.96 0.78");
AddMiniStat(container, body, "Build Score", stats.ScoreCache.BuildScore.ToString("F1"), "0.04 0.62", "0.31 0.69");
AddMiniStat(container, body, "Survival Score", stats.ScoreCache.SurvivalScore.ToString("F1"), "0.365 0.62", "0.635 0.69");
AddMiniStat(container, body, "Rank", "#" + GetPlayerRank(recordUserId, scope), "0.69 0.62", "0.96 0.69", GetRankDisplayColor(GetPlayerRank(recordUserId, scope)));
var currentTier = GetRankTier(stats.ScoreCache.OverallScore);
var nextTier = GetNextRankTier(stats.ScoreCache.OverallScore);
var progressPct = GetTierProgressPercent(stats.ScoreCache.OverallScore);
AddLabel(container, body, "Tier Progression", 16, "0.04 0.49", "0.30 0.55", UiAccentBlue, TextAnchor.MiddleLeft);
AddDivider(container, body, "0.04 0.465", "0.96 0.469");
AddMiniStat(container, body, "Current Tier", GetTierDisplay(currentTier), "0.04 0.38", "0.31 0.45", GetTierColor(currentTier));
AddMiniStat(container, body, "Next Tier", GetTierDisplay(nextTier), "0.365 0.38", "0.635 0.45", GetTierColor(nextTier));
AddMiniStat(container, body, "Progress", progressPct.ToString("F0") + "%", "0.69 0.38", "0.96 0.45");
AddProgressBar(container, body, progressPct, "0.69 0.345", "0.96 0.36", GetTierBarColor(currentTier, nextTier));
AddLabel(container, body, "Activity Snapshot", 16, "0.04 0.25", "0.30 0.31", UiAccentBlue, TextAnchor.MiddleLeft);
AddDivider(container, body, "0.04 0.225", "0.96 0.229");
AddMiniStat(container, body, "Kills", stats.PvP.Kills.ToString(), "0.04 0.14", "0.31 0.21");
AddMiniStat(container, body, "Nodes Gathered", stats.Farm.NodesHarvested.ToString(), "0.365 0.14", "0.635 0.21");
AddMiniStat(container, body, "Structures Built", stats.Build.StructuresBuilt.ToString(), "0.69 0.14", "0.96 0.21");
break;
}
}
private void AddLeaderboardSection(CuiElementContainer container, string parent, BasePlayer player, RankScope scope, ulong targetUserId) {
var body = container.Add(new CuiPanel {
Image = {
Color = UiBgPanel
}
, RectTransform = {
AnchorMin = "0.03 0.04", AnchorMax = "0.97 0.60"
}
}
, parent);
var category = scope == RankScope.Team ? "team" : "overall";
var key = GetLeaderboardCacheKey(scope, category);
List<LeaderboardEntry> entries;
if (!_leaderboardCache.TryGetValue(key, out entries)) entries = new List<LeaderboardEntry>();
AddLabel(container, body, "Top Rankings", 19, "0.03 0.90", "0.97 0.98", UiTextPrimary, TextAnchor.MiddleLeft);
AddLabel(container, body, "Competitive ladder for " + GetScopeTitle(scope), 14, "0.03 0.84", "0.60 0.90", UiTextMuted, TextAnchor.MiddleLeft);
if (entries.Count == 0) {
AddDivider(container, body, "0.05 0.775", "0.95 0.779");
AddLabel(container, body, "No leaderboard data available yet.", 17, "0.05 0.45", "0.95 0.60", UiTextSoft, TextAnchor.MiddleCenter);
return;
}
var selfRank = -1;
if (scope != RankScope.Team) {
for (var i = 0; i < entries.Count; i++) {
if (entries[i].EntryType == "player" && entries[i].Id == player.userID) {
selfRank = i + 1;
break;
}
}
}
var max = Mathf.Min(20, entries.Count);
const int rowsPerColumn = 10;
const float rowHeight = 0.060f;
const float rowGap = 0.010f;
const float yTop = 0.75f;
var useTwoColumns = max > rowsPerColumn;
if (useTwoColumns) {
AddLabel(container, body, "Rank", 12, "0.06 0.79", "0.14 0.84", UiTextMuted, TextAnchor.MiddleLeft);
AddLabel(container, body, "Name", 12, "0.15 0.79", "0.38 0.84", UiTextMuted, TextAnchor.MiddleLeft);
AddLabel(container, body, "Score", 12, "0.39 0.79", "0.46 0.84", UiTextMuted, TextAnchor.MiddleRight);
AddLabel(container, body, "Rank", 12, "0.53 0.79", "0.61 0.84", UiTextMuted, TextAnchor.MiddleLeft);
AddLabel(container, body, "Name", 12, "0.62 0.79", "0.85 0.84", UiTextMuted, TextAnchor.MiddleLeft);
AddLabel(container, body, "Score", 12, "0.86 0.79", "0.93 0.84", UiTextMuted, TextAnchor.MiddleRight);
}
else {
AddLabel(container, body, "Rank", 12, "0.06 0.79", "0.16 0.84", UiTextMuted, TextAnchor.MiddleLeft);
AddLabel(container, body, "Name", 12, "0.17 0.79", "0.74 0.84", UiTextMuted, TextAnchor.MiddleLeft);
AddLabel(container, body, "Score", 12, "0.76 0.79", "0.93 0.84", UiTextMuted, TextAnchor.MiddleRight);
}
AddDivider(container, body, "0.05 0.775", "0.95 0.779");
for (var i = 0; i < max; i++) {
var entry = entries[i];
var columnIndex = useTwoColumns ? i / rowsPerColumn : 0;
var rowIndex = useTwoColumns ? i % rowsPerColumn : i;
var top = yTop - (rowIndex * (rowHeight + rowGap));
var bottom = top - rowHeight;
// ... [Code preview truncated due to length. Full source in attached file] ...