Compare commits

...

4 Commits
v1.0.72 ... v74

Author SHA1 Message Date
Michael Wilson
4e8c18abc7 Implement Core & Plugin Service Collection (#129) 2023-11-26 14:15:58 +10:00
Roflmuffin
8d1891a3a8 Merge branch 'main' of github.com:roflmuffin/CounterStrikeSharp 2023-11-26 13:19:54 +10:00
Roflmuffin
6bc43444f7 feat: add trigger touch start and end hooks 2023-11-26 13:19:40 +10:00
miguno
f0c7869f4a Check if userid is valid before accessing its fields, and explain why (#133) 2023-11-26 10:03:20 +10:00
38 changed files with 1047 additions and 670 deletions

View File

@@ -139,6 +139,20 @@
"linux": "\\x55\\x48\\x89\\xE5\\x41\\x57\\x41\\x56\\x41\\x55\\x41\\x54\\x49\\x89\\xFC\\x53\\x48\\x83\\xEC\\x38\\x4C\\x8D\\x2D\\x2A\\x2A\\x2A\\x2A\\x49\\x8B\\x7D\\x00\\x48\\x85\\xFF\\x0F\\x84\\x2A\\x2A\\x2A\\x2A"
}
},
"CBaseTrigger_StartTouch": {
"signatures": {
"library": "server",
"windows": "\\x41\\x56\\x41\\x57\\x48\\x83\\xEC\\x58\\x48\\x8B\\x01",
"linux": "\\x55\\x48\\x89\\xE5\\x41\\x56\\x49\\x89\\xF6\\x41\\x55\\x49\\x89\\xFD\\x41\\x54\\x53\\xBB"
}
},
"CBaseTrigger_EndTouch": {
"signatures": {
"library": "server",
"windows": "\\x40\\x53\\x57\\x41\\x55\\x48\\x83\\xEC\\x40",
"linux": "\\x55\\xBA\\xFF\\xFF\\xFF\\xFF\\x48\\x89\\xE5\\x41\\x57\\x41\\x56\\x41\\x55\\x49"
}
},
"GameEntitySystem": {
"offsets": {
"windows": 88,

View File

@@ -17,8 +17,13 @@ The first parameter type must be a subclass of the `GameEvent` class. The names
[GameEventHandler]
public HookResult OnPlayerConnect(EventPlayerConnect @event, GameEventInfo info)
{
// Userid will give you a reference to a CCSPlayerController class
Logger.LogInformation("Player {Name} has connected!", @event.Userid.PlayerName);
// Userid will give you a reference to a CCSPlayerController class.
// Before accessing any of its fields, you must first check if the Userid
// handle is actually valid, otherwise you may run into runtime exceptions.
// See the documentation section on Referencing Players for details.
if (@event.Userid.IsValid) {
Logger.LogInformation("Player {Name} has connected!", @event.Userid.PlayerName);
}
return HookResult.Continue;
}

View File

@@ -0,0 +1,71 @@
---
title: Dependency Injection
description: How to make use of dependency injection in CounterStrikeSharp
sidebar:
order: 1
---
`CounterStrikeSharp` uses a standard <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0" target="_blank">`IServiceCollection`</a> to allow for dependency injection in plugins.
There are a handful of standard services that are predefined for you (`ILogger` for logging for instance), with more to come in the future. To add your own scoped & singleton services to the container, you can create a new class that implements the `IPluginServiceCollection<T>` interface for your plugin.
```csharp
public class TestPlugin : BasePlugin
{
// Plugin code...
}
public class TestPluginServiceCollection : IPluginServiceCollection<TestPlugin>
{
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<ExampleInjectedClass>();
serviceCollection.AddLogging(builder => ...);
}
}
```
CounterStrikeSharp will search your assembly for any implementations of `IPlugin` and then any implementations of `IPluginServiceCollection<T>` where `T` is your plugin. It will then configure the service provider and then request a singleton instance of your plugin before proceeding to the load step.
In this way, any dependencies that are listed in your plugin class constructor will automatically get injected at instantation time (before load).
### Example
```csharp
public class TestInjectedClass
{
private readonly ILogger<TestInjectedClass> _logger;
public TestInjectedClass(ILogger<TestInjectedClass> logger)
{
_logger = logger;
}
public void Hello()
{
_logger.LogInformation("Hello World from Test Injected Class");
}
}
public class TestPluginServiceCollection : IPluginServiceCollection<SamplePlugin>
{
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<TestInjectedClass>();
}
}
public class SamplePlugin : BasePlugin
{
private readonly TestInjectedClass _testInjectedClass;
public SamplePlugin(TestInjectedClass testInjectedClass)
{
_testInjectedClass = testInjectedClass;
}
public override void Load(bool hotReload)
{
_testInjectedClass.Hello();
}
}
```

View File

@@ -1,6 +1,8 @@
---
title: Getting Started
description: How to get started installing & using CounterStrikeSharp.
sidebar:
order: 0
---
# Installation

View File

@@ -1,6 +1,8 @@
---
title: Hello World Plugin
description: How to write your first plugin for CounterStrikeSharp
sidebar:
order: 0
---
## Creating a New Project

View File

@@ -0,0 +1,67 @@
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Plugin.Host;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
namespace CounterStrikeSharp.API;
public static class Bootstrap
{
[UnmanagedCallersOnly]
// Used by .NET Host in C++ to initiate loading
public static int Run()
{
try
{
// Path to /game/csgo/addons/counterstrikesharp
var contentRoot = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent.FullName;
using var host = Host.CreateDefaultBuilder()
.UseContentRoot(contentRoot)
.ConfigureServices(services =>
{
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddCoreLogging(contentRoot);
});
services.AddSingleton<IScriptHostConfiguration, ScriptHostConfiguration>();
services.AddScoped<Application>();
services.AddSingleton<IPluginManager, PluginManager>();
services.AddScoped<IPluginContextQueryHandler, PluginContextQueryHandler>();
services.Scan(i => i.FromCallingAssembly()
.AddClasses(c => c.AssignableTo<IStartupService>())
.AsSelfWithInterfaces()
.WithSingletonLifetime());
})
.Build();
using IServiceScope scope = host.Services.CreateScope();
// TODO: Improve static singleton access
GameData.GameDataProvider = scope.ServiceProvider.GetRequiredService<GameDataProvider>();
var application = scope.ServiceProvider.GetRequiredService<Application>();
application.Start();
return 1;
}
catch (Exception e)
{
Console.Error.WriteLine(e);
Log.Fatal(e, "Failed to start application");
return 0;
}
}
}

View File

