diff --git a/LabApi/LabApi.csproj b/LabApi/LabApi.csproj
index edd29d88..04e4906d 100644
--- a/LabApi/LabApi.csproj
+++ b/LabApi/LabApi.csproj
@@ -62,12 +62,17 @@
+
+
+
+
+
diff --git a/LabApi/Loader/Features/Configuration/LabApiConfig.cs b/LabApi/Loader/Features/Configuration/LabApiConfig.cs
index 47bea231..e9206027 100644
--- a/LabApi/Loader/Features/Configuration/LabApiConfig.cs
+++ b/LabApi/Loader/Features/Configuration/LabApiConfig.cs
@@ -27,4 +27,28 @@ public class LabApiConfig
///
[Description("Whether to allow loading plugins even if they were built for a different major version of LabAPI.")]
public bool LoadUnsupportedPlugins { get; set; }
+
+ ///
+ /// Gets or sets the list of NuGet package source URLs used when resolving
+ /// and downloading dependencies from NuGet repositories.
+ ///
+ ///
+ /// Each entry in this list represents a NuGet feed endpoint (for example,
+ /// the official https://api.nuget.org/v3/index.json source).
+ /// Multiple sources can be specified to support private or custom feeds.
+ ///
+ [Description("List of NuGet package sources to use when resolving dependencies via NuGet.")]
+ public string[] NugetPackageSources { get; set; } = ["https://api.nuget.org/v3/index.json"];
+
+ ///
+ /// Gets or sets a value indicating whether dependencies should be automatically
+ /// downloaded from the configured NuGet sources when they are missing or outdated.
+ ///
+ ///
+ /// When set to , the system attempts to retrieve and install
+ /// required packages automatically during dependency resolution.
+ /// Disabling this option may require manual dependency management.
+ ///
+ [Description("Automatically download dependencies from NuGet when missing or outdated.")]
+ public bool AutomaticallyDownloadDependencies { get; set; } = true;
}
diff --git a/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs b/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs
new file mode 100644
index 00000000..f473db23
--- /dev/null
+++ b/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs
@@ -0,0 +1,89 @@
+using LabApi.Features.Console;
+using System;
+using System.Reflection;
+
+namespace LabApi.Loader.Features.NuGet.Models;
+
+///
+/// Represents a dependency entry within a NuGet package,
+/// including its identifier and version, and provides
+/// helper methods for installation and status checking.
+///
+public class NuGetDependency
+{
+ ///
+ /// Gets or sets the unique identifier (name) of the NuGet dependency.
+ ///
+ public required string Id { get; set; }
+
+ ///
+ /// Gets or sets the semantic version string of the dependency (e.g. "1.2.3").
+ ///
+ public required string Version { get; set; }
+
+ ///
+ /// Installs this NuGet dependency by downloading it from the configured source.
+ ///
+ ///
+ /// A instance representing the downloaded package,
+ /// or if the installation failed or the package could not be found.
+ ///
+ ///
+ /// This method delegates to .
+ ///
+ public NuGetPackage? Install() => NuGetPackagesManager.DownloadNugetPackage(Id, Version);
+
+ ///
+ /// Determines whether this dependency is already installed
+ /// or loaded in the current AppDomain.
+ ///
+ ///
+ /// true if the dependency is installed or the corresponding assembly is already loaded;
+ /// otherwise, false.
+ ///
+ public bool IsInstalled()
+ {
+ if (NuGetPackagesManager.Packages.TryGetValue($"{Id}.{Version}", out NuGetPackage package))
+ {
+ return string.Equals(package.Version, Version, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (IsAssemblyAlreadyLoaded(Id))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks whether an assembly with the given identifier is already loaded
+ /// into the current application domain.
+ ///
+ /// The dependency or assembly identifier to check.
+ ///
+ /// true if an assembly with the specified ID is already loaded;
+ /// otherwise, false.
+ ///
+ private bool IsAssemblyAlreadyLoaded(string id)
+ {
+ try
+ {
+ foreach (Assembly? asm in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ string asmName = asm.GetName().Name ?? string.Empty;
+
+ if (asmName.Equals(id, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn($"{NuGetPackagesManager.Prefix} Failed to check if assembly '{id}' is already loaded: {ex.Message}");
+ }
+
+ return false;
+ }
+}
diff --git a/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs
new file mode 100644
index 00000000..0edc773c
--- /dev/null
+++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs
@@ -0,0 +1,173 @@
+using LabApi.Features.Console;
+using LabApi.Loader.Features.Misc;
+using LabApi.Loader.Features.Paths;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+namespace LabApi.Loader.Features.NuGet.Models;
+
+///
+/// Represents a NuGet package loaded by LabApi, including its metadata,
+/// content, and dependency information.
+///
+public class NuGetPackage
+{
+ ///
+ /// Identifies the plugin tag used to mark NuGet package as plugin.
+ ///
+ private const string LabApiPluginTag = "labapi-plugin";
+
+ ///
+ /// Gets or sets the unique package identifier (name).
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the package version string (e.g. "1.2.3").
+ ///
+ public string Version { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the tag metadata from the .nuspec file (used to identify plugins, etc.).
+ ///
+ public string Tags { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the compiled assembly content of the package (if applicable).
+ ///
+ public byte[]? RawAssembly { get; set; } = null;
+
+ ///
+ /// Gets or sets the full path of NuGet package.
+ ///
+ public string FilePath { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the name of the NuGet package file (e.g. "MyPlugin.1.0.0.nupkg").
+ ///
+ public required string FileName { get; set; }
+
+ ///
+ /// Gets or sets the raw file contents of the NuGet package (.nupkg file).
+ ///
+ public required byte[] FileContent { get; set; }
+
+ ///
+ /// Gets or sets the list of dependencies defined by this package.
+ ///
+ public List Dependencies { get; set; } = new List();
+
+ ///
+ /// Gets a value indicating whether this package is marked as a LabApi plugin.
+ /// Determined by the presence of the "labapi-plugin" tag.
+ ///
+ public bool IsPlugin => Tags
+ .ToLower()
+ .Contains(LabApiPluginTag);
+
+ ///
+ /// Gets or sets a value indicating whether the package is already loaded.
+ ///
+ public bool IsLoaded { get; set; }
+
+ ///
+ /// Extracts the NuGet package file (.nupkg) to the appropriate directory
+ /// (plugins or dependencies), depending on whether it is a plugin.
+ ///
+ ///
+ /// The full path to the extracted file if successful; otherwise, null.
+ ///
+ public string? ExtractToFolder()
+ {
+ string? folder = GetFinalFolder();
+
+ if (folder == null)
+ {
+ Logger.Warn($"{NuGetPackagesManager.Prefix} Could not extract package '{Id}' v{Version} to {(IsPlugin ? "plugins" : "dependencies")} folder: no valid path found!");
+ return null;
+ }
+
+ string targetFile = Path.Combine(folder, FileName);
+
+ File.WriteAllBytes(targetFile, FileContent);
+
+ FilePath = targetFile;
+
+ return targetFile;
+ }
+
+ ///
+ /// Loads package.
+ ///
+ public void Load()
+ {
+ if (IsLoaded)
+ {
+ return;
+ }
+
+ if (RawAssembly?.Length == 0)
+ {
+ Logger.Warn($"{NuGetPackagesManager.Prefix} Package '{Id}' v{Version} does not contain a valid assembly, skipping...");
+ return;
+ }
+
+ Assembly assembly = Assembly.Load(RawAssembly);
+
+ if (IsPlugin)
+ {
+ try
+ {
+ AssemblyUtils.ResolveEmbeddedResources(assembly);
+ }
+ catch (Exception e)
+ {
+ Logger.Error($"{NuGetPackagesManager.Prefix} Failed to resolve embedded resources for package '{Id}' v{Version}");
+ Logger.Error(e);
+ }
+
+ try
+ {
+ PluginLoader.InstantiatePlugins(assembly.GetTypes(), assembly, FilePath);
+ }
+ catch (Exception e)
+ {
+ Logger.Error($"{NuGetPackagesManager.Prefix} Couldn't load the plugin inside package '{Id}' v{Version}");
+ Logger.Error(e);
+ }
+ }
+ else
+ {
+ PluginLoader.Dependencies.Add(assembly);
+ }
+
+ Logger.Info($"{NuGetPackagesManager.Prefix} Package '{Id}' v{Version} was loaded!");
+
+ IsLoaded = true;
+ }
+
+ ///
+ /// Resolves and returns the final folder path for the package extraction,
+ /// creating directories if necessary.
+ ///
+ ///
+ /// The full directory path where the package should be extracted,
+ /// or null if no valid path could be determined.
+ ///
+ private string? GetFinalFolder()
+ {
+ foreach (string path in IsPlugin ? PluginLoader.Config.PluginPaths : PluginLoader.Config.DependencyPaths)
+ {
+ string resolvedPath = PluginLoader.ResolvePath(path);
+ string fullPath = Path.Combine(IsPlugin ? PathManager.Plugins.FullName : PathManager.Dependencies.FullName, resolvedPath);
+
+ Directory.CreateDirectory(fullPath);
+
+ return fullPath;
+ }
+
+ return null;
+ }
+}
diff --git a/LabApi/Loader/Features/NuGet/Models/NuGetPackageIndex.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackageIndex.cs
new file mode 100644
index 00000000..79a788cc
--- /dev/null
+++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackageIndex.cs
@@ -0,0 +1,26 @@
+using System.Runtime.Serialization;
+
+namespace LabApi.Loader.Features.NuGet.Models;
+
+///
+/// Represents the root structure of a NuGet service index (index.json),
+/// which describes the available service endpoints for a NuGet source.
+///
+///
+/// The NuGet service index (usually located at https://api.nuget.org/v3/index.json)
+/// provides metadata about the repository’s available APIs, such as
+/// PackageBaseAddress, SearchQueryService, and others.
+///
+public class NuGetPackageIndex
+{
+ ///
+ /// Gets or sets the list of service resources exposed by the NuGet source.
+ ///
+ ///
+ /// Each resource entry describes an endpoint (for example,
+ /// a package base address or search service) and its supported version.
+ /// These are typically represented by the type.
+ ///
+ [DataMember(Name = "resources")]
+ public NuGetPackageResource[] Resources { get; set; } = [];
+}
diff --git a/LabApi/Loader/Features/NuGet/Models/NuGetPackageResource.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackageResource.cs
new file mode 100644
index 00000000..1b8ff500
--- /dev/null
+++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackageResource.cs
@@ -0,0 +1,37 @@
+using System.Runtime.Serialization;
+
+namespace LabApi.Loader.Features.NuGet.Models;
+
+///
+/// Represents a single resource entry within a NuGet service index (index.json).
+///
+///
+/// Each resource describes a specific NuGet service endpoint and its purpose,
+/// such as the PackageBaseAddress (used to download packages) or
+/// SearchQueryService (used to search packages).
+///
+public class NuGetPackageResource
+{
+ ///
+ /// Gets or sets the type of the NuGet service resource.
+ ///
+ ///
+ /// The @type field defines the role of the resource, for example:
+ ///
+ /// - PackageBaseAddress/3.0.0 — base URL for downloading package files
+ ///
+ ///
+ [DataMember(Name = "@type")]
+ public string Type { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the absolute URL of the service endpoint.
+ ///
+ ///
+ /// The @id value is typically a fully qualified HTTPS URL that identifies
+ /// the service’s base address. For example:
+ /// https://api.nuget.org/v3-flatcontainer/ or
+ ///
+ [DataMember(Name = "@id")]
+ public string Id { get; set; } = string.Empty;
+}
diff --git a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs
new file mode 100644
index 00000000..4b310a90
--- /dev/null
+++ b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs
@@ -0,0 +1,486 @@
+using LabApi.Features.Console;
+using LabApi.Loader.Features.Configuration;
+using LabApi.Loader.Features.NuGet.Models;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Xml.Linq;
+using Utf8Json;
+
+namespace LabApi.Loader.Features.NuGet;
+
+///
+/// Provides functionality for reading, downloading, extracting,
+/// and managing NuGet packages within the LabApi loader system.
+///
+public class NuGetPackagesManager
+{
+ ///
+ /// Prefix used for console log messages originating from NuGet operations.
+ ///
+ public const string Prefix = "[NUGET]";
+
+ private static HttpClient _client = new HttpClient();
+
+ private static readonly Dictionary _packageBaseAddressCache = new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Defines the ordered list of human-readable framework names (e.g. ".NETFramework4.8"),
+ /// used to determine preferred dependency groups in nuspec metadata.
+ ///
+ private static string[] _frameworkPriority = new[]
+ {
+ ".NETFramework4.8",
+ ".NETFramework4.7.2",
+ ".NETFramework4.7.1",
+ ".NETFramework4.7",
+ ".NETFramework4.6.2",
+ ".NETFramework4.6.1",
+ ".NETFramework4.6",
+ ".NETFramework4.5.2",
+ ".NETFramework4.5.1",
+ ".NETFramework4.5",
+ ".NETFramework4.0",
+ ".NETStandard2.1",
+ ".NETStandard2.0",
+ ".NETStandard1.6",
+ ".NETStandard1.5",
+ ".NETStandard1.4",
+ ".NETStandard1.3",
+ ".NETStandard1.2",
+ ".NETStandard1.1",
+ ".NETStandard1.0",
+ };
+
+ ///
+ /// Defines the ordered list of NuGet Target Framework Monikers (TFMs)
+ /// used to prioritize assembly selection inside .nupkg archives.
+ ///
+ private static string[] _frameworkVersionPriorities = new[]
+ {
+ "net48",
+ "net472",
+ "net471",
+ "net47",
+ "net462",
+ "net461",
+ "net46",
+ "net452",
+ "net451",
+ "net45",
+ "net40",
+ "netstandard2.1",
+ "netstandard2.0",
+ "netstandard1.6",
+ "netstandard1.5",
+ "netstandard1.4",
+ "netstandard1.3",
+ "netstandard1.2",
+ "netstandard1.1",
+ "netstandard1.0",
+ };
+
+ ///
+ /// Gets the dictionary of all loaded NuGet dependencies,
+ /// indexed by their package ID (case-insensitive).
+ ///
+ ///
+ /// This collection stores all NuGet packages that have been
+ /// successfully loaded or installed by the loader.
+ /// Keys represent the dependency ID, and values contain the corresponding
+ /// instance and its metadata.
+ ///
+ public static Dictionary Packages { get; } = new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Resolves any missing NuGet dependencies by checking all loaded packages
+ /// and automatically downloading missing ones from configured NuGet sources.
+ ///
+ public static void ResolveMissingNuGetDependencies()
+ {
+ Logger.Info($"{Prefix} Resolving missing NuGet dependencies...");
+
+ int resolvedCount = 0;
+
+ Queue packagesToResolve = new Queue(Packages.Values);
+
+ while (packagesToResolve.Count != 0)
+ {
+ NuGetPackage package = packagesToResolve.Dequeue();
+
+ foreach (NuGetDependency dep in package.Dependencies)
+ {
+ if (dep.IsInstalled())
+ {
+ continue;
+ }
+
+ Logger.Warn($"{Prefix} Package '{package.Id}' v{package.Version} has missing dependency '{dep.Id}' v{dep.Version}{(PluginLoader.Config.AutomaticallyDownloadDependencies ? ", attempting to resolve..." : ".")}");
+
+ if (!PluginLoader.Config.AutomaticallyDownloadDependencies)
+ {
+ continue;
+ }
+
+ try
+ {
+ NuGetPackage? downloadedPackage = dep.Install();
+
+ if (downloadedPackage == null)
+ {
+ continue;
+ }
+
+ packagesToResolve.Enqueue(downloadedPackage);
+ resolvedCount++;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"{Prefix} Failed to resolve dependency '{dep.Id}' v{dep.Version}");
+ Logger.Error(ex);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Reads and parses a NuGet package from the specified file path.
+ ///
+ /// The full file system path to the .nupkg file.
+ /// A populated instance.
+ /// Thrown when the specified package file does not exist.
+ public static NuGetPackage ReadPackage(string path)
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"Package file not found: {path}");
+ }
+
+ byte[] data = File.ReadAllBytes(path);
+ string name = Path.GetFileName(path);
+
+ return ReadPackage(name, path, data);
+ }
+
+ ///
+ /// Reads and parses a NuGet package from a byte array.
+ ///
+ /// The file name of the package.
+ /// The file full path.
+ /// The binary contents of the .nupkg file.
+ /// A populated instance.
+ public static NuGetPackage ReadPackage(string name, string fullPath, byte[] data)
+ {
+ using MemoryStream ms = new MemoryStream(data);
+ return ReadPackage(name, fullPath, ms);
+ }
+
+ ///
+ /// Reads and parses a NuGet package from a memory stream.
+ ///
+ /// The package file name.
+ /// The full path of a package.
+ /// A memory stream containing the .nupkg content.
+ /// A populated with metadata, dependencies, and assembly data loaded.
+ public static NuGetPackage ReadPackage(string name, string fullPath, MemoryStream stream)
+ {
+ NuGetPackage package = new NuGetPackage()
+ {
+ FilePath = fullPath,
+ FileName = name,
+ FileContent = stream.ToArray(),
+ };
+
+ using ZipArchive archive = new ZipArchive(stream);
+ ZipArchiveEntry bestEntry = GetBestVersion(archive);
+
+ if (bestEntry != null)
+ {
+ using MemoryStream ms = new MemoryStream();
+ using Stream entryStream = bestEntry.Open();
+
+ entryStream.CopyTo(ms);
+
+ package.RawAssembly = ms.ToArray();
+ }
+
+ string? nuspecXml = ReadNuspecFromNupkg(archive);
+
+ if (nuspecXml == null)
+ {
+ return package;
+ }
+
+ GetMetadata(package, nuspecXml);
+
+ return package;
+ }
+
+ ///
+ /// Downloads a NuGet package from the official NuGet repository and installs it
+ /// into the appropriate directory (plugins or dependencies).
+ ///
+ /// The ID of the package to download.
+ /// The version of the package to download.
+ public static NuGetPackage? DownloadNugetPackage(string packageId, string version)
+ {
+ string[] sources = PluginLoader.Config.NugetPackageSources;
+
+ if (sources == null || sources.Length == 0)
+ {
+ Logger.Error($"{Prefix} No NuGet package sources defined in configuration!");
+ return null;
+ }
+
+ byte[]? packageData = null;
+ string? successfulSource = null;
+
+ foreach (string source in sources)
+ {
+ string baseAddress = GetCachedPackageBaseAddress(source);
+ string idLower = packageId.ToLowerInvariant();
+ string verLower = version.ToLowerInvariant();
+
+ if (string.IsNullOrEmpty(baseAddress))
+ {
+ continue;
+ }
+
+ string downloadUrl = $"{baseAddress.TrimEnd('/')}/{idLower}/{verLower}/{idLower}.{verLower}.nupkg";
+
+ try
+ {
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
+
+ using HttpResponseMessage response = _client.SendAsync(request).GetAwaiter().GetResult();
+
+ if (response.IsSuccessStatusCode)
+ {
+ packageData = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
+ successfulSource = source;
+ break;
+ }
+ else
+ {
+ Logger.Warn($"{Prefix} Failed to download '{packageId}' v{version} from {downloadUrl}");
+ Logger.Error($"{Prefix} HTTP {(int)response.StatusCode} - {response.ReasonPhrase}");
+ }
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.Warn($"{Prefix} Failed to download '{packageId}' v{version} from {downloadUrl}");
+ Logger.Error($"{Prefix} HTTP error: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn($"{Prefix} Unexpected error while downloading from '{source}': {ex.Message}");
+ }
+ }
+
+ if (packageData == null)
+ {
+ Logger.Error($"{Prefix} Failed to download package '{packageId}' v{version} from all configured sources.");
+ return null;
+ }
+
+ // Proceed to install
+ NuGetPackage package = ReadPackage($"{packageId}.{version}.nupkg", string.Empty, packageData);
+
+ // Extracts nuget package to specific folder if thats plugin or dependency.
+ string? path = package.ExtractToFolder();
+
+ if (path == null)
+ {
+ Logger.Error($"{Prefix} Failed to extract package '{packageId}' v{version}");
+ return null;
+ }
+
+ Packages.Add($"{package.Id}.{package.Version}", package);
+
+ Logger.Info($"{Prefix} Successfully downloaded '{packageId}' v{version} from {successfulSource}");
+ return package;
+ }
+
+ ///
+ /// Resolves and caches the PackageBaseAddress for a given NuGet v3 source.
+ /// If the source does not expose a valid index.json or lacks the resource, returns an empty string.
+ ///
+ /// The NuGet feed base URL (e.g. https://api.nuget.org/v3/index.json).
+ /// The resolved PackageBaseAddress URL, or an empty string if unavailable.
+ private static string GetCachedPackageBaseAddress(string sourceUrl)
+ {
+ if (_packageBaseAddressCache.TryGetValue(sourceUrl, out string cached))
+ {
+ return cached;
+ }
+
+ string normalized = sourceUrl.TrimEnd('/');
+
+ string indexUrl = normalized.EndsWith("index.json", StringComparison.OrdinalIgnoreCase)
+ ? normalized
+ : $"{normalized}/index.json";
+
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, indexUrl);
+
+ using HttpResponseMessage response = _client.SendAsync(request).GetAwaiter().GetResult();
+
+ string data;
+ if (response.IsSuccessStatusCode)
+ {
+ data = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
+ }
+ else
+ {
+ Logger.Warn($"{Prefix} Failed to read packages source from '{indexUrl}'");
+
+ _packageBaseAddressCache[sourceUrl] = string.Empty;
+ return string.Empty;
+ }
+
+ NuGetPackageIndex index = JsonSerializer.Deserialize(data);
+
+ foreach (NuGetPackageResource resource in index.Resources)
+ {
+ if (!resource.Type.Contains("PackageBaseAddress"))
+ {
+ continue;
+ }
+
+ _packageBaseAddressCache[sourceUrl] = resource.Id;
+ return resource.Id;
+ }
+
+ _packageBaseAddressCache[sourceUrl] = string.Empty;
+ return string.Empty;
+ }
+
+ ///
+ /// Selects the most appropriate assembly file from a NuGet archive
+ /// based on the internal framework version priority list.
+ ///
+ /// The opened representing the .nupkg file.
+ /// The that best matches the preferred framework; otherwise null.
+ private static ZipArchiveEntry GetBestVersion(ZipArchive archive)
+ {
+ IEnumerable libEntries = archive.Entries.Where(x => x.FullName.StartsWith("lib/net") && x.FullName.EndsWith(".dll"));
+
+ Dictionary sortedEntries = new Dictionary();
+
+ foreach (ZipArchiveEntry entry in libEntries)
+ {
+ string[] name = entry.FullName.Split('/');
+
+ string frameworkVersion = name[1];
+
+ if (!_frameworkVersionPriorities.Contains(frameworkVersion))
+ {
+ continue;
+ }
+
+ int index = _frameworkVersionPriorities.IndexOf(frameworkVersion);
+
+ sortedEntries.Add(index, entry);
+ }
+
+ KeyValuePair bestEntry = sortedEntries.OrderBy(x => x.Key).FirstOrDefault();
+
+ return bestEntry.Value;
+ }
+
+ ///
+ /// Extracts and returns the XML content of the .nuspec file from within a NuGet archive.
+ ///
+ /// The instance representing the package.
+ /// The XML string of the .nuspec file, or null if not found.
+ private static string? ReadNuspecFromNupkg(ZipArchive entry)
+ {
+ ZipArchiveEntry nuspecEntry = entry.Entries.FirstOrDefault(e => e.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase));
+
+ if (nuspecEntry == null)
+ {
+ return null;
+ }
+
+ using StreamReader reader = new StreamReader(nuspecEntry.Open());
+ return reader.ReadToEnd();
+ }
+
+ ///
+ /// Reads metadata and dependency information from the .nuspec XML
+ /// and populates the corresponding fields in the instance.
+ ///
+ /// The package instance to populate.
+ /// The raw XML content of the .nuspec file.
+ private static void GetMetadata(NuGetPackage package, string nuspecXml)
+ {
+ XDocument doc = XDocument.Parse(nuspecXml);
+ XNamespace ns = doc.Root.GetDefaultNamespace();
+
+ XElement metadata = doc.Descendants(ns + "metadata").FirstOrDefault();
+
+ if (metadata == null)
+ {
+ return;
+ }
+
+ package.Id = metadata.Element(ns + "id")?.Value ?? string.Empty;
+ package.Version = metadata.Element(ns + "version")?.Value ?? string.Empty;
+ package.Tags = metadata.Element(ns + "tags")?.Value ?? string.Empty;
+
+ XElement depsElement = metadata.Element(ns + "dependencies");
+
+ if (depsElement == null)
+ {
+ return;
+ }
+
+ IEnumerable groups = depsElement.Elements(ns + "group");
+ XElement? selectedGroup = null;
+
+ foreach (string framework in _frameworkPriority)
+ {
+ selectedGroup = groups.FirstOrDefault(g =>
+ string.Equals(g.Attribute("targetFramework")?.Value, framework, StringComparison.OrdinalIgnoreCase));
+
+ if (selectedGroup != null)
+ {
+ break;
+ }
+ }
+
+ if (selectedGroup == null)
+ {
+ selectedGroup = groups.LastOrDefault();
+ }
+
+ List dependencies = new List();
+
+ if (selectedGroup != null)
+ {
+ string? groupFramework = selectedGroup.Attribute("targetFramework")?.Value;
+ foreach (XElement dep in selectedGroup.Elements(ns + "dependency"))
+ {
+ dependencies.Add(new NuGetDependency
+ {
+ Id = dep.Attribute("id")?.Value ?? string.Empty,
+ Version = dep.Attribute("version")?.Value ?? string.Empty,
+ });
+ }
+ }
+
+ foreach (XElement dep in depsElement.Elements(ns + "dependency"))
+ {
+ dependencies.Add(new NuGetDependency
+ {
+ Id = dep.Attribute("id")?.Value ?? string.Empty,
+ Version = dep.Attribute("version")?.Value ?? string.Empty,
+ });
+ }
+
+ package.Dependencies = dependencies;
+ }
+}
diff --git a/LabApi/Loader/PluginLoader.cs b/LabApi/Loader/PluginLoader.cs
index 0e86b10f..378f3ea5 100644
--- a/LabApi/Loader/PluginLoader.cs
+++ b/LabApi/Loader/PluginLoader.cs
@@ -5,6 +5,8 @@
using LabApi.Features.Wrappers;
using LabApi.Loader.Features.Configuration;
using LabApi.Loader.Features.Misc;
+using LabApi.Loader.Features.NuGet;
+using LabApi.Loader.Features.NuGet.Models;
using LabApi.Loader.Features.Paths;
using LabApi.Loader.Features.Plugins;
using LabApi.Loader.Features.Plugins.Enums;
@@ -25,6 +27,7 @@ public static partial class PluginLoader
{
private const string LoggerPrefix = "[LOADER]";
private const string DllSearchPattern = "*.dll";
+ private const string NupkgSearchPattern = "*.nupkg";
private const string PdbFileExtension = ".pdb";
private const string LabApiConfigName = "LabApi-{0}.yml";
@@ -75,6 +78,8 @@ public static void Initialize()
// We register all the commands in LabAPI to avoid plugin command conflicts.
CommandLoader.RegisterCommands();
+ RegisterNuGetPackage();
+
// We first load all the dependencies and store them in the dependencies list
LoadAllDependencies();
@@ -100,6 +105,16 @@ public static void LoadAllDependencies()
// We load all the dependencies from the configured dependency directories
Logger.Info($"{LoggerPrefix} Loading all dependencies");
+ foreach (NuGetPackage package in NuGetPackagesManager.Packages.Values)
+ {
+ if (package.IsPlugin)
+ {
+ continue;
+ }
+
+ package.Load();
+ }
+
foreach (string dependencyPath in Config.DependencyPaths)
{
string resolvedPath = ResolvePath(dependencyPath);
@@ -121,13 +136,8 @@ public static void LoadDependencies(IEnumerable files)
{
try
{
- // We load the assembly from the specified file.
Assembly assembly = Assembly.Load(File.ReadAllBytes(file.FullName));
-
- // And we add the assembly to the dependencies list.
Dependencies.Add(assembly);
-
- // We finally log that the dependency has been loaded.
Logger.Info($"{LoggerPrefix} Successfully loaded {assembly.FullName}");
}
catch (Exception e)
@@ -161,6 +171,16 @@ public static void LoadAllPlugins()
}
}
+ foreach (NuGetPackage package in NuGetPackagesManager.Packages.Values)
+ {
+ if (!package.IsPlugin)
+ {
+ continue;
+ }
+
+ package.Load();
+ }
+
// Then we finally enable all the plugins
Logger.Info($"{LoggerPrefix} Enabling all plugins");
EnablePlugins(Plugins.Keys.OrderBy(static plugin => plugin.Priority));
@@ -291,6 +311,67 @@ public static void EnablePlugin(Plugin plugin)
}
}
+ private static void RegisterNuGetPackage()
+ {
+ List files = new List();
+
+ foreach (string dependencyPath in Config.DependencyPaths)
+ {
+ string resolvedPath = ResolvePath(dependencyPath);
+ string fullPath = Path.Combine(PathManager.Dependencies.FullName, resolvedPath);
+
+ Directory.CreateDirectory(fullPath);
+
+ files.AddRange(new DirectoryInfo(fullPath).GetFiles(NupkgSearchPattern));
+ }
+
+ foreach (string pluginPath in Config.PluginPaths)
+ {
+ string resolvedPath = ResolvePath(pluginPath);
+ string fullPath = Path.Combine(PathManager.Plugins.FullName, resolvedPath);
+
+ Directory.CreateDirectory(fullPath);
+
+ files.AddRange(new DirectoryInfo(fullPath).GetFiles(NupkgSearchPattern));
+ }
+
+ foreach (FileInfo file in files)
+ {
+ RegisterNuGetPackage(file);
+ }
+
+ NuGetPackagesManager.ResolveMissingNuGetDependencies();
+ }
+
+ ///
+ /// Loads package info from a NuGet package (.nupkg).
+ ///
+ private static void RegisterNuGetPackage(FileInfo file)
+ {
+ try
+ {
+ NuGetPackage package = NuGetPackagesManager.ReadPackage(file.FullName);
+
+ string id = $"{package.Id}.{package.Version}";
+
+ if (NuGetPackagesManager.Packages.ContainsKey(id))
+ {
+ Logger.Warn($"{LoggerPrefix} Duplicate NuGet package dependency '{id}' found in '{file.FullName}', skipping...");
+ return;
+ }
+
+ NuGetPackagesManager.Packages.Add(id, package);
+ return;
+ }
+ catch (Exception e)
+ {
+ Logger.Error($"{LoggerPrefix} Failed to read package '{file.FullName}'");
+ Logger.Error(e);
+ }
+
+ return;
+ }
+
///
/// Loads or creates the LabAPI configuration file.
///
@@ -353,7 +434,7 @@ private static void ResolveTransparentlyModdedFlag()
}
}
- private static string ResolvePath(string path)
+ public static string ResolvePath(string path)
{
return path.Replace("$port", Server.Port.ToString());
}
@@ -428,7 +509,7 @@ Are you running an older version of the server?
return false;
}
- private static void InstantiatePlugins(Type[] types, Assembly assembly, string filePath)
+ internal static void InstantiatePlugins(Type[] types, Assembly assembly, string filePath)
{
foreach (Type type in types)
{