Compare commits

...

4 Commits

Author SHA1 Message Date
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
20 changed files with 503 additions and 59 deletions

View File

@@ -56,7 +56,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

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

@@ -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,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 `PermissionHelper` attribute.
```csharp
[PermissionHelper("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

@@ -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<PermissionHelperAttribute>()?.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;
}
[PermissionHelper("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;
}
[PermissionHelper("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

@@ -0,0 +1,193 @@
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);
}
[PermissionHelper("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"));
}
[PermissionHelper("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;
var steamId = new SteamID(player.SteamID);
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 add a flag to.</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;
var data = GetPlayerAdminData(new SteamID(player.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 PermissionHelperAttribute : Attribute
{
public string[] RequiredPermissions { get; }
public PermissionHelperAttribute(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<PermissionHelperAttribute>()?.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;