C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Timers", "Von", "2.1.3")]
[Description("Advanced timer plugin with multi-timer and targeting support")]
public class Timers : RustPlugin
{
#region Fields
private const string PERMISSION_USE = "timers.use";
private const string TIMERS_UI_PANEL = "TimersUI";
private const int MAX_TIMERS = 5;
private Configuration config;
private List<TimerData> activeTimers = new List<TimerData>();
private HashSet<BasePlayer> playersWithUI = new HashSet<BasePlayer>();
private HashSet<BasePlayer> playersWithTimersUI = new HashSet<BasePlayer>();
private Dictionary<ulong, TimerCreationData> tempTimerData = new Dictionary<ulong, TimerCreationData>();
private Dictionary<ulong, string> tempEditTimerId = new Dictionary<ulong, string>();
private Dictionary<ulong, string> playerUiSnapshots = new Dictionary<ulong, string>();
private Timer updateTimer;
private Timer saveTimer;
private HashSet<ulong> playersInSettings = new HashSet<ulong>();
#endregion
#region Enums
public enum TimerPosition
{
TopRight,
TopLeft,
BottomRight,
BottomLeft
}
public enum TimerState
{
Active,
Paused,
Inactive
}
#endregion
#region Data Structures
private class SavedData
{
public List<SavedTimer> Timers { get; set; } = new List<SavedTimer>();
}
private class SavedTimer
{
public string Id { get; set; }
public DateTime EndTime { get; set; }
public string Text { get; set; }
public HashSet<string> TargetSteamIds { get; set; }
public HashSet<string> TargetGroups { get; set; }
public string CreatorId { get; set; }
public bool IsGlobal { get; set; }
public bool ShowCountdown { get; set; }
public string TextColor { get; set; }
public string BackgroundColor { get; set; }
public TimerState State { get; set; }
public double PausedTime { get; set; }
public bool ExecuteCommand { get; set; }
public string EndCommand { get; set; }
public static SavedTimer FromTimerData(TimerData timer)
{
return new SavedTimer
{
Id = timer.Id,
EndTime = timer.EndTime,
Text = timer.Text,
TargetSteamIds = timer.TargetSteamIds,
TargetGroups = timer.TargetGroups,
CreatorId = timer.CreatorId,
IsGlobal = timer.IsGlobal,
ShowCountdown = timer.ShowCountdown,
TextColor = timer.TextColor,
BackgroundColor = timer.BackgroundColor,
State = timer.State,
PausedTime = timer.PausedTime,
ExecuteCommand = timer.ExecuteCommand,
EndCommand = timer.EndCommand
};
}
public TimerData ToTimerData()
{
var timerData = new TimerData(Text, 0, CreatorId, IsGlobal, false)
{
Id = Id,
EndTime = EndTime,
TargetSteamIds = TargetSteamIds,
TargetGroups = TargetGroups,
ShowCountdown = ShowCountdown,
TextColor = TextColor,
BackgroundColor = BackgroundColor,
State = State,
PausedTime = PausedTime,
ExecuteCommand = ExecuteCommand,
EndCommand = EndCommand ?? string.Empty
};
return timerData;
}
}
private class TimerData
{
public string Id { get; set; }
public DateTime EndTime { get; set; }
public string Text { get; set; }
public HashSet<string> TargetSteamIds { get; set; }
public HashSet<string> TargetGroups { get; set; }
public string CreatorId { get; set; }
public bool IsGlobal { get; set; }
public bool ShowCountdown { get; set; }
public string TextColor { get; set; }
public string BackgroundColor { get; set; }
public TimerState State { get; set; }
public double PausedTime { get; set; }
public bool ExecuteCommand { get; set; }
public string EndCommand { get; set; }
public TimerData(string text, int durationSeconds, string creatorId, bool isGlobal = false, bool startImmediately = true)
{
Id = Guid.NewGuid().ToString("N");
Text = text;
TargetSteamIds = new HashSet<string>();
TargetGroups = new HashSet<string>();
CreatorId = creatorId;
IsGlobal = isGlobal;
ShowCountdown = true;
TextColor = "1 1 1 1";
BackgroundColor = "0 0 0 0.8";
ExecuteCommand = false;
EndCommand = string.Empty;
State = startImmediately ? TimerState.Active : TimerState.Inactive;
if (startImmediately)
{
EndTime = DateTime.Now.AddSeconds(durationSeconds);
PausedTime = 0;
}
else
{
EndTime = DateTime.MinValue;
PausedTime = durationSeconds;
}
}
public bool ShouldShowToPlayer(BasePlayer player, Oxide.Core.Libraries.Permission permission)
{
if (IsGlobal || TargetSteamIds.Count == 0 && TargetGroups.Count == 0)
return true;
if (TargetSteamIds.Contains(player.UserIDString))
return true;
return TargetGroups.Any(group => permission.UserHasGroup(player.UserIDString, group));
}
public string GetTimeString()
{
TimeSpan remaining;
if (State == TimerState.Active)
{
remaining = EndTime - DateTime.Now;
if (remaining.TotalSeconds <= 0)
remaining = TimeSpan.Zero;
}
else if (State == TimerState.Paused || State == TimerState.Inactive)
{
remaining = TimeSpan.FromSeconds(PausedTime);
}
else
{
remaining = TimeSpan.Zero;
}
if (!ShowCountdown)
return Text;
string timeStr;
if (remaining.TotalDays >= 1)
timeStr = $"{(int)remaining.TotalDays}d {remaining.Hours}h";
else if (remaining.TotalHours >= 1)
timeStr = $"{(int)remaining.TotalHours}h {remaining.Minutes}m";
else if (remaining.TotalMinutes >= 1)
timeStr = $"{remaining.Minutes}m {remaining.Seconds}s";
else
timeStr = $"{remaining.Seconds}s";
return string.IsNullOrEmpty(Text) ? timeStr : $"{Text}: {timeStr}";
}
}
private class TimerCreationData
{
public int Duration { get; set; }
public string Text { get; set; }
public HashSet<string> TargetSteamIds { get; set; }
public HashSet<string> TargetGroups { get; set; }
public bool IsGlobal { get; set; }
public bool ShowCountdown { get; set; }
public string TextColor { get; set; }
public string BackgroundColor { get; set; }
public bool StartImmediately { get; set; }
public bool IsEditing { get; set; }
public TimerState OriginalState { get; set; }
public bool ExecuteCommand { get; set; }
public string EndCommand { get; set; }
public TimerCreationData()
{
Duration = 300;
Text = string.Empty;
TargetSteamIds = new HashSet<string>();
TargetGroups = new HashSet<string>();
IsGlobal = true;
ShowCountdown = true;
TextColor = "1 1 1 1";
BackgroundColor = "0 0 0 0.8";
StartImmediately = true;
IsEditing = false;
OriginalState = TimerState.Active;
ExecuteCommand = false;
EndCommand = string.Empty;
}
}
private class Configuration
{
public string BackgroundColor = "0 0 0 0.8";
public string TextColor = "1 1 1 1";
public string HighlightColor = "0.2 0.8 0.2 1";
public bool EnableNotifications = false;
public int DefaultDuration = 300;
public int MaxDuration = 86400;
public int FontSize = 14;
public float UIScale = 1.0f;
public bool TransparentBackground = false;
public TimerPosition TimerPosition = TimerPosition.TopRight;
}
#endregion
#region Data Management
private void SaveData()
{
if (!activeTimers.Any())
{
Interface.Oxide.DataFileSystem.DeleteDataFile(Name + "_data");
return;
}
var data = new SavedData
{
Timers = activeTimers.Select(SavedTimer.FromTimerData).ToList()
};
Interface.Oxide.DataFileSystem.WriteObject(Name + "_data", data);
}
private void LoadData()
{
try
{
var data = Interface.Oxide.DataFileSystem.ReadObject<SavedData>(Name + "_data");
if (data?.Timers != null)
{
LoadTimersFromSavedData(data);
return;
}
var oldData = Interface.Oxide.DataFileSystem.ReadObject<List<SavedTimer>>(Name + "_data");
if (oldData != null)
{
var newData = new SavedData { Timers = oldData };
LoadTimersFromSavedData(newData);
SaveData();
return;
}
}
catch (Exception ex)
{
PrintError($"Error loading timer data: {ex.Message}");
activeTimers.Clear();
}
}
private void LoadTimersFromSavedData(SavedData data)
{
var now = DateTime.Now;
var validTimers = data.Timers
.Where(t => t.State == TimerState.Active ? t.EndTime > now : (t.State == TimerState.Paused || t.State == TimerState.Inactive))
.Select(t => t.ToTimerData())
.ToList();
activeTimers.Clear();
activeTimers.AddRange(validTimers);
activeTimers.Sort((a, b) => a.EndTime.CompareTo(b.EndTime));
if (validTimers.Count != data.Timers.Count)
{
SaveData();
}
}
#endregion
#region Oxide Hooks
private void Init()
{
permission.RegisterPermission(PERMISSION_USE, this);
cmd.AddConsoleCommand("timer", this, nameof(ConsoleCommand));
cmd.AddChatCommand("timers", this, nameof(ChatCommand));
LoadConfig();
LoadData();
InitializeTimerSystem();
}
private void OnServerInitialized()
{
foreach (var player in BasePlayer.activePlayerList)
{
OnPlayerConnected(player);
}
if (saveTimer != null && !saveTimer.Destroyed)
saveTimer.Destroy();
saveTimer = timer.Every(300f, SaveData);
}
private void OnServerSave() => SaveData();
private void Unload()
{
if (updateTimer != null && !updateTimer.Destroyed)
updateTimer.Destroy();
if (saveTimer != null && !saveTimer.Destroyed)
saveTimer.Destroy();
SaveData();
foreach (var player in BasePlayer.activePlayerList)
{
DestroyTimerUI(player);
}
}
private void OnPlayerConnected(BasePlayer player)
{
if (activeTimers.Any(t => t.State == TimerState.Active && t.ShouldShowToPlayer(player, permission)))
{
CreateTimerUI(player);
}
}
private void OnPlayerDisconnected(BasePlayer player)
{
DestroyTimerUI(player);
playersWithUI.Remove(player);
tempTimerData.Remove(player.userID);
tempEditTimerId.Remove(player.userID);
playersInSettings.Remove(player.userID);
}
#endregion
#region Timer System
private void InitializeTimerSystem()
{
if (updateTimer != null && !updateTimer.Destroyed)
{
updateTimer.Destroy();
}
updateTimer = timer.Every(1f, UpdateTimers);
}
private void UpdateTimers()
{
var expiredTimers = activeTimers.Where(t => t.State == TimerState.Active && t.EndTime != DateTime.MinValue && t.EndTime <= DateTime.Now).ToList();
foreach (var expiredTimer in expiredTimers)
{
HandleTimerExpiration(expiredTimer);
activeTimers.Remove(expiredTimer);
}
if (expiredTimers.Any())
{
UpdateAllUI();
SaveData();
}
else if (activeTimers.Any(t => t.State == TimerState.Active))
{
var players = BasePlayer.activePlayerList.Where(p => playersWithUI.Contains(p)).ToList();
foreach (var player in players)
{
var snapshot = BuildPlayerSnapshot(player);
if (string.IsNullOrEmpty(snapshot))
{
DestroyTimerUI(player);
playersWithUI.Remove(player);
playerUiSnapshots.Remove(player.userID);
continue;
}
if (!playerUiSnapshots.TryGetValue(player.userID, out var previous) || previous != snapshot)
{
UpdateTimerUI(player);
playerUiSnapshots[player.userID] = snapshot;
}
}
}
}
private void HandleTimerExpiration(TimerData timer)
{
if (timer.ExecuteCommand && !string.IsNullOrWhiteSpace(timer.EndCommand))
{
RunEndCommand(timer.EndCommand);
}
if (config.EnableNotifications)
{
foreach (var player in BasePlayer.activePlayerList)
{
if (!timer.ShouldShowToPlayer(player, permission))
continue;
SendReply(player, $"Timer completed: {timer.Text}");
}
}
SaveData();
}
private void RunEndCommand(string command)
{
if (string.IsNullOrWhiteSpace(command))
return;
try
{
ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet(), command.Trim());
}
catch (Exception ex)
{
PrintError($"Failed to run timer end command '{command}': {ex.Message}");
}
}
private void StartTimer(BasePlayer creator, TimerCreationData data)
{
if (creator == null || data == null)
{
PrintError("StartTimer called with null creator or data");
return;
}
if (!TryCreateTimer(data, creator.UserIDString, out var timer, out var error))
{
SendReply(creator, error);
return;
}
if (timer.State == TimerState.Active)
SendReply(creator, $"Timer started{GetTargetInfo(timer)}!");
else
SendReply(creator, $"Timer created{GetTargetInfo(timer)}!");
}
private string GetTargetInfo(TimerData timer)
{
if (timer.IsGlobal)
return " for all players";
List<string> targets = new List<string>();
if (timer.TargetSteamIds.Count > 0)
targets.Add($"{timer.TargetSteamIds.Count} players");
if (timer.TargetGroups.Count > 0)
targets.Add($"{timer.TargetGroups.Count} groups");
return targets.Count > 0 ? $" for {string.Join(" and ", targets)}" : "";
}
private bool TryCreateTimer(TimerCreationData data, string creatorId, out TimerData timer, out string error)
{
timer = null;
error = null;
if (data == null)
{
error = "Invalid timer data";
return false;
}
if (config == null)
{
LoadDefaultConfig();
}
if (activeTimers.Count >= MAX_TIMERS)
{
error = "You have reached the maximum number of active timers!";
return false;
}
if (data.Duration <= 0 || data.Duration > config.MaxDuration)
{
error = $"Duration must be between 1 and {config.MaxDuration} seconds!";
return false;
}
timer = new TimerData(data.Text ?? string.Empty, data.Duration, creatorId, data.IsGlobal, data.StartImmediately)
{
TargetSteamIds = data.TargetSteamIds ?? new HashSet<string>(),
TargetGroups = data.TargetGroups ?? new HashSet<string>(),
ShowCountdown = data.ShowCountdown,
TextColor = data.TextColor,
BackgroundColor = data.BackgroundColor,
ExecuteCommand = data.ExecuteCommand,
EndCommand = data.EndCommand ?? string.Empty
};
activeTimers.Add(timer);
activeTimers.Sort((a, b) => a.EndTime.CompareTo(b.EndTime));
UpdateAllUI();
SaveData();
return true;
}
private void StopTimer(string timerId)
{
var timer = activeTimers.FirstOrDefault(t => t.Id == timerId);
if (timer == null)
return;
activeTimers.Remove(timer);
UpdateAllUI();
SaveData();
}
private void UpdateTimerFromEdit(TimerData timer, TimerCreationData data)
{
if (timer == null || data == null) return;
timer.Text = data.Text ?? string.Empty;
timer.TargetSteamIds = data.TargetSteamIds ?? new HashSet<string>();
timer.TargetGroups = data.TargetGroups ?? new HashSet<string>();
timer.IsGlobal = timer.TargetSteamIds.Count == 0 && timer.TargetGroups.Count == 0;
timer.ShowCountdown = data.ShowCountdown;
timer.TextColor = data.TextColor;
timer.BackgroundColor = data.BackgroundColor;
timer.ExecuteCommand = data.ExecuteCommand;
timer.EndCommand = data.EndCommand ?? string.Empty;
int duration = Math.Max(1, data.Duration);
if (data.IsEditing && data.OriginalState == TimerState.Paused && !data.StartImmediately)
{
timer.State = TimerState.Paused;
timer.EndTime = DateTime.MinValue;
timer.PausedTime = duration;
return;
}
if (data.StartImmediately)
{
timer.State = TimerState.Active;
timer.EndTime = DateTime.Now.AddSeconds(duration);
timer.PausedTime = 0;
}
else
{
timer.State = TimerState.Inactive;
timer.EndTime = DateTime.MinValue;
timer.PausedTime = duration;
}
}
private int GetRemainingSeconds(TimerData timer)
{
if (timer == null) return 0;
if (timer.State == TimerState.Active)
{
var remaining = (timer.EndTime - DateTime.Now).TotalSeconds;
return Math.Max(1, (int)Math.Ceiling(remaining));
}
if (timer.State == TimerState.Paused || timer.State == TimerState.Inactive)
return Math.Max(1, (int)Math.Ceiling(timer.PausedTime));
return 0;
}
private void OpenEditTimerUI(BasePlayer player, TimerData timer)
{
if (player == null || timer == null) return;
if (!tempTimerData.ContainsKey(player.userID))
tempTimerData[player.userID] = new TimerCreationData();
var data = tempTimerData[player.userID];
data.Duration = GetRemainingSeconds(timer);
data.Text = timer.Text ?? string.Empty;
data.TargetSteamIds = new HashSet<string>(timer.TargetSteamIds ?? new HashSet<string>());
data.TargetGroups = new HashSet<string>(timer.TargetGroups ?? new HashSet<string>());
data.IsGlobal = timer.IsGlobal;
data.ShowCountdown = timer.ShowCountdown;
data.TextColor = timer.TextColor;
data.BackgroundColor = timer.BackgroundColor;
data.ExecuteCommand = timer.ExecuteCommand;
data.EndCommand = timer.EndCommand ?? string.Empty;
data.StartImmediately = timer.State == TimerState.Active;
data.IsEditing = true;
data.OriginalState = timer.State;
tempEditTimerId[player.userID] = timer.Id;
ShowTimerCreationUI(player);
}
private void UpdateAllUI()
{
foreach (var player in BasePlayer.activePlayerList)
{
if (activeTimers.Any(t => t.State == TimerState.Active && t.ShouldShowToPlayer(player, permission)))
{
if (!playersWithUI.Contains(player))
{
CreateTimerUI(player);
}
else
{
UpdateTimerUI(player);
}
}
else if (playersWithUI.Contains(player))
{
DestroyTimerUI(player);
playersWithUI.Remove(player);
}
}
}
#endregion
#region Commands
private void ConsoleCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
var isRcon = player == null;
if (!isRcon && !permission.UserHasPermission(player.UserIDString, PERMISSION_USE))
{
SendReply(player, "You don't have permission to use this command!");
return;
}
if (!arg.HasArgs())
{
SendReply(arg, "Usage: timer <start|stop|list> [duration] [text] [@group1 @group2 steamid1 steamid2 ...]");
return;
}
switch (arg.GetString(0).ToLower())
{
case "start":
if (!arg.HasArgs(2))
{
SendReply(arg, "Usage: timer start <seconds> [text] [@group1 @group2 steamid1 steamid2 ...]");
return;
}
if (!int.TryParse(arg.GetString(1), out int seconds))
{
SendReply(arg, "Please provide a valid number of seconds!");
return;
}
var textParts = new List<string>();
var data = new TimerCreationData
{
Duration = seconds,
Text = string.Empty,
IsGlobal = true,
TargetSteamIds = new HashSet<string>(),
TargetGroups = new HashSet<string>()
};
for (int i = 2; i < arg.Args.Length; i++)
{
string argValue = arg.GetString(i);
if (argValue.StartsWith("@"))
{
data.TargetGroups.Add(argValue.Substring(1));
data.IsGlobal = false;
}
else if (IsSteamId(argValue))
{
data.TargetSteamIds.Add(argValue);
data.IsGlobal = false;
}
else
{
textParts.Add(argValue);
}
}
data.Text = textParts.Count > 0 ? string.Join(" ", textParts) : string.Empty;
if (!TryCreateTimer(data, isRcon ? "RCON" : player.UserIDString, out var newTimer, out var error))
{
SendReply(arg, error);
return;
}
SendReply(arg, $"Timer started{GetTargetInfo(newTimer)}!");
break;
case "stop":
if (!activeTimers.Any())
{
SendReply(arg, "No active timers!");
return;
}
if (arg.Args.Length == 1)
{
string timerList = string.Join("\n", activeTimers.Select((t, i) => $"{i + 1}. {t.GetTimeString()}"));
SendReply(arg, $"Active timers:\n{timerList}\nUse: timer stop <number> or timer stop \"text\"");
return;
}
if (int.TryParse(arg.GetString(1), out int index) && index > 0 && index <= activeTimers.Count)
{
var timer = activeTimers[index - 1];
StopTimer(timer.Id);
SendReply(arg, $"Timer {index} stopped!");
}
else
{
string text = string.Join(" ", arg.Args.Skip(1));
var timer = activeTimers.FirstOrDefault(t => t.Text.Equals(text, StringComparison.OrdinalIgnoreCase));
if (timer != null)
{
StopTimer(timer.Id);
SendReply(arg, $"Timer '{text}' stopped!");
}
else
{
SendReply(arg, $"No timer found with text '{text}'");
}
}
break;
case "list":
if (!activeTimers.Any())
{
SendReply(arg, "No active timers!");
return;
}
string list = string.Join("\n", activeTimers.Select((t, i) => $"{i + 1}. {t.GetTimeString()}"));
SendReply(arg, $"Active timers:\n{list}");
break;
default:
SendReply(arg, "Usage: timer <start|stop|list> [duration] [text] [@group1 @group2 steamid1 steamid2 ...]");
break;
}
}
private void ChatCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PERMISSION_USE))
{
SendReply(player, "You don't have permission to use this command!");
return;
}
if (playersInSettings.Contains(player.userID))
ShowSettingsUI(player);
else
CreateTimersUI(player);
}
#endregion
#region UI System
private void CreateTimerUI(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
DestroyTimerUI(player);
if (!activeTimers.Any()) return;
var elements = new CuiElementContainer();
float startY = 0.98f;
float timerHeight = 0.03f;
float spacing = 0.005f;
var visibleTimers = activeTimers
.Where(t => t.State == TimerState.Active && t.ShouldShowToPlayer(player, permission))
.ToList();
for (int i = 0; i < visibleTimers.Count; i++)
{
var timer = visibleTimers[i];
float currentY = startY - (i * (timerHeight + spacing));
string displayText = timer.GetTimeString();
float aspectRatio = (float)Screen.width / Screen.height;
float baseAspect = 16f / 9f;
float aspectMultiplier = Math.Min(aspectRatio / baseAspect, 1.5f);
float charWidthRelative = 0.006f;
float paddingRelative = 0.008f;
float minWidthRelative = 0.05f;
float textWidth = displayText.Length * charWidthRelative;
float totalWidth = textWidth + (paddingRelative * 2);
float width = Math.Max(totalWidth, minWidthRelative) * aspectMultiplier;
string containerName = $"Timer_Container_{i}";
float margin = 0.001f;
string anchorMin, anchorMax;
switch (config.TimerPosition)
{
case TimerPosition.TopRight:
float rightEdge = 1.0f - margin;
anchorMin = $"{rightEdge - width} {currentY - timerHeight}";
anchorMax = $"{rightEdge} {currentY}";
break;
case TimerPosition.TopLeft:
float leftEdge = margin;
anchorMin = $"{leftEdge} {currentY - timerHeight}";
anchorMax = $"{leftEdge + width} {currentY}";
break;
case TimerPosition.BottomRight:
float rightEdgeBottom = 1.0f - margin;
float bottomY = 0.15f + (i * (timerHeight + spacing));
anchorMin = $"{rightEdgeBottom - width} {bottomY}";
anchorMax = $"{rightEdgeBottom} {bottomY + timerHeight}";
break;
case TimerPosition.BottomLeft:
float leftEdgeBottom = margin;
float bottomYLeft = 0.02f + (i * (timerHeight + spacing));
anchorMin = $"{leftEdgeBottom} {bottomYLeft}";
anchorMax = $"{leftEdgeBottom + width} {bottomYLeft + timerHeight}";
break;
default:
float defaultRightEdge = 1.0f - margin;
anchorMin = $"{defaultRightEdge - width} {currentY - timerHeight}";
anchorMax = $"{defaultRightEdge} {currentY}";
break;
}
elements.Add(new CuiElement
{
Parent = "Hud",
Components = {
new CuiImageComponent {
Color = timer.BackgroundColor
},
new CuiRectTransformComponent {
AnchorMin = anchorMin,
AnchorMax = anchorMax
}
},
Name = containerName
});
elements.Add(new CuiElement
{
Parent = containerName,
Components = {
new CuiTextComponent {
Text = displayText,
FontSize = config.FontSize,
Align = TextAnchor.MiddleCenter,
Color = timer.TextColor
},
new CuiRectTransformComponent {
AnchorMin = "0 0",
AnchorMax = "1 1"
}
},
Name = $"Timer_Text_{i}"
});
}
if (elements.Count > 0)
{
CuiHelper.AddUi(player, elements);
playersWithUI.Add(player);
playerUiSnapshots[player.userID] = BuildPlayerSnapshot(player);
}
else
{
playerUiSnapshots.Remove(player.userID);
}
}
private void UpdateTimerUI(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
CreateTimerUI(player);
}
private void DestroyTimerUI(BasePlayer player)
{
if (player == null) return;
int count = Math.Max(MAX_TIMERS, activeTimers.Count);
for (int i = 0; i < count; i++)
{
CuiHelper.DestroyUi(player, $"Timer_Container_{i}");
CuiHelper.DestroyUi(player, $"Timer_Text_{i}");
}
playerUiSnapshots.Remove(player.userID);
}
private void DestroyAllUIs(BasePlayer player)
{
if (player == null) return;
CuiHelper.DestroyUi(player, TIMERS_UI_PANEL);
playersWithTimersUI.Remove(player);
CuiHelper.DestroyUi(player, "TimerCreationUI");
CuiHelper.DestroyUi(player, "GroupsOverlay");
playersInSettings.Remove(player.userID);
}
private string BuildPlayerSnapshot(BasePlayer player)
{
if (player == null) return string.Empty;
var visibleTimers = activeTimers
.Where(t => t.State == TimerState.Active && t.ShouldShowToPlayer(player, permission))
.Select(t => $"{t.Id}:{t.GetTimeString()}")
.ToList();
if (visibleTimers.Count == 0)
return string.Empty;
return string.Join("|", visibleTimers);
}
private void ShowSettingsUI(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
DestroyAllUIs(player);
var elements = new CuiElementContainer();
elements.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.5 0.5", AnchorMax = "0.5 0.5", OffsetMin = "-400 -250", OffsetMax = "400 250" },
Image = { Color = "0.1 0.1 0.1 0.98" },
CursorEnabled = true
}, "Overlay", TIMERS_UI_PANEL);
elements.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0.9", AnchorMax = "1 1" },
Image = { Color = "0.8 0.8 0.8 0.2" }
}, TIMERS_UI_PANEL, "Header");
elements.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Text = { Text = "TIMER SETTINGS", FontSize = 20, Font = "robotocondensed-bold.ttf", Align = TextAnchor.MiddleCenter }
}, "Header");
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.92 0.92", AnchorMax = "0.98 0.98" },
Button = { Command = "timer.close", Color = "0.8 0.2 0.2 0.8" },
Text = { Text = "✕", FontSize = 14, Align = TextAnchor.MiddleCenter }
}, TIMERS_UI_PANEL);
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.82 0.92", AnchorMax = "0.90 0.98" },
Button = { Command = "timer.settings.back", Color = "0.2 0.6 0.8 0.8" },
Text = { Text = "BACK", FontSize = 14, Align = TextAnchor.MiddleCenter }
}, TIMERS_UI_PANEL);
elements.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.1 0.75", AnchorMax = "0.9 0.85" },
Text = { Text = "Timer Position:", FontSize = 18, Align = TextAnchor.MiddleCenter }
}, TIMERS_UI_PANEL);
float buttonWidth = 0.35f;
float buttonHeight = 0.12f;
float spacingX = 0.1f;
float spacingY = 0.08f;
var positions = new[]
{
new { Position = TimerPosition.TopLeft, Text = "TOP LEFT", X = 0.1f, Y = 0.55f },
new { Position = TimerPosition.TopRight, Text = "TOP RIGHT", X = 0.55f, Y = 0.55f },
new { Position = TimerPosition.BottomLeft, Text = "BOTTOM LEFT", X = 0.1f, Y = 0.35f },
new { Position = TimerPosition.BottomRight, Text = "BOTTOM RIGHT", X = 0.55f, Y = 0.35f }
};
foreach (var pos in positions)
{
bool isSelected = config.TimerPosition == pos.Position;
string buttonColor = isSelected ? "0.2 0.8 0.2 0.8" : "0.2 0.6 0.8 0.8";
elements.Add(new CuiButton
{
RectTransform = {
AnchorMin = $"{pos.X} {pos.Y}",
AnchorMax = $"{pos.X + buttonWidth} {pos.Y + buttonHeight}"
},
Button = { Command = $"timer.settings.position {pos.Position}", Color = buttonColor },
Text = { Text = pos.Text, FontSize = 14, Align = TextAnchor.MiddleCenter }
}, TIMERS_UI_PANEL);
if (isSelected)
{
elements.Add(new CuiLabel
{
RectTransform = {
AnchorMin = $"{pos.X} {pos.Y - 0.05f}",
AnchorMax = $"{pos.X + buttonWidth} {pos.Y}"
},
Text = { Text = "✓ CURRENT", FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "0.2 0.8 0.2 1" }
}, TIMERS_UI_PANEL);
}
}
CuiHelper.AddUi(player, elements);
playersWithTimersUI.Add(player);
}
private void CreateTimersUI(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
DestroyAllUIs(player);
var elements = new CuiElementContainer();
elements.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.5 0.5", AnchorMax = "0.5 0.5", OffsetMin = "-400 -250", OffsetMax = "400 250" },
Image = { Color = "0.1 0.1 0.1 0.98" },
CursorEnabled = true
}, "Overlay", TIMERS_UI_PANEL);
elements.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0.9", AnchorMax = "1 1" },
Image = { Color = "0.8 0.8 0.8 0.2" }
}, TIMERS_UI_PANEL, "Header");
elements.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Text = { Text = "TIMERS", FontSize = 20, Font = "robotocondensed-bold.ttf", Align = TextAnchor.MiddleCenter }
}, "Header");
elements.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0.82", AnchorMax = "1 0.88" },
Image = { Color = "0.1 0.1 0.1 0.8" }
}, TIMERS_UI_PANEL, "NavMenu");
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.92 0.92", AnchorMax = "0.98 0.98" },
Button = { Command = "timer.close", Color = "0.8 0.2 0.2 0.8" },
Text = { Text = "✕", FontSize = 14, Align = TextAnchor.MiddleCenter }
}, TIMERS_UI_PANEL);
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.82 0.92", AnchorMax = "0.90 0.98" },
Button = { Command = "timer.settings", Color = "0.2 0.6 0.8 0.8" },
Text = { Text = "SETTINGS", FontSize = 14, Align = TextAnchor.MiddleCenter }
}, TIMERS_UI_PANEL);
if (!activeTimers.Any())
{
elements.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.1 0.4", AnchorMax = "0.9 0.6" },
Text = { Text = "No active timers", FontSize = 16, Align = TextAnchor.MiddleCenter }
}, TIMERS_UI_PANEL);
}
else
{
float startY = 0.75f;
float itemHeight = 0.08f;
float spacing = 0.01f;
for (int i = 0; i < activeTimers.Count; i++)
{
var timer = activeTimers[i];
float currentY = startY - (i * (itemHeight + spacing));
string containerName = $"TimerItem_{i}";
elements.Add(new CuiPanel
{
RectTransform = { AnchorMin = $"0.05 {currentY - itemHeight}", AnchorMax = $"0.95 {currentY}" },
Image = { Color = timer.ShouldShowToPlayer(player, permission) ? "0.15 0.15 0.15 1" : "0.12 0.12 0.12 1" }
}, TIMERS_UI_PANEL, containerName);
string timerInfo = timer.GetTimeString();
if (!timer.IsGlobal)
{
string targetInfo = "";
if (timer.TargetGroups.Count > 0)
targetInfo += $" [Groups: {string.Join(", ", timer.TargetGroups)}]";
if (timer.TargetSteamIds.Count > 0)
targetInfo += $" [{timer.TargetSteamIds.Count} players]";
timerInfo += targetInfo;
}
string stateText = "";
switch (timer.State)
{
case TimerState.Active:
stateText = " [ACTIVE]";
break;
case TimerState.Paused:
stateText = " [PAUSED]";
break;
case TimerState.Inactive:
stateText = " [INACTIVE]";
break;
}
timerInfo += stateText;
elements.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.02 0", AnchorMax = "0.55 1" },
Text = {
Text = timerInfo,
FontSize = 14,
Align = TextAnchor.MiddleLeft,
Color = timer.ShouldShowToPlayer(player, permission) ? "1 1 1 1" : "0.7 0.7 0.7 1"
}
}, containerName);
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.57 0.2", AnchorMax = "0.68 0.8" },
Button = { Command = $"timer.edit {timer.Id}", Color = "0.2 0.6 0.8 0.9" },
Text = { Text = "EDIT", FontSize = 11, Align = TextAnchor.MiddleCenter }
}, containerName);
if (timer.State == TimerState.Active)
{
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.70 0.2", AnchorMax = "0.81 0.8" },
Button = { Command = $"timer.pause {timer.Id}", Color = "0.8 0.6 0.2 0.9" },
Text = { Text = "⏸ PAUSE", FontSize = 11, Align = TextAnchor.MiddleCenter }
}, containerName);
}
else
{
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.70 0.2", AnchorMax = "0.81 0.8" },
Button = { Command = $"timer.play {timer.Id}", Color = "0.2 0.8 0.2 0.9" },
Text = { Text = "▶ PLAY", FontSize = 11, Align = TextAnchor.MiddleCenter }
}, containerName);
}
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.83 0.2", AnchorMax = "0.98 0.8" },
Button = { Command = $"timer.stop {timer.Id}", Color = "0.7 0.2 0.2 0.9" },
Text = { Text = "STOP", FontSize = 12, Align = TextAnchor.MiddleCenter }
}, containerName);
}
}
elements.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.35 0.1", AnchorMax = "0.65 0.15" },
Button = { Command = "timer.create", Color = "0.2 0.7 0.2 0.9" },
Text = { Text = "CREATE TIMER", FontSize = 14, Align = TextAnchor.MiddleCenter }
}, TIMERS_UI_PANEL);
CuiHelper.AddUi(player, elements);
playersWithTimersUI.Add(player);
}
[ConsoleCommand("timer.close")]
private void TimerCloseCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
DestroyAllUIs(player);
}
[ConsoleCommand("timer.create")]
private void TimerCreateCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
ShowTimerCreationUI(player);
}
private void ShowTimerCreationUI(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
DestroyAllUIs(player);
if (!tempTimerData.ContainsKey(player.userID))
tempTimerData[player.userID] = new TimerCreationData();
var currentData = tempTimerData[player.userID];
string titleText = currentData.IsEditing ? "EDIT TIMER" : "CREATE TIMER";
string actionText = currentData.IsEditing ? "SAVE TIMER" : "CREATE TIMER";
var elements = new CuiElementContainer();
elements.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.5 0.5", AnchorMax = "0.5 0.5", OffsetMin = "-400 -250", OffsetMax = "400 250" },
Image = { Color = "0.1 0.1 0.1 0.98" },
CursorEnabled = true,
KeyboardEnabled = true
}, "Overla
// ... [Code preview truncated due to length. Full source in attached file] ...