C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using DatabaseConnection = Oxide.Core.Database.Connection;
using Sql = Oxide.Core.Database.Sql;
namespace Oxide.Plugins
{
[Info("PowerfulWhitelist", "codeboy", "1.0.1")]
[Description("Whitelist for PowerfulAdmin | embedded panel UI, standalone commands")]
internal class PowerfulWhitelist : RustPlugin
{
#region Classes
private static class Permissions
{
public const string View = "powerfulwhitelist.view";
public const string Manage = "powerfulwhitelist.manage";
public const string Bypass = "powerfulwhitelist.bypass";
public static readonly string[] All = { View, Manage, Bypass };
}
private enum StorageBackend
{
Json,
Sqlite,
MySql
}
private enum StorageState
{
None,
Connecting,
Connected,
Switching,
Failed,
SwitchFailed
}
private class WhitelistEntry
{
[JsonProperty("steamid")] public ulong SteamId;
[JsonProperty("name")] public string Name = "";
[JsonProperty("by")] public string By = "";
[JsonProperty("by_steamid")] public ulong BySteamId;
[JsonProperty("created")] public long Created;
}
private class ViewerState
{
public Dictionary<string, object> Context;
public Dictionary<string, string> Palette;
public int Page;
public string Search = "";
public ulong RemoveConfirm;
public bool DialogOpen;
public string DraftSteamId = "";
public string DraftName = "";
public bool PickerOpen;
public string PickerSearch = "";
public int PickerPage;
public string StorageSelection = "";
public bool StorageConfirm;
public StorageState StorageStatus = StorageState.None;
}
private class PickCandidate
{
public ulong SteamId;
public string Name = "";
public bool Online;
}
private class MySqlConfig
{
[JsonProperty(Order = 1)] public string Host = "127.0.0.1";
[JsonProperty(Order = 2)] public int Port = 3306;
[JsonProperty(Order = 3)] public string Database = "powerfulwhitelist";
[JsonProperty(Order = 4)] public string User = "root";
[JsonProperty(Order = 5)] public string Password = "";
}
private class ConfigData
{
[JsonProperty(Order = 1)] public bool Enabled;
[JsonProperty(Order = 2)] public bool AllowAdmins = true;
[JsonProperty(Order = 3)] public bool KickOnEnable = true;
[JsonProperty(Order = 4)] public string ServerName = "";
[JsonProperty(Order = 5)] public string InfoUrl = "";
[JsonProperty(Order = 6)] [JsonConverter(typeof(StringEnumConverter))]
public StorageBackend Storage = StorageBackend.Json;
[JsonProperty(Order = 7)] public int SyncInterval = 60;
[JsonProperty(Order = 8)] public MySqlConfig MySql = new();
[JsonProperty(Order = 100)] public VersionNumber Version;
}
#endregion
#region Fields
[PluginReference] private Plugin PowerfulAdmin;
private ConfigData _config;
private readonly Dictionary<ulong, WhitelistEntry> _whitelist = new();
private readonly Dictionary<ulong, WhitelistEntry> _pendingAdd = new();
private readonly HashSet<ulong> _pendingRemove = new();
private readonly Dictionary<ulong, ViewerState> _viewers = new();
private bool _sectionRegistered;
private Database _database;
private bool _dbReady;
private bool _dbEnabled;
private bool _storageBusy;
private Timer _refreshTimer;
private Timer _retryTimer;
private const float ReconnectInterval = 30f;
private const int PerPage = 6;
private const int PickerPerPage = 12;
private const string RootLayer = "ui.PowerfulWhitelist";
private const string SectionId = "whitelist";
private const string SectionIcon = "assets/icons/check.png";
#endregion
#region Hooks
private void OnServerInitialized()
{
foreach (var permissionName in Permissions.All)
permission.RegisterPermission(permissionName, this);
RegisterSection();
StartStorage();
}
private void Unload()
{
PowerfulAdmin?.Call("API_RemoveSection", SectionId);
foreach (var player in BasePlayer.activePlayerList)
if (player != null)
CuiHelper.DestroyUi(player, RootLayer);
_refreshTimer?.Destroy();
_retryTimer?.Destroy();
if (_config.Storage == StorageBackend.Json)
SaveData();
else
_database?.Close();
}
private void OnPluginLoaded(Plugin plugin)
{
if (plugin == null || plugin.Name != "PowerfulAdmin")
return;
PowerfulAdmin = plugin;
_sectionRegistered = false;
RegisterSection();
}
private void OnPluginUnloaded(Plugin plugin)
{
if (plugin == null || plugin.Name != "PowerfulAdmin")
return;
PowerfulAdmin = null;
_sectionRegistered = false;
}
private object CanClientLogin(Network.Connection connection)
{
if (!Enforcing)
return null;
if (IsAllowed(connection.userid))
return null;
Log($"rejected non-whitelisted {connection.username} ({connection.userid}) on login");
return BuildRejectText(connection.userid.ToString());
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null)
return;
RefreshName(player);
if (!Enforcing)
return;
if (IsAllowed(player.userID))
return;
player.Kick(BuildRejectText(player.UserIDString));
}
private void RefreshName(BasePlayer player)
{
if (player == null || !_whitelist.TryGetValue(player.userID, out var entry))
return;
var name = player.displayName;
if (string.IsNullOrEmpty(name) || name == entry.Name)
return;
entry.Name = name;
PersistAdd(entry);
RefreshViewers();
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null)
return;
_viewers.Remove(player.userID);
}
#endregion
#region Section registration
private void RegisterSection()
{
PowerfulAdmin ??= plugins.Find("PowerfulAdmin");
if (PowerfulAdmin == null || _sectionRegistered)
return;
var definition = new Dictionary<string, object>
{
["id"] = SectionId,
["title"] = (Func<BasePlayer, string>)(viewer =>
GetLocalizedMessage(TranslateKey.UiTitle, viewer)),
["icon"] = SectionIcon,
["category"] = "nav.cat.tools",
["permission"] = Permissions.View,
["settingsHeight"] = (Func<BasePlayer, int>)SettingsHeight,
["open"] = (Action<BasePlayer>)UI_OpenSection,
["cardRows"] = (Func<string, List<string[]>>)(steamId =>
ulong.TryParse(steamId, out var parsed) ? UI_BuildCardRows(parsed) : null),
["settings"] = (Action<BasePlayer, CuiElementContainer, string, int>)UI_DrawSettings
};
_sectionRegistered = PowerfulAdmin.Call("API_AddSection", definition) is bool added && added;
}
private void UI_OpenSection(BasePlayer player)
{
if (player == null)
return;
var context = PowerfulAdmin?.Call("API_SectionContext", player) as Dictionary<string, object>;
var viewer = Viewer(player.userID);
viewer.Context = context;
viewer.Palette = context != null && context.TryGetValue("palette", out var palette)
? palette as Dictionary<string, string>
: null;
viewer.RemoveConfirm = 0;
viewer.DialogOpen = false;
viewer.PickerOpen = false;
UI_Refresh(player);
}
#endregion
#region Methods
private static long Now() => DateTimeOffset.Now.ToUnixTimeSeconds();
private bool IsAllowed(ulong steamId)
{
if (UsesDatabase && !_dbReady)
return true;
if (_whitelist.ContainsKey(steamId))
return true;
var idString = steamId.ToString();
if (permission.UserHasPermission(idString, Permissions.Bypass))
return true;
if (_config.AllowAdmins && IsServerAdmin(steamId))
return true;
return false;
}
private static bool IsServerAdmin(ulong steamId) =>
ServerUsers.Is(steamId, ServerUsers.UserGroup.Owner) ||
ServerUsers.Is(steamId, ServerUsers.UserGroup.Moderator);
private bool AddEntry(ulong steamId, string name, string by, ulong bySteamId = 0)
{
if (steamId == 0 || _whitelist.ContainsKey(steamId))
return false;
_whitelist[steamId] = new WhitelistEntry
{
SteamId = steamId,
Name = string.IsNullOrEmpty(name) ? steamId.ToString() : name,
By = string.IsNullOrEmpty(by) ? "Server" : by,
BySteamId = bySteamId,
Created = Now()
};
PersistAdd(_whitelist[steamId]);
Log($"{by} added {name} ({steamId}) to the whitelist");
Interface.CallHook("OnPowerfulWhitelistAdded", steamId, name, by);
return true;
}
private bool RemoveEntry(ulong steamId, string by)
{
if (!_whitelist.Remove(steamId))
return false;
PersistRemove(steamId);
Log($"{by} removed {steamId} from the whitelist");
Interface.CallHook("OnPowerfulWhitelistRemoved", steamId, by);
if (Enforcing)
{
var online = BasePlayer.FindByID(steamId);
if (online != null && online.IsConnected && !IsAllowed(steamId))
online.Kick(BuildRejectText(online.UserIDString));
}
return true;
}
private int EnforceKick(BasePlayer except)
{
if (!Enforcing)
return 0;
var targets = Pool.Get<List<BasePlayer>>();
foreach (var player in BasePlayer.activePlayerList)
if (player != null && player != except && !IsAllowed(player.userID))
targets.Add(player);
var count = targets.Count;
foreach (var player in targets)
player.Kick(BuildRejectText(player.UserIDString));
Pool.FreeUnmanaged(ref targets);
return count;
}
private string BuildRejectText(string userId)
{
var text = lang.GetMessage(TranslateKey.Reject, this, userId);
if (!string.IsNullOrEmpty(_config.ServerName))
text = _config.ServerName + "\n\n" + text;
if (!string.IsNullOrEmpty(_config.InfoUrl))
text += "\n" + string.Format(lang.GetMessage(TranslateKey.RejectInfo, this, userId), _config.InfoUrl);
return text;
}
private static bool ResolveTarget(string token, out ulong steamId, out string name)
{
steamId = 0;
name = "";
if (string.IsNullOrWhiteSpace(token))
return false;
token = token.Trim();
if (ulong.TryParse(token, out var parsed))
{
steamId = parsed;
name = BasePlayer.FindAwakeOrSleepingByID(parsed)?.displayName ?? token;
return true;
}
var match = BasePlayer.FindAwakeOrSleeping(token);
if (match == null)
return false;
steamId = match.userID;
name = match.displayName;
return true;
}
private List<WhitelistEntry> FilterEntries(string search, out int total)
{
IEnumerable<WhitelistEntry> query = _whitelist.Values;
if (!string.IsNullOrEmpty(search))
{
var needle = search.Trim();
query = query.Where(entry =>
(!string.IsNullOrEmpty(entry.Name) &&
entry.Name.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) ||
entry.SteamId.ToString().Contains(needle));
}
var list = query.OrderByDescending(entry => entry.Created).ToList();
total = list.Count;
return list;
}
private static string FormatDate(long unix)
{
if (unix <= 0)
return "-";
return DateTimeOffset.FromUnixTimeSeconds(unix).LocalDateTime.ToString("dd.MM.yyyy");
}
private string GetLocalizedMessage(string key, BasePlayer player) =>
lang.GetMessage(key, this, player?.UserIDString);
private void Notify(BasePlayer player, string message)
{
if (string.IsNullOrEmpty(message))
return;
if (player == null)
{
Puts(message);
return;
}
player.ChatMessage(
$"<color=#{AccentHex(player)}>{GetLocalizedMessage(TranslateKey.Prefix, player)}</color> " +
$"<color=#9b9ba3>›</color> <color=#d7d9e0>{message}</color>");
}
private string AccentHex(BasePlayer player)
{
var accent = "0.357 0.776 0.902 1";
if (player != null && _viewers.TryGetValue(player.userID, out var viewer) && viewer.Palette != null &&
viewer.Palette.TryGetValue("accent", out var value) && !string.IsNullOrEmpty(value))
accent = value;
return RgbaToHex(accent);
}
private static string RgbaToHex(string rgba)
{
var parts = rgba.Split(' ');
if (parts.Length < 3)
return "5ac6e6";
var culture = System.Globalization.CultureInfo.InvariantCulture;
float.TryParse(parts[0], System.Globalization.NumberStyles.Float, culture, out var r);
float.TryParse(parts[1], System.Globalization.NumberStyles.Float, culture, out var g);
float.TryParse(parts[2], System.Globalization.NumberStyles.Float, culture, out var b);
return
$"{(int)(Mathf.Clamp01(r) * 255):X2}{(int)(Mathf.Clamp01(g) * 255):X2}{(int)(Mathf.Clamp01(b) * 255):X2}";
}
private void Log(string message)
{
Puts(message);
PowerfulAdmin?.Call("API_Log", "[PowerfulWhitelist] " + message, "Cmd");
}
private bool HasPermission(BasePlayer player, string permissionName) =>
player != null && permission.UserHasPermission(player.UserIDString, permissionName);
private static string[] ConsoleArguments(ConsoleSystem.Arg arg)
{
if (arg == null || !arg.HasArgs())
return Array.Empty<string>();
var arguments = new string[arg.Args.Length];
for (var index = 0; index < arguments.Length; index++)
arguments[index] = arg.GetString(index);
return arguments;
}
private bool IsConsoleAllowed(ConsoleSystem.Arg arg, string permissionName)
{
var player = arg?.Player();
if (player == null)
return true;
return HasPermission(player, permissionName);
}
private void ReplyTo(ConsoleSystem.Arg arg, string message)
{
if (arg?.Player() != null)
Notify(arg.Player(), message);
else
Puts(message);
}
private string StatusWord(BasePlayer player) =>
Enforcing
? GetLocalizedMessage(TranslateKey.StateOn, player)
: GetLocalizedMessage(TranslateKey.StateOff, player);
#endregion
#region Commands
[ChatCommand("whitelist")]
private void CommandChatWhitelist(BasePlayer player, string command, string[] arguments)
{
HandleChat(player, arguments);
}
[ChatCommand("wl")]
private void CommandChatWhitelistAlias(BasePlayer player, string command, string[] arguments)
{
HandleChat(player, arguments);
}
private void HandleChat(BasePlayer player, string[] arguments)
{
if (!HasPermission(player, Permissions.View) && !HasPermission(player, Permissions.Manage))
{
Notify(player, GetLocalizedMessage(TranslateKey.NoPermission, player));
return;
}
if (arguments == null || arguments.Length == 0 ||
arguments[0].Equals("help", StringComparison.OrdinalIgnoreCase))
{
Notify(player, GetLocalizedMessage(TranslateKey.Usage, player));
Notify(player, string.Format(GetLocalizedMessage(TranslateKey.Status, player), StatusWord(player),
_whitelist.Count));
return;
}
var sub = arguments[0].ToLowerInvariant();
var rest = arguments.Skip(1).ToArray();
if (sub == "add")
DoAdd(player, rest, message => Notify(player, message));
else if (sub == "remove" || sub == "del")
DoRemove(player, rest, message => Notify(player, message));
else if (sub == "list")
DoList(player, rest, message => Notify(player, message));
else if (sub == "on" || sub == "enable")
DoEnable(player, true, message => Notify(player, message));
else if (sub == "off" || sub == "disable")
DoEnable(player, false, message => Notify(player, message));
else if (sub == "enforce" || sub == "kick")
DoEnforce(player, message => Notify(player, message));
else if (sub == "status")
Notify(player, string.Format(GetLocalizedMessage(TranslateKey.Status, player), StatusWord(player),
_whitelist.Count));
else
Notify(player, GetLocalizedMessage(TranslateKey.Usage, player));
}
[ConsoleCommand("pwl")]
private void CommandConsole(ConsoleSystem.Arg arg)
{
var player = arg?.Player();
var arguments = ConsoleArguments(arg);
var sub = arguments.Length > 0 ? arguments[0].ToLowerInvariant() : "help";
var rest = arguments.Skip(1).ToArray();
if (sub == "add")
{
if (IsConsoleAllowed(arg, Permissions.Manage))
DoAdd(player, rest, message => ReplyTo(arg, message));
}
else if (sub == "remove" || sub == "del")
{
if (IsConsoleAllowed(arg, Permissions.Manage))
DoRemove(player, rest, message => ReplyTo(arg, message));
}
else if (sub == "list")
{
if (IsConsoleAllowed(arg, Permissions.View))
DoList(player, rest, message => ReplyTo(arg, message));
}
else if (sub == "on" || sub == "enable")
{
if (IsConsoleAllowed(arg, Permissions.Manage))
DoEnable(player, true, message => ReplyTo(arg, message));
}
else if (sub == "off" || sub == "disable")
{
if (IsConsoleAllowed(arg, Permissions.Manage))
DoEnable(player, false, message => ReplyTo(arg, message));
}
else if (sub == "enforce" || sub == "kick")
{
if (IsConsoleAllowed(arg, Permissions.Manage))
DoEnforce(player, message => ReplyTo(arg, message));
}
else if (sub == "status")
{
if (IsConsoleAllowed(arg, Permissions.View))
ReplyTo(arg, string.Format(GetLocalizedMessage(TranslateKey.Status, player), StatusWord(player),
_whitelist.Count));
}
else if (IsConsoleAllowed(arg, Permissions.View))
ReplyTo(arg, GetLocalizedMessage(TranslateKey.Usage, player));
}
private void DoAdd(BasePlayer admin, string[] arguments, Action<string> reply)
{
if (arguments == null || arguments.Length == 0)
{
reply(GetLocalizedMessage(TranslateKey.Usage, admin));
return;
}
if (!ResolveTarget(arguments[0], out var steamId, out var name))
{
reply(string.Format(GetLocalizedMessage(TranslateKey.NotFound, admin), arguments[0]));
return;
}
if (!AddEntry(steamId, name, admin?.displayName ?? "Server", admin?.userID ?? 0))
{
reply(string.Format(GetLocalizedMessage(TranslateKey.AlreadyListed, admin), name));
return;
}
reply(string.Format(GetLocalizedMessage(TranslateKey.Added, admin), name));
}
private void DoRemove(BasePlayer admin, string[] arguments, Action<string> reply)
{
if (arguments == null || arguments.Length == 0)
{
reply(GetLocalizedMessage(TranslateKey.Usage, admin));
return;
}
if (!ResolveTarget(arguments[0], out var steamId, out var name))
{
reply(string.Format(GetLocalizedMessage(TranslateKey.NotFound, admin), arguments[0]));
return;
}
if (!RemoveEntry(steamId, admin?.displayName ?? "Server"))
{
reply(string.Format(GetLocalizedMessage(TranslateKey.NotListed, admin), name));
return;
}
reply(string.Format(GetLocalizedMessage(TranslateKey.Removed, admin), name));
}
private void DoList(BasePlayer admin, string[] arguments, Action<string> reply)
{
var entries = FilterEntries("", out var total);
if (total == 0)
{
reply(GetLocalizedMessage(TranslateKey.ListEmpty, admin));
return;
}
var perPage = 20;
var pages = (total + perPage - 1) / perPage;
var page = arguments != null && arguments.Length > 0 && int.TryParse(arguments[0], out var parsed)
? Mathf.Clamp(parsed, 1, pages)
: 1;
var lines = entries.Skip((page - 1) * perPage).Take(perPage)
.Select(entry => $"{entry.SteamId} · {entry.Name} · {entry.By} · {FormatDate(entry.Created)}");
reply(string.Format(GetLocalizedMessage(TranslateKey.ListHeader, admin), page, pages, total) + "\n" +
string.Join("\n", lines));
}
private void DoEnable(BasePlayer admin, bool enable, Action<string> reply)
{
if (_config.Enabled == enable)
{
reply(GetLocalizedMessage(enable ? TranslateKey.EnabledOn : TranslateKey.EnabledOff, admin));
return;
}
var wasEnforcing = Enforcing;
_config.Enabled = enable;
SaveConfig();
Log($"{(admin?.displayName ?? "Server")} turned this-server whitelist {(enable ? "on" : "off")}");
reply(GetLocalizedMessage(enable ? TranslateKey.EnabledOn : TranslateKey.EnabledOff, admin));
if (Enforcing && !wasEnforcing && _config.KickOnEnable)
{
var kicked = EnforceKick(admin);
if (kicked > 0)
reply(string.Format(GetLocalizedMessage(TranslateKey.Enforced, admin), kicked));
}
}
private void DoEnforce(BasePlayer admin, Action<string> reply)
{
if (!Enforcing)
{
reply(GetLocalizedMessage(TranslateKey.EnabledOff, admin));
return;
}
var kicked = EnforceKick(admin);
reply(kicked > 0
? string.Format(GetLocalizedMessage(TranslateKey.Enforced, admin), kicked)
: GetLocalizedMessage(TranslateKey.NothingToKick, admin));
}
#endregion
#region UI commands
private bool UI_Guard(ConsoleSystem.Arg arg, out BasePlayer player, out ViewerState viewer,
string permissionName)
{
player = arg?.Player();
viewer = null;
if (player == null)
return false;
if (!_viewers.TryGetValue(player.userID, out viewer) || viewer.Context == null)
return false;
return HasPermission(player, permissionName);
}
[ConsoleCommand("pwl.ui.refresh")]
private void CommandUiRefresh(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out _, Permissions.View))
return;
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.search")]
private void CommandUiSearch(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.View))
return;
var value = arg.HasArgs() ? string.Join(" ", ConsoleArguments(arg)).Trim() : "";
if (value == GetLocalizedMessage(TranslateKey.UiSearch, player).Trim())
value = "";
viewer.Search = value;
viewer.Page = 0;
viewer.RemoveConfirm = 0;
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.page")]
private void CommandUiPage(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.View))
return;
viewer.Page = arg.GetInt(0, 0);
viewer.RemoveConfirm = 0;
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.toggle")]
private void CommandUiToggle(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out _, Permissions.Manage))
return;
DoEnable(player, !_config.Enabled, message => Notify(player, message));
PowerfulAdmin?.Call("API_RefreshSettings", player);
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.dbtoggle")]
private void CommandUiDbToggle(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out _, Permissions.Manage))
return;
if (!UsesDatabase || _database?.Connection == null)
{
Notify(player, GetLocalizedMessage(TranslateKey.DbToggleUnavailable, player));
UI_Refresh(player);
return;
}
var wasEnforcing = Enforcing;
_dbEnabled = !_dbEnabled;
WriteDbEnabled();
Log($"{player.displayName} turned database whitelist {(_dbEnabled ? "on" : "off")}");
Notify(player, GetLocalizedMessage(_dbEnabled ? TranslateKey.DbEnabledOn : TranslateKey.DbEnabledOff, player));
if (Enforcing && !wasEnforcing && _config.KickOnEnable)
{
var kicked = EnforceKick(player);
if (kicked > 0)
Notify(player, string.Format(GetLocalizedMessage(TranslateKey.Enforced, player), kicked));
}
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.enforce")]
private void CommandUiEnforce(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out _, Permissions.Manage))
return;
DoEnforce(player, message => Notify(player, message));
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.remove")]
private void CommandUiRemove(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
viewer.RemoveConfirm = ulong.TryParse(arg.GetString(0), out var steamId) ? steamId : 0;
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.removeno")]
private void CommandUiRemoveNo(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
viewer.RemoveConfirm = 0;
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.removeyes")]
private void CommandUiRemoveYes(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
viewer.RemoveConfirm = 0;
if (ulong.TryParse(arg.GetString(0), out var steamId))
{
RemoveEntry(steamId, player.displayName);
PowerfulAdmin?.Call("API_RefreshPlayerCard", steamId.ToString());
}
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.addopen")]
private void CommandUiAddOpen(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
viewer.DialogOpen = true;
viewer.DraftSteamId = "";
viewer.DraftName = "";
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.addclose")]
private void CommandUiAddClose(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
viewer.DialogOpen = false;
viewer.PickerOpen = false;
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.draft")]
private void CommandUiDraft(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
var field = arg.GetString(0);
var value = arg.HasArgs(2) ? string.Join(" ", ConsoleArguments(arg).Skip(1)).Trim() : "";
if (field == "sid")
{
viewer.DraftSteamId = value;
viewer.DraftName = "";
}
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.addconfirm")]
private void CommandUiAddConfirm(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
if (!ulong.TryParse(viewer.DraftSteamId.Trim(), out var steamId) || !steamId.IsSteamId())
{
Notify(player, GetLocalizedMessage(TranslateKey.BadSteamid, player));
return;
}
var name = string.IsNullOrEmpty(viewer.DraftName)
? BasePlayer.FindAwakeOrSleepingByID(steamId)?.displayName ?? viewer.DraftSteamId
: viewer.DraftName;
if (!AddEntry(steamId, name, player.displayName, player.userID))
{
Notify(player, string.Format(GetLocalizedMessage(TranslateKey.AlreadyListed, player), name));
return;
}
PowerfulAdmin?.Call("API_RefreshPlayerCard", steamId.ToString());
viewer.DialogOpen = false;
viewer.PickerOpen = false;
viewer.Page = 0;
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.pickopen")]
private void CommandUiPickOpen(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
viewer.PickerOpen = true;
viewer.PickerSearch = "";
viewer.PickerPage = 0;
UI_Refresh(player);
}
[ConsoleCommand("pwl.ui.pickclose")]
private void CommandUiPickClose(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
viewer.PickerOpen = false;
CuiHelper.DestroyUi(player, RootLayer + ".picker");
}
[ConsoleCommand("pwl.ui.pickersearch")]
private void CommandUiPickerSearch(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
var value = arg.HasArgs() ? string.Join(" ", ConsoleArguments(arg)).Trim() : "";
if (value == GetLocalizedMessage(TranslateKey.UiSearch, player).Trim())
value = "";
viewer.PickerSearch = value;
viewer.PickerPage = 0;
if (viewer.PickerOpen)
UI_DrawPlayerPicker(player, viewer);
}
[ConsoleCommand("pwl.ui.pickerpage")]
private void CommandUiPickerPage(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
viewer.PickerPage = arg.GetInt(0, 0);
if (viewer.PickerOpen)
UI_DrawPlayerPicker(player, viewer);
}
[ConsoleCommand("pwl.ui.pick")]
private void CommandUiPick(ConsoleSystem.Arg arg)
{
if (!UI_Guard(arg, out var player, out var viewer, Permissions.Manage))
return;
if (ulong.TryParse(arg.GetString(0), out var steamId))
{
viewer.DraftSteamId = steamId.ToString();
viewer.DraftName = arg.HasArgs(2) ? string.Join(" ", ConsoleArguments(arg).Skip(1)).Trim() : "";
}
viewer.PickerOpen = false;
viewer.DialogOpen = true;
UI_Refresh(player);
}
#endregion
#region Config commands
private bool CanEditSettings(BasePlayer player) =>
player != null && permission.UserHasPermission(player.UserIDString, "powerfuladmin.settings");
[ConsoleCommand("pwl.cfg.toggle")]
private void CommandConfigToggle(ConsoleSystem.Arg arg)
{
var player = arg?.Player();
if (player == null || !CanEditSettings(player))
return;
var key = arg.GetString(0);
if (key == "allowadmins")
_config.AllowAdmins = !_config.AllowAdmins;
else if (key == "kickonenable")
_config.KickOnEnable = !_config.KickOnEnable;
else
return;
SaveConfig();
PowerfulAdmin?.Call("API_RefreshSettings", player);
}
[ConsoleCommand("pwl.cfg.input")]
private void CommandConfigInput(ConsoleSystem.Arg arg)
{
var player = arg?.Player();
if (player == null || !CanEditSettings(player))
return;
var key = arg.GetString(0);
var value = arg.HasArgs(2) ? string.Join(" ", ConsoleArguments(arg).Skip(1)).Trim() : "";
if (key == "servername")
_config.ServerName = value;
else if (key == "infourl")
_config.InfoUrl = value;
else
return;
SaveConfig();
PowerfulAdmin?.Call("API_RefreshSettings", player);
}
[ConsoleCommand("pwl.cfg.storagesel")]
private void CommandConfigStorageSelect(ConsoleSystem.Arg arg)
{
var player = arg?.Player();
if (player == null || !CanEditSettings(player) || _storageBusy)
return;
var selection = arg.GetString(0).ToLowerInvariant();
if (selection != "json" && selection != "sqlite" && selection != "mysql")
return;
var viewer = Viewer(player.userID);
viewer.StorageSelection = selection;
viewer.StorageConfirm = false;
viewer.StorageStatus = StorageState.None;
PowerfulAdmin?.Call("API_RefreshSettings", player);
}
[ConsoleCommand("pwl.cfg.dbfield")]
private void CommandConfigDbField(ConsoleSystem.Arg arg)
{
var player = arg?.Player();
if (player == null || !CanEditSettings(player))
return;
var field = arg.GetString(0);
var value = arg.HasArgs(2) ? string.Join(" ", ConsoleArguments(arg).Skip(1)).Trim() : "";
if (field == "host")
_config.MySql.Host = value;
else if (field == "port" && int.TryParse(value, out var port))
_config.MySql.Port = port;
else if (field == "database")
_config.MySql.Database = value;
else if (field == "user")
_config.MySql.User = value;
else if (field == "password")
_config.MySql.Password = value;
else if (field != "port")
return;
SaveConfig();
Viewer(player.userID).StorageStatus = StorageState.None;
PowerfulAdmin?.Call("API_RefreshSettings", player);
}
[ConsoleCommand("pwl.cfg.dbtest")]
private void CommandConfigDbTest(ConsoleSystem.Arg arg)
{
var player = arg?.Player();
if (player == null || !CanEditSettings(player) || _storageBusy)
return;
var viewer = Viewer(player.userID);
viewer.StorageStatus = StorageState.Connecting;
PowerfulAdmin?.Call("API_RefreshSettings", player);
var test = new Database(StorageBackend.MySql, this, $"{Name}.db", _config.MySql,
warning => PrintWarning(warning));
if (!test.Open())
{
viewer.StorageStatus = StorageState.Failed;
PowerfulAdmin?.Call("API_RefreshSettings", player);
return;
}
test.Query(Sql.Builder.Append("SELECT 1 AS ok"), rows =>
{
test.Close();
viewer.StorageStatus = rows != null && rows.Count > 0 ? StorageState.Connected : StorageState.Failed;
PowerfulAdmin?.Call("API_RefreshSettings", player);
});
}
[ConsoleCommand("pwl.cfg.storageapply")]
private void CommandConfigStorageApply(ConsoleSystem.Arg arg)
{
var player = arg?.Player();
if (player == null || !CanEditSettings(player) || _storageBusy)
return;
var viewer = Viewer(player.userID);
var selected = SelectedBackend(viewer);
if (selected == StorageId(_config.Storage))
{
viewer.StorageConfirm = false;
PowerfulAdmin?.Call("API_RefreshSettings", player);
return;
}
if (!arg.HasArgs())
{
viewer.StorageConfirm = true;
PowerfulAdmin?.Call("API_RefreshSettings", player);
return;
}
viewer.StorageConfirm = false;
if (!arg.GetBool(0))
{
PowerfulAdmin?.Call("API_RefreshSettings", player);
return;
}
_storageBusy = true;
viewer.StorageStatus = StorageState.Switching;
PowerfulAdmin?.Call("API_RefreshSettings", player);
SwitchStorage(player, viewer, ParseBackend(selected));
}
private static StorageBackend ParseBackend(string id) =>
id == "mysql" ? StorageBackend.MySql :
id == "sqlite" ? StorageBackend.Sqlite : StorageBackend.Json;
#endregion
#region UI
private static string ThemeColor(ViewerState viewer, string key, string fallback) =>
viewer.Palette != null && viewer.Palette.TryGetValue(key, out var value) && !string.IsNullOrEmpty(value)
? value
: fallback;
private static int GetContextInt(ViewerState viewer, string key, int fallback) =>
viewer.Context != null && viewer.Context.TryGetValue(key, out var value) && value != null &&
int.TryParse(value.ToString(), out var parsed)
? parsed
: fallback;
private static string GetContextString(ViewerState viewer, string key, string fallback) =>
viewer.Context != null && viewer.Context.TryGetValue(key, out var value) && value != null
? value.ToString()
: fallback;
private ViewerState Viewer(ulong id)
{
if (!_viewers.TryGetValue(id, out var viewer))
{
viewer = new ViewerState();
_viewers[id] = viewer;
}
return viewer;
}
private void UI_Refresh(BasePlayer player)
{
if (player == null || !player.IsConnected)
return;
var viewer = Viewer(player.userID);
if (viewer.Context == null)
return;
var entries = FilterEntries(viewer.Search, out var total);
UI_DrawList(player, viewer, entries, total);
}
private void UI_DrawList(BasePlayer player, ViewerState viewer, List<WhitelistEntry> entries, int total)
{
var parent = GetContextString(viewer, "parent", "Overlay");
var bodyWidth = GetContextInt(viewer, "bodyW", 744);
var pad = GetContextInt(viewer, "pad", 18);
var fontBold = ThemeColor(viewer, "fontBold", "robotocondensed-bold.ttf");
var fontRegular = ThemeColor(viewer, "fontRegular", "robotocondensed-regular.ttf");
var fontMono = ThemeColor(viewer, "fontMono", "droidsansmono.ttf");
var accent = ThemeColor(viewer, "accent", "0.357 0.776 0.902 1");
var accentText = ThemeColor(viewer, "accentText", "0.039 0.059 0.090 1");
var surface = ThemeColor(viewer, "surface", "0.086 0.125 0.180 1");
var elevated = ThemeColor(viewer, "elev", "0.118 0.169 0.239 1");
var border = ThemeColor(viewer, "border", "0.165 0.227 0.322 1");
var pageColor = ThemeColor(viewer, "page", "0.039 0.059 0.090 1");
var text = ThemeColor(viewer, "text", "0.902 0.933 0.973 1");
var text2 = ThemeColor(viewer, "text2", "0.561 0.627 0.722 1");
var text3 = ThemeColor(viewer, "text3", "0.392 0.451 0.541 1");
var danger = ThemeColor(viewer, "danger", "0.949 0.376 0.384 1");
var success = ThemeColor(viewer, "success", "0.388 0.800 0.518 1");
var successSoft = ThemeColor(viewer, "successSoft", "0.388 0.800 0.518 0.15");
var warn = ThemeColor(viewer, "warn", "0.949 0.647 0.345 1");
var clear = ThemeColor(viewer, "clear", "0 0 0 0");
var canManage = HasPermission(player, Permissions.Manage);
var userId = player.UserIDString;
var pages = Math.Max(1, (total + PerPage - 1) / PerPage);
if (viewer.Page >= pages)
viewer.Page = pages - 1;
if (viewer.Page < 0)
viewer.Page = 0;
var container = new CuiElementContainer();
container.Add(new CuiElement
{
Parent = parent, Name = RootLayer, DestroyUi = RootLayer,
Components =
{
new CuiImageComponent { Color = clear },
new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" }
}
});
container.Add(new CuiLabel
{
Text =
{
Text = string.Format(lang.GetMessage(TranslateKey.UiCount, this, userId), total),
FontSize = 10, Font = fontRegular, Align = TextAnchor.UpperLeft, Color = text2
},
RectTransform =
{ AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = $"{pad} -53", OffsetMax = $"{-pad} -37" }
}, RootLayer);
var searchWidth = 200;
var searchX = bodyWidth - pad - searchWidth;
if (canManage)
searchX -= 96;
container.Add(new CuiElement
{
Parent = RootLayer, Name = RootLayer + ".search",
Components =
{
new CuiImageComponent { Color = pageColor },
new CuiOutlineComponent { Color = border, Distance = "1 1" },
new CuiRectTransformComponent
{
AnchorMin = "0 1", AnchorMax = "0 1", OffsetMin = $"{searchX} -42",
OffsetMax = $"{searchX + searchWidth} -14"
}
}
});
var searchEmpty = string.IsNullOrEmpty(viewer.Search);
container.Add(new CuiElement
{
Parent = RootLayer + ".search",
Components =
{
new CuiInputFieldComponent
{
Text = searchEmpty ? lang.GetMessage(TranslateKey.UiSearch, this, userId) : viewer.Search,
FontSize = 11, Font = fontRegular, Align = TextAnchor.MiddleLeft,
Color = searchEmpty ? text3 : text, CharsLimit = 32, NeedsKeyboard = true,
Command = "pwl.ui.search "
},
new CuiRectTransformComponent
// ... [Code preview truncated due to length. Full source in attached file] ...