Иконка ресурса

FREE PopControl : 1.7.0 1.7.0

Нет прав для скачивания
C#:
using Oxide.Core;
using System;
using System.Collections.Generic;
using Oxide.Core.Libraries.Covalence;
using UnityEngine; // Time.realtimeSinceStartup

namespace Oxide.Plugins
{
    [Info("PopControl", "Papa", "1.7.0")]
    [Description("A dynamic server population controlled based upon current occupancy.")]
    public class PopControl : CovalencePlugin
    {
        private const string DefaultColorLabel = "#ff2E36";
        private const string DefaultColorNumber = "#FFFFFF";
        private const string DefaultBotName = "Population Bot";
        private const ulong DefaultBotSteamId = 76561199680521298;

        private ConfigData _cfg;
        private Timer _debounceTimer;
        private Timer _broadcastTimer;
        private int _originalMaxPlayers;
        private bool _capturedOriginalMaxPlayers;

        // !pop cooldown tracker
        private double _nextPopAllowed;
        private double _nextChatSendErrorLog;

        private class ConfigData
        {
            // Elastic scaling
            public int   MaxPlayersOffset       = 1;
            public float CooldownPeriodSeconds  = 10f;
            public int   MaximumPopulation      = 200;
            public int   MinimumPopulation      = 100;
            public bool  ConsoleMessagesEnabled = true;
            public bool  NeverBelowOnline       = true;
            public bool  IncludeJoiningInScaling = true;
            public bool  IncludeQueuedInScaling  = false;
            public float QueuedWeight            = 1f;
            public int   QueueBuffer             = 0;

            // Chat features
            public bool  EnableChatCommands        = true;
            public bool  EnablePeriodicBroadcasts  = false;
            public float BroadcastIntervalSeconds  = 300f;

            // !pop global cooldown
            public float PopCommandCooldownSeconds = 10f;
            public bool  ShowPopCooldownFeedback   = true;

            // Colors (labels & numbers)
            public string ColorLabel  = DefaultColorLabel; // red highlight
            public string ColorNumber = DefaultColorNumber; // numbers

            // Bot cosmetics
            public string BotName     = DefaultBotName;

            // SteamID64 used to resolve the avatar shown in chat (0 = disabled / no forced icon)
            public ulong  BotSteamId  = DefaultBotSteamId;

            // NEW: When true, use the same chat.add signature that your working BetterBroadcast uses:
            //   chat.add, 2, <SteamID64>, <Message>
            // When false, use the name-override signature:
            //   chat.add, "<SteamID64>", "<BotName>", <Message>
            public bool   UseMode2ForIcon         = true;
        }

