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

FREE SmoothRestarter : 3.1.1 3.1.1

Нет прав для скачивания
C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
 // #define UNITY_ASSERTIONS // Uncomment this if you have any issues with the plugin, assertion log can help locate problematic code
// #define SIMULATE_OXIDE_PATCH
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using ConVar;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using UnityEngine.UI;
using Debug = UnityEngine.Debug;
using Pool = Facepunch.Pool;
namespace Oxide.Plugins {
    [Info("SmoothRestarter", "2CHEVSKII", "3.1.1")]
    [Description("A reliable way to shutdown your server when you need it")]
    public class SmoothRestarter : CovalencePlugin {
#region Permission names
        const string PERMISSION_STATUS = "smoothrestarter.status", PERMISSION_RESTART = "smoothrestarter.restart", PERMISSION_CANCEL = "smoothrestarter.cancel";
#endregion
#region LangAPI keys
        const string M_CHAT_PREFIX = "Chat prefix", M_NO_PERMISSION = "No permission", M_KICK_REASON = "Kick reason", M_HELP = "Help message", M_HELP_HELP = "Help message: Help", M_HELP_STATUS = "Help message: Status", M_HELP_RESTART = "Help message: Restart", M_HELP_CANCEL = "Help message: Cancel", M_RESTARTING_ALREADY = "Restarting already", M_NOT_RESTARTING = "Not restarting", M_CANCEL_SUCCESS = "Cancelled successfully", M_RESTART_REASON_TIMED = "Restart reason: Timed", M_RESTART_REASON_OXIDE = "Restart reason: Oxide update", M_RESTART_REASON_COMMAND = "Restart reason: Command", M_RESTART_REASON_API = "Restart reason: API call", M_ANNOUNCE_RESTART_INIT = "Announcement: Restart initiated", M_ANNOUNCE_COUNTDOWN_TICK = "Announcement: Countdown tick", M_ANNOUNCE_RESTART_CANCELLED = "Announcement: Restart cancelled", M_RESTART_SUCCESS = "Restart initiated", M_STATUS_RESTARTING = "Status: Restarting", M_STATUS_RESTARTING_NATIVE = "Status: Restarting (global.restart)", M_STATUS_PLANNED = "Status: Restart planned", M_STATUS_NO_PLANNED = "Status: No planned restarts", M_UI_TITLE = "UI title", M_UI_COUNTDOWN = "UI countdown format";
#endregion
#if SIMULATE_OXIDE_PATCH
        static readonly VersionNumber CurrentOxideRustVersion = new VersionNumber(0, 0, 0);
#else
        static readonly VersionNumber CurrentOxideRustVersion = Interface.Oxide.GetAllExtensions().First(e => e.Name == "Rust").Version;
#endif
        static SmoothRestarter Instance;
        readonly FieldInfo nativeRestartRoutine = typeof(ServerMgr).GetField("restartCoroutine", BindingFlags.Instance | BindingFlags.NonPublic );
#region LangAPI EN dictionary
        readonly Dictionary<string, string> defaultMessagesEn = new Dictionary<string, string> {
            [M_CHAT_PREFIX]
            = "<color=#d9770f>Smooth Restarter</color>:", [M_NO_PERMISSION] = "<color=#f04c32>You</color> have no permission to use this command", [M_KICK_REASON] = "Server is restarting", [M_HELP] = "/sr <color=#1a97ba>[command]</color> <color=#1aba8f>[arguments]</color>\n" + "Commands: " + "<color=#1a97ba>help</color>, <color=#1a97ba>status</color>, <color=#1a97ba>restart</color>, <color=#1a97ba>cancel</color>\n" + "To get information about command usage, type '/sr <color=#1a97ba>help</color> <color=#1aba8f>[command]</color>'", [M_HELP_HELP] = "/sr <color=#1a97ba>help</color> <color=#1aba8f>[command]</color> - Outputs general help message or command usage help if command is specified", [M_HELP_STATUS] = "/sr <color=#1a97ba>status</color> - Outputs current restart status", [M_HELP_RESTART] = "/sr <color=#1a97ba>restart</color> <color=#1aba8f>[time]</color> - Initiates new restart process\n" + "Time must be in one of the following formats:\n" + "<color=#77ba20>123</color> - delay before restart in seconds\n" + "<color=#77ba20>123</color><h|m|s> - delay before restart in <hours|minutes|seconds>\n" + "<color=#77ba20>1</color>h <color=#77ba20>2</color>m <color=#77ba20>3</color>s - delay before restart in hr+min+sec, all optional\n" + "<color=#77ba20>1</color>:<color=#77ba20>23</color> - schedule restart on 1:23 (24hr format)", [M_HELP_CANCEL] = "/sr <color=#1a97ba>cancel</color> - Cancels current restart process", [M_RESTARTING_ALREADY] = "Cannot do restart - already restarting.Use '/sr <color=#1a97ba>status</color>' to get info about current restart process, or try '/sr <color=#1a97ba>cancel</color>' to cancel current restart before starting new one", [M_NOT_RESTARTING] = "Cannot cancel restart - plugin does not perform a restart currently.", [M_CANCEL_SUCCESS] = "Restart was successfully cancelled", [M_RESTART_REASON_TIMED] = "Planned", [M_RESTART_REASON_OXIDE] = "New Oxide update is out", [M_RESTART_REASON_COMMAND] = "Command from <color=#dbc30b>{0}</color>", [M_RESTART_REASON_API] = "API call from <color=#dbc30b>{0}</color>", [M_ANNOUNCE_RESTART_INIT] = "Server will be restarted in <color=#a4db0b>{0:sfmt::<hr?+ hours ><min?+ minutes ><sec?+ seconds>}</color> ({1})", [M_ANNOUNCE_COUNTDOWN_TICK] = "<color=#a4db0b>{0:sfmt::<hr?+h ><min?+min ><sec?+s>}</color> left before server restart", [M_ANNOUNCE_RESTART_CANCELLED] = "Server restart was cancelled", [M_RESTART_SUCCESS] = "Restart initiated successfully", [M_STATUS_RESTARTING] = "Server is restarting, <color=#a4db0b>{1:sfmt::<min#0.0!+ min ><sec?+ seconds>}</color> left", [M_STATUS_RESTARTING_NATIVE] = "Server is restarting natively", [M_STATUS_PLANNED] = "Server restart planned at <color=#a4db0b>{0:hh\\:mm}</color> (<color=#a4db0b>{1:sfmt::<hr?+ hours ><min?+ minutes ><sec?+ seconds>}</color> left)", [M_STATUS_NO_PLANNED] = "Server is not restarting, no planned restarts found", [M_UI_TITLE] = "SmoothRestarter", [M_UI_COUNTDOWN] = "{0:sfmt::<min#2!>:<sec#2>} left"
        }
        ;
#endregion
        readonly Regex timeParseRegex = new Regex(@"^\s*(?:(\d+)|(\d+\s*[HhMmSs])|(?!$)((?:(?<h>\d+)[Hh])?\s*(?:(?<m>\d+)[Mm])?\s*(?:(?<s>\d+)[Ss])?)|(\d{1,2}:\d{1,2}))\s*$", RegexOptions.Compiled );
        SmoothRestart component;
        PluginSettings settings;
        bool isNewOxideOut;
        bool IsRestarting => IsRestartingNative || IsRestartingComponent;
        bool IsRestartingNative => ServerMgr.Instance.Restarting;
        bool IsRestartingComponent => component.IsRestarting;
#region Oxide hooks
        // ReSharper disable UnusedMember.Local
        void Init() {
            Instance = this;
            if (!settings.EnableUi) {
                Unsubscribe(nameof(OnUserConnected));
                Unsubscribe(nameof(OnUserDisconnected));
            }
            permission.RegisterPermission(PERMISSION_STATUS, this);
            permission.RegisterPermission(PERMISSION_RESTART, this);
            permission.RegisterPermission(PERMISSION_CANCEL, this);
            AddCovalenceCommand(settings.Commands, "CommandHandler");
            //Log("{0}", BitConverter.GetBytes(decimal.GetBits(0.123m)[3])[2]);
        }
        void OnServerInitialized() {
            component = ServerMgr.Instance.gameObject.AddComponent<SmoothRestart>();
            if (settings.EnableUi) foreach (var player in players.Connected) {
                OnUserConnected(player);
            }
        }
        void OnUserConnected(IPlayer user) {
            var player = (BasePlayer)user.Object;
            player.gameObject.AddComponent<SmoothRestarterUi>();
        }
        void OnUserDisconnected(IPlayer user) {
            var player = (BasePlayer)user.Object;
            UnityEngine.Object.Destroy(player.GetComponent<SmoothRestarterUi>());
        }
        void Unload() {
            if (IsRestartingComponent) {
                component.CancelRestart(this);
            }
            if (settings.EnableUi) {
                SmoothRestarterUi.Cleanup();
            }
            UnityEngine.Object.Destroy(component);
            Instance = null;
        }
        // ReSharper restore UnusedMember.Local
#endregion
#region Command handler
        // ReSharper disable once UnusedMember.Local
        void CommandHandler(IPlayer player, string _, string[] args) {
            if (args.Length == 0) {
                Message(player, M_HELP);
            }
            else {
                var command = args[0];
                switch (command) {
                    case "status" : if (CheckPermission(player, PERMISSION_STATUS)) {
                        DateTime dateTime;
                        //Message(player, GetStatus(out dateTime), dateTime.TimeOfDay, (dateTime - DateTime.Now).TotalSeconds);
                        MessageWithCustomFormatter(player, SmoothTimeFormatter.Instance, GetStatus(out dateTime), dateTime.TimeOfDay, (dateTime - DateTime.Now).TotalSeconds);
                    }
                    break;
                    case "restart" : if (CheckPermission(player, PERMISSION_RESTART)) {
                        if (IsRestarting) {
                            Message(player, M_RESTARTING_ALREADY);
                        }
                        else {
                            TimeSpan time;
                            bool isTod;
                            if (args.Length == 1 || !TryParseTime(string.Join(" ", args.Skip(1)), out time, out isTod)) {
                                Message(player, M_HELP_RESTART);
                                return;
                            }
                            DateTime restartTime;
                            if (!isTod) {
                                restartTime = DateTime.Now + time;
                            }
                            else if (time < DateTime.Now.TimeOfDay) {
                                restartTime = DateTime.Today.AddDays(1) + time;
                            }
                            else {
                                restartTime = DateTime.Today + time;
                            }
                            component.DoRestart(restartTime, RestartReason.Command, player);
                            Message(player, M_RESTART_SUCCESS);
                        }
                    }
                    break;
                    case "cancel" : if (CheckPermission(player, PERMISSION_CANCEL)) {
                        if (IsRestartingNative) {
                            CancelNativeRestart();
                        }
                        else if (IsRestartingComponent) {
                            component.CancelRestart(player);
                        }
                        else {
                            Message(player, M_NOT_RESTARTING);
                            return;
                        }
                        Message(player, M_CANCEL_SUCCESS);
                    }
                    break;
                    case "help" : Message(player, GetHelp(args.Skip(1)));
                    break;
                    default : Message(player, M_HELP);
                    break;
                }
            }
        }
        string GetHelp(IEnumerable<string> args) {
            var arg = args.FirstOrDefault();
            if (arg != null) {
                switch (arg.ToLower()) {
                    case "help" : return M_HELP_HELP;
                    case "status" : return M_HELP_STATUS;
                    case "restart" : return M_HELP_RESTART;
                    case "cancel" : return M_HELP_CANCEL;
                }
            }
            return M_HELP;
        }
        string GetStatus(out DateTime restartTime) {
            restartTime = default(DateTime);
            if (IsRestartingNative) {
                return M_STATUS_RESTARTING_NATIVE;
            }
            if (IsRestartingComponent) {
                restartTime = component.CurrentRestartTime.Value;
                return M_STATUS_RESTARTING;
            }
            if (settings.RestartTimes.Count > 0) {
                double _;
                restartTime = component.FindNextRestartTime(out _);
                return M_STATUS_PLANNED;
            }
            return M_STATUS_NO_PLANNED;
        }
        bool TryParseTime(string argString, out TimeSpan time, out bool isTod) {
            Match match = timeParseRegex.Match(argString);
            isTod = false;
            if (match.Groups[1].Success) {
                var seconds = int.Parse(match.Groups[1].Value);
                time = TimeSpan.FromSeconds(seconds);
            }
            else if (match.Groups[2].Success) {
                var str = match.Groups[2].Value;
                var specifier = str[str.Length - 1];
                var number = int.Parse(str.Remove(str.Length - 1).TrimEnd());
                switch (specifier) {
                    case 'H' : case 'h' : number *= 3600;
                    break;
                    case 'M' : case 'm' : number *= 60;
                    break;
                }
                time = TimeSpan.FromSeconds(number);
            }
            else if (match.Groups[3].Success) {
                var h = match.Groups["h"].Success ? int.Parse(match.Groups["h"].Value) : 0;
                var m = match.Groups["m"].Success ? int.Parse(match.Groups["m"].Value) : 0;
                var s = match.Groups["s"].Success ? int.Parse(match.Groups["s"].Value) : 0;
                time = new TimeSpan(h, m, s);
            }
            else if (TimeSpan.TryParse(match.Groups[4].Value, out time)) {
                isTod = true;
            }
            else {
                time = default(TimeSpan);
                return false;
            }
            if (time.TotalSeconds == 0) {
                return false;
            }
            return true;
        }
        bool CheckPermission(IPlayer player, string perm) {
            if (player.HasPermission(perm)) {
                return true;
            }
            Message(player, M_NO_PERMISSION);
            return false;
        }
#endregion
#region Utility
        void FetchLatestOxideRustVersion(Action<Exception, VersionNumber> callback) {
            PluginLog("Fetching latest Oxide.Rust version...");
            webrequest.Enqueue("https://umod.org/games/rust.json", null, (responseCode, json) => {
                if (responseCode != 200) {
                    callback(new Exception($"Failed to fetch latest Oxide.Rust version from uMod.org API - code {responseCode}" ), default(VersionNumber) );
                }
                else {
                    try {
                        var response = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
                        if (response == null) {
                            throw new Exception("Response is null");
                        }
                        var latestVersionStr = response["latest_release_version"];
                        var array = ((string)latestVersionStr).Split('.');
                        var version = new VersionNumber(int.Parse(array[0]), int.Parse(array[1]), int.Parse(array[2]));
                        callback(null, version);
                    }
                    catch (Exception e) {
                        callback(new Exception($"Failed to deserialize uMod.org API response: {e.Message}"), default(VersionNumber) );
                    }
                }
            }
            , this);
        }
        void PluginLog(string format, params object[] args) {
            if (settings.EnableLog) {
                Log(format, args);
            }
        }
        void KickAll() {
            foreach (var player in players.Connected.ToArray()) {
                player.Kick(GetMessage(player, M_KICK_REASON));
            }
        }
        void CancelNativeRestart() {
            Debug.Assert(IsRestartingNative, "Cancelling native restart while !IsRestartingNative");
            var routine = (IEnumerator)nativeRestartRoutine.GetValue(ServerMgr.Instance);
            ServerMgr.Instance.StopCoroutine(routine);
            nativeRestartRoutine.SetValue(ServerMgr.Instance, null);
            ConsoleNetwork.BroadcastToAllClients("chat.add", new object[] {
                2, 0, "<color=#fff>SERVER</color> Restart interrupted!"
            }
            );
            PluginLog("Native restart was cancelled");
        }
#endregion
#region Plugin API
        // ReSharper disable UnusedMember.Local
        [HookMethod(nameof(IsSmoothRestarting))]
        public bool IsSmoothRestarting() {
            return IsRestartingComponent;
        }
        [HookMethod(nameof(GetPlannedRestarts))]
        public IReadOnlyCollection<TimeSpan> GetPlannedRestarts() {
            return settings.RestartTimes;
        }
        [HookMethod(nameof(GetCurrentRestartTime))]
        public DateTime? GetCurrentRestartTime() {
            return component.CurrentRestartTime;
        }
        [HookMethod(nameof(GetCurrentRestartReason))]
        public int? GetCurrentRestartReason() {
            return (int?)component.CurrentRestartReason;
        }
        [HookMethod(nameof(GetCurrentRestartInitiator))]
        public object GetCurrentRestartInitiator() {
            return component.CurrentRestartInitiator;
        }
        [HookMethod(nameof(InitSmoothRestart))]
        public bool InitSmoothRestart(DateTime restartTime, Plugin initiator) {
            if (initiator == null || !initiator.IsLoaded) {
                return false;
            }
            if (restartTime < DateTime.Now) {
                return false;
            }
            if (IsRestartingComponent) {
                return false;
            }
            if (IsRestartingNative) {
                CancelNativeRestart();
            }
            component.DoRestart(restartTime, RestartReason.ApiCall, initiator);
            return true;
        }
        [HookMethod(nameof(CancelSmoothRestart))]
        public bool CancelSmoothRestart(Plugin canceller) {
            if (canceller == null || !canceller.IsLoaded) {
                return false;
            }
            if (!IsRestartingComponent) {
                return false;
            }
            component.CancelRestart(canceller);
            return true;
        }
        // ReSharper restore UnusedMember.Local
#endregion
#region LangAPI
        protected override void LoadDefaultMessages() {
            lang.RegisterMessages(defaultMessagesEn, this, "en");
        }
        string GetMessage(IPlayer player, string langKey) {
            return lang.GetMessage(langKey, this, player.Id);
        }
        void Message(IPlayer player, string langKey, params object[] args) {
            player.Message(GetMessage(player, langKey), GetMessage(player, M_CHAT_PREFIX), args);
        }
        void MessageWithCustomFormatter(IPlayer player, IFormatProvider formatProvider, string langKey, params object[] args) {
            var format = GetMessage(player, langKey);
            var message = string.Format(formatProvider, format, args);
            player.Message(message, GetMessage(player, M_CHAT_PREFIX));
        }
        void MessageRaw(IPlayer player, string message, params object[] args) {
            player.Message(message, GetMessage(player, M_CHAT_PREFIX), args);
        }
        void AnnounceRestartInit(float secondsLeft, RestartReason reason, object initiator = null) {
            foreach (IPlayer player in players.Connected) {
                var reasonStr = GetRestartReasonString(player, reason, initiator);
                //Message(player, M_ANNOUNCE_RESTART_INIT, secondsLeft.ToString("0"), reasonStr);
                MessageWithCustomFormatter(player, SmoothTimeFormatter.Instance, M_ANNOUNCE_RESTART_INIT, secondsLeft, reasonStr );
            }
        }
        void Announce(string langKey, params object[] args) {
            foreach (IPlayer player in players.Connected) {
                Message(player, langKey, args);
            }
        }
        void AnnounceWithCustomFormatter(IFormatProvider formatProvider, string langKey, params object[] args) {
            foreach (IPlayer player in players.Connected) {
                MessageWithCustomFormatter(player, formatProvider, langKey, args);
            }
        }
        void AnnounceRaw(string message, params object[] args) {
            foreach (var player in players.Connected) {
                MessageRaw(player, message, args);
            }
        }
        string GetRestartReasonString(IPlayer player, RestartReason reason, object initiator = null) {
            Debug.Assert((int)reason < 2 || initiator != null);
            string reasonStr;
            switch (reason) {
                case RestartReason.Timed : reasonStr = GetMessage(player, M_RESTART_REASON_TIMED);
                break;
                case RestartReason.OxideUpdate : reasonStr = GetMessage(player, M_RESTART_REASON_OXIDE);
                break;
                case RestartReason.Command : reasonStr = string.Format(GetMessage(player, M_RESTART_REASON_COMMAND), ((IPlayer)initiator).Name);
                break;
                default : reasonStr = string.Format(GetMessage(player, M_RESTART_REASON_API), ((Plugin)initiator).Name);
                break;
            }
            return reasonStr;
        }
#endregion
#region Configuration
        protected override void LoadDefaultConfig() {
            settings = PluginSettings.GetDefaults();
            SaveConfig();
        }
        protected override void LoadConfig() {
            base.LoadConfig();
            try {
                settings = Config.ReadObject<PluginSettings>();
                if (settings == null) {
                    throw new Exception("Config is null");
                }
                if (!settings.Validate()) {
                    LogWarning("Errors found in the configuration file, corrected values will be saved");
                    SaveConfig();
                }
            }
            catch (Exception e) {
                LogError("Error while loading configuration: {0}", e.Message);
                LoadDefaultConfig();
            }
        }
        protected override void SaveConfig() {
            Config.WriteObject(settings);
        }
#endregion
#region Nested types
        enum RestartReason {
            Timed, OxideUpdate, Command, ApiCall
        }
#region Configuration
        class PluginSettings {
            float[] uiPosCached;
            HashSet<TimeSpan> restartTimesCached;
            [JsonProperty("Daily restarts")]
            public string[] DailyRestart {
                get;
                set;
            }
            [JsonProperty("Restart when new Oxide.Rust is out")]
            public bool OxideUpdateRestart {
                get;
                set;
            }
            [JsonProperty("Initiate countdown at")]
            public int RestartCountdownMax {
                get;
                set;
            }
            [JsonProperty("Enable UI")]
            public bool EnableUi {
                get;
                set;
            }
            [JsonProperty("UI position (X,Y)")]
            public string UiPosition {
                get;
                set;
            }
            [JsonProperty("UI scale")]
            public float UiScale {
                get;
                set;
            }
            [JsonProperty("Enable console logs")]
            public bool EnableLog {
                get;
                set;
            }
            [JsonProperty("Commands")]
            public string[] Commands {
                get;
                set;
            }
            [JsonProperty("Disable chat countdown notifications")]
            public bool DisableChatCountdown {
                get;
                set;
            }
            [JsonProperty("Custom countdown reference points")]
            public int[] CountdownRefPts {
                get;
                set;
            }
            [JsonProperty("Use custom countdown reference points")]
            public bool UseCustomCountdownRefPts {
                get;
                set;
            }
            [JsonIgnore]
            public float UiX => uiPosCached[0];
            [JsonIgnore]
            public float UiY => uiPosCached[1];
            [JsonIgnore]
            public HashSet<TimeSpan> RestartTimes => restartTimesCached;
            public static PluginSettings GetDefaults() {
                return new PluginSettings {
                    DailyRestart = new[] {
                        "0:00"
                    }
                    , OxideUpdateRestart = true, RestartCountdownMax = 300, EnableUi = true, EnableLog = true, UiPosition = "0.92, 0.92", UiScale = 1.0f, Commands = new[] {
                        "sr", "srestart", "smoothrestart", "smoothrestarter"
                    }
                    , DisableChatCountdown = false, UseCustomCountdownRefPts = false, CountdownRefPts = new[] {
                        60, 50, 40, 30, 25, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
                    }
                }
                ;
            }
#region Validation methods
            public bool Validate() {
                bool valid = true;
                if (DailyRestart == null) {
                    Instance.LogWarning("Daily restart times cannot be null");
                    DailyRestart = Array.Empty<string>();
                    valid = false;
                }
                if (!ParseRestartTimes(out restartTimesCached)) {
                    DailyRestart = restartTimesCached.Select(rt => rt.ToString("hh\\:mm")).ToArray();
                    valid = false;
                }
                if (RestartCountdownMax < 0) {
                    Instance.LogWarning("Restart countdown cannot be less than zero");
                    RestartCountdownMax = 0;
                    valid = false;
                }
                if (UiScale < 0.3f) {
                    UiScale = 0.3f;
                    Instance.LogWarning("UI scale cannot be less than 0.3");
                    valid = false;
                }
                else if (UiScale > 3f) {
                    UiScale = 3f;
                    Instance.LogWarning("UI scale cannot be greater than 3");
                    valid = false;
                }
                uiPosCached = new float[2];
                if (!ParseUiPos(out uiPosCached[0], out uiPosCached[1])) {
                    uiPosCached[0] = 0.92f;
                    uiPosCached[1] = 0.92f;
                    UiPosition = "0.92, 0.92";
                    valid = false;
                }
                if (Commands == null || Commands.Length == 0) {
                    Commands = new[] {
                        "sr", "srestart", "smoothrestart", "smoothrestarter"
                    }
                    ;
                    valid = false;
                }
                if (CountdownRefPts == null) {
                    CountdownRefPts = new[] {
                        60, 50, 40, 30, 25, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
                    }
                    ;
                    valid = false;
                }
                if (UseCustomCountdownRefPts && CountdownRefPts.Length == 0) {
                    CountdownRefPts = new[] {
                        30, 10, 5
                    }
                    ;
                    valid = false;
                }
                CountdownRefPts = CountdownRefPts.OrderByDescending(_ => _).ToArray();
                return valid;
            }
            bool ParseUiPos(out float fX, out float fY) {
                fX = fY = 0f;
                var array = UiPosition.Split(',');
                if (array.Length != 2) {
                    Instance.LogWarning("Invalid format of UI position: {0}", UiPosition);
                    return false;
                }
                bool valid = true;
                if (!float.TryParse(array[0], NumberStyles.Number, CultureInfo.InvariantCulture, out fX) || fX < 0f || fX > 1f) {
                    Instance.LogWarning("Invalid value for UI position X coordinate: {0}", array[0]);
                    valid = false;
                }
                if (!float.TryParse(array[1], NumberStyles.Number, CultureInfo.InvariantCulture, out fY) || fY < 0f || fY > 1f) {
                    Instance.LogWarning("Invalid value for UI position Y coordinate: {0}", array[1]);
                    valid = false;
                }
                return valid;
            }
            bool ParseRestartTimes(out HashSet<TimeSpan> restartTimes) {
                bool valid = true;
                restartTimes = new HashSet<TimeSpan>();
                foreach (var rt in DailyRestart) {
                    TimeSpan result;
                    if (!TimeSpan.TryParse(rt, out result)) {
                        valid = false;
                        Instance.LogWarning("Invalid time specifier: {0}", rt);
                    }
                    if (!restartTimes.Add(result)) {
                        valid = false;
                        Instance.LogWarning("Restart time duplication: {0}", rt);
                    }
                }
                return valid;
            }
#endregion
        }
#endregion
#region SmoothRestarterUi
        class SmoothRestarterUi : MonoBehaviour {
            const string UI_BACKGROUND = "smoothrestarter.ui::background", UI_TITLE = "smoothrestarter.ui::title", UI_PROGRESS_BAR = "smoothrestarter.ui::progress_bar", UI_SECONDS = "smoothrestarter.ui::seconds_left";
            static HashSet<SmoothRestarterUi> AllComponents;
            static Dictionary<string, string> UiSecondsCache;
            static Dictionary<string, string> UiProgressbarCache;
            string mainPanel;
            BasePlayer player;
            bool isVisible;
            string locale;
            bool IsVisible {
                set {
                    Debug.Assert(value != isVisible, "Double set isVisible to " + value);
                    SetVisible(value);
                }
            }
            public static void Cleanup() {
                if (AllComponents == null) {
                    return;
                }
                foreach (var component in AllComponents.ToArray()) {
                    DestroyImmediate(component);
                }
                UiSecondsCache = null;
                UiProgressbarCache = null;
                AllComponents = null;
            }
#region Unity methods
            void Awake() {
                if (AllComponents == null) {
                    AllComponents = new HashSet<SmoothRestarterUi>();
                }
                //if (mainPanel == null)
                //{
                //}
                player = GetComponent<BasePlayer>();
                locale = Instance.lang.GetLanguage(player.UserIDString);
                var uiTitle = Instance.GetMessage(player.IPlayer, M_UI_TITLE);
                var container = new CuiElementContainer {
                    new CuiElement {
                        FadeOut = 0.2f, Name = UI_BACKGROUND, Parent = "Hud", Components = {
                            new CuiImageComponent {
                                Color = "0.2 0.2 0.2 0.3", FadeIn = 0.2f, ImageType = Image.Type.Simple, Material = "assets/content/ui/uibackgroundblur.mat"
                            }
                            , new CuiRectTransformComponent {
                                AnchorMin = $"{Instance.settings.UiX - 0.08f * Instance.settings.UiScale} " + $"{Instance.settings.UiY - 0.05f * Instance.settings.UiScale}", AnchorMax = $"{Instance.settings.UiX + 0.08f * Instance.settings.UiScale} " + $"{Instance.settings.UiY + 0.05f * Instance.settings.UiScale}"
                            }
                        }
                    }
                    , new CuiElement {
                        FadeOut = 0.2f, Name = UI_TITLE, Parent = UI_BACKGROUND, Components = {
                            new CuiTextComponent {
                                Color = "0.8 0.8 0.8 0.95", FadeIn = 0.2f, Align = TextAnchor.MiddleCenter, Font = "robotocondensed-bold.ttf", FontSize = 16, Text = uiTitle  /*nameof(SmoothRestarter)*/
                            }
                            , new CuiRectTransformComponent {
                                AnchorMin = "0 0.6", AnchorMax = "1 1"
                            }
                        }
                    }
                }
                ;
                mainPanel = SerializeUi(container);
                if (UiSecondsCache == null) {
                    UiSecondsCache = new Dictionary<string, string>();
                }
                if (UiProgressbarCache == null) {
                    UiProgressbarCache = new Dictionary<string, string>();
                }
                AllComponents.Add(this);
            }
            void Start() {
                InvokeRepeating(nameof(UiTick), 1f, 1f);
            }
            void OnDestroy() {
                if (isVisible) {
                    IsVisible = false;
                }
                AllComponents?.Remove(this);
            }
#endregion
            void SetVisible(bool visible) {
                Debug.Assert(player && player.IsConnected, "Player is null or disconnected");
                if (visible) {
                    CuiHelper.AddUi(player, mainPanel);
                }
                else {
                    CuiHelper.DestroyUi(player, UI_SECONDS);
                    CuiHelper.DestroyUi(player, UI_PROGRESS_BAR);
                    CuiHelper.DestroyUi(player, UI_TITLE);
                    CuiHelper.DestroyUi(player, UI_BACKGROUND);
                }
                isVisible = visible;
            }
            void UiTick() {
                if (Instance.IsRestartingComponent) {
                    if (!isVisible) {
                        IsVisible = true;
                    }
                    var secondsLeft = (Instance.component.CurrentRestartTime.Value - DateTime.Now).TotalSeconds;
                    UpdateSeconds(secondsLeft);
                    UpdateProgressBar(GetFraction(secondsLeft));
                }
                else if (isVisible) {
                    IsVisible = false;
                }
            }
#region Ui update methods
            void UpdateSeconds(double secondsLeft) {
                CuiHelper.DestroyUi(player, UI_SECONDS);
                CuiHelper.AddUi(player, LookupUiDictionary(locale, (int)secondsLeft, UiSecondsCache, CreateSeconds));
            }
            void UpdateProgressBar(float fraction) {
                Debug.Assert(fraction >= 0f && fraction <= 1, "Progress bar fraction is outside of 0..1 range: " + fraction);
                CuiHelper.DestroyUi(player, UI_PROGRESS_BAR);
                CuiHelper.AddUi(player, LookupUiDictionary("any", fraction, UiProgressbarCache, CreateProgressBar));
            }
            string CreateSeconds(int secondsLeft) {
                return SerializeUi(new CuiElementContainer {
                    new CuiElement {
                        FadeOut = 0.2f, Name = UI_SECONDS, Parent = UI_BACKGROUND, Components = {
                            new CuiTextComponent {
                                Color = "0.8 0.8 0.8 0.95", FadeIn = 0.2f, Align = TextAnchor.MiddleCenter, Font = "robotocondensed-bold.ttf", FontSize = 16, Text = string.Format(SmoothTimeFormatter.Instance, Instance.GetMessage(player.IPlayer, M_UI_COUNTDOWN), secondsLeft )
                            }
                            , new CuiRectTransformComponent {
                                AnchorMin = "0 0", AnchorMax = "1 0.45"
                            }
                        }
                    }
                }
                );
            }
            string CreateProgressBar(float fraction) {
                return SerializeUi(new CuiElementContainer {
                    new CuiElement {
                        FadeOut = 0.2f, Name = UI_PROGRESS_BAR, Parent = UI_BACKGROUND, Components = {
                            new CuiImageComponent {
                                Color = GetFractionColor(fraction), FadeIn = 0f, ImageType = Image.Type.Simple, Material = "assets/content/ui/namefontmaterial.mat"
                            }
                            , new CuiRectTransformComponent {
                                AnchorMin = $"{0.5f - fraction * 0.45f} 0.45", AnchorMax = $"{0.5f + fraction * 0.45f} 0.6"
                            }
                        }
                    }
                }
                );
            }
#endregion
#region Utility
            float GetFraction(double secondsLeft) {
                var norm = Instance.settings.RestartCountdownMax;
                if (secondsLeft > norm) {
                    return 1f;
                }
                return (float)(secondsLeft / norm);
            }
            string GetFractionColor(float fraction) {
                Debug.Assert(fraction <= 1 && fraction >= 0, "GetFractionColor: fraction is not clamped");
                float r;
                float g;
                if (fraction > 0.5f) {
                    r = Mathf.Lerp(.85f, .15f, fraction / 2);
                    g = 0.85f;
                }
                else {
                    r = 0.85f;
                    g = Mathf.Lerp(.15f, .85f, fraction * 2);
                }
                return $"{r} {g} 0.2 1";
            }
            static string LookupUiDictionary<T>(string locale, T value, Dictionary<string, string> dict, Func<T, string> createFunction) {
                string key = $"{locale}->{value}";
                string returnVal;
                if (!dict.TryGetValue(key, out returnVal)) {
                    returnVal = createFunction(value);
                    dict[key] = returnVal;
                }
                return returnVal;
            }
            string SerializeUi(CuiElementContainer container) {
                return JsonConvert.SerializeObject(container, Formatting.None, new JsonSerializerSettings {
                    DefaultValueHandling = DefaultValueHandling.Ignore
                }
                ).Replace("\\n", "\n");
            }
#endregion
        }
#endregion
#region SmoothRestart
        class SmoothRestart : MonoBehaviour {
            IEnumerator restartRoutine;
            Queue<DateTime> restartQueue;
            int rtCountdownStart;
            bool doTimedRestarts;
            bool doRestartChecks;
            public DateTime? CurrentRestartTime {
                get;
                private set;
            }
            public RestartReason? CurrentRestartReason {
                get;
                private set;
            }
            public object CurrentRestartInitiator {
                get;
                private set;
            }
            public bool IsRestarting => restartRoutine != null;
            bool DoRestartChecks {
                set {
                    Debug.Assert(value != doRestartChecks, "Double set DoRestartChecks to " + value);
                    if (doRestartChecks != value) {
                        doRestartChecks = value;
                        if (value) {
                            InvokeRepeating(nameof(RestartCheck), 1f, 1f);
                        }
                        else {
                            CancelInvoke(nameof(RestartCheck));
                        }
                    }
                }
            }
#region Unity methods
            void Awake() {
                rtCountdownStart = Instance.settings.RestartCountdownMax;
                if (Instance.settings.RestartTimes.Count > 0) {
                    restartQueue = new Queue<DateTime>(Instance.settings.RestartTimes.Select(ts => DateTime.Today + ts) );
                    doTimedRestarts = true;
                    Instance.PluginLog("Loaded {0} restart time(s):\n{1}", restartQueue.Count, string.Join("\n", restartQueue.Select(ts => ts.TimeOfDay.ToString("hh\\:mm").PadLeft(50))) );
                }
                if (Instance.settings.OxideUpdateRestart) {
                    Invoke(nameof(OxideVersionCheck), 5f);
                }
            }
            void Start() {
                if (doTimedRestarts || Instance.settings.OxideUpdateRestart) {
                    DoRestartChecks = true;
                }
            }
            void OnDestroy() {
                if (IsRestarting) {
                    CancelRestart(Instance);
                }
            }
#endregion
#region Public API
            public void DoRestart(DateTime restartTime, RestartReason restartReason, object restartInitiator) {
                Debug.Assert(!Instance.IsRestarting, "DoRestart while restarting already");
                Debug.Assert(restartTime > DateTime.Now, "Initiating restart in the past");
                var totalSecondsLeft = (float)(restartTime - DateTime.Now).TotalSeconds;
                DoRestartChecks = false;
                restartRoutine = InitRestartRoutine(totalSecondsLeft);
                CurrentRestartTime = restartTime;
                CurrentRestartReason = restartReason;
                CurrentRestartInitiator = restartInitiator;
                StartCoroutine(restartRoutine);
                OnRestartInit(totalSecondsLeft, restartReason, restartInitiator);
            }
            public void CancelRestart(object canceller) {
                Debug.Assert(canceller != null, "Restart canceller is null");
                StopCoroutine(restartRoutine);
                Cleanup();
                DoRestartChecks = true;
                OnRestartCancelled(canceller);
            }
            public DateTime FindNextRestartTime(out double diff) {
                DateTime restartTime;
                DateTime now = DateTime.Now;
                while ((restartTime = restartQueue.Peek()) < now) CycleQueue();
                diff = (restartTime - now).TotalSeconds;
                return restartTime;
            }
#endregion
            void OxideVersionCheck() {
                Debug.Assert(Instance.settings.OxideUpdateRestart, "OxideVersionCheck called when config value is false");
                Debug.Assert(!Instance.isNewOxideOut, "OxideVersionCheck called when already new oxide patch detected");
                Instance.FetchLatestOxideRustVersion((e, v) => {
                    if (e != null) {
                        Instance.PluginLog(e.Message);
                        Instance.PluginLog("Scheduling check after 1 minute...");
                        Invoke(nameof(OxideVersionCheck), 60f);
                    }
                    else if (v > CurrentOxideRustVersion) {
                        Instance.PluginLog("New Oxide.Rust version detected {0} -> {1}", CurrentOxideRustVersion, v);
                        Instance.isNewOxideOut = true;
                    }
                    else {
                        Instance.PluginLog("Current Oxide.Rust version is up-to-date, scheduling check after 10 minutes...");
                        Invoke(nameof(OxideVersionCheck), 600f);
                    }
                }
                );
            }
            void RestartCheck() {
                Debug.Assert(!IsRestarting, "Restart check while IsRestarting");
                if (Instance.IsRestartingNative) {
                    return;
                    // since we have no observer on native restart routine, we do not pause invokes of this function, but simply interrupt it here
                    // might wanna think about a workaround to raise event when native restart has started/interrupted (Harmony patch probably)
                }
                if (Instance.isNewOxideOut) {
                    DoRestart(DateTime.Now.AddSeconds(rtCountdownStart), RestartReason.OxideUpdate, null);
                }
                else {
                    DateTime restartTime;
                    if (NeedsRestartOnTime(out restartTime)) {
                        CycleQueue();
                        DoRestart(restartTime, RestartReason.Timed, null);
                    }
                }
            }
            bool NeedsRestartOnTime(out DateTime restartTime) {
                if (restartQueue == null || restartQueue.Count == 0) {
                    restartTime = default(DateTime);
                    return false;
                }
                double diff;
                restartTime = FindNextRestartTime(out diff);
                if (diff <= rtCountdownStart) {
                    return true;
                }
                return false;
            }
            void CycleQueue() => restartQueue.Enqueue(restartQueue.Dequeue().AddDays(1));
#region Component events
            void OnRestartInit(float secondsLeft, RestartReason reason, object initiator) {
                Instance.PluginLog("Server restart initiated in {0} seconds, reason: {1}, initiator: {2}", secondsLeft, reason, initiator );
                if (Interface.CallHook("OnSmoothRestartInit", secondsLeft, reason, initiator) == null) {
                    Instance.AnnounceRestartInit(secondsLeft, reason, initiator);
                }
            }
            void OnRestartTick(int secondsLeft) {
                Instance.PluginLog("Server restart in progress, {0} seconds left", secondsLeft);
                if (Interface.CallHook("OnSmoothRestartTick", secondsLeft) == null && !Instance.settings.DisableChatCountdown) {
                    // The code below is so harsh Im feeling embarassed with it, but it is a solution I came with at the moment
                    // which does not force me to rewrite the whole messaging system
                    //foreach (var player in Instance.players.Connected)
                    //{
                    //    var message = string.Format(
                    //        new IntTimeFormatter(),
                    //        Instance.GetMessage(player, M_ANNOUNCE_COUNTDOWN_TICK),
                    //        secondsLeft
                    //    );
                    //    Instance.MessageRaw(player, message);
                    //}
                    // I rewrote the messaging system :/
                    Instance.AnnounceWithCustomFormatter(SmoothTimeFormatter.Instance, M_ANNOUNCE_COUNTDOWN_TICK, secondsLeft );
                }
            }
            void OnRestartCancelled(object canceller) {
                Instance.PluginLog("Server restart was cancelled by {0}", canceller);
                Cleanup();
                if (Interface.CallHook("OnSmoothRestartCancelled", canceller) == null) {
                    Instance.Announce(M_ANNOUNCE_RESTART_CANCELLED);
                }
            }
#endregion
            IEnumerator InitRestartRoutine(float totalSecondsLeft) {
                Debug.Assert(totalSecondsLeft > 0);
                while (totalSecondsLeft > 0) {
                    var nextCd = GetNextCountdownValue(Mathf.CeilToInt(totalSecondsLeft - 1f));
                    yield return new WaitForSecondsRealtime(totalSecondsLeft - nextCd);
                    OnRestartTick(nextCd);
                    totalSecondsLeft = n
// ... [Code preview truncated due to length. Full source in attached file] ...

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