Compare commits

...

17 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
Michael Wilson
67d3d044fd release: v1.0.344 2025-10-27 05:36:18 +00:00
roflmuffin
f50540583d chore(schema): update schema to latest 2025-10-27 05:15:04 +00:00
roflmuffin
97957f6220 fix(schema): allow for negative enum values in source schema file 2025-10-27 05:14:15 +00:00
roflmuffin
0c2f1cd078 [no ci] chore: update devcontainer location 2025-10-22 20:18:59 +10:00
Michael Wilson
9eefe9c61a release: v1.0.343 2025-10-20 01:03:13 +00:00
Nocky
7be329466a feat: add BuyWithCtrl to AcquireMethod enum
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2025-10-19 23:25:46 +00:00
Matlord93
a21f0b5277 fix: update ConVar flag retrieval that adapts to different Source 2 SDK versions (#1059)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2025-10-18 08:10:39 +00:00
dxqshka
b4ba7d8ca0 feat(experimental): add NuGet Dependency Resolver for Plugins (#1012) 2025-10-18 07:35:40 +00:00
Michal
0eb73eb348 feat: add FindVirtualTable method (#1075) 2025-10-18 10:59:33 +10:00
Markus
43c1c89596 feat: use shared libgcc and libc++ (#1007)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2025-10-17 06:53:42 +00:00
宇宙 猫
53996666f8 feat: implement TerminateSelf(string reason) to allow plugins to safely terminate themselves (#1047)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2025-10-17 06:30:59 +00:00
schwarper
a8510d183d feat: add core translations & processtargetstring (#1051) 2025-10-17 16:19:49 +10:00
49 changed files with 306543 additions and 116801 deletions

View File

@@ -18,4 +18,4 @@
"ghcr.io/devcontainers/features/dotnet": "8.0",
"ghcr.io/devcontainers/features/node": "lts"
}
}
}

View File

@@ -1,3 +1,26 @@
## 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))
* chore: update devcontainer location ([0c2f1cd](https://github.com/roflmuffin/CounterStrikeSharp/commit/0c2f1cd078c0c54a6cf1fb402082b3f38c6303d2))
## What's Changed in v1.0.343
* feat: add `BuyWithCtrl` to `AcquireMethod` enum by [@NockyCZ](https://github.com/NockyCZ) in [#697](https://github.com/roflmuffin/CounterStrikeSharp/pull/697) ([7be3294](https://github.com/roflmuffin/CounterStrikeSharp/commit/7be329466ad7d40a92608e7d6c4e2c6cd1a05a3c))
* fix: update ConVar flag retrieval that adapts to different Source 2 SDK versions by [@Matlord93](https://github.com/Matlord93) in [#1059](https://github.com/roflmuffin/CounterStrikeSharp/pull/1059) ([a21f0b5](https://github.com/roflmuffin/CounterStrikeSharp/commit/a21f0b5277541434fa71f595d7c0c420305e9a50))
* feat(experimental): add NuGet Dependency Resolver for Plugins by [@dxqwww](https://github.com/dxqwww) in [#1012](https://github.com/roflmuffin/CounterStrikeSharp/pull/1012) ([b4ba7d8](https://github.com/roflmuffin/CounterStrikeSharp/commit/b4ba7d8ca02bdf487ee9424f2bdb119510ab1d2c))
* feat: add FindVirtualTable method by [@SlynxCZ](https://github.com/SlynxCZ) in [#1075](https://github.com/roflmuffin/CounterStrikeSharp/pull/1075) ([0eb73eb](https://github.com/roflmuffin/CounterStrikeSharp/commit/0eb73eb3487f7c0200b14c58b34aaa39b2408e29))
* feat: use shared libgcc and libc++ by [@markus-wa](https://github.com/markus-wa) in [#1007](https://github.com/roflmuffin/CounterStrikeSharp/pull/1007) ([43c1c89](https://github.com/roflmuffin/CounterStrikeSharp/commit/43c1c8959605ccafa54f8fc155ef3e37016ed7f6))
* feat: implement `TerminateSelf(string reason)` to allow plugins to safely terminate themselves by [@ELDment](https://github.com/ELDment) in [#1047](https://github.com/roflmuffin/CounterStrikeSharp/pull/1047) ([5399666](https://github.com/roflmuffin/CounterStrikeSharp/commit/53996666f8fbc99a989af5e79dae710912439115))
* feat: add core translations & processtargetstring by [@schwarper](https://github.com/schwarper) in [#1051](https://github.com/roflmuffin/CounterStrikeSharp/pull/1051) ([a8510d1](https://github.com/roflmuffin/CounterStrikeSharp/commit/a8510d183d1edc6dd9ed97536def64a4d219c135))
## New Contributors
* [@NockyCZ](https://github.com/NockyCZ) made their first contribution in [#697](https://github.com/roflmuffin/CounterStrikeSharp/pull/697)
* [@Matlord93](https://github.com/Matlord93) made their first contribution in [#1059](https://github.com/roflmuffin/CounterStrikeSharp/pull/1059)
* [@dxqwww](https://github.com/dxqwww) made their first contribution in [#1012](https://github.com/roflmuffin/CounterStrikeSharp/pull/1012)
## What's Changed in v1.0.342
* fix: update Sigs & CTakeDamageResult & EmitSound_t by [@himenekocn](https://github.com/himenekocn) in [#1071](https://github.com/roflmuffin/CounterStrikeSharp/pull/1071) ([34598dd](https://github.com/roflmuffin/CounterStrikeSharp/commit/34598dd56ea2e9e18229185dc225db00a336bb5d))

View File

@@ -4,6 +4,7 @@
"FollowCS2ServerGuidelines": true,
"PluginHotReloadEnabled": true,
"PluginAutoLoadEnabled": true,
"PluginResolveNugetPackages": false,
"ServerLanguage": "en",
"UnlockConCommands": true,
"UnlockConVars": true,

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

@@ -0,0 +1,23 @@
{
"menu.button.previous": "Prev",
"menu.button.next": "Next",
"menu.button.close": "Close",
"all": "all players",
"bots": "bots",
"humans": "humans",
"alive": "alive players",
"dead": "dead players",
"notme": "all players except self",
"ct": "ct players",
"t": "t players",
"spec": "spec players",
"No matching client": "No matching client was found.",
"No matching clients": "No matching clients were found.",
"Target must be alive": "This command can only be used on alive players.",
"Target must be dead": "This command can only be used on dead players.",
"Unable to target": "You cannot target this player.",
"Cannot target bot": "Unable to perform this command on a bot.",
"More than one client matched": "More than one client matched the given pattern."
}

View File

@@ -0,0 +1,23 @@
{
"menu.button.previous": "Geri",
"menu.button.next": "İleri",
"menu.button.close": ıkış",
"all": "tüm oyuncular",
"bots": "botlar",
"humans": "insanlar",
"alive": "hayatta olan oyuncular",
"dead": "ölü oyuncular",
"notme": "kendi hariç tüm oyuncular",
"ct": "CT oyuncular",
"t": "T oyuncular",
"spec": "izleyici oyuncular",
"No matching client": "{white}Eşleşen bir istemci bulunamadı.",
"No matching clients": "{white}Eşleşen istemciler bulunamadı.",
"Target must be alive": "{white}Bu komut yalnızca hayatta olan oyunculara uygulanabilir.",
"Target must be dead": "{white}Bu komut yalnızca ölü oyunculara uygulanabilir.",
"Unable to target": "{white}Bu oyuncu hedeflenemez.",
"Cannot target bot": "{white}Bu komut bir bota uygulanamaz.",
"More than one client matched": "{white}Verilen kalıba birden fazla istemci eşleşti."
}

View File

@@ -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
@@ -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.

View File

@@ -14,7 +14,6 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof -Wno-reorder")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfpmath=sse -msse -fno-strict-aliasing")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-threadsafe-statics -v -fvisibility=default")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libgcc -static-libstdc++")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs=libprotobuf.a")
set(

View File

@@ -11,8 +11,10 @@ using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Serilog;
@@ -44,7 +46,11 @@ public static class Bootstrap
services.AddSingleton<IPluginManager, PluginManager>();
services.AddSingleton<IPlayerLanguageManager, PlayerLanguageManager>();
services.AddScoped<IPluginContextQueryHandler, PluginContextQueryHandler>();
services.AddSingleton<ICommandManager, CommandManager>();
services.AddSingleton<ICommandManager, CommandManager>();
services.TryAddSingleton<IStringLocalizerFactory, CoreJsonStringLocalizerFactory>();
services.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
services.TryAddTransient(typeof(IStringLocalizer), typeof(StringLocalizer));
services.Scan(i => i.FromCallingAssembly()
.AddClasses(c => c.AssignableTo<IStartupService>())
@@ -71,4 +77,4 @@ public static class Bootstrap
return 0;
}
}
}
}

View File

@@ -8,7 +8,7 @@ using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Core
{
public class NativeAPI {
public static bool AddListener(string name, InputArgument callback){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
@@ -1459,6 +1459,18 @@ namespace CounterStrikeSharp.API.Core
}
}
public static IntPtr FindVirtualTable(string modulepath, string vtablename){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(modulepath);
ScriptContext.GlobalScriptContext.Push(vtablename);
ScriptContext.GlobalScriptContext.SetIdentifier(0xB4A0F13C);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (IntPtr)ScriptContext.GlobalScriptContext.GetResult(typeof(IntPtr));
}
}
public static int GetNetworkVectorSize(IntPtr vec){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();

View File

@@ -17,6 +17,7 @@
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Plugin;
@@ -28,6 +29,7 @@ using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -35,6 +37,8 @@ namespace CounterStrikeSharp.API.Core
public sealed class Application
{
private static Application _instance = null!;
public static IStringLocalizer Localizer => Instance._localizer;
public ILogger Logger { get; }
public static Application Instance => _instance!;
@@ -48,11 +52,12 @@ namespace CounterStrikeSharp.API.Core
private readonly IPluginContextQueryHandler _pluginContextQueryHandler;
private readonly IPlayerLanguageManager _playerLanguageManager;
private readonly ICommandManager _commandManager;
private readonly IStringLocalizer _localizer;
public Application(ILoggerFactory loggerFactory, IScriptHostConfiguration scriptHostConfiguration,
GameDataProvider gameDataProvider, CoreConfig coreConfig, IPluginManager pluginManager,
IPluginContextQueryHandler pluginContextQueryHandler, IPlayerLanguageManager playerLanguageManager,
ICommandManager commandManager)
ICommandManager commandManager, IStringLocalizer localizer)
{
Logger = loggerFactory.CreateLogger("Core");
_scriptHostConfiguration = scriptHostConfiguration;
@@ -62,11 +67,28 @@ namespace CounterStrikeSharp.API.Core
_pluginContextQueryHandler = pluginContextQueryHandler;
_playerLanguageManager = playerLanguageManager;
_commandManager = commandManager;
_localizer = localizer;
_instance = this;
}
public void Start()
{
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
if ((e.ExceptionObject as Exception) is PluginTerminationException pluginEx)
{
return;
}
};
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
if (e.Exception.InnerExceptions.Any(ex => ex is PluginTerminationException))
{
e.SetObserved();
}
};
Logger.LogInformation("CounterStrikeSharp is starting up...");
_coreConfig.Load();
@@ -122,123 +144,135 @@ namespace CounterStrikeSharp.API.Core
switch (info.GetArg(1))
{
case "list":
{
info.ReplyToCommand(
$" List of all plugins currently loaded by CounterStrikeSharp: {_pluginManager.GetLoadedPlugins().Count()} plugins loaded.");
foreach (var plugin in _pluginManager.GetLoadedPlugins())
{
var sb = new StringBuilder();
sb.AppendFormat(" [#{0}:{1}]: \"{2}\" ({3})", plugin.PluginId,
plugin.State.ToString().ToUpper(), plugin.Plugin?.ModuleName ?? "Unknown",
plugin.Plugin?.ModuleVersion ?? "Unknown");
if (!string.IsNullOrEmpty(plugin.Plugin?.ModuleAuthor))
sb.AppendFormat(" by {0}", plugin.Plugin.ModuleAuthor);
if (!string.IsNullOrEmpty(plugin.Plugin?.ModuleDescription))
{
sb.Append("\n");
sb.Append(" ");
sb.Append(plugin.Plugin.ModuleDescription);
}
info.ReplyToCommand(sb.ToString());
}
break;
}
case "start":
case "load":
{
if (info.ArgCount < 3)
{
info.ReplyToCommand(
"Valid usage: css_plugins start/load [relative plugin path || absolute plugin path] (e.g \"TestPlugin\", \"plugins/TestPlugin/TestPlugin.dll\")\n");
$" List of all plugins currently loaded by CounterStrikeSharp: {_pluginManager.GetLoadedPlugins().Count()} plugins loaded.");
foreach (var plugin in _pluginManager.GetLoadedPlugins())
{
var sb = new StringBuilder();
sb.AppendFormat(" [#{0}:{1}]: \"{2}\" ({3})", plugin.PluginId,
plugin.State.ToString().ToUpper(), plugin.Plugin?.ModuleName ?? "Unknown",
plugin.Plugin?.ModuleVersion ?? "Unknown");
if (!string.IsNullOrEmpty(plugin.Plugin?.ModuleAuthor))
sb.AppendFormat(" by {0}", plugin.Plugin.ModuleAuthor);
if (!string.IsNullOrEmpty(plugin.Plugin?.ModuleDescription))
{
sb.Append("\n");
sb.Append(" ");
sb.Append(plugin.Plugin.ModuleDescription);
}
if (plugin.State == PluginState.Unloaded && !string.IsNullOrEmpty(plugin.TerminationReason))
{
sb.Append("\n");
sb.AppendFormat(" Termination Reason: {0}", plugin.TerminationReason);
}
info.ReplyToCommand(sb.ToString());
}
break;
}
// 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);
var plugin = _pluginContextQueryHandler.FindPluginByModulePath(path);
if (plugin == null)
case "start":
case "load":
{
try
if (info.ArgCount < 3)
{
_pluginManager.LoadPlugin(path);
plugin = _pluginContextQueryHandler.FindPluginByModulePath(path);
info.ReplyToCommand(
"Valid usage: css_plugins start/load [relative plugin path || absolute plugin path] (e.g \"TestPlugin\", \"plugins/TestPlugin/TestPlugin.dll\")\n");
break;
}
// 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.GetFileName(path)}.dll"
: path
);
var plugin = _pluginContextQueryHandler.FindPluginByModulePath(path);
if (plugin == null)
{
try
{
_pluginManager.LoadPlugin(path);
plugin = _pluginContextQueryHandler.FindPluginByModulePath(path);
plugin.Plugin.OnAllPluginsLoaded(false);
}
catch (Exception e)
{
info.ReplyToCommand($"Could not load plugin \"{path}\"");
Logger.LogError(e, "Could not load plugin \"{Path}\"", path);
}
}
else
{
plugin.Load(false);
plugin.Plugin.OnAllPluginsLoaded(false);
}
catch (Exception e)
{
info.ReplyToCommand($"Could not load plugin \"{path}\"");
Logger.LogError(e, "Could not load plugin \"{Path}\"", path);
}
}
else
{
plugin.Load(false);
plugin.Plugin.OnAllPluginsLoaded(false);
}
break;
}
break;
}
case "stop":
case "unload":
{
if (info.ArgCount < 3)
{
info.ReplyToCommand(
"Valid usage: css_plugins stop/unload [plugin name || #plugin id] (e.g \"TestPlugin\", \"1\")\n");
if (info.ArgCount < 3)
{
info.ReplyToCommand(
"Valid usage: css_plugins stop/unload [plugin name || #plugin id] (e.g \"TestPlugin\", \"1\")\n");
break;
}
var pluginIdentifier = info.GetArg(2);
string path;
path = Path.Combine(_scriptHostConfiguration.RootPath,
!pluginIdentifier.EndsWith(".dll") ? $"plugins/{pluginIdentifier}/{pluginIdentifier}.dll" : pluginIdentifier);
var plugin = _pluginContextQueryHandler.FindPluginByIdOrName(pluginIdentifier)
?? _pluginContextQueryHandler.FindPluginByModulePath(path);
if (plugin == null)
{
info.ReplyToCommand($"Could not unload plugin \"{pluginIdentifier}\"");
break;
}
plugin.Unload(false);
break;
}
var pluginIdentifier = info.GetArg(2);
string path;
path = Path.Combine(_scriptHostConfiguration.RootPath,
!pluginIdentifier.EndsWith(".dll") ? $"plugins/{pluginIdentifier}/{pluginIdentifier}.dll" : pluginIdentifier);
var plugin = _pluginContextQueryHandler.FindPluginByIdOrName(pluginIdentifier)
?? _pluginContextQueryHandler.FindPluginByModulePath(path);
if (plugin == null)
{
info.ReplyToCommand($"Could not unload plugin \"{pluginIdentifier}\"");
break;
}
plugin.Unload(false);
break;
}
case "restart":
case "reload":
{
if (info.ArgCount < 3)
{
info.ReplyToCommand(
"Valid usage: css_plugins restart/reload [plugin name || #plugin id] (e.g \"TestPlugin\", \"#1\")\n");
if (info.ArgCount < 3)
{
info.ReplyToCommand(
"Valid usage: css_plugins restart/reload [plugin name || #plugin id] (e.g \"TestPlugin\", \"#1\")\n");
break;
}
var pluginIdentifier = info.GetArg(2);
var plugin = _pluginContextQueryHandler.FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not reload plugin \"{pluginIdentifier}\"");
break;
}
plugin.Unload(true);
plugin.Load(true);
plugin.Plugin.OnAllPluginsLoaded(true);
break;
}
var pluginIdentifier = info.GetArg(2);
var plugin = _pluginContextQueryHandler.FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not reload plugin \"{pluginIdentifier}\"");
break;
}
plugin.Unload(true);
plugin.Load(true);
plugin.Plugin.OnAllPluginsLoaded(true);
break;
}
default:
info.ReplyToCommand("Valid usage: css_plugins [option]\n" +
" list - List all plugins currently loaded.\n" +

View File

@@ -53,20 +53,27 @@ namespace CounterStrikeSharp.API.Core
public abstract string ModuleName { get; }
public abstract string ModuleVersion { get; }
public virtual string ModuleAuthor { get; }
public virtual string ModuleDescription { get; }
public string ModulePath { get; set; }
public string ModuleDirectory => Path.GetDirectoryName(ModulePath);
public ILogger Logger { get; set; }
public ICommandManager CommandManager { get; set; }
public IStringLocalizer Localizer { get; set; }
internal Plugin.ISelfPluginControl SelfControl { get; set; }
public void TerminateSelf(string reason)
{
SelfControl?.TerminateSelf(reason);
}
public virtual void Load(bool hotReload)
{
}
@@ -74,7 +81,7 @@ namespace CounterStrikeSharp.API.Core
public virtual void Unload(bool hotReload)
{
}
public virtual void OnAllPluginsLoaded(bool hotReload)
{
}
@@ -116,7 +123,7 @@ namespace CounterStrikeSharp.API.Core
public readonly Dictionary<Delegate, CallbackSubscriber> Handlers =
new Dictionary<Delegate, CallbackSubscriber>();
public readonly Dictionary<Delegate, CallbackSubscriber> CommandListeners =
new Dictionary<Delegate, CallbackSubscriber>();
@@ -132,7 +139,7 @@ namespace CounterStrikeSharp.API.Core
public readonly List<CommandDefinition> CommandDefinitions = new List<CommandDefinition>();
public readonly List<Timer> Timers = new List<Timer>();
public delegate HookResult GameEventHandler<T>(T @event, GameEventInfo info) where T : GameEvent;
private void RegisterEventHandlerInternal<T>(string name, GameEventHandler<T> handler, bool post)
@@ -156,7 +163,7 @@ namespace CounterStrikeSharp.API.Core
var name = typeof(T).GetCustomAttribute<EventNameAttribute>()?.Name;
RegisterEventHandlerInternal(name, handler, hookMode == HookMode.Post);
}
/// <summary>
/// De-registers a game event handler.
/// </summary>
@@ -164,7 +171,7 @@ namespace CounterStrikeSharp.API.Core
public void DeregisterEventHandler<T>(GameEventHandler<T> handler, HookMode hookMode = HookMode.Post) where T : GameEvent
{
var name = typeof(T).GetCustomAttribute<EventNameAttribute>()!.Name;
if (!Handlers.TryGetValue(handler, out var subscriber)) return;
NativeAPI.UnhookEvent(name, subscriber.GetInputArgument(), hookMode == HookMode.Post);
@@ -195,7 +202,7 @@ namespace CounterStrikeSharp.API.Core
CommandDefinitions.Add(definition);
CommandManager.RegisterCommand(definition);
}
private void AddCommand(CommandDefinition definition)
{
CommandDefinitions.Add(definition);
@@ -319,9 +326,9 @@ namespace CounterStrikeSharp.API.Core
throw new ArgumentException("Listener of type T is invalid and does not have a name attribute",
nameof(T));
}
if (!Listeners.TryGetValue(handler, out var subscriber)) return;
NativeAPI.RemoveListener(listenerName, subscriber.GetInputArgument());
FunctionReference.Remove(subscriber.GetReferenceIdentifier());
Listeners.Remove(handler);
@@ -408,7 +415,7 @@ namespace CounterStrikeSharp.API.Core
.Where(method =>
method.GetParameters().FirstOrDefault()?.ParameterType.IsSubclassOf(typeof(GameEvent)) == true)
.ToArray();
var listenerHandlers = methods
.Where(method => method.GetCustomAttribute(typeof(ListenerHandlerAttribute<>)) != null)
.ToArray();
@@ -440,7 +447,7 @@ namespace CounterStrikeSharp.API.Core
throw new ArgumentException("Listener of type T is invalid and does not have a name attribute",
listenerType.Name);
var listenerDelegate = Delegate.CreateDelegate(listenerType, instance, listnerHandler);
var listenerDelegate = Delegate.CreateDelegate(listenerType, instance, listnerHandler);
registerListener.MakeGenericMethod(listenerType).Invoke(this, [listenerDelegate]);
}
@@ -502,29 +509,29 @@ namespace CounterStrikeSharp.API.Core
{
var convars = type
.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
.Where(prop => prop.FieldType.IsGenericType &&
.Where(prop => prop.FieldType.IsGenericType &&
prop.FieldType.GetGenericTypeDefinition() == typeof(FakeConVar<>));
foreach (var prop in convars)
{
object propValue = prop.GetValue(instance); // FakeConvar<?> instance
var propValueType = prop.FieldType.GenericTypeArguments[0];
var name = prop.FieldType.GetProperty("Name", BindingFlags.Public | BindingFlags.Instance)
.GetValue(propValue);
var description = prop.FieldType.GetProperty("Description", BindingFlags.Public | BindingFlags.Instance)
.GetValue(propValue);
MethodInfo executeCommandMethod = prop.FieldType
.GetMethod("ExecuteCommand", BindingFlags.Instance | BindingFlags.NonPublic);
this.AddCommand((string)name, (string) description, (caller, command) =>
this.AddCommand((string)name, (string)description, (caller, command) =>
{
executeCommandMethod.Invoke(propValue, new object[] {caller, command});
executeCommandMethod.Invoke(propValue, new object[] { caller, command });
});
}
}
/// <summary>
/// Used to bind a fake ConVar to a plugin command. Only required for ConVars that are not public properties of the plugin class.
/// </summary>
@@ -549,7 +556,7 @@ namespace CounterStrikeSharp.API.Core
NativeAPI.HookEntityOutput(classname, outputName, subscriber.GetInputArgument(), mode);
EntityOutputHooks[handler] = subscriber;
}
public void HookUserMessage(int messageId, UserMessage.UserMessageHandler handler, HookMode mode = HookMode.Pre)
{
var subscriber = new CallbackSubscriber(handler, handler,
@@ -558,7 +565,7 @@ namespace CounterStrikeSharp.API.Core
NativeAPI.HookUsermessage(messageId, subscriber.GetInputArgument(), mode);
Handlers[handler] = subscriber;
}
public void UnhookUserMessage(int messageId, UserMessage.UserMessageHandler handler, HookMode mode = HookMode.Pre)
{
if (!Handlers.TryGetValue(handler, out var subscriber)) return;
@@ -641,7 +648,7 @@ namespace CounterStrikeSharp.API.Core
{
subscriber.Dispose();
}
foreach (var subscriber in CommandListeners.Values)
{
subscriber.Dispose();

View File

@@ -53,6 +53,9 @@ namespace CounterStrikeSharp.API.Core
[JsonPropertyName("PluginAutoLoadEnabled")]
public bool PluginAutoLoadEnabled { get; set; } = true;
[JsonPropertyName("PluginResolveNugetPackages")]
public bool PluginResolveNugetPackages { get; set; }
[JsonPropertyName("ServerLanguage")]
public string ServerLanguage { get; set; } = "en";
@@ -115,6 +118,8 @@ namespace CounterStrikeSharp.API.Core
/// </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;

View File

@@ -1,4 +1,4 @@
/*
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -107,6 +107,11 @@ namespace CounterStrikeSharp.API.Core
}
catch (Exception e)
{
if ((e.InnerException ?? e) is Plugin.PluginTerminationException pluginEx)
{
return;
}
Application.Instance.Logger.LogError(e, "Error invoking callback");
}
finally

View File

@@ -34,5 +34,11 @@ public interface IScriptHostConfiguration
/// Gets the absolute path to the directory that contains CounterStrikeSharp game data.
/// e.g. /game/csgo/addons/counterstrikesharp/gamedata
/// </summary>
string GameDataPath { get; }
}
string GameDataPath { get; }
/// <summary>
/// Gets the absolute path to the directory that contains CounterStrikeSharp translation files.
/// e.g. /game/csgo/addons/counterstrikesharp/lang
/// </summary>
string LanguagePath { get; }
}

View File

@@ -9,7 +9,8 @@ internal sealed class ScriptHostConfiguration : IScriptHostConfiguration
public string PluginPath { get; }
public string SharedPath { get; }
public string ConfigsPath { get; }
public string GameDataPath { get; }
public string GameDataPath { get; }
public string LanguagePath { get; }
public ScriptHostConfiguration(IHostEnvironment hostEnvironment)
{
@@ -17,6 +18,7 @@ internal sealed class ScriptHostConfiguration : IScriptHostConfiguration
SharedPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "shared" });
PluginPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "plugins" });
ConfigsPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "configs" });
GameDataPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "gamedata" });
GameDataPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "gamedata" });
LanguagePath = Path.Join(new[] { hostEnvironment.ContentRootPath, "lang" });
}
}
}

View File

@@ -0,0 +1,6 @@
namespace CounterStrikeSharp.API.Core.Plugin.Host;
public interface IPluginContextDependencyResolver
{
public string? ResolvePath();
}

View File

@@ -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");
}
}

View File

@@ -1,127 +1,251 @@
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();
_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))
{
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");
}
}
}
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();
}
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

@@ -31,10 +31,17 @@ using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using System.Threading;
using System;
namespace CounterStrikeSharp.API.Core.Plugin
{
public class PluginContext : IPluginContext
public interface ISelfPluginControl
{
void TerminateSelf(string reason);
}
public class PluginContext : IPluginContext, ISelfPluginControl
{
public PluginState State { get; set; } = PluginState.Unregistered;
public IPlugin Plugin { get; private set; }
@@ -50,10 +57,12 @@ namespace CounterStrikeSharp.API.Core.Plugin
private readonly string _path;
private readonly FileSystemWatcher _fileWatcher;
private readonly IServiceProvider _applicationServiceProvider;
public string FilePath => _path;
private IServiceScope _serviceScope;
public string TerminationReason { get; private set; }
// TOOD: ServiceCollection
private ILogger _logger = CoreLogging.Factory.CreateLogger<PluginContext>();
@@ -65,7 +74,7 @@ namespace CounterStrikeSharp.API.Core.Plugin
_hostConfiguration = hostConfiguration;
_path = path;
PluginId = id;
Loader = PluginLoader.CreateFromAssemblyFile(path,
new[]
{
@@ -77,7 +86,7 @@ namespace CounterStrikeSharp.API.Core.Plugin
config.IsUnloadable = true;
config.PreferSharedTypes = true;
});
if (CoreConfig.PluginHotReloadEnabled)
{
_fileWatcher = new FileSystemWatcher
@@ -113,14 +122,14 @@ namespace CounterStrikeSharp.API.Core.Plugin
Load(hotReload: true);
Plugin.OnAllPluginsLoaded(hotReload: true);
});
return Task.CompletedTask;
}
public void Load(bool hotReload = false)
{
if (State == PluginState.Loaded) return;
using (Loader.EnterContextualReflection())
{
var defaultAssembly = Loader.LoadDefaultAssembly();
@@ -178,7 +187,7 @@ namespace CounterStrikeSharp.API.Core.Plugin
method?.Invoke(pluginServiceCollection, new object[] { serviceCollection });
}
}
serviceCollection.AddScoped<ICommandManager>(c => _commandManager);
serviceCollection.DecorateSingleton<ICommandManager, PluginCommandManagerDecorator>();
@@ -215,7 +224,33 @@ namespace CounterStrikeSharp.API.Core.Plugin
Plugin.Logger = ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(pluginType);
Plugin.InitializeConfig(Plugin, pluginType);
Plugin.Load(hotReload);
if (Plugin is BasePlugin basePlugin)
{
basePlugin.SelfControl = this;
}
this.TerminationReason = string.Empty;
try
{
Plugin.Load(hotReload);
}
catch (Exception ex)
{
if ((ex.InnerException ?? ex) is PluginTerminationException pluginEx)
{
_logger.LogCritical("Terminating plugin {Name} with reason: {Reason}", Plugin.ModuleName, pluginEx.TerminationReason);
this.TerminationReason = pluginEx.TerminationReason;
}
else
{
_logger.LogError(ex, "Failed to load plugin {Name}", Plugin.ModuleName);
this.TerminationReason = ex.Message ?? "Unknown";
}
Unload(hotReload);
return;
}
_logger.LogInformation("Finished loading plugin {Name}", Plugin.ModuleName);
@@ -233,12 +268,60 @@ namespace CounterStrikeSharp.API.Core.Plugin
_logger.LogInformation("Unloading plugin {Name}", Plugin.ModuleName);
Plugin.Unload(hotReload);
Plugin.Dispose();
_serviceScope.Dispose();
try
{
Plugin.Unload(hotReload);
}
catch
{
_logger.LogError("Failed to unload {Name} during error recovery, forcing cleanup", Plugin.ModuleName);
return;
}
finally
{
Plugin.Dispose();
_serviceScope.Dispose();
}
_logger.LogInformation("Finished unloading plugin {Name}", cachedName);
}
public void TerminateWithReason(string reason)
{
this.TerminationReason = reason;
switch (State)
{
case PluginState.Unloaded:
case PluginState.Loading:
break;
case PluginState.Loaded:
_logger.LogInformation("Terminating plugin {Name} with reason: {Reason}", Plugin.ModuleName, reason);
Unload(false);
break;
}
// Force execution flow interruption via globally-handled exception to prevent stack unwinding
throw new PluginTerminationException(reason);
}
void ISelfPluginControl.TerminateSelf(string reason)
{
if (State != PluginState.Unloaded)
{
if (Thread.CurrentThread.IsThreadPoolThread)
{
Server.NextFrame(() => TerminateWithReason(reason));
}
else
{
TerminateWithReason(reason);
}
// **Failsafe mechanism** ensures execution termination
// Prevents control flow leakage back to plugin execution context
throw new NotImplementedException();
}
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
namespace CounterStrikeSharp.API.Core.Plugin
{
public class PluginTerminationException : Exception
{
public string PluginName { get; }
public string TerminationReason { get; }
public PluginTerminationException(string reason) : base($"Plugin terminated: {reason}")
{
TerminationReason = reason;
}
public PluginTerminationException(string pluginName, string reason) : base($"Plugin '{pluginName}' terminated: {reason}")
{
PluginName = pluginName;
TerminationReason = reason;
}
public PluginTerminationException(string reason, Exception innerException) : base($"Plugin terminated: {reason}", innerException)
{
TerminationReason = reason;
}
public PluginTerminationException(string pluginName, string reason, Exception innerException) : base($"Plugin '{pluginName}' terminated: {reason}", innerException)
{
PluginName = pluginName;
TerminationReason = reason;
}
}
}

View File

@@ -66,6 +66,10 @@ public partial class CCSGameRules : CTeamplayRules
[SchemaMember("CCSGameRules", "m_bMatchWaitingForResume")]
public ref bool MatchWaitingForResume => ref Schema.GetRef<bool>(this.Handle, "CCSGameRules", "m_bMatchWaitingForResume");
// m_iFreezeTime
[SchemaMember("CCSGameRules", "m_iFreezeTime")]
public ref Int32 FreezeTime => ref Schema.GetRef<Int32>(this.Handle, "CCSGameRules", "m_iFreezeTime");
// m_iRoundTime
[SchemaMember("CCSGameRules", "m_iRoundTime")]
public ref Int32 RoundTime => ref Schema.GetRef<Int32>(this.Handle, "CCSGameRules", "m_iRoundTime");
@@ -350,10 +354,6 @@ public partial class CCSGameRules : CTeamplayRules
[SchemaMember("CCSGameRules", "m_endMatchOnThink")]
public ref bool EndMatchOnThink => ref Schema.GetRef<bool>(this.Handle, "CCSGameRules", "m_endMatchOnThink");
// m_iFreezeTime
[SchemaMember("CCSGameRules", "m_iFreezeTime")]
public ref Int32 FreezeTime => ref Schema.GetRef<Int32>(this.Handle, "CCSGameRules", "m_iFreezeTime");
// m_iNumTerrorist
[SchemaMember("CCSGameRules", "m_iNumTerrorist")]
public ref Int32 NumTerrorist => ref Schema.GetRef<Int32>(this.Handle, "CCSGameRules", "m_iNumTerrorist");

View File

@@ -24,6 +24,6 @@ public partial class CCSPlayer_PingServices : CPlayerPawnComponent
// m_hPlayerPing
[SchemaMember("CCSPlayer_PingServices", "m_hPlayerPing")]
public CHandle<CBaseEntity> PlayerPing => Schema.GetDeclaredClass<CHandle<CBaseEntity>>(this.Handle, "CCSPlayer_PingServices", "m_hPlayerPing");
public CHandle<CPlayerPing> PlayerPing => Schema.GetDeclaredClass<CHandle<CPlayerPing>>(this.Handle, "CCSPlayer_PingServices", "m_hPlayerPing");
}

View File

@@ -0,0 +1,21 @@
// <auto-generated />
#nullable enable
#pragma warning disable CS1591
using System;
using System.Diagnostics;
using System.Drawing;
using CounterStrikeSharp;
using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using CounterStrikeSharp.API.Core.Attributes;
namespace CounterStrikeSharp.API.Core;
public partial class CFuncRetakeBarrier : CDynamicProp
{
public CFuncRetakeBarrier (IntPtr pointer) : base(pointer) {}
}

View File

@@ -38,6 +38,10 @@ public partial class CPropDoorRotating : CBasePropDoor
[SchemaMember("CPropDoorRotating", "m_eCurrentOpenDirection")]
public ref PropDoorRotatingOpenDirection_e CurrentOpenDirection => ref Schema.GetRef<PropDoorRotatingOpenDirection_e>(this.Handle, "CPropDoorRotating", "m_eCurrentOpenDirection");
// m_eDefaultCheckDirection
[SchemaMember("CPropDoorRotating", "m_eDefaultCheckDirection")]
public ref doorCheck_e DefaultCheckDirection => ref Schema.GetRef<doorCheck_e>(this.Handle, "CPropDoorRotating", "m_eDefaultCheckDirection");
// m_flAjarAngle
[SchemaMember("CPropDoorRotating", "m_flAjarAngle")]
public ref float AjarAngle => ref Schema.GetRef<float>(this.Handle, "CPropDoorRotating", "m_flAjarAngle");

View File

@@ -38,4 +38,8 @@ public partial class CRetakeGameRules : NativeObject
[SchemaMember("CRetakeGameRules", "m_iBombSite")]
public ref Int32 BombSite => ref Schema.GetRef<Int32>(this.Handle, "CRetakeGameRules", "m_iBombSite");
// m_hBombPlanter
[SchemaMember("CRetakeGameRules", "m_hBombPlanter")]
public CHandle<CCSPlayerPawn> BombPlanter => Schema.GetDeclaredClass<CHandle<CCSPlayerPawn>>(this.Handle, "CRetakeGameRules", "m_hBombPlanter");
}

View File

@@ -26,6 +26,10 @@ public partial class CTakeDamageResult : NativeObject
[SchemaMember("CTakeDamageResult", "m_nHealthLost")]
public ref Int32 HealthLost => ref Schema.GetRef<Int32>(this.Handle, "CTakeDamageResult", "m_nHealthLost");
// m_nHealthBefore
[SchemaMember("CTakeDamageResult", "m_nHealthBefore")]
public ref Int32 HealthBefore => ref Schema.GetRef<Int32>(this.Handle, "CTakeDamageResult", "m_nHealthBefore");
// m_nDamageDealt
[SchemaMember("CTakeDamageResult", "m_nDamageDealt")]
public ref Int32 DamageDealt => ref Schema.GetRef<Int32>(this.Handle, "CTakeDamageResult", "m_nDamageDealt");

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

@@ -8,7 +8,7 @@ namespace CounterStrikeSharp.API.Core;
public enum loadout_slot_t : uint
{
LOADOUT_SLOT_PROMOTED = 0xFFFFFFFF,
LOADOUT_SLOT_PROMOTED = 0xFFFFFFFE,
LOADOUT_SLOT_INVALID = 0xFFFFFFFF,
LOADOUT_SLOT_MELEE = 0x0,
LOADOUT_SLOT_C4 = 0x1,

View File

@@ -0,0 +1,24 @@
using CounterStrikeSharp.API.Core.Hosting;
using Microsoft.Extensions.Localization;
namespace CounterStrikeSharp.API.Core.Translations;
public class CoreJsonStringLocalizerFactory : IStringLocalizerFactory
{
private IScriptHostConfiguration _scriptHostConfiguration;
public CoreJsonStringLocalizerFactory(IScriptHostConfiguration scriptHostConfiguration)
{
_scriptHostConfiguration = scriptHostConfiguration;
}
public IStringLocalizer Create(Type resourceSource)
{
return new JsonStringLocalizer(_scriptHostConfiguration.LanguagePath);
}
public IStringLocalizer Create(string baseName, string location)
{
return new JsonStringLocalizer(_scriptHostConfiguration.LanguagePath);
}
}

View File

@@ -70,7 +70,7 @@ public class JsonStringLocalizer : IStringLocalizer
result = _resourceManager.GetFallbackString(name);
}
// Fallback to the default culture (en-US) if the resource is not found for the current culture.
// Fallback to the default culture (whatever is in core.json) if the resource is not found for the current culture.
if (result == null && !culture.Equals(CultureInfo.DefaultThreadCurrentUICulture))
{
result = _resourceManager.GetString(name, CultureInfo.DefaultThreadCurrentUICulture!);
@@ -119,4 +119,4 @@ public class JsonStringLocalizer : IStringLocalizer
return resourceNames;
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
namespace CounterStrikeSharp.API.Modules.Commands.Targeting;
/// <summary>
/// Specifies filters for processing command targets.
/// </summary>
[Flags]
public enum ProcessTargetFilterFlag
{
/// <summary>
/// No filter applied.
/// </summary>
None = 0,
/// <summary>
/// Only allow alive players as targets.
/// </summary>
FilterAlive = 1 << 0,
/// <summary>
/// Only allow dead players as targets.
/// </summary>
FilterDead = 1 << 1,
/// <summary>
/// Filter out targets that the command issuer cannot target due to immunity rules.
/// </summary>
FilterNoImmunity = 1 << 2,
/// <summary>
/// Do not allow multiple target patterns like @all, @ct, etc.
/// </summary>
FilterNoMulti = 1 << 3,
/// <summary>
/// Do not allow bots to be targeted.
/// </summary>
FilterNoBots = 1 << 4
}

View File

@@ -0,0 +1,65 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
namespace CounterStrikeSharp.API.Modules.Commands.Targeting;
/// <summary>
/// Represents the result of a target processing operation.
/// </summary>
public enum ProcessTargetResultFlag
{
/// <summary>
/// Target(s) were successfully found and filtered.
/// </summary>
TargetFound,
/// <summary>
/// No target was found matching the initial pattern.
/// </summary>
TargetNone,
/// <summary>
/// A single target was found, but they were not alive as required by the filter.
/// Or a multi-target filter resulted in no alive players.
/// </summary>
TargetNotAlive,
/// <summary>
/// A single target was found, but they were not dead as required by the filter.
/// Or a multi-target filter resulted in no dead players.
/// </summary>
TargetNotDead,
/// <summary>
/// The target is immune and cannot be targeted by the command issuer.
/// </summary>
TargetImmune,
/// <summary>
/// A multi-target filter (like @all) resulted in no players after filtering.
/// </summary>
TargetEmptyFilter,
/// <summary>
/// The target was found, but it was not a human player as required by the filter.
/// </summary>
TargetNotHuman,
/// <summary>
/// The target string was ambiguous and matched more than one player when a single target was expected.
/// </summary>
TargetAmbiguous
}

View File

@@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
@@ -120,4 +122,155 @@ public class Target
return new TargetResult() { Players = Utilities.GetPlayers().Where(player => TargetPredicate(player, caller, _gameRulesEntity?.GameRules)).ToList() };
}
/// <summary>
/// Processes a target string, finds matching players, and applies specified filters.
/// </summary>
/// <param name="player">The player who executed the command.</param>
/// <param name="targetString">The target string (e.g., player name, #userid, @all).</param>
/// <param name="filter">Flags to filter the found targets.</param>
/// <param name="tnIsMl">If true, the target name buffer will be an ML phrase. Otherwise, it will be normal string.</param>
/// <param name="targetname">
/// An output list that will contain the resolved target names. These may be localization keys
/// (e.g., "all", "ct") if <paramref name="tnIsMl"/> is true, or actual player names otherwise.
/// </param>
/// <param name="players">An output list that will be populated with the player entities matching the target string.</param>
public static ProcessTargetResultFlag ProcessTargetString(CCSPlayerController? player,
string targetString, ProcessTargetFilterFlag filter, bool tnIsMl,
out string targetname, out List<CCSPlayerController> players)
{
targetname = string.Empty;
players = new Target(targetString).GetTarget(player).Players;
if (players.Count == 0)
{
return ProcessTargetResultFlag.TargetNone;
}
if (players.Count > 1 && filter.HasFlag(ProcessTargetFilterFlag.FilterNoMulti))
{
return ProcessTargetResultFlag.TargetAmbiguous;
}
if (filter.HasFlag(ProcessTargetFilterFlag.FilterNoImmunity))
{
players.RemoveAll(target => player != null && !AdminManager.CanPlayerTarget(new SteamID(player.SteamID), new SteamID(target.SteamID)));
if (players.Count == 0)
{
return ProcessTargetResultFlag.TargetImmune;
}
}
if (filter.HasFlag(ProcessTargetFilterFlag.FilterNoBots))
{
players.RemoveAll(p => p.IsBot);
if (players.Count == 0)
{
return ProcessTargetResultFlag.TargetNotHuman;
}
}
if (filter.HasFlag(ProcessTargetFilterFlag.FilterAlive))
{
players.RemoveAll(p => p.PlayerPawn.Value?.LifeState != (byte)LifeState_t.LIFE_ALIVE);
if (players.Count == 0)
{
return ProcessTargetResultFlag.TargetNotAlive;
}
}
if (filter.HasFlag(ProcessTargetFilterFlag.FilterDead))
{
players.RemoveAll(p => p.PlayerPawn.Value?.LifeState == (byte)LifeState_t.LIFE_ALIVE);
if (players.Count == 0)
{
return ProcessTargetResultFlag.TargetNotDead;
}
}
if (tnIsMl)
{
if (!TargetTypeMap.TryGetValue(targetString, out TargetType type))
type = TargetType.PlayerMe;
targetname = type switch
{
TargetType.GroupAll => "all",
TargetType.GroupBots => "bots",
TargetType.GroupHumans => "humans",
TargetType.GroupAlive => "alive",
TargetType.GroupDead => "dead",
TargetType.GroupNotMe => "notme",
TargetType.TeamCt => "ct",
TargetType.TeamT => "t",
TargetType.TeamSpec => "spec",
_ => players[0].PlayerName
};
}
else
{
targetname = string.Join(", ", players.Select(p => p.PlayerName));
}
return ProcessTargetResultFlag.TargetFound;
}
/// <summary>
/// Wraps ProcessTargetString() and handles producing error messages for bad targets.
/// </summary>
/// <param name="player">The player who executed the command.</param>
/// <param name="targetString">The target string (e.g., player name, #userid, @all).</param>
/// <param name="nobots">Optional. Set to true if bots should NOT be targetted</param>
/// <param name="immunity">Optional. Set to false to ignore target immunity.</param>
public static CCSPlayerController? FindTarget(CCSPlayerController player, string targetString, bool nobots = false, bool immunity = true)
{
var filter = ProcessTargetFilterFlag.FilterNoMulti;
if (nobots)
filter |= ProcessTargetFilterFlag.FilterNoBots;
if (!immunity)
filter |= ProcessTargetFilterFlag.FilterNoImmunity;
ProcessTargetResultFlag result;
if ((result = ProcessTargetString(player, targetString, filter, false, out var targetname, out var players)) == ProcessTargetResultFlag.TargetFound)
return players[0];
ReplyToTargetError(player, result);
return null;
}
/// <summary>
/// Replies to a client with a given message describing a targetting failure reason.
/// </summary>
/// <param name="player">The player who executed the command.</param>
/// <param name="resultFlag">The <see cref="ProcessTargetResultFlag"/> value indicating why it is failed.</param>
public static void ReplyToTargetError(CCSPlayerController player, ProcessTargetResultFlag resultFlag)
{
switch (resultFlag)
{
case ProcessTargetResultFlag.TargetNone:
player.PrintToChat(Application.Localizer["No matching client"]);
break;
case ProcessTargetResultFlag.TargetEmptyFilter:
player.PrintToChat(Application.Localizer["No matching clients"]);
break;
case ProcessTargetResultFlag.TargetNotAlive:
player.PrintToChat(Application.Localizer["Target must be alive"]);
break;
case ProcessTargetResultFlag.TargetNotDead:
player.PrintToChat(Application.Localizer["Target must be dead"]);
break;
case ProcessTargetResultFlag.TargetImmune:
player.PrintToChat(Application.Localizer["Unable to target"]);
break;
case ProcessTargetResultFlag.TargetNotHuman:
player.PrintToChat(Application.Localizer["Cannot target bot"]);
break;
case ProcessTargetResultFlag.TargetAmbiguous:
player.PrintToChat(Application.Localizer["More than one client matched"]);
break;
}
}
}

View File

@@ -101,19 +101,19 @@ public class CenterHtmlMenuInstance : BaseMenuInstance
if (HasPrevButton)
{
builder.AppendFormat($"<font color='{centerHtmlMenu.PrevPageColor}'>!7</font> &#60;- Prev");
builder.AppendFormat($"<font color='{centerHtmlMenu.PrevPageColor}'>!7</font> &#60;- {Application.Localizer["menu.button.previous"]}");
builder.AppendLine("<br>");
}
if (HasNextButton)
{
builder.AppendFormat($"<font color='{centerHtmlMenu.NextPageColor}'>!8</font> -> Next");
builder.AppendFormat($"<font color='{centerHtmlMenu.NextPageColor}'>!8</font> -> {Application.Localizer["menu.button.next"]}");
builder.AppendLine("<br>");
}
if (centerHtmlMenu.ExitButton)
{
builder.AppendFormat($"<font color='{centerHtmlMenu.CloseColor}'>!9</font> -> Close");
builder.AppendFormat($"<font color='{centerHtmlMenu.CloseColor}'>!9</font> -> {Application.Localizer["menu.button.close"]}");
builder.AppendLine("<br>");
}
@@ -135,4 +135,4 @@ public class CenterHtmlMenuInstance : BaseMenuInstance
var onTick = new Core.Listeners.OnTick(Display);
_plugin.RemoveListener("OnTick", onTick);
}
}
}

View File

@@ -63,17 +63,17 @@ public class ChatMenuInstance : BaseMenuInstance
if (HasPrevButton)
{
Player.PrintToChat($" {chatMenu.PrevPageColor}!7 {ChatColors.Default}-> Prev");
Player.PrintToChat($" {chatMenu.PrevPageColor}!7 {ChatColors.Default}-> {Application.Localizer["menu.button.previous"]}");
}
if (HasNextButton)
{
Player.PrintToChat($" {chatMenu.NextPageColor}!8 {ChatColors.Default}-> Next");
Player.PrintToChat($" {chatMenu.NextPageColor}!8 {ChatColors.Default}-> {Application.Localizer["menu.button.next"]}");
}
if (Menu.ExitButton)
{
Player.PrintToChat($" {chatMenu.CloseColor}!9 {ChatColors.Default}-> Close");
Player.PrintToChat($" {chatMenu.CloseColor}!9 {ChatColors.Default}-> {Application.Localizer["menu.button.close"]}");
}
}
}
@@ -91,4 +91,4 @@ public static class ChatMenus
{
MenuManager.OnKeyPress(player, key);
}
}
}

View File

@@ -52,17 +52,17 @@ public class ConsoleMenuInstance : BaseMenuInstance
if (HasPrevButton)
{
Player.PrintToConsole("css_7 -> Prev");
Player.PrintToConsole($"css_7 -> {Application.Localizer["menu.button.previous"]}");
}
if (HasNextButton)
{
Player.PrintToConsole("css_8 -> Next");
Player.PrintToConsole($"css_8 -> {Application.Localizer["menu.button.next"]}");
}
if (Menu.ExitButton)
{
Player.PrintToConsole("css_9 -> Close");
Player.PrintToConsole($"css_9 -> {Application.Localizer["menu.button.close"]}");
}
}
}
}

View File

@@ -20,4 +20,5 @@ public enum AcquireMethod : int
{
PickUp = 0,
Buy,
BuyWithCtrl,
};

View File

@@ -21,6 +21,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;

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

@@ -5,7 +5,6 @@
*/
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using QuickGraph;
@@ -45,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)
{
@@ -77,8 +211,8 @@ internal static partial class Program
public static void Main(string[] args)
{
var outputPath =
args.SingleOrDefault() ??
throw new Exception("Expected a single CLI argument: <output path .cs>");
args.FirstOrDefault() ??
"../CounterStrikeSharp.API/Core/Schema";
// Concat together all enums and classes
var allEnums = new SortedDictionary<string, SchemaEnum>();
@@ -90,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;
@@ -199,6 +335,7 @@ internal static partial class Program
// Manually whitelist some classes
visited.Add("CTakeDamageInfo");
visited.Add("CTakeDamageResult");
visited.Add("CEntitySubclassVDataBase");
visited.Add("CFiringModeFloat");
visited.Add("CFiringModeInt");
@@ -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>();
@@ -302,7 +438,8 @@ internal static partial class Program
if (IgnoreClasses.Contains(field.Type.Inner!.Name)) continue;
}
var requiresNewKeyword = parentFields.Any(x => x.clazz.CsPropertyNameForField(x.clazz.Name, x.field) == schemaClass.CsPropertyNameForField(schemaClassName, field));
var requiresNewKeyword = parentFields.Any(x =>
x.clazz.CsPropertyNameForField(x.clazz.Name, x.field) == schemaClass.CsPropertyNameForField(schemaClassName, field));
var handleParams = $"this.Handle, \"{schemaClassName}\", \"{field.Name}\"";
@@ -314,7 +451,7 @@ internal static partial class Program
var getter = $"return Schema.GetString({handleParams});";
var setter = $"Schema.SetString({handleParams}, value{(field.Type.ArraySize != null ? ", " + field.Type.ArraySize : "")});";
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)}");
$"\tpublic {(requiresNewKeyword ? "new " : "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)}");
builder.AppendLine($"\t{{");
builder.AppendLine(
$"\t\tget {{ {getter} }}");
@@ -329,7 +466,7 @@ internal static partial class Program
var getter = $"return Schema.GetString({handleParams});";
var setter = $"Schema.SetStringBytes({handleParams}, value, {field.Type.ArraySize});";
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)}");
$"\tpublic {(requiresNewKeyword ? "new " : "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)}");
builder.AppendLine($"\t{{");
builder.AppendLine(
$"\t\tget {{ {getter} }}");
@@ -344,7 +481,7 @@ internal static partial class Program
var getter = $"return Schema.GetUtf8String({handleParams});";
var setter = $"Schema.SetString({handleParams}, value);";
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)}");
$"\tpublic {(requiresNewKeyword ? "new " : "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)}");
builder.AppendLine($"\t{{");
builder.AppendLine(
$"\t\tget {{ {getter} }}");
@@ -358,7 +495,7 @@ internal static partial class Program
var getter =
$"Schema.GetFixedArray<{SanitiseTypeName(field.Type.Inner!.CsTypeName)}>({handleParams}, {field.Type.ArraySize});";
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}Span<{SanitiseTypeName(field.Type.Inner!.CsTypeName)}> {schemaClass.CsPropertyNameForField(schemaClassName, field)} => {getter}");
$"\tpublic {(requiresNewKeyword ? "new " : "")}Span<{SanitiseTypeName(field.Type.Inner!.CsTypeName)}> {schemaClass.CsPropertyNameForField(schemaClassName, field)} => {getter}");
builder.AppendLine();
}
else if (field.Type.Category == SchemaTypeCategory.DeclaredClass &&
@@ -366,7 +503,7 @@ internal static partial class Program
{
var getter = $"Schema.GetDeclaredClass<{SanitiseTypeName(field.Type.CsTypeName)}>({handleParams});";
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)} => {getter}");
$"\tpublic {(requiresNewKeyword ? "new " : "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)} => {getter}");
builder.AppendLine();
}
else if ((field.Type.Category == SchemaTypeCategory.Builtin ||
@@ -375,7 +512,7 @@ internal static partial class Program
{
var getter = $"ref Schema.GetRef<{SanitiseTypeName(field.Type.CsTypeName)}>({handleParams});";
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}ref {SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)} => {getter}");
$"\tpublic {(requiresNewKeyword ? "new " : "")}ref {SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)} => {getter}");
builder.AppendLine();
}
else if (field.Type.Category == SchemaTypeCategory.Ptr)
@@ -384,7 +521,7 @@ internal static partial class Program
if (inner.Category != SchemaTypeCategory.DeclaredClass) continue;
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)} => Schema.GetPointer<{SanitiseTypeName(inner.CsTypeName)}>({handleParams});");
$"\tpublic {(requiresNewKeyword ? "new " : "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)} => Schema.GetPointer<{SanitiseTypeName(inner.CsTypeName)}>({handleParams});");
builder.AppendLine();
}
else if (field.Type is { Category: SchemaTypeCategory.Atomic, Name: "Color" })
@@ -392,7 +529,7 @@ internal static partial class Program
var getter = $"return Schema.GetCustomMarshalledType<{field.Type.CsTypeName}>({handleParams});";
var setter = $"Schema.SetCustomMarshalledType<{field.Type.CsTypeName}>({handleParams}, value);";
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)}");
$"\tpublic {(requiresNewKeyword ? "new " : "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)}");
builder.AppendLine($"\t{{");
builder.AppendLine(
$"\t\tget {{ {getter} }}");
@@ -405,7 +542,7 @@ internal static partial class Program
{
var getter = $"Schema.GetDeclaredClass<{SanitiseTypeName(field.Type.CsTypeName)}>({handleParams});";
builder.AppendLine(
$"\tpublic {(requiresNewKeyword ? "new ": "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)} => {getter}");
$"\tpublic {(requiresNewKeyword ? "new " : "")}{SanitiseTypeName(field.Type.CsTypeName)} {schemaClass.CsPropertyNameForField(schemaClassName, field)} => {getter}");
builder.AppendLine();
}
}
@@ -435,20 +572,28 @@ internal static partial class Program
builder.AppendLine($"public enum {SanitiseTypeName(enumName)} : {EnumType(schemaEnum.Align)}");
builder.AppendLine("{");
var maxValue = schemaEnum.Align switch
{
1 => byte.MaxValue,
2 => ushort.MaxValue,
4 => uint.MaxValue,
8 => ulong.MaxValue,
_ => throw new ArgumentOutOfRangeException()
};
// Write enum items
foreach (var enumItem in schemaEnum.Items)
{
var value = enumItem.Value < maxValue ? enumItem.Value : maxValue;
builder.AppendLine($"\t{enumItem.Name} = 0x{value:X},");
string value;
if (schemaEnum.Align == 8)
{
value = unchecked((ulong)enumItem.Value).ToString("X");
}
else if (schemaEnum.Align == 4)
{
value = unchecked((uint)enumItem.Value).ToString("X");
}
else if (schemaEnum.Align == 2)
{
value = unchecked((ushort)enumItem.Value).ToString("X");
}
else
{
value = unchecked((byte)enumItem.Value).ToString("X");
}
builder.AppendLine($"\t{enumItem.Name} = 0x{value},");
}
builder.AppendLine("}");

