C#:
/*
███████╗██╗ ███████╗██╗ ██╗ ███████╗ ██████╗
██╔════╝██║ ██╔════╝ ██║ ██╔╝ ██╔════╝ ██╔══██╗
█████╗ ██║ █████╗ █████╔╝ █████╗ ██████╔╝
██╔══╝ ██║ ██╔══╝ ██╔═ ██╗ ██╔══╝ ██╔══██╗
██║ ███████╗███████╗██║ ██╗ ███████╗ ██║ ██║
╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝
* Flexer
* Copyright © Flexer. All rights reserved.
*/
using System.Collections.Generic;
using System.Globalization;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using Oxide.Core;
namespace Oxide.Plugins {
[Info("PluginInfo", "Flexer", "1.0.1")]
[Description("Displays rotating informational messages below the hotbar. Movable per player with interactive panel.")]
public class PluginInfo : RustPlugin {
private ConfigData config;
private int messageIndex = 0;
private const string panelName = "InfoPanelUI";
private System.Random rng = new System.Random();
private Dictionary<ulong, Vector2> playerOffsets = new Dictionary<ulong, Vector2>();
private Dictionary<ulong, Vector2> playerScales = new Dictionary<ulong, Vector2>();
private class ConfigData {
public float IntervalSeconds {
get;
set;
}
public string RotationMode {
get;
set;
}
public string PanelColor {
get;
set;
}
public string TextColor {
get;
set;
}
public int FontSize {
get;
set;
}
public string AnchorMin {
get;
set;
}
public string AnchorMax {
get;
set;
}
public float MoveIncrement {
get;
set;
}
public string MoveUIAnchorMin {
get;
set;
}
// fixed location for controller
public string MoveUIAnchorMax {
get;
set;
}
public List<string> Messages {
get;
set;
}
}
protected override void LoadDefaultConfig() {
config = new ConfigData {
IntervalSeconds = 15f, RotationMode = "Sequential", PanelColor = "0.1 0.1 0.1 0.75", TextColor = "1 1 1 1", FontSize = 15, AnchorMin = "0.345 0.110", AnchorMax = "0.64 0.150", MoveIncrement = 0.005f, MoveUIAnchorMin = "0.05 0.70", MoveUIAnchorMax = "0.45 0.94", Messages = new List<string> {
"<size=24><b>Flexer</b></size>", "Press <color=#ffd479>/home</color> to teleport!", "Vote daily to get rewards at <color=#aaffaa>example.com/vote</color>", "Need help? Type <color=#ffaaaa>/help</color>", "Join our Discord at <color=#7289da>discord.gg/yourserver</color>", "Check out the shop: <color=#ffcc00>www.yourserver.com/shop</color>"
}
}
;
SaveConfig();
}
protected override void LoadConfig() {
base.LoadConfig();
config = Config.ReadObject<ConfigData>() ?? new ConfigData();
if (config.IntervalSeconds <= 0f) config.IntervalSeconds = 15f;
if (string.IsNullOrEmpty(config.RotationMode)) config.RotationMode = "Sequential";
if (string.IsNullOrEmpty(config.PanelColor)) config.PanelColor = "0.1 0.1 0.1 0.75";
if (string.IsNullOrEmpty(config.TextColor)) config.TextColor = "1 1 1 1";
if (config.FontSize <= 0) config.FontSize = 15;
if (string.IsNullOrEmpty(config.AnchorMin)) config.AnchorMin = "0.345 0.110";
if (string.IsNullOrEmpty(config.AnchorMax)) config.AnchorMax = "0.64 0.150";
if (config.MoveIncrement <= 0f) config.MoveIncrement = 0.005f;
if (string.IsNullOrEmpty(config.MoveUIAnchorMin)) config.MoveUIAnchorMin = "0.05 0.70";
if (string.IsNullOrEmpty(config.MoveUIAnchorMax)) config.MoveUIAnchorMax = "0.45 0.94";
if (config.Messages == null || config.Messages.Count == 0) {
PrintWarning("No messages found in config, loading defaults.");
LoadDefaultConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(config, true);
void Init() {
permission.RegisterPermission("plugininfo.move", this);
}
void OnServerInitialized() {
LoadPlayerOffsets();
LoadPlayerScales();
timer.Every(config.IntervalSeconds, RotateMessage);
foreach (var player in BasePlayer.activePlayerList) {
var offset = playerOffsets.ContainsKey(player.userID) ? playerOffsets[player.userID] : Vector2.zero;
CreateUI(player, GetCurrentMessage(), offset);
}
}
void OnPlayerInit(BasePlayer player) {
var offset = playerOffsets.ContainsKey(player.userID) ? playerOffsets[player.userID] : Vector2.zero;
CreateUI(player, GetCurrentMessage(), offset);
}
void OnPlayerDisconnected(BasePlayer player) {
DestroyMovementPanel(player);
DestroyUI(player);
}
void Unload() {
foreach (var player in BasePlayer.activePlayerList) {
DestroyUI(player);
DestroyMovementPanel(player);
}
}
private void RotateMessage() {
if (config.Messages == null || config.Messages.Count == 0) return;
var mode = (config.RotationMode ?? "Sequential").Trim().ToLowerInvariant();
if (mode == "random") messageIndex = rng.Next(config.Messages.Count);
else messageIndex = (messageIndex + 1) % config.Messages.Count;
string message = config.Messages[messageIndex];
foreach (var player in BasePlayer.activePlayerList) {
DestroyUI(player);
var offset = playerOffsets.ContainsKey(player.userID) ? playerOffsets[player.userID] : Vector2.zero;
CreateUI(player, message, offset);
}
}
private string GetCurrentMessage() {
if (config.Messages == null || config.Messages.Count == 0) return "No messages configured.";
return config.Messages[messageIndex];
}
private bool TryParseAnchorPair(string s, out Vector2 v) {
v = Vector2.zero;
if (string.IsNullOrWhiteSpace(s)) return false;
var parts = s.Split(' ');
if (parts.Length != 2) return false;
if (!float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var x)) return false;
if (!float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var y)) return false;
v = new Vector2(x, y);
return true;
}
private float Clamp01(float f) => Mathf.Clamp01(f);
private Vector2 Clamp01(Vector2 v) => new Vector2(Clamp01(v.x), Clamp01(v.y));
private void CreateUI(BasePlayer player, string text, Vector2 offset) {
var container = new CuiElementContainer();
Vector2 min, max;
if (!TryParseAnchorPair(config.AnchorMin, out min)) min = new Vector2(0.345f, 0.110f);
if (!TryParseAnchorPair(config.AnchorMax, out max)) max = new Vector2(0.64f, 0.150f);
var size = max - min;
var scale = playerScales.ContainsKey(player.userID) ? playerScales[player.userID] : Vector2.one;
var scaledMax = min + new Vector2(size.x * Mathf.Clamp(scale.x, 0.25f, 3f), size.y * Mathf.Clamp(scale.y, 0.25f, 3f));
var minWith = Clamp01(min + offset);
var maxWith = Clamp01(scaledMax + offset);
if (maxWith.x <= minWith.x) maxWith.x = Mathf.Min(1f, minWith.x + (scaledMax.x - min.x));
if (maxWith.y <= minWith.y) maxWith.y = Mathf.Min(1f, minWith.y + (scaledMax.y - min.y));
var panel = container.Add(new CuiPanel {
Image = {
Color = config.PanelColor
}
, RectTransform = {
AnchorMin = $"{minWith.x.ToString(CultureInfo.InvariantCulture)} {minWith.y.ToString(CultureInfo.InvariantCulture)}", AnchorMax = $"{maxWith.x.ToString(CultureInfo.InvariantCulture)} {maxWith.y.ToString(CultureInfo.InvariantCulture)}"
}
, CursorEnabled = false
}
, "Hud", panelName);
container.Add(new CuiLabel {
Text = {
Text = text, FontSize = config.FontSize, Align = TextAnchor.MiddleCenter, Color = config.TextColor
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "1 1"
}
}
, panel);
CuiHelper.AddUi(player, container);
}
private void DestroyUI(BasePlayer player) => CuiHelper.DestroyUi(player, panelName);
private const string movePanelName = "MovePanelUI";
private readonly Dictionary<ulong, float> playerMoveStep = new Dictionary<ulong, float>();
private readonly Dictionary<ulong, bool> playerSizeMode = new Dictionary<ulong, bool>();
[ChatCommand("panelmove")]
private void cmdShowMovePanel(BasePlayer player, string command, string[] args) {
if (!permission.UserHasPermission(player.UserIDString, "plugininfo.move")) {
SendReply(player, "You don't have permission to move this panel.");
return;
}
if (args != null && args.Length > 0 && args[0].Equals("off", System.StringComparison.OrdinalIgnoreCase)) {
DestroyMovementPanel(player);
return;
}
ShowMovementPanel(player);
}
private void ShowMovementPanel(BasePlayer player) {
DestroyMovementPanel(player);
if (!playerMoveStep.ContainsKey(player.userID)) playerMoveStep[player.userID] = (config?.MoveIncrement > 0 ? config.MoveIncrement : 0.005f);
if (!playerSizeMode.ContainsKey(player.userID)) playerSizeMode[player.userID] = false;
if (!playerScales.ContainsKey(player.userID)) playerScales[player.userID] = Vector2.one;
Vector2 mvMin, mvMax;
if (!TryParseAnchorPair(config.MoveUIAnchorMin, out mvMin)) mvMin = new Vector2(0.05f, 0.70f);
if (!TryParseAnchorPair(config.MoveUIAnchorMax, out mvMax)) mvMax = new Vector2(0.45f, 0.94f);
mvMin = Clamp01(mvMin);
mvMax = Clamp01(mvMax);
var ui = new CuiElementContainer();
var root = ui.Add(new CuiPanel {
Image = {
Color = "0 0 0 0.88"
}
, RectTransform = {
AnchorMin = $"{mvMin.x} {mvMin.y}", AnchorMax = $"{mvMax.x} {mvMax.y}"
}
, CursorEnabled = true
}
, "Hud", movePanelName);
ui.Add(new CuiPanel {
Image = {
Color = "0.15 0.15 0.15 1"
}
, RectTransform = {
AnchorMin = "0 0.78", AnchorMax = "1 1"
}
}
, root);
ui.Add(new CuiLabel {
Text = {
Text = "Move Panel", FontSize = 14, Align = TextAnchor.MiddleLeft, Color = "1 1 1 1"
}
, RectTransform = {
AnchorMin = "0.03 0.80", AnchorMax = "0.7 0.99"
}
}
, root);
AddBtn(ui, "X", "movepanel close", "0.92 0.80", "0.985 0.99", 14, "0.85 0.3 0.3 1", root);
float step = playerMoveStep[player.userID];
bool sizeMode = playerSizeMode[player.userID];
string modeText = sizeMode ? "Mode: Size" : "Mode: Move";
ui.Add(new CuiLabel {
Text = {
Text = $"Step: {step:0.###}", FontSize = 12, Align = TextAnchor.MiddleLeft, Color = "1 1 1 1"
}
, RectTransform = {
AnchorMin = "0.03 0.58", AnchorMax = "0.20 0.74"
}
}
, root);
AddBtn(ui, "–", "movepanel stepdec", "0.21 0.60", "0.26 0.72", 16, "0.6 0.6 0.6 1", root);
AddBtn(ui, "+", "movepanel stepinc", "0.27 0.60", "0.32 0.72", 16, "0.6 0.6 0.6 1", root);
AddBtn(ui, modeText, "movepanel modetoggle", "0.33 0.60", "0.60 0.72", 12, "0.35 0.55 0.9 1", root);
AddBtn(ui, "Reset", "movepanel reset", "0.03 0.40", "0.19 0.56", 12, "0.5 0.5 0.5 1", root);
AddBtn(ui, "Center", "movepanel center", "0.21 0.40", "0.37 0.56", 12, "0.5 0.5 0.5 1", root);
AddBtn(ui, "SizeReset", "movepanel sizereset", "0.39 0.40", "0.57 0.56", 12, "0.5 0.5 0.5 1", root);
AddBtn(ui, "↑", "movepanel up", "0.73 0.58", "0.81 0.74", 18, "0.85 0.85 0.85 1", root);
AddBtn(ui, "←", "movepanel left", "0.64 0.42", "0.72 0.58", 18, "0.85 0.85 0.85 1", root);
AddBtn(ui, "→", "movepanel right", "0.82 0.42", "0.90 0.58", 18, "0.85 0.85 0.85 1", root);
AddBtn(ui, "↓", "movepanel down", "0.73 0.26", "0.81 0.42", 18, "0.85 0.85 0.85 1", root);
CuiHelper.AddUi(player, ui);
}
private void AddBtn(CuiElementContainer c, string text, string cmd, string min, string max, int font, string color, string parent) {
c.Add(new CuiButton {
Button = {
Command = cmd, Color = color
}
, RectTransform = {
AnchorMin = min, AnchorMax = max
}
, Text = {
Text = text, FontSize = font, Align = TextAnchor.MiddleCenter
}
}
, parent);
}
private void DestroyMovementPanel(BasePlayer player) => CuiHelper.DestroyUi(player, movePanelName);
[ConsoleCommand("movepanel")]
private void cmdMovePanel(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, "plugininfo.move")) return;
var a = arg.Args;
if (a == null || a.Length == 0) {
ShowMovementPanel(player);
return;
}
if (!playerOffsets.ContainsKey(player.userID)) playerOffsets[player.userID] = Vector2.zero;
if (!playerScales.ContainsKey(player.userID)) playerScales[player.userID] = Vector2.one;
if (!playerMoveStep.ContainsKey(player.userID)) playerMoveStep[player.userID] = (config?.MoveIncrement > 0 ? config.MoveIncrement : 0.005f);
if (!playerSizeMode.ContainsKey(player.userID)) playerSizeMode[player.userID] = false;
var offset = playerOffsets[player.userID];
var scale = playerScales[player.userID];
float step = Mathf.Max(0.0005f, playerMoveStep[player.userID]);
bool sizeMode = playerSizeMode[player.userID];
bool refreshUI = true;
void SaveOffset(Vector2 o) {
playerOffsets[player.userID] = o;
SavePlayerOffsets();
DestroyUI(player);
CreateUI(player, GetCurrentMessage(), o);
}
void SaveScale(Vector2 s) {
s.x = Mathf.Clamp(s.x, 0.25f, 3f);
s.y = Mathf.Clamp(s.y, 0.25f, 3f);
playerScales[player.userID] = s;
SavePlayerScales();
DestroyUI(player);
CreateUI(player, GetCurrentMessage(), offset);
}
switch (a[0].ToString().ToLowerInvariant()) {
case "up" : if (sizeMode) SaveScale(new Vector2(scale.x, scale.y + step));
else SaveOffset(new Vector2(offset.x, offset.y + step));
break;
case "down" : if (sizeMode) SaveScale(new Vector2(scale.x, scale.y - step));
else SaveOffset(new Vector2(offset.x, offset.y - step));
break;
case "left" : if (sizeMode) SaveScale(new Vector2(scale.x - step, scale.y));
else SaveOffset(new Vector2(offset.x - step, offset.y));
break;
case "right" : if (sizeMode) SaveScale(new Vector2(scale.x + step, scale.y));
else SaveOffset(new Vector2(offset.x + step, offset.y));
break;
case "stepinc" : playerMoveStep[player.userID] = Mathf.Min(step * 2f, 0.1f);
break;
case "stepdec" : playerMoveStep[player.userID] = Mathf.Max(step * 0.5f, 0.0005f);
break;
case "modetoggle" : playerSizeMode[player.userID] = !playerSizeMode[player.userID];
break;
case "reset" : offset = Vector2.zero;
SaveOffset(offset);
break;
case "center" : Vector2 min, max;
if (!TryParseAnchorPair(config.AnchorMin, out min)) min = new Vector2(0.345f, 0.110f);
if (!TryParseAnchorPair(config.AnchorMax, out max)) max = new Vector2(0.64f, 0.150f);
var size = max - min;
var desiredMin = new Vector2(0.5f - size.x * 0.5f, 0.5f - size.y * 0.5f);
offset = desiredMin - min;
SaveOffset(offset);
break;
case "sizereset" : SaveScale(Vector2.one);
break;
case "close" : DestroyMovementPanel(player);
refreshUI = false;
break;
default : break;
}
if (refreshUI) {
DestroyMovementPanel(player);
ShowMovementPanel(player);
}
}
private void SavePlayerOffsets() => Interface.Oxide.DataFileSystem.WriteObject("PluginInfoOffsets", playerOffsets);
private void LoadPlayerOffsets() => playerOffsets = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, Vector2>>("PluginInfoOffsets") ?? new Dictionary<ulong, Vector2>();
private void SavePlayerScales() => Interface.Oxide.DataFileSystem.WriteObject("PluginInfoScales", playerScales);
private void LoadPlayerScales() => playerScales = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, Vector2>>("PluginInfoScales") ?? new Dictionary<ulong, Vector2>();
}
}