C#:
using System;
using System.IO;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Collections.Concurrent;
using Facepunch;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using Newtonsoft.Json;
using Rust;
namespace Oxide.Plugins {
[Info("RustRoyale", "Potaetobag", "1.3.6"), Description("Rust Royale custom tournament game mode with point-based scoring system.")]
class RustRoyale : RustPlugin {
private bool initialized = false;
private const string ConfigUiPanelName = "RustRoyale_Config_UI";
private int _langDownloadsCompleted = 0;
private const int RequiredLangDownloads = 3;
private readonly Dictionary<string, string> pendingConfig = new Dictionary<string, string>();
private readonly Dictionary<ulong, int> npcKillCounts = new();
private readonly Dictionary<ulong, int> animalKillCounts = new();
private HashSet<ulong> welcomeOptOut = new HashSet<ulong>();
private string WelcomeOptOutFile => $"{DataDirectory}/WelcomeOptOut.json";
private Dictionary<ulong, string> teamLeaderNames = new();
private string TeamLeadersFile => $"{DataDirectory}/TeamLeaders.json";
private Lang lang;
private bool hasRebuiltCaps = false;
private string WithIndefiniteArticle(string word, string langCode) {
var lang = Interface.Oxide.GetLibrary<Lang>();
if (string.IsNullOrEmpty(word)) {
var unknown = lang.GetMessage("UnknownEntity", this, langCode) ?? "unknown entity";
if (langCode.StartsWith("en")) {
char first = unknown.ToLowerInvariant()[0];
var fmtTpl = "aeiou".Contains(first) ? lang.GetMessage("ArticleTemplateAE", this, langCode) : lang.GetMessage("ArticleTemplateA", this, langCode);
return string.Format(fmtTpl, unknown);
}
else {
var genericTpl = lang.GetMessage("ArticleTemplate", this, langCode) ?? "a {0}";
return string.Format(genericTpl, unknown);
}
}
if (langCode.StartsWith("en")) {
char first = word.ToLowerInvariant()[0];
var fmtTpl = "aeiou".Contains(first) ? lang.GetMessage("ArticleTemplateAE", this, langCode) : lang.GetMessage("ArticleTemplateA", this, langCode);
return string.Format(fmtTpl, word);
}
else {
var genericTpl = lang.GetMessage("ArticleTemplate", this, langCode) ?? "a {0}";
return string.Format(genericTpl, word);
}
}
#region Configuration
private ConfigData Configuration;
private class ConfigData {
public string DefaultLanguage = "en";
public bool EnableDebugLogs = false;
public string DiscordWebhookUrl {
get;
set;
}
= "";
public string ChatFormat {
get;
set;
}
= "[<color=#d97559>RustRoyale</color>] {message}";
public string ChatIconSteamId {
get;
set;
}
= "76561199815164411";
public string ChatUsername {
get;
set;
}
= "[RustRoyale]";
public string Timezone {
get;
set;
}
= "UTC";
public string StartDay {
get;
set;
}
= "Friday";
public bool AutoStartEnabled {
get;
set;
}
= true;
public bool AutoEnrollEnabled {
get;
set;
}
= true;
public bool PenaltyOnExitEnabled {
get;
set;
}
= true;
public bool ShowWelcomeUI {
get;
set;
}
= true;
public bool EnableAutoUpdate {
get;
set;
}
= false;
public int PenaltyPointsOnExit {
get;
set;
}
= 25;
public int StartHour {
get;
set;
}
= 12;
public int StartMinute {
get;
set;
}
= 0;
public int DurationHours {
get;
set;
}
= 125;
public int DataRetentionDays {
get;
set;
}
= 30;
public int TopPlayersToTrack {
get;
set;
}
= 3;
public int TopClansToTrack {
get;
set;
}
= 3;
public int JoinCutoffHours {
get;
set;
}
= 0;
// 0 = No late‑join cut‑off
public int NpcKillCap {
get;
set;
}
= 0;
public int AnimalKillCap {
get;
set;
}
= 0;
public List<int> NotificationIntervals {
get;
set;
}
= new List<int> {
600, 60
}
;
// Default: every 10 minutes (600 seconds) and last minute (60 seconds)
public Dictionary<string, int> ScoreRules {
get;
set;
}
= new Dictionary<string, int> {
{
"KILL", 15
}
, // Human player kills another human player
{
"DEAD", -12
}
, // Human player is killed by another human player
{
"JOKE", -10
}
, // Death by traps, self-inflicted damage
{
"NPC", 2
}
, // Kill an NPC (Murderer, Zombie, Scientist, Scarecrow)
{
"ENT", 30
}
, // Kill a Helicopter or Bradley Tank
{
"BRUH", -10
}
, // Death by an NPC, Helicopter, or Bradley
{
"WHY", 3
}
// Award points for killing an animal (wolf, boar, bear, stag, deer) from over Xm away
}
;
public float AnimalKillDistance {
get;
set;
}
= 150f;
public Dictionary<string, int> KitPrices {
get;
set;
}
= new Dictionary<string, int> {
{
"Starter", 5
}
, {
"Bronze", 20
}
, {
"Silver", 45
}
, {
"Gold", 70
}
, {
"Platinum", 110
}
, {
"Mystery Box", 65
}
}
;
}
#endregion
#region Configuration Handling
protected override void LoadDefaultConfig() {
Puts("Creating default configuration...");
Configuration = new ConfigData();
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(Configuration, true);
protected override void LoadConfig() {
try {
base.LoadConfig();
Configuration = Config.ReadObject<ConfigData>();
}
catch {
PrintWarning("Configuration file is corrupted or missing. Creating a new one.");
LoadDefaultConfig();
}
}
private void LogInfo(string message) => Puts($"[RustRoyale] {message}");
private void LogWarn(string message) => PrintWarning($"[RustRoyale] {message}");
private void LogErr(string message) => PrintError($"[RustRoyale] {message}");
private void DebugLog(string message) {
if (Configuration?.EnableDebugLogs != true) return;
Puts($"[RustRoyale] [Debug] {message}");
}
private void ValidateConfiguration() {
bool updated = false;
void SetDefault<T>(Func<T> getter, Action<T> setter, T defaultValue, string label, Func<T, bool> isInvalid) {
if (isInvalid(getter())) {
PrintWarning($"Invalid {label}. Defaulting to {defaultValue}.");
setter(defaultValue);
updated = true;
}
}
if (!Enum.TryParse(Configuration.StartDay, true, out DayOfWeek _)) {
PrintWarning("Invalid StartDay in configuration. Defaulting to Friday.");
Configuration.StartDay = "Friday";
updated = true;
}
SetDefault(() => Configuration.PenaltyPointsOnExit, v => Configuration.PenaltyPointsOnExit = v, 25, "PenaltyPointsOnExit", v => v < 0);
SetDefault(() => Configuration.AnimalKillDistance, v => Configuration.AnimalKillDistance = v, 150f, "AnimalKillDistance", v => v <= 0);
SetDefault(() => Configuration.StartHour, v => Configuration.StartHour = v, 14, "StartHour", v => v < 0 || v > 23);
SetDefault(() => Configuration.StartMinute, v => Configuration.StartMinute = v, 0, "StartMinute", v => v < 0 || v > 59);
SetDefault(() => Configuration.DataRetentionDays, v => Configuration.DataRetentionDays = v, 30, "DataRetentionDays", v => v <= 0);
SetDefault(() => Configuration.TopPlayersToTrack, v => Configuration.TopPlayersToTrack = v, 3, "TopPlayersToTrack", v => v <= 0);
SetDefault(() => Configuration.NpcKillCap, v => Configuration.NpcKillCap = v, 0, "NpcKillCap", v => v < 0);
SetDefault(() => Configuration.AnimalKillCap, v => Configuration.AnimalKillCap = v, 0, "AnimalKillCap", v => v < 0);
SetDefault(() => Configuration.TopClansToTrack, v => Configuration.TopClansToTrack = v, 3, "TopClansToTrack", v => v <= 0);
SetDefault(() => Configuration.JoinCutoffHours, v => Configuration.JoinCutoffHours = v, 6, "JoinCutoffHours", v => v < 0 || v > Configuration.DurationHours);
if (Configuration.DurationHours < 0.0167 || Configuration.DurationHours > 872) {
PrintWarning($"Invalid DurationHours: {Configuration.DurationHours}. Defaulting to 72.");
Configuration.DurationHours = 72;
updated = true;
}
else if (Configuration.DurationHours < 1) {
DebugLog($"Short tournament duration detected: {Configuration.DurationHours} hours. This is valid.");
}
if (Configuration.ScoreRules == null || Configuration.ScoreRules.Count == 0) {
PrintWarning("ScoreRules missing or empty. Using defaults.");
Configuration.ScoreRules = new Dictionary<string, int> {
{
"KILL", 5
}
, {
"DEAD", -3
}
, {
"JOKE", -1
}
, {
"NPC", 1
}
, {
"ENT", 20
}
, {
"BRUH", -2
}
, {
"WHY", 5
}
}
;
updated = true;
}
if (!typeof(bool).IsAssignableFrom(Configuration.EnableAutoUpdate.GetType())) {
PrintWarning("EnableAutoUpdate is invalid or missing. Defaulting to false.");
Configuration.EnableAutoUpdate = false;
updated = true;
}
if (Configuration.KitPrices == null || Configuration.KitPrices.Count == 0) {
PrintWarning("KitPrices missing or empty. Using defaults.");
Configuration.KitPrices = new Dictionary<string, int> {
{
"Starter", 5
}
, {
"Bronze", 25
}
, {
"Silver", 50
}
, {
"Gold", 75
}
, {
"Platinum", 100
}
, {
"Mystery Box", 50
}
}
;
updated = true;
}
if (Configuration.NotificationIntervals == null || !Configuration.NotificationIntervals.Any()) {
PrintWarning("NotificationIntervals is invalid or missing. Defaulting to every 10 minutes and the last minute.");
Configuration.NotificationIntervals = new List<int> {
600, 60
}
;
updated = true;
}
else {
var cleaned = Configuration.NotificationIntervals.Where(x => x > 0).Distinct().ToList();
if (cleaned.Count != Configuration.NotificationIntervals.Count) {
PrintWarning("NotificationIntervals contained invalid or duplicate values. Cleaning up the list.");
Configuration.NotificationIntervals = cleaned;
updated = true;
}
}
if (Configuration.ShowWelcomeUI != true && Configuration.ShowWelcomeUI != false) {
PrintWarning("Invalid ShowWelcomeUI. Defaulting to true.");
Configuration.ShowWelcomeUI = true;
updated = true;
}
if (updated) {
SaveConfig();
Puts("Configuration updated with validated defaults.");
}
Puts($"Configuration validated: StartDay={Configuration.StartDay}, StartHour={Configuration.StartHour}, DurationHours={Configuration.DurationHours}, " + $"DataRetentionDays={Configuration.DataRetentionDays}, TopPlayersToTrack={Configuration.TopPlayersToTrack}, " + $"TopClansToTrack={Configuration.TopClansToTrack}, NotificationIntervals={string.Join(", ", Configuration.NotificationIntervals)}.");
}
#endregion
#region Plugin Handle
private void OnUnload() {
if (saveDataTimer != null) {
saveDataTimer.Destroy();
saveDataTimer = null;
}
CleanupTimers();
SaveParticipantsData();
SavePlayerLanguages();
}
private void CleanupTimers() {
if (countdownTimer != null) {
countdownTimer.Destroy();
countdownTimer = null;
}
if (tournamentDurationTimer != null) {
tournamentDurationTimer.Destroy();
tournamentDurationTimer = null;
}
if (tournamentHeartbeatTimer != null) {
tournamentHeartbeatTimer.Destroy();
tournamentHeartbeatTimer = null;
}
}
#endregion
#region Language
private Dictionary<ulong, string> playerLanguages = new();
private void SavePlayerLanguages() => Interface.Oxide.DataFileSystem.WriteObject("RustRoyale_PlayerLanguages", playerLanguages);
private void LoadPlayerLanguages() => playerLanguages = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, string>>("RustRoyale_PlayerLanguages");
private string GetPlayerLang(BasePlayer player) {
if (playerLanguages.TryGetValue(player.userID, out var lang) && Translations.ContainsKey(lang)) return lang;
return Configuration.DefaultLanguage;
}
private string Lang(string key, BasePlayer player = null, Dictionary<string, string> tokens = null, string langOverride = null) {
string lang = langOverride ?? (player != null ? GetPlayerLang(player) : Configuration.DefaultLanguage);
if (!Translations.TryGetValue(lang, out var dict) || !dict.TryGetValue(key, out var message)) {
if (!Translations.TryGetValue("en", out var fallbackDict) || !fallbackDict.TryGetValue(key, out message)) {
Puts($"[Warning] Missing translation key '{key}' in '{lang}' and fallback 'en'");
return key;
}
}
if (tokens != null) {
foreach (var token in tokens) message = message.Replace("{" + token.Key + "}", token.Value);
}
return message;
}
private void EnsureLatestLangFile(string langCode) {
string url = $"https://raw.githubusercontent.com/potaetobag/RustRoyale/main/lang/{langCode}/RustRoyale.json";
string fullPath = Path.Combine(Interface.Oxide.RootDirectory, "oxide", "lang", langCode, "RustRoyale.json");
webrequest.Enqueue(url, null, (code, response) => {
if (code != 200 || string.IsNullOrEmpty(response)) {
Puts($"[LangLoader] Failed to fetch '{langCode}' language file. HTTP Code: {code}");
}
else {
try {
bool shouldWrite = true;
if (File.Exists(fullPath)) {
string currentContent = File.ReadAllText(fullPath);
shouldWrite = currentContent.Trim() != response.Trim();
}
if (shouldWrite) {
string dir = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
Puts($"[LangLoader] Attempting to write to: {fullPath}");
File.WriteAllText(fullPath, response);
Puts($"[LangLoader] Updated language file: oxide/lang/{langCode}/RustRoyale.json");
}
else {
Puts($"[LangLoader] oxide/lang/{langCode}/RustRoyale.json is already up to date.");
}
}
catch (Exception ex) {
Puts($"[LangLoader] Error saving oxide/lang/{langCode}/RustRoyale.json: {ex.Message}");
}
}
_langDownloadsCompleted++;
Puts($"[LangLoader] Completed {_langDownloadsCompleted}/{RequiredLangDownloads} language downloads.");
if (_langDownloadsCompleted >= RequiredLangDownloads) {
LoadTranslations();
Puts("[LangLoader] All language files downloaded. Continuing initialization.");
ContinueInitialization();
}
}
, this);
}
#endregion
#region Timezone Handling
private TimeZoneInfo GetTimezone() {
try {
return TimeZoneInfo.FindSystemTimeZoneById(Configuration.Timezone);
}
catch (TimeZoneNotFoundException) {
PrintWarning($"Invalid Timezone '{Configuration.Timezone}'. Defaulting to UTC.");
return TimeZoneInfo.Utc;
}
catch (Exception ex) {
PrintError($"Error loading timezone '{Configuration.Timezone}': {ex.Message}");
return TimeZoneInfo.Utc;
}
}
private DateTime GetLocalTime(DateTime utcTime) {
var timezone = GetTimezone();
return TimeZoneInfo.ConvertTimeFromUtc(utcTime, timezone);
}
private DateTime GetUtcTime(DateTime localTime) {
var timezone = GetTimezone();
return TimeZoneInfo.ConvertTimeToUtc(localTime, timezone);
}
#endregion
#region UI
private void AddCuiLabel(CuiElementContainer container, string parent, string anchorMin, string anchorMax, string text, int fontSize = 13, TextAnchor align = TextAnchor.MiddleLeft) {
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = anchorMin, AnchorMax = anchorMax
}
, Text = {
Text = text, FontSize = fontSize, Align = align
}
}
, parent);
}
private void AddCuiPanel(CuiElementContainer container, string parent, string anchorMin, string anchorMax, string color, string name = null) {
container.Add(new CuiPanel {
Image = {
Color = color
}
, RectTransform = {
AnchorMin = anchorMin, AnchorMax = anchorMax
}
}
, parent, name);
}
private void AddCuiButton(CuiElementContainer container, string parent, string anchorMin, string anchorMax, string text, string command, string color = "0.5 0.5 0.5 1", int fontSize = 13) {
container.Add(new CuiButton {
Button = {
Command = command, Color = color
}
, RectTransform = {
AnchorMin = anchorMin, AnchorMax = anchorMax
}
, Text = {
Text = text, FontSize = fontSize, Align = TextAnchor.MiddleCenter
}
}
, parent);
}
private void AddCuiRawImage(CuiElementContainer container, string parent, string anchorMin, string anchorMax, string url) {
container.Add(new CuiElement {
Parent = parent, Components = {
new CuiRawImageComponent {
Url = url
}
, new CuiRectTransformComponent {
AnchorMin = anchorMin, AnchorMax = anchorMax
}
}
}
);
}
private void LoadWelcomeOptOut() {
if (File.Exists(WelcomeOptOutFile)) {
welcomeOptOut = JsonConvert.DeserializeObject<HashSet<ulong>>(File.ReadAllText(WelcomeOptOutFile)) ?? new HashSet<ulong>();
}
}
private void SaveWelcomeOptOut() {
EnsureDataDirectory();
File.WriteAllText(WelcomeOptOutFile, JsonConvert.SerializeObject(welcomeOptOut, Formatting.Indented));
}
private const string WelcomeUiPanelName = "RustRoyale_Welcome_UI";
private void ShowWelcomeUI(BasePlayer player) {
CuiHelper.DestroyUi(player, WelcomeUiPanelName);
var container = new CuiElementContainer();
container.Add(new CuiPanel {
Image = {
Color = "0 0 0 0.6"
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "1 1"
}
, CursorEnabled = true
}
, "Overlay", WelcomeUiPanelName);
container.Add(new CuiPanel {
Image = {
Color = "0.1 0.1 0.1 0.95"
}
, RectTransform = {
AnchorMin = "0.15 0.1", AnchorMax = "0.85 0.9"
}
}
, WelcomeUiPanelName, $"{WelcomeUiPanelName}.main");
string scrollView = $"{WelcomeUiPanelName}.scroll";
container.Add(new CuiElement {
Name = scrollView, Parent = $"{WelcomeUiPanelName}.main", Components = {
new CuiScrollViewComponent(), new CuiRectTransformComponent {
AnchorMin = "0.05 0.1", AnchorMax = "0.95 0.95"
}
}
}
);
float y = 0.95f;
void AddTextBlock(string text, int size = 14) {
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = $"0 {y - 0.18f}", AnchorMax = $"1 {y}"
}
, Text = {
Text = text, FontSize = size, Align = TextAnchor.UpperLeft
}
}
, scrollView);
y -= 0.06f;
}
AddCuiRawImage(container, scrollView, "0.4 0.91", "0.6 0.98", "https://panels.twitch.tv/panel-1025512005-image-9ae6176b-7901-47e0-a0a2-2c16fed78df3");
y -= 0.07f;
AddTextBlock(Lang("WelcomeTitle", player), 18);
AddTextBlock(Lang("WelcomeDescription", player));
y -= 0.1f;
AddTextBlock(Lang("WelcomeDiscord", player));
y -= 0.05f;
float baseY = y;
float colH = 0.035f;
float colGap = 0.005f;
float rowSize = colH + colGap;
int maxRows = Math.Max(Math.Max(3, Configuration.ScoreRules.Count), Configuration.KitPrices.Count);
float col1Min = 0f, col1Max = 0.32f;
float col2Min = 0.34f, col2Max = 0.66f;
float col3Min = 0.68f, col3Max = 1f;
AddCuiLabel(container, scrollView, $"{col1Min} {baseY}", $"{col1Max} {baseY + colH}", $"<b>{Lang("StatsHeader", player)}</b>");
AddCuiLabel(container, scrollView, $"{col2Min} {baseY}", $"{col2Max} {baseY + colH}", $"<b>{Lang("RulesHeader", player)}</b>");
AddCuiLabel(container, scrollView, $"{col3Min} {baseY}", $"{col3Max} {baseY + colH}", $"<b>{Lang("KitPricesHeader", player)}</b>");
y = baseY - rowSize;
for (int i = 0; i < maxRows; i++) {
if (i < 3) {
string statText = "";
if (i == 0) {
statText = string.Format(Lang("Duration_Label", player), Configuration.DurationHours);
}
else if (i == 1) {
string timeLeft;
if (tournamentStartTime > DateTime.MinValue) {
var end = tournamentStartTime.AddHours(Configuration.DurationHours);
var span = end - DateTime.UtcNow;
timeLeft = span.TotalSeconds > 0 ? $"{(int)span.TotalHours}h {span.Minutes}m" : Lang("TournamentEnded", player);
}
else timeLeft = Lang("NotAvailable", player);
statText = string.Format(Lang("TimeLeft_Label", player), timeLeft);
}
else if (i == 2) {
statText = string.Format(Lang("Participants_Label", player), participants.Count);
}
AddCuiLabel(container, scrollView, $"{col1Min} {y - colH}", $"{col1Max} {y}", $"• {statText}", 13);
}
if (i < Configuration.ScoreRules.Count) {
var kv = Configuration.ScoreRules.ElementAt(i);
string key = kv.Key;
int value = kv.Value;
string friendly = "";
string cap = "";
switch (key) {
case "KILL" : friendly = Lang("Kill_Friendly", player);
break;
case "DEAD" : friendly = Lang("Dead_Friendly", player);
break;
case "JOKE" : friendly = Lang("Joke_Friendly", player);
break;
case "ENT" : friendly = Lang("Ent_Friendly", player);
break;
case "BRUH" : friendly = Lang("Bruh_Friendly", player);
break;
case "NPC" : if (Configuration.NpcKillCap > 0) cap = string.Format(Lang("MaxCapFormat", player), Configuration.NpcKillCap);
else cap = Lang("NoCap", player);
friendly = string.Format(Lang("NpcLabelFormat", player), cap);
break;
case "WHY" : if (Configuration.AnimalKillCap > 0) cap = string.Format(Lang("MaxCapFormat", player), Configuration.AnimalKillCap);
else cap = Lang("NoCap", player);
friendly = string.Format(Lang("AnimalKillFormat", player), Configuration.AnimalKillDistance, cap);
break;
default : friendly = key;
break;
}
string tpl = Lang("ScoreRuleLine", player);
string lineText = string.Format(tpl, friendly, value);
AddCuiLabel(container, scrollView, $"{col2Min} {y - colH}", $"{col2Max} {y}", lineText, 13);
}
if (i < Configuration.KitPrices.Count) {
var kit = Configuration.KitPrices.ElementAt(i);
string kitName = Lang($"Kit_{kit.Key}", player) ?? kit.Key;
string tpl = Lang("Kit_Label", player);
string text = string.Format(tpl, kitName, kit.Value);
AddCuiLabel(container, scrollView, $"{col3Min} {y - colH}", $"{col3Max} {y}", $"• {text}", 13);
}
y -= rowSize;
}
string toggleLabel = welcomeOptOut.Contains(player.userID) ? Lang("Toggle_ShowNextTime", player) : Lang("Toggle_DontShowAgain", player);
string toggleCmd = $"welcomeui_toggle {player.userID}";
container.Add(new CuiButton {
Button = {
Command = toggleCmd, Color = "0.2 0.5 0.8 1"
}
, RectTransform = {
AnchorMin = "0.35 0.01", AnchorMax = "0.49 0.06"
}
, Text = {
Text = toggleLabel, FontSize = 12, Align = TextAnchor.MiddleCenter
}
}
, $"{WelcomeUiPanelName}.main");
AddCuiButton(container, $"{WelcomeUiPanelName}.main", "0.51 0.01", "0.65 0.06", Lang("Button_Close", player), "welcomeui_close", "0.7 0.2 0.2 1");
y -= 0.07f;
AddTextBlock(Lang("HelpCommands_Text", player));
var pluginTitles = Interface.Oxide.RootPluginManager .GetPlugins() .Select(p => p.Title) .Where(t => !string.IsNullOrEmpty(t)) .Distinct();
AddTextBlock($"Plugins: {string.Join(", ", pluginTitles)}");
CuiHelper.AddUi(player, container);
}
[ConsoleCommand("welcomeui_close")]
private void CloseWelcomeUI(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player != null) {
CuiHelper.DestroyUi(player, WelcomeUiPanelName);
}
}
[ConsoleCommand("welcomeui_toggle")]
private void ToggleWelcomeUI(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null || arg.Args.Length < 1) return;
ulong id = player.userID;
if (welcomeOptOut.Contains(id)) {
welcomeOptOut.Remove(id);
}
else {
welcomeOptOut.Add(id);
}
SaveWelcomeOptOut();
ShowWelcomeUI(player);
}
void AddCuiInput(CuiElementContainer container, string parent, string key, string label, string value, float xLabel, float xInput, float y) {
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = $"{xLabel} {y}", AnchorMax = $"{xLabel + 0.2f} {y + 0.035f}"
}
, Text = {
Text = label, FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, parent);
container.Add(new CuiPanel {
Image = {
Color = "0.8 0.8 0.8 0.3"
}
, RectTransform = {
AnchorMin = $"{xInput} {y}", AnchorMax = $"{xInput + 0.2f} {y + 0.035f}"
}
}
, parent);
container.Add(new CuiElement {
Name = $"input_{key}", Parent = parent, Components = {
new CuiRectTransformComponent {
AnchorMin = $"{xInput} {y}", AnchorMax = $"{xInput + 0.2f} {y + 0.035f}"
}
, new CuiInputFieldComponent {
Text = value, Align = TextAnchor.MiddleCenter, Command = $"config_set {key}"
}
}
}
);
}
private void ShowConfigUI(BasePlayer player) {
CuiHelper.DestroyUi(player, ConfigUiPanelName);
var container = new CuiElementContainer();
container.Add(new CuiPanel {
Image = {
Color = "0 0 0 0.8"
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "1 1"
}
, CursorEnabled = true
}
, "Overlay", ConfigUiPanelName);
container.Add(new CuiPanel {
Image = {
Color = "0.1 0.1 0.1 0.95"
}
, RectTransform = {
AnchorMin = "0.2 0.1", AnchorMax = "0.8 0.9"
}
}
, ConfigUiPanelName, $"{ConfigUiPanelName}.main");
container.Add(new CuiPanel {
Image = {
Color = "0.25 0.35 0.45 1"
}
, RectTransform = {
AnchorMin = "0 0.93", AnchorMax = "1 1"
}
}
, $"{ConfigUiPanelName}.main", $"{ConfigUiPanelName}.header");
AddCuiLabel(container, $"{ConfigUiPanelName}.header", "0.02 0", "0.9 1", "RustRoyale Configuration", 18);
container.Add(new CuiButton {
Button = {
Color = "0.8 0.2 0.2 1", Command = "config_cancel"
}
, RectTransform = {
AnchorMin = "0.93 0.1", AnchorMax = "0.98 0.9"
}
, Text = {
Text = "<b>X</b>", FontSize = 16, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.header");
float y = 0.81f;
float xLeftLabel = 0.05f, xLeftInput = 0.25f;
float xRightLabel = 0.50f, xRightInput = 0.70f;
void AddToggle(string label, string key, bool value, float xLabel, float xButton, float y) {
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = $"{xLabel} {y}", AnchorMax = $"{xLabel + 0.2f} {y + 0.035f}"
}
, Text = {
Text = label, FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
container.Add(new CuiPanel {
Image = {
Color = "0.8 0.8 0.8 0.3"
}
, RectTransform = {
AnchorMin = $"{xButton} {y}", AnchorMax = $"{xButton + 0.2f} {y + 0.035f}"
}
}
, $"{ConfigUiPanelName}.main");
container.Add(new CuiButton {
Button = {
Command = $"config_set {key} {!value}", Color = value ? "0.3 0.6 0.3 1" : "0.6 0.3 0.3 1"
}
, RectTransform = {
AnchorMin = $"{xButton} {y}", AnchorMax = $"{xButton + 0.2f} {y + 0.035f}"
}
, Text = {
Text = value.ToString(), FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
}
AddCuiInput(container, $"{ConfigUiPanelName}.main", "Timezone", "Server Timezone", Configuration.Timezone, xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "StartDay", "Tournament Start Day", Configuration.StartDay, xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "StartHour", "Start Hour (0–23)", Configuration.StartHour.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "StartMinute", "Start Minute (0–59)", Configuration.StartMinute.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "DurationHours", "Tournament Duration (Hours)", Configuration.DurationHours.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "TopPlayersToTrack", "Top Players to Track", Configuration.TopPlayersToTrack.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "TopClansToTrack", "Top Clans to Track", Configuration.TopClansToTrack.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "JoinCutoffHours", "Join Cutoff Time (Hours)", Configuration.JoinCutoffHours.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "AnimalKillDistance", "Animal Kill Distance (m)", Configuration.AnimalKillDistance.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "NpcKillCap", "Max NPC Kills Per Player (0 = Unlimited)", Configuration.NpcKillCap.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "AnimalKillCap", "Max Animal Kills Per Player (0 = Unlimited)", Configuration.AnimalKillCap.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddCuiInput(container, $"{ConfigUiPanelName}.main", "PenaltyPointsOnExit", "Penalty Points on Exit", Configuration.PenaltyPointsOnExit.ToString(), xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddToggle("Enable Plugin Auto-Update", "EnableAutoUpdate", Configuration.EnableAutoUpdate, xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddToggle("Show Welcome Message", "ShowWelcomeUI", Configuration.ShowWelcomeUI, xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
AddToggle("Auto-Start Tournament", "AutoStartEnabled", Configuration.AutoStartEnabled, xLeftLabel, xLeftInput + 0.02f, y);
y -= 0.05f;
float toggleY = 0.81f;
string currentLang = Configuration.DefaultLanguage;
int currentIndex = Array.IndexOf(SupportedLanguages, currentLang);
int nextIndex = (currentIndex + 1) % SupportedLanguages.Length;
string nextLang = SupportedLanguages[nextIndex];
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = $"{xRightLabel} {toggleY}", AnchorMax = $"{xRightLabel + 0.2f} {toggleY + 0.035f}"
}
, Text = {
Text = "Default Language", FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
container.Add(new CuiButton {
Button = {
Color = "0.2 0.4 0.8 1", Command = $"config_set DefaultLanguage {nextLang}"
}
, RectTransform = {
AnchorMin = $"{xRightInput + 0.02f} {toggleY}", AnchorMax = $"{xRightInput + 0.22f} {toggleY + 0.035f}"
}
, Text = {
Text = currentLang, FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
toggleY -= 0.05f;
AddToggle("Auto-Enroll Players", "AutoEnrollEnabled", Configuration.AutoEnrollEnabled, xRightLabel, xRightInput + 0.02f, toggleY);
toggleY -= 0.05f;
AddToggle("Penalize Players Who Leave", "PenaltyOnExitEnabled", Configuration.PenaltyOnExitEnabled, xRightLabel, xRightInput + 0.02f, toggleY);
toggleY -= 0.05f;
y -= 0.05f;
float groupY = 0.66f;
foreach (var kv in Configuration.ScoreRules) {
float marginX = xRightLabel + 0.22f;
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = $"{xRightLabel} {groupY}", AnchorMax = $"{xRightLabel + 0.2f} {groupY + 0.035f}"
}
, Text = {
Text = $"ScoreRules[{kv.Key}]", FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
container.Add(new CuiPanel {
Image = {
Color = "0.8 0.8 0.8 0.3"
}
, RectTransform = {
AnchorMin = $"{marginX} {groupY}", AnchorMax = $"{marginX + 0.2f} {groupY + 0.035f}"
}
}
, $"{ConfigUiPanelName}.main");
container.Add(new CuiElement {
Name = $"ScoreRule_{kv.Key}", Parent = $"{ConfigUiPanelName}.main", Components = {
new CuiRectTransformComponent {
AnchorMin = $"{marginX} {groupY}", AnchorMax = $"{marginX + 0.2f} {groupY + 0.035f}"
}
, new CuiInputFieldComponent {
Text = kv.Value.ToString(), Align = TextAnchor.MiddleCenter, Command = $"config_set ScoreRules.{kv.Key}"
}
}
}
);
groupY -= 0.05f;
}
groupY -= 0.01f;
foreach (var kv in Configuration.KitPrices) {
float marginX = xRightLabel + 0.22f;
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = $"{xRightLabel} {groupY}", AnchorMax = $"{xRightLabel + 0.2f} {groupY + 0.035f}"
}
, Text = {
Text = $"KitPrices[{kv.Key}]", FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
container.Add(new CuiPanel {
Image = {
Color = "0.8 0.8 0.8 0.3"
}
, RectTransform = {
AnchorMin = $"{marginX} {groupY}", AnchorMax = $"{marginX + 0.2f} {groupY + 0.035f}"
}
}
, $"{ConfigUiPanelName}.main");
container.Add(new CuiElement {
Name = $"KitPrice_{kv.Key}", Parent = $"{ConfigUiPanelName}.main", Components = {
new CuiRectTransformComponent {
AnchorMin = $"{marginX} {groupY}", AnchorMax = $"{marginX + 0.2f} {groupY + 0.035f}"
}
, new CuiInputFieldComponent {
Text = kv.Value.ToString(), Align = TextAnchor.MiddleCenter, Command = $"config_set KitPrices.{kv.Key}"
}
}
}
);
groupY -= 0.05f;
}
container.Add(new CuiButton {
Button = {
Command = "config_save", Color = "0.2 0.6 0.2 1"
}
, RectTransform = {
AnchorMin = "0.27 0.02", AnchorMax = "0.47 0.07"
}
, Text = {
Text = Lang("ConfigSave"), FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
container.Add(new CuiButton {
Button = {
Command = "config_cancel", Color = "0.6 0.2 0.2 1"
}
, RectTransform = {
AnchorMin = "0.53 0.02", AnchorMax = "0.73 0.07"
}
, Text = {
Text = Lang("ConfigCancel"), FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
string currentVersion = GetCurrentPluginVersion();
string versionStatus = "Up to date";
string versionColor = "#00ff00";
if (!string.IsNullOrEmpty(LatestPluginVersion)) {
var current = ParseVersion(currentVersion);
var latest = ParseVersion(LatestPluginVersion);
if (latest > current) {
versionStatus = $"Update available ({LatestPluginVersion})";
versionColor = "#ff0000";
}
}
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = "0.20 0.84", AnchorMax = "0.80 0.93"
}
, Text = {
Text = $"<color={versionColor}>RustRoyale v{currentVersion} - {versionStatus}</color>", FontSize = 13, Align = TextAnchor.MiddleCenter
}
}
, $"{ConfigUiPanelName}.main");
CuiHelper.AddUi(player, container);
}
private void CloseConfigUI(BasePlayer player) {
CuiHelper.DestroyUi(player, ConfigUiPanelName);
}
[ConsoleCommand("config_set")]
private void ConsoleConfigSet(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null || !HasAdminPermission(player)) return;
var
// ... [Code preview truncated due to length. Full source in attached file] ...