Compare commits

...

16 Commits
v15 ... v1.0.26

Author SHA1 Message Date
Roflmuffin
cc7dd5ca96 ci: I have the utmost confidence 2023-11-10 20:16:04 +10:00
Roflmuffin
ebc361b2f8 ci: publish to api.nuget.org 2023-11-10 20:05:52 +10:00
Roflmuffin
c72eff2546 ci: fix nuget source 2023-11-10 20:00:09 +10:00
Roflmuffin
4b432e9efc ci: add package write permission 2023-11-10 19:52:13 +10:00
Roflmuffin
22bbf835c7 Merge remote-tracking branch 'origin/main' into main 2023-11-10 19:51:17 +10:00
Roflmuffin
092a6077c3 ci: try publishing nuget package 2023-11-10 19:49:44 +10:00
pedrotski
4430060efd Update README.md (#37)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2023-11-10 19:07:45 +10:00
Roflmuffin
77ea6fd80d fix: prevent server crash on duplicate command registration, fixes #51 2023-11-10 19:06:40 +10:00
Roflmuffin
f18df3df2b docs: update console command expected usage docs 2023-11-10 19:02:46 +10:00
Roflmuffin
4ce1ec2cf5 feat: add disabled plugins folder, and source folder for source code 2023-11-10 18:56:37 +10:00
Roflmuffin
9005f3c29c fix: my bad merging skills 2023-11-10 18:52:20 +10:00
Roflmuffin
b7ace4256a feat: change permission helper attribute to RequiresPermissions 2023-11-10 18:45:04 +10:00
Roflmuffin
b725f7f79a Basic admin system framework (plus some cleanup) (#44)
Co-authored-by: zonical <zonicalguy@gmail.com>
2023-11-10 18:40:20 +10:00
Roflmuffin
cb6d86a54d feat: implement IEquatable<T> for SteamID 2023-11-10 17:29:45 +10:00
Roflmuffin
d4a2ae68e1 Merge branch 'main' of github.com:roflmuffin/CounterStrikeSharp into main 2023-11-09 23:25:40 +10:00
Roflmuffin
82c92f555b chore: simplify auto-copy configs folder 2023-11-09 23:22:35 +10:00
27 changed files with 591 additions and 62 deletions

View File

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

View File

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

View File

@@ -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/).

View File

@@ -0,0 +1,15 @@
{
"Erikj": {
"identity": "76561197960265731",
"flags": [
"@css/kick",
"@css/ban"
]
},
"Another erikj": {
"identity": "STEAM_0:1:1",
"flags": [
"@anotherscope/foobar"
]
}
}

View 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

View File

@@ -0,0 +1 @@
Place your source code for plugins here.

View 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.

View File

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

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

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

View 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);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -88,6 +88,7 @@ namespace CounterStrikeSharp.API
return players;
}
[Obsolete]
public static void ReplyToCommand(CCSPlayerController? player, string msg, bool console = false)
{
if (player != null)

View File

@@ -15,6 +15,7 @@
*/
using System;
using System.Drawing;
using System.IO;
using System.Linq;
using CounterStrikeSharp.API;

View File

@@ -5,6 +5,7 @@
<Platforms>AnyCPU;x86</Platforms>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>

View File

@@ -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);
}