Compare commits

..

13 Commits

Author SHA1 Message Date
Dawid Bepierszcz
099e91b43b Update FunCommands module project path in build workflow
Corrected the PROJECT_PATH_FUNCOMMANDSMODULE environment variable to point to the new location of the CS2-SimpleAdmin_FunCommands.csproj file within the build workflow configuration.
2025-10-19 16:08:45 +02:00
Dawid Bepierszcz
206c18db66 Add permission override support for menus
Menus can now specify a command name for permission override checking, allowing server admins to control menu visibility via CounterStrikeSharp's admin system. Updated API, MenuManager, and FunCommands module to support this feature. Also updated slap command to emit a sound, fixed SQL migration for IP address type, and bumped version to 1.7.8-beta-2.
2025-10-19 16:06:03 +02:00
Dawid Bepierszcz
78318102fe Refactor fun commands to external module
Commented out fun command implementations (noclip, godmode, freeze, unfreeze, resize) in funcommands.cs and removed their registration from RegisterCommands.cs. These commands are now intended to be provided by the new CS2-SimpleAdmin_FunCommands external module, improving modularity and maintainability.
2025-10-19 03:12:58 +02:00
Dawid Bepierszcz
2edacc2b3f Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2025-10-03 12:10:52 +02:00
Dawid Bepierszcz
e1e66441f2 Remove custom ClearBuildFiles target and cleanup csproj
Commented out the GenerateDependencyFile property and removed the ClearBuildFiles target from the project file. Also reformatted and cleaned up the ItemGroup for migration scripts, improving maintainability.
2025-10-03 12:10:50 +02:00
Dawid Bepierszcz
cc54b9e879 Update README.md 2025-10-03 02:35:45 +02:00
Dawid Bepierszcz
640e618f3b Update README.md 2025-10-03 02:34:03 +02:00
Dawid Bepierszcz
23d174c4a5 Update build.yml 2025-10-03 02:26:07 +02:00
Dawid Bepierszcz
b7371adf26 Update build.yml 2025-10-03 02:23:07 +02:00
Dawid Bepierszcz
9154748ce6 Update build.yml 2025-10-03 02:19:05 +02:00
Dawid Bepierszcz
da9830ee05 Bump version to 1.7.7-alpha-10 and update release workflow
Updated the version to 1.7.7-alpha-10 in both the code and VERSION file. Fixed artifact naming and release name in the GitHub Actions workflow. Also, modified Helper.cs to return DateTime.UtcNow in ActualDateTime().
2025-10-03 02:16:49 +02:00
Dawid Bepierszcz
b41ac992c0 Update README.md 2025-10-03 02:13:51 +02:00
Dawid Bepierszcz
af0bda8f3a Update README.md 2025-10-03 02:04:10 +02:00
72 changed files with 6129 additions and 1612 deletions

View File