File diff suppressed because one or more lines are too long

View File

@@ -2,4 +2,4 @@
public record SchemaEnumItem(
string Name,
ulong Value);
long Value);

View File

@@ -32,3 +32,14 @@ void* FindSignature(const char* moduleName, const char* bytesStr)
return module->FindSignature(bytesStr);
}
void* FindVirtualTable(const char* moduleName, const char* vtableName)
{
auto module = counterstrikesharp::modules::GetModuleByName(moduleName);
if (module == nullptr)
{
return nullptr;
}
return module->FindVirtualTable(vtableName);
}

View File

@@ -13,3 +13,4 @@
#endif
void* FindSignature(const char* moduleName, const char* bytesStr);
void* FindVirtualTable(const char* moduleName, const char* vtableName);

View File

@@ -11,17 +11,79 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>.
*/
#define private public
#include "core/log.h"
#include "scripting/autonative.h"
#include "scripting/script_engine.h"
// ---- Flag setter compatible with various SDKs ----
template <typename T>
concept HasAddClear = requires(T* t, uint64_t f) {
t->AddFlags(f);
t->ClearFlags(f);
};
template <typename T>
concept HasAddRemove = requires(T* t, uint64_t f) {
t->AddFlags(f);
t->RemoveFlags(f);
};
template <typename T>
concept HasSetFlagBit = requires(T* t, uint64_t f) {
t->SetFlag(f, true);
t->SetFlag(f, false);
};
template <typename T> void SetAllFlagsCompat(T* data, uint64_t desired)
{
uint64_t cur = data->GetFlags();
uint64_t add = desired & ~cur;
uint64_t rem = cur & ~desired;
if constexpr (HasAddClear<T>)
{
if (add) data->AddFlags(add);
if (rem) data->ClearFlags(rem);
}
else if constexpr (HasAddRemove<T>)
{
if (add) data->AddFlags(add);
if (rem) data->RemoveFlags(rem);
}
else if constexpr (HasSetFlagBit<T>)
{
// Fallback: set/clear bitwise
for (int i = 0; i < 64; i)
{
uint64_t bit = (1ULL << i);
bool want = (desired & bit) != 0;
data->SetFlag(bit, want);
}
}
else
{
static_assert(sizeof(T) == 0, "ConVarData hat keine passende Flags-API (Add/Clear/Remove/SetFlag).");
}
}
// ------------------------------------------------------
// First STL/SPDLOG, then SDK with the hack and clean up immediately afterwards
#ifdef private
#undef private
#endif
#ifdef protected
#undef protected
#endif
#define private public
#include <eiface.h>
#include <convar.h>
#undef private
#ifdef protected
#undef protected
#endif
namespace counterstrikesharp {
@@ -37,7 +99,7 @@ static void SetConvarFlags(ScriptContext& script_context)
}
auto flags = script_context.GetArgument<uint64_t>(1);
ref.GetConVarData()->m_nFlags = flags;
SetAllFlagsCompat(ref.GetConVarData(), flags);
}
static void GetConvarFlags(ScriptContext& script_context)
@@ -51,7 +113,7 @@ static void GetConvarFlags(ScriptContext& script_context)
return;
}
script_context.SetResult(ref.GetConVarData()->m_nFlags);
script_context.SetResult(ref.GetConVarData()->GetFlags());
}
static void GetConvarType(ScriptContext& script_context)
@@ -407,8 +469,6 @@ static void CreateConVar(ScriptContext& script_context)
auto hasMin = script_context.GetArgument<bool>(4);
auto hasMax = script_context.GetArgument<bool>(5);
// default, min, max is 6,7,8
ConVarRefAbstract cvar(name);
if (cvar.IsValidRef())
{

View File

@@ -34,6 +34,14 @@ void* FindSignatureNative(ScriptContext& scriptContext)
return FindSignature(moduleName, bytesStr);
}
void* FindVirtualTableNative(ScriptContext& scriptContext)
{
auto moduleName = scriptContext.GetArgument<const char*>(0);
auto vtableName = scriptContext.GetArgument<const char*>(1);
return FindVirtualTable(moduleName, vtableName);
}
ValveFunction* CreateVirtualFunctionBySignature(ScriptContext& script_context)
{
auto ptr = script_context.GetArgument<unsigned long>(0);
@@ -167,6 +175,7 @@ REGISTER_NATIVES(memory, {
ScriptEngine::RegisterNativeHandler("HOOK_FUNCTION", HookFunction);
ScriptEngine::RegisterNativeHandler("UNHOOK_FUNCTION", UnhookFunction);
ScriptEngine::RegisterNativeHandler("FIND_SIGNATURE", FindSignatureNative);
ScriptEngine::RegisterNativeHandler("FIND_VIRTUAL_TABLE", FindVirtualTableNative);
ScriptEngine::RegisterNativeHandler("GET_NETWORK_VECTOR_SIZE", GetNetworkVectorSize);
ScriptEngine::RegisterNativeHandler("GET_NETWORK_VECTOR_ELEMENT_AT", GetNetworkVectorElementAt);
ScriptEngine::RegisterNativeHandler("REMOVE_ALL_NETWORK_VECTOR_ELEMENTS", RemoveAllNetworkVectorElements);

View File

@@ -4,6 +4,7 @@ HOOK_FUNCTION: function:pointer, hook:callback, post:bool -> void
UNHOOK_FUNCTION: function:pointer, hook:callback, post:bool -> void
EXECUTE_VIRTUAL_FUNCTION: function:pointer, bypass:bool, arguments:object[] -> any
FIND_SIGNATURE: modulePath:string, signature:string -> pointer
FIND_VIRTUAL_TABLE: modulePath:string, vtablename:string -> pointer
GET_NETWORK_VECTOR_SIZE: vec:pointer -> int
GET_NETWORK_VECTOR_ELEMENT_AT: vec:pointer, index:int -> pointer
REMOVE_ALL_NETWORK_VECTOR_ELEMENTS: vec:pointer -> void