Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions LabApi/LabApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,17 @@
<Reference Include="Mirror" HintPath="$(SL_REFERENCES)\Mirror.dll" />
<Reference Include="NorthwoodLib" HintPath="$(SL_REFERENCES)\NorthwoodLib.dll" />
<Reference Include="Pooling" HintPath="$(SL_REFERENCES)\Pooling.dll" />
<Reference Include="System.IO.Compression.FileSystem" HintPath="$(UNITY_REFERENCES)\System.IO.Compression.FileSystem.dll" />
<Reference Include="System.Net.Http" />
<Reference Include="UnityEngine.CoreModule" HintPath="$(UNITY_REFERENCES)\UnityEngine.CoreModule.dll" />
<Reference Include="UnityEngine.PhysicsModule" HintPath="$(UNITY_REFERENCES)\UnityEngine.PhysicsModule.dll" />
<Reference Include="UnityEngine.AudioModule" HintPath="$(UNITY_REFERENCES)\UnityEngine.AudioModule.dll" />
<Reference Include="mscorlib" HintPath="$(UNITY_REFERENCES)\mscorlib.dll" />
<Reference Include="System" HintPath="$(UNITY_REFERENCES)\System.dll" />
<Reference Include="System.Core" HintPath="$(UNITY_REFERENCES)\System.Core.dll" />
<Reference Include="System.IO.Compression" HintPath="$(UNITY_REFERENCES)\System.IO.Compression.dll" />
<Reference Include="System.Xml.Linq" HintPath="$(UNITY_REFERENCES)\System.Xml.Linq.dll" />
<Reference Include="System.Runtime.Serialization" HintPath="$(UNITY_REFERENCES)\System.Runtime.Serialization.dll" />
<Reference Include="netstandard" HintPath="$(UNITY_REFERENCES)\netstandard.dll" />
</ItemGroup>

Expand Down
24 changes: 24 additions & 0 deletions LabApi/Loader/Features/Configuration/LabApiConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,28 @@ public class LabApiConfig
/// <seealso cref="LabApi.Loader.Features.Plugins.Configuration.Properties.UnsupportedLoading"/>
[Description("Whether to allow loading plugins even if they were built for a different major version of LabAPI.")]
public bool LoadUnsupportedPlugins { get; set; }

/// <summary>
/// Gets or sets the list of NuGet package source URLs used when resolving
/// and downloading dependencies from NuGet repositories.
/// </summary>
/// <remarks>
/// Each entry in this list represents a NuGet feed endpoint (for example,
/// the official <c>https://api.nuget.org/v3/index.json</c> source).
/// Multiple sources can be specified to support private or custom feeds.
/// </remarks>
[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"];

/// <summary>
/// Gets or sets a value indicating whether dependencies should be automatically
/// downloaded from the configured NuGet sources when they are missing or outdated.
/// </summary>
/// <remarks>
/// When set to <see langword="true"/>, the system attempts to retrieve and install
/// required packages automatically during dependency resolution.
/// Disabling this option may require manual dependency management.
/// </remarks>
[Description("Automatically download dependencies from NuGet when missing or outdated.")]
public bool AutomaticallyDownloadDependencies { get; set; } = true;
}
89 changes: 89 additions & 0 deletions LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using LabApi.Features.Console;
using System;
using System.Reflection;

namespace LabApi.Loader.Features.NuGet.Models;

/// <summary>
/// Represents a dependency entry within a NuGet package,
/// including its identifier and version, and provides
/// helper methods for installation and status checking.
/// </summary>
public class NuGetDependency
{
/// <summary>
/// Gets or sets the unique identifier (name) of the NuGet dependency.
/// </summary>
public required string Id { get; set; }

/// <summary>
/// Gets or sets the semantic version string of the dependency (e.g. "1.2.3").
/// </summary>
public required string Version { get; set; }

/// <summary>
/// Installs this NuGet dependency by downloading it from the configured source.
/// </summary>
/// <returns>
/// A <see cref="NuGetPackage"/> instance representing the downloaded package,
/// or <see langword="null"/> if the installation failed or the package could not be found.
/// </returns>
/// <remarks>
/// This method delegates to <see cref="NuGetPackagesManager.DownloadNugetPackage(string, string)"/>.
/// </remarks>
public NuGetPackage? Install() => NuGetPackagesManager.DownloadNugetPackage(Id, Version);

/// <summary>
/// Determines whether this dependency is already installed
/// or loaded in the current AppDomain.
/// </summary>
/// <returns>
/// <c>true</c> if the dependency is installed or the corresponding assembly is already loaded;
/// otherwise, <c>false</c>.
/// </returns>
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;
}