@@ -16,6 +16,8 @@ env:
PROJECT_NAME_CS2_SIMPLEADMIN: "CS2-SimpleAdmin"
PROJECT_PATH_CS2_SIMPLEADMINAPI: "CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj"
PROJECT_NAME_CS2_SIMPLEADMINAPI: "CS2-SimpleAdminApi"
PROJECT_PATH_FUNCOMMANDSMODULE: "Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.csproj"
PROJECT_NAME_FUNCOMMANDSMODULE: "CS2-SimpleAdmin_FunCommands"
PROJECT_PATH_STEALTHMODULE: "Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.csproj"
PROJECT_NAME_STEALTHMODULE: "CS2-SimpleAdmin_StealthModule"
OUTPUT_PATH: "./counterstrikesharp"
@@ -45,21 +47,25 @@ jobs:
dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}
dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }}
dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}
dotnet restore ${{ env.PROJECT_PATH_FUNCOMMANDSMODULE }}
dotnet build ${{ env.PROJECT_PATH_FUNCOMMANDSMODULE }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_FUNCOMMANDSMODULE }}
dotnet restore ${{ env.PROJECT_PATH_STEALTHMODULE }}
dotnet build ${{ env.PROJECT_PATH_STEALTHMODULE }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_STEALTHMODULE }}
- name: Combine projects
run: |
mkdir -p ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin
mkdir -p ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin_FunCommands
mkdir -p ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin_StealthModule
mkdir -p ${{ env.OUTPUT_PATH }}/shared/CS2-SimpleAdminApi
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/* ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin/
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_FUNCOMMANDSMODULE }}/* ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin_FunCommands
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_STEALTHMODULE }}/* ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin_StealthModule
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/* ${{ env.OUTPUT_PATH }}/shared/CS2-SimpleAdminApi/
- name: Zip Main Build Output
run: zip -r CS2-SimpleAdmin${{ steps.get_version.outputs.VERSION }}.zip ${{ env.OUTPUT_PATH }}
run: zip -r CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip ${{ env.OUTPUT_PATH }}
- name: Extract & Zip StatusBlocker Linux
run: |
@@ -80,7 +86,7 @@ jobs:
with:
name: CS2-SimpleAdmin-Build-Artifacts
path: |
CS2-SimpleAdmin${{ steps.get_version.outputs.VERSION }}.zip
CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip
StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip
StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip
@@ -97,15 +103,15 @@ jobs:
name: CS2-SimpleAdmin-Build-Artifacts
path: .
- name: Unzip main build artifact
run: unzip CS2-SimpleAdmin${{ needs.build.outputs.build_version }}.zip -d ./counterstrikesharp
run: unzip CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}.zip -d ./counterstrikesharp
- name: Publish combined release
uses: ncipollo/release-action@v1.14.0
with:
artifacts: |
CS2-SimpleAdmin${{ needs.build.outputs.build_version }}.zip
CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}.zip
StatusBlocker-linux-${{ needs.build.outputs.build_version }}.zip
StatusBlocker-windows-${{ needs.build.outputs.build_version }}.zip
name: "CS2-SimpleAdmin Build #${{ needs.build.outputs.build_version }}"
name: "CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}"
tag: "build-${{ needs.build.outputs.build_version }}"
body: |
Place the files in your server as follows:

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ CS2-SimpleAdmin.sln.DotSettings.user
Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.sln.DotSettings.user
CS2-SimpleAdmin_BanSoundModule — kopia
*.user
CLAUDE.md

View File

@@ -1,27 +1,33 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Localization;
namespace CS2_SimpleAdmin.Api;
public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
{
public event Action? OnSimpleAdminReady;
public void OnSimpleAdminReadyEvent() => OnSimpleAdminReady?.Invoke();
public PlayerInfo GetPlayerInfo(CCSPlayerController player)
{
if (!player.UserId.HasValue)
throw new KeyNotFoundException("Player with specific UserId not found");
return CS2_SimpleAdmin.PlayersInfo[player.SteamID];
return !player.UserId.HasValue
? throw new KeyNotFoundException("Player with specific UserId not found")
: CS2_SimpleAdmin.PlayersInfo[player.SteamID];
}
public string GetConnectionString() => CS2_SimpleAdmin.Instance.DbConnectionString;
public string GetServerAddress() => CS2_SimpleAdmin.IpAddress;
public int? GetServerId() => CS2_SimpleAdmin.ServerId;
public Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetPlayerMuteStatus(CCSPlayerController player)
public Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetPlayerMuteStatus(
CCSPlayerController player)
{
return PlayerPenaltyManager.GetAllPlayerPenalties(player.Slot);
}
@@ -32,16 +38,20 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public event Action<int, bool>? OnAdminToggleSilent;
public void OnPlayerPenaltiedEvent(PlayerInfo player, PlayerInfo? admin, PenaltyType penaltyType, string reason,
int duration, int? penaltyId) => OnPlayerPenaltied?.Invoke(player, admin, penaltyType, reason, duration, penaltyId, CS2_SimpleAdmin.ServerId);
int duration, int? penaltyId) => OnPlayerPenaltied?.Invoke(player, admin, penaltyType, reason, duration,
penaltyId, CS2_SimpleAdmin.ServerId);
public void OnPlayerPenaltiedAddedEvent(SteamID player, PlayerInfo? admin, PenaltyType penaltyType, string reason,
int duration, int? penaltyId) => OnPlayerPenaltiedAdded?.Invoke(player, admin, penaltyType, reason, duration, penaltyId, CS2_SimpleAdmin.ServerId);
int duration, int? penaltyId) => OnPlayerPenaltiedAdded?.Invoke(player, admin, penaltyType, reason, duration,
penaltyId, CS2_SimpleAdmin.ServerId);
public void OnAdminShowActivityEvent(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs) => OnAdminShowActivity?.Invoke(messageKey, callerName, dontPublish, messageArgs);
public void OnAdminShowActivityEvent(string messageKey, string? callerName = null, bool dontPublish = false,
params object[] messageArgs) => OnAdminShowActivity?.Invoke(messageKey, callerName, dontPublish, messageArgs);
public void OnAdminToggleSilentEvent(int slot, bool status) => OnAdminToggleSilent?.Invoke(slot, status);
public void IssuePenalty(CCSPlayerController player, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1)
public void IssuePenalty(CCSPlayerController player, CCSPlayerController? admin, PenaltyType penaltyType,
string reason, int duration = -1)
{
switch (penaltyType)
{
@@ -80,7 +90,8 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
}
}
public void IssuePenalty(SteamID steamid, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1)
public void IssuePenalty(SteamID steamid, CCSPlayerController? admin, PenaltyType penaltyType, string reason,
int duration = -1)
{
switch (penaltyType)
{
@@ -163,8 +174,144 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
}
}
public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs)
public TargetResult? GetTarget(CommandInfo command)
{
return CS2_SimpleAdmin.GetTarget(command);
}
public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false,
params object[] messageArgs)
{
Helper.ShowAdminActivity(messageKey, callerName, dontPublish, messageArgs);
}
public void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null,
bool dontPublish = false)
{
Helper.ShowAdminActivityTranslated(translatedMessage, callerName, dontPublish);
}
public void ShowAdminActivityLocalized(object moduleLocalizer, string messageKey, string? callerName = null,
bool dontPublish = false, params object[] messageArgs)
{
if (moduleLocalizer is not IStringLocalizer localizer)
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
Helper.ShowAdminActivityLocalized(localizer, messageKey, callerName, dontPublish, messageArgs);
}
public void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")
{
Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryName, permission);
}
public void RegisterMenu(string categoryId, string menuId, string menuName,
Func<CCSPlayerController, object> menuFactory, string? permission = null, string? commandName = null)
{
Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuName, BuilderFactory, permission, commandName);
return;
MenuBuilder BuilderFactory(CCSPlayerController player)
{
if (menuFactory(player) is not MenuBuilder menuBuilder)
throw new InvalidOperationException("Menu factory must return MenuBuilder");
// Dodaj automatyczną obsługę przycisku 'Wróć'
menuBuilder.WithBackAction(p =>
{
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
{
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
}
else
{
Menus.MenuManager.Instance.OpenMainMenu(p);
}
});
return menuBuilder;
}
}
public void UnregisterMenu(string categoryId, string menuId)
{
Menus.MenuManager.Instance.UnregisterMenu(categoryId, menuId);
}
public object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)
{
var builder = new MenuBuilder(title);
builder.WithBackAction(p =>
{
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
{
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
}
else
{
Menus.MenuManager.Instance.OpenMainMenu(p);
}
});
return builder;
}
public List<CCSPlayerController> GetValidPlayers()
{
return Helper.GetValidPlayers();
}
public object CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin,
Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)
{
var menu = (MenuBuilder)CreateMenuWithBack(title, categoryId, admin);
var players = Helper.GetValidPlayers().Where(filter);
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
menu.AddOption(playerName, _ =>
{
if (player.IsValid)
{
onSelect(admin, player);
}
});
}
return menu;
}
public void AddMenuOption(object menu, string name, Action<CCSPlayerController> action, bool disabled = false,
string? permission = null)
{
if (menu is not MenuBuilder menuBuilder)
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
menuBuilder.AddOption(name, action, disabled, permission);
}
public void AddSubMenu(object menu, string name, Func<CCSPlayerController, object> subMenuFactory,
bool disabled = false, string? permission = null)
{
if (menu is not MenuBuilder menuBuilder)
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
menuBuilder.AddSubMenu(name, player =>
{
var subMenu = subMenuFactory(player);
if (subMenu is not MenuBuilder builder)
throw new InvalidOperationException("SubMenu factory must return MenuBuilder");
return builder;
}, disabled, permission);
}
public void OpenMenu(object menu, CCSPlayerController player)
{
if (menu is not MenuBuilder menuBuilder)
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
menuBuilder.OpenMenu(player);
}
}

View File

@@ -1,5 +1,4 @@
using System.Reflection;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Capabilities;
@@ -8,6 +7,7 @@ using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Logging;
using MySqlConnector;
@@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
public override string ModuleAuthor => "daffyy & Dliix66";
public override string ModuleVersion => "1.7.7-alpha-9";
public override string ModuleVersion => "1.7.8-beta-2";
public override void Load(bool hotReload)
{
@@ -67,6 +67,9 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
PlayersTimer?.Kill();
PlayersTimer = null;
PlayerManager.CheckPlayersTimer();
Menus.MenuManager.Instance.InitializeDefaultCategories();
BasicMenu.Initialize();
}
public override void OnAllPluginsLoaded(bool hotReload)
@@ -83,7 +86,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
AddTimer(6.0f, () => ReloadAdmins(null));
RegisterEvents();
RegisterCommands.InitializeCommands();
AddTimer(0.5f, RegisterCommands.InitializeCommands);
if (!CoreConfig.UnlockConCommands)
{
@@ -232,7 +235,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
WarnManager = new WarnManager(DatabaseProvider);
}
private static TargetResult? GetTarget(CommandInfo command, int argument = 1)
internal static TargetResult? GetTarget(CommandInfo command, int argument = 1)
{
var matches = command.GetArgTargetResult(argument);

View File

@@ -15,7 +15,7 @@
<DebugSymbols>false</DebugSymbols>
<PublishTrimmed>true</PublishTrimmed>
<DebuggerSupport>false</DebuggerSupport>
<GenerateDependencyFile>false</GenerateDependencyFile>
<!-- <GenerateDependencyFile>false</GenerateDependencyFile>-->
</PropertyGroup>
<ItemGroup>
@@ -34,18 +34,6 @@
<ItemGroup>
<ProjectReference Include="..\CS2-SimpleAdminApi\CS2-SimpleAdminApi.csproj" />
</ItemGroup>
<Target Name="ClearBuildFiles" AfterTargets="PostBuildEvent">
<ItemGroup>
<FilesToDelete Include="$(OutDir)Tomlyn.dll" />
<FilesToDelete Include="$(OutDir)Serilog*.dll" />
<FilesToDelete Include="$(OutDir)CS2-SimpleAdminApi.*" />
<FilesToDelete Include="$(OutDir)McMaster*" />
<FilesToDelete Include="$(OutDir)Scrutor.dll" />
</ItemGroup>
<Delete Files="@(FilesToDelete)" />
</Target>
<ItemGroup>
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
<None Update="Database\Migrations\Mysql\001_CreateTables.sql">
@@ -93,6 +81,9 @@
<None Update="Database\Migrations\Mysql\015_steamidToBigInt.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\016_OptimizeTablesAndIndexes.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\001_CreateTables.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@@ -138,6 +129,9 @@
<None Update="Database\Migrations\Sqlite\015_steamidToBigInt.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\016_OptimizeTablesAndIndexes.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>

View File

@@ -65,24 +65,11 @@ public static class RegisterCommands
new("css_vote", CS2_SimpleAdmin.Instance.OnVoteCommand),
new("css_noclip", CS2_SimpleAdmin.Instance.OnNoclipCommand),
new("css_freeze", CS2_SimpleAdmin.Instance.OnFreezeCommand),
new("css_unfreeze", CS2_SimpleAdmin.Instance.OnUnfreezeCommand),
new("css_godmode", CS2_SimpleAdmin.Instance.OnGodCommand),
new("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand),
new("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand),
new("css_give", CS2_SimpleAdmin.Instance.OnGiveCommand),
new("css_strip", CS2_SimpleAdmin.Instance.OnStripCommand),
new("css_hp", CS2_SimpleAdmin.Instance.OnHpCommand),
new("css_speed", CS2_SimpleAdmin.Instance.OnSpeedCommand),
new("css_gravity", CS2_SimpleAdmin.Instance.OnGravityCommand),
new("css_resize", CS2_SimpleAdmin.Instance.OnResizeCommand),
new("css_money", CS2_SimpleAdmin.Instance.OnMoneyCommand),
new("css_team", CS2_SimpleAdmin.Instance.OnTeamCommand),
new("css_rename", CS2_SimpleAdmin.Instance.OnRenameCommand),
new("css_prename", CS2_SimpleAdmin.Instance.OnPrenameCommand),
new("css_respawn", CS2_SimpleAdmin.Instance.OnRespawnCommand),
new("css_tp", CS2_SimpleAdmin.Instance.OnGotoCommand),
new("css_bring", CS2_SimpleAdmin.Instance.OnBringCommand),
new("css_pluginsmanager", CS2_SimpleAdmin.Instance.OnPluginManagerCommand),
@@ -160,23 +147,12 @@ public static class RegisterCommands
{ "css_addsilence", new Command { Aliases = ["css_addsilence"] } },
{ "css_unsilence", new Command { Aliases = ["css_unsilence"] } },
{ "css_vote", new Command { Aliases = ["css_vote"] } },
{ "css_noclip", new Command { Aliases = ["css_noclip"] } },
{ "css_freeze", new Command { Aliases = ["css_freeze"] } },
{ "css_unfreeze", new Command { Aliases = ["css_unfreeze"] } },
{ "css_godmode", new Command { Aliases = ["css_godmode"] } },
{ "css_slay", new Command { Aliases = ["css_slay"] } },
{ "css_slap", new Command { Aliases = ["css_slap"] } },
{ "css_give", new Command { Aliases = ["css_give"] } },
{ "css_strip", new Command { Aliases = ["css_strip"] } },
{ "css_hp", new Command { Aliases = ["css_hp"] } },
{ "css_speed", new Command { Aliases = ["css_speed"] } },
{ "css_gravity", new Command { Aliases = ["css_gravity"] } },
{ "css_resize", new Command { Aliases = ["css_resize", "css_size"] } },
{ "css_money", new Command { Aliases = ["css_money"] } },
{ "css_team", new Command { Aliases = ["css_team"] } },
{ "css_rename", new Command { Aliases = ["css_rename"] } },
{ "css_prename", new Command { Aliases = ["css_prename"] } },
{ "css_respawn", new Command { Aliases = ["css_respawn"] } },
{ "css_resize", new Command { Aliases = ["css_resize", "css_size"] } },
{ "css_tp", new Command { Aliases = ["css_tp", "css_tpto", "css_goto"] } },
{ "css_bring", new Command { Aliases = ["css_bring", "css_tphere"] } },
{ "css_pluginsmanager", new Command { Aliases = ["css_pluginsmanager", "css_pluginmanager"] } },
@@ -205,8 +181,8 @@ public static class RegisterCommands
var commandsConfig = JsonSerializer.Deserialize<CommandsConfig>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (commandsConfig?.Commands == null) return;
if (commandsConfig?.Commands != null)
{
foreach (var command in commandsConfig.Commands)
{
if (command.Value.Aliases == null) continue;
@@ -222,8 +198,9 @@ public static class RegisterCommands
CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback);
}
}
}
foreach (var (name, definitions) in RegisterCommands._commandDefinitions)
foreach (var (name, definitions) in _commandDefinitions)
{
foreach (var definition in definitions)
{

View File

@@ -316,7 +316,7 @@ public partial class CS2_SimpleAdmin
var canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban");
if (duration <= 0 && canPermBan == false)
if (duration <= 0 && !canPermBan)
{
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}");
return false;

View File

@@ -33,7 +33,7 @@ public partial class CS2_SimpleAdmin
[CommandHelper(usage: "[#userid or name]", whoCanExecute: CommandUsage.CLIENT_ONLY)]
public void OnPenaltiesCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller == null || caller.IsValid == false || !caller.UserId.HasValue || DatabaseProvider == null)
if (caller == null || !caller.IsValid || !caller.UserId.HasValue || DatabaseProvider == null)
return;
var userId = caller.UserId.Value;
@@ -160,7 +160,7 @@ public partial class CS2_SimpleAdmin
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
public void OnAdminVoiceCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller == null || caller.IsValid == false)
if (caller == null || !caller.IsValid)
return;
if (command.ArgCount > 1)
@@ -205,7 +205,7 @@ public partial class CS2_SimpleAdmin
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
public void OnAdminCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller == null || caller.IsValid == false)
if (caller == null || !caller.IsValid)
return;
AdminMenu.OpenMenu(caller);

View File

@@ -959,7 +959,7 @@ public partial class CS2_SimpleAdmin
var canPermMute = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permmute");
if (duration <= 0 && canPermMute == false)
if (duration <= 0 && !canPermMute)
{
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}");
return false;

View File

@@ -1,307 +1,307 @@
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Enables or disables no-clip mode for specified player(s).
/// </summary>
/// <param name="caller">The player issuing the command.</param>
/// <param name="command">The command input containing targets.</param>
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/cheats")]
public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player.IsValid &&
player is { IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
NoClip(caller, player, callerName);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Toggles no-clip mode for a player and shows admin activity messages.
/// </summary>
/// <param name="caller">The player/admin toggling no-clip.</param>
/// <param name="player">The target player whose no-clip state changes.</param>
/// <param name="callerName">Optional caller name for messages.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Toggle no-clip mode for the player
player.Pawn.Value?.ToggleNoclip();
// Determine message keys and arguments for the no-clip notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_noclip_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
/// <summary>
/// Enables or disables god mode for specified player(s).
/// </summary>
/// <param name="caller">The player issuing the command.</param>
/// <param name="command">The command input containing targets.</param>
[RequiresPermissions("@css/cheats")]
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnGodCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is {IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
God(caller, player, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Toggles god mode for a player and notifies admins.
/// </summary>
/// <param name="caller">The player/admin toggling god mode.</param>
/// <param name="player">The target player whose god mode changes.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Toggle god mode for the player
if (!GodPlayers.Add(player.Slot))
{
GodPlayers.Remove(player.Slot);
}
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// Determine message key and arguments for the god mode notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_god_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Freezes target player(s) for an optional specified duration.
/// </summary>
/// <param name="caller">The player issuing the freeze command.</param>
/// <param name="command">The command input containing targets and duration.</param>
[CommandHelper(1, "<#userid or name> [duration]")]
[RequiresPermissions("@css/slay")]
public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
int.TryParse(command.GetArg(2), out var time);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
Freeze(caller, player, time, callerName, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Resizes the target player(s) models to a specified scale.
/// </summary>
/// <param name="caller">The player issuing the resize command.</param>
/// <param name="command">The command input containing targets and scale factor.</param>
[CommandHelper(1, "<#userid or name> [size]")]
[RequiresPermissions("@css/slay")]
public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (!caller!.CanTarget(player)) return;
var sceneNode = player.PlayerPawn.Value!.CBodyComponent?.SceneNode;
if (sceneNode == null) return;
sceneNode.GetSkeletonInstance().Scale = size;
player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture));
Server.NextWorldUpdate(() =>
{
Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent");
});
var (activityMessageKey, adminActivityArgs) =
("sa_admin_resize_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Freezes a single player and optionally schedules automatic unfreeze after a duration.
/// </summary>
/// <param name="caller">The player/admin freezing the player.</param>
/// <param name="player">The player to freeze.</param>
/// <param name="time">Duration of freeze in seconds.</param>
/// <param name="callerName">Optional name for notifications.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time, string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Freeze player pawn
player.Pawn.Value?.Freeze();
// Determine message keys and arguments for the freeze notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_freeze_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Schedule unfreeze for the player if time is specified
if (time > 0)
{
Instance.AddTimer(time, () => player.Pawn.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
// Log the command and send Discord notification
if (command == null)
Helper.LogCommand(caller, $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}");
}
/// <summary>
/// Unfreezes target player(s).
/// </summary>
/// <param name="caller">The player issuing the unfreeze command.</param>
/// <param name="command">The command input with targets.</param>
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/slay")]
public void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
Unfreeze(caller, player, callerName, command);
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Unfreezes a single player and notifies admins.
/// </summary>
/// <param name="caller">The player/admin unfreezing the player.</param>
/// <param name="player">The player to unfreeze.</param>
/// <param name="callerName">Optional name for notifications.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void Unfreeze(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Unfreeze player pawn
player.Pawn.Value?.Unfreeze();
// Determine message keys and arguments for the unfreeze notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_unfreeze_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Log the command and send Discord notification
if (command == null)
Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
}
// using System.Globalization;
// using CounterStrikeSharp.API;
// using CounterStrikeSharp.API.Core;
// using CounterStrikeSharp.API.Modules.Admin;
// using CounterStrikeSharp.API.Modules.Commands;
//
// namespace CS2_SimpleAdmin;
//
// public partial class CS2_SimpleAdmin
// {
// /// <summary>
// /// Enables or disables no-clip mode for specified player(s).
// /// </summary>
// /// <param name="caller">The player issuing the command.</param>
// /// <param name="command">The command input containing targets.</param>
// [CommandHelper(1, "<#userid or name>")]
// [RequiresPermissions("@css/cheats")]
// public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
//
// var targets = GetTarget(command);
// if (targets == null) return;
// var playersToTarget = targets.Players.Where(player =>
// player.IsValid &&
// player is { IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// if (caller!.CanTarget(player))
// {
// NoClip(caller, player, callerName);
// }
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Toggles no-clip mode for a player and shows admin activity messages.
// /// </summary>
// /// <param name="caller">The player/admin toggling no-clip.</param>
// /// <param name="player">The target player whose no-clip state changes.</param>
// /// <param name="callerName">Optional caller name for messages.</param>
// /// <param name="command">Optional command info for logging.</param>
// internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
// {
// if (!player.IsValid) return;
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
// // Toggle no-clip mode for the player
// player.Pawn.Value?.ToggleNoclip();
//
// // Determine message keys and arguments for the no-clip notification
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_noclip_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
//
// // Log the command
// if (command == null)
// Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// }
//
// /// <summary>
// /// Enables or disables god mode for specified player(s).
// /// </summary>
// /// <param name="caller">The player issuing the command.</param>
// /// <param name="command">The command input containing targets.</param>
//
// [RequiresPermissions("@css/cheats")]
// [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
// public void OnGodCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// var targets = GetTarget(command);
// if (targets == null) return;
//
// var playersToTarget = targets.Players.Where(player => player.IsValid && player is {IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// if (player.Connected != PlayerConnectedState.PlayerConnected)
// return;
//
// if (caller!.CanTarget(player))
// {
// God(caller, player, command);
// }
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Toggles god mode for a player and notifies admins.
// /// </summary>
// /// <param name="caller">The player/admin toggling god mode.</param>
// /// <param name="player">The target player whose god mode changes.</param>
// /// <param name="command">Optional command info for logging.</param>
// internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null)
// {
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
// // Toggle god mode for the player
// if (!GodPlayers.Add(player.Slot))
// {
// GodPlayers.Remove(player.Slot);
// }
//
// // Log the command
// if (command == null)
// Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
//
// // Determine message key and arguments for the god mode notification
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_god_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
// }
//
// /// <summary>
// /// Freezes target player(s) for an optional specified duration.
// /// </summary>
// /// <param name="caller">The player issuing the freeze command.</param>
// /// <param name="command">The command input containing targets and duration.</param>
// [CommandHelper(1, "<#userid or name> [duration]")]
// [RequiresPermissions("@css/slay")]
// public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// int.TryParse(command.GetArg(2), out var time);
//
// var targets = GetTarget(command);
// if (targets == null) return;
// var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// if (caller!.CanTarget(player))
// {
// Freeze(caller, player, time, callerName, command);
// }
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Resizes the target player(s) models to a specified scale.
// /// </summary>
// /// <param name="caller">The player issuing the resize command.</param>
// /// <param name="command">The command input containing targets and scale factor.</param>
// [CommandHelper(1, "<#userid or name> [size]")]
// [RequiresPermissions("@css/slay")]
// public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size);
//
// var targets = GetTarget(command);
// if (targets == null) return;
// var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// if (!caller!.CanTarget(player)) return;
//
// var sceneNode = player.PlayerPawn.Value!.CBodyComponent?.SceneNode;
// if (sceneNode == null) return;
//
// sceneNode.GetSkeletonInstance().Scale = size;
// player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture));
//
// Server.NextWorldUpdate(() =>
// {
// Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent");
// });
//
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_resize_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Freezes a single player and optionally schedules automatic unfreeze after a duration.
// /// </summary>
// /// <param name="caller">The player/admin freezing the player.</param>
// /// <param name="player">The player to freeze.</param>
// /// <param name="time">Duration of freeze in seconds.</param>
// /// <param name="callerName">Optional name for notifications.</param>
// /// <param name="command">Optional command info for logging.</param>
// internal static void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time, string? callerName = null, CommandInfo? command = null)
// {
// if (!player.IsValid) return;
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
// // Freeze player pawn
// player.Pawn.Value?.Freeze();
//
// // Determine message keys and arguments for the freeze notification
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_freeze_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
//
// // Schedule unfreeze for the player if time is specified
// if (time > 0)
// {
// Instance.AddTimer(time, () => player.Pawn.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
// }
//
// // Log the command and send Discord notification
// if (command == null)
// Helper.LogCommand(caller, $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}");
// }
//
// /// <summary>
// /// Unfreezes target player(s).
// /// </summary>
// /// <param name="caller">The player issuing the unfreeze command.</param>
// /// <param name="command">The command input with targets.</param>
// [CommandHelper(1, "<#userid or name>")]
// [RequiresPermissions("@css/slay")]
// public void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
//
// var targets = GetTarget(command);
// if (targets == null) return;
// var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// Unfreeze(caller, player, callerName, command);
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Unfreezes a single player and notifies admins.
// /// </summary>
// /// <param name="caller">The player/admin unfreezing the player.</param>
// /// <param name="player">The player to unfreeze.</param>
// /// <param name="callerName">Optional name for notifications.</param>
// /// <param name="command">Optional command info for logging.</param>
// internal static void Unfreeze(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
// {
// if (!player.IsValid) return;
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
// // Unfreeze player pawn
// player.Pawn.Value?.Unfreeze();
//
// // Determine message keys and arguments for the unfreeze notification
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_unfreeze_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
//
// // Log the command and send Discord notification
// if (command == null)
// Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// }
// }

View File

@@ -11,9 +11,6 @@ namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
internal static readonly Dictionary<CCSPlayerController, float> SpeedPlayers = [];
internal static readonly Dictionary<CCSPlayerController, float> GravityPlayers = [];
/// <summary>
/// Executes the 'slay' command, forcing the targeted players to commit suicide.
/// Checks player validity and permissions.
@@ -72,451 +69,6 @@ public partial class CS2_SimpleAdmin
Helper.LogCommand(caller, $"css_slay {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
/// <summary>
/// Executes the 'give' command to provide a specified weapon to targeted players.
/// Enforces server rules for prohibited weapons.
/// </summary>
/// <param name="caller">Player or console issuing the command.</param>
/// <param name="command">Command details, including targets and weapon name.</param>
[RequiresPermissions("@css/cheats")]
[CommandHelper(minArgs: 2, usage: "<#userid or name> <weapon>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnGiveCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
var weaponName = command.GetArg(2);
// check if weapon is knife
if (weaponName.Contains("_knife") || weaponName.Contains("bayonet"))
{
if (CoreConfig.FollowCS2ServerGuidelines)
{
command.ReplyToCommand($"Cannot Give {weaponName} because it's illegal to be given.");
return;
}
}
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
GiveWeapon(caller, player, weaponName, callerName, command);
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Gives a weapon identified by name to a player, handling ambiguous matches and logging.
/// </summary>
/// <param name="caller">Admin issuing the command.</param>
/// <param name="player">Target player to receive the weapon.</param>
/// <param name="weaponName">Weapon name or partial name.</param>
/// <param name="callerName">Optional name to display in notifications.</param>
/// <param name="command">Optional command info for logging.</param>
private static void GiveWeapon(CCSPlayerController? caller, CCSPlayerController player, string weaponName, string? callerName = null, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
var weapons = WeaponHelper.GetWeaponsByPartialName(weaponName);
switch (weapons.Count)
{
case 0:
return;
case > 1:
{
var weaponList = string.Join(", ", weapons.Select(w => w.EnumMemberValue));
command?.ReplyToCommand($"Found weapons with a similar name: {weaponList}");
return;
}
}
// Give weapon to the player
player.GiveNamedItem(weapons.First().EnumValue);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_giveweapon {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {weaponName}");
// Determine message keys and arguments for the weapon give notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_give_message",
new object[] { "CALLER", player.PlayerName, weaponName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Gives a specific weapon to a player, with notifications and logging.
/// </summary>
/// <param name="caller">Admin issuing the command.</param>
/// <param name="player">Target player.</param>
/// <param name="weapon">Weapon item object.</param>
/// <param name="callerName">Optional caller name for notifications.</param>
/// <param name="command">Optional command info.</param>
internal static void GiveWeapon(CCSPlayerController? caller, CCSPlayerController player, CsItem weapon, string? callerName = null, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Give weapon to the player
player.GiveNamedItem(weapon);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_giveweapon {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {weapon.ToString()}");
// Determine message keys and arguments for the weapon give notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_give_message",
new object[] { "CALLER", player.PlayerName, weapon.ToString() });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Executes the 'strip' command, removing all weapons from targeted players.
/// Checks player validity and permissions.
/// </summary>
/// <param name="caller">Player or console issuing the command.</param>
/// <param name="command">Command details including targets.</param>
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnStripCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
StripWeapons(caller, player, callerName, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Removes all weapons from a player, with notifications and logging.
/// </summary>
/// <param name="caller">Admin or console issuing the strip command.</param>
/// <param name="player">Target player.</param>
/// <param name="callerName">Optional caller name.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void StripWeapons(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Check if player is valid, alive, and connected
if (!player.IsValid || player.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE || player.Connected != PlayerConnectedState.PlayerConnected)
return;
// Strip weapons from the player
player.RemoveWeapons();
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_strip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// Determine message keys and arguments for the weapon strip notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_strip_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Sets health value on targeted players.
/// </summary>
/// <param name="caller">Admin or console issuing the command.</param>
/// <param name="command">Command details including targets and health value.</param>
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> <health>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnHpCommand(CCSPlayerController? caller, CommandInfo command)
{
int.TryParse(command.GetArg(2), out var health);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
SetHp(caller, player, health, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Changes health of a player and logs the action.
/// </summary>
/// <param name="caller">Admin or console calling the method.</param>
/// <param name="player">Target player.</param>
/// <param name="health">Health value to set.</param>
/// <param name="command">Optional command info.</param>
internal static void SetHp(CCSPlayerController? caller, CCSPlayerController player, int health, CommandInfo? command = null)
{
if (!player.IsValid || player.IsHLTV) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Set player's health
player.SetHp(health);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_hp {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {health}");
// Determine message keys and arguments for the HP set notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_hp_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Sets movement speed on targeted players.
/// </summary>
/// <param name="caller">Admin or console issuing the command.</param>
/// <param name="command">Command details including targets and speed.</param>
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> <speed>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSpeedCommand(CCSPlayerController? caller, CommandInfo command)
{
float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var speed);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
SetSpeed(caller, player, speed, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Changes speed of a player and logs the action.
/// </summary>
/// <param name="caller">Admin or console calling the method.</param>
/// <param name="player">Target player.</param>
/// <param name="speed">Speed value to set.</param>
/// <param name="command">Optional command info.</param>
internal static void SetSpeed(CCSPlayerController? caller, CCSPlayerController player, float speed, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Set player's speed
player.SetSpeed(speed);
if (speed == 1f)
SpeedPlayers.Remove(player);
else
SpeedPlayers[player] = speed;
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_speed {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {speed}");
// Determine message keys and arguments for the speed set notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_speed_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Sets gravity on targeted players.
/// </summary>
/// <param name="caller">Admin or console issuing the command.</param>
/// <param name="command">Command details including targets and gravity value.</param>
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> <gravity>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnGravityCommand(CCSPlayerController? caller, CommandInfo command)
{
float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var gravity);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
SetGravity(caller, player, gravity, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Changes gravity of a player and logs the action.
/// </summary>
/// <param name="caller">Admin or console calling the method.</param>
/// <param name="player">Target player.</param>
/// <param name="gravity">Gravity value to set.</param>
/// <param name="command">Optional command info.</param>
internal static void SetGravity(CCSPlayerController? caller, CCSPlayerController player, float gravity, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Set player's gravity
player.SetGravity(gravity);
if (gravity == 1f)
GravityPlayers.Remove(player);
else
GravityPlayers[player] = gravity;
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_gravity {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {gravity}");
// Determine message keys and arguments for the gravity set notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_gravity_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Sets the money amount for the targeted players.
/// </summary>
/// <param name="caller">The player/admin executing the command.</param>
/// <param name="command">The command containing target player and money value.</param>
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> <money>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnMoneyCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
int.TryParse(command.GetArg(2), out var money);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
SetMoney(caller, player, money, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Applies money value to a single targeted player and logs the operation.
/// </summary>
/// <param name="caller">The player/admin setting the money.</param>
/// <param name="player">The player whose money will be set.</param>
/// <param name="money">The value of money to set.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void SetMoney(CCSPlayerController? caller, CCSPlayerController player, int money, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Set player's money
player.SetMoney(money);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_money {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {money}");
// Determine message keys and arguments for the money set notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_money_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Applies damage as a slap effect to the targeted players.
/// </summary>
@@ -568,6 +120,7 @@ public partial class CS2_SimpleAdmin
// Apply slap damage to the player
player.Pawn.Value?.Slap(damage);
player.EmitSound("BaseGrenade.JumpThrowM");
// Log the command
if (command == null)
@@ -802,75 +355,6 @@ public partial class CS2_SimpleAdmin
});
}
/// <summary>
/// Respawns targeted players, restoring their state.
/// </summary>
/// <param name="caller">The admin or player issuing respawn.</param>
/// <param name="command">The command including target players.</param>
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/cheats")]
public void OnRespawnCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
Respawn(caller, player, callerName, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Respawns a specified player and updates admin notifications.
/// </summary>
/// <param name="caller">Admin or player executing respawn.</param>
/// <param name="player">Player to respawn.</param>
/// <param name="callerName">Optional admin name.</param>
/// <param name="command">Optional command info.</param>
internal static void Respawn(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
// Check if the caller can target the player
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Ensure the player's pawn is valid before attempting to respawn
if (_cBasePlayerControllerSetPawnFunc == null || player.PlayerPawn.Value == null || !player.PlayerPawn.IsValid) return;
// Perform the respawn operation
var playerPawn = player.PlayerPawn.Value;
_cBasePlayerControllerSetPawnFunc.Invoke(player, playerPawn, true, false);
VirtualFunction.CreateVoid<CCSPlayerController>(player.Handle, GameData.GetOffset("CCSPlayerController_Respawn"))(player);
if (player.UserId.HasValue && PlayersInfo.TryGetValue(player.SteamID, out var value) && value.DiePosition != null)
playerPawn.Teleport(value.DiePosition?.Position, value.DiePosition?.Angle);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_respawn {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// Determine message key and arguments for the respawn notification
var activityMessageKey = "sa_admin_respawn_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName };
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
/// <summary>
/// Teleports targeted player(s) to another player's location.
/// </summary>

View File

@@ -17,8 +17,9 @@ public class Migration(string migrationsPath)
if (files.Count == 0) return;
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using (var cmd = connection.CreateCommand())
{
if (migrationsPath.Contains("sqlite", StringComparison.CurrentCultureIgnoreCase))
{
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS sa_migrations (
@@ -27,6 +28,16 @@ public class Migration(string migrationsPath)
);
""";
}
else
{
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS sa_migrations (
id INT PRIMARY KEY AUTO_INCREMENT,
version VARCHAR(128) NOT NULL
);
""";
}
await cmd.ExecuteNonQueryAsync();
}

View File

@@ -20,7 +20,6 @@ CREATE TABLE IF NOT EXISTS `sa_groups_servers` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `sa_admins` ADD `group_id` INT NULL AFTER `created`;
ALTER TABLE `sa_groups_flags` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE CASCADE;
ALTER TABLE `sa_groups_servers` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE CASCADE;
ALTER TABLE `sa_admins` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE SET NULL;

View File

@@ -1,8 +1,6 @@
CREATE TABLE IF NOT EXISTS `sa_players_ips` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`steamid` bigint(20) NOT NULL,
`address` varchar(64) NOT NULL,
`used_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `steamid` (`steamid`,`address`)
PRIMARY KEY (`steamid`, `address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@@ -1,11 +1,3 @@
DELETE FROM `sa_players_ips`
WHERE `id` NOT IN (
SELECT * FROM (
SELECT MIN(`id`)
FROM `sa_players_ips`
GROUP BY `steamid`
) AS `keep_ids`
);
DELETE FROM sa_players_ips WHERE INET_ATON(address) IS NULL AND address IS NOT NULL;
UPDATE `sa_players_ips` SET `address` = INET_ATON(address);
ALTER TABLE `sa_players_ips` CHANGE `address` `address` INT UNSIGNED NOT NULL;

View File

@@ -0,0 +1,33 @@
-- Migration 016: Optimize tables and indexes
-- Add proper indexes for all tables to improve query performance
-- Optimize sa_players_ips table indexes
-- Add index on used_at for efficient date-based queries
ALTER TABLE `sa_players_ips` ADD INDEX IF NOT EXISTS `idx_used_at` (`used_at` DESC);
-- Optimize sa_bans table indexes
-- Add composite indexes for common query patterns
CREATE INDEX IF NOT EXISTS `idx_bans_steamid_status` ON `sa_bans` (`player_steamid`, `status`);
CREATE INDEX IF NOT EXISTS `idx_bans_ip_status` ON `sa_bans` (`player_ip`, `status`);
CREATE INDEX IF NOT EXISTS `idx_bans_status_ends` ON `sa_bans` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_server_status` ON `sa_bans` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_created` ON `sa_bans` (`created` DESC);
-- Optimize sa_admins table indexes
CREATE INDEX IF NOT EXISTS `idx_admins_steamid` ON `sa_admins` (`player_steamid`);
CREATE INDEX IF NOT EXISTS `idx_admins_server_ends` ON `sa_admins` (`server_id`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_admins_ends` ON `sa_admins` (`ends`);
-- Optimize sa_mutes table indexes (in addition to migration 014)
-- Add index for expire queries
CREATE INDEX IF NOT EXISTS `idx_mutes_status_ends` ON `sa_mutes` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_server_status` ON `sa_mutes` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_created` ON `sa_mutes` (`created` DESC);
-- Optimize sa_warns table indexes (if exists)
CREATE INDEX IF NOT EXISTS `idx_warns_steamid_status` ON `sa_warns` (`player_steamid`, `status`);
CREATE INDEX IF NOT EXISTS `idx_warns_status_ends` ON `sa_warns` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_warns_server_status` ON `sa_warns` (`server_id`, `status`, `ends`);
-- Add index on sa_servers for faster lookups
CREATE INDEX IF NOT EXISTS `idx_servers_hostname` ON `sa_servers` (`hostname`);

View File

@@ -1,7 +1,6 @@
CREATE TABLE IF NOT EXISTS `sa_players_ips` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`steamid` INTEGER NOT NULL,
`address` VARCHAR(64) NOT NULL,
`address` INTEGER NOT NULL
`used_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (`steamid`, `address`)
PRIMARY KEY (`steamid`, `address`)
);

View File

@@ -1,9 +1,4 @@
DELETE FROM `sa_players_ips`
WHERE `id` NOT IN (
SELECT MIN(`id`)
FROM `sa_players_ips`
GROUP BY `steamid`
);
TRUNCATE TABLE `sa_players_ips`;
ALTER TABLE `sa_players_ips` ADD `name` VARCHAR(64) NULL DEFAULT NULL;
CREATE INDEX IF NOT EXISTS `idx_sa_players_ips_used_at` ON `sa_players_ips` (`used_at` DESC);

View File

@@ -0,0 +1,33 @@
-- Migration 016: Optimize tables and indexes
-- Add proper indexes for all tables to improve query performance
-- Optimize sa_players_ips table indexes
-- Add index on used_at for efficient date-based queries
CREATE INDEX IF NOT EXISTS `idx_used_at` ON `sa_players_ips` (`used_at` DESC);
-- Optimize sa_bans table indexes
-- Add composite indexes for common query patterns
CREATE INDEX IF NOT EXISTS `idx_bans_steamid_status` ON `sa_bans` (`player_steamid`, `status`);
CREATE INDEX IF NOT EXISTS `idx_bans_ip_status` ON `sa_bans` (`player_ip`, `status`);
CREATE INDEX IF NOT EXISTS `idx_bans_status_ends` ON `sa_bans` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_server_status` ON `sa_bans` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_created` ON `sa_bans` (`created` DESC);
-- Optimize sa_admins table indexes
CREATE INDEX IF NOT EXISTS `idx_admins_steamid` ON `sa_admins` (`player_steamid`);
CREATE INDEX IF NOT EXISTS `idx_admins_server_ends` ON `sa_admins` (`server_id`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_admins_ends` ON `sa_admins` (`ends`);
-- Optimize sa_mutes table indexes (in addition to migration 014)
-- Add index for expire queries
CREATE INDEX IF NOT EXISTS `idx_mutes_status_ends` ON `sa_mutes` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_server_status` ON `sa_mutes` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_created` ON `sa_mutes` (`created` DESC);
-- Optimize sa_warns table indexes (if exists)
CREATE INDEX IF NOT EXISTS `idx_warns_steamid_status` ON `sa_warns` (`player_steamid`, `status`);
CREATE INDEX IF NOT EXISTS `idx_warns_status_ends` ON `sa_warns` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_warns_server_status` ON `sa_warns` (`server_id`, `status`, `ends`);
-- Add index on sa_servers for faster lookups
CREATE INDEX IF NOT EXISTS `idx_servers_hostname` ON `sa_servers` (`hostname`);

View File

@@ -9,6 +9,15 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
{
var connection = new MySqlConnection(connectionString);
await connection.OpenAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
await cmd.ExecuteNonQueryAsync();
cmd.CommandText = "SET time_zone = '+00:00';";
await cmd.ExecuteNonQueryAsync();
return connection;
}

View File

@@ -96,9 +96,6 @@ public partial class CS2_SimpleAdmin
CachedPlayers.Remove(player);
SilentPlayers.Remove(player.Slot);
GodPlayers.Remove(player.Slot);
SpeedPlayers.Remove(player);
GravityPlayers.Remove(player);
if (player.IsBot)
return HookResult.Continue;
@@ -247,10 +244,6 @@ public partial class CS2_SimpleAdmin
Logger.LogCritical("[OnRoundStart]");
#endif
GodPlayers.Clear();
SpeedPlayers.Clear();
GravityPlayers.Clear();
foreach (var player in PlayersInfo.Values)
{
player.DiePosition = null;
@@ -467,8 +460,6 @@ public partial class CS2_SimpleAdmin
GodPlayers.Clear();
SilentPlayers.Clear();
SpeedPlayers.Clear();
GravityPlayers.Clear();
PlayerPenaltyManager.RemoveAllPenalties();
}
@@ -494,13 +485,9 @@ public partial class CS2_SimpleAdmin
{
var player = @event.Userid;
if (player?.UserId == null || !player.IsValid || player.IsHLTV || player.Connected != PlayerConnectedState.PlayerConnected)
return HookResult.Continue;
SpeedPlayers.Remove(player);
GravityPlayers.Remove(player);
if (!PlayersInfo.ContainsKey(player.SteamID) || @event.Attacker == null)
if (player?.UserId == null || !player.IsValid || player.IsHLTV ||
player.Connected != PlayerConnectedState.PlayerConnected || !PlayersInfo.ContainsKey(player.SteamID) ||
@event.Attacker == null)
return HookResult.Continue;
var playerPosition = player.PlayerPawn.Value?.AbsOrigin;

View File

@@ -338,7 +338,7 @@ public static class PlayerExtensions
foreach (var part in Helper.SeparateLines(sb.ToString()))
{
var lineWithPrefix = localizer["sa_prefix"] + part.Trim();
var lineWithPrefix = (CS2_SimpleAdmin._localizer?["sa_prefix"] ?? "") + part.Trim();
controller.PrintToChat(lineWithPrefix);
}
}

View File

@@ -450,7 +450,7 @@ internal static class Helper
var formattedMessageArgs = messageArgs.Select(arg => arg.ToString() ?? string.Empty).ToArray();
if (dontPublish == false && publishActions.Any(messageKey.Contains))
if (!dontPublish && publishActions.Any(messageKey.Contains))
{
CS2_SimpleAdmin.SimpleAdminApi?.OnAdminShowActivityEvent(messageKey, callerName, dontPublish, messageArgs);
}
@@ -499,6 +499,89 @@ internal static class Helper
}
}
/// <summary>
/// Shows admin activity with a custom translated message (for modules with their own localizer).
/// </summary>
public static void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null, bool dontPublish = false)
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return;
if (CS2_SimpleAdmin._localizer == null) return;
if (string.IsNullOrWhiteSpace(callerName))
callerName = CS2_SimpleAdmin._localizer["sa_console"];
var validPlayers = GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false });
if (!validPlayers.Any())
return;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 3)
{
validPlayers = validPlayers.Where(c =>
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") ||
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban"));
}
foreach (var controller in validPlayers.ToList())
{
// Replace "CALLER" placeholder based on showActivityType
var message = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{
1 => translatedMessage.Replace("CALLER", AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]),
_ => translatedMessage.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]),
};
// Send the pre-translated message to the player
controller.PrintToChat(message);
}
}
/// <summary>
/// Shows admin activity using module's localizer for per-player language support.
/// Each player receives the message in their configured language using SendLocalizedMessage.
/// </summary>
public static void ShowAdminActivityLocalized(IStringLocalizer moduleLocalizer, string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs)
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return;
if (CS2_SimpleAdmin._localizer == null) return;
if (string.IsNullOrWhiteSpace(callerName))
callerName = CS2_SimpleAdmin._localizer["sa_console"];
var formattedMessageArgs = messageArgs.Select(arg => arg.ToString() ?? string.Empty).ToArray();
var validPlayers = GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false });
if (!validPlayers.Any())
return;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 3)
{
validPlayers = validPlayers.Where(c =>
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") ||
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban"));
}
foreach (var controller in validPlayers.ToList())
{
var currentMessageArgs = (string[])formattedMessageArgs.Clone();
// Replace "CALLER" placeholder based on showActivityType
for (var i = 0; i < currentMessageArgs.Length; i++)
{
var arg = currentMessageArgs[i];
currentMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{
1 => arg.Replace("CALLER", AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]),
_ => arg.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]),
};
}
// Send the localized message to each player using their language
controller.SendLocalizedMessage(moduleLocalizer, messageKey, currentMessageArgs.Cast<object>().ToArray());
}
}
public static void DisplayCenterMessage(
CCSPlayerController player,
string messageKey,
@@ -794,7 +877,7 @@ internal static class Helper
if (CS2_SimpleAdmin.DiscordWebhookClientLog == null || CS2_SimpleAdmin._localizer == null)
return;
if (caller != null && caller.IsValid == false)
if (caller != null && !caller.IsValid)
caller = null;
var callerName = caller == null ? CS2_SimpleAdmin._localizer["sa_console"] : caller.PlayerName;
@@ -806,32 +889,38 @@ internal static class Helper
commandString]));
}
public static IMenu? CreateMenu(string title, Action<CCSPlayerController>? backAction = null)
#pragma warning disable CS8604
public static IMenu? CreateMenu(string title, Action<CCSPlayerController>? backAction = null, Action<CCSPlayerController>? resetAction = null)
{
var menuType = CS2_SimpleAdmin.Instance.Config.MenuConfigs.MenuType.ToLower();
if (CS2_SimpleAdmin.MenuApi == null)
{
return null;
}
var menuType = CS2_SimpleAdmin.Instance.Config.MenuConfigs.MenuType.ToLower();
var menu = menuType switch
{
_ when menuType.Equals("selectable", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenu(title),
CS2_SimpleAdmin.MenuApi.GetMenu(title, backAction, resetAction),
_ when menuType.Equals("dynamic", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ButtonMenu),
CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ButtonMenu, backAction, resetAction),
_ when menuType.Equals("center", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.CenterMenu),
CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.CenterMenu, backAction, resetAction),
_ when menuType.Equals("chat", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ChatMenu),
CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ChatMenu, backAction, resetAction),
_ when menuType.Equals("console", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ConsoleMenu),
CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ConsoleMenu, backAction, resetAction),
_ => CS2_SimpleAdmin.MenuApi?.GetMenu(title)
_ => CS2_SimpleAdmin.MenuApi.GetMenu(title, backAction, resetAction)
};
return menu;
}
#pragma warning restore CS8604
internal static IPluginManager? GetPluginManager()
{
@@ -937,6 +1026,7 @@ public static class Time
{
public static DateTime ActualDateTime()
{
return DateTime.UtcNow;
string timezoneId = CS2_SimpleAdmin.Instance.Config.Timezone;
DateTime utcNow = DateTime.UtcNow;

View File

@@ -29,7 +29,6 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
try
{
var sql = databaseProvider.GetAddBanQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64,

View File

@@ -70,39 +70,44 @@ internal class CacheManager: IDisposable
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
var ipHistory =
(await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC")).ToList();
// Optimization: Load IP history and build cache in single pass
var ipHistory = await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY steamid, address, used_at DESC");
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
{
var ipSet = group
.GroupBy(x => x.address)
.Select(g =>
{
var latest = g.MaxBy(x => x.used_at);
return new IpRecord(
g.Key,
latest.used_at,
string.IsNullOrEmpty(latest.name)
? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
: latest.name
);
})
.ToHashSet(new IpRecordComparer());
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var currentSteamId = 0UL;
var currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
var latestIpTimestamps = new Dictionary<uint, DateTime>();
_playerIpsCache.AddOrUpdate(
group.Key,
_ => ipSet,
(_, existingSet) =>
foreach (var record in ipHistory)
{
foreach (var ip in ipSet)
// When we encounter a new steamid, save the previous one
if (record.steamid != currentSteamId && currentSteamId != 0)
{
existingSet.Remove(ip);
existingSet.Add(ip);
_playerIpsCache[currentSteamId] = currentIpSet;
currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
latestIpTimestamps.Clear();
}
return existingSet;
});
currentSteamId = record.steamid;
// Only keep the latest timestamp for each IP
if (!latestIpTimestamps.TryGetValue(record.address, out var existingTimestamp) ||
record.used_at > existingTimestamp)
{
latestIpTimestamps[record.address] = record.used_at;
currentIpSet.Add(new IpRecord(
record.address,
record.used_at,
string.IsNullOrEmpty(record.name) ? unknownName : record.name
));
}
}
// Don't forget the last steamid
if (currentSteamId != 0)
{
_playerIpsCache[currentSteamId] = currentIpSet;
}
}
@@ -151,7 +156,9 @@ internal class CacheManager: IDisposable
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<BanRecord> updatedBans;
var allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
// Optimization: Only get IDs for comparison if we need to check for deletions
// Most of the time bans are just added/updated, not deleted
HashSet<int>? allIds = null;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
@@ -166,6 +173,14 @@ internal class CacheManager: IDisposable
""",
new { lastUpdate = _lastUpdateTime }
));
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0)
{
allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
}
updatedBans = updatedList;
}
else
{
@@ -180,8 +195,22 @@ internal class CacheManager: IDisposable
""",
new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId }
));
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0)
{
allIds = (await connection.QueryAsync<int>(
"SELECT id FROM sa_bans WHERE server_id = @serverId",
new { serverId = CS2_SimpleAdmin.ServerId }
)).ToHashSet();
}
updatedBans = updatedList;
}
// Optimization: Only process deletions if we have the full ID list
if (allIds != null)
{
foreach (var id in _banCache.Keys)
{
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
@@ -203,6 +232,7 @@ internal class CacheManager: IDisposable
_ipIndex.TryRemove(ipUInt, out _);
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
@@ -245,12 +275,21 @@ internal class CacheManager: IDisposable
}
}
// Update cache with new/modified bans
var hasUpdates = false;
foreach (var ban in updatedBans)
{
_banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban);
hasUpdates = true;
}
// Always rebuild indexes if there were any updates
// This ensures status changes (ACTIVE -> UNBANNED) are reflected
if (hasUpdates)
{
RebuildIndexes();
}
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
}
catch (Exception)
@@ -267,37 +306,37 @@ internal class CacheManager: IDisposable
_steamIdIndex.Clear();
_ipIndex.Clear();
foreach (var ban in _banCache.Values)
{
if (ban.StatusEnum != BanStatus.ACTIVE)
continue;
// Optimization: Cache config value to avoid repeated property access
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
var checkIpBans = banType != 0;
if (ban.PlayerSteamId != null)
// Optimization: Pre-filter only ACTIVE bans to avoid checking status in loop
var activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE);
foreach (var ban in activeBans)
{
var steamId = ban.PlayerSteamId;
_steamIdIndex.AddOrUpdate(
steamId.Value,
key => [ban],
(key, list) =>
// Index by Steam ID
if (ban.PlayerSteamId.HasValue)
{
list.Add(ban);
return list;
});
var steamId = ban.PlayerSteamId.Value;
if (!_steamIdIndex.TryGetValue(steamId, out var steamList))
{
steamList = new List<BanRecord>();
_steamIdIndex[steamId] = steamList;
}
steamList.Add(ban);
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) continue;
if (ban.PlayerIp != null &&
// Index by IP (only if IP bans are enabled)
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
{
_ipIndex.AddOrUpdate(
ipUInt,
key => [ban],
(key, list) =>
if (!_ipIndex.TryGetValue(ipUInt, out var ipList))
{
list.Add(ban);
return list;
});
ipList = new List<BanRecord>();
_ipIndex[ipUInt] = ipList;
}
ipList.Add(ban);
}
}
}
@@ -332,16 +371,16 @@ internal class CacheManager: IDisposable
{
var ipAsUint = IpHelper.IpToUint(ipAddress);
var results = new List<(ulong, DateTime, string)>();
var comparer = _playerIpsCache.Comparer;
// Optimization: Direct lookup using HashSet.Contains instead of TryGetValue
var searchRecord = new IpRecord(ipAsUint, default, null!);
foreach (var (steamId, ipSet) in _playerIpsCache)
{
if (!ipSet.TryGetValue(new IpRecord(ipAsUint, Time.ActualDateTime(), "Unknown"), out var actualEntry)) continue;
results.Add((steamId, actualEntry.UsedAt, actualEntry.PlayerName));
// Optimization: Single pass through the set
foreach (var entry in ipSet)
{
if (entry.Ip == ipAsUint && !Equals(entry, actualEntry))
if (entry.Ip == ipAsUint)
{
results.Add((steamId, entry.UsedAt, entry.PlayerName));
}

View File

@@ -95,66 +95,36 @@ internal class PlayerManager
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
if (CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(
steamId, ipAddress))
{
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP,
name = @playerName
WHERE steamid = @SteamID AND address = @IPAddress;
// Eliminates the need for SELECT COUNT and duplicate UPDATE queries
var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64;
var ipUint = IpHelper.IpToUint(ipAddress);
// MySQL: INSERT ... ON DUPLICATE KEY UPDATE pattern
const string upsertQuery = """
INSERT INTO `sa_players_ips` (steamid, name, address, used_at)
VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
used_at = CURRENT_TIMESTAMP,
name = @playerName;
""";
await connection.ExecuteAsync(updateQuery, new
await connection.ExecuteAsync(upsertQuery, new
{
SteamID = steamId64,
playerName,
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
else
{
const string selectQuery =
"SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;";
var recordExists = await connection.ExecuteScalarAsync<int>(selectQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
IPAddress = ipUint
});
if (recordExists > 0)
{
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP,
name = @playerName
WHERE steamid = @SteamID AND address = @IPAddress;
""";
await connection.ExecuteAsync(updateQuery, new
{
playerName,
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
else
{
const string insertQuery = """
INSERT INTO `sa_players_ips` (steamid, name, address, used_at)
VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP);
""";
await connection.ExecuteAsync(insertQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
playerName,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
}
// // Cache will be updated on next refresh cycle
// if (!CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(steamId, ipAddress))
// {
// // IP association will be reflected after cache refresh
// }
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(
$"Unable to save ip address for {playerInfo.Name} ({ipAddress}) {ex.Message}");
$"Unable to save ip address for {playerInfo.Name} ({ipAddress}): {ex.Message}");
}
playerInfo.AccountsAssociated =
@@ -313,31 +283,6 @@ internal class PlayerManager
/// </remarks>
public void CheckPlayersTimer()
{
CS2_SimpleAdmin.Instance.AddTimer(0.12f, () =>
{
if (CS2_SimpleAdmin.SpeedPlayers.Count > 0)
{
foreach (var (player, speed) in CS2_SimpleAdmin.SpeedPlayers)
{
if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.SetSpeed(speed);
}
}
}
if (CS2_SimpleAdmin.GravityPlayers.Count > 0)
{
foreach (var (player, gravity) in CS2_SimpleAdmin.GravityPlayers)
{
if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.SetGravity(gravity);
}
}
}
}, TimerFlags.REPEAT);
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
{
#if DEBUG
@@ -346,18 +291,26 @@ internal class PlayerManager
if (CS2_SimpleAdmin.DatabaseProvider == null)
return;
var tempPlayers = Helper.GetValidPlayers()
.Select(p => new
// Optimization: Get players once and avoid allocating anonymous types
var validPlayers = Helper.GetValidPlayers();
if (validPlayers.Count == 0)
return;
// Use ValueTuple instead of anonymous type - better performance and less allocations
var tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
foreach (var p in validPlayers)
{
p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot,
})
.ToList();
tempPlayers.Add((p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot));
}
var pluginInstance = CS2_SimpleAdmin.Instance;
var config = _config.OtherSettings; // Cache config access
_ = Task.Run(async () =>
{
try
{
// Run all expire tasks in parallel
var expireTasks = new[]
{
pluginInstance.BanManager.ExpireOldBans(),
@@ -385,21 +338,32 @@ internal class PlayerManager
if (pluginInstance.CacheManager == null)
return;
var bannedPlayers = tempPlayers.AsValueEnumerable()
.Where(player =>
// Optimization: Cache ban type and multi-account check to avoid repeated config access
var banType = config.BanType;
var checkMultiAccounts = config.CheckMultiAccountsByIp;
var bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>();
// Manual loop instead of LINQ - better performance
foreach (var player in tempPlayers)
{
var playerName = player.PlayerName;
var steamId = player.SteamID;
var ip = player.IpAddress?.Split(':')[0];
return CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
bool isBanned = banType switch
{
0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null),
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
_ => checkMultiAccounts
? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip)
: pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip)
};
}).ToList();
if (isBanned)
{
bannedPlayers.Add(player);
}
}
if (bannedPlayers.Count > 0)
{
@@ -410,32 +374,38 @@ internal class PlayerManager
}
}
if (_config.OtherSettings.TimeMode == 0)
if (config.TimeMode == 0)
{
// Optimization: Manual projection instead of LINQ
var onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count);
foreach (var player in tempPlayers)
{
onlinePlayers.Add((player.SteamID, player.UserId, player.Slot));
}
if (onlinePlayers.Count > 0)
{
var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList();
if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return;
await pluginInstance.MuteManager.CheckOnlineModeMutes(onlinePlayers);
}
}
});
try
{
// Optimization: Process penalties without LINQ allocations
var players = Helper.GetValidPlayers();
var penalizedSlots = players
.Where(player => PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
.Select(player => new
foreach (var player in players)
{
Player = player,
IsMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _),
IsSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _),
IsGagged = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _)
});
if (!PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
continue;
foreach (var entry in penalizedSlots)
var isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
var isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
// Only reset voice flags if not muted or silenced
if (!isMuted && !isSilenced)
{
if (!entry.IsMuted && !entry.IsSilenced)
{
entry.Player.VoiceFlags = VoiceFlags.Normal;
player.VoiceFlags = VoiceFlags.Normal;
}
}

View File

@@ -35,29 +35,43 @@ public class ServerManager
{
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return;
if (_getIpTryCount > 32 && Helper.GetServerIp().StartsWith("0.0.0.0") || string.IsNullOrEmpty(Helper.GetServerIp()))
// Optimization: Get server IP once and reuse
var serverIp = Helper.GetServerIp();
var isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0");
// Check if we've exceeded retry limit with invalid IP
if (_getIpTryCount > 32 && isInvalidIp)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load server data - can't fetch ip address!");
return;
}
var ipAddress = ConVar.Find("ip")?.StringValue;
// Optimization: Cache ConVar lookups
var ipConVar = ConVar.Find("ip");
var ipAddress = ipConVar?.StringValue;
// Use Helper IP if ConVar IP is invalid
if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))
{
ipAddress = Helper.GetServerIp();
ipAddress = serverIp;
if (_getIpTryCount <= 32 && (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0")))
// Retry if still invalid and under retry limit
if (_getIpTryCount <= 32 && isInvalidIp)
{
_getIpTryCount++;
LoadServerData();
return;
}
}
var address = $"{ipAddress}:{ConVar.Find("hostport")?.GetPrimitiveValue<int>()}";
var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var rconPassword = ConVar.Find("rcon_password")?.StringValue ?? "";
// Optimization: Cache remaining ConVar lookups
var hostportConVar = ConVar.Find("hostport");
var hostnameConVar = ConVar.Find("hostname");
var rconPasswordConVar = ConVar.Find("rcon_password");
var address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue<int>()}";
var hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var rconPassword = rconPasswordConVar?.StringValue ?? "";
CS2_SimpleAdmin.IpAddress = address;
Task.Run(async () =>
@@ -118,6 +132,8 @@ public class ServerManager
}
}
});
CS2_SimpleAdmin.SimpleAdminApi?.OnSimpleAdminReadyEvent();
});
}
}

View File

@@ -7,6 +7,11 @@ namespace CS2_SimpleAdmin.Menus;
public static class AdminMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
MenuManager.Instance.OpenMainMenu(admin);
}
public static IMenu? CreateMenu(string title, Action<CCSPlayerController>? backAction = null)
{
return Helper.CreateMenu(title, backAction);
@@ -27,44 +32,44 @@ public static class AdminMenu
// }
}
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false)
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
);
return;
}
var menu = CreateMenu(localizer?["sa_title"] ?? "SimpleAdmin");
List<ChatMenuOptionData> options =
[
new(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)),
new(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)),
new(localizer?["sa_menu_fun_commands"] ?? "Fun Commands", () => FunActionsMenu.OpenMenu(admin)),
];
var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
if (customCommands.Count > 0)
{
options.Add(new ChatMenuOptionData(localizer?["sa_menu_custom_commands"] ?? "Custom Commands", () => CustomCommandsMenu.OpenMenu(admin)));
}
if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"))
options.Add(new ChatMenuOptionData(localizer?["sa_menu_admins_manage"] ?? "Admins Manage", () => ManageAdminsMenu.OpenMenu(admin)));
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
}
if (menu != null) OpenMenu(admin, menu);
}
// public static void OpenMenu(CCSPlayerController admin)
// {
// if (admin.IsValid == false)
// return;
//
// var localizer = CS2_SimpleAdmin._localizer;
// if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false)
// {
// admin.PrintToChat(localizer?["sa_prefix"] ??
// "[SimpleAdmin] " +
// (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
// );
// return;
// }
//
// var menu = CreateMenu(localizer?["sa_title"] ?? "SimpleAdmin");
// List<ChatMenuOptionData> options =
// [
// new(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)),
// new(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)),
// new(localizer?["sa_menu_fun_commands"] ?? "Fun Commands", () => FunActionsMenu.OpenMenu(admin)),
// ];
//
// var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
// if (customCommands.Count > 0)
// {
// options.Add(new ChatMenuOptionData(localizer?["sa_menu_custom_commands"] ?? "Custom Commands", () => CustomCommandsMenu.OpenMenu(admin)));
// }
//
// if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"))
// options.Add(new ChatMenuOptionData(localizer?["sa_menu_admins_manage"] ?? "Admins Manage", () => ManageAdminsMenu.OpenMenu(admin)));
//
// foreach (var menuOptionData in options)
// {
// var menuName = menuOptionData.Name;
// menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
// }
//
// if (menu != null) OpenMenu(admin, menu);
// }
}

View File

@@ -0,0 +1,602 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using CS2_SimpleAdminApi;
namespace CS2_SimpleAdmin.Menus;
public abstract class BasicMenu
{
/// <summary>
/// Initializes all menus in the system by registering them with the MenuManager.
/// </summary>
public static void Initialize()
{
var manager = MenuManager.Instance;
// Players category menus
manager.RegisterMenu("players", "slap", "Slap Player", CreateSlapMenu, "@css/slay");
manager.RegisterMenu("players", "slay", "Slay Player", CreateSlayMenu, "@css/slay");
manager.RegisterMenu("players", "kick", "Kick Player", CreateKickMenu, "@css/kick");
manager.RegisterMenu("players", "warn", "Warn Player", CreateWarnMenu, "@css/kick");
manager.RegisterMenu("players", "ban", "Ban Player", CreateBanMenu, "@css/ban");
manager.RegisterMenu("players", "gag", "Gag Player", CreateGagMenu, "@css/chat");
manager.RegisterMenu("players", "mute", "Mute Player", CreateMuteMenu, "@css/chat");
manager.RegisterMenu("players", "silence", "Silence Player", CreateSilenceMenu, "@css/chat");
manager.RegisterMenu("players", "team", "Force Team", CreateForceTeamMenu, "@css/kick");
// Server category menus
manager.RegisterMenu("server", "plugins", "Manage Plugins", CreatePluginsMenu, "@css/root");
manager.RegisterMenu("server", "changemap", "Change Map", CreateChangeMapMenu, "@css/changemap");
manager.RegisterMenu("server", "restart", "Restart Game", CreateRestartGameMenu, "@css/generic");
manager.RegisterMenu("server", "custom", "Custom Commands", CreateCustomCommandsMenu, "@css/generic");
// Admin category menus
manager.RegisterMenu("admin", "add", "Add Admin", CreateAddAdminMenu, "@css/root");
manager.RegisterMenu("admin", "remove", "Remove Admin", CreateRemoveAdminMenu, "@css/root");
manager.RegisterMenu("admin", "reload", "Reload Admins", CreateReloadAdminsMenu, "@css/root");
}
/// <summary>
/// Creates menu for slapping players with selectable damage amounts.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the slap menu.</returns>
private static MenuBuilder CreateSlapMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var slapMenu = new MenuBuilder(localizer?["sa_slap"] ?? "Slap Player");
var players = Helper.GetValidPlayers().Where(admin.CanTarget);
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
slapMenu.AddSubMenu(playerName, () => CreateSlapDamageMenu(admin, player));
}
return slapMenu.WithBackButton();
}
/// <summary>
/// Creates damage selection submenu for slapping a specific player.
/// </summary>
/// <param name="admin">The admin player executing the slap.</param>
/// <param name="target">The target player to be slapped.</param>
/// <returns>A MenuBuilder instance for the slap damage menu.</returns>
private static MenuBuilder CreateSlapDamageMenu(CCSPlayerController admin, CCSPlayerController target)
{
var slapDamageMenu = new MenuBuilder($"Slap: {target.PlayerName}");
var damages = new[] { 0, 1, 5, 10, 50, 100 };
foreach (var damage in damages)
{
slapDamageMenu.AddOption($"{damage} HP", _ =>
{
if (target.IsValid)
{
CS2_SimpleAdmin.Slap(admin, target, damage);
// Keep menu open for consecutive slaps
CreateSlapDamageMenu(admin, target).OpenMenu(admin);
}
});
}
return slapDamageMenu.WithBackButton();
}
/// <summary>
/// Creates menu for slaying (killing) players.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the slay menu.</returns>
private static MenuBuilder CreateSlayMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var slayMenu = new MenuBuilder(localizer?["sa_slay"] ?? "Slay Player");
var players = Helper.GetValidPlayers().Where(admin.CanTarget);
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
slayMenu.AddOption(playerName, _ =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Slay(admin, player);
}
});
}
return slayMenu.WithBackButton();
}
/// <summary>
/// Creates menu for kicking players with reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the kick menu.</returns>
private static MenuBuilder CreateKickMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var kickMenu = new MenuBuilder(localizer?["sa_kick"] ?? "Kick Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, player, "Kick", PenaltyType.Kick,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Kick(admin, player, reason, admin.PlayerName);
}
}));
}
return kickMenu.WithBackButton();
}
/// <summary>
/// Creates menu for warning players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the warn menu.</returns>
private static MenuBuilder CreateWarnMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var warnMenu = new MenuBuilder(localizer?["sa_warn"] ?? "Warn Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
warnMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Warn",
(_, _, duration) => CreateReasonMenu(admin, player, "Warn", PenaltyType.Warn,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason, admin.PlayerName);
}
})));
}
return warnMenu.WithBackButton();
}
/// <summary>
/// Creates menu for banning players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the ban menu.</returns>
private static MenuBuilder CreateBanMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var banMenu = new MenuBuilder(localizer?["sa_ban"] ?? "Ban Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
banMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Ban",
(_, _, duration) => CreateReasonMenu(admin, player, "Ban", PenaltyType.Ban,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason, admin.PlayerName);
}
})));
}
return banMenu.WithBackButton();
}
/// <summary>
/// Creates menu for gagging (text chat muting) players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the gag menu.</returns>
private static MenuBuilder CreateGagMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var gagMenu = new MenuBuilder(localizer?["sa_gag"] ?? "Gag Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
gagMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Gag",
(_, _, duration) => CreateReasonMenu(admin, player, "Gag", PenaltyType.Gag,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
}
})));
}
return gagMenu.WithBackButton();
}
/// <summary>
/// Creates menu for muting (voice chat muting) players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the mute menu.</returns>
private static MenuBuilder CreateMuteMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var muteMenu = new MenuBuilder(localizer?["sa_mute"] ?? "Mute Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
muteMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Mute",
(_, _, duration) => CreateReasonMenu(admin, player, "Mute", PenaltyType.Mute,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
}
})));
}
return muteMenu.WithBackButton();
}
/// <summary>
/// Creates menu for silencing (both text and voice chat muting) players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the silence menu.</returns>
private static MenuBuilder CreateSilenceMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var silenceMenu = new MenuBuilder(localizer?["sa_silence"] ?? "Silence Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
silenceMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Silence",
(_, _, duration) => CreateReasonMenu(admin, player, "Silence", PenaltyType.Silence,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
}
})));
}
return silenceMenu.WithBackButton();
}
/// <summary>
/// Creates menu for forcing players to switch teams.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the force team menu.</returns>
private static MenuBuilder CreateForceTeamMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var teamMenu = new MenuBuilder(localizer?["sa_team_force"] ?? "Force Team");
var players = Helper.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
teamMenu.AddSubMenu(playerName, () => CreateTeamSelectionMenu(admin, player));
}
return teamMenu.WithBackButton();
}
/// <summary>
/// Creates team selection submenu for forcing a specific player to a team.
/// </summary>
/// <param name="admin">The admin player executing the team change.</param>
/// <param name="target">The target player to be moved.</param>
/// <returns>A MenuBuilder instance for the team selection menu.</returns>
private static MenuBuilder CreateTeamSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var localizer = CS2_SimpleAdmin._localizer;
var teamSelectionMenu = new MenuBuilder($"Force Team: {target.PlayerName}");
var teams = new[]
{
(localizer?["sa_team_ct"] ?? "CT", "ct", CsTeam.CounterTerrorist),
(localizer?["sa_team_t"] ?? "T", "t", CsTeam.Terrorist),
(localizer?["sa_team_swap"] ?? "Swap", "swap", CsTeam.Spectator),
(localizer?["sa_team_spec"] ?? "Spec", "spec", CsTeam.Spectator)
};
foreach (var (name, teamName, teamNum) in teams)
{
teamSelectionMenu.AddOption(name, _ =>
{
if (target.IsValid)
{
CS2_SimpleAdmin.ChangeTeam(admin, target, teamName, teamNum, true);
}
});
}
return teamSelectionMenu.WithBackButton();
}
/// <summary>
/// Creates menu for managing server plugins.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the plugins menu.</returns>
private static MenuBuilder CreatePluginsMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var pluginsMenu = new MenuBuilder(localizer?["sa_menu_pluginsmanager_title"] ?? "Manage Plugins");
pluginsMenu.AddOption("Open Plugins Manager", _ =>
{
admin.ExecuteClientCommandFromServer("css_pluginsmanager");
});
return pluginsMenu.WithBackButton();
}
/// <summary>
/// Creates menu for changing the current map (includes default and workshop maps).
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the change map menu.</returns>
private static MenuBuilder CreateChangeMapMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var mapMenu = new MenuBuilder(localizer?["sa_changemap"] ?? "Change Map");
// Add default maps
var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps;
foreach (var map in maps)
{
mapMenu.AddOption(map, _ =>
{
CS2_SimpleAdmin.Instance.ChangeMap(admin, map);
});
}
// Add workshop maps
var wsMaps = CS2_SimpleAdmin.Instance.Config.WorkshopMaps;
foreach (var wsMap in wsMaps)
{
mapMenu.AddOption($"{wsMap.Key} (WS)", _ =>
{
CS2_SimpleAdmin.Instance.ChangeWorkshopMap(admin, wsMap.Value?.ToString() ?? wsMap.Key);
});
}
return mapMenu.WithBackButton();
}
/// <summary>
/// Creates menu for restarting the current game/round.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the restart game menu.</returns>
private static MenuBuilder CreateRestartGameMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var restartMenu = new MenuBuilder(localizer?["sa_restart_game"] ?? "Restart Game");
restartMenu.AddOption("Restart Round", _ =>
{
CS2_SimpleAdmin.RestartGame(admin);
});
return restartMenu.WithBackButton();
}
/// <summary>
/// Creates menu for executing custom server commands defined in configuration.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the custom commands menu.</returns>
private static MenuBuilder CreateCustomCommandsMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var customMenu = new MenuBuilder(localizer?["sa_menu_custom_commands"] ?? "Custom Commands");
var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
foreach (var customCommand in customCommands)
{
if (string.IsNullOrEmpty(customCommand.DisplayName) || string.IsNullOrEmpty(customCommand.Command))
continue;
var steamId = new SteamID(admin.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, customCommand.Flag))
continue;
customMenu.AddOption(customCommand.DisplayName, _ =>
{
Helper.TryLogCommandOnDiscord(admin, customCommand.Command);
if (customCommand.ExecuteOnClient)
admin.ExecuteClientCommandFromServer(customCommand.Command);
else
Server.ExecuteCommand(customCommand.Command);
});
}
return customMenu.WithBackButton();
}
/// <summary>
/// Creates menu for adding admin privileges to players.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the add admin menu.</returns>
private static MenuBuilder CreateAddAdminMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var addAdminMenu = new MenuBuilder(localizer?["sa_admin_add"] ?? "Add Admin");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
addAdminMenu.AddSubMenu(playerName, () => CreateAdminFlagsMenu(admin, player));
}
return addAdminMenu.WithBackButton();
}
/// <summary>
/// Creates admin flags selection submenu for granting specific permissions to a player.
/// </summary>
/// <param name="admin">The admin player granting permissions.</param>
/// <param name="target">The target player to receive admin privileges.</param>
/// <returns>A MenuBuilder instance for the admin flags menu.</returns>
private static MenuBuilder CreateAdminFlagsMenu(CCSPlayerController admin, CCSPlayerController target)
{
var flagsMenu = new MenuBuilder($"Add Admin: {target.PlayerName}");
foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags)
{
var hasFlag = AdminManager.PlayerHasPermissions(target, adminFlag.Flag);
flagsMenu.AddOption(adminFlag.Name, _ =>
{
if (target.IsValid)
{
CS2_SimpleAdmin.AddAdmin(admin, target.SteamID.ToString(), target.PlayerName, adminFlag.Flag, 10);
}
}, hasFlag); // Disabled if player already has this flag
}
return flagsMenu.WithBackButton();
}
/// <summary>
/// Creates menu for removing admin privileges from players.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the remove admin menu.</returns>
private static MenuBuilder CreateRemoveAdminMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var removeAdminMenu = new MenuBuilder(localizer?["sa_admin_remove"] ?? "Remove Admin");
var adminPlayers = Helper.GetValidPlayers().Where(p =>
AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0 &&
p != admin &&
admin.CanTarget(p));
foreach (var player in adminPlayers)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
removeAdminMenu.AddOption(playerName, _ =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.RemoveAdmin(admin, player.SteamID.ToString());
}
});
}
return removeAdminMenu.WithBackButton();
}
/// <summary>
/// Creates menu for reloading admin list from database.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the reload admins menu.</returns>
private static MenuBuilder CreateReloadAdminsMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var reloadMenu = new MenuBuilder(localizer?["sa_admin_reload"] ?? "Reload Admins");
reloadMenu.AddOption("Reload Admins", _ =>
{
CS2_SimpleAdmin.Instance.ReloadAdmins(admin);
});
return reloadMenu.WithBackButton();
}
/// <summary>
/// Creates duration selection submenu for time-based penalties (ban, mute, gag, etc.).
/// </summary>
/// <param name="admin">The admin player selecting duration.</param>
/// <param name="player">The target player for the penalty.</param>
/// <param name="actionName">The name of the penalty action.</param>
/// <param name="onSelectAction">Callback action executed when duration is selected.</param>
/// <returns>A MenuBuilder instance for the duration menu.</returns>
private static MenuBuilder CreateDurationMenu(CCSPlayerController admin, CCSPlayerController player, string actionName,
Action<CCSPlayerController, CCSPlayerController, int> onSelectAction)
{
var durationMenu = new MenuBuilder($"{actionName} Duration: {player.PlayerName}");
foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations)
{
durationMenu.AddOption(durationItem.Name, _ =>
{
onSelectAction(admin, player, durationItem.Duration);
});
}
return durationMenu.WithBackButton();
}
/// <summary>
/// Creates reason selection submenu for penalties with predefined reasons from configuration.
/// </summary>
/// <param name="admin">The admin player selecting reason.</param>
/// <param name="player">The target player for the penalty.</param>
/// <param name="actionName">The name of the penalty action.</param>
/// <param name="penaltyType">The type of penalty to determine which reason list to use.</param>
/// <param name="onSelectAction">Callback action executed when reason is selected.</param>
/// <returns>A MenuBuilder instance for the reason menu.</returns>
private static MenuBuilder CreateReasonMenu(CCSPlayerController admin, CCSPlayerController player, string actionName,
PenaltyType penaltyType, Action<CCSPlayerController, CCSPlayerController, string> onSelectAction)
{
var reasonMenu = new MenuBuilder($"{actionName} Reason: {player.PlayerName}");
var reasons = penaltyType switch
{
PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons,
PenaltyType.Kick => CS2_SimpleAdmin.Instance.Config.MenuConfigs.KickReasons,
PenaltyType.Mute => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons,
PenaltyType.Warn => CS2_SimpleAdmin.Instance.Config.MenuConfigs.WarnReasons,
PenaltyType.Gag or PenaltyType.Silence => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons,
_ => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons
};
foreach (var reason in reasons)
{
reasonMenu.AddOption(reason, _ =>
{
onSelectAction(admin, player, reason);
});
}
return reasonMenu.WithBackButton();
}
}

View File

@@ -9,11 +9,11 @@ public static class CustomCommandsMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false)
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +

View File

@@ -8,7 +8,6 @@ public static class DurationMenu
public static void OpenMenu(CCSPlayerController admin, string menuName, CCSPlayerController player, Action<CCSPlayerController, CCSPlayerController, int> onSelectAction)
{
var menu = AdminMenu.CreateMenu(menuName);
foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations)
{
menu?.AddMenuOption(durationItem.Name, (_, _) => { onSelectAction(admin, player, durationItem.Duration); });
@@ -20,7 +19,6 @@ public static class DurationMenu
public static void OpenMenu(CCSPlayerController admin, string menuName, DisconnectedPlayer player, Action<CCSPlayerController, DisconnectedPlayer, int> onSelectAction)
{
var menu = AdminMenu.CreateMenu(menuName);
foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations)
{
menu?.AddMenuOption(durationItem.Name, (_, _) => { onSelectAction(admin, player, durationItem.Duration); });

View File

@@ -1,267 +1,267 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
namespace CS2_SimpleAdmin.Menus;
public static class FunActionsMenu
{
private static Dictionary<int, CsItem>? _weaponsCache;
private static Dictionary<int, CsItem> GetWeaponsCache
{
get
{
if (_weaponsCache != null) return _weaponsCache;
var weaponsArray = Enum.GetValues(typeof(CsItem));
// avoid duplicates in the menu
_weaponsCache = new Dictionary<int, CsItem>();
foreach (CsItem item in weaponsArray)
{
if (item == CsItem.Tablet)
continue;
_weaponsCache[(int)item] = item;
}
return _weaponsCache;
}
}
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false)
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
);
return;
}
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_fun_commands"] ?? "Fun Commands");
List<ChatMenuOptionData> options = [];
//var hasCheats = AdminManager.PlayerHasPermissions(admin, "@css/cheats");
//var hasSlay = AdminManager.PlayerHasPermissions(admin, "@css/slay");
// options added in order
if (AdminManager.CommandIsOverriden("css_god")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_god"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
options.Add(new ChatMenuOptionData(localizer?["sa_godmode"] ?? "God Mode", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_godmode"] ?? "God Mode", GodMode)));
if (AdminManager.CommandIsOverriden("css_noclip")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_noclip"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
options.Add(new ChatMenuOptionData(localizer?["sa_noclip"] ?? "No Clip", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_noclip"] ?? "No Clip", NoClip)));
if (AdminManager.CommandIsOverriden("css_respawn")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_respawn"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
options.Add(new ChatMenuOptionData(localizer?["sa_respawn"] ?? "Respawn", () => PlayersMenu.OpenDeadMenu(admin, localizer?["sa_respawn"] ?? "Respawn", Respawn)));
if (AdminManager.CommandIsOverriden("css_give")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_give"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
options.Add(new ChatMenuOptionData(localizer?["sa_give_weapon"] ?? "Give Weapon", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_give_weapon"] ?? "Give Weapon", GiveWeaponMenu)));
if (AdminManager.CommandIsOverriden("css_strip")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_strip"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_strip_weapons"] ?? "Strip Weapons", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_strip_weapons"] ?? "Strip Weapons", StripWeapons)));
if (AdminManager.CommandIsOverriden("css_freeze")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_freeze"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_freeze"] ?? "Freeze", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_freeze"] ?? "Freeze", Freeze)));
if (AdminManager.CommandIsOverriden("css_hp")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_hp"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_set_hp"] ?? "Set Hp", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_hp"] ?? "Set Hp", SetHpMenu)));
if (AdminManager.CommandIsOverriden("css_speed")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_speed"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_set_speed"] ?? "Set Speed", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_speed"] ?? "Set Speed", SetSpeedMenu)));
if (AdminManager.CommandIsOverriden("css_gravity")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gravity"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_set_gravity"] ?? "Set Gravity", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_gravity"] ?? "Set Gravity", SetGravityMenu)));
if (AdminManager.CommandIsOverriden("css_money")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_money"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_set_money"] ?? "Set Money", () => PlayersMenu.OpenMenu(admin, localizer?["sa_set_money"] ?? "Set Money", SetMoneyMenu)));
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void GodMode(CCSPlayerController admin, CCSPlayerController player)
{
CS2_SimpleAdmin.God(admin, player);
}
private static void NoClip(CCSPlayerController admin, CCSPlayerController player)
{
CS2_SimpleAdmin.NoClip(admin, player);
}
private static void Respawn(CCSPlayerController? admin, CCSPlayerController player)
{
CS2_SimpleAdmin.Respawn(admin, player);
}
private static void GiveWeaponMenu(CCSPlayerController admin, CCSPlayerController player)
{
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_give_weapon"] ?? "Give Weapon"}: {player.PlayerName}");
foreach (var weapon in GetWeaponsCache)
{
menu?.AddMenuOption(weapon.Value.ToString(), (_, _) => { GiveWeapon(admin, player, weapon.Value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void GiveWeapon(CCSPlayerController admin, CCSPlayerController player, CsItem weaponValue)
{
CS2_SimpleAdmin.GiveWeapon(admin, player, weaponValue);
}
private static void StripWeapons(CCSPlayerController admin, CCSPlayerController player)
{
CS2_SimpleAdmin.StripWeapons(admin, player);
}
private static void Freeze(CCSPlayerController admin, CCSPlayerController player)
{
if (!(player.PlayerPawn.Value?.IsValid ?? false))
return;
if (player.PlayerPawn.Value.MoveType != MoveType_t.MOVETYPE_INVALID)
CS2_SimpleAdmin.Freeze(admin, player, -1);
else
CS2_SimpleAdmin.Unfreeze(admin, player);
}
private static void SetHpMenu(CCSPlayerController admin, CCSPlayerController player)
{
var hpArray = new[]
{
new Tuple<string, int>("1", 1),
new Tuple<string, int>("10", 10),
new Tuple<string, int>("25", 25),
new Tuple<string, int>("50", 50),
new Tuple<string, int>("100", 100),
new Tuple<string, int>("200", 200),
new Tuple<string, int>("500", 500),
new Tuple<string, int>("999", 999)
};
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_hp"] ?? "Set Hp"}: {player.PlayerName}");
foreach (var (optionName, value) in hpArray)
{
menu?.AddMenuOption(optionName, (_, _) => { SetHp(admin, player, value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SetHp(CCSPlayerController admin, CCSPlayerController player, int hp)
{
CS2_SimpleAdmin.SetHp(admin, player, hp);
}
private static void SetSpeedMenu(CCSPlayerController admin, CCSPlayerController player)
{
var speedArray = new[]
{
new Tuple<string, float>("0.1", .1f),
new Tuple<string, float>("0.25", .25f),
new Tuple<string, float>("0.5", .5f),
new Tuple<string, float>("0.75", .75f),
new Tuple<string, float>("1", 1),
new Tuple<string, float>("2", 2),
new Tuple<string, float>("3", 3),
new Tuple<string, float>("4", 4)
};
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_speed"] ?? "Set Speed"}: {player.PlayerName}");
foreach (var (optionName, value) in speedArray)
{
menu?.AddMenuOption(optionName, (_, _) => { SetSpeed(admin, player, value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SetSpeed(CCSPlayerController admin, CCSPlayerController player, float speed)
{
CS2_SimpleAdmin.SetSpeed(admin, player, speed);
}
private static void SetGravityMenu(CCSPlayerController admin, CCSPlayerController player)
{
var gravityArray = new[]
{
new Tuple<string, float>("0.1", .1f),
new Tuple<string, float>("0.25", .25f),
new Tuple<string, float>("0.5", .5f),
new Tuple<string, float>("0.75", .75f),
new Tuple<string, float>("1", 1),
new Tuple<string, float>("2", 2)
};
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_gravity"] ?? "Set Gravity"}: {player.PlayerName}");
foreach (var (optionName, value) in gravityArray)
{
menu?.AddMenuOption(optionName, (_, _) => { SetGravity(admin, player, value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SetGravity(CCSPlayerController admin, CCSPlayerController player, float gravity)
{
CS2_SimpleAdmin.SetGravity(admin, player, gravity);
}
private static void SetMoneyMenu(CCSPlayerController admin, CCSPlayerController player)
{
var moneyArray = new[]
{
new Tuple<string, int>("$0", 0),
new Tuple<string, int>("$1000", 1000),
new Tuple<string, int>("$2500", 2500),
new Tuple<string, int>("$5000", 5000),
new Tuple<string, int>("$10000", 10000),
new Tuple<string, int>("$16000", 16000)
};
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_money"] ?? "Set Money"}: {player.PlayerName}");
foreach (var (optionName, value) in moneyArray)
{
menu?.AddMenuOption(optionName, (_, _) => { SetMoney(admin, player, value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SetMoney(CCSPlayerController admin, CCSPlayerController player, int money)
{
CS2_SimpleAdmin.SetMoney(admin, player, money);
}
}
// using CounterStrikeSharp.API.Core;
// using CounterStrikeSharp.API.Modules.Admin;
// using CounterStrikeSharp.API.Modules.Entities;
// using CounterStrikeSharp.API.Modules.Entities.Constants;
//
// namespace CS2_SimpleAdmin.Menus;
//
// public static class FunActionsMenu
// {
// private static Dictionary<int, CsItem>? _weaponsCache;
//
// private static Dictionary<int, CsItem> GetWeaponsCache
// {
// get
// {
// if (_weaponsCache != null) return _weaponsCache;
//
// var weaponsArray = Enum.GetValues(typeof(CsItem));
//
// // avoid duplicates in the menu
// _weaponsCache = new Dictionary<int, CsItem>();
// foreach (CsItem item in weaponsArray)
// {
// if (item == CsItem.Tablet)
// continue;
//
// _weaponsCache[(int)item] = item;
// }
//
// return _weaponsCache;
// }
// }
//
// public static void OpenMenu(CCSPlayerController admin)
// {
// if (!admin.IsValid)
// return;
//
// var localizer = CS2_SimpleAdmin._localizer;
// if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
// {
// admin.PrintToChat(localizer?["sa_prefix"] ??
// "[SimpleAdmin] " +
// (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
// );
// return;
// }
//
// var menu = AdminMenu.CreateMenu(localizer?["sa_menu_fun_commands"] ?? "Fun Commands");
// List<ChatMenuOptionData> options = [];
//
// //var hasCheats = AdminManager.PlayerHasPermissions(admin, "@css/cheats");
// //var hasSlay = AdminManager.PlayerHasPermissions(admin, "@css/slay");
//
// // options added in order
//
// if (AdminManager.CommandIsOverriden("css_god")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_god"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
// options.Add(new ChatMenuOptionData(localizer?["sa_godmode"] ?? "God Mode", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_godmode"] ?? "God Mode", GodMode)));
// if (AdminManager.CommandIsOverriden("css_noclip")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_noclip"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
// options.Add(new ChatMenuOptionData(localizer?["sa_noclip"] ?? "No Clip", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_noclip"] ?? "No Clip", NoClip)));
// if (AdminManager.CommandIsOverriden("css_respawn")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_respawn"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
// options.Add(new ChatMenuOptionData(localizer?["sa_respawn"] ?? "Respawn", () => PlayersMenu.OpenDeadMenu(admin, localizer?["sa_respawn"] ?? "Respawn", Respawn)));
// if (AdminManager.CommandIsOverriden("css_give")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_give"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
// options.Add(new ChatMenuOptionData(localizer?["sa_give_weapon"] ?? "Give Weapon", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_give_weapon"] ?? "Give Weapon", GiveWeaponMenu)));
//
// if (AdminManager.CommandIsOverriden("css_strip")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_strip"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_strip_weapons"] ?? "Strip Weapons", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_strip_weapons"] ?? "Strip Weapons", StripWeapons)));
// if (AdminManager.CommandIsOverriden("css_freeze")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_freeze"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_freeze"] ?? "Freeze", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_freeze"] ?? "Freeze", Freeze)));
// if (AdminManager.CommandIsOverriden("css_hp")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_hp"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_set_hp"] ?? "Set Hp", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_hp"] ?? "Set Hp", SetHpMenu)));
// if (AdminManager.CommandIsOverriden("css_speed")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_speed"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_set_speed"] ?? "Set Speed", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_speed"] ?? "Set Speed", SetSpeedMenu)));
// if (AdminManager.CommandIsOverriden("css_gravity")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gravity"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_set_gravity"] ?? "Set Gravity", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_gravity"] ?? "Set Gravity", SetGravityMenu)));
// if (AdminManager.CommandIsOverriden("css_money")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_money"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_set_money"] ?? "Set Money", () => PlayersMenu.OpenMenu(admin, localizer?["sa_set_money"] ?? "Set Money", SetMoneyMenu)));
//
// foreach (var menuOptionData in options)
// {
// var menuName = menuOptionData.Name;
// menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action(); }, menuOptionData.Disabled);
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void GodMode(CCSPlayerController admin, CCSPlayerController player)
// {
// CS2_SimpleAdmin.God(admin, player);
// }
//
// private static void NoClip(CCSPlayerController admin, CCSPlayerController player)
// {
// CS2_SimpleAdmin.NoClip(admin, player);
// }
//
// private static void Respawn(CCSPlayerController? admin, CCSPlayerController player)
// {
// CS2_SimpleAdmin.Respawn(admin, player);
// }
//
// private static void GiveWeaponMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_give_weapon"] ?? "Give Weapon"}: {player.PlayerName}");
//
// foreach (var weapon in GetWeaponsCache)
// {
// menu?.AddMenuOption(weapon.Value.ToString(), (_, _) => { GiveWeapon(admin, player, weapon.Value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void GiveWeapon(CCSPlayerController admin, CCSPlayerController player, CsItem weaponValue)
// {
// CS2_SimpleAdmin.GiveWeapon(admin, player, weaponValue);
// }
//
// private static void StripWeapons(CCSPlayerController admin, CCSPlayerController player)
// {
// CS2_SimpleAdmin.StripWeapons(admin, player);
// }
//
// private static void Freeze(CCSPlayerController admin, CCSPlayerController player)
// {
// if (!(player.PlayerPawn.Value?.IsValid ?? false))
// return;
//
// if (player.PlayerPawn.Value.MoveType != MoveType_t.MOVETYPE_INVALID)
// CS2_SimpleAdmin.Freeze(admin, player, -1);
// else
// CS2_SimpleAdmin.Unfreeze(admin, player);
// }
//
// private static void SetHpMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var hpArray = new[]
// {
// new Tuple<string, int>("1", 1),
// new Tuple<string, int>("10", 10),
// new Tuple<string, int>("25", 25),
// new Tuple<string, int>("50", 50),
// new Tuple<string, int>("100", 100),
// new Tuple<string, int>("200", 200),
// new Tuple<string, int>("500", 500),
// new Tuple<string, int>("999", 999)
// };
//
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_hp"] ?? "Set Hp"}: {player.PlayerName}");
//
// foreach (var (optionName, value) in hpArray)
// {
// menu?.AddMenuOption(optionName, (_, _) => { SetHp(admin, player, value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void SetHp(CCSPlayerController admin, CCSPlayerController player, int hp)
// {
// CS2_SimpleAdmin.SetHp(admin, player, hp);
// }
//
// private static void SetSpeedMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var speedArray = new[]
// {
// new Tuple<string, float>("0.1", .1f),
// new Tuple<string, float>("0.25", .25f),
// new Tuple<string, float>("0.5", .5f),
// new Tuple<string, float>("0.75", .75f),
// new Tuple<string, float>("1", 1),
// new Tuple<string, float>("2", 2),
// new Tuple<string, float>("3", 3),
// new Tuple<string, float>("4", 4)
// };
//
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_speed"] ?? "Set Speed"}: {player.PlayerName}");
//
// foreach (var (optionName, value) in speedArray)
// {
// menu?.AddMenuOption(optionName, (_, _) => { SetSpeed(admin, player, value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void SetSpeed(CCSPlayerController admin, CCSPlayerController player, float speed)
// {
// CS2_SimpleAdmin.SetSpeed(admin, player, speed);
// }
//
// private static void SetGravityMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var gravityArray = new[]
// {
// new Tuple<string, float>("0.1", .1f),
// new Tuple<string, float>("0.25", .25f),
// new Tuple<string, float>("0.5", .5f),
// new Tuple<string, float>("0.75", .75f),
// new Tuple<string, float>("1", 1),
// new Tuple<string, float>("2", 2)
// };
//
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_gravity"] ?? "Set Gravity"}: {player.PlayerName}");
//
// foreach (var (optionName, value) in gravityArray)
// {
// menu?.AddMenuOption(optionName, (_, _) => { SetGravity(admin, player, value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void SetGravity(CCSPlayerController admin, CCSPlayerController player, float gravity)
// {
// CS2_SimpleAdmin.SetGravity(admin, player, gravity);
// }
//
// private static void SetMoneyMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var moneyArray = new[]
// {
// new Tuple<string, int>("$0", 0),
// new Tuple<string, int>("$1000", 1000),
// new Tuple<string, int>("$2500", 2500),
// new Tuple<string, int>("$5000", 5000),
// new Tuple<string, int>("$10000", 10000),
// new Tuple<string, int>("$16000", 16000)
// };
//
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_money"] ?? "Set Money"}: {player.PlayerName}");
//
// foreach (var (optionName, value) in moneyArray)
// {
// menu?.AddMenuOption(optionName, (_, _) => { SetMoney(admin, player, value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void SetMoney(CCSPlayerController admin, CCSPlayerController player, int money)
// {
// CS2_SimpleAdmin.SetMoney(admin, player, money);
// }
// }

View File

@@ -8,11 +8,11 @@ public static class ManageAdminsMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root") == false)
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +

View File

@@ -10,11 +10,11 @@ public static class ManagePlayersMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false)
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +

View File

@@ -8,11 +8,11 @@ public static class ManageServerMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false)
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +

View File

@@ -0,0 +1,170 @@
using CounterStrikeSharp.API.Core;
namespace CS2_SimpleAdmin.Menus;
public class MenuBuilder(string title)
{
private readonly List<MenuOption> _options = [];
private MenuBuilder? _parentMenu;
private Action<CCSPlayerController>? _backAction;
private Action<CCSPlayerController>? _resetAction;
/// <summary>
/// Adds a menu option with an action.
/// </summary>
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null)
{
_options.Add(new MenuOption
{
Name = name,
Action = action,
Disabled = disabled,
Permission = permission
});
return this;
}
/// <summary>
/// Adds a menu option that opens a submenu.
/// </summary>
public MenuBuilder AddSubMenu(string name, Func<MenuBuilder> subMenuFactory, bool disabled = false, string? permission = null)
{
_options.Add(new MenuOption
{
Name = name,
Action = player =>
{
var subMenu = subMenuFactory();
subMenu.SetParent(this);
// Automatically add back button to submenu
subMenu.WithBackButton();
subMenu.OpenMenu(player);
},
Disabled = disabled,
Permission = permission
});
return this;
}
/// <summary>
/// Adds a menu option that opens a submenu (with player parameter in factory).
/// </summary>
public MenuBuilder AddSubMenu(string name, Func<CCSPlayerController, MenuBuilder> subMenuFactory, bool disabled = false, string? permission = null)
{
_options.Add(new MenuOption
{
Name = name,
Action = player =>
{
var subMenu = subMenuFactory(player);
subMenu.SetParent(this);
// Automatically add back button to submenu
subMenu.WithBackButton();
subMenu.OpenMenu(player);
},
Disabled = disabled,
Permission = permission
});
return this;
}
/// <summary>
/// Adds a back button to return to the previous menu.
/// </summary>
public MenuBuilder WithBackButton()
{
if (_parentMenu != null)
{
_backAction = player => _parentMenu.OpenMenu(player);
// Add back option at the end of menu
// AddOption(backButtonText, _backAction);
}
return this;
}
/// <summary>
/// Sets the parent menu (for navigation).
/// </summary>
private void SetParent(MenuBuilder parent)
{
_parentMenu = parent;
_backAction = player => _parentMenu.OpenMenu(player);
}
/// <summary>
/// Opens the menu for a player.
/// </summary>
/// <param name="player">The player to open the menu for.</param>
public void OpenMenu(CCSPlayerController player)
{
if (!player.IsValid) return;
// Use MenuManager dependency
var menu = Helper.CreateMenu(title, _backAction);
if (menu == null) return;
foreach (var option in _options)
{
// Check permissions if required
if (!string.IsNullOrEmpty(option.Permission))
{
var steamId = new CounterStrikeSharp.API.Modules.Entities.SteamID(player.SteamID);
if (!CounterStrikeSharp.API.Modules.Admin.AdminManager.PlayerHasPermissions(steamId, option.Permission))
{
continue; // Skip option if player doesn't have permission
}
}
menu.AddMenuOption(option.Name, (menuPlayer, menuOption) =>
{
option.Action?.Invoke(menuPlayer);
}, option.Disabled);
}
menu.Open(player);
}
/// <summary>
/// Clears all menu options.
/// </summary>
/// <returns>This MenuBuilder instance for chaining.</returns>
public MenuBuilder Clear()
{
_options.Clear();
return this;
}
/// <summary>
/// Sets a reset action for the menu.
/// </summary>
/// <param name="resetAction">The action to execute on reset.</param>
/// <returns>This MenuBuilder instance for chaining.</returns>
public MenuBuilder WithResetAction(Action<CCSPlayerController> resetAction)
{
_resetAction = resetAction;
return this;
}
/// <summary>
/// Sets a custom back action for the menu.
/// </summary>
/// <param name="backAction">The action to execute when going back (nullable).</param>
/// <returns>This MenuBuilder instance for chaining.</returns>
public MenuBuilder WithBackAction(Action<CCSPlayerController>? backAction)
{
_backAction = backAction;
return this;
}
}
/// <summary>
/// Represents an option within a menu.
/// </summary>
public class MenuOption
{
public string Name { get; set; } = string.Empty;
public Action<CCSPlayerController>? Action { get; set; }
public bool Disabled { get; set; }
public string? Permission { get; set; }
}

View File

@@ -0,0 +1,225 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
public class MenuManager
{
private static MenuManager? _instance;
public static MenuManager Instance => _instance ??= new MenuManager();
private readonly Dictionary<string, Func<CCSPlayerController, MenuBuilder>> _menuFactories = [];
private readonly Dictionary<string, MenuCategory> _menuCategories = [];
/// <summary>
/// Provides public access to menu categories (for API usage).
/// </summary>
/// <returns>Dictionary of menu categories keyed by category ID.</returns>
public Dictionary<string, MenuCategory> GetMenuCategories()
{
return _menuCategories;
}
/// <summary>
/// Registers a new menu category with specified permissions.
/// </summary>
/// <param name="categoryId">Unique identifier for the category.</param>
/// <param name="categoryName">Display name of the category.</param>
/// <param name="permission">Required permission to access this category (default: @css/generic).</param>
public void RegisterCategory(string categoryId, string categoryName, string permission = "@css/generic")
{
_menuCategories[categoryId] = new MenuCategory
{
Id = categoryId,
Name = categoryName,
Permission = permission,
MenuFactories = new Dictionary<string, Func<CCSPlayerController, MenuBuilder>>()
};
}
/// <summary>
/// Registers a menu within a category (API for other plugins).
/// </summary>
/// <param name="categoryId">The category to add this menu to.</param>
/// <param name="menuId">Unique identifier for the menu.</param>
/// <param name="menuName">Display name of the menu.</param>
/// <param name="menuFactory">Factory function that creates the menu for a player.</param>
/// <param name="permission">Required permission to access this menu (optional).</param>
/// <param name="commandName">Command name for permission override checking (optional, e.g., "css_god").</param>
public void RegisterMenu(string categoryId, string menuId, string menuName, Func<CCSPlayerController, MenuBuilder> menuFactory, string? permission = null, string? commandName = null)
{
if (!_menuCategories.ContainsKey(categoryId))
{
RegisterCategory(categoryId, categoryId); // Auto-create category if it doesn't exist
}
_menuCategories[categoryId].MenuFactories[menuId] = menuFactory;
_menuCategories[categoryId].MenuNames[menuId] = menuName;
if (permission != null)
{
_menuCategories[categoryId].MenuPermissions[menuId] = permission;
}
if (commandName != null)
{
_menuCategories[categoryId].MenuCommandNames[menuId] = commandName;
}
}
/// <summary>
/// Unregisters a menu from a category.
/// </summary>
/// <param name="categoryId">The category containing the menu.</param>
/// <param name="menuId">The menu to unregister.</param>
public void UnregisterMenu(string categoryId, string menuId)
{
if (!_menuCategories.TryGetValue(categoryId, out var category)) return;
category.MenuFactories.Remove(menuId);
_menuCategories[categoryId].MenuNames.Remove(menuId);
_menuCategories[categoryId].MenuPermissions.Remove(menuId);
_menuCategories[categoryId].MenuCommandNames.Remove(menuId);
}
/// <summary>
/// Creates the main admin menu for a player with accessible categories.
/// </summary>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the main menu.</returns>
public MenuBuilder CreateMainMenu(CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
var mainMenu = new MenuBuilder(localizer?["sa_title"] ?? "SimpleAdmin");
foreach (var category in _menuCategories.Values)
{
if (category.MenuFactories.Count <= 0) continue;
// Check category permissions
var steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, category.Permission))
continue;
// Pass player to CreateCategoryMenu
mainMenu.AddSubMenu(category.Name, () => CreateCategoryMenu(category, player),
permission: category.Permission);
}
return mainMenu;
}
/// <summary>
/// Creates a category submenu containing all registered menus in that category.
/// </summary>
/// <param name="category">The menu category to create.</param>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the category menu.</returns>
private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player)
{
var categoryMenu = new MenuBuilder(category.Name);
foreach (var kvp in category.MenuFactories)
{
var menuId = kvp.Key;
var menuFactory = kvp.Value;
var menuName = category.MenuNames.TryGetValue(menuId, out var name) ? name : menuId;
var permission = category.MenuPermissions.TryGetValue(menuId, out var perm) ? perm : null;
var commandName = category.MenuCommandNames.TryGetValue(menuId, out var cmd) ? cmd : null;
// Check permissions with command override support
var steamId = new SteamID(player.SteamID);
// If commandName is provided, check for permission overrides
if (!string.IsNullOrEmpty(commandName))
{
bool hasPermission;
// Check if command has overridden permissions
if (AdminManager.CommandIsOverriden(commandName))
{
var overriddenPermission = AdminManager.GetPermissionOverrides(commandName);
hasPermission = AdminManager.PlayerHasPermissions(steamId, overriddenPermission);
}
else if (!string.IsNullOrEmpty(permission))
{
// Use default permission if no override exists
hasPermission = AdminManager.PlayerHasPermissions(steamId, permission);
}
else
{
// No permission required
hasPermission = true;
}
if (!hasPermission)
continue;
}
// Fallback to standard permission check if no commandName provided
else if (!string.IsNullOrEmpty(permission))
{
if (!AdminManager.PlayerHasPermissions(steamId, permission))
continue;
}
// Call the actual factory with player parameter
categoryMenu.AddSubMenu(menuName, () => menuFactory(player), permission: permission);
}
return categoryMenu.WithBackButton();
}
/// <summary>
/// Opens the main admin menu for a player.
/// </summary>
/// <param name="player">The player to open the menu for.</param>
public void OpenMainMenu(CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
var steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, "@css/generic"))
{
player.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command"));
return;
}
CreateMainMenu(player).OpenMenu(player);
}
/// <summary>
/// Initializes default menu categories (Players, Server, Admin).
/// </summary>
public void InitializeDefaultCategories()
{
var localizer = CS2_SimpleAdmin._localizer;
RegisterCategory("players", localizer?["sa_menu_players_manage"] ?? "Manage Players", "@css/generic");
RegisterCategory("server", localizer?["sa_menu_server_manage"] ?? "Server Management", "@css/generic");
// RegisterCategory("fun", localizer?["sa_menu_fun_commands"] ?? "Fun Commands", "@css/generic");
RegisterCategory("admin", localizer?["sa_menu_admins_manage"] ?? "Admin Management", "@css/root");
}
/// <summary>
/// Public method for creating category menus (for API usage).
/// </summary>
/// <param name="category">The menu category to create.</param>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the category menu.</returns>
public MenuBuilder CreateCategoryMenuPublic(MenuCategory category, CCSPlayerController player)
{
return CreateCategoryMenu(category, player);
}
}
/// <summary>
/// Represents a menu category containing multiple menus.
/// </summary>
public class MenuCategory
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Permission { get; set; } = "@css/generic";
public Dictionary<string, Func<CCSPlayerController, MenuBuilder>> MenuFactories { get; set; } = [];
public Dictionary<string, string> MenuNames { get; set; } = [];
public Dictionary<string, string> MenuPermissions { get; set; } = [];
public Dictionary<string, string> MenuCommandNames { get; set; } = [];
}

View File

@@ -8,7 +8,7 @@ public static class PlayersMenu
{
public static void OpenRealPlayersMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
{
OpenMenu(admin, menuName, onSelectAction, p => p.IsBot == false);
OpenMenu(admin, menuName, onSelectAction, p => !p.IsBot);
}
public static void OpenAdminPlayersMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController?, bool>? enableFilter = null)
@@ -37,7 +37,7 @@ public static class PlayersMenu
var playerName = player != null && player.PlayerName.Length > 26 ? player.PlayerName[..26] : player?.PlayerName;
var optionName = HttpUtility.HtmlEncode(playerName);
if (player != null && enableFilter != null && enableFilter(player) == false)
if (player != null && enableFilter != null && !enableFilter(player))
continue;
var enabled = admin.CanTarget(player);
@@ -47,7 +47,7 @@ public static class PlayersMenu
{
if (player != null) onSelectAction.Invoke(admin, player);
},
enabled == false);
!enabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);

View File

@@ -1 +1 @@
1.7.7-alpha-9
1.7.8-beta-2

View File

@@ -1,6 +1,7 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdminApi;
@@ -9,6 +10,8 @@ public interface ICS2_SimpleAdminApi
{
public static readonly PluginCapability<ICS2_SimpleAdminApi?> PluginCapability = new("simpleadmin:api");
public event Action? OnSimpleAdminReady;
/// <summary>
/// Gets player information associated with the specified player controller.
/// </summary>
@@ -83,6 +86,25 @@ public interface ICS2_SimpleAdminApi
/// </summary>
public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs);
/// <summary>
/// Shows an admin activity message with a custom translated message (for modules with their own localizer).
/// </summary>
/// <param name="translatedMessage">Already translated message to display to players.</param>
/// <param name="callerName">Name of the admin executing the action (optional).</param>
/// <param name="dontPublish">If true, won't trigger publish events.</param>
public void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null, bool dontPublish = false);
/// <summary>
/// Shows an admin activity message using module's localizer for per-player language support.
/// This method sends messages in each player's configured language.
/// </summary>
/// <param name="moduleLocalizer">The module's IStringLocalizer instance.</param>
/// <param name="messageKey">The translation key from the module's lang files.</param>
/// <param name="callerName">Name of the admin executing the action (optional).</param>
/// <param name="dontPublish">If true, won't trigger publish events.</param>
/// <param name="messageArgs">Arguments to format the localized message.</param>
public void ShowAdminActivityLocalized(object moduleLocalizer, string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs);
/// <summary>
/// Returns true if the specified admin player is in silent mode (not broadcasting activity).
/// </summary>
@@ -102,4 +124,60 @@ public interface ICS2_SimpleAdminApi
/// Unregisters an existing command by its name.
/// </summary>
public void UnRegisterCommand(string name);
/// <summary>
/// Gets target players from command
/// </summary>
TargetResult? GetTarget(CommandInfo command);
/// <summary>
/// Returns the list of current valid players, available to call from other plugins.
/// </summary>
List<CCSPlayerController> GetValidPlayers();
/// <summary>
/// Registers a menu category.
/// </summary>
void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic");
/// <summary>
/// Registers a menu in a category.
/// </summary>
/// <param name="categoryId">The category to add this menu to.</param>
/// <param name="menuId">Unique identifier for the menu.</param>
/// <param name="menuName">Display name of the menu.</param>
/// <param name="menuFactory">Factory function that creates the menu for a player.</param>
/// <param name="permission">Required permission to access this menu (optional).</param>
/// <param name="commandName">Command name for permission override checking (optional, e.g., "css_god").</param>
void RegisterMenu(string categoryId, string menuId, string menuName, Func<CCSPlayerController, object> menuFactory, string? permission = null, string? commandName = null);
/// <summary>
/// Unregisters a menu from a category.
/// </summary>
void UnregisterMenu(string categoryId, string menuId);
/// <summary>
/// Creates a menu with an automatic back button.
/// </summary>
object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player);
/// <summary>
/// Creates a menu with a list of players with filter and action.
/// </summary>
object CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin, Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect);
/// <summary>
/// Adds an option to the menu (extension method helper).
/// </summary>
void AddMenuOption(object menu, string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null);
/// <summary>
/// Adds a submenu to the menu (extension method helper).
/// </summary>
void AddSubMenu(object menu, string name, Func<CCSPlayerController, object> subMenuFactory, bool disabled = false, string? permission = null);
/// <summary>
/// Opens a menu for a player.
/// </summary>
void OpenMenu(object menu, CCSPlayerController player);
}

View File

@@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CS2-SimpleAdmin_FunCommands", "CS2-SimpleAdmin_FunCommands\CS2-SimpleAdmin_FunCommands.csproj", "{72713A40-688F-401F-8211-3D28B068C791}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{72713A40-688F-401F-8211-3D28B068C791}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72713A40-688F-401F-8211-3D28B068C791}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72713A40-688F-401F-8211-3D28B068C791}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72713A40-688F-401F-8211-3D28B068C791}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,237 @@
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.Logging;
namespace CS2_SimpleAdmin_FunCommands;
public partial class CS2_SimpleAdmin_FunCommands
{
private void God(CCSPlayerController? caller, CCSPlayerController player)
{
if (!caller.CanTarget(player)) return;
var callerName = caller?.PlayerName ?? "Console";
// Toggle god mode for the player (like in main plugin)
if (!GodPlayers.Add(player.Slot))
{
GodPlayers.Remove(player.Slot);
}
// Show admin activity using module's own localizer with per-player language support
var activityArgs = new object[] { "CALLER", player.PlayerName };
if (caller == null || !_sharedApi!.IsAdminSilent(caller))
{
if (Localizer != null)
{
_sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_god_message", callerName, false,
activityArgs);
}
else
{
_sharedApi!.ShowAdminActivity("fun_admin_god_message", callerName, false, activityArgs);
}
}
// Log command using API
_sharedApi!.LogCommand(caller,
$"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
private void NoClip(CCSPlayerController? caller, CCSPlayerController player)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
var callerName = caller?.PlayerName ?? "Console";
// Toggle no-clip mode using PlayerExtensions
player.Pawn.Value?.ToggleNoclip();
// Show admin activity using module's own localizer with per-player language support
var activityArgs = new object[] { "CALLER", player.PlayerName };
if (caller == null || !_sharedApi!.IsAdminSilent(caller))
{
if (Localizer != null)
{
_sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_noclip_message", callerName, false,
activityArgs);
}
else
{
_sharedApi!.ShowAdminActivity("fun_admin_noclip_message", callerName, false, activityArgs);
}
}
// Log command using API
_sharedApi!.LogCommand(caller,
$"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
private void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
var callerName = caller?.PlayerName ?? "Console";
// Freeze player using PlayerExtensions
player.Pawn.Value?.Freeze();
// Show admin activity using module's own localizer with per-player language support
var activityArgs = new object[] { "CALLER", player.PlayerName };
if (caller == null || !_sharedApi!.IsAdminSilent(caller))
{
if (Localizer != null)
{
_sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_freeze_message", callerName, false,
activityArgs);
}
else
{
_sharedApi!.ShowAdminActivity("fun_admin_freeze_message", callerName, false, activityArgs);
}
}
// Auto-unfreeze after duration
if (time > 0)
{
AddTimer(time, () => player.Pawn.Value?.Unfreeze(),
CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
// Log command using API
_sharedApi!.LogCommand(caller,
$"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}");
}
private void Unfreeze(CCSPlayerController? caller, CCSPlayerController player)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
var callerName = caller?.PlayerName ?? "Console";
// Unfreeze player using PlayerExtensions
player.Pawn.Value?.Unfreeze();
// Show admin activity using module's own localizer with per-player language support
var activityArgs = new object[] { "CALLER", player.PlayerName };
if (caller == null || !_sharedApi!.IsAdminSilent(caller))
{
if (Localizer != null)
{
_sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_unfreeze_message", callerName, false,
activityArgs);
}
else
{
_sharedApi!.ShowAdminActivity("fun_admin_unfreeze_message", callerName, false, activityArgs);
}
}
// Log command using API
_sharedApi!.LogCommand(caller,
$"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
/// <summary>
/// Respawns a player and teleports them to their last death position if available.
/// This demonstrates using the GetPlayerInfo API to access player data.
/// </summary>
private void Respawn(CCSPlayerController? caller, CCSPlayerController player)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
var callerName = caller?.PlayerName ?? "Console";
// Respawn the player
player.Respawn();
// Get death position from API and teleport player to it
// BEST PRACTICE: Use API to access player data like death position
if (_sharedApi != null && player.UserId.HasValue)
{
try
{
var playerInfo = _sharedApi.GetPlayerInfo(player);
// Teleport to death position if available
if (playerInfo?.DiePosition != null && player.PlayerPawn?.Value != null)
{
player.PlayerPawn.Value.Teleport(
playerInfo.DiePosition.Position,
playerInfo.DiePosition.Angle);
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to get player info for respawn: {ex.Message}");
}
}
// Show admin activity using module's own localizer with per-player language support
var activityArgs = new object[] { "CALLER", player.PlayerName };
if (caller == null || !_sharedApi!.IsAdminSilent(caller))
{
if (Localizer != null)
{
_sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_respawn_message", callerName, false,
activityArgs);
}
else
{
_sharedApi!.ShowAdminActivity("fun_admin_respawn_message", callerName, false, activityArgs);
}
}
// Log command using API
_sharedApi!.LogCommand(caller,
$"css_respawn {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
/// <summary>
/// Resizes a player's model to the specified scale.
/// </summary>
private void Resize(CCSPlayerController? caller, CCSPlayerController player, float size)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
var callerName = caller?.PlayerName ?? "Console";
// Resize the player
var sceneNode = player.PlayerPawn.Value!.CBodyComponent?.SceneNode;
if (sceneNode != null)
{
sceneNode.GetSkeletonInstance().Scale = size;
player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture));
Server.NextWorldUpdate(() =>
{
Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent");
});
}
// Show admin activity using module's own localizer with per-player language support
var activityArgs = new object[] { "CALLER", player.PlayerName, size.ToString(CultureInfo.InvariantCulture) };
if (caller == null || !_sharedApi!.IsAdminSilent(caller))
{
if (Localizer != null)
{
_sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_resize_message", callerName, false,
activityArgs);
}
else
{
_sharedApi!.ShowAdminActivity("fun_admin_resize_message", callerName, false, activityArgs);
}
}
// Log command using API
_sharedApi!.LogCommand(caller,
$"css_resize {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {size}");
}
}

View File

@@ -0,0 +1,496 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Logging;
using System.Globalization;
namespace CS2_SimpleAdmin_FunCommands;
/// <summary>
/// CS2-SimpleAdmin Fun Commands Module
///
/// This module serves as a REFERENCE IMPLEMENTATION for creating CS2-SimpleAdmin modules.
/// Study this code to learn best practices for:
/// - Command registration from configuration
/// - Menu creation with SimpleAdmin API
/// - Per-player translation support
/// - Proper cleanup on module unload
/// - Code organization using partial classes
///
/// File Structure:
/// - CS2-SimpleAdmin_FunCommands.cs (this file) - Plugin initialization and registration
/// - Commands.cs - Command handlers
/// - Actions.cs - Action methods (God, NoClip, Freeze, etc.)
/// - Menus.cs - Menu creation
/// - Config.cs - Configuration with command lists
/// - lang/ - Translation files (13 languages)
///
/// See README.md for detailed explanation of all patterns demonstrated here.
/// </summary>
public partial class CS2_SimpleAdmin_FunCommands : BasePlugin, IPluginConfig<Config>
{
public Config Config { get; set; }
/// <summary>
/// BEST PRACTICE: Cache expensive operations
/// Weapons enum values don't change, so we cache them on first access
/// </summary>
private static Dictionary<int, CsItem>? _weaponsCache;
private static Dictionary<int, CsItem> GetWeaponsCache()
{
if (_weaponsCache != null) return _weaponsCache;
var weaponsArray = Enum.GetValues(typeof(CsItem));
_weaponsCache = new Dictionary<int, CsItem>();
foreach (CsItem item in weaponsArray)
{
if (item == CsItem.Tablet) continue; // Skip tablet (invalid weapon)
_weaponsCache[(int)item] = item;
}
return _weaponsCache;
}
/// <summary>
/// Track players with god mode enabled
/// HashSet for O(1) lookup performance
/// </summary>
private static readonly HashSet<int> GodPlayers = [];
/// <summary>
/// Track players with modified speed
/// Dictionary for storing speed values per player
/// </summary>
private static readonly Dictionary<CCSPlayerController, float> SpeedPlayers = [];
/// <summary>
/// Track players with modified gravity
/// Dictionary for storing gravity values per player
/// </summary>
private static readonly Dictionary<CCSPlayerController, float> GravityPlayers = [];
/// <summary>
/// BEST PRACTICE: Use capability system to get SimpleAdmin API
/// This ensures your module works even if SimpleAdmin loads after your module
/// </summary>
private ICS2_SimpleAdminApi? _sharedApi;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
/// <summary>
/// BEST PRACTICE: Track menu registration state to prevent duplicate registrations
/// </summary>
private bool _menusRegistered = false;
// Plugin metadata
public override string ModuleName => "CS2-SimpleAdmin Fun Commands";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "Your Name";
public override string ModuleDescription => "Fun commands extension for CS2-SimpleAdmin";
/// <summary>
/// BEST PRACTICE: Initialize plugin after all plugins are loaded
/// This ensures SimpleAdmin API is available
/// </summary>
public override void OnAllPluginsLoaded(bool hotReload)
{
// STEP 1: Get SimpleAdmin API using capability system
_sharedApi = _pluginCapability.Get();
if (_sharedApi == null)
{
Logger.LogError("CS2-SimpleAdmin API not found - make sure CS2-SimpleAdmin is loaded!");
Unload(false);
return;
}
// STEP 2: Register commands (can be done immediately)
RegisterFunCommands();
// STEP 3: Register menus (wait for SimpleAdmin to be ready)
// BEST PRACTICE: Use event + fallback to handle both normal load and hot reload
_sharedApi.OnSimpleAdminReady += RegisterFunMenus;
RegisterFunMenus(); // Fallback for hot reload case
// STEP 4: Start timer to maintain speed and gravity modifications
StartSpeedGravityTimer();
}
public override void Unload(bool hotReload)
{
if (_sharedApi == null) return;
// Unregister commands
if (Config.NoclipCommands.Count > 0)
{
foreach (var command in Config.NoclipCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.GodCommands.Count > 0)
{
foreach (var command in Config.GodCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.FreezeCommands.Count > 0)
{
foreach (var command in Config.FreezeCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.UnfreezeCommands.Count > 0)
{
foreach (var command in Config.UnfreezeCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.RespawnCommands.Count > 0)
{
foreach (var command in Config.RespawnCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.GiveCommands.Count > 0)
{
foreach (var command in Config.GiveCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.StripCommands.Count > 0)
{
foreach (var command in Config.StripCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.HpCommands.Count > 0)
{
foreach (var command in Config.HpCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.SpeedCommands.Count > 0)
{
foreach (var command in Config.SpeedCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.GravityCommands.Count > 0)
{
foreach (var command in Config.GravityCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.MoneyCommands.Count > 0)
{
foreach (var command in Config.MoneyCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
if (Config.ResizeCommands.Count > 0)
{
foreach (var command in Config.ResizeCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
// Unregister menus
if (Config.NoclipCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "noclip");
if (Config.GodCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "god");
if (Config.RespawnCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "respawn");
if (Config.GiveCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "give");
if (Config.StripCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "strip");
if (Config.FreezeCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "freeze");
if (Config.HpCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "hp");
if (Config.SpeedCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "speed");
if (Config.GravityCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "gravity");
if (Config.MoneyCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "money");
if (Config.ResizeCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "resize");
_sharedApi.OnSimpleAdminReady -= RegisterFunMenus;
}
private void RegisterFunCommands()
{
if (_sharedApi == null) return;
if (Config.NoclipCommands.Count > 0)
{
foreach (var command in Config.NoclipCommands)
{
_sharedApi.RegisterCommand(command, "Enable noclip", OnNoclipCommand);
}
}
if (Config.GodCommands.Count > 0)
{
foreach (var command in Config.GodCommands)
{
_sharedApi.RegisterCommand(command, "Enable god mode", OnGodCommand);
}
}
if (Config.FreezeCommands.Count > 0)
{
foreach (var command in Config.FreezeCommands)
{
_sharedApi.RegisterCommand(command, "Freeze player", OnFreezeCommand);
}
}
if (Config.UnfreezeCommands.Count > 0)
{
foreach (var command in Config.UnfreezeCommands)
{
_sharedApi.RegisterCommand(command, "Unfreeze player", OnUnfreezeCommand);
}
}
if (Config.RespawnCommands.Count > 0)
{
foreach (var command in Config.RespawnCommands)
{
_sharedApi.RegisterCommand(command, "Respawn player", OnRespawnCommand);
}
}
if (Config.GiveCommands.Count > 0)
{
foreach (var command in Config.GiveCommands)
{
_sharedApi.RegisterCommand(command, "Give weapon", OnGiveWeaponCommand);
}
}
if (Config.StripCommands.Count > 0)
{
foreach (var command in Config.StripCommands)
{
_sharedApi.RegisterCommand(command, "Strip weapons", OnStripWeaponsCommand);
}
}
if (Config.HpCommands.Count > 0)
{
foreach (var command in Config.HpCommands)
{
_sharedApi.RegisterCommand(command, "Set HP", OnSetHpCommand);
}
}
if (Config.SpeedCommands.Count > 0)
{
foreach (var command in Config.SpeedCommands)
{
_sharedApi.RegisterCommand(command, "Set speed", OnSetSpeedCommand);
}
}
if (Config.GravityCommands.Count > 0)
{
foreach (var command in Config.GravityCommands)
{
_sharedApi.RegisterCommand(command, "Set gravity", OnSetGravityCommand);
}
}
if (Config.MoneyCommands.Count > 0)
{
foreach (var command in Config.MoneyCommands)
{
_sharedApi.RegisterCommand(command, "Set money", OnSetMoneyCommand);
}
}
if (Config.ResizeCommands.Count > 0)
{
foreach (var command in Config.ResizeCommands)
{
_sharedApi.RegisterCommand(command, "Resize player", OnSetResizeCommand);
}
}
}
private void RegisterFunMenus()
{
if (_sharedApi == null || _menusRegistered) return;
try
{
_sharedApi.RegisterMenuCategory("fun", Localizer?["fun_category_name"] ?? "Fun Commands", "@css/generic");
// Register menus with command names for permission override support
// Server admins can override default permissions via CounterStrikeSharp admin system
// Example: If "css_god" is overridden to "@css/vip", only VIPs will see the God Mode menu
if (Config.GodCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "god",
Localizer?["fun_menu_god"] ?? "God Mode",
CreateGodModeMenu, "@css/cheats", "css_god");
if (Config.NoclipCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "noclip",
Localizer?["fun_menu_noclip"] ?? "No Clip",
CreateNoClipMenu, "@css/cheats", "css_noclip");
if (Config.RespawnCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "respawn",
Localizer?["fun_menu_respawn"] ?? "Respawn",
CreateRespawnMenu, "@css/cheats", "css_respawn");
if (Config.GiveCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "give",
Localizer?["fun_menu_give"] ?? "Give Weapon",
CreateGiveWeaponMenu, "@css/cheats", "css_give");
if (Config.StripCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "strip",
Localizer?["fun_menu_strip"] ?? "Strip Weapons",
CreateStripWeaponsMenu, "@css/slay", "css_strip");
if (Config.FreezeCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "freeze",
Localizer?["fun_menu_freeze"] ?? "Freeze",
CreateFreezeMenu, "@css/slay", "css_freeze");
if (Config.HpCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "hp",
Localizer?["fun_menu_hp"] ?? "Set HP",
CreateSetHpMenu, "@css/slay", "css_hp");
if (Config.SpeedCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "speed",
Localizer?["fun_menu_speed"] ?? "Set Speed",
CreateSetSpeedMenu, "@css/slay", "css_speed");
if (Config.GravityCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "gravity",
Localizer?["fun_menu_gravity"] ?? "Set Gravity",
CreateSetGravityMenu, "@css/slay", "css_gravity");
if (Config.MoneyCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "money",
Localizer?["fun_menu_money"] ?? "Set Money",
CreateSetMoneyMenu, "@css/slay", "css_money");
if (Config.ResizeCommands.Count > 0)
_sharedApi.RegisterMenu("fun", "resize",
Localizer?["fun_menu_resize"] ?? "Resize Player",
CreateSetResizeMenu, "@css/slay", "css_resize");
_menusRegistered = true;
Logger.LogInformation("Fun menus registered successfully!");
}
catch (Exception ex)
{
Logger.LogError($"Failed to register Fun menus: {ex.Message}");
}
}
public void OnConfigParsed(Config config)
{
Config = config;
}
/// <summary>
/// Starts a repeating timer to maintain speed and gravity modifications for players.
/// This ensures that speed/gravity changes persist even after respawns or round changes.
/// </summary>
private void StartSpeedGravityTimer()
{
AddTimer(0.12f, () =>
{
// Early exit if no players have modified speed or gravity
var hasSpeedPlayers = SpeedPlayers.Count > 0;
var hasGravityPlayers = GravityPlayers.Count > 0;
if (!hasSpeedPlayers && !hasGravityPlayers)
return;
if (hasSpeedPlayers)
{
// Iterate through players with modified speed
foreach (var kvp in SpeedPlayers)
{
var player = kvp.Key;
// Early validation check - avoid accessing PlayerPawn if player is invalid
if (player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected)
{
var pawn = player.PlayerPawn?.Value;
if (pawn != null && pawn.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
player.SetSpeed(kvp.Value);
}
}
}
}
if (hasGravityPlayers)
{
// Iterate through players with modified gravity
foreach (var kvp in GravityPlayers)
{
var player = kvp.Key;
if (player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected)
{
var pawn = player.PlayerPawn?.Value;
if (pawn != null && pawn.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
player.SetGravity(kvp.Value);
}
}
}
}
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.REPEAT);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CS2_SimpleAdmin_FunCommands</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="CS2-SimpleAdminApi">
<HintPath>..\CS2-SimpleAdminApi.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.340" />
</ItemGroup>
<ItemGroup>
<None Update="lang\**\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,320 @@
using System.Globalization;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities.Constants;
namespace CS2_SimpleAdmin_FunCommands;
public partial class CS2_SimpleAdmin_FunCommands
{
// =================================
// COMMAND HANDLERS
// =================================
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/cheats")]
private void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
NoClip(caller, player);
}
});
}
[RequiresPermissions("@css/cheats")]
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnGodCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
God(caller, player);
}
});
}
[CommandHelper(1, "<#userid or name> [duration]")]
[RequiresPermissions("@css/slay")]
private void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
{
int.TryParse(command.GetArg(2), out var time);
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
Freeze(caller, player, time);
}
});
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/slay")]
private void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
Unfreeze(caller, player);
}
});
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/cheats")]
private void OnRespawnCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
Respawn(caller, player);
}
});
}
[CommandHelper(2, "<#userid or name> <weapon>")]
[RequiresPermissions("@css/cheats")]
private void OnGiveWeaponCommand(CCSPlayerController? caller, CommandInfo command)
{
var weaponName = command.GetArg(2);
if (Enum.TryParse(weaponName, true, out CsItem weapon))
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
player.GiveNamedItem(weapon);
LogAndShowActivity(caller, player, "fun_admin_give_message", "css_give", weapon.ToString());
}
});
}
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/slay")]
private void OnStripWeaponsCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
player.RemoveWeapons();
LogAndShowActivity(caller, player, "fun_admin_strip_message", "css_strip");
}
});
}
[CommandHelper(2, "<#userid or name> <hp>")]
[RequiresPermissions("@css/slay")]
private void OnSetHpCommand(CCSPlayerController? caller, CommandInfo command)
{
if (int.TryParse(command.GetArg(2), out var hp))
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
player.SetHp(hp);
LogAndShowActivity(caller, player, "fun_admin_hp_message", "css_hp", hp.ToString());
}
});
}
}
[CommandHelper(2, "<#userid or name> <speed>")]
[RequiresPermissions("@css/slay")]
private void OnSetSpeedCommand(CCSPlayerController? caller, CommandInfo command)
{
if (float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var speed))
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
player.SetSpeed(speed);
// Track speed modification for timer
if (speed == 1f)
SpeedPlayers.Remove(player);
else
SpeedPlayers[player] = speed;
LogAndShowActivity(caller, player, "fun_admin_speed_message", "css_speed", speed.ToString(CultureInfo.InvariantCulture));
}
});
}
}
[CommandHelper(2, "<#userid or name> <gravity>")]
[RequiresPermissions("@css/slay")]
private void OnSetGravityCommand(CCSPlayerController? caller, CommandInfo command)
{
if (float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var gravity))
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
player.SetGravity(gravity);
// Track gravity modification for timer
if (gravity == 1f)
GravityPlayers.Remove(player);
else
GravityPlayers[player] = gravity;
LogAndShowActivity(caller, player, "fun_admin_gravity_message", "css_gravity", gravity.ToString(CultureInfo.InvariantCulture));
}
});
}
}
[CommandHelper(2, "<#userid or name> <money>")]
[RequiresPermissions("@css/slay")]
private void OnSetMoneyCommand(CCSPlayerController? caller, CommandInfo command)
{
if (int.TryParse(command.GetArg(2), out var money))
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
player.SetMoney(money);
LogAndShowActivity(caller, player, "fun_admin_money_message", "css_money", money.ToString());
}
});
}
}
[CommandHelper(2, "<#userid or name> <size>")]
[RequiresPermissions("@css/slay")]
private void OnSetResizeCommand(CCSPlayerController? caller, CommandInfo command)
{
if (float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size))
{
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
Resize(caller, player, size);
}
});
}
}
// =================================
// HELPER METHOD FOR ACTIVITIES WITH INDIVIDUAL COMMAND LOGGING
// =================================
private void LogAndShowActivity(CCSPlayerController? caller, CCSPlayerController target, string messageKey, string baseCommand, params string[] extraArgs)
{
var callerName = caller?.PlayerName ?? "Console";
// Build activity args
var args = new List<object> { "CALLER", target.PlayerName };
args.AddRange(extraArgs);
// Show admin activity using module's own localizer with per-player language support
if (caller == null || !_sharedApi!.IsAdminSilent(caller))
{
// Use module's own translations with automatic per-player language support
if (Localizer != null)
{
// This will send the message in each player's configured language
_sharedApi!.ShowAdminActivityLocalized(Localizer, messageKey, callerName, false, args.ToArray());
}
else
{
// Fallback to old method if localizer is not available
_sharedApi!.ShowAdminActivity(messageKey, callerName, false, args.ToArray());
}
}
// Build and log command using API string method
var logCommand = $"{baseCommand} {(string.IsNullOrEmpty(target.PlayerName) ? target.SteamID.ToString() : target.PlayerName)}";
if (extraArgs.Length > 0)
{
logCommand += $" {string.Join(" ", extraArgs)}";
}
_sharedApi!.LogCommand(caller, logCommand);
}
}

View File

@@ -0,0 +1,21 @@
using CounterStrikeSharp.API.Core;
namespace CS2_SimpleAdmin_FunCommands;
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
public List<string> NoclipCommands { get; set; } = ["css_noclip"];
public List<string> GodCommands { get; set; } = ["css_god"];
public List<string> FreezeCommands { get; set; } = ["css_freeze"];
public List<string> UnfreezeCommands { get; set; } = ["css_unfreeze"];
public List<string> RespawnCommands { get; set; } = ["css_respawn"];
public List<string> GiveCommands { get; set; } = ["css_give"];
public List<string> StripCommands { get; set; } = ["css_strip"];
public List<string> HpCommands { get; set; } = ["css_hp"];
public List<string> SpeedCommands { get; set; } = ["css_speed"];
public List<string> GravityCommands { get; set; } = ["css_gravity"];
public List<string> MoneyCommands { get; set; } = ["css_money"];
public List<string> ResizeCommands { get; set; } = ["css_resize"];
}

View File

@@ -0,0 +1,69 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
namespace CS2_SimpleAdmin_FunCommands;
public partial class CS2_SimpleAdmin_FunCommands
{
[GameEventHandler]
public HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var player = @event.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
// Check if player has god mode (similar to main plugin)
if (!GodPlayers.Contains(player.Slot)) return HookResult.Continue;
// Cancel damage
@event.DmgHealth = 0;
@event.DmgArmor = 0;
// Reset health to full
if (player.PlayerPawn?.Value == null) return HookResult.Continue;
player.PlayerPawn.Value.Health = player.PlayerPawn.Value.MaxHealth;
Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_iHealth");
return HookResult.Continue;
}
[GameEventHandler]
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
// Remove player from god mode, speed, and gravity tracking on death
GodPlayers.Remove(player.Slot);
SpeedPlayers.Remove(player);
GravityPlayers.Remove(player);
return HookResult.Continue;
}
[GameEventHandler]
public HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info)
{
// Clear all fun command modifications at round start
GodPlayers.Clear();
SpeedPlayers.Clear();
GravityPlayers.Clear();
return HookResult.Continue;
}
[GameEventHandler]
public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
{
var player = @event.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
// Clean up player from all tracking when they disconnect
GodPlayers.Remove(player.Slot);
SpeedPlayers.Remove(player);
GravityPlayers.Remove(player);
return HookResult.Continue;
}
}

View File

@@ -0,0 +1,411 @@
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
namespace CS2_SimpleAdmin_FunCommands;
/// <summary>
/// Menu creation methods for Fun Commands module.
/// This file demonstrates different menu patterns using SimpleAdmin API.
///
/// PERMISSION OVERRIDE SUPPORT:
/// ============================
/// When registering menus in RegisterFunMenus(), you can pass a command name (e.g., "css_god")
/// as the last parameter. This enables automatic permission override checking via CounterStrikeSharp's
/// admin system.
///
/// How it works:
/// 1. Server admin overrides a command's permissions (e.g., css_god requires @css/vip instead of @css/cheats)
/// 2. SimpleAdmin's menu system automatically checks for overrides when displaying menus
/// 3. If override exists, it uses the overridden permission; otherwise, uses the default permission
///
/// Example from RegisterFunMenus():
/// _sharedApi.RegisterMenu("fun", "god",
/// Localizer?["fun_menu_god"] ?? "God Mode",
/// CreateGodModeMenu,
/// "@css/cheats", // Default permission
/// "css_god"); // Command name for override checking
///
/// This means developers don't need to manually check permissions in their menu factory methods!
/// </summary>
public partial class CS2_SimpleAdmin_FunCommands
{
// =================================
// SIMPLE PLAYER SELECTION MENUS
// =================================
// Pattern: Direct player selection with immediate action
// Use CreateMenuWithPlayers when you just need to select a player and execute an action
/// <summary>
/// Creates a simple player selection menu for god mode.
/// PATTERN: CreateMenuWithPlayers with method reference
/// </summary>
private object CreateGodModeMenu(CCSPlayerController admin)
{
return _sharedApi!.CreateMenuWithPlayers(
Localizer?["fun_menu_god"] ?? "God Mode", // Menu title from translation
"fun", // Category ID (for back button navigation)
admin, // Admin opening the menu
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player), // Filter: only alive, targetable players
God); // Action to execute (method reference)
}
private object CreateNoClipMenu(CCSPlayerController admin)
{
return _sharedApi!.CreateMenuWithPlayers(
Localizer?["fun_menu_noclip"] ?? "No Clip",
"fun",
admin,
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player),
NoClip);
}
/// <summary>
/// Creates a player selection menu for respawn command.
/// PATTERN: CreateMenuWithPlayers with method reference
/// </summary>
private object CreateRespawnMenu(CCSPlayerController admin)
{
return _sharedApi!.CreateMenuWithPlayers(
Localizer?["fun_menu_respawn"] ?? "Respawn", // Menu title from translation
"fun", // Category ID
admin, // Admin
admin.CanTarget, // Filter: only targetable players (no LifeState check - can respawn dead players)
Respawn); // Use the Respawn method which includes death position teleport
}
// =================================
// NESTED MENUS - PLAYER → VALUE SELECTION
// =================================
// Pattern: First select player, then select a value/option for that player
// Use CreateMenuWithBack + AddSubMenu for multi-level menus
/// <summary>
/// Creates a nested menu: Player selection → Weapon selection.
/// PATTERN: CreateMenuWithBack + foreach + AddSubMenu
/// </summary>
private object CreateGiveWeaponMenu(CCSPlayerController admin)
{
var menu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_give"] ?? "Give Weapon",
"fun",
admin);
var players = _sharedApi.GetValidPlayers().Where(p =>
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
// AddSubMenu automatically adds a "Back" button to the submenu
// The lambda receives 'p' but we use captured 'player' variable (closure)
_sharedApi.AddSubMenu(menu, playerName, p => CreateWeaponSelectionMenu(admin, player));
}
return menu;
}
/// <summary>
/// Creates weapon selection submenu for a specific player.
/// PATTERN: CreateMenuWithBack + foreach + AddMenuOption
/// </summary>
private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var weaponMenu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_give_player", target.PlayerName] ?? $"Give Weapon: {target.PlayerName}",
"fun",
admin);
// Loop through cached weapons (performance optimization)
foreach (var weapon in GetWeaponsCache())
{
// AddMenuOption for each selectable option
// IMPORTANT: Always validate target.IsValid before executing action
_sharedApi.AddMenuOption(weaponMenu, weapon.Value.ToString(), _ =>
{
if (target.IsValid) // Player might disconnect before selection
{
target.GiveNamedItem(weapon.Value);
LogAndShowActivity(admin, target, "fun_admin_give_message", $"css_give", weapon.Value.ToString());
}
});
}
return weaponMenu;
}
private object CreateStripWeaponsMenu(CCSPlayerController admin)
{
return _sharedApi!.CreateMenuWithPlayers(
Localizer?["fun_menu_strip"] ?? "Strip Weapons",
"fun",
admin,
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player),
(adminPlayer, targetPlayer) =>
{
targetPlayer.RemoveWeapons();
LogAndShowActivity(adminPlayer, targetPlayer, "fun_admin_strip_message", "css_strip");
});
}
private object CreateFreezeMenu(CCSPlayerController admin)
{
return _sharedApi!.CreateMenuWithPlayers(
Localizer?["fun_menu_freeze"] ?? "Freeze",
"fun",
admin,
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player),
(adminPlayer, targetPlayer) => { Freeze(adminPlayer, targetPlayer, -1); });
}
/// <summary>
/// Creates a nested menu for setting player HP with predefined values.
/// PATTERN: Same as Give Weapon (player selection → value selection)
/// This is a common pattern you'll use frequently!
/// </summary>
private object CreateSetHpMenu(CCSPlayerController admin)
{
var menu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_hp"] ?? "Set HP",
"fun",
admin);
var players = _sharedApi.GetValidPlayers().Where(p =>
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
_sharedApi.AddSubMenu(menu, playerName, p => CreateHpSelectionMenu(admin, player));
}
return menu;
}
/// <summary>
/// Creates HP value selection submenu.
/// TIP: Use arrays for predefined values - easy to modify and maintain
/// </summary>
private object CreateHpSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var hpSelectionMenu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_hp_player", target.PlayerName] ?? $"Set HP: {target.PlayerName}",
"fun",
admin);
// Predefined HP values - easy to customize
var hpValues = new[] { 1, 10, 25, 50, 100, 200, 500, 999 };
foreach (var hp in hpValues)
{
_sharedApi.AddMenuOption(hpSelectionMenu,
Localizer?["fun_menu_hp_value", hp] ?? $"{hp} HP",
_ =>
{
if (target.IsValid)
{
target.SetHp(hp);
LogAndShowActivity(admin, target, "fun_admin_hp_message", "css_hp", hp.ToString());
}
});
}
return hpSelectionMenu;
}
private object CreateSetSpeedMenu(CCSPlayerController admin)
{
var menu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_speed"] ?? "Set Speed",
"fun",
admin);
var players = _sharedApi.GetValidPlayers().Where(p =>
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
_sharedApi.AddSubMenu(menu, playerName, p => CreateSpeedSelectionMenu(admin, player));
}
return menu;
}
/// <summary>
/// Creates speed value selection submenu.
/// TIP: Use tuples (value, display) when you need different internal value vs display text
/// Example: (0.5f, "0.5") - float value for code, string for display
/// </summary>
private object CreateSpeedSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var speedSelectionMenu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_speed_player", target.PlayerName] ?? $"Set Speed: {target.PlayerName}",
"fun",
admin);
// Tuple pattern: (actualValue, displayText)
// Useful when display text differs from actual value
var speedValues = new[]
{
(0.1f, "0.1"), (0.25f, "0.25"), (0.5f, "0.5"), (0.75f, "0.75"), (1f, "1"), (2f, "2"), (3f, "3"), (4f, "4")
};
foreach (var (speed, display) in speedValues)
{
_sharedApi.AddMenuOption(speedSelectionMenu,
Localizer?["fun_menu_speed_value", display] ?? $"Speed {display}",
_ =>
{
if (target.IsValid)
{
target.SetSpeed(speed);
// Track speed modification for timer
if (speed == 1f)
SpeedPlayers.Remove(target);
else
SpeedPlayers[target] = speed;
LogAndShowActivity(admin, target, "fun_admin_speed_message", "css_speed", speed.ToString(CultureInfo.InvariantCulture));
}
});
}
return speedSelectionMenu;
}
private object CreateSetGravityMenu(CCSPlayerController admin)
{
var menu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_gravity"] ?? "Set Gravity",
"fun",
admin);
var players = _sharedApi.GetValidPlayers().Where(p =>
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
_sharedApi.AddSubMenu(menu, playerName, p => CreateGravitySelectionMenu(admin, player));
}
return menu;
}
private object CreateGravitySelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var gravitySelectionMenu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_gravity_player", target.PlayerName] ?? $"Set Gravity: {target.PlayerName}",
"fun",
admin);
var gravityValues = new[]
{ (0.1f, "0.1"), (0.25f, "0.25"), (0.5f, "0.5"), (0.75f, "0.75"), (1f, "1"), (2f, "2") };
foreach (var (gravity, display) in gravityValues)
{
_sharedApi.AddMenuOption(gravitySelectionMenu,
Localizer?["fun_menu_gravity_value", display] ?? $"Gravity {display}",
_ =>
{
if (target.IsValid)
{
target.SetGravity(Convert.ToSingle(gravity, CultureInfo.InvariantCulture));
// Track gravity modification for timer
if (gravity == 1f)
GravityPlayers.Remove(target);
else
GravityPlayers[target] = gravity;
LogAndShowActivity(admin, target, "fun_admin_gravity_message", "css_gravity", gravity.ToString(CultureInfo.InvariantCulture));
}
});
}
return gravitySelectionMenu;
}
private object CreateSetMoneyMenu(CCSPlayerController admin)
{
var menu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_money"] ?? "Set Money",
"fun",
admin);
var players = _sharedApi.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
_sharedApi.AddSubMenu(menu, playerName, p => CreateMoneySelectionMenu(admin, player));
}
return menu;
}
private object CreateMoneySelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var moneySelectionMenu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_money_player", target.PlayerName] ?? $"Set Money: {target.PlayerName}",
"fun",
admin);
var moneyValues = new[] { 0, 1000, 2500, 5000, 10000, 16000 };
foreach (var money in moneyValues)
{
_sharedApi.AddMenuOption(moneySelectionMenu,
Localizer?["fun_menu_money_value", money] ?? $"${money}",
_ =>
{
if (target.IsValid)
{
target.SetMoney(money);
LogAndShowActivity(admin, target, "fun_admin_money_message", "css_money", money.ToString());
}
});
}
return moneySelectionMenu;
}
private object CreateSetResizeMenu(CCSPlayerController admin)
{
var menu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_resize"] ?? "Resize Player",
"fun",
admin);
var players = _sharedApi.GetValidPlayers().Where(p =>
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
_sharedApi.AddSubMenu(menu, playerName, p => CreateResizeSelectionMenu(admin, player));
}
return menu;
}
private object CreateResizeSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var resizeSelectionMenu = _sharedApi!.CreateMenuWithBack(
Localizer?["fun_menu_resize_player", target.PlayerName] ?? $"Resize: {target.PlayerName}",
"fun",
admin);
var resizeValues = new[]
{ (0.5f, "0.5"), (0.75f, "0.75"), (1f, "1"), (1.25f, "1.25"), (1.5f, "1.5"), (2f, "2"), (3f, "3") };
foreach (var (resize, display) in resizeValues)
{
_sharedApi.AddMenuOption(resizeSelectionMenu,
Localizer?["fun_menu_resize_value", display] ?? $"Size {display}",
_ =>
{
if (target.IsValid)
{
Resize(admin, target, resize);
}
});
}
return resizeSelectionMenu;
}
}

View File

@@ -0,0 +1,271 @@
using System.Drawing;
using System.Globalization;
using System.Numerics;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.UserMessages;
namespace CS2_SimpleAdmin_FunCommands;
public static class PlayerExtensions
{
/// <summary>
/// Slaps the player pawn by applying optional damage and adding a random velocity knockback.
/// </summary>
/// <param name="pawn">The player pawn to slap.</param>
/// <param name="damage">The amount of damage to apply (default is 0).</param>
public static void Slap(this CBasePlayerPawn pawn, int damage = 0)
{
PerformSlap(pawn, damage);
}
/// <summary>
/// Determines if the player controller can target another player controller, respecting admin permissions and immunity.
/// </summary>
/// <param name="controller">The player controller who wants to target.</param>
/// <param name="target">The player controller being targeted.</param>
/// <returns>True if targeting is allowed, false otherwise.</returns>
public static bool CanTarget(this CCSPlayerController? controller, CCSPlayerController? target)
{
if (controller is null || target is null) return true;
if (target.IsBot) return true;
return AdminManager.CanPlayerTarget(controller, target) ||
AdminManager.CanPlayerTarget(new SteamID(controller.SteamID),
new SteamID(target.SteamID)) ||
AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(target);
}
/// <summary>
/// Checks if the controller can target a player by SteamID, considering targeting permissions and immunities.
/// </summary>
/// <param name="controller">The attacker player controller.</param>
/// <param name="steamId">The SteamID of the target player.</param>
/// <returns>True if targeting is permitted, false otherwise.</returns>
public static bool CanTarget(this CCSPlayerController? controller, SteamID steamId)
{
if (controller is null) return true;
return AdminManager.CanPlayerTarget(new SteamID(controller.SteamID), steamId) ||
AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(steamId);
}
/// <summary>
/// Sets the movement speed modifier of the player controller.
/// </summary>
/// <param name="controller">The player controller.</param>
/// <param name="speed">The speed modifier value.</param>
public static void SetSpeed(this CCSPlayerController? controller, float speed)
{
var playerPawnValue = controller?.PlayerPawn.Value;
if (playerPawnValue == null) return;
playerPawnValue.VelocityModifier = speed;
}
/// <summary>
/// Sets the gravity scale for the player controller.
/// </summary>
/// <param name="controller">The player controller.</param>
/// <param name="gravity">The gravity scale.</param>
public static void SetGravity(this CCSPlayerController? controller, float gravity)
{
var playerPawnValue = controller?.PlayerPawn.Value;
if (playerPawnValue == null) return;
playerPawnValue.ActualGravityScale = gravity;
}
/// <summary>
/// Sets the player's in-game money amount.
/// </summary>
/// <param name="controller">The player controller.</param>
/// <param name="money">The amount of money to set.</param>
public static void SetMoney(this CCSPlayerController? controller, int money)
{
var moneyServices = controller?.InGameMoneyServices;
if (moneyServices == null) return;
moneyServices.Account = money;
if (controller != null) Utilities.SetStateChanged(controller, "CCSPlayerController", "m_pInGameMoneyServices");
}
/// <summary>
/// Sets the player's health points.
/// </summary>
/// <param name="controller">The player controller.</param>
/// <param name="health">The health value, default is 100.</param>
public static void SetHp(this CCSPlayerController? controller, int health = 100)
{
if (controller == null) return;
if (health <= 0 || controller.PlayerPawn.Value == null || controller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) return;
controller.PlayerPawn.Value.Health = health;
if (health > 100)
{
controller.PlayerPawn.Value.MaxHealth = health;
}
Utilities.SetStateChanged(controller.PlayerPawn.Value, "CBaseEntity", "m_iHealth");
}
/// <summary>
/// Buries the player pawn by moving it down by a depth offset.
/// </summary>
/// <param name="pawn">The player pawn to bury.</param>
/// <param name="depth">The depth offset (default 10 units).</param>
public static void Bury(this CBasePlayerPawn pawn, float depth = 10f)
{
var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
pawn.AbsOrigin!.Z - depth);
var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
pawn.Teleport(newPos, newRotation, newVelocity);
}
/// <summary>
/// Unburies the player pawn by moving it up by a depth offset.
/// </summary>
/// <param name="pawn">The player pawn to unbury.</param>
/// <param name="depth">The depth offset (default 15 units).</param>
public static void Unbury(this CBasePlayerPawn pawn, float depth = 15f)
{
var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
pawn.AbsOrigin!.Z + depth);
var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
pawn.Teleport(newPos, newRotation, newVelocity);
}
/// <summary>
/// Freezes the player pawn, disabling movement.
/// </summary>
/// <param name="pawn">The player pawn to freeze.</param>
public static void Freeze(this CBasePlayerPawn pawn)
{
pawn.MoveType = MoveType_t.MOVETYPE_INVALID;
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 11); // invalid
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
}
/// <summary>
/// Unfreezes the player pawn, enabling movement.
/// </summary>
/// <param name="pawn">The player pawn to unfreeze.</param>
public static void Unfreeze(this CBasePlayerPawn pawn)
{
pawn.MoveType = MoveType_t.MOVETYPE_WALK;
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 2); // walk
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
}
/// <summary>
/// Changes the player's color tint to specified RGBA values.
/// </summary>
/// <param name="pawn">The pawn to colorize.</param>
/// <param name="r">Red component (0-255).</param>
/// <param name="g">Green component (0-255).</param>
/// <param name="b">Blue component (0-255).</param>
/// <param name="a">Alpha (transparency) component (0-255).</param>
public static void Colorize(this CBasePlayerPawn pawn, int r = 255, int g = 255, int b = 255, int a = 255)
{
pawn.Render = Color.FromArgb(a, r, g, b);
Utilities.SetStateChanged(pawn, "CBaseModelEntity", "m_clrRender");
}
/// <summary>
/// Toggles noclip mode for the player pawn.
/// </summary>
/// <param name="pawn">The player pawn.</param>
public static void ToggleNoclip(this CBasePlayerPawn pawn)
{
if (pawn.MoveType == MoveType_t.MOVETYPE_NOCLIP)
{
pawn.MoveType = MoveType_t.MOVETYPE_WALK;
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 2); // walk
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
}
else
{
pawn.MoveType = MoveType_t.MOVETYPE_NOCLIP;
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 8); // noclip
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
}
}
/// <summary>
/// Teleports a player controller to the position, rotation, and velocity of another player controller.
/// </summary>
/// <param name="controller">The controller to teleport.</param>
/// <param name="target">The target controller whose position to copy.</param>
public static void TeleportPlayer(this CCSPlayerController? controller, CCSPlayerController? target)
{
if (controller?.PlayerPawn.Value == null && target?.PlayerPawn.Value == null)
return;
if (
controller?.PlayerPawn.Value is { AbsOrigin: not null, AbsRotation: not null } &&
target?.PlayerPawn.Value is { AbsOrigin: not null, AbsRotation: not null }
)
{
controller.PlayerPawn.Value.Teleport(
target.PlayerPawn.Value.AbsOrigin,
target.PlayerPawn.Value.AbsRotation,
target.PlayerPawn.Value.AbsVelocity
);
}
}
/// <summary>
/// Applies a slap effect to the given player pawn, optionally inflicting damage and adding velocity knockback.
/// </summary>
/// <param name="pawn">The player pawn to slap.</param>
/// <param name="damage">The amount of damage to deal (default is 0).</param>
private static void PerformSlap(CBasePlayerPawn pawn, int damage = 0)
{
if (pawn.LifeState != (int)LifeState_t.LIFE_ALIVE)
return;
var controller = pawn.Controller.Value?.As<CCSPlayerController>();
/* Teleport in a random direction - thank you, Mani!*/
/* Thank you AM & al!*/
var random = new Random();
var vel = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
vel.X += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1);
vel.Y += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1);
vel.Z += random.Next(200) + 100;
pawn.AbsVelocity.X = vel.X;
pawn.AbsVelocity.Y = vel.Y;
pawn.AbsVelocity.Z = vel.Z;
if (controller != null && controller.IsValid)
{
var shakeMessage = UserMessage.FromPartialName("Shake");
shakeMessage.SetFloat("duration", 1);
shakeMessage.SetFloat("amplitude", 10);
shakeMessage.SetFloat("frequency", 1f);
shakeMessage.SetInt("command", 0);
shakeMessage.Recipients.Add(controller);
shakeMessage.Send();
}
if (damage <= 0)
return;
pawn.Health -= damage;
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
if (pawn.Health <= 0)
pawn.CommitSuicide(true, true);
}
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "أوامر المرح",
"fun_admin_give_message": "{lightred}{0}{default} أعطى {lightred}{1}{default} {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} أخذ جميع أسلحة اللاعب {lightred}{1}{default}!",
"fun_admin_hp_message": "{lightred}{0}{default} غيّر عدد نقاط الحياة لـ {lightred}{1}{default}!",
"fun_admin_speed_message": "{lightred}{0}{default} غيّر السرعة لـ {lightred}{1}{default}!",
"fun_admin_gravity_message": "{lightred}{0}{default} غيّر الجاذبية لـ {lightred}{1}{default}!",
"fun_admin_money_message": "{lightred}{0}{default} غيّر المال لـ {lightred}{1}{default}!",
"fun_admin_god_message": "{lightred}{0}{default} غيّر وضع الله لـ {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} فعّل/ألغى نمط اللا تصادم لـ {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} جمد {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} أذاب {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} أحيى {lightred}{1}{default}!",
"fun_menu_god": "وضع الله",
"fun_menu_noclip": "اللا تصادم",
"fun_menu_respawn": "إحياء",
"fun_menu_give": "إعطاء سلاح",
"fun_menu_give_player": "إعطاء سلاح: {0}",
"fun_menu_strip": "نزع الأسلحة",
"fun_menu_freeze": "تجميد",
"fun_menu_hp": "ضبط الحياة",
"fun_menu_hp_player": "ضبط الحياة: {0}",
"fun_menu_speed": "ضبط السرعة",
"fun_menu_speed_player": "ضبط السرعة: {0}",
"fun_menu_gravity": "ضبط الجاذبية",
"fun_menu_gravity_player": "ضبط الجاذبية: {0}",
"fun_menu_money": "ضبط المال",
"fun_menu_money_player": "ضبط المال: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "السرعة {0}",
"fun_menu_gravity_value": "الجاذبية {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} غيّر حجم {lightred}{1}{default} إلى {lightred}{2}{default}!",
"fun_menu_resize": "تغيير الحجم",
"fun_menu_resize_player": "تغيير الحجم: {0}",
"fun_menu_resize_value": "الحجم {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Spaß-Befehle",
"fun_admin_give_message": "{lightred}{0}{default} hat {lightred}{1}{default} ein {lightred}{2}{default} gegeben!",
"fun_admin_strip_message": "{lightred}{0}{default} hat alle Waffen von Spieler {lightred}{1}{default} entfernt!",
"fun_admin_hp_message": "{lightred}{0}{default} hat die Lebenspunkte von {lightred}{1}{default} geändert!",
"fun_admin_speed_message": "{lightred}{0}{default} hat die Geschwindigkeit von {lightred}{1}{default} geändert!",
"fun_admin_gravity_message": "{lightred}{0}{default} hat die Schwerkraft von {lightred}{1}{default} geändert!",
"fun_admin_money_message": "{lightred}{0}{default} hat das Geld von {lightred}{1}{default} geändert!",
"fun_admin_god_message": "{lightred}{0}{default} hat den Gottmodus von {lightred}{1}{default} geändert!",
"fun_admin_noclip_message": "{lightred}{0}{default} hat den Noclip-Modus für {lightred}{1}{default} aktiviert/deaktiviert!",
"fun_admin_freeze_message": "{lightred}{0}{default} hat {lightred}{1}{default} eingefroren!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} hat {lightred}{1}{default} aufgetaut!",
"fun_admin_respawn_message": "{lightred}{0}{default} hat {lightred}{1}{default} wiederbelebt!",
"fun_menu_god": "Gottmodus",
"fun_menu_noclip": "Noclip",
"fun_menu_respawn": "Wiederbeleben",
"fun_menu_give": "Waffe Geben",
"fun_menu_give_player": "Waffe Geben: {0}",
"fun_menu_strip": "Waffen Entfernen",
"fun_menu_freeze": "Einfrieren",
"fun_menu_hp": "HP Festlegen",
"fun_menu_hp_player": "HP Festlegen: {0}",
"fun_menu_speed": "Geschwindigkeit Festlegen",
"fun_menu_speed_player": "Geschwindigkeit Festlegen: {0}",
"fun_menu_gravity": "Schwerkraft Festlegen",
"fun_menu_gravity_player": "Schwerkraft Festlegen: {0}",
"fun_menu_money": "Geld Festlegen",
"fun_menu_money_player": "Geld Festlegen: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Geschwindigkeit {0}",
"fun_menu_gravity_value": "Schwerkraft {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} hat die Größe von {lightred}{1}{default} auf {lightred}{2}{default} geändert!",
"fun_menu_resize": "Größe Ändern",
"fun_menu_resize_player": "Größe Ändern: {0}",
"fun_menu_resize_value": "Größe {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Fun Commands",
"fun_admin_give_message": "{lightred}{0}{default} gave {lightred}{1}{default} a {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} took all of player {lightred}{1}{default} weapons!",
"fun_admin_hp_message": "{lightred}{0}{default} changed {lightred}{1}{default} hp amount!",
"fun_admin_speed_message": "{lightred}{0}{default} changed speed for {lightred}{1}{default}!",
"fun_admin_gravity_message": "{lightred}{0}{default} changed gravity for {lightred}{1}{default}!",
"fun_admin_money_message": "{lightred}{0}{default} changed money for {lightred}{1}{default}!",
"fun_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} toggled noclip for {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} froze {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} unfroze {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} respawned {lightred}{1}{default}!",
"fun_menu_god": "God Mode",
"fun_menu_noclip": "No Clip",
"fun_menu_respawn": "Respawn",
"fun_menu_give": "Give Weapon",
"fun_menu_give_player": "Give Weapon: {0}",
"fun_menu_strip": "Strip Weapons",
"fun_menu_freeze": "Freeze",
"fun_menu_hp": "Set HP",
"fun_menu_hp_player": "Set HP: {0}",
"fun_menu_speed": "Set Speed",
"fun_menu_speed_player": "Set Speed: {0}",
"fun_menu_gravity": "Set Gravity",
"fun_menu_gravity_player": "Set Gravity: {0}",
"fun_menu_money": "Set Money",
"fun_menu_money_player": "Set Money: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Speed {0}",
"fun_menu_gravity_value": "Gravity {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} resized {lightred}{1}{default} to {lightred}{2}{default}!",
"fun_menu_resize": "Resize Player",
"fun_menu_resize_player": "Resize: {0}",
"fun_menu_resize_value": "Size {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Comandos Divertidos",
"fun_admin_give_message": "{lightred}{0}{default} dio {lightred}{1}{default} un {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} quitó todas las armas del jugador {lightred}{1}{default}!",
"fun_admin_hp_message": "{lightred}{0}{default} cambió la cantidad de HP de {lightred}{1}{default}!",
"fun_admin_speed_message": "{lightred}{0}{default} cambió la velocidad de {lightred}{1}{default}!",
"fun_admin_gravity_message": "{lightred}{0}{default} cambió la gravedad de {lightred}{1}{default}!",
"fun_admin_money_message": "{lightred}{0}{default} cambió el dinero de {lightred}{1}{default}!",
"fun_admin_god_message": "{lightred}{0}{default} cambió el modo dios de {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} alternó noclip para {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} congeló a {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} descongeló a {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} reapareció a {lightred}{1}{default}!",
"fun_menu_god": "Modo Dios",
"fun_menu_noclip": "Noclip",
"fun_menu_respawn": "Reaparecer",
"fun_menu_give": "Dar Arma",
"fun_menu_give_player": "Dar Arma: {0}",
"fun_menu_strip": "Quitar Armas",
"fun_menu_freeze": "Congelar",
"fun_menu_hp": "Establecer HP",
"fun_menu_hp_player": "Establecer HP: {0}",
"fun_menu_speed": "Establecer Velocidad",
"fun_menu_speed_player": "Establecer Velocidad: {0}",
"fun_menu_gravity": "Establecer Gravedad",
"fun_menu_gravity_player": "Establecer Gravedad: {0}",
"fun_menu_money": "Establecer Dinero",
"fun_menu_money_player": "Establecer Dinero: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Velocidad {0}",
"fun_menu_gravity_value": "Gravedad {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} cambió el tamaño de {lightred}{1}{default} a {lightred}{2}{default}!",
"fun_menu_resize": "Cambiar Tamaño",
"fun_menu_resize_player": "Cambiar Tamaño: {0}",
"fun_menu_resize_value": "Tamaño {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "دستورات سرگرمی",
"fun_admin_give_message": "{lightred}{0}{default} {lightred}{2}{default} را به {lightred}{1}{default} داد!",
"fun_admin_strip_message": "{lightred}{0}{default} تمام سلاح‌های بازیکن {lightred}{1}{default} را گرفت!",
"fun_admin_hp_message": "{lightred}{0}{default} مقدار سلامت {lightred}{1}{default} را تغییر داد!",
"fun_admin_speed_message": "{lightred}{0}{default} سرعت {lightred}{1}{default} را تغییر داد!",
"fun_admin_gravity_message": "{lightred}{0}{default} جاذبه {lightred}{1}{default} را تغییر داد!",
"fun_admin_money_message": "{lightred}{0}{default} پول {lightred}{1}{default} را تغییر داد!",
"fun_admin_god_message": "{lightred}{0}{default} حالت خدا را برای {lightred}{1}{default} تغییر داد!",
"fun_admin_noclip_message": "{lightred}{0}{default} ناپدیدی را برای {lightred}{1}{default} فعال/غیرفعال کرد!",
"fun_admin_freeze_message": "{lightred}{0}{default} {lightred}{1}{default} را یخ‌زده کرد!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} {lightred}{1}{default} را از حالت یخ خارج کرد!",
"fun_admin_respawn_message": "{lightred}{0}{default} {lightred}{1}{default} را دوباره زنده کرد!",
"fun_menu_god": "حالت خدا",
"fun_menu_noclip": "ناپدیدی",
"fun_menu_respawn": "احیا",
"fun_menu_give": "دادن سلاح",
"fun_menu_give_player": "دادن سلاح: {0}",
"fun_menu_strip": "گرفتن سلاح‌ها",
"fun_menu_freeze": "یخ زدن",
"fun_menu_hp": "تنظیم سلامت",
"fun_menu_hp_player": "تنظیم سلامت: {0}",
"fun_menu_speed": "تنظیم سرعت",
"fun_menu_speed_player": "تنظیم سرعت: {0}",
"fun_menu_gravity": "تنظیم جاذبه",
"fun_menu_gravity_player": "تنظیم جاذبه: {0}",
"fun_menu_money": "تنظیم پول",
"fun_menu_money_player": "تنظیم پول: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "سرعت {0}",
"fun_menu_gravity_value": "جاذبه {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} اندازه {lightred}{1}{default} را به {lightred}{2}{default} تغییر داد!",
"fun_menu_resize": "تغییر اندازه",
"fun_menu_resize_player": "تغییر اندازه: {0}",
"fun_menu_resize_value": "اندازه {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Commandes Amusantes",
"fun_admin_give_message": "{lightred}{0}{default} a donné {lightred}{2}{default} à {lightred}{1}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} a retiré toutes les armes de {lightred}{1}{default}!",
"fun_admin_hp_message": "{lightred}{0}{default} a modifié la quantité de HP de {lightred}{1}{default}!",
"fun_admin_speed_message": "{lightred}{0}{default} a modifié la vitesse de {lightred}{1}{default}!",
"fun_admin_gravity_message": "{lightred}{0}{default} a modifié la gravité de {lightred}{1}{default}!",
"fun_admin_money_message": "{lightred}{0}{default} a modifié l'argent de {lightred}{1}{default}!",
"fun_admin_god_message": "{lightred}{0}{default} a modifié le mode dieu de {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} a activé/désactivé le noclip pour {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} a gelé {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} a dégivré {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} a réapparu {lightred}{1}{default}!",
"fun_menu_god": "Mode Dieu",
"fun_menu_noclip": "Noclip",
"fun_menu_respawn": "Réapparition",
"fun_menu_give": "Donner Arme",
"fun_menu_give_player": "Donner Arme: {0}",
"fun_menu_strip": "Retirer Armes",
"fun_menu_freeze": "Geler",
"fun_menu_hp": "Définir HP",
"fun_menu_hp_player": "Définir HP: {0}",
"fun_menu_speed": "Définir Vitesse",
"fun_menu_speed_player": "Définir Vitesse: {0}",
"fun_menu_gravity": "Définir Gravité",
"fun_menu_gravity_player": "Définir Gravité: {0}",
"fun_menu_money": "Définir Argent",
"fun_menu_money_player": "Définir Argent: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Vitesse {0}",
"fun_menu_gravity_value": "Gravité {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} a redimensionné {lightred}{1}{default} à {lightred}{2}{default}!",
"fun_menu_resize": "Redimensionner",
"fun_menu_resize_player": "Redimensionner: {0}",
"fun_menu_resize_value": "Taille {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Izklaidējošas Komandas",
"fun_admin_give_message": "{lightred}{0}{default} iedeva {lightred}{1}{default} {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} noņēma visus {lightred}{1}{default} ieročus!",
"fun_admin_hp_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} HP daudzumu!",
"fun_admin_speed_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} ātrumu!",
"fun_admin_gravity_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} gravitāciju!",
"fun_admin_money_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} naudu!",
"fun_admin_god_message": "{lightred}{0}{default} mainīja dieva režīmu priekš {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} aktivizēja/deaktivizēja noclip priekš {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} sasaldēja {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} atkausēja {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} atdzīvināja {lightred}{1}{default}!",
"fun_menu_god": "Dieva Režīms",
"fun_menu_noclip": "Noclip",
"fun_menu_respawn": "Atdzīvināt",
"fun_menu_give": "Dot Ieroci",
"fun_menu_give_player": "Dot Ieroci: {0}",
"fun_menu_strip": "Noņemt Ieročus",
"fun_menu_freeze": "Sasaldēt",
"fun_menu_hp": "Uzstādīt HP",
"fun_menu_hp_player": "Uzstādīt HP: {0}",
"fun_menu_speed": "Uzstādīt Ātrumu",
"fun_menu_speed_player": "Uzstādīt Ātrumu: {0}",
"fun_menu_gravity": "Uzstādīt Gravitāciju",
"fun_menu_gravity_player": "Uzstādīt Gravitāciju: {0}",
"fun_menu_money": "Uzstādīt Naudu",
"fun_menu_money_player": "Uzstādīt Naudu: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Ātrums {0}",
"fun_menu_gravity_value": "Gravitācija {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} izmēru uz {lightred}{2}{default}!",
"fun_menu_resize": "Mainīt Izmēru",
"fun_menu_resize_player": "Mainīt Izmēru: {0}",
"fun_menu_resize_value": "Izmērs {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Komendy Rozrywkowe",
"fun_admin_give_message": "{lightred}{0}{default} dał {lightred}{1}{default} przedmiot {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} zabrał wszystkie bronie {lightred}{1}{default}!",
"fun_admin_hp_message": "{lightred}{0}{default} zmienił ilość hp dla {lightred}{1}{default}!",
"fun_admin_speed_message": "{lightred}{0}{default} zmienił prędkość dla {lightred}{1}{default}!",
"fun_admin_gravity_message": "{lightred}{0}{default} zmienił grawitację dla {lightred}{1}{default}!",
"fun_admin_money_message": "{lightred}{0}{default} zmienił pieniądze dla {lightred}{1}{default}!",
"fun_admin_god_message": "{lightred}{0}{default} zmienił tryb Boga dla {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} ustawił latanie dla {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} zamroził {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} odmroził {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} odrodził {lightred}{1}{default}!",
"fun_menu_god": "Tryb Boga",
"fun_menu_noclip": "Latanie",
"fun_menu_respawn": "Odrodzenie",
"fun_menu_give": "Daj Broń",
"fun_menu_give_player": "Daj Broń: {0}",
"fun_menu_strip": "Zabierz Bronie",
"fun_menu_freeze": "Zamrożenie",
"fun_menu_hp": "Ustaw HP",
"fun_menu_hp_player": "Ustaw HP: {0}",
"fun_menu_speed": "Ustaw Prędkość",
"fun_menu_speed_player": "Ustaw Prędkość: {0}",
"fun_menu_gravity": "Ustaw Grawitację",
"fun_menu_gravity_player": "Ustaw Grawitację: {0}",
"fun_menu_money": "Ustaw Pieniądze",
"fun_menu_money_player": "Ustaw Pieniądze: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Prędkość {0}",
"fun_menu_gravity_value": "Grawitacja {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} zmienił rozmiar {lightred}{1}{default} na {lightred}{2}{default}!",
"fun_menu_resize": "Zmień Rozmiar",
"fun_menu_resize_player": "Zmień Rozmiar: {0}",
"fun_menu_resize_value": "Rozmiar {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Comandos Divertidos",
"fun_admin_give_message": "{lightred}{0}{default} deu {lightred}{1}{default} um(a) {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} tirou todas as armas de {lightred}{1}{default}!",
"fun_admin_hp_message": "{lightred}{0}{default} mudou a quantidade de HP de {lightred}{1}{default}!",
"fun_admin_speed_message": "{lightred}{0}{default} mudou a velocidade de {lightred}{1}{default}!",
"fun_admin_gravity_message": "{lightred}{0}{default} mudou a gravidade de {lightred}{1}{default}!",
"fun_admin_money_message": "{lightred}{0}{default} mudou a quantidade de dinheiro de {lightred}{1}{default}!",
"fun_admin_god_message": "{lightred}{0}{default} mudou o modo Deus de {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} ativou/desativou o noclip para {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} congelou {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} descongelou {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} reanimou {lightred}{1}{default}!",
"fun_menu_god": "Modo Deus",
"fun_menu_noclip": "Noclip",
"fun_menu_respawn": "Reanimar",
"fun_menu_give": "Dar Arma",
"fun_menu_give_player": "Dar Arma: {0}",
"fun_menu_strip": "Remover Armas",
"fun_menu_freeze": "Congelar",
"fun_menu_hp": "Definir HP",
"fun_menu_hp_player": "Definir HP: {0}",
"fun_menu_speed": "Definir Velocidade",
"fun_menu_speed_player": "Definir Velocidade: {0}",
"fun_menu_gravity": "Definir Gravidade",
"fun_menu_gravity_player": "Definir Gravidade: {0}",
"fun_menu_money": "Definir Dinheiro",
"fun_menu_money_player": "Definir Dinheiro: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Velocidade {0}",
"fun_menu_gravity_value": "Gravidade {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} redimensionou {lightred}{1}{default} para {lightred}{2}{default}!",
"fun_menu_resize": "Redimensionar",
"fun_menu_resize_player": "Redimensionar: {0}",
"fun_menu_resize_value": "Tamanho {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Comandos Divertidos",
"fun_admin_give_message": "{lightred}{0}{default} deu {lightred}{1}{default} um(a) {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} tirou todas as armas de {lightred}{1}{default}!",
"fun_admin_hp_message": "{lightred}{0}{default} mudou a quantidade de HP de {lightred}{1}{default}!",
"fun_admin_speed_message": "{lightred}{0}{default} mudou a velocidade de {lightred}{1}{default}!",
"fun_admin_gravity_message": "{lightred}{0}{default} mudou a gravidade de {lightred}{1}{default}!",
"fun_admin_money_message": "{lightred}{0}{default} mudou a quantidade de dinheiro de {lightred}{1}{default}!",
"fun_admin_god_message": "{lightred}{0}{default} mudou o modo Deus de {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} ativou/desativou o noclip para {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} congelou {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} descongelou {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} reanimou {lightred}{1}{default}!",
"fun_menu_god": "Modo Deus",
"fun_menu_noclip": "Noclip",
"fun_menu_respawn": "Reanimar",
"fun_menu_give": "Dar Arma",
"fun_menu_give_player": "Dar Arma: {0}",
"fun_menu_strip": "Remover Armas",
"fun_menu_freeze": "Congelar",
"fun_menu_hp": "Definir HP",
"fun_menu_hp_player": "Definir HP: {0}",
"fun_menu_speed": "Definir Velocidade",
"fun_menu_speed_player": "Definir Velocidade: {0}",
"fun_menu_gravity": "Definir Gravidade",
"fun_menu_gravity_player": "Definir Gravidade: {0}",
"fun_menu_money": "Definir Dinheiro",
"fun_menu_money_player": "Definir Dinheiro: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Velocidade {0}",
"fun_menu_gravity_value": "Gravidade {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} redimensionou {lightred}{1}{default} para {lightred}{2}{default}!",
"fun_menu_resize": "Redimensionar",
"fun_menu_resize_player": "Redimensionar: {0}",
"fun_menu_resize_value": "Tamanho {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Развлекательные Команды",
"fun_admin_give_message": "{lightred}{0}{default} дал {lightred}{1}{default} {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} забрал все оружие у {lightred}{1}{default}!",
"fun_admin_hp_message": "{lightred}{0}{default} изменил количество HP у {lightred}{1}{default}!",
"fun_admin_speed_message": "{lightred}{0}{default} изменил скорость {lightred}{1}{default}!",
"fun_admin_gravity_message": "{lightred}{0}{default} изменил гравитацию для {lightred}{1}{default}!",
"fun_admin_money_message": "{lightred}{0}{default} изменил количество денег у {lightred}{1}{default}!",
"fun_admin_god_message": "{lightred}{0}{default} изменил режим бога для {lightred}{1}{default}!",
"fun_admin_noclip_message": "{lightred}{0}{default} включил/выключил noclip для {lightred}{1}{default}!",
"fun_admin_freeze_message": "{lightred}{0}{default} заморозил {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} разморозил {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} возродил {lightred}{1}{default}!",
"fun_menu_god": "Режим Бога",
"fun_menu_noclip": "Noclip",
"fun_menu_respawn": "Возрождение",
"fun_menu_give": "Выдать Оружие",
"fun_menu_give_player": "Выдать Оружие: {0}",
"fun_menu_strip": "Забрать Оружие",
"fun_menu_freeze": "Заморозить",
"fun_menu_hp": "Установить HP",
"fun_menu_hp_player": "Установить HP: {0}",
"fun_menu_speed": "Установить Скорость",
"fun_menu_speed_player": "Установить Скорость: {0}",
"fun_menu_gravity": "Установить Гравитацию",
"fun_menu_gravity_player": "Установить Гравитацию: {0}",
"fun_menu_money": "Установить Деньги",
"fun_menu_money_player": "Установить Деньги: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Скорость {0}",
"fun_menu_gravity_value": "Гравитация {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} изменил размер {lightred}{1}{default} на {lightred}{2}{default}!",
"fun_menu_resize": "Изменить Размер",
"fun_menu_resize_player": "Изменить Размер: {0}",
"fun_menu_resize_value": "Размер {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "Eğlence Komutları",
"fun_admin_give_message": "{lightred}{0}{default} {lightred}{1}{default}'e {lightred}{2}{default} verdi!",
"fun_admin_strip_message": "{lightred}{0}{default} {lightred}{1}{default}'in tüm silahlarını aldı!",
"fun_admin_hp_message": "{lightred}{0}{default} {lightred}{1}{default}'in HP miktarını değiştirdi!",
"fun_admin_speed_message": "{lightred}{0}{default} {lightred}{1}{default}'in hızını değiştirdi!",
"fun_admin_gravity_message": "{lightred}{0}{default} {lightred}{1}{default}'in yer çekimini değiştirdi!",
"fun_admin_money_message": "{lightred}{0}{default} {lightred}{1}{default}'in parasını değiştirdi!",
"fun_admin_god_message": "{lightred}{0}{default} {lightred}{1}{default}'in tanrı modunu değiştirdi!",
"fun_admin_noclip_message": "{lightred}{0}{default} {lightred}{1}{default} için noclip'i açtı/kapatı!",
"fun_admin_freeze_message": "{lightred}{0}{default} {lightred}{1}{default}'i dondurdu!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} {lightred}{1}{default}'in dondurmasını çözdü!",
"fun_admin_respawn_message": "{lightred}{0}{default} {lightred}{1}{default}'i yeniden doğurdu!",
"fun_menu_god": "Tanrı Modu",
"fun_menu_noclip": "Noclip",
"fun_menu_respawn": "Yeniden Doğma",
"fun_menu_give": "Silah Ver",
"fun_menu_give_player": "Silah Ver: {0}",
"fun_menu_strip": "Silahları Al",
"fun_menu_freeze": "Dondur",
"fun_menu_hp": "HP Ayarla",
"fun_menu_hp_player": "HP Ayarla: {0}",
"fun_menu_speed": "Hız Ayarla",
"fun_menu_speed_player": "Hız Ayarla: {0}",
"fun_menu_gravity": "Yer Çekimi Ayarla",
"fun_menu_gravity_player": "Yer Çekimi Ayarla: {0}",
"fun_menu_money": "Para Ayarla",
"fun_menu_money_player": "Para Ayarla: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "Hız {0}",
"fun_menu_gravity_value": "Yer Çekimi {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} {lightred}{1}{default}'in boyutunu {lightred}{2}{default} olarak değiştirdi!",
"fun_menu_resize": "Boyut Değiştir",
"fun_menu_resize_player": "Boyut Değiştir: {0}",
"fun_menu_resize_value": "Boyut {0}"
}

