Compare commits

...

17 Commits

Author SHA1 Message Date
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
Roflmuffin
983d91491d fix: update gamedata 2024-02-29 12:01:01 +10:00
BuSheeZy
71507b1e25 fix: allow using an empty flag array in overrides (#351)
Co-authored-by: Ryan Bucshon <busheezy@users.noreply.github.com>
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2024-02-29 01:35:21 +00:00
B3none
cfe14b35fe [no ci] Bump the min api version in the fake convars example plugin (#350) 2024-02-28 15:08:23 +10:00
Poggu
5a6cdf0da3 feat: improve entity validation (#348) 2024-02-27 16:12:56 +10:00
Michael Wilson
a5399dd5fe feat: add FakeConVar class (#325) 2024-02-26 16:55:19 +10:00
Roflmuffin
ab996c34e9 fix: use function reference for next frame tasks
fixes #178
2024-02-26 09:20:56 +10:00
B3none
6e2e25b96e Re-implemented css_lang command (#343) 2024-02-26 08:56:16 +10:00
Roflmuffin
1142c9f063 [no ci] remove untriaged label when milestoning issues 2024-02-24 14:58:01 +10:00
Michael Wilson
72178c9598 feat: add chat color method for player controller (#339) 2024-02-24 14:56:56 +10:00
80 changed files with 1441 additions and 96 deletions

View File

@@ -0,0 +1,20 @@
name: Label new issues
on:
issues:
types:
- milestoned
jobs:
label_issues:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Remove label
run: gh issue edit "$NUMBER" --remove-label "$LABELS"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABELS: untriaged

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

@@ -28,8 +28,8 @@
},
"CCSPlayerController_Respawn": {
"offsets": {
"windows": 245,
"linux": 247
"windows": 244,
"linux": 246
}
},
"CBasePlayerController_SetPawn": {
@@ -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

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

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

@@ -3,6 +3,8 @@ items:
href: HelloWorld.md
- name: Commands
href: WithCommands.md
- name: Fake ConVars
href: WithFakeConvars.md
- name: Config
href: WithConfig.md
- name: Dependency Injection
@@ -13,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,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,9 @@
using CounterStrikeSharp.API.Modules.Cvars;
namespace WithFakeConvars;
public static class ConVars
{
// This convar is registered from the plugin instance but can be used anywhere.
public static FakeConVar<int> ExampleStaticCvar = new("example_static", "An example static cvar");
}

View File

@@ -0,0 +1,21 @@
using CounterStrikeSharp.API.Modules.Cvars.Validators;
namespace WithFakeConvars;
// This is an example of a custom validator that checks if a number is even.
public class EvenNumberValidator : IValidator<int>
{
public bool Validate(int value, out string? errorMessage)
{
if (value % 2 == 0)
{
errorMessage = null;
return true;
}
else
{
errorMessage = "Value must be an even number";
return false;
}
}
}

View File

@@ -0,0 +1,2 @@
# With Fake Convars
This is an example that shows how to register "fake" convars, which are actually console commands that track their internal state.

View File

@@ -0,0 +1,12 @@
<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" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,58 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
namespace WithFakeConvars;
[MinimumApiVersion(175)]
public class WithFakeConvarsPlugin : BasePlugin
{
public override string ModuleName => "Example: With Fake Convars";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that registers some console variables";
// FakeConVar is a class that can be used to create custom console variables.
// You can specify a name, description, default value, and custom validators.
public FakeConVar<bool> BoolCvar = new("example_bool", "An example boolean cvar", true);
// Range validator is an inbuilt validator that can be used to ensure that a value is within a certain range.
public FakeConVar<int> ExampleIntCvar = new("example_int", "An example integer cvar", 10, flags: ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 100));
public FakeConVar<float> ExampleFloatCvar = new("example_float", "An example float cvar", 10, flags: ConVarFlags.FCVAR_NONE, new RangeValidator<float>(5, 20));
public FakeConVar<string> ExampleStringCvar = new("example_string", "An example string cvar", "default");
// Replicated, Cheat & Protected flags are supported.
public FakeConVar<float> ExamplePublicCvar = new("example_public_float", "An example public float cvar", 5,
ConVarFlags.FCVAR_REPLICATED);
// Can only be changed if sv_cheats is enabled.
public FakeConVar<float> ExampleCheatCvar = new("example_cheat_float", "An example cheat float cvar", 5,
ConVarFlags.FCVAR_CHEAT);
// Protected cvars do not output their value when queried.
public FakeConVar<float> ExampleProtectedCvar = new("example_protected_float", "An example cheat float cvar", 5,
ConVarFlags.FCVAR_PROTECTED);
// You can create your own custom validators by implementing the IValidator interface.
public FakeConVar<int> ExampleEvenNumberCvar = new("example_even_number", "An example even number cvar", 0, flags: ConVarFlags.FCVAR_NONE, new EvenNumberValidator());
public FakeConVar<int> RequiresRestartCvar = new("example_requires_restart", "A cvar that requires a restart when changed");
public override void Load(bool hotReload)
{
// You can subscribe to the ValueChanged event to execute code when the value of a cvar changes.
// In this example, we restart the game when the value of RequiresRestartCvar is greater than 5.
RequiresRestartCvar.ValueChanged += (sender, value) =>
{
if (value > 5)
{
Server.ExecuteCommand("mp_restartgame 1");
}
};
RegisterFakeConVars(typeof(ConVars));
}
}

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(143)]
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(142)]
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

View File

@@ -102,9 +102,18 @@ public class AdminTests
[Fact]
public void ShouldAddCommandPermissionOverridesAtRuntime()
{
Assert.False(AdminManager.CommandIsOverriden("runtime_command"));
AdminManager.AddPermissionOverride("runtime_command", "@runtime/override");
Assert.True(AdminManager.CommandIsOverriden("runtime_command"));
Assert.Equal("@runtime/override", AdminManager.GetPermissionOverrides("runtime_command").Single());
Assert.False(AdminManager.CommandIsOverriden("runtime_command_a"));
AdminManager.AddPermissionOverride("runtime_command_a", "@runtime/override");
Assert.True(AdminManager.CommandIsOverriden("runtime_command_a"));
Assert.Equal("@runtime/override", AdminManager.GetPermissionOverrides("runtime_command_a").Single());
}
[Fact]
public void ShouldAddCommandPermissionOverridesWithEmpty()
{
Assert.False(AdminManager.CommandIsOverriden("runtime_command_b"));
AdminManager.AddPermissionOverride("runtime_command_b");
Assert.True(AdminManager.CommandIsOverriden("runtime_command_b"));
Assert.False(AdminManager.GetPermissionOverrides("runtime_command_b").Any());
}
}

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.NativeAPI.QueueTaskForNextFrame(System.IntPtr)</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.QueueTaskForNextWorldUpdate(System.IntPtr)</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.Plugin.Host.PluginManager.#ctor(CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration,Microsoft.Extensions.Logging.ILogger{CounterStrikeSharp.API.Core.Plugin.Host.PluginManager},System.IServiceProvider)</Target>
@@ -439,6 +451,18 @@
<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>
@@ -1039,4 +1063,16 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP1002</DiagnosticId>
<Target>Microsoft.Extensions.Localization.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP1002</DiagnosticId>
<Target>Serilog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
</Suppressions>

View File

@@ -23,6 +23,7 @@ namespace CounterStrikeSharp.API
[Flags]
public enum ConVarFlags : Int64
{
FCVAR_NONE = 0,
FCVAR_LINKED_CONCOMMAND = (1 << 0),
FCVAR_DEVELOPMENTONLY =

View File

@@ -502,20 +502,20 @@ namespace CounterStrikeSharp.API.Core
}
}
public static void QueueTaskForNextFrame(IntPtr callback){
public static void QueueTaskForNextFrame(InputArgument callback){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(callback);
ScriptContext.GlobalScriptContext.Push((InputArgument)callback);
ScriptContext.GlobalScriptContext.SetIdentifier(0x9FE394D8);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static void QueueTaskForNextWorldUpdate(IntPtr callback){
public static void QueueTaskForNextWorldUpdate(InputArgument callback){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(callback);
ScriptContext.GlobalScriptContext.Push((InputArgument)callback);
ScriptContext.GlobalScriptContext.SetIdentifier(0xAD51A0C9);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();

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;
}
@@ -245,20 +249,17 @@ namespace CounterStrikeSharp.API.Core
break;
}
}
[CommandHelper(usage: "[language code, e.g. \"de\", \"pl\", \"en\"]",
whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnLangCommand(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
SteamID steamId = (SteamID)player.SteamID;
var steamId = (SteamID)player.SteamID;
if (command.ArgCount == 1)
{
var language = _playerLanguageManager.GetLanguage(steamId);
command.ReplyToCommand(string.Format("Current language is \"{0}\" ({1})", language.Name,
language.NativeName));
command.ReplyToCommand($"Current language is \"{language.Name}\" ({language.NativeName})");
return;
}
@@ -297,6 +298,11 @@ namespace CounterStrikeSharp.API.Core
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded.",
});
_commandManager.RegisterCommand(new("css_lang", "Set Counter-Strike Sharp language.", OnLangCommand)
{
ExecutableBy = CommandUsage.CLIENT_AND_SERVER,
UsageHint = "[language code, e.g. \"de\", \"pl\", \"en\"]",
});
}
}
}

