Compare commits

...

56 Commits

Author SHA1 Message Date
Poggu
7baf0a25e2 Prevent calling natives on non-main thread (#170)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
Co-authored-by: Roflmuffin <shortguy014@gmail.com>
2023-12-04 18:26:36 +10:00
Roflmuffin
9fdbb9500b fix: run deleted plugin handler in next world update 2023-12-04 18:14:27 +10:00
Roflmuffin
0ab3cf429a feat: add NextWorldUpdate helper to run on next pre world update
also runs hot reload in next world update
2023-12-04 17:43:52 +10:00
chte
1f9630b92d Enriched SteamID (#163)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2023-12-03 19:50:53 +10:00
Roflmuffin
02bf2483d3 docs: add database (dapper) example plugin 2023-12-03 19:23:23 +10:00
Roflmuffin
cb181b6a49 docs: add database (dapper) example plugin 2023-12-03 19:23:02 +10:00
Nexd
cc21dca5a0 Exposing from ISource2Server and IVEngineServer2 (#159)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
Co-authored-by: Roflmuffin <shortguy014@gmail.com>
2023-12-03 15:17:04 +10:00
Roflmuffin
5721d060ea feat: add overload for PrintToCenterHtml that accepts duration
closes #127
2023-12-03 14:18:10 +10:00
Roflmuffin
220521d571 fix: free callback property on game event unhook 2023-12-03 14:02:42 +10:00
Roflmuffin
5698b511e9 fix: use authorized Steam ID for admin system
also fix reference equality for SteamID
2023-12-03 13:34:11 +10:00
Roflmuffin
48c9d195ff feat: add IpAddress to CCSPlayerController 2023-12-03 13:11:06 +10:00
Roflmuffin
603827d331 fix: remove reference equality for CEntityInstance 2023-12-03 13:02:33 +10:00
roflmuffin
e557d54c32 fix: fires client authorize on map change (fixes #162)
Also adds `AuthorizedSteamID` property which is guaranteed to be valid with Steam API
2023-12-02 16:35:52 +10:00
Nexd
48d3ade5cf VirtualFunction & MemoryFunction rework to support arbitrary binary path (#158) 2023-12-02 15:41:01 +10:00
DRANIX
77b05e912e fix github actions xml warnings (#164) 2023-12-02 12:10:32 +10:00
Michael Wilson
1354b4972d docs: add some example plugins (#154) 2023-12-01 13:27:55 +10:00
johnoclockdk
8b5eb7e38d Update README.md (#153) 2023-11-30 17:08:09 +10:00
Roflmuffin
2dd62c44d3 docs: add missing core config doc 2023-11-30 16:07:28 +10:00
Roflmuffin
f811338ce4 feat: add option to disable plugin hot reload, closes #151
Also adds default values to unmanaged core config
2023-11-30 13:49:24 +10:00
Daniel Saewitz
194c340ae7 Improve plugin setup docs (#152) 2023-11-30 09:58:38 +10:00
Roflmuffin
6b0912d3cd hotfix: revert entity enumeration 2023-11-29 23:09:38 +10:00
Roflmuffin
2d3aa09aa4 hotfix: allow handles to be written to again 2023-11-29 22:35:54 +10:00
Roflmuffin
911084e71e feat: Add Schema Size Native 2023-11-29 21:50:57 +10:00
Roflmuffin
5b99206568 Merge remote-tracking branch 'origin/main' into feature/add-schema-class-size 2023-11-29 21:46:17 +10:00
Roflmuffin
4bfdf28beb Merge branch 'feature/entity-handle-overhaul' into feature/add-schema-class-size 2023-11-29 21:45:54 +10:00
Michael Wilson
9bcd0f7e92 Entity Handle Overhaul (#142) 2023-11-29 21:45:31 +10:00
Roflmuffin
11c6486ec5 chore: update test plugin version 2023-11-29 21:42:59 +10:00
Roflmuffin
ee69560a66 fix: bad style 2023-11-29 21:41:51 +10:00
Roflmuffin
d37e5e194a fix: use IntPtr.Zero instead of 0 2023-11-29 21:38:45 +10:00
Roflmuffin
c4740d1cc9 feat: add schema class size native, cast native objects to input argument 2023-11-29 21:06:55 +10:00
Roflmuffin
7e92f178fd feat: add Slot to player controller 2023-11-29 20:24:23 +10:00
Roflmuffin
107ca08132 Merge branch 'main' into feature/entity-handle-overhaul 2023-11-29 20:21:38 +10:00
Roflmuffin
8cda8d9a50 feat: wrap ExecuteClientCommand and add sound example 2023-11-29 20:10:45 +10:00
Roflmuffin
3d59a05de8 feat: remove native call from native entity instantiation 2023-11-29 20:03:49 +10:00
Roflmuffin
77b7040d6c feat: add GetAllEntities method, update implementation 2023-11-29 19:52:14 +10:00
Roflmuffin
75de9732ef feat: move entity system into managed code for perf 2023-11-29 19:43:46 +10:00
Roflmuffin
575c859ddb fix: add brute force fallback for enum member attribute, fixes #150 2023-11-29 17:25:31 +10:00
Nexd
e12a7cb17a fix: wildcard bytes for signatures (resolves #123 and related issues) (#148) 2023-11-28 23:57:26 +10:00
Roflmuffin
7c7f52a219 feat: update test plugin 2023-11-27 22:31:05 +10:00
Roflmuffin
cd593fb238 feat: add EntityIndex back to api compat, mark as obsolete 2023-11-27 21:44:43 +10:00
Roflmuffin
c5cc65be48 fix: remove expensive calls in bullet impact event 2023-11-27 21:24:18 +10:00
Roflmuffin
59928bbcc5 feat: add NativeEntity class 2023-11-27 20:35:38 +10:00
Roflmuffin
319b116c5f fix: bugs in config manager & plugin load, fixes #138 2023-11-27 11:42:34 +10:00
Michael Wilson
e0dc053d22 Config Example Parsing (#136) 2023-11-26 20:30:21 +10:00
Charles_
f2e0dac32d Feature: ProcessTargetString() & GetPlayerFromSteamId() (#121)
Co-authored-by: Roflmuffin <shortguy014@gmail.com>
2023-11-26 15:03:01 +10:00
Michael Wilson
4e8c18abc7 Implement Core & Plugin Service Collection (#129) 2023-11-26 14:15:58 +10:00
Roflmuffin
8d1891a3a8 Merge branch 'main' of github.com:roflmuffin/CounterStrikeSharp 2023-11-26 13:19:54 +10:00
Roflmuffin
6bc43444f7 feat: add trigger touch start and end hooks 2023-11-26 13:19:40 +10:00
miguno
f0c7869f4a Check if userid is valid before accessing its fields, and explain why (#133) 2023-11-26 10:03:20 +10:00
Robert
3e38ed3c77 feat: Added ability to GiveNamedItem using the new CsItem Enum (#105) 2023-11-25 10:40:49 +10:00
Roflmuffin
7e9e7c6665 fix: wrong chat colors 2023-11-24 21:21:03 +10:00
Roflmuffin
9a018f295b feat: add player pawn post think signature 2023-11-24 21:04:22 +10:00
Michael Wilson
8b725d435f Dynamic Hooks (#78) 2023-11-24 19:59:47 +10:00
Nexd
ea3596417a Add methods to respawn players (#114) 2023-11-24 15:19:27 +10:00
Abner Santos
123f41914e docs: Additional info admin module documentation (#116) 2023-11-23 10:44:02 +10:00
Hackmastr
8f69076405 Update TestPlugin.cs, (#115) 2023-11-23 09:03:30 +10:00
148 changed files with 5947 additions and 1074 deletions

6
.gitmodules vendored
View File

@@ -17,3 +17,9 @@
[submodule "libraries/GameTracking-CS2"]
path = libraries/GameTracking-CS2
url = https://github.com/SteamDatabase/GameTracking-CS2
[submodule "libraries/DynoHook"]
path = libraries/DynoHook
url = git@github.com:qubka/DynoHook.git
[submodule "libraries/asmjit"]
path = libraries/asmjit
url = git@github.com:asmjit/asmjit.git

View File

@@ -8,7 +8,9 @@ include("makefiles/shared.cmake")
add_subdirectory(libraries/spdlog)
add_subdirectory(libraries/dyncall)
add_subdirectory(libraries/funchook)
add_subdirectory(libraries/DynoHook)
set_property(TARGET dynohook PROPERTY DYNO_ARCH_X86 64)
set_property(TARGET funchook-static PROPERTY POSITION_INDEPENDENT_CODE ON)
SET(SOURCE_FILES
@@ -81,6 +83,7 @@ SET(SOURCE_FILES
src/core/managers/server_manager.h
src/scripting/natives/natives_server.cpp
libraries/nlohmann/json.hpp
src/scripting/natives/natives_dynamichooks.cpp
)

View File

@@ -63,6 +63,10 @@ public class HelloWorldPlugin : BasePlugin
public override string ModuleVersion => "0.0.1";
public override string ModuleAuthor => "roflmuffin";
public override string ModuleDescription => "Simple hello world plugin";
public override void Load(bool hotReload)
{
Logger.LogInformation("Plugin loaded successfully!");

View File

@@ -0,0 +1,6 @@
{
"PublicChatTrigger": [ "!" ],
"SilentChatTrigger": [ "/" ],
"FollowCS2ServerGuidelines": true,
"PluginHotReloadEnabled": true
}

View File

@@ -1,5 +0,0 @@
{
"PublicChatTrigger": [ "!" ],
"SilentChatTrigger": [ "/" ],
"FollowCS2ServerGuidelines": true
}

View File

@@ -26,6 +26,26 @@
"linux": 89
}
},
"CCSPlayerController_Respawn": {
"offsets": {
"windows": 241,
"linux": 243
}
},
"CCSPlayerPawn_Respawn": {
"signatures": {
"library": "server",
"windows": "\\x40\\x53\\x48\\x83\\xEC\\x20\\x8B\\x91\\x38\\x0B\\x00\\x00\\x48\\x8B\\xD9",
"linux": "\\x8B\\x8F\\x40\\x0E\\x00\\x00\\x83\\xF9\\xFF\\x0F\\x84\\xD9\\x01"
}
},
"CCSPlayerPawnBase_PostThink": {
"signatures": {
"library": "server",
"windows": "\\x48\\x8B\\xC4\\x48\\x89\\x48\\x08\\x55\\x53\\x56\\x57\\x41\\x56\\x48\\x8D\\xA8\\xD8\\xFE\\xFF\\xFF",
"linux": "\\x55\\x48\\x89\\xE5\\x41\\x57\\x49\\x89\\xFF\\x41\\x56\\x41\\x55\\x41\\x54\\x53\\x48\\x81\\xEC\\x2A\\x2A\\x2A\\x2A\\xE8\\x2A\\x2A\\x2A\\x2A\\x4C"
}
},
"GiveNamedItem": {
"signatures": {
"library": "server",
@@ -51,12 +71,12 @@
"signatures": {
"library": "server",
"windows": "\\x48\\x89\\x5C\\x24\\x10\\x48\\x89\\x7C\\x24\\x20\\x55\\x48\\x8B\\xEC\\x48\\x83\\xEC\\x50",
"linux": "\\x55\\x48\\x89\\xF2\\x48\\x89\\xE5\\x41\\x54\\x49\\x89\\xFC\\x48\\x8D\\x7D\\xE0\\x48\\x83\\xEC\\x18\\x48\\x8D\\x05\\xA5"
"linux": "\\x55\\x48\\x89\\xF2\\x48\\x89\\xE5\\x41\\x54\\x49\\x89\\xFC\\x48\\x8D\\x2A\\x2A\\x48\\x83\\x2A\\x2A\\x2A\\x8D\\x05\\x2A\\x2A\\x2A\\x00\\x48\\x8B\\x30\\x48\\x8B\\x06\\xFF\\x2A\\x2A\\x48\\x8B\\x45\\x2A\\x48\\x8D\\x2A\\x2A\\x4C\\x89\\x2A\\x48\\x89\\x45\\x2A\\x2A\\x68\\xFC"
}
},
"CCSPlayer_ItemServices_DropActivePlayerWeapon": {
"offsets": {
"windows": 20,
"windows": 18,
"linux": 19
}
},
@@ -83,7 +103,7 @@
"signatures": {
"library": "server",
"windows": "\\x48\\x83\\xEC\\x48\\xC6\\x44\\x24\\x30\\x00\\x4C\\x8B\\xC1",
"linux": "\\x48\\x8D\\x05\\xC9\\xC2\\xBC"
"linux": "\\x48\\x8D\\x05\\x2A\\x2A\\x2A\\x2A\\x55\\x48\\x89\\xFA"
}
},
"CBaseEntity_DispatchSpawn": {
@@ -112,6 +132,27 @@
"linux": 147
}
},
"CBaseEntity_TakeDamageOld": {
"signatures": {
"library": "server",
"windows": "\\x40\\x56\\x57\\x48\\x83\\xEC\\x58\\x48\\x8B\\x41\\x10",
"linux": "\\x55\\x48\\x89\\xE5\\x41\\x57\\x41\\x56\\x41\\x55\\x41\\x54\\x49\\x89\\xFC\\x53\\x48\\x83\\xEC\\x38\\x4C\\x8D\\x2D\\x2A\\x2A\\x2A\\x2A\\x49\\x8B\\x7D\\x00\\x48\\x85\\xFF\\x0F\\x84\\x2A\\x2A\\x2A\\x2A"
}
},
"CBaseTrigger_StartTouch": {
"signatures": {
"library": "server",
"windows": "\\x41\\x56\\x41\\x57\\x48\\x83\\xEC\\x58\\x48\\x8B\\x01",
"linux": "\\x55\\x48\\x89\\xE5\\x41\\x56\\x49\\x89\\xF6\\x41\\x55\\x49\\x89\\xFD\\x41\\x54\\x53\\xBB"
}
},
"CBaseTrigger_EndTouch": {
"signatures": {
"library": "server",
"windows": "\\x40\\x53\\x57\\x41\\x55\\x48\\x83\\xEC\\x40",
"linux": "\\x55\\xBA\\xFF\\xFF\\xFF\\xFF\\x48\\x89\\xE5\\x41\\x57\\x41\\x56\\x41\\x55\\x49"
}
},
"GameEntitySystem": {
"offsets": {
"windows": 88,

View File

@@ -16,6 +16,10 @@ Adding an Admin is as simple as creating a new entry in the `configs/admins.json
"ZoNiCaL": {
"identity": "76561198808392634",
"flags": ["@css/changemap", "@css/generic"]
},
"another ZoNiCaL": {
"identity": "STEAM_0:1:1",
"flags": ["@css/generic"]
}
}
```
@@ -53,4 +57,5 @@ However there is a somewhat standardized list of flags that it is advised you us
:::note
CounterStrikeSharp does not implement traditional admin command such as `!slay`, `!kick`, and `!ban`. It is up to individual plugins to implement these commands.
:::
:::

View File

@@ -18,8 +18,25 @@ Groups can be created to group a series of permissions together under one tag. T
}
```
You can add admins to groups using the `groups` array in `configs/admins.json`
```json
{
"erikj": {
"identity": "76561198808392634",
"flags": ["@mycustomplugin/admin"],
"groups": ["#css/simple-admin"]
},
"Another erikj": {
"identity": "STEAM_0:1:1",
"flags": ["@mycustomplugin/admin"],
"groups": ["#css/simple-admin"]
}
}
```
:::note
All group names MUST start with a hashtag # character, otherwise CounterStrikeSharp will recognize the group.
All group names MUST start with a hashtag # character, otherwise CounterStrikeSharp won't recognize the group.
:::
Admins can be assigned to multiple groups and they will inherit their flags. You can manually assign groups to players in code with `AdminManager.AddPlayerToGroup` and `AdminManager.RemovePlayerFromGroup`.
Admins can be assigned to multiple groups and they will inherit their flags. You can manually assign groups to players in code with `AdminManager.AddPlayerToGroup` and `AdminManager.RemovePlayerFromGroup`.

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
---
title: Hello World Plugin
description: How to write your first plugin for CounterStrikeSharp
sidebar:
order: 0
---
## Creating a New Project
@@ -66,19 +68,20 @@ Now build your project using your ide or the `dotnet build` command. You should
### Installing your Plugin
Locate the `plugins` folder in your CS2 dedicated server (`/game/csgo/addons/counterstrikesharp/plugins`) and create a new folder with the exact same name as your output .dll file. In this example it would be `HelloWorldPlugin.dll`, so I will make a new folder called `HelloWorldPlugin`. Inside of this folder, copy and paste all of the `.dll` files _except_ the `CounterStrikeSharp.API.dll` file. Once completed, the folder should look as follows:
Locate the `plugins` folder in your CS2 dedicated server (`/game/csgo/addons/counterstrikesharp/plugins`) and create a new folder with the exact same name as your output .dll file. In this example it would be `HelloWorldPlugin.dll`, so I will make a new folder called `HelloWorldPlugin`. Inside of this folder, copy and paste the: `HelloWorldPlugin.deps.json`, `HelloWorldPlugin.dll`, and `HelloWorldPlugin.pdb` files. Once completed, the folder should look as follows:
```shell
.
└── SamplePlugin
├── McMaster.NETCore.Plugins.dll
├── Microsoft.DotNet.PlatformAbstractions.dll
── Microsoft.Extensions.DependencyModel.dll
├── SamplePlugin.deps.json
├── SamplePlugin.dll
└── SamplePlugin.pdb
└── HelloWorldPlugin
├── HelloWorldPlugin.deps.json
├── HelloWorldPlugin.dll
── HelloWorldPlugin.pdb
```
:::caution
If you have installed external nuget packages for your plugin, you may need to include their respective `.dll`s. For example, if you utilize the `Stateless` C# library, include `stateless.dll` in your `HelloWorld` plugin directory.
:::
:::note
Note that some of these dependencies may change depending on the version of CounterStrikeSharp being used.
:::

View File

@@ -25,6 +25,10 @@ public class HelloWorldPlugin : BasePlugin
public override string ModuleVersion => "0.0.1";
public override string ModuleAuthor => "roflmuffin";
public override string ModuleDescription => "Simple hello world plugin";
public override void Load(bool hotReload)
{
Logger.LogInformation("Plugin loaded successfully!");

View File

@@ -23,4 +23,8 @@ receive a ban.
:::note
Disable this option at your own risk.
:::
:::
## PluginHotReloadEnabled
When enabled, plugins are automatically reloaded when their .dll file is updated.

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,24 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using Microsoft.Extensions.Logging;
namespace HelloWorld;
[MinimumApiVersion(80)]
public class HelloWorldPlugin : BasePlugin
{
public override string ModuleName => "Example: Hello World";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that says hello world!";
public override void Load(bool hotReload)
{
Logger.LogInformation("Hello World! We are loading!");
}
public override void Unload(bool hotReload)
{
Logger.LogInformation("Hello World! We are unloading!");
}
}

View File

@@ -0,0 +1,2 @@
# Hello World
This is a minimal "Hello World" example that executes code on load & unload.

View File

@@ -73,7 +73,7 @@ namespace WarcraftPlugin
var victim = @event.Userid;
var headshot = @event.Headshot;
if (attacker.IsValid && victim.IsValid && (attacker.EntityIndex.Value.Value != victim.EntityIndex.Value.Value) && !attacker.IsBot)
if (attacker.IsValid && victim.IsValid && (attacker != victim) && !attacker.IsBot)
{
var weaponName = attacker.PlayerPawn.Value.WeaponServices.ActiveWeapon.Value.DesignerName;

View File

@@ -0,0 +1,4 @@
# With Commands
This is an example that shows how to register chat & console commands.
All commands that are prefixed with "css_" will automatically be registered as a chat command without the prefix. i.e. `css_ping` can be called with `!ping` or `/ping`.

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,63 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
namespace WithCommands;
[MinimumApiVersion(80)]
public class WithCommandsPlugin : BasePlugin
{
public override string ModuleName => "Example: With Commands";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that registers some commands";
public override void Load(bool hotReload)
{
// All commands that are prefixed with "css_" will automatically be registered as a chat command without the prefix.
// i.e. `css_ping` can be called with `!ping` or `/ping`.
// Commands can be registered using the instance `AddCommand` method.
AddCommand("css_ping", "Responds to the caller with \"pong\"", (player, commandInfo) =>
{
// The player is null, then the command has been called by the server console.
if (player == null)
{
commandInfo.ReplyToCommand("pong server");
return;
}
commandInfo.ReplyToCommand("pong");
});
}
// Commands can also be registered using the `Command` attribute.
[ConsoleCommand("css_hello", "Responds to the caller with \"pong\"")]
// The `CommandHelper` attribute can be used to provide additional information about the command.
[CommandHelper(minArgs: 1, usage: "[name]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/cvar")]
public void OnHelloCommand(CCSPlayerController? player, CommandInfo commandInfo)
{
// The first argument is the command name, in this case "css_hello".
commandInfo.GetArg(0); // css_hello
// The second argument is the first argument passed to the command, in this case "name".
// The `minArgs` helper parameter is used to ensure that the second argument is present.
var name = commandInfo.GetArg(1);
commandInfo.ReplyToCommand($"Hello {name}");
}
// Permissions can be added to commands using the `RequiresPermissions` attribute.
// See the admin documentation for more information on permissions.
[RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 1, usage: "[id]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSpecialCommand(CCSPlayerController? player, CommandInfo commandInfo)
{
var id = commandInfo.GetArg(1);
Server.ExecuteCommand($"kick {id}");
}
}

View File

@@ -0,0 +1,2 @@
# WithConfig
This example shows how you can implement the `IPluginConfig` interface to allow for semi-automatic config parsing & loading.

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,38 @@
using System.Text.Json.Serialization;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
namespace WithConfig;
public class SampleConfig : BasePluginConfig
{
[JsonPropertyName("ChatPrefix")] public string ChatPrefix { get; set; } = "My Cool Plugin";
[JsonPropertyName("ChatInterval")] public float ChatInterval { get; set; } = 60;
}
[MinimumApiVersion(80)]
public class WithConfigPlugin : BasePlugin, IPluginConfig<SampleConfig>
{
public override string ModuleName => "Example: With Config";
public override string ModuleVersion => "1.0.0";
public SampleConfig Config { get; set; }
public void OnConfigParsed(SampleConfig config)
{
// Do manual verification of the config and override any invalid values
if (config.ChatInterval > 60)
{
config.ChatInterval = 60;
}
if (config.ChatPrefix.Length > 25)
{
throw new Exception($"Invalid value has been set to config value 'ChatPrefix': {config.ChatPrefix}");
}
// Once we've validated the config, we can set it to the instance
Config = config;
}
}

View File

@@ -0,0 +1,2 @@
# With Database (Dapper)
Simple SQLite database example using Dapper library to track kills.

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\managed\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.24" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.14" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,89 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using Dapper;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
namespace WithDatabaseDapper;
[MinimumApiVersion(80)]
public class WithDatabaseDapperPlugin : BasePlugin
{
public override string ModuleName => "Example: With Database (Dapper)";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A plugin that reads and writes from the database.";
private SqliteConnection _connection = null!;
public override void Load(bool hotReload)
{
Logger.LogInformation("Loading database from {Path}", Path.Join(ModuleDirectory, "database.db"));
_connection = new SqliteConnection($"Data Source={Path.Join(ModuleDirectory, "database.db")}");
_connection.Open();
// Create the table if it doesn't exist
// Run in a separate thread to avoid blocking the main thread
Task.Run(async () =>
{
await _connection.ExecuteAsync(@"
CREATE TABLE IF NOT EXISTS `players` (
`steamid` UNSIGNED BIG INT NOT NULL,
`kills` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`steamid`));");
});
}
[GameEventHandler]
public HookResult OnPlayerKilled(EventPlayerDeath @event, GameEventInfo info)
{
// Don't count suicides.
if (@event.Attacker == @event.Userid) return HookResult.Continue;
// Capture the steamid of the player as `@event` will not be available outside of this function.
var steamId = @event.Attacker.AuthorizedSteamID?.SteamId64;
if (steamId == null) return HookResult.Continue;
// Run in a separate thread to avoid blocking the main thread
Task.Run(async () =>
{
// insert or update the player's kills
await _connection.ExecuteAsync(@"
INSERT INTO `players` (`steamid`, `kills`) VALUES (@SteamId, 1)
ON CONFLICT(`steamid`) DO UPDATE SET `kills` = `kills` + 1;",
new
{
SteamId = steamId
});
});
return HookResult.Continue;
}
[ConsoleCommand("css_kills", "Get count of kills for a player")]
public void OnKillsCommand(CCSPlayerController? player, CommandInfo commandInfo)
{
if (player == null) return;
// Capture the SteamID of the player as `@event` will not be available outside of this function.
var steamId = player.AuthorizedSteamID.SteamId64;
// Run in a separate thread to avoid blocking the main thread
Task.Run(async () =>
{
var result = await _connection.QueryFirstOrDefaultAsync(@"SELECT `kills` FROM `players` WHERE `steamid` = @SteamId;",
new
{
SteamId = steamId
});
// Print the result to the player's chat. Note that this needs to be run on the game thread.
// So we use `Server.NextFrame` to run it on the next game tick.
Server.NextFrame(() => { player.PrintToChat($"Kills: {result?.kills ?? 0}"); });
});
}
}

View File

@@ -0,0 +1,2 @@
# With Dependency Injection
This example shows how can you implement the `IPluginServiceCollection` interface to register your services in a provided DI container.

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,50 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace WithDependencyInjection;
[MinimumApiVersion(80)]
public class WithDependencyInjectionPlugin : BasePlugin
{
public override string ModuleName => "Example: Dependency Injection";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "An example plugin that uses dependency injection.";
private readonly TestInjectedClass _testInjectedClass;
public WithDependencyInjectionPlugin(TestInjectedClass testInjectedClass)
{
_testInjectedClass = testInjectedClass;
}
public override void Load(bool hotReload)
{
_testInjectedClass.SayHello();
}
}
public class WithDependencyInjectionPluginServiceCollection : IPluginServiceCollection<WithDependencyInjectionPlugin>
{
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<TestInjectedClass>();
}
}
public class TestInjectedClass
{
private readonly ILogger<TestInjectedClass> _logger;
public TestInjectedClass(ILogger<TestInjectedClass> logger)
{
_logger = logger;
}
public void SayHello()
{
_logger.LogInformation("Hello World from Test Injected Class");
}
}

View File

@@ -0,0 +1,2 @@
# With Game Event Handlers
This is example shows how you can subscribe to legacy Source 1 game events.

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,54 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using Microsoft.Extensions.Logging;
namespace WithGameEventHandlers;
[MinimumApiVersion(80)]
public class WithGameEventHandlersPlugin : BasePlugin
{
public override string ModuleName => "Example: With Game Event Handlers";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that subscribes to game events";
public override void Load(bool hotReload)
{
// Subscriptions can be added via the instance method
RegisterEventHandler<EventPlayerDeath>((@event, info) =>
{
// You can use `info.DontBroadcast` to set the dont broadcast flag on the event (in pre handlers)
// This will prevent the event from being broadcast to other clients.
// In this example we prevent kill-feed messages from being broadcast if it was not a headshot.
if (!@event.Headshot)
{
@event.Attacker.PrintToChat($"Skipping player_death broadcast");
info.DontBroadcast = true;
}
return HookResult.Continue;
}, HookMode.Pre);
}
// Subscriptions can be added via an attribute
[GameEventHandler]
public HookResult OnPlayerBlind(EventPlayerBlind @event, GameEventInfo info)
{
Logger.LogInformation("Player was just blinded for {Duration}", @event.BlindDuration);
return HookResult.Continue;
}
// The event name is inferred from the event type you pass to the first argument.
// e.g. EventRoundStart becomes "round_start"
// Note: You can use the `HookMode` enum to specify the hook mode
// If you do not specify a hook mode, it will default to `HookMode.Post`
[GameEventHandler(HookMode.Pre)]
public HookResult OnEventRoundStartPre(EventRoundStart @event, GameEventInfo info)
{
Logger.LogInformation("Round has started with Timelimit: {Timelimit}", @event.Timelimit);
return HookResult.Continue;
}
}

1
libraries/DynoHook Submodule

Submodule libraries/DynoHook added at d7f8ebb059

1
libraries/asmjit Submodule

Submodule libraries/asmjit added at 0dd16b0a98

View File

@@ -16,14 +16,16 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof -Wno-reorder")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfpmath=sse -msse -fno-strict-aliasing")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-threadsafe-statics -v -fvisibility=default")
set(COUNTER_STRIKE_SHARP_LINK_LIBRARIES
${SOURCESDK_LIB}/linux64/libtier0.so
${SOURCESDK_LIB}/linux64/tier1.a
${SOURCESDK_LIB}/linux64/interfaces.a
${SOURCESDK_LIB}/linux64/mathlib.a
spdlog
dynload_s
dyncall_s
distorm
funchook-static
SET(
COUNTER_STRIKE_SHARP_LINK_LIBRARIES
${SOURCESDK_LIB}/linux64/libtier0.so
${SOURCESDK_LIB}/linux64/tier1.a
${SOURCESDK_LIB}/linux64/interfaces.a
${SOURCESDK_LIB}/linux64/mathlib.a
spdlog
dynload_s
dyncall_s
distorm
funchook-static
dynohook
)

View File

@@ -14,6 +14,8 @@ set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING
# TODO: Use C++20 instead.
set(CMAKE_CXX_STANDARD 17)
Set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
Set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC")
set(CMAKE_STATIC_LIBRARY_PREFIX "")
set(SOURCESDK_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libraries/hl2sdk-cs2)
@@ -45,6 +47,7 @@ include_directories(
libraries/spdlog/include
libraries/tl
libraries/funchook/include
libraries/DynoHook/src
libraries
)

View File

@@ -17,4 +17,5 @@ set(COUNTER_STRIKE_SHARP_LINK_LIBRARIES
dyncall_s
distorm
funchook-static
dynohook
)

View File

@@ -30,5 +30,19 @@ namespace CounterStrikeSharp.API
{
Handle = pointer;
}
/// <summary>
/// Returns a new instance of the specified type using the pointer from the passed in object.
/// </summary>
/// <remarks>
/// Useful for creating a new instance of a class that inherits from NativeObject.
/// e.g. <code>var weaponServices = playerWeaponServices.As&lt;CCSPlayer_WeaponServices&gt;();</code>
/// </remarks>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T As<T>() where T : NativeObject
{
return (T)Activator.CreateInstance(typeof(T), this.Handle);
}
}
}

View File

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

View File

@@ -157,6 +157,80 @@ namespace CounterStrikeSharp.API.Core
}
}
public static string GetClientConvarValue(int clientindex, string convarname){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(clientindex);
ScriptContext.GlobalScriptContext.Push(convarname);
ScriptContext.GlobalScriptContext.SetIdentifier(0xAE4B1B79);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (string)ScriptContext.GlobalScriptContext.GetResult(typeof(string));
}
}
public static void SetFakeClientConvarValue(int clientindex, string convarname, string convarvalue){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(clientindex);
ScriptContext.GlobalScriptContext.Push(convarname);
ScriptContext.GlobalScriptContext.Push(convarvalue);
ScriptContext.GlobalScriptContext.SetIdentifier(0x4C61E8BB);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static T DynamicHookGetReturn<T>(IntPtr hook, int datatype){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(hook);
ScriptContext.GlobalScriptContext.Push(datatype);
ScriptContext.GlobalScriptContext.SetIdentifier(0x4F5B80D0);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (T)ScriptContext.GlobalScriptContext.GetResult(typeof(T));
}
}
public static void DynamicHookSetReturn<T>(IntPtr hook, int datatype, T value){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(hook);
ScriptContext.GlobalScriptContext.Push(datatype);
ScriptContext.GlobalScriptContext.Push(value);
ScriptContext.GlobalScriptContext.SetIdentifier(0xDB297E44);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static T DynamicHookGetParam<T>(IntPtr hook, int datatype, int paramindex){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(hook);
ScriptContext.GlobalScriptContext.Push(datatype);
ScriptContext.GlobalScriptContext.Push(paramindex);
ScriptContext.GlobalScriptContext.SetIdentifier(0x5F5ABDD5);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (T)ScriptContext.GlobalScriptContext.GetResult(typeof(T));
}
}
public static void DynamicHookSetParam<T>(IntPtr hook, int datatype, int paramindex, T value){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(hook);
ScriptContext.GlobalScriptContext.Push(datatype);
ScriptContext.GlobalScriptContext.Push(paramindex);
ScriptContext.GlobalScriptContext.Push(value);
ScriptContext.GlobalScriptContext.SetIdentifier(0xA96CFBC1);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static string GetMapName(){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
@@ -405,6 +479,16 @@ namespace CounterStrikeSharp.API.Core
}
}
public static void QueueTaskForNextWorldUpdate(IntPtr callback){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(callback);
ScriptContext.GlobalScriptContext.SetIdentifier(0xAD51A0C9);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static IntPtr GetValveInterface(int interfacetype, string interfacename){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
@@ -484,6 +568,49 @@ namespace CounterStrikeSharp.API.Core
}
}
public static uint GetRefFromEntityPointer(IntPtr entitypointer){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(entitypointer);
ScriptContext.GlobalScriptContext.SetIdentifier(0xAF13DA94);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (uint)ScriptContext.GlobalScriptContext.GetResult(typeof(uint));
}
}
public static IntPtr GetEntityPointerFromRef(uint entityref){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(entityref);
ScriptContext.GlobalScriptContext.SetIdentifier(0xDBC17174);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (IntPtr)ScriptContext.GlobalScriptContext.GetResult(typeof(IntPtr));
}
}
public static IntPtr GetConcreteEntityListPointer(){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.SetIdentifier(0x5756DB36);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (IntPtr)ScriptContext.GlobalScriptContext.GetResult(typeof(IntPtr));
}
}
public static bool IsRefValidEntity(uint entityref){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(entityref);
ScriptContext.GlobalScriptContext.SetIdentifier(0x6E38A1FC);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (bool)ScriptContext.GlobalScriptContext.GetResult(typeof(bool));
}
}
public static void PrintToConsole(int index, string message){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
@@ -505,6 +632,28 @@ namespace CounterStrikeSharp.API.Core
}
}
public static ulong GetPlayerAuthorizedSteamid(int slot){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(slot);
ScriptContext.GlobalScriptContext.SetIdentifier(0xD1F30B3B);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (ulong)ScriptContext.GlobalScriptContext.GetResult(typeof(ulong));
}
}
public static string GetPlayerIpAddress(int slot){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(slot);
ScriptContext.GlobalScriptContext.SetIdentifier(0x46A45CB0);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (string)ScriptContext.GlobalScriptContext.GetResult(typeof(string));
}
}
public static void HookEvent(string name, InputArgument callback, bool ispost){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
@@ -803,6 +952,30 @@ namespace CounterStrikeSharp.API.Core
}
}
public static void HookFunction(IntPtr function, InputArgument hook, bool post){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(function);
ScriptContext.GlobalScriptContext.Push((InputArgument)hook);
ScriptContext.GlobalScriptContext.Push(post);
ScriptContext.GlobalScriptContext.SetIdentifier(0xA6C8BA9B);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static void UnhookFunction(IntPtr function, InputArgument hook, bool post){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(function);
ScriptContext.GlobalScriptContext.Push((InputArgument)hook);
ScriptContext.GlobalScriptContext.Push(post);
ScriptContext.GlobalScriptContext.SetIdentifier(0x2051B00);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static T ExecuteVirtualFunction<T>(IntPtr function, object[] arguments){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
@@ -893,6 +1066,17 @@ namespace CounterStrikeSharp.API.Core
}
}
public static int GetSchemaClassSize(string classname){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(classname);
ScriptContext.GlobalScriptContext.SetIdentifier(0x9CE4FC56);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (int)ScriptContext.GlobalScriptContext.GetResult(typeof(int));
}
}
public static IntPtr GetEconItemSystem(){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
@@ -903,6 +1087,16 @@ namespace CounterStrikeSharp.API.Core
}
}
public static bool IsServerPaused(){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.SetIdentifier(0xB216AAAC);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (bool)ScriptContext.GlobalScriptContext.GetResult(typeof(bool));
}
}
public static IntPtr CreateTimer(float interval, InputArgument callback, int flags){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();

View File

@@ -0,0 +1,254 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System.Linq;
using System.Text;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
{
public sealed class Application
{
private static Application _instance = null!;
public ILogger Logger { get; }
public static Application Instance => _instance!;
public static string RootDirectory => Instance._scriptHostConfiguration.RootPath;
private readonly IScriptHostConfiguration _scriptHostConfiguration;
private readonly GameDataProvider _gameDataProvider;
private readonly CoreConfig _coreConfig;
private readonly IPluginManager _pluginManager;
private readonly IPluginContextQueryHandler _pluginContextQueryHandler;
public Application(ILoggerFactory loggerFactory, IScriptHostConfiguration scriptHostConfiguration,
GameDataProvider gameDataProvider, CoreConfig coreConfig, IPluginManager pluginManager,
IPluginContextQueryHandler pluginContextQueryHandler)
{
Logger = loggerFactory.CreateLogger("Core");
_scriptHostConfiguration = scriptHostConfiguration;
_gameDataProvider = gameDataProvider;
_coreConfig = coreConfig;
_pluginManager = pluginManager;
_pluginContextQueryHandler = pluginContextQueryHandler;
_instance = this;
}
public void Start()
{
Logger.LogInformation("CounterStrikeSharp is starting up...");
_coreConfig.Load();
_gameDataProvider.Load();
var adminGroupsPath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admin_groups.json");
Logger.LogInformation("Loading Admin Groups from {Path}", adminGroupsPath);
AdminManager.LoadAdminGroups(adminGroupsPath);
var adminPath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admins.json");
Logger.LogInformation("Loading Admins from {Path}", adminPath);
AdminManager.LoadAdminData(adminPath);
var overridePath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admin_overrides.json");
Logger.LogInformation("Loading Admin Command Overrides from {Path}", overridePath);
AdminManager.LoadCommandOverrides(overridePath);
AdminManager.MergeGroupPermsIntoAdmins();
_pluginManager.Load();
for (var i = 1; i <= 9; i++)
{
CommandUtils.AddStandaloneCommand("css_" + i, "Command Key Handler", (player, info) =>
{
if (player == null) return;
var key = Convert.ToInt32(info.GetArg(0).Split("_")[1]);
ChatMenus.OnKeyPress(player, key);
});
}
RegisterPluginCommands();
}
[RequiresPermissions("@css/generic")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnCSSCommand(CCSPlayerController? caller, CommandInfo info)
{
var currentVersion = Api.GetVersion();
info.ReplyToCommand(
" CounterStrikeSharp was created and is maintained by Michael \"roflmuffin\" Wilson.\n" +
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison and CS2Fixes.\n" +
" See ACKNOWLEDGEMENTS.md for more information.\n" +
" Current API Version: " + currentVersion, true);
return;
}
[RequiresPermissions("@css/generic")]
[CommandHelper(minArgs: 1,
usage: "[option]\n" +
" list - List all plugins currently loaded.\n" +
" start / load - Loads a plugin not currently loaded.\n" +
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded.",
whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnCSSPluginCommand(CCSPlayerController? caller, CommandInfo info)
{
switch (info.GetArg(1))
{
case "list":
{
info.ReplyToCommand(
$" List of all plugins currently loaded by CounterStrikeSharp: {_pluginManager.GetLoadedPlugins().Count()} plugins loaded.",
true);
foreach (var plugin in _pluginManager.GetLoadedPlugins())
{
var sb = new StringBuilder();
sb.AppendFormat(" [#{0}:{1}]: \"{2}\" ({3})", plugin.PluginId,
plugin.State.ToString().ToUpper(), plugin.Plugin.ModuleName,
plugin.Plugin.ModuleVersion);
if (!string.IsNullOrEmpty(plugin.Plugin.ModuleAuthor))
sb.AppendFormat(" by {0}", plugin.Plugin.ModuleAuthor);
if (!string.IsNullOrEmpty(plugin.Plugin.ModuleDescription))
{
sb.Append("\n");
sb.Append(" ");
sb.Append(plugin.Plugin.ModuleDescription);
}
info.ReplyToCommand(sb.ToString(), true);
}
break;
}
case "start":
case "load":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand(
"Valid usage: css_plugins start/load [relative plugin path || absolute plugin path] (e.g \"TestPlugin\", \"plugins/TestPlugin/TestPlugin.dll\")\n",
true);
break;
}
// If our arugment doesn't end in ".dll" - try and construct a path similar to PluginName/PluginName.dll.
// We'll assume we have a full path if we have ".dll".
var path = info.GetArg(2);
if (!path.EndsWith(".dll"))
{
path = Path.Combine(_scriptHostConfiguration.RootPath, $"plugins/{path}/{path}.dll");
}
else
{
path = Path.Combine(_scriptHostConfiguration.RootPath, path);
}
var plugin = _pluginContextQueryHandler.FindPluginByModulePath(path);
if (plugin == null)
{
try
{
_pluginManager.LoadPlugin(path);
} catch (Exception e)
{
info.ReplyToCommand($"Could not load plugin \"{path}\")", true);
}
}
else
{
plugin.Load(false);
}
break;
}
case "stop":
case "unload":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand(
"Valid usage: css_plugins stop/unload [plugin name || #plugin id] (e.g \"TestPlugin\", \"1\")\n",
true);
break;
}
var pluginIdentifier = info.GetArg(2);
IPluginContext? plugin = _pluginContextQueryHandler.FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not unload plugin \"{pluginIdentifier}\")", true);
break;
}
plugin.Unload(false);
break;
}
case "restart":
case "reload":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand(
"Valid usage: css_plugins restart/reload [plugin name || #plugin id] (e.g \"TestPlugin\", \"#1\")\n",
true);
break;
}
var pluginIdentifier = info.GetArg(2);
var plugin = _pluginContextQueryHandler.FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not reload plugin \"{pluginIdentifier}\")", true);
break;
}
plugin.Unload(true);
plugin.Load(true);
break;
}
default:
info.ReplyToCommand("Valid usage: css_plugins [option]\n" +
" list - List all plugins currently loaded.\n" +
" start / load - Loads a plugin not currently loaded.\n" +
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded."
, true);
break;
}
}
private void RegisterPluginCommands()
{
CommandUtils.AddStandaloneCommand("css", "Counter-Strike Sharp options.", OnCSSCommand);
CommandUtils.AddStandaloneCommand("css_plugins", "Counter-Strike Sharp plugin options.",
OnCSSPluginCommand);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,355 +0,0 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using CounterStrikeSharp.API.Core.Logging;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace CounterStrikeSharp.API.Core
{
public sealed class GlobalContext
{
private static GlobalContext _instance = null;
public ILogger Logger { get; }
public static GlobalContext Instance => _instance;
public static string RootDirectory => _instance.rootDir.FullName;
private DirectoryInfo rootDir;
private readonly List<PluginContext> _loadedPlugins = new();
public GlobalContext()
{
rootDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent;
_instance = this;
Logger = CoreLogging.Factory.CreateLogger("Core");
Logger.LogInformation("CounterStrikeSharp is starting up...");
}
~GlobalContext()
{
foreach (var plugin in _loadedPlugins)
{
plugin.Unload();
}
}
public void OnNativeUnload()
{
foreach (var plugin in _loadedPlugins)
{
plugin.Unload();
}
}
public void InitGlobalContext()
{
var coreConfigPath = Path.Combine(rootDir.FullName, "configs", "core.json");
Logger.LogInformation("Loading CoreConfig from {Path}", coreConfigPath);
CoreConfig.Load(coreConfigPath);
var gameDataPath = Path.Combine(rootDir.FullName, "gamedata", "gamedata.json");
Logger.LogInformation("Loading GameData from {Path}", gameDataPath);
GameData.Load(gameDataPath);
var adminGroupsPath = Path.Combine(rootDir.FullName, "configs", "admin_groups.json");
Logger.LogInformation("Loading Admin Groups from {Path}", adminGroupsPath);
AdminManager.LoadAdminGroups(adminGroupsPath);
var adminPath = Path.Combine(rootDir.FullName, "configs", "admins.json");
Logger.LogInformation("Loading Admins from {Path}", adminPath);
AdminManager.LoadAdminData(adminPath);
var overridePath = Path.Combine(rootDir.FullName, "configs", "admin_overrides.json");
Logger.LogInformation("Loading Admin Command Overrides from {Path}", overridePath);
AdminManager.LoadCommandOverrides(overridePath);
AdminManager.MergeGroupPermsIntoAdmins();
for (var i = 1; i <= 9; i++)
{
CommandUtils.AddStandaloneCommand("css_" + i, "Command Key Handler", (player, info) =>
{
if (player == null) return;
var key = Convert.ToInt32(info.GetArg(0).Split("_")[1]);
ChatMenus.OnKeyPress(player, key);
});
}
Logger.LogInformation("Loading C# plugins...");
var pluginCount = LoadAllPlugins();
Logger.LogInformation("All managed modules were loaded. {PluginCount} plugins loaded.", pluginCount);
RegisterPluginCommands();
}
private void LoadPlugin(string path)
{
var existingPlugin = FindPluginByModulePath(path);
if (existingPlugin != null)
{
throw new FileLoadException("Plugin is already loaded.");
}
var plugin = new PluginContext(path, _loadedPlugins.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1);
plugin.Load();
_loadedPlugins.Add(plugin);
}
private int LoadAllPlugins()
{
DirectoryInfo modulesDirectoryInfo;
try
{
modulesDirectoryInfo = new DirectoryInfo(Path.Combine(rootDir.FullName, "plugins"));
}
catch (Exception e)
{
Logger.LogError(e, "Error finding plugin path");
return 0;
}
DirectoryInfo[] properModulesDirectories;
try
{
properModulesDirectories = modulesDirectoryInfo.GetDirectories();
}
catch
{
properModulesDirectories = Array.Empty<DirectoryInfo>();
}
var filePaths = properModulesDirectories
.Where(d => d.GetFiles().Any((f) => f.Name == d.Name + ".dll"))
.Select(d => d.GetFiles().First((f) => f.Name == d.Name + ".dll").FullName)
.ToArray();
foreach (var path in filePaths)
{
try
{
LoadPlugin(path);
}
catch (Exception e)
{
Logger.LogError(e, "Failed to load plugin from {Path}", path);
}
}
return _loadedPlugins.Count;
}
public void UnloadAllPlugins()
{
foreach (var plugin in _loadedPlugins)
{
plugin.Unload();
_loadedPlugins.Remove(plugin);
}
}
private PluginContext? FindPluginByType(Type moduleClass)
{
return _loadedPlugins.FirstOrDefault(x => x.PluginType == moduleClass);
}
private PluginContext? FindPluginById(int id)
{
return _loadedPlugins.FirstOrDefault(x => x.PluginId == id);
}
private PluginContext? FindPluginByModuleName(string name)
{
return _loadedPlugins.FirstOrDefault(x => x.Name == name);
}
private PluginContext? FindPluginByModulePath(string path)
{
return _loadedPlugins.FirstOrDefault(x => x.PluginPath == path);
}
private PluginContext? FindPluginByIdOrName(string query)
{
PluginContext? plugin = null;
if (Int32.TryParse(query, out var pluginNumber))
{
plugin = FindPluginById(pluginNumber);
if (plugin != null) return plugin;
}
plugin = FindPluginByModuleName(query);
return plugin;
}
[RequiresPermissions("@css/generic")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnCSSCommand(CCSPlayerController? caller, CommandInfo info)
{
var currentVersion = Api.GetVersion();
info.ReplyToCommand(" CounterStrikeSharp was created and is maintained by Michael \"roflmuffin\" Wilson.\n" +
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison and CS2Fixes.\n" +
" See ACKNOWLEDGEMENTS.md for more information.\n" +
" Current API Version: " + currentVersion, true);
return;
}
[RequiresPermissions("@css/generic")]
[CommandHelper(minArgs: 1,
usage: "[option]\n" +
" list - List all plugins currently loaded.\n" +
" start / load - Loads a plugin not currently loaded.\n" +
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded.",
whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnCSSPluginCommand(CCSPlayerController? caller, CommandInfo info)
{
switch (info.GetArg(1))
{
case "list":
{
info.ReplyToCommand($" List of all plugins currently loaded by CounterStrikeSharp: {_loadedPlugins.Count} plugins loaded.", true);
foreach (var plugin in _loadedPlugins)
{
var sb = new StringBuilder();
sb.AppendFormat(" [#{0}]: \"{1}\" ({2})", plugin.PluginId, plugin.Name, plugin.Version);
if (!string.IsNullOrEmpty(plugin.Author)) sb.AppendFormat(" by {0}", plugin.Author);
if (!string.IsNullOrEmpty(plugin.Description))
{
sb.Append("\n");
sb.Append(" ");
sb.Append(plugin.Description);
}
info.ReplyToCommand(sb.ToString(), true);
}
break;
}
case "start":
case "load":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand("Valid usage: css_plugins start/load [relative plugin path || absolute plugin path] (e.g \"TestPlugin\", \"plugins/TestPlugin/TestPlugin.dll\")\n", true);
break;
}
// If our arugment doesn't end in ".dll" - try and construct a path similar to PluginName/PluginName.dll.
// We'll assume we have a full path if we have ".dll".
var path = info.GetArg(2);
if (!path.EndsWith(".dll"))
{
path = Path.Combine(rootDir.FullName, $"plugins/{path}/{path}.dll");
}
else
{
path = Path.Combine(rootDir.FullName, path);
}
try
{
LoadPlugin(path);
}
catch (Exception e)
{
Logger.LogError(e, "Failed to load plugin from {Path}", path);
}
break;
}
case "stop":
case "unload":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand("Valid usage: css_plugins stop/unload [plugin name || #plugin id] (e.g \"TestPlugin\", \"1\")\n", true);
break;
}
var pluginIdentifier = info.GetArg(2);
PluginContext? plugin = FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not unload plugin \"{pluginIdentifier}\")", true);
break;
}
plugin.Unload();
_loadedPlugins.Remove(plugin);
break;
}
case "restart":
case "reload":
{
if (info.ArgCount < 2)
{
info.ReplyToCommand("Valid usage: css_plugins restart/reload [plugin name || #plugin id] (e.g \"TestPlugin\", \"#1\")\n", true);
break;
}
var pluginIdentifier = info.GetArg(2);
var plugin = FindPluginByIdOrName(pluginIdentifier);
if (plugin == null)
{
info.ReplyToCommand($"Could not reload plugin \"{pluginIdentifier}\")", true);
break;
}
plugin.Unload(true);
plugin.Load(true);
break;
}
default:
info.ReplyToCommand("Valid usage: css_plugins [option]\n" +
" list - List all plugins currently loaded.\n" +
" start / load - Loads a plugin not currently loaded.\n" +
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded."
, true);
break;
}
}
private void RegisterPluginCommands()
{
CommandUtils.AddStandaloneCommand("css", "Counter-Strike Sharp options.", OnCSSCommand);
CommandUtils.AddStandaloneCommand("css_plugins", "Counter-Strike Sharp plugin options.", OnCSSPluginCommand);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,10 @@ namespace CounterStrikeSharp.API.Core
{
T Config { get; set; }
/// <summary>
/// Called when the `ConfigManager` has parsed the configuration file for this plugin
/// </summary>
/// <param name="config">Parsed config instance</param>
public void OnConfigParsed(T config);
}
}

View File

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

View File

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

View File

@@ -125,6 +125,18 @@ namespace CounterStrikeSharp.API.Core
{
return new InputArgument(value);
}
[SecurityCritical]
public static implicit operator InputArgument(NativeObject value)
{
return new InputArgument(value.Handle);
}
[SecurityCritical]
public static implicit operator InputArgument(NativeEntity value)
{
return new InputArgument(value.Handle);
}
[SecurityCritical]
public static unsafe implicit operator InputArgument(void* value)

View File

@@ -126,5 +126,26 @@ namespace CounterStrikeSharp.API.Core
/// <param name="hostname">New hostname of the server</param>
[ListenerName("OnHostNameChanged")]
public delegate void OnHostNameChanged(string hostname);
/// <summary>
/// Called before the server enters fatal shutdown.
/// </summary>
[ListenerName("OnPreFatalShutdown")]
public delegate void OnServerPreFatalShutdown();
/// <summary>
/// Called when the server is in a loading stage.
/// </summary>
/// <param name="frameTime"></param>
[ListenerName("OnUpdateWhenNotInGame")]
public delegate void OnUpdateWhenNotInGame(float frameTime);
/// <summary>
/// Called before the world updates.
/// This seems to be called even when the server is hibernating.
/// </summary>
/// <param name="simulating"><see langword="true"/> if simulating, <see langword="false"/> otherwise</param>
[ListenerName("OnServerPreWorldUpdate")]
public delegate void OnServerPreWorldUpdate(bool simulating);
}
}

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,11 @@ namespace CounterStrikeSharp.API.Core;
public partial class CBasePlayerPawn
{
/// <summary>
/// Force player suicide
/// </summary>
/// <param name="explode"></param>
/// <param name="force"></param>
public void CommitSuicide(bool explode, bool force)
{
VirtualFunction.CreateVoid<IntPtr, bool, bool>(Handle, GameData.GetOffset("CBasePlayerPawn_CommitSuicide"))(Handle, explode, force);

View File

@@ -1,4 +1,8 @@
using System;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
@@ -10,8 +14,7 @@ public partial class CCSPlayerController
{
get
{
if (EntityIndex == null) return null;
return NativeAPI.GetUseridFromIndex((int)this.EntityIndex.Value.Value);
return NativeAPI.GetUseridFromIndex((int)this.Index);
}
}
@@ -24,9 +27,20 @@ public partial class CCSPlayerController
return VirtualFunctions.GiveNamedItem(PlayerPawn.Value.ItemServices.Handle, item, 0, 0, 0, 0);
}
public IntPtr GiveNamedItem(CsItem item)
{
string? itemString = EnumUtils.GetEnumMemberAttributeValue(item);
if (string.IsNullOrWhiteSpace(itemString))
{
return IntPtr.Zero;
}
return this.GiveNamedItem(itemString);
}
public void PrintToConsole(string message)
{
NativeAPI.PrintToConsole((int)EntityIndex.Value.Value, $"{message}\n\0");
NativeAPI.PrintToConsole((int)Index, $"{message}\n\0");
}
public void PrintToChat(string message)
@@ -38,13 +52,15 @@ public partial class CCSPlayerController
{
VirtualFunctions.ClientPrint(this.Handle, HudDestination.Center, message, 0, 0, 0, 0);
}
public void PrintToCenterHtml(string message) => PrintToCenterHtml(message, 5);
public void PrintToCenterHtml(string message)
public void PrintToCenterHtml(string message, int duration)
{
var @event = new EventShowSurvivalRespawnStatus(true)
{
LocToken = message,
Duration = 5,
Duration = duration,
Userid = this
};
@event.FireEventToClient(this);
@@ -79,6 +95,31 @@ public partial class CCSPlayerController
itemServices.RemoveWeapons();
}
/// <summary>
/// Force player suicide
/// </summary>
/// <param name="explode"></param>
/// <param name="force"></param>
public void CommitSuicide(bool explode, bool force)
{
if (!PlayerPawn.IsValid) return;
if (!PlayerPawn.Value.IsValid) return;
PlayerPawn.Value.CommitSuicide(explode, force);
}
/// <summary>
/// Respawn player
/// </summary>
public void Respawn()
{
if (!PlayerPawn.IsValid) return;
if (!PlayerPawn.Value.IsValid) return;
VirtualFunctions.CCSPlayerPawn_Respawn(PlayerPawn.Value.Handle);
VirtualFunction.CreateVoid<IntPtr>(Handle, GameData.GetOffset("CCSPlayerController_Respawn"))(Handle);
}
public bool IsBot => ((PlayerFlags)Flags).HasFlag(PlayerFlags.FL_FAKECLIENT);
/// <summary>
@@ -103,8 +144,94 @@ public partial class CCSPlayerController
team);
}
/// <summary>
/// Get a ConVar value for given player
/// </summary>
/// <param name="conVar">Name of the convar to retrieve</param>
/// <returns>ConVar string value</returns>
public string GetConVarValue(string conVar)
{
return NativeAPI.GetClientConvarValue(this.Slot, conVar);
}
public string GetConVarValue(ConVar? conVar)
{
if (conVar == null)
{
throw new Exception("Invalid convar passed to 'GetConVarValue'");
}
return GetConVarValue(conVar.Name);
}
/// <summary>
/// Sets a ConVar value on a fake client (bot).
/// </summary>
/// <param name="conVar">Console variable name</param>
/// <param name="value">String value to set</param>
/// <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)");
}
NativeAPI.SetFakeClientConvarValue(this.Slot, conVar, value);
}
/// <summary>
/// <inheritdoc cref="SetFakeClientConVar(string,string)"/>
/// </summary>
/// <exception cref="ArgumentException"><paramref name="conVar"/> is <see langword="null"/></exception>
/// <inheritdoc cref="SetFakeClientConVar(string,string)" select="exception"/>
public void SetFakeClientConVar(ConVar conVar, string value)
{
if (conVar == null)
{
throw new ArgumentException("Invalid convar passed to 'SetFakeClientConVar'");
}
SetFakeClientConVar(conVar.Name, value);
}
/// <summary>
/// Gets the active pawns button state. Will work even if the player is dead or observing.
/// </summary>
public PlayerButtons Buttons => (PlayerButtons)Pawn.Value.MovementServices!.Buttons.ButtonStates[0];
public void ExecuteClientCommand(string command) => NativeAPI.IssueClientCommand(Slot, command);
public int Slot => (int)Index - 1;
/// <summary>
/// Returns the authorized SteamID of this user which has been validated with the SteamAPI.
/// </summary>
public SteamID? AuthorizedSteamID
{
get
{
if (!this.IsValid) return null;
var authorizedSteamId = NativeAPI.GetPlayerAuthorizedSteamid(this.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>
public string? IpAddress
{
get
{
if (!this.IsValid) return null;
var ipAddress = NativeAPI.GetPlayerIpAddress(this.Slot);
if (string.IsNullOrWhiteSpace(ipAddress)) return null;
return ipAddress;
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using CounterStrikeSharp.API.Modules.Memory;
namespace CounterStrikeSharp.API.Core;
public partial class CCSPlayerPawn
{
/// <summary>
/// Respawn player
/// </summary>
public void Respawn()
{
if (!Controller.IsValid) return;
if (!Controller.Value.IsValid) return;
VirtualFunctions.CCSPlayerPawn_Respawn(Handle);
VirtualFunction.CreateVoid<IntPtr>(Controller.Value.Handle, GameData.GetOffset("CCSPlayerController_Respawn"))(Controller.Value.Handle);
}
}

View File

@@ -12,23 +12,36 @@ namespace CounterStrikeSharp.API.Core;
public partial class CEntityInstance : IEquatable<CEntityInstance>
{
public bool IsValid => Handle != IntPtr.Zero;
public CEntityInstance(IntPtr pointer) : base(pointer)
{
}
public CEntityIndex? EntityIndex => IsValid ? Entity?.EntityHandle.Index : null;
public CEntityInstance(uint rawHandle) : base(rawHandle)
{
}
/// <summary>
/// Checks that the entity handle is valid and the handle points to a valid entity
/// </summary>
public bool IsValid => EntityHandle.IsValid && Handle != IntPtr.Zero;
[Obsolete("Use Index instead", true)]
public CEntityIndex? EntityIndex => new CEntityIndex(EntityHandle.Index);
public uint Index => EntityHandle.Index;
public string DesignerName => IsValid ? Entity?.DesignerName : null;
public void Remove() => VirtualFunctions.UTIL_Remove(this.Handle);
public bool Equals(CEntityInstance? other)
{
return this.Handle == other?.Handle;
return this.EntityHandle.Equals(other?.EntityHandle);
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is CEntityInstance other && Equals(other);
return obj is CEntityInstance other && Equals(other);
}
public override int GetHashCode()
@@ -50,5 +63,5 @@ public partial class CEntityInstance : IEquatable<CEntityInstance>
public partial class CEntityIdentity
{
public unsafe CEntityInstance EntityInstance => new(Unsafe.Read<IntPtr>((void*)Handle));
public unsafe CHandle<CEntityInstance> EntityHandle => new(Handle + 0x10);
public unsafe CHandle<CEntityInstance> EntityHandle => new(this.Handle + 0x10);
}

View File

@@ -0,0 +1,8 @@
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Core;
public partial class CPlayerPawnComponent
{
public PointerTo<CBasePlayerPawn> Pawn => new PointerTo<CBasePlayerPawn>(this.Handle + 0x30);
}

View File

@@ -9420,10 +9420,8 @@ public partial class CEntityIdentity : NativeObject
}
public partial class CEntityInstance : NativeObject
public partial class CEntityInstance : NativeEntity
{
public CEntityInstance (IntPtr pointer) : base(pointer) {}
// m_iszPrivateVScripts
public string PrivateVScripts
{
@@ -19806,6 +19804,63 @@ public partial class CTablet : CCSWeaponBase
}
public partial class CTakeDamageInfo : NativeObject
{
public CTakeDamageInfo (IntPtr pointer) : base(pointer) {}
// m_vecDamageForce
public Vector DamageForce => Schema.GetDeclaredClass<Vector>(this.Handle, "CTakeDamageInfo", "m_vecDamageForce");
// m_vecDamagePosition
public Vector DamagePosition => Schema.GetDeclaredClass<Vector>(this.Handle, "CTakeDamageInfo", "m_vecDamagePosition");
// m_vecReportedPosition
public Vector ReportedPosition => Schema.GetDeclaredClass<Vector>(this.Handle, "CTakeDamageInfo", "m_vecReportedPosition");
// m_vecDamageDirection
public Vector DamageDirection => Schema.GetDeclaredClass<Vector>(this.Handle, "CTakeDamageInfo", "m_vecDamageDirection");
// m_hInflictor
public CHandle<CBaseEntity> Inflictor => Schema.GetDeclaredClass<CHandle<CBaseEntity>>(this.Handle, "CTakeDamageInfo", "m_hInflictor");
// m_hAttacker
public CHandle<CBaseEntity> Attacker => Schema.GetDeclaredClass<CHandle<CBaseEntity>>(this.Handle, "CTakeDamageInfo", "m_hAttacker");
// m_hAbility
public CHandle<CBaseEntity> Ability => Schema.GetDeclaredClass<CHandle<CBaseEntity>>(this.Handle, "CTakeDamageInfo", "m_hAbility");
// m_flDamage
public ref float Damage => ref Schema.GetRef<float>(this.Handle, "CTakeDamageInfo", "m_flDamage");
// m_bitsDamageType
public ref Int32 BitsDamageType => ref Schema.GetRef<Int32>(this.Handle, "CTakeDamageInfo", "m_bitsDamageType");
// m_iDamageCustom
public ref Int32 DamageCustom => ref Schema.GetRef<Int32>(this.Handle, "CTakeDamageInfo", "m_iDamageCustom");
// m_iAmmoType
public ref byte AmmoType => ref Schema.GetRef<byte>(this.Handle, "CTakeDamageInfo", "m_iAmmoType");
// m_flOriginalDamage
public ref float OriginalDamage => ref Schema.GetRef<float>(this.Handle, "CTakeDamageInfo", "m_flOriginalDamage");
// m_bShouldBleed
public ref bool ShouldBleed => ref Schema.GetRef<bool>(this.Handle, "CTakeDamageInfo", "m_bShouldBleed");
// m_bShouldSpark
public ref bool ShouldSpark => ref Schema.GetRef<bool>(this.Handle, "CTakeDamageInfo", "m_bShouldSpark");
// m_nDamageFlags
public ref TakeDamageFlags_t DamageFlags => ref Schema.GetRef<TakeDamageFlags_t>(this.Handle, "CTakeDamageInfo", "m_nDamageFlags");
// m_nNumObjectsPenetrated
public ref Int32 NumObjectsPenetrated => ref Schema.GetRef<Int32>(this.Handle, "CTakeDamageInfo", "m_nNumObjectsPenetrated");
// m_bInTakeDamageFlow
public ref bool InTakeDamageFlow => ref Schema.GetRef<bool>(this.Handle, "CTakeDamageInfo", "m_bInTakeDamageFlow");
}
public partial class CTankTargetChange : CPointEntity
{
public CTankTargetChange (IntPtr pointer) : base(pointer) {}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using CounterStrikeSharp.API.Core.Hosting;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core.Plugin.Host;
public class PluginManager : IPluginManager
{
private readonly HashSet<PluginContext> _loadedPluginContexts = new();
private readonly IScriptHostConfiguration _scriptHostConfiguration;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<PluginManager> _logger;
public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ILogger<PluginManager> logger, IServiceProvider serviceProvider)
{
_scriptHostConfiguration = scriptHostConfiguration;
_logger = logger;
_serviceProvider = serviceProvider;
}
public void Load()
{
var pluginDirectories = Directory.GetDirectories(_scriptHostConfiguration.PluginPath);
var pluginAssemblyPaths = pluginDirectories
.Select(dir => Path.Combine(dir, Path.GetFileName(dir) + ".dll"))
.Where(File.Exists)
.ToArray();
foreach (var path in pluginAssemblyPaths)
{
try
{
LoadPlugin(path);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to load plugin from {Path}", path);
}
}
}
public IEnumerable<PluginContext> GetLoadedPlugins()
{
return _loadedPluginContexts;
}
public void LoadPlugin(string path)
{
var plugin = new PluginContext(_serviceProvider, _scriptHostConfiguration, path, _loadedPluginContexts.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1);
_loadedPluginContexts.Add(plugin);
plugin.Load();
}
}

View File

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

View File

@@ -0,0 +1,215 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace CounterStrikeSharp.API.Core.Plugin
{
public class PluginContext : IPluginContext
{
public PluginState State { get; set; } = PluginState.Unregistered;
public IPlugin Plugin { get; private set; }
private PluginLoader Loader { get; set; }
private IServiceProvider ServiceProvider { get; set; }
public int PluginId { get; }
private readonly IScriptHostConfiguration _hostConfiguration;
private readonly string _path;
private readonly FileSystemWatcher _fileWatcher;
private readonly IServiceProvider _applicationServiceProvider;
// TOOD: ServiceCollection
private ILogger _logger = CoreLogging.Factory.CreateLogger<PluginContext>();
public PluginContext(IServiceProvider applicationServiceProvider, IScriptHostConfiguration hostConfiguration, string path, int id)
{
_applicationServiceProvider = applicationServiceProvider;
_hostConfiguration = hostConfiguration;
_path = path;
PluginId = id;
Loader = PluginLoader.CreateFromAssemblyFile(path,
new[]
{
typeof(IPlugin), typeof(ILogger), typeof(IServiceCollection), typeof(IPluginServiceCollection<>)
}, config =>
{
config.EnableHotReload = true;
config.IsUnloadable = true;
});
if (CoreConfig.PluginHotReloadEnabled)
{
_fileWatcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(path)
};
_fileWatcher.Deleted += async (s, e) =>
{
Server.NextWorldUpdate(() =>
{
if (e.FullPath == path)
{
_logger.LogInformation("Plugin {Name} has been deleted, unloading...", Plugin.ModuleName);
Unload(true);
}
});
};
_fileWatcher.Filter = "*.dll";
_fileWatcher.EnableRaisingEvents = true;
Loader.Reloaded += async (s, e) => await OnReloadedAsync(s, e);
}
}
private Task OnReloadedAsync(object sender, PluginReloadedEventArgs eventargs)
{
Server.NextWorldUpdate(() =>
{
_logger.LogInformation("Reloading plugin {Name}", Plugin.ModuleName);
Loader = eventargs.Loader;
Unload(hotReload: true);
Load(hotReload: true);
});
return Task.CompletedTask;
}
public void Load(bool hotReload = false)
{
if (State == PluginState.Loaded) return;
using (Loader.EnterContextualReflection())
{
var defaultAssembly = Loader.LoadDefaultAssembly();
Type pluginType = defaultAssembly.GetTypes()
.FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t));
if (pluginType == null) throw new Exception("Unable to find plugin in assembly");
var serviceCollection = new ServiceCollection();
serviceCollection.Scan(scan =>
scan.FromAssemblies(defaultAssembly)
.AddClasses(c => c.AssignableTo<IPlugin>())
.AsSelf()
.WithSingletonLifetime()
);
serviceCollection.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddSerilog(new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With(new PluginNameEnricher(this))
.WriteTo.Console(
outputTemplate:
"{Timestamp:HH:mm:ss} [{Level:u4}] (plugin:{PluginName}) {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
Path.Join(new[]
{
_hostConfiguration.RootPath, "logs",
$"log-{pluginType.Assembly.GetName().Name}.txt"
}), rollingInterval: RollingInterval.Day,
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] plugin:{PluginName} {Message:lj}{NewLine}{Exception}")
.WriteTo.File(Path.Join(new[] { _hostConfiguration.RootPath, "logs", $"log-all.txt" }),
rollingInterval: RollingInterval.Day, shared: true,
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u4}] plugin:{PluginName} {Message:lj}{NewLine}{Exception}")
.CreateLogger());
});
Type interfaceType = typeof(IPluginServiceCollection<>).MakeGenericType(pluginType);
Type[] serviceCollectionConfiguratorTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => interfaceType.IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
.ToArray();
if (serviceCollectionConfiguratorTypes.Any())
{
foreach (var t in serviceCollectionConfiguratorTypes)
{
var pluginServiceCollection = Activator.CreateInstance(t);
MethodInfo method = t.GetMethod("ConfigureServices");
method?.Invoke(pluginServiceCollection, new object[] { serviceCollection });
}
}
serviceCollection.AddSingleton(this);
ServiceProvider = serviceCollection.BuildServiceProvider();
var minimumApiVersion = pluginType.GetCustomAttribute<MinimumApiVersion>()?.Version;
var currentVersion = Api.GetVersion();
// Ignore version 0 for local development
if (currentVersion > 0 && minimumApiVersion != null && minimumApiVersion > currentVersion)
throw new Exception(
$"Plugin \"{Path.GetFileName(_path)}\" requires a newer version of CounterStrikeSharp. The plugin expects version [{minimumApiVersion}] but the current version is [{currentVersion}].");
_logger.LogInformation("Loading plugin {Name}", pluginType.Assembly.GetName().Name);
Plugin = ServiceProvider.GetRequiredService(pluginType) as IPlugin;
if (Plugin == null) throw new Exception("Unable to create plugin instance");
State = PluginState.Loading;
Plugin.ModulePath = _path;
Plugin.RegisterAllAttributes(Plugin);
Plugin.Logger = ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(pluginType);
Plugin.InitializeConfig(Plugin, pluginType);
Plugin.Load(hotReload);
_logger.LogInformation("Finished loading plugin {Name}", Plugin.ModuleName);
State = PluginState.Loaded;
}
}
public void Unload(bool hotReload = false)
{
if (State == PluginState.Unloaded) return;
State = PluginState.Unloaded;
var cachedName = Plugin.ModuleName;
_logger.LogInformation("Unloading plugin {Name}", Plugin.ModuleName);
Plugin.Unload(hotReload);
Plugin.Dispose();
_logger.LogInformation("Finished unloading plugin {Name}", cachedName);
}
}
}

View File

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

View File

@@ -1,137 +0,0 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Logging;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
{
public class PluginContext
{
private BasePlugin _plugin;
private PluginLoader _assemblyLoader;
public string Name => _plugin?.ModuleName;
public string Version => _plugin?.ModuleVersion;
public string Description => _plugin.ModuleDescription;
public string Author => _plugin.ModuleAuthor;
public Type PluginType => _plugin?.GetType();
public string PluginPath => _plugin?.ModulePath;
public int PluginId { get; }
private readonly string _path;
private readonly FileSystemWatcher _fileWatcher;
// TOOD: ServiceCollection
private ILogger _logger = CoreLogging.Factory.CreateLogger<PluginContext>();
public PluginContext(string path, int id)
{
_path = path;
PluginId = id;
_assemblyLoader = PluginLoader.CreateFromAssemblyFile(path, new[] { typeof(IPlugin) }, config =>
{
config.EnableHotReload = true;
config.IsUnloadable = true;
});
_fileWatcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(path)
};
_fileWatcher.Deleted += async (s, e) =>
{
if (e.FullPath == path)
{
_logger.LogInformation("Plugin {Name} has been deleted, unloading...", Name);
Unload(true);
}
};
_fileWatcher.Filter = "*.dll";
_fileWatcher.EnableRaisingEvents = true;
_assemblyLoader.Reloaded += async (s, e) => await OnReloadedAsync(s, e);
}
private Task OnReloadedAsync(object sender, PluginReloadedEventArgs eventargs)
{
_logger.LogInformation("Reloading plugin {Name}", Name);
_assemblyLoader = eventargs.Loader;
Unload(hotReload: true);
Load(hotReload: true);
return Task.CompletedTask;
}
public void Load(bool hotReload = false)
{
using (_assemblyLoader.EnterContextualReflection())
{
Type pluginType = _assemblyLoader.LoadDefaultAssembly().GetTypes()
.FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t));
if (pluginType == null) throw new Exception("Unable to find plugin in DLL");
var minimumApiVersion = pluginType.GetCustomAttribute<MinimumApiVersion>()?.Version;
var currentVersion = Api.GetVersion();
// Ignore version 0 for local development
if (currentVersion > 0 && minimumApiVersion != null && minimumApiVersion > currentVersion)
throw new Exception(
$"Plugin \"{Path.GetFileName(_path)}\" requires a newer version of CounterStrikeSharp. The plugin expects version [{minimumApiVersion}] but the current version is [{currentVersion}].");
_logger.LogInformation("Loading plugin {Name}", pluginType.Name);
_plugin = (BasePlugin)Activator.CreateInstance(pluginType)!;
_plugin.ModulePath = _path;
_plugin.RegisterAllAttributes(_plugin);
_plugin.Logger = PluginLogging.CreatePluginLogger(this);
_plugin.InitializeConfig(_plugin, pluginType);
_plugin.Load(hotReload);
_logger.LogInformation("Finished loading plugin {Name}", Name);
}
}
public void Unload(bool hotReload = false)
{
var cachedName = Name;
_logger.LogInformation("Unloading plugin {Name}", Name);
_plugin.Unload(hotReload);
_plugin.Dispose();
if (!hotReload)
{
_assemblyLoader.Dispose();
_fileWatcher.Dispose();
}
_logger.LogInformation("Finished unloading plugin {Name}", Name);
}
}
}

View File

@@ -205,6 +205,16 @@ namespace CounterStrikeSharp.API.Core
return;
}
else if (arg is NativeObject nativeObject)
{
Push(context, (InputArgument)nativeObject);
return;
}
else if (arg is NativeEntity nativeValue)
{
Push(context, (InputArgument)nativeValue);
return;
}
if (Marshal.SizeOf(arg.GetType()) <= 8)
{

View File

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

View File

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

View File

@@ -94,7 +94,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of groups.
if (player == null) return true;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
var playerData = GetPlayerAdminData((SteamID)player.SteamID);
var playerData = GetPlayerAdminData((SteamID)player.AuthorizedSteamID);
return playerData?.Groups.IsSupersetOf(groups) ?? false;
}
@@ -119,7 +119,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return; }
AddPlayerToGroup((SteamID)player.SteamID, groups);
AddPlayerToGroup((SteamID)player.AuthorizedSteamID, groups);
}
/// <summary>
@@ -161,7 +161,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return; }
RemovePlayerFromGroup((SteamID)player.SteamID, true, groups);
RemovePlayerFromGroup((SteamID)player.AuthorizedSteamID, true, groups);
}
/// <summary>

View File

@@ -102,7 +102,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of permissions.
if (player == null) return true;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
var playerData = GetPlayerAdminData((SteamID)player.SteamID);
var playerData = GetPlayerAdminData(player.AuthorizedSteamID);
return playerData?.Flags.IsSupersetOf(flags) ?? false;
}
@@ -136,7 +136,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of permissions.
if (player == null) return true;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
var playerData = GetPlayerAdminData((SteamID)player.SteamID);
var playerData = GetPlayerAdminData((SteamID)player.AuthorizedSteamID);
return playerData?.CommandOverrides.ContainsKey(command) ?? false;
}
@@ -165,7 +165,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of permissions.
if (player == null) return true;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
var playerData = GetPlayerAdminData((SteamID)player.SteamID);
var playerData = GetPlayerAdminData((SteamID)player.AuthorizedSteamID);
return playerData?.CommandOverrides.GetValueOrDefault(command) ?? false;
}
@@ -193,7 +193,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of permissions.
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return; }
SetPlayerCommandOverride((SteamID)player.SteamID, command, state);
SetPlayerCommandOverride((SteamID)player.AuthorizedSteamID, command, state);
}
/// <summary>
@@ -235,7 +235,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
AddPlayerPermissions((SteamID)player.SteamID, flags);
AddPlayerPermissions((SteamID)player.AuthorizedSteamID, flags);
}
/// <summary>
@@ -278,7 +278,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
RemovePlayerPermissions((SteamID)player.SteamID, flags);
RemovePlayerPermissions((SteamID)player.AuthorizedSteamID, flags);
}
/// <summary>
@@ -306,7 +306,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
ClearPlayerPermissions((SteamID)player.SteamID);
ClearPlayerPermissions((SteamID)player.AuthorizedSteamID);
}
/// <summary>
@@ -335,7 +335,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
SetPlayerImmunity((SteamID)player.SteamID, value);
SetPlayerImmunity((SteamID)player.AuthorizedSteamID, value);
}
/// <summary>
@@ -366,10 +366,10 @@ namespace CounterStrikeSharp.API.Modules.Admin
if (target == null) return false;
if (!target.IsValid || target.Connected != PlayerConnectedState.PlayerConnected) return false;
var callerData = GetPlayerAdminData((SteamID)caller.SteamID);
var callerData = GetPlayerAdminData((SteamID)caller.AuthorizedSteamID);
if (callerData == null) return false;
var targetData = GetPlayerAdminData((SteamID)target.SteamID);
var targetData = GetPlayerAdminData((SteamID)target.AuthorizedSteamID);
if (targetData == null) return true;
return callerData.Immunity >= targetData.Immunity;

View File

@@ -31,7 +31,8 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
// If we have a command in the "command_overrides" section in "configs/admins.json",
// we skip the checks below and just return the defined value.
var adminData = AdminManager.GetPlayerAdminData((SteamID)caller.SteamID);
if (caller?.AuthorizedSteamID == null) return false;
var adminData = AdminManager.GetPlayerAdminData(caller.AuthorizedSteamID);
if (adminData == null) return false;
if (adminData.CommandOverrides.ContainsKey(Command))
{

View File

@@ -23,7 +23,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
var groupPermissions = Permissions.Where(perm => perm.StartsWith(PermissionCharacters.GroupPermissionChar));
var userPermissions = Permissions.Where(perm => perm.StartsWith(PermissionCharacters.UserPermissionChar));
var adminData = AdminManager.GetPlayerAdminData((SteamID)caller.SteamID);
var adminData = AdminManager.GetPlayerAdminData(caller.AuthorizedSteamID);
if (adminData == null) return false;
return (groupPermissions.Intersect(adminData.Groups).Count() + userPermissions.Intersect(adminData.Flags).Count()) > 0;
}

View File

@@ -0,0 +1,14 @@
using CounterStrikeSharp.API.Modules.Commands.Targeting;
namespace CounterStrikeSharp.API.Modules.Commands;
public static class CommandExtensions
{
/// <summary>
/// Treats the argument at the specified index as a target string (@all, @me etc.) and returns the result.
/// </summary>
public static TargetResult GetArgTargetResult(this CommandInfo commandInfo, int index)
{
return new Target(commandInfo.GetArg(index)).GetTarget(commandInfo.CallingPlayer);
}
}

View File

@@ -25,13 +25,14 @@ namespace CounterStrikeSharp.API.Modules.Commands
public delegate HookResult CommandListenerCallback(CCSPlayerController? player, CommandInfo commandInfo);
private CCSPlayerController _player;
public IntPtr Handle { get; private set; }
public CCSPlayerController? CallingPlayer { get; }
public IntPtr Handle { get; }
internal CommandInfo(IntPtr pointer, CCSPlayerController player)
{
Handle = pointer;
_player = player;
CallingPlayer = player;
}
public int ArgCount => NativeAPI.CommandGetArgCount(Handle);
@@ -42,12 +43,12 @@ namespace CounterStrikeSharp.API.Modules.Commands
public string ArgByIndex(int index) => NativeAPI.CommandGetArgByIndex(Handle, index);
public string GetArg(int index) => NativeAPI.CommandGetArgByIndex(Handle, index);
public void ReplyToCommand(string message, bool console = false) {
if (_player != null)
if (CallingPlayer != null)
{
if (console) { _player.PrintToConsole(message); }
else _player.PrintToChat(message);
if (console) { CallingPlayer.PrintToConsole(message); }
else CallingPlayer.PrintToChat(message);
}
else
{

View File

@@ -0,0 +1,126 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Modules.Commands.Targeting;
public class Target
{
private TargetType Type { get; }
private string Raw { get; }
private string Slug { get; }
private static readonly Dictionary<string, TargetType> TargetTypeMap = new(StringComparer.OrdinalIgnoreCase)
{
{ "@all", TargetType.GroupAll },
{ "@bots", TargetType.GroupBots },
{ "@human", TargetType.GroupHumans },
{ "@alive", TargetType.GroupAlive },
{ "@dead", TargetType.GroupDead },
{ "@!me", TargetType.GroupNotMe },
{ "@me", TargetType.PlayerMe },
{ "@ct", TargetType.TeamCt },
{ "@t", TargetType.TeamT },
{ "@spec", TargetType.TeamSpec }
};
private static bool ConstTargetType(string target, out TargetType targetType)
{
targetType = TargetType.Invalid;
if (!target.StartsWith("@"))
{
return false;
}
return TargetTypeMap.TryGetValue(target, out targetType);
}
private bool IdTargetType(string target,
out TargetType targetType,
[MaybeNullWhen(false)] out string slug)
{
targetType = TargetType.Invalid;
slug = null!;
if (!target.StartsWith("#"))
{
return false;
}
slug = target.TrimStart('#');
if (slug.StartsWith("STEAM")) targetType = TargetType.IdSteamEscaped;
else if (!ulong.TryParse(slug, out _)) targetType = TargetType.ExplicitName;
else if (slug.Length == 17) targetType = TargetType.IdSteam64;
else targetType = TargetType.IdUserid;
return true;
}
public Target(string target)
{
Raw = target.Trim();
if (ConstTargetType(Raw, out var targetType))
{
Type = targetType;
Slug = Raw;
}
else if (IdTargetType(Raw, out targetType, out var slug))
{
Type = targetType;
Slug = slug;
}
else
{
Type = TargetType.ImplicitName;
Slug = Raw;
}
}
private bool TargetPredicate(CCSPlayerController player, CCSPlayerController? caller)
{
switch (Type)
{
case TargetType.TeamCt:
return player.TeamNum == (byte)CsTeam.CounterTerrorist;
case TargetType.TeamT:
return player.TeamNum == (byte)CsTeam.Terrorist;
case TargetType.TeamSpec:
return player.TeamNum == (byte)CsTeam.Spectator;
case TargetType.GroupAll:
return true;
case TargetType.GroupBots:
return player.IsBot;
case TargetType.GroupHumans:
return !player.IsBot;
case TargetType.GroupAlive:
return player.PlayerPawn is { IsValid: true, Value.LifeState: (byte)LifeState_t.LIFE_ALIVE };
case TargetType.GroupDead:
return player.PlayerPawn is { IsValid: true, Value.LifeState: (byte)LifeState_t.LIFE_DEAD or (byte)LifeState_t.LIFE_DYING };
case TargetType.GroupNotMe:
return player.SteamID != caller?.SteamID;
case TargetType.PlayerMe:
return player.SteamID == caller?.SteamID;
case TargetType.IdUserid:
return player.UserId.ToString() == Slug;
case TargetType.IdSteamEscaped:
return ((SteamID)player.SteamID).SteamId2 == Slug;
case TargetType.IdSteam64:
return ((SteamID)player.SteamID).SteamId64.ToString() == Slug;
case TargetType.ExplicitName:
case TargetType.ImplicitName:
return player.PlayerName.Contains(Slug, StringComparison.OrdinalIgnoreCase);
default:
return false;
}
}
public TargetResult GetTarget(CCSPlayerController? caller)
{
var players = Utilities.GetPlayers().Where(player => TargetPredicate(player, caller)).ToList();
return new TargetResult() { Players = players };
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace CounterStrikeSharp.API.Modules.Commands.Targeting;
public class TargetResult : IEnumerable<CCSPlayerController>
{
public List<CCSPlayerController> Players { get; set; } = new();
public IEnumerator<CCSPlayerController> GetEnumerator()
{
return Players.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@@ -0,0 +1,25 @@
namespace CounterStrikeSharp.API.Modules.Commands.Targeting;
public enum TargetType
{
TeamCt, // @ct
TeamT, // @t
TeamSpec, // @spec
GroupAll, // @all
GroupBots, // @bots
GroupHumans, // @human
GroupAlive, // @alive
GroupDead, // @dead
GroupNotMe, // @!me
PlayerMe, // @me
IdUserid, // #4
IdSteamEscaped, // "#STEAM_0:1:8614"
IdSteam64, // #76561198116940237
ExplicitName, // #name
ImplicitName, // name
Invalid
}

View File

@@ -43,10 +43,11 @@ namespace CounterStrikeSharp.API.Modules.Config
{
string directoryPath = Path.Combine(_pluginConfigsFolderPath, pluginName);
string configPath = Path.Combine(directoryPath, $"{pluginName}.json");
string exampleConfigPath = Path.Combine(directoryPath, $"{pluginName}.example.json");
T config = (T)Activator.CreateInstance(typeof(T))!;
if (!File.Exists(configPath))
if (!File.Exists(configPath) && !File.Exists(exampleConfigPath))
{
try
{
@@ -56,8 +57,10 @@ namespace CounterStrikeSharp.API.Modules.Config
}
StringBuilder builder = new StringBuilder();
builder.Append($"// This configuration was automatically generated by CounterStrikeSharp for plugin '{pluginName}', at {DateTimeOffset.Now:yyyy/MM/dd hh:mm:ss}\n");
builder.Append(JsonSerializer.Serialize<T>(config, new JsonSerializerOptions { WriteIndented = true }));
builder.Append(
$"// This configuration was automatically generated by CounterStrikeSharp for plugin '{pluginName}', at {DateTimeOffset.Now:yyyy/MM/dd hh:mm:ss}\n");
builder.Append(JsonSerializer.Serialize<T>(config,
new JsonSerializerOptions { WriteIndented = true }));
File.WriteAllText(configPath, builder.ToString());
return config;
}
@@ -65,6 +68,17 @@ namespace CounterStrikeSharp.API.Modules.Config
{
_logger.LogError(ex, "Failed to generate configuration file for {PluginName}", pluginName);
}
} else if (File.Exists(exampleConfigPath) && !File.Exists(configPath))
{
try
{
_logger.LogInformation("Copying example configuration file for {PluginName}", pluginName);
File.Copy(exampleConfigPath, configPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to copy example configuration file for {PluginName}", pluginName);
}
}
try

View File

@@ -0,0 +1,241 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace CounterStrikeSharp.API.Modules.Entities.Constants
{
public enum CsItem
{
//-----------------------------------------
//EQUIPMENT
//-----------------------------------------
[EnumMember(Value = "item_kevlar")]
Kevlar = 000,
[EnumMember(Value = "item_assaultsuit")]
AssaultSuit = 001,
KevlarHelmet = AssaultSuit,
[EnumMember(Value = "weapon_taser")]
Taser = 002,
Zeus = Taser,
[EnumMember(Value = "weapon_snowball")]
Snowball = 003,
[EnumMember(Value = "weapon_shield")]
Shield = 004,
[EnumMember(Value = "weapon_c4")]
C4 = 005,
Bomb = C4,
[EnumMember(Value = "weapon_healthshot")]
Healthshot = 006,
[EnumMember(Value = "weapon_breachcharge")]
BreachCharge = 007,
[EnumMember(Value = "weapon_tablet")]
Tablet = 008,
[EnumMember(Value = "weapon_bumpmine")]
Bumpmine = 009,
//-----------------------------------------
//GRENADES
//-----------------------------------------
[EnumMember(Value = "weapon_smokegrenade")]
Smoke = 100,
SmokeGrenade = Smoke,
[EnumMember(Value = "weapon_flashbang")]
Flashbang = 101,
FlashbangGrenade = Flashbang,
[EnumMember(Value = "weapon_hegrenade")]
HighExplosive = 102,
HE = HighExplosive,
HighExplosiveGrenade = HighExplosive,
HEGrenade = HighExplosive,
[EnumMember(Value = "weapon_molotov")]
Molotov = 103,
[EnumMember(Value = "weapon_incgrenade")]
Incendiary = 104,
IncGrenade = Incendiary,
IncendiaryGrenade = Incendiary,
[EnumMember(Value = "weapon_decoy")]
Decoy = 105,
DecoyGrenade = Decoy,
//XRay-Grenade
[EnumMember(Value = "weapon_tagrenade")]
TacticalAwareness = 106,
TAGrenade = TacticalAwareness,
XRayGrenade = TacticalAwareness,
//Dangerzone: Better HighExplosive
[EnumMember(Value = "weapon_frag")]
Frag = 107,
FragGrenade = Frag,
//Dangerzone: Better Molotov
[EnumMember(Value = "weapon_firebomb")]
Firebomb = 108,
//Dangerzone: Decoy but Footsteps instead of gun sounds
[EnumMember(Value = "weapon_diversion")]
Diversion = 109,
//-----------------------------------------
//PISTOLS
//-----------------------------------------
[EnumMember(Value = "weapon_deagle")]
Deagle = 200,
DesertEagle = Deagle,
[EnumMember(Value = "weapon_glock")]
Glock = 201,
Glock18 = Glock,
[EnumMember(Value = "weapon_usp_silencer")]
USPS = 202,
USP = USPS,
[EnumMember(Value = "weapon_hkp2000")]
HKP2000 = 203,
P2000 = HKP2000,
P2K = HKP2000,
[EnumMember(Value = "weapon_elite")]
Elite = 204,
DualBerettas = Elite,
Dualies = Elite,
[EnumMember(Value = "weapon_tec9")]
Tec9 = 205,
[EnumMember(Value = "weapon_p250")]
P250 = 206,
[EnumMember(Value = "weapon_cz75a")]
CZ = 207,
CZ75 = CZ,
[EnumMember(Value = "weapon_fiveseven")]
FiveSeven = 208,
[EnumMember(Value = "weapon_revolver")]
Revolver = 209,
R8 = Revolver,
//-----------------------------------------
//MID-TIER
//-----------------------------------------
[EnumMember(Value = "weapon_mac10")]
Mac10 = 300,
[EnumMember(Value = "weapon_mp9")]
MP9 = 301,
[EnumMember(Value = "weapon_mp7")]
MP7 = 302,
[EnumMember(Value = "weapon_p90")]
P90 = 303,
[EnumMember(Value = "weapon_mp5sd")]
MP5SD = 304,
MP5 = MP5SD,
[EnumMember(Value = "weapon_bizon")]
Bizon = 305,
PPBizon = Bizon,
[EnumMember(Value = "weapon_ump45")]
UMP45 = 306,
UMP = UMP45,
[EnumMember(Value = "weapon_xm1014")]
XM1014 = 307,
[EnumMember(Value = "weapon_nova")]
Nova = 308,
[EnumMember(Value = "weapon_mag7")]
MAG7 = 309,
[EnumMember(Value = "weapon_sawedoff")]
SawedOff = 310,
[EnumMember(Value = "weapon_m249")]
M249 = 311,
[EnumMember(Value = "weapon_negev")]
Negev = 312,
//-----------------------------------------
//RIFLES
//-----------------------------------------
[EnumMember(Value = "weapon_ak47")]
AK47 = 400,
[EnumMember(Value = "weapon_m4a1_silencer")]
M4A1S = 401,
SilencedM4 = M4A1S,
[EnumMember(Value = "weapon_m4a1")]
M4A1 = 402,
M4A4 = M4A1,
[EnumMember(Value = "weapon_galilar")]
GalilAR = 403,
Galil = GalilAR,
[EnumMember(Value = "weapon_famas")]
Famas = 404,
[EnumMember(Value = "weapon_sg556")]
SG556 = 405,
SG553 = SG556,
Krieg = SG556,
[EnumMember(Value = "weapon_awp")]
AWP = 406,
[EnumMember(Value = "weapon_aug")]
AUG = 407,
[EnumMember(Value = "weapon_ssg08")]
SSG08 = 408,
Scout = SSG08,
[EnumMember(Value = "weapon_scar20")]
SCAR20 = 409,
AutoSniperCT = SCAR20,
[EnumMember(Value = "weapon_g3sg1")]
G3SG1 = 410,
AutoSniperT = G3SG1,
//-----------------------------------------
//KNIFE
//-----------------------------------------
[EnumMember(Value = "weapon_knife_t")]
DefaultKnifeT = 500,
KnifeT = DefaultKnifeT,
[EnumMember(Value = "weapon_knife")]
DefaultKnifeCT = 501,
KnifeCT = DefaultKnifeCT,
Knife = DefaultKnifeCT,
}
}

View File

@@ -0,0 +1,32 @@
namespace CounterStrikeSharp.API.Modules.Entities.Constants;
/// <summary>
/// Represents the instance types for a Steam account.
/// </summary>
public enum SteamAccountInstance
{
/// <summary>
/// Invalid instance.
/// </summary>
Invalid = -1,
/// <summary>
/// All instances.
/// </summary>
All = 0,
/// <summary>
/// Desktop instance.
/// </summary>
Desktop = 1,
/// <summary>
/// Console instance.
/// </summary>
Console = 2,
/// <summary>
/// Web instance.
/// </summary>
Web = 4
}

View File

@@ -0,0 +1,70 @@
using System.Runtime.Serialization;
namespace CounterStrikeSharp.API.Modules.Entities.Constants;
/// <summary>
/// Represents the types of Steam accounts.
/// </summary>
public enum SteamAccountType
{
/// <summary>
/// Invalid account type.
/// </summary>
[EnumMember(Value = "I")]
Invalid = 0,
/// <summary>
/// Individual account type.
/// </summary>
[EnumMember(Value = "U")]
Individual,
/// <summary>
/// MultiSeat account type.
/// </summary>
[EnumMember(Value = "M")]
MultiSeat,
/// <summary>
/// Game Server account type.
/// </summary>
[EnumMember(Value = "G")]
GameServer,
/// <summary>
/// Anonymous Game Server account type.
/// </summary>
[EnumMember(Value = "A")]
AnonGameServer,
/// <summary>
/// Pending account type.
/// </summary>
[EnumMember(Value = "P")]
Pending,
/// <summary>
/// Content Server account type.
/// </summary>
[EnumMember(Value = "C")]
ContentServer,
/// <summary>
/// Clan account type.
/// </summary>
[EnumMember(Value = "g")]
Clan,
/// <summary>
/// Chat account type.
/// </summary>
[EnumMember(Value = "T")]
Chat,
/// <summary>
/// Console user account type.
/// </summary>
[EnumMember(Value = "c")]
ConsoleUser,
/// <summary>
/// P2P Super Seeder account type.
/// </summary>
[EnumMember(Value = " ")]
P2PSuperSeeder,
/// <summary>
/// Anonymous user account type.
/// </summary>
[EnumMember(Value = "a")]
AnonUser,
}

View File

@@ -0,0 +1,32 @@
namespace CounterStrikeSharp.API.Modules.Entities.Constants;
/// <summary>
/// Represents the universe of a Steam account.
/// </summary>
public enum SteamAccountUniverse
{
/// <summary>
/// Individual / unspecified universe.
/// </summary>
Unspecified = 0,
/// <summary>
/// Public universe.
/// </summary>
Public = 1,
/// <summary>
/// Beta universe.
/// </summary>
Beta = 2,
/// <summary>
/// Internal universe.
/// </summary>
Internal = 3,
/// <summary>
/// Development universe.
/// </summary>
Dev = 4,
}

View File

@@ -0,0 +1,78 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Modules.Entities;
public static class EntitySystem
{
private static Lazy<IntPtr> ConcreteEntityListPointer = new(NativeAPI.GetConcreteEntityListPointer);
private const int MaxEntities = 32768;
private const int MaxEntitiesPerChunk = 512;
private const int MaxChunks = MaxEntities / MaxEntitiesPerChunk;
private const int SizeOfEntityIdentity = 0x78;
private const int HandleOffset = 0x10;
private const uint InvalidEHandleIndex = 0xFFFFFFFF;
static unsafe Span<IntPtr> IdentityChunks => new((void*)ConcreteEntityListPointer.Value, MaxChunks);
public static IntPtr FirstActiveEntity => Marshal.ReadIntPtr(ConcreteEntityListPointer.Value, MaxEntitiesPerChunk);
public static IntPtr? GetEntityByHandle(uint raw)
{
return GetEntityByHandle(new CHandle<CEntityInstance>(raw));
}
public static IntPtr? GetEntityByHandle<T>(CHandle<T> handle) where T : NativeEntity
{
if (!handle.IsValid)
return null;
IntPtr pChunkToUse = IdentityChunks[(int)(handle.Index / MaxEntitiesPerChunk)];
if (pChunkToUse == IntPtr.Zero)
return null;
IntPtr pIdentityPtr = IntPtr.Add(pChunkToUse, SizeOfEntityIdentity * (int)(handle.Index % MaxEntitiesPerChunk));
if (pIdentityPtr == IntPtr.Zero)
return null;
var foundHandle = new CEntityHandle(pIdentityPtr + HandleOffset);
if (foundHandle.Raw != handle.Raw)
return null;
return Marshal.ReadIntPtr(pIdentityPtr);
}
public static IntPtr? GetEntityByIndex(uint index)
{
if ((int)index <= -1 || index >= MaxEntities - 1) return null;
IntPtr pChunkToUse = IdentityChunks[(int)(index / MaxEntitiesPerChunk)];
if (pChunkToUse == IntPtr.Zero)
return null;
IntPtr pIdentityPtr = IntPtr.Add(pChunkToUse, SizeOfEntityIdentity * (int)(index % MaxEntitiesPerChunk));
if (pIdentityPtr == IntPtr.Zero)
return null;
var foundHandle = new CEntityHandle(pIdentityPtr + HandleOffset);
if (foundHandle.Index != index)
return null;
return Marshal.ReadIntPtr(pIdentityPtr);
}
public static uint GetRawHandleFromEntityPointer(IntPtr pointer)
{
if (pointer == IntPtr.Zero)
return InvalidEHandleIndex;
return Schema.GetPointer<CEntityIdentity?>(pointer, "CEntityInstance", "m_pEntity")?.EntityHandle.Raw ??
InvalidEHandleIndex;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Modules.Entities
{
@@ -12,6 +13,7 @@ namespace CounterStrikeSharp.API.Modules.Entities
public static explicit operator SteamID(ulong u) => new(u);
public static explicit operator SteamID(string s) => new(s);
ulong ParseId(string id)
{
var parts = id.Split(':');
@@ -34,7 +36,7 @@ namespace CounterStrikeSharp.API.Modules.Entities
public string SteamId3
{
get => $"[U:1:{SteamId64 - Base}]";
get => $"[{EnumUtils.GetEnumMemberAttributeValue(AccountType)}:{(int)AccountUniverse}:{SteamId64 - Base}]";
set => SteamId64 = ParseId3(value);
}
@@ -44,20 +46,54 @@ namespace CounterStrikeSharp.API.Modules.Entities
set => SteamId64 = (ulong)value + Base;
}
public int AccountId => (int)((SteamId64 >> 0) & 0xFFFFFFFF);
public SteamAccountInstance AccountInstance =>
(SteamAccountInstance)((SteamId64 >> 32) & 0xFFFFF);
public SteamAccountType AccountType =>
(SteamAccountType)((SteamId64 >> 52) & 0xF);
public SteamAccountUniverse AccountUniverse =>
(SteamAccountUniverse)((SteamId64 >> 56) & 0xF);
public bool IsValid()
{
if (AccountUniverse == SteamAccountUniverse.Unspecified
|| AccountType == SteamAccountType.Invalid
|| AccountInstance == SteamAccountInstance.Invalid)
return false;
if (AccountType == SteamAccountType.Individual
&& (AccountId == 0 || AccountInstance != SteamAccountInstance.Desktop))
return false;
if (AccountType == SteamAccountType.Clan
&& (AccountId == 0 || AccountInstance != SteamAccountInstance.All))
return false;
if (AccountType == SteamAccountType.GameServer && AccountId == 0)
return false;
return true;
}
public override string ToString() => $"[SteamID {SteamId64}, {SteamId2}, {SteamId3}]";
public Uri ToCommunityUrl()
{
string url = string.Empty;
if (AccountType == SteamAccountType.Individual)
url = "https://steamcommunity.com/profiles/" + SteamId64;
if (AccountType == SteamAccountType.Clan)
url = "https://steamcommunity.com/gid/" + SteamId64;
return new Uri(url);
}
public bool Equals(SteamID? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return SteamId64 == other.SteamId64;
return other != null && SteamId64 == other.SteamId64;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
if (obj?.GetType() != this.GetType()) return false;
return Equals((SteamID)obj);
}

View File

@@ -77,7 +77,7 @@ namespace CounterStrikeSharp.API.Modules.Events
break;
case var _ when value is CCSPlayerController player:
// When I was testing this, the code seems to expect a slot, even though it is called index
SetEntityIndex(name, (int)player.EntityIndex.Value.Value - 1);
SetEntityIndex(name, (int)player.Index - 1);
break;
case var _ when value is string s:
SetString(name, s);
@@ -124,6 +124,6 @@ namespace CounterStrikeSharp.API.Modules.Events
public void FireEvent(bool dontBroadcast) => NativeAPI.FireEvent(Handle, dontBroadcast);
public void FireEventToClient(CCSPlayerController player) => NativeAPI.FireEventToClient(Handle, (int)player.EntityIndex.Value.Value);
public void FireEventToClient(CCSPlayerController player) => NativeAPI.FireEventToClient(Handle, (int)player.Index);
}
}

View File

@@ -73,9 +73,26 @@ namespace CounterStrikeSharp.API.Modules.Memory
return types[Enum.GetUnderlyingType(type)];
}
GlobalContext.Instance.Logger.LogWarning("Error retrieving data type for type {Type}", type.FullName);
Core.Application.Instance.Logger.LogWarning("Error retrieving data type for type {Type}", type.FullName);
return null;
}
public static DataType ToValidDataType(this Type type)
{
if (types.ContainsKey(type)) return types[type];
if (typeof(NativeObject).IsAssignableFrom(type))
{
return DataType.DATA_TYPE_POINTER;
}
if (type.IsEnum && types.ContainsKey(Enum.GetUnderlyingType(type)))
{
return types[Enum.GetUnderlyingType(type)];
}
throw new NotSupportedException("Data type not supported:" + type.FullName);
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CounterStrikeSharp.API.Core;
namespace CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
public abstract class BaseMemoryFunction : NativeObject
{
private static Dictionary<string, IntPtr> _createdFunctions = new();
private static IntPtr CreateValveFunctionBySignature(string signature, DataType returnType,
DataType[] argumentTypes)
{
if (!_createdFunctions.TryGetValue(signature, out var function))
{
try
{
function = NativeAPI.CreateVirtualFunctionBySignature(IntPtr.Zero, Addresses.ServerPath, signature,
argumentTypes.Length, (int)returnType, argumentTypes.Cast<object>().ToArray());
_createdFunctions[signature] = function;
}
catch (Exception)
{
}
}
return function;
}
private static IntPtr CreateValveFunctionBySignature(string signature, string binarypath, DataType returnType,
DataType[] argumentTypes)
{
if (!_createdFunctions.TryGetValue(signature, out var function))
{
try
{
function = NativeAPI.CreateVirtualFunctionBySignature(IntPtr.Zero, binarypath, signature,
argumentTypes.Length, (int)returnType, argumentTypes.Cast<object>().ToArray());
_createdFunctions[signature] = function;
}
catch (Exception)
{
}
}
return function;
}
public BaseMemoryFunction(string signature, DataType returnType, DataType[] parameters) : base(
CreateValveFunctionBySignature(signature, returnType, parameters))
{
}
public BaseMemoryFunction(string signature, string binarypath, DataType returnType, DataType[] parameters) : base(
CreateValveFunctionBySignature(signature, binarypath, returnType, parameters))
{
}
public void Hook(Func<DynamicHook, HookResult> handler, HookMode mode)
{
NativeAPI.HookFunction(Handle, handler, mode == HookMode.Post);
}
public void Unhook(Func<DynamicHook, HookResult> handler, HookMode mode)
{
NativeAPI.UnhookFunction(Handle, handler, mode == HookMode.Post);
}
protected T InvokeInternal<T>(params object[] args)
{
return NativeAPI.ExecuteVirtualFunction<T>(Handle, args);
}
protected void InvokeInternalVoid(params object[] args)
{
NativeAPI.ExecuteVirtualFunction<object>(Handle, args);
}
}

View File

@@ -0,0 +1,31 @@
using System;
using CounterStrikeSharp.API.Core;
namespace CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
public class DynamicHook : NativeObject
{
public DynamicHook(IntPtr pointer) : base(pointer)
{
}
public T GetParam<T>(int index)
{
return NativeAPI.DynamicHookGetParam<T>(Handle, (int)typeof(T).ToValidDataType(), index);
}
public T GetReturn<T>(int index)
{
return NativeAPI.DynamicHookGetReturn<T>(Handle, (int)typeof(T).ToValidDataType());
}
public void SetParam<T>(int index, T value)
{
NativeAPI.DynamicHookSetParam(Handle, (int)typeof(T).ToValidDataType(), index, value);
}
public void SetReturn<T>(T value)
{
NativeAPI.DynamicHookSetReturn(Handle, (int)typeof(T).ToValidDataType(), value);
}
}

View File

@@ -0,0 +1,279 @@
using System;
namespace CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
public class MemoryFunctionVoid : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID, Array.Empty<DataType>())
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID, Array.Empty<DataType>())
{
}
public void Invoke()
{
InvokeInternalVoid();
}
}
public class MemoryFunctionVoid<T1> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[] { typeof(T1).ToValidDataType() })
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[] { typeof(T1).ToValidDataType() })
{
}
public void Invoke(T1 arg1)
{
InvokeInternalVoid(arg1);
}
}
public class MemoryFunctionVoid<T1, T2> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[] { typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType() })
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[] { typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType() })
{
}
public void Invoke(T1 arg1, T2 arg2)
{
InvokeInternalVoid(arg1, arg2);
}
}
public class MemoryFunctionVoid<T1, T2, T3> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[] { typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(), typeof(T3).ToValidDataType() })
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[] { typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(), typeof(T3).ToValidDataType() })
{
}
public void Invoke(T1 arg1, T2 arg2, T3 arg3)
{
InvokeInternalVoid(arg1, arg2, arg3);
}
}
public class MemoryFunctionVoid<T1, T2, T3, T4> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType()
})
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType()
})
{
}
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
InvokeInternalVoid(arg1, arg2, arg3, arg4);
}
}
public class MemoryFunctionVoid<T1, T2, T3, T4, T5> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType()
})
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType()
})
{
}
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
{
InvokeInternalVoid(arg1, arg2, arg3, arg4, arg5);
}
}
public class MemoryFunctionVoid<T1, T2, T3, T4, T5, T6> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType()
})
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType()
})
{
}
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
{
InvokeInternalVoid(arg1, arg2, arg3, arg4, arg5, arg6);
}
}
public class MemoryFunctionVoid<T1, T2, T3, T4, T5, T6, T7> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType(),
typeof(T7).ToValidDataType()
})
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType(),
typeof(T7).ToValidDataType()
})
{
}
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
{
InvokeInternalVoid(arg1, arg2, arg3, arg4, arg5, arg6, arg7);
}
}
public class MemoryFunctionVoid<T1, T2, T3, T4, T5, T6, T7, T8> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType(),
typeof(T7).ToValidDataType(), typeof(T8).ToValidDataType()
})
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType(),
typeof(T7).ToValidDataType(), typeof(T8).ToValidDataType()
})
{
}
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
{
InvokeInternalVoid(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
}
}
public class MemoryFunctionVoid<T1, T2, T3, T4, T5, T6, T7, T8, T9> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType(),
typeof(T7).ToValidDataType(), typeof(T8).ToValidDataType(),
typeof(T9).ToValidDataType()
})
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType(),
typeof(T7).ToValidDataType(), typeof(T8).ToValidDataType(),
typeof(T9).ToValidDataType()
})
{
}
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9)
{
InvokeInternalVoid(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
}
}
public class MemoryFunctionVoid<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType(),
typeof(T7).ToValidDataType(), typeof(T8).ToValidDataType(),
typeof(T9).ToValidDataType(), typeof(T10).ToValidDataType()
})
{
}
public MemoryFunctionVoid(string signature, string binarypath) : base(signature, binarypath, DataType.DATA_TYPE_VOID,
new[]
{
typeof(T1).ToValidDataType(), typeof(T2).ToValidDataType(),
typeof(T3).ToValidDataType(), typeof(T4).ToValidDataType(),
typeof(T5).ToValidDataType(), typeof(T6).ToValidDataType(),
typeof(T7).ToValidDataType(), typeof(T8).ToValidDataType(),
typeof(T9).ToValidDataType(), typeof(T10).ToValidDataType()
})
{
}
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10)
{
InvokeInternalVoid(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
}
}

Some files were not shown because too many files have changed in this diff Show More