Compare commits

...

20 Commits

Author SHA1 Message Date
Sürat ÜNLÜGÜN
a07dd9d7d4 Fix CanPlayerTarget Immu (#222) 2023-12-25 23:26:34 +10:00
B3none
d527038fba Added a parameter for people to optionally remove the entity when calling RemoveItemByDesignerName (#214) 2023-12-24 19:40:23 +10:00
pedrotski
ca85922270 Update Getting Started Guide (#217) 2023-12-24 10:02:41 +10:00
B3none
b837479f98 Added links to referenced projects in the credits for the README.md (#210) 2023-12-19 11:15:30 +10:00
Hackmastr
1e42f72655 Docs: Using DOTNET_SYSTEM_GLOBALIZATION_INVARIANT is no longer valid. (#209)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2023-12-19 11:14:31 +10:00
Michael Wilson
1ad1828e30 feat: Add SchemaMember attribute to schema objects` (#208) 2023-12-17 13:12:26 +10:00
Charles_
563a5d7b3a feat: Added RemovePlayerItem() to CBasePlayerPawn. (#200)
Co-authored-by: roflmuffin <shortguy014@gmail.com>
2023-12-16 14:24:08 +10:00
Charlie Thomson
983b673b4c feat: Add OnClientVoice listener (#204)
Co-authored-by: roflmuffin <shortguy014@gmail.com>
2023-12-16 14:18:40 +10:00
Roflmuffin
74fd0e0832 fix: offsets for CBaseEntity derived classes
Also adds test commands to test plugin for future validation
2023-12-14 15:14:34 +10:00
Michael Wilson
44e3f2240c chore: license updates (#199) 2023-12-14 11:03:18 +10:00
BuSheeZy
8af219e7a8 CHANGE: visual adjustments (#198) 2023-12-14 10:44:48 +10:00
Poggu
bff04e7795 Add voice manager (ability to override voice chat / mute players) (#179)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
Co-authored-by: roflmuffin <shortguy014@gmail.com>
2023-12-12 17:46:45 +10:00
Michael Wilson
d495ac6230 chore: bump version 2023-12-12 10:55:55 +10:00
roflmuffin
f78abf0c81 fix: fallback to en language files for invariant mode 2023-12-12 10:51:38 +10:00
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
94 changed files with 11523 additions and 4984 deletions

View File

@@ -22,6 +22,22 @@ saul/demofile-net, https://github.com/saul/demofile-net/blob/main/LICENSE:
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
neverlosecc/source2gen, https://github.com/neverlosecc/source2gen
source2gen - Source2 games SDK generator
Copyright 2023 neverlosecc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Source2ZE/CS2Fixes:
alliedmodders/sourcemod:
Source-Python-Dev-Team/Source.Python:

View File

@@ -75,6 +75,7 @@ SET(SOURCE_FILES
src/scripting/natives/natives_memory.cpp
src/scripting/natives/natives_schema.cpp
src/scripting/natives/natives_entities.cpp
src/scripting/natives/natives_voice.cpp
src/core/managers/entity_manager.cpp
src/core/managers/entity_manager.h
src/core/managers/chat_manager.cpp
@@ -83,6 +84,8 @@ SET(SOURCE_FILES
src/core/managers/server_manager.h
src/scripting/natives/natives_server.cpp
libraries/nlohmann/json.hpp
src/core/managers/voice_manager.cpp
src/core/managers/voice_manager.h
src/scripting/natives/natives_dynamichooks.cpp
)

View File

@@ -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?
@@ -43,7 +43,7 @@ These features are the core of the platform and work pretty well/have a low risk
- [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
- [Install Docs](https://docs.cssharp.dev/guides/getting-started/): Installation instructions
- [Install Docs](https://docs.cssharp.dev/docs/guides/getting-started.html): Installation instructions
- [Example Plugin](managed/TestPlugin/TestPlugin.cs): Test plugin with basic functionality
## Examples
@@ -91,8 +91,8 @@ public class HelloWorldPlugin : BasePlugin
## Credits
A lot of code has been borrowed from SourceMod as well as Source.Python, two pioneering source engine plugin frameworks which this project lends a lot of its credit to.
I've also used the scripting context & native system that is implemented in FiveM for GTA5. Also shoutout to the [CS2Fixes](https://github.com/Source2ZE/CS2Fixes) project for providing good reverse-engineering information so shortly after CS2 release.
A lot of code has been borrowed from [SourceMod](https://github.com/alliedmodders/sourcemod) as well as [Source.Python](https://github.com/Source-Python-Dev-Team/Source.Python), two pioneering source engine plugin frameworks which this project lends a lot of its credit to.
I've also used the scripting context & native system that is implemented in [FiveM](https://github.com/citizenfx/fivem) for GTA5. Also shoutout to the [CS2Fixes](https://github.com/Source2ZE/CS2Fixes) project for providing good reverse-engineering information so shortly after CS2 release.
## How to Build
@@ -128,3 +128,4 @@ Build
```bash
cmake --build . --config Debug
```

View File

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

View File

@@ -28,8 +28,8 @@
},
"CCSPlayerController_Respawn": {
"offsets": {
"windows": 241,
"linux": 243
"windows": 242,
"linux": 244
}
},
"CCSPlayerPawn_Respawn": {
@@ -122,14 +122,21 @@
},
"CBasePlayerPawn_CommitSuicide": {
"offsets": {
"windows": 356,
"linux": 356
"windows": 357,
"linux": 357
}
},
"CBasePlayerPawn_RemovePlayerItem": {
"signatures": {
"library": "server",
"linux": "\\x55\\x48\\x89\\x2A\\x41\\x2A\\x49\\x89\\x2A\\x41\\x2A\\x49\\x89\\x2A\\xE8\\x2A\\x2A\\x2A\\x2A\\x49\\x39",
"windows": "\\x48\\x85\\xD2\\x0F\\x84\\x2A\\x2A\\x2A\\x2A\\x48\\x89\\x5C\\x24\\x08\\x57\\x48\\x83\\xEC\\x30\\x48\\x8B\\xDA"
}
},
"CBaseEntity_Teleport": {
"offsets": {
"windows": 148,
"linux": 147
"windows": 149,
"linux": 148
}
},
"CBaseEntity_TakeDamageOld": {

View File

@@ -5,31 +5,52 @@ description: How to get started installing & using CounterStrikeSharp.
# Getting Started
How to get started installing & using CounterStrikeSharp.
In this guide you will learn how to install CounterStrikeSharp onto your vanilla Counter-Strike 2 server. `CounterStrikeSharp` uses `Metamod:Source` as its main way of communicating with the game server, so both frameworks will need to be installed.
If you're more of a visual person, here is a <a href="https://www.youtube.com/watch?v=FlsKzStHJuY" target="_blank">Youtube video</a> that covers everything.
## Prerequisites
- <a href="https://www.sourcemm.net/downloads.php/?branch=master" target="_blank">Metamod: Source 2.X Dev Build</a>
- <a href="https://github.com/roflmuffin/CounterStrikeSharp/releases" target="_blank">CounterStrikeSharp With Runtime</a>
## 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>.
1. Extract Metamod and copy the `/addons/` directory to `/game/csgo/`.
2. Inside `/game/csgo/`, locate `gameinfo.gi`.
3. Create a new line underneath `Game_LowViolence csgo_lv` and add `Game csgo/addons/metamod`.
4. Restart your game server.
Your `gameinfo.gi` should look like <a href="images/gameinfogi-example.png" target="_blank">this</a>. Type `meta list` in your server console to see if Metamod is loaded.
## 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**.
1. Extract CounterStrikeSharp and copy the `/addons/` directory to `/game/csgo/`.
2. Restart your game server.
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
```
> [!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.
> For Windows servers, you must have <a href="https://aka.ms/vs/17/release/vc_redist.x64.exe" target="_blank">Visual Studio Redistributables</a> installed otherwise CounterStrikeSharp will not work.
> [!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!
## Upgrading CounterStrikeSharp
To upgrade CounterStrikeSharp you simply need to download the latest release and copy it to your server, the same as the original installation.
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.
CounterStrikeSharp is designed in a way where your configuration files will not be overwritten if you do this. As CounterStrikeSharp is already installed, you may download the non `with-runtime` build, but you will need to ensure your .NET runtime is up-to-date yourself.
## Troubleshooting
- If this is your first time installing, you **MUST** download the `with-runtime` version. This includes a copy of the .NET runtime, which is required to run the plugin.
- Depending on your OS you might also either need to install `libicu` / `icu-libs` / `libicu-dev` using your package manager for .NET to run.
- If you get `Unknown Command` when typing `meta list` into your console, double-check the folders are copied over correctly and that your `gameinfo.gi` file is correctly modified.
Your folder structure should look like this:
```shell
<server_path>/game/csgo/addons > tree -L 2
@@ -49,15 +70,3 @@ addons
├── 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

@@ -32,3 +32,7 @@ receive a ban.
## 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

@@ -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)]

View File

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

View File

@@ -13,5 +13,9 @@ items:
href: WithGameEventHandlers.md
- name: Database (Dapper)
href: WithDatabase.md
- name: Translations
href: WithTranslations.md
- name: Voice Overrides
href: WithVoiceOverrides.md
- name: Warcraft Plugin
href: WarcraftPlugin.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -14,6 +14,7 @@ description: Write Counter-Strike 2 server plugins in C#.
<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-secondary btn-lg fw-bold my-5">Download <i class="bi bi-download"></a>
</div>
</div>

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,2 @@
# With Voice Overrides
Provides examples how to manipulate player voice flags & listening overrides to prevent certain players from hearing others.

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,79 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.Logging;
namespace WithVoiceOverrides;
[MinimumApiVersion(80)]
public class WithVoiceOverridesPlugin : BasePlugin
{
public override string ModuleName => "Example: With Voice Overrides";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A plugin that manipulates voice flags";
[ConsoleCommand("css_hearall")]
public void OnHearAllCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller is null) return;
if (caller.VoiceFlags.HasFlag(VoiceFlags.ListenAll))
{
caller.VoiceFlags = VoiceFlags.Normal;
command.ReplyToCommand("Voice set back to default");
}
else
{
caller.VoiceFlags = VoiceFlags.ListenAll;
command.ReplyToCommand("Can hear both teams");
}
}
[ConsoleCommand("css_muteself")]
public void OnMuteSelfCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller is null) return;
if (caller.VoiceFlags.HasFlag(VoiceFlags.Muted))
{
caller.VoiceFlags = VoiceFlags.Normal;
command.ReplyToCommand("Unmuted yourself");
}
else
{
caller.VoiceFlags = VoiceFlags.Muted;
command.ReplyToCommand("Muted yourself");
}
}
[ConsoleCommand("css_muteothers")]
[CommandHelper(minArgs: 1, usage: "[target]")]
public void OnMuteOthersCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller is null) return;
var targetResult = command.GetArgTargetResult(1);
foreach (var player in targetResult.Players)
{
if (player == caller) continue;
var existingOverride = caller.GetListenOverride(player);
if (existingOverride == ListenOverride.Mute)
{
caller.SetListenOverride(player, ListenOverride.Default);
command.ReplyToCommand($"Now hearing {player.PlayerName}");
}
else
{
caller.SetListenOverride(player, ListenOverride.Mute);
command.ReplyToCommand($"Muted {player.PlayerName}");
}
}
}
}

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

@@ -10,6 +10,7 @@
<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>
@@ -20,9 +21,25 @@
<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,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,92 @@
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.InvariantCulture))
{
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

@@ -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

@@ -1311,5 +1311,51 @@ namespace CounterStrikeSharp.API.Core
return (bool)ScriptContext.GlobalScriptContext.GetResult(typeof(bool));
}
}
public static void SetClientListening(IntPtr receiver, IntPtr sender, uint listen){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(receiver);
ScriptContext.GlobalScriptContext.Push(sender);
ScriptContext.GlobalScriptContext.Push(listen);
ScriptContext.GlobalScriptContext.SetIdentifier(0xD38BEE77);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static ListenOverride GetClientListening(IntPtr receiver, IntPtr sender){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(receiver);
ScriptContext.GlobalScriptContext.Push(sender);
ScriptContext.GlobalScriptContext.SetIdentifier(0xE95644E3);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (ListenOverride)ScriptContext.GlobalScriptContext.GetResult(typeof(ListenOverride));
}
}
public static void SetClientVoiceFlags(IntPtr client, uint flags){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(client);
ScriptContext.GlobalScriptContext.Push(flags);
ScriptContext.GlobalScriptContext.SetIdentifier(0x48EB2FC8);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static uint GetClientVoiceFlags(IntPtr client){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(client);
ScriptContext.GlobalScriptContext.SetIdentifier(0x9685205C);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (uint)ScriptContext.GlobalScriptContext.GetResult(typeof(uint));
}
}
}
}

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();
@@ -98,7 +104,7 @@ namespace CounterStrikeSharp.API.Core
info.ReplyToCommand(
" CounterStrikeSharp was created and is maintained by Michael \"roflmuffin\" Wilson.\n" +
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison and CS2Fixes.\n" +
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison, source2gen and CS2Fixes.\n" +
" See ACKNOWLEDGEMENTS.md for more information.\n" +
" Current API Version: " + currentVersion, true);
return;
@@ -152,7 +158,7 @@ namespace CounterStrikeSharp.API.Core
true);
break;
}
// If our arugment doesn't end in ".dll" - try and construct a path similar to PluginName/PluginName.dll.
// We'll assume we have a full path if we have ".dll".
var path = info.GetArg(2);
@@ -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);
}
@@ -181,7 +188,7 @@ namespace CounterStrikeSharp.API.Core
{
plugin.Load(false);
}
break;
}
@@ -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,14 @@
namespace CounterStrikeSharp.API.Core.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public class SchemaMemberAttribute : Attribute
{
public string ClassName { get; }
public string MemberName { get; }
public SchemaMemberAttribute(string className, string memberName)
{
ClassName = className;
MemberName = memberName;
}
}

View File

@@ -21,12 +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
@@ -58,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)
{
}
@@ -170,39 +174,53 @@ 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)
var data = AdminManager.GetCommandOverrideData(name);
if (data != null)
{
attr.Command = name;
if (!attr.CanExecuteCommand(caller))
{
command.ReplyToCommand("[CSS] You do not have the correct permissions to execute this command.");
return;
}
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);
}
}
}
// 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)
// 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)
{
var attrType = (data.CheckType == "all") ? typeof(RequiresPermissions) : typeof(RequiresPermissionsOr);
var attr = (BaseRequiresPermissions)Activator.CreateInstance(attrType, args: AdminManager.GetPermissionOverrides(name));
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;
}
}
@@ -210,15 +228,17 @@ namespace CounterStrikeSharp.API.Core
// Do not execute if we shouldn't be calling this command.
var helperAttribute = methodInfo?.GetCustomAttribute<CommandHelperAttribute>();
if (helperAttribute != null)
if (helperAttribute != null)
{
switch (helperAttribute.WhoCanExcecute)
{
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.");
}
@@ -226,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;
}
}

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
@@ -138,6 +145,29 @@ 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)
{
try
{
_logger.LogWarning("Server Language \"{ServerLanguage}\" is not supported, falling back to \"en\"",
ServerLanguage);
_coreConfig.ServerLanguage = "en";
serverCulture = new CultureInfo("en");
}
catch (Exception)
{
_logger.LogWarning("Server is running in invariant mode, translations will not be available.");
serverCulture = CultureInfo.InvariantCulture;
}
}
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
@@ -54,6 +55,8 @@ namespace CounterStrikeSharp.API.Core
string ModulePath { get; internal set; }
ILogger Logger { get; set; }
IStringLocalizer Localizer { get; set; }
void RegisterAllAttributes(object instance);

View File

@@ -93,6 +93,13 @@ namespace CounterStrikeSharp.API.Core
[ListenerName("OnClientDisconnectPost")]
public delegate void OnClientDisconnectPost(int playerSlot);
/// <summary>
/// Called when a client transmits voice data
/// </summary>
/// <param name="playerSlot">The player slot of the client.</param>
[ListenerName("OnClientVoice")]
public delegate void OnClientVoice(int playerSlot);
/// <summary>
/// Called when a client has been authorized by Steam.
/// </summary>

View File

@@ -14,4 +14,13 @@ public partial class CBasePlayerPawn
{
VirtualFunction.CreateVoid<IntPtr, bool, bool>(Handle, GameData.GetOffset("CBasePlayerPawn_CommitSuicide"))(Handle, explode, force);
}
/// <summary>
/// Remove Player Item
/// </summary>
/// <param name="weapon"></param>
public void RemovePlayerItem(CBasePlayerWeapon weapon)
{
VirtualFunctions.RemovePlayerItemVirtual(Handle, weapon.Handle);
}
}

View File

@@ -202,6 +202,21 @@ public partial class CCSPlayerController
public void ExecuteClientCommand(string command) => NativeAPI.IssueClientCommand(Slot, command);
/// <summary>
/// Overrides who a player can hear in voice chat.
/// </summary>
/// <param name="sender">Player talking in the voice chat</param>
/// <param name="override">Whether the talker should be heard</param>
public void SetListenOverride(CCSPlayerController sender, ListenOverride @override)
{
NativeAPI.SetClientListening(Handle, sender.Handle, (Byte)@override);
}
public ListenOverride GetListenOverride(CCSPlayerController sender)
{
return NativeAPI.GetClientListening(Handle, sender.Handle);
}
public int Slot => (int)Index - 1;
/// <summary>
@@ -234,4 +249,16 @@ public partial class CCSPlayerController
return ipAddress;
}
}
/// <summary>
/// Determines how the player interacts with voice chat.
/// </summary>
public VoiceFlags VoiceFlags
{
get => (VoiceFlags)NativeAPI.GetClientVoiceFlags(Handle);
set
{
NativeAPI.SetClientVoiceFlags(Handle, (Byte)value);
}
}
}

File diff suppressed because it is too large Load Diff

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;
@@ -43,6 +46,8 @@ namespace CounterStrikeSharp.API.Core.Plugin
private readonly string _path;
private readonly FileSystemWatcher _fileWatcher;
private readonly IServiceProvider _applicationServiceProvider;
public string FilePath => _path;
// TOOD: ServiceCollection
private ILogger _logger = CoreLogging.Factory.CreateLogger<PluginContext>();
@@ -163,8 +168,12 @@ namespace CounterStrikeSharp.API.Core.Plugin
method?.Invoke(pluginServiceCollection, new object[] { serviceCollection });
}
}
serviceCollection.AddSingleton<IPluginContext>(this);
serviceCollection.TryAddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
serviceCollection.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
serviceCollection.TryAddTransient(typeof(IStringLocalizer), typeof(StringLocalizer));
serviceCollection.AddSingleton(this);
ServiceProvider = serviceCollection.BuildServiceProvider();
var minimumApiVersion = pluginType.GetCustomAttribute<MinimumApiVersion>()?.Version;
@@ -185,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,153 @@
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(string cultureName)
{
TryLoadResourceSet(cultureName);
_resourcesCache.TryGetValue(cultureName, out ConcurrentDictionary<string, string> resources);
return resources;
}
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 string GetFallbackString(string name)
{
GetResourceSet("en");
if (_resourcesCache.ContainsKey("en"))
{
if (_resourcesCache["en"].TryGetValue(name, out string value))
{
return value;
}
}
return null;
}
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(string cultureName)
{
if (!_resourcesCache.ContainsKey(cultureName))
{
var file = Path.Combine(ResourcesPath, $"{cultureName}.json");
var resources = LoadJsonResources(file);
_resourcesCache.TryAdd(cultureName,
new ConcurrentDictionary<string, string>(resources.ToDictionary(r => r.Key, r => r.Value)));
}
}
private void TryLoadResourceSet(CultureInfo culture)
{
TryLoadResourceSet(culture.Name);
}
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,122 @@
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 en if running in invariant mode.
if (result == null && culture.Equals(CultureInfo.InvariantCulture))
{
result = _resourceManager.GetFallbackString(name);
}
// 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);
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Concurrent;
using System.Globalization;
using CounterStrikeSharp.API.Modules.Entities;
namespace CounterStrikeSharp.API.Core.Translations;
public class PlayerLanguageManager : IPlayerLanguageManager
{
private readonly ConcurrentDictionary<SteamID, CultureInfo> _playerLanguages = new();
public static IPlayerLanguageManager Instance { get; private set; } = null!;
public PlayerLanguageManager()
{
Instance = this;
}
public void SetLanguage(SteamID steamId, CultureInfo cultureInfo)
{
_playerLanguages[steamId] = cultureInfo;
}
public CultureInfo GetLanguage(SteamID steamId)
{
return _playerLanguages.TryGetValue(steamId, out var cultureInfo) ? cultureInfo : GetDefaultLanguage();
}
public CultureInfo GetDefaultLanguage()
{
return CultureInfo.DefaultThreadCurrentUICulture!;
}
}

View File

@@ -0,0 +1,22 @@
using System.Reflection;
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Core.Translations;
public static class StringExtensions
{
public static string ReplaceColorTags(this string message)
{
var modifiedValue = message;
foreach (var field in typeof(ChatColors).GetFields())
{
string pattern = $"{{{field.Name}}}";
if (modifiedValue.Contains(pattern, StringComparison.OrdinalIgnoreCase))
{
modifiedValue = modifiedValue.Replace(pattern, field.GetValue(null)?.ToString() ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
}
return modifiedValue.Equals(message) ? message : $" {modifiedValue}";
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.Localization;
namespace CounterStrikeSharp.API.Core.Translations;
internal class StringLocalizer : IStringLocalizer
{
private IStringLocalizer _localizer;
public StringLocalizer(IStringLocalizerFactory factory)
{
var type = typeof(StringLocalizer);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
_localizer = factory.Create(string.Empty, assemblyName.FullName);
}
public LocalizedString this[string name] => _localizer[name];
public LocalizedString this[string name, params object[] arguments] => _localizer[name, arguments];
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) => _localizer.GetAllStrings(includeParentCultures);
}

View File

@@ -0,0 +1,25 @@
using System.Globalization;
namespace CounterStrikeSharp.API.Core.Translations;
public sealed class WithTemporaryCulture : IDisposable
{
private readonly CultureInfo _originalCulture;
public WithTemporaryCulture(CultureInfo culture)
{
_originalCulture = CultureInfo.CurrentCulture;
SetCulture(culture);
}
private void SetCulture(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
}
public void Dispose()
{
SetCulture(_originalCulture);
}
}

View File

@@ -25,6 +25,7 @@
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="7.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Scrutor" Version="4.2.2" />
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />

View File

@@ -2,12 +2,14 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Modules.Admin
{
@@ -64,18 +66,18 @@ namespace CounterStrikeSharp.API.Modules.Admin
// Loop over each of the admins. If one of our admins is in a group,
// add the flags from the group to their admin definition and change
// the admin's immunity if it's higher.
foreach (var adminData in Admins.Values)
foreach (var (admin, data) in Admins)
{
var groups = adminData.Groups;
var groups = data.Groups;
foreach (var group in groups)
{
// roflmuffin is probably smart enough to condense this function down ;)
if (Groups.TryGetValue(group, out var groupData))
{
adminData.Flags.UnionWith(groupData.Flags);
if (groupData.Immunity > adminData.Immunity)
AddPlayerPermissions(admin, groupData.Flags.ToArray());
if (groupData.Immunity > data.Immunity)
{
adminData.Immunity = groupData.Immunity;
data.Immunity = groupData.Immunity;
}
}
}
@@ -94,8 +96,20 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of groups.
if (player == null) return true;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
var playerData = GetPlayerAdminData((SteamID)player.AuthorizedSteamID);
return playerData?.Groups.IsSupersetOf(groups) ?? false;
var playerData = GetPlayerAdminData(player.AuthorizedSteamID);
if (playerData == null) return false;
var playerGroups = groups.ToHashSet();
foreach (var domain in playerData.Flags)
{
if (playerData.DomainHasRootFlag(domain.Key))
{
playerGroups.ExceptWith(groups.Where(group => group.Contains(domain.Key + '/')));
}
}
return playerData.Groups.IsSupersetOf(playerGroups);
}
/// <summary>
@@ -104,10 +118,22 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// <param name="steamId">SteamID of the player.</param>
/// <param name="groups">Groups to check for.</param>
/// <returns>True if a player is part of all of the groups provided, false if not.</returns>
public static bool PlayerInGroup(SteamID steamId, params string[] groups)
public static bool PlayerInGroup(SteamID? steamId, params string[] groups)
{
if (steamId == null) return false;
var playerData = GetPlayerAdminData(steamId);
return playerData?.Groups.IsSupersetOf(groups) ?? false;
if (playerData == null) return false;
var playerGroups = groups.ToHashSet<string>();
foreach (var domain in playerData.Flags)
{
if (playerData.DomainHasRootFlag(domain.Key))
{
playerGroups.ExceptWith(groups.Where(group => group.Contains(domain.Key + '/')));
}
}
return playerData.Groups.IsSupersetOf(playerGroups);
}
/// <summary>
@@ -119,7 +145,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return; }
AddPlayerToGroup((SteamID)player.AuthorizedSteamID, groups);
AddPlayerToGroup(player.AuthorizedSteamID, groups);
}
/// <summary>
@@ -127,8 +153,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// </summary>
/// <param name="steamId">SteamID of the player.</param>
/// <param name="groups">Groups to add the player to.</param>
public static void AddPlayerToGroup(SteamID steamId, params string[] groups)
public static void AddPlayerToGroup(SteamID? steamId, params string[] groups)
{
if (steamId == null) return;
var data = GetPlayerAdminData(steamId);
if (data == null)
{
@@ -144,7 +171,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
if (Groups.TryGetValue(group, out var groupDef))
{
data.Flags.UnionWith(groupDef.Flags);
AddPlayerPermissions(steamId, groupDef.Flags.ToArray());
groupDef.CommandOverrides.ToList().ForEach(x => data.CommandOverrides[x.Key] = x.Value);
}
}
@@ -161,7 +188,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return; }
RemovePlayerFromGroup((SteamID)player.AuthorizedSteamID, true, groups);
RemovePlayerFromGroup(player.AuthorizedSteamID, true, groups);
}
/// <summary>
@@ -170,8 +197,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// <param name="steamId">SteamID of the player.</param>
/// <param name="removeInheritedFlags">If true, all of the flags that the player inherited from being in the group will be removed.</param>
/// <param name="groups"></param>
public static void RemovePlayerFromGroup(SteamID steamId, bool removeInheritedFlags = true, params string[] groups)
public static void RemovePlayerFromGroup(SteamID? steamId, bool removeInheritedFlags = true, params string[] groups)
{
if (steamId == null) return;
var data = GetPlayerAdminData(steamId);
if (data == null) return;
@@ -183,7 +211,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
if (Groups.TryGetValue(group, out var groupDef))
{
data.Flags.ExceptWith(groupDef.Flags);
RemovePlayerPermissions(steamId, groupDef.Flags.ToArray());
}
}
}

View File

@@ -1,18 +1,14 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Utils;
using System.IO;
using System.Linq;
using System.Reflection;
using CounterStrikeSharp.API.Core.Logging;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Modules.Admin
{
public static partial class AdminManager
{
static AdminManager()
public static void AddCommands()
{
CommandUtils.AddStandaloneCommand("css_admins_reload", "Reloads the admin file.", ReloadAdminsCommand);
CommandUtils.AddStandaloneCommand("css_admins_list", "List admins and their flags.", ListAdminsCommand);
@@ -37,6 +33,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
Admins.Clear();
var rootDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent;
LoadAdminData(Path.Combine(rootDir.FullName, "configs", "admins.json"));
MergeGroupPermsIntoAdmins();
}
[RequiresPermissions(permissions:"@css/generic")]
@@ -45,7 +42,11 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
foreach (var (steamId, data) in Admins)
{
command.ReplyToCommand($"{steamId.SteamId64}, {steamId.SteamId2} - {string.Join(", ", data.Flags)}");
command.ReplyToCommand($"{steamId.SteamId64}, {steamId.SteamId2} - FLAGS: ");
foreach (var domain in data.Flags.Keys)
{
command.ReplyToCommand($" Domain {domain}: {string.Join(", ", data.Flags[domain])}");
}
}
}
@@ -56,6 +57,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
Groups.Clear();
var rootDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent;
LoadAdminGroups(Path.Combine(rootDir.FullName, "configs", "admin_groups.json"));
MergeGroupPermsIntoAdmins();
}
[RequiresPermissions(permissions: "@css/generic")]

View File

@@ -3,24 +3,110 @@ using System.IO;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Text.Json;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Commands;
using System.Reflection;
using System.Numerics;
using System.Linq;
using CounterStrikeSharp.API.Core.Logging;
using Microsoft.Extensions.Logging;
using System.Text.Json.Nodes;
using System.Numerics;
using CounterStrikeSharp.API.Modules.Utils;
using System.Diagnostics.Eventing.Reader;
namespace CounterStrikeSharp.API.Modules.Admin
{
public partial class AdminData
{
[JsonPropertyName("identity")] public required string Identity { get; init; }
[JsonPropertyName("flags")] public HashSet<string> Flags { get; init; } = new();
// Flags loaded from file. Do not use this for actual comparisons.
[JsonPropertyName("flags")] public HashSet<string> _flags { get; init; } = new();
[JsonPropertyName("immunity")] public uint Immunity { get; set; } = 0;
[JsonPropertyName("command_overrides")] public Dictionary<string, bool> CommandOverrides { get; init; } = new();
// Key is the domain of the flag "e.g "css, os, kzsurf"). This should NOT include the @ character.
// Value is a hashmap of the flags inside of the domain (e.g "@css/generic")
public Dictionary<string, HashSet<string>> Flags { get; init; } = new();
public void InitalizeFlags()
{
AddFlags(_flags);
}
/// <summary>
/// Checks to see if a domain has a root flag inside of it.
/// </summary>
/// <param name="domain">Domain to check for.</param>
/// <returns>True if "@{domain}/root" or "@{domain}/*" is present, false if not.</returns>
public bool DomainHasRootFlag(string domain)
{
if (!Flags.ContainsKey(domain)) return false;
if (Flags[domain].Contains("@" + domain + "/root")) return true;
else if (Flags[domain].Contains("@" + domain + "/*")) return true;
else return false;
}
/// <summary>
/// Returns a list of all domains for flags.
/// </summary>
/// <returns></returns>
public string[] GetFlagDomains()
{
return Flags.Keys.ToArray();
}
/// <summary>
/// Returns a HashSet of all flags.
/// </summary>
/// <returns></returns>
public HashSet<string> GetAllFlags()
{
var flags = new HashSet<string>();
foreach (var domainFlags in Flags.Values)
{
flags.UnionWith(domainFlags);
}
return flags;
}
public void AddFlags(HashSet<string> flags)
{
var domains = flags.Where(
flag => flag.StartsWith(PermissionCharacters.UserPermissionChar))
.Distinct()
.Select(domain => domain.Split('/').First()[1..]);
foreach (var domain in domains)
{
if (!Flags.ContainsKey(domain))
{
Flags[domain] = new HashSet<string>();
}
Flags[domain].UnionWith(flags.Where(flag => flag.StartsWith(PermissionCharacters.UserPermissionChar + domain + '/')).ToHashSet());
}
}
public void RemoveFlags(HashSet<string> flags)
{
var domains = flags.Where(
flag => flag.StartsWith(PermissionCharacters.UserPermissionChar))
.Distinct()
.Select(domain => domain.Split('/').First()[1..]);
foreach (var domain in domains)
{
if (!Flags.ContainsKey(domain)) continue;
var domainFlags = flags.Where(flag => flag.StartsWith(PermissionCharacters.UserPermissionChar + domain + '/')).ToHashSet();
Flags[domain].ExceptWith(flags);
if (Flags[domain].Count() == 0) Flags.Remove(domain);
}
}
public bool DomainHasFlags(string domain, string[] flags, bool ignoreRoot = false)
{
if (!Flags.ContainsKey(domain)) return false;
if (DomainHasRootFlag(domain) && !ignoreRoot) return true;
return Flags[domain].IsSupersetOf(flags);
}
}
public static partial class AdminManager
@@ -39,16 +125,28 @@ namespace CounterStrikeSharp.API.Modules.Admin
_logger.LogWarning("Admin data file not found. Skipping admin data load.");
return;
}
var adminsFromFile = JsonSerializer.Deserialize<Dictionary<string, AdminData>>(File.ReadAllText(adminDataPath), new JsonSerializerOptions() { ReadCommentHandling = JsonCommentHandling.Skip });
var settings = new JsonSerializerOptions() { ReadCommentHandling = JsonCommentHandling.Skip };
var adminsFromFile = JsonSerializer.Deserialize<Dictionary<string, AdminData>>(File.ReadAllText(adminDataPath), settings);
if (adminsFromFile == null) { throw new FileNotFoundException(); }
foreach (var adminDef in adminsFromFile.Values)
{
adminDef.InitalizeFlags();
Console.WriteLine($"Domains: {adminDef.Flags.Count}");
if (SteamID.TryParse(adminDef.Identity, out var steamId))
{
if (Admins.ContainsKey(steamId!))
{
Admins[steamId!].Flags.UnionWith(adminDef.Flags);
// Merge domains together if we already have pre-existing values.
foreach (var (domain, flags) in adminDef.Flags)
{
if (Admins[steamId!].Flags.ContainsKey(domain))
{
Admins[steamId!].Flags[domain].UnionWith(flags);
}
}
if (adminDef.Immunity > Admins[steamId!].Immunity)
{
Admins[steamId!].Immunity = adminDef.Immunity;
@@ -74,8 +172,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// </summary>
/// <param name="steamId">SteamID object of the player.</param>
/// <returns>AdminData class if data found, null if not.</returns>
public static AdminData? GetPlayerAdminData(SteamID steamId)
public static AdminData? GetPlayerAdminData(SteamID? steamId)
{
if (steamId == null) return null;
return Admins.GetValueOrDefault(steamId);
}
@@ -83,8 +182,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// Removes a players admin data. This is not saved to "configs/admins.json"
/// </summary>
/// <param name="steamId">Steam ID remove admin data from.</param>
public static void RemovePlayerAdminData(SteamID steamId)
public static void RemovePlayerAdminData(SteamID? steamId)
{
if (steamId == null) return;
Admins.Remove(steamId);
}
@@ -102,8 +202,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of permissions.
if (player == null) return true;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
var playerData = GetPlayerAdminData(player.AuthorizedSteamID);
return playerData?.Flags.IsSupersetOf(flags) ?? false;
return PlayerHasPermissions(player.AuthorizedSteamID, flags);
}
/// <summary>
@@ -112,10 +211,36 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// <param name="steamId">Steam ID object.</param>
/// <param name="flags">Flags to look for in the players permission flags.</param>
/// <returns>True if flags are present, false if not.</returns>
public static bool PlayerHasPermissions(SteamID steamId, params string[] flags)
public static bool PlayerHasPermissions(SteamID? steamId, params string[] flags)
{
if (steamId == null) return false;
var playerData = GetPlayerAdminData(steamId);
return playerData?.Flags.IsSupersetOf(flags) ?? false;
if (playerData == null) return false;
// Check to see that all of the domains in the flags that we're checking are
// present in our player data.
var localDomains = flags.Where(
flag => flag.StartsWith(PermissionCharacters.UserPermissionChar))
.Distinct()
.Select(domain => domain.Split('/').First()[1..])
.ToHashSet();
var playerFlagDomains = playerData.GetFlagDomains().ToHashSet();
if (!playerFlagDomains.IsSupersetOf(localDomains)) return false;
// Loop through all of the domains and see if we have the required flags
// for every domain.
bool returnValue = true;
foreach (var domain in playerData.Flags)
{
if (!playerData.DomainHasFlags(domain.Key,
flags
.Where(flag => flag.StartsWith(PermissionCharacters.UserPermissionChar + domain.Key + '/'))
.ToArray()))
{
returnValue = false; break;
}
}
return returnValue;
}
#endregion
@@ -136,7 +261,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of permissions.
if (player == null) return true;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
var playerData = GetPlayerAdminData((SteamID)player.AuthorizedSteamID);
var playerData = GetPlayerAdminData(player.AuthorizedSteamID);
return playerData?.CommandOverrides.ContainsKey(command) ?? false;
}
@@ -147,8 +272,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// <param name="steamId">Steam ID object.</param>
/// <param name="command">Name of the command to check for.</param>
/// <returns>True if override exists, false if not.</returns>
public static bool PlayerHasCommandOverride(SteamID steamId, string command)
public static bool PlayerHasCommandOverride(SteamID? steamId, string command)
{
if (steamId == null) return false;
var playerData = GetPlayerAdminData(steamId);
return playerData?.CommandOverrides.ContainsKey(command) ?? false;
}
@@ -165,7 +291,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of permissions.
if (player == null) return true;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
var playerData = GetPlayerAdminData((SteamID)player.AuthorizedSteamID);
var playerData = GetPlayerAdminData(player.AuthorizedSteamID);
return playerData?.CommandOverrides.GetValueOrDefault(command) ?? false;
}
@@ -175,8 +301,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// <param name="steamId">Steam ID object.</param>
/// <param name="command">Name of the command to check for.</param>
/// <returns>True if override is active, false if not.</returns>
public static bool GetPlayerCommandOverrideState(SteamID steamId, string command)
public static bool GetPlayerCommandOverrideState(SteamID? steamId, string command)
{
if (steamId == null) return false;
var playerData = GetPlayerAdminData(steamId);
return playerData?.CommandOverrides.GetValueOrDefault(command) ?? false;
}
@@ -193,7 +320,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
// The server console should have access to all commands, regardless of permissions.
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return; }
SetPlayerCommandOverride((SteamID)player.AuthorizedSteamID, command, state);
SetPlayerCommandOverride(player.AuthorizedSteamID, command, state);
}
/// <summary>
@@ -202,8 +329,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// <param name="steamId">SteamID to add a flag to.</param>
/// <param name="command">Name of the command to check for.</param>
/// <param name="state">New state of the command override.</param>
public static void SetPlayerCommandOverride(SteamID steamId, string command, bool state)
public static void SetPlayerCommandOverride(SteamID? steamId, string command, bool state)
{
if (steamId == null) return;
var data = GetPlayerAdminData(steamId);
if (data == null)
{
@@ -235,7 +363,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
AddPlayerPermissions((SteamID)player.AuthorizedSteamID, flags);
AddPlayerPermissions(player.AuthorizedSteamID, flags);
}
/// <summary>
@@ -244,27 +372,23 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// </summary>
/// <param name="steamId">SteamID to add a flag to.</param>
/// <param name="flags">Flags to add for the player.</param>
public static void AddPlayerPermissions(SteamID steamId, params string[] flags)
public static void AddPlayerPermissions(SteamID? steamId, params string[] flags)
{
if (steamId == null) return;
var data = GetPlayerAdminData(steamId);
if (data == null)
{
data = new AdminData()
{
Identity = steamId.SteamId64.ToString(),
Flags = new(flags),
Flags = new(),
Groups = new()
};
Admins[steamId] = data;
return;
}
foreach (var flag in flags)
{
data.Flags.Add(flag);
}
Admins[steamId] = data;
Admins[steamId].AddFlags(flags.ToHashSet<string>());
}
/// <summary>
@@ -278,7 +402,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
RemovePlayerPermissions((SteamID)player.AuthorizedSteamID, flags);
RemovePlayerPermissions(player.AuthorizedSteamID, flags);
}
/// <summary>
@@ -287,13 +411,12 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// </summary>
/// <param name="steamId">Steam ID to remove flags from.</param>
/// <param name="flags">Flags to remove from the player.</param>
public static void RemovePlayerPermissions(SteamID steamId, params string[] flags)
public static void RemovePlayerPermissions(SteamID? steamId, params string[] flags)
{
if (steamId == null) return;
var data = GetPlayerAdminData(steamId);
if (data == null) return;
data.Flags.ExceptWith(flags);
Admins[steamId] = data;
Admins[steamId].RemoveFlags(flags.ToHashSet<string>());
}
/// <summary>
@@ -306,7 +429,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
ClearPlayerPermissions((SteamID)player.AuthorizedSteamID);
ClearPlayerPermissions(player.AuthorizedSteamID);
}
/// <summary>
@@ -314,8 +437,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// "configs/admins.json".
/// </summary>
/// <param name="steamId">Steam ID to remove flags from.</param>
public static void ClearPlayerPermissions(SteamID steamId)
public static void ClearPlayerPermissions(SteamID? steamId)
{
if (steamId == null) return;
var data = GetPlayerAdminData(steamId);
if (data == null) return;
@@ -335,7 +459,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
if (player == null) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
SetPlayerImmunity((SteamID)player.AuthorizedSteamID, value);
SetPlayerImmunity(player.AuthorizedSteamID, value);
}
/// <summary>
@@ -343,8 +467,9 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// </summary>
/// <param name="steamId">Steam ID of the player.</param>
/// <param name="value">New immunity value.</param>
public static void SetPlayerImmunity(SteamID steamId, uint value)
public static void SetPlayerImmunity(SteamID? steamId, uint value)
{
if (steamId == null) return;
var data = GetPlayerAdminData(steamId);
if (data == null) return;
@@ -366,10 +491,10 @@ namespace CounterStrikeSharp.API.Modules.Admin
if (target == null) return false;
if (!target.IsValid || target.Connected != PlayerConnectedState.PlayerConnected) return false;
var callerData = GetPlayerAdminData((SteamID)caller.AuthorizedSteamID);
var callerData = GetPlayerAdminData(caller.AuthorizedSteamID);
if (callerData == null) return false;
var targetData = GetPlayerAdminData((SteamID)target.AuthorizedSteamID);
var targetData = GetPlayerAdminData(target.AuthorizedSteamID);
if (targetData == null) return true;
return callerData.Immunity >= targetData.Immunity;
@@ -381,12 +506,15 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// <param name="caller">Caller of the command.</param>
/// <param name="target">Target of the command.</param>
/// <returns></returns>
public static bool CanPlayerTarget(SteamID caller, SteamID target)
public static bool CanPlayerTarget(SteamID? caller, SteamID? target)
{
if (caller == null) return false;
if (target == null) return false;
var callerData = GetPlayerAdminData(caller);
if (callerData == null) return false;
var targetData = GetPlayerAdminData(caller);
var targetData = GetPlayerAdminData(target);
if (targetData == null) return true;
return callerData.Immunity >= targetData.Immunity;

View File

@@ -11,7 +11,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
/// <summary>
/// The permissions for the command.
/// </summary>
public string[] Permissions { get; }
public HashSet<string> Permissions { get; }
/// <summary>
/// The name of the command that is attached to this attribute.
/// </summary>
@@ -23,7 +23,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
public BaseRequiresPermissions(params string[] permissions)
{
Permissions = permissions;
Permissions = permissions.ToHashSet();
Command = "";
}

View File

@@ -24,8 +24,14 @@ namespace CounterStrikeSharp.API.Modules.Admin
var groupPermissions = Permissions.Where(perm => perm.StartsWith(PermissionCharacters.GroupPermissionChar));
var userPermissions = Permissions.Where(perm => perm.StartsWith(PermissionCharacters.UserPermissionChar));
if (!AdminManager.PlayerInGroup(caller, groupPermissions.ToArray())) return false;
if (!AdminManager.PlayerHasPermissions(caller, userPermissions.ToArray())) return false;
if (!AdminManager.PlayerHasPermissions(caller, userPermissions.ToArray()))
{
return false;
}
if (!AdminManager.PlayerInGroup(caller, groupPermissions.ToArray()))
{
return false;
}
return true;
}

View File

@@ -20,12 +20,28 @@ namespace CounterStrikeSharp.API.Modules.Admin
}
if (!base.CanExecuteCommand(caller)) return false;
var groupPermissions = Permissions.Where(perm => perm.StartsWith(PermissionCharacters.GroupPermissionChar));
var userPermissions = Permissions.Where(perm => perm.StartsWith(PermissionCharacters.UserPermissionChar));
var adminData = AdminManager.GetPlayerAdminData(caller.AuthorizedSteamID);
if (adminData == null) return false;
return (groupPermissions.Intersect(adminData.Groups).Count() + userPermissions.Intersect(adminData.Flags).Count()) > 0;
// Check to see if the caller has a root flag for any of the domains in our permissions.
// If they do, remove all of the user flags and groups that belong to the domain
// from our permission check.
var domains = Permissions.Where(
flag => flag.StartsWith(PermissionCharacters.GroupPermissionChar))
.Distinct()
.Select(domain => domain.Split('/').First()[1..]);
foreach (var domain in domains)
{
if (adminData.DomainHasRootFlag(domain))
{
Permissions.RemoveWhere(flag => flag.Contains(domain + '/'));
}
}
var groupPermissions = Permissions.Where(perm => perm.StartsWith(PermissionCharacters.GroupPermissionChar));
var userPermissions = Permissions.Where(perm => perm.StartsWith(PermissionCharacters.UserPermissionChar));
return (groupPermissions.Intersect(adminData.Groups).Count() + userPermissions.Intersect(adminData.GetAllFlags()).Count()) > 0;
}
}
}

View File

@@ -29,7 +29,7 @@ namespace CounterStrikeSharp.API.Modules.Commands
public IntPtr Handle { get; }
internal CommandInfo(IntPtr pointer, CCSPlayerController player)
internal CommandInfo(IntPtr pointer, CCSPlayerController? player)
{
Handle = pointer;
CallingPlayer = player;

View File

@@ -2,6 +2,9 @@
namespace CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
#pragma warning disable CS8601 // Possible null reference assignment.
#pragma warning disable CS8604 // Possible null reference argument.
public class MemoryFunctionVoid : BaseMemoryFunction
{
public MemoryFunctionVoid(string signature) : base(signature, DataType.DATA_TYPE_VOID, Array.Empty<DataType>())
@@ -276,4 +279,7 @@ public class MemoryFunctionVoid<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> : BaseM
{
InvokeInternalVoid(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
}
}
}
#pragma warning restore CS8601 // Possible null reference assignment.
#pragma warning restore CS8604 // Possible null reference argument.

View File

@@ -2,6 +2,9 @@
namespace CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
#pragma warning disable CS8601 // Possible null reference assignment.
#pragma warning disable CS8604 // Possible null reference argument.
public class MemoryFunctionWithReturn<TResult> : BaseMemoryFunction
{
public MemoryFunctionWithReturn(string signature) : base(signature, typeof(TResult).ToValidDataType(),
@@ -264,4 +267,7 @@ public class MemoryFunctionWithReturn<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T
{
return InvokeInternal<TResult>(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
}
}
}
#pragma warning restore CS8601 // Possible null reference assignment.
#pragma warning restore CS8604 // Possible null reference argument.

View File

@@ -19,6 +19,8 @@ using System.Collections.Generic;
using System.Linq;
using CounterStrikeSharp.API.Core;
#pragma warning disable CS8601 // Possible null reference assignment.
namespace CounterStrikeSharp.API.Modules.Memory
{
public partial class VirtualFunction
@@ -587,4 +589,6 @@ namespace CounterStrikeSharp.API.Modules.Memory
#endregion
}
}
}
#pragma warning restore CS8601 // Possible null reference assignment.

View File

@@ -21,6 +21,8 @@ using CounterStrikeSharp.API.Core;
namespace CounterStrikeSharp.API.Modules.Memory;
#pragma warning disable CS8601 // Possible null reference assignment.
public partial class VirtualFunction
{
private static Dictionary<string, IntPtr> _createdFunctions = new();
@@ -1147,4 +1149,6 @@ public partial class VirtualFunction
};
}
#endregion
}
}
#pragma warning restore CS8601 // Possible null reference assignment.

View File

@@ -76,4 +76,8 @@ public static class VirtualFunctions
public static MemoryFunctionVoid<CBaseTrigger, CBaseEntity> CBaseTrigger_EndTouchFunc = new (GameData.GetSignature("CBaseTrigger_EndTouch"));
public static Action<CBaseTrigger, CBaseEntity> CBaseTrigger_EndTouch = CBaseTrigger_EndTouchFunc.Invoke;
public static MemoryFunctionVoid<IntPtr, IntPtr> RemovePlayerItemFunc =
new(GameData.GetSignature("CBasePlayerPawn_RemovePlayerItem"));
public static Action<IntPtr, IntPtr> RemovePlayerItemVirtual = RemovePlayerItemFunc.Invoke;
}

View File

@@ -2,6 +2,8 @@
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace CounterStrikeSharp.API.Modules.Utils;
@@ -17,36 +19,48 @@ public class CommandUtils
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)
var data = AdminManager.GetCommandOverrideData(name);
if (data != null)
{
attr.Command = name;
if (!attr.CanExecuteCommand(caller))
{
command.ReplyToCommand("[CSS] You do not have the correct permissions to execute this command.");
return;
}
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);
}
}
}
// 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)
// 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)
{
var attrType = (data.CheckType == "all") ? typeof(RequiresPermissions) : typeof(RequiresPermissionsOr);
var attr = (BaseRequiresPermissions)Activator.CreateInstance(attrType, args: AdminManager.GetPermissionOverrides(name));
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;
}
}
@@ -72,7 +86,12 @@ public class CommandUtils
// 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;
}
}

View File

@@ -75,6 +75,41 @@ namespace CounterStrikeSharp.API
return new Target(pattern).GetTarget(player);
}
public static bool RemoveItemByDesignerName(this CCSPlayerController player, string designerName)
{
return RemoveItemByDesignerName(player, designerName, false);
}
public static bool RemoveItemByDesignerName(this CCSPlayerController player, string designerName, bool shouldRemoveEntity)
{
CHandle<CBasePlayerWeapon>? item = null;
if (player.PlayerPawn.Value == null || player.PlayerPawn.Value.WeaponServices == null) return false;
foreach(var weapon in player.PlayerPawn.Value.WeaponServices.MyWeapons)
{
if (weapon is not { IsValid: true, Value.IsValid: true })
continue;
if (weapon.Value.DesignerName != designerName)
continue;
item = weapon;
}
if (item != null && item.Value != null)
{
player.PlayerPawn.Value.RemovePlayerItem(item.Value);
if (shouldRemoveEntity)
{
item.Value.Remove();
}
return true;
}
return false;
}
public static IEnumerable<T> FindAllEntitiesByDesignerName<T>(string designerName) where T : CEntityInstance
{
var pEntity = new CEntityIdentity(EntitySystem.FirstActiveEntity);

View File

@@ -0,0 +1,36 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
namespace CounterStrikeSharp.API
{
[Flags]
public enum VoiceFlags : Byte
{
Normal = 0,
Muted = (1 << 0),
All = (1 << 1),
ListenAll = (1 << 2),
Team = (1 << 3),
ListenTeam = (1 << 4),
}
public enum ListenOverride
{
Default = 0,
Mute,
Hear
}
}

View File

@@ -233,7 +233,9 @@ internal static partial class Program
continue;
// Putting these in the too hard basket for now.
if (field.Name == "m_VoteOptions" || field.Name == "m_aShootSounds") continue;
if (field.Name == "m_VoteOptions" || field.Name == "m_aShootSounds" || field.Name == "m_pVecRelationships") continue;
if (IgnoreClasses.Contains(field.Type.Name)) continue;
if (field.Type.Category == SchemaTypeCategory.Bitfield) continue;
if (field.Type is { Category: SchemaTypeCategory.Atomic, Atomic: SchemaAtomicCategory.Collection })
{
@@ -242,9 +244,10 @@ internal static partial class Program
var handleParams = $"this.Handle, \"{schemaClassName}\", \"{field.Name}\"";
builder.AppendLine($" // {field.Name}");
builder.AppendLine($"\t// {field.Name}");
builder.AppendLine($"\t[SchemaMember(\"{schemaClassName}\", \"{field.Name}\")]");
if (field.Type is { Category: SchemaTypeCategory.FixedArray, CsTypeName: "string" })
if (field.Type is { Category: SchemaTypeCategory.FixedArray, CsTypeName: "string" } or { Category: SchemaTypeCategory.Ptr, CsTypeName: "string" })
{
var getter = $"return Schema.GetString({handleParams});";
var setter = $"Schema.SetString({handleParams}, value);";

View File

@@ -4816,10 +4816,10 @@
}
},
{
"name": "m_eThrowStatus",
"name": "m_bThrowAnimating",
"type": {
"category": 6,
"name": "EGrenadeThrowState"
"category": 0,
"name": "bool"
}
},
{
@@ -4849,6 +4849,40 @@
"category": 5,
"name": "GameTime_t"
}
},
{
"name": "m_bJustPulledPin",
"type": {
"category": 0,
"name": "bool"
}
},
{
"name": "m_nNextHoldTick",
"type": {
"category": 5,
"name": "GameTick_t"
}
},
{
"name": "m_flNextHoldFrac",
"type": {
"category": 0,
"name": "float32"
}
},
{
"name": "m_hSwitchToWeaponAfterThrow",
"type": {
"atomic": 1,
"category": 4,
"inner": {
"category": 5,
"name": "CCSWeaponBase"
},
"name": "CHandle< CCSWeaponBase >",
"outer": "CHandle"
}
}
],
"parent": "CCSWeaponBase"
@@ -6312,7 +6346,7 @@
"category": 0,
"name": "char"
},
"name": "char[4096]"
"name": "char[260]"
}
},
{
@@ -6842,6 +6876,13 @@
"name": "int64"
}
},
{
"name": "m_nLastRealCommandNumberExecuted",
"type": {
"category": 0,
"name": "int32"
}
},
{
"name": "m_iIgnoreGlobalChat",
"type": {
@@ -16738,10 +16779,17 @@
}
},
{
"name": "m_bPlayerFireEventIsPrimary",
"name": "m_ePlayerFireEvent",
"type": {
"category": 0,
"name": "bool"
"category": 6,
"name": "PlayerAnimEvent_t"
}
},
{
"name": "m_ePlayerFireEventAttackType",
"type": {
"category": 6,
"name": "WeaponAttackType_t"
}
},
{
@@ -16799,7 +16847,7 @@
"category": 5,
"name": "HSequence"
},
"name": "HSequence[6]"
"name": "HSequence[7]"
}
},
{
@@ -16951,10 +16999,17 @@
}
},
{
"name": "m_flPostponeFireReadyTime",
"name": "m_nPostponeFireReadyTicks",
"type": {
"category": 5,
"name": "GameTime_t"
"name": "GameTick_t"
}
},
{
"name": "m_flPostponeFireReadyFrac",
"type": {
"category": 0,
"name": "float32"
}
},
{
@@ -32791,40 +32846,6 @@
],
"parent": "CLogicalEntity"
},
"CLogicEventListener": {
"fields": [
{
"name": "m_strEventName",
"type": {
"atomic": 0,
"category": 4,
"name": "CUtlString"
}
},
{
"name": "m_bIsEnabled",
"type": {
"category": 0,
"name": "bool"
}
},
{
"name": "m_nTeam",
"type": {
"category": 0,
"name": "int32"
}
},
{
"name": "m_OnEventFired",
"type": {
"category": 5,
"name": "CEntityIOOutput"
}
}
],
"parent": "CLogicalEntity"
},
"CLogicGameEvent": {
"fields": [
{
@@ -34452,35 +34473,7 @@
"parent": "CLogicalEntity"
},
"CMelee": {
"fields": [
{
"name": "m_flThrowAt",
"type": {
"category": 5,
"name": "GameTime_t"
}
},
{
"name": "m_hThrower",
"type": {
"atomic": 1,
"category": 4,
"inner": {
"category": 5,
"name": "CBaseEntity"
},
"name": "CHandle< CBaseEntity >",
"outer": "CHandle"
}
},
{
"name": "m_bDidThrowDamage",
"type": {
"category": 0,
"name": "bool"
}
}
],
"fields": [],
"parent": "CCSWeaponBase"
},
"CMeshletDescriptor": {
@@ -54566,10 +54559,6 @@
"fields": [],
"parent": "CBaseTrigger"
},
"CTriggerHostageReset": {
"fields": [],
"parent": "CBaseTrigger"
},
"CTriggerHurt": {
"fields": [
{
@@ -70344,7 +70333,7 @@
"category": 0,
"name": "char"
},
"name": "char[4096]"
"name": "char[260]"
}
},
{
@@ -76074,6 +76063,20 @@
"category": 0,
"name": "float32"
}
},
{
"name": "m_flTickInterval",
"type": {
"category": 0,
"name": "float32"
}
},
{
"name": "m_flTickStartTime",
"type": {
"category": 0,
"name": "float64"
}
}
]
},
@@ -90386,23 +90389,6 @@
}
]
},
"C4LightEffect_t": {
"align": 4,
"items": [
{
"name": "eLightEffectNone",
"value": 0
},
{
"name": "eLightEffectDropped",
"value": 1
},
{
"name": "eLightEffectThirdPersonHeld",
"value": 2
}
]
},
"CAnimationGraphVisualizerPrimitiveType": {
"align": 4,
"items": [
@@ -91374,23 +91360,6 @@
}
]
},
"EGrenadeThrowState": {
"align": 4,
"items": [
{
"name": "NotThrowing",
"value": 0
},
{
"name": "Throwing",
"value": 1
},
{
"name": "ThrowComplete",
"value": 2
}
]
},
"EInButtonState": {
"align": 4,
"items": [
@@ -96428,7 +96397,7 @@
},
{
"name": "ALL_CONTEXTS",
"value": 4293918720
"value": 18446744073708503040
},
{
"name": "ALL_SCENTS",

View File

@@ -40,7 +40,7 @@ public record SchemaFieldType
}
public bool IsString =>
Category == SchemaTypeCategory.FixedArray
(Category == SchemaTypeCategory.FixedArray || Category == SchemaTypeCategory.Ptr)
&& Inner!.Category == SchemaTypeCategory.Builtin
&& Inner.Name == "char";
@@ -98,7 +98,8 @@ public record SchemaFieldType
public string CsTypeName => Category switch
{
SchemaTypeCategory.Builtin => BuiltinToCsKeyword(Name),
SchemaTypeCategory.Ptr => $"{Inner!.CsTypeName}?",
SchemaTypeCategory.Ptr => IsString
? "string" : $"{Inner!.CsTypeName}?",
SchemaTypeCategory.FixedArray => IsString
? "string"
: $"{Inner!.CsTypeName}[]",

View File

@@ -28,6 +28,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithEntityOutputHooks", "..
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CounterStrikeSharp.API.Tests", "CounterStrikeSharp.API.Tests\CounterStrikeSharp.API.Tests.csproj", "{BBA80E1B-109D-4ABD-9ADF-46EB0FEDFCD3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithTranslations", "..\examples\WithTranslations\WithTranslations.csproj", "{BB44E08E-CCA8-4E22-A132-11B2F69D1890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithVoiceOverrides", "..\examples\WithVoiceOverrides\WithVoiceOverrides.csproj", "{6FA3107D-42AF-42A0-BF51-2230D13268B5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -86,6 +90,14 @@ Global
{BBA80E1B-109D-4ABD-9ADF-46EB0FEDFCD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBA80E1B-109D-4ABD-9ADF-46EB0FEDFCD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBA80E1B-109D-4ABD-9ADF-46EB0FEDFCD3}.Release|Any CPU.Build.0 = Release|Any CPU
{BB44E08E-CCA8-4E22-A132-11B2F69D1890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB44E08E-CCA8-4E22-A132-11B2F69D1890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB44E08E-CCA8-4E22-A132-11B2F69D1890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB44E08E-CCA8-4E22-A132-11B2F69D1890}.Release|Any CPU.Build.0 = Release|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{57E64289-5D69-4AA1-BEF0-D0D96A55EE8F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
@@ -97,5 +109,7 @@ Global
{3032F3FA-E20A-4581-9A08-2FB5FF1524F4} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{A641D8D7-35F1-48AB-AABA-EDFB6B7FC49B} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{31EABE0B-871F-497B-BF36-37FFC6FAD15F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{BB44E08E-CCA8-4E22-A132-11B2F69D1890} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{6FA3107D-42AF-42A0-BF51-2230D13268B5} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
EndGlobalSection
EndGlobal

View File

@@ -16,6 +16,7 @@
using System;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
@@ -23,6 +24,7 @@ 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 CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
@@ -31,6 +33,7 @@ using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace TestPlugin
@@ -462,6 +465,43 @@ namespace TestPlugin
player.PlayerPawn.Value.CommitSuicide(true, true);
}
[CommandHelper(minArgs: 1, usage: "[weaponName]")]
[ConsoleCommand("css_strip", "Removes weapon by name")]
public void OnStripActiveWeapon(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
if (!player.PlayerPawn.IsValid) return;
player.RemoveItemByDesignerName(command.GetArg(1));
}
[ConsoleCommand("css_stripweapons", "Removes player weapons")]
public void OnStripWeapons(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
if (!player.PlayerPawn.IsValid) return;
player.RemoveWeapons();
}
[ConsoleCommand("css_teleportup", "Teleports the player up")]
public void OnTeleport(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
if (!player.PlayerPawn.IsValid) return;
player.PlayerPawn.Value.Teleport(player.PlayerPawn.Value.AbsOrigin.With(z: player.PlayerPawn.Value.AbsOrigin.Z + 100), player.PlayerPawn.Value.AbsRotation, new Vector(IntPtr.Zero));
}
[ConsoleCommand("css_respawn", "Respawns the player")]
public void OnRespawn(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
if (!player.PlayerPawn.IsValid) return;
player.Respawn();
}
[ConsoleCommand("cssharp_attribute", "This is a custom attribute event")]
public void OnCommand(CCSPlayerController? player, CommandInfo command)
@@ -522,6 +562,13 @@ namespace TestPlugin
}
}
[ConsoleCommand("css_localetest", "Test Translations")]
public void OnCommandLocaleTest(CCSPlayerController? player, CommandInfo command)
{
Logger.LogInformation("Current Culture is {Culture}", CultureInfo.CurrentCulture);
command.ReplyToCommand(Localizer["testPlugin.maxPlayersAnnouncement", Server.MaxPlayers]);
}
[ConsoleCommand("css_sound", "Play a sound to client")]
public void OnCommandSound(CCSPlayerController? player, CommandInfo command)
{

View File

@@ -13,4 +13,10 @@
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="lang\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
{
"testPlugin.maxPlayersAnnouncement": "The server currently has a max player count of {0} players (GB)"
}

View File

@@ -0,0 +1,3 @@
{
"testPlugin.maxPlayersAnnouncement": "The server currently has a max player count of {0} players",
}

View File

@@ -0,0 +1,3 @@
{
"testPlugin.maxPlayersAnnouncement": "Le serveur compte actuellement un nombre maximum de {0} joueurs."
}

View File

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

View File

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

View File

@@ -1,3 +1,12 @@
// Copyright (C) 2023 neverlosecc
// See end of file for extended copyright information.
/**
* =============================================================================
* Source2Gen
* Copyright (C) 2023 neverlose (https://github.com/neverlosecc/source2gen)
* =============================================================================
**/
#pragma once
#include <vector>
@@ -392,3 +401,18 @@ class CSchemaSystem
return CALL_VIRTUAL(CSchemaSystemTypeScope*, 13, this, m_module_name, nullptr);
}
};
// source2gen - Source2 games SDK generator
// Copyright 2023 neverlosecc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

View File

@@ -20,6 +20,7 @@
#include "interfaces/cs2_interfaces.h"
#include "core/managers/entity_manager.h"
#include "core/managers/server_manager.h"
#include "core/managers/voice_manager.h"
#include <public/game/server/iplayerinfo.h>
#include <public/entity2/entitysystem.h>
@@ -77,6 +78,7 @@ ConCommandManager conCommandManager;
EntityManager entityManager;
ChatManager chatManager;
ServerManager serverManager;
VoiceManager voiceManager;
bool gameLoopInitialized = false;
GetLegacyGameEventListener_t* GetLegacyGameEventListener = nullptr;

View File

@@ -53,6 +53,7 @@ class HookManager;
class EntityManager;
class ChatManager;
class ServerManager;
class VoiceManager;
class CCoreConfig;
class CGameConfig;
@@ -98,6 +99,7 @@ extern TimerSystem timerSystem;
extern ChatCommands chatCommands;
extern ChatManager chatManager;
extern ServerManager serverManager;
extern VoiceManager voiceManager;
extern HookManager hookManager;
extern SourceHook::ISourceHook *source_hook;

View File

@@ -31,6 +31,7 @@
#include "core/managers/player_manager.h"
#include "core/managers/con_command_manager.h"
#include "core/managers/voice_manager.h"
#include <public/eiface.h>
#include <public/inetchannelinfo.h>
@@ -45,8 +46,10 @@
SH_DECL_HOOK4_void(IServerGameClients, ClientActive, SH_NOATTRIB, 0, CPlayerSlot, bool, const char*,
uint64);
SH_DECL_HOOK5_void(IServerGameClients, ClientDisconnect, SH_NOATTRIB, 0, CPlayerSlot, ENetworkDisconnectionReason,
const char*, uint64, const char*);
SH_DECL_HOOK5_void(IServerGameClients, ClientDisconnect, SH_NOATTRIB, 0, CPlayerSlot,
ENetworkDisconnectionReason, const char*, uint64, const char*);
SH_DECL_HOOK1_void(IServerGameClients, ClientVoice, SH_NOATTRIB, 0, CPlayerSlot);
SH_DECL_HOOK4_void(IServerGameClients, ClientPutInServer, SH_NOATTRIB, 0, CPlayerSlot, char const*,
int, uint64);
SH_DECL_HOOK1_void(IServerGameClients, ClientSettingsChanged, SH_NOATTRIB, 0, CPlayerSlot);
@@ -75,6 +78,8 @@ void PlayerManager::OnAllInitialized()
SH_MEMBER(this, &PlayerManager::OnClientDisconnect_Post), true);
SH_ADD_HOOK(IServerGameClients, ClientCommand, globals::serverGameClients,
SH_MEMBER(this, &PlayerManager::OnClientCommand), false);
SH_ADD_HOOK(IServerGameClients, ClientVoice, globals::serverGameClients,
SH_MEMBER(this, &PlayerManager::OnClientVoice), true);
m_on_client_connect_callback = globals::callbackManager.CreateCallback("OnClientConnect");
m_on_client_connected_callback = globals::callbackManager.CreateCallback("OnClientConnected");
@@ -83,6 +88,7 @@ void PlayerManager::OnAllInitialized()
m_on_client_disconnect_callback = globals::callbackManager.CreateCallback("OnClientDisconnect");
m_on_client_disconnect_post_callback =
globals::callbackManager.CreateCallback("OnClientDisconnectPost");
m_on_client_voice_callback = globals::callbackManager.CreateCallback("OnClientVoice");
m_on_client_authorized_callback = globals::callbackManager.CreateCallback("OnClientAuthorized");
}
@@ -100,6 +106,8 @@ void PlayerManager::OnShutdown()
SH_MEMBER(this, &PlayerManager::OnClientDisconnect_Post), true);
SH_REMOVE_HOOK(IServerGameClients, ClientCommand, globals::serverGameClients,
SH_MEMBER(this, &PlayerManager::OnClientCommand), false);
SH_REMOVE_HOOK(IServerGameClients, ClientVoice, globals::serverGameClients,
SH_MEMBER(this, &PlayerManager::OnClientVoice), true);
globals::callbackManager.ReleaseCallback(m_on_client_connect_callback);
globals::callbackManager.ReleaseCallback(m_on_client_connected_callback);
@@ -107,6 +115,7 @@ void PlayerManager::OnShutdown()
globals::callbackManager.ReleaseCallback(m_on_client_disconnect_callback);
globals::callbackManager.ReleaseCallback(m_on_client_disconnect_post_callback);
globals::callbackManager.ReleaseCallback(m_on_client_authorized_callback);
globals::callbackManager.ReleaseCallback(m_on_client_voice_callback);
}
bool PlayerManager::OnClientConnect(CPlayerSlot slot, const char* pszName, uint64 xuid,
@@ -120,8 +129,10 @@ bool PlayerManager::OnClientConnect(CPlayerSlot slot, const char* pszName, uint6
CPlayer* pPlayer = &m_players[client];
if (pPlayer->IsConnected()) {
OnClientDisconnect(slot, ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID, pszName, xuid, pszNetworkID);
OnClientDisconnect_Post(slot, ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID, pszName, xuid, pszNetworkID);
OnClientDisconnect(slot, ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID, pszName,
xuid, pszNetworkID);
OnClientDisconnect_Post(slot, ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID,
pszName, xuid, pszNetworkID);
}
pPlayer->Initialize(pszName, pszNetworkID, slot);
@@ -222,8 +233,7 @@ void PlayerManager::OnClientPutInServer(CPlayerSlot slot, char const* pszName, i
m_on_client_put_in_server_callback->Execute();
}
void PlayerManager::OnClientDisconnect(CPlayerSlot slot,
ENetworkDisconnectionReason reason,
void PlayerManager::OnClientDisconnect(CPlayerSlot slot, ENetworkDisconnectionReason reason,
const char* pszName, uint64 xuid, const char* pszNetworkID)
{
CSSHARP_CORE_TRACE("[PlayerManager][OnClientDisconnect] - {}, {}, {}", slot.Get(), pszName,
@@ -246,8 +256,7 @@ void PlayerManager::OnClientDisconnect(CPlayerSlot slot,
// globals::entityListener.HandleEntityDeleted(pPlayer->GetBaseEntity(), client);
}
void PlayerManager::OnClientDisconnect_Post(CPlayerSlot slot,
ENetworkDisconnectionReason reason,
void PlayerManager::OnClientDisconnect_Post(CPlayerSlot slot, ENetworkDisconnectionReason reason,
const char* pszName, uint64 xuid,
const char* pszNetworkID) const
{
@@ -269,16 +278,27 @@ void PlayerManager::OnClientDisconnect_Post(CPlayerSlot slot,
m_on_client_disconnect_post_callback->Execute();
}
void PlayerManager::OnClientVoice(CPlayerSlot slot) const
{
CSSHARP_CORE_TRACE("[PlayerManager][OnClientVoice] - {}", slot.Get());
m_on_client_voice_callback->ScriptContext().Reset();
m_on_client_voice_callback->ScriptContext().Push(slot.Get());
m_on_client_voice_callback->Execute();
}
void PlayerManager::OnLevelEnd()
{
CSSHARP_CORE_TRACE("[PlayerManager][OnLevelEnd]");
for (int i = 0; i <= m_max_clients; i++) {
for (int i = 0; i <= MaxClients(); i++) {
if (m_players[i].IsConnected()) {
OnClientDisconnect(m_players[i].m_slot, ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID, m_players[i].GetName(), 0,
m_players[i].GetIpAddress());
OnClientDisconnect_Post(m_players[i].m_slot, ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID, m_players[i].GetName(), 0,
m_players[i].GetIpAddress());
OnClientDisconnect(m_players[i].m_slot,
ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID,
m_players[i].GetName(), 0, m_players[i].GetIpAddress());
OnClientDisconnect_Post(m_players[i].m_slot,
ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID,
m_players[i].GetName(), 0, m_players[i].GetIpAddress());
}
}
m_player_count = 0;
@@ -286,10 +306,13 @@ void PlayerManager::OnLevelEnd()
void PlayerManager::OnClientCommand(CPlayerSlot slot, const CCommand& args) const
{
CSSHARP_CORE_TRACE("[PlayerManager][OnClientCommand] - {}, {}, {}", slot.Get(), args.Arg(0), (void*)&args);
CSSHARP_CORE_TRACE("[PlayerManager][OnClientCommand] - {}, {}, {}", slot.Get(), args.Arg(0),
(void*)&args);
const char* cmd = args.Arg(0);
globals::voiceManager.OnClientCommand(slot, args);
auto result = globals::conCommandManager.ExecuteCommandCallbacks(
cmd, CCommandContext(CommandTarget_t::CT_NO_TARGET, slot), args, HookMode::Pre);
@@ -302,11 +325,11 @@ int PlayerManager::ListenClient() const { return m_listen_client; }
int PlayerManager::NumPlayers() const { return m_player_count; }
int PlayerManager::MaxClients() const { return m_max_clients; }
int PlayerManager::MaxClients() const { return globals::getGlobalVars()->maxClients; }
CPlayer* PlayerManager::GetPlayerBySlot(int client) const
{
if (client > m_max_clients || client < 0) {
if (client > MaxClients() || client < 0) {
return nullptr;
}
@@ -426,7 +449,6 @@ INetChannelInfo* CPlayer::GetNetInfo() const { return globals::engine->GetPlayer
PlayerManager::PlayerManager()
{
m_max_clients = 64;
m_players = new CPlayer[66];
m_player_count = 0;
m_user_id_lookup = new int[USHRT_MAX + 1];
@@ -441,7 +463,7 @@ void PlayerManager::RunAuthChecks()
m_last_auth_check_time = globals::timerSystem.GetTickedTime();
for (int i = 0; i <= m_max_clients; i++) {
for (int i = 0; i <= MaxClients(); i++) {
if (m_players[i].IsConnected()) {
if (m_players[i].IsAuthorized() || m_players[i].IsFakeClient())
continue;
@@ -532,6 +554,17 @@ float CPlayer::GetLatency() const
return GetNetInfo()->GetLatency(FLOW_INCOMING) + GetNetInfo()->GetLatency(FLOW_OUTGOING);
}
void CPlayer::SetListen(CPlayerSlot slot, ListenOverride listen)
{
m_listenMap[slot.Get()] = listen;
}
void CPlayer::SetVoiceFlags(VoiceFlag_t flags) { m_voiceFlag = flags; }
VoiceFlag_t CPlayer::GetVoiceFlags() { return m_voiceFlag; }
ListenOverride CPlayer::GetListen(CPlayerSlot slot) const { return m_listenMap[slot.Get()]; }
void CPlayer::Connect()
{
if (m_is_in_game) {
@@ -551,6 +584,9 @@ void CPlayer::Disconnect()
m_user_id = -1;
m_is_authorized = false;
m_ip_address.clear();
m_selfMutes->ClearAll();
memset(m_listenMap, 0, sizeof m_listenMap);
m_voiceFlag = 0;
}
QAngle CPlayer::GetAbsAngles() const { return m_info->GetAbsAngles(); }

View File

@@ -45,30 +45,50 @@ namespace counterstrikesharp {
class ScriptCallback;
class CBaseEntityWrapper;
class CPlayer {
enum ListenOverride
{
Listen_Default = 0,
Listen_Mute,
Listen_Hear
};
enum VoiceFlagValue
{
Speak_Normal = 0,
Speak_Muted = 1 << 0,
Speak_All = 1 << 1,
Speak_ListenAll = 1 << 2,
Speak_Team = 1 << 3,
Speak_ListenTeam = 1 << 4,
};
typedef uint8_t VoiceFlag_t;
class CPlayer
{
friend class PlayerManager;
public:
public:
CPlayer();
public:
void Initialize(const char *name, const char *ip, CPlayerSlot slot);
public:
void Initialize(const char* name, const char* ip, CPlayerSlot slot);
void Connect();
void Disconnect();
IPlayerInfo *GetPlayerInfo() const;
IPlayerInfo* GetPlayerInfo() const;
bool WasCountedAsInGame() const;
int GetUserId();
bool IsAuthStringValidated() const;
void Authorize();
public:
const char *GetName() const;
const CSteamID *GetSteamId();
void SetSteamId(const CSteamID *steam_id);
public:
const char* GetName() const;
const CSteamID* GetSteamId();
void SetSteamId(const CSteamID* steam_id);
bool IsConnected() const;
bool IsFakeClient() const;
bool IsAuthorized() const;
void PrintToConsole(const char *message) const;
void PrintToConsole(const char* message) const;
// void PrintToChat(const char *message);
// void PrintToHint(const char *message);
// void PrintToCenter(const char *message);
@@ -76,26 +96,30 @@ public:
Vector GetAbsOrigin() const;
bool IsAlive() const;
bool IsInGame() const;
void Kick(const char *kickReason);
const char *GetWeaponName() const;
void Kick(const char* kickReason);
const char* GetWeaponName() const;
void ChangeTeam(int team) const;
int GetTeam() const;
int GetArmor() const;
int GetFrags() const;
int GetDeaths() const;
const char *GetKeyValue(const char *key) const;
const char* GetKeyValue(const char* key) const;
Vector GetMaxSize() const;
Vector GetMinSize() const;
int GetMaxHealth() const;
const char *GetIpAddress() const;
const char *GetModelName() const;
const char* GetIpAddress() const;
const char* GetModelName() const;
int GetUserId() const;
float GetTimeConnected() const;
float GetLatency() const;
void SetListen(CPlayerSlot slot, ListenOverride listen);
void SetVoiceFlags(VoiceFlag_t flags);
VoiceFlag_t GetVoiceFlags();
ListenOverride GetListen(CPlayerSlot slot) const;
public:
public:
std::string m_name;
IPlayerInfo *m_info = nullptr;
IPlayerInfo* m_info = nullptr;
std::string m_auth_id;
bool m_is_connected = false;
bool m_is_fake_client = false;
@@ -105,72 +129,64 @@ public:
CPlayerSlot m_slot = CPlayerSlot(-1);
const CSteamID* m_steamId;
std::string m_ip_address;
void SetName(const char *name);
INetChannelInfo *GetNetInfo() const;
ListenOverride m_listenMap[66] = {};
VoiceFlag_t m_voiceFlag = 0;
CPlayerBitVec m_selfMutes[64] = {};
void SetName(const char* name);
INetChannelInfo* GetNetInfo() const;
};
class PlayerManager : public GlobalClass {
class PlayerManager : public GlobalClass
{
friend class CPlayer;
public:
public:
PlayerManager();
void OnStartup() override;
void OnAllInitialized() override;
bool OnClientConnect(CPlayerSlot slot,
const char *pszName,
uint64 xuid,
const char *pszNetworkID,
bool unk1,
CBufferString *pRejectReason);
bool OnClientConnect_Post(CPlayerSlot slot,
const char *pszName,
uint64 xuid,
const char *pszNetworkID,
bool unk1,
CBufferString *pRejectReason);
void OnClientPutInServer(CPlayerSlot slot, char const *pszName, int type, uint64 xuid);
void OnClientDisconnect(CPlayerSlot slot,
ENetworkDisconnectionReason reason,
const char *pszName,
uint64 xuid,
const char *pszNetworkID);
void OnClientDisconnect_Post(CPlayerSlot slot,
ENetworkDisconnectionReason reason,
const char *pszName,
uint64 xuid,
const char *pszNetworkID) const;
bool OnClientConnect(CPlayerSlot slot, const char* pszName, uint64 xuid,
const char* pszNetworkID, bool unk1, CBufferString* pRejectReason);
bool OnClientConnect_Post(CPlayerSlot slot, const char* pszName, uint64 xuid,
const char* pszNetworkID, bool unk1, CBufferString* pRejectReason);
void OnClientPutInServer(CPlayerSlot slot, char const* pszName, int type, uint64 xuid);
void OnClientDisconnect(CPlayerSlot slot, ENetworkDisconnectionReason reason,
const char* pszName, uint64 xuid, const char* pszNetworkID);
void OnClientDisconnect_Post(CPlayerSlot slot, ENetworkDisconnectionReason reason,
const char* pszName, uint64 xuid, const char* pszNetworkID) const;
void OnClientVoice(CPlayerSlot slot) const;
void OnAuthorized(CPlayer* player) const;
void OnServerActivate(edict_t *pEdictList, int edictCount, int clientMax) const;
void OnServerActivate(edict_t* pEdictList, int edictCount, int clientMax) const;
void OnThink(bool last_tick) const;
void OnShutdown() override;
void OnLevelEnd() override;
void OnClientCommand(CPlayerSlot slot, const CCommand &args) const;
void OnClientCommand(CPlayerSlot slot, const CCommand& args) const;
int ListenClient() const;
void RunAuthChecks();
public:
public:
int NumPlayers() const;
int MaxClients() const;
CPlayer *GetPlayerBySlot(int client) const;
CPlayer *GetClientOfUserId(int user_id) const;
CPlayer* GetPlayerBySlot(int client) const;
CPlayer* GetClientOfUserId(int user_id) const;
private:
void InvalidatePlayer(CPlayer *pPlayer) const;
private:
void InvalidatePlayer(CPlayer* pPlayer) const;
CPlayer *m_players;
CPlayer* m_players;
int m_max_clients = 0;
int m_player_count = 0;
int *m_user_id_lookup;
int* m_user_id_lookup;
int m_listen_client;
bool m_is_listen_server;
float m_last_auth_check_time = 0;
ScriptCallback *m_on_client_connect_callback;
ScriptCallback *m_on_client_put_in_server_callback;
ScriptCallback *m_on_client_connected_callback;
ScriptCallback *m_on_client_disconnect_callback;
ScriptCallback *m_on_client_disconnect_post_callback;
ScriptCallback *m_on_client_authorized_callback;
ScriptCallback* m_on_client_connect_callback;
ScriptCallback* m_on_client_put_in_server_callback;
ScriptCallback* m_on_client_connected_callback;
ScriptCallback* m_on_client_disconnect_callback;
ScriptCallback* m_on_client_disconnect_post_callback;
ScriptCallback* m_on_client_voice_callback;
ScriptCallback* m_on_client_authorized_callback;
};
} // namespace counterstrikesharp
} // namespace counterstrikesharp

View File

@@ -0,0 +1,129 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
#include "core/managers/voice_manager.h"
#include "core/managers/player_manager.h"
#include <public/eiface.h>
#include "scripting/callback_manager.h"
#include <schema.h>
#include <entity2/entitysystem.h>
SH_DECL_HOOK3(IVEngineServer2, SetClientListening, SH_NOATTRIB, 0, bool, CPlayerSlot, CPlayerSlot,
bool);
namespace counterstrikesharp {
VoiceManager::VoiceManager() {}
VoiceManager::~VoiceManager() {}
void VoiceManager::OnAllInitialized()
{
SH_ADD_HOOK(IVEngineServer2, SetClientListening, globals::engine,
SH_MEMBER(this, &VoiceManager::SetClientListening), false);
}
void VoiceManager::OnShutdown()
{
SH_REMOVE_HOOK(IVEngineServer2, SetClientListening, globals::engine,
SH_MEMBER(this, &VoiceManager::SetClientListening), false);
}
bool VoiceManager::SetClientListening(CPlayerSlot iReceiver, CPlayerSlot iSender, bool bListen)
{
auto pReceiver = globals::playerManager.GetPlayerBySlot(iReceiver.Get());
auto pSender = globals::playerManager.GetPlayerBySlot(iSender.Get());
if (pReceiver && pSender)
{
auto listenOverride = pReceiver->GetListen(iSender);
auto senderFlags = pSender->GetVoiceFlags();
auto receiverFlags = pReceiver->GetVoiceFlags();
if (pReceiver->m_selfMutes->Get(iSender.Get()))
{
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, false));
}
if (senderFlags & Speak_Muted)
{
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, false));
}
if (listenOverride == Listen_Mute)
{
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, false));
} else if (listenOverride == Listen_Hear) {
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, true));
}
if ((senderFlags & Speak_All) || (receiverFlags & Speak_ListenAll)) {
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, true));
}
if ((senderFlags & Speak_Team) || (receiverFlags & Speak_ListenTeam))
{
static auto classKey = hash_32_fnv1a_const("CBaseEntity");
static auto memberKey = hash_32_fnv1a_const("m_iTeamNum");
const static auto m_key = schema::GetOffset("CBaseEntity", classKey, "m_iTeamNum", memberKey);
auto receiverController = globals::entitySystem->GetBaseEntity(CEntityIndex(iReceiver.Get() + 1));
auto senderController = globals::entitySystem->GetBaseEntity(CEntityIndex(iSender.Get() + 1));
if (receiverController && senderController)
{
auto receiverTeam = *reinterpret_cast<std::add_pointer_t<unsigned int>>(
(uintptr_t)(receiverController) + m_key.offset);
auto senderTeam = *reinterpret_cast<std::add_pointer_t<unsigned int>>(
(uintptr_t)(senderController) + m_key.offset);
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen,
&IVEngineServer2::SetClientListening,
(iReceiver, iSender, receiverTeam == senderTeam));
}
}
}
RETURN_META_VALUE(MRES_IGNORED, bListen);
}
void VoiceManager::OnClientCommand(CPlayerSlot slot, const CCommand& args)
{
auto pPlayer = globals::playerManager.GetPlayerBySlot(slot.Get());
if (!pPlayer)
return;
if (args.ArgC() > 1 && stricmp(args.Arg(0), "vban") == 0)
{
// clients just refuse to send vban for indexes over 32 and all 4 fields are just the same number, so we only get the first one
//for (int i = 1; (i < args.ArgC()) && (i < 3); i++) {
unsigned int mask = 0;
sscanf(args.Arg(1), "%x", &mask);
pPlayer->m_selfMutes->SetDWord(0, mask);
//}
}
}
} // namespace counterstrikesharp

View File

@@ -0,0 +1,38 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
#pragma once
#include "core/globals.h"
#include "core/global_listener.h"
#include "scripting/script_engine.h"
namespace counterstrikesharp {
class ScriptCallback;
class VoiceManager : public GlobalClass
{
public:
VoiceManager();
~VoiceManager();
void OnAllInitialized() override;
void OnShutdown() override;
bool SetClientListening(CPlayerSlot iReceiver, CPlayerSlot iSender, bool bListen);
void OnClientCommand(CPlayerSlot slot, const CCommand& args);
private:
};
} // namespace counterstrikesharp

View File

@@ -0,0 +1,129 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
#include "scripting/autonative.h"
#include "scripting/script_engine.h"
#include "core/managers/player_manager.h"
#include <public/entity2/entitysystem.h>
namespace counterstrikesharp {
void SetClientListening(ScriptContext& scriptContext)
{
auto receiver = scriptContext.GetArgument<CBaseEntity*>(0);
auto sender = scriptContext.GetArgument<CBaseEntity*>(1);
auto listen = scriptContext.GetArgument<ListenOverride>(2);
if (!receiver) {
scriptContext.ThrowNativeError("Receiver is a null pointer");
return;
}
if (!sender) {
scriptContext.ThrowNativeError("Sender is a null pointer");
return;
}
auto iSenderSlot = sender->GetEntityIndex().Get() - 1;
if (iSenderSlot < 0 || iSenderSlot >= globals::getGlobalVars()->maxClients)
scriptContext.ThrowNativeError("Invalid sender");
auto pPlayer = globals::playerManager.GetPlayerBySlot(receiver->GetEntityIndex().Get() - 1);
if (pPlayer == nullptr) {
scriptContext.ThrowNativeError("Invalid receiver");
return;
}
pPlayer->SetListen(iSenderSlot, listen);
}
ListenOverride GetClientListening(ScriptContext& scriptContext)
{
auto receiver = scriptContext.GetArgument<CBaseEntity*>(0);
auto sender = scriptContext.GetArgument<CBaseEntity*>(1);
if (!receiver) {
scriptContext.ThrowNativeError("Receiver is a null pointer");
return Listen_Default;
}
if (!sender) {
scriptContext.ThrowNativeError("Sender is a null pointer");
return Listen_Default;
}
auto iSenderSlot = sender->GetEntityIndex().Get() - 1;
if (iSenderSlot < 0 || iSenderSlot >= globals::getGlobalVars()->maxClients)
scriptContext.ThrowNativeError("Invalid sender");
auto pPlayer = globals::playerManager.GetPlayerBySlot(receiver->GetEntityIndex().Get() - 1);
if (pPlayer == nullptr) {
scriptContext.ThrowNativeError("Invalid receiver");
return Listen_Default;
}
return pPlayer->GetListen(iSenderSlot);
}
void SetClientVoiceFlags(ScriptContext& scriptContext)
{
auto client = scriptContext.GetArgument<CBaseEntity*>(0);
auto flags = scriptContext.GetArgument<VoiceFlag_t>(1);
if (!client) {
scriptContext.ThrowNativeError("Receiver is a null pointer");
return;
}
auto pPlayer = globals::playerManager.GetPlayerBySlot(client->GetEntityIndex().Get() - 1);
if (pPlayer == nullptr) {
scriptContext.ThrowNativeError("Invalid receiver");
return;
}
pPlayer->SetVoiceFlags(flags);
}
VoiceFlag_t GetClientVoiceFlags(ScriptContext& scriptContext)
{
auto client = scriptContext.GetArgument<CBaseEntity*>(0);
if (!client) {
scriptContext.ThrowNativeError("Receiver is a null pointer");
return VoiceFlag_t{};
}
auto pPlayer = globals::playerManager.GetPlayerBySlot(client->GetEntityIndex().Get() - 1);
if (pPlayer == nullptr) {
scriptContext.ThrowNativeError("Invalid receiver");
}
return pPlayer->GetVoiceFlags();
}
REGISTER_NATIVES(voice, {
ScriptEngine::RegisterNativeHandler("SET_CLIENT_LISTENING", SetClientListening);
ScriptEngine::RegisterNativeHandler("GET_CLIENT_LISTENING", GetClientListening);
ScriptEngine::RegisterNativeHandler("SET_CLIENT_VOICE_FLAGS", SetClientVoiceFlags);
ScriptEngine::RegisterNativeHandler("GET_CLIENT_VOICE_FLAGS", GetClientVoiceFlags);
})
} // namespace counterstrikesharp

View File

@@ -0,0 +1,4 @@
SET_CLIENT_LISTENING: receiver:pointer, sender:pointer, listen:uint -> void
GET_CLIENT_LISTENING: receiver:pointer, sender:pointer -> ListenOverride
SET_CLIENT_VOICE_FLAGS: client:pointer, flags:uint -> void
GET_CLIENT_VOICE_FLAGS: client:pointer -> uint

View File

@@ -63,6 +63,8 @@ public class Mapping
return "[CastFrom(typeof(ulong))]SteamID";
case "HookMode":
return "HookMode";
case "ListenOverride":
return "ListenOverride";
case "DataType_t":
return "DataType";
case "any":