@@ -0,0 +1,256 @@
/*
* 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/>. *
*/
using System.Linq;
using System.Text;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
{
public sealed class Application
{
private static Application _instance = null!;
public ILogger Logger { get; }
public static Application Instance => _instance!;
public static string RootDirectory => Instance._scriptHostConfiguration.RootPath;
private readonly IScriptHostConfiguration _scriptHostConfiguration;
private readonly GameDataProvider _gameDataProvider;
private readonly CoreConfig _coreConfig;
private readonly IPluginManager _pluginManager;
private readonly IPluginContextQueryHandler _pluginContextQueryHandler;
public Application(ILoggerFactory loggerFactory, IScriptHostConfiguration scriptHostConfiguration,
GameDataProvider gameDataProvider, CoreConfig coreConfig, IPluginManager pluginManager,
IPluginContextQueryHandler pluginContextQueryHandler)
{
Logger = loggerFactory.CreateLogger("Core");
_scriptHostConfiguration = scriptHostConfiguration;
_gameDataProvider = gameDataProvider;
_coreConfig = coreConfig;
_pluginManager = pluginManager;
_pluginContextQueryHandler = pluginContextQueryHandler;
_instance = this;
}
public void Start()
{
Logger.LogInformation("CounterStrikeSharp is starting up...");
_coreConfig.Load();
_gameDataProvider.Load();
var adminGroupsPath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admin_groups.json");
Logger.LogInformation("Loading Admin Groups from {Path}", adminGroupsPath);
AdminManager.LoadAdminGroups(adminGroupsPath);
var adminPath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admins.json");
Logger.LogInformation("Loading Admins from {Path}", adminPath);
AdminManager.LoadAdminData(adminPath);
var overridePath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admin_overrides.json");
Logger.LogInformation("Loading Admin Command Overrides from {Path}", overridePath);
AdminManager.LoadCommandOverrides(overridePath);
AdminManager.MergeGroupPermsIntoAdmins();
_pluginManager.Load();
for (var i = 1; i <= 9; i++)
{
CommandUtils.AddStandaloneCommand("css_" + i, "Command Key Handler", (player, info) =>
{
if (player == null) return;
var key = Convert.ToInt32(info.GetArg(0).Split("_")[1]);
ChatMenus.OnKeyPress(player, key);
});
}
RegisterPluginCommands();
}
[RequiresPermissions("@css/generic")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnCSSCommand(CCSPlayerController? caller, CommandInfo info)
{
var currentVersion = Api.GetVersion();
info.ReplyToCommand(
" CounterStrikeSharp was created and is maintained by Michael \"roflmuffin\" Wilson.\n" +
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison and CS2Fixes.\n" +
" See ACKNOWLEDGEMENTS.md for more information.\n" +
" Current API Version: " + currentVersion, true);
return;
}
[RequiresPermissions("@css/generic")]
[CommandHelper(minArgs: 1,
usage: "[option]\n" +
" list - List all plugins currently loaded.\n" +
" start / load - Loads a plugin not currently loaded.\n" +
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded.",
whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnCSSPluginCommand(CCSPlayerController? caller, CommandInfo info)
{
switch (info.GetArg(1))
{
case "list":
{
info.ReplyToCommand(
$" List of all plugins currently loaded by CounterStrikeSharp: {_pluginManager.GetLoadedPlugins().Count()} plugins loaded.",
true);
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,
plugin.Plugin.ModuleVersion);
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(), true);
}
break;
}
case "start":
case "load":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand(
"Valid usage: css_plugins start/load [relative plugin path || absolute plugin path] (e.g \"TestPlugin\", \"plugins/TestPlugin/TestPlugin.dll\")\n",
true);
break;
}
var plugin = _pluginContextQueryHandler.FindPluginByModulePath(info.GetArg(2));
if (plugin == null)
{
info.ReplyToCommand("Could not find plugin to load.");
break;
}
plugin.Load(false);
// If our arugment 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);
if (!path.EndsWith(".dll"))
{
path = Path.Combine(_scriptHostConfiguration.RootPath, $"plugins/{path}/{path}.dll");
}
else
{
path = Path.Combine(_scriptHostConfiguration.RootPath, path);
}
try
{
// LoadPlugin(path);
}
catch (Exception e)
{
Logger.LogError(e, "Failed to load plugin from {Path}", path);
}
break;
}
case "stop":
case "unload":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand(
"Valid usage: css_plugins stop/unload [plugin name || #plugin id] (e.g \"TestPlugin\", \"1\")\n",
true);
break;
}
var pluginIdentifier = info.GetArg(2);
IPluginContext? plugin = _pluginContextQueryHandler.FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not unload plugin \"{pluginIdentifier}\")", true);
break;
}
plugin.Unload(false);
break;
}
case "restart":
case "reload":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand(
"Valid usage: css_plugins restart/reload [plugin name || #plugin id] (e.g \"TestPlugin\", \"#1\")\n",
true);
break;
}
var pluginIdentifier = info.GetArg(2);
var plugin = _pluginContextQueryHandler.FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not reload plugin \"{pluginIdentifier}\")", true);
break;
}
plugin.Unload(true);
plugin.Load(true);
break;
}
default:
info.ReplyToCommand("Valid usage: css_plugins [option]\n" +
" list - List all plugins currently loaded.\n" +
" start / load - Loads a plugin not currently loaded.\n" +
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded."
, true);
break;
}
}
private void RegisterPluginCommands()
{
CommandUtils.AddStandaloneCommand("css", "Counter-Strike Sharp options.", OnCSSCommand);
CommandUtils.AddStandaloneCommand("css_plugins", "Counter-Strike Sharp plugin options.",
OnCSSPluginCommand);
}
}
}

View File

