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

FREE FeedMeUpdates : 1.6.5 1.6.5

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using System.Security.Cryptography;
namespace Oxide.Plugins {
    [Info("FeedMeUpdates", "frankie290651", "1.6.5")]
    [Description("Highly configurable plugin for Oxide framework to orchestrate Server/Oxide/Plugins updates.")]
    public class FeedMeUpdates : CovalencePlugin {
#region Configuration
        private class ConfigData {
            public string ServerDirectory {
                get;
                set;
            }
            = "";
            public string SteamCmdPath {
                get;
                set;
            }
            = "";
            public string UpdaterExecutablePath {
                get;
                set;
            }
            = "";
            public bool ShowUpdaterConsole {
                get;
                set;
            }
            = false;
            public string ServerStartScript {
                get;
                set;
            }
            = "";
            public bool RunServerScriptHidden {
                get;
                set;
            }
            = false;
            public string ServerTmuxSession {
                get;
                set;
            }
            = "";
            public bool RustOnService {
                get;
                set;
            }
            = false;
            public string ServiceName {
                get;
                set;
            }
            = "";
            public string ServiceType {
                get;
                set;
            }
            = "";
            public int HttpTimeoutMs {
                get;
                set;
            }
            = 3000;
            public bool StartupScan {
                get;
                set;
            }
            = false;
            public int MaxAttempts {
                get;
                set;
            }
            = 0;
            public int CheckIntervalMinutes {
                get;
                set;
            }
            = 10;
            public int CountdownMinutes {
                get;
                set;
            }
            = 5;
            public bool UpdatePlugins {
                get;
                set;
            }
            = false;
            public bool OnlyServerProtocolUpdate {
                get;
                set;
            }
            = false;
            public bool UseScheme {
                get;
                set;
            }
            = false;
            public string SchemeFile {
                get;
                set;
            }
            = "";
            public string DailyRestartTime {
                get;
                set;
            }
            = "";
            public int MinutesBeforeRestart {
                get;
                set;
            }
            = 60;
            public bool DiscordNotificationsEnabled {
                get;
                set;
            }
            = false;
            public string DiscordWebhookUrl {
                get;
                set;
            }
            = "";
            public int BeforeForceWipeRange {
                get;
                set;
            }
            = 15;
            public string CustomWipeDay {
                get;
                set;
            }
            = "";
            public string CustomWipeTime {
                get;
                set;
            }
            = "";
            public string ServerIdentity {
                get;
                set;
            }
            = "";
            public string NextWipeServerName {
                get;
                set;
            }
            = "";
            public string NextWipeServerDescription {
                get;
                set;
            }
            = "";
            public string NextWipeMapUrl {
                get;
                set;
            }
            = "";
            public string NextWipeLevel {
                get;
                set;
            }
            = "";
            public string NextWipeSeed {
                get;
                set;
            }
            = "";
            public bool NextWipeRandomSeed {
                get;
                set;
            }
            = false;
            public string NextWipeMapsize {
                get;
                set;
            }
            = "";
            public bool NextWipeKeepBps {
                get;
                set;
            }
            = true;
            public bool NextWipeResetRustPlus {
                get;
                set;
            }
            = false;
            public bool NextWipeDeletePlayerData {
                get;
                set;
            }
            = false;
            public string NextWipeDeletePluginDatafiles {
                get;
                set;
            }
            = "";
            public string UpdaterMarkerFileName {
                get;
                set;
            }
            = "updateresult.json";
            public string UpdaterLockFileName {
                get;
                set;
            }
            = "updating.lock";
            public string MarkersSubfolder {
                get;
                set;
            }
            = "markers";
        }
        private ConfigData configData;
        // Writes a default configuration file when none is present or on reset.
        protected override void LoadDefaultConfig() {
            Config.WriteObject(new ConfigData(), true);
        }
#endregion
#region Constants
        private const string DataFileName = "FeedMeUpdatesData";
#endregion
#region Runtime State
        private bool? lastOxideCompat = null;
        private string lastOxideProto = null;
        private string lastCompatNote = null;
        private bool debugging = false;
        private bool quitBeforeExecOnDebugging = true;
        private bool enablePlugin = true;
        private bool SystemUpdating = false;
        private Timer DailyRestartCheckRepeater;
        private bool pendingServerUpdate = false;
        private bool pendingOxideUpdate = false;
        private bool tempPendingServerUpdate = false;
        private bool tempPendingOxideUpdate = false;
        private string localOxideVersion = null;
        private string localProtocol = null;
        private string localBuildFromServerVersion = null;
        private string LocalServerVersionString = "";
        private string LocalSteamBuildID = "";
        private string cachedRemoteBuild = null;
        private string cachedRemoteOxide = null;
        private Timer checkTimer;
        private volatile bool isFetchingRemote = false;
        private volatile bool countdownActive = false;
        private string cachedPluginVersion = null;
        private int trynumber = 0;
        private bool tmuxAvailable = false;
        private bool gnomeAvailable = false;
        private bool DoRestartTimeCheck = false;
        private int targetHour = 0;
        private int targetMinute = 0;
        Scheme scheme = new Scheme();
        private bool lastNetworkFail = false;
        private int lastOxideStatusCode = 0;
        private const string UAValue = "FeedMeUpdates";
        private bool updaterValidAtInit = true;
        private int? minutesBeforeForceWipe = null;
        private Timer fwTimer;
        private bool nextUpdateIsForce = false;
        private int? minutesBeforeCustomWipe = null;
        private string pluginDatafilesToRemoveString = "";
#endregion
#region Resilient HTTP Wrapper
        private enum HttpBackend {
            Unknown, WebRequest, WebClient
        }
        private HttpBackend currentHttpBackend = HttpBackend.Unknown;
        private volatile bool httpBackendTested = false;
        private readonly object httpBackendLock = new object();
        // Detects and selects the most reliable HTTP backend available on this server.
        private void EnsureHttpBackendDetected() {
            if (httpBackendTested) return;
            lock (httpBackendLock) {
                if (httpBackendTested) return;
                httpBackendTested = true;
                try {
                    webrequest.Enqueue("https://api.github.com/", null, (code, response) => {
                        timer.Once(0f, () => {
                            currentHttpBackend = HttpBackend.WebRequest;
                        }
                        );
                    }
                    , this);
                    Task.Run(async () => {
                        await Task.Delay(1500).ConfigureAwait(false);
                        if (currentHttpBackend == HttpBackend.Unknown) currentHttpBackend = HttpBackend.WebRequest;
                    }
                    );
                }
                catch {
                    currentHttpBackend = HttpBackend.WebClient;
                }
            }
        }
        // Performs a resilient HTTP GET with timeout and fallback between backends.
        private (int statusCode, string body) ResilientHttpGetSync(string url, Dictionary<string, string> headers = null, int timeoutMs = 0) {
            EnsureHttpBackendDetected();
            int effectiveTimeout = timeoutMs > 0 ? timeoutMs : (configData?.HttpTimeoutMs > 0 ? configData.HttpTimeoutMs : 3000);
            try {
                var task = Task.Run(async () => {
                    if (currentHttpBackend == HttpBackend.WebRequest) {
                        try {
                            return await HttpGetViaWebRequestAsync(url).ConfigureAwait(false);
                        }
                        catch (Exception ex) {
                            Puts("WebRequest GET failed, fallback WebClient: " + ex.Message);
                            currentHttpBackend = HttpBackend.WebClient;
                            return await HttpGetViaWebClientAsync(url, headers).ConfigureAwait(false);
                        }
                    }
                    return await HttpGetViaWebClientAsync(url, headers).ConfigureAwait(false);
                }
                );
                if (task.Wait(effectiveTimeout)) return task.Result;
                return (-1, null);
            }
            catch (Exception ex) {
                Puts("ResilientHttpGetSync error: " + ex.Message);
                return (-1, null);
            }
        }
        // Performs a resilient HTTP POST with timeout and backend fallback.
        private (int statusCode, string body) ResilientHttpPostSync(string url, Dictionary<string, string> headers, string payload, int timeoutMs = 0) {
            EnsureHttpBackendDetected();
            int effectiveTimeout = timeoutMs > 0 ? timeoutMs : (configData?.HttpTimeoutMs > 0 ? configData.HttpTimeoutMs : 3000);
            try {
                var task = Task.Run(async () => {
                    if (currentHttpBackend == HttpBackend.WebRequest) {
                        try {
                            return await HttpPostViaWebRequestAsync(url, payload).ConfigureAwait(false);
                        }
                        catch (Exception ex) {
                            Puts("WebRequest POST failed, fallback WebClient: " + ex.Message);
                            currentHttpBackend = HttpBackend.WebClient;
                            return await HttpPostViaWebClientAsync(url, headers, payload).ConfigureAwait(false);
                        }
                    }
                    return await HttpPostViaWebClientAsync(url, headers, payload).ConfigureAwait(false);
                }
                );
                if (task.Wait(effectiveTimeout)) return task.Result;
                return (-1, null);
            }
            catch (Exception ex) {
                Puts("ResilientHttpPostSync error: " + ex.Message);
                return (-1, null);
            }
        }
        // Executes an async HTTP GET using Oxide's webrequest API and returns status/body.
        private Task<(int statusCode, string body)> HttpGetViaWebRequestAsync(string url) {
            var tcs = new TaskCompletionSource<(int, string)>();
            try {
                webrequest.Enqueue(url, null, (code, response) => {
                    timer.Once(0f, () => tcs.TrySetResult((code, response)));
                }
                , this);
            }
            catch (Exception ex) {
                tcs.TrySetException(ex);
            }
            return tcs.Task;
        }
        // Executes an async HTTP POST using Oxide's webrequest API and returns status/body.
        private Task<(int statusCode, string body)> HttpPostViaWebRequestAsync(string url, string payload) {
            var tcs = new TaskCompletionSource<(int, string)>();
            try {
                webrequest.Enqueue(url, payload ?? "", (code, response) => {
                    timer.Once(0f, () => tcs.TrySetResult((code, response)));
                }
                , this);
            }
            catch (Exception ex) {
                tcs.TrySetException(ex);
            }
            return tcs.Task;
        }
        // Executes an async HTTP GET using WebClient and returns status/body.
        private async Task<(int statusCode, string body)> HttpGetViaWebClientAsync(string url, Dictionary<string, string> headers = null) {
            try {
                using (var wc = new WebClient()) {
                    wc.Headers["User-Agent"] = UAValue;
                    if (headers != null) {
                        foreach (var kv in headers) {
                            if (kv.Key.Equals("User-Agent", StringComparison.OrdinalIgnoreCase)) continue;
                            try {
                                wc.Headers[kv.Key] = kv.Value;
                            }
                            catch {
                            }
                        }
                    }
                    var body = await wc.DownloadStringTaskAsync(url).ConfigureAwait(false);
                    return (200, body);
                }
            }
            catch (WebException wex) {
                int code = -1;
                string body = null;
                try {
                    if (wex.Response is HttpWebResponse resp) {
                        code = (int)resp.StatusCode;
                        using (var rs = resp.GetResponseStream()) using (var sr = new StreamReader(rs)) body = sr.ReadToEnd();
                    }
                }
                catch {
                }
                return (code, body);
            }
            catch (Exception ex) {
                return (-1, ex.Message);
            }
        }
        // Executes an async HTTP POST using WebClient and returns status/body.
        private async Task<(int statusCode, string body)> HttpPostViaWebClientAsync(string url, Dictionary<string, string> headers, string payload) {
            try {
                using (var wc = new WebClient()) {
                    wc.Headers["User-Agent"] = UAValue;
                    wc.Headers[HttpRequestHeader.ContentType] = "application/json";
                    if (headers != null) {
                        foreach (var kv in headers) {
                            if (kv.Key.Equals("User-Agent", StringComparison.OrdinalIgnoreCase)) continue;
                            if (kv.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) continue;
                            try {
                                wc.Headers[kv.Key] = kv.Value;
                            }
                            catch {
                            }
                        }
                    }
                    var body = await wc.UploadStringTaskAsync(url, "POST", payload ?? "").ConfigureAwait(false);
                    return (200, body);
                }
            }
            catch (WebException wex) {
                int code = -1;
                string body = null;
                try {
                    if (wex.Response is HttpWebResponse resp) {
                        code = (int)resp.StatusCode;
                        using (var rs = resp.GetResponseStream()) using (var sr = new StreamReader(rs)) body = sr.ReadToEnd();
                    }
                }
                catch {
                }
                return (code, body);
            }
            catch (Exception ex) {
                return (-1, ex.Message);
            }
        }
#endregion
#region Scheme Parsing
        private class Scheme {
            public bool isValid {
                set;
                get;
            }
            = false;
            public string SchemePath {
                get;
                set;
            }
            = "";
            public List<schemeInstruction> instructions {
                get;
                set;
            }
            = null;
        }
        public class schemeInstruction {
            public bool isValid {
                get;
                set;
            }
            = false;
            public int RowNum {
                get;
                set;
            }
            = 0;
            public int ErrorColumn {
                get;
                set;
            }
            = 0;
            public string ErrorText {
                get;
                set;
            }
            = "";
            public bool OxideUpdateEvent {
                get;
                set;
            }
            = false;
            public bool OxideProtocolMatchEvent {
                get;
                set;
            }
            = false;
            public bool OxideProtocolMismatchEvent {
                get;
                set;
            }
            = false;
            public bool OxideProtocolUnknownEvent {
                get;
                set;
            }
            = false;
            public bool OxideProtocolErrorEvent {
                get;
                set;
            }
            = false;
            public bool ServerEvent {
                get;
                set;
            }
            = false;
            public bool UpdateServer {
                get;
                set;
            }
            = false;
            public bool UpdateOxide {
                get;
                set;
            }
            = false;
        }
        // Parses a single scheme instruction row into a structured object with validation.
        public schemeInstruction ReadSchemeInstruction(int row, string rowContent) {
            var instr = new schemeInstruction();
            instr.RowNum = row;
            if (string.IsNullOrWhiteSpace(rowContent)) {
                instr.ErrorText = "Empty instruction";
                return instr;
            }
            try {
                var parts = rowContent.Split(new[] {
                    '='
                }
                , 2);
                if (parts.Length != 2) {
                    instr.ErrorText = "Invalid instruction format, expected 'events=action'";
                    return instr;
                }
                var left = parts[0].Trim();
                var right = parts[1].Trim();
                if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right)) {
                    instr.ErrorText = "Empty left or right side in instruction";
                    return instr;
                }
                if (right.Equals("oxide", StringComparison.OrdinalIgnoreCase)) {
                    instr.UpdateOxide = true;
                    instr.UpdateServer = false;
                }
                else if (right.Equals("server", StringComparison.OrdinalIgnoreCase)) {
                    instr.UpdateOxide = false;
                    instr.UpdateServer = true;
                }
                else if (right.Equals("both", StringComparison.OrdinalIgnoreCase)) {
                    instr.UpdateOxide = true;
                    instr.UpdateServer = true;
                }
                else {
                    instr.ErrorText = $"Action '{right}' is not valid (expected 'server', 'oxide' or 'both')";
                    return instr;
                }
                var leftParts = left.Split(new[] {
                    '+'
                }
                , StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
                if (leftParts.Length == 0) {
                    instr.ErrorText = "No events specified on left side";
                    return instr;
                }
                bool serverSet = false;
                bool oxideSet = false;
                foreach (var ev in leftParts) {
                    if (ev.IndexOf("server", StringComparison.OrdinalIgnoreCase) >= 0 && !serverSet) {
                        instr.ServerEvent = true;
                        serverSet = true;
                        continue;
                    }
                    if (ev.IndexOf("oxide", StringComparison.OrdinalIgnoreCase) >= 0 && !oxideSet) {
                        var m = Regex.Match(ev, @"oxide\s*\(([^)]*)\)", RegexOptions.IgnoreCase);
                        if (!m.Success) {
                            instr.OxideProtocolErrorEvent = true;
                            instr.OxideProtocolMatchEvent = true;
                            instr.OxideProtocolMismatchEvent = true;
                            instr.OxideProtocolUnknownEvent = true;
                            instr.OxideUpdateEvent = true;
                            oxideSet = true;
                        }
                        else {
                            var flags = m.Groups[1].Value;
                            if (string.IsNullOrEmpty(flags)) {
                                instr.ErrorText = "Oxide parentheses present but empty";
                                return instr;
                            }
                            foreach (char c in flags) {
                                if (c == 'e') {
                                    instr.OxideProtocolErrorEvent = true;
                                    instr.OxideUpdateEvent = true;
                                    oxideSet = true;
                                }
                                else if (c == 's') {
                                    instr.OxideProtocolMatchEvent = true;
                                    instr.OxideUpdateEvent = true;
                                    oxideSet = true;
                                }
                                else if (c == 'c') {
                                    instr.OxideProtocolMismatchEvent = true;
                                    instr.OxideUpdateEvent = true;
                                    oxideSet = true;
                                }
                                else if (c == 'u') {
                                    instr.OxideProtocolUnknownEvent = true;
                                    instr.OxideUpdateEvent = true;
                                    oxideSet = true;
                                }
                                else if (char.IsWhiteSpace(c)) {
                                }
                                else {
                                    instr.ErrorText = $"Unknown oxide protocol event flag: '{c}'";
                                    return instr;
                                }
                            }
                        }
                        continue;
                    }
                    instr.ErrorText = $"Unknown event specifier: '{ev}' (expected 'server' or 'oxide(...)')";
                    return instr;
                }
                if ((instr.ServerEvent || instr.OxideUpdateEvent) && string.IsNullOrEmpty(instr.ErrorText)) instr.isValid = true;
                else instr.ErrorText = "No valid events parsed";
            }
            catch (Exception ex) {
                instr.ErrorText = "Error parsing instruction: " + ex.Message;
            }
            return instr;
        }
        // Reads and validates the entire scheme file, enabling scheme-driven behavior if valid.
        public void ReadSchemeFile() {
            if (string.IsNullOrEmpty(configData.SchemeFile)) {
                Puts("Scheme file path not configured.");
                return;
            }
            if (!File.Exists(configData.SchemeFile)) {
                Puts("No scheme file found at: \"" + configData.SchemeFile + "\"");
                return;
            }
            string[] rows = File.ReadAllLines(configData.SchemeFile);
            int x = 1;
            scheme.SchemePath = configData.SchemeFile;
            scheme.instructions = new List<schemeInstruction>();
            scheme.isValid = true;
            foreach (string rawRow in rows) {
                var row = rawRow?.Trim() ?? "";
                if (string.IsNullOrEmpty(row) || row.StartsWith("//")) {
                    x++;
                    continue;
                }
                schemeInstruction sIn = ReadSchemeInstruction(x, row);
                if (!sIn.isValid) scheme.isValid = false;
                scheme.instructions.Add(sIn);
                x++;
            }
            if (scheme.isValid) Puts("Scheme correctly parsed. Now running on scheme behaviour.");
            else {
                Puts("Unable to parse scheme file. The following instructions are invalid:");
                foreach (schemeInstruction si in scheme.instructions) {
                    if (!si.isValid) Puts("(" + si.RowNum.ToString() + ") " + si.ErrorText);
                }
                Puts("UseScheme has been disabled. Plugin is going back to default behaviour.");
                configData.UseScheme = false;
            }
        }
#endregion
#region Discord Notifications
        // Escapes a string for safe embedding in JSON payloads.
        private string EscapeJsonString(string s) {
            if (s == null) return "";
            return s.Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\r", "") .Replace("\n", "\\n");
        }
        // Sends a message to Discord via webhook, handling network/timeout errors.
        private async Task SendDiscordNotification(string message) {
            if (configData?.DiscordNotificationsEnabled != true) return;
            if (string.IsNullOrEmpty(configData?.DiscordWebhookUrl)) return;
            try {
                var payload = "{\"content\":\"" + EscapeJsonString(message) + "\"}";
                var headers = new Dictionary<string, string> {
                    {
                        "Content-Type", "application/json"
                    }
                }
                ;
                var prevBackend = currentHttpBackend;
                currentHttpBackend = HttpBackend.WebClient;
                try {
                    var result = await Task.Run(() => ResilientHttpPostSync(configData.DiscordWebhookUrl, headers, payload, configData.HttpTimeoutMs)).ConfigureAwait(false);
                    timer.Once(0f, () => {
                        if (result.statusCode >= 200 && result.statusCode < 300) Puts("Discord notification sent successfully.");
                        else Puts($"Error sending Discord notification: HTTP {result.statusCode} - {result.body}");
                    }
                    );
                }
                finally {
                    currentHttpBackend = prevBackend;
                }
            }
            catch (Exception ex) {
                timer.Once(0f, () => Puts("Error sending Discord notification: " + ex.Message));
            }
        }
        // Composes and sends a Discord notification at the start of an update/wipe.
        private void NotifyDiscordUpdateStart(bool updateServer, bool updateOxide, string remoteOxideVersion, bool protocolChanged) {
            if (configData?.DiscordNotificationsEnabled != true) return;
            var updateType = (updateServer && updateOxide) ? "Server & Oxide" : (updateServer ? "Server" : (updateOxide ? "Oxide" : "No update"));
            var protocolMsg = updateOxide ? (protocolChanged ? "Protocol NUMBER CHANGE" : "No protocol change") : "";
            var oxideMsg = updateOxide ? $"Remote Oxide version: {remoteOxideVersion ?? "unknown"}" : "";
            var msg = "";
            if (updateType == "No update") {
                msg = $"[FeedMeUpdates] Starting custom wipe.";
            }
            else {
                if (nextUpdateIsForce) updateType += " (ForceWipe)";
                msg = $"[FeedMeUpdates] Starting update: {updateType}\n" + (string.IsNullOrEmpty(oxideMsg) ? "" : oxideMsg + "\n") + (string.IsNullOrEmpty(protocolMsg) ? "" : protocolMsg);
            }
            _ = SendDiscordNotification(msg);
        }
        // Composes and sends a Discord notification after an update/wipe ends.
        private void NotifyDiscordUpdateResult(string result, string updateId, bool pluginsUpdated, List<string> pluginsList, string failureReason, string wiped, string wipe_info) {
            if (configData?.DiscordNotificationsEnabled != true) return;
            var msg = "";
            if (updateId == "wipe") {
                msg = $"[FeedMeUpdates] wipe finished: {(string.IsNullOrEmpty(result) ? "UNKNOWN" : result.Trim().ToUpper())} (ID: {updateId ?? "unknown"})\n";
                if (!string.IsNullOrEmpty(result) && result.ToLower().Trim() == "success") {
                    msg += "Wipe executed SUCCESSFULLY.";
                }
                else {
                    msg += "Wipe FAILED.";
                    if (!string.IsNullOrEmpty(failureReason)) {
                        msg += $"\nFailure reason: {failureReason}";
                    }
                }
                if (wiped == "yes") {
                    msg += $"\nServer wiped.";
                }
                else {
                    msg += $"\nServer not wiped";
                }
                if (wipe_info != "") {
                    msg += $"\nWipe ALERTS reported:";
                    string[] wipewarnings = wipe_info.Split("]", StringSplitOptions.RemoveEmptyEntries);
                    foreach (string warning in wipewarnings) {
                        var w = warning.TrimStart('[').Trim();
                        msg += $"\n - {w}";
                    }
                }
            }
            else {
                msg = $"[FeedMeUpdates] Update finished: {(string.IsNullOrEmpty(result) ? "UNKNOWN" : result.Trim().ToUpper())} (ID: {updateId ?? "unknown"})\n";
                if (!string.IsNullOrEmpty(result) && result.ToLower().Trim() == "success") {
                    msg += "Update executed SUCCESSFULLY.";
                    if (pluginsUpdated && pluginsList != null && pluginsList.Count > 0) {
                        msg += $"\nUpdated plugins: {string.Join(", ", pluginsList)}";
                    }
                }
                else {
                    msg += "Update FAILED.";
                    if (!string.IsNullOrEmpty(failureReason)) {
                        msg += $"\nFailure reason: {failureReason}";
                    }
                }
                if(wiped == "yes") {
                    msg += $"\nServer wiped.";
                }
                else {
                    msg += $"\nServer not wiped";
                }
                if (wipe_info != "") {
                    msg += $"\nWipe ALERTS reported:";
                    string[] wipewarnings = wipe_info.Split("]", StringSplitOptions.RemoveEmptyEntries);
                    foreach (string warning in wipewarnings) {
                        var w = warning.TrimStart('[').Trim();
                        msg += $"\n - {w}";
                    }
                }
            }
            _ = SendDiscordNotification(msg);
        }
#endregion
#region Init / Unload
        // Initializes plugin state, reads config, registers commands and schedules tasks.
        private void Init() {
            permission.RegisterPermission("feedme.run", this);
            try {
                cachedPluginVersion = Version.ToString();
            }
            catch {
                cachedPluginVersion = "unknown";
            }
            AddCovalenceCommand("feedme.testrun", nameof(Cmd_TestRun));
            AddCovalenceCommand("feedme.status", nameof(Cmd_Status));
            AddCovalenceCommand("feedme.version", nameof(Cmd_Version));
            AddCovalenceCommand("feedme.set", nameof(Cmd_SetConfig));
            try {
                configData = Config.ReadObject<ConfigData>() ?? new ConfigData();
                if (configData.HttpTimeoutMs <= 0) configData.HttpTimeoutMs = 3000;
                if (string.IsNullOrEmpty(configData.ServerStartScript) && !configData.RustOnService) {
                    configData.ServerStartScript = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\rust-server\start_server.bat" : "/home/rust/rust-server/start_server.sh";
                    Config.WriteObject(configData, true);
                }
            }
            catch (Exception ex) {
                Puts("Error loading config: " + ex.Message);
                configData = new ConfigData();
            }
            try {
                string resolved, reason;
                if (!ValidateUpdaterExecutableAtInit(out resolved, out reason)) {
                    updaterValidAtInit = false;
                    Puts("Updater executable validation failed at init: " + (reason ?? "unknown reason"));
                }
                else {
                    updaterValidAtInit = true;
                    Puts("Updater executable validated at init: " + (resolved ?? "<unknown>"));
                }
            }
            catch (Exception ex) {
                updaterValidAtInit = false;
                Puts("Updater executable validation error at init: " + ex.Message);
            }
            Puts($"FeedMeUpdates initialized (v{cachedPluginVersion}).");
            try {
                ProcessSteamTempAndDataFile();
            }
            catch (Exception ex) {
                Puts("Error ProcessSteamTempAndDataFile: " + ex.Message);
            }
            try {
                ProcessTryNumber();
            }
            catch (Exception ex) {
                Puts("Error ProcessTryNumber: " + ex.Message);
            }
            try {
                ProcessUpdaterMarkerIfPresent();
            }
            catch (Exception ex) {
                Puts("Marker processing error: " + ex.Message);
            }
            try {
                ProcessUpdaterLockIfPresent();
            }
            catch (Exception ex) {
                Puts("Lock processing error: " + ex.Message);
            }
            AutoUpdateTask();
            pluginDatafilesToRemoveString = BuildFileListSingleArg(ParseFileListStrictJsonArray(configData.NextWipeDeletePluginDatafiles));
            if(!string.IsNullOrEmpty(configData.NextWipeDeletePluginDatafiles)) {
                if(string.IsNullOrEmpty(pluginDatafilesToRemoveString)) Puts("List of plugin datafiles to delete at wipe in invalid. Plugin datafile removal disabled");
            }
            else {
                configData.NextWipeDeletePluginDatafiles = "";
                pluginDatafilesToRemoveString = "";
            }
            int sizeValue = 0;
            if (!string.IsNullOrEmpty(configData.NextWipeMapsize)) {
                if (int.TryParse(configData.NextWipeMapsize, out sizeValue)) {
                    if (sizeValue == 0 || sizeValue < 1000 || sizeValue > 6000) {
                        configData.NextWipeMapsize = "";
                        Puts("Map size specified for the next wipe is out of range (accepted range 1000-6000). In case of wipe map size won't be changed.");
                    }
                }
                else {
                    configData.NextWipeMapsize = "";
                    Puts("Map size specified for the next is invalid. In case of wipe map size won't be changed");
                }
            }
            else {
                configData.NextWipeMapsize = "";
            }
            if (configData.NextWipeRandomSeed) {
                var rndui = new System.Random();
                var _bytes = new byte[4];
                rndui.NextBytes(_bytes);
                uint _seed = BitConverter.ToUInt32(_bytes, 0);
                configData.NextWipeSeed = _seed.ToString(CultureInfo.InvariantCulture);
            }
            else {
                if (!string.IsNullOrEmpty(configData.NextWipeSeed)) {
                    if (!TryParseSeedDecimal(configData.NextWipeSeed)) {
                        configData.NextWipeSeed = "";
                        Puts("Seed value specified for the next is invalid. In case of wipe the seed won't be changed");
                    }
                }
                else {
                    configData.NextWipeSeed = "";
                }
            }
            if (configData.MaxAttempts != 0 && trynumber >= configData.MaxAttempts) enablePlugin = false;
            else enablePlugin = true;
            try {
                try {
                    var rp = server.Protocol;
                    var rb = server.Version;
                    localProtocol = rp?.ToString();
                    localBuildFromServerVersion = rb?.ToString();
                }
                catch (Exception exServer) {
                    Puts("Error obtaining server.Protocol / server.Version: " + exServer.Message);
                }
                localOxideVersion = GetOxideVersionFromDll();
                LocalServerVersionString = localBuildFromServerVersion ?? "";
                Puts($"Local detected: protocol={(localProtocol ?? "null")} | server.Version={(localBuildFromServerVersion ?? "null")} | LocalSteamBuildID={(string.IsNullOrEmpty(LocalSteamBuildID) ? "<empty>" : LocalSteamBuildID)} | Oxide={(localOxideVersion ?? "unknown")}");
            }
            catch (Exception ex) {
                Puts("Error reading local values: " + ex.Message);
                return;
            }
            if (configData.UseScheme) {
                try {
                    ReadSchemeFile();
                }
                catch (Exception ex) {
                    Puts("Scheme reading error: " + ex.Message);
                    configData.UseScheme = false;
                    Puts("UseScheme has been disabled. Plugin is going back to default behaviour.");
                }
            }
            bool[] availableUnixStuff = DetectGnomeAndTmux();
            gnomeAvailable = availableUnixStuff[0];
            tmuxAvailable = availableUnixStuff[1];
            if (!string.IsNullOrEmpty(configData.DailyRestartTime) && configData.MinutesBeforeRestart > 0) {
                if (ValidateRestartTime()) DoRestartTimeCheck = true;
                if (DoRestartTimeCheck) {
                    int secondsToNextMinute = 60 - DateTime.Now.Second;
                    timer.Once(secondsToNextMinute, () => {
                        DailyRestartCheckerMethod();
                        DailyRestartCheckRepeater = timer.Every(60f, () => DailyRestartCheckerMethod());
                    }
                    );
                }
            }
            fwTimer = timer.Every(60f, () => WipeTimeCheck());
        }
        // Cleans up repeating timers and scheduled tasks when the plugin unloads.
        void Unload() {
            if (checkTimer != null) {
                checkTimer?.Destroy();
                checkTimer = null;
            }
            if (DailyRestartCheckRepeater != null) {
                DailyRestartCheckRepeater?.Destroy();
                DailyRestartCheckRepeater = null;
            }
            if (fwTimer != null) {
                fwTimer?.Destroy();
                fwTimer = null;
            }
        }
#endregion
#region Datafile Management
        // Ensures the try number is present in the datafile or initializes it.
        private void ProcessTryNumber() {
            if (!ReadTryNumber()) {
                if (!WriteTryNumber()) {
                    Puts("Unable to define try number in datafile.");
                }
            }
        }
        // Reads the try number value from the datafile.
        private bool ReadTryNumber() {
            try {
                var dict = ReadDataFileDict();
                if (dict == null) {
                    return false;
                }
                if (dict.ContainsKey("TryNumber") && !string.IsNullOrEmpty(dict["TryNumber"])) {
                    trynumber = Int32.Parse(dict["TryNumber"]);
                    return true;
                }
            }
            catch (Exception ex) {
                Puts("Error reading datafile: " + ex.Message);
            }
            return false;
        }
        // Writes the current try number back to the datafile.
        private bool WriteTryNumber() {
            try {
                var dict = ReadDataFileDict() ?? new Dictionary<string, string>();
                dict["TryNumber"] = trynumber.ToString();
                WriteDataFileDict(dict);
            }
            catch (Exception ex) {
                Puts("Unexpected error when saving trynumber to datafile: " + ex.Message);
                return false;
            }
            return true;
        }
        // Handles steambid.temp detection, promotion to datafile, and cleanup.
        private void ProcessSteamTempAndDataFile() {
            if (string.IsNullOrEmpty(configData.ServerDirectory)) {
                Puts("ServerDirectory not configured; skipping steambid.temp handling.");
                return;
            }
            var tempPath = Path.Combine(configData.ServerDirectory, "steambid.temp");
            if (File.Exists(tempPath)) {
                try {
                    var id = File.ReadAllText(tempPath).Trim();
                    if (!string.IsNullOrEmpty(id)) {
                        LocalSteamBuildID = id;
                        SaveLocalSteamToDataFile();
                        Puts($"steambid.temp found: LocalSteamBuildID set to '{LocalSteamBuildID}', saved in datafile.");
                    }
                    else {
                        Puts("steambid.temp present but empty.");
                    }
                    try {
                        File.Delete(tempPath);
                        Puts("steambid.temp deleted after reading.");
                    }
                    catch (Exception exDel) {
                        Puts("Unable to delete steambid.temp: " + exDel.Message);
                    }
                    return;
                }
                catch (Exception ex) {
                    Puts("Error reading steambid.temp: " + ex.Message);
                }
            }
            var loaded = LoadLocalSteamFromDataFile();
            if (loaded) Puts($"LocalSteamBuildID loaded from datafile: {(string.IsNullOrEmpty(LocalSteamBuildID) ? "<empty>" : LocalSteamBuildID)}");
        }
        // Loads LocalSteamBuildID from the persisted datafile storage.
        private bool LoadLocalSteamFromDataFile() {
            try {
                var dict = ReadDataFileDict();
                if (dict == null) return false;
                if (dict.ContainsKey("LocalSteamBuildID") && !string.IsNullOrEmpty(dict["LocalSteamBuildID"])) {
                    LocalSteamBuildID = dict["LocalSteamBuildID"];
                    return true;
                }
            }
            catch (Exception ex) {
                Puts("Error reading datafile: " + ex.Message);
            }
            return false;
        }
        // Saves LocalSteamBuildID to the datafile storage for later use.
        private void SaveLocalSteamToDataFile() {
            try {
                var dict = ReadDataFileDict() ?? new Dictionary<string, string>();
                dict["LocalSteamBuildID"] = LocalSteamBuildID ?? "";
                WriteDataFileDict(dict);
            }
            catch (Exception ex) {
                Puts("Error writing datafile (LocalSteamBuildID): " + ex.Message);
            }
        }
        // Persists a remote build ID to the datafile (to promote after success).
        private void SaveRemoteSteamToDataFile(string remoteBuild) {
            try {
                var dict = ReadDataFileDict() ?? new Dictionary<string, string>();
                dict["RemoteSteamBuildID"] = remoteBuild ?? "";
                WriteDataFileDict(dict);
                Puts($"RemoteSteamBuildID '{remoteBuild}' saved in datafile.");
            }
            catch (Exception ex) {
                Puts("Error writing datafile (RemoteSteamBuildID): " + ex.Message);
            }
        }
        // Removes the remote build ID entry from the datafile after promotion.
        private void RemoveRemoteSteamFromDataFile() {
            try {
                var dict = ReadDataFileDict();
                if (dict == null) return;
                if (dict.ContainsKey("RemoteSteamBuildID")) {
                    dict.Remove("RemoteSteamBuildID");
                    WriteDataFileDict(dict);
                    Puts("RemoteSteamBuildID removed from datafile.");
                }
            }
            catch (Exception ex) {
                Puts("Error removing RemoteSteamBuildID: " + ex.Message);
            }
        }
        // Reads the plugin's dictionary-shaped datafile into memory.
        private Dictionary<string, string> ReadDataFileDict() {
            try {
                var dict = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, string>>(DataFileName);
                return dict ?? new Dictionary<string, string>();
            }
            catch (Exception ex) {
                Puts("Generic datafile read error: " + ex.Message);
                return new Dictionary<string, string>();
            }
        }
        // Writes a dictionary back into the plugin's datafile storage.
        private void WriteDataFileDict(Dictionary<string, string> dict) {
            try {
                Interface.Oxide.DataFileSystem.WriteObject(DataFileName, dict);
            }
            catch (Exception ex) {
                Puts("Generic datafile write error: " + ex.Message);
            }
        }
#endregion
#region Marker & Lock Handling
        // Processes updater result marker file, promotes build IDs, archives marker, and notifies.
        private void ProcessUpdaterMarkerIfPresent() {
            try {
                if (string.IsNullOrEmpty(configData.ServerDirec
// ... [Code preview truncated due to length. Full source in attached file] ...

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