mirror of
https://github.com/roflmuffin/CounterStrikeSharp.git
synced 2025-12-05 23:58:24 -08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc7dd5ca96 | ||
|
|
ebc361b2f8 | ||
|
|
c72eff2546 | ||
|
|
4b432e9efc | ||
|
|
22bbf835c7 | ||
|
|
092a6077c3 | ||
|
|
4430060efd | ||
|
|
77ea6fd80d | ||
|
|
f18df3df2b | ||
|
|
4ce1ec2cf5 | ||
|
|
9005f3c29c | ||
|
|
b7ace4256a | ||
|
|
b725f7f79a | ||
|
|
cb6d86a54d | ||
|
|
d4a2ae68e1 | ||
|
|
82c92f555b |
13
.github/workflows/cmake-single-platform.yml
vendored
13
.github/workflows/cmake-single-platform.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: registry.gitlab.steamos.cloud/steamrt/sniper/sdk:latest
|
||||
@@ -39,7 +40,9 @@ jobs:
|
||||
- uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
- run: dotnet publish -c Release /p:Version=1.0.${{ env.BUILD_NUMBER }} managed/CounterStrikeSharp.API
|
||||
- run: |
|
||||
dotnet publish -c Release /p:Version=1.0.${{ env.BUILD_NUMBER }} managed/CounterStrikeSharp.API
|
||||
dotnet pack -c Release /p:Version=1.0.${{ env.BUILD_NUMBER }} managed/CounterStrikeSharp.API
|
||||
|
||||
- name: Configure CMake
|
||||
run: cmake -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
|
||||
@@ -56,7 +59,6 @@ jobs:
|
||||
- name: Add API to Artifacts
|
||||
run: |
|
||||
mkdir -p build/output/addons/counterstrikesharp/api
|
||||
mkdir -p build/output/addons/counterstrikesharp/plugins
|
||||
cp -r managed/CounterStrikeSharp.API/bin/Release/net7.0/publish/* build/output/addons/counterstrikesharp/api
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
@@ -88,4 +90,9 @@ jobs:
|
||||
tag_name: v${{ env.BUILD_NUMBER }}
|
||||
files: |
|
||||
counterstrikesharp-build-${{ env.BUILD_NUMBER }}-${{ env.GITHUB_SHA_SHORT }}.zip
|
||||
counterstrikesharp-with-runtime-build-${{ env.BUILD_NUMBER }}-${{ env.GITHUB_SHA_SHORT }}.zip
|
||||
counterstrikesharp-with-runtime-build-${{ env.BUILD_NUMBER }}-${{ env.GITHUB_SHA_SHORT }}.zip
|
||||
|
||||
- name: Publish NuGet package
|
||||
run: |
|
||||
dotnet nuget push managed/CounterStrikeSharp.API/bin/Release/CounterStrikeSharp.API.1.0.${{ env.BUILD_NUMBER }}.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||
dotnet nuget push managed/CounterStrikeSharp.API/bin/Release/CounterStrikeSharp.API.1.0.${{ env.BUILD_NUMBER }}.snupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||
|
||||
@@ -114,7 +114,8 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
PREFIX ""
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/addons/counterstrikesharp/bin/linuxsteamrt64"
|
||||
)
|
||||
add_custom_target(build-time-make-directory ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/addons/metamod")
|
||||
configure_file(configs/counterstrikesharp.vdf "${CMAKE_BINARY_DIR}/addons/metamod/counterstrikesharp.vdf" COPYONLY)
|
||||
configure_file(configs/gamedata.json "${CMAKE_BINARY_DIR}/addons/counterstrikesharp/gamedata/gamedata.json" COPYONLY)
|
||||
add_custom_command(
|
||||
TARGET ${PROJECT_NAME} PRE_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/configs ${CMAKE_BINARY_DIR}
|
||||
)
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
CounterStrikeSharp is a server side modding framework for Counter-Strike: Global Offensive. This project attempts to implement a .NET Core scripting layer on top of a Metamod Source Plugin, allowing developers to create plugins that interact with the game server in a modern language (C#) to facilitate the creation of maintainable and testable code.
|
||||
|
||||
[Come and join our Discord](https://discord.gg/X7r3PmuYKq)
|
||||
|
||||
## History
|
||||
|
||||
This project is an ongoing migration of a previous project (titled [VSP.NET](https://github.com/roflmuffin/vspdotnet)) whereby a scripting layer was added to a Valve Server Plugin for CSGO.
|
||||
@@ -16,7 +18,7 @@ As a result, there are a few key philosophies and trade-offs that drive the proj
|
||||
- Supporting both platforms is a lot of work for 1 person, so there are no real plans to support Windows.
|
||||
|
||||
## Install
|
||||
Development builds are currently available through GitHub actions, you can download the latest build from [there](https://github.com/roflmuffin/CounterStrikeSharp/actions/workflows/cmake-single-platform.yml).
|
||||
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/).
|
||||
|
||||
|
||||
15
configs/addons/counterstrikesharp/configs/admins.json
Normal file
15
configs/addons/counterstrikesharp/configs/admins.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Erikj": {
|
||||
"identity": "76561197960265731",
|
||||
"flags": [
|
||||
"@css/kick",
|
||||
"@css/ban"
|
||||
]
|
||||
},
|
||||
"Another erikj": {
|
||||
"identity": "STEAM_0:1:1",
|
||||
"flags": [
|
||||
"@anotherscope/foobar"
|
||||
]
|
||||
}
|
||||
}
|
||||
4
configs/addons/counterstrikesharp/plugins/README.txt
Normal file
4
configs/addons/counterstrikesharp/plugins/README.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Place plugins in this folder. Each plugin should be in its own subfolder, e.g.
|
||||
|
||||
TestPlugin/TestPlugin.dll
|
||||
AnotherPlugin/AnotherPlugin.dll
|
||||
1
configs/addons/counterstrikesharp/source/README.txt
Normal file
1
configs/addons/counterstrikesharp/source/README.txt
Normal file
@@ -0,0 +1 @@
|
||||
Place your source code for plugins here.
|
||||
37
docs/src/content/docs/features/admin-framework.md
Normal file
37
docs/src/content/docs/features/admin-framework.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Admin Framework
|
||||
description: A guide on using the Admin Framework in plugins.
|
||||
---
|
||||
|
||||
## Admin Framework
|
||||
|
||||
CounterStrikeSharp has a basic framework which allows plugin developers to assign permissions to commands. When CSS is initialized, a list of admins are loaded from `configs/admins.json`.
|
||||
|
||||
## Adding Admins
|
||||
|
||||
Adding an Admin is as simple as creating a new entry in the `configs/admins.json` file. The important things you need to declare are the SteamID identifier and the permissions they have. CounterStrikeSharp will do all the heavy-lifting to decipher your SteamID. If you're familar with SourceMod, permission definitions are slightly different as they're defined by an array of strings instead of a string of characters.
|
||||
|
||||
```json
|
||||
{
|
||||
"ZoNiCaL": {
|
||||
"identity": "76561198808392634",
|
||||
"flags": ["can_manipulate_players", "admin_messages"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also manually assign permissions to players in code with `AddPlayerPermissions` and `RemovePlayerPermissions`. These changes are not saved to `configs/admins.json`.
|
||||
|
||||
## Assigning permissions to a Command
|
||||
|
||||
Assigning permissions to a Command is as easy as tagging the Command method (function callback) with a `RequiresPermissions` attribute.
|
||||
|
||||
```csharp
|
||||
[RequiresPermissions("can_execute_test_command", "other_permission")]
|
||||
public void OnMyCommand(CCSPlayerController? caller, CommandInfo info)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
CounterStrikeSharp handles all of the permission checks behind the scenes for you.
|
||||
@@ -64,3 +64,39 @@ Command String: custom_command "Test Quoted" 5 13
|
||||
First Argument: custom_command
|
||||
Second Argument: Test Quoted
|
||||
```
|
||||
|
||||
## Helper Attribute
|
||||
|
||||
CounterStrikeSharp provides the `CommandHelper` attribute for Command methods (function callback) to simplify the process of checking for the correct amount of arguments and to restrict commands to being executed by the server console or by players (or both!).
|
||||
|
||||
```csharp
|
||||
[ConsoleCommand("freeze", "Freezes a client.")]
|
||||
[CommandHelper(minArgs: 1, usage: "[target]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
If a client tries to execute the command without the `[target]` argument, it will print a message to them in chat:
|
||||
|
||||
```shell
|
||||
[CSS] Expected usage: "!freeze [target]".
|
||||
```
|
||||
|
||||
If a command is executed by the wrong user, it will print a message to them:
|
||||
|
||||
```shell
|
||||
[CSS] This command can only be executed by clients.
|
||||
```
|
||||
|
||||
Valid `CommandUsage` values:
|
||||
|
||||
```csharp
|
||||
public enum CommandUsage
|
||||
{
|
||||
CLIENT_AND_SERVER = 0,
|
||||
CLIENT_ONLY,
|
||||
SERVER_ONLY
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,8 +24,10 @@ using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using CounterStrikeSharp.API.Core.Attributes;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Events;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CounterStrikeSharp.API.Modules.Listeners;
|
||||
using CounterStrikeSharp.API.Modules.Timers;
|
||||
|
||||
@@ -154,19 +156,50 @@ namespace CounterStrikeSharp.API.Core
|
||||
{
|
||||
var wrappedHandler = new Action<int, IntPtr>((i, ptr) =>
|
||||
{
|
||||
if (i == -1)
|
||||
var caller = (i != -1) ? new CCSPlayerController(NativeAPI.GetEntityFromIndex(i + 1)) : null;
|
||||
var command = new CommandInfo(ptr, caller);
|
||||
|
||||
var methodInfo = handler?.GetMethodInfo();
|
||||
// Do not execute if we shouldn't be calling this command.
|
||||
var helperAttribute = methodInfo?.GetCustomAttribute<CommandHelperAttribute>();
|
||||
if (helperAttribute != null)
|
||||
{
|
||||
handler?.Invoke(null, new CommandInfo(ptr, 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;
|
||||
case CommandUsage.SERVER_ONLY:
|
||||
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.");
|
||||
}
|
||||
|
||||
// Technically the command itself counts as the first argument,
|
||||
// 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}\".");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not execute command if we do not have the correct permissions.
|
||||
var permissions = methodInfo?.GetCustomAttribute<RequiresPermissions>()?.RequiredPermissions;
|
||||
if (permissions != null && !AdminManager.PlayerHasPermissions(caller, permissions))
|
||||
{
|
||||
command.ReplyToCommand("[CSS] You do not have the correct permissions to execute this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
var entity = new CCSPlayerController(NativeAPI.GetEntityFromIndex(i + 1));
|
||||
var command = new CommandInfo(ptr, entity);
|
||||
handler?.Invoke(entity.IsValid ? entity : null, command);
|
||||
handler?.Invoke(caller, command);
|
||||
});
|
||||
|
||||
var methodInfo = handler?.GetMethodInfo();
|
||||
var helperAttribute = methodInfo?.GetCustomAttribute<CommandHelperAttribute>();
|
||||
|
||||
var subscriber = new CallbackSubscriber(handler, wrappedHandler, () => { RemoveCommand(name, handler); });
|
||||
NativeAPI.AddCommand(name, description, false, (int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND, subscriber.GetInputArgument());
|
||||
NativeAPI.AddCommand(name, description, (helperAttribute?.WhoCanExcecute == CommandUsage.SERVER_ONLY),
|
||||
(int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND, subscriber.GetInputArgument());
|
||||
CommandHandlers[handler] = subscriber;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ namespace CounterStrikeSharp.API.Core;
|
||||
class LoadedGameData
|
||||
{
|
||||
[JsonPropertyName("signatures")] public Signatures? Signatures { get; set; }
|
||||
|
||||
|
||||
[JsonPropertyName("offsets")] public Offsets? Offsets { get; set; }
|
||||
}
|
||||
|
||||
@@ -37,9 +35,16 @@ public static class GameData
|
||||
|
||||
public static void Load(string gameDataPath)
|
||||
{
|
||||
_methods = JsonSerializer.Deserialize<Dictionary<string, LoadedGameData>>(File.ReadAllText(gameDataPath));
|
||||
try
|
||||
{
|
||||
_methods = JsonSerializer.Deserialize<Dictionary<string, LoadedGameData>>(File.ReadAllText(gameDataPath));
|
||||
|
||||
Console.WriteLine($"Loaded game data with {_methods.Count} methods.");
|
||||
Console.WriteLine($"Loaded game data with {_methods.Count} methods.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to load game data: {ex.ToString()}");
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetSignature(string key)
|
||||
|
||||
@@ -20,8 +20,10 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Menu;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
|
||||
namespace CounterStrikeSharp.API.Core
|
||||
{
|
||||
@@ -57,17 +59,20 @@ namespace CounterStrikeSharp.API.Core
|
||||
}
|
||||
public void InitGlobalContext()
|
||||
{
|
||||
Console.WriteLine("Loading GameData");
|
||||
Console.WriteLine("Loading GameData from \"gamedata/gamedata.json\"");
|
||||
GameData.Load(Path.Combine(rootDir.FullName, "gamedata", "gamedata.json"));
|
||||
|
||||
Console.WriteLine("Loading Admins from \"configs/admins.json\"");
|
||||
AdminManager.Load(Path.Combine(rootDir.FullName, "configs", "admins.json"));
|
||||
|
||||
for (int i = 1; i <= 9; i++)
|
||||
{
|
||||
AddCommand("css_" + i, "Command Key Handler", (player, info) =>
|
||||
CommandUtils.AddStandaloneCommand("css_" + i, "Command Key Handler", (player, info) =>
|
||||
{
|
||||
if (player == null) return;
|
||||
var key = Convert.ToInt32(info.GetArg(0).Split("_")[1]);
|
||||
ChatMenus.OnKeyPress(player, key);
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
Console.WriteLine("Loading C# plugins...");
|
||||
@@ -178,24 +183,34 @@ namespace CounterStrikeSharp.API.Core
|
||||
return plugin;
|
||||
}
|
||||
|
||||
[RequiresPermissions("can_execute_css_commands")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
private void OnCSSCommand(CCSPlayerController? caller, CommandInfo info)
|
||||
{
|
||||
var currentVersion = Api.GetVersion();
|
||||
|
||||
Utilities.ReplyToCommand(caller, " CounterStrikeSharp was created and is maintained by Michael \"roflmuffin\" Wilson.\n" +
|
||||
info.ReplyToCommand(" CounterStrikeSharp was created and is maintained by Michael \"roflmuffin\" Wilson.\n" +
|
||||
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison and CS2Fixes.\n" +
|
||||
" See ACKNOWLEDGEMENTS.md for more information.\n" +
|
||||
" Current API Version: " + currentVersion, true);
|
||||
return;
|
||||
}
|
||||
|
||||
[RequiresPermissions("can_execute_css_commands")]
|
||||
[CommandHelper(minArgs: 1,
|
||||
usage: "[option]\n" +
|
||||
" list - List all plugins currently loaded.\n" +
|
||||
" start / load - Loads a plugin not currently loaded.\n" +
|
||||
" stop / unload - Unloads a plugin currently loaded.\n" +
|
||||
" restart / reload - Reloads a plugin currently loaded.",
|
||||
whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
private void OnCSSPluginCommand(CCSPlayerController? caller, CommandInfo info)
|
||||
{
|
||||
switch (info.GetArg(1))
|
||||
{
|
||||
case "list":
|
||||
{
|
||||
Utilities.ReplyToCommand(caller, $" List of all plugins currently loaded by CounterStrikeSharp: {_loadedPlugins.Count} plugins loaded.", true);
|
||||
info.ReplyToCommand($" List of all plugins currently loaded by CounterStrikeSharp: {_loadedPlugins.Count} plugins loaded.", true);
|
||||
|
||||
foreach (var plugin in _loadedPlugins)
|
||||
{
|
||||
@@ -208,8 +223,8 @@ namespace CounterStrikeSharp.API.Core
|
||||
sb.Append(" ");
|
||||
sb.Append(plugin.Description);
|
||||
}
|
||||
Utilities.ReplyToCommand(caller, sb.ToString(), true);
|
||||
|
||||
info.ReplyToCommand(sb.ToString(), true);
|
||||
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -219,7 +234,7 @@ namespace CounterStrikeSharp.API.Core
|
||||
{
|
||||
if (info.ArgCount < 2)
|
||||
{
|
||||
Utilities.ReplyToCommand(caller, "Valid usage: css_plugins start/load [relative plugin path || absolute plugin path] (e.g \"TestPlugin\", \"plugins/TestPlugin/TestPlugin.dll\")\n", true);
|
||||
info.ReplyToCommand("Valid usage: css_plugins start/load [relative plugin path || absolute plugin path] (e.g \"TestPlugin\", \"plugins/TestPlugin/TestPlugin.dll\")\n", true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -252,7 +267,7 @@ namespace CounterStrikeSharp.API.Core
|
||||
{
|
||||
if (info.ArgCount < 2)
|
||||
{
|
||||
Utilities.ReplyToCommand(caller, "Valid usage: css_plugins stop/unload [plugin name || #plugin id] (e.g \"TestPlugin\", \"1\")\n", true);
|
||||
info.ReplyToCommand("Valid usage: css_plugins stop/unload [plugin name || #plugin id] (e.g \"TestPlugin\", \"1\")\n", true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -260,7 +275,7 @@ namespace CounterStrikeSharp.API.Core
|
||||
PluginContext? plugin = FindPluginByIdOrName(pluginIdentifier);
|
||||
if (plugin == null)
|
||||
{
|
||||
Utilities.ReplyToCommand(caller, $"Could not unload plugin \"{pluginIdentifier}\")", true);
|
||||
info.ReplyToCommand($"Could not unload plugin \"{pluginIdentifier}\")", true);
|
||||
break;
|
||||
}
|
||||
plugin.Unload();
|
||||
@@ -273,7 +288,7 @@ namespace CounterStrikeSharp.API.Core
|
||||
{
|
||||
if (info.ArgCount < 2)
|
||||
{
|
||||
Utilities.ReplyToCommand(caller, "Valid usage: css_plugins restart/reload [plugin name || #plugin id] (e.g \"TestPlugin\", \"#1\")\n", true);
|
||||
info.ReplyToCommand("Valid usage: css_plugins restart/reload [plugin name || #plugin id] (e.g \"TestPlugin\", \"#1\")\n", true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -282,7 +297,7 @@ namespace CounterStrikeSharp.API.Core
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
Utilities.ReplyToCommand(caller, $"Could not reload plugin \"{pluginIdentifier}\")", true);
|
||||
info.ReplyToCommand($"Could not reload plugin \"{pluginIdentifier}\")", true);
|
||||
break;
|
||||
}
|
||||
plugin.Unload(true);
|
||||
@@ -291,7 +306,7 @@ namespace CounterStrikeSharp.API.Core
|
||||
}
|
||||
|
||||
default:
|
||||
Utilities.ReplyToCommand(caller, "Valid usage: css_plugins [option]\n" +
|
||||
info.ReplyToCommand("Valid usage: css_plugins [option]\n" +
|
||||
" list - List all plugins currently loaded.\n" +
|
||||
" start / load - Loads a plugin not currently loaded.\n" +
|
||||
" stop / unload - Unloads a plugin currently loaded.\n" +
|
||||
@@ -304,33 +319,9 @@ namespace CounterStrikeSharp.API.Core
|
||||
|
||||
public void RegisterPluginCommands()
|
||||
{
|
||||
AddCommand("css", "Counter-Strike Sharp options.", OnCSSCommand, false);
|
||||
AddCommand("css_plugins", "Counter-Strike Sharp plugin options.", OnCSSPluginCommand, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary way for base CSS to add commands without a plugin context
|
||||
*/
|
||||
private void AddCommand(string name, string description, CommandInfo.CommandCallback handler, bool serverOnly)
|
||||
{
|
||||
var wrappedHandler = new Action<int, IntPtr>((i, ptr) =>
|
||||
{
|
||||
if (i == -1)
|
||||
{
|
||||
handler?.Invoke(null, new CommandInfo(ptr, null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverOnly) return;
|
||||
|
||||
var entity = new CCSPlayerController(NativeAPI.GetEntityFromIndex(i + 1));
|
||||
var command = new CommandInfo(ptr, entity);
|
||||
handler?.Invoke(entity.IsValid ? entity : null, command);
|
||||
});
|
||||
|
||||
var subscriber = new BasePlugin.CallbackSubscriber(handler, wrappedHandler, () => { });
|
||||
NativeAPI.AddCommand(name, description, serverOnly, (int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND,
|
||||
subscriber.GetInputArgument());
|
||||
CommandUtils.AddStandaloneCommand("css", "Counter-Strike Sharp options.", OnCSSCommand);
|
||||
CommandUtils.AddStandaloneCommand("css_plugins", "Counter-Strike Sharp plugin options.", OnCSSPluginCommand);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,19 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<Authors>Roflmuffin</Authors>
|
||||
<Description>Official server side runtime assembly for CounterStrikeSharp</Description>
|
||||
<PackageProjectUrl>http://docs.cssharp.dev/</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/roflmuffin/CounterStrikeSharp</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Modules\Commands\CommandInfo" />
|
||||
|
||||
213
managed/CounterStrikeSharp.API/Modules/Admin/AdminManager.cs
Normal file
213
managed/CounterStrikeSharp.API/Modules/Admin/AdminManager.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
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;
|
||||
|
||||
namespace CounterStrikeSharp.API.Modules.Admin
|
||||
{
|
||||
public partial class AdminData
|
||||
{
|
||||
[JsonPropertyName("identity")] public required string Identity { get; init; }
|
||||
[JsonPropertyName("flags")] public required HashSet<string> Flags { get; init; }
|
||||
}
|
||||
|
||||
public static class AdminManager
|
||||
{
|
||||
private static readonly Dictionary<SteamID, AdminData> Admins = new();
|
||||
|
||||
static AdminManager()
|
||||
{
|
||||
CommandUtils.AddStandaloneCommand("css_admins_reload", "Reloads the admin file.", ReloadAdminsCommand);
|
||||
CommandUtils.AddStandaloneCommand("css_admins_list", "List admins and their flags.", ListAdminsCommand);
|
||||
}
|
||||
|
||||
[RequiresPermissions("can_reload_admins")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
private static void ReloadAdminsCommand(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
Admins.Clear();
|
||||
var rootDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent;
|
||||
Load(Path.Combine(rootDir.FullName, "configs", "admins.json"));
|
||||
}
|
||||
|
||||
[RequiresPermissions("can_reload_admins")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
private static void ListAdminsCommand(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
foreach (var (steamId, data) in Admins)
|
||||
{
|
||||
command.ReplyToCommand($"{steamId.SteamId64}, {steamId.SteamId2} - {string.Join(", ", data.Flags)}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Load(string adminDataPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(adminDataPath))
|
||||
{
|
||||
Console.WriteLine("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 });
|
||||
if (adminsFromFile == null) { throw new FileNotFoundException(); }
|
||||
foreach (var adminDef in adminsFromFile.Values)
|
||||
{
|
||||
if (SteamID.TryParse(adminDef.Identity, out var steamId))
|
||||
{
|
||||
if (Admins.ContainsKey(steamId!))
|
||||
{
|
||||
Admins[steamId!].Flags.UnionWith(adminDef.Flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
Admins.Add(steamId!, adminDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"Loaded admin data with {Admins.Count} admins.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to load admin data: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grabs the admin data for a player that was loaded from "configs/admins.json".
|
||||
/// </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)
|
||||
{
|
||||
return Admins.GetValueOrDefault(steamId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if a player has access to a certain set of permission flags.
|
||||
/// </summary>
|
||||
/// <param name="player">Player or server console.</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(CCSPlayerController? player, params string[] flags)
|
||||
{
|
||||
// This is here for cases where the server console is attempting to call commands.
|
||||
// The server console should have access to all commands, regardless of permissions.
|
||||
if (player == null) return true;
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) { return false; }
|
||||
var playerData = GetPlayerAdminData((SteamID)player.SteamID);
|
||||
return playerData?.Flags.IsSupersetOf(flags) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if a player has access to a certain set of permission flags.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
var playerData = GetPlayerAdminData(steamId);
|
||||
return playerData?.Flags.IsSupersetOf(flags) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily adds a permission flag to the player. These flags are not saved to
|
||||
/// "configs/admins.json".
|
||||
/// </summary>
|
||||
/// <param name="player">Player controller to add a flag to.</param>
|
||||
/// <param name="flags">Flags to add for the player.</param>
|
||||
public static void AddPlayerPermissions(CCSPlayerController? player, params string[] flags)
|
||||
{
|
||||
if (player == null) return;
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
|
||||
AddPlayerPermissions((SteamID)player.SteamID, flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily adds a permission flag to the player. These flags are not saved to
|
||||
/// "configs/admins.json".
|
||||
/// </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)
|
||||
{
|
||||
var data = GetPlayerAdminData(steamId);
|
||||
if (data == null)
|
||||
{
|
||||
data = new AdminData()
|
||||
{
|
||||
Identity = steamId.SteamId64.ToString(),
|
||||
Flags = new(flags)
|
||||
};
|
||||
|
||||
Admins[steamId] = data;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var flag in flags)
|
||||
{
|
||||
data.Flags.Add(flag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily removes a permission flag to the player. These flags are not saved to
|
||||
/// "configs/admins.json".
|
||||
/// </summary>
|
||||
/// <param name="player">Player controller to remove flags from.</param>
|
||||
/// <param name="flags">Flags to remove from the player.</param>
|
||||
public static void RemovePlayerPermissions(CCSPlayerController? player, params string[] flags)
|
||||
{
|
||||
if (player == null) return;
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
|
||||
|
||||
RemovePlayerPermissions((SteamID)player.SteamID, flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily removes a permission flag to the player. These flags are not saved to
|
||||
/// "configs/admins.json".
|
||||
/// </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)
|
||||
{
|
||||
var data = GetPlayerAdminData(steamId);
|
||||
if (data == null) return;
|
||||
|
||||
data.Flags.ExceptWith(flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a players admin data. This is not saved to "configs/admins.json"
|
||||
/// </summary>
|
||||
/// <param name="player">Player controller to remove admin data from.</param>
|
||||
public static void RemovePlayerAdminData(CCSPlayerController? player)
|
||||
{
|
||||
if (player == null) return;
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected || player.IsBot) return;
|
||||
|
||||
RemovePlayerAdminData((SteamID)player.SteamID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
Admins.Remove(steamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace CounterStrikeSharp.API.Modules.Admin
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class RequiresPermissions : Attribute
|
||||
{
|
||||
public string[] RequiredPermissions { get; }
|
||||
|
||||
public RequiresPermissions(params string[] permissions)
|
||||
{
|
||||
RequiredPermissions = permissions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CounterStrikeSharp.API.Modules.Commands
|
||||
{
|
||||
public enum CommandUsage
|
||||
{
|
||||
CLIENT_AND_SERVER = 0,
|
||||
CLIENT_ONLY,
|
||||
SERVER_ONLY
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class CommandHelperAttribute : Attribute
|
||||
{
|
||||
public int MinArgs { get; }
|
||||
public string Usage { get; }
|
||||
public CommandUsage WhoCanExcecute { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="minArgs">The minimum amount of arguments required to execute this command.</param>
|
||||
/// <param name="usage">If the command fails, this string is printed to the caller to show the CommandUtils intended usage.</param>
|
||||
/// <param name="whoCanExecute">Restricts the command so it can only be executed by players, the server console, or both (see CommandUsage).</param>
|
||||
public CommandHelperAttribute(int minArgs = 0, string usage = "", CommandUsage whoCanExecute = CommandUsage.CLIENT_AND_SERVER)
|
||||
{
|
||||
MinArgs = minArgs;
|
||||
Usage = usage;
|
||||
WhoCanExcecute = whoCanExecute;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,11 @@ namespace CounterStrikeSharp.API.Modules.Commands
|
||||
public string ArgByIndex(int index) => NativeAPI.CommandGetArgByIndex(Handle, index);
|
||||
public string GetArg(int index) => NativeAPI.CommandGetArgByIndex(Handle, index);
|
||||
|
||||
public void ReplyToCommand(string message) {
|
||||
public void ReplyToCommand(string message, bool console = false) {
|
||||
if (_player != null)
|
||||
{
|
||||
_player.PrintToChat(message);
|
||||
if (console) { _player.PrintToConsole(message); }
|
||||
else _player.PrintToChat(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
|
||||
namespace CounterStrikeSharp.API.Modules.Entities
|
||||
{
|
||||
public class SteamID
|
||||
public class SteamID : IEquatable<SteamID>
|
||||
{
|
||||
const long Base = 76561197960265728;
|
||||
public ulong SteamId64 { get; set; }
|
||||
@@ -12,7 +12,6 @@ namespace CounterStrikeSharp.API.Modules.Entities
|
||||
|
||||
public static explicit operator SteamID(ulong u) => new(u);
|
||||
public static explicit operator SteamID(string s) => new(s);
|
||||
|
||||
ulong ParseId(string id)
|
||||
{
|
||||
var parts = id.Split(':');
|
||||
@@ -46,5 +45,55 @@ namespace CounterStrikeSharp.API.Modules.Entities
|
||||
}
|
||||
|
||||
public override string ToString() => $"[SteamID {SteamId64}, {SteamId2}, {SteamId3}]";
|
||||
|
||||
public bool Equals(SteamID? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return SteamId64 == other.SteamId64;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj)) return false;
|
||||
if (ReferenceEquals(this, obj)) return true;
|
||||
if (obj.GetType() != this.GetType()) return false;
|
||||
return Equals((SteamID)obj);
|
||||
}
|
||||
|
||||
public static bool TryParse(string s, out SteamID? steamId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ulong.TryParse(s, out var steamid64))
|
||||
{
|
||||
steamId = new SteamID(steamid64);
|
||||
return true;
|
||||
}
|
||||
|
||||
steamId = new SteamID(s);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
steamId = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return SteamId64.GetHashCode();
|
||||
}
|
||||
|
||||
public static bool operator ==(SteamID? left, SteamID? right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SteamID? left, SteamID? right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CounterStrikeSharp.API.Modules.Utils;
|
||||
|
||||
public class CommandUtils
|
||||
{
|
||||
public static void AddStandaloneCommand(string name, string description, CommandInfo.CommandCallback handler)
|
||||
{
|
||||
var wrappedHandler = new Action<int, IntPtr>((i, ptr) =>
|
||||
{
|
||||
var caller = (i != -1) ? new CCSPlayerController(NativeAPI.GetEntityFromIndex(i + 1)) : null;
|
||||
var command = new CommandInfo(ptr, caller);
|
||||
|
||||
var methodInfo = handler?.GetMethodInfo();
|
||||
// Do not execute if we shouldn't be calling this command.
|
||||
var helperAttribute = methodInfo?.GetCustomAttribute<CommandHelperAttribute>();
|
||||
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;
|
||||
case CommandUsage.SERVER_ONLY:
|
||||
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.");
|
||||
}
|
||||
|
||||
// Technically the command itself counts as the first argument,
|
||||
// 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}\".");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not execute command if we do not have the correct permissions.
|
||||
var permissions = methodInfo?.GetCustomAttribute<RequiresPermissions>()?.RequiredPermissions;
|
||||
if (permissions != null && !AdminManager.PlayerHasPermissions(caller, permissions))
|
||||
{
|
||||
command.ReplyToCommand("[CSS] You do not have the correct permissions to execute this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
handler?.Invoke(caller, command);
|
||||
});
|
||||
|
||||
var methodInfo = handler?.GetMethodInfo();
|
||||
var helperAttribute = methodInfo?.GetCustomAttribute<CommandHelperAttribute>();
|
||||
|
||||
var subscriber = new BasePlugin.CallbackSubscriber(handler, wrappedHandler, () => { });
|
||||
NativeAPI.AddCommand(name, description, (helperAttribute?.WhoCanExcecute == CommandUsage.SERVER_ONLY),
|
||||
(int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND, subscriber.GetInputArgument());
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ namespace CounterStrikeSharp.API
|
||||
return players;
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public static void ReplyToCommand(CCSPlayerController? player, string msg, bool console = false)
|
||||
{
|
||||
if (player != null)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CounterStrikeSharp.API;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<Platforms>AnyCPU;x86</Platforms>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -37,6 +37,11 @@ static ConCommandInfo* AddCommand(ScriptContext& script_context)
|
||||
CSSHARP_CORE_TRACE("Adding command {}, {}, {}, {}, {}", name, description, server_only, flags,
|
||||
(void*)callback);
|
||||
|
||||
if (globals::conCommandManager.FindCommand(name)) {
|
||||
script_context.ThrowNativeError("Failed to add command \"%s\", command already exists.", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return globals::conCommandManager.AddCommand(name, description, server_only, flags, callback);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user