C#:
#region
using System;
using System.IO;
using Oxide.Core;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using Newtonsoft.Json;
using Oxide.Core.Plugins;
using System.Collections;
using Oxide.Game.Rust.Cui;
using System.Globalization;
using JetBrains.Annotations;
using UnityEngine.Networking;
using System.Collections.Generic;
using System.Text.RegularExpressions;
#endregion
namespace Oxide.Plugins;
[Info("UpdateChecker", "tofurahie", "4.6.1")]
[Description("Update checker for all of your plugins")]
internal class UpdateChecker : RustPlugin
{
#region GUI
private class UI
{
private const string Layer = "UI_UpdateChecker";
public static void MainParent(ref CuiElementContainer container, string name = null, string aMin = "0.5 0.5", string aMax = "0.5 0.5",
bool overAll = true, bool keyboardEnabled = true, bool cursorEnabled = true) => container.Add(new CuiPanel
{
KeyboardEnabled = keyboardEnabled,
CursorEnabled = cursorEnabled,
RectTransform = { AnchorMin = aMin, AnchorMax = aMax },
Image = { Color = "0 0 0 0" }
}, overAll ? "Overlay" : "Hud", Layer + ".bg" + name, Layer + ".bg" + name);
public static void Panel(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string bgColor = "0.33 0.33 0.33 1", string material = null,
string sprite = null, int itemID = 0, ulong skinID = 0) => container.Add(new CuiElement
{
Parent = Layer + parent,
Name = name != null && name.Contains(".other.") ? name.Replace(".other.", "") : Layer + name,
DestroyUi = destroy == null ? null : Layer + destroy,
Components =
{
new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
new CuiImageComponent { Color = HexToRustFormat(bgColor), Material = material, Sprite = sprite, ItemId = itemID, SkinId = skinID },
},
});
public static void Icon(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", int itemID = 0, ulong skinID = 0) => container.Add(new CuiElement
{
Parent = Layer + parent,
Name = Layer + name,
DestroyUi = destroy == null ? null : Layer + destroy,
Components =
{
new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
new CuiImageComponent { ItemId = itemID, SkinId = skinID },
},
});
public static void Image(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string image = "", string color = "1 1 1 1") => container.Add(new CuiElement
{
Parent = Layer + parent,
Name = Layer + name,
DestroyUi = destroy == null ? null : Layer + destroy,
Components =
{
new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
new CuiRawImageComponent
{
Png = !image.StartsWith("http") && !image.StartsWith("www") ? image : null,
Url = image.StartsWith("http") || image.StartsWith("www") ? image : null, Color = HexToRustFormat(color),
Sprite = "assets/content/textures/generic/fulltransparent.tga"
},
},
});
public static void Label(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string text = null, int fontSize = 16, string color = "1 1 1 1",
TextAnchor align = TextAnchor.MiddleCenter, string outlineDistance = null, string outlineColor = "0 0 0 1",
VerticalWrapMode wrapMode = VerticalWrapMode.Truncate, string font = "robotocondensed-regular.ttf") => container.Add(new CuiElement
{
Parent = Layer + parent,
Name = Layer + name,
DestroyUi = destroy == null ? null : Layer + destroy,
Components =
{
new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
new CuiTextComponent
{ Text = text, FontSize = fontSize, Color = HexToRustFormat(color), Align = align, Font = font, VerticalOverflow = wrapMode },
outlineDistance == null
? new CuiOutlineComponent { Distance = "0 0", Color = "0 0 0 0" }
: new CuiOutlineComponent
{ Distance = outlineDistance, Color = HexToRustFormat(outlineColor) },
},
});
public static void Button(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string text = null, int fontSize = 16, string color = "1 1 1 1",
string command = null, string bgColor = "0 0 0 0", VerticalWrapMode wrapMode = VerticalWrapMode.Truncate,
TextAnchor align = TextAnchor.MiddleCenter, string font = "robotocondensed-regular.ttf", string material = null, string sprite = null) =>
container.Add(new CuiButton
{
RectTransform = { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
Text = { Text = text, FontSize = fontSize, Color = HexToRustFormat(color), Align = align, Font = font, VerticalOverflow = wrapMode },
Button =
{
Command = command, Close = command == null ? Layer + name : null, Color = HexToRustFormat(bgColor), Material = material, Sprite = sprite
}
}, Layer + parent, command == null ? null : Layer + name, destroy == null ? null : Layer + destroy);
public static void Input(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string text = null, int limit = 40, int fontSize = 16, string color = "1 1 1 1",
string command = null, TextAnchor align = TextAnchor.MiddleCenter, bool autoFocus = false, bool hudMenuInput = false, bool readOnly = false,
bool isPassword = false, bool needsKeyboard = false, bool singleLine = true, string font = "robotocondensed-regular.ttf") => container.Add(
new CuiElement
{
Parent = Layer + parent,
Name = Layer + name,
DestroyUi = destroy == null ? null : Layer + destroy,
Components =
{
new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
new CuiInputFieldComponent
{
Text = text, Command = command, CharsLimit = limit, FontSize = fontSize, Color = HexToRustFormat(color), Align = align, Font = font,
Autofocus = autoFocus, IsPassword = isPassword, ReadOnly = readOnly, HudMenuInput = hudMenuInput, NeedsKeyboard = needsKeyboard,
LineType = singleLine ? InputField.LineType.SingleLine : InputField.LineType.MultiLineNewline
},
}
});
public static void Outline(ref CuiElementContainer container, string layer, string size = "1 1 1 1", string color = "0 0 0 1",
bool external = false)
{
var borders = size.Split(' ');
if (borders[0] != "0")
Panel(ref container, layer, aMin: "0 1", aMax: "1 1", oMin: $"-{borders[0]} {(external ? "0" : "-" + borders[0])}",
oMax: $"{borders[0]} {(external ? borders[0] : "0")}", bgColor: color);
if (borders[1] != "0")
Panel(ref container, layer, aMin: "1 0", aMax: "1 1", oMin: $"{(external ? "0" : "-" + borders[1])} -{borders[1]}",
oMax: $"{(external ? borders[1] : "0")} {borders[1]}", bgColor: color);
if (borders[2] != "0")
Panel(ref container, layer, aMin: "0 0", aMax: "1 0", oMin: $"-{borders[2]} {(external ? "-" + borders[2] : "0")}",
oMax: $"{borders[2]} {(external ? "0" : borders[2])}", bgColor: color);
if (borders[3] != "0")
Panel(ref container, layer, aMin: "0 0", aMax: "0 1", oMin: $"{(external ? "-" + borders[3] : "0")} -{borders[3]}",
oMax: $"{(external ? "0" : borders[3])} {borders[3]}", bgColor: color);
}
public static string HexToRustFormat(string hex)
{
if (string.IsNullOrEmpty(hex))
return hex;
Color color;
if (hex.Contains(":"))
return ColorUtility.TryParseHtmlString(hex.Substring(0, hex.IndexOf(":", StringComparison.Ordinal)), out color)
? $"{color.r:F2} {color.g:F2} {color.b:F2} {hex.Substring(hex.IndexOf(":", StringComparison.Ordinal) + 1, hex.Length - hex.IndexOf(":", StringComparison.Ordinal) - 1)}"
: hex;
return ColorUtility.TryParseHtmlString(hex, out color) ? $"{color.r:F2} {color.g:F2} {color.b:F2} {color.a:F2}" : hex;
}
public static void Create(BasePlayer player, CuiElementContainer container)
{
CuiHelper.AddUi(player, container);
}
public static void CreateToAll(CuiElementContainer container, string layer)
{
foreach (var player in BasePlayer.activePlayerList)
CuiHelper.AddUi(player, container);
}
public static void Destroy(BasePlayer player, string layer) => CuiHelper.DestroyUi(player, Layer + layer);
public static void DestroyToAll(string layer)
{
foreach (var player in BasePlayer.activePlayerList)
Destroy(player, layer);
}
}
#endregion
#region Static
private const int PLUGIN_SCAN_BATCH_SIZE = 20;
private const string PERM = "updatechecker.setup";
private static readonly Regex PluginInfoRegex = new(@"Info\(\""(.*?)\"",\""(.*?)\"",\""(.*?)\""", RegexOptions.Compiled);
private static readonly Regex CamelBoundaryRegex = new(@"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", RegexOptions.Compiled);
private static readonly Regex LeadingFileIdRegex = new(@"^\d+-", RegexOptions.Compiled);
private IEnumerator _checkNewVersionCoroutine, _sendDiscordMessageCoroutine, _updatePluginListCoroutine;
private bool _isChecking;
private readonly HashSet<string> _configIgnoreList = new()
{
"RustEdit"
};
#region Classes
[Serializable]
private class Configuration
{
[JsonProperty("Command to open UI")]
public string Command = "ucsetup";
[JsonProperty("Command to send the test message to your discord")]
public string CommandDiscord = "uctest";
[JsonProperty("Console command to check orphaned config files")]
public string CommandCheckOrphanedConfigFiles = "uc_check_config";
[JsonProperty("Console command to move orphaned config files")]
public string CommandDeleteOrphanedConfigFiles = "uc_clean_config";
[JsonProperty("Discord WebHook")]
public string DiscordWebHook = "";
[JsonProperty("Discord message ID")]
public string MessageID = "";
[JsonProperty("Check updates on load [disable it if you have problem with the config]")]
public bool CheckUpdateOnLoad = true;
[JsonProperty("Embed side line color [hex]")]
public string EmbedLineColor = "#ffffff";
[JsonProperty("Difference between UTC and your time [in minutes]")]
public int UTC = 60;
[JsonProperty("Use 24 time format")]
public bool TimeFormat = true;
[JsonProperty("Check Interval(In minutes)")]
public int CheckMinutes = 60;
[JsonProperty("Ignore 'All plugins have the latest version' discord message")]
public bool IgnoreAllPluginsUpToDateMessage = false;
[JsonProperty("Ignore not found plugins")]
public bool IgnoreNotFound = false;
[JsonProperty("Ignore not loaded plugins")]
public bool IgnoreNotLoaded = true;
[JsonProperty("Add a link to the plugin to be updated")]
public bool UseURL = true;
[JsonProperty("Remove missing plugins from config automatically")]
public bool AutoRemoveMissingPlugins = true;
[JsonProperty("List of plugins", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<PluginInfo> ListOfPlugins = new();
[JsonProperty("Enable auto search")]
public Dictionary<string, bool> AutoSearch = new()
{
["uMod"] = true,
["Codefling"] = true,
["Lone.Design"] = true,
["Chaos"] = true,
["RustWorkshop"] = true,
["Github"] = true,
["ModPulse"] = true,
["RustPlugins"] = true,
["ServerArmour"] = true,
["ImperialPlugins"] = true,
["MyVector"] = true,
["SkyPlugins"] = true,
["Game4Freak"] = true,
};
[JsonProperty("Links to version lists [json: plugin name -> Major, Minor, Patch, Author, Url]", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<string> VersionLists = new()
{
"https://www.robjmaps.com/downloads/Plugin_Versions.json"
};
}
[Serializable]
private class PluginInfo
{
[JsonIgnore]
public bool IsFounded;
[JsonIgnore]
public bool IsLoaded;
[JsonIgnore]
public string SearchUrl;
[JsonIgnore]
public string Title;
[JsonProperty("Name")]
public string Name;
[JsonProperty("Author")]
public string Author;
[JsonProperty("Plugin version")]
public string Version;
[JsonProperty("Link to plugin")]
public string Url;
[JsonProperty("Marketplace")]
public string Marketplace;
[JsonProperty("Ignore")]
public bool Ignore;
public PluginInfo(string author, string name, string title, string version, string url = "", string marketplace = "", bool ignore = false, bool isLoaded = false)
{
Name = name;
Author = author;
Title = title;
Version = version;
Url = url;
Marketplace = marketplace;
Ignore = ignore;
IsLoaded = isLoaded;
}
}
[Serializable]
private class RequestData
{
[JsonProperty("status")]
public int Status { get; set; }
[JsonProperty("data")]
public PluginData[] Data { get; set; }
}
[Serializable]
private class PluginData
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("manualName")]
public string ManualName { get; set; }
[JsonProperty("author")]
public string Author { get; set; }
[JsonProperty("latestVersion")]
public string LatestVersion { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("slug")]
public string Slug { get; set; }
[JsonProperty("marketplace")]
public string Marketplace { get; set; }
[JsonProperty("tags")]
public string Tags { get; set; }
}
private class MarketplaceResponse
{
public PluginData[] Data;
public bool RateLimited;
public bool Failed;
}
[Serializable]
private class ListedVersion
{
public int Major { get; set; }
public int Minor { get; set; }
public int Patch { get; set; }
public string Author { get; set; }
public string Url { get; set; }
public string Marketplace { get; set; }
public string Version => $"{Major}.{Minor}.{Patch}";
public bool IsWrittenBy(string author)
{
return string.IsNullOrWhiteSpace(Author) || string.Equals(Author.Trim(), author?.Trim(), StringComparison.OrdinalIgnoreCase);
}
}
[Serializable]
private class AboutPlugin
{
public string Name;
public string Title;
public string Author;
public string Version;
public bool IsLoaded;
public AboutPlugin(string name, string title, string author, string version, bool isLoaded)
{
Name = name;
Title = title;
Author = author;
Version = version;
IsLoaded = isLoaded;
}
}
#endregion
#endregion
#region OxideHooks
[UsedImplicitly]
[HookMethod(nameof(OnServerInitialized))]
private void OnServerInitialized()
{
cmd.AddChatCommand(_config.Command, this, nameof(ChatSetupCommand));
cmd.AddChatCommand(_config.CommandDiscord, this, nameof(DiscordTestCommand));
cmd.AddConsoleCommand(_config.CommandCheckOrphanedConfigFiles, this, nameof(console_uc_check_config));
cmd.AddConsoleCommand(_config.CommandDeleteOrphanedConfigFiles, this, nameof(console_uc_clean_config));
permission.RegisterPermission(PERM, this);
if (_config.CheckUpdateOnLoad)
CheckPlugins();
timer.Every(60 * Math.Max(_config.CheckMinutes, 1), CheckPlugins);
}
[UsedImplicitly]
[HookMethod(nameof(Unload))]
private void Unload()
{
UI.DestroyToAll(".bg");
if (_updatePluginListCoroutine != null)
ServerMgr.Instance.StopCoroutine(_updatePluginListCoroutine);
if (_checkNewVersionCoroutine != null)
ServerMgr.Instance.StopCoroutine(_checkNewVersionCoroutine);
if (_sendDiscordMessageCoroutine != null)
ServerMgr.Instance.StopCoroutine(_sendDiscordMessageCoroutine);
}
#endregion
#region Commands
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
private void console_uc_check_config(ConsoleSystem.Arg arg)
{
if (arg.HasArgs() || arg.Player() != null)
return;
UpdatePluginList();
var configsNames = "";
foreach (var check in Directory.GetFiles(Interface.Oxide.ConfigDirectory, "*.json").Select(Path.GetFileNameWithoutExtension))
if (_config.ListOfPlugins.All(x => !string.Equals(x.Name, check, StringComparison.CurrentCultureIgnoreCase)) && !_configIgnoreList.Contains(check))
configsNames += check + ", ";
if (configsNames == string.Empty)
{
PrintWarning($"All config files are in use");
return;
}
PrintWarning(configsNames.TrimEnd().TrimEnd(',') +
$"\nYou can move orphaned config files to UpdateCheckerConfigsBackup via command \"{_config.CommandDeleteOrphanedConfigFiles}\"");
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
private void console_uc_clean_config(ConsoleSystem.Arg arg)
{
if (arg.HasArgs() || arg.Player() != null)
return;
UpdatePluginList();
Directory.CreateDirectory(Interface.Oxide.ConfigDirectory + "/UpdateCheckerConfigsBackup");
var count = 0;
foreach (var check in Directory.GetFiles(Interface.Oxide.ConfigDirectory, "*.json"))
if (_config.ListOfPlugins.All(x => !string.Equals(x.Name, Path.GetFileNameWithoutExtension(check), StringComparison.CurrentCultureIgnoreCase)) &&
!_configIgnoreList.Contains(Path.GetFileNameWithoutExtension(check)))
{
File.Move(check, Interface.Oxide.ConfigDirectory + $"/UpdateCheckerConfigsBackup/{Path.GetFileNameWithoutExtension(check)}.json");
count++;
}
if (count == 0)
{
PrintWarning("All config files are in use");
return;
}
PrintError($"{count} config files ${(count > 1 ? "were" : "was")} moved to {Interface.Oxide.ConfigDirectory + $"/UpdateCheckerConfigsBackup"}");
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
private void ChatSetupCommand(BasePlayer player, string command, string[] args)
{
if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PERM))
{
SendReply(player, "You don't have permissions to use this command");
return;
}
ShowUIBG(player);
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
private void DiscordTestCommand(BasePlayer player, string command, string[] args)
{
if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PERM))
{
SendReply(player, "You don't have permissions to use this command");
return;
}
var updateList = _config.AutoSearch.ToDictionary(static check => check.Key,
static _ => (new List<string>(), new List<string> { "Your discord webhook is working" }));
_sendDiscordMessageCoroutine = SendMessageDiscord(updateList);
ServerMgr.Instance.StartCoroutine(_sendDiscordMessageCoroutine);
}
[ConsoleCommand("checkupdates")]
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
private void ConsolecheckupdatesCommand(ConsoleSystem.Arg arg)
{
if (arg == null || arg.Player() != null)
return;
if (_isChecking)
{
PrintWarning("The plugin already scans other plugins for updates");
return;
}
CheckPlugins();
}
[ConsoleCommand("UI_UC")]
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
private void UI_UC(ConsoleSystem.Arg arg)
{
if (!arg.HasArgs())
return;
var player = arg.Player();
if (player == null || (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PERM)))
return;
switch (arg.GetString(0))
{
case "SEARCH":
ShowUIPlugins(player, string.Join(" ", arg.Args.Skip(1)).Replace(" ", ""));
break;
case "PAGE":
ShowUIPlugins(player, string.Join(" ", arg.Args.Skip(2)), arg.GetInt(1));
break;
case "CHECK":
ShowUICurrentPluginInfo(player,
_config.ListOfPlugins.FirstOrDefault(x =>
string.Equals(x.Name, string.Join(" ", arg.Args.Skip(1)), StringComparison.CurrentCultureIgnoreCase)));
break;
case "CHANGEURL":
ChangePluginUrl(arg.GetString(1), arg.GetString(2));
break;
case "CHANGEIGNORE":
{
var pluginInfo = _config.ListOfPlugins.FirstOrDefault(x => string.Equals(x.Name, arg.GetString(1)));
if (pluginInfo == null)
break;
pluginInfo.Ignore = !pluginInfo.Ignore;
ShowUICurrentPluginInfo(player, pluginInfo);
SaveConfigKeepingManualEdits(pluginInfo.Name);
break;
}
}
}
#endregion
#region Functions
private void ChangePluginUrl(string name, string url)
{
var pluginInfo = _config.ListOfPlugins.FirstOrDefault(x => string.Equals(x.Name, name));
if (pluginInfo == null || string.Equals(pluginInfo.Url, url, StringComparison.Ordinal))
return;
pluginInfo.Url = url;
SaveConfigKeepingManualEdits(pluginInfo.Name);
}
private IEnumerator SendMessageDiscord(Dictionary<string, (List<string>, List<string>)> infoList)
{
if (string.IsNullOrEmpty(_config.DiscordWebHook))
{
_isChecking = false;
yield break;
}
var updateListBuilder = new StringBuilder();
foreach (var checks in infoList)
foreach (var check in checks.Value.Item2)
updateListBuilder.Append(check);
var updateList = updateListBuilder.ToString();
var isAllUpToDate = updateList.Contains("All plugins are up to date");
var message = new
{
content = "",
embeds = new[]
{
new
{
title = "",
description = updateList,
color = isAllUpToDate
? 65280
: int.Parse(_config.EmbedLineColor.Substring(1), NumberStyles.HexNumber),
}
}
};
var spaceBuilder = new StringBuilder();
if (updateList.Length < 4000)
{
using var request = string.IsNullOrEmpty(_config.MessageID)
? new UnityWebRequest(_config.DiscordWebHook, "POST")
: new UnityWebRequest($"{_config.DiscordWebHook}/messages/{_config.MessageID}", "PATCH");
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)));
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
PrintError(request.responseCode == 404
? "Failed to find the message with the specified ID in the config, please check it and try again."
: "The discord hook is borken, please check it and try again.");
_isChecking = false;
yield break;
}
foreach (var check in infoList)
foreach (var url in check.Value.Item2)
{
spaceBuilder.Append(url);
if (spaceBuilder.Length + url.Length + "\nThe list of updates has reached its limit, please update your plugins.".Length <= 4000)
continue;
message = new
{
content = "",
embeds = new[]
{
new
{
title = "",
description = spaceBuilder.ToString(),
color = int.Parse(_config.EmbedLineColor.Substring(1), NumberStyles.HexNumber),
},
new
{
title = "",
description = "The list of updates has reached its limit, please update your plugins.",
color = 16711680,
}
}
};
using var request = string.IsNullOrEmpty(_config.MessageID)
? new UnityWebRequest(_config.DiscordWebHook, "POST")
: new UnityWebRequest($"{_config.DiscordWebHook}/messages/{_config.MessageID}", "PATCH");
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)));
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
PrintError(request.responseCode == 404
? "Failed to find the message with the specified ID in the config, please check it and try again."
: "The discord hook is borken, please check it and try again.");
yield break;
}
_isChecking = false;
}
private void CheckPlugins()
{
_isChecking = true;
ReloadConfigFromDisk();
_updatePluginListCoroutine = CheckPluginsRoutine();
ServerMgr.Instance.StartCoroutine(_updatePluginListCoroutine);
}
private IEnumerator CheckPluginsRoutine()
{
var allPlugins = new List<AboutPlugin>();
var scanRoutine = GetAllPluginsRoutine(allPlugins);
while (scanRoutine.MoveNext())
yield return scanRoutine.Current;
UpdatePluginList(allPlugins);
SaveConfigKeepingManualEdits();
_checkNewVersionCoroutine = CheckNewVersion();
ServerMgr.Instance.StartCoroutine(_checkNewVersionCoroutine);
}
private List<AboutPlugin> GetAllPlugins()
{
var allFoundedPlugins = new List<AboutPlugin>();
var scanRoutine = GetAllPluginsRoutine(allFoundedPlugins);
while (scanRoutine.MoveNext())
{
}
return allFoundedPlugins;
}
private IEnumerator GetAllPluginsRoutine(List<AboutPlugin> allFoundedPlugins)
{
foreach (var check in plugins.GetAll().Where(static x => !x.IsCorePlugin))
allFoundedPlugins.Add(new AboutPlugin(check.Name, check.Title, check.Author, check.Version.ToString(), check.IsLoaded));
var foundedNames = new HashSet<string>(allFoundedPlugins.Select(static x => x.Name), StringComparer.OrdinalIgnoreCase);
var filesInBatch = 0;
foreach (var fileSystemInfo in new DirectoryInfo(Interface.Oxide.PluginDirectory).GetFiles("*" + "cs")
.Where(static f => (f.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden))
{
if (!foundedNames.Contains(Path.GetFileNameWithoutExtension(fileSystemInfo.Name)) && TryParseAboutPlugin(fileSystemInfo, out var aboutPlugin))
allFoundedPlugins.Add(aboutPlugin);
filesInBatch++;
if (filesInBatch < PLUGIN_SCAN_BATCH_SIZE)
continue;
filesInBatch = 0;
yield return null;
}
}
private static bool TryParseAboutPlugin(FileInfo fileSystemInfo, out AboutPlugin aboutPlugin)
{
aboutPlugin = null;
var regexInfo = PluginInfoRegex.Match(File.ReadAllText(fileSystemInfo.FullName).Replace(" ", ""));
if (!regexInfo.Success)
return false;
aboutPlugin = new AboutPlugin(Path.GetFileNameWithoutExtension(fileSystemInfo.Name), regexInfo.Groups[1].Value, regexInfo.Groups[2].Value,
regexInfo.Groups[3].Value, false);
return true;
}
private void UpdatePluginList()
{
UpdatePluginList(GetAllPlugins());
}
private void UpdatePluginList(List<AboutPlugin> allPlugins)
{
var listChanged = false;
if (_config.AutoRemoveMissingPlugins)
{
var foundedNames = new HashSet<string>(allPlugins.Select(static x => x.Name), StringComparer.OrdinalIgnoreCase);
var removedNames = new List<string>();
for (var index = _config.ListOfPlugins.Count - 1; index >= 0; index--)
{
var pluginInfo = _config.ListOfPlugins[index];
if (foundedNames.Contains(pluginInfo.Name) || File.Exists(Path.Combine(Interface.Oxide.PluginDirectory, pluginInfo.Name + ".cs")))
continue;
removedNames.Add(pluginInfo.Name);
_config.ListOfPlugins.RemoveAt(index);
}
if (removedNames.Count != 0)
{
PrintWarning($"Removed from the list, no such plugin on the server: {string.Join(", ", removedNames)}");
listChanged = true;
}
}
var pluginsByName = new Dictionary<string, PluginInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var pluginInfo in _config.ListOfPlugins)
pluginsByName.TryAdd(pluginInfo.Name, pluginInfo);
foreach (var entry in allPlugins)
{
if (pluginsByName.TryGetValue(entry.Name, out PluginInfo pluginInfo))
{
pluginInfo.Author = entry.Author;
pluginInfo.Title = entry.Title;
pluginInfo.Version = entry.Version;
pluginInfo.IsLoaded = entry.IsLoaded;
}
else
{
var newPluginInfo = new PluginInfo(entry.Author, entry.Name, entry.Title, entry.Version, "", "", false, entry.IsLoaded);
_config.ListOfPlugins.Add(newPluginInfo);
pluginsByName[newPluginInfo.Name] = newPluginInfo;
listChanged = true;
}
}
if (listChanged)
_config.ListOfPlugins = _config.ListOfPlugins.OrderBy(static x => x.Name).ToList();
}
private IEnumerator CheckNewVersion()
{
_isChecking = true;
var listedVersions = new Dictionary<string, ListedVersion>(StringComparer.OrdinalIgnoreCase);
var listsRoutine = LoadVersionLists(listedVersions);
while (listsRoutine.MoveNext())
yield return listsRoutine.Current;
var updateList = _config.AutoSearch.ToDictionary(static check => check.Key, static _ => (new List<string>(), new List<string>()));
updateList["uMod"] = (
new List<string> { $"{DateTime.UtcNow.AddMinutes(_config.UTC).ToString(_config.TimeFormat ? "d MMM yyyy HH:mm" : "d MMM yyyy hh:mm tt")}" },
new List<string>
{ $"### {DateTime.UtcNow.AddMinutes(_config.UTC).ToString(_config.TimeFormat ? "d MMM yyyy HH:mm" : "d MMM yyyy hh:mm tt")}" });
updateList.Add("NotFound", (new List<string> { "\n[NotFound]" }, new List<string> { "\n[NotFound]" }));
updateList.Add("NotLoaded", (new List<string> { "\n[NotLoaded]" }, new List<string> { "\n[NotLoaded]" }));
var isUpToDate = true;
var searchedPlugins = new List<PluginData>();
var linkedPlugins = new List<string>();
for (var index = 0; index < _config.ListOfPlugins.Count; index++)
{
if (!IsLoaded)
yield break;
var check = _config.ListOfPlugins[index];
if (check.Ignore)
continue;
if (!check.IsLoaded)
{
if (_config.IgnoreNotLoaded)
continue;
updateList["NotLoaded"].Item1[0] += $" {check.Name},";
updateList["NotLoaded"].Item2[0] += $" {check.Name},";
}
if (string.IsNullOrEmpty(check.Url) && listedVersions.TryGetValue(check.Name, out var listed) && listed.IsWrittenBy(check.Author))
{
ApplyLatestVersion(check, listed.Marketplace, listed.Version, listed.Url, updateList, ref isUpToDate);
continue;
}
check.SearchUrl = string.IsNullOrEmpty(check.Url) ? null : NormalizeUrl(check.Url);
var response = new MarketplaceResponse();
var searchRoutine = RequestMarketplace(string.IsNullOrEmpty(check.SearchUrl) ? check.Name : check.SearchUrl, response);
while (searchRoutine.MoveNext())
yield return searchRoutine.Current;
if (response.RateLimited)
{
PrintError("Waiting 30 seconds for rate limit");
index--;
yield return new WaitForSeconds(30f);
continue;
}
if (response.Failed)
continue;
check.IsFounded = false;
var foundedPlugins = response.Data;
var hasExactUrl = !string.IsNullOrEmpty(check.SearchUrl) && foundedPlugins != null &&
foundedPlugins.Any(x => IsSameUrl(check.SearchUrl, x.Url, false));
if (!string.IsNullOrEmpty(check.SearchUrl) && !hasExactUrl)
{
var nameResponse = new MarketplaceResponse();
var nameRoutine = RequestMarketplace(check.Name, nameResponse);
while (nameRoutine.MoveNext())
yield return nameRoutine.Current;
if (nameResponse.RateLimited)
{
PrintError("Waiting 30 seconds for rate limit");
index--;
yield return new WaitForSeconds(30f);
continue;
}
if (nameResponse.Data != null)
{
foundedPlugins = foundedPlugins == null ? nameResponse.Data : foundedPlugins.Concat(nameResponse.Data).ToArray();
hasExactUrl = foundedPlugins.Any(x => IsSameUrl(check.SearchUrl, x.Url, false));
}
}
var hadUrl = !string.IsNullOrEmpty(check.Url);
searchedPlugins.Clear();
if (foundedPlugins != null)
searchedPlugins.AddRange(foundedPlugins);
TryResolvePlugin(check, foundedPlugins, hasExactUrl, allowWeakMatch: false, updateList, ref isUpToDate);
if (!check.IsFounded)
{
var rescueRateLimited = false;
foreach (var rescueQuery in GetRescueQueries(check))
{
var rescueResponse = new MarketplaceResponse();
var rescueRoutine = RequestMarketplace(rescueQuery, rescueResponse);
while (rescueRoutine.MoveNext())
yield return rescueRoutine.Current;
if (rescueResponse.RateLimited)
{
rescueRateLimited = true;
break;
}
if (rescueResponse.Failed || rescueResponse.Data == null)
continue;
searchedPlugins.AddRange(rescueResponse.Data);
if (TryResolvePlugin(check, rescueResponse.Data, hasExactUrl: false, allowWeakMatch: false, updateList, ref isUpToDate))
break;
}
if (rescueRateLimited)
{
PrintError("Waiting 30 seconds for rate limit");
index--;
yield return new WaitForSeconds(30f);
continue;
}
if (!check.IsFounded)
TryResolvePlugin(check, searchedPlugins.ToArray(), hasExactUrl, allowWeakMatch: true, updateList, ref isUpToDate);
}
if (!hadUrl && !string.IsNullOrEmpty(check.Url))
linkedPlugins.Add($"{check.Name} -> {check.Url}");
if (check.IsFounded)
continue;
updateList["NotFound"].Item1[0] += $" {check.Name},";
updateList["NotFound"].Item2[0] += $" {check.Name},";
}
if (linkedPlugins.Count != 0)
PrintWarning($"The link was empty and has been filled in from the search:\n{string.Join("\n", linkedPlugins)}");
updateList["NotFound"].Item1[0] = updateList["NotFound"].Item1[0].TrimEnd(',');
updateList["NotFound"].Item2[0] = updateList["NotFound"].Item2[0].TrimEnd(',');
updateList["NotLoaded"].Item1[0] = updateList["NotLoaded"].Item1[0].TrimEnd(',');
updateList["NotLoaded"].Item2[0] = updateList["NotLoaded"].Item2[0].TrimEnd(',');
if (_config.IgnoreNotFound || !updateList["NotFound"].Item1[0].Contains(" "))
updateList.Remove("NotFound");
if (!updateList["NotLoaded"].Item1[0].Contains(" "))
updateList.Remove("NotLoaded");
if (isUpToDate)
{
updateList["uMod"].Item1.Add("\nAll plugins are up to date");
updateList["uMod"].Item2.Add("\n**All plugins are up to date**");
}
if (updateList.Any(static x => x.Value.Item2.Count != 0) || (!_config.IgnoreNotFound && updateList["NotFound"].Item2[0].Split(' ').Length != 1))
{
var printWarningBuilder = new StringBuilder();
foreach (var check in updateList)
foreach (var item in check.Value.Item1)
printWarningBuilder.Append(item);
PrintWarning(printWarningBuilder.ToString());
if (!isUpToDate || (isUpToDate && !_config.IgnoreAllPluginsUpToDateMessage))
{
_sendDiscordMessageCoroutine = SendMessageDiscord(updateList);
ServerMgr.Instance.StartCoroutine(_sendDiscordMessageCoroutine);
}
}
SaveConfigKeepingManualEdits();
}
private IEnumerator LoadVersionLists(Dictionary<string, ListedVersion> listedVersions)
{
foreach (var listLink in _config.VersionLists)
{
if (!Uri.TryCreate(NormalizeUrl(listLink), UriKind.Absolute, out var listUri))
{
PrintError($"The version list link is not a valid URL: {listLink}");
continue;
}
var marketplace = listUri.Host.StartsWith("www.", StringComparison.OrdinalIgnoreCase) ? listUri.Host.Substring("www.".Length) : listUri.Host;
_config.AutoSearch.TryAdd(marketplace, true);
if (!_config.AutoSearch[marketplace])
continue;
using var request = UnityWebRequest.Get(listUri);
request.SetRequestHeader("User-Agent", $"Update Checker/{Version}");
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
PrintError($"Failed to load the version list {listLink}: {request.error}");
continue;
}
Dictionary<string, ListedVersion> versions;
try
{
versions = JsonConvert.DeserializeObject<Dictionary<string, ListedVersion>>(request.downloadHandler.text);
}
catch (Exception exception)
{
PrintError($"The version list {listLink} is not readable: {exception.Message}");
continue;
}
if (versions == null)
continue;
foreach (var version in versions)
{
if (version.Value == null)
continue;
version.Value.Marketplace = marketplace;
listedVersions.TryAdd(version.Key, version.Value);
}
}
}
private bool TryResolvePlugin(PluginInfo check, PluginData[] foundedPlugins, bool hasExactUrl, bool allowWeakMatch,
Dictionary<string, (List<string>, List<string>)> updateList, ref bool isUpToDate)
{
if (foundedPlugins == null || foundedPlugins.Length == 0)
return false;
var validPlugins = foundedPlugins.Where(static x => !string.IsNullOrEmpty(x.Name) && !string.IsNullOrEmpty(x.Marketplace)).ToList();
if (validPlugins.Count == 0)
return false;
foreach (var pluginData in validPlugins)
RegisterMarketplace(pluginData.Marketplace, updateList);
var marketplaceOrder = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var marketplace in _config.AutoSearch.Keys)
marketplaceOrder[marketplace] = marketplaceOrder.Count;
var orderedFoundedPlugins = validPlugins.OrderBy(x => marketplaceOrder.TryGetValue(x.Marketplace, out var order) ? order : -1).ToList();
List<PluginData> candidates;
if (!string.IsNullOrEmpty(check.SearchUrl))
{
candidates = orderedFoundedPlugins.Where(x => IsSameUrl(check.SearchUrl, x.Url, !hasExactUrl)).ToList();
}
else
{
var enabledPlugins = orderedFoundedPlugins
.Where(x => !_config.AutoSearch.TryGetValue(x.Marketplace, out var isEnabled) || isEnabled).ToList();
var checkNameLower = check.Name.ToLower();
var checkTitleLower = string.IsNullOrEmpty(check.Title) ? null : check.Title.Replace(" ", "").ToLower();
var checkAuthorLower = check.Author.ToLower();
candidates = enabledPlugins.Where(x => x.Author != null && x.Author.ToLower().Contains(checkAuthorLower) &&
(IsNameMatch(x, checkNameLower) || checkTitleLower != null && IsNameMatch(x, checkTitleLower))).ToList();
if (candidates.Count == 0)
candidates = enabledPlugins.Where(x => IsExactNameMatch(x, checkNameLower) ||
checkTitleLower != null && IsExactNameMatch(x, checkTitleLower)).ToList();
if (candidates.Count == 0 && allowWeakMatch)
candidates = enabledPlugins.Where(x => IsNameMatch(x, checkNameLower) ||
checkTitleLower != null && IsNameMatch(x, checkTitleLower)).ToList();
if (candidates.Count == 0 && allowWeakMatch && enabledPlugins.Count != 0)
{
var firstNameLower = enabledPlugins[0].Name.ToLower();
candidates = enabledPlugins.Where(x => x.Tags != null && x.Tags.Contains(firstNameLower)).ToList();
}
}
if (candidates.Count == 0)
return false;
var newPluginData = candidates[0];
foreach (var candidate in candidates)
{
if (string.IsNullOrEmpty(candidate.LatestVersion))
continue;
if (string.IsNullOrEmpty(newPluginData.LatestVersion) || ParseVersion(candidate.LatestVersion) > ParseVersion(newPluginData.LatestVersion))
newPluginData = candidate;
}
if (string.IsNullOrEmpty(check.Url))
check.Url = newPluginData.Url;
ApplyLatestVersion(check, newPluginData.Marketplace, newPluginData.LatestVersion, check.Url, updateList, ref isUpToDate);
return true;
}
private void ApplyLatestVersion(PluginInfo check, string marketplace, string latestVersion, string url,
Dictionary<string, (List<string>, List<string>)> updateList, ref bool isUpToDate)
{
check.IsFounded = true;
check.Marketplace = marketplace;
RegisterMarketplace(marketplace, updateList);
if (string.IsNullOrEmpty(latestVersion) || ParseVersion(latestVersion) <= ParseVersion(check.Version))
return;
isUpToDate = false;
Interface.Oxide.CallHook("OnUpdateCheckerUpdateFound", check.Name, check.Version, latestVersion, url, marketplace);
var link = _config.UseURL && !string.IsNullOrEmpty(url) ? url : null;
var (consoleLines, discordLines) = updateList[marketplace];
consoleLin
// ... [Code preview truncated due to length. Full source in attached file] ...