/// <summary>
/// Checks whether an assembly with the given identifier is already loaded
/// into the current application domain.
/// </summary>
/// <param name="id">The dependency or assembly identifier to check.</param>
/// <returns>
/// <c>true</c> if an assembly with the specified ID is already loaded;
/// otherwise, <c>false</c>.
/// </returns>
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;
}
}
173 changes: 173 additions & 0 deletions LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a NuGet package loaded by LabApi, including its metadata,
/// content, and dependency information.
/// </summary>
public class NuGetPackage
{
/// <summary>
/// Identifies the plugin tag used to mark NuGet package as plugin.
/// </summary>
private const string LabApiPluginTag = "labapi-plugin";

/// <summary>
/// Gets or sets the unique package identifier (name).
/// </summary>
public string Id { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the package version string (e.g. "1.2.3").
/// </summary>
public string Version { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the tag metadata from the .nuspec file (used to identify plugins, etc.).
/// </summary>
public string Tags { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the compiled assembly content of the package (if applicable).
/// </summary>
public byte[]? RawAssembly { get; set; } = null;

/// <summary>
/// Gets or sets the full path of NuGet package.
/// </summary>
public string FilePath { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the name of the NuGet package file (e.g. "MyPlugin.1.0.0.nupkg").
/// </summary>
public required string FileName { get; set; }

/// <summary>
/// Gets or sets the raw file contents of the NuGet package (.nupkg file).
/// </summary>
public required byte[] FileContent { get; set; }

/// <summary>
/// Gets or sets the list of dependencies defined by this package.
/// </summary>
public List<NuGetDependency> Dependencies { get; set; } = new List<NuGetDependency>();

/// <summary>
/// Gets a value indicating whether this package is marked as a LabApi plugin.
/// Determined by the presence of the "labapi-plugin" tag.
/// </summary>
public bool IsPlugin => Tags
.ToLower()
.Contains(LabApiPluginTag);

/// <summary>
/// Gets or sets a value indicating whether the package is already loaded.
/// </summary>
public bool IsLoaded { get; set; }

/// <summary>
/// Extracts the NuGet package file (.nupkg) to the appropriate directory
/// (plugins or dependencies), depending on whether it is a plugin.
/// </summary>
/// <returns>
/// The full path to the extracted file if successful; otherwise, <c>null</c>.
/// </returns>
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;
}

/// <summary>
/// Loads package.
/// </summary>
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;
}

/// <summary>
/// Resolves and returns the final folder path for the package extraction,
/// creating directories if necessary.
/// </summary>
/// <returns>
/// The full directory path where the package should be extracted,
/// or <c>null</c> if no valid path could be determined.
/// </returns>
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;
}
}
26 changes: 26 additions & 0 deletions LabApi/Loader/Features/NuGet/Models/NuGetPackageIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Runtime.Serialization;

namespace LabApi.Loader.Features.NuGet.Models;

/// <summary>
/// Represents the root structure of a NuGet service index (<c>index.json</c>),
/// which describes the available service endpoints for a NuGet source.
/// </summary>
/// <remarks>
/// The NuGet service index (usually located at <c>https://api.nuget.org/v3/index.json</c>)
/// provides metadata about the repository’s available APIs, such as
/// <c>PackageBaseAddress</c>, <c>SearchQueryService</c>, and others.
/// </remarks>
public class NuGetPackageIndex
{
/// <summary>
/// Gets or sets the list of service resources exposed by the NuGet source.
/// </summary>
/// <remarks>
/// 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 <see cref="NuGetPackageResource"/> type.
/// </remarks>
[DataMember(Name = "resources")]
public NuGetPackageResource[] Resources { get; set; } = [];
}
37 changes: 37 additions & 0 deletions LabApi/Loader/Features/NuGet/Models/NuGetPackageResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Runtime.Serialization;

namespace LabApi.Loader.Features.NuGet.Models;

/// <summary>
/// Represents a single resource entry within a NuGet service index (<c>index.json</c>).
/// </summary>
/// <remarks>
/// Each resource describes a specific NuGet service endpoint and its purpose,
/// such as the <c>PackageBaseAddress</c> (used to download packages) or
/// <c>SearchQueryService</c> (used to search packages).
/// </remarks>
public class NuGetPackageResource
{
/// <summary>
/// Gets or sets the type of the NuGet service resource.
/// </summary>
/// <remarks>
/// The <c>@type</c> field defines the role of the resource, for example:
/// <list type="bullet">
/// <item><description><c>PackageBaseAddress/3.0.0</c> — base URL for downloading package files</description></item>
/// </list>
/// </remarks>
[DataMember(Name = "@type")]
public string Type { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the absolute URL of the service endpoint.
/// </summary>
/// <remarks>
/// The <c>@id</c> value is typically a fully qualified HTTPS URL that identifies
/// the service’s base address. For example:
/// <c>https://api.nuget.org/v3-flatcontainer/</c> or
/// </remarks>
[DataMember(Name = "@id")]
public string Id { get; set; } = string.Empty;
}
Loading