Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"FollowCS2ServerGuidelines": true,
"PluginHotReloadEnabled": true,
"PluginAutoLoadEnabled": true,
"PluginResolveNugetPackages": false,
"ServerLanguage": "en",
"UnlockConCommands": true,
"UnlockConVars": true,
Expand Down
36 changes: 36 additions & 0 deletions docfx/docs/features/shared-plugin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ description: How to add inter-plugin communication to CounterStrikeSharp plugins

# Shared Plugin API

> [!NOTE]
> **New (experimental)**: You can now resolve plugin dependencies directly from your local **NuGet packages cache** instead of copying every DLL into the `shared/` folder. See **Dependency Resolution** below. This feature **disabled by default.**

How to expose and use shared plugin APIs between multiple plugins.

## Creating a Contract Library
Expand Down Expand Up @@ -65,3 +68,36 @@ balance.Add(500);
```

This value _MUST_ be checked for null, as if there are no plugins providing implementations for a given capability, this method will return null, and you must handle this flow in your plugin.


## Dependency Resolution

CounterStrikeSharp supports two complementary ways to resolve **external** assemblies used by your plugins and shared contracts:

1. **Shared Folder Resolution (manual)**: copy dependency DLLs into `shared/<PackageName>/<Assembly>.dll`.
2. **NuGet Dependency Resolver (auto)**: when enabled, resolves missing assemblies from the local **NuGet packages root**

### Enabling the NuGet Resolver

Add the following property to your core config (disabled by default):

```json
{
...
"PluginResolveNugetPackages": true
...
}
```

> [!NOTE]
> The engine looks for assemblies in the NuGet cache defined by the `NUGET_PACKAGES` environment variable, or falls back to the default user cache (e.g., `~/.nuget/packages` on Linux/macOS, `%UserProfile%\.nuget\packages` on Windows).

### Dependencies Resolution Order

When the NuGet resolver is **enabled**, resolution proceeds in this general order:

1. **Plugins directory** (in-place assemblies)
2. `shared/` **folder** (existing shared assemblies mechanism)
3. **NuGet cache** (auto-resolver)

This lets you keep proven `shared/` workflows while reducing manual copying for common NuGet dependencies.
5 changes: 5 additions & 0 deletions managed/CounterStrikeSharp.API/Core/CoreConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ internal sealed partial class CoreConfigData
[JsonPropertyName("PluginAutoLoadEnabled")]
public bool PluginAutoLoadEnabled { get; set; } = true;

[JsonPropertyName("PluginResolveNugetPackages")]
public bool PluginResolveNugetPackages { get; set; }

[JsonPropertyName("ServerLanguage")]
public string ServerLanguage { get; set; } = "en";

Expand Down Expand Up @@ -115,6 +118,8 @@ public partial class CoreConfig
/// </summary>
public static bool PluginAutoLoadEnabled => _coreConfig.PluginAutoLoadEnabled;

public static bool PluginResolveNugetPackages => _coreConfig.PluginResolveNugetPackages;

public static string ServerLanguage => _coreConfig.ServerLanguage;

public static bool UnlockConCommands => _coreConfig.UnlockConCommands;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CounterStrikeSharp.API.Core.Plugin.Host;

public interface IPluginContextDependencyResolver
{
public string? ResolvePath();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Reflection;
using Microsoft.Extensions.DependencyModel;

namespace CounterStrikeSharp.API.Core.Plugin.Host;

public class PluginContextNuGetDependencyResolver : IPluginContextDependencyResolver
{
private const string NuGetPackagesEnvName = "NUGET_PACKAGES";

private readonly string _rootAssemblyName;
private readonly string _rootAssemblyPath;
private readonly AssemblyName _assemblyName;

public PluginContextNuGetDependencyResolver(string rootAssemblyName,
string rootAssemblyPath,
AssemblyName assemblyName)
{
_rootAssemblyName = rootAssemblyName;
_rootAssemblyPath = rootAssemblyPath;
_assemblyName = assemblyName;
}

public string? ResolvePath()
{
var packagesRoot = GetNuGetPackagesRoot();
if (string.IsNullOrWhiteSpace(packagesRoot))
{
return null;
}

var packageName = _assemblyName.Name;
if (string.IsNullOrWhiteSpace(packageName))
{
return null;
}

var dependenciesPath = Path.Combine(_rootAssemblyPath, $"{_rootAssemblyName}.deps.json");
if (!File.Exists(dependenciesPath))
{
return null;
}

using var dependenciesStream = File.OpenRead(dependenciesPath);

using var dependencyReader = new DependencyContextJsonReader();
var context = dependencyReader.Read(dependenciesStream);

var dependencyPath = string.Empty;
foreach (var dependency in context.RuntimeLibraries)
{
if (dependency.Name == packageName)
{
if (string.IsNullOrWhiteSpace(dependency.Path) || !dependency.RuntimeAssemblyGroups.Any())
{
return null;
}

var runtimeAssemblyGroup = dependency.RuntimeAssemblyGroups[0];
if (!runtimeAssemblyGroup.AssetPaths.Any())
{
return null;
}

dependencyPath = Path.Combine(dependency.Path, runtimeAssemblyGroup.AssetPaths[0]);
break;
}
}

if (string.IsNullOrWhiteSpace(dependencyPath))
{
return null;
}

return Path.Combine(packagesRoot, dependencyPath);
}

private static string? GetNuGetPackagesRoot()
{
var nugetPath = Environment.GetEnvironmentVariable(NuGetPackagesEnvName);
if (!string.IsNullOrWhiteSpace(nugetPath) && Directory.Exists(nugetPath))
{
return nugetPath;
}

var userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (string.IsNullOrWhiteSpace(userProfilePath))
{
return null;
}

return Path.Combine(userProfilePath, ".nuget", "packages");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
Expand Down Expand Up @@ -36,6 +37,17 @@ private void LoadLibrary(string path)
config => { config.PreferSharedTypes = true; });
var assembly = loader.LoadDefaultAssembly();

if (CoreConfig.PluginResolveNugetPackages)
{
foreach (var assemblyName in assembly.GetReferencedAssemblies())
{
if (TryLoadDependency(path, assembly.GetName().Name, assemblyName, out var dependency))
{
_sharedAssemblies.TryAdd(dependency.GetName().Name, dependency);
}
}
}

_sharedAssemblies[assembly.GetName().Name] = assembly;
}

Expand All @@ -46,7 +58,7 @@ private void LoadSharedLibraries()
.Select(dir => Path.Combine(dir, Path.GetFileName(dir) + ".dll"))
.Where(File.Exists)
.ToArray();

foreach (var sharedAssemblyPath in sharedAssemblyPaths)
{
try
Expand Down Expand Up @@ -78,6 +90,11 @@ public void Load()

if (!_sharedAssemblies.TryGetValue(name.Name, out var assembly))
{
if (CoreConfig.PluginResolveNugetPackages && TryLoadExternalLibrary(name, out assembly))
{
return assembly;
}

return null;
}

Expand All @@ -98,7 +115,7 @@ public void Load()
}
}
}

foreach (var plugin in _loadedPluginContexts)
{
try
Expand All @@ -112,6 +129,57 @@ public void Load()
}
}

private bool TryLoadExternalLibrary(AssemblyName assemblyName, out Assembly? assembly)
{
assembly = null;
if (!TryResolveReflectionAssemblyPath(out var pluginName, out var pluginPath))
{
return false;
}

if (!TryLoadDependency(pluginPath, pluginName, assemblyName, out assembly))
{
return false;
}

return true;
}

private bool TryLoadDependency(string pluginAssemblyPath,
string pluginAssemblyName,
AssemblyName dependencyAssemblyName,
out Assembly? assembly)
{
assembly = null;

var dependencyName = dependencyAssemblyName.Name!;
if (string.IsNullOrEmpty(pluginAssemblyPath) || _sharedAssemblies.ContainsKey(dependencyName))
{
return false;
}

var resolver = new PluginContextNuGetDependencyResolver(
rootAssemblyName: pluginAssemblyName,
rootAssemblyPath: Path.GetDirectoryName(pluginAssemblyPath)!,
assemblyName: dependencyAssemblyName);

var dependencyPath = resolver.ResolvePath();
if (string.IsNullOrWhiteSpace(dependencyPath))
{
return false;
}

var loader = PluginLoader.CreateFromAssemblyFile(dependencyPath, configure: c =>
{
c.PreferSharedTypes = true;
});

assembly = loader.LoadDefaultAssembly();
_sharedAssemblies[dependencyAssemblyName.Name!] = assembly;

return true;
}

public IEnumerable<PluginContext> GetLoadedPlugins()
{
return _loadedPluginContexts;
Expand All @@ -124,4 +192,27 @@ public void LoadPlugin(string path)
_loadedPluginContexts.Add(plugin);
plugin.Load();
}
}

private static bool TryResolveReflectionAssemblyPath(out string? assemblyName, out string? assemblyPath)
{
assemblyPath = null;
assemblyName = null;

if (AssemblyLoadContext.CurrentContextualReflectionContext is var reflectionContext && reflectionContext is null)
{
return false;
}

var mainAssemblyPathField = reflectionContext
.GetType()
.GetField("_mainAssemblyPath", BindingFlags.NonPublic | BindingFlags.Instance);

if (mainAssemblyPathField is null)
{
return false;
}

assemblyPath = (string)mainAssemblyPathField.GetValue(reflectionContext)!;
return !string.IsNullOrEmpty(assemblyPath);
}
}
Loading