Compare commits

...

28 Commits

Author SHA1 Message Date
Charles_
bcacc42d0e feat: Added support for custom gamedata files. (#194) 2023-12-12 09:44:28 +10:00
HerrMagiic
8235d5e728 Update docs link in README.md (#193) 2023-12-11 23:11:03 +10:00
Michael Wilson
56739356d5 Plugin Translations (#146) 2023-12-11 16:19:50 +10:00
ZoNiCaL
aaba87551d Admin improvements round 2! (this time it's personal) (#143)
Co-authored-by: Roflmuffin <shortguy014@gmail.com>
2023-12-11 13:24:14 +10:00
Roflmuffin
a3466dd5d1 chore: cleanup null reference warnings in virtual funcs 2023-12-11 11:48:55 +10:00
johnoclockdk
c8604760f2 Update docs (#176) 2023-12-11 10:41:10 +10:00
Michael Wilson
d50a945317 feat: defers calls to HookEvent until after game loop mode init (#187) 2023-12-10 22:13:03 +10:00
Michael Wilson
55396e005c fix: discord links (#190) 2023-12-09 14:11:27 +10:00
Roflmuffin
98b2b01992 Merge branch 'FixSteamIdOnWindowsServer' into main 2023-12-08 12:34:52 +10:00
Roflmuffin
a537be89e4 tests: update tests, throw out of range exception <= 0 2023-12-08 12:32:46 +10:00
Roflmuffin
c07d5d2aa9 Merge remote-tracking branch 'origin/main' into FixSteamIdOnWindowsServer 2023-12-08 12:25:50 +10:00
Michael Wilson
1cc95555fe feat: add basic tests project with SteamID tests (#186) 2023-12-08 12:24:35 +10:00
Roflmuffin
378c28dfd0 chore: bump hl2sdk version 2023-12-08 12:22:54 +10:00
TheR00st3r
c7343c3b7a Fix SteamId on Windows Server #182 2023-12-07 21:19:07 +01:00
Michael Wilson
62f6b09f50 Add VData Access (#181) 2023-12-07 22:58:16 +10:00
Michael Wilson
2a15a8de71 Add Entity Output Hooks (#174)
Co-authored-by: Poggu <poggu@seznam.cz>
Co-authored-by: KillStr3aK <statser07@gmail.com>
Co-authored-by: Poggu <45881722+Poggicek@users.noreply.github.com>
2023-12-07 17:36:33 +10:00
laper32
1d6bee02cd docs: win32 related (#177)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2023-12-07 17:35:04 +10:00
Daniel Saewitz
72e1f22e14 Fix svg colors/optimization (#175) 2023-12-06 07:39:10 +10:00
roflmuffin
a8a238bdee docx: update 404 page 2023-12-05 23:18:49 +10:00
roflmuffin
cec8ef3d30 docs: exclude system object inheritance 2023-12-05 23:17:31 +10:00
roflmuffin
c7384df396 docs: update docfx fonts to match old docs 2023-12-05 23:00:05 +10:00
BuSheeZy
84d4998a72 Replace documenation with docfx (#165) 2023-12-05 22:50:54 +10:00
roflmuffin
1b194318af chore: update hl2sdk, gitignore 2023-12-05 13:18:19 +10:00
Michael Wilson
22d0dd8200 fix: always run preworld update if tasks is empty (#172) 2023-12-05 12:57:58 +10:00
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
156 changed files with 3677 additions and 4628 deletions

View File

@@ -3,7 +3,7 @@ name: Build & Publish
on:
push:
paths-ignore:
- 'docs/**'
- 'docfx/**'
branches: [ "main" ]
pull_request:
branches: [ "main" ]
@@ -117,7 +117,21 @@ jobs:
with:
dotnet-version: '7.0.x'
- run: |
- name: Install dependencies
run: dotnet restore managed/CounterStrikeSharp.sln
- name: Run tests
run: dotnet test --logger trx --results-directory "TestResults-${{ env.GITHUB_SHA_SHORT }}" managed/CounterStrikeSharp.API.Tests/CounterStrikeSharp.API.Tests.csproj
- name: Upload dotnet test results
uses: actions/upload-artifact@v3
with:
name: test-results-${{ env.GITHUB_SHA_SHORT }}
path: TestResults-${{ env.GITHUB_SHA_SHORT }}
if: ${{ always() }}
- name: Publish artifacts
run: |
dotnet publish -c Release /p:Version=1.0.${{ env.BUILD_NUMBER }} managed/CounterStrikeSharp.API
dotnet pack -c Release /p:Version=1.0.${{ env.BUILD_NUMBER }} managed/CounterStrikeSharp.API

View File

@@ -1,42 +1,46 @@
name: Deploy to GitHub Pages
on:
# Trigger the workflow every time you push to the `main` branch
# Using a different branch name? Replace `main` with your branchs name
push:
paths:
- 'docs/**'
branches: [ main ]
branches:
- main
# Allows you to run this workflow manually from the Actions tab on GitHub.
workflow_dispatch:
# Allow this job to clone the repo and create a page deployment
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout your repository using git
uses: actions/checkout@v3
- name: Install, build, and upload your site
uses: withastro/action@v1
with:
path: ./docs # The root location of your Astro project inside the repository. (optional)
node-version: 20 # The specific version of Node that should be used to build your site. Defaults to 18. (optional)
package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
concurrency:
group: "pages"
cancel-in-progress: false
deploy:
needs: build
runs-on: ubuntu-latest
jobs:
publish-docs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Dotnet Setup
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.x
- run: dotnet tool update -g docfx
- run: docfx docfx/docfx.json
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: "docfx/_site"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v2

7
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.ccls-cache/
.cmake/
cmake-build-debug/
cmake-build-debug*/
.kdev4/
.vscode/
generated/
@@ -549,3 +549,8 @@ $RECYCLE.BIN/
_NCrunch*
.idea/
# docfx
docfx/_site/
docfx/api/
docfx/_exported_templates/

View File

@@ -2,7 +2,7 @@
CounterStrikeSharp is a server side modding framework for Counter-Strike: Global Offensive. This project attempts to implement a .NET Core scripting layer on top of a Metamod Source Plugin, allowing developers to create plugins that interact with the game server in a modern language (C#) to facilitate the creation of maintainable and testable code.
[Come and join our Discord](https://discord.gg/X7r3PmuYKq)
[Come and join our Discord](https://discord.gg/eAZU3guKWU)
## History
@@ -14,7 +14,7 @@ Due to the architectural changes of CS2, the plugin is being rebuilt on the grou
Download the latest build from [here](https://github.com/roflmuffin/CounterStrikeSharp/releases). (Download the with runtime version if this is your first time installing).
Detailed installation instructions can be found in the [docs](https://docs.cssharp.dev/guides/getting-started/).
Detailed installation instructions can be found in the [docs](https://docs.cssharp.dev/docs/guides/getting-started.html).
## What works?
@@ -39,7 +39,7 @@ These features are the core of the platform and work pretty well/have a low risk
## Links
- [Join the Discord](https://discord.gg/X7r3PmuYKq): Ask questions, provide suggestions
- [Join the Discord](https://discord.gg/eAZU3guKWU): Ask questions, provide suggestions
- [Read the docs](https://docs.cssharp.dev/): Getting started guide, hello world plugin example
- [Issue tracker](https://github.com/roflmuffin/CounterStrikeSharp/issues): Raise any issues here
- [Builds](https://github.com/roflmuffin/CounterStrikeSharp/actions): Download latest unstable dev snapshot

View File

@@ -2,5 +2,6 @@
"PublicChatTrigger": [ "!" ],
"SilentChatTrigger": [ "/" ],
"FollowCS2ServerGuidelines": true,
"PluginHotReloadEnabled": true
"PluginHotReloadEnabled": true,
"ServerLanguage": "en"
}

View File

@@ -164,5 +164,12 @@
"windows": 91,
"linux": 91
}
},
"CEntityIOOutput_FireOutputInternal": {
"signatures": {
"library": "server",
"windows": "\\x48\\x8B\\xC4\\x4C\\x89\\x48\\x20\\x55\\x57\\x41\\x54\\x41\\x56",
"linux": "\\x55\\x48\\x89\\xE5\\x41\\x57\\x41\\x56\\x41\\x55\\x41\\x54\\x53\\x48\\x83\\xEC\\x58\\x4C\\x8B\\x6F\\x08"
}
}
}

3
docfx/404.md Normal file
View File

@@ -0,0 +1,3 @@
# 404
We recently changed our docs. Your page may exist at a different location.

46
docfx/docfx.json Normal file
View File

@@ -0,0 +1,46 @@
{
"metadata": [
{
"src": [
{
"src": "../managed/CounterStrikeSharp.API",
"files": ["**/*.csproj"],
"exclude": ["**/bin/**", "**/obj/**"]
}
],
"filter": "filterConfig.yml",
"dest": "api",
"namespaceLayout": "nested"
}
],
"build": {
"content": [
{
"files": ["**/*.{md,yml}"],
"exclude": ["_site/**"]
}
],
"resource": [
{
"files": ["images/**"]
}
],
"output": "_site",
"template": ["default", "modern", "layouts/cssharp"],
"globalMetadata": {
"_appFaviconPath": "images/cssharp.svg",
"_appFooter": "<a href=\"https://docs.cssharp.dev\">docs.cssharp.dev</a>",
"_appLogoPath": "images/cssharp.svg",
"_appName": "CounterStrikeSharp",
"_appTitle": "CounterStrikeSharp",
"_disableNewTab": true,
"_enableNewTab": true,
"_enableSearch": true,
"pdf": false
},
"sitemap": {
"baseUrl": "https://docs.cssharp.dev",
"changefreq": "hourly"
}
}
}

View File

@@ -3,6 +3,10 @@ title: Admin Command Attributes
description: A guide on using the Admin Command Attributes in plugins.
---
# Admin Command Attributes
A guide on using the Admin Command Attributes in plugins.
## Assigning permissions to a Command
Assigning permissions to a Command is as easy as tagging the Command method (function callback) with either a `RequiresPermissions` or `RequiresPermissionsOr` attribute. The difference between the two attributes is that `RequiresPermissionsOr` needs only one permission to be present on the caller to be passed, while `RequiresPermissions` needs the caller to have all permissions listed. CounterStrikeSharp handles all of the permission checks behind the scenes for you.

View File

@@ -3,6 +3,10 @@ title: Defining Admin Groups
description: A guide on how to define admin groups for CounterStrikeSharp.
---
# Defining Admin Groups
A guide on how to define admin groups for CounterStrikeSharp.
## Adding Groups
Groups can be created to group a series of permissions together under one tag. They are defined in `configs/admin_groups.json`. The important things you need to declare is the name of the group and the permissions they have.
@@ -34,9 +38,9 @@ You can add admins to groups using the `groups` array in `configs/admins.json`
}
```
:::note
All group names MUST start with a hashtag # character, otherwise CounterStrikeSharp won't recognize the group.
:::
> [!NOTE]
> 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`.

View File

@@ -3,6 +3,10 @@ title: Defining Admin Immunity
description: A guide on how to define immunity for admins for CounterStrikeSharp.
---
# Defining Admin Immunity
A guide on how to define immunity for admins for CounterStrikeSharp.
## Player Immunity
Admins can be assigned an immunity value, similar to SourceMod. If an admin or player with a lower immunity value targets another admin or player with a larger immunity value, then the command will fail. You can define an immunity value by adding the `immunity` key to each admin in `configs/admins.json`.
@@ -31,6 +35,5 @@ You can even assign an immunity value to groups. If an admin has a lower immunit
}
```
:::note
CounterStrikeSharp does not automatically handle immunity checking. It is up to individual plugins to handle immunity checks as they can implement different ways of targeting players. This can be done in code with `AdminManager.CanPlayerTarget`. You can also set a player's immunity in code with `AdminManager.SetPlayerImmunity`.
:::
> [!NOTE]
> CounterStrikeSharp does not automatically handle immunity checking. It is up to individual plugins to handle immunity checks as they can implement different ways of targeting players. This can be done in code with `AdminManager.CanPlayerTarget`. You can also set a player's immunity in code with `AdminManager.SetPlayerImmunity`.

View File

@@ -3,6 +3,10 @@ title: Defining Admins
description: A guide on how to define admins for CounterStrikeSharp.
---
# Defining Admins
A guide on how to define admins for CounterStrikeSharp.
## Admin Framework
CounterStrikeSharp has a basic framework which allows plugin developers to assign permissions to commands. When CSS is initialized, a list of admins are loaded from `configs/admins.json`.
@@ -26,9 +30,8 @@ Adding an Admin is as simple as creating a new entry in the `configs/admins.json
You can also manually assign permissions to players in code with `AdminManager.AddPlayerPermissions` and `AdminManager.RemovePlayerPermissions`. These changes are not saved to `configs/admins.json`.
:::note
All user permissions MUST start with an at-symbol @ character, otherwise CounterStrikeSharp will not recognize the permission.
:::
> [!NOTE]
> All user permissions MUST start with an at-symbol @ character, otherwise CounterStrikeSharp will not recognize the permission.
### Standard Permissions
@@ -55,7 +58,5 @@ However there is a somewhat standardized list of flags that it is advised you us
@css/root # Magically enables all flags and ignores immunity values.
```
:::note
CounterStrikeSharp does not implement traditional admin command such as `!slay`, `!kick`, and `!ban`. It is up to individual plugins to implement these commands.
:::
> [!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

@@ -3,6 +3,10 @@ title: Defining Command Overrides
description: A guide on how to define command overrides for CounterStrikeSharp.
---
# Defining Command Overrides
A guide on how to define command overrides for CounterStrikeSharp.
## Defining Admin and Group specific overrides
Command permissions can be overriden so specific admins or groups can execute the command, regardless of any permissions they may or may not have. You can define command overrides by adding the `command_overrides` key to each admin in `configs/admins.json` or group in `configs/admin_groups.json`. Command overrides can either be set to `true`, meaning the admin/group can execute the command, or `false`, meaning the admin/group cannot execute the command at all.

View File

@@ -0,0 +1,14 @@
- name: Admin Command Attributes
href: admin-command-attributes.md
- name: Defining Admins
href: defining-admins.md
- name: Defining Command Overrides
href: defining-command-overrides.md
- name: Defining Admin Groups
href: defining-admin-groups.md
- name: Defining Admin Immunity
href: defining-admin-immunity.md

View File

@@ -3,6 +3,10 @@ title: Console Commands
description: How to add a new console command
---
# Console Commands
How to add a new console command
## Adding a Console Command
### Automatic registration

View File

@@ -3,6 +3,10 @@ title: Console Variables
description: How to read & write console variables (ConVars).
---
# Console Variables
How to read & write console variables (ConVars).
## Finding a ConVar
Use the `ConVar.Find` static method to find a reference to an existing ConVar (or `null`).

View File

@@ -3,15 +3,18 @@ title: Game Events
description: How to listen to Source 1 style game events.
---
# Game Events
How to listen to Source 1 style game events.
## Adding an Event Listener
### Automatic registration
CounterStrikeSharp will automatically register event listeners marked with a `GameEventHandler` attribute on the `BasePlugin` class. These listeners are automatically registered/deregistered for you on hot reload.
:::note
The first parameter type must be a subclass of the `GameEvent` class. The names are automatically generated from the [game event list](https://cs2.poggu.me/dumped-data/game-events).
:::
> [!NOTE]
> The first parameter type must be a subclass of the `GameEvent` class. The names are automatically generated from the [game event list](https://cs2.poggu.me/dumped-data/game-events).
```csharp
[GameEventHandler]

View File

@@ -3,6 +3,10 @@ title: Global Listeners
description: How to subscribe to CounterStrikeSharp global listeners.
---
# Global Listeners
How to subscribe to CounterStrikeSharp global listeners.
## Adding a Listener
Global listeners come in a variety of shapes so there is no automatic registration for these, they must be registered in the `OnLoad` of your plugin (or anywhere you have access to the plugin instance). You can find the full list of event listeners in the `Listeners` class as seen below.

View File

@@ -0,0 +1,11 @@
- name: Console Commands
href: console-commands.md
- name: Console Variables
href: console-variables.md
- name: Game Events
href: game-events.md
- name: Global Listeners
href: global-listeners.md

View File

@@ -1,10 +1,12 @@
---
title: Dependency Injection
description: How to make use of dependency injection in CounterStrikeSharp
sidebar:
order: 1
---
# Dependency Injection
How to make use of dependency injection in CounterStrikeSharp
`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.

View File

@@ -0,0 +1,63 @@
---
title: Getting Started
description: How to get started installing & using CounterStrikeSharp.
---
# Getting Started
How to get started installing & using CounterStrikeSharp.
## Installing Metamod
`CounterStrikeSharp` uses `Metamod:Source` as its main way of communicating with the game server. To install it, you can follow the detailed instructions found <a href="https://cs2.poggu.me/metamod/installation/" target="_blank">here</a>.
## Installing CounterStrikeSharp
Download the latest release of CounterStrikeSharp from <a href="https://github.com/roflmuffin/CounterStrikeSharp/releases/latest" target="_blank">GitHub releases pages</a>.
> [!CAUTION]
> If this is your first time installing, you will need to download the `with-runtime` version. This includes a copy of the .NET runtime, which is required to run the plugin.
> Depending on the os you might also either need to install `libicu` / `icu-libs` / `libicu-dev` using your package manager for .NET to run or setting `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true` in your servers environment variables. You can find more infos about that <a href="https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-invariant-mode.md#enabling-the-invariant-mode" target="_blank">here</a>
>
> Subsequent upgrades will not require the runtime, unless a version bump of the .NET runtime is required (i.e. from 7.0.x to 8.0.x). We will inform you when this occurs.
> [!CAUTION]
> For Windows users, you must ensure that you have installed **Visual Studio Redistributables**.
> If not, you can download it here: <a href="https://aka.ms/vs/17/release/vc_redist.x64.exe" target="_blank">Download</a>
> > This link will download VC Redistributable directly.
>
> You must install it before starting the server, otherwise CSS will not work!
Extract the `addons` folder to the `/csgo/` directory of the dedicated server. The contents of your addons folder should contain both the `counterstrikesharp` folder and the `metamod` folder as seen below.
```shell
<server_path>/game/csgo/addons > tree -L 2
addons
├── counterstrikesharp
│ ├── api
│ ├── bin
│ ├── dotnet
│ ├── plugins
│ └── gamedata
├── metamod
│ ├── bin
│ ├── counterstrikesharp.vdf
│ ├── metaplugins.ini
│ └── README.txt
├── metamod.vdf
└── metamod_x64.vdf
```
## Start the Server
Launch your CS2 dedicated server as normal. If everything is working correctly, you should see a message in the console that says `CSSharp: CounterStrikeSharp.API Loaded Successfully.`.
Running the command `meta list` in the console should show 1 plugin loaded 🎉
```shell
meta list
Listing 1 plugin:
[01] CounterStrikeSharp (0.1.0) by Roflmuffin
```

View File

@@ -1,10 +1,12 @@
---
title: Hello World Plugin
description: How to write your first plugin for CounterStrikeSharp
sidebar:
order: 0
---
# Hello World Plugin
How to write your first plugin for CounterStrikeSharp
## Creating a New Project
First, ensure you have the relevant .NET 7.0 SDK for your platform installed on your machine. You can find the links to the latest downloads on the <a href="https://dotnet.microsoft.com/en-us/download/dotnet/7.0" target="_blank"> official Microsoft download page</a>.
@@ -36,12 +38,11 @@ Use your IDE (Visual Studio/Rider) to add a reference to the `CounterStrikeSharp
</Project>
```
:::tip
Instead of manually adding a reference to `CounterStrikeSharp.Api.dll`, you can install the NuGet package `CounterStrikeSharp.Api` using the following:
```shell
dotnet add package CounterStrikeSharp.API
```
:::
> [!TIP]
> Instead of manually adding a reference to `CounterStrikeSharp.Api.dll`, you can > install the NuGet package `CounterStrikeSharp.Api` using the following:
> ```shell
> dotnet add package CounterStrikeSharp.API
> ```
### Creating a Plugin File
@@ -78,20 +79,17 @@ Locate the `plugins` folder in your CS2 dedicated server (`/game/csgo/addons/cou
└── 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.
:::
> [!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.
:::
> [!NOTE]
> Note that some of these dependencies may change depending on the version of CounterStrikeSharp being used.
### Start the Server
Now start your CS2 dedicated server. Just before the `CounterStrikeSharp.API Loaded Successfully.` message you should see your `Hello World!` console write that we called from the load function, neat!
:::note[Hot Reloading!]
By default, CounterStrikeSharp will automatically hot reload your plugin if you replace the .dll file in your plugin folder. When it does so, it will call the `Unload` and `Load` functions respectively, with the `hotReload` flag set to true.
It is worth noting that the framework will automatically deregister any event handlers or listeners for you automatically, so you can safely reregister these on load without checking this flag. However you may want to do some specific actions on a hot reload, which you can do in your `Load()` call by checking the flag!
:::
> [!NOTE]
> By default, CounterStrikeSharp will automatically hot reload your plugin if you replace the .dll file in your plugin folder. When it does so, it will call the `Unload` and `Load` functions respectively, with the `hotReload` flag set to true.
>
> It is worth noting that the framework will automatically deregister any event handlers or listeners for you automatically, so you can safely reregister these on load without checking this flag. However you may want to do some specific actions on a hot reload, which you can do in your `Load()` call by checking the flag!

View File

@@ -0,0 +1,8 @@
- name: Getting Started
href: getting-started.md
- name: Hello World Plugin
href: hello-world-plugin.md
- name: Dependency Injection
href: dependency-injection.md

View File

@@ -3,6 +3,10 @@ title: Core Configuration
description: Summary for core configuration values
---
# Core Configuration
Summary for core configuration values
## PublicChatTrigger
List of characters to use for public chat triggers.
@@ -21,10 +25,14 @@ Enabling this option will block plugins from using functionality that is known t
This option only has any effect on CS2. Note that this does NOT guarantee that you cannot
receive a ban.
:::note
Disable this option at your own risk.
:::
> [!NOTE]
> Disable this option at your own risk.
## PluginHotReloadEnabled
When enabled, plugins are automatically reloaded when their .dll file is updated.
## ServerLanguage
Configures the default language to use for server commands & messages. The format for the culture name based on RFC 4646 is `languagecode2-country`/`regioncode2`, where `languagecode2` is the two-letter language code and `country/regioncode2` is the two-letter subculture code. Examples include `ja-JP` for Japanese (Japan) and `en-US` for English (United States). Defaults to "en".

View File

@@ -3,6 +3,10 @@ title: Referencing Players
description: Difference between player slots, indexes, userids, controllers & pawns.
---
# Referencing Players
Difference between player slots, indexes, userids, controllers & pawns.
## Controllers & Pawns
All players in CS2 are split between a player controller & a player pawn. The player controller represents the player on the server, and the player pawn represents the players physical character in the game world. This means to edit a players health for example, you would need to edit their `PlayerPawn`'s health; but to check for a player's SteamID, you would check the `PlayerController`.
@@ -44,13 +48,12 @@ var player = Utilities.GetPlayerFromIndex(index);
var player = Utilities.GetPlayerFromSlot(slot);
```
:::note[Entity Safety]
Wherever possible, you should check the validity of any handle you are accessing before assuming it is safe to use.
```csharp
RegisterEventHandler<EventPlayerSpawn>((@event, info) =>
{
if (!@event.Userid.IsValid) return 0; // Checks that the PlayerController is valid
if (!@event.Userid.PlayerPawn.IsValid) return 0; // Checks that the value of the CHandle is pointing to a valid PlayerPawn.
}
```
:::
> [!NOTE]
> Wherever possible, you should check the validity of any handle you are accessing before assuming it is safe to use.
> ```csharp
> RegisterEventHandler<EventPlayerSpawn>((@event, info) =>
> {
> if (!@event.Userid.IsValid) return 0; // Checks that the PlayerController is valid
> if (!@event.Userid.PlayerPawn.IsValid) return 0; // Checks that the value of the CHandle is pointing to a valid PlayerPawn.
> }
> ```

View File

@@ -0,0 +1,5 @@
- name: Core Configuration
href: core-configuration.md
- name: Referencing Players
href: referencing-players.md

16
docfx/docs/toc.yml Normal file
View File

@@ -0,0 +1,16 @@
items:
- name: Guides
href: guides/toc.yml
expanded: true
- name: Features
href: features/toc.yml
expanded: true
- name: Admin Framework
href: admin-framework/toc.yml
expanded: true
- name: Reference
href: reference/toc.yml
expanded: true

View File

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

View File

@@ -0,0 +1,13 @@
# Warcraft Plugin
This Warcraft plugin is migrated from the previous VSP.NET project to give you an idea of the kind of power this scripting runtime is capable of.
<a href="https://github.com/roflmuffin/CounterStrikeSharp/tree/main/examples/WarcraftPlugin" class="btn btn-secondary">View project on Github <i class="bi bi-github"></i></a>
## Demonstrated
- Hook events
- Create commands
- Use third party libraries
- SQLite
- Entity manipulation

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

19
docfx/examples/toc.yml Normal file
View File

@@ -0,0 +1,19 @@
items:
- name: Hello World
href: HelloWorld.md
- name: Commands
href: WithCommands.md
- name: Config
href: WithConfig.md
- name: Dependency Injection
href: WithDependencyInjection.md
- name: Entity Output Hooks
href: WithEntityOutputHooks.md
- name: Game Event Handlers
href: WithGameEventHandlers.md
- name: Database (Dapper)
href: WithDatabase.md
- name: Translations
href: WithTranslations.md
- name: Warcraft Plugin
href: WarcraftPlugin.md

4
docfx/filterConfig.yml Normal file
View File

@@ -0,0 +1,4 @@
apiRules:
- exclude:
uidRegex: ^System\.Object
type: Type

1
docfx/images/cssharp.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" fill="none"><path fill="#F89A0F" d="m37.456 1.262-19.008.148a91.03 91.03 0 0 0-5.96.215A26.294 26.294 0 0 0 1.523 4.793C.643 5.26 0 6.07 0 7.06a2.464 2.464 0 0 0 3.704 2.128l.015-.007a20.538 20.538 0 0 1 7.93-2.495c8.024-.858 13.602 2.018 17.367 5.397 4.138 3.712 7.967 11.271 6.576 19.56-.831 4.962-3.122 9.146-6.69 12.397l-.007.007c-.646.59-1.18 1.274-1.18 2.147a2.547 2.547 0 0 0 4.158 1.965c.102-.075.22-.185.276-.238a27.692 27.692 0 0 0 6.652-9.698l10.126-24.027c.476-1.085.877-2.203 1.001-3.292.537-4.762-2.007-7.502-5.227-8.769-1.648-.65-3.968-.846-6.5-.869l-.745-.004Zm2.74 3.032c1.799.075 3.553 1.372 3.326 3.741-.276 2.865-3.137 4.865-4.384 6.747h4.384c.264.385.264 1.637 0 2.022-2.204-.215-4.66-.178-7.083-.17-1.036-3.783 5.367-6.327 4.72-9.441-.306-1.47-2.403-1.512-2.025.676h-2.529c-.045-2.506 1.796-3.651 3.59-3.575Z"/><path fill="#A079DF" d="M28.422 21.551a2.807 2.807 0 0 0-.34-1.413 2.7 2.7 0 0 0-1.028-.998c-3.772-2.174-7.54-4.34-11.304-6.516a2.774 2.774 0 0 0-3.01.03c-1.5.888-9.01 5.186-11.247 6.486A2.616 2.616 0 0 0 .12 21.55V34.66c0 .525.113.982.333 1.387a2.6 2.6 0 0 0 1.039 1.02c2.237 1.3 9.748 5.602 11.248 6.49a2.778 2.778 0 0 0 3.008.026c3.765-2.173 7.54-4.339 11.309-6.512.434-.24.794-.594 1.04-1.024.226-.427.339-.904.328-1.387l-.004-13.108Z"/><path fill="#270068" d="M14.317 28.06.454 36.05a2.6 2.6 0 0 0 1.039 1.02c2.237 1.3 9.748 5.601 11.248 6.49a2.778 2.778 0 0 0 3.008.026c3.765-2.173 7.54-4.339 11.309-6.512.434-.24.794-.594 1.04-1.024l-13.781-7.99Z"/><path fill="#390091" d="M28.422 21.551a2.807 2.807 0 0 0-.34-1.413l-13.769 7.925 13.78 7.983c.227-.427.34-.904.33-1.387V21.55Z"/><path fill="#fff" d="M22.48 25.078v1.492h1.494v-1.492h.744v1.492h1.493v.745h-1.493v1.493h1.493v.748h-1.493v1.49h-.744v-1.49H22.48v1.49h-.748v-1.49H20.24v-.748h1.493v-1.493H20.24v-.745h1.493v-1.492h.748Zm1.494 2.237H22.48v1.493h1.493v-1.493Z"/><path fill="#fff" d="M14.347 17.583c3.893 0 7.295 2.116 9.109 5.253l-.015-.026-4.58 2.638a5.236 5.236 0 0 0-4.453-2.582h-.06a5.238 5.238 0 1 0 4.565 7.809l-.023.038 4.574 2.645a10.519 10.519 0 0 1-8.996 5.27h-.12a10.522 10.522 0 1 1 0-21.045Z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

24
docfx/index.md Normal file
View File

@@ -0,0 +1,24 @@
---
_layout: landing
uid: home
title: CounterStrikeSharp
_appTitle: ''
description: Write Counter-Strike 2 server plugins in C#.
---
<div class="row justify-content-md-center">
<div class="col-12 col-lg-10 col-xl-8 col-xxl-6">
<div class="text-center">
<img src="images/cssharp.svg" height="128" width="128">
<h1 class="h1">CounterStrikeSharp</h1>
<span>CounterStrikeSharp is a simpler way to write CS2 server plugins.</span>
<div>
<a href="docs/guides/getting-started.md" class="btn btn-primary btn-lg fw-bold my-5">Get Started <i class="bi bi-arrow-right"></a>
<a href="https://github.com/roflmuffin/CounterStrikeSharp/releases/latest" class="btn btn-primary btn-lg fw-bold my-5">Download <i class="bi bi-arrow-right"></a>
</div>
</div>
[!code-csharp[](../examples/HelloWorld/HelloWorldPlugin.cs)]
</div>
</div>

View File

@@ -0,0 +1,152 @@
{{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}}
{{!include(/^public/.*/)}}
{{!include(favicon.ico)}}
{{!include(logo.svg)}}
<!DOCTYPE html>
<html {{#_lang}}lang="{{_lang}}"{{/_lang}}>
<head>
<meta charset="utf-8">
{{#redirect_url}}
<meta http-equiv="refresh" content="0;URL='{{redirect_url}}'">
{{/redirect_url}}
{{^redirect_url}}
<title>{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}">
{{#_description}}<meta name="description" content="{{_description}}">{{/_description}}
<link rel="icon" href="{{_rel}}{{{_appFaviconPath}}}{{^_appFaviconPath}}favicon.ico{{/_appFaviconPath}}">
<link rel="stylesheet" href="{{_rel}}public/docfx.min.css">
<link rel="stylesheet" href="{{_rel}}public/main.css">
<meta name="docfx:navrel" content="{{_navRel}}">
<meta name="docfx:tocrel" content="{{_tocRel}}">
{{#_noindex}}<meta name="searchOption" content="noindex">{{/_noindex}}
{{#_enableSearch}}<meta name="docfx:rel" content="{{_rel}}">{{/_enableSearch}}
{{#_disableNewTab}}<meta name="docfx:disablenewtab" content="true">{{/_disableNewTab}}
{{#_disableTocFilter}}<meta name="docfx:disabletocfilter" content="true">{{/_disableTocFilter}}
{{#docurl}}<meta name="docfx:docurl" content="{{docurl}}">{{/docurl}}
<meta name="loc:inThisArticle" content="{{__global.inThisArticle}}">
<meta name="loc:searchResultsCount" content="{{__global.searchResultsCount}}">
<meta name="loc:searchNoResults" content="{{__global.searchNoResults}}">
<meta name="loc:tocFilter" content="{{__global.tocFilter}}">
<meta name="loc:nextArticle" content="{{__global.nextArticle}}">
<meta name="loc:prevArticle" content="{{__global.prevArticle}}">
<meta name="loc:themeLight" content="{{__global.themeLight}}">
<meta name="loc:themeDark" content="{{__global.themeDark}}">
<meta name="loc:themeAuto" content="{{__global.themeAuto}}">
<meta name="loc:changeTheme" content="{{__global.changeTheme}}">
<meta name="loc:copy" content="{{__global.copy}}">
<meta name="loc:downloadPdf" content="{{__global.downloadPdf}}">
{{/redirect_url}}
<script data-goatcounter="https://cssharp.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100;0,9..40,200;0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;0,9..40,800;0,9..40,900;0,9..40,1000;1,9..40,100;1,9..40,200;1,9..40,300;1,9..40,400;1,9..40,500;1,9..40,600;1,9..40,700;1,9..40,800;1,9..40,900;1,9..40,1000&family=JetBrains+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800&display=swap" rel="stylesheet">
</head>
{{^redirect_url}}
<script type="module" src="./{{_rel}}public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
{{#_googleAnalyticsTagId}}
<script async src="https://www.googletagmanager.com/gtag/js?id={{_googleAnalyticsTagId}}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', '{{_googleAnalyticsTagId}}');
</script>
{{/_googleAnalyticsTagId}}
<body class="tex2jax_ignore" data-layout="{{_layout}}{{layout}}" data-yaml-mime="{{yamlmime}}">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="{{_appLogoUrl}}{{^_appLogoUrl}}{{_rel}}index.html{{/_appLogoUrl}}">
<img id="logo" class="svg" src="{{_rel}}{{{_appLogoPath}}}{{^_appLogoPath}}logo.svg{{/_appLogoPath}}" alt="{{_appName}}" >
{{_appName}}
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
{{#_enableSearch}}
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="{{__global.search}}" autocomplete="off" aria-label="Search">
</form>
{{/_enableSearch}}
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" style="margin-top: -.65em; margin-left: -.8em"
type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas"
aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="{{uid}}">
{{!body}}
</article>
{{^_disableContribution}}
<div class="contribution d-print-none">
{{#sourceurl}}
<a href="{{sourceurl}}" class="edit-link">{{__global.improveThisDoc}}</a>
{{/sourceurl}}
{{^sourceurl}}{{#docurl}}
<a href="{{docurl}}" class="edit-link">{{__global.improveThisDoc}}</a>
{{/docurl}}{{/sourceurl}}
</div>
{{/_disableContribution}}
{{^_disableNextArticle}}
<div class="next-article d-print-none border-top" id="nextArticle"></div>
{{/_disableNextArticle}}
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
{{#_enableSearch}}
<div class="container-xxl search-results" id="search-results"></div>
{{/_enableSearch}}
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
{{{_appFooter}}}{{^_appFooter}}<span>Made with <a href="https://dotnet.github.io/docfx">docfx</a></span>{{/_appFooter}}
</div>
</div>
</footer>
</body>
{{/redirect_url}}
</html>

View File

@@ -0,0 +1,4 @@
:root {
--bs-font-sans-serif: "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: "Jetbrains Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

View File

@@ -0,0 +1,14 @@
export default {
iconLinks: [
{
icon: "github",
href: "https://github.com/roflmuffin/CounterStrikeSharp",
title: "GitHub",
},
{
icon: "discord",
href: "https://discord.gg/eAZU3guKWU",
title: "Discord",
},
],
};

6
docfx/toc.yml Normal file
View File

@@ -0,0 +1,6 @@
- name: Docs
href: docs/
- name: Examples
href: examples/
- name: API
href: api/

21
docs/.gitignore vendored
View File

@@ -1,21 +0,0 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

View File

@@ -1,6 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}

View File

@@ -1,61 +0,0 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
// https://astro.build/config
export default defineConfig({
integrations: [
starlight({
title: 'CounterStrikeSharp Docs',
customCss: [
'@fontsource/dm-sans/400.css',
'@fontsource/dm-sans/500.css',
'@fontsource/dm-sans/700.css',
'@fontsource/jetbrains-mono/400.css',
'@fontsource/jetbrains-mono/600.css',
'./src/styles/custom.css',
],
head: [
{
tag: 'script',
attrs: {
src: 'https://gc.zgo.at/count.js',
'data-goatcounter': 'https://cssharp.goatcounter.com/count',
async: true,
},
},
],
social: {
github: 'https://github.com/roflmuffin/CounterStrikeSharp',
},
sidebar: [
{
label: 'Guides',
autogenerate: { directory: 'guides' },
},
{
label: 'Features',
autogenerate: { directory: 'features' },
},
{
label: 'Admin Framework',
autogenerate: { directory: 'admin-framework' },
},
{
label: 'Reference',
autogenerate: { directory: 'reference' },
},
],
editLink: {
baseUrl:
'https://github.com/roflmuffin/CounterStrikeSharp/edit/main/docs/',
},
}),
],
base: '/',
site: 'https://docs.cssharp.dev',
markdown: {
shikiConfig: {
wrap: false,
},
},
});

View File

@@ -1,22 +0,0 @@
{
"name": "",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.11.1",
"@fontsource/dm-sans": "^5.0.15",
"@fontsource/jetbrains-mono": "^5.0.15",
"astro": "^3.2.3",
"sharp": "^0.32.5"
},
"devDependencies": {
"prettier": "^3.0.3"
}
}

4118
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
import { defineCollection } from 'astro:content';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
};

View File

@@ -1,56 +0,0 @@
---
title: Getting Started
description: How to get started installing & using CounterStrikeSharp.
sidebar:
order: 0
---
# Installation
### Installing Metamod
`CounterStrikeSharp` uses `Metamod:Source` as its main way of communicating with the game server. To install it, you can follow the detailed instructions found <a href="https://cs2.poggu.me/metamod/installation/" target="_blank">here</a>.
### Installing CounterStrikeSharp
Download the latest release of CounterStrikeSharp from <a href="https://github.com/roflmuffin/CounterStrikeSharp/actions/workflows/cmake-single-platform.yml" target="_blank">GitHub actions build pages</a> (just choose the latest development snapshot). **You may need to be logged into GitHub to gain access to the downloads**.
:::caution[.NET Runtime]
If this is your first time installing, you will need to download the `with-runtime` version. This includes a copy of the .NET runtime, which is required to run the plugin.
Depending on the os you might also either need to install `libicu` / `icu-libs` / `libicu-dev` using your package manager for .NET to run or setting `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true` in your servers environment variables. You can find more infos about that <a href="https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-invariant-mode.md#enabling-the-invariant-mode" target="_blank">here</a>
Subsequent upgrades will not require the runtime, unless a version bump of the .NET runtime is required (i.e. from 7.0.x to 8.0.x). We will inform you when this occurs.
:::
Extract the `addons` folder to the `/csgo/` directory of the dedicated server. The contents of your addons folder should contain both the `counterstrikesharp` folder and the `metamod` folder as seen below.
```shell
<server_path>/game/csgo/addons > tree -L 2
addons
├── counterstrikesharp
│   ├── api
│   ├── bin
│   ├── dotnet
│   ├── plugins
│ └── gamedata
├── metamod
│   ├── bin
│   ├── counterstrikesharp.vdf
│   ├── metaplugins.ini
│   └── README.txt
├── metamod.vdf
└── metamod_x64.vdf
```
### Start the Server
Launch your CS2 dedicated server as normal. If everything is working correctly, you should see a message in the console that says `CSSharp: CounterStrikeSharp.API Loaded Successfully.`.
Running the command `meta list` in the console should show 1 plugin loaded 🎉
```shell
meta list
Listing 1 plugin:
[01] CounterStrikeSharp (0.1.0) by Roflmuffin
```

View File

@@ -1,52 +0,0 @@
---
title: CounterStrikeSharp
description: Write Counter-Strike 2 server plugins in C#.
template: splash
hero:
tagline: <code class="csharp">CounterStrikeSharp</code> is a simpler way to write CS2 server plugins.
actions:
- text: Get started
link: ./guides/getting-started/
icon: right-arrow
variant: primary
- text: Browse source
link: https://github.com/roflmuffin/CounterStrikeSharp/
icon: github
---
```csharp
using CounterStrikeSharp.API.Core;
namespace HelloWorldPlugin;
public class HelloWorldPlugin : BasePlugin
{
public override string ModuleName => "Hello World Plugin";
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!");
}
[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);
return HookResult.Continue;
}
[ConsoleCommand("issue_warning", "Issue warning to player")]
public void OnCommand(CCSPlayerController? player, CommandInfo command)
{
Logger.LogWarning("Player shouldn't be doing that");
}
}
```

2
docs/src/env.d.ts vendored
View File

@@ -1,2 +0,0 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View File

@@ -1,19 +0,0 @@
:root {
--sl-font: 'DM Sans', serif;
--sl-font-mono: 'Jetbrains Mono', monospace;
}
.hero {
grid-template-columns: 1fr;
}
.hero .copy {
align-items: center;
}
.hero .actions {
justify-content: center;
}
.hero {
}

View File

@@ -1,3 +0,0 @@
{
"extends": "astro/tsconfigs/strict"
}

View File

@@ -0,0 +1,2 @@
# With Entity Output Hooks
This example shows how to implement hooks for entity output, such as StartTouch, OnPickup etc.

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,43 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using Microsoft.Extensions.Logging;
namespace WithEntityOutputHooks;
[MinimumApiVersion(80)]
public class WithEntityOutputHooksPlugin : BasePlugin
{
public override string ModuleName => "Example: With Entity Output Hooks";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that showcases entity output hooks";
public override void Load(bool hotReload)
{
HookEntityOutput("weapon_knife", "OnPlayerPickup", (CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay) =>
{
Logger.LogInformation("weapon_knife called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay);
return HookResult.Continue;
});
}
// Output hooks can use wildcards to match multiple entities
[EntityOutputHook("*", "OnPlayerPickup")]
public HookResult OnPickup(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay)
{
Logger.LogInformation("[EntityOutputHook Attribute] Called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay);
return HookResult.Continue;
}
// Output hooks can use wildcards to match multiple output names
[EntityOutputHook("func_buyzone", "*")]
public HookResult OnTouchStart(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay)
{
Logger.LogInformation("[EntityOutputHook Attribute] Buyzone called output ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay);
return HookResult.Continue;
}
}

View File

@@ -0,0 +1,8 @@
# With Translations
This example shows how to use an `IStringLocalizer` and language `json` files to provide localization for your plugins.
## How to use
1. Add the `IStringLocalizer` to your services with dependency injection, or use the `Localizer` provided on the plugin instance.
2. Add a `lang` folder to your plugin, and add a `json` file for each language you want to support. The name of the file should be a locale code, like `en.json` or `fr.json` etc.
3. Ensure that the `lang` folder is shipped with your plugin, see the example `.csproj` file for an example to auto-copy to the output folder.
4. Use the `IStringLocalizer` to localize your strings.

View File

@@ -0,0 +1,15 @@
<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>
<ItemGroup>
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,52 @@
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.Logging;
namespace WithTranslations;
[MinimumApiVersion(80)]
public class WithTranslationsPlugin : BasePlugin
{
public override string ModuleName => "Example: With Translations";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that provides translations";
public override void Load(bool hotReload)
{
// A global `Localizer` is provided on the plugin instance.
// You can also use dependency injection to inject `IStringLocalizer` in your own services.
Logger.LogInformation("This message is in the server language: {Message}", Localizer["test.translation"]);
// IStringLocalizer can accept standard string format arguments.
// "This number has 2 decimal places {0:n2}" -> "This number has 2 decimal places 123.55"
Logger.LogInformation(Localizer["test.format", 123.551]);
// This message has colour codes
Server.PrintToChatAll(Localizer["test.colors"]);
// This message has colour codes and formatted values
Server.PrintToChatAll(Localizer["test.colors.withformat", 123.551]);
}
[ConsoleCommand("css_replylanguage", "Test Translations")]
public void OnCommandReplyLanguage(CCSPlayerController? player, CommandInfo command)
{
// Commands are executed in a players provided culture (or fallback to server culture).
// Players can configure their language using the `!lang` or `css_lang` command.
Logger.LogInformation("Current Culture is {Culture}", CultureInfo.CurrentCulture);
command.ReplyToCommand(Localizer["test.translation"]);
if (player != null)
{
// You can also get the players language using the `GetLanguage` extension method.
// This will always return a culture, defaulting to the server culture if the user has not configured it.
var language = player.GetLanguage();
}
}
}

View File

@@ -0,0 +1,3 @@
{
"test.translation": "This is the english (GB) translation"
}

View File

@@ -0,0 +1,6 @@
{
"test.translation": "This is the english translation",
"test.format": "This number has 2 decimal places {0:n2}",
"test.colors": "{orange}This{default} text has {green}green{default} text",
"test.colors.withformat": "{orange}{0:n2}{default}"
}

View File

@@ -0,0 +1,3 @@
{
"test.translation": "This is the french translation"
}

View File

@@ -0,0 +1,110 @@
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CounterStrikeSharp.API.Tests;
[Collection("Logging collection")]
public class AdminTests
{
public AdminTests()
{
AdminManager.LoadAdminData(TestUtils.GetTestPath("admins.json"));
AdminManager.LoadAdminGroups(TestUtils.GetTestPath("admin_groups.json"));
AdminManager.LoadCommandOverrides(TestUtils.GetTestPath("admin_overrides.json"));
AdminManager.MergeGroupPermsIntoAdmins();
}
[Fact]
public void ShouldReturnValidAdminData()
{
var adminData = AdminManager.GetPlayerAdminData((SteamID)76561197960265731);
Assert.NotNull(adminData);
Assert.Equal("#css/admin", adminData.Groups.Single());
}
[Fact]
public void ShouldUseGroupImmunity()
{
var adminData = AdminManager.GetPlayerAdminData((SteamID)76561197960265731);
Assert.NotNull(adminData);
Assert.Equal(125u, adminData.Immunity); // Group immunity is 125, Admin immunity is 100
AdminManager.SetPlayerImmunity((SteamID)76561197960265731, 150u);
Assert.Equal(150u, adminData.Immunity); // Group immunity is 125, Admin immunity is 100
}
[Fact]
public void ShouldReturnNullAdminData()
{
var adminData = AdminManager.GetPlayerAdminData((SteamID)76561197960265732);
Assert.Null(adminData);
}
[Fact]
public void ShouldReturnValidCommandOverrides()
{
var adminData = AdminManager.GetPlayerAdminData((SteamID)76561197960265731);
Assert.NotNull(adminData);
Assert.True(adminData.CommandOverrides["fake_command"]);
Assert.False(adminData.CommandOverrides["css"]);
}
[Fact]
public void ShouldHandleWildcardDomainFlags()
{
// User has @mycustomplugin/* so should have the admin subflag despite it not being in their group.
Assert.True(AdminManager.PlayerHasPermissions((SteamID)76561197960265731, "@mycustomplugin/admin"));
// User has @mycustomplugin/* so should have the admin subflag despite it not existing anywhere else.
Assert.True(AdminManager.PlayerHasPermissions((SteamID)76561197960265731, "@mycustomplugin/fake"));
// User has @css/root so should have the slay subflag despite it not being in their group.
Assert.True(AdminManager.PlayerHasPermissions((SteamID)76561197960265731, "@css/slay"));
// Flag provided explicitly in the admins.json file
Assert.True(AdminManager.PlayerHasPermissions((SteamID)76561197960265731, "@css/custom-flag-2"));
}
[Fact]
public void ShouldAddFlagsAtRuntime()
{
// Existing player
Assert.False(AdminManager.PlayerHasPermissions((SteamID)76561197960265731, "@runtime/flag1"));
AdminManager.AddPlayerPermissions((SteamID)76561197960265731, "@runtime/flag1");
Assert.True(AdminManager.PlayerHasPermissions((SteamID)76561197960265731, "@runtime/flag1"));
// Non-existent player
Assert.False(AdminManager.PlayerHasPermissions((SteamID)76561197960265730, "@runtime/flag1"));
AdminManager.AddPlayerPermissions((SteamID)76561197960265730, "@runtime/flag1");
Assert.True(AdminManager.PlayerHasPermissions((SteamID)76561197960265730, "@runtime/flag1"));
AdminManager.ClearPlayerPermissions((SteamID)76561197960265730);
Assert.False(AdminManager.PlayerHasPermissions((SteamID)76561197960265730, "@runtime/flag1"));
Assert.Empty(AdminManager.GetPlayerAdminData((SteamID)76561197960265730)!.GetAllFlags());
}
[Fact]
public void ShouldAddPlayerToGroup()
{
AdminManager.AddPlayerToGroup(new SteamID("STEAM_0:1:3"), "#css/root");
var adminData = AdminManager.GetPlayerAdminData(new SteamID("STEAM_0:1:3"));
Assert.NotNull(adminData);
Assert.Equal("#css/root", adminData.Groups.Single());
}
[Fact]
public void ShouldAddPlayerPermissionOverridesAtRuntime()
{
Assert.False(AdminManager.PlayerHasCommandOverride((SteamID)76561197960265731, "runtime_command"));
AdminManager.SetPlayerCommandOverride((SteamID)76561197960265731, "runtime_command", true);
Assert.True(AdminManager.PlayerHasCommandOverride((SteamID)76561197960265731, "runtime_command"));
}
[Fact]
public void ShouldAddCommandPermissionOverridesAtRuntime()
{
Assert.False(AdminManager.CommandIsOverriden("runtime_command"));
AdminManager.AddPermissionOverride("runtime_command", "@runtime/override");
Assert.True(AdminManager.CommandIsOverriden("runtime_command"));
Assert.Equal("@runtime/override", AdminManager.GetPermissionOverrides("runtime_command").Single());
}
}

View File

@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Update="Resources\**\*.*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="lang\en.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="lang\en-GB.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="lang\fr.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
using CounterStrikeSharp.API.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Tests.Fixtures;
public class CoreLoggingFixture : IDisposable
{
public CoreLoggingFixture()
{
var serviceProvider = new ServiceCollection()
.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddCoreLogging(TestUtils.GetTestPath(""));
})
.BuildServiceProvider();
}
public void Dispose()
{
// TODO release managed resources here
}
}

View File

@@ -0,0 +1,6 @@
namespace CounterStrikeSharp.API.Tests.Fixtures;
[CollectionDefinition("Logging collection")]
public class LoggingCollection : ICollectionFixture<CoreLoggingFixture>
{
}

View File

@@ -0,0 +1,22 @@
{
"#css/admin": {
"flags": [
"@css/reservation",
"@css/generic",
"@css/kick",
"@css/ban",
"@css/unban",
"@css/vip",
"@css/changemap",
"@css/cvar",
"@css/config",
"@css/chat",
"@css/vote",
"@css/password",
"@css/rcon",
"@css/cheats",
"@css/root"
],
"immunity": 125
}
}

View File

@@ -0,0 +1,9 @@
{
"example_command": {
"flags": [
"@css/custom-permission"
],
"check_type": "all",
"enabled": true
}
}

View File

@@ -0,0 +1,23 @@
{
"Erikj": {
"identity": "76561197960265731",
"immunity": 100,
"flags": [
"@css/custom-flag-1",
"@css/custom-flag-2",
"@mycustomplugin/*"
],
"groups": [
"#css/admin"
],
"command_overrides": {
"css_plugins": true,
"css": false,
"fake_command": true
}
},
"Another erikj": {
"identity": "STEAM_0:1:1",
"flags": ["@mycustomplugin/admin"]
}
}

View File

@@ -0,0 +1,3 @@
{
"test.translation": "This is the english (GB) translation"
}

View File

@@ -0,0 +1,6 @@
{
"test.translation": "This is the english translation",
"test.format": "This number has 2 decimal places {0:n2}",
"test.colors": "{orange}This{default} text has {green}green{default} text",
"test.colors.withformat": "{orange}{0:n2}{default}"
}

View File

@@ -0,0 +1,3 @@
{
"test.translation": "This is the french translation"
}

View File

@@ -0,0 +1,52 @@
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
namespace CounterStrikeSharp.API.Tests;
public class SteamIdTests
{
[Theory]
[InlineData(76561197960524373ul, 76561197960524373ul, "STEAM_0:1:129322", "[U:1:258645]", 258645, SteamAccountType.Individual, SteamAccountInstance.Desktop, SteamAccountUniverse.Public, true)]
[InlineData(258645, 76561197960524373ul, "STEAM_0:1:129322", "[U:1:258645]", 258645, SteamAccountType.Individual, SteamAccountInstance.Desktop, SteamAccountUniverse.Public, true)]
[InlineData(76561197960265728ul, 76561197960265728ul, "STEAM_0:0:0", "[U:1:0]", 0, SteamAccountType.Individual, SteamAccountInstance.Desktop, SteamAccountUniverse.Public, false)]
[InlineData(103582791429521412ul, 103582791429521412ul, "STEAM_0:0:13510796734627842", "[g:1:27021593469255684]", 4, SteamAccountType.Clan, SteamAccountInstance.All, SteamAccountUniverse.Public, true)]
public void ValidateSteamId(ulong parseValue, ulong steamId64, string steamId2, string steamId3, int steamId32, SteamAccountType accountType, SteamAccountInstance accountInstance, SteamAccountUniverse accountUniverse, bool valid)
{
var steamId = new SteamID(parseValue);
Assert.Equal(steamId64, steamId.SteamId64);
Assert.Equal(steamId2, steamId.SteamId2);
Assert.Equal(steamId3, steamId.SteamId3);
Assert.Equal(steamId32, steamId.SteamId32);
Assert.Equal(accountType, steamId.AccountType);
Assert.Equal(accountInstance, steamId.AccountInstance);
Assert.Equal(accountUniverse, steamId.AccountUniverse);
Assert.Equal(valid, steamId.IsValid());
}
[Theory]
[InlineData(76561197960524373ul, 76561197960524373ul)]
[InlineData("STEAM_0:1:129322", 76561197960524373ul)]
[InlineData("[U:1:258645]", 76561197960524373ul)]
public void CanCastLongToString(dynamic parseable, ulong longValue)
{
Assert.Equal(longValue, ((SteamID)parseable).SteamId64);
}
[Fact]
public void CanUseValueEquality()
{
var steamId1 = new SteamID(76561197960524373ul);
var steamId2 = new SteamID(76561197960524373ul);
var steamId3 = new SteamID(76561197960265728ul);
Assert.True(steamId1 == steamId2);
Assert.True(steamId1 != steamId3);
}
[Fact]
public void ThrowsOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new SteamID(0));
}
}

View File

@@ -0,0 +1,14 @@
using System.Reflection;
namespace CounterStrikeSharp.API.Tests;
public static class TestUtils
{
public static string GetTestPath(string relativePath)
{
var codeBaseUrl = new Uri(Assembly.GetExecutingAssembly().Location);
var codeBasePath = Uri.UnescapeDataString(codeBaseUrl.AbsolutePath);
var dirPath = Path.GetDirectoryName(codeBasePath);
return Path.Combine(dirPath, "Resources", relativePath);
}
}

View File

@@ -0,0 +1,87 @@
using System.Globalization;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Translations;
using Microsoft.Extensions.Localization;
using Moq;
namespace CounterStrikeSharp.API.Tests;
public class TranslationTests
{
private readonly JsonStringLocalizerFactory _factory;
private readonly IStringLocalizer _localizer;
public TranslationTests()
{
var pluginContextMock = new Mock<IPluginContext>();
pluginContextMock.SetupGet(x => x.FilePath).Returns(TestUtils.GetTestPath("test_plugin.dll"));
_factory = new JsonStringLocalizerFactory(pluginContextMock.Object);
_localizer = _factory.Create(this.GetType());
// This is generally derived from the core config, but for the sake of these tests we default to `en`.
var serverCulture = CultureInfo.GetCultureInfo("en");
CultureInfo.DefaultThreadCurrentUICulture = serverCulture;
CultureInfo.DefaultThreadCurrentCulture = serverCulture;
CultureInfo.CurrentUICulture = serverCulture;
CultureInfo.CurrentCulture = serverCulture;
}
[Fact]
public void TranslatesLanguagesCorrectly()
{
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("en")))
{
Assert.Equal("This is the english translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("en-US")))
{
Assert.Equal("This is the english translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("en-GB")))
{
Assert.Equal("This is the english (GB) translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("fr")))
{
Assert.Equal("This is the french translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("fr-FR")))
{
Assert.Equal("This is the french translation", _localizer["test.translation"]);
}
}
[Fact]
public void ShouldFallbackToServerLanguage()
{
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("de")))
{
Assert.Equal("This is the english translation", _localizer["test.translation"]);
}
}
[Fact]
public void ShouldReturnKeyIfNotFound()
{
Assert.Equal("test.notfound", _localizer["test.notfound"]);
}
[Fact]
public void ShouldHandleFormatStrings()
{
Assert.Equal("This number has 2 decimal places 0.25", _localizer["test.format", 0.251]);
}
[Fact]
public void HandlesColorFormatting()
{
// Sets invisible pre-color if there is a color code in the string.
Assert.Equal(" \x10This\x01 text has \x04green\x01 text", _localizer["test.colors"]);
Assert.Equal($" {'\x10'}1.25\x01", _localizer["test.colors.withformat", 1.25]);
}
}

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
@@ -7,6 +8,7 @@ using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Core.Translations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -38,6 +40,7 @@ public static class Bootstrap
services.AddSingleton<IScriptHostConfiguration, ScriptHostConfiguration>();
services.AddScoped<Application>();
services.AddSingleton<IPluginManager, PluginManager>();
services.AddSingleton<IPlayerLanguageManager, PlayerLanguageManager>();
services.AddScoped<IPluginContextQueryHandler, PluginContextQueryHandler>();
services.Scan(i => i.FromCallingAssembly()

View File

@@ -479,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();
@@ -644,6 +654,32 @@ namespace CounterStrikeSharp.API.Core
}
}
public static void HookEntityOutput(string classname, string outputname, InputArgument callback, HookMode mode){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(classname);
ScriptContext.GlobalScriptContext.Push(outputname);
ScriptContext.GlobalScriptContext.Push((InputArgument)callback);
ScriptContext.GlobalScriptContext.Push(mode);
ScriptContext.GlobalScriptContext.SetIdentifier(0x15245242);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static void UnhookEntityOutput(string classname, string outputname, InputArgument callback, HookMode mode){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(classname);
ScriptContext.GlobalScriptContext.Push(outputname);
ScriptContext.GlobalScriptContext.Push((InputArgument)callback);
ScriptContext.GlobalScriptContext.Push(mode);
ScriptContext.GlobalScriptContext.SetIdentifier(0x87DBD139);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static void HookEvent(string name, InputArgument callback, bool ispost){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();

View File

@@ -14,13 +14,16 @@
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System.Globalization;
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.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Logging;
@@ -40,10 +43,11 @@ namespace CounterStrikeSharp.API.Core
private readonly CoreConfig _coreConfig;
private readonly IPluginManager _pluginManager;
private readonly IPluginContextQueryHandler _pluginContextQueryHandler;
private readonly IPlayerLanguageManager _playerLanguageManager;
public Application(ILoggerFactory loggerFactory, IScriptHostConfiguration scriptHostConfiguration,
GameDataProvider gameDataProvider, CoreConfig coreConfig, IPluginManager pluginManager,
IPluginContextQueryHandler pluginContextQueryHandler)
IPluginContextQueryHandler pluginContextQueryHandler, IPlayerLanguageManager playerLanguageManager)
{
Logger = loggerFactory.CreateLogger("Core");
_scriptHostConfiguration = scriptHostConfiguration;
@@ -51,6 +55,7 @@ namespace CounterStrikeSharp.API.Core
_coreConfig = coreConfig;
_pluginManager = pluginManager;
_pluginContextQueryHandler = pluginContextQueryHandler;
_playerLanguageManager = playerLanguageManager;
_instance = this;
}
@@ -61,19 +66,20 @@ namespace CounterStrikeSharp.API.Core
_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 adminGroupsPath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admin_groups.json");
Logger.LogInformation("Loading Admin Groups from {Path}", adminGroupsPath);
AdminManager.LoadAdminGroups(adminGroupsPath);
var overridePath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admin_overrides.json");
Logger.LogInformation("Loading Admin Command Overrides from {Path}", overridePath);
AdminManager.LoadCommandOverrides(overridePath);
AdminManager.MergeGroupPermsIntoAdmins();
AdminManager.AddCommands();
_pluginManager.Load();
@@ -172,7 +178,8 @@ namespace CounterStrikeSharp.API.Core
try
{
_pluginManager.LoadPlugin(path);
} catch (Exception e)
}
catch (Exception e)
{
info.ReplyToCommand($"Could not load plugin \"{path}\")", true);
}
@@ -244,11 +251,44 @@ namespace CounterStrikeSharp.API.Core
}
}
[CommandHelper(usage: "[language code, e.g. \"de\", \"pl\", \"en\"]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnLangCommand(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
SteamID steamId = (SteamID)player.SteamID;
if (command.ArgCount == 1)
{
var language = _playerLanguageManager.GetLanguage(steamId);
command.ReplyToCommand(string.Format("Current language is \"{0}\" ({1})", language.Name, language.NativeName));
return;
}
if (command.ArgCount != 2)
{
return;
}
try
{
var language = command.GetArg(1);
var cultureInfo = CultureInfo.GetCultures(CultureTypes.AllCultures).Single(x => x.Name == language);
_playerLanguageManager.SetLanguage(steamId, cultureInfo);
command.ReplyToCommand($"Language set to {cultureInfo.NativeName}");
}
catch (InvalidOperationException)
{
command.ReplyToCommand("Language not found.");
}
}
private void RegisterPluginCommands()
{
CommandUtils.AddStandaloneCommand("css", "Counter-Strike Sharp options.", OnCSSCommand);
CommandUtils.AddStandaloneCommand("css_plugins", "Counter-Strike Sharp plugin options.",
OnCSSPluginCommand);
CommandUtils.AddStandaloneCommand("css_lang", "Set Counter-Strike Sharp language", OnLangCommand);
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace CounterStrikeSharp.API.Core.Attributes.Registration;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class EntityOutputHookAttribute : Attribute
{
public string Classname { get; }
public string OutputName { get; }
public EntityOutputHookAttribute(string classname, string outputName)
{
Classname = classname;
OutputName = outputName;
}
}

View File

@@ -21,11 +21,14 @@ using System.Linq;
using System.Reflection;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Config;
using CounterStrikeSharp.API.Modules.Entities;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -36,6 +39,13 @@ namespace CounterStrikeSharp.API.Core
public BasePlugin()
{
RegisterListener<Listeners.OnMapEnd>(() =>
{
foreach (KeyValuePair<Delegate, EntityIO.EntityOutputCallback> callback in EntitySingleOutputHooks)
{
UnhookSingleEntityOutputInternal(callback.Value.Classname, callback.Value.Output, callback.Value.Handler);
}
});
}
public abstract string ModuleName { get; }
@@ -50,6 +60,8 @@ namespace CounterStrikeSharp.API.Core
public string ModuleDirectory => Path.GetDirectoryName(ModulePath);
public ILogger Logger { get; set; }
public IStringLocalizer Localizer { get; set; }
public virtual void Load(bool hotReload)
{
}
@@ -108,6 +120,12 @@ namespace CounterStrikeSharp.API.Core
public readonly Dictionary<Delegate, CallbackSubscriber> Listeners =
new Dictionary<Delegate, CallbackSubscriber>();
public readonly Dictionary<Delegate, CallbackSubscriber> EntityOutputHooks =
new Dictionary<Delegate, CallbackSubscriber>();
internal readonly Dictionary<Delegate, EntityIO.EntityOutputCallback> EntitySingleOutputHooks =
new Dictionary<Delegate, EntityIO.EntityOutputCallback>();
public readonly List<Timer> Timers = new List<Timer>();
public delegate HookResult GameEventHandler<T>(T @event, GameEventInfo info) where T : GameEvent;
@@ -157,38 +175,52 @@ namespace CounterStrikeSharp.API.Core
var caller = (i != -1) ? new CCSPlayerController(NativeAPI.GetEntityFromIndex(i + 1)) : null;
var command = new CommandInfo(ptr, caller);
using var temporaryCulture = new WithTemporaryCulture(caller.GetLanguage());
var methodInfo = handler?.GetMethodInfo();
if (!AdminManager.CommandIsOverriden(name))
// We do not need to do permission checks on commands executed from the server console.
// The server will always be allowed to execute commands (unless marked as client only like above)
if (caller != null)
{
// Do not execute command if we do not have the correct permissions.
var permissions = methodInfo?.GetCustomAttributes<BaseRequiresPermissions>();
if (permissions != null)
var adminData = AdminManager.GetPlayerAdminData(caller!.AuthorizedSteamID);
var permissionsToCheck = new List<BaseRequiresPermissions>();
// If our command is overriden, we dynamically create a new permissions attribute
// based on the data that is stored in admin_overrides.json.
if (AdminManager.CommandIsOverriden(name))
{
foreach (var attr in permissions)
{
attr.Command = name;
if (!attr.CanExecuteCommand(caller))
{
command.ReplyToCommand("[CSS] You do not have the correct permissions to execute this command.");
return;
}
}
}
}
// If this command has it's permissions overriden, we will do an AND check for all permissions.
else
{
// I don't know if this is the most sane implementation of this, can be edited in code review.
var data = AdminManager.GetCommandOverrideData(name);
if (data != null)
{
var attrType = (data.CheckType == "all") ? typeof(RequiresPermissions) : typeof(RequiresPermissionsOr);
var attr = (BaseRequiresPermissions)Activator.CreateInstance(attrType, args: AdminManager.GetPermissionOverrides(name));
if (attr != null) permissionsToCheck.Add(attr);
}
}
// The permissions for this command are not being overriden here, so we
// grab the permissions to check straight from the attribute.
else
{
var permissions = methodInfo?.GetCustomAttributes<BaseRequiresPermissions>();
if (permissions != null) permissionsToCheck.AddRange(permissions);
}
foreach (var attr in permissionsToCheck)
{
attr.Command = name;
if (!attr.CanExecuteCommand(caller))
{
command.ReplyToCommand("[CSS] You do not have the correct permissions to execute this command.");
var responseStr = (attr.GetType() == typeof(RequiresPermissions)) ?
"You are missing the correct permissions" : "You do not have one of the correct permissions";
var flags = attr.Permissions.Except(adminData?.GetAllFlags() ?? new HashSet<string>());
flags = flags.Except(adminData?.Groups ?? new HashSet<string>());
command.ReplyToCommand($"[CSS] {responseStr} ({string.Join(", ", flags)}) to execute this command.");
return;
}
}
@@ -202,9 +234,11 @@ namespace CounterStrikeSharp.API.Core
{
case CommandUsage.CLIENT_AND_SERVER: break; // Allow command through.
case CommandUsage.CLIENT_ONLY:
if (caller == null || !caller.IsValid) { command.ReplyToCommand("[CSS] This command can only be executed by clients."); return; } break;
if (caller == null || !caller.IsValid) { command.ReplyToCommand("[CSS] This command can only be executed by clients."); return; }
break;
case CommandUsage.SERVER_ONLY:
if (caller != null && caller.IsValid) { command.ReplyToCommand("[CSS] This command can only be executed by the server."); return; } break;
if (caller != null && caller.IsValid) { command.ReplyToCommand("[CSS] This command can only be executed by the server."); return; }
break;
default: throw new ArgumentException("Unrecognised CommandUsage value passed in CommandHelperAttribute.");
}
@@ -212,7 +246,12 @@ namespace CounterStrikeSharp.API.Core
// but we'll just ignore that for this check.
if (helperAttribute.MinArgs != 0 && command.ArgCount - 1 < helperAttribute.MinArgs)
{
command.ReplyToCommand($"[CSS] Expected usage: \"!{command.ArgByIndex(0)} {helperAttribute.Usage}\".");
// Remove the "css_" from the beginning of the command name if it's present.
// Most of the time, users will be calling commands from chat.
var commandCalled = command.ArgByIndex(0);
var properCommandName = (commandCalled.StartsWith("css_")) ? commandCalled.Replace("css_", "") : commandCalled;
command.ReplyToCommand($"[CSS] Expected usage: \"!{properCommandName} {helperAttribute.Usage}\".");
return;
}
}
@@ -354,6 +393,7 @@ namespace CounterStrikeSharp.API.Core
{
this.RegisterAttributeHandlers(instance);
this.RegisterConsoleCommandAttributeHandlers(instance);
this.RegisterEntityOutputAttributeHandlers(instance);
}
public void InitializeConfig(object instance, Type pluginType)
@@ -430,6 +470,77 @@ namespace CounterStrikeSharp.API.Core
}
}
public void RegisterEntityOutputAttributeHandlers(object instance)
{
var handlers = instance.GetType()
.GetMethods()
.Where(method => method.GetCustomAttributes<EntityOutputHookAttribute>().Any())
.ToArray();
foreach (var handler in handlers)
{
var attributes = handler.GetCustomAttributes<EntityOutputHookAttribute>();
foreach (var outputInfo in attributes)
{
HookEntityOutput(outputInfo.Classname, outputInfo.OutputName, handler.CreateDelegate<EntityIO.EntityOutputHandler>(instance));
}
}
}
public void HookEntityOutput(string classname, string outputName, EntityIO.EntityOutputHandler handler, HookMode mode = HookMode.Pre)
{
var subscriber = new CallbackSubscriber(handler, handler,
() => UnhookEntityOutput(classname, outputName, handler));
NativeAPI.HookEntityOutput(classname, outputName, subscriber.GetInputArgument(), mode);
EntityOutputHooks[handler] = subscriber;
}
public void UnhookEntityOutput(string classname, string outputName, EntityIO.EntityOutputHandler handler, HookMode mode = HookMode.Pre)
{
if (!EntityOutputHooks.TryGetValue(handler, out var subscriber)) return;
NativeAPI.UnhookEntityOutput(classname, outputName, subscriber.GetInputArgument(), mode);
FunctionReference.Remove(subscriber.GetReferenceIdentifier());
EntityOutputHooks.Remove(handler);
}
public void HookSingleEntityOutput(CEntityInstance entityInstance, string outputName, EntityIO.EntityOutputHandler handler)
{
// since we wrap around the plugin handler we need to do this to ensure that the plugin callback is only called
// if the entity instance is the same.
EntityIO.EntityOutputHandler internalHandler = (output, name, activator, caller, value, delay) =>
{
if (caller == entityInstance)
{
return handler(output, name, activator, caller, value, delay);
}
return HookResult.Continue;
};
HookEntityOutput(entityInstance.DesignerName, outputName, internalHandler);
// because of ^ we would not be able to unhook since we passed the 'internalHandler' and that's what is being stored, not the original handler
// but the plugin could only pass the original handler for unhooking.
// (this dictionary does not needed to be cleared on dispose as it has no unmanaged reference and those are already being disposed, but on map end)
// (the internal class is needed to be able to remove them on map start)
EntitySingleOutputHooks[handler] = new EntityIO.EntityOutputCallback(entityInstance.DesignerName, outputName, internalHandler);
}
public void UnhookSingleEntityOutput(CEntityInstance entityInstance, string outputName, EntityIO.EntityOutputHandler handler)
{
UnhookSingleEntityOutputInternal(entityInstance.DesignerName, outputName, handler);
}
private void UnhookSingleEntityOutputInternal(string classname, string outputName, EntityIO.EntityOutputHandler handler)
{
if (!EntitySingleOutputHooks.TryGetValue(handler, out var internalHandler)) return;
UnhookEntityOutput(classname, outputName, internalHandler.Handler);
EntitySingleOutputHooks.Remove(handler);
}
public void Dispose()
{
Dispose(true);
@@ -464,6 +575,11 @@ namespace CounterStrikeSharp.API.Core
subscriber.Dispose();
}
foreach (var subscriber in EntityOutputHooks.Values)
{
subscriber.Dispose();
}
foreach (var timer in Timers)
{
timer.Kill();

View File

@@ -1,4 +1,4 @@
/*
/*
* 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
@@ -23,6 +23,8 @@ using CounterStrikeSharp.API.Modules.Utils;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using Microsoft.Extensions.Hosting;
@@ -46,6 +48,9 @@ namespace CounterStrikeSharp.API.Core
[JsonPropertyName("PluginHotReloadEnabled")]
public bool PluginHotReloadEnabled { get; set; } = true;
[JsonPropertyName("ServerLanguage")]
public string ServerLanguage { get; set; } = "en";
}
/// <summary>
@@ -88,6 +93,8 @@ namespace CounterStrikeSharp.API.Core
/// When enabled, plugins are automatically reloaded when their .dll file is updated.
/// </summary>
public static bool PluginHotReloadEnabled => _coreConfig.PluginHotReloadEnabled;
public static string ServerLanguage => _coreConfig.ServerLanguage;
}
public partial class CoreConfig : IStartupService
@@ -139,6 +146,20 @@ namespace CounterStrikeSharp.API.Core
_logger.LogWarning(ex, "Failed to load core configuration, fallback values will be used");
}
var serverCulture = CultureInfo.GetCultures(CultureTypes.AllCultures)
.FirstOrDefault(x => x.Name == ServerLanguage);
if (serverCulture == null)
{
_logger.LogWarning("Server Language \"{ServerLanguage}\" is not supported, falling back to \"en\"", ServerLanguage);
serverCulture = new CultureInfo("en");
_coreConfig.ServerLanguage = "en";
}
CultureInfo.DefaultThreadCurrentUICulture = serverCulture;
CultureInfo.DefaultThreadCurrentCulture = serverCulture;
CultureInfo.CurrentUICulture = serverCulture;
CultureInfo.CurrentCulture = serverCulture;
_logger.LogInformation("Successfully loaded core configuration");
}
}

View File

@@ -34,34 +34,58 @@ public class Offsets
public sealed class GameDataProvider : IStartupService
{
private readonly string _gameDataFilePath;
private readonly string _gameDataDirectoryPath;
public Dictionary<string,LoadedGameData> Methods;
private readonly ILogger<GameDataProvider> _logger;
public GameDataProvider(IScriptHostConfiguration scriptHostConfiguration, ILogger<GameDataProvider> logger)
{
_logger = logger;
_gameDataFilePath = Path.Join(scriptHostConfiguration.GameDataPath, "gamedata.json");
_gameDataDirectoryPath = scriptHostConfiguration.GameDataPath;
}
public void Load()
{
try
{
Methods = JsonSerializer.Deserialize<Dictionary<string, LoadedGameData>>(File.ReadAllText(_gameDataFilePath))!;
Methods = new Dictionary<string, LoadedGameData>();
foreach (string filePath in Directory.EnumerateFiles(_gameDataDirectoryPath, "*.json"))
{
string jsonContent = File.ReadAllText(filePath);
Dictionary<string, LoadedGameData> loadedMethods = JsonSerializer.Deserialize<Dictionary<string, LoadedGameData>>(jsonContent)!;
foreach (KeyValuePair<string, LoadedGameData> loadedMethod in loadedMethods)
{
if (Methods.ContainsKey(loadedMethod.Key))
{
_logger.LogWarning("GameData Method \"{Key}\" loaded a duplicate entry from {filePath}.", loadedMethod.Key, filePath);
}
Methods[loadedMethod.Key] = loadedMethod.Value;
}
if (loadedMethods != null)
{
_logger.LogInformation("Successfully loaded {Count} game data entries from {Path}", loadedMethods.Count, filePath);
}
else
{
_logger.LogWarning("Unable to load game data entries from {Path}, game data file is empty", filePath);
}
}
}
catch (Exception ex)
{
_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)
{
Application.Instance.Logger.LogDebug("Getting signature: {Key}", key);

View File

@@ -16,6 +16,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -55,6 +56,8 @@ namespace CounterStrikeSharp.API.Core
ILogger Logger { get; set; }
IStringLocalizer Localizer { get; set; }
void RegisterAllAttributes(object instance);
void InitializeConfig(object instance, Type pluginType);

View File

@@ -1,4 +1,4 @@
using System;
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
@@ -26,4 +26,6 @@ public partial class CBaseEntity
/// Shorthand for accessing an entity's CBodyComponent?.SceneNode?.AbsRotation;
/// </summary>
public QAngle? AbsRotation => CBodyComponent?.SceneNode?.AbsRotation;
public T? GetVData<T>() where T : CEntitySubclassVDataBase => (T)Activator.CreateInstance(typeof(T), Marshal.ReadIntPtr(SubclassID.Handle + 8));
}

View File

@@ -0,0 +1,6 @@
namespace CounterStrikeSharp.API.Core;
public partial class CBasePlayerWeapon
{
public CBasePlayerWeaponVData? VData => GetVData<CBasePlayerWeaponVData>();
}

View File

@@ -0,0 +1,6 @@
namespace CounterStrikeSharp.API.Core;
public partial class CCSWeaponBase
{
public new CCSWeaponBaseVData? VData => GetVData<CCSWeaponBaseVData>();
}

View File

@@ -0,0 +1,49 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Core;
public partial class CEntityIOOutput
{
public EntityIOConnection_t? Connections => Utilities.GetPointer<EntityIOConnection_t>(Handle + 8);
public EntityIOOutputDesc_t Description => new(Marshal.ReadIntPtr(Handle + 16));
}
public class EntityIOOutputDesc_t : NativeObject
{
public EntityIOOutputDesc_t(IntPtr pointer) : base(pointer)
{
}
public string Name => Utilities.ReadStringUtf8(Handle + 0);
public unsafe ref uint Flags => ref Unsafe.AsRef<uint>((void*)(Handle + 8));
public unsafe ref uint OutputOffset => ref Unsafe.AsRef<uint>((void*)(Handle + 16));
}
public class EntityIOConnection_t : EntityIOConnectionDesc_t
{
public EntityIOConnection_t(IntPtr pointer) : base(pointer)
{
}
public unsafe ref bool MarkedForRemoval => ref Unsafe.AsRef<bool>((void*)(Handle + 40));
public EntityIOConnection_t? Next => Utilities.GetPointer<EntityIOConnection_t>(Handle + 48);
}
public class EntityIOConnectionDesc_t : NativeObject
{
public EntityIOConnectionDesc_t(IntPtr pointer) : base(pointer)
{
}
public string TargetDesc => Utilities.ReadStringUtf8(Handle + 0);
public string TargetInput => Utilities.ReadStringUtf8(Handle + 8);
public string ValueOverride => Utilities.ReadStringUtf8(Handle + 16);
public CEntityHandle Target => new(Handle + 24);
public unsafe ref EntityIOTargetType_t TargetType => ref Unsafe.AsRef<EntityIOTargetType_t>((void*)(Handle + 28));
public unsafe ref int TimesToFire => ref Unsafe.AsRef<int>((void*)(Handle + 32));
public unsafe ref float Delay => ref Unsafe.AsRef<float>((void*)(Handle + 36));
}

View File

@@ -0,0 +1,13 @@
namespace CounterStrikeSharp.API.Core;
/// <summary>
/// Placeholder for CVariant
/// <see href="https://github.com/alliedmodders/hl2sdk/blob/cs2/public/variant.h"/>
/// <remarks>A lot of entity outputs do not use this value</remarks>
/// </summary>
public class CVariant : NativeObject
{
public CVariant(IntPtr pointer) : base(pointer)
{
}
}

View File

@@ -4827,6 +4827,51 @@ public partial class CBasePlayerPawn : CBaseCombatCharacter
}
public partial class CBasePlayerVData : CEntitySubclassVDataBase
{
public CBasePlayerVData (IntPtr pointer) : base(pointer) {}
// m_flHeadDamageMultiplier
public CSkillFloat HeadDamageMultiplier => Schema.GetDeclaredClass<CSkillFloat>(this.Handle, "CBasePlayerVData", "m_flHeadDamageMultiplier");
// m_flChestDamageMultiplier
public CSkillFloat ChestDamageMultiplier => Schema.GetDeclaredClass<CSkillFloat>(this.Handle, "CBasePlayerVData", "m_flChestDamageMultiplier");
// m_flStomachDamageMultiplier
public CSkillFloat StomachDamageMultiplier => Schema.GetDeclaredClass<CSkillFloat>(this.Handle, "CBasePlayerVData", "m_flStomachDamageMultiplier");
// m_flArmDamageMultiplier
public CSkillFloat ArmDamageMultiplier => Schema.GetDeclaredClass<CSkillFloat>(this.Handle, "CBasePlayerVData", "m_flArmDamageMultiplier");
// m_flLegDamageMultiplier
public CSkillFloat LegDamageMultiplier => Schema.GetDeclaredClass<CSkillFloat>(this.Handle, "CBasePlayerVData", "m_flLegDamageMultiplier");
// m_flHoldBreathTime
public ref float HoldBreathTime => ref Schema.GetRef<float>(this.Handle, "CBasePlayerVData", "m_flHoldBreathTime");
// m_flDrowningDamageInterval
public ref float DrowningDamageInterval => ref Schema.GetRef<float>(this.Handle, "CBasePlayerVData", "m_flDrowningDamageInterval");
// m_nDrowningDamageInitial
public ref Int32 DrowningDamageInitial => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerVData", "m_nDrowningDamageInitial");
// m_nDrowningDamageMax
public ref Int32 DrowningDamageMax => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerVData", "m_nDrowningDamageMax");
// m_nWaterSpeed
public ref Int32 WaterSpeed => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerVData", "m_nWaterSpeed");
// m_flUseRange
public ref float UseRange => ref Schema.GetRef<float>(this.Handle, "CBasePlayerVData", "m_flUseRange");
// m_flUseAngleTolerance
public ref float UseAngleTolerance => ref Schema.GetRef<float>(this.Handle, "CBasePlayerVData", "m_flUseAngleTolerance");
// m_flCrouchTime
public ref float CrouchTime => ref Schema.GetRef<float>(this.Handle, "CBasePlayerVData", "m_flCrouchTime");
}
public partial class CBasePlayerWeapon : CEconEntity
{
public CBasePlayerWeapon (IntPtr pointer) : base(pointer) {}
@@ -4857,6 +4902,70 @@ public partial class CBasePlayerWeapon : CEconEntity
}
public partial class CBasePlayerWeaponVData : CEntitySubclassVDataBase
{
public CBasePlayerWeaponVData (IntPtr pointer) : base(pointer) {}
// m_bBuiltRightHanded
public ref bool BuiltRightHanded => ref Schema.GetRef<bool>(this.Handle, "CBasePlayerWeaponVData", "m_bBuiltRightHanded");
// m_bAllowFlipping
public ref bool AllowFlipping => ref Schema.GetRef<bool>(this.Handle, "CBasePlayerWeaponVData", "m_bAllowFlipping");
// m_bIsFullAuto
public ref bool IsFullAuto => ref Schema.GetRef<bool>(this.Handle, "CBasePlayerWeaponVData", "m_bIsFullAuto");
// m_nNumBullets
public ref Int32 NumBullets => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerWeaponVData", "m_nNumBullets");
// m_sMuzzleAttachment
public string MuzzleAttachment
{
get { return Schema.GetUtf8String(this.Handle, "CBasePlayerWeaponVData", "m_sMuzzleAttachment"); }
set { Schema.SetString(this.Handle, "CBasePlayerWeaponVData", "m_sMuzzleAttachment", value); }
}
// m_iFlags
public ref ItemFlagTypes_t Flags => ref Schema.GetRef<ItemFlagTypes_t>(this.Handle, "CBasePlayerWeaponVData", "m_iFlags");
// m_nPrimaryAmmoType
public ref byte PrimaryAmmoType => ref Schema.GetRef<byte>(this.Handle, "CBasePlayerWeaponVData", "m_nPrimaryAmmoType");
// m_nSecondaryAmmoType
public ref byte SecondaryAmmoType => ref Schema.GetRef<byte>(this.Handle, "CBasePlayerWeaponVData", "m_nSecondaryAmmoType");
// m_iMaxClip1
public ref Int32 MaxClip1 => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerWeaponVData", "m_iMaxClip1");
// m_iMaxClip2
public ref Int32 MaxClip2 => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerWeaponVData", "m_iMaxClip2");
// m_iDefaultClip1
public ref Int32 DefaultClip1 => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerWeaponVData", "m_iDefaultClip1");
// m_iDefaultClip2
public ref Int32 DefaultClip2 => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerWeaponVData", "m_iDefaultClip2");
// m_iWeight
public ref Int32 Weight => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerWeaponVData", "m_iWeight");
// m_bAutoSwitchTo
public ref bool AutoSwitchTo => ref Schema.GetRef<bool>(this.Handle, "CBasePlayerWeaponVData", "m_bAutoSwitchTo");
// m_bAutoSwitchFrom
public ref bool AutoSwitchFrom => ref Schema.GetRef<bool>(this.Handle, "CBasePlayerWeaponVData", "m_bAutoSwitchFrom");
// m_iRumbleEffect
public ref RumbleEffect_t RumbleEffect => ref Schema.GetRef<RumbleEffect_t>(this.Handle, "CBasePlayerWeaponVData", "m_iRumbleEffect");
// m_iSlot
public ref Int32 Slot => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerWeaponVData", "m_iSlot");
// m_iPosition
public ref Int32 Position => ref Schema.GetRef<Int32>(this.Handle, "CBasePlayerWeaponVData", "m_iPosition");
}
public partial class CBaseProp : CBaseAnimGraph
{
public CBaseProp (IntPtr pointer) : base(pointer) {}
@@ -8932,6 +9041,269 @@ public partial class CCSWeaponBaseGun : CCSWeaponBase
}
public partial class CCSWeaponBaseVData : CBasePlayerWeaponVData
{
public CCSWeaponBaseVData (IntPtr pointer) : base(pointer) {}
// m_WeaponType
public ref CSWeaponType WeaponType => ref Schema.GetRef<CSWeaponType>(this.Handle, "CCSWeaponBaseVData", "m_WeaponType");
// m_WeaponCategory
public ref CSWeaponCategory WeaponCategory => ref Schema.GetRef<CSWeaponCategory>(this.Handle, "CCSWeaponBaseVData", "m_WeaponCategory");
// m_GearSlot
public ref gear_slot_t GearSlot => ref Schema.GetRef<gear_slot_t>(this.Handle, "CCSWeaponBaseVData", "m_GearSlot");
// m_GearSlotPosition
public ref Int32 GearSlotPosition => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_GearSlotPosition");
// m_DefaultLoadoutSlot
public ref loadout_slot_t DefaultLoadoutSlot => ref Schema.GetRef<loadout_slot_t>(this.Handle, "CCSWeaponBaseVData", "m_DefaultLoadoutSlot");
// m_sWrongTeamMsg
public string WrongTeamMsg
{
get { return Schema.GetUtf8String(this.Handle, "CCSWeaponBaseVData", "m_sWrongTeamMsg"); }
set { Schema.SetString(this.Handle, "CCSWeaponBaseVData", "m_sWrongTeamMsg", value); }
}
// m_nPrice
public ref Int32 Price => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nPrice");
// m_nKillAward
public ref Int32 KillAward => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nKillAward");
// m_nPrimaryReserveAmmoMax
public ref Int32 PrimaryReserveAmmoMax => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nPrimaryReserveAmmoMax");
// m_nSecondaryReserveAmmoMax
public ref Int32 SecondaryReserveAmmoMax => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nSecondaryReserveAmmoMax");
// m_bMeleeWeapon
public ref bool MeleeWeapon => ref Schema.GetRef<bool>(this.Handle, "CCSWeaponBaseVData", "m_bMeleeWeapon");
// m_bHasBurstMode
public ref bool HasBurstMode => ref Schema.GetRef<bool>(this.Handle, "CCSWeaponBaseVData", "m_bHasBurstMode");
// m_bIsRevolver
public ref bool IsRevolver => ref Schema.GetRef<bool>(this.Handle, "CCSWeaponBaseVData", "m_bIsRevolver");
// m_bCannotShootUnderwater
public ref bool CannotShootUnderwater => ref Schema.GetRef<bool>(this.Handle, "CCSWeaponBaseVData", "m_bCannotShootUnderwater");
// m_szName
public string Name
{
get { return Schema.GetUtf8String(this.Handle, "CCSWeaponBaseVData", "m_szName"); }
set { Schema.SetString(this.Handle, "CCSWeaponBaseVData", "m_szName", value); }
}
// m_szAnimExtension
public string AnimExtension
{
get { return Schema.GetUtf8String(this.Handle, "CCSWeaponBaseVData", "m_szAnimExtension"); }
set { Schema.SetString(this.Handle, "CCSWeaponBaseVData", "m_szAnimExtension", value); }
}
// m_eSilencerType
public ref CSWeaponSilencerType SilencerType => ref Schema.GetRef<CSWeaponSilencerType>(this.Handle, "CCSWeaponBaseVData", "m_eSilencerType");
// m_nCrosshairMinDistance
public ref Int32 CrosshairMinDistance => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nCrosshairMinDistance");
// m_nCrosshairDeltaDistance
public ref Int32 CrosshairDeltaDistance => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nCrosshairDeltaDistance");
// m_flCycleTime
public CFiringModeFloat CycleTime => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flCycleTime");
// m_flMaxSpeed
public CFiringModeFloat MaxSpeed => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flMaxSpeed");
// m_flSpread
public CFiringModeFloat Spread => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flSpread");
// m_flInaccuracyCrouch
public CFiringModeFloat InaccuracyCrouch => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyCrouch");
// m_flInaccuracyStand
public CFiringModeFloat InaccuracyStand => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyStand");
// m_flInaccuracyJump
public CFiringModeFloat InaccuracyJump => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyJump");
// m_flInaccuracyLand
public CFiringModeFloat InaccuracyLand => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyLand");
// m_flInaccuracyLadder
public CFiringModeFloat InaccuracyLadder => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyLadder");
// m_flInaccuracyFire
public CFiringModeFloat InaccuracyFire => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyFire");
// m_flInaccuracyMove
public CFiringModeFloat InaccuracyMove => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyMove");
// m_flRecoilAngle
public CFiringModeFloat RecoilAngle => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flRecoilAngle");
// m_flRecoilAngleVariance
public CFiringModeFloat RecoilAngleVariance => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flRecoilAngleVariance");
// m_flRecoilMagnitude
public CFiringModeFloat RecoilMagnitude => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flRecoilMagnitude");
// m_flRecoilMagnitudeVariance
public CFiringModeFloat RecoilMagnitudeVariance => Schema.GetDeclaredClass<CFiringModeFloat>(this.Handle, "CCSWeaponBaseVData", "m_flRecoilMagnitudeVariance");
// m_nTracerFrequency
public CFiringModeInt TracerFrequency => Schema.GetDeclaredClass<CFiringModeInt>(this.Handle, "CCSWeaponBaseVData", "m_nTracerFrequency");
// m_flInaccuracyJumpInitial
public ref float InaccuracyJumpInitial => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyJumpInitial");
// m_flInaccuracyJumpApex
public ref float InaccuracyJumpApex => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyJumpApex");
// m_flInaccuracyReload
public ref float InaccuracyReload => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyReload");
// m_nRecoilSeed
public ref Int32 RecoilSeed => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nRecoilSeed");
// m_nSpreadSeed
public ref Int32 SpreadSeed => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nSpreadSeed");
// m_flTimeToIdleAfterFire
public ref float TimeToIdleAfterFire => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flTimeToIdleAfterFire");
// m_flIdleInterval
public ref float IdleInterval => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flIdleInterval");
// m_flAttackMovespeedFactor
public ref float AttackMovespeedFactor => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flAttackMovespeedFactor");
// m_flHeatPerShot
public ref float HeatPerShot => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flHeatPerShot");
// m_flInaccuracyPitchShift
public ref float InaccuracyPitchShift => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyPitchShift");
// m_flInaccuracyAltSoundThreshold
public ref float InaccuracyAltSoundThreshold => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flInaccuracyAltSoundThreshold");
// m_flBotAudibleRange
public ref float BotAudibleRange => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flBotAudibleRange");
// m_szUseRadioSubtitle
public string UseRadioSubtitle
{
get { return Schema.GetUtf8String(this.Handle, "CCSWeaponBaseVData", "m_szUseRadioSubtitle"); }
set { Schema.SetString(this.Handle, "CCSWeaponBaseVData", "m_szUseRadioSubtitle", value); }
}
// m_bUnzoomsAfterShot
public ref bool UnzoomsAfterShot => ref Schema.GetRef<bool>(this.Handle, "CCSWeaponBaseVData", "m_bUnzoomsAfterShot");
// m_bHideViewModelWhenZoomed
public ref bool HideViewModelWhenZoomed => ref Schema.GetRef<bool>(this.Handle, "CCSWeaponBaseVData", "m_bHideViewModelWhenZoomed");
// m_nZoomLevels
public ref Int32 ZoomLevels => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nZoomLevels");
// m_nZoomFOV1
public ref Int32 ZoomFOV1 => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nZoomFOV1");
// m_nZoomFOV2
public ref Int32 ZoomFOV2 => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nZoomFOV2");
// m_flZoomTime0
public ref float ZoomTime0 => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flZoomTime0");
// m_flZoomTime1
public ref float ZoomTime1 => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flZoomTime1");
// m_flZoomTime2
public ref float ZoomTime2 => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flZoomTime2");
// m_flIronSightPullUpSpeed
public ref float IronSightPullUpSpeed => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flIronSightPullUpSpeed");
// m_flIronSightPutDownSpeed
public ref float IronSightPutDownSpeed => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flIronSightPutDownSpeed");
// m_flIronSightFOV
public ref float IronSightFOV => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flIronSightFOV");
// m_flIronSightPivotForward
public ref float IronSightPivotForward => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flIronSightPivotForward");
// m_flIronSightLooseness
public ref float IronSightLooseness => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flIronSightLooseness");
// m_angPivotAngle
public QAngle PivotAngle => Schema.GetDeclaredClass<QAngle>(this.Handle, "CCSWeaponBaseVData", "m_angPivotAngle");
// m_vecIronSightEyePos
public Vector IronSightEyePos => Schema.GetDeclaredClass<Vector>(this.Handle, "CCSWeaponBaseVData", "m_vecIronSightEyePos");
// m_nDamage
public ref Int32 Damage => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nDamage");
// m_flHeadshotMultiplier
public ref float HeadshotMultiplier => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flHeadshotMultiplier");
// m_flArmorRatio
public ref float ArmorRatio => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flArmorRatio");
// m_flPenetration
public ref float Penetration => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flPenetration");
// m_flRange
public ref float Range => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flRange");
// m_flRangeModifier
public ref float RangeModifier => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flRangeModifier");
// m_flFlinchVelocityModifierLarge
public ref float FlinchVelocityModifierLarge => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flFlinchVelocityModifierLarge");
// m_flFlinchVelocityModifierSmall
public ref float FlinchVelocityModifierSmall => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flFlinchVelocityModifierSmall");
// m_flRecoveryTimeCrouch
public ref float RecoveryTimeCrouch => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flRecoveryTimeCrouch");
// m_flRecoveryTimeStand
public ref float RecoveryTimeStand => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flRecoveryTimeStand");
// m_flRecoveryTimeCrouchFinal
public ref float RecoveryTimeCrouchFinal => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flRecoveryTimeCrouchFinal");
// m_flRecoveryTimeStandFinal
public ref float RecoveryTimeStandFinal => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flRecoveryTimeStandFinal");
// m_nRecoveryTransitionStartBullet
public ref Int32 RecoveryTransitionStartBullet => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nRecoveryTransitionStartBullet");
// m_nRecoveryTransitionEndBullet
public ref Int32 RecoveryTransitionEndBullet => ref Schema.GetRef<Int32>(this.Handle, "CCSWeaponBaseVData", "m_nRecoveryTransitionEndBullet");
// m_flThrowVelocity
public ref float ThrowVelocity => ref Schema.GetRef<float>(this.Handle, "CCSWeaponBaseVData", "m_flThrowVelocity");
// m_vSmokeColor
public Vector SmokeColor => Schema.GetDeclaredClass<Vector>(this.Handle, "CCSWeaponBaseVData", "m_vSmokeColor");
// m_szAnimClass
public string AnimClass
{
get { return Schema.GetUtf8String(this.Handle, "CCSWeaponBaseVData", "m_szAnimClass"); }
set { Schema.SetString(this.Handle, "CCSWeaponBaseVData", "m_szAnimClass", value); }
}
}
public partial class CDamageRecord : NativeObject
{
public CDamageRecord (IntPtr pointer) : base(pointer) {}
@@ -9443,6 +9815,12 @@ public partial class CEntityIOOutput : NativeObject
}
public partial class CEntitySubclassVDataBase : NativeObject
{
public CEntitySubclassVDataBase (IntPtr pointer) : base(pointer) {}
}
public partial class CEnvBeam : CBeam
{
public CEnvBeam (IntPtr pointer) : base(pointer) {}
@@ -10981,6 +11359,45 @@ public partial class CEnvWindShared : NativeObject
}
public partial class CExampleSchemaVData_Monomorphic : NativeObject
{
public CExampleSchemaVData_Monomorphic (IntPtr pointer) : base(pointer) {}
// m_nExample1
public ref Int32 Example1 => ref Schema.GetRef<Int32>(this.Handle, "CExampleSchemaVData_Monomorphic", "m_nExample1");
// m_nExample2
public ref Int32 Example2 => ref Schema.GetRef<Int32>(this.Handle, "CExampleSchemaVData_Monomorphic", "m_nExample2");
}
public partial class CExampleSchemaVData_PolymorphicBase : NativeObject
{
public CExampleSchemaVData_PolymorphicBase (IntPtr pointer) : base(pointer) {}
// m_nBase
public ref Int32 Base => ref Schema.GetRef<Int32>(this.Handle, "CExampleSchemaVData_PolymorphicBase", "m_nBase");
}
public partial class CExampleSchemaVData_PolymorphicDerivedA : CExampleSchemaVData_PolymorphicBase
{
public CExampleSchemaVData_PolymorphicDerivedA (IntPtr pointer) : base(pointer) {}
// m_nDerivedA
public ref Int32 DerivedA => ref Schema.GetRef<Int32>(this.Handle, "CExampleSchemaVData_PolymorphicDerivedA", "m_nDerivedA");
}
public partial class CExampleSchemaVData_PolymorphicDerivedB : CExampleSchemaVData_PolymorphicBase
{
public CExampleSchemaVData_PolymorphicDerivedB (IntPtr pointer) : base(pointer) {}
// m_nDerivedB
public ref Int32 DerivedB => ref Schema.GetRef<Int32>(this.Handle, "CExampleSchemaVData_PolymorphicDerivedB", "m_nDerivedB");
}
public partial class CFilterAttributeInt : CBaseFilter
{
public CFilterAttributeInt (IntPtr pointer) : base(pointer) {}
@@ -11194,6 +11611,24 @@ public partial class CFireSmoke : CBaseFire
}
public partial class CFiringModeFloat : NativeObject
{
public CFiringModeFloat (IntPtr pointer) : base(pointer) {}
// m_flValues
public Span<float> Values => Schema.GetFixedArray<float>(this.Handle, "CFiringModeFloat", "m_flValues", 2);
}
public partial class CFiringModeInt : NativeObject
{
public CFiringModeInt (IntPtr pointer) : base(pointer) {}
// m_nValues
public Span<Int32> Values => Schema.GetFixedArray<Int32>(this.Handle, "CFiringModeInt", "m_nValues", 2);
}
public partial class CFish : CBaseAnimGraph
{
public CFish (IntPtr pointer) : base(pointer) {}
@@ -14982,6 +15417,15 @@ public partial class CMomentaryRotButton : CRotButton
}
public partial class CMoodVData : NativeObject
{
public CMoodVData (IntPtr pointer) : base(pointer) {}
// m_nMoodType
public ref MoodType_t MoodType => ref Schema.GetRef<MoodType_t>(this.Handle, "CMoodVData", "m_nMoodType");
}
public partial class CMotorController : NativeObject
{
public CMotorController (IntPtr pointer) : base(pointer) {}
@@ -15078,6 +15522,70 @@ public partial class CMultiSource : CLogicalEntity
}
public partial class CNavHullPresetVData : NativeObject
{
public CNavHullPresetVData (IntPtr pointer) : base(pointer) {}
// m_vecNavHulls
public NetworkedVector<string> NavHulls => Schema.GetDeclaredClass<NetworkedVector<string>>(this.Handle, "CNavHullPresetVData", "m_vecNavHulls");
}
public partial class CNavHullVData : NativeObject
{
public CNavHullVData (IntPtr pointer) : base(pointer) {}
// m_bAgentEnabled
public ref bool AgentEnabled => ref Schema.GetRef<bool>(this.Handle, "CNavHullVData", "m_bAgentEnabled");
// m_agentRadius
public ref float AgentRadius => ref Schema.GetRef<float>(this.Handle, "CNavHullVData", "m_agentRadius");
// m_agentHeight
public ref float AgentHeight => ref Schema.GetRef<float>(this.Handle, "CNavHullVData", "m_agentHeight");
// m_agentShortHeightEnabled
public ref bool AgentShortHeightEnabled => ref Schema.GetRef<bool>(this.Handle, "CNavHullVData", "m_agentShortHeightEnabled");
// m_agentShortHeight
public ref float AgentShortHeight => ref Schema.GetRef<float>(this.Handle, "CNavHullVData", "m_agentShortHeight");
// m_agentMaxClimb
public ref float AgentMaxClimb => ref Schema.GetRef<float>(this.Handle, "CNavHullVData", "m_agentMaxClimb");
// m_agentMaxSlope
public ref Int32 AgentMaxSlope => ref Schema.GetRef<Int32>(this.Handle, "CNavHullVData", "m_agentMaxSlope");
// m_agentMaxJumpDownDist
public ref float AgentMaxJumpDownDist => ref Schema.GetRef<float>(this.Handle, "CNavHullVData", "m_agentMaxJumpDownDist");
// m_agentMaxJumpHorizDistBase
public ref float AgentMaxJumpHorizDistBase => ref Schema.GetRef<float>(this.Handle, "CNavHullVData", "m_agentMaxJumpHorizDistBase");
// m_agentMaxJumpUpDist
public ref float AgentMaxJumpUpDist => ref Schema.GetRef<float>(this.Handle, "CNavHullVData", "m_agentMaxJumpUpDist");
// m_agentBorderErosion
public ref Int32 AgentBorderErosion => ref Schema.GetRef<Int32>(this.Handle, "CNavHullVData", "m_agentBorderErosion");
}
public partial class CNavLinkAnimgraphVar : NativeObject
{
public CNavLinkAnimgraphVar (IntPtr pointer) : base(pointer) {}
// m_strAnimgraphVar
public string StrAnimgraphVar
{
get { return Schema.GetUtf8String(this.Handle, "CNavLinkAnimgraphVar", "m_strAnimgraphVar"); }
set { Schema.SetString(this.Handle, "CNavLinkAnimgraphVar", "m_strAnimgraphVar", value); }
}
// m_unAlignmentDegrees
public ref UInt32 AlignmentDegrees => ref Schema.GetRef<UInt32>(this.Handle, "CNavLinkAnimgraphVar", "m_unAlignmentDegrees");
}
public partial class CNavLinkAreaEntity : CPointEntity
{
public CNavLinkAreaEntity (IntPtr pointer) : base(pointer) {}
@@ -15135,6 +15643,21 @@ public partial class CNavLinkAreaEntity : CPointEntity
}
public partial class CNavLinkMovementVData : NativeObject
{
public CNavLinkMovementVData (IntPtr pointer) : base(pointer) {}
// m_bIsInterpolated
public ref bool IsInterpolated => ref Schema.GetRef<bool>(this.Handle, "CNavLinkMovementVData", "m_bIsInterpolated");
// m_unRecommendedDistance
public ref UInt32 RecommendedDistance => ref Schema.GetRef<UInt32>(this.Handle, "CNavLinkMovementVData", "m_unRecommendedDistance");
// m_vecAnimgraphVars
public NetworkedVector<CNavLinkAnimgraphVar> AnimgraphVars => Schema.GetDeclaredClass<NetworkedVector<CNavLinkAnimgraphVar>>(this.Handle, "CNavLinkMovementVData", "m_vecAnimgraphVars");
}
public partial class CNavSpaceInfo : CPointEntity
{
public CNavSpaceInfo (IntPtr pointer) : base(pointer) {}
@@ -17816,6 +18339,34 @@ public partial class CPrecipitationBlocker : CBaseModelEntity
}
public partial class CPrecipitationVData : CEntitySubclassVDataBase
{
public CPrecipitationVData (IntPtr pointer) : base(pointer) {}
// m_flInnerDistance
public ref float InnerDistance => ref Schema.GetRef<float>(this.Handle, "CPrecipitationVData", "m_flInnerDistance");
// m_nAttachType
public ref ParticleAttachment_t AttachType => ref Schema.GetRef<ParticleAttachment_t>(this.Handle, "CPrecipitationVData", "m_nAttachType");
// m_bBatchSameVolumeType
public ref bool BatchSameVolumeType => ref Schema.GetRef<bool>(this.Handle, "CPrecipitationVData", "m_bBatchSameVolumeType");
// m_nRTEnvCP
public ref Int32 RTEnvCP => ref Schema.GetRef<Int32>(this.Handle, "CPrecipitationVData", "m_nRTEnvCP");
// m_nRTEnvCPComponent
public ref Int32 RTEnvCPComponent => ref Schema.GetRef<Int32>(this.Handle, "CPrecipitationVData", "m_nRTEnvCPComponent");
// m_szModifier
public string Modifier
{
get { return Schema.GetUtf8String(this.Handle, "CPrecipitationVData", "m_szModifier"); }
set { Schema.SetString(this.Handle, "CPrecipitationVData", "m_szModifier", value); }
}
}
public partial class CPredictedViewModel : CBaseViewModel
{
public CPredictedViewModel (IntPtr pointer) : base(pointer) {}
@@ -18109,6 +18660,15 @@ public partial class CRagdollPropAttached : CRagdollProp
}
public partial class CRangeFloat : NativeObject
{
public CRangeFloat (IntPtr pointer) : base(pointer) {}
// m_pValue
public Span<float> Value => Schema.GetFixedArray<float>(this.Handle, "CRangeFloat", "m_pValue", 2);
}
public partial class CRectLight : CBarnLight
{
public CRectLight (IntPtr pointer) : base(pointer) {}
@@ -18958,6 +19518,24 @@ public partial class CSkeletonInstance : CGameSceneNode
}
public partial class CSkillFloat : NativeObject
{
public CSkillFloat (IntPtr pointer) : base(pointer) {}
// m_pValue
public Span<float> Value => Schema.GetFixedArray<float>(this.Handle, "CSkillFloat", "m_pValue", 4);
}
public partial class CSkillInt : NativeObject
{
public CSkillInt (IntPtr pointer) : base(pointer) {}
// m_pValue
public Span<Int32> Value => Schema.GetFixedArray<Int32>(this.Handle, "CSkillInt", "m_pValue", 4);
}
public partial class CSkyboxReference : CBaseEntity
{
public CSkyboxReference (IntPtr pointer) : base(pointer) {}
@@ -21386,6 +21964,12 @@ public partial class InfoForResourceTypeCTextureBase : NativeObject
}
public partial class InfoForResourceTypeCVDataResource : NativeObject
{
public InfoForResourceTypeCVDataResource (IntPtr pointer) : base(pointer) {}
}
public partial class InfoForResourceTypeIMaterial2 : NativeObject
{
public InfoForResourceTypeIMaterial2 (IntPtr pointer) : base(pointer) {}

View File

@@ -6,6 +6,7 @@ public interface IPluginContext
IPlugin Plugin { get; }
int PluginId { get; }
string FilePath { get; }
void Load(bool hotReload);
void Unload(bool hotReload);
}

View File

@@ -20,8 +20,11 @@ using System.Threading.Tasks;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using CounterStrikeSharp.API.Core.Translations;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -44,6 +47,8 @@ namespace CounterStrikeSharp.API.Core.Plugin
private readonly FileSystemWatcher _fileWatcher;
private readonly IServiceProvider _applicationServiceProvider;
public string FilePath => _path;
// TOOD: ServiceCollection
private ILogger _logger = CoreLogging.Factory.CreateLogger<PluginContext>();
@@ -72,12 +77,15 @@ namespace CounterStrikeSharp.API.Core.Plugin
};
_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";
@@ -87,11 +95,14 @@ namespace CounterStrikeSharp.API.Core.Plugin
}
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;
}
@@ -158,7 +169,11 @@ namespace CounterStrikeSharp.API.Core.Plugin
}
}
serviceCollection.AddSingleton(this);
serviceCollection.AddSingleton<IPluginContext>(this);
serviceCollection.TryAddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
serviceCollection.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
serviceCollection.TryAddTransient(typeof(IStringLocalizer), typeof(StringLocalizer));
ServiceProvider = serviceCollection.BuildServiceProvider();
var minimumApiVersion = pluginType.GetCustomAttribute<MinimumApiVersion>()?.Version;
@@ -179,6 +194,7 @@ namespace CounterStrikeSharp.API.Core.Plugin
Plugin.ModulePath = _path;
Plugin.RegisterAllAttributes(Plugin);
Plugin.Localizer = ServiceProvider.GetRequiredService<IStringLocalizer>();
Plugin.Logger = ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(pluginType);
Plugin.InitializeConfig(Plugin, pluginType);

View File

@@ -0,0 +1,11 @@
using System.Globalization;
using CounterStrikeSharp.API.Modules.Entities;
namespace CounterStrikeSharp.API.Core.Translations;
public interface IPlayerLanguageManager
{
void SetLanguage(SteamID steamId, CultureInfo cultureInfo);
CultureInfo GetLanguage(SteamID steamId);
CultureInfo GetDefaultLanguage();
}

View File

@@ -0,0 +1,125 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
namespace CounterStrikeSharp.API.Core.Translations
{
public class JsonResourceManager
{
private static readonly JsonDocumentOptions _jsonDocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
private ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _resourcesCache = new();
public JsonResourceManager(string resourcesPath)
{
ResourcesPath = resourcesPath;
}
public string ResourcesPath { get; }
public virtual ConcurrentDictionary<string, string> GetResourceSet(CultureInfo culture, bool tryParents)
{
TryLoadResourceSet(culture);
if (tryParents)
{
var allResources = new ConcurrentDictionary<string, string>();
do
{
TryLoadResourceSet(culture);
if (_resourcesCache.TryGetValue(culture.Name, out ConcurrentDictionary<string, string> resources))
{
foreach (var entry in resources)
{
allResources.TryAdd(entry.Key, entry.Value);
}
}
culture = culture.Parent;
} while (culture != CultureInfo.InvariantCulture);
return allResources;
}
else
{
_resourcesCache.TryGetValue(culture.Name, out ConcurrentDictionary<string, string> resources);
return resources;
}
}
public virtual string GetString(string name)
{
var culture = CultureInfo.CurrentUICulture;
GetResourceSet(culture, tryParents: true);
if (_resourcesCache.Count == 0)
{
return null;
}
do
{
if (_resourcesCache.ContainsKey(culture.Name))
{
if (_resourcesCache[culture.Name].TryGetValue(name, out string value))
{
return value;
}
}
culture = culture.Parent;
} while (culture != culture.Parent);
return null;
}
public virtual string? GetString(string name, CultureInfo culture)
{
var values = GetResourceSet(culture, tryParents: true);
if (values.Count == 0)
{
return null;
}
return values.TryGetValue(name, out string value)
? value
: null;
}
private void TryLoadResourceSet(CultureInfo culture)
{
if (!_resourcesCache.ContainsKey(culture.Name))
{
var file = Path.Combine(ResourcesPath, $"{culture.Name}.json");
var resources = LoadJsonResources(file);
_resourcesCache.TryAdd(culture.Name,
new ConcurrentDictionary<string, string>(resources.ToDictionary(r => r.Key, r => r.Value)));
}
}
private static IDictionary<string, string> LoadJsonResources(string filePath)
{
var resources = new Dictionary<string, string>();
if (File.Exists(filePath))
{
using var reader = new StreamReader(filePath);
using var document = JsonDocument.Parse(reader.BaseStream, _jsonDocumentOptions);
resources = document.RootElement.EnumerateObject().ToDictionary(e => e.Name, e => e.Value.ToString());
}
return resources;
}
}
}

View File

@@ -0,0 +1,116 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Resources;
using Microsoft.Extensions.Localization;
namespace CounterStrikeSharp.API.Core.Translations;
public class JsonStringLocalizer : IStringLocalizer
{
private readonly JsonResourceManager _resourceManager;
private readonly JsonStringProvider _resourceStringProvider;
public JsonStringLocalizer(string langPath)
{
_resourceManager = new JsonResourceManager(langPath);
_resourceStringProvider = new(_resourceManager);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
return GetAllStrings(includeParentCultures, CultureInfo.CurrentUICulture);
}
public LocalizedString this[string name]
{
get
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var value = GetStringSafely(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var format = GetStringSafely(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
protected string? GetStringSafely(string name, CultureInfo? culture = null)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
culture = culture ?? CultureInfo.CurrentUICulture;
var result = _resourceManager.GetString(name, culture);
// Fallback to the default culture (en-US) if the resource is not found for the current culture.
if (result == null && !culture.Equals(CultureInfo.DefaultThreadCurrentUICulture))
{
result = _resourceManager.GetString(name, CultureInfo.DefaultThreadCurrentUICulture!);
}
return result?.ReplaceColorTags();
}
protected virtual IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures, CultureInfo culture)
{
if (culture == null)
{
throw new ArgumentNullException(nameof(culture));
}
var resourceNames = includeParentCultures
? GetResourceNamesFromCultureHierarchy(culture)
: _resourceStringProvider.GetAllResourceStrings(culture, true);
foreach (var name in resourceNames)
{
var value = GetStringSafely(name, culture);
yield return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
private IEnumerable<string> GetResourceNamesFromCultureHierarchy(CultureInfo startingCulture)
{
var currentCulture = startingCulture;
var resourceNames = new HashSet<string>();
while (currentCulture != currentCulture.Parent)
{
var cultureResourceNames = _resourceStringProvider.GetAllResourceStrings(currentCulture, false);
if (cultureResourceNames != null)
{
foreach (var resourceName in cultureResourceNames)
{
resourceNames.Add(resourceName);
}
}
currentCulture = currentCulture.Parent;
}
return resourceNames;
}
}

View File

@@ -0,0 +1,25 @@
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Plugin;
using Microsoft.Extensions.Localization;
namespace CounterStrikeSharp.API.Core.Translations;
public class JsonStringLocalizerFactory : IStringLocalizerFactory
{
private readonly IPluginContext _pluginContext;
public JsonStringLocalizerFactory(IPluginContext pluginContext)
{
_pluginContext = pluginContext;
}
public IStringLocalizer Create(Type resourceSource)
{
return new JsonStringLocalizer(Path.Join(Path.GetDirectoryName(_pluginContext.FilePath), "lang"));
}
public IStringLocalizer Create(string baseName, string location)
{
return new JsonStringLocalizer(Path.Join(Path.GetDirectoryName(_pluginContext.FilePath), "lang"));
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Resources;
namespace CounterStrikeSharp.API.Core.Translations;
public class JsonStringProvider
{
private readonly ConcurrentDictionary<string, IList<string>> _resourceNamesCache = new();
private readonly JsonResourceManager _jsonResourceManager;
public JsonStringProvider(JsonResourceManager jsonResourceManager)
{
_jsonResourceManager = jsonResourceManager;
}
private string GetResourceCacheKey(CultureInfo culture)
{
return $"Culture={culture.Name}";
}
public IList<string> GetAllResourceStrings(CultureInfo culture, bool throwOnMissing)
{
var cacheKey = GetResourceCacheKey(culture);
return _resourceNamesCache.GetOrAdd(cacheKey, _ =>
{
var resourceSet = _jsonResourceManager.GetResourceSet(culture, tryParents: false);
if (resourceSet == null)
{
if (throwOnMissing)
{
throw new MissingManifestResourceException($"The manifest resource for the culture '{culture.Name}' is missing.");
}
else
{
return null;
}
}
var names = new List<string>();
foreach (var entry in resourceSet)
{
names.Add(entry.Key);
}
return names;
});
}
}

View File

@@ -0,0 +1,17 @@
using System.Globalization;
using CounterStrikeSharp.API.Modules.Entities;
namespace CounterStrikeSharp.API.Core.Translations;
public static class PlayerLanguageExtensions
{
/// <summary>
/// Returns the players configured language, as set using the "css_lang" command.
/// </summary>
public static CultureInfo GetLanguage(this CCSPlayerController? player)
{
if (player == null || !player.IsValid) return PlayerLanguageManager.Instance.GetDefaultLanguage();
return PlayerLanguageManager.Instance.GetLanguage((SteamID)player.SteamID);
}
}

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