View File

@@ -28,6 +28,7 @@ using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Config;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
@@ -72,6 +73,10 @@ namespace CounterStrikeSharp.API.Core
public virtual void Unload(bool hotReload)
{
}
public virtual void OnAllPluginsLoaded(bool hotReload)
{
}
public class CallbackSubscriber : IDisposable
{
@@ -308,6 +313,7 @@ namespace CounterStrikeSharp.API.Core
this.RegisterAttributeHandlers(instance);
this.RegisterConsoleCommandAttributeHandlers(instance);
this.RegisterEntityOutputAttributeHandlers(instance);
this.RegisterFakeConVars(instance);
}
public void InitializeConfig(object instance, Type pluginType)
@@ -410,6 +416,43 @@ namespace CounterStrikeSharp.API.Core
}
}
public void RegisterFakeConVars(Type type, object instance = null)
{
var convars = type
.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
.Where(prop => prop.FieldType.IsGenericType &&
prop.FieldType.GetGenericTypeDefinition() == typeof(FakeConVar<>));
foreach (var prop in convars)
{
object propValue = prop.GetValue(instance); // FakeConvar<?> instance
var propValueType = prop.FieldType.GenericTypeArguments[0];
var name = prop.FieldType.GetProperty("Name", BindingFlags.Public | BindingFlags.Instance)
.GetValue(propValue);
var description = prop.FieldType.GetProperty("Description", BindingFlags.Public | BindingFlags.Instance)
.GetValue(propValue);
MethodInfo executeCommandMethod = prop.FieldType
.GetMethod("ExecuteCommand", BindingFlags.Instance | BindingFlags.NonPublic);
this.AddCommand((string)name, (string) description, (caller, command) =>
{
executeCommandMethod.Invoke(propValue, new object[] {caller, command});
});
}
}
/// <summary>
/// Used to bind a fake ConVar to a plugin command. Only required for ConVars that are not public properties of the plugin class.
/// </summary>
/// <param name="convar"></param>
/// <typeparam name="T"></typeparam>
public void RegisterFakeConVars(object instance)
{
RegisterFakeConVars(instance.GetType(), instance);
}
public void HookEntityOutput(string classname, string outputName, EntityIO.EntityOutputHandler handler, HookMode mode = HookMode.Pre)
{
var subscriber = new CallbackSubscriber(handler, handler,

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

@@ -15,6 +15,7 @@
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
@@ -22,9 +23,22 @@ using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
{
/// <summary>
/// Describes the lifetime of a function reference.
/// </summary>
public enum FunctionLifetime
{
/// <summary>Delegate will be removed after the first invocation.</summary>
SingleUse,
/// <summary>Delegate will remain in memory for the lifetime of the application.</summary>
Permanent
}
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;
@@ -59,12 +73,13 @@ namespace CounterStrikeSharp.API.Core
if (typeof(NativeObject).IsAssignableFrom(param.ParameterType))
{
obj = Activator.CreateInstance(param.ParameterType,
new[] {scriptContext.GetArgument(typeof(IntPtr), i)});
new[] { scriptContext.GetArgument(typeof(IntPtr), i) });
}
else
{
obj = scriptContext.GetArgument(param.ParameterType, i);
}
return obj;
}).ToArray();
@@ -79,6 +94,15 @@ namespace CounterStrikeSharp.API.Core
{
Application.Instance.Logger.LogError(e, "Error invoking callback");
}
finally
{
if (Lifetime == FunctionLifetime.SingleUse)
{
Remove(Identifier);
if (references.ContainsKey(m_method))
references.Remove(m_method, out _);
}
}
});
s_callback = dg;
}
@@ -89,24 +113,24 @@ namespace CounterStrikeSharp.API.Core
public static FunctionReference Create(Delegate method)
{
if (references.ContainsKey(method))
if (references.TryGetValue(method, out var existingReference))
{
return references[method];
return existingReference;
}
var reference = new FunctionReference(method);
var referenceId = Register(reference);
reference.Identifier = referenceId;
return reference;
}
private static Dictionary<int, FunctionReference> ms_references = new Dictionary<int, FunctionReference>();
private static ConcurrentDictionary<int, FunctionReference> ms_references = new ConcurrentDictionary<int, FunctionReference>();
private static int ms_referenceId;
private static Dictionary<Delegate, FunctionReference> references =
new Dictionary<Delegate, FunctionReference>();
private static ConcurrentDictionary<Delegate, FunctionReference> references =
new ConcurrentDictionary<Delegate, FunctionReference>();
private static int Register(FunctionReference reference)
{
@@ -139,7 +163,7 @@ namespace CounterStrikeSharp.API.Core
{
if (ms_references.TryGetValue(reference, out var funcRef))
{
ms_references.Remove(reference);
ms_references.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

@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using System;
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
@@ -6,14 +7,20 @@ namespace CounterStrikeSharp.API.Core;
public partial class CBaseEntity
{
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void Teleport(Vector position, QAngle angles, Vector velocity)
{
Guard.IsValidEntity(this);
VirtualFunction.CreateVoid<IntPtr, IntPtr, IntPtr, IntPtr>(Handle, GameData.GetOffset("CBaseEntity_Teleport"))(
Handle, position.Handle, angles.Handle, velocity.Handle);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void DispatchSpawn()
{
Guard.IsValidEntity(this);
VirtualFunctions.CBaseEntity_DispatchSpawn(Handle, IntPtr.Zero);
}
@@ -25,7 +32,13 @@ public partial class CBaseEntity
/// <summary>
/// Shorthand for accessing an entity's CBodyComponent?.SceneNode?.AbsRotation;
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public QAngle? AbsRotation => CBodyComponent?.SceneNode?.AbsRotation;
public T? GetVData<T>() where T : CEntitySubclassVDataBase => (T)Activator.CreateInstance(typeof(T), Marshal.ReadIntPtr(SubclassID.Handle + 4));
public T? GetVData<T>() where T : CEntitySubclassVDataBase
{
Guard.IsValidEntity(this);
return (T) Activator.CreateInstance(typeof(T), Marshal.ReadIntPtr(SubclassID.Handle + 4));
}
}

View File

@@ -20,8 +20,11 @@ namespace CounterStrikeSharp.API.Core;
public partial class CBaseModelEntity
{
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void SetModel(string model)
{
Guard.IsValidEntity(this);
VirtualFunctions.SetModel(Handle, model);
}
}

View File

@@ -1,5 +1,4 @@
using System;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
@@ -10,10 +9,13 @@ namespace CounterStrikeSharp.API.Core;
public partial class CBasePlayerController
{
public void SetPawn(CBasePlayerPawn? pawn)
{
if (pawn is null) return;
if (!pawn.IsValid) return;
VirtualFunctions.CBasePlayerController_SetPawnFunc.Invoke(this, pawn, true, false);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void SetPawn(CBasePlayerPawn? pawn)
{
Guard.IsValidEntity(this);
if (pawn is null) return;
if (!pawn.IsValid) return;
VirtualFunctions.CBasePlayerController_SetPawnFunc.Invoke(this, pawn, true, false);
}
}

View File

@@ -10,17 +10,24 @@ public partial class CBasePlayerPawn
/// </summary>
/// <param name="explode"></param>
/// <param name="force"></param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void CommitSuicide(bool explode, bool force)
{
Guard.IsValidEntity(this);
VirtualFunction.CreateVoid<IntPtr, bool, bool>(Handle, GameData.GetOffset("CBasePlayerPawn_CommitSuicide"))(Handle, explode, force);
}
/// <summary>
/// Remove Player Item
/// </summary>
/// <param name="weapon"></param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void RemovePlayerItem(CBasePlayerWeapon weapon)
{
Guard.IsValidEntity(this);
Guard.IsValidEntity(weapon);
VirtualFunctions.RemovePlayerItemVirtual(Handle, weapon.Handle);
}
}

View File

@@ -13,8 +13,11 @@ public partial class CCSPlayerController
public int? UserId => NativeAPI.GetUseridFromIndex((int)Index);
public CsTeam Team => (CsTeam)TeamNum;
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public IntPtr GiveNamedItem(string item)
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return 0;
if (PlayerPawn.Value == null) return 0;
if (!PlayerPawn.Value.IsValid) return 0;
@@ -34,25 +37,37 @@ public partial class CCSPlayerController
return GiveNamedItem(itemString);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void PrintToConsole(string message)
{
Guard.IsValidEntity(this);
NativeAPI.PrintToConsole((int)Index, $"{message}\n\0");
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void PrintToChat(string message)
{
Guard.IsValidEntity(this);
VirtualFunctions.ClientPrint(Handle, HudDestination.Chat, message, 0, 0, 0, 0);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void PrintToCenter(string message)
{
Guard.IsValidEntity(this);
VirtualFunctions.ClientPrint(Handle, HudDestination.Center, message, 0, 0, 0, 0);
}
public void PrintToCenterHtml(string message) => PrintToCenterHtml(message, 5);
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void PrintToCenterHtml(string message, int duration)
{
Guard.IsValidEntity(this);
var @event = new EventShowSurvivalRespawnStatus(true)
{
LocToken = message,
@@ -65,8 +80,10 @@ public partial class CCSPlayerController
/// <summary>
/// Drops the active player weapon on the ground.
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void DropActiveWeapon()
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return;
if (PlayerPawn.Value == null) return;
if (!PlayerPawn.Value.IsValid) return;
@@ -83,8 +100,10 @@ public partial class CCSPlayerController
/// <summary>
/// Removes every weapon from the player.
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void RemoveWeapons()
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return;
if (PlayerPawn.Value == null) return;
if (!PlayerPawn.Value.IsValid) return;
@@ -99,8 +118,10 @@ public partial class CCSPlayerController
/// </summary>
/// <param name="explode"></param>
/// <param name="force"></param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void CommitSuicide(bool explode, bool force)
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return;
if (PlayerPawn.Value == null) return;
if (!PlayerPawn.Value.IsValid) return;
@@ -111,8 +132,10 @@ public partial class CCSPlayerController
/// <summary>
/// Respawn player
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void Respawn()
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return;
if (PlayerPawn.Value == null) return;
if (!PlayerPawn.Value.IsValid) return;
@@ -128,8 +151,11 @@ public partial class CCSPlayerController
/// Forcibly switches the team of the player, the player will remain alive and keep their weapons.
/// </summary>
/// <param name="team">The team to switch to</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void SwitchTeam(CsTeam team)
{
Guard.IsValidEntity(this);
VirtualFunctions.SwitchTeam(Handle, (byte)team);
}
@@ -140,8 +166,11 @@ public partial class CCSPlayerController
/// </remarks>
/// </summary>
/// <param name="team">The team to change to</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void ChangeTeam(CsTeam team)
{
Guard.IsValidEntity(this);
VirtualFunction.CreateVoid<IntPtr, CsTeam>(Handle, GameData.GetOffset("CCSPlayerController_ChangeTeam"))(Handle,
team);
}
@@ -151,8 +180,11 @@ public partial class CCSPlayerController
/// </summary>
/// <param name="conVar">Name of the convar to retrieve</param>
/// <returns>ConVar string value</returns>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public string GetConVarValue(string conVar)
{
Guard.IsValidEntity(this);
return NativeAPI.GetClientConvarValue(Slot, conVar);
}
@@ -171,13 +203,12 @@ public partial class CCSPlayerController
/// </summary>
/// <param name="conVar">Console variable name</param>
/// <param name="value">String value to set</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
/// <exception cref="InvalidOperationException">Player is not a bot</exception>
public void SetFakeClientConVar(string conVar, string value)
{
if (!IsBot)
{
throw new InvalidOperationException("'SetFakeClientConVar' can only be called for fake clients (bots)");
}
Guard.IsValidEntity(this);
if (!IsBot) throw new InvalidOperationException("'SetFakeClientConVar' can only be called for fake clients (bots)");
NativeAPI.SetFakeClientConvarValue(Slot, conVar, value);
}
@@ -207,27 +238,45 @@ public partial class CCSPlayerController
/// Note: Only works for some commands, marked with the FCVAR_CLIENT_CAN_EXECUTE flag (not many).
/// </summary>
/// <param name="command"></param>
public void ExecuteClientCommand(string command) => NativeAPI.IssueClientCommand(Slot, command);
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void ExecuteClientCommand(string command)
{
Guard.IsValidEntity(this);
NativeAPI.IssueClientCommand(Slot, command);
}
/// <summary>
/// Issue the specified command directly from the server (mimics the server executing the command with the given player context).
/// <remarks>Works with server commands like `kill`, `explode`, `noclip`, etc. </remarks>
/// </summary>
/// <param name="command"></param>
public void ExecuteClientCommandFromServer(string command) => NativeAPI.IssueClientCommandFromServer(Slot, command);
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void ExecuteClientCommandFromServer(string command)
{
Guard.IsValidEntity(this);
NativeAPI.IssueClientCommandFromServer(Slot, command);
}
/// <summary>
/// Overrides who a player can hear in voice chat.
/// </summary>
/// <param name="sender">Player talking in the voice chat</param>
/// <param name="override">Whether the talker should be heard</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void SetListenOverride(CCSPlayerController sender, ListenOverride @override)
{
Guard.IsValidEntity(this);
NativeAPI.SetClientListening(Handle, sender.Handle, (Byte)@override);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public ListenOverride GetListenOverride(CCSPlayerController sender)
{
Guard.IsValidEntity(this);
return NativeAPI.GetClientListening(Handle, sender.Handle);
}
@@ -236,27 +285,31 @@ public partial class CCSPlayerController
/// <summary>
/// Returns the authorized SteamID of this user which has been validated with the SteamAPI.
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public SteamID? AuthorizedSteamID
{
get
{
if (!IsValid) return null;
Guard.IsValidEntity(this);
var authorizedSteamId = NativeAPI.GetPlayerAuthorizedSteamid(Slot);
if ((long)authorizedSteamId == -1) return null;
return (SteamID)authorizedSteamId;
}
}
/// <summary>
/// Returns the IP address (and possibly port) of this player.
/// <remarks>Returns 127.0.0.1 if the player is a bot.</remarks>
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public string? IpAddress
{
get
{
if (!IsValid) return null;
Guard.IsValidEntity(this);
var ipAddress = NativeAPI.GetPlayerIpAddress(Slot);
if (string.IsNullOrWhiteSpace(ipAddress)) return null;
@@ -267,9 +320,20 @@ public partial class CCSPlayerController
/// <summary>
/// Determines how the player interacts with voice chat.
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public VoiceFlags VoiceFlags
{
get => (VoiceFlags)NativeAPI.GetClientVoiceFlags(Handle);
set => NativeAPI.SetClientVoiceFlags(Handle, (Byte)value);
get
{
Guard.IsValidEntity(this);
return (VoiceFlags)NativeAPI.GetClientVoiceFlags(Handle);
}
set
{
Guard.IsValidEntity(this);
NativeAPI.SetClientVoiceFlags(Handle, (Byte)value);
}
}
}

View File

@@ -23,16 +23,26 @@ public partial class CCSPlayer_ItemServices
/// <summary>
/// Drops the active player weapon on the ground.
/// </summary>
/// <exception cref="InvalidOperationException">ItemServices points to null</exception>
public void DropActivePlayerWeapon(CBasePlayerWeapon activeWeapon)
{
if(Handle == IntPtr.Zero)
throw new InvalidOperationException("ItemServices points to null.");
Guard.IsValidEntity(activeWeapon);
VirtualFunction.CreateVoid<nint, nint>(Handle, GameData.GetOffset("CCSPlayer_ItemServices_DropActivePlayerWeapon"))(Handle, activeWeapon.Handle);
}
/// <summary>
/// Removes every weapon from the player.
/// </summary>
/// <exception cref="InvalidOperationException">ItemServices points to null</exception>
public void RemoveWeapons()
{
if (Handle == IntPtr.Zero)
throw new InvalidOperationException("ItemServices points to null.");
VirtualFunction.CreateVoid<nint>(Handle, GameData.GetOffset("CCSPlayer_ItemServices_RemoveWeapons"))(Handle);
}
}

View File

@@ -32,7 +32,12 @@ public partial class CEntityInstance : IEquatable<CEntityInstance>
public string DesignerName => IsValid ? Entity?.DesignerName : null;
public void Remove() => VirtualFunctions.UTIL_Remove(this.Handle);
public void Remove()
{
Guard.IsValidEntity(this);
VirtualFunctions.UTIL_Remove(this.Handle);
}
public bool Equals(CEntityInstance? other)
{
@@ -58,7 +63,7 @@ public partial class CEntityInstance : IEquatable<CEntityInstance>
{
return !Equals(left, right);
}
/// <summary>
/// Calls a named input method on an entity.
/// <example>
@@ -72,8 +77,11 @@ public partial class CEntityInstance : IEquatable<CEntityInstance>
/// <param name="caller">Entity that is sending the event, <see langword="null"/> for no entity</param>
/// <param name="value">String variant value to send with the event</param>
/// <param name="outputId">Unknown, defaults to 0</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void AcceptInput(string inputName, CEntityInstance? activator = null, CEntityInstance? caller = null, string value = "", int outputId = 0)
{
Guard.IsValidEntity(this);
VirtualFunctions.AcceptInput(Handle, inputName, activator?.Handle ?? IntPtr.Zero, caller?.Handle ?? IntPtr.Zero, value, 0);
}
}

View File

@@ -23,8 +23,11 @@ public partial class CGameSceneNode
/// <summary>
/// Gets the <see cref="CSkeletonInstance"/> instance from the node.
/// </summary>
/// <exception cref="InvalidOperationException">GameSceneNode points to null</exception>
public CSkeletonInstance GetSkeletonInstance()
{
if (Handle == IntPtr.Zero) throw new InvalidOperationException("GameSceneNode points to null.");
return new CSkeletonInstance(VirtualFunction.Create<nint, nint>(Handle, GameData.GetOffset("CGameSceneNode_GetSkeletonInstance"))(Handle));
}
}

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,6 +68,22 @@ public class PluginManager : IPluginManager
.Where(File.Exists)
.ToArray();
AssemblyLoadContext.Default.Resolving += (context, name) =>
{
if (!_loadedSharedLibs)
{
LoadSharedLibraries();
_loadedSharedLibs = true;
}
if (!_sharedAssemblies.TryGetValue(name.FullName, out var assembly))
{
return null;
}
return assembly;
};
foreach (var path in pluginAssemblyPaths)
{
try
@@ -42,6 +95,11 @@ public class PluginManager : IPluginManager
_logger.LogError(e, "Failed to load plugin from {Path}", path);
}
}
foreach (var plugin in _loadedPluginContexts)
{
plugin.Plugin.OnAllPluginsLoaded(false);
}
}
public IEnumerable<PluginContext> GetLoadedPlugins()
@@ -51,7 +109,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

@@ -0,0 +1,11 @@
namespace CounterStrikeSharp.API
{
public static class Guard
{
public static void IsValidEntity(CEntityInstance ent)
{
if (!ent.IsValid)
throw new InvalidOperationException("Entity is not valid");
}
}
}

View File

@@ -62,7 +62,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
CommandOverrides.TryGetValue(commandName, out var overrideDef);
if (overrideDef == null) return false;
return overrideDef.Enabled && overrideDef?.Flags.Count() > 0;
return overrideDef.Enabled;
}
/// <summary>

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

@@ -0,0 +1,140 @@
using System.Collections.Generic;
using System.ComponentModel;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
namespace CounterStrikeSharp.API.Modules.Cvars;
public class FakeConVar<T> where T : IComparable<T>
{
private readonly IEnumerable<IValidator<T>>? _customValidators;
public FakeConVar(string name, string description, T defaultValue = default(T), ConVarFlags flags = ConVarFlags.FCVAR_NONE,
params IValidator<T>[] customValidators)
{
_customValidators = customValidators;
Name = name;
Description = description;
Value = defaultValue;
Flags = flags;
}
public ConVarFlags Flags { get; set; }
public string Name { get; }
public string Description { get; }
public event EventHandler<T> ValueChanged;
private T _value;
public T Value
{
get => _value;
set => SetValue(value);
}
internal void ExecuteCommand(CCSPlayerController? player, CommandInfo args)
{
if (player != null && !Flags.HasFlag(ConVarFlags.FCVAR_REPLICATED))
{
return;
}
if (args.ArgCount < 2)
{
if (Flags.HasFlag(ConVarFlags.FCVAR_PROTECTED) && player != null)
{
args.ReplyToCommand($"{args.GetArg(0)} = <protected>");
}
else
{
args.ReplyToCommand($"{args.GetArg(0)} = {Value.ToString()}");
}
return;
}
if (Flags.HasFlag(ConVarFlags.FCVAR_CHEAT))
{
var cheats = ConVar.Find("sv_cheats")!.GetPrimitiveValue<bool>();
if (!cheats)
{
args.ReplyToCommand($"SV: Convar '{Name}' is cheat protected, change ignored");
return;
}
}
if (player != null)
{
return;
}
try
{
// TODO(dotnet8): Replace with IParsable<T>
bool success = true;
T parsedValue = default(T);
TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
if (converter.CanConvertFrom(typeof(string)))
{
try
{
parsedValue = (T)converter.ConvertFromString(args.ArgString);
}
catch
{
success = typeof(T) == typeof(bool) && TryConvertCustomBoolean(args.ArgString, out parsedValue);
}
}
if (!success)
{
args.ReplyToCommand($"Error: String '{args.GetArg(1)}' can't be converted to {typeof(T).Name}");
args.ReplyToCommand($"Failed to parse input ConVar '{Name}' from string '{args.GetArg(1)}'");
return;
}
SetValue(parsedValue);
}
catch (Exception ex)
{
args.ReplyToCommand($"Error: {ex.Message}");
}
}
private bool TryConvertCustomBoolean(string input, out T result)
{
input = input.Trim().ToLowerInvariant();
if (input == "1" || input == "true")
{
result = (T)(object)true;
return true;
}
else if (input == "0" || input == "false")
{
result = (T)(object)false;
return true;
}
result = default(T);
return false;
}
private void SetValue(T value)
{
if (_customValidators != null)
{
foreach (var validator in _customValidators)
{
if (!validator.Validate(value, out var error))
{
throw new ArgumentException($"{error ?? "Invalid value provided"}");
}
}
}
_value = value;
ValueChanged?.Invoke(this, _value);
}
}

View File

@@ -0,0 +1,6 @@
namespace CounterStrikeSharp.API.Modules.Cvars.Validators;
public interface IValidator<in T>
{
bool Validate(T value, out string? errorMessage);
}

View File

@@ -0,0 +1,27 @@
namespace CounterStrikeSharp.API.Modules.Cvars.Validators;
public class RangeValidator<T> : IValidator<T> where T : IComparable<T>
{
private readonly T _min;
private readonly T _max;
public RangeValidator(T min, T max)
{
_min = min;
_max = max;
}
public bool Validate(T value, out string? errorMessage)
{
if (value.CompareTo(_min) >= 0 && value.CompareTo(_max) <= 0)
{
errorMessage = null;
return true;
}
else
{
errorMessage = $"Value must be between {_min} and {_max}";
return false;
}
}
}

View File

@@ -0,0 +1,14 @@
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Modules.Extensions;
public static class PlayerExtensions
{
/// <summary>
/// <inheritdoc cref="ChatColors.ForPlayer"/>
/// </summary>
public static char GetChatColor(this CCSPlayerController player)
{
return ChatColors.ForPlayer(player);
}
}

View File

@@ -0,0 +1,15 @@
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Modules.Extensions;
public static class TeamExtensions
{
/// <summary>
/// <inheritdoc cref="ChatColors.ForTeam"/>
/// </summary>
/// <returns></returns>
public static char GetChatColor(this CsTeam team)
{
return ChatColors.ForTeam(team);
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@@ -78,11 +79,15 @@ public class Schema
public static T GetSchemaValue<T>(IntPtr handle, string className, string propertyName)
{
if (handle == IntPtr.Zero) throw new ArgumentNullException(nameof(handle), "Schema target points to null.");
return NativeAPI.GetSchemaValueByName<T>(handle, (int)typeof(T).ToDataType(), className, propertyName);
}
public static void SetSchemaValue<T>(IntPtr handle, string className, string propertyName, T value)
{
if (handle == IntPtr.Zero) throw new ArgumentNullException(nameof(handle), "Schema target points to null.");
if (CoreConfig.FollowCS2ServerGuidelines && _cs2BadList.Contains(propertyName))
{
throw new Exception($"Cannot set or get '{className}::{propertyName}' with \"FollowCS2ServerGuidelines\" option enabled.");
@@ -93,11 +98,15 @@ public class Schema
public static T GetDeclaredClass<T>(IntPtr pointer, string className, string memberName)
{
if (pointer == IntPtr.Zero) throw new ArgumentNullException(nameof(pointer), "Schema target points to null.");
return (T)Activator.CreateInstance(typeof(T), pointer + GetSchemaOffset(className, memberName));
}
public static unsafe ref T GetRef<T>(IntPtr pointer, string className, string memberName)
{
if (pointer == IntPtr.Zero) throw new ArgumentNullException(nameof(pointer), "Schema target points to null.");
return ref Unsafe.AsRef<T>((void*)(pointer + GetSchemaOffset(className, memberName)));
}
@@ -114,6 +123,8 @@ public class Schema
public static T GetPointer<T>(IntPtr pointer, string className, string memberName)
{
if (pointer == IntPtr.Zero) throw new ArgumentNullException(nameof(pointer), "Schema target points to null.");
var pointerTo = Marshal.ReadIntPtr(pointer + GetSchemaOffset(className, memberName));
if (pointerTo == IntPtr.Zero)
{
@@ -125,6 +136,8 @@ public class Schema
public static unsafe Span<T> GetFixedArray<T>(IntPtr pointer, string className, string memberName, int count)
{
if (pointer == IntPtr.Zero) throw new ArgumentNullException(nameof(pointer), "Schema target points to null.");
Span<T> span = new((void*)(pointer + GetSchemaOffset(className, memberName)), count);
return span;
}

View File

@@ -42,4 +42,37 @@ public class ChatColors
[Obsolete("Use ChatColors.DarkRed instead.")]
public static char Darkred = '\x02';
}
/// <summary>
/// Returns a chat color based on a team.
/// <remarks>Blue for CT, Yellow for T, LightPurple for Spectator</remarks>
/// </summary>
/// <exception cref="Exception"></exception>
public static char ForTeam(CsTeam team)
{
switch (team)
{
case CsTeam.None:
return White;
case CsTeam.Spectator:
return LightPurple;
case CsTeam.CounterTerrorist:
return LightBlue;
case CsTeam.Terrorist:
return Yellow;
default:
throw new ArgumentException($"Invalid team: ${team}");
}
}
/// <summary>
/// Returns a chat color for a player based on their team.
/// <remarks>Blue for CT, Yellow for T, LightPurple for Spectator</remarks>
/// </summary>
/// <exception cref="Exception"></exception>
public static char ForPlayer(CCSPlayerController player)
{
return ForTeam(player.Team);
}
}

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

@@ -40,10 +40,6 @@ namespace CounterStrikeSharp.API
public static float GameFrameTime => NativeAPI.GetGameFrameTime();
public static double EngineTime => NativeAPI.GetEngineTime();
public static void PrecacheModel(string name) => NativeAPI.PrecacheModel(name);
// public static void PrecacheSound(string name) => Sound.PrecacheSound(name);
// Currently only used to keep the delegate from being garbage collected
private static List<Action> nextFrameTasks = new List<Action>();
/// <summary>
/// Queue a task to be executed on the next game frame.
@@ -51,9 +47,9 @@ namespace CounterStrikeSharp.API
/// </summary>
public static void NextFrame(Action task)
{
nextFrameTasks.Add(task);
var ptr = Marshal.GetFunctionPointerForDelegate(task);
NativeAPI.QueueTaskForNextFrame(ptr);
var functionReference = FunctionReference.Create(task);
functionReference.Lifetime = FunctionLifetime.SingleUse;
NativeAPI.QueueTaskForNextFrame(functionReference);
}
/// <summary>
@@ -63,9 +59,9 @@ namespace CounterStrikeSharp.API
/// <param name="task"></param>
public static void NextWorldUpdate(Action task)
{
nextFrameTasks.Add(task);
var ptr = Marshal.GetFunctionPointerForDelegate(task);
NativeAPI.QueueTaskForNextWorldUpdate(ptr);
var functionReference = FunctionReference.Create(task);
functionReference.Lifetime = FunctionLifetime.SingleUse;
NativeAPI.QueueTaskForNextWorldUpdate(functionReference);
}
public static void PrintToChatAll(string message)

View File

@@ -209,8 +209,11 @@ namespace CounterStrikeSharp.API
/// <param name="className" example="CBaseEntity">Schema field class name</param>
/// <param name="fieldName" example="m_iHealth">Schema field name</param>
/// <param name="extraOffset">Any additional offset to the schema field</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public static void SetStateChanged(CBaseEntity entity, string className, string fieldName, int extraOffset = 0)
{
Guard.IsValidEntity(entity);
if (!Schema.IsSchemaFieldNetworked(className, fieldName))
{
Application.Instance.Logger.LogWarning("Field {ClassName}:{FieldName} is not networked, but SetStateChanged was called on it.", className, fieldName);

View File

@@ -32,6 +32,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithTranslations", "..\exam
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithVoiceOverrides", "..\examples\WithVoiceOverrides\WithVoiceOverrides.csproj", "{6FA3107D-42AF-42A0-BF51-2230D13268B5}"
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
@@ -98,6 +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}
@@ -111,5 +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()

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,8 @@
#include "core/log.h"
#include "scripting/callback_manager.h"
#include "core/game_system.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 +58,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 +85,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()
@@ -197,4 +203,16 @@ void ServerManager::AddTaskForNextWorldUpdate(std::function<void()>&& task)
std::lock_guard<std::mutex> lock(m_nextWorldUpdateTasksLock);
m_nextWorldUpdateTasks.push_back(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

@@ -20,6 +20,8 @@
#include "core/global_listener.h"
#include "scripting/script_engine.h"
#include "core/game_system.h"
namespace counterstrikesharp {
class ScriptCallback;
@@ -31,7 +33,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,6 +54,8 @@ private:
ScriptCallback *on_server_update_when_not_in_game;
ScriptCallback *on_server_pre_world_update;
ScriptCallback *on_server_precache_resources;
std::vector<std::function<void()>> m_nextWorldUpdateTasks;
std::mutex m_nextWorldUpdateTasksLock;
};

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

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

@@ -21,8 +21,8 @@ TRACE_FILTER_PROXY_SET_TRACE_TYPE_CALLBACK: trace_filter:pointer, callback:point
TRACE_FILTER_PROXY_SET_SHOULD_HIT_ENTITY_CALLBACK: trace_filter:pointer, callback:pointer -> void
NEW_TRACE_RESULT: -> pointer
GET_TICKED_TIME: -> double
QUEUE_TASK_FOR_NEXT_FRAME: callback:pointer -> void
QUEUE_TASK_FOR_NEXT_WORLD_UPDATE: callback:pointer -> void
QUEUE_TASK_FOR_NEXT_FRAME: callback:func -> void
QUEUE_TASK_FOR_NEXT_WORLD_UPDATE: callback:func -> void
GET_VALVE_INTERFACE: interfaceType:int, interfaceName:string -> pointer
GET_COMMAND_PARAM_VALUE: param:string, dataType:DataType_t, defaultValue:any -> any
PRINT_TO_SERVER_CONSOLE: msg:string -> void

View File

@@ -29,6 +29,12 @@
namespace counterstrikesharp {
CBaseEntity* GetEntityFromIndex(ScriptContext& script_context) {
if (!globals::entitySystem)
{
script_context.ThrowNativeError("Entity system is not yet initialized");
return nullptr;
}
auto entityIndex = script_context.GetArgument<int>(0);
return globals::entitySystem->GetBaseEntity(CEntityIndex(entityIndex));
@@ -47,6 +53,11 @@ const char* GetDesignerName(ScriptContext& scriptContext) {
}
void* GetEntityPointerFromHandle(ScriptContext& scriptContext) {
if (!globals::entitySystem) {
scriptContext.ThrowNativeError("Entity system is not yet initialized");
return nullptr;
}
auto handle = scriptContext.GetArgument<CEntityHandle*>(0);
if (!handle->IsValid()) {
@@ -57,6 +68,11 @@ void* GetEntityPointerFromHandle(ScriptContext& scriptContext) {
}
void* GetEntityPointerFromRef(ScriptContext& scriptContext) {
if (!globals::entitySystem) {
scriptContext.ThrowNativeError("Entity system yet is not initialized");
return nullptr;
}
auto ref = scriptContext.GetArgument<unsigned int>(0);
if (ref == INVALID_EHANDLE_INDEX) {
@@ -87,6 +103,11 @@ unsigned int GetRefFromEntityPointer(ScriptContext& scriptContext) {
}
bool IsRefValidEntity(ScriptContext& scriptContext) {
if (!globals::entitySystem) {
scriptContext.ThrowNativeError("Entity system yet is not initialized");
return false;
}
auto ref = scriptContext.GetArgument<unsigned int>(0);
if (ref == INVALID_EHANDLE_INDEX) {
@@ -110,10 +131,20 @@ void PrintToConsole(ScriptContext& scriptContext) {
}
CEntityIdentity* GetFirstActiveEntity(ScriptContext& script_context) {
if (!globals::entitySystem) {
script_context.ThrowNativeError("Entity system yet is not initialized");
return nullptr;
}
return globals::entitySystem->m_EntityList.m_pFirstActiveEntity;
}
void* GetConcreteEntityListPointer(ScriptContext& script_context) {
if (!globals::entitySystem) {
script_context.ThrowNativeError("Entity system yet is not initialized");
return nullptr;
}
return &globals::entitySystem->m_EntityList;
}

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 =>
{