mirror of
https://github.com/roflmuffin/CounterStrikeSharp.git
synced 2025-12-07 08:26:34 -08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e8c18abc7 | ||
|
|
8d1891a3a8 | ||
|
|
6bc43444f7 | ||
|
|
f0c7869f4a |
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
71
docs/src/content/docs/guides/dependency-injection.md
Normal file
71
docs/src/content/docs/guides/dependency-injection.md
Normal 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: How to get started installing & using CounterStrikeSharp.
|
||||
sidebar:
|
||||
order: 0
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
title: Hello World Plugin
|
||||
description: How to write your first plugin for CounterStrikeSharp
|
||||
sidebar:
|
||||
order: 0
|
||||
---
|
||||
|
||||
## Creating a New Project
|
||||
|
||||
67
managed/CounterStrikeSharp.API/Bootstrap.cs
Normal file
67
managed/CounterStrikeSharp.API/Bootstrap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
256
managed/CounterStrikeSharp.API/Core/Application.cs
Normal file
256
managed/CounterStrikeSharp.API/Core/Application.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
6
managed/CounterStrikeSharp.API/Core/IStartupService.cs
Normal file
6
managed/CounterStrikeSharp.API/Core/IStartupService.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace CounterStrikeSharp.API.Core;
|
||||
|
||||
public interface IStartupService
|
||||
{
|
||||
public void Load();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CounterStrikeSharp.API.Core.Plugin.Host;
|
||||
|
||||
public interface IPluginManager
|
||||
{
|
||||
public void Load();
|
||||
public IEnumerable<PluginContext> GetLoadedPlugins();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
11
managed/CounterStrikeSharp.API/Core/Plugin/IPluginContext.cs
Normal file
11
managed/CounterStrikeSharp.API/Core/Plugin/IPluginContext.cs
Normal 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);
|
||||
}
|
||||
206
managed/CounterStrikeSharp.API/Core/Plugin/PluginContext.cs
Normal file
206
managed/CounterStrikeSharp.API/Core/Plugin/PluginContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace CounterStrikeSharp.API.Core.Plugin;
|
||||
|
||||
public enum PluginState
|
||||
{
|
||||
Unregistered,
|
||||
Loading,
|
||||
Loaded,
|
||||
Unloaded,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
5
managed/CounterStrikeSharp.API/GlobalUsings.cs
Normal file
5
managed/CounterStrikeSharp.API/GlobalUsings.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
// Global using directives
|
||||
|
||||
global using System;
|
||||
global using System.IO;
|
||||
global using CounterStrikeSharp.API.Core;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -14,8 +14,6 @@
|
||||
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace CounterStrikeSharp.API
|
||||
{
|
||||
[Flags]
|
||||
|
||||
@@ -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);
|
||||
|
||||
29
managed/TestPlugin/TestPluginServiceCollection.cs
Normal file
29
managed/TestPlugin/TestPluginServiceCollection.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user