View File

@@ -0,0 +1,40 @@
{
"fun_category_name": "趣味命令",
"fun_admin_give_message": "{lightred}{0}{default} 给了 {lightred}{1}{default} {lightred}{2}{default}!",
"fun_admin_strip_message": "{lightred}{0}{default} 移除了 {lightred}{1}{default} 的所有武器!",
"fun_admin_hp_message": "{lightred}{0}{default} 修改了 {lightred}{1}{default} 的生命值!",
"fun_admin_speed_message": "{lightred}{0}{default} 修改了 {lightred}{1}{default} 的速度!",
"fun_admin_gravity_message": "{lightred}{0}{default} 修改了 {lightred}{1}{default} 的重力!",
"fun_admin_money_message": "{lightred}{0}{default} 修改了 {lightred}{1}{default} 的金钱!",
"fun_admin_god_message": "{lightred}{0}{default} 切换了 {lightred}{1}{default} 的上帝模式!",
"fun_admin_noclip_message": "{lightred}{0}{default} 切换了 {lightred}{1}{default} 的穿墙模式!",
"fun_admin_freeze_message": "{lightred}{0}{default} 冻结了 {lightred}{1}{default}!",
"fun_admin_unfreeze_message": "{lightred}{0}{default} 解冻了 {lightred}{1}{default}!",
"fun_admin_respawn_message": "{lightred}{0}{default} 复活了 {lightred}{1}{default}!",
"fun_menu_god": "上帝模式",
"fun_menu_noclip": "穿墙模式",
"fun_menu_respawn": "复活",
"fun_menu_give": "给予武器",
"fun_menu_give_player": "给予武器: {0}",
"fun_menu_strip": "移除武器",
"fun_menu_freeze": "冻结",
"fun_menu_hp": "设置生命值",
"fun_menu_hp_player": "设置生命值: {0}",
"fun_menu_speed": "设置速度",
"fun_menu_speed_player": "设置速度: {0}",
"fun_menu_gravity": "设置重力",
"fun_menu_gravity_player": "设置重力: {0}",
"fun_menu_money": "设置金钱",
"fun_menu_money_player": "设置金钱: {0}",
"fun_menu_hp_value": "{0} HP",
"fun_menu_speed_value": "速度 {0}",
"fun_menu_gravity_value": "重力 {0}",
"fun_menu_money_value": "${0}",
"fun_admin_resize_message": "{lightred}{0}{default} 将 {lightred}{1}{default} 的大小改为 {lightred}{2}{default}!",
"fun_menu_resize": "调整大小",
"fun_menu_resize_player": "调整大小: {0}",
"fun_menu_resize_value": "大小 {0}"
}