        #region Config
        protected override void LoadDefaultConfig()
        {
            _cfg = new ConfigData();
            SaveConfig();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            _cfg = Config.ReadObject<ConfigData>() ?? new ConfigData();

            bool changed = false;

            if (_cfg.CooldownPeriodSeconds < 0.1f)     { _cfg.CooldownPeriodSeconds = 0.1f; changed = true; }
            if (_cfg.MaxPlayersOffset < 0)             { _cfg.MaxPlayersOffset = 0; changed = true; }
            if (_cfg.MinimumPopulation < 1)            { _cfg.MinimumPopulation = 1; changed = true; }
            if (_cfg.MaximumPopulation < _cfg.MinimumPopulation)
            { _cfg.MaximumPopulation = _cfg.MinimumPopulation; changed = true; }
            if (_cfg.QueuedWeight < 0f)              { _cfg.QueuedWeight = 0f; changed = true; }
            if (_cfg.QueuedWeight > 5f)              { _cfg.QueuedWeight = 5f; changed = true; }
            if (_cfg.QueueBuffer < 0)                { _cfg.QueueBuffer = 0; changed = true; }
            if (_cfg.QueueBuffer > 500)              { _cfg.QueueBuffer = 500; changed = true; }
            if (_cfg.BroadcastIntervalSeconds < 10f)   { _cfg.BroadcastIntervalSeconds = 10f; changed = true; }
            if (_cfg.PopCommandCooldownSeconds < 1f)   { _cfg.PopCommandCooldownSeconds = 1f; changed = true; }

            if (!IsValidHexColor(_cfg.ColorLabel))
            {
                PrintWarning($"Config ColorLabel '{_cfg.ColorLabel}' is invalid. Resetting to default {DefaultColorLabel}.");
                _cfg.ColorLabel = DefaultColorLabel;
                changed = true;
            }

            if (!IsValidHexColor(_cfg.ColorNumber))
            {
                PrintWarning($"Config ColorNumber '{_cfg.ColorNumber}' is invalid. Resetting to default {DefaultColorNumber}.");
                _cfg.ColorNumber = DefaultColorNumber;
                changed = true;
            }

            if (string.IsNullOrWhiteSpace(_cfg.BotName))
            {
                _cfg.BotName = DefaultBotName;
                changed = true;
            }

            if (!IsLikelySteamId64(_cfg.BotSteamId))
            {
                if (_cfg.BotSteamId != 0)
                {
                    PrintWarning($"Config BotSteamId '{_cfg.BotSteamId}' does not look like a SteamID64. " +
                                 $"Resetting to default {DefaultBotSteamId}.");
                    _cfg.BotSteamId = DefaultBotSteamId;
                    changed = true;
                }
            }

            if (changed) SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(_cfg, true);

        private bool IsLikelySteamId64(ulong id)
        {
            const ulong MinSteamId64 = 76561197960265728; // universe 1 lower bound
            return id == 0 || id >= MinSteamId64;
        }

        private bool IsValidHexColor(string value)
        {
            if (string.IsNullOrWhiteSpace(value) || value[0] != '#') return false;
            if (value.Length != 7 && value.Length != 9) return false;

            for (int i = 1; i < value.Length; i++)
            {
                if (!Uri.IsHexDigit(value[i])) return false;
            }

            return true;
        }
        #endregion

        #region Lifecycle
        private void Init()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["PopCooldown"] = "!pop is on cooldown ({0:0}s).",
                ["LabelFull"] = "full",
                ["LabelQueued"] = "queued",
                ["LabelJoining"] = "joining",
                ["LabelSleeping"] = "sleeping",
                ["LabelPlayersOnline"] = "players online",
                ["UsageBotId"] = "Usage: ep.botid <steamid64|off>",
                ["UsageIconMode"] = "Usage: ep.iconmode <2|name>",
                ["InsufficientPermission"] = "Insufficient permission.",
                ["ApplyDone"] = "[LWPop] applied.",
                ["BotIdDisabled"] = "[LWPop] BotSteamId disabled (no forced chat icon).",
                ["InvalidSteamId64"] = "[LWPop] Invalid SteamID64. Example: 76561198000000000",
                ["BotIdSet"] = "[LWPop] BotSteamId set to {0}. Try: ep.testmsg",
                ["IconModeSet2"] = "[LWPop] Icon mode set to '2' (chat.add, 2, steamid, message).",
                ["IconModeSetName"] = "[LWPop] Icon mode set to 'name' (chat.add, steamid, botName, message).",
                ["TestMsgPrefix"] = "[Test]",
                ["TestMsgSent"] = "[LWPop] Test message sent to online players."
            }, this);
        }

        private void OnServerInitialized()
        {
            if (!_capturedOriginalMaxPlayers)
            {
                _originalMaxPlayers = ConVar.Server.maxplayers;
                _capturedOriginalMaxPlayers = true;

                if (_cfg.ConsoleMessagesEnabled)
                    Puts($"[LWPop] Captured original maxplayers value: {_originalMaxPlayers}");
            }

            ApplyNow();
            RestartBroadcastTimer();
            _nextPopAllowed = 0; // allow immediate first !pop
        }

        private void Unload()
        {
            _debounceTimer?.Destroy();
            _broadcastTimer?.Destroy();
            _debounceTimer = null;
            _broadcastTimer = null;

            if (_capturedOriginalMaxPlayers)
            {
                ConVar.Server.maxplayers = _originalMaxPlayers;

                if (_cfg.ConsoleMessagesEnabled)
                    Puts($"[LWPop] Restored maxplayers to original value: {_originalMaxPlayers}");
            }
            else if (_cfg.ConsoleMessagesEnabled)
            {
                Puts("[LWPop] Skipped maxplayers restore because original value was not captured.");
            }
        }
        #endregion

        #region Hooks: player count changes
        private void OnPlayerConnected(BasePlayer _) => DebouncedApply();
        private void OnPlayerDisconnected(BasePlayer _, string __) => DebouncedApply();
        #endregion

        #region Elastic core
        private void DebouncedApply()
        {
            _debounceTimer?.Destroy();
            _debounceTimer = timer.Once(_cfg.CooldownPeriodSeconds, () =>
            {
                _debounceTimer = null;
                ApplyNow();
            });
        }

        private void ApplyNow()
        {
            int online = BasePlayer.activePlayerList?.Count ?? 0;
            int joining, queued;
            GetQueueCounts(out joining, out queued);
            int target = ComputeTargetMaxPlayers(online, joining, queued);

            if (ConVar.Server.maxplayers != target)
            {
                ConVar.Server.maxplayers = target;
                if (_cfg.ConsoleMessagesEnabled)
                    Puts($"[LWPop] maxplayers -> {target} (online={online}, joining={joining}, queued={queued}, offset={_cfg.MaxPlayersOffset}, bounds={_cfg.MinimumPopulation}-{_cfg.MaximumPopulation})");
            }
        }

        private int ComputeTargetMaxPlayers(int online, int joining, int queued)
        {
            int scaledQueued = _cfg.IncludeQueuedInScaling
                ? Mathf.RoundToInt(queued * _cfg.QueuedWeight)
                : 0;
            int demand = online;
            if (_cfg.IncludeJoiningInScaling) demand += joining;
            demand += scaledQueued + _cfg.QueueBuffer;

            int target = demand + _cfg.MaxPlayersOffset;
            if (target < _cfg.MinimumPopulation) target = _cfg.MinimumPopulation;
            if (target > _cfg.MaximumPopulation) target = _cfg.MaximumPopulation;
            if (_cfg.NeverBelowOnline && target < online) target = online;
            return target;
        }
        #endregion

        #region Pop stats & rendering
        private struct PopStats
        {
            public int Online;
            public int Max;
            public int Joining;
            public int Queued;
            public int Sleeping;
        }

        private PopStats GetPopStats()
        {
            int online = BasePlayer.activePlayerList?.Count ?? 0;
            int sleeping = BasePlayer.sleepingPlayerList?.Count ?? 0;

            int joining, queued;
            GetQueueCounts(out joining, out queued);

            return new PopStats
            {
                Online   = online,
                Max      = ConVar.Server.maxplayers,
                Joining  = joining,
                Queued   = queued,
                Sleeping = sleeping
            };
        }

        private void GetQueueCounts(out int joining, out int queued)
        {
            joining = 0;
            queued = 0;

            var cq = ServerMgr.Instance?.connectionQueue;
            if (cq == null) return;

            joining = cq.Joining;
            queued = cq.Queued;
        }

        private string N(int v) => v.ToString("N0");

        private string Msg(string key, string playerId = null, params object[] args)
        {
            var value = lang.GetMessage(key, this, playerId);
            if (args == null || args.Length == 0) return value;
            return string.Format(value, args);
        }

        private string BuildMessage(PopStats s, string playerId = null)
        {
            int util = (s.Max <= 0) ? 0 : Mathf.Clamp(Mathf.RoundToInt(100f * (float)s.Online / (float)s.Max), 0, 100);

            // Only show utilization in chat when 90%+
            string utilPart = util >= 90
                ? $" (<color={_cfg.ColorNumber}>{util}% {Msg("LabelFull", playerId)}</color>)"
                : string.Empty;

            string queuedPart = s.Queued >= 1
                ? $" (<color={_cfg.ColorNumber}>{N(s.Queued)}</color> <color={_cfg.ColorLabel}>{Msg("LabelQueued", playerId)}</color>)"
                : string.Empty;

            return
                $"<color={_cfg.ColorNumber}>{N(s.Online)}</color>/<color={_cfg.ColorNumber}>{N(s.Max)}</color> " +
                $"<color={_cfg.ColorLabel}>{Msg("LabelPlayersOnline", playerId)}</color>{utilPart}, " +
                $"<color={_cfg.ColorNumber}>{N(s.Joining)}</color> <color={_cfg.ColorLabel}>{Msg("LabelJoining", playerId)}</color>{queuedPart}, " +
                $"<color={_cfg.ColorNumber}>{N(s.Sleeping)}</color> <color={_cfg.ColorLabel}>{Msg("LabelSleeping", playerId)}</color>";
        }
        #endregion

        #region Chat: /pop (private) and !pop (public with cooldown)
        [Command("pop")] // "/pop" -> PRIVATE reply with bot avatar (if configured)
        private void CmdPop(IPlayer player, string command, string[] args)
        {
            if (!_cfg.EnableChatCommands || player == null) return;

            var s = GetPopStats();
            var msg = BuildMessage(s, player.Id);

            var bp = player.Object as BasePlayer;
            if (bp?.net?.connection != null)
            {
                SendToOneWithIcon(bp, msg);
            }
            else
            {
                player.Reply(msg);
            }
        }

        // "!pop" -> PUBLIC broadcast with global cooldown and bot avatar
        private void OnUserChat(IPlayer player, string message)
        {
            if (!_cfg.EnableChatCommands || player == null || string.IsNullOrWhiteSpace(message)) return;
            if (!message.Trim().Equals("!pop", StringComparison.OrdinalIgnoreCase)) return;

            double now = Time.realtimeSinceStartup;
            if (now < _nextPopAllowed)
            {
                if (_cfg.ShowPopCooldownFeedback)
                {
                    var remaining = Math.Ceiling(_nextPopAllowed - now);
                    player.Reply(Msg("PopCooldown", player.Id, remaining));
                }
                return;
            }
            _nextPopAllowed = now + _cfg.PopCommandCooldownSeconds;

            var s = GetPopStats();
            var msg = BuildMessage(s, player.Id);
            BroadcastWithIcon(msg);
        }
        #endregion

        #region Broadcast helpers
        /// <summary>
        /// Sends a message to one player, forcing the chat icon from BotSteamId.
        /// Mode2 (default):   chat.add, 2, <SteamID64>, <Message>     // uses Steam profile name; matches your working reference
        /// NameOverride mode: chat.add, "<SteamID64>", "<BotName>", <Message>
        /// If BotSteamId == 0, falls back to ChatMessage (no forced icon).
        /// </summary>
        private void SendToOneWithIcon(BasePlayer p, string message)
        {
            try
            {
                if (_cfg.BotSteamId != 0UL)
                {
                    if (_cfg.UseMode2ForIcon)
                    {
                        p.SendConsoleCommand("chat.add", 2, _cfg.BotSteamId, message);
                    }
                    else
                    {
                        p.SendConsoleCommand("chat.add", _cfg.BotSteamId.ToString(), _cfg.BotName, message);
                    }
                }
                else
                {
                    p.ChatMessage(message);
                }
            }
            catch (Exception ex)
            {
                RateLimitedChatError($"SendToOneWithIcon: {p?.UserIDString ?? "unknown"}", ex);
                try
                {
                    p.ChatMessage(message);
                }
                catch (Exception fallbackEx)
                {
                    RateLimitedChatError($"ChatMessageFallback: {p?.UserIDString ?? "unknown"}", fallbackEx);
                }
            }
        }

        private void RateLimitedChatError(string context, Exception ex)
        {
            double now = Time.realtimeSinceStartup;
            if (now < _nextChatSendErrorLog) return;

            _nextChatSendErrorLog = now + 30d;
            PrintWarning($"[LWPop] Chat send failure ({context}): {ex.GetType().Name} - {ex.Message}");
        }

        /// <summary>
        /// Broadcast to all using the chosen chat.add signature so avatar resolves properly.
        /// </summary>
        private void BroadcastWithIcon(string message)
        {
            var list = BasePlayer.activePlayerList;
            if (list == null || list.Count == 0) return;

            foreach (var p in list)
            {
                if (p == null || p.net?.connection == null) continue;
                SendToOneWithIcon(p, message);
            }
        }
        #endregion

        #region Periodic broadcast
        private void RestartBroadcastTimer()
        {
            _broadcastTimer?.Destroy();
            _broadcastTimer = null;

            if (!_cfg.EnablePeriodicBroadcasts) return;

            _broadcastTimer = timer.Every(_cfg.BroadcastIntervalSeconds, () =>
            {
                if ((BasePlayer.activePlayerList?.Count ?? 0) == 0) return;
                var s = GetPopStats();
                BroadcastWithIcon(BuildMessage(s));
            });
        }
        #endregion

        #region Minimal console helpers
        [ConsoleCommand("ep.apply")]
        private void CmdApply(ConsoleSystem.Arg arg)
        {
            if (!IsServerOrOwner(arg)) return;
            ApplyNow();
            arg?.ReplyWith(Msg("ApplyDone", GetArgPlayerId(arg)));
        }

        [ConsoleCommand("ep.status")]
        private void CmdStatus(ConsoleSystem.Arg arg)
        {
            if (!IsServerOrOwner(arg)) return;

            var s = GetPopStats();
            int preview = ComputeTargetMaxPlayers(s.Online, s.Joining, s.Queued);

            int util = (s.Max <= 0) ? 0 : Mathf.Clamp(Mathf.RoundToInt(100f * (float)s.Online / (float)s.Max), 0, 100);

            arg?.ReplyWith(
                $"[LWPop] online={s.Online}, joining={s.Joining}, queued={s.Queued}, sleeping={s.Sleeping}, " +
                $"max={s.Max}, util={util}%, preview={preview}, offset={_cfg.MaxPlayersOffset}, bounds={_cfg.MinimumPopulation}-{_cfg.MaximumPopulation}, " +
                $"includeJoining={_cfg.IncludeJoiningInScaling}, includeQueued={_cfg.IncludeQueuedInScaling}, queuedWeight={_cfg.QueuedWeight:0.##}, queueBuffer={_cfg.QueueBuffer}, " +
                $"cooldown={_cfg.CooldownPeriodSeconds:0.###}s, broadcasts={_cfg.EnablePeriodicBroadcasts} every {_cfg.BroadcastIntervalSeconds:0.#}s, " +
                $"popCooldown={_cfg.PopCommandCooldownSeconds:0.#}s, botName='{_cfg.BotName}', botSteamId={_cfg.BotSteamId}, mode2={_cfg.UseMode2ForIcon}"
            );
        }

        // Set or clear the SteamID64 used for the chat icon at runtime.
        //   ep.botid 7656119xxxxxxxxxx  -> sets and saves
        //   ep.botid off               -> disables forced icon (BotSteamId=0)
        [ConsoleCommand("ep.botid")]
        private void CmdBotId(ConsoleSystem.Arg arg)
        {
            if (!IsServerOrOwner(arg)) return;

            var a = arg?.Args;
            if (a == null || a.Length != 1)
            {
                arg.ReplyWith(Msg("UsageBotId", GetArgPlayerId(arg)));
                return;
            }

            if (a[0].Equals("off", StringComparison.OrdinalIgnoreCase))
            {
                _cfg.BotSteamId = 0;
                SaveConfig();
                arg.ReplyWith(Msg("BotIdDisabled", GetArgPlayerId(arg)));
                return;
            }

            if (!ulong.TryParse(a[0], out var newId) || !IsLikelySteamId64(newId))
            {
                arg.ReplyWith(Msg("InvalidSteamId64", GetArgPlayerId(arg)));
                return;
            }

            _cfg.BotSteamId = newId;
            SaveConfig();
            arg.ReplyWith(Msg("BotIdSet", GetArgPlayerId(arg), newId));
        }

        // Toggle the chat.add signature:
        //   ep.iconmode 2     -> use (2, steamid, message)  [default; uses Steam profile name]
        //   ep.iconmode name  -> use (steamid, botName, message)
        [ConsoleCommand("ep.iconmode")]
        private void CmdIconMode(ConsoleSystem.Arg arg)
        {
            if (!IsServerOrOwner(arg)) return;

            var a = arg?.Args;
            if (a == null || a.Length != 1)
            {
                arg.ReplyWith(Msg("UsageIconMode", GetArgPlayerId(arg)));
                return;
            }

            if (a[0].Equals("2", StringComparison.OrdinalIgnoreCase))
            {
                _cfg.UseMode2ForIcon = true;
                SaveConfig();
                arg.ReplyWith(Msg("IconModeSet2", GetArgPlayerId(arg)));
            }
            else if (a[0].Equals("name", StringComparison.OrdinalIgnoreCase))
            {
                _cfg.UseMode2ForIcon = false;
                SaveConfig();
                arg.ReplyWith(Msg("IconModeSetName", GetArgPlayerId(arg)));
            }
            else
            {
                arg.ReplyWith(Msg("UsageIconMode", GetArgPlayerId(arg)));
            }
        }

        // Quick test message with current settings
        [ConsoleCommand("ep.testmsg")]
        private void CmdTestMsg(ConsoleSystem.Arg arg)
        {
            if (!IsServerOrOwner(arg)) return;
            var s = GetPopStats();
            var playerId = GetArgPlayerId(arg);
            BroadcastWithIcon($"{Msg("TestMsgPrefix", playerId)} steamid={_cfg.BotSteamId}, mode2={_cfg.UseMode2ForIcon} | {BuildMessage(s, playerId)}");
            arg.ReplyWith(Msg("TestMsgSent", playerId));
        }
        #endregion

        #region Helpers
        private string GetArgPlayerId(ConsoleSystem.Arg arg) => arg?.Player()?.UserIDString;

        private bool IsServerOrOwner(ConsoleSystem.Arg arg)
        {
            if (arg == null) return false;
            if (arg.Connection == null) return true; // server console/RCON
            if (arg.Connection.authLevel >= 2) return true;
            arg.ReplyWith(Msg("InsufficientPermission", GetArgPlayerId(arg)));
            return false;
        }
        #endregion
    }
}

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху