C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins {
[Info("HydroUI", "belisario-afk + copilot", "2.4.0")]
[Description("Fullscreen welcome, menu toggle, player quick panel, full Track Editor wired to /hydro chat, with input-focus fixes and race-mode HUD tag.")]
public class HydroUI : RustPlugin {
#region Plugin References
[PluginReference]
private Plugin HydroRust;
[PluginReference]
private Plugin HydroLobby;
[PluginReference]
private Plugin ImageLibrary;
#endregion
#region Constants
private const string ROOT_START_MENU = "HydroUI.StartMenu";
private const string ROOT_HUD = "HydroUI.HUD";
private const string ROOT_TOGGLE = "HydroUI.MenuToggle";
private const string ROOT_ADMIN = "HydroUI.Admin";
private const string ROOT_PLAYER = "HydroUI.PlayerPanel";
private const string START_HEADER = "HydroUI.Start.Header";
private const string START_BODY = "HydroUI.Start.Body";
private const string START_TAB_WELCOME = "HydroUI.Start.Tab.Welcome";
private const string START_TAB_PLAY = "HydroUI.Start.Tab.Play";
private const string HUD_TEXT = "HydroUI.HUD.Text";
private const string HUD_PROGRESS_FILL = "HydroUI.HUD.Progress.Fill";
private const string HUD_BOOST_FILL = "HydroUI.HUD.Boost.Fill";
private const string HUD_SPEED_TEXT = "HydroUI.HUD.Speed.Text";
private const string HUD_LAP_TEXT = "HydroUI.HUD.Lap.Text";
private const string HUD_COMPASS_TEXT = "HydroUI.HUD.Compass.Text";
private const string ADMIN_HEADER = "HydroUI.Admin.Header";
private const string ADMIN_SUMMARY_BAR = "HydroUI.Admin.SummaryBar";
private const string ADMIN_SUMMARY_BAR_INNER = "HydroUI.Admin.SummaryBar.Inner";
private const string ADMIN_TRACK_LIST = "HydroUI.Admin.TrackList";
private const string ADMIN_TRACKLIST_BODY = "HydroUI.Admin.TrackList.Body";
private const string ADMIN_INSPECTOR = "HydroUI.Admin.Inspector";
private const string ADMIN_INSPECTOR_BODY = "HydroUI.Admin.Inspector.Body";
private const string IMG_START_BG = "hydro_ui_start_bg";
private const string IMG_TRACK_DEFAULT = "hydro_ui_track_default";
private const float TICK_HUD = 0.10f;
private const float TICK_ANIM = 0.05f;
private const float TICK_LOCK = 0.10f;
private const float TICK_EDITOR_REFRESH = 0.50f;
private const int TRACKS_PER_PAGE = 16;
#endregion
#region Config
private class Theme {
[JsonProperty]
public string Name = "HydroBlue";
[JsonProperty]
public string Primary = "0.0 0.75 1.0 0.95";
[JsonProperty]
public string Accent = "0.2 0.9 1.0 0.95";
[JsonProperty]
public string Background = "0.02 0.05 0.08 0.92";
[JsonProperty]
public string Panel = "0.07 0.10 0.15 0.96";
[JsonProperty]
public string Text = "1 1 1 1";
[JsonProperty]
public string MutedText = "0.85 0.92 1 0.90";
[JsonProperty]
public string Progress = "0.0 0.75 1.0 0.95";
[JsonProperty]
public string Boost = "1.0 0.7 0.1 0.95";
[JsonProperty]
public string Alert = "1.0 0.25 0.25 0.95";
[JsonProperty]
public string Good = "0.25 1.0 0.4 0.95";
[JsonProperty]
public string Battle = "1.0 0.3 0.3 0.95";
}
private class UITextures {
[JsonProperty]
public string StartBackgroundUrl = "https://i.imgur.com/z7X6G8R.png";
[JsonProperty]
public string TrackDefaultUrl = "https://i.imgur.com/GXq8mZZ.png";
[JsonProperty]
public string LogoUrl = "";
}
private class ChatCmds {
[JsonProperty]
public string NewTrack = "/hydro track.new {name}";
[JsonProperty]
public string SelectTrack = "/hydro track.select {name}";
[JsonProperty]
public string Save = "/hydro track.save";
[JsonProperty]
public string Rename = "/hydro track.rename {name}";
[JsonProperty]
public string SetLaps = "/hydro track.laps {laps}";
[JsonProperty]
public string SetStart = "/hydro track.setstart";
[JsonProperty]
public string SetFinish = "/hydro track.setfinish";
[JsonProperty]
public string MoveSelectedToAim = "/hydro track.move";
[JsonProperty]
public string AddCpAim = "/hydro track.addcp.aim {radius} {index}";
[JsonProperty]
public string AddBoostAim = "/hydro track.addboost.aim {strength} {duration} {radius}";
[JsonProperty]
public string DeleteSelected = "/hydro track.delete.selected";
[JsonProperty]
public string DeleteTrackByName = "/hydro track.delete {name}";
[JsonProperty]
public string ReloadMarkers = "/hydro track.reload";
[JsonProperty]
public string RadiusPlus = "/hydro track.radius +";
[JsonProperty]
public string RadiusMinus = "/hydro track.radius -";
[JsonProperty]
public string SetRadius = "/hydro track.setradius {radius}";
[JsonProperty]
public string SetColor = "/hydro track.setcolor {color}";
[JsonProperty]
public string RaceJoin = "/hydro race.join";
[JsonProperty]
public string RaceLeave = "/hydro race.leave";
[JsonProperty]
public string Stats = "/hydro stats";
// Voting
[JsonProperty]
public string VoteNormal = "/hydro vote normal";
[JsonProperty]
public string VoteBattle = "/hydro vote battle";
}
private class PluginConfig {
[JsonProperty]
public float HudScaleDefault = 1.0f;
[JsonProperty]
public bool HudCompactDefault = false;
[JsonProperty]
public List<Theme> Themes = new List<Theme> {
new Theme {
Name = "HydroBlue"
}
}
;
[JsonProperty]
public UITextures Textures = new UITextures();
[JsonProperty]
public ChatCmds Chat = new ChatCmds();
[JsonProperty]
public float FadeInSeconds = 0.35f;
[JsonProperty]
public float FadeOutSeconds = 0.25f;
[JsonProperty]
public float TweenSpeed = 8f;
[JsonProperty]
public string StartMenuAnchorMin = "0 0";
[JsonProperty]
public string StartMenuAnchorMax = "1 1";
[JsonProperty]
public float StartHeaderHeight = 0.14f;
[JsonProperty]
public string HudAnchorMin = "0.23 0.92";
[JsonProperty]
public string HudAnchorMax = "0.77 0.99";
[JsonProperty]
public string HudCompactAnchorMin = "0.35 0.94";
[JsonProperty]
public string HudCompactAnchorMax = "0.65 0.99";
[JsonProperty]
public string AdminAnchorMin = "0.76 0.05";
[JsonProperty]
public string AdminAnchorMax = "0.99 0.95";
[JsonProperty]
public string MenuToggleAnchorMin = "0.96 0.46";
[JsonProperty]
public string MenuToggleAnchorMax = "0.995 0.54";
[JsonProperty]
public string PlayerPanelAnchorMin = "0.84 0.02";
[JsonProperty]
public string PlayerPanelAnchorMax = "0.995 0.16";
[JsonProperty]
public float RadiusStep = 0.5f;
}
private PluginConfig config;
protected override void LoadDefaultConfig() {
config = new PluginConfig();
SaveConfig();
}
protected override void LoadConfig() {
base.LoadConfig();
try {
config = Config.ReadObject<PluginConfig>() ?? new PluginConfig();
}
catch {
PrintWarning("Invalid config; using defaults.");
config = new PluginConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config, true);
#endregion
#region Prefs & State
private class PlayerPrefs {
public ulong UserId;
public float HudScale;
public bool HudCompact;
public string ThemeName;
}
private class HudData {
public string TrackName = "";
public int Lap;
public int TotalLaps;
public int Checkpoint;
public int TotalCheckpoints;
public float Progress01;
public float Speed;
public float Boost01;
public int Position;
public int Racers;
public bool IsRace;
public bool Finished;
public float FinishTime;
public string RaceMode = "";
public bool Voting;
public int VoteSecondsRemaining;
}
private class PlayerUIState {
public BasePlayer Player;
public bool StartMenuVisible;
public int CurrentTab = 1;
public bool HudVisible;
public bool AdminVisible;
public float StartAlpha;
public float HudAlpha;
public float AdminAlpha;
public HudData Target = new HudData();
public HudData Display = new HudData();
public string LastHudText = "";
public string LastSpeedText = "";
public string LastLapText = "";
public string LastCompassText = "";
public float TargetYaw;
public float DisplayYaw;
public bool MenuToggleAllowed;
public bool MenuToggleVisible;
public bool WelcomeLocked;
public Vector3 LockPosition;
public int TrackListPage;
public List<string> TrackList = new List<string>();
public string EditorTrackName = "";
public string EditorSelectedType = "None";
public int EditorCpIndex = -1;
public int EditorBoostIndex = -1;
public int EditorCpCount;
public int EditorBoostCount;
public int EditorLaps = 1;
public string EditorDesigner = "Unknown";
public string EditorImageKey = "";
public bool AdminCursorEnabled = true;
public bool PlayerPanelVisible;
// Input debounce markers by key ("new","select","delete")
public Dictionary<string, float> ActiveInputEditing = new Dictionary<string, float>();
// Avoid rebuilding track list during periodic refresh
public bool TrackListDirty;
}
private readonly Dictionary<ulong, PlayerPrefs> prefs = new Dictionary<ulong, PlayerPrefs>();
private readonly Dictionary<ulong, PlayerUIState> states = new Dictionary<ulong, PlayerUIState>();
private const string DATA_PREFS = "HydroUI_Prefs";
private Timer hudTick;
private Timer animTick;
private Timer lockTick;
private Timer editorRefreshTick;
#endregion
#region Images
private void InitImages() {
if (ImageLibrary == null) {
PrintWarning("ImageLibrary not found; UI will use colors.");
return;
}
AddImage(IMG_START_BG, config.Textures.StartBackgroundUrl);
AddImage(IMG_TRACK_DEFAULT, config.Textures.TrackDefaultUrl);
if (!string.IsNullOrEmpty(config.Textures.LogoUrl)) AddImage("hydro_ui_logo", config.Textures.LogoUrl);
}
private void AddImage(string shortname, string url) {
if (ImageLibrary == null || string.IsNullOrEmpty(url)) return;
ImageLibrary.CallHook("AddImage", url, shortname);
}
private string GetImage(string shortname) {
if (ImageLibrary == null) return string.Empty;
return ImageLibrary.CallHook("GetImage", shortname) as string ?? "";
}
#endregion
#region Hooks & Timers
private void Init() {
LoadConfig();
LoadPrefs();
}
private void OnServerInitialized() {
InitImages();
StartTimers();
foreach (var p in BasePlayer.activePlayerList) {
EnsureState(p);
AutoShowLockedWelcome(p);
}
}
private void Unload() {
StopTimers();
DestroyAllUI();
SavePrefs();
}
private void OnPlayerConnected(BasePlayer player) {
EnsureState(player);
AutoShowLockedWelcome(player);
}
private void OnPlayerDisconnected(BasePlayer player, string reason) {
HideAll(player, false);
states.Remove(player.userID);
}
private void StartTimers() {
hudTick?.Destroy();
animTick?.Destroy();
lockTick?.Destroy();
editorRefreshTick?.Destroy();
hudTick = timer.Every(TICK_HUD, HudTick);
animTick = timer.Every(TICK_ANIM, AnimTick);
lockTick = timer.Every(TICK_LOCK, LockTick);
editorRefreshTick = timer.Every(TICK_EDITOR_REFRESH, EditorRefreshTick);
}
private void StopTimers() {
hudTick?.Destroy();
animTick?.Destroy();
lockTick?.Destroy();
editorRefreshTick?.Destroy();
hudTick = animTick = lockTick = editorRefreshTick = null;
}
private void HudTick() {
foreach (var st in states.Values) {
var player = st.Player;
if (player == null || !player.IsConnected) continue;
PullHudTarget(player, st);
LerpHud(st, config.TweenSpeed, TICK_HUD);
if (st.HudVisible) UpdateHudElements(st);
}
}
private void AnimTick() {
foreach (var st in states.Values) {
st.StartAlpha = MoveToward(st.StartAlpha, st.StartMenuVisible ? 1f : 0f, TICK_ANIM / config.FadeInSeconds);
st.HudAlpha = MoveToward(st.HudAlpha, st.HudVisible ? 1f : 0f, TICK_ANIM / config.FadeInSeconds);
st.AdminAlpha = MoveToward(st.AdminAlpha, st.AdminVisible ? 1f : 0f, TICK_ANIM / config.FadeInSeconds);
}
}
private void LockTick() {
foreach (var st in states.Values) {
var player = st.Player;
if (player == null || !player.IsConnected) continue;
if (!st.WelcomeLocked) continue;
if (Vector3.Distance(player.transform.position, st.LockPosition) > 0.25f) {
player.EnsureDismounted();
player.Teleport(st.LockPosition);
player.UpdateNetworkGroup();
player.SendNetworkUpdateImmediate();
}
}
}
private void EditorRefreshTick() {
foreach (var st in states.Values) {
if (!st.AdminVisible) continue;
var player = st.Player;
if (player == null || !player.IsConnected) continue;
// Only refresh summary/inspector periodically to preserve input focus in track list
var ctxObj = HydroRust?.Call("UI_GetEditorContext", player) as IDictionary<string, object>;
if (ctxObj != null) {
st.EditorTrackName = SafeString(ctxObj, "TrackName");
st.EditorSelectedType = SafeString(ctxObj, "SelectedType");
st.EditorCpIndex = SafeInt(ctxObj, "SelectedCheckpointIndex");
st.EditorBoostIndex = SafeInt(ctxObj, "SelectedBoostIndex");
st.EditorCpCount = SafeInt(ctxObj, "CheckpointCount");
st.EditorBoostCount = SafeInt(ctxObj, "BoostCount");
st.EditorLaps = SafeInt(ctxObj, "Laps");
st.EditorDesigner = SafeString(ctxObj, "Designer");
st.EditorImageKey = SafeString(ctxObj, "ImageKey");
}
UpdateSummaryBar(player, st);
UpdateInspectorPanel(player, st);
// Track list is not rebuilt here to avoid stealing input focus
// It is refreshed explicitly on open/pagination and after name mutations
}
}
private static float MoveToward(float current, float target, float step) {
if (Math.Abs(current - target) <= step) return target;
return current + Mathf.Sign(target - current) * step;
}
private void LerpHud(PlayerUIState st, float speed, float dt) {
st.Display.Speed = Mathf.Lerp(st.Display.Speed, st.Target.Speed, 1f - Mathf.Exp(-speed * dt));
st.Display.Progress01 = Mathf.Lerp(st.Display.Progress01, st.Target.Progress01, 1f - Mathf.Exp(-speed * dt));
st.Display.Boost01 = Mathf.Lerp(st.Display.Boost01, st.Target.Boost01, 1f - Mathf.Exp(-speed * dt));
st.Display.Lap = st.Target.Lap;
st.Display.TotalLaps = st.Target.TotalLaps;
st.Display.Checkpoint = st.Target.Checkpoint;
st.Display.TotalCheckpoints = st.Target.TotalCheckpoints;
st.Display.TrackName = st.Target.TrackName;
st.Display.Position = st.Target.Position;
st.Display.Racers = st.Target.Racers;
st.Display.IsRace = st.Target.IsRace;
st.Display.Finished = st.Target.Finished;
st.Display.FinishTime = st.Target.FinishTime;
st.Display.RaceMode = st.Target.RaceMode;
st.Display.Voting = st.Target.Voting;
st.Display.VoteSecondsRemaining = st.Target.VoteSecondsRemaining;
st.DisplayYaw = Mathf.LerpAngle(st.DisplayYaw, st.TargetYaw, 1f - Mathf.Exp(-speed * dt));
}
#endregion
#region HUD
private void PullHudTarget(BasePlayer player, PlayerUIState st) {
var data = HydroRust?.Call("UI_GetHudData", player) as IDictionary<string, object>;
if (data != null) {
st.Target.TrackName = SafeString(data, "TrackName");
st.Target.Lap = SafeInt(data, "Lap");
st.Target.TotalLaps = SafeInt(data, "TotalLaps");
st.Target.Checkpoint = SafeInt(data, "Checkpoint");
st.Target.TotalCheckpoints = SafeInt(data, "TotalCheckpoints");
st.Target.Progress01 = Mathf.Clamp01(SafeFloat(data, "Progress01"));
st.Target.Speed = Mathf.Max(0f, SafeFloat(data, "Speed"));
st.Target.Boost01 = Mathf.Clamp01(SafeFloat(data, "Boost01"));
st.Target.Position = Mathf.Max(0, SafeInt(data, "Position"));
st.Target.Racers = Mathf.Max(0, SafeInt(data, "Racers"));
st.Target.IsRace = SafeBool(data, "IsRace");
st.Target.Finished = SafeBool(data, "Finished");
st.Target.FinishTime = Mathf.Max(0f, SafeFloat(data, "FinishTime"));
st.Target.RaceMode = SafeString(data, "RaceMode");
st.Target.Voting = SafeBool(data, "Voting");
st.Target.VoteSecondsRemaining = SafeInt(data, "VoteSecondsRemaining");
}
else {
st.Target.TrackName = "";
st.Target.Lap = 0;
st.Target.TotalLaps = 0;
st.Target.Checkpoint = 0;
st.Target.TotalCheckpoints = 0;
st.Target.Progress01 = 0f;
st.Target.Boost01 = 0f;
st.Target.Position = 0;
st.Target.Racers = 0;
st.Target.IsRace = false;
st.Target.Finished = false;
st.Target.FinishTime = 0f;
var boat = player.GetMounted()?.GetComponentInParent<BaseBoat>();
var rb = boat ? boat.GetComponent<Rigidbody>() ?? boat.GetComponentInChildren<Rigidbody>() : null;
st.Target.Speed = rb != null ? rb.velocity.magnitude : 0f;
}
st.TargetYaw = player?.eyes != null ? player.eyes.rotation.eulerAngles.y : 0f;
}
private void UpdateHudElements(PlayerUIState st) {
var player = st.Player;
if (player == null || !player.IsConnected) return;
var theme = GetTheme(GetPrefs(player.userID).ThemeName);
if (!HasUi(player, ROOT_HUD)) BuildHudShell(player, theme);
SetBarFill(player, HUD_PROGRESS_FILL, st.Display.Progress01, theme.Progress);
SetBarFill(player, HUD_BOOST_FILL, st.Display.Boost01, theme.Boost);
string speedText = st.Display.Speed.ToString("0.0", CultureInfo.InvariantCulture) + " m/s";
if (speedText != st.LastSpeedText) {
st.LastSpeedText = speedText;
CuiHelper.DestroyUi(player, HUD_SPEED_TEXT);
var c = new CuiElementContainer();
c.Add(new CuiLabel {
Text = {
Text = speedText, FontSize = 14, Align = TextAnchor.MiddleRight, Color = theme.Text
}
, RectTransform = {
AnchorMin = "0.70 0.05", AnchorMax = "0.98 0.95"
}
}
, ROOT_HUD, HUD_SPEED_TEXT);
CuiHelper.AddUi(player, c);
}
string lapText = st.Display.TotalLaps > 0 ? "Lap " + Mathf.Clamp(st.Display.Lap, 0, st.Display.TotalLaps) + "/" + st.Display.TotalLaps : "";
if (lapText != st.LastLapText) {
st.LastLapText = lapText;
CuiHelper.DestroyUi(player, HUD_LAP_TEXT);
var c = new CuiElementContainer();
c.Add(new CuiLabel {
Text = {
Text = lapText, FontSize = 14, Align = TextAnchor.MiddleLeft, Color = theme.Text
}
, RectTransform = {
AnchorMin = "0.02 0.05", AnchorMax = "0.30 0.95"
}
}
, ROOT_HUD, HUD_LAP_TEXT);
CuiHelper.AddUi(player, c);
}
string modeTag = "";
if (st.Display.Voting) modeTag = "[Voting]";
else if (!string.IsNullOrEmpty(st.Display.RaceMode)) modeTag = st.Display.RaceMode == "Battle" ? "[Battle]" : "[Race]";
string cpLine = st.Display.TotalCheckpoints > 0 ? "CP " + Mathf.Clamp(st.Display.Checkpoint, 0, st.Display.TotalCheckpoints) + "/" + st.Display.TotalCheckpoints : "";
string racePos = st.Display.IsRace && st.Display.Position > 0 && st.Display.Racers > 0 ? "Pos " + st.Display.Position + "/" + st.Display.Racers : (st.Display.IsRace ? "RACE" : "TIME");
string title = string.IsNullOrEmpty(st.Display.TrackName) ? "HydroRust" : (string.IsNullOrEmpty(modeTag) ? st.Display.TrackName : st.Display.TrackName + " " + modeTag);
string status = st.Display.Finished ? ("Finished: " + st.Display.FinishTime.ToString("0.00", CultureInfo.InvariantCulture) + "s") : "";
string hudLine = (title + " " + cpLine + " " + racePos + " " + status).Trim();
if (hudLine != st.LastHudText) {
st.LastHudText = hudLine;
CuiHelper.DestroyUi(player, HUD_TEXT);
var c = new CuiElementContainer();
c.Add(new CuiLabel {
Text = {
Text = hudLine, FontSize = 13, Align = TextAnchor.MiddleCenter, Color = theme.MutedText
}
, RectTransform = {
AnchorMin = "0.18 0.05", AnchorMax = "0.82 0.95"
}
}
, ROOT_HUD, HUD_TEXT);
CuiHelper.AddUi(player, c);
}
string compass = Mathf.Repeat(st.DisplayYaw + 360f, 360f).ToString("0", CultureInfo.InvariantCulture) + "°";
if (compass != st.LastCompassText) {
st.LastCompassText = compass;
CuiHelper.DestroyUi(player, HUD_COMPASS_TEXT);
var c = new CuiElementContainer();
c.Add(new CuiLabel {
Text = {
Text = compass, FontSize = 12, Align = TextAnchor.MiddleCenter, Color = theme.MutedText
}
, RectTransform = {
AnchorMin = "0.45 0.65", AnchorMax = "0.55 0.95"
}
}
, ROOT_HUD, HUD_COMPASS_TEXT);
CuiHelper.AddUi(player, c);
}
}
private void BuildHudShell(BasePlayer player, Theme theme) {
var p = GetPrefs(player.userID);
string min = p.HudCompact ? config.HudCompactAnchorMin : config.HudAnchorMin;
string max = p.HudCompact ? config.HudCompactAnchorMax : config.HudAnchorMax;
var container = new CuiElementContainer();
container.Add(new CuiPanel {
Image = {
Color = theme.Background
}
, RectTransform = {
AnchorMin = min, AnchorMax = max
}
, CursorEnabled = false
}
, "Hud", ROOT_HUD);
container.Add(new CuiPanel {
Image = {
Color = theme.Panel
}
, RectTransform = {
AnchorMin = "0.02 0.55", AnchorMax = "0.98 0.75"
}
, CursorEnabled = false
}
, ROOT_HUD);
container.Add(new CuiPanel {
Image = {
Color = theme.Progress
}
, RectTransform = {
AnchorMin = "0.02 0.55", AnchorMax = "0.02 0.75"
}
, CursorEnabled = false
}
, ROOT_HUD, HUD_PROGRESS_FILL);
container.Add(new CuiPanel {
Image = {
Color = theme.Panel
}
, RectTransform = {
AnchorMin = "0.02 0.20", AnchorMax = "0.98 0.40"
}
, CursorEnabled = false
}
, ROOT_HUD);
container.Add(new CuiPanel {
Image = {
Color = theme.Boost
}
, RectTransform = {
AnchorMin = "0.02 0.20", AnchorMax = "0.02 0.40"
}
, CursorEnabled = false
}
, ROOT_HUD, HUD_BOOST_FILL);
CuiHelper.AddUi(player, container);
}
private void SetBarFill(BasePlayer player, string name, float t, string color) {
t = Mathf.Clamp01(t);
float maxX = Mathf.Lerp(0.02f, 0.98f, t);
string min = name == HUD_PROGRESS_FILL ? "0.02 0.55" : "0.02 0.20";
string mx = name == HUD_PROGRESS_FILL ? maxX.ToString("0.00", CultureInfo.InvariantCulture) + " 0.75" : maxX.ToString("0.00", CultureInfo.InvariantCulture) + " 0.40";
CuiHelper.DestroyUi(player, name);
var c = new CuiElementContainer();
c.Add(new CuiPanel {
Image = {
Color = color
}
, RectTransform = {
AnchorMin = min, AnchorMax = mx
}
, CursorEnabled = false
}
, ROOT_HUD, name);
CuiHelper.AddUi(player, c);
}
#endregion
#region Welcome Menu
private void AutoShowLockedWelcome(BasePlayer player) {
var st = EnsureState(player);
st.MenuToggleAllowed = false;
st.WelcomeLocked = true;
st.LockPosition = player.transform.position;
st.CurrentTab = 1;
st.StartMenuVisible = true;
BuildStartMenu(player);
}
[ChatCommand("hydro.menu")]
private void CmdMenu(BasePlayer player, string cmd, string[] args) {
var st = EnsureState(player);
st.CurrentTab = 1;
st.StartMenuVisible = true;
if (!st.MenuToggleAllowed) {
st.WelcomeLocked = true;
st.LockPosition = player.transform.position;
}
BuildStartMenu(player);
}
[ConsoleCommand("hydro_ui.menu.tab")]
private void CCMenuTab(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
int tab = Mathf.Clamp(arg.GetInt(0), 1, 2);
var st = EnsureState(player);
st.CurrentTab = tab;
if (!st.StartMenuVisible) st.StartMenuVisible = true;
BuildStartMenu(player);
}
[ConsoleCommand("hydro_ui.menu.toggle")]
private void CCMenuToggle(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
var st = EnsureState(player);
if (!st.MenuToggleAllowed) {
player.ChatMessage("Menu locked until lobby join.");
return;
}
if (st.StartMenuVisible) {
DestroyStartMenu(player);
st.StartMenuVisible = false;
}
else {
st.CurrentTab = 2;
st.StartMenuVisible = true;
BuildStartMenu(player);
}
}
[ConsoleCommand("hydro_ui.joinlobby")]
private void CCJoinLobby(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
if (HydroLobby != null) HydroLobby.Call("SendToLobby", player);
else player.ChatMessage("Lobby plugin missing.");
var st = EnsureState(player);
st.WelcomeLocked = false;
st.MenuToggleAllowed = true;
DestroyStartMenu(player);
st.StartMenuVisible = false;
ShowMenuToggle(player);
ShowPlayerPanel(player);
}
private void BuildStartMenu(BasePlayer player) {
var st = EnsureState(player);
var theme = GetTheme(GetPrefs(player.userID).ThemeName);
DestroyStartMenu(player);
var c = new CuiElementContainer();
c.Add(new CuiPanel {
Image = {
Color = theme.Background
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "1 1"
}
, CursorEnabled = true
}
, "Overlay", ROOT_START_MENU);
var bg = GetImage(IMG_START_BG);
if (!string.IsNullOrEmpty(bg)) {
c.Add(new CuiElement {
Parent = ROOT_START_MENU, Components = {
new CuiRawImageComponent {
Png = bg, Color = "1 1 1 0.85"
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "1 1"
}
}
}
);
}
float headerMinY = Mathf.Clamp01(1f - config.StartHeaderHeight);
string headerMin = "0 " + headerMinY.ToString("0.###", CultureInfo.InvariantCulture);
c.Add(new CuiPanel {
Image = {
Color = theme.Panel
}
, RectTransform = {
AnchorMin = headerMin, AnchorMax = "1 1"
}
, CursorEnabled = true
}
, ROOT_START_MENU, START_HEADER);
AddTextButton(c, START_HEADER, "Welcome", "0.02 0.15", "0.20 0.85", "hydro_ui.menu.tab 1", theme.Panel, theme.Text, 16);
AddTextButton(c, START_HEADER, "Play", "0.22 0.15", "0.40 0.85", "hydro_ui.menu.tab 2", theme.Panel, theme.Text, 16);
string bodyMax = "1 " + headerMinY.ToString("0.###", CultureInfo.InvariantCulture);
c.Add(new CuiPanel {
Image = {
Color = "0 0 0 0"
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = bodyMax
}
, CursorEnabled = true
}
, ROOT_START_MENU, START_BODY);
if (st.CurrentTab == 1) BuildWelcomeTab(c, START_BODY, player, theme);
else BuildPlayTab(c, START_BODY, player, theme);
CuiHelper.AddUi(player, c);
}
private void BuildWelcomeTab(CuiElementContainer c, string parent, BasePlayer player, Theme theme) {
c.Add(new CuiLabel {
Text = {
Text = "Welcome to HydroRust", FontSize = 30, Align = TextAnchor.MiddleCenter, Color = theme.Text
}
, RectTransform = {
AnchorMin = "0.18 0.62", AnchorMax = "0.82 0.74"
}
}
, parent, START_TAB_WELCOME + ".title");
string logo = GetImage("hydro_ui_logo");
if (!string.IsNullOrEmpty(logo)) {
c.Add(new CuiElement {
Parent = parent, Components = {
new CuiRawImageComponent {
Png = logo, Color = "1 1 1 1"
}
, new CuiRectTransformComponent {
AnchorMin = "0.47 0.48", AnchorMax = "0.53 0.58"
}
}
}
);
}
c.Add(new CuiLabel {
Text = {
Text = "Go to the Play tab and click Join Lobby to start racing. You are locked here until you join.", FontSize = 16, Align = TextAnchor.MiddleCenter, Color = theme.MutedText
}
, RectTransform = {
AnchorMin = "0.20 0.40", AnchorMax = "0.80 0.48"
}
}
, parent, START_TAB_WELCOME + ".subtitle");
var st = EnsureState(player);
st.WelcomeLocked = !st.MenuToggleAllowed;
st.LockPosition = player.transform.position;
}
private void BuildPlayTab(CuiElementContainer c, string parent, BasePlayer player, Theme theme) {
c.Add(new CuiPanel {
Image = {
Color = theme.Panel
}
, RectTransform = {
AnchorMin = "0.30 0.40", AnchorMax = "0.70 0.65"
}
, CursorEnabled = true
}
, parent, START_TAB_PLAY + ".tile");
string png = GetImage(IMG_TRACK_DEFAULT);
if (!string.IsNullOrEmpty(png)) {
c.Add(new CuiElement {
Parent = START_TAB_PLAY + ".tile", Components = {
new CuiRawImageComponent {
Png = png, Color = "1 1 1 0.85"
}
, new CuiRectTransformComponent {
AnchorMin = "0 0.35", AnchorMax = "1 1"
}
}
}
);
}
c.Add(new CuiLabel {
Text = {
Text = "Join Lobby", FontSize = 20, Align = TextAnchor.MiddleCenter, Color = theme.Text
}
, RectTransform = {
AnchorMin = "0.05 0.10", AnchorMax = "0.95 0.30"
}
}
, START_TAB_PLAY + ".tile");
c.Add(new CuiButton {
Button = {
Command = "hydro_ui.joinlobby", Color = "0 0 0 0"
}
, Text = {
Text = ""
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "1 1"
}
}
, START_TAB_PLAY + ".tile");
}
private void DestroyStartMenu(BasePlayer player) => CuiHelper.DestroyUi(player, ROOT_START_MENU);
#endregion
#region Toggle Button
private void ShowMenuToggle(BasePlayer player) {
var st = EnsureState(player);
var theme = GetTheme(GetPrefs(player.userID).ThemeName);
DestroyMenuToggle(player);
var c = new CuiElementContainer();
c.Add(new CuiPanel {
Image = {
Color = theme.Panel
}
, RectTransform = {
AnchorMin = config.MenuToggleAnchorMin, AnchorMax = config.MenuToggleAnchorMax
}
, CursorEnabled = false
}
, "Overlay", ROOT_TOGGLE);
c.Add(new CuiButton {
Button = {
Command = "hydro_ui.menu.toggle", Color = theme.Primary
}
, Text = {
Text = "≡", FontSize = 18, Align = TextAnchor.MiddleCenter, Color = theme.Text
}
, RectTransform = {
AnchorMin = "0.05 0.05", AnchorMax = "0.95 0.95"
}
}
, ROOT_TOGGLE);
CuiHelper.AddUi(player, c);
st.MenuToggleVisible = true;
}
private void DestroyMenuToggle(BasePlayer player) {
CuiHelper.DestroyUi(player, ROOT_TOGGLE);
EnsureState(player).MenuToggleVisible = false;
}
#endregion
#region Player Quick Panel
private void ShowPlayerPanel(BasePlayer player) {
var st = EnsureState(player);
var theme = GetTheme(GetPrefs(player.userID).ThemeName);
DestroyPlayerPanel(player);
var c = new CuiElementContainer();
c.Add(new CuiPanel {
Image = {
Color = theme.Background
}
, RectTransform = {
AnchorMin = config.PlayerPanelAnchorMin, AnchorMax = config.PlayerPanelAnchorMax
}
, CursorEnabled = false
}
, "Overlay", ROOT_PLAYER);
AddTextButton(c, ROOT_PLAYER, "Join Race", "0.05 0.60", "0.60 0.90", "hydroui.player.join", theme.Good, theme.Text, 14);
AddTextButton(c, ROOT_PLAYER, "Leave Race", "0.62 0.60", "0.95 0.90", "hydroui.player.leave", theme.Alert, theme.Text, 14);
AddTextButton(c, ROOT_PLAYER, "Stats", "0.05 0.10", "0.22 0.45", "hydroui.player.stats", theme.Panel, theme.Text, 12);
AddTextButton(c, ROOT_PLAYER, "Vote Normal", "0.24 0.10", "0.52 0.45", "hydroui.vote.normal", theme.Panel, theme.Text, 12);
AddTextButton(c, ROOT_PLAYER, "Vote Battle", "0.54 0.10", "0.82 0.45", "hydroui.vote.battle", theme.Battle, theme.Text, 12);
CuiHelper.AddUi(player, c);
st.PlayerPanelVisible = true;
}
private void DestroyPlayerPanel(BasePlayer player) {
CuiHelper.DestroyUi(player, ROOT_PLAYER);
EnsureState(player).PlayerPanelVisible = false;
}
[ConsoleCommand("hydroui.player.join")]
private void CCPlayerJoin(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
RunChat(player, config.Chat.RaceJoin);
}
[ConsoleCommand("hydroui.player.leave")]
private void CCPlayerLeave(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
RunChat(player, config.Chat.RaceLeave);
}
[ConsoleCommand("hydroui.player.stats")]
private void CCPlayerStats(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
RunChat(player, config.Chat.Stats);
}
[ConsoleCommand("hydroui.vote.normal")]
private void CCVoteNormal(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
RunChat(player, config.Chat.VoteNormal);
}
[ConsoleCommand("hydroui.vote.battle")]
private void CCVoteBattle(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
RunChat(player, config.Chat.VoteBattle);
}
#endregion
#region Admin Track Editor
[ChatCommand("hydroui")]
private void CmdHydroAdminUI(BasePlayer player, string cmd, string[] args) {
if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, "hydrorust.admin")) {
player.ChatMessage("No permission.");
return;
}
DestroyStartMenu(player);
ShowAdminUI(player);
player.ChatMessage("Editor opened. Use New or the New Track field (press Enter), then Save.");
}
[ConsoleCommand("hydroui.toggle")]
private void CCToggleEditor(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
var st = EnsureState(player);
if (st.AdminVisible) {
DestroyAdminUI(player);
st.AdminVisible = false;
}
else {
DestroyStartMenu(player);
ShowAdminUI(player);
}
}
[ConsoleCommand("hydroui.cursor")]
private void CCCursor(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
var st = EnsureState(player);
st.AdminCursorEnabled = !st.AdminCursorEnabled;
if (st.AdminVisible) ShowAdminUI(player);
player.ChatMessage("Editor cursor " + (st.AdminCursorEnabled ? "ENABLED" : "DISABLED"));
}
[ConsoleCommand("hydroui.setpanel")]
private void CCSetPanel(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
float minX = arg.GetFloat(0), minY = arg.GetFloat(1), maxX = arg.GetFloat(2), maxY = arg.GetFloat(3);
minX = Mathf.Clamp01(minX);
minY = Mathf.Clamp01(minY);
maxX = Mathf.Clamp01(maxX);
maxY = Mathf.Clamp01(maxY);
if (maxX <= minX || maxY <= minY) {
player.ChatMessage("Invalid anchors. Example: hydroui.setpanel 0.76 0.05 0.99 0.95");
return;
}
config.AdminAnchorMin = minX.ToString("0.##", CultureInfo.InvariantCulture) + " " + minY.ToString("0.##", CultureInfo.InvariantCulture);
config.AdminAnchorMax = maxX.ToString("0.##", CultureInfo.InvariantCulture) + " " + maxY.ToString("0.##", CultureInfo.InvariantCulture);
SaveConfig();
if (EnsureState(player).AdminVisible) ShowAdminUI(player);
player.ChatMessage("Admin panel anchors updated.");
}
[ConsoleCommand("hydroui.resetpanel")]
private void CCResetPanel(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
config.AdminAnchorMin = "0.76 0.05";
config.AdminAnchorMax = "0.99 0.95";
SaveConfig();
if (EnsureState(player).AdminVisible) ShowAdminUI(player);
player.ChatMessage("Admin panel anchors reset.");
}
private void ShowAdminUI(BasePlayer player) {
var st = EnsureState(player);
st.AdminVisible = true;
DestroyAdminUI(player);
var theme = GetTheme(GetPrefs(player.userID).ThemeName);
var c = new CuiElementContainer();
// Root side panel
c.Add(new CuiPanel {
Image = {
Color = theme.Background
}
, RectTransform = {
AnchorMin = config.AdminAnchorMin, AnchorMax = config.AdminAnchorMax
}
, CursorEnabled = st.AdminCursorEnabled
}
, "Overlay", ROOT_ADMIN);
// Header (New, Save, Reload, Cursor, Close)
c.Add(new CuiPanel {
Image = {
Color = theme.Panel
}
, RectTransform = {
AnchorMin = "0 0.94", AnchorMax = "1 1"
}
, CursorEnabled = st.AdminCursorEnabled
}
, ROOT_ADMIN, ADMIN_HEADER);
AddTextButton(c, ADMIN_HEADER, "New", "0.01 0.10", "0.14 0.90", "hydroui.autonew", theme.Primary, theme.Text, 14);
AddTextButton(c, ADMIN_HEADER, "Save", "0.16 0.10", "0.29 0.90", "hydroui.savetrack", theme.Good, theme.Text, 14);
AddTextButton(c, ADMIN_HEADER, "Reload", "0.31 0.10", "0.46 0.90", "hydroui.reload", theme.Panel, theme.Text, 14);
AddTextButton(c, ADMIN_HEADER, st.AdminCursorEnabled ? "Cursor Off" : "Cursor On", "0.48 0.10", "0.70 0.90", "hydroui.cursor", theme.Panel, theme.Text, 14);
AddTextButton(c, ADMIN_HEADER, "Close", "0.82 0.10", "0.99 0.90", "hydroui.close", theme.Alert, theme.Text, 14);
// Summary bar
c.Add(new CuiPanel {
Image = {
Color = theme.Panel
}
, RectTransform = {
AnchorMin = "0 0.88", AnchorMax = "1 0.94"
}
, CursorEnabled = false
}
, ROOT_ADMIN, ADMIN_SUMMARY_BAR);
c.Add(new CuiPanel {
Image = {
Color = "0 0 0 0"
}
, RectTransform = {
AnchorMin = "0.01 0.05", AnchorMax = "0.99 0.95"
}
, CursorEnabled = false
}
, ADMIN_SUMMARY_BAR, ADMIN_SUMMARY_BAR_INNER);
// Track list
c.Add(new CuiPanel {
Image = {
Color = theme.Panel
}
, RectTransform = {
AnchorMin = "0.02 0.46", AnchorMax = "0.98 0.86"
}
, CursorEnabled = st.AdminCursor
// ... [Code preview truncated due to length. Full source in attached file] ...