diff --git a/EXILED/Exiled.Loader/ConfigManager.cs b/EXILED/Exiled.Loader/ConfigManager.cs index e925517be..f04835e61 100644 --- a/EXILED/Exiled.Loader/ConfigManager.cs +++ b/EXILED/Exiled.Loader/ConfigManager.cs @@ -11,6 +11,7 @@ namespace Exiled.Loader using System.Collections.Generic; using System.IO; using System.Linq; + using System.Reflection; using API.Enums; using API.Extensions; @@ -19,13 +20,19 @@ namespace Exiled.Loader using Exiled.API.Features; using Exiled.API.Features.Pools; + using LabApi.Loader.Features.Plugins.Configuration; using YamlDotNet.Core; + using YamlDotNet.Serialization; + + using LabPlugin = LabApi.Loader.Features.Plugins.Plugin; /// /// Used to handle plugin configs. /// public static class ConfigManager { + private static readonly MethodInfo PropertiesSetter = typeof(LabPlugin).GetProperty("Properties", BindingFlags.Public | BindingFlags.Instance)?.GetSetMethod(true); + /// /// Loads all the plugin configs. /// @@ -281,5 +288,186 @@ public static void ReloadRemoteAdmin() player.ReferenceHub.serverRoles.RefreshPermissions(); } } + + /// + /// Reloads all LabAPI configs. + /// + public static void ReloadLabAPIConfigs() + { + try + { + // this is 10x more readable than Exileds current config management system LOL + foreach (LabPlugin plugin in Loader.LabAPIPlugins.Keys) + { + LoadLabAPIConfig(plugin); + SaveLabAPIConfig(plugin); + } + } + catch (Exception ex) + { + Log.Error(ex); + } + } + + /// + /// Attempts to load a config for a LabAPI plugin. + /// + /// The LabAPI plugin. + /// I love it when the modding framework people call the best has all plugin loading methods private and the config loading methods don't take custom directory paths. + public static void LoadLabAPIConfig(LabPlugin plugin) + { + Type pluginType = plugin.GetType(); + Type type = pluginType; + while (type is not null) + { + type = type.BaseType; + + if (type is { IsGenericType: true }) + { + Type genericTypeDef = type.GetGenericTypeDefinition(); + + if (genericTypeDef == typeof(LabApi.Loader.Features.Plugins.Plugin<>)) + break; + } + } + + if (type is null) + return; + + Type configType = type.GetGenericArguments().FirstOrDefault(); + if (configType is null) + { + Log.Error($"Failed to load config for LabAPI plugin {plugin.Name}, could not get the generic of TConfig!"); + return; + } + + ConstructorInfo parameterless = configType.GetConstructors().FirstOrDefault(ctor => ctor.GetParameters().Length == 0); + + if (parameterless is null) + { + Log.Error($"Failed to load config for LabAPI plugin {plugin.Name}, config type has no parameterless constructor!"); + return; + } + + MethodInfo configSetter = pluginType.GetProperty("Config")?.GetSetMethod(); + if (configSetter is null) + { + Log.Error($"Failed to load config for LabAPI plugin {plugin.Name}, no setter for property \"Config\" was found!"); + return; + } + + string configPath = Paths.GetConfigPath(plugin.Name); + + if (!File.Exists(configPath)) + { + Log.Warn($"LabAPI Plugin {plugin.Name} doesn't have default configs, generating..."); + configSetter.Invoke(plugin, new[] { parameterless.Invoke(null) }); + return; + } + + IDeserializer deserializer = LabApi.Loader.Features.Yaml.YamlConfigParser.Deserializer; + MethodInfo deserialize = deserializer.GetType().GetMethods().Single(method => method.Name == "Deserialize" && method.IsGenericMethod && method.GetParameters().FirstOrDefault()?.ParameterType == typeof(string)).MakeGenericMethod(configType); + try + { + configSetter.Invoke(plugin, new[] { deserialize.Invoke(deserializer, new object[] { File.ReadAllText(configPath) }) }); + } + catch (TargetInvocationException ex) + { + if (ex.InnerException is YamlException yamlException) + { + Log.Error($"{plugin.Name} configs could not be loaded, some of them are in a wrong format, default configs will be loaded instead!\n{yamlException}"); + configSetter.Invoke(plugin, new[] { parameterless.Invoke(null) }); + } + else + { + throw; + } + } + } + + /// + /// Saves a config for a LabAPI plugin. + /// + /// The LabAPI plugin. + public static void SaveLabAPIConfig(LabPlugin plugin) + { + Type pluginType = plugin.GetType(); + Type type = pluginType; + while (type is not null) + { + type = type.BaseType; + + if (type is { IsGenericType: true }) + { + Type genericTypeDef = type.GetGenericTypeDefinition(); + + if (genericTypeDef == typeof(LabApi.Loader.Features.Plugins.Plugin<>)) + break; + } + } + + if (type is null) + return; + + MethodInfo configGetter = pluginType.GetProperty("Config")?.GetGetMethod(); + if (configGetter is null) + { + Log.Error($"Failed to save config for LabAPI plugin {plugin.Name}, no getter for property \"Config\" was found!"); + return; + } + + string config = LabApi.Loader.Features.Yaml.YamlConfigParser.Serializer.Serialize(configGetter.Invoke(plugin, null)); + string configPath = Paths.GetConfigPath(plugin.Name); + + try + { + Directory.CreateDirectory(Path.Combine(Paths.IndividualConfigs, plugin.Name)); + File.WriteAllText(configPath, config); + } + catch (Exception exception) + { + Log.Error($"An error has occurred while saving configs to {configPath} path: {exception}"); + } + } + + /// + /// Loads the properties of a LabAPI plugin. + /// + /// The LabAPI plugin. + /// Whether the properties were successfully retrieved. + public static bool LoadLabAPIProperties(LabPlugin plugin) + { + if (PropertiesSetter is null) + { + Log.Error("Cannot load LabAPI properties as the setter from reflection is null!"); + return false; + } + + ISerializer serializer = LabApi.Loader.Features.Yaml.YamlConfigParser.Serializer; + IDeserializer deserializer = LabApi.Loader.Features.Yaml.YamlConfigParser.Deserializer; + + string configPath = Path.Combine(Paths.IndividualConfigs, plugin.Name, $"{Server.Port}-properties.yml"); + + if (!File.Exists(configPath)) + { + Log.Warn($"LabAPI Plugin {plugin.Name} doesn't have default properties, generating..."); + PropertiesSetter.Invoke(plugin, new object[] { Properties.CreateDefault() }); + File.WriteAllText(configPath, serializer.Serialize(plugin.Properties!)); + return true; + } + + try + { + PropertiesSetter.Invoke(plugin, new[] { deserializer.Deserialize(File.ReadAllText(configPath), typeof(Properties)) }); + } + catch (YamlException yamlException) + { + Log.Error($"{plugin.Name} properties could not be loaded, default properties will be loaded instead!\n{yamlException}"); + PropertiesSetter.Invoke(plugin, new object[] { Properties.CreateDefault() }); + File.WriteAllText(configPath, serializer.Serialize(plugin.Properties!)); + } + + return true; + } } } \ No newline at end of file diff --git a/EXILED/Exiled.Loader/Loader.cs b/EXILED/Exiled.Loader/Loader.cs index 18ef26fdc..5dce161c0 100644 --- a/EXILED/Exiled.Loader/Loader.cs +++ b/EXILED/Exiled.Loader/Loader.cs @@ -23,17 +23,25 @@ namespace Exiled.Loader using CommandSystem.Commands.Shared; using Exiled.API.Features; + using Exiled.API.Features.Pools; using Features; using Features.Configs; using Features.Configs.CustomConverters; + using LabApi.Loader; + using LabApi.Loader.Features.Misc; + using LabApi.Loader.Features.Plugins.Configuration; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NodeDeserializers; + using LabPlugin = LabApi.Loader.Features.Plugins.Plugin; + /// /// Used to handle plugins. /// public class Loader { + private static readonly MethodInfo FilePathSetter = typeof(LabPlugin).GetProperty("FilePath", BindingFlags.Public | BindingFlags.Instance)?.GetSetMethod(true); + /// /// Initializes a new instance of the class. /// @@ -67,6 +75,11 @@ public Loader() /// public static SortedSet> Plugins { get; } = new(PluginPriorityComparer.Instance); + /// + /// Gets the plugins list. + /// + public static Dictionary LabAPIPlugins { get; } = new(); + /// /// Gets a dictionary containing the file paths of assemblies. /// @@ -226,6 +239,60 @@ public static IPlugin CreatePlugin(Assembly assembly) return null; } + /// + /// Create a plugin instance. + /// + /// The plugin assembly. + /// The path of the assembly. + /// Returns the created plugin instance or . + public static LabPlugin CreateLabAPIPlugin(Assembly assembly, string path) + { + try + { + AssemblyUtils.ResolveEmbeddedResources(assembly); + } + catch (Exception ex) + { + Log.Error("Failed to resolve embedded resources for assembly '" + path + "'"); + string[] missingDependencies = AssemblyUtils.GetMissingDependencies(assembly).ToArray(); + if (!missingDependencies.Any()) + return null; + Log.Error("Missing dependencies:\n" + string.Join("\n", missingDependencies.Select(x => "-\t " + x))); + Log.Error(ex); + } + + LabPlugin plugin = null; + + if (FilePathSetter is null) + { + Log.Error("FilePath setter for LabAPI Plugin type is null!"); + } + + try + { + foreach (Type type in assembly.GetTypes()) + { + if (!type.IsSubclassOf(typeof(LabPlugin)) || type.IsAbstract || Activator.CreateInstance(type) is not LabPlugin instance) + continue; + + FilePathSetter?.Invoke(instance, new object[] { path }); + + plugin = instance; + } + } + catch (Exception ex) + { + Log.Error(" Couldn't load the LabAPI plugin inside '" + path + "'"); + string[] missingDependencies = AssemblyUtils.GetMissingDependencies(assembly).ToArray(); + if (!missingDependencies.Any()) + return null; + Log.Error("Missing dependencies:\n" + string.Join("\n", missingDependencies.Select(x => "-\t " + x))); + Log.Error(ex); + } + + return plugin; + } + /// /// Enables all plugins. /// @@ -268,6 +335,38 @@ public static void EnablePlugins() Log.Error($"Plugin \"{plugin.Name}\" threw an exception while enabling: {exception}"); } } + + foreach (LabPlugin plugin in LabAPIPlugins.Keys.OrderBy(plugin => plugin.Priority)) + { + try + { + if (ConfigManager.LoadLabAPIProperties(plugin)) + { + Properties properties = plugin.Properties; + if (properties is { IsEnabled: true }) + { + // copy pasted from LabAPI plugin enabling + try + { + CustomNetworkManager.Modded = true; + plugin.RegisterCommands(); + plugin.Enable(); + + Log.Info($"LabAPI plugin {plugin.Name} v{plugin.Version.Major}.{plugin.Version.Minor}.{plugin.Version.Build} by {plugin.Author} has been enabled!"); + } + catch (Exception ex) + { + Log.Error($"Couldn't enable the LabAPI plugin {plugin}"); + Log.Error(ex); + } + } + } + } + catch (Exception exception) + { + Log.Error($"Plugin \"{plugin.Name}\" threw an exception while enabling: {exception}"); + } + } } /// @@ -294,12 +393,14 @@ public static void ReloadPlugins() } Plugins.Clear(); + LabAPIPlugins.Clear(); Server.PluginAssemblies.Clear(); Locations.Clear(); LoadPlugins(); ConfigManager.Reload(); + ConfigManager.ReloadLabAPIConfigs(); TranslationManager.Reload(); EnablePlugins(); @@ -322,6 +423,12 @@ public static void DisablePlugins() Log.Error($"Plugin \"{plugin.Name}\" threw an exception while disabling: {exception}"); } } + + foreach (LabPlugin plugin in LabAPIPlugins.Keys) + { + plugin.UnregisterCommands(); + plugin.Disable(); + } } /// @@ -331,6 +438,14 @@ public static void DisablePlugins() /// The desired plugin, null if not found. public static IPlugin GetPlugin(string args) => Plugins.FirstOrDefault(x => x.Name == args || x.Prefix == args); + /// + /// Gets a LabAPI plugin Exiled loaded by its name. + /// + /// The name of the plugin. + /// The desired plugin, null if not found. + /// This method does not check LabAPI's loaded plugins, only LabAPI plugins EXILED loaded. + public static LabPlugin GetLabAPIPlugin(string args) => LabAPIPlugins.Keys.FirstOrDefault(x => x.Name == args); + /// /// Runs the plugin manager, by loading all dependencies, plugins, configs and then enables all plugins. /// @@ -391,6 +506,7 @@ public IEnumerator Run(Assembly[] dependencies = null) LoadPlugins(); ConfigManager.Reload(); + ConfigManager.ReloadLabAPIConfigs(); TranslationManager.Reload(); EnablePlugins(); @@ -485,6 +601,7 @@ private static void LoadPluginsFromDirectory(string dir = null) Locations[assembly] = assemblyPath; } + List failed = ListPool.Pool.Get(); foreach (Assembly assembly in Locations.Keys) { if (Locations[assembly].Contains("dependencies")) @@ -493,7 +610,10 @@ private static void LoadPluginsFromDirectory(string dir = null) IPlugin plugin = CreatePlugin(assembly); if (plugin == null) + { + failed.Add(assembly); continue; + } if (Plugins.Any(p => p.Name == plugin.Name)) continue; @@ -505,6 +625,23 @@ private static void LoadPluginsFromDirectory(string dir = null) Server.PluginAssemblies.Add(assembly, plugin); Plugins.Add(plugin); } + + // inefficient enumeration (could be forced inside earlier foreach), but a lot easier to read. + foreach (Assembly attempt in failed) + { + LabPlugin plugin = CreateLabAPIPlugin(attempt, Locations[attempt]); + + if (plugin == null) + continue; + + if (Plugins.Any(p => p.Name == plugin.Name) || LabAPIPlugins.Keys.Any(p => p.Name == plugin.Name)) + continue; + + Log.Info("Successfully loaded LabAPI plugin " + plugin.Name); + LabAPIPlugins.Add(plugin, attempt); + } + + ListPool.Pool.Return(failed); } ///