Compare commits

..

16 Commits

Author SHA1 Message Date
roflmuffin
36a97bfffd fix: disable autoload plugin but allow shared library loading 2024-03-09 15:29:45 +10:00
roflmuffin
178f7472c6 feat: add PluginAutoLoadEnabled config option
closes #370
2024-03-09 14:29:43 +10:00
luxury fabka
cba5144bbf remove unused arguments (#334)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2024-03-08 05:08:33 +00:00
roflmuffin
0de951cb6f fix: allows game events to be freed, frees event in print to center html
closes #346
2024-03-08 10:39:59 +10:00
Michael Wilson
c3d44a87bc feat: use concurrent queue for next frame & world update tasks (#365) 2024-03-06 21:08:22 +10:00
B3none
bd3c0c76e3 [no ci] Update minimum api version for shared api docs (#364) 2024-03-06 15:42:34 +10:00
Yarukon
656c0e3a84 Fix TerminateRound params (#363) 2024-03-04 10:59:02 +00:00
Michael Wilson
40c842149c fix: add error handling to OnAllPluginsLoaded 2024-03-04 17:21:50 +10:00
roflmuffin
64d1c0a9f4 chore: remove erroneous log 2024-03-04 16:56:49 +10:00
roflmuffin
a6de51c444 fix: use concurrent dictionary for function reference 2024-03-04 13:41:18 +10:00
roflmuffin
2535ac0575 feat: add assembly name lazy loading of shared libraries 2024-03-04 12:15:37 +10:00
Michael Wilson
bc61323315 chore: migrate to protobufs submodule (#362) 2024-03-04 10:48:52 +10:00
roflmuffin
241817b7f2 feat: update game events dump from Feb 14 update 2024-03-04 10:34:58 +10:00
BuSheeZy
fbcdce34fc fix: allow empty overrides to skip checks (#357)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2024-03-04 00:13:17 +00:00
Michael Wilson
daf0d25f36 Shared Plugin APIs/Capabilities (#253)
Co-authored-by: B3none <ablackham2000@gmail.com>
Co-authored-by: B3none <24966460+B3none@users.noreply.github.com>
2024-03-04 09:45:34 +10:00
Yarukon
12485be29f Adding the proper way to do resource precache (#358) 2024-03-02 15:10:37 +10:00
67 changed files with 4754 additions and 179 deletions

6
.gitmodules vendored
View File

@@ -14,12 +14,12 @@
[submodule "libraries/dyncall"]
path = libraries/dyncall
url = https://github.com/LWJGL-CI/dyncall
[submodule "libraries/GameTracking-CS2"]
path = libraries/GameTracking-CS2
url = https://github.com/SteamDatabase/GameTracking-CS2
[submodule "libraries/DynoHook"]
path = libraries/DynoHook
url = https://github.com/qubka/DynoHook
[submodule "libraries/asmjit"]
path = libraries/asmjit
url = https://github.com/asmjit/asmjit
[submodule "libraries/Protobufs"]
path = libraries/Protobufs
url = https://github.com/SteamDatabase/Protobufs

View File

@@ -90,6 +90,8 @@ SET(SOURCE_FILES
src/core/managers/voice_manager.cpp
src/core/managers/voice_manager.h
src/scripting/natives/natives_dynamichooks.cpp
src/core/game_system.h
src/core/game_system.cpp
)
@@ -101,8 +103,8 @@ if (LINUX)
)
endif()
set(PROTO_DIRS -I${CMAKE_CURRENT_SOURCE_DIR}/libraries/GameTracking-CS2/Protobufs)
file(GLOB PROTOS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/GameTracking-CS2/Protobufs/*.proto")
set(PROTO_DIRS -I${CMAKE_CURRENT_SOURCE_DIR}/libraries/Protobufs/csgo)
file(GLOB PROTOS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/Protobufs/csgo/*.proto")
## Generate protobuf source & headers
if (LINUX)

View File

@@ -3,5 +3,6 @@
"SilentChatTrigger": [ "/" ],
"FollowCS2ServerGuidelines": true,
"PluginHotReloadEnabled": true,
"PluginAutoLoadEnabled": true,
"ServerLanguage": "en"
}

View File

@@ -206,5 +206,18 @@
"windows": "\\x4C\\x89\\x4C\\x24\\x20\\x53\\x55\\x57\\x41\\x54\\x41\\x56\\x48\\x81\\xEC",
"linux": "\\x55\\x48\\x89\\xE5\\x41\\x57\\x41\\x56\\x41\\x55\\x41\\x54\\x49\\x89\\xD4\\x53\\x48\\x89\\xF3\\x48\\x83\\xEC\\x58"
}
},
"IGameSystem_InitAllSystems_pFirst": {
"signatures": {
"library": "server",
"windows": "\\x48\\x8B\\x1D\\x2A\\x2A\\x2A\\x2A\\x48\\x85\\xDB\\x0F\\x84\\x2A\\x2A\\x2A\\x2A\\xBE\\x2A\\x2A\\x2A\\x2A\\x0F\\x1F\\x00\\x48\\x8B\\x7B\\x10",
"linux": "\\x4C\\x8B\\x35\\x2A\\x2A\\x2A\\x2A\\x4D\\x85\\xF6\\x75\\x2D\\xE9\\x2A\\x2A\\x2A\\x2A\\x0F\\x1F\\x40\\x00\\x48\\x8B\\x05"
}
},
"CEntityResourceManifest_AddResource": {
"offsets": {
"windows": 2,
"linux": 2
}
}
}

View File

@@ -0,0 +1 @@
This folder should contain any shared APIs, in the same DLL structure as the plugins folder (MySharedApi/MySharedApi.dll)

View File

@@ -0,0 +1,67 @@
---
title: Shared Plugin API (Capabilities)
description: How to add inter-plugin communication to CounterStrikeSharp plugins.
---
# Shared Plugin API
How to expose and use shared plugin APIs between multiple plugins.
## Creating a Contract Library
Inter-plugin communication requires a contract/shared library that simply exposes the shape of the API, using simple interfaces. e.g.
```csharp
public interface IBalanceHandler
{
decimal Balance { get; }
// These are just here to show that you can have methods on your shared types.
// You could also add a Setter to the Balance property.
public decimal Add(decimal amount);
public decimal Subtract(decimal amount);
}
```
This library ideally should not contain any business logic, and simply define the schema for callers.
This library should be placed in the `shared` subfolder, in the same folder layout as the plugins folder. So if a contract DLL is named `MySharedApi.dll` its file path should be: `shared/MySharedApi/MySharedApi.dll`.
## Creating a Capability
A capability can be declared with a simple static variable in your plugin class. A `PlayerCapability` is a player specific capability, and a `PluginCapability` is generic functionality that a plugin can expose.
```csharp
public static PlayerCapability<IBalanceHandler> BalanceCapability { get; } = new("myplugin:balance");
public static PluginCapability<IBalanceService> BalanceServiceCapability { get; } = new("myplugin:balance_service");
```
For every plugin that wishes to use this new "Balance API", they must ensure they create a capability using the shared API interface (`IBalanceHandler`), as well as use the same name (`myplugin:balance`).
## Registering a Capability
If you are the plugin that is expected to provide the basis of the API (i.e. you are providing a currency/balance plugin which does nothing except store users balances), then you will need to provide the implementation that other callers will use. This is done through the use of static members on the `Capabilities` class:
```csharp
// Player capabilities are given the calling player context
Capabilities.RegisterPlayerCapability(BalanceCapability, player => new BalanceHandler(player));
// Plugin capabilities can simply return an instance of the interface
Capabilities.RegisterPluginCapability(BalanceServiceCapability, () => new BalanceService());
```
### Using Capabilities
To utilise a capability, simply call the `.Get()` method provided on the static capability you declared earlier, i.e.
```csharp
var balance = BalanceCapability.Get(player);
var balanceService = BalanceServiceCapability.Get();
if (balance == null) return;
balance.Add(500);
```
This value _MUST_ be checked for null, as if there are no plugins providing implementations for a given capability, this method will return null, and you must handle this flow in your plugin.

View File

@@ -9,3 +9,6 @@
- name: Global Listeners
href: global-listeners.md
- name: Shared Plugin API
href: shared-plugin-api.md

View File

@@ -33,6 +33,10 @@ receive a ban.
When enabled, plugins are automatically reloaded when their .dll file is updated.
## PluginAutoLoadEnabled
When enabled, plugins are automatically loaded from the plugins directory on server start.
## ServerLanguage
Configures the default language to use for server commands & messages. The format for the culture name based on RFC 4646 is `languagecode2-country`/`regioncode2`, where `languagecode2` is the two-letter language code and `country/regioncode2` is the two-letter subculture code. Examples include `ja-JP` for Japanese (Japan) and `en-US` for English (United States). Defaults to "en".

View File

@@ -0,0 +1,11 @@
[!INCLUDE [WithSharedTypes](../../examples/WithSharedTypes/README.md)]
<a href="https://github.com/roflmuffin/CounterStrikeSharp/tree/main/examples/WithSharedTypes" class="btn btn-secondary">View project on Github <i class="bi bi-github"></i></a>
[!code-csharp[](../../examples/WithSharedTypes/WithSharedTypesPlugin.cs)]
[!INCLUDE [WithSharedTypesConsumer](../../examples/WithSharedTypesConsumer/README.md)]
<a href="https://github.com/roflmuffin/CounterStrikeSharp/tree/main/examples/WithSharedTypesConsumer" class="btn btn-secondary">View project on Github <i class="bi bi-github"></i></a>
[!code-csharp[](../../examples/WithSharedTypesConsumer/WithSharedTypesConsumerPlugin.cs)]

View File

@@ -15,6 +15,8 @@ items:
href: WithGameEventHandlers.md
- name: Database (Dapper)
href: WithDatabase.md
- name: Shared Plugin Types
href: WithSharedTypes.md
- name: Translations
href: WithTranslations.md
- name: Voice Overrides

View File

@@ -0,0 +1,11 @@
namespace MySharedTypes.Contracts;
public interface IBalanceHandler
{
decimal Balance { get; }
// These are just here to show that you can have methods on your shared types.
// You could also add a Setter to the Balance property.
public decimal Add(decimal amount);
public decimal Subtract(decimal amount);
}

View File

@@ -0,0 +1,6 @@
namespace MySharedTypes.Contracts;
public interface IBalanceService
{
public void ClearAllBalances();
}

View File

@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,33 @@
using CounterStrikeSharp.API.Core;
using MySharedTypes.Contracts;
namespace WithSharedTypes;
public class BalanceHandler : IBalanceHandler
{
private readonly CCSPlayerController _player;
// This could be a database, a file, or a dictionary like this.
internal static readonly Dictionary<CCSPlayerController, decimal> Balances = new();
public BalanceHandler(CCSPlayerController player)
{
_player = player;
}
public decimal Balance
{
get => Balances.TryGetValue(_player, out var balance) ? balance : 0;
set => Balances[_player] = value;
}
public decimal Add(decimal amount)
{
return Balance += amount;
}
public decimal Subtract(decimal amount)
{
return Balance -= amount;
}
}

View File

@@ -0,0 +1,11 @@
using MySharedTypes.Contracts;
namespace WithSharedTypes;
public class BalanceService : IBalanceService
{
public void ClearAllBalances()
{
BalanceHandler.Balances.Clear();
}
}

View File

@@ -0,0 +1,5 @@
# With Shared Types (Capabilities)
An example plugin that exposes a balance contract library, to use as a shared library between multiple plugins.
This allows one plugin to expose a capability for a player or plugin, and other plugins to use the exposed API.

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\managed\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
<ProjectReference Include="..\MySharedTypes.Contracts\MySharedTypes.Contracts\MySharedTypes.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,57 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Capabilities;
using MySharedTypes.Contracts;
namespace WithSharedTypes;
[MinimumApiVersion(184)]
public class WithSharedTypesPlugin : BasePlugin
{
public override string ModuleName => "Example: Shared Types";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that shares types between multiple plugins";
// Declares a player capability, that stores some sort of functionality for a player.
// In this case, it's a balance handler, which is used to store a player's balance.
// Note that we use the same name for the capability as the one in the other plugin.
// IBalanceHandler is defined in MySharedTypes.Contracts, which is a shared library and placed in the `shared/` subfolder.
public static PlayerCapability<IBalanceHandler> BalanceCapability { get; } = new("myplugin:balance");
// Declares a player capability of a primitive type, in this case, a decimal.
public static PlayerCapability<Decimal> BalanceCapabilityDecimal { get; } = new("myplugin:balance_decimal");
// Plugin capabilities are similar to player capabilities, but they are not tied to a player, and are just generic APIs
// that are exposed by a plugin. In this case, we expose a balance service, which is used to clear all balances.
public static PluginCapability<IBalanceService> BalanceServiceCapability { get; } = new("myplugin:balance_service");
public override void Load(bool hotReload)
{
// Register the capability implementations here. Note that plugins don't need to register an implementation if it is already implemented in another plugin.
Capabilities.RegisterPlayerCapability(BalanceCapability, player => new BalanceHandler(player));
Capabilities.RegisterPluginCapability(BalanceServiceCapability, () => new BalanceService());
Capabilities.RegisterPlayerCapability(BalanceCapabilityDecimal, (player) => new BalanceHandler(player).Balance);
AddCommand("css_balance", "Gets your current balance", (player, info) =>
{
if (player == null) return;
player.PrintToChat($"Your balance is {BalanceCapability.Get(player)?.Balance}");
});
AddCommand("css_give", "Gives you money", (player, info) =>
{
if (player == null) return;
var balance = BalanceCapability.Get(player);
if (balance == null) return;
balance.Add(100);
player.PrintToChat($"Your balance is now {balance.Balance}");
});
}
public override void Unload(bool hotReload)
{
}
}

View File

@@ -0,0 +1,2 @@
# With Shared Types (Consumer Plugin)
Uses the decimal balance shared library.

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\managed\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
<ProjectReference Include="..\MySharedTypes.Contracts\MySharedTypes.Contracts\MySharedTypes.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,56 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Core.Plugin;
using MySharedTypes.Contracts;
namespace WithSharedTypesConsumer;
[MinimumApiVersion(184)]
public class WithSharedTypesConsumerPlugin : BasePlugin
{
public override string ModuleName => "Example: Shared Types (Consumer)";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that utilises the balance api from another plugin";
public static PlayerCapability<IBalanceHandler> BalanceCapability { get; } = new("myplugin:balance");
public static PlayerCapability<Decimal> BalanceCapabilityDecimal { get; } = new("myplugin:balance_decimal");
public static PluginCapability<IBalanceService> BalanceServiceCapability { get; } = new("myplugin:balance_service");
public override void Load(bool hotReload)
{
AddCommand("css_subtract", "Subtracts 50 from your balance", (player, info) =>
{
if (player == null) return;
var balance = BalanceCapability.Get(player);
if (balance == null) return;
balance.Subtract(50);
player.PrintToChat($"Your balance is now {balance.Balance}");
});
AddCommand("css_clearbalances", "Clears all balances", (player, info) =>
{
if (player == null) return;
var service = BalanceServiceCapability.Get();
if (service == null) return;
service.ClearAllBalances();
var balance = BalanceCapability.Get(player);
if (balance == null) return;
player.PrintToChat($"Your balance is now {balance.Balance}");
});
AddCommand("css_decimalbalance", "Gets your current balance", (player, info) =>
{
if (player == null) return;
player.PrintToChat($"Your balance is {BalanceCapabilityDecimal.Get(player)}");
});
}
public override void Unload(bool hotReload)
{
}
}

1
libraries/Protobufs Submodule

Submodule libraries/Protobufs added at 686a0628e6

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,7 @@ include_directories(
libraries/tl
libraries/funchook/include
libraries/DynoHook/src
libraries/moodycamel
libraries
)

View File

@@ -427,6 +427,18 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.FunctionReference.Create(System.Delegate)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.FunctionReference.Get(System.Int32)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.NativeAPI.QueueTaskForNextFrame(System.IntPtr)</Target>
@@ -451,6 +463,24 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.Translations.JsonStringLocalizerFactory.#ctor(CounterStrikeSharp.API.Core.Plugin.IPluginContext)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.IPlugin.OnAllPluginsLoaded(System.Boolean)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration.SharedPath</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:CounterStrikeSharp.API.Core.IPlugin.CommandManager</Target>

View File

@@ -749,6 +749,16 @@ namespace CounterStrikeSharp.API.Core
}
}
public static void FreeEvent(IntPtr gameevent){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(gameevent);
ScriptContext.GlobalScriptContext.SetIdentifier(0x7E8B60C2);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static void FireEvent(IntPtr gameevent, bool dontbroadcast){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();

View File

@@ -130,11 +130,11 @@ namespace CounterStrikeSharp.API.Core
{
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))
plugin.State.ToString().ToUpper(), plugin.Plugin?.ModuleName ?? "Unknown",
plugin.Plugin?.ModuleVersion ?? "Unknown");
if (!string.IsNullOrEmpty(plugin.Plugin?.ModuleAuthor))
sb.AppendFormat(" by {0}", plugin.Plugin.ModuleAuthor);
if (!string.IsNullOrEmpty(plugin.Plugin.ModuleDescription))
if (!string.IsNullOrEmpty(plugin.Plugin?.ModuleDescription))
{
sb.Append("\n");
sb.Append(" ");
@@ -175,6 +175,8 @@ namespace CounterStrikeSharp.API.Core
try
{
_pluginManager.LoadPlugin(path);
plugin = _pluginContextQueryHandler.FindPluginByModulePath(path);
plugin.Plugin.OnAllPluginsLoaded(false);
}
catch (Exception e)
{
@@ -185,6 +187,7 @@ namespace CounterStrikeSharp.API.Core
else
{
plugin.Load(false);
plugin.Plugin.OnAllPluginsLoaded(false);
}
break;
@@ -233,6 +236,7 @@ namespace CounterStrikeSharp.API.Core
plugin.Unload(true);
plugin.Load(true);
plugin.Plugin.OnAllPluginsLoaded(true);
break;
}

View File

@@ -73,6 +73,10 @@ namespace CounterStrikeSharp.API.Core
public virtual void Unload(bool hotReload)
{
}
public virtual void OnAllPluginsLoaded(bool hotReload)
{
}
public class CallbackSubscriber : IDisposable
{

View File

@@ -0,0 +1,25 @@
namespace CounterStrikeSharp.API.Core.Capabilities;
public static class Capabilities
{
public static void RegisterPluginCapability<T>(PluginCapability<T> capability, Func<T> supplier)
{
if (!PluginCapability<T>.Providers.ContainsKey(capability.Name))
{
PluginCapability<T>.Providers.Add(capability.Name, new());
}
PluginCapability<T>.Providers[capability.Name].Add(supplier);
}
public static void RegisterPlayerCapability<T>(PlayerCapability<T> capability,
Func<CCSPlayerController, T> supplier)
{
if (!PlayerCapability<T>.Providers.ContainsKey(capability.Name))
{
PlayerCapability<T>.Providers.Add(capability.Name, new());
}
PlayerCapability<T>.Providers[capability.Name].Add(supplier);
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace CounterStrikeSharp.API.Core.Capabilities;
public sealed class PlayerCapability<T>
{
public string Name { get; }
internal static readonly Dictionary<string, List<Func<CCSPlayerController, T>>> Providers = new();
public PlayerCapability(string name)
{
Name = name;
}
public T? Get(CCSPlayerController entity)
{
foreach (var provider in Providers[Name])
{
return provider(entity);
}
return default;
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace CounterStrikeSharp.API.Core.Capabilities;
public sealed class PluginCapability<T>
{
public string Name { get; }
internal static readonly Dictionary<string, List<Func<T>>> Providers = new();
public PluginCapability(string name)
{
Name = name;
}
public T? Get()
{
foreach (var provider in Providers[Name])
{
return provider();
}
return default;
}
}

View File

@@ -106,6 +106,11 @@ public class CommandManager : ICommandManager
foreach (var attr in permissionsToCheck)
{
if (attr.Permissions.Count == 0)
{
continue;
}
attr.Command = name;
if (!attr.CanExecuteCommand(caller))
{

View File

@@ -50,6 +50,9 @@ namespace CounterStrikeSharp.API.Core
[JsonPropertyName("PluginHotReloadEnabled")]
public bool PluginHotReloadEnabled { get; set; } = true;
[JsonPropertyName("PluginAutoLoadEnabled")]
public bool PluginAutoLoadEnabled { get; set; } = true;
[JsonPropertyName("ServerLanguage")]
public string ServerLanguage { get; set; } = "en";
}
@@ -95,7 +98,13 @@ namespace CounterStrikeSharp.API.Core
/// </summary>
public static bool PluginHotReloadEnabled => _coreConfig.PluginHotReloadEnabled;
/// <summary>
/// When enabled, plugins are automatically loaded from the plugins directory on server start.
/// </summary>
public static bool PluginAutoLoadEnabled => _coreConfig.PluginAutoLoadEnabled;
public static string ServerLanguage => _coreConfig.ServerLanguage;
}
public partial class CoreConfig : IStartupService

View File

@@ -14,10 +14,9 @@
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -29,140 +28,149 @@ namespace CounterStrikeSharp.API.Core
{
/// <summary>Delegate will be removed after the first invocation.</summary>
SingleUse,
/// <summary>Delegate will remain in memory for the lifetime of the application.</summary>
/// <summary>Delegate will remain in memory for the lifetime of the application (or until <see cref="FunctionReference.Remove"/> is called).</summary>
Permanent
}
/// <summary>
/// Represents a reference to a function that can be called from native code.
/// </summary>
public class FunctionReference
{
private readonly Delegate m_method;
public FunctionLifetime Lifetime { get; set; } = FunctionLifetime.Permanent;
public unsafe delegate void CallbackDelegate(fxScriptContext* context);
private CallbackDelegate s_callback;
private FunctionReference(Delegate method)
private static readonly ConcurrentDictionary<int, FunctionReference> IdToFunctionReferencesMap = new();
private static readonly ConcurrentDictionary<Delegate, FunctionReference> TargetMethodToFunctionReferencesMap = new();
private static readonly object ReferenceCounterLock = new();
private static int _referenceCounter;
private readonly Delegate _targetMethod;
private readonly CallbackDelegate _nativeCallback;
private readonly TaskCompletionSource _taskCompletionSource = new();
private FunctionReference(Delegate method, FunctionLifetime lifetime)
{
m_method = method;
unsafe
{
var dg = new CallbackDelegate((fxScriptContext* context) =>
{
try
{
var scriptContext = new ScriptContext(context);
if (method.Method.GetParameters().FirstOrDefault()?.ParameterType == typeof(ScriptContext))
{
var returnO = m_method.DynamicInvoke(scriptContext);
if (returnO != null)
{
scriptContext.SetResult(returnO, context);
}
return;
}
var paramsList = method.Method.GetParameters().Select((x, i) =>
{
var param = method.Method.GetParameters()[i];
object obj = null;
if (typeof(NativeObject).IsAssignableFrom(param.ParameterType))
{
obj = Activator.CreateInstance(param.ParameterType,
new[] { scriptContext.GetArgument(typeof(IntPtr), i) });
}
else
{
obj = scriptContext.GetArgument(param.ParameterType, i);
}
return obj;
}).ToArray();
var returnObj = m_method.DynamicInvoke(paramsList);
if (returnObj != null)
{
scriptContext.SetResult(returnObj, context);
}
}
catch (Exception e)
{
Application.Instance.Logger.LogError(e, "Error invoking callback");
}
finally
{
if (Lifetime == FunctionLifetime.SingleUse)
{
Remove(Identifier);
if (references.ContainsKey(m_method))
references.Remove(m_method);
}
}
});
s_callback = dg;
}
Lifetime = lifetime;
_targetMethod = method;
_nativeCallback = CreateWrappedCallback();
}
/// <summary>
/// <inheritdoc cref="FunctionLifetime"/>
/// </summary>
public FunctionLifetime Lifetime { get; }
/// <summary>
/// For <see cref="FunctionLifetime.SingleUse"/> function references, this task will complete when
/// the function has finished invoking.
/// </summary>
public Task CompletionTask => _taskCompletionSource.Task;
public int Identifier { get; private set; }
public static FunctionReference Create(Delegate method)
private unsafe CallbackDelegate CreateWrappedCallback()
{
if (references.ContainsKey(method))
return context =>
{
return references[method];
try
{
var scriptContext = new ScriptContext(context);
// Allow for manual handling of the script context
if (_targetMethod.Method.GetParameters().FirstOrDefault()?.ParameterType == typeof(ScriptContext))
{
var returnValue = _targetMethod.DynamicInvoke(scriptContext);
if (returnValue != null)
{
scriptContext.SetResult(returnValue, context);
}
return;
}
var parameterList = _targetMethod.Method.GetParameters().Select((_, i) =>
{
var parameter = _targetMethod.Method.GetParameters()[i];
return scriptContext.GetArgument(parameter.ParameterType, i);
}).ToArray();
var returnObj = _targetMethod.DynamicInvoke(parameterList);
if (returnObj != null)
{
scriptContext.SetResult(returnObj, context);
}
}
catch (Exception e)
{
Application.Instance.Logger.LogError(e, "Error invoking callback");
}
finally
{
if (Lifetime == FunctionLifetime.SingleUse)
{
RemoveSelf();
}
_taskCompletionSource.TrySetResult();
}
};
}
public static FunctionReference Create(Delegate method, FunctionLifetime lifetime = FunctionLifetime.Permanent)
{
// We always want to create a new reference if the lifetime is single use.
if (lifetime == FunctionLifetime.Permanent && TargetMethodToFunctionReferencesMap.TryGetValue(method, out var existingReference))
{
return existingReference;
}
var reference = new FunctionReference(method);
var reference = new FunctionReference(method, lifetime);
var referenceId = Register(reference);
reference.Identifier = referenceId;
return reference;
}
private static Dictionary<int, FunctionReference> ms_references = new Dictionary<int, FunctionReference>();
private static int ms_referenceId;
private static Dictionary<Delegate, FunctionReference> references =
new Dictionary<Delegate, FunctionReference>();
private static int Register(FunctionReference reference)
{
var thisRefId = ms_referenceId;
ms_references[thisRefId] = reference;
references[reference.m_method] = reference;
unchecked { ms_referenceId++; }
return thisRefId;
}
public static FunctionReference Get(int reference)
{
if (ms_references.ContainsKey(reference))
lock (ReferenceCounterLock)
{
return ms_references[reference];
}
var thisRefId = _referenceCounter;
IdToFunctionReferencesMap[thisRefId] = reference;
TargetMethodToFunctionReferencesMap[reference._targetMethod] = reference;
return null;
unchecked
{
_referenceCounter++;
}
return thisRefId;
}
}
public IntPtr GetFunctionPointer()
public IntPtr GetFunctionPointer() => Marshal.GetFunctionPointerForDelegate(_nativeCallback);
private void RemoveSelf()
{
IntPtr cb = Marshal.GetFunctionPointerForDelegate(s_callback);
return cb;
Remove(Identifier);
}
public static void Remove(int reference)
{
if (ms_references.TryGetValue(reference, out var funcRef))
if (IdToFunctionReferencesMap.TryGetValue(reference, out var functionReference))
{
ms_references.Remove(reference);
if (TargetMethodToFunctionReferencesMap.ContainsKey(functionReference._targetMethod))
{
TargetMethodToFunctionReferencesMap.Remove(functionReference._targetMethod, out _);
}
IdToFunctionReferencesMap.Remove(reference, out _);
Application.Instance.Logger.LogDebug("Removing function/callback reference: {Reference}", reference);
}

View File

@@ -7119,6 +7119,15 @@ namespace CounterStrikeSharp.API.Core
}
}
[EventName("warmup_end")]
public class EventWarmupEnd : GameEvent
{
public EventWarmupEnd(IntPtr pointer) : base(pointer){}
public EventWarmupEnd(bool force) : base("warmup_end", force){}
}
[EventName("weapon_fire")]
public class EventWeaponFire : GameEvent
{

View File

@@ -18,6 +18,12 @@ public interface IScriptHostConfiguration
/// </summary>
string PluginPath { get; }
/// <summary>
/// Gets the absolute path to the directory that contains CounterStrikeSharp plugin shared APIS.
/// e.g. /game/csgo/addons/counterstrikesharp/shared
/// </summary>
string SharedPath { get; }
/// <summary>
/// Gets the absolute path to the directory that contains CounterStrikeSharp configs.
/// e.g. /game/csgo/addons/counterstrikesharp/configs

View File

@@ -7,12 +7,14 @@ internal sealed class ScriptHostConfiguration : IScriptHostConfiguration
{
public string RootPath { get; }
public string PluginPath { get; }
public string SharedPath { get; }
public string ConfigsPath { get; }
public string GameDataPath { get; }
public ScriptHostConfiguration(IHostEnvironment hostEnvironment)
{
RootPath = Path.Join(new[] { hostEnvironment.ContentRootPath });
SharedPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "shared" });
PluginPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "plugins" });
ConfigsPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "configs" });
GameDataPath = Path.Join(new[] { hostEnvironment.ContentRootPath, "gamedata" });

View File

@@ -53,6 +53,14 @@ namespace CounterStrikeSharp.API.Core
/// </summary>
void Unload(bool hotReload);
/// <summary>
/// Will be called by CounterStrikeSharp after all plugins have been loaded.
/// This will also be called for convenience after a reload or a late l oad, so that you don't have to handle
/// re-wiring everything.
/// </summary>
/// <param name="hotReload"></param>
void OnAllPluginsLoaded(bool hotReload);
string ModulePath { get; internal set; }
ILogger Logger { get; set; }

View File

@@ -2,6 +2,7 @@
using System;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Core
{
@@ -154,5 +155,13 @@ namespace CounterStrikeSharp.API.Core
/// <param name="simulating"><see langword="true"/> if simulating, <see langword="false"/> otherwise</param>
[ListenerName("OnServerPreWorldUpdate")]
public delegate void OnServerPreWorldUpdate(bool simulating);
/// <summary>
/// Called when the server precaching resources (only when initial startup / changing map).
/// </summary>
/// <param name="manifest">Resource Manifest</param>
[ListenerName("OnServerPrecacheResources")]
public delegate void OnServerPrecacheResources(ResourceManifest manifest);
}
}
}

View File

@@ -26,6 +26,6 @@ public partial class CCSGameRules
/// </summary>
public void TerminateRound(float delay, RoundEndReason roundEndReason)
{
VirtualFunctions.TerminateRound(Handle, roundEndReason, delay);
VirtualFunctions.TerminateRound(Handle, roundEndReason, delay, 0, 0);
}
}

View File

@@ -75,6 +75,7 @@ public partial class CCSPlayerController
Userid = this
};
@event.FireEventToClient(this);
@event.Free();
}
/// <summary>

View File

@@ -1,7 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Core.Hosting;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -14,8 +18,11 @@ public class PluginManager : IPluginManager
private readonly ICommandManager _commandManager;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<PluginManager> _logger;
private readonly Dictionary<string, Assembly> _sharedAssemblies = new();
private bool _loadedSharedLibs = false;
public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ICommandManager commandManager, ILogger<PluginManager> logger, IServiceProvider serviceProvider, IServiceScopeFactory serviceScopeFactory)
public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ICommandManager commandManager,
ILogger<PluginManager> logger, IServiceProvider serviceProvider, IServiceScopeFactory serviceScopeFactory)
{
_scriptHostConfiguration = scriptHostConfiguration;
_commandManager = commandManager;
@@ -23,6 +30,36 @@ public class PluginManager : IPluginManager
_serviceProvider = serviceProvider;
}
private void LoadLibrary(string path)
{
var loader = PluginLoader.CreateFromAssemblyFile(path, new[] { typeof(IPlugin), typeof(PluginCapability<>), typeof(PlayerCapability<>) },
config => { config.PreferSharedTypes = true; });
var assembly = loader.LoadDefaultAssembly();
_sharedAssemblies[assembly.GetName().FullName] = assembly;
}
private void LoadSharedLibraries()
{
var sharedDirectory = Directory.GetDirectories(_scriptHostConfiguration.SharedPath);
var sharedAssemblyPaths = sharedDirectory
.Select(dir => Path.Combine(dir, Path.GetFileName(dir) + ".dll"))
.Where(File.Exists)
.ToArray();
foreach (var sharedAssemblyPath in sharedAssemblyPaths)
{
try
{
LoadLibrary(sharedAssemblyPath);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load shared assembly from {Path}", sharedAssemblyPath);
}
}
}
public void Load()
{
var pluginDirectories = Directory.GetDirectories(_scriptHostConfiguration.PluginPath);
@@ -31,15 +68,46 @@ public class PluginManager : IPluginManager
.Where(File.Exists)
.ToArray();
foreach (var path in pluginAssemblyPaths)
AssemblyLoadContext.Default.Resolving += (context, name) =>
{
if (!_loadedSharedLibs)
{
LoadSharedLibraries();
_loadedSharedLibs = true;
}
if (!_sharedAssemblies.TryGetValue(name.FullName, out var assembly))
{
return null;
}
return assembly;
};
if (CoreConfig.PluginAutoLoadEnabled)
{
foreach (var path in pluginAssemblyPaths)
{
try
{
LoadPlugin(path);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load plugin from {Path}", path);
}
}
}
foreach (var plugin in _loadedPluginContexts)
{
try
{
LoadPlugin(path);
plugin.Plugin?.OnAllPluginsLoaded(false);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load plugin from {Path}", path);
_logger.LogError(e, "OnAllPluginsLoaded failed");
}
}
}
@@ -51,7 +119,8 @@ public class PluginManager : IPluginManager
public void LoadPlugin(string path)
{
var plugin = new PluginContext(_serviceProvider, _commandManager, _scriptHostConfiguration, path, _loadedPluginContexts.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1);
var plugin = new PluginContext(_serviceProvider, _commandManager, _scriptHostConfiguration, path,
_loadedPluginContexts.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1);
_loadedPluginContexts.Add(plugin);
plugin.Load();
}

View File

@@ -18,6 +18,7 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
@@ -64,7 +65,7 @@ namespace CounterStrikeSharp.API.Core.Plugin
_hostConfiguration = hostConfiguration;
_path = path;
PluginId = id;
Loader = PluginLoader.CreateFromAssemblyFile(path,
new[]
{
@@ -76,7 +77,7 @@ namespace CounterStrikeSharp.API.Core.Plugin
config.IsUnloadable = true;
config.PreferSharedTypes = true;
});
if (CoreConfig.PluginHotReloadEnabled)
{
_fileWatcher = new FileSystemWatcher
@@ -110,6 +111,7 @@ namespace CounterStrikeSharp.API.Core.Plugin
Loader = eventargs.Loader;
Unload(hotReload: true);
Load(hotReload: true);
Plugin.OnAllPluginsLoaded(hotReload: true);
});
return Task.CompletedTask;
@@ -118,12 +120,12 @@ namespace CounterStrikeSharp.API.Core.Plugin
public void Load(bool hotReload = false)
{
if (State == PluginState.Loaded) return;
using (Loader.EnterContextualReflection())
{
var defaultAssembly = Loader.LoadDefaultAssembly();
Type pluginType = defaultAssembly.GetTypes()
Type pluginType = defaultAssembly.GetExportedTypes()
.FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t));
if (pluginType == null) throw new Exception("Unable to find plugin in assembly");

View File

@@ -16,6 +16,7 @@
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<!-- <GenerateCompatibilitySuppressionFile>true</GenerateCompatibilitySuppressionFile> -->
</PropertyGroup>
<PropertyGroup>
<ApiCompatValidateAssemblies>true</ApiCompatValidateAssemblies>

View File

@@ -1,5 +1,7 @@
// Global using directives
global using System;
global using System.Linq;
global using System.IO;
global using System.Collections.Generic;
global using CounterStrikeSharp.API.Core;

View File

@@ -132,7 +132,6 @@ namespace CounterStrikeSharp.API.Modules.Admin
foreach (var adminDef in adminsFromFile.Values)
{
adminDef.InitalizeFlags();
Console.WriteLine($"Domains: {adminDef.Flags.Count}");
if (SteamID.TryParse(adminDef.Identity, out var steamId))
{

View File

@@ -32,12 +32,16 @@ namespace CounterStrikeSharp.API.Modules.Events
public class GameEvent : NativeObject
{
// Used to track freeable state for manually created events.
private bool _freeable = false;
public GameEvent(IntPtr pointer) : base(pointer)
{
}
public GameEvent(string name, bool force) : this(NativeAPI.CreateEvent(name, force))
{
_freeable = true;
}
public string EventName => NativeAPI.GetEventName(Handle);
@@ -121,8 +125,28 @@ namespace CounterStrikeSharp.API.Modules.Events
protected void SetEntityIndex(string name, int value) => NativeAPI.SetEventEntityIndex(Handle, name, value);
public void FireEvent(bool dontBroadcast) => NativeAPI.FireEvent(Handle, dontBroadcast);
public void FireEvent(bool dontBroadcast)
{
NativeAPI.FireEvent(Handle, dontBroadcast);
_freeable = false;
}
public void FireEventToClient(CCSPlayerController player) => NativeAPI.FireEventToClient(Handle, (int)player.Index);
/// <summary>
/// Used to manually free the event.
/// <remarks>If <see cref="FireEvent"/> is called, Free will be called automatically.</remarks>
/// </summary>
public void Free()
{
if (!_freeable)
{
throw new InvalidOperationException("Event is not able to be freed.");
}
NativeAPI.FreeEvent(Handle);
_freeable = false;
}
}
}

View File

@@ -14,7 +14,7 @@ public class DynamicHook : NativeObject
return NativeAPI.DynamicHookGetParam<T>(Handle, (int)typeof(T).ToValidDataType(), index);
}
public T GetReturn<T>(int index)
public T GetReturn<T>()
{
return NativeAPI.DynamicHookGetReturn<T>(Handle, (int)typeof(T).ToValidDataType());
}

View File

@@ -46,10 +46,10 @@ public static class VirtualFunctions
public static Action<IntPtr, string> SetModel = SetModelFunc.Invoke;
public static MemoryFunctionVoid<nint, RoundEndReason, float> TerminateRoundFunc =
public static MemoryFunctionVoid<nint, RoundEndReason, float, nint, byte> TerminateRoundFunc =
new(GameData.GetSignature("CCSGameRules_TerminateRound"));
public static Action<IntPtr, RoundEndReason, float> TerminateRound = TerminateRoundFunc.Invoke;
public static Action<IntPtr, RoundEndReason, float, nint, byte> TerminateRound = TerminateRoundFunc.Invoke;
public static MemoryFunctionWithReturn<string, int, IntPtr> UTIL_CreateEntityByNameFunc =
new(GameData.GetSignature("UTIL_CreateEntityByName"));

View File

@@ -0,0 +1,20 @@
using CounterStrikeSharp.API.Modules.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CounterStrikeSharp.API.Modules.Utils
{
public class ResourceManifest : NativeObject
{
private Action<nint, string> _AddResource;
public ResourceManifest(nint pointer) : base(pointer)
{
_AddResource = VirtualFunction.CreateVoid<nint, string>(Handle, GameData.GetOffset("CEntityResourceManifest_AddResource"));
}
public void AddResource(string resourcePath) => _AddResource(Handle, resourcePath);
}
}

View File

@@ -19,6 +19,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
@@ -41,15 +42,35 @@ namespace CounterStrikeSharp.API
public static double EngineTime => NativeAPI.GetEngineTime();
public static void PrecacheModel(string name) => NativeAPI.PrecacheModel(name);
/// <summary>
/// <inheritdoc cref="NextFrame"/>
/// Returns Task that completes once the synchronous task has been completed.
/// </summary>
public static Task NextFrameAsync(Action task)
{
var functionReference = FunctionReference.Create(task, FunctionLifetime.SingleUse);
NativeAPI.QueueTaskForNextFrame(functionReference);
return functionReference.CompletionTask;
}
/// <summary>
/// Queue a task to be executed on the next game frame.
/// <remarks>Does not execute if the server is hibernating.</remarks>
/// </summary>
public static void NextFrame(Action task)
{
var functionReference = FunctionReference.Create(task);
functionReference.Lifetime = FunctionLifetime.SingleUse;
NativeAPI.QueueTaskForNextFrame(functionReference);
NextFrameAsync(task);
}
/// <summary>
/// <inheritdoc cref="NextWorldUpdate"/>
/// Returns Task that completes once the synchronous task has been completed.
/// </summary>
public static Task NextWorldUpdateAsync(Action task)
{
var functionReference = FunctionReference.Create(task, FunctionLifetime.SingleUse);
NativeAPI.QueueTaskForNextWorldUpdate(functionReference);
return functionReference.CompletionTask;
}
/// <summary>
@@ -59,9 +80,7 @@ namespace CounterStrikeSharp.API
/// <param name="task"></param>
public static void NextWorldUpdate(Action task)
{
var functionReference = FunctionReference.Create(task);
functionReference.Lifetime = FunctionLifetime.SingleUse;
NativeAPI.QueueTaskForNextWorldUpdate(functionReference);
NextWorldUpdateAsync(task);
}
public static void PrintToChatAll(string message)

View File

@@ -34,6 +34,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithVoiceOverrides", "..\ex
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithFakeConvars", "..\examples\WithFakeConvars\WithFakeConvars.csproj", "{1309954E-FAF7-47A5-9FF9-C7263B33E4E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithSharedTypes", "..\examples\WithSharedTypes\WithSharedTypes.csproj", "{4E5289B5-E81D-421C-B340-B98B6FFE09D1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySharedTypes.Contracts", "..\examples\MySharedTypes.Contracts\MySharedTypes.Contracts\MySharedTypes.Contracts.csproj", "{A37676EA-CF2F-424D-85A1-C359D07A679D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithSharedTypesConsumer", "..\examples\WithSharedTypesConsumer\WithSharedTypesConsumer.csproj", "{76AD7BB0-A096-4336-83E2-B32CAE4E9933}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -100,10 +106,22 @@ Global
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Release|Any CPU.Build.0 = Release|Any CPU
{4E5289B5-E81D-421C-B340-B98B6FFE09D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E5289B5-E81D-421C-B340-B98B6FFE09D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E5289B5-E81D-421C-B340-B98B6FFE09D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E5289B5-E81D-421C-B340-B98B6FFE09D1}.Release|Any CPU.Build.0 = Release|Any CPU
{1309954E-FAF7-47A5-9FF9-C7263B33E4E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1309954E-FAF7-47A5-9FF9-C7263B33E4E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1309954E-FAF7-47A5-9FF9-C7263B33E4E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1309954E-FAF7-47A5-9FF9-C7263B33E4E3}.Release|Any CPU.Build.0 = Release|Any CPU
{A37676EA-CF2F-424D-85A1-C359D07A679D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A37676EA-CF2F-424D-85A1-C359D07A679D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A37676EA-CF2F-424D-85A1-C359D07A679D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A37676EA-CF2F-424D-85A1-C359D07A679D}.Release|Any CPU.Build.0 = Release|Any CPU
{76AD7BB0-A096-4336-83E2-B32CAE4E9933}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76AD7BB0-A096-4336-83E2-B32CAE4E9933}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76AD7BB0-A096-4336-83E2-B32CAE4E9933}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76AD7BB0-A096-4336-83E2-B32CAE4E9933}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{57E64289-5D69-4AA1-BEF0-D0D96A55EE8F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
@@ -117,6 +135,9 @@ Global
{31EABE0B-871F-497B-BF36-37FFC6FAD15F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{BB44E08E-CCA8-4E22-A132-11B2F69D1890} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{6FA3107D-42AF-42A0-BF51-2230D13268B5} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{4E5289B5-E81D-421C-B340-B98B6FFE09D1} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{1309954E-FAF7-47A5-9FF9-C7263B33E4E3} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{A37676EA-CF2F-424D-85A1-C359D07A679D} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{76AD7BB0-A096-4336-83E2-B32CAE4E9933} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
EndGlobalSection
EndGlobal

View File

@@ -35,6 +35,7 @@ using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using static CounterStrikeSharp.API.Core.Listeners;
namespace TestPlugin
{
@@ -166,6 +167,19 @@ namespace TestPlugin
return HookResult.Continue;
}), HookMode.Pre);
// Precache resources
RegisterListener<Listeners.OnServerPrecacheResources>((manifest) =>
{
manifest.AddResource("path/to/model");
manifest.AddResource("path/to/material");
manifest.AddResource("path/to/particle");
});
}
public override void OnAllPluginsLoaded(bool hotReload)
{
Logger.LogInformation("All plugins loaded!");
}
private void SetupConvars()

View File

@@ -51,6 +51,7 @@ bool CCoreConfig::Init(char* conf_error, int conf_error_size)
SilentChatTrigger = m_json.value("SilentChatTrigger", SilentChatTrigger);
FollowCS2ServerGuidelines = m_json.value("FollowCS2ServerGuidelines", FollowCS2ServerGuidelines);
PluginHotReloadEnabled = m_json.value("PluginHotReloadEnabled", PluginHotReloadEnabled);
PluginAutoLoadEnabled = m_json.value("PluginAutoLoadEnabled", PluginAutoLoadEnabled);
ServerLanguage = m_json.value("ServerLanguage", ServerLanguage);
} catch (const std::exception& ex) {
V_snprintf(conf_error, conf_error_size, "Failed to parse CoreConfig file: %s", ex.what());

View File

@@ -29,6 +29,7 @@ class CCoreConfig
std::vector<std::string> SilentChatTrigger = { std::string("/") };
bool FollowCS2ServerGuidelines = true;
bool PluginHotReloadEnabled = true;
bool PluginAutoLoadEnabled = true;
std::string ServerLanguage = "en";
using json = nlohmann::json;

66
src/core/game_system.cpp Normal file
View File

@@ -0,0 +1,66 @@
/**
* =============================================================================
* CS2Fixes
* Copyright (C) 2023-2024 Source2ZE
* =============================================================================
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 3.0, as published by the
* Free Software Foundation.
*
* This program 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
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "core/log.h"
#include "core/globals.h"
#include "core/gameconfig.h"
#include "core/game_system.h"
#include "core/managers/server_manager.h"
CBaseGameSystemFactory** CBaseGameSystemFactory::sm_pFirst = nullptr;
CGameSystem g_GameSystem;
IGameSystemFactory* CGameSystem::sm_Factory = nullptr;
// This mess is needed to get the pointer to sm_pFirst so we can insert game systems
bool InitGameSystems()
{
// This signature directly points to the instruction referencing sm_pFirst, and the opcode is 3
// bytes so we skip those
uint8* ptr = (uint8*)counterstrikesharp::globals::gameConfig->ResolveSignature("IGameSystem_InitAllSystems_pFirst") + 3;
if (!ptr) {
CSSHARP_CORE_ERROR("Failed to InitGameSystems, see warnings above.");
return false;
}
// Grab the offset as 4 bytes
uint32 offset = *(uint32*)ptr;
// Go to the next instruction, which is the starting point of the relative jump
ptr += 4;
// Now grab our pointer
CBaseGameSystemFactory::sm_pFirst = (CBaseGameSystemFactory**)(ptr + offset);
// And insert the game system(s)
CGameSystem::sm_Factory = new CGameSystemStaticFactory<CGameSystem>("CSSharp_GameSystem", &g_GameSystem);
return true;
}
GS_EVENT_MEMBER(CGameSystem, BuildGameSessionManifest)
{
IEntityResourceManifest* pResourceManifest = msg->m_pResourceManifest;
CSSHARP_CORE_INFO("CGameSystem::BuildGameSessionManifest");
counterstrikesharp::globals::serverManager.OnPrecacheResources(pResourceManifest);
}

47
src/core/game_system.h Normal file
View File

@@ -0,0 +1,47 @@
/**
* =============================================================================
* CS2Fixes
* Copyright (C) 2023-2024 Source2ZE
* =============================================================================
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 3.0, as published by the
* Free Software Foundation.
*
* This program 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
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "core/log.h"
#include "entitysystem.h"
#include "igamesystemfactory.h"
bool InitGameSystems();
class CGameSystem : public CBaseGameSystem
{
public:
GS_EVENT(BuildGameSessionManifest);
void Shutdown() override
{
CSSHARP_CORE_INFO("CGameSystem::Shutdown");
delete sm_Factory;
}
void SetGameSystemGlobalPtrs(void* pValue) override
{
if (sm_Factory)
sm_Factory->SetGlobalPtr(pValue);
}
bool DoesGameSystemReallocate() override { return sm_Factory->ShouldAutoAdd(); }
static IGameSystemFactory* sm_Factory;
};

View File

@@ -19,6 +19,9 @@
#include "core/log.h"
#include "scripting/callback_manager.h"
#include "core/game_system.h"
#include <concurrentqueue.h>
SH_DECL_HOOK1_void(ISource2Server, ServerHibernationUpdate, SH_NOATTRIB, 0, bool);
SH_DECL_HOOK0_void(ISource2Server, GameServerSteamAPIActivated, SH_NOATTRIB, 0);
SH_DECL_HOOK0_void(ISource2Server, GameServerSteamAPIDeactivated, SH_NOATTRIB, 0);
@@ -56,6 +59,8 @@ void ServerManager::OnAllInitialized() {
on_server_pre_fatal_shutdown = globals::callbackManager.CreateCallback("OnPreFatalShutdown");
on_server_update_when_not_in_game = globals::callbackManager.CreateCallback("OnUpdateWhenNotInGame");
on_server_pre_world_update = globals::callbackManager.CreateCallback("OnServerPreWorldUpdate");
on_server_precache_resources = globals::callbackManager.CreateCallback("OnServerPrecacheResources");
}
void ServerManager::OnShutdown() {
@@ -81,6 +86,8 @@ void ServerManager::OnShutdown() {
globals::callbackManager.ReleaseCallback(on_server_pre_fatal_shutdown);
globals::callbackManager.ReleaseCallback(on_server_update_when_not_in_game);
globals::callbackManager.ReleaseCallback(on_server_pre_world_update);
globals::callbackManager.ReleaseCallback(on_server_precache_resources);
}
void* ServerManager::GetEconItemSystem()
@@ -170,17 +177,17 @@ void ServerManager::UpdateWhenNotInGame(float flFrameTime)
void ServerManager::PreWorldUpdate(bool bSimulating)
{
std::lock_guard<std::mutex> lock(m_nextWorldUpdateTasksLock);
std::vector<std::function<void()>> out_list(1024);
if (!m_nextWorldUpdateTasks.empty()) {
CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} at time {1}", m_nextWorldUpdateTasks.size(),
globals::getGlobalVars()->curtime);
auto size = m_nextWorldUpdateTasks.try_dequeue_bulk(out_list.begin(), 1024);
for (size_t i = 0; i < m_nextWorldUpdateTasks.size(); i++) {
m_nextWorldUpdateTasks[i]();
if (size > 0) {
CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} at time {1}", size,
globals::getGlobalVars()->curtime);
for (size_t i = 0; i < size; i++) {
out_list[i]();
}
m_nextWorldUpdateTasks.clear();
}
auto callback = globals::serverManager.on_server_pre_world_update;
@@ -194,7 +201,18 @@ void ServerManager::PreWorldUpdate(bool bSimulating)
void ServerManager::AddTaskForNextWorldUpdate(std::function<void()>&& task)
{
std::lock_guard<std::mutex> lock(m_nextWorldUpdateTasksLock);
m_nextWorldUpdateTasks.push_back(std::forward<decltype(task)>(task));
m_nextWorldUpdateTasks.enqueue(std::forward<decltype(task)>(task));
}
void ServerManager::OnPrecacheResources(IEntityResourceManifest* pResourceManifest)
{
CSSHARP_CORE_TRACE("Precache resources");
auto callback = globals::serverManager.on_server_precache_resources;
if (callback && callback->GetFunctionCount()) {
callback->ScriptContext().Reset();
callback->ScriptContext().Push(pResourceManifest);
callback->Execute();
}
}
} // namespace counterstrikesharp

View File

@@ -19,6 +19,9 @@
#include "core/globals.h"
#include "core/global_listener.h"
#include "scripting/script_engine.h"
#include <concurrentqueue.h>
#include "core/game_system.h"
namespace counterstrikesharp {
class ScriptCallback;
@@ -31,7 +34,8 @@ public:
void OnShutdown() override;
void* GetEconItemSystem();
bool IsPaused();
void AddTaskForNextWorldUpdate(std::function<void()> &&task);
void AddTaskForNextWorldUpdate(std::function<void()>&& task);
void OnPrecacheResources(IEntityResourceManifest* pResourceManifest);
private:
void ServerHibernationUpdate(bool bHibernating);
@@ -51,8 +55,9 @@ private:
ScriptCallback *on_server_update_when_not_in_game;
ScriptCallback *on_server_pre_world_update;
std::vector<std::function<void()>> m_nextWorldUpdateTasks;
std::mutex m_nextWorldUpdateTasksLock;
ScriptCallback *on_server_precache_resources;
moodycamel::ConcurrentQueue<std::function<void()>> m_nextWorldUpdateTasks;
};
} // namespace counterstrikesharp

View File

@@ -20,6 +20,7 @@
#include "core/log.h"
#include "core/coreconfig.h"
#include "core/gameconfig.h"
#include "core/game_system.h"
#include "core/timer_system.h"
#include "core/utils.h"
#include "core/managers/entity_manager.h"
@@ -144,6 +145,12 @@ bool CounterStrikeSharpMMPlugin::Load(PluginId id, ISmmAPI* ismm, char* error, s
CSSHARP_CORE_ERROR("Failed to initialize .NET runtime");
}
if (!InitGameSystems()) {
CSSHARP_CORE_ERROR("Failed to initialize GameSystem!");
return false;
}
CSSHARP_CORE_INFO("Initialized GameSystem.");
CSSHARP_CORE_INFO("Hooks added.");
// Used by Metamod Console Commands
@@ -186,9 +193,7 @@ void CounterStrikeSharpMMPlugin::AllPluginsLoaded()
void CounterStrikeSharpMMPlugin::AddTaskForNextFrame(std::function<void()>&& task)
{
std::lock_guard<std::mutex> lock(m_nextTasksLock);
m_nextTasks.push_back(std::forward<decltype(task)>(task));
m_nextTasks.try_enqueue(std::forward<decltype(task)>(task));
}
void CounterStrikeSharpMMPlugin::Hook_GameFrame(bool simulating, bool bFirstTick, bool bLastTick)
@@ -201,19 +206,18 @@ void CounterStrikeSharpMMPlugin::Hook_GameFrame(bool simulating, bool bFirstTick
*/
globals::timerSystem.OnGameFrame(simulating);
std::lock_guard<std::mutex> lock(m_nextTasksLock);
std::vector<std::function<void()>> out_list(1024);
if (m_nextTasks.empty())
return;
auto size = m_nextTasks.try_dequeue_bulk(out_list.begin(), 1024);
CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} on tick number {1}", m_nextTasks.size(),
globals::getGlobalVars()->tickcount);
if (size > 0) {
CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} on tick number {1}", size,
globals::getGlobalVars()->tickcount);
for (size_t i = 0; i < m_nextTasks.size(); i++) {
m_nextTasks[i]();
for (size_t i = 0; i < size; i++) {
out_list[i]();
}
}
m_nextTasks.clear();
}
// Potentially might not work

View File

@@ -23,6 +23,7 @@
#include <sh_vector.h>
#include <vector>
#include "entitysystem.h"
#include "concurrentqueue.h"
namespace counterstrikesharp {
class ScriptCallback;
@@ -63,8 +64,7 @@ public:
const char *GetLogTag() override;
private:
std::vector<std::function<void()>> m_nextTasks;
std::mutex m_nextTasksLock;
moodycamel::ConcurrentQueue<std::function<void()>> m_nextTasks;
};
static ScriptCallback *on_activate_callback;

View File

@@ -4,7 +4,7 @@ rm -rf temp/ generated/
mkdir -p temp generated
mkdir -p generated
cp ../../libraries/GameTracking-CS2/Protobufs/*.proto temp/
cp ../../libraries/Protobufs/csgo/*.proto temp/
cp -r google/ temp/
for file in temp/*.proto; do

View File

@@ -63,7 +63,7 @@ static void FireEvent(ScriptContext &script_context) {
}
static void FireEventToClient(ScriptContext& script_context) {
static void FireEventToClient(ScriptContext& script_context) {
auto game_event = script_context.GetArgument<IGameEvent*>(0);
int entityIndex = script_context.GetArgument<int>(1);
if (!game_event) {
@@ -76,7 +76,17 @@ static void FireEvent(ScriptContext &script_context) {
}
pListener->FireGameEvent(game_event);
}
}
static void FreeEvent(ScriptContext& script_context) {
auto game_event = script_context.GetArgument<IGameEvent*>(0);
if (!game_event) {
script_context.ThrowNativeError("Invalid game event");
}
globals::gameEventManager->FreeEvent(game_event);
managed_game_events.erase(std::remove(managed_game_events.begin(), managed_game_events.end(), game_event), managed_game_events.end());
}
static const char *GetEventName(ScriptContext &script_context) {
IGameEvent *game_event = script_context.GetArgument<IGameEvent *>(0);
@@ -263,6 +273,7 @@ REGISTER_NATIVES(events, {
ScriptEngine::RegisterNativeHandler("HOOK_EVENT", HookEvent);
ScriptEngine::RegisterNativeHandler("UNHOOK_EVENT", UnhookEvent);
ScriptEngine::RegisterNativeHandler("CREATE_EVENT", CreateEvent);
ScriptEngine::RegisterNativeHandler("FREE_EVENT", FreeEvent);
ScriptEngine::RegisterNativeHandler("FIRE_EVENT", FireEvent);
ScriptEngine::RegisterNativeHandler("FIRE_EVENT_TO_CLIENT", FireEventToClient);

View File

@@ -1,6 +1,7 @@
HOOK_EVENT: name:string, callback:func, isPost:bool -> void
UNHOOK_EVENT: name:string, callback:func, isPost:bool -> void
CREATE_EVENT: name:string, force:bool -> pointer
FREE_EVENT: gameEvent:pointer -> void
FIRE_EVENT: gameEvent:pointer, dontBroadcast:bool -> void
FIRE_EVENT_TO_CLIENT: gameEvent:pointer, clientIndex:int -> void
GET_EVENT_NAME: gameEvent:pointer -> string

View File

@@ -23,7 +23,7 @@ namespace CodeGen.Natives
static void Main(string[] args)
{
Generators.GenerateNatives();
Generators.GenerateGameEvents();
Generators.GenerateGameEvents().GetAwaiter().GetResult();
}
}
}

View File

@@ -30,16 +30,25 @@ public partial class Generators
public string? Comment { get; set; }
}
private static List<GameEvent> GetGameEvents()
private static HttpClient _httpClient = new HttpClient();
private static string BaseUrl = "https://raw.githubusercontent.com/SteamDatabase/GameTracking-CS2/master/";
private static List<string> GameEventFiles = new List<string>()
{
"game/core/pak01_dir/resource/core.gameevents",
"game/csgo/pak01_dir/resource/game.gameevents",
"game/csgo/pak01_dir/resource/mod.gameevents"
};
private static async Task<List<GameEvent>> GetGameEvents()
{
// temporary, not committing resource files directly to git for now
var pathToSearch = @"/home/michael/Steam/cs2-ds/game/csgo/events/resource";
if (!Directory.Exists(pathToSearch)) Environment.Exit(0);
var allGameEvents = new Dictionary<string, GameEvent>();
foreach (string file in Directory.EnumerateFiles(pathToSearch, "*.gameevents", SearchOption.AllDirectories).OrderBy(Path.GetFileName))
foreach (string url in GameEventFiles)
// foreach (string file in Directory.EnumerateFiles(pathToSearch, "*.gameevents", SearchOption.AllDirectories).OrderBy(Path.GetFileName))
{
var deserialized = VdfConvert.Deserialize(File.ReadAllText(file));
var file = await _httpClient.GetStringAsync($"{BaseUrl}/{url}");
var deserialized = VdfConvert.Deserialize(file);
var properties =
deserialized.Value.Where(x => x.Type == VTokenType.Property);
@@ -79,12 +88,9 @@ public partial class Generators
return allGameEvents.Values.ToList();
}
public static void GenerateGameEvents()
public static async Task GenerateGameEvents()
{
var pathToSearch = @"/home/michael/Steam/cs2-ds/game/csgo/events/resource";
if (!Directory.Exists(pathToSearch)) return;
var allGameEvents = GetGameEvents();
var allGameEvents = await GetGameEvents();
var gameEventsString = string.Join("\n", allGameEvents.OrderBy(x => x.NamePascalCase).Select(gameEvent =>
{