Compare commits

...

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
851a317db5 Initial plan 2025-11-04 06:43:51 +00:00
Ravid Atia
55542dba7c feat: allow plugins to be loaded from subdirectories (#1031)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2025-10-30 05:27:42 +00:00
Michael Wilson
33538eca60 release: v1.0.345 2025-10-30 01:41:23 +00:00
Michael Wilson
b4e83dfb4a fix: update linux signature for GetCSWeaponDataFromKey 2025-10-30 11:40:04 +10:00
roflmuffin
4ff2732d8a feat(schema): update schema generator to use @GAMMACASE schema dumper format 2025-10-27 06:48:05 +00:00
9 changed files with 305541 additions and 116722 deletions

View File

@@ -1,3 +1,7 @@
## What's Changed in v1.0.345
* fix: update linux signature for GetCSWeaponDataFromKey ([b4e83df](https://github.com/roflmuffin/CounterStrikeSharp/commit/b4e83dfb4a1a1723c08ac79ea68da4ab8a0255fd))
* feat(schema): update schema generator to use @GAMMACASE schema dumper format ([4ff2732](https://github.com/roflmuffin/CounterStrikeSharp/commit/4ff2732d8a55297c18cea6181b9022f56cd8fae3))
## What's Changed in v1.0.344
* chore(schema): update schema to latest ([f505405](https://github.com/roflmuffin/CounterStrikeSharp/commit/f50540583d079a6cf546ca590147905ba5eb2c83))
* fix(schema): allow for negative enum values in source schema file ([97957f6](https://github.com/roflmuffin/CounterStrikeSharp/commit/97957f62208fa782f89e9629ddde1944aaadd149))

View File

@@ -92,7 +92,7 @@
"signatures": {
"library": "server",
"windows": "48 89 5C 24 ? 57 48 83 EC ? 33 FF 4C 8B CA 8B D9",
"linux": "55 31 D2 48 89 E5 41 57 41 56 41 55 41 54 41 89 FC"
"linux": "55 31 D2 48 89 E5 41 56 41 55 41 54"
}
},
"CCSPlayer_ItemServices_GiveNamedItem": {

View File

@@ -187,7 +187,13 @@ namespace CounterStrikeSharp.API.Core
// If our argument doesn't end in ".dll" - try and construct a path similar to PluginName/PluginName.dll.
// We'll assume we have a full path if we have ".dll".
var path = info.GetArg(2);
path = Path.Combine(_scriptHostConfiguration.RootPath, !path.EndsWith(".dll") ? $"plugins/{path}/{path}.dll" : path);
path = Path.Combine(
_scriptHostConfiguration.RootPath,
!path.EndsWith(".dll")
? $"plugins/{path}/{Path.GetFileName(path)}.dll"
: path
);
var plugin = _pluginContextQueryHandler.FindPluginByModulePath(path);

View File

@@ -1,218 +1,251 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Core.Hosting;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core.Plugin.Host;
public class PluginManager : IPluginManager
{
private readonly HashSet<PluginContext> _loadedPluginContexts = new();
private readonly IScriptHostConfiguration _scriptHostConfiguration;
private readonly ICommandManager _commandManager;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<PluginManager> _logger;
private readonly Dictionary<string, Assembly> _sharedAssemblies = new();
private bool _loadedSharedLibs = false;
public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ICommandManager commandManager,
ILogger<PluginManager> logger, IServiceProvider serviceProvider, IServiceScopeFactory serviceScopeFactory)
{
_scriptHostConfiguration = scriptHostConfiguration;
_commandManager = commandManager;
_logger = logger;
_serviceProvider = serviceProvider;
}
private void LoadLibrary(string path)
{
var loader = PluginLoader.CreateFromAssemblyFile(path, new[] { typeof(IPlugin), typeof(PluginCapability<>), typeof(PlayerCapability<>) },
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;
}
private void LoadSharedLibraries()
{
var sharedDirectory = Directory.GetDirectories(_scriptHostConfiguration.SharedPath);
var sharedAssemblyPaths = sharedDirectory
.Select(dir => Path.Combine(dir, Path.GetFileName(dir) + ".dll"))
.Where(File.Exists)
.ToArray();
foreach (var sharedAssemblyPath in sharedAssemblyPaths)
{
try
{
LoadLibrary(sharedAssemblyPath);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load shared assembly from {Path}", sharedAssemblyPath);
}
}
}
public void Load()
{
var pluginDirectories = Directory.GetDirectories(_scriptHostConfiguration.PluginPath);
var pluginAssemblyPaths = pluginDirectories
.Select(dir => Path.Combine(dir, Path.GetFileName(dir) + ".dll"))
.Where(File.Exists)
.ToArray();
AssemblyLoadContext.Default.Resolving += (context, name) =>
{
if (!_loadedSharedLibs)
{
LoadSharedLibraries();
_loadedSharedLibs = true;
}
if (!_sharedAssemblies.TryGetValue(name.Name, out var assembly))
{
if (CoreConfig.PluginResolveNugetPackages && TryLoadExternalLibrary(name, out assembly))
{
return assembly;
}
return null;
}
return assembly;
};
if (CoreConfig.PluginAutoLoadEnabled)
{
foreach (var path in pluginAssemblyPaths)
{
try
{
LoadPlugin(path);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load plugin from {Path}", path);
}
}
}
foreach (var plugin in _loadedPluginContexts)
{
try
{
plugin.Plugin?.OnAllPluginsLoaded(false);
}
catch (Exception e)
{
_logger.LogError(e, "OnAllPluginsLoaded failed");
}
}
}
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;
}
public void LoadPlugin(string path)
{
var plugin = new PluginContext(_serviceProvider, _commandManager, _scriptHostConfiguration, path,
_loadedPluginContexts.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1);
_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);
}
}
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Core.Hosting;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core.Plugin.Host;
public class PluginManager : IPluginManager
{
private readonly HashSet<PluginContext> _loadedPluginContexts = new();
private readonly IScriptHostConfiguration _scriptHostConfiguration;
private readonly ICommandManager _commandManager;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<PluginManager> _logger;
private readonly Dictionary<string, Assembly> _sharedAssemblies = new();
private bool _loadedSharedLibs = false;
public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ICommandManager commandManager,
ILogger<PluginManager> logger, IServiceProvider serviceProvider, IServiceScopeFactory serviceScopeFactory)
{
_scriptHostConfiguration = scriptHostConfiguration;
_commandManager = commandManager;
_logger = logger;
_serviceProvider = serviceProvider;
}
private void LoadLibrary(string path)
{
var loader = PluginLoader.CreateFromAssemblyFile(path, new[] { typeof(IPlugin), typeof(PluginCapability<>), typeof(PlayerCapability<>) },
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;
}
private void LoadSharedLibraries()
{
var sharedDirectory = Directory.GetDirectories(_scriptHostConfiguration.SharedPath);
var sharedAssemblyPaths = sharedDirectory
.Select(dir => Path.Combine(dir, Path.GetFileName(dir) + ".dll"))
.Where(File.Exists)
.ToArray();
foreach (var sharedAssemblyPath in sharedAssemblyPaths)
{
try
{
LoadLibrary(sharedAssemblyPath);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load shared assembly from {Path}", sharedAssemblyPath);
}
}
}
public void Load()
{
var pluginAssemblyPaths = GetPluginsAssemblyPaths();
AssemblyLoadContext.Default.Resolving += (context, name) =>
{
if (!_loadedSharedLibs)
{
LoadSharedLibraries();
_loadedSharedLibs = true;
}
if (!_sharedAssemblies.TryGetValue(name.Name, out var assembly))
{
if (CoreConfig.PluginResolveNugetPackages && TryLoadExternalLibrary(name, out assembly))
{
return assembly;
}
return null;
}
return assembly;
};
if (CoreConfig.PluginAutoLoadEnabled)
{
foreach (var path in pluginAssemblyPaths)
{
try
{
LoadPlugin(path);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load plugin from {Path}", path);
}
}
}
foreach (var plugin in _loadedPluginContexts)
{
try
{
plugin.Plugin?.OnAllPluginsLoaded(false);
}
catch (Exception e)
{
_logger.LogError(e, "OnAllPluginsLoaded failed");
}
}
}
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;
}
public void LoadPlugin(string path)
{
var plugin = new PluginContext(_serviceProvider, _commandManager, _scriptHostConfiguration, path,
_loadedPluginContexts.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1);
_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);
}
private string[] GetPluginsAssemblyPaths()
{
// Skip "disabled" at root level
var rootSubDirs = Directory.GetDirectories(_scriptHostConfiguration.PluginPath)
.Where(d => !Path.GetFileName(d).Equals("disabled", StringComparison.OrdinalIgnoreCase));
var pluginDirectories = new List<string>();
foreach (var subDir in rootSubDirs)
{
var stack = new Stack<string>();
stack.Push(subDir);
while (stack.Count > 0)
{
var currentDir = stack.Pop();
var dirName = Path.GetFileName(currentDir);
var expectedDll = Path.Combine(currentDir, dirName + ".dll");
if (File.Exists(expectedDll))
{
pluginDirectories.Add(currentDir);
// Stop scanning deeper in this directory
continue;
}
// Add subdirectories to stack for further scanning
foreach (var child in Directory.GetDirectories(currentDir))
stack.Push(child);
}
}
return pluginDirectories
.Select(d => Path.Combine(d, Path.GetFileName(d) + ".dll"))
.ToArray();
}
}

View File

@@ -0,0 +1,16 @@
// <auto-generated />
#nullable enable
#pragma warning disable CS1591
using System;
namespace CounterStrikeSharp.API.Core;
public enum PulseTestEnumColor_t : uint
{
BLACK = 0x0,
WHITE = 0x1,
RED = 0x2,
GREEN = 0x3,
BLUE = 0x4,
}

View File

@@ -0,0 +1,14 @@
// <auto-generated />
#nullable enable
#pragma warning disable CS1591
using System;
namespace CounterStrikeSharp.API.Core;
public enum PulseTestEnumShape_t : uint
{
CIRCLE = 0x64,
SQUARE = 0xC8,
TRIANGLE = 0x12C,
}

View File

@@ -0,0 +1,74 @@
namespace CounterStrikeSharp.SchemaGen;
public record NewSchemaModule(
GameInfo game_info,
DumperInfo dumper_info,
string[] dump_flags,
SchemaDef[] defs);
public record GameInfo(
string ClientVersion,
string ServerVersion,
string PatchVersion,
string ProductName,
string appID,
string ServerAppID,
string SourceRevision,
string VersionDate,
string VersionTime);
public record DumperInfo(
string version,
string dump_date,
int dump_format_version);
public record SchemaDef(
string type,
string name,
string? scope,
string? project,
int? size,
int? alignment,
SchemaTraits? traits);
public record SchemaTraits(
int? parent_class_idx,
string[]? flags,
SchemaMetaTag[]? metatags,
int? multi_depth,
int? single_depth,
SchemaBaseClass[]? baseclasses,
SchemaMember[]? members,
SchemaEnumField[]? fields);
public record SchemaMetaTag(
string name,
string? value);
public record SchemaBaseClass(
int offset,
int ref_idx);
public record SchemaMember(
string name,
int offset,
MemberTraits? traits);
public record MemberTraits(
SchemaMetaTag[]? metatags,
SchemaSubtype? subtype);
public record SchemaSubtype(
string type,
string? name,
int? size,
int? alignment,
SchemaSubtype[]? template,
int? ref_idx,
int? element_size,
int? count,
SchemaSubtype? subtype);
public record SchemaEnumField(
string name,
long value);

View File

@@ -44,7 +44,142 @@ internal static partial class Program
"Unknown"
};
public static string SanitiseTypeName(string typeName) => typeName.Replace(":", "");
public static string SanitiseTypeName(string typeName) =>
typeName.Replace(":", "")
.Replace("< ", "<")
.Replace(" >", ">");
private static (Dictionary<string, SchemaEnum>, Dictionary<string, SchemaClass>) ConvertNewSchemaToOld(NewSchemaModule newSchema)
{
var enums = new Dictionary<string, SchemaEnum>();
var classes = new Dictionary<string, SchemaClass>();
var defLookup = newSchema.defs.Select((def, idx) => new { def, idx }).ToDictionary(x => x.idx, x => x.def);
for (int i = 0; i < newSchema.defs.Length; i++)
{
var def = newSchema.defs[i];
if (def.type == "enum" && def.traits?.fields != null)
{
var enumItems = def.traits.fields.Select(f => new SchemaEnumItem(f.name, f.value)).ToList();
enums[def.name] = new SchemaEnum(def.alignment ?? 4, enumItems);
}
}
for (int i = 0; i < newSchema.defs.Length; i++)
{
var def = newSchema.defs[i];
if (def.type == "class" && def.traits != null)
{
string? parentName = null;
if (def.traits.baseclasses != null && def.traits.baseclasses.Length > 0)
{
var parentIdx = def.traits.baseclasses[0].ref_idx;
if (defLookup.TryGetValue(parentIdx, out var parentDef))
{
parentName = parentDef.name;
}
}
var fields = new List<SchemaField>();
if (def.traits.members != null)
{
foreach (var member in def.traits.members)
{
if (member.traits?.subtype != null)
{
var fieldType = ConvertSubtypeToFieldType(member.traits.subtype, defLookup);
var metadata = member.traits.metatags?.ToDictionary(m => m.name, m => m.value ?? "") ??
new Dictionary<string, string>();
fields.Add(new SchemaField(member.name, fieldType, metadata));
}
}
}
classes[def.name] = new SchemaClass(i, def.name, parentName, fields);
}
}
return (enums, classes);
}
private static SchemaFieldType ConvertSubtypeToFieldType(SchemaSubtype subtype, Dictionary<int, SchemaDef> defLookup)
{
if (subtype.type == "ref" && subtype.ref_idx.HasValue)
{
if (defLookup.TryGetValue(subtype.ref_idx.Value, out var referencedDef))
{
return ConvertSubtypeToFieldType(new SchemaSubtype(
referencedDef.type == "class"
? "declared_class"
: (referencedDef.type == "enum" ? "declared_enum" : referencedDef.type),
referencedDef.name,
referencedDef.size,
referencedDef.alignment,
null,
null,
null,
null,
null
), defLookup);
}
}
SchemaTypeCategory category = subtype.type switch
{
"builtin" => SchemaTypeCategory.Builtin,
"atomic" => SchemaTypeCategory.Atomic,
"ptr" => SchemaTypeCategory.Ptr,
"fixed_array" => SchemaTypeCategory.FixedArray,
"declared_class" => SchemaTypeCategory.DeclaredClass,
"declared_enum" => SchemaTypeCategory.DeclaredEnum,
"bitfield" => SchemaTypeCategory.Bitfield,
_ => SchemaTypeCategory.None
};
SchemaAtomicCategory? atomic = null;
if (subtype.type == "atomic" && subtype.name != null)
{
if (subtype.name.Contains("CUtlVector") || subtype.name.Contains("CNetworkUtlVectorBase"))
{
atomic = SchemaAtomicCategory.Collection;
}
else if (subtype.name.Contains("CHandle") || subtype.name.Contains("CWeakHandle"))
{
atomic = SchemaAtomicCategory.T;
}
else
{
atomic = SchemaAtomicCategory.Basic;
}
}
SchemaFieldType? innerType = null;
if (subtype.template != null && subtype.template.Length > 0)
{
innerType = ConvertSubtypeToFieldType(subtype.template[0], defLookup);
}
else if (subtype.subtype != null)
{
innerType = ConvertSubtypeToFieldType(subtype.subtype, defLookup);
}
string typeName = subtype.name ?? "unknown";
if (category == SchemaTypeCategory.FixedArray && subtype.count.HasValue && innerType != null)
{
typeName = $"{innerType.Name}[{subtype.count.Value}]";
}
return new SchemaFieldType(
typeName,
category,
atomic,
innerType
);
}
private static StringBuilder GetTemplate(bool includeUsings)
{
@@ -89,16 +224,18 @@ internal static partial class Program
{
var schemaPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Schema", schemaFile);
var schema = JsonSerializer.Deserialize<SchemaModule>(
var newSchema = JsonSerializer.Deserialize<NewSchemaModule>(
File.ReadAllText(schemaPath),
SerializerOptions)!;
foreach (var (enumName, schemaEnum) in schema.Enums)
var (enums, classes) = ConvertNewSchemaToOld(newSchema);
foreach (var (enumName, schemaEnum) in enums)
{
allEnums[enumName] = schemaEnum;
}
foreach (var (className, schemaClass) in schema.Classes)
foreach (var (className, schemaClass) in classes)
{
if (IgnoreClasses.Contains(className))
continue;
@@ -209,7 +346,6 @@ internal static partial class Program
visited.Add("DecalGroupOption_t");
visited.Add("DestructibleHitGroupToDestroy_t");
var classBuilder = GetTemplate(true);
var visitedClassNames = new HashSet<string>();

File diff suppressed because one or more lines are too long