View File

@@ -0,0 +1,384 @@
# CS2-SimpleAdmin Fun Commands Module
This module serves as a **reference implementation** for creating CS2-SimpleAdmin modules. It demonstrates best practices for menu creation, command registration, translation support, and API usage.
## 📚 What This Module Teaches
This module is designed to be educational and shows you how to:
1.**Register commands dynamically** from configuration
2.**Create menu categories** and menu items
3.**Use per-player translations** with `ShowAdminActivityLocalized`
4.**Handle player targeting** and validation
5.**Implement proper cleanup** on module unload
6.**Structure code** using partial classes for organization
7.**Cache data** for performance (weapons cache)
8.**Use configuration** to enable/disable features
## 🎯 Features
This module provides fun admin commands:
- **God Mode** (`css_god`) - Toggle god mode for players
- **No Clip** (`css_noclip`) - Enable no-clip mode
- **Freeze/Unfreeze** (`css_freeze`, `css_unfreeze`) - Freeze players in place
- **Respawn** (`css_respawn`) - Respawn dead players
- **Give Weapon** (`css_give`) - Give weapons to players
- **Strip Weapons** (`css_strip`) - Remove all player weapons
- **Set HP** (`css_hp`) - Set player health
- **Set Speed** (`css_speed`) - Modify player movement speed
- **Set Gravity** (`css_gravity`) - Change player gravity
- **Set Money** (`css_money`) - Set player money
## 📁 File Structure
```
CS2-SimpleAdmin_FunCommands/
├── CS2-SimpleAdmin_FunCommands.cs # Main plugin file - initialization, registration
├── Commands.cs # Command handlers
├── Actions.cs # Action methods (God, NoClip, Freeze, etc.)
├── Menus.cs # Menu creation using SimpleAdmin API
├── Config.cs # Configuration with command lists
└── lang/ # Translation files (13 languages)
├── en.json
├── pl.json
├── ru.json
└── ... (10 more languages)
```
## 🔍 Code Organization Explained
### 1. Main Plugin File (`CS2-SimpleAdmin_FunCommands.cs`)
**Key Concepts Demonstrated:**
```csharp
public partial class CS2_SimpleAdmin_FunCommands : BasePlugin, IPluginConfig<Config>
{
// ✅ BEST PRACTICE: Use capability system to get API
private ICS2_SimpleAdminApi? _sharedApi;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
// ✅ BEST PRACTICE: Cache expensive data
private static Dictionary<int, CsItem>? _weaponsCache;
// ✅ BEST PRACTICE: Track menu registration state
private bool _menusRegistered = false;
public override void OnAllPluginsLoaded(bool hotReload)
{
// Get the API
_sharedApi = _pluginCapability.Get();
// Register commands
RegisterFunCommands();
// ✅ BEST PRACTICE: Wait for SimpleAdmin to be ready before registering menus
_sharedApi.OnSimpleAdminReady += RegisterFunMenus;
RegisterFunMenus(); // Fallback for hot reload
}
}
```
**Why partial classes?**
- Separates concerns (commands, actions, menus)
- Makes code easier to navigate
- Each file has a specific purpose
### 2. Configuration (`Config.cs`)
**Key Concept:** Command lists for flexibility
```csharp
public class Config : IBasePluginConfig
{
// ✅ BEST PRACTICE: Allow multiple command aliases
public List<string> NoclipCommands { get; set; } = ["css_noclip"];
public List<string> GodCommands { get; set; } = ["css_god"];
// ... more command lists
}
```
**Benefits:**
- Users can disable features by emptying the list
- Users can add command aliases (e.g., `["css_god", "css_godmode"]`)
- Menus only register if commands exist
### 3. Commands (`Commands.cs`)
**Key Concepts Demonstrated:**
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/cheats")]
private void OnGodCommand(CCSPlayerController? caller, CommandInfo command)
{
// ✅ BEST PRACTICE: Use API to get targets (handles target syntax)
var targets = _sharedApi!.GetTarget(command);
if (targets == null) return;
// ✅ BEST PRACTICE: Filter for alive players
var playersToTarget = targets.Players.Where(player =>
player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
// ✅ BEST PRACTICE: Check targeting permissions
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
God(caller, player);
}
});
// ✅ BEST PRACTICE: Always log commands
_sharedApi.LogCommand(caller, command);
}
```
### 4. Actions (`Actions.cs`)
**Key Concepts Demonstrated:**
```csharp
private void God(CCSPlayerController? caller, CCSPlayerController player)
{
// Perform the action
if (!GodPlayers.Add(player.Slot))
{
GodPlayers.Remove(player.Slot);
}
// ✅ BEST PRACTICE: Use per-player language support
var activityArgs = new object[] { "CALLER", player.PlayerName };
if (caller == null || !_sharedApi!.IsAdminSilent(caller))
{
if (Localizer != null)
{
// Each player sees message in their configured language!
_sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_god_message", callerName, false, activityArgs);
}
}
// ✅ BEST PRACTICE: Log the action
_sharedApi!.LogCommand(caller, $"css_god {player.PlayerName}");
}
```
### 5. Menus (`Menus.cs`)
**Key Concepts Demonstrated:**
#### Simple Player Selection Menu
```csharp
private object CreateGodModeMenu(CCSPlayerController admin)
{
// ✅ BEST PRACTICE: Use CreateMenuWithPlayers for simple player selection
return _sharedApi!.CreateMenuWithPlayers("God Mode", "fun", admin,
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player),
God); // Direct method reference
}
```
#### Nested Menu with Value Selection
```csharp
private object CreateSetHpMenu(CCSPlayerController admin)
{
// ✅ BEST PRACTICE: Use CreateMenuWithBack for menus with back button
var menu = _sharedApi!.CreateMenuWithBack("Set HP", "fun", admin);
var players = _sharedApi.GetValidPlayers().Where(p =>
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
foreach (var player in players)
{
// ✅ BEST PRACTICE: AddSubMenu automatically adds back button to submenu
_sharedApi.AddSubMenu(menu, playerName, p => CreateHpSelectionMenu(admin, player));
}
return menu;
}
private object CreateHpSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var hpMenu = _sharedApi!.CreateMenuWithBack($"Set HP: {target.PlayerName}", "fun", admin);
var hpValues = new[] { 1, 10, 25, 50, 100, 200, 500, 999 };
foreach (var hp in hpValues)
{
// ✅ BEST PRACTICE: AddMenuOption for simple actions
_sharedApi.AddMenuOption(hpMenu, $"{hp} HP", _ =>
{
// ✅ BEST PRACTICE: Always validate before executing
if (target.IsValid)
{
target.SetHp(hp);
LogAndShowActivity(admin, target, "fun_admin_hp_message", "css_hp", hp.ToString());
}
});
}
return hpMenu;
}
```
### 6. Translations
**Key Concept:** Module-specific translations
```json
// lang/en.json
{
"fun_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!",
"fun_admin_hp_message": "{lightred}{0}{default} changed {lightred}{1}{default} hp amount!"
}
```
**Why module translations?**
- Your module is independent from SimpleAdmin
- You can update translations without affecting main plugin
- Each player sees messages in their language automatically
## 🛠️ How to Use This as a Template
### Step 1: Copy the Module
```bash
cp -r CS2-SimpleAdmin_FunCommands YourModuleName
```
### Step 2: Rename Files
- Rename `.csproj` file
- Rename all `.cs` files to match your module name
- Update namespace in all files
### Step 3: Update References
- Change `namespace CS2_SimpleAdmin_FunCommands` to `namespace YourModuleName`
- Update plugin metadata (name, version, author, description)
### Step 4: Modify Config
```csharp
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
// Add your own command lists
public List<string> YourCommands { get; set; } = ["css_yourcommand"];
}
```
### Step 5: Add Your Commands
Look at `Commands.cs` for examples of command handlers
### Step 6: Add Your Menus
Look at `Menus.cs` for examples of menu creation
### Step 7: Add Translations
Create language files in `lang/{language}.json` (e.g., `lang/en.json`, `lang/pl.json`)
## 📖 Learning Path
If you're new to module development, study files in this order:
1. **Config.cs** - Understand configuration structure
2. **CS2-SimpleAdmin_FunCommands.cs** - See initialization and API acquisition
3. **Commands.cs** - Learn command registration and handling
4. **Actions.cs** - Understand action methods and translations
5. **Menus.cs** - Study menu creation patterns
## 🎓 Best Practices Demonstrated
### ✅ Command Registration
```csharp
// Dynamic registration based on config
if (Config.GodCommands.Count > 0)
{
foreach (var command in Config.GodCommands)
{
_sharedApi.RegisterCommand(command, "Enable god mode", OnGodCommand);
}
}
```
### ✅ Target Validation
```csharp
// Always check if player can be targeted
if (!caller.CanTarget(player)) return;
// Always validate player state
if (!player.IsValid) return;
```
### ✅ Translation Usage
```csharp
// Use module's localizer for per-player language support
if (Localizer != null)
{
_sharedApi.ShowAdminActivityLocalized(Localizer, "translation_key", callerName, false, args);
}
```
### ✅ Cleanup on Unload
```csharp
public override void Unload(bool hotReload)
{
// Unregister all commands
if (Config.GodCommands.Count > 0)
{
foreach (var command in Config.GodCommands)
{
_sharedApi.UnRegisterCommand(command);
}
}
// Unregister all menus
if (Config.GodCommands.Count > 0)
_sharedApi.UnregisterMenu("fun", "god");
// Remove event handlers
_sharedApi.OnSimpleAdminReady -= RegisterFunMenus;
}
```
### ✅ Data Caching
```csharp
// Cache expensive operations
private static Dictionary<int, CsItem> GetWeaponsCache()
{
if (_weaponsCache != null) return _weaponsCache;
var weaponsArray = Enum.GetValues(typeof(CsItem));
_weaponsCache = new Dictionary<int, CsItem>();
// ... populate cache
return _weaponsCache;
}
```
## 🔗 Related Documentation
- **[MODULE_DEVELOPMENT.md](../MODULE_DEVELOPMENT.md)** - Complete API reference
- **[TRANSLATION_EXAMPLE.md](../TRANSLATION_EXAMPLE.md)** - Translation usage guide
- **[CS2-SimpleAdminApi](../../CS2-SimpleAdminApi/)** - API interface definitions
## 💡 Tips
1. **Start Simple** - Begin with one command and one menu, then expand
2. **Test Thoroughly** - Test with multiple players, different permissions, and languages
3. **Handle Errors** - Always validate player state before actions
4. **Log Everything** - Use `_sharedApi.LogCommand()` for all admin actions
5. **Use Partial Classes** - Split your code into logical files
6. **Comment Your Code** - Especially if others will use it as reference
## 🐛 Common Mistakes to Avoid
-**Don't** forget to check `IsValid` before accessing player properties
-**Don't** skip permission checks (`CanTarget`, `RequiresPermissions`)
-**Don't** forget to unregister commands/menus in `Unload()`
-**Don't** use SimpleAdmin's translations - create your own
-**Don't** hardcode command names - use config lists instead
## 🤝 Contributing
If you improve this module or find better patterns, please contribute back so others can learn!
## 📄 License
This module is provided as-is for educational purposes. Feel free to use it as a template for your own modules.

View File

@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
}
}

View File

@@ -0,0 +1,602 @@
# Module Development Guide - CS2-SimpleAdmin
> **🎓 New to module development?** Start with the [Fun Commands Module](./CS2-SimpleAdmin_FunCommands/) - it's a fully documented reference implementation showing all best practices!
This guide explains how to create modules for CS2-SimpleAdmin with custom commands, menus, and translations.
## 📖 Table of Contents
1. [Quick Start](#quick-start)
2. [Learning Resources](#learning-resources)
3. [API Methods Reference](#api-methods)
4. [Menu Patterns](#menu-patterns)
5. [Best Practices](#best-practices)
6. [Common Patterns](#common-patterns)
7. [Troubleshooting](#troubleshooting)
## 🚀 Quick Start
### Step 1: Study the Example Module
The **[CS2-SimpleAdmin_FunCommands](./CS2-SimpleAdmin_FunCommands/)** module is your best learning resource. It demonstrates:
✅ Command registration from config
✅ Dynamic menu creation
✅ Per-player translation support
✅ Proper cleanup on unload
✅ Code organization with partial classes
✅ All menu patterns you'll need
**Start here:** Read [`CS2-SimpleAdmin_FunCommands/README.md`](./CS2-SimpleAdmin_FunCommands/README.md)
### Step 2: Create Your Module Structure
```
YourModule/
├── YourModule.csproj # Project file
├── YourModule.cs # Main plugin class
├── Commands.cs # Command handlers (optional)
├── Menus.cs # Menu creation (optional)
├── Config.cs # Configuration
└── lang/ # Translations
├── en.json
├── pl.json
└── ru.json
```
### Step 3: Minimal Working Example
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
public class MyModule : BasePlugin
{
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found");
return;
}
// Register menus after API is ready
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Fallback for hot reload
}
private void RegisterMenus()
{
if (_api == null) return;
// 1. Register a new category
_api.RegisterMenuCategory("mymodule", "My Module", "@css/generic");
// 2. Register menu items in the category
_api.RegisterMenu("mymodule", "action1", "Action 1", CreateAction1Menu, "@css/generic");
_api.RegisterMenu("mymodule", "action2", "Action 2", CreateAction2Menu, "@css/kick");
}
private object CreateAction1Menu(CCSPlayerController admin)
{
// Create a menu with automatic back button
var menu = _api!.CreateMenuWithBack("Action 1 Menu", "mymodule", admin);
// Add menu options
_api.AddMenuOption(menu, "Option 1", player =>
{
player.PrintToChat("You selected Option 1");
});
_api.AddMenuOption(menu, "Option 2", player =>
{
player.PrintToChat("You selected Option 2");
});
return menu;
}
private object CreateAction2Menu(CCSPlayerController admin)
{
// Use the built-in player selection menu
return _api!.CreateMenuWithPlayers("Select Player", "mymodule", admin,
player => player.IsValid && admin.CanTarget(player),
(adminPlayer, targetPlayer) =>
{
adminPlayer.PrintToChat($"You selected {targetPlayer.PlayerName}");
});
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Clean up registered menus
_api.UnregisterMenu("mymodule", "action1");
_api.UnregisterMenu("mymodule", "action2");
_api.OnSimpleAdminReady -= RegisterMenus;
}
}
```
## 📚 Learning Resources
### For Beginners
1. **Start Here:** [`CS2-SimpleAdmin_FunCommands/README.md`](./CS2-SimpleAdmin_FunCommands/README.md)
- Explains every file and pattern
- Shows code organization
- Demonstrates all menu types
2. **Read the Code:** Study these files in order:
- `Config.cs` - Simple configuration
- `CS2-SimpleAdmin_FunCommands.cs` - Plugin initialization
- `Commands.cs` - Command registration
- `Menus.cs` - Menu creation patterns
3. **Translations:** [`TRANSLATION_EXAMPLE.md`](./TRANSLATION_EXAMPLE.md)
- How to use module translations
- Per-player language support
- Best practices
### Key Concepts
| Concept | What It Does | Example File |
|---------|-------------|--------------|
| **API Capability** | Get access to SimpleAdmin API | `CS2-SimpleAdmin_FunCommands.cs:37` |
| **Command Registration** | Register console commands | `Commands.cs:15-34` |
| **Menu Registration** | Add menus to admin menu | `CS2-SimpleAdmin_FunCommands.cs:130-141` |
| **Translations** | Per-player language support | `Actions.cs:20-31` |
| **Cleanup** | Unregister on plugin unload | `CS2-SimpleAdmin_FunCommands.cs:63-97` |
## 🎯 Menu Patterns
The FunCommands module demonstrates **3 essential menu patterns** you'll use in every module:
### Pattern 1: Simple Player Selection
**When to use:** Select a player and immediately execute an action
```csharp
// Example: God Mode menu
private object CreateGodModeMenu(CCSPlayerController admin)
{
return _api.CreateMenuWithPlayers(
"God Mode", // Title
"yourmodule", // Category ID
admin, // Admin
player => player.IsValid && admin.CanTarget(player), // Filter
(adminPlayer, target) => // Action
{
// Execute action immediately
ToggleGodMode(target);
});
}
```
**See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:21-29`
### Pattern 2: Nested Menu (Player → Value)
**When to use:** Select a player, then select a value/option for that player
```csharp
// Example: Set HP menu (player selection)
private object CreateSetHpMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Set HP", "yourmodule", admin);
var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
// AddSubMenu automatically adds back button to submenu
_api.AddSubMenu(menu, player.PlayerName,
p => CreateHpValueMenu(admin, player));
}
return menu;
}
// Example: Set HP menu (value selection)
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api.CreateMenuWithBack($"HP for {target.PlayerName}", "yourmodule", admin);
var values = new[] { 50, 100, 200, 500 };
foreach (var hp in values)
{
_api.AddMenuOption(menu, $"{hp} HP", _ =>
{
if (target.IsValid) // Always validate!
{
target.PlayerPawn.Value.Health = hp;
}
});
}
return menu;
}
```
**See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:134-173`
### Pattern 3: Nested Menu with Complex Data
**When to use:** Need to display more complex options (like weapons with icons, items with descriptions)
```csharp
// Example: Give Weapon menu
private object CreateGiveWeaponMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Give Weapon", "yourmodule", admin);
var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName,
p => CreateWeaponSelectionMenu(admin, player));
}
return menu;
}
private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api.CreateMenuWithBack($"Weapons for {target.PlayerName}", "yourmodule", admin);
// Use cached data for performance
foreach (var weapon in GetWeaponsCache())
{
_api.AddMenuOption(menu, weapon.Value.ToString(), _ =>
{
if (target.IsValid)
{
target.GiveNamedItem(weapon.Value);
}
});
}
return menu;
}
```
**See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:67-109`
## 📋 API Methods Reference
### 1. Category Management
#### `RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")`
Registers a new menu category that appears in the main admin menu.
**Parameters:**
- `categoryId` - Unique identifier for the category (e.g., "fun", "vip", "economy")
- `categoryName` - Display name shown in menu (e.g., "Fun Commands")
- `permission` - Required permission to see the category (default: "@css/generic")
**Example:**
```csharp
_api.RegisterMenuCategory("vip", "VIP Features", "@css/vip");
```
### 2. Menu Registration
#### `RegisterMenu(string categoryId, string menuId, string menuName, Func<CCSPlayerController, object> menuFactory, string? permission = null)`
Registers a menu item within a category.
**Parameters:**
- `categoryId` - The category to add this menu to
- `menuId` - Unique identifier for the menu
- `menuName` - Display name in the category menu
- `menuFactory` - Function that creates the menu when selected (receives admin player)
- `permission` - Optional permission required to see this menu item
**Example:**
```csharp
_api.RegisterMenu("fun", "godmode", "God Mode", CreateGodModeMenu, "@css/cheats");
```
#### `UnregisterMenu(string categoryId, string menuId)`
Removes a menu item from a category.
**Example:**
```csharp
_api.UnregisterMenu("fun", "godmode");
```
### 3. Menu Creation
#### `CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)`
Creates a menu with an automatic "Back" button that returns to the category menu.
**Parameters:**
- `title` - Menu title
- `categoryId` - Category this menu belongs to (for back navigation)
- `player` - The admin player viewing the menu
**Returns:** `object` (MenuBuilder instance)
**Example:**
```csharp
var menu = _api.CreateMenuWithBack("Weapon Selection", "fun", admin);
```
#### `CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin, Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)`
Creates a menu with a list of players, filtered and with automatic back button.
**Parameters:**
- `title` - Menu title
- `categoryId` - Category for back navigation
- `admin` - The admin player viewing the menu
- `filter` - Function to filter which players appear in the menu
- `onSelect` - Action to execute when a player is selected (receives admin and target)
**Returns:** `object` (MenuBuilder instance)
**Example:**
```csharp
return _api.CreateMenuWithPlayers("Select Player to Kick", "admin", admin,
player => player.IsValid && admin.CanTarget(player),
(adminPlayer, targetPlayer) =>
{
// Kick the selected player
Server.ExecuteCommand($"css_kick {targetPlayer.UserId}");
});
```
### 4. Menu Manipulation
#### `AddMenuOption(object menu, string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null)`
Adds a clickable option to a menu.
**Parameters:**
- `menu` - The menu object (from CreateMenuWithBack)
- `name` - Display name of the option
- `action` - Action to execute when clicked (receives the player who clicked)
- `disabled` - Whether the option is disabled (grayed out)
- `permission` - Optional permission required to see this option
**Example:**
```csharp
_api.AddMenuOption(menu, "Give AK-47", player =>
{
player.GiveNamedItem("weapon_ak47");
}, permission: "@css/cheats");
```
#### `AddSubMenu(object menu, string name, Func<CCSPlayerController, object> subMenuFactory, bool disabled = false, string? permission = null)`
Adds a submenu option that opens another menu. **Automatically adds a back button to the submenu.**
**Parameters:**
- `menu` - The parent menu
- `name` - Display name of the submenu option
- `subMenuFactory` - Function that creates the submenu (receives the player)
- `disabled` - Whether the option is disabled
- `permission` - Optional permission required
**Example:**
```csharp
_api.AddSubMenu(menu, "Weapon Category", player =>
{
var weaponMenu = _api.CreateMenuWithBack("Weapons", "fun", player);
_api.AddMenuOption(weaponMenu, "AK-47", p => p.GiveNamedItem("weapon_ak47"));
_api.AddMenuOption(weaponMenu, "AWP", p => p.GiveNamedItem("weapon_awp"));
return weaponMenu;
});
```
#### `OpenMenu(object menu, CCSPlayerController player)`
Opens a menu for a specific player.
**Example:**
```csharp
var menu = _api.CreateMenuWithBack("Custom Menu", "fun", player);
_api.AddMenuOption(menu, "Test", p => p.PrintToChat("Test!"));
_api.OpenMenu(menu, player);
```
## Advanced Examples
### Nested Menus with Player Selection
```csharp
private object CreateGiveWeaponMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Give Weapon", "fun", admin);
var players = _api.GetValidPlayers()
.Where(p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
foreach (var player in players)
{
// Add submenu for each player - automatic back button will be added
_api.AddSubMenu(menu, player.PlayerName, p => CreateWeaponSelectionMenu(admin, player));
}
return menu;
}
private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var weaponMenu = _api.CreateMenuWithBack($"Weapons for {target.PlayerName}", "fun", admin);
var weapons = new[] { "weapon_ak47", "weapon_m4a1", "weapon_awp", "weapon_deagle" };
foreach (var weapon in weapons)
{
_api.AddMenuOption(weaponMenu, weapon, _ =>
{
if (target.IsValid)
{
target.GiveNamedItem(weapon);
admin.PrintToChat($"Gave {weapon} to {target.PlayerName}");
}
});
}
return weaponMenu;
}
```
### Dynamic Menu with Value Selection
```csharp
private object CreateSetHpMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Set HP", "admin", admin);
var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, p => CreateHpValueMenu(admin, player));
}
return menu;
}
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var hpMenu = _api.CreateMenuWithBack($"HP for {target.PlayerName}", "admin", admin);
var hpValues = new[] { 1, 50, 100, 200, 500, 1000 };
foreach (var hp in hpValues)
{
_api.AddMenuOption(hpMenu, $"{hp} HP", _ =>
{
if (target.IsValid && target.PlayerPawn?.Value != null)
{
target.PlayerPawn.Value.Health = hp;
admin.PrintToChat($"Set {target.PlayerName} HP to {hp}");
}
});
}
return hpMenu;
}
```
### Permission-Based Menu Options
```csharp
private object CreateAdminToolsMenu(CCSPlayerController admin)
{
var menu = _api.CreateMenuWithBack("Admin Tools", "admin", admin);
// Only root admins can see this
_api.AddMenuOption(menu, "Dangerous Action", player =>
{
player.PrintToChat("Executing dangerous action...");
}, permission: "@css/root");
// All admins can see this
_api.AddMenuOption(menu, "Safe Action", player =>
{
player.PrintToChat("Executing safe action...");
}, permission: "@css/generic");
return menu;
}
```
## Best Practices
1. **Always check for API availability**
```csharp
if (_api == null) return;
```
2. **Validate player state before actions**
```csharp
if (target.IsValid && target.PlayerPawn?.Value != null)
{
// Safe to perform action
}
```
3. **Use descriptive category and menu IDs**
- Good: `"economy"`, `"vip_features"`, `"fun_commands"`
- Bad: `"cat1"`, `"menu"`, `"test"`
4. **Clean up on unload**
```csharp
public override void Unload(bool hotReload)
{
_api?.UnregisterMenu("mymodule", "mymenu");
_api.OnSimpleAdminReady -= RegisterMenus;
}
```
5. **Use appropriate permissions**
- `@css/generic` - All admins
- `@css/ban` - Admins who can ban
- `@css/kick` - Admins who can kick
- `@css/root` - Root admins only
- Custom permissions from your module
6. **Handle hot reload**
```csharp
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Fallback for hot reload case
```
## Automatic Back Button
The menu system **automatically adds a "Back" button** to all submenus created with:
- `CreateMenuWithBack()` - Returns to the category menu
- `AddSubMenu()` - Returns to the parent menu
You don't need to manually add back buttons - the system handles navigation automatically!
## Getting Valid Players
Use the API helper method to get valid, connected players:
```csharp
var players = _api.GetValidPlayers();
// With filtering
var alivePlayers = _api.GetValidPlayers()
.Where(p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE);
var targetablePlayers = _api.GetValidPlayers()
.Where(p => admin.CanTarget(p));
```
## Complete Module Example
See the `CS2-SimpleAdmin_FunCommands` module in the `Modules/` directory for a complete, production-ready example of:
- Category registration
- Multiple menu types
- Nested menus with automatic back buttons
- Player selection menus
- Permission-based access
- Proper cleanup on unload
## Troubleshooting
**Q: My category doesn't appear in the admin menu**
- Ensure you're calling `RegisterMenuCategory()` after the API is ready
- Check that the player has the required permission
- Verify the category has at least one menu registered with `RegisterMenu()`
**Q: Back button doesn't work**
- Make sure you're using `CreateMenuWithBack()` instead of creating menus manually
- The `categoryId` parameter must match the category you registered
- Use `AddSubMenu()` for nested menus - it handles back navigation automatically
**Q: Menu appears but is empty**
- Check that you're adding options with `AddMenuOption()` or `AddSubMenu()`
- Verify permission checks aren't filtering out all options
- Ensure player validation in filters isn't too restrictive
**Q: API is null in OnAllPluginsLoaded**
- Wait for the `OnSimpleAdminReady` event instead of immediate registration
- Make sure CS2-SimpleAdmin is loaded before your module

View File

@@ -0,0 +1,268 @@
# Module Translation Guide
> **🎓 New to translations?** This guide shows you how to add multi-language support to your module!
## Why Use Module Translations?
When you use SimpleAdmin API's translation system, **each player automatically sees messages in their preferred language**!
**Example:**
- 🇬🇧 **Player with English:** "Admin gave PlayerName a weapon!"
- 🇵🇱 **Player with Polish:** "Admin dał PlayerName broń!"
- 🇷🇺 **Player with Russian:** "Admin дал PlayerName оружие!"
**All from ONE line of code!**
## Quick Start
### Step 1: Create Your Translation Files
Create a `lang` folder in your module with translation files for each language:
```
YourModule/
└── lang/
├── en.json
├── pl.json
└── ru.json
```
**Example: `lang/en.json`**
```json
{
"yourmod_admin_action": "{lightred}{0}{default} performed action on {lightred}{1}{default}!"
}
```
**Example: `lang/pl.json`**
```json
{
"yourmod_admin_action": "{lightred}{0}{default} wykonał akcję na {lightred}{1}{default}!"
}
```
### Step 2: Use in Your Code
**✅ RECOMMENDED METHOD:** `ShowAdminActivityLocalized`
```csharp
// Show activity with per-player language support
var args = new object[] { "CALLER", targetPlayer.PlayerName };
if (admin == null || !_api.IsAdminSilent(admin))
{
if (Localizer != null)
{
// Each player sees this in their language!
_api.ShowAdminActivityLocalized(
Localizer, // Your module's localizer
"yourmod_admin_action", // Translation key
admin.PlayerName, // Caller name
false, // dontPublish
args); // Message arguments
}
}
```
That's it! SimpleAdmin handles the rest.
## Complete Example
```csharp
using CounterStrikeSharp.API.Core;
using CS2_SimpleAdminApi;
public partial class MyModule : BasePlugin
{
private ICS2_SimpleAdminApi? _api;
private void GiveWeaponToPlayer(CCSPlayerController admin, CCSPlayerController target, string weaponName)
{
// Give the weapon
target.GiveNamedItem(weaponName);
var callerName = admin.PlayerName;
// Show activity using module's localizer - each player sees it in their language!
if (admin == null || !_api!.IsAdminSilent(admin))
{
var args = new object[] { "CALLER", target.PlayerName, weaponName };
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(Localizer, "yourmod_admin_give_message", callerName, false, args);
}
}
// Log the command
_api!.LogCommand(admin, $"css_give {target.PlayerName} {weaponName}");
}
}
```
## 🔑 Important: The "CALLER" Placeholder
**Always use `"CALLER"` as the first argument** in your translation messages!
The API automatically replaces `"CALLER"` based on the server's `ShowActivityType` configuration:
| ShowActivityType | What Players See |
|-----------------|-----------------|
| `1` | Non-admins see "Admin", admins see real name |
| `2+` | Everyone sees real admin name |
**Example:**
```json
{
"yourmod_message": "{0} did something to {1}"
This will be replaced with "Admin" or admin's name
}
```
```csharp
var args = new object[] { "CALLER", targetPlayer.PlayerName };
// ↑ API replaces this automatically
```
## 💡 Pro Tips
### Tip 1: Use a Helper Method
Create a reusable helper to reduce code duplication:
```csharp
/// <summary>
/// Helper method to show activity and log command
/// Copy this to your module!
/// </summary>
private void LogAndShowActivity(
CCSPlayerController? caller,
CCSPlayerController target,
string translationKey,
string baseCommand,
params string[] extraArgs)
{
var callerName = caller?.PlayerName ?? "Console";
// Build args: "CALLER" + target name + any extra args
var args = new List<object> { "CALLER", target.PlayerName };
args.AddRange(extraArgs);
// Show activity with per-player language support
if (caller == null || !_api.IsAdminSilent(caller))
{
if (Localizer != null)
{
_api.ShowAdminActivityLocalized(
Localizer,
translationKey,
callerName,
false,
args.ToArray());
}
}
// Build and log command
var logCommand = $"{baseCommand} {target.PlayerName}";
if (extraArgs.Length > 0)
{
logCommand += $" {string.Join(" ", extraArgs)}";
}
_api.LogCommand(caller, logCommand);
}
```
**Usage:**
```csharp
// Simple action
LogAndShowActivity(admin, target, "yourmod_kick_message", "css_kick");
// Action with parameters
LogAndShowActivity(admin, target, "yourmod_hp_message", "css_hp", "100");
```
### Tip 2: Translation Key Naming Convention
Use a consistent prefix for your module:
```json
{
"yourmod_admin_action1": "...",
"yourmod_admin_action2": "...",
"yourmod_error_notarget": "..."
}
```
This prevents conflicts with other modules and makes it clear which module owns the translation.
### Tip 3: Color Formatting
Use CounterStrikeSharp color tags in your translations:
```json
{
"yourmod_message": "{lightred}{0}{default} gave {green}{1}{default} a {yellow}{2}{default}!"
}
```
**Available colors:**
- `{default}`, `{white}`, `{darkred}`, `{green}`, `{lightyellow}`
- `{lightblue}`, `{olive}`, `{lime}`, `{red}`, `{purple}`
- `{grey}`, `{yellow}`, `{gold}`, `{silver}`, `{blue}`
- `{darkblue}`, `{bluegrey}`, `{magenta}`, `{lightred}`, `{orange}`
## 📖 Real Example: Fun Commands Module
The **[CS2-SimpleAdmin_FunCommands](./CS2-SimpleAdmin_FunCommands/)** module is a perfect reference:
**Translation files:** `Modules/CS2-SimpleAdmin_FunCommands/lang/`
- Has 13 languages (en, pl, ru, de, fr, es, etc.)
- Shows proper key naming (`fun_admin_*`)
- Demonstrates color usage
**Code examples:** `Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Actions.cs`
- Lines 20-31: God mode with translations
- Lines 48-59: NoClip with translations
- Lines 76-86: Freeze with translations
**Helper method:** `Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Commands.cs:274-306`
## ❌ Common Mistakes
### Mistake 1: Forgetting "CALLER"
```csharp
// ❌ WRONG
var args = new object[] { admin.PlayerName, target.PlayerName };
// ✅ CORRECT
var args = new object[] { "CALLER", target.PlayerName };
```
### Mistake 2: Using SimpleAdmin's Translations
```csharp
// ❌ WRONG - Uses SimpleAdmin's keys
_api.ShowAdminActivity("sa_admin_kick", ...)
// ✅ CORRECT - Uses YOUR module's keys
_api.ShowAdminActivityLocalized(Localizer, "yourmod_kick", ...)
```
### Mistake 3: Not Checking Localizer
```csharp
// ❌ WRONG - Will crash if Localizer is null
_api.ShowAdminActivityLocalized(Localizer, "key", ...)
// ✅ CORRECT - Check first
if (Localizer != null)
{
_api.ShowAdminActivityLocalized(Localizer, "key", ...)
}
```
## 🔗 See Also
- **[MODULE_DEVELOPMENT.md](./MODULE_DEVELOPMENT.md)** - Complete module development guide
- **[CS2-SimpleAdmin_FunCommands/README.md](./CS2-SimpleAdmin_FunCommands/README.md)** - Reference implementation
- **[CounterStrikeSharp Localization](https://docs.cssharp.dev/guides/localization/)** - Official CSS localization docs
---
**Need help?** Study the FunCommands module - it demonstrates all these patterns correctly!

View File

@@ -1,3 +1,14 @@
<p align="center">
<a href="https://github.com/daffyyyy/CS2-SimpleAdmin/actions/workflows/build.yml">
<img src="https://github.com/daffyyyy/CS2-SimpleAdmin/actions/workflows/build.yml/badge.svg" alt="Build and Publish" />
</a>
<a href="https://github.com/daffyyyy/CS2-SimpleAdmin/releases/latest">
<img src="https://img.shields.io/github/v/release/daffyyyy/CS2-SimpleAdmin?color=orange" alt="GitHub Release" />
</a>
<img src="https://img.shields.io/badge/Made_with-a_lot_of_tea_%F0%9F%8D%B5-red" alt="Made with a lot of tea 🍵" />
</p>
[![Join the support Discord](https://img.shields.io/badge/Discord-Support-blue?logo=discord&logoColor=white)](https://discord.com/channels/1160907911501991946/1180627186865156126)
# CS2-SimpleAdmin
---
@@ -92,4 +103,4 @@ If you find CS2-SimpleAdmin useful, consider supporting the ongoing development:
---
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
This project is licensed under the GPL-3.0 license - see the [LICENSE](LICENSE) file for details.