@@ -18,25 +18,19 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Listeners;
using CounterStrikeSharp.API.Modules.Timers;
using McMaster.NETCore.Plugins;
using CounterStrikeSharp.API.Modules.Config;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
{
public abstract class BasePlugin : IPlugin, IDisposable
public abstract class BasePlugin : IPlugin
{
private bool _disposed;
@@ -51,7 +45,7 @@ namespace CounterStrikeSharp.API.Core
public virtual string ModuleDescription { get; }
public string ModulePath { get; internal set; }
public string ModulePath { get; set; }
public string ModuleDirectory => Path.GetDirectoryName(ModulePath);
public ILogger Logger { get; set; }
@@ -316,7 +310,7 @@ namespace CounterStrikeSharp.API.Core
.Select(p => p.GetCustomAttribute<CastFromAttribute>()?.Type)
.ToArray();
GlobalContext.Instance.Logger.LogDebug("Registering listener for {ListenerName} with {ParameterCount} parameters",
Application.Instance.Logger.LogDebug("Registering listener for {ListenerName} with {ParameterCount} parameters",
listenerName, parameterTypes.Length);
var wrappedHandler = new Action<ScriptContext>(context =>

View File

@@ -19,12 +19,13 @@ using System.IO;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using CounterStrikeSharp.API.Modules.Utils;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using System.Collections.Generic;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -34,17 +35,20 @@ namespace CounterStrikeSharp.API.Core
/// </summary>
internal sealed partial class CoreConfigData
{
[JsonPropertyName("PublicChatTrigger")] public IEnumerable<string> PublicChatTrigger { get; set; } = new HashSet<string>() { "!" };
[JsonPropertyName("PublicChatTrigger")]
public IEnumerable<string> PublicChatTrigger { get; set; } = new HashSet<string>() { "!" };
[JsonPropertyName("SilentChatTrigger")] public IEnumerable<string> SilentChatTrigger { get; set; } = new HashSet<string>() { "/" };
[JsonPropertyName("SilentChatTrigger")]
public IEnumerable<string> SilentChatTrigger { get; set; } = new HashSet<string>() { "/" };
[JsonPropertyName("FollowCS2ServerGuidelines")] public bool FollowCS2ServerGuidelines { get; set; } = true;
[JsonPropertyName("FollowCS2ServerGuidelines")]
public bool FollowCS2ServerGuidelines { get; set; } = true;
}
/// <summary>
/// Configuration related to the Core API.
/// </summary>
public static partial class CoreConfig
public partial class CoreConfig
{
/// <summary>
/// List of characters to use for public chat triggers.
@@ -78,49 +82,56 @@ namespace CounterStrikeSharp.API.Core
public static bool FollowCS2ServerGuidelines => _coreConfig.FollowCS2ServerGuidelines;
}
public static partial class CoreConfig
public partial class CoreConfig : IStartupService
{
private static CoreConfigData _coreConfig = new CoreConfigData();
// TODO: ServiceCollection
private static ILogger _logger = CoreLogging.Factory.CreateLogger("CoreConfig");
static CoreConfig()
private readonly ILogger<CoreConfig> _logger;
private readonly string _coreConfigPath;
public CoreConfig(IScriptHostConfiguration scriptHostConfiguration, ILogger<CoreConfig> logger)
{
CommandUtils.AddStandaloneCommand("css_core_reload", "Reloads the core configuration file.", ReloadCoreConfigCommand);
_logger = logger;
_coreConfigPath = Path.Join(scriptHostConfiguration.ConfigsPath, "core.json");
}
[RequiresPermissions("@css/config")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private static void ReloadCoreConfigCommand(CCSPlayerController? player, CommandInfo command)
private void ReloadCoreConfigCommand(CCSPlayerController? player, CommandInfo command)
{
var rootDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent;
Load(Path.Combine(rootDir.FullName, "configs", "core.json"));
Load();
}
public static void Load(string coreConfigPath)
public void Load()
{
if (!File.Exists(coreConfigPath))
CommandUtils.AddStandaloneCommand("css_core_reload", "Reloads the core configuration file.",
ReloadCoreConfigCommand);
if (!File.Exists(_coreConfigPath))
{
_logger.LogWarning("Core configuration could not be found at path \"{CoreConfigPath}\", fallback values will be used.", coreConfigPath);
_logger.LogWarning(
"Core configuration could not be found at path \"{CoreConfigPath}\", fallback values will be used.",
_coreConfigPath);
return;
}
try
{
var data = JsonSerializer.Deserialize<CoreConfigData>(File.ReadAllText(coreConfigPath), new JsonSerializerOptions() { ReadCommentHandling = JsonCommentHandling.Skip });
var data = JsonSerializer.Deserialize<CoreConfigData>(File.ReadAllText(_coreConfigPath),
new JsonSerializerOptions() { ReadCommentHandling = JsonCommentHandling.Skip });
if (data != null)
{
_coreConfig = data;
}
_logger.LogInformation("Successfully loaded core configuration.");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load core configuration, fallback values will be used");
}
_logger.LogInformation("Successfully loaded core configuration");
}
}
}
}

View File

@@ -77,7 +77,7 @@ namespace CounterStrikeSharp.API.Core
}
catch (Exception e)
{
GlobalContext.Instance.Logger.LogError(e, "Error invoking callback");
Application.Instance.Logger.LogError(e, "Error invoking callback");
}
});
s_callback = dg;
@@ -141,7 +141,7 @@ namespace CounterStrikeSharp.API.Core
{
ms_references.Remove(reference);
GlobalContext.Instance.Logger.LogDebug("Removing function/callback reference: {Reference}", reference);
Application.Instance.Logger.LogDebug("Removing function/callback reference: {Reference}", reference);
}
}
}

View File

@@ -5,11 +5,12 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using CounterStrikeSharp.API.Core.Hosting;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core;
class LoadedGameData
public class LoadedGameData
{
[JsonPropertyName("signatures")] public Signatures? Signatures { get; set; }
[JsonPropertyName("offsets")] public Offsets? Offsets { get; set; }
@@ -31,33 +32,45 @@ public class Offsets
[JsonPropertyName("linux")] public int Linux { get; set; }
}
public static class GameData
public sealed class GameDataProvider : IStartupService
{
private static Dictionary<string, LoadedGameData> _methods;
private readonly string _gameDataFilePath;
public Dictionary<string,LoadedGameData> Methods;
private readonly ILogger<GameDataProvider> _logger;
public static void Load(string gameDataPath)
public GameDataProvider(IScriptHostConfiguration scriptHostConfiguration, ILogger<GameDataProvider> logger)
{
_logger = logger;
_gameDataFilePath = Path.Join(scriptHostConfiguration.GameDataPath, "gamedata.json");
}
public void Load()
{
try
{
_methods = JsonSerializer.Deserialize<Dictionary<string, LoadedGameData>>(File.ReadAllText(gameDataPath))!;
GlobalContext.Instance.Logger.LogInformation("Loaded game data with {Count} methods.", _methods.Count);
Methods = JsonSerializer.Deserialize<Dictionary<string, LoadedGameData>>(File.ReadAllText(_gameDataFilePath))!;
}
catch (Exception ex)
{
GlobalContext.Instance.Logger.LogError(ex, "Failed to load game data");
_logger.LogError(ex, "Failed to load game data");
}
_logger.LogInformation("Successfully loaded {Count} game data entries from {Path}", Methods.Count, _gameDataFilePath);
}
}
public static class GameData
{
internal static GameDataProvider GameDataProvider { get; set; } = null!;
public static string GetSignature(string key)
{
GlobalContext.Instance.Logger.LogDebug("Getting signature: {Key}", key);
if (!_methods.ContainsKey(key))
Application.Instance.Logger.LogDebug("Getting signature: {Key}", key);
if (!GameDataProvider.Methods.ContainsKey(key))
{
throw new ArgumentException($"Method {key} not found in gamedata.json");
}
var methodMetadata = _methods[key];
var methodMetadata = GameDataProvider.Methods[key];
if (methodMetadata.Signatures == null)
{
throw new InvalidOperationException($"No signatures found for {key} in gamedata.json");
@@ -79,12 +92,12 @@ public static class GameData
public static int GetOffset(string key)
{
if (!_methods.ContainsKey(key))
if (!GameDataProvider.Methods.ContainsKey(key))
{
throw new Exception($"Method {key} not found in gamedata.json");
}
var methodMetadata = _methods[key];
var methodMetadata = GameDataProvider.Methods[key];
if (methodMetadata.Offsets == null)
{

View File

@@ -1,355 +0,0 @@
/*
* 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/>. *
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using CounterStrikeSharp.API.Core.Logging;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace CounterStrikeSharp.API.Core
{
public sealed class GlobalContext
{
private static GlobalContext _instance = null;
public ILogger Logger { get; }
public static GlobalContext Instance => _instance;
public static string RootDirectory => _instance.rootDir.FullName;
private DirectoryInfo rootDir;
private readonly List<PluginContext> _loadedPlugins = new();
public GlobalContext()
{
rootDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent;
_instance = this;
Logger = CoreLogging.Factory.CreateLogger("Core");
Logger.LogInformation("CounterStrikeSharp is starting up...");
}
~GlobalContext()
{
foreach (var plugin in _loadedPlugins)
{
plugin.Unload();
}
}
public void OnNativeUnload()
{
foreach (var plugin in _loadedPlugins)
{
plugin.Unload();
}
}
public void InitGlobalContext()
{
var coreConfigPath = Path.Combine(rootDir.FullName, "configs", "core.json");
Logger.LogInformation("Loading CoreConfig from {Path}", coreConfigPath);
CoreConfig.Load(coreConfigPath);
var gameDataPath = Path.Combine(rootDir.FullName, "gamedata", "gamedata.json");
Logger.LogInformation("Loading GameData from {Path}", gameDataPath);
GameData.Load(gameDataPath);
var adminGroupsPath = Path.Combine(rootDir.FullName, "configs", "admin_groups.json");
Logger.LogInformation("Loading Admin Groups from {Path}", adminGroupsPath);
AdminManager.LoadAdminGroups(adminGroupsPath);
var adminPath = Path.Combine(rootDir.FullName, "configs", "admins.json");
Logger.LogInformation("Loading Admins from {Path}", adminPath);
AdminManager.LoadAdminData(adminPath);
var overridePath = Path.Combine(rootDir.FullName, "configs", "admin_overrides.json");
Logger.LogInformation("Loading Admin Command Overrides from {Path}", overridePath);
AdminManager.LoadCommandOverrides(overridePath);
AdminManager.MergeGroupPermsIntoAdmins();
for (var i = 1; i <= 9; i++)
{
CommandUtils.AddStandaloneCommand("css_" + i, "Command Key Handler", (player, info) =>
{
if (player == null) return;
var key = Convert.ToInt32(info.GetArg(0).Split("_")[1]);
ChatMenus.OnKeyPress(player, key);
});
}
Logger.LogInformation("Loading C# plugins...");
var pluginCount = LoadAllPlugins();
Logger.LogInformation("All managed modules were loaded. {PluginCount} plugins loaded.", pluginCount);
RegisterPluginCommands();
}
private void LoadPlugin(string path)
{
var existingPlugin = FindPluginByModulePath(path);
if (existingPlugin != null)
{
throw new FileLoadException("Plugin is already loaded.");
}
var plugin = new PluginContext(path, _loadedPlugins.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1);
plugin.Load();
_loadedPlugins.Add(plugin);
}
private int LoadAllPlugins()
{
DirectoryInfo modulesDirectoryInfo;
try
{
modulesDirectoryInfo = new DirectoryInfo(Path.Combine(rootDir.FullName, "plugins"));
}
catch (Exception e)
{
Logger.LogError(e, "Error finding plugin path");
return 0;
}
DirectoryInfo[] properModulesDirectories;
try
{
properModulesDirectories = modulesDirectoryInfo.GetDirectories();
}
catch
{
properModulesDirectories = Array.Empty<DirectoryInfo>();
}
var filePaths = properModulesDirectories
.Where(d => d.GetFiles().Any((f) => f.Name == d.Name + ".dll"))
.Select(d => d.GetFiles().First((f) => f.Name == d.Name + ".dll").FullName)
.ToArray();
foreach (var path in filePaths)
{
try
{
LoadPlugin(path);
}
catch (Exception e)
{
Logger.LogError(e, "Failed to load plugin from {Path}", path);
}
}
return _loadedPlugins.Count;
}
public void UnloadAllPlugins()
{
foreach (var plugin in _loadedPlugins)
{
plugin.Unload();
_loadedPlugins.Remove(plugin);
}
}
private PluginContext? FindPluginByType(Type moduleClass)
{
return _loadedPlugins.FirstOrDefault(x => x.PluginType == moduleClass);
}
private PluginContext? FindPluginById(int id)
{
return _loadedPlugins.FirstOrDefault(x => x.PluginId == id);
}
private PluginContext? FindPluginByModuleName(string name)
{
return _loadedPlugins.FirstOrDefault(x => x.Name == name);
}
private PluginContext? FindPluginByModulePath(string path)
{
return _loadedPlugins.FirstOrDefault(x => x.PluginPath == path);
}
private PluginContext? FindPluginByIdOrName(string query)
{
PluginContext? plugin = null;
if (Int32.TryParse(query, out var pluginNumber))
{
plugin = FindPluginById(pluginNumber);
if (plugin != null) return plugin;
}
plugin = FindPluginByModuleName(query);
return plugin;
}
[RequiresPermissions("@css/generic")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnCSSCommand(CCSPlayerController? caller, CommandInfo info)
{
var currentVersion = Api.GetVersion();
info.ReplyToCommand(" CounterStrikeSharp was created and is maintained by Michael \"roflmuffin\" Wilson.\n" +
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison and CS2Fixes.\n" +
" See ACKNOWLEDGEMENTS.md for more information.\n" +
" Current API Version: " + currentVersion, true);
return;
}
[RequiresPermissions("@css/generic")]
[CommandHelper(minArgs: 1,
usage: "[option]\n" +
" list - List all plugins currently loaded.\n" +
" start / load - Loads a plugin not currently loaded.\n" +
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded.",
whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnCSSPluginCommand(CCSPlayerController? caller, CommandInfo info)
{
switch (info.GetArg(1))
{
case "list":
{
info.ReplyToCommand($" List of all plugins currently loaded by CounterStrikeSharp: {_loadedPlugins.Count} plugins loaded.", true);
foreach (var plugin in _loadedPlugins)
{
var sb = new StringBuilder();
sb.AppendFormat(" [#{0}]: \"{1}\" ({2})", plugin.PluginId, plugin.Name, plugin.Version);
if (!string.IsNullOrEmpty(plugin.Author)) sb.AppendFormat(" by {0}", plugin.Author);
if (!string.IsNullOrEmpty(plugin.Description))
{
sb.Append("\n");
sb.Append(" ");
sb.Append(plugin.Description);
}
info.ReplyToCommand(sb.ToString(), true);
}
break;
}
case "start":
case "load":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand("Valid usage: css_plugins start/load [relative plugin path || absolute plugin path] (e.g \"TestPlugin\", \"plugins/TestPlugin/TestPlugin.dll\")\n", true);
break;
}
// If our arugment 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);
if (!path.EndsWith(".dll"))
{
path = Path.Combine(rootDir.FullName, $"plugins/{path}/{path}.dll");
}
else
{
path = Path.Combine(rootDir.FullName, path);
}
try
{
LoadPlugin(path);
}
catch (Exception e)
{
Logger.LogError(e, "Failed to load plugin from {Path}", path);
}
break;
}
case "stop":
case "unload":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand("Valid usage: css_plugins stop/unload [plugin name || #plugin id] (e.g \"TestPlugin\", \"1\")\n", true);
break;
}
var pluginIdentifier = info.GetArg(2);
PluginContext? plugin = FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not unload plugin \"{pluginIdentifier}\")", true);
break;
}
plugin.Unload();
_loadedPlugins.Remove(plugin);
break;
}
case "restart":
case "reload":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand("Valid usage: css_plugins restart/reload [plugin name || #plugin id] (e.g \"TestPlugin\", \"#1\")\n", true);
break;
}
var pluginIdentifier = info.GetArg(2);
var plugin = FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not reload plugin \"{pluginIdentifier}\")", true);
break;
}
plugin.Unload(true);
plugin.Load(true);
break;
}
default:
info.ReplyToCommand("Valid usage: css_plugins [option]\n" +
" list - List all plugins currently loaded.\n" +
" start / load - Loads a plugin not currently loaded.\n" +
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded."
, true);
break;
}
}
private void RegisterPluginCommands()
{
CommandUtils.AddStandaloneCommand("css", "Counter-Strike Sharp options.", OnCSSCommand);
CommandUtils.AddStandaloneCommand("css_plugins", "Counter-Strike Sharp plugin options.", OnCSSPluginCommand);
}
}
}

View File

@@ -22,53 +22,12 @@ using System.Security;
namespace CounterStrikeSharp.API.Core
{
public class MethodAttribute<T> where T : Attribute
{
public MethodAttribute(T attribute, MethodInfo method)
{
Attribute = attribute;
Method = method;
}
public T Attribute;
public MethodInfo Method;
}
public static class Helpers
{
private static MethodAttribute<T>[] FindMethodAttributes<T>(BasePlugin plugin) where T: Attribute
{
return plugin
.GetType()
.GetMethods()
.Where(m => m.GetCustomAttributes(typeof(T), false).Length > 0)
.Select(x => new MethodAttribute<T>(x.GetCustomAttribute<T>(), x))
.ToArray();
}
private const string dllPath = "counterstrikesharp";
[SecurityCritical]
[DllImport(dllPath, EntryPoint = "InvokeNative")]
public static extern void InvokeNative(IntPtr ptr);
[UnmanagedCallersOnly]
// Used by .NET Host in C++ to initiate loading
public static int LoadAllPlugins()
{
try
{
var globalContext = new GlobalContext();
globalContext.InitGlobalContext();
return 1;
}
catch (Exception e)
{
Console.WriteLine(e);
return 0;
}
}
public delegate void Callback();
}
}

View File

@@ -0,0 +1,32 @@
namespace CounterStrikeSharp.API.Core.Hosting;
/// <summary>
/// Provides information about the CounterStrikeSharp host configuration.
/// </summary>
public interface IScriptHostConfiguration
{
/// <summary>
/// Gets the absolute path to the directory that contains CounterStrikeSharp files.
/// e.g. /game/csgo/addons/counterstrikesharp
/// </summary>
string RootPath { get; }
/// <summary>
/// Gets the absolute path to the directory that contains CounterStrikeSharp plugins.
/// e.g. /game/csgo/addons/counterstrikesharp/plugins
/// </summary>
string PluginPath { get; }
/// <summary>
/// Gets the absolute path to the directory that contains CounterStrikeSharp configs.
/// e.g. /game/csgo/addons/counterstrikesharp/configs
/// </summary>
string ConfigsPath { get; }
/// <summary>
/// Gets the absolute path to the directory that contains CounterStrikeSharp game data.
/// e.g. /game/csgo/addons/counterstrikesharp/gamedata
/// </summary>
string GameDataPath { get; }
}

View File

@@ -0,0 +1,20 @@
using System.IO;
using Microsoft.Extensions.Hosting;
namespace CounterStrikeSharp.API.Core.Hosting;
internal sealed class ScriptHostConfiguration : IScriptHostConfiguration
{
public string RootPath { get; }
public string PluginPath { get; }
public string ConfigsPath { get; }
public string GameDataPath { get; }
public ScriptHostConfiguration(IHostEnvironment hostEnvironment)
{
RootPath = Path.Join(new[] { hostEnvironment.ContentRootPath });
PluginPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "plugins" });
ConfigsPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "configs" });
GameDataPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "gamedata" });
}
}

View File

@@ -14,28 +14,30 @@
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
{
/// <summary>
/// Interface which every CounterStrikeSharp plugin must implement. Module will be created with parameterless constructor and then Load method will be called.
/// </summary>
public interface IPlugin
public interface IPlugin : IDisposable
{
/// <summary>
/// Name of the plugin.
/// </summary>
string ModuleName
{
get;
}
string ModuleName { get; }
/// <summary>
/// Module version.
/// </summary>
string ModuleVersion
{
get;
}
string ModuleVersion { get; }
string ModuleAuthor { get; }
string ModuleDescription { get; }
/// <summary>
/// This method is called by CounterStrikeSharp on plugin load and should be treated as plugin constructor.
@@ -48,5 +50,13 @@ namespace CounterStrikeSharp.API.Core
/// Event handlers, listeners etc. will automatically be deregistered.
/// </summary>
void Unload(bool hotReload);
string ModulePath { get; internal set; }
ILogger Logger { get; set; }
void RegisterAllAttributes(object instance);
void InitializeConfig(object instance, Type pluginType);
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
namespace CounterStrikeSharp.API.Core;
/// <summary>
/// Represents a service collection configuration for a plugin.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IPluginServiceCollection<T> where T : IPlugin
{
/// <summary>
/// Used to configure services exposed for dependency injection.
/// </summary>
public void ConfigureServices(IServiceCollection serviceCollection);
}

View File

@@ -0,0 +1,6 @@
namespace CounterStrikeSharp.API.Core;
public interface IStartupService
{
public void Load();
}

View File

@@ -2,28 +2,40 @@ using System;
using System.IO;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Core;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace CounterStrikeSharp.API.Core.Logging;
public static class CoreLogging
{
public static ILoggerFactory Factory { get; }
static CoreLogging()
{
var logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With<SourceContextEnricher>()
.WriteTo.Console(outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u4}] (cssharp:{SourceContext}) {Message:lj}{NewLine}{Exception}")
.WriteTo.File(Path.Join(new[] {GlobalContext.RootDirectory, "logs", $"log-cssharp.txt"}), rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] (cssharp:{SourceContext}) {Message:lj}{NewLine}{Exception}")
.WriteTo.File(Path.Join(new[] {GlobalContext.RootDirectory, "logs", $"log-all.txt"}), rollingInterval: RollingInterval.Day, shared: true, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] (cssharp:{SourceContext}) {Message:lj}{NewLine}{Exception}")
.CreateLogger();
public static ILoggerFactory Factory { get; private set; }
private static Logger? SerilogLogger { get; set; }
Factory =
LoggerFactory.Create(builder =>
{
builder.AddSerilog(logger);
});
public static void AddCoreLogging(this ILoggingBuilder builder, string contentRoot)
{
if (SerilogLogger == null)
{
SerilogLogger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With<SourceContextEnricher>()
.WriteTo.Console(
outputTemplate:
"{Timestamp:HH:mm:ss} [{Level:u4}] (cssharp:{SourceContext}) {Message:lj}{NewLine}{Exception}")
.WriteTo.File(Path.Join(new[] { contentRoot, "logs", $"log-cssharp.txt" }),
rollingInterval: RollingInterval.Day,
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] (cssharp:{SourceContext}) {Message:lj}{NewLine}{Exception}")
.WriteTo.File(Path.Join(new[] { contentRoot, "logs", $"log-all.txt" }),
rollingInterval: RollingInterval.Day, shared: true,
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] (cssharp:{SourceContext}) {Message:lj}{NewLine}{Exception}")
.CreateLogger();
Factory =
LoggerFactory.Create(builder => { builder.AddSerilog(SerilogLogger); });
}
builder.AddSerilog(SerilogLogger);
}
}

View File

@@ -1,32 +0,0 @@
using System.IO;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace CounterStrikeSharp.API.Core.Logging;
public class PluginLogging
{
/// <summary>
/// Creates a logger scoped to a specific plugin
/// <remarks>Eventually this should probably come from a service collection</remarks>
/// </summary>
public static ILogger CreatePluginLogger(PluginContext pluginContext)
{
var logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With(new PluginNameEnricher(pluginContext))
.WriteTo.Console(outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u4}] (plugin:{PluginName}) {Message:lj}{NewLine}{Exception}")
.WriteTo.File(Path.Join(new[] {GlobalContext.RootDirectory, "logs", $"log-{pluginContext.PluginType.Name}.txt"}), rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] plugin:{PluginName} {Message:lj}{NewLine}{Exception}")
.WriteTo.File(Path.Join(new[] {GlobalContext.RootDirectory, "logs", $"log-all.txt"}), rollingInterval: RollingInterval.Day, shared: true, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] plugin:{PluginName} {Message:lj}{NewLine}{Exception}")
.CreateLogger();
using ILoggerFactory loggerFactory =
LoggerFactory.Create(builder =>
{
builder.AddSerilog(logger);
});
return loggerFactory.CreateLogger(pluginContext.PluginType);
}
}

View File

@@ -1,3 +1,4 @@
using CounterStrikeSharp.API.Core.Plugin;
using Serilog.Core;
using Serilog.Events;
@@ -16,7 +17,7 @@ public class PluginNameEnricher : ILogEventEnricher
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var property = propertyFactory.CreateProperty(PropertyName, Context.PluginType.Name);
var property = propertyFactory.CreateProperty(PropertyName, Context.Plugin.ModuleName);
logEvent.AddPropertyIfAbsent(property);
}
}

View File

@@ -0,0 +1,14 @@
namespace CounterStrikeSharp.API.Core.Plugin.Host;
public interface IPluginContextQueryHandler
{
IPluginContext? FindPluginByType(Type moduleClass);
IPluginContext? FindPluginById(int id);
IPluginContext? FindPluginByModuleName(string name);
IPluginContext? FindPluginByModulePath(string path);
IPluginContext? FindPluginByIdOrName(string query);
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace CounterStrikeSharp.API.Core.Plugin.Host;
public interface IPluginManager
{
public void Load();
public IEnumerable<PluginContext> GetLoadedPlugins();
}

View File

@@ -0,0 +1,38 @@
using System.Linq;
namespace CounterStrikeSharp.API.Core.Plugin.Host;
public class PluginContextQueryHandler : IPluginContextQueryHandler
{
private readonly IPluginManager _pluginManager;
public PluginContextQueryHandler(IPluginManager pluginManager)
{
_pluginManager = pluginManager;
}
public IPluginContext? FindPluginByType(Type moduleClass)
{
return _pluginManager.GetLoadedPlugins().FirstOrDefault(x => x.Plugin.GetType() == moduleClass);
}
public IPluginContext? FindPluginById(int id)
{
return _pluginManager.GetLoadedPlugins().FirstOrDefault(x => x.PluginId == id);
}
public IPluginContext? FindPluginByModuleName(string name)
{
return _pluginManager.GetLoadedPlugins().FirstOrDefault(x => x.Plugin.ModuleName == name);
}
public IPluginContext? FindPluginByModulePath(string path)
{
return _pluginManager.GetLoadedPlugins().FirstOrDefault(x => x.Plugin.ModulePath == path);
}
public IPluginContext? FindPluginByIdOrName(string query)
{
return _pluginManager.GetLoadedPlugins().FirstOrDefault(x => x.PluginId.ToString() == query || x.Plugin.ModuleName == query);
}
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using CounterStrikeSharp.API.Core.Hosting;
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 IServiceProvider _serviceProvider;
private readonly ILogger<PluginManager> _logger;
public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ILogger<PluginManager> logger, IServiceProvider serviceProvider)
{
_scriptHostConfiguration = scriptHostConfiguration;
_logger = logger;
_serviceProvider = serviceProvider;
}
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();
foreach (var path in pluginAssemblyPaths)
{
try
{
LoadPlugin(path);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load plugin from {Path}", path);
}
}
}
public IEnumerable<PluginContext> GetLoadedPlugins()
{
return _loadedPluginContexts;
}
private void LoadPlugin(string path)
{
var plugin = new PluginContext(_serviceProvider, _scriptHostConfiguration, path, _loadedPluginContexts.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1);
_loadedPluginContexts.Add(plugin);
plugin.Load();
}
}

View File

@@ -0,0 +1,11 @@
namespace CounterStrikeSharp.API.Core.Plugin;
public interface IPluginContext
{
PluginState State { get; }
IPlugin Plugin { get; }
int PluginId { get; }
void Load(bool hotReload);
void Unload(bool hotReload);
}

View File

@@ -0,0 +1,206 @@
/*
* 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/>. *
*/
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace CounterStrikeSharp.API.Core.Plugin
{
public class PluginContext : IPluginContext
{
public PluginState State { get; set; } = PluginState.Unregistered;
public IPlugin Plugin { get; private set; }
private PluginLoader Loader { get; set; }
private IServiceProvider ServiceProvider { get; set; }
public int PluginId { get; }
private readonly IScriptHostConfiguration _hostConfiguration;
private readonly string _path;
private readonly FileSystemWatcher _fileWatcher;
private readonly IServiceProvider _applicationServiceProvider;
// TOOD: ServiceCollection
private ILogger _logger = CoreLogging.Factory.CreateLogger<PluginContext>();
public PluginContext(IServiceProvider applicationServiceProvider, IScriptHostConfiguration hostConfiguration, string path, int id)
{
_applicationServiceProvider = applicationServiceProvider;
_hostConfiguration = hostConfiguration;
_path = path;
PluginId = id;
Loader = PluginLoader.CreateFromAssemblyFile(path,
new[]
{
typeof(IPlugin), typeof(ILogger), typeof(IServiceCollection), typeof(IPluginServiceCollection<>)
}, config =>
{
config.EnableHotReload = true;
config.IsUnloadable = true;
});
_fileWatcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(path)
};
_fileWatcher.Deleted += async (s, e) =>
{
if (e.FullPath == path)
{
_logger.LogInformation("Plugin {Name} has been deleted, unloading...", Plugin.ModuleName);
Unload(true);
}
};
_fileWatcher.Filter = "*.dll";
_fileWatcher.EnableRaisingEvents = true;
Loader.Reloaded += async (s, e) => await OnReloadedAsync(s, e);
}
private Task OnReloadedAsync(object sender, PluginReloadedEventArgs eventargs)
{
_logger.LogInformation("Reloading plugin {Name}", Plugin.ModuleName);
Loader = eventargs.Loader;
Unload(hotReload: true);
Load(hotReload: true);
return Task.CompletedTask;
}
public void Load(bool hotReload = false)
{
if (State == PluginState.Loaded) return;
using (Loader.EnterContextualReflection())
{
var defaultAssembly = Loader.LoadDefaultAssembly();
Type pluginType = defaultAssembly.GetTypes()
.FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t));
if (pluginType == null) throw new Exception("Unable to find plugin in assembly");
var serviceCollection = new ServiceCollection();
serviceCollection.Scan(scan =>
scan.FromAssemblies(defaultAssembly)
.AddClasses(c => c.AssignableTo<IPlugin>())
.AsSelf()
.WithSingletonLifetime()
);
serviceCollection.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddSerilog(new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With(new PluginNameEnricher(this))
.WriteTo.Console(
outputTemplate:
"{Timestamp:HH:mm:ss} [{Level:u4}] (plugin:{PluginName}) {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
Path.Join(new[]
{
_hostConfiguration.RootPath, "logs",
$"log-{pluginType.Assembly.GetName().Name}.txt"
}), rollingInterval: RollingInterval.Day,
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] plugin:{PluginName} {Message:lj}{NewLine}{Exception}")
.WriteTo.File(Path.Join(new[] { _hostConfiguration.RootPath, "logs", $"log-all.txt" }),
rollingInterval: RollingInterval.Day, shared: true,
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] plugin:{PluginName} {Message:lj}{NewLine}{Exception}")
.CreateLogger());
});
Type interfaceType = typeof(IPluginServiceCollection<>).MakeGenericType(pluginType);
Type[] serviceCollectionConfiguratorTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => interfaceType.IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
.ToArray();
if (serviceCollectionConfiguratorTypes.Any())
{
foreach (var t in serviceCollectionConfiguratorTypes)
{
var pluginServiceCollection = Activator.CreateInstance(t);
MethodInfo method = t.GetMethod("ConfigureServices");
method?.Invoke(pluginServiceCollection, new object[] { serviceCollection });
}
}
serviceCollection.AddSingleton(this);
ServiceProvider = serviceCollection.BuildServiceProvider();
var minimumApiVersion = pluginType.GetCustomAttribute<MinimumApiVersion>()?.Version;
var currentVersion = Api.GetVersion();
// Ignore version 0 for local development
if (currentVersion > 0 && minimumApiVersion != null && minimumApiVersion > currentVersion)
throw new Exception(
$"Plugin \"{Path.GetFileName(_path)}\" requires a newer version of CounterStrikeSharp. The plugin expects version [{minimumApiVersion}] but the current version is [{currentVersion}].");
_logger.LogInformation("Loading plugin {Name}", pluginType.Assembly.GetName().Name);
Plugin = ServiceProvider.GetRequiredService(pluginType) as IPlugin;
if (Plugin == null) throw new Exception("Unable to create plugin instance");
State = PluginState.Loading;
Plugin.ModulePath = _path;
Plugin.RegisterAllAttributes(Plugin);
Plugin.Logger = ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(pluginType);
Plugin.InitializeConfig(Plugin, pluginType);
Plugin.Load(hotReload);
_logger.LogInformation("Finished loading plugin {Name}", Plugin.ModuleName);
State = PluginState.Loaded;
}
}
public void Unload(bool hotReload = false)
{
if (State == PluginState.Unloaded) return;
State = PluginState.Unloaded;
var cachedName = Plugin.ModuleName;
_logger.LogInformation("Unloading plugin {Name}", Plugin.ModuleName);
Plugin.Unload(hotReload);
Plugin.Dispose();
_logger.LogInformation("Finished unloading plugin {Name}", cachedName);
}
}
}

View File

@@ -0,0 +1,9 @@
namespace CounterStrikeSharp.API.Core.Plugin;
public enum PluginState
{
Unregistered,
Loading,
Loaded,
Unloaded,
}

View File

@@ -1,137 +0,0 @@
/*
* 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/>. *
*/
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Logging;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
{
public class PluginContext
{
private BasePlugin _plugin;
private PluginLoader _assemblyLoader;
public string Name => _plugin?.ModuleName;
public string Version => _plugin?.ModuleVersion;
public string Description => _plugin.ModuleDescription;
public string Author => _plugin.ModuleAuthor;
public Type PluginType => _plugin?.GetType();
public string PluginPath => _plugin?.ModulePath;
public int PluginId { get; }
private readonly string _path;
private readonly FileSystemWatcher _fileWatcher;
// TOOD: ServiceCollection
private ILogger _logger = CoreLogging.Factory.CreateLogger<PluginContext>();
public PluginContext(string path, int id)
{
_path = path;
PluginId = id;
_assemblyLoader = PluginLoader.CreateFromAssemblyFile(path, new[] { typeof(IPlugin) }, config =>
{
config.EnableHotReload = true;
config.IsUnloadable = true;
});
_fileWatcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(path)
};
_fileWatcher.Deleted += async (s, e) =>
{
if (e.FullPath == path)
{
_logger.LogInformation("Plugin {Name} has been deleted, unloading...", Name);
Unload(true);
}
};
_fileWatcher.Filter = "*.dll";
_fileWatcher.EnableRaisingEvents = true;
_assemblyLoader.Reloaded += async (s, e) => await OnReloadedAsync(s, e);
}
private Task OnReloadedAsync(object sender, PluginReloadedEventArgs eventargs)
{
_logger.LogInformation("Reloading plugin {Name}", Name);
_assemblyLoader = eventargs.Loader;
Unload(hotReload: true);
Load(hotReload: true);
return Task.CompletedTask;
}
public void Load(bool hotReload = false)
{
using (_assemblyLoader.EnterContextualReflection())
{
Type pluginType = _assemblyLoader.LoadDefaultAssembly().GetTypes()
.FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t));
if (pluginType == null) throw new Exception("Unable to find plugin in DLL");
var minimumApiVersion = pluginType.GetCustomAttribute<MinimumApiVersion>()?.Version;
var currentVersion = Api.GetVersion();
// Ignore version 0 for local development
if (currentVersion > 0 && minimumApiVersion != null && minimumApiVersion > currentVersion)
throw new Exception(
$"Plugin \"{Path.GetFileName(_path)}\" requires a newer version of CounterStrikeSharp. The plugin expects version [{minimumApiVersion}] but the current version is [{currentVersion}].");
_logger.LogInformation("Loading plugin {Name}", pluginType.Name);
_plugin = (BasePlugin)Activator.CreateInstance(pluginType)!;
_plugin.ModulePath = _path;
_plugin.RegisterAllAttributes(_plugin);
_plugin.Logger = PluginLogging.CreatePluginLogger(this);
_plugin.InitializeConfig(_plugin, pluginType);
_plugin.Load(hotReload);
_logger.LogInformation("Finished loading plugin {Name}", Name);
}
}
public void Unload(bool hotReload = false)
{
var cachedName = Name;
_logger.LogInformation("Unloading plugin {Name}", Name);
_plugin.Unload(hotReload);
_plugin.Dispose();
if (!hotReload)
{
_assemblyLoader.Dispose();
_fileWatcher.Dispose();
}
_logger.LogInformation("Finished unloading plugin {Name}", Name);
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
@@ -23,7 +23,10 @@
<ItemGroup>
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Scrutor" Version="4.2.2" />
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />

View File

@@ -0,0 +1,5 @@
// Global using directives
global using System;
global using System.IO;
global using CounterStrikeSharp.API.Core;

View File

@@ -73,7 +73,7 @@ namespace CounterStrikeSharp.API.Modules.Memory
return types[Enum.GetUnderlyingType(type)];
}
GlobalContext.Instance.Logger.LogWarning("Error retrieving data type for type {Type}", type.FullName);
Core.Application.Instance.Logger.LogWarning("Error retrieving data type for type {Type}", type.FullName);
return null;
}

View File

@@ -70,4 +70,10 @@ public static class VirtualFunctions
public static MemoryFunctionVoid<CCSPlayerPawnBase> CCSPlayerPawnBase_PostThinkFunc = new (GameData.GetSignature("CCSPlayerPawnBase_PostThink"));
public static Action<CCSPlayerPawnBase> CCSPlayerPawnBase_PostThink = CCSPlayerPawnBase_PostThinkFunc.Invoke;
public static MemoryFunctionVoid<CBaseTrigger, CBaseEntity> CBaseTrigger_StartTouchFunc = new (GameData.GetSignature("CBaseTrigger_StartTouch"));
public static Action<CBaseTrigger, CBaseEntity> CBaseTrigger_StartTouch = CBaseTrigger_StartTouchFunc.Invoke;
public static MemoryFunctionVoid<CBaseTrigger, CBaseEntity> CBaseTrigger_EndTouchFunc = new (GameData.GetSignature("CBaseTrigger_EndTouch"));
public static Action<CBaseTrigger, CBaseEntity> CBaseTrigger_EndTouch = CBaseTrigger_EndTouchFunc.Invoke;
}

View File

@@ -14,29 +14,29 @@
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
namespace CounterStrikeSharp.API.Modules.Utils
namespace CounterStrikeSharp.API.Modules.Utils;
public class ChatColors
{
public class ChatColors
{
public static char Default = '\x01';
public static char White = '\x01';
public static char Darkred = '\x02';
public static char Green = '\x04';
public static char LightYellow = '\x09';
public static char LightBlue = '\x0B';
public static char Olive = '\x05';
public static char Lime = '\x06';
public static char Red = '\x07';
public static char LightPurple = '\x03';
public static char Purple = '\x0E';
public static char Grey = '\x08';
public static char Yellow = '\x09';
public static char Gold = '\x10';
public static char Silver = '\x0A';
public static char Blue = '\x0B';
public static char DarkBlue = '\x0C';
public static char BlueGrey = '\x0A';
public static char Magenta = '\x0E';
public static char LightRed = '\x0F';
}
public static char Default = '\x01';
public static char White = '\x01';
public static char Darkred = '\x02';
public static char Green = '\x04';
public static char LightYellow = '\x09';
public static char LightBlue = '\x0B';
public static char Olive = '\x05';
public static char Lime = '\x06';
public static char Red = '\x07';
public static char LightPurple = '\x03';
public static char Purple = '\x0E';
public static char Grey = '\x08';
public static char Yellow = '\x09';
public static char Gold = '\x10';
public static char Silver = '\x0A';
public static char Blue = '\x0B';
public static char DarkBlue = '\x0C';
public static char BlueGrey = '\x0A';
public static char Magenta = '\x0E';
public static char LightRed = '\x0F';
public static char Orange = '\x10';
}

View File

@@ -14,8 +14,6 @@
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System;
namespace CounterStrikeSharp.API
{
[Flags]

View File

@@ -61,6 +61,13 @@ namespace TestPlugin
Config = config;
}
private TestInjectedClass _testInjectedClass;
public SamplePlugin(TestInjectedClass testInjectedClass)
{
_testInjectedClass = testInjectedClass;
}
public override void Load(bool hotReload)
{
// Basic usage of the configuration system
@@ -105,6 +112,28 @@ namespace TestPlugin
var result = virtualFunc() - 8;
Logger.LogInformation("Result of virtual func call is {Pointer:X}", result);
_testInjectedClass.Hello();
VirtualFunctions.CBaseTrigger_StartTouchFunc.Hook(h =>
{
var trigger = h.GetParam<CBaseTrigger>(0);
var entity = h.GetParam<CBaseEntity>(1);
Logger.LogInformation("Trigger {Trigger} touched by {Entity}", trigger.DesignerName, entity.DesignerName);
return HookResult.Continue;
}, HookMode.Post);
VirtualFunctions.CBaseTrigger_EndTouchFunc.Hook(h =>
{
var trigger = h.GetParam<CBaseTrigger>(0);
var entity = h.GetParam<CBaseEntity>(1);
Logger.LogInformation("Trigger left {Trigger} by {Entity}", trigger.DesignerName, entity.DesignerName);
return HookResult.Continue;
}, HookMode.Post);
VirtualFunctions.UTIL_RemoveFunc.Hook(hook =>
{
var entityInstance = hook.GetParam<CEntityInstance>(0);

View File

@@ -0,0 +1,29 @@
using System;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace TestPlugin;
public class TestInjectedClass
{
private readonly ILogger<TestInjectedClass> _logger;
public TestInjectedClass(ILogger<TestInjectedClass> logger)
{
_logger = logger;
}
public void Hello()
{
_logger.LogInformation("Hello World from Test Injected Class");
}
}
public class TestPluginServiceCollection : IPluginServiceCollection<SamplePlugin>
{
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<TestInjectedClass>();
}
}

View File

@@ -199,16 +199,16 @@ bool CDotNetManager::Initialize()
const std::string dotnetlib_path =
std::string((base_dir + "/api/CounterStrikeSharp.API.dll").c_str());
#endif
const auto dotnet_type = STR("CounterStrikeSharp.API.Core.Helpers, CounterStrikeSharp.API");
const auto dotnet_type = STR("CounterStrikeSharp.API.Bootstrap, CounterStrikeSharp.API");
// Namespace, assembly name
typedef int(CORECLR_DELEGATE_CALLTYPE * custom_entry_point_fn)();
custom_entry_point_fn entry_point = nullptr;
const int rc = load_assembly_and_get_function_pointer(
dotnetlib_path.c_str(), dotnet_type, STR("LoadAllPlugins"), UNMANAGEDCALLERSONLY_METHOD,
dotnetlib_path.c_str(), dotnet_type, STR("Run"), UNMANAGEDCALLERSONLY_METHOD,
nullptr, reinterpret_cast<void**>(&entry_point));
if (entry_point == nullptr) {
CSSHARP_CORE_ERROR("Trying to get entry point \"LoadAllPlugins\" but failed.");
CSSHARP_CORE_ERROR("Trying to get entry point \"Bootstrap::Run\" but failed.");
return false;
}
@@ -216,7 +216,7 @@ bool CDotNetManager::Initialize()
"Failure: load_assembly_and_get_function_pointer()");
if (const int invoke_result_code = entry_point(); invoke_result_code == 0) {
CSSHARP_CORE_ERROR("LoadAllPlugins return failure.");
CSSHARP_CORE_ERROR("Bootstrap::Run return failure.");
return false;
}