Compare commits

...

384 Commits

Author SHA1 Message Date
Dawid Bepierszcz
0dded66e5d Fix closure issues in menus and update dependencies
Captured player and duration variables in menu callbacks to prevent closure-related bugs. Updated package versions in project files and bumped plugin version to 1.7.8-beta-5. Improved player validation and message localization logic.
2025-11-06 02:24:43 +01:00
Dawid Bepierszcz
038641dbdf Comment out MySQL index migration and remove Sqlite optimization
Commented out all index creation statements in the MySQL migration 016 for table and index optimization. Removed the corresponding Sqlite migration 016 entirely. Also replaced TRUNCATE TABLE with DELETE FROM in Sqlite migration 013 for sa_players_ips to improve compatibility.
2025-10-30 18:17:47 +01:00
Dawid Bepierszcz
a03964c08a Add per-player menu localization and refactor menus
Introduces per-player localization for menu categories and items using translation keys and IStringLocalizer, allowing modules and the main plugin to display menu names in the player's language. Refactors menu registration and builder logic to use translation keys, updates API and documentation, and adds database provider upsert query abstraction for player IPs. Also updates version to 1.7.8-beta-4 and corrects a translation string typo.
2025-10-30 01:41:08 +01:00
Dawid Bepierszcz
b0d8696756 Add CS2-SimpleAdmin documentation site
Introduces a new documentation site for CS2-SimpleAdmin using Docusaurus, including developer API references, tutorials, user guides, and module documentation. Removes the CleanModule example and updates FunCommands and ExampleModule. Also updates main plugin and API files to support new documentation and module structure.
2025-10-20 01:27:01 +02:00
Dawid Bepierszcz
21a5de6b3d Update StatusBlocker plugin to v1.1.0
Replaced StatusBlocker v1.0.9 binaries with v1.1.0 for both Linux and Windows in the METAMOD PLUGIN directory.
2025-10-19 18:01:41 +02:00
Dawid Bepierszcz
718536eaef Update player sound effects and StatusBlocker plugin
Changed sound effects for player suicide and slap commands in playercommands.cs. Replaced StatusBlocker v1.0.7 plugin files with v1.0.9 for both Linux and Windows in the StealthModule.
2025-10-19 16:53:17 +02:00
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
Dawid Bepierszcz
e2529cd646 Update build.yml 2025-10-03 01:44:23 +02:00
Dawid Bepierszcz
9d2cd34845 Update build.yml 2025-10-03 01:41:48 +02:00
Dawid Bepierszcz
5701455de0 Refactor database layer and add module/plugin improvements
Reworked the database layer to support both MySQL and SQLite via new provider classes and migration scripts for each backend. Updated the build workflow to support building and packaging additional modules, including StealthModule and BanSoundModule, and improved artifact handling. Refactored command registration to allow dynamic registration/unregistration and improved API event handling. Updated dependencies, project structure, and configuration checks for better reliability and extensibility. Added new language files, updated versioning, and removed obsolete files.

**⚠️ Warning: SQLite support is currently experimental.
Using this version requires reconfiguration of your database settings!
Plugin now uses UTC time. Please adjust your configurations accordingly!
**
2025-10-03 01:37:03 +02:00
Dawid Bepierszcz
b97426313b 1.7.7-alpha
- Fixed steamid only bans
- Added missing multiservermode
- Better caching
2025-05-25 01:00:14 +02:00
Dawid Bepierszcz
3ab63c05db 1.7.7-alpha-small-optimizations
- Clear bans cache on plugin reload
- Changed ip history to int
2025-05-23 03:28:10 +02:00
Dawid Bepierszcz
f654d6b085 1.7.7-alpha-connection-fix
- Fixed lag when player connecting
- Added command to force refresh ban cache (css_reloadbans)
2025-05-21 13:11:01 +02:00
Dawid Bepierszcz
676a18d9b4 1.7.7-alpha-associated-accounts (missing migration) 2025-05-21 03:13:05 +02:00
Dawid Bepierszcz
d75a092047 1.7.7-alpha-associated-accounts
- Added possibility to view player associated account (by ip)
2025-05-21 03:08:02 +02:00
Dawid Bepierszcz
d34ca64970 1.7.7-alpha-fix
- Added missing migration
- Added missing ignoredips
2025-05-21 00:38:54 +02:00
Dawid Bepierszcz
f69f1277f8 1.7.7-alpha
- Fixed (@all,@team,@lifestate) spam with discord webhook
- Currently `small` actions for ban optimizations
2025-05-20 23:51:59 +02:00
Dawid Bepierszcz
b6c876d709 1.7.6a
- Changed PawnIsAlive to LifeState
- Changed AdminCache - now it only removes flags added in the database - not all assigned to player
- Added config variable `IgnoredIps` to ignore ip check on connect, useful when proxying
2025-03-25 11:39:00 +01:00
Dawid Bepierszcz
888d6b0152 1.7.5a
- Changed api domain
- Changed link to wiki
- Added command `css_rresize` to resize player model
- Fixed saving IP addresses, from now they will not be saved if the check option in config is disabled
2025-03-09 01:01:45 +01:00
Dawid Bepierszcz
5d62c743dd AntiDLL Module Fix
Fixed banning when detecting a forbidden event
2025-02-20 01:28:28 +01:00
Dawid Bepierszcz
62b1987fde AntiDLL module
(?) Fix for AntiDLL module
2025-02-19 14:51:36 +01:00
Dawid Bepierszcz
708ae6cb90 1.7.4b
【UPDATE 1.7.4b】

**🆕  What's new and what's changed:**
- Added support for MySQL SSL (DatabaseSSlMode in config)
2025-02-19 12:27:24 +01:00
Dawid Bepierszcz
2d77e86d59 Hotfix for AntiDLL module 2025-02-18 20:18:16 +01:00
Dawid Bepierszcz
babcbc2119 1.7.4a
【UPDATE 1.7.4a】

**🆕  What's new and what's changed:**
- Exposed event `OnAdminShowActivity` - fire when the function of informing players of admin action is performed
- Exposed `ShowAdminActivity`
- Added module to integration with AntiDLL
- Added module to integration with Redis (plugin send info about penalties between all servers)
2025-02-18 20:11:05 +01:00
Dawid Bepierszcz
64e5f1156e 1.7.3a
【UPDATE 1.7.3a】

🆕  What's new and what's changed:
- Fixed problem when you can't add admin because of the same group names on multiple servers from now on it takes in order -> server id and if there is no -> global
- Added ability to add penalties via SteamID
- Added ability to display penalty messages only for admins (ShowActivityType = 3)
- Reverted OnClientConnect to EventPlayerFullConnect
2025-02-09 15:37:01 +01:00
Dawid Bepierszcz
8cc0398f6b 1.7.2c
- Fixed no permissions in menu and perm penalty (due to authorization issue on steam)
- Fixed non-async action in async method
2025-01-26 02:10:33 +01:00
Dawid Bepierszcz
ab14956ae5 1.7.2b
- Revert temp fix for kick
2025-01-16 03:28:17 +01:00
Dawid Bepierszcz
9163caeb0d Update Helper.cs 2025-01-15 13:09:27 +01:00
Dawid Bepierszcz
f2e4b84b29 1.7.2a
- Temp fix for player.Disconnect
- Fix for no reasons
- Fix for no time used
- Added check for hibernation
- Some changes in unwarn action
2025-01-15 12:56:03 +01:00
Dawid Bepierszcz
3f1b6b3bf7 1.7.1a
【UPDATE 1.7.1a】

🆕  What's new and what's changed:
- Fixed 2x player lock text
- Added immunity check when using css_addX
- MenuManagerCS2 has been updated
- Added return of lock ID by api (check example_module)
- Fixed reasons for locks, no longer need to use `“”` if there is a space in them
- Improved handling of lock times, from now on you can use:
-- 1mo - 1 month
-- 1w - 1 week
-- 1d - 1 day
-- 1h - 1 hour
-- 1m - 1 minute
-- 1 - 1 minute
You can also combine it, e.g:
10d5h - 10 days and 5 hours
2w3d - 2 weeks and 3 days
1w1d1h1 - 1 week, 1 day and 1 minute
So for example css_ban player 10d5h Super reason for my ban
- Added a `rcon_password` column in the `sa_servers` table that populates with the rcon password
- Fixed `banid` and banning
- Fixed too fast kick (it was caused by checking players online)

⚠️  **Remember to update all files + api**
2025-01-08 19:24:47 +01:00
Dawid Bepierszcz
8af805632a 1.7.0a
- Fixed css_warn (unfreeze player after 5s)
- Fixed server loading from database (reduced delay)
- Added checking the player in an earlier phase of the connection
- Fixed admin name when using action from menu
2024-12-16 01:55:38 +01:00
Dawid Bepierszcz
8c94a867d3 Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-11-26 23:37:52 +01:00
Dawid Bepierszcz
023e1a031b 1.6.9c
- Added config variable to enable/disable checking for banned multiaccounts by ip `CheckMultiAccountsByIp`
- Fixed css_speed after player hurt
- Finally fixed vote kick (callvote)
- Small fix for admins loading from database
2024-11-26 23:37:49 +01:00
Dawid Bepierszcz
1f1c214357 Merge pull request #183 from 1370533448/patch-1
Update zh-Hans.json
2024-11-26 13:09:00 +01:00
Dawid Bepierszcz
2defb2fe14 Hotfix - server loading
- Fix for server loading
2024-11-22 23:01:31 +01:00
Dawid Bepierszcz
b2ebe136c3 1.6.9a
- Added `IsAdminSilent` to api
- Fixed disabling noclip via admin menu
- Fixed (?) hibernation problem
- Added discord webhook fire when adding ban or mute for offline player
- Updated css and mysqlconnector
2024-11-22 22:31:06 +01:00
Ktm
5668c0ad7b Update zh-Hans.json
Incorrect translation of time and reason leads to incorrect display
2024-11-17 03:41:30 +08:00
Dawid Bepierszcz
70a62d4b63 HOTFIX - Ban via menu 2024-11-10 19:13:39 +01:00
Dawid Bepierszcz
95818e2742 1.6.8a
- Updated css version
- Updated mysqlconnector version
- Added information for the player until when he has a chat blocked
- Closing menu after ban
2024-11-10 17:30:48 +01:00
Dawid Bepierszcz
7154843d1d Update ManageServerMenu.cs 2024-10-31 03:51:49 +01:00
Dawid Bepierszcz
c42d2ddeeb 1.6.7a
```diff
+ Added PluginsManager
```
2024-10-31 03:45:22 +01:00
Dawid Bepierszcz
7a69c5387a 1.6.6a
```diff
+ Reapply gravity/speed with timer
+ Added shake effect for slap
+ Fixed css_gravity and css_speed command
+ Fixed css_give command, for example weapon_knife returns weapon_knife instead of weapon_knife and weapon_knife_t
+ Small code improvements
```
2024-10-31 00:30:16 +01:00
Dawid Bepierszcz
82b82722a6 1.6.5a
- Possibility to force menu type by config
- Possibility to reload admins by css command `css_admins_reload`
2024-10-24 13:39:11 +02:00
Dawid Bepierszcz
b38b9a0751 Updated clean module 2024-10-19 09:07:25 +02:00
Dawid Bepierszcz
5a9367ae89 1.6.4a
- New give command
- Added `@css/permmute` flag to allow perm penalties
- Fixed mute when player silenced
- Added CleanModule - allow to clean weapons on ground
2024-10-19 03:52:33 +02:00
Dawid Bepierszcz
d30ac80a36 Update VERSION 2024-10-08 00:54:34 +02:00
Dawid Bepierszcz
9820d74095 1.6.3b
```diff
+ Small code cleanup
+ FIXED ban check on connect
```
2024-10-08 00:54:20 +02:00
Dawid Bepierszcz
d0207f3d0b Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-10-06 13:48:44 +02:00
Dawid Bepierszcz
2bee514e32 1.6.3a
```diff
+ Recompiled for css 276 - this is the minimum version on which the plugin works
+ Added ability to check player penalties for admin with `@css/kick` flag - css_comms [#userid/name].
+ Added a new setting `ShowBanMenuIfNoTime` which allows you to disable the display of the menu when you don't specify a penalty time
```
2024-10-06 13:48:42 +02:00
Dawid Bepierszcz
9d87fd8701 Merge pull request #161 from yMenn/main
Add Portuguese (Portugal) translations
2024-10-05 12:02:17 +02:00
Dawid Bepierszcz
6847da2af0 1.6.2a
```diff
+ Added duration and reason menu when admin try to ban/mute without specific time
+ Added `NotifyPenaltiesToAdminOnConnect` config setting to disable notifications globally
+ Added immunity check when using game vote to kick player

- Removed `Discord.Net.Webhook` package (too heavy)
```
2024-10-05 11:45:09 +02:00
ymenn
757a39f90e Added pt-PT translations 2024-10-05 10:17:08 +01:00
Dawid Bepierszcz
bd817d6652 1.6.1b
- Fixed rare problem when system uses other timezone than plugin
- Fixed time type in discord webhook
2024-09-29 20:55:03 +02:00
Dawid Bepierszcz
4206ad18b2 Fixed? rename 2024-09-29 18:43:36 +02:00
Dawid Bepierszcz
774d812723 Update playercommands.cs 2024-09-29 18:43:03 +02:00
Dawid Bepierszcz
cf5c1d8440 Fixed? prename 2024-09-29 18:42:04 +02:00
Dawid Bepierszcz
efa2a392d6 Translations 2024-09-29 18:32:50 +02:00
Dawid Bepierszcz
32520c7e84 1.6.1a
🆕  **What's new and what's changed:**
```diff
+ Added `css_hidecomms` command - Disable showing penalties when player connect
+ Added customizable commands in `Commands.json` file in plugin config directory
  - U can disable command by removing aliases or rename/add more aliases (remember to not remove key, edit only Aliases)
+ Added missing `Console` translation `sa_console`
+ Added `LogCommand` to api
```
2024-09-29 18:29:04 +02:00
Dawid Bepierszcz
702a0315b8 Fixed gag 2024-09-28 23:44:12 +02:00
Dawid Bepierszcz
b1ab6e0e0d Alias for css_disconnected
- css_last
- css_lastX (X = value from config)
2024-09-28 18:33:41 +02:00
Dawid Bepierszcz
e272449f3c Added example module 2024-09-28 18:24:29 +02:00
Dawid Bepierszcz
05893a90d3 1.6.0a
```diff
+ `Refactored Code`: Improved code structure for better maintainability.
+ `Player Penalties Command`: Introduced the `css_penalties` command to display player penalties.
+ `Admin Penalties Information`: Added functionality to provide information for admins regarding penalties of connecting players.
+ `Disconnected Players Command`: Added the `css_disconnected` command to show a list of disconnected players.
+ `Colorful Messages`: Implemented the `css_cssay` command to send colorful messages, e.g., `css_cssay {lightgreen}Test`.
+ `Respawn Functionality`: Updated the `css_respawn` command to respawn players at their death location.
+ `Menu Type Management`: Introduced the `css_menus` command to change the menu type via `MenuManagerCS2`.
+ `Dynamic Menu Control`: Enhanced menu interaction with dynamic controls using WASD + ER keys.
+ `Language File Updates`: Updated language files for better localization.
+ `API Integration`: Added a simple API for external interaction.
+ `Configurable Timezone`: Introduced timezone settings in the configuration.
+ `Admin Activity Display Options`: Added configurable settings for displaying admin activity:
  + `0`: Do not show
  + `1`: Hide admin name
  + `2`: Show admin name
+ `Discord Notification Customization`: Made Discord duration notifications customizable with `{relative}` and `{normal}` placeholders.
+ Improved command logging
+

`Configuration Options:`
+ `Timezone`
+ `Other Settings`
+ `Disconnected Players History Count`
+ `Show Activity Type`
```
2024-09-28 18:03:03 +02:00
Dawid Bepierszcz
a3b8d8cfa7 . 2024-09-28 17:50:47 +02:00
Dawid Bepierszcz
1cf541bb51 Merge pull request #154 from KillerRoi/patch-2
Update ar.json
2024-09-03 21:34:48 +02:00
Dawid Bepierszcz
3fb254bd0a Merge pull request #153 from KillerRoi/patch-1
Update en.json
2024-09-03 21:34:30 +02:00
Killer Roi
a26df7fedd Update en.json 2024-09-04 01:04:06 +05:30
Killer Roi
446dc9f4f5 Update ar.json 2024-09-04 01:01:40 +05:30
Killer Roi
3a8e0dd1fc Update en.json 2024-09-04 00:57:44 +05:30
Dawid Bepierszcz
b90ed05d1b Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-08-18 22:24:18 +02:00
Dawid Bepierszcz
6df98fb164 1.5.2c
- Menu overrides for fun commands
2024-08-18 22:23:30 +02:00
Dawid Bepierszcz
100482cb17 Update FunActionsMenu.cs 2024-08-17 21:28:09 +02:00
Dawid Bepierszcz
8f2f95ca23 1.5.2b
- Fix for command overrides in menu
- Fix for kick with banid when player disconnected
2024-08-17 18:43:11 +02:00
Dawid Bepierszcz
79270acafe PenaltyWebhook block thread - fix 2024-08-13 21:14:43 +02:00
Dawid Bepierszcz
f95581e7c0 mysql compatibility 2024-08-12 09:55:52 +02:00
Dawid Bepierszcz
af46f0bca1 Warn migration 2024-08-12 02:05:54 +02:00
Dawid Bepierszcz
f56e3f6fe9 Warns - last?
- Warn list + unwarn
2024-08-12 01:57:30 +02:00
Dawid Bepierszcz
4ef07c8bf7 Warns - second
- Warn menu
- Warn reasons (config)
2024-08-12 00:14:43 +02:00
Dawid Bepierszcz
0674b7e492 Warns - first
-WarnManager
- css_warn
- Warn config
- Warn languages

~ Warn menu
~ Warn table
~ *css_unwarn
~ css_addwarn
2024-08-11 22:54:54 +02:00
Dawid Bepierszcz
c4cb308147 Admin immunity + Discord
- Fixed command logging + small refactor
- Fixed admin immunity
- Separated penalty webhook
- More customizable penalty webhook
2024-08-05 03:28:42 +02:00
Dawid Bepierszcz
bb0a236f28 1.5.1a
- Possibility to use new line in translations
- Improved initializing
- More errors logging
- Localized Message refactor
2024-07-15 21:50:06 +02:00
Dawid Bepierszcz
3bc51330af 1.5.0a v3 2024-07-04 22:02:02 +02:00
Dawid Bepierszcz
5fdefc9614 1.5.0a v2 2024-07-03 23:07:12 +02:00
Dawid Bepierszcz
bcbcb83a35 1.5.0a
- Fixed immunity in menu
- Added new command `css_prename` to  perm rename player (until the server restarts - don't set new name to remove perm rename) // @css/ban)
2024-07-03 22:59:26 +02:00
Dawid Bepierszcz
6cf6b1c919 Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-07-03 19:18:28 +02:00
Dawid Bepierszcz
3b98f19a7c 1.4.9a
- Plugin save all players ips to database, when player banned then all used ips are banned too (for ban evading)
- Minor changes
2024-07-03 19:18:25 +02:00
Dawid Bepierszcz
83fdf1dd56 Merge pull request #133 from Truba55/patch-1
German translation
2024-06-29 15:09:21 +02:00
Dawid Bepierszcz
6fc8b015ba 1.4.8b
- Improved admin loading
2024-06-29 15:06:32 +02:00
Dawid Bepierszcz
6b5e36087e Update Config.cs 2024-06-29 11:35:38 +02:00
Dawid Bepierszcz
a395a7d9c8 1.4.8a
- Removed message of checking server ip
- Fixed some commands
- Changed workshopmaps config
- Probably fixed votes (untested)  - let me know if not
- Minimum css version - 246
2024-06-29 11:33:41 +02:00
Truba55
d7e46525a5 German translation 2024-06-27 15:48:47 +02:00
Dawid Bepierszcz
cdd771511b 1.4.7a
- Fixed crash when using command on invalid player
- Using banid command to reduce number of queries to database (only if unlock commands enabled in css config)
- Minor changes
2024-06-23 12:59:14 +02:00
Dawid Bepierszcz
8e4724fb3e 1.4.6b
- Fix for alternative get server ip method
2024-06-18 03:12:00 +02:00
Dawid Bepierszcz
985b1ae61f 1.4.6a
- Alternative method to get server ip
- Config option to disable update check
- Improved live bans check
- Added new permission `@css/showip` to show ip in `css_who` and `css_players` commands
2024-06-17 11:08:46 +02:00
Dawid Bepierszcz
00facafdcb 1.4.5a
- Added `ReloadAdminsEveryMapChange` - Reloading sql admins on map start
- Ability to use any valid steamid instead of steamid64
- Fixed chat commands when gagged
- Votes now respect `UseChatMenu` config setting
2024-06-09 17:55:33 +02:00
Dawid Bepierszcz
962529e445 1.4.4b - Fix
- Fixed admins / group loading
2024-05-16 22:40:14 +02:00
Dawid Bepierszcz
873fed17c9 1.4.4b
- Fetch admins and groups data only once
- Added missing migration
2024-05-16 21:51:13 +02:00
Dawid Bepierszcz
fc2958c84f Update Config.cs 2024-05-06 22:59:23 +02:00
Dawid Bepierszcz
4244104d6e 1.4.4a
- MultiServerMode fix
- New feature `TimeMode`
2024-05-06 22:56:57 +02:00
Dawid Bepierszcz
d5a6ceacb6 1.4.3d
- Fixed banning without permban flag
- Updated css
2024-05-03 15:08:47 +02:00
Dawid Bepierszcz
14cc9d3b00 1.4.3c
- Fixed rare problems with expiring bans
- More checks for `BanType`
- Minor changes about validating players
- Added `css_players -duplicate` to list players with same ip address
2024-05-02 13:52:53 +02:00
Dawid Bepierszcz
9fb256d39f 1.4.3c
- Fixed rare problems with expiring bans
- More checks for `BanType`
- Minor changes about validating players
- Added `css_players -duplicate` to list players with same ip address
2024-05-02 13:51:07 +02:00
Dawid Bepierszcz
c25d3c4bda 1.4.3b
- Groups support now NULL value as `server_id` (global group)
- Added json output for `css_players`  (Panel request)
- AdminSQLManager Refactor
2024-05-01 22:46:04 +02:00
Dawid Bepierszcz
ebf9e06bcd Update 001_CreateTables.sql 2024-05-01 01:29:33 +02:00
Dawid Bepierszcz
c72c7231f7 Update 001_CreateTables.sql 2024-05-01 01:26:48 +02:00
Dawid Bepierszcz
9c870b8fc3 Merge pull request #105 from Dliix66/feature/adminSplitBan
Feature/admin split ban and more
2024-04-30 14:09:00 +02:00
Dawid Bepierszcz
cf7a87d959 Merge branch 'feature/adminSplitBan' of https://github.com/Dliix66/CS2-SimpleAdmin into pr/105 2024-04-30 14:07:58 +02:00
Dawid Bepierszcz
9759b34505 Update Config.cs 2024-04-30 14:07:34 +02:00
Dawid Bepierszcz
5a90171842 Merge branch 'main' into feature/adminSplitBan 2024-04-30 14:06:26 +02:00
Dawid Bepierszcz
19f8b68c1c 1.4.3a
- New feature permban permission (@css/permban)
- CustomCommand fix
- Version checker
- Minor changes
- Updated to CounterStrikeSharp 225
- Bump version to 1.4.3a
2024-04-30 14:05:20 +02:00
Valentin Barat
342d4f717f Fixed config loading 2024-04-29 14:18:54 +02:00
Dawid Bepierszcz
731bc229d3 1.4.2b
- Fixed migrations for mysql 5.x
2024-04-29 10:56:49 +02:00
Valentin Barat
bb8cec7cf8 Merge branch 'main' into feature/adminSplitBan 2024-04-29 10:31:37 +02:00
Valentin Barat
ad4c721a27 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	Menus/CustomCommandsMenu.cs
2024-04-29 10:31:20 +02:00
Valentin Barat
7efa2cdad3 Fixed perm ban issue with console 2024-04-29 10:27:52 +02:00
Dawid Bepierszcz
c321502937 1.4.2a
- Config upgrade
- Translatable  and customizable menu
- More async
- Minor changes
2024-04-29 00:04:42 +02:00
Valentin Barat
13ab223c86 Merge branch 'main' into feature/adminSplitBan 2024-04-28 02:26:39 +02:00
Dliix66
c73584edae Merge branch 'daffyyyy:main' into main 2024-04-28 02:20:16 +02:00
Dawid Bepierszcz
aefa6c6355 1.4.1a
- Minor changes
2024-04-28 02:16:24 +02:00
Dawid Bepierszcz
806b5038ca Update 004_MoveOldFlagsToFlagsTable.sql 2024-04-27 13:29:56 +02:00
Dawid Bepierszcz
b45e112534 Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-04-27 01:44:24 +02:00
Dawid Bepierszcz
e1650df72e 1.4.1a
- Full AdminManager integration
2024-04-27 01:44:12 +02:00
Dawid Bepierszcz
8caa8669ea Update 005_CreateUnbansTable.sql 2024-04-26 13:03:18 +02:00
Dawid Bepierszcz
516d9d9014 1.4.0b
- Recompiled with css v215
2024-04-26 09:15:25 +02:00
Dawid Bepierszcz
c71391bca4 1.4.0b
- Recompiled with css v215
2024-04-26 09:15:08 +02:00
Dawid Bepierszcz
2c66b899d0 Update Config.cs 2024-04-25 20:35:00 +02:00
Dawid Bepierszcz
1cfff8684f 1.4.0a
- Ability to set the plugin mode (MultiServerMode) - When its `true`, then plugin respects penalties from all servers, when its `false` then plugin respects penalties only from this server
- Renamed command `css_reladmin` to `css_reloadadmins` (It reload admins and groups)
- Groups
2024-04-25 20:33:37 +02:00
Valentin Barat
0bbf1948bf Added localization in EN/FR 2024-04-25 01:02:46 +02:00
Valentin Barat
b4103bfc25 Made base max ban duration to 1day 2024-04-25 00:52:40 +02:00
Valentin Barat
a0b2e59357 Better duration check 2024-04-25 00:47:54 +02:00
Valentin Barat
fb251aadef Added max ban duration and perm ban right 2024-04-25 00:46:23 +02:00
Dliix66
00343b6b66 Merge branch 'daffyyyy:main' into main 2024-04-25 00:26:01 +02:00
Dawid Bepierszcz
270b36fa26 1.3.9a
**MAJOR UPDATE**

- New database schema
- Added `css_admins_flags` table
- Added `css_unmutes` table
- Added `css_unbans` table
2024-04-20 18:56:41 +02:00
Dawid Bepierszcz
7a8fd066f7 1.3.8b
- Plugin checks every minute if a player is banned
- Minor changes
- Removed bot_quota warning
- NET8
2024-04-16 22:08:44 +02:00
Valentin Barat
90556e66b2 Fixed client commands in custom commands 2024-04-14 19:36:28 +02:00
Dawid Bepierszcz
143f0b7e1b Merge pull request #94 from dollannn/main
Feature: Penalty embed
2024-04-02 19:57:22 +02:00
Dawid Bepierszcz
d8a1a1e471 Merge branch 'main' of https://github.com/dollannn/CS2-SimpleAdmin into pr/94 2024-04-02 19:56:25 +02:00
Dawid Bepierszcz
3abf246f9b fixed reloadadmins 2024-04-02 19:55:48 +02:00
Dawid Bepierszcz
d1fdaa4ecf Merge branch 'main' into main 2024-04-02 19:52:22 +02:00
Dawid Bepierszcz
89d27e1bd3 Added translation
- Minor changes
- Version bump
- Added translation
2024-04-02 19:42:16 +02:00
Dawid Bepierszcz
bd82a981f5 Merge pull request #85 from Dliix66/main
Fixed Discord web hooks with the menu and several bug fixes/improvments on the menus
2024-04-02 18:34:18 +02:00
Dollan
052cea5ce3 feat: embed is now integrated with all penalties 2024-04-01 18:30:45 +02:00
Dollan
ba2bf90340 fix: add hostname as embed description 2024-04-01 18:30:03 +02:00
Dollan
8f80e13100 feat: implement penaltylog on gag 2024-03-31 01:23:35 +01:00
Dollan
a377871951 fix: if punishment is 0 ie permanent show permanent 2024-03-31 01:23:15 +01:00
Dollan
8df1e70d5a feat: add penalty embed helper function 2024-03-31 01:06:42 +01:00
Dollan
d8e30e02e9 fix: remove repetitive webhook snippet and replace it with helper function 2024-03-30 22:50:16 +01:00
Dollan
b2c56d3fa1 feat: add helper function to send logs to discord webhook 2024-03-30 22:41:27 +01:00
Dawid Bepierszcz
5609748d19 Update README.md 2024-03-30 10:57:25 +01:00
Dawid Bepierszcz
9d513a92a1 Fix css_sa_upgrade 2024-03-30 10:56:25 +01:00
Dawid Bepierszcz
3a977f688c 1.3.7a
- Throwing more informations
- Updated CounterStrikeSharp
- Fixed problem with expiring bans via `css_addban`
- Added metrics
- Minor changes
2024-03-30 10:49:06 +01:00
Dawid Bepierszcz
143dc138f0 1.3.6.d - fix 2024-03-24 07:13:20 +01:00
Dawid Bepierszcz
a8904f2d8a 1.3.6d - IMPORTANT
- Fixed pool size
- Fixed `css_rename`
- Updated CounterStrikeSharp
- Minor changes
2024-03-23 23:41:17 +01:00
Valentin Barat
39404e52ac Fixed hmtl-incompatible player names 2024-03-21 23:36:56 +01:00
Valentin Barat
0cca38b10b Improved player selection in menus 2024-03-16 16:08:28 +01:00
Valentin Barat
aac5caa565 fixed typo in freeze 2024-03-16 16:02:04 +01:00
Valentin Barat
90c0564f56 Discord webhook from menus 2024-03-16 16:01:26 +01:00
Valentin Barat
0f4a2835d2 Added gravity and set money in fun commands menu 2024-03-16 11:51:44 +01:00
Valentin Barat
c6126ab2ec Fixed Respawn menu that was disabling dead players instead of alive ones 2024-03-16 11:32:58 +01:00
Valentin Barat
60c76562f9 Added ExecuteOnClient option on custom commands 2024-03-13 11:46:09 +01:00
Valentin Barat
ac940259f7 Moved custom commands to its specific menu 2024-03-13 11:43:33 +01:00
Dawid Bepierszcz
cce265c6b7 1.3.6c
- Fixed kick reason
- Small changes
- Fixed map name in css_map command
2024-03-11 01:16:34 +01:00
Dawid Bepierszcz
525905194c 1.3.6b
- Fixed ban checking
- Small changes
- Updated css
2024-03-09 12:06:50 +01:00
Dawid Bepierszcz
2ab2f9c4dc 1.3.6a
- Minor changes
- Added `css_gravity` command
- Added `css_money` command
- Changed Utc time to LocalTime
- Updated translations (ChatGPT generated)
- Updated css version
2024-03-07 13:34:31 +01:00
Dawid Bepierszcz
da6fb2fc22 1.3.5a
- Added custom command to menu
- Better ungag/unmute/unsilence handling
- Fixed css_psay from console
2024-03-02 22:16:38 +01:00
Dawid Bepierszcz
7d5166cf4b Merge pull request #67 from Dliix66/main
Added custom server commands
2024-03-02 22:14:14 +01:00
Dawid Bepierszcz
a8c4c1f9fa - 1.3.4a
- Resolved conflicts?
2024-03-02 21:53:48 +01:00
Dawid Bepierszcz
3c42acf15e Revert "1.3.5a"
This reverts commit f61a5ff6a8.
2024-03-02 21:53:06 +01:00
Dawid Bepierszcz
f61a5ff6a8 1.3.5a
- Resolved conflicts
2024-03-02 21:52:21 +01:00
Dawid Bepierszcz
229b8d73a3 1.3.4a
- Minor changes
- Escape kick reason @poggu suggestion
- Auto-updater for config
- Using UTC time
- Added expiring IP bans after x days (`ExpireOldIpBans` in config => value = days, 0 = disabled)
- Added exception message to database error
- Fixed? ungag/unmute/unsilence commands
- Updated css version to `178`
- Changed `css_adminhelp` command to use new file `admin_help.txt` as output
2024-03-01 12:38:46 +01:00
Valentin Barat
650c115a88 Updated version to 1.4.0a 2024-03-01 02:20:28 +01:00
Valentin Barat
4d3cefc495 Updated config version to 7 2024-03-01 02:13:50 +01:00
Valentin Barat
5d58465c74 Working custom server commands 2024-03-01 02:12:10 +01:00
Dawid Bepierszcz
5bf966f9cd 1.3.3a
- Fixed godmode
- Added logging commands to simpleadmin logs file
- Fixed votes?
- Added updating player_ip and player_name after connect with ban issued by css_addban
2024-02-21 13:14:46 +01:00
Dawid Bepierszcz
619bdfbb14 lang files 2024-02-14 01:56:26 +01:00
Dawid Bepierszcz
f4f669d498 Merge pull request #62 from Dliix66/feature/menu
Fixed maps not being displayed in the menus
2024-02-14 01:54:17 +01:00
Dawid Bepierszcz
8e1a1b2ecf Small changes
- Small changes
- Added `ru` and `pt-br` lang
2024-02-14 01:53:13 +01:00
Valentin Barat
64803ebff2 Updated version to 1.3.2b 2024-02-13 23:54:57 +01:00
Valentin Barat
285429dc66 Fixed instance default value not having the config set 2024-02-13 23:46:46 +01:00
Dawid Bepierszcz
5b36a5fc62 Merge pull request #49 from Dliix66/feature/menu
Changed css_admin command to open the admin menu
2024-02-13 01:37:23 +01:00
Dawid Bepierszcz
4e898a6509 Minor changes 2024-02-13 01:35:45 +01:00
Valentin Barat
af66802b37 Added Change Team 2024-02-13 00:33:07 +01:00
Valentin Barat
6c44281ca5 using Helper.GetValidPlayers() 2024-02-13 00:15:39 +01:00
Valentin Barat
5edddbb70f added kick reason 2024-02-13 00:11:38 +01:00
Valentin Barat
a8a35544c0 Fixed freeze 2024-02-12 23:47:23 +01:00
Valentin Barat
74852f0651 added client only of css_admin 2024-02-12 23:37:55 +01:00
Valentin Barat
1882b14657 Using type instance 2024-02-12 23:32:35 +01:00
Valentin Barat
c3dd3bed10 Added player names in sub menus 2024-02-12 22:53:08 +01:00
Valentin Barat
af73447b9e Added Admins management menu 2024-02-12 22:52:50 +01:00
Valentin Barat
7386a2aae2 Made Admin menu visible only for root 2024-02-12 22:16:07 +01:00
Valentin Barat
effe97a210 enabled fun actions menu 2024-02-12 22:10:00 +01:00
Valentin Barat
3b6b7fbcab Added alive player filter to PlayersMenu 2024-02-12 22:09:52 +01:00
Valentin Barat
9a7a143478 re-refactored css_who 2024-02-12 22:03:09 +01:00
Valentin Barat
fe834d8abc Fixed kick targets 2024-02-12 22:01:25 +01:00
Valentin Barat
eb9519c156 Fun commands done 2024-02-12 22:01:09 +01:00
Valentin Barat
8750a66eef Fun menu done without guns, need to test 2024-02-12 17:46:45 +01:00
Valentin Barat
3317182b9a Created fun menu 2024-02-12 17:00:47 +01:00
Valentin Barat
fa5296fec7 Fixed slap moving the player's aim 2024-02-12 16:44:50 +01:00
Valentin Barat
0a2d19accd Added menu type config 2024-02-12 16:41:00 +01:00
Valentin Barat
c2f7b3be08 added french translation 2024-02-12 16:23:30 +01:00
Valentin Barat
b31bb871a6 Added default Maps in the config 2024-02-12 15:56:45 +01:00
Valentin Barat
00819beec5 restored css_admin to open the menu 2024-02-12 15:42:55 +01:00
Valentin Barat
ae8d9db5e4 Added Silence command in menu 2024-02-12 15:15:49 +01:00
Valentin Barat
a3ebcccb6f Merge finished 2024-02-12 15:11:31 +01:00
Valentin Barat
dd3ccf4069 Merge branch 'main' into feature/menu
# Conflicts:
#	CS2-SimpleAdmin.cs
#	Config.cs
#	Extensions/PlayerExtensions.cs
2024-02-12 14:40:55 +01:00
Valentin Barat
3413200d8e using config maps 2024-02-12 14:21:12 +01:00
Valentin Barat
6dccddf929 Updated config to add workshop maps 2024-02-12 14:21:02 +01:00
Valentin Barat
751c97b4cf Added restart game and change map menus 2024-02-12 14:19:44 +01:00
Dawid Bepierszcz
b4c259595e Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-02-12 14:11:44 +01:00
Dawid Bepierszcz
d8e8073dd6 Fix? for messages 2024-02-12 14:11:34 +01:00
Valentin Barat
377049c020 Added css_restartgame 2024-02-12 14:07:29 +01:00
Dawid Bepierszcz
74c773aaea Merge pull request #58 from daffyyyy/dev
Update CS2-SimpleAdmin.cs
2024-02-12 13:44:54 +01:00
Dawid Bepierszcz
e2df860b6f Update CS2-SimpleAdmin.cs 2024-02-12 13:44:05 +01:00
Dawid Bepierszcz
b1e497ea3f Merge pull request #57 from daffyyyy/dev
1.3.1a
2024-02-12 13:04:27 +01:00
Dawid Bepierszcz
953234078c 1.3.1a
- Added `css_tp`
- Added `css_bring`
- Added logging commands to discord
- Small refactor
2024-02-12 13:03:02 +01:00
Dawid Bepierszcz
bda704e843 test
test
2024-02-12 13:00:38 +01:00
Dawid Bepierszcz
e4e1023684 Update build.yml 2024-02-12 12:58:27 +01:00
Dawid Bepierszcz
5d02343268 Update build.yml 2024-02-12 12:56:57 +01:00
Valentin Barat
778c93b170 Fixed merge issues 2024-02-12 11:46:16 +01:00
Valentin Barat
9cecc19060 Merge branch 'main' into feature/menu
# Conflicts:
#	CS2-SimpleAdmin.cs
2024-02-12 11:41:44 +01:00
Dawid Bepierszcz
f0b5b820e2 Merge pull request #56 from RoyZ-iwnl/main
Create zh-Hans.json
2024-02-12 11:13:09 +01:00
Dawid Bepierszcz
6a182fff9d 1.3.0f
- Fixed `css_kick`
- Fixed gag/mute/silence on connect
- Additional check in immunity
2024-02-11 16:27:54 +01:00
RoyZ
cd8ee681b2 Create zh-Hans.json
add Chinese translation
2024-02-11 14:13:09 +08:00
Dawid Bepierszcz
42849c231f Merge pull request #54 from criskkky/main
1.3.0e
2024-02-11 04:16:38 +01:00
Dawid Bepierszcz
76914be555 Update es.json 2024-02-11 04:15:04 +01:00
cristóbal
aadcaa0e64 1.3.0e
Updated: Spanish Language Support
2024-02-11 00:04:08 -03:00
Dawid Bepierszcz
0b2a520a07 1.3.0e
- Added `css_sa_upgrade` command
2024-02-11 03:39:42 +01:00
Dawid Bepierszcz
79bbe0f4c5 1.3.0e
- Added `css_rename`
- Added `css_silence`
- Added `css_addsilence`
- Added `css_unsilence`
- PlayerPenaltyManager class
- Fix for invalid players

New commands localized only for `pl` and `en`, if u can please make pr for other languages
2024-02-11 03:24:27 +01:00
Dawid Bepierszcz
01ceb104c5 Update Events.cs 2024-02-10 15:31:09 +01:00
Dawid Bepierszcz
e401fe7c8b 1.3.0d
- Fixed noclip
- Fixed freeze
- Fixed vote
2024-02-09 21:54:55 +01:00
Dawid Bepierszcz
fbed647699 1.3.0c
- CounterStrikeSharp v163
2024-02-08 11:11:53 +01:00
Dawid Bepierszcz
aa95815fbd Added info about crashes with bots 2024-02-06 00:57:28 +01:00
Dawid Bepierszcz
3793385ce4 1.3.0b
- Minor changes
- Fixed `css_players`
- Probably fixed problems with taking actions with bots
2024-02-04 21:04:22 +01:00
Valentin Barat
c8c0a3f0ba Updating pawn's HP on slap 2024-02-04 20:36:18 +01:00
Valentin Barat
5991f85919 Fixed defualt kick reason 2024-02-04 20:34:14 +01:00
Valentin Barat
a85e5c69ee Last merge conflicts 2024-02-04 15:49:37 +01:00
Valentin Barat
40ad4b995c Merge branch 'main' into feature/menu
# Conflicts:
#	CS2-SimpleAdmin.cs
2024-02-04 15:10:30 +01:00
Valentin Barat
d3062b5f7e Fixed respawn 2024-02-04 14:54:08 +01:00
Dawid Bepierszcz
131030a2cd 1.3.0a
- Fixed crashing on servers with a lot of players
- Every command chat message respect player language for now css_lang or use https://github.com/aprox2/GeoLocationLanguageManagerPlugin to detect language related on player ip
- Fixed css_respawn
- Fixed css_vote player can't vote multiple times
- Added TeamSwitchType to config, if set to 0 plugin always slay player on css_team command
- Minor changes
2024-02-04 13:25:15 +01:00
Valentin Barat
a9f3196441 Merge done? 2024-02-03 23:37:01 +01:00
Valentin Barat
bdb90b0cc6 Merge branch 'main' into feature/menu
# Conflicts:
#	CS2-SimpleAdmin.cs
2024-02-03 23:30:25 +01:00
Dawid Bepierszcz
93faad27c1 Update build.yml 2024-02-02 23:00:16 +01:00
Dawid Bepierszcz
7251516bb4 Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-02-02 22:57:06 +01:00
Dawid Bepierszcz
175d6df250 1.2.9a - fix
- Fixed native in non-main thread
2024-02-02 22:57:05 +01:00
Dawid Bepierszcz
7e5dc7b612 Update build.yml 2024-02-02 22:16:18 +01:00
Dawid Bepierszcz
0572ad7d32 1.2.9a
- Major changes
- Fixed `css_respawn`
- Added discord webhook
- Refactoring database class
2024-02-02 22:02:09 +01:00
Dawid Bepierszcz
86e837931c 1.2.9a
- Major changes
- Fixed `css_respawn`
- Added discord webhook
- Refactoring database class
2024-02-02 21:59:21 +01:00
Dawid Bepierszcz
b2f1afd7e7 Merge pull request #48 from originalaidn/main
huge modification
2024-02-02 21:53:23 +01:00
AiDN™
705bacccae Merge branch 'daffyyyy:main' into main 2024-02-01 13:30:04 +01:00
AiDN™
58086c4009 added windows signature
0 compile error/warning, but not quite sure if this is the safest/best way to check signatures
2024-02-01 13:29:12 +01:00
AiDN™
f15061793a edited css_say logging 2024-02-01 13:14:19 +01:00
Valentin Barat
5035f88da0 Revert to internal 2024-02-01 00:19:05 +01:00
Dawid Bepierszcz
4e597d73a5 Small changes
- Correct version
- Null warnings
2024-02-01 00:03:34 +01:00
Dawid Bepierszcz
13d36dca12 Revert "Small changes"
This reverts commit 9acab82f86.
2024-02-01 00:01:57 +01:00
Dawid Bepierszcz
9acab82f86 Small changes
- Correct version
- null warnings
2024-01-31 23:57:14 +01:00
Valentin Barat
66f0ebe08c Enabled other menus for visual screenshot 2024-01-31 23:08:26 +01:00
Valentin Barat
0cd1653185 Commented out css_who as its not relevant IMO 2024-01-31 23:06:04 +01:00
Valentin Barat
fb827f4f0d Changed commands to open the menu 2024-01-31 23:04:11 +01:00
Valentin Barat
6848847f21 default null instance 2024-01-31 22:10:45 +01:00
Valentin Barat
a80de2d7dc Change team down 2024-01-31 22:05:48 +01:00
Valentin Barat
a266f776a8 Duration in minutes 2024-01-31 21:45:42 +01:00
Valentin Barat
60ebb698a3 ban/gag/mute 2024-01-31 21:44:38 +01:00
Valentin Barat
5fbd21aec2 Slay and kick done 2024-01-31 18:03:08 +01:00
Valentin Barat
232f487d01 Fixed who 2024-01-31 17:39:03 +01:00
Valentin Barat
f622701f5e Made slap menu 2024-01-31 17:29:28 +01:00
Valentin Barat
65b33c17ea Moved css_who command to ManagePlayersMenu.cs 2024-01-31 17:19:13 +01:00
Valentin Barat
4fd268b235 Base menu structure done 2024-01-31 17:06:54 +01:00
Valentin Barat
bad4289c5c Added instance getter on the base plugin class for the menus 2024-01-31 16:05:08 +01:00
Valentin Barat
fd97e43490 Updated version to 1.3.0 2024-01-31 15:59:19 +01:00
Dawid Bepierszcz
a59cf5a7da Merge pull request #47 from KillerRoi/main
Added Arabic Translation
2024-01-31 12:58:10 +01:00
originalaidn
dd7a2f4c16 huge modification
- modified respawn, now the players need to respawn
- added stealth command as hide, removed the team selection menu (from CSS discord)
- removed silentmode
- added discord webhook logging system (need to set in config -DiscordWebhook and edit Version to 4)

in the future, need to add every color to replace thing and maybe need to remove those replace messages and add to sendwebhook but not yet.
2024-01-31 11:17:12 +01:00
KillerRoi
fbf395d327 Fixed 2024-01-28 20:49:05 +05:30
KillerRoi
8ca82804a2 Fixed sa_adminhelp not being readable 2024-01-28 20:48:02 +05:30
KillerRoi
4b1cb3573b Fixed 2024-01-28 20:47:05 +05:30
KillerRoi
5dc35ddb73 Fixed second line not being able to read 2024-01-28 20:43:11 +05:30
KillerRoi
31592bf1a5 .. 2024-01-28 20:42:24 +05:30
KillerRoi
a3471e2091 .. 2024-01-28 20:40:59 +05:30
KillerRoi
dfd9bd5329 Added Arabic Translation 2024-01-28 20:39:29 +05:30
Dawid Bepierszcz
e028bef5b0 1.2.8d
- Minor changes
- Added the ability to change the map to a workshop map via the `css_map ws:id/name` command
- Fixed the `css_wsmap` command and retained it for backward compatibility
- Changed gags from userid to steamid
2024-01-27 23:56:13 +01:00
Dawid Bepierszcz
67ad1851bf 1.2.8c
- Updated CounterStrikeSharp to 159
- Minor changes
2024-01-26 13:45:16 +01:00
Dawid Bepierszcz
e584316a28 1.2.8b
- Changing connect event, in previous sometimes player == null
2024-01-25 21:09:24 +01:00
Dawid Bepierszcz
450a7804c6 1.2.8a
- Small fixes
2024-01-25 01:00:31 +01:00
Dawid Bepierszcz
855f087a7b 1.2.8a
- Updated minimum CounterStrikeSharp version to **154**
- Fixed? issue with checking ban status for player with authorization problem
- Added additional checks for immunity
- Added new config variable `BanType` if `1` = `SteamID + IP`, if `0` = `SteamID`
2024-01-24 23:50:49 +01:00
Dawid Bepierszcz
b5600d7e73 1.2.7e
- Minor changes
2024-01-20 17:24:18 +01:00
Dawid Bepierszcz
6640d00442 Update README.md 2024-01-20 02:25:05 +01:00
Dawid Bepierszcz
8aee44f709 Create FUNDING.yml 2024-01-20 02:18:41 +01:00
Dawid Bepierszcz
374f6002eb 1.2.7d
- Fix for console commands exception (caused by silentmode)
- Updated css
2024-01-19 21:03:24 +01:00
Dawid Bepierszcz
99ce2cdf6a 1.2.7c
- `css_hide` - Hide on scoreboard
Code stolen from https://github.com/DeadSwimek/cs2-hideadmin
Thanks DeadSwimek ;]
2024-01-17 23:23:31 +01:00
Dawid Bepierszcz
38020e6509 Update README.md 2024-01-17 23:12:22 +01:00
Dawid Bepierszcz
d751bdadbd 1.2.7b
- Back to net7
- SilentMode for admins
- Adding global admins via command
2024-01-17 23:08:52 +01:00
Dawid Bepierszcz
64c806f3d7 Update CS2-SimpleAdmin.cs 2024-01-15 02:39:33 +01:00
Dawid Bepierszcz
8c4cb49d02 1.2.7a
- CounterStrikeSharp 146
2024-01-15 02:39:20 +01:00
Dawid Bepierszcz
2d41a62b52 1.2.6e
- Finally? fixed sqladmins
2024-01-13 23:12:23 +01:00
Dawid Bepierszcz
48baac0a7b Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-01-13 00:19:15 +01:00
Dawid Bepierszcz
8d365b670e 1.2.6d
- Probably workaround for sql admins
2024-01-13 00:19:13 +01:00
Dawid Bepierszcz
6a82d72244 Update README.md 2024-01-10 03:14:01 +01:00
Dawid Bepierszcz
e602eaa445 Update README.md 2024-01-10 03:13:23 +01:00
Dawid Bepierszcz
75f18d3c0a Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-01-10 00:03:37 +01:00
Dawid Bepierszcz
40611e9126 Grammar 2024-01-10 00:03:29 +01:00
Dawid Bepierszcz
fa8b7a877f Merge pull request #29 from rcon420/patch-2
lv grammar changes
2024-01-10 00:01:07 +01:00
Dawid Bepierszcz
71af90031e Database connection exception fix, additional ip check (problem with steam auth) 2024-01-09 23:05:49 +01:00
rcon_password
07924a6a74 lv grammar changes 2024-01-09 22:52:56 +02:00
Dawid Bepierszcz
f78cd888de Fix 2024-01-09 12:08:14 +01:00
Dawid Bepierszcz
d173816492 Update AdminSQLManager.cs 2024-01-08 22:34:11 +01:00
Dawid Bepierszcz
dd5e72e873 Update 1.2.6a
- Added server_id
- Added immunity
2024-01-08 22:29:36 +01:00
Dawid Bepierszcz
cfe79109a8 Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-01-05 01:04:13 +01:00
Dawid Bepierszcz
18122cdc08 1.2.5a
-Minor changes
- Fixed `css_speed`
2024-01-05 01:04:08 +01:00
Dawid Bepierszcz
ac92f1cb4e Merge pull request #13 from BMathers35/beta
Turkish language support
2024-01-04 17:06:15 +01:00
Dawid Bepierszcz
6d6ff53b02 Merge pull request #20 from DEADLYDEVIL-IR/main
Persian Language Support
2024-01-04 17:06:05 +01:00
Dawid Bepierszcz
675c7bdcee Merge pull request #23 from criskkky/main
Spanish Language Support
2024-01-04 17:05:40 +01:00
cristóbal
c839e69804 Add files via upload 2024-01-03 22:32:38 -03:00
DEADLYDEVIL
3930d2d4bd add some changes 2023-12-25 21:54:19 +03:30
DEADLYDEVIL
6a5165a513 Add persian lang 2023-12-24 19:11:33 +03:30
daffyyyy
5dc14e3301 12.4b 2023-12-18 14:30:26 +01:00
daffyyyy
27582b72af 1.2.4b
- Cache for mysql admins
2023-12-18 14:29:56 +01:00
daffyyyy
f95031a3f5 1.2.4a
- mysql admins
- minor changes
2023-12-18 13:46:58 +01:00
BMathers
7a73212d2e Turkish language support 2023-12-15 02:16:08 +03:00
daffyyyy
270e3bd858 1.2.3a
- Fixed pern mute when entering the server
- New command `css_strip`
- Changed `css_team` command added `swap` as team, and `-k` argument to kill player
2023-12-14 21:43:40 +01:00
daffyyyy
b8075ffa8c Update lv.json 2023-12-13 20:20:55 +01:00
daffyyyy
28f6cf63fe 1.2.2a UPDATE
- Fixed css_addban, css_addmute
- New command `css_hp`
- New command `css_god`
- New command `css_speed`
2023-12-13 20:20:12 +01:00
Dawid Bepierszcz
e040dbced5 Merge pull request #11 from rcon420/patch-1
Create lv.json
2023-12-13 19:57:00 +01:00
rcon_password
7e7fc5c885 Create lv.json 2023-12-13 18:19:16 +02:00
daffyyyy
15ab2d9e65 Update CS2-SimpleAdmin.cs 2023-12-12 20:06:01 +01:00
daffyyyy
2105d80f52 Fixed css_team message 2023-12-12 20:01:30 +01:00
daffyyyy
d677551463 Update CS2-SimpleAdmin.cs 2023-12-12 19:51:39 +01:00
daffyyyy
efd9ab5c22 css_vote and small fixes 2023-12-12 19:46:19 +01:00
daffyyyy
e6fb8bc402 css_rcon caller fix 2023-12-12 13:31:27 +01:00
daffyyyy
9712f04e38 Update Events.cs 2023-12-12 12:51:31 +01:00
daffyyyy
719caeafef Probably fix for auto ungag? 2023-12-12 12:50:02 +01:00
daffyyyy
22bb7ebac0 Update README.md 2023-12-12 12:31:54 +01:00
daffyyyy
b813a422a2 Update CS2-SimpleAdmin.cs 2023-12-12 12:24:09 +01:00
daffyyyy
36e5cd48a5 Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2023-12-12 12:22:16 +01:00
daffyyyy
34342c5134 1.2.0a UPDATE
Required CSS 124
2023-12-12 12:20:27 +01:00
Dawid Bepierszcz
5183661c2a Merge pull request #9 from DEADLYDEVIL-IR/main
Added CSS_GIVE
2023-12-10 15:28:17 +01:00
BuildTools
6c075cdafe fixed weapon checking 2023-12-10 01:33:59 +03:30
BuildTools
a56b5f7b3f fixed banned weapon checking 2023-12-10 01:15:52 +03:30
BuildTools
07b79cb91a Added a check for knifes 2023-12-10 00:56:30 +03:30
BuildTools
607c38e992 changed permissions to @css/give 2023-12-09 10:58:21 +03:30
BuildTools
dbdefa8168 fixed writing player name after giving waepon in chat 2023-12-09 10:53:13 +03:30
BuildTools
b8048e1500 Fixed server printing map instead of given weapon 2023-12-09 10:49:19 +03:30
BuildTools
59975bb49d Add the css_give command and add in README.MD 2023-12-09 10:43:33 +03:30
daffyyyy
454643431a Update Events.cs 2023-12-06 03:01:07 +01:00
daffyyyy
6ac370df74 Update CS2-SimpleAdmin.cs 2023-12-06 02:55:07 +01:00
daffyyyy
f3a1200492 Changed adminchat to teamchat 2023-12-06 02:49:31 +01:00
daffyyyy
90ca6c1ac2 Admin chat fix 2023-12-06 02:44:30 +01:00
daffyyyy
29fe968155 css_asay and @message 2023-12-06 02:39:08 +01:00
daffyyyy
fdbf1b1a5c Respecting immunity and css_team command 2023-12-06 01:55:30 +01:00
daffyyyy
dcc85b21c3 Hotfix for cssharp 101
Hotfix for non-main thread
2023-12-04 17:34:21 +01:00
daffyyyy
9c293e6201 . 2023-12-04 00:43:47 +01:00
daffyyyy
bd0f21b2ad Auto unmute after mute, max 30 minutes 2023-12-04 00:16:21 +01:00
daffyyyy
13e170a9c4 Changed to async, instantly mute in CS2-Tags plugin
- Changed to async
- instantly mute in CS2-Tags plugin
2023-12-03 23:37:44 +01:00
daffyyyy
696b8c3877 Added css_wsmap
- css_wsmap <name or id> - Change workshop map // @css/changemap
2023-12-03 21:18:43 +01:00
Dawid Bepierszcz
f19c594568 Merge pull request #1 from stefanx111/main
admin name in css_say
2023-12-03 21:11:00 +01:00
Dawid Bepierszcz
90efff5d24 Update README.md 2023-12-03 21:09:20 +01:00
Dawid Bepierszcz
2ea03caeed Update build.yml 2023-12-03 21:08:37 +01:00
daffyyyy
d16b92e694 IP address banning
- IP address banning

**Require CounterStrikeSharp v98**
2023-12-03 21:05:38 +01:00
daffyyyy
0f32daa4c0 KickTime in config, and gag 2023-12-03 19:22:34 +01:00
StefanX
86378324a7 revert: range syntax instead of Substring 2023-12-03 18:19:33 +02:00
StefanX
bce6bd4348 admin name in css_say 2023-12-03 17:31:10 +02:00
daffyyyy
5ea71f1679 New commands, addban and unban 2023-12-03 13:37:54 +01:00
daffyyyy
53812148b3 Fixed map permission 2023-12-03 01:23:46 +01:00
daffyyyy
d496977c76 Forgotten column with the reason for the ban
OOOooops :D
2023-12-03 01:22:18 +01:00
daffyyyy
35e6a52ad8 Additional commands 2023-12-03 00:50:02 +01:00
daffyyyy
68a446a515 css_admin command
Added css_admin command for displaying all commands
2023-12-03 00:15:55 +01:00
daffyyyy
0a0c0cfbc4 Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2023-12-02 23:28:33 +01:00
daffyyyy
39e73763b7 Fix in creating table 2023-12-02 23:28:09 +01:00
Dawid Bepierszcz
ad621b9cb8 Update README.md 2023-12-02 23:18:06 +01:00
daffyyyy
31bc91e0a4 Map and say commands
Source https://github.com/Hackmastr/css-basic-admin
2023-12-02 23:16:04 +01:00
daffyyyy
fa508c965a Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2023-12-02 23:02:57 +01:00
daffyyyy
d2d1f41e80 Fixed failstart :D 2023-12-02 23:02:36 +01:00
Dawid Bepierszcz
ffcd623628 Update README.md 2023-12-02 22:48:52 +01:00
Dawid Bepierszcz
e4a6c733ab Create build.yml 2023-12-02 22:47:17 +01:00
daffyyyy
6bcb10c5cf Initial commit 2023-12-02 22:44:11 +01:00
236 changed files with 56456 additions and 0 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
ko_fi: daffyy

128
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,128 @@
name: Build and Publish
on:
workflow_dispatch:
push:
branches: [ "main" ]
paths-ignore:
- '**/README.md'
pull_request:
branches: [ "main" ]
paths-ignore:
- '**/README.md'
env:
PROJECT_PATH_CS2_SIMPLEADMIN: "CS2-SimpleAdmin/CS2-SimpleAdmin.csproj"
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"
TMP_PATH: "./tmp"
jobs:
build:
runs-on: ubuntu-latest
permissions: write-all
outputs:
build_version: ${{ steps.get_version.outputs.VERSION }}
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Get Version
id: get_version
run: echo "VERSION=$(cat CS2-SimpleAdmin/VERSION)" >> $GITHUB_OUTPUT
- name: Restore & Build All Projects
run: |
dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }}
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 }}
- name: Extract & Zip StatusBlocker Linux
run: |
mkdir -p statusblocker-linux &&
tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-linux.tar.gz -C statusblocker-linux &&
cd statusblocker-linux &&
zip -r ../StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip ./*
- name: Extract & Zip StatusBlocker Windows
run: |
mkdir -p statusblocker-windows &&
tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-windows.tar.gz -C statusblocker-windows &&
cd statusblocker-windows &&
zip -r ../StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip ./*
- name: Upload all artifacts
uses: actions/upload-artifact@v4
with:
name: CS2-SimpleAdmin-Build-Artifacts
path: |
CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip
StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip
StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip
publish:
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: write-all
steps:
- uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: CS2-SimpleAdmin-Build-Artifacts
path: .
- name: Unzip main build artifact
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
StatusBlocker-linux-${{ needs.build.outputs.build_version }}.zip
StatusBlocker-windows-${{ needs.build.outputs.build_version }}.zip
name: "CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}"
tag: "build-${{ needs.build.outputs.build_version }}"
body: |
Place the files in your server as follows:
- CS2-SimpleAdmin:
Place the files inside the addons/counterstrikesharp directory.
After the first launch, configure the plugin using the JSON config file at:
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json
- StatusBlocker:
Place the plugin files directly into the addons directory.
This plugin is a Metamod module for the StealthModule and does not require a subfolder.
Remember to restart or reload your game server after installing and configuring the plugins.

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
bin/
obj/
.vs/
.git
.vscode/
.idea/
Modules/CS2-SimpleAdmin_PlayTimeModule
CS2-SimpleAdmin.sln.DotSettings.user
Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.sln.DotSettings.user
CS2-SimpleAdmin_BanSoundModule — kopia
*.user
CLAUDE.md

20
CS2-SimpleAdmin-docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,41 @@
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
## Installation
```bash
yarn
```
## Local Development
```bash
yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
## Build
```bash
yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
## Deployment
Using SSH:
```bash
USE_SSH=true yarn deploy
```
Not using SSH:
```bash
GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

View File

@@ -0,0 +1,465 @@
---
sidebar_position: 2
---
# Commands API
Complete reference for command registration and management.
## Command Registration
### RegisterCommand
Register a new command that integrates with CS2-SimpleAdmin.
```csharp
void RegisterCommand(string name, string? description, CommandInfo.CommandCallback callback)
```
**Parameters:**
- `name` - Command name (e.g., "css_mycommand")
- `description` - Command description (optional)
- `callback` - Method to call when command is executed
**Example:**
```csharp
_api.RegisterCommand("css_mycommand", "My custom command", OnMyCommand);
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Command logic here
}
```
**Throws:**
- `ArgumentException` - If command name is null or empty
- `ArgumentNullException` - If callback is null
---
### UnRegisterCommand
Unregister a previously registered command.
```csharp
void UnRegisterCommand(string commandName)
```
**Parameters:**
- `commandName` - Name of command to unregister
**Example:**
```csharp
_api.UnRegisterCommand("css_mycommand");
```
**Best Practice:**
Always unregister commands in your plugin's `Unload()` method:
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.UnRegisterCommand("css_mycommand");
}
```
---
## Target Parsing
### GetTarget
Parse player targets from command arguments.
```csharp
TargetResult? GetTarget(CommandInfo command)
```
**Parameters:**
- `command` - Command info containing arguments
**Returns:**
- `TargetResult` - Contains matched players
- `null` - If no targets found or error
**Example:**
```csharp
[CommandHelper(1, "<#userid or name>")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players)
{
// Do something with player
}
}
```
**Supported Target Syntax:**
- `@all` - All players
- `@ct` - All Counter-Terrorists
- `@t` - All Terrorists
- `@spec` - All spectators
- `@alive` - All alive players
- `@dead` - All dead players
- `@bot` - All bots
- `@human` - All human players
- `@me` - Command caller
- `#123` - Player by user ID
- `PlayerName` - Player by name (partial match)
---
## Command Logging
### LogCommand (with CommandInfo)
Log a command execution with full command info.
```csharp
void LogCommand(CCSPlayerController? caller, CommandInfo command)
```
**Parameters:**
- `caller` - Player who executed command (null for console)
- `command` - Command info object
**Example:**
```csharp
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Execute command logic
// Log the command
_api!.LogCommand(caller, command);
}
```
---
### LogCommand (with string)
Log a command execution with custom command string.
```csharp
void LogCommand(CCSPlayerController? caller, string command)
```
**Parameters:**
- `caller` - Player who executed command (null for console)
- `command` - Command string to log
**Example:**
```csharp
private void DoAction(CCSPlayerController? caller, CCSPlayerController target)
{
// Perform action
// Log with custom string
_api!.LogCommand(caller, $"css_mycommand {target.PlayerName}");
}
```
---
## Complete Example
### Basic Command
```csharp
private void RegisterCommands()
{
_api!.RegisterCommand("css_hello", "Say hello to a player", OnHelloCommand);
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnHelloCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid))
{
player.PrintToChat($"Hello {player.PlayerName}!");
}
_api.LogCommand(caller, command);
}
public override void Unload(bool hotReload)
{
_api?.UnRegisterCommand("css_hello");
}
```
---
### Command with Permission Check
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/ban")]
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
// Get targets
var targets = _api!.GetTarget(command);
if (targets == null) return;
// Filter for players caller can target
var validPlayers = targets.Players
.Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
.ToList();
foreach (var player in validPlayers)
{
// Issue ban
_api.IssuePenalty(player, caller, PenaltyType.Ban, "Banned via command", 1440);
}
_api.LogCommand(caller, command);
}
```
---
### Command with Arguments
```csharp
[CommandHelper(2, "<#userid or name> <value>")]
[RequiresPermissions("@css/slay")]
private void OnSetHpCommand(CCSPlayerController? caller, CommandInfo command)
{
// Parse HP value
if (!int.TryParse(command.GetArg(2), out int hp))
{
caller?.PrintToChat("Invalid HP value!");
return;
}
// Get targets
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && p.PawnIsAlive))
{
player.PlayerPawn?.Value?.SetHealth(hp);
}
_api.LogCommand(caller, $"css_sethp {hp}");
}
```
---
## Best Practices
### 1. Always Validate Targets
```csharp
var targets = _api!.GetTarget(command);
if (targets == null) return; // No targets found
// Filter for valid players
var validPlayers = targets.Players
.Where(p => p.IsValid && !p.IsBot)
.ToList();
if (validPlayers.Count == 0)
{
caller?.PrintToChat("No valid targets found!");
return;
}
```
### 2. Check Immunity
```csharp
foreach (var player in targets.Players)
{
// Check if caller can target this player (immunity check)
if (!caller!.CanTarget(player))
{
caller.PrintToChat($"You cannot target {player.PlayerName}!");
continue;
}
// Safe to target player
DoAction(player);
}
```
### 3. Always Log Commands
```csharp
// Log every admin command execution
_api.LogCommand(caller, command);
```
### 4. Use CommandHelper Attribute
```csharp
// Specify minimum args and usage
[CommandHelper(minArgs: 1, usage: "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnCommand(CCSPlayerController? caller, CommandInfo command)
{
// CounterStrikeSharp validates args automatically
}
```
### 5. Cleanup on Unload
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister ALL commands
_api.UnRegisterCommand("css_command1");
_api.UnRegisterCommand("css_command2");
}
```
---
## Common Patterns
### Multiple Aliases
```csharp
// Register same command with multiple aliases
var aliases = new[] { "css_mycommand", "css_mycmd", "css_mc" };
foreach (var alias in aliases)
{
_api.RegisterCommand(alias, "My command", OnMyCommand);
}
// Unregister all
public override void Unload(bool hotReload)
{
foreach (var alias in aliases)
{
_api?.UnRegisterCommand(alias);
}
}
```
### Command from Config
```csharp
// In Config.cs
public List<string> MyCommands { get; set; } = ["css_mycommand"];
// In Plugin
private void RegisterCommands()
{
foreach (var cmd in Config.MyCommands)
{
_api!.RegisterCommand(cmd, "Description", OnMyCommand);
}
}
// Allows users to add aliases or disable by clearing list
```
### Target Filtering
```csharp
// Get only alive players
var alivePlayers = targets.Players
.Where(p => p.IsValid && p.PawnIsAlive)
.ToList();
// Get only enemy team
var enemies = targets.Players
.Where(p => p.IsValid && p.Team != caller!.Team)
.ToList();
// Get targetable players
var targetable = targets.Players
.Where(p => p.IsValid && caller!.CanTarget(p))
.ToList();
```
---
## Error Handling
### Command Registration Errors
```csharp
try
{
_api.RegisterCommand("css_mycommand", "Description", OnMyCommand);
}
catch (ArgumentException ex)
{
Logger.LogError($"Failed to register command: {ex.Message}");
}
```
### Target Parsing Errors
```csharp
var targets = _api!.GetTarget(command);
if (targets == null)
{
// Target parsing failed
// Error message already sent to caller by SimpleAdmin
return;
}
if (targets.Players.Count == 0)
{
caller?.PrintToChat("No players matched your target!");
return;
}
```
---
## Performance Tips
### Cache Command Lists
```csharp
// Don't create new list every time
private readonly List<string> _commandAliases = new() { "css_cmd1", "css_cmd2" };
private void RegisterCommands()
{
foreach (var cmd in _commandAliases)
{
_api!.RegisterCommand(cmd, "Description", OnCommand);
}
}
```
### Efficient Target Filtering
```csharp
// ✅ Good - single LINQ query
var players = targets.Players
.Where(p => p.IsValid && !p.IsBot && p.PawnIsAlive && caller!.CanTarget(p))
.ToList();
// ❌ Bad - multiple iterations
var players = targets.Players.Where(p => p.IsValid).ToList();
players = players.Where(p => !p.IsBot).ToList();
players = players.Where(p => p.PawnIsAlive).ToList();
```
---
## Related APIs
- **[Menus API](menus)** - Create interactive menus
- **[Penalties API](penalties)** - Issue penalties from commands
- **[Utilities API](utilities)** - Helper functions for commands

View File

@@ -0,0 +1,642 @@
---
sidebar_position: 5
---
# Events API
Complete reference for CS2-SimpleAdmin event system.
## Event System Overview
CS2-SimpleAdmin exposes events that allow modules to react to plugin actions and state changes.
**All events use C# event delegates and should be subscribed to in `OnAllPluginsLoaded` and unsubscribed in `Unload`.**
---
## Plugin Lifecycle Events
### OnSimpleAdminReady
Fired when CS2-SimpleAdmin is fully initialized and ready.
```csharp
event Action? OnSimpleAdminReady
```
**When Fired:**
- After plugin load
- After database initialization
- After migrations complete
- Menu system ready
**Example:**
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
// Subscribe to ready event
_api.OnSimpleAdminReady += OnSimpleAdminReady;
// Also call directly for hot reload case
OnSimpleAdminReady();
}
private void OnSimpleAdminReady()
{
Logger.LogInformation("SimpleAdmin is ready!");
// Register menus (requires SimpleAdmin to be ready)
RegisterMenus();
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.OnSimpleAdminReady -= OnSimpleAdminReady;
}
```
**Best Practice:**
Always call your handler directly after subscribing to handle hot reload:
```csharp
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // ← Also call directly
```
---
## Penalty Events
### OnPlayerPenaltied
Fired when an **online player** receives a penalty.
```csharp
event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltied
```
**Parameters:**
1. `PlayerInfo player` - Player who received penalty
2. `PlayerInfo? admin` - Admin who issued penalty (null if console)
3. `PenaltyType penaltyType` - Type of penalty
4. `string reason` - Penalty reason
5. `int duration` - Duration in minutes (0 = permanent)
6. `int? penaltyId` - Database penalty ID (null if not stored)
7. `int? serverId` - Server ID (null in single-server mode)
**Example:**
```csharp
_api.OnPlayerPenaltied += OnPlayerPenaltied;
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
var adminName = admin?.PlayerName ?? "Console";
Logger.LogInformation($"{adminName} penaltied {player.PlayerName}: {type} ({duration}m) - {reason}");
// React to specific penalty types
switch (type)
{
case PenaltyType.Ban:
// Log ban to external system
LogBanToWebhook(player, admin, reason, duration);
break;
case PenaltyType.Warn:
// Check warning count
if (player.Warnings >= 3)
{
Logger.LogWarning($"{player.PlayerName} has {player.Warnings} warnings!");
}
break;
}
}
```
---
### OnPlayerPenaltiedAdded
Fired when a penalty is added to an **offline player** by SteamID.
```csharp
event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltiedAdded
```
**Parameters:**
1. `SteamID steamId` - Target player's SteamID
2. `PlayerInfo? admin` - Admin who issued penalty
3. `PenaltyType penaltyType` - Type of penalty
4. `string reason` - Penalty reason
5. `int duration` - Duration in minutes
6. `int? penaltyId` - Database penalty ID
7. `int? serverId` - Server ID
**Example:**
```csharp
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
private void OnPlayerPenaltiedAdded(
SteamID steamId,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
var adminName = admin?.PlayerName ?? "Console";
Logger.LogInformation($"Offline penalty: {adminName} -> SteamID {steamId}: {type} ({duration}m)");
// Log to external database or webhook
if (type == PenaltyType.Ban)
{
LogOfflineBan(steamId, admin, reason, duration);
}
}
```
---
## Admin Activity Events
### OnAdminShowActivity
Fired when an admin action is displayed to players.
```csharp
event Action<string, string?, bool, object>? OnAdminShowActivity
```
**Parameters:**
1. `string messageKey` - Translation key for the message
2. `string? callerName` - Admin name (null if console)
3. `bool dontPublish` - If true, don't broadcast to other systems
4. `object messageArgs` - Arguments for message formatting
**Example:**
```csharp
_api.OnAdminShowActivity += OnAdminShowActivity;
private void OnAdminShowActivity(
string messageKey,
string? callerName,
bool dontPublish,
object messageArgs)
{
if (dontPublish) return;
Logger.LogInformation($"Admin activity: {messageKey} by {callerName ?? "Console"}");
// Log to Discord, database, etc.
LogAdminAction(messageKey, callerName, messageArgs);
}
```
---
### OnAdminToggleSilent
Fired when an admin toggles silent mode.
```csharp
event Action<int, bool>? OnAdminToggleSilent
```
**Parameters:**
1. `int slot` - Player slot of admin
2. `bool status` - New silent status (true = silent, false = normal)
**Example:**
```csharp
_api.OnAdminToggleSilent += OnAdminToggleSilent;
private void OnAdminToggleSilent(int slot, bool status)
{
var player = Utilities.GetPlayerFromSlot(slot);
if (player == null) return;
var statusText = status ? "enabled" : "disabled";
Logger.LogInformation($"{player.PlayerName} {statusText} silent mode");
// Update UI or external systems
UpdateAdminStatus(player, status);
}
```
---
## Complete Examples
### Ban Logging System
```csharp
public class BanLogger
{
private ICS2_SimpleAdminApi? _api;
public void Initialize(ICS2_SimpleAdminApi api)
{
_api = api;
// Subscribe to both ban events
_api.OnPlayerPenaltied += OnPlayerPenaltied;
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
}
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
if (type != PenaltyType.Ban) return;
// Log to file
File.AppendAllText("bans.log",
$"[{DateTime.Now}] {player.PlayerName} ({player.SteamId}) " +
$"banned by {admin?.PlayerName ?? "Console"} " +
$"for {duration} minutes: {reason}\n");
// Send to Discord webhook
SendDiscordNotification(
$"🔨 **Ban Issued**\n" +
$"Player: {player.PlayerName}\n" +
$"Admin: {admin?.PlayerName ?? "Console"}\n" +
$"Duration: {FormatDuration(duration)}\n" +
$"Reason: {reason}"
);
}
private void OnPlayerPenaltiedAdded(
SteamID steamId,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
if (type != PenaltyType.Ban) return;
File.AppendAllText("bans.log",
$"[{DateTime.Now}] Offline ban: SteamID {steamId} " +
$"by {admin?.PlayerName ?? "Console"} " +
$"for {duration} minutes: {reason}\n");
}
private string FormatDuration(int minutes)
{
if (minutes == 0) return "Permanent";
if (minutes < 60) return $"{minutes} minutes";
if (minutes < 1440) return $"{minutes / 60} hours";
return $"{minutes / 1440} days";
}
}
```
---
### Warning Escalation System
```csharp
public class WarningEscalation
{
private ICS2_SimpleAdminApi? _api;
public void Initialize(ICS2_SimpleAdminApi api)
{
_api = api;
_api.OnPlayerPenaltied += OnPlayerPenaltied;
}
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
// Only handle warnings
if (type != PenaltyType.Warn) return;
Logger.LogInformation($"{player.PlayerName} now has {player.Warnings} warnings");
// Auto-escalate based on warning count
if (player.Warnings >= 3)
{
// 3 warnings = 1 hour ban
_api.IssuePenalty(
GetPlayerController(player.SteamId),
null,
PenaltyType.Ban,
"Automatic: 3 warnings",
60
);
}
else if (player.Warnings >= 5)
{
// 5 warnings = 1 day ban
_api.IssuePenalty(
GetPlayerController(player.SteamId),
null,
PenaltyType.Ban,
"Automatic: 5 warnings",
1440
);
}
}
}
```
---
### Admin Activity Monitor
```csharp
public class AdminMonitor
{
private readonly Dictionary<string, int> _adminActions = new();
public void Initialize(ICS2_SimpleAdminApi api)
{
api.OnAdminShowActivity += OnAdminShowActivity;
}
private void OnAdminShowActivity(
string messageKey,
string? callerName,
bool dontPublish,
object messageArgs)
{
if (callerName == null) return; // Ignore console actions
// Track admin actions
if (!_adminActions.ContainsKey(callerName))
{
_adminActions[callerName] = 0;
}
_adminActions[callerName]++;
// Log every 10th action
if (_adminActions[callerName] % 10 == 0)
{
Logger.LogInformation(
$"{callerName} has performed {_adminActions[callerName]} admin actions"
);
}
// Alert if admin is very active
if (_adminActions[callerName] > 100)
{
Logger.LogWarning($"{callerName} has performed many actions ({_adminActions[callerName]})");
}
}
}
```
---
## Best Practices
### 1. Always Unsubscribe
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
// Subscribe
_api.OnPlayerPenaltied += OnPlayerPenaltied;
_api.OnAdminShowActivity += OnAdminShowActivity;
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
// ALWAYS unsubscribe
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
_api.OnAdminShowActivity -= OnAdminShowActivity;
}
```
### 2. Handle Null Admins
```csharp
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin, // ← Can be null!
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
var adminName = admin?.PlayerName ?? "Console";
// Use adminName safely
}
```
### 3. Use Events for Integration
```csharp
// ✅ Good - React to penalties
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
if (type == PenaltyType.Ban)
{
NotifyExternalSystem(player, reason);
}
};
// ❌ Bad - Wrapping penalty methods
// Don't wrap IssuePenalty, use events instead
```
### 4. Check Event Parameters
```csharp
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
// Check nullable parameters
if (penaltyId.HasValue)
{
Logger.LogInformation($"Penalty ID: {penaltyId.Value}");
}
if (serverId.HasValue)
{
Logger.LogInformation($"Server ID: {serverId.Value}");
}
}
```
### 5. OnSimpleAdminReady Pattern
```csharp
// ✅ Good - Handles both normal load and hot reload
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus();
// ❌ Bad - Only works on normal load
_api.OnSimpleAdminReady += RegisterMenus;
```
---
## Common Patterns
### Event-Based Statistics
```csharp
public class ServerStatistics
{
private int _totalBans;
private int _totalMutes;
private int _totalWarnings;
public void Initialize(ICS2_SimpleAdminApi api)
{
api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
switch (type)
{
case PenaltyType.Ban:
_totalBans++;
break;
case PenaltyType.Mute:
case PenaltyType.Gag:
case PenaltyType.Silence:
_totalMutes++;
break;
case PenaltyType.Warn:
_totalWarnings++;
break;
}
};
}
public void PrintStatistics()
{
Logger.LogInformation($"Server Statistics:");
Logger.LogInformation($"Total Bans: {_totalBans}");
Logger.LogInformation($"Total Mutes: {_totalMutes}");
Logger.LogInformation($"Total Warnings: {_totalWarnings}");
}
}
```
### Conditional Event Handling
```csharp
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// Only handle bans
if (type != PenaltyType.Ban) return;
// Only handle permanent bans
if (duration != 0) return;
// Only handle admin-issued bans
if (admin == null) return;
// Process permanent admin bans
NotifyImportantBan(player, admin, reason);
};
```
---
## Performance Considerations
### Async Operations in Events
```csharp
// ⚠️ Be careful with async in event handlers
_api.OnPlayerPenaltied += async (player, admin, type, reason, duration, id, sid) =>
{
// Don't block the game thread
await Task.Run(() =>
{
// Long-running operation
LogToExternalDatabase(player, type, reason);
});
};
```
### Efficient Event Handlers
```csharp
// ✅ Good - Quick processing
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// Quick logging
Logger.LogInformation($"Ban: {player.PlayerName}");
};
// ❌ Bad - Heavy processing
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// Don't do expensive operations synchronously
SendEmailNotification(player); // ← This blocks the game thread!
};
```
---
## Troubleshooting
### Event Not Firing
**Check:**
1. Did you subscribe to the event?
2. Is `_api` not null?
3. Are you testing the right scenario?
4. Check server console for errors
### Memory Leaks
**Always unsubscribe:**
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unsubscribe ALL events
_api.OnSimpleAdminReady -= OnReady;
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
// ... etc
}
```
---
## Related APIs
- **[Penalties API](penalties)** - Issue penalties that trigger events
- **[Commands API](commands)** - Commands that may trigger admin activity
- **[Utilities API](utilities)** - Helper functions for event handlers

View File

@@ -0,0 +1,706 @@
---
sidebar_position: 3
---
# Menus API
Complete reference for creating interactive admin menus.
## Menu Categories
### RegisterMenuCategory
Register a new menu category in the admin menu.
```csharp
void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")
```
**Parameters:**
- `categoryId` - Unique identifier for the category
- `categoryName` - Display name shown in menu
- `permission` - Required permission to see category (default: "@css/generic")
**Example:**
```csharp
_api.RegisterMenuCategory("mycategory", "My Custom Category", "@css/generic");
```
**Best Practice:**
Register categories in the `OnSimpleAdminReady` event handler:
```csharp
_api.OnSimpleAdminReady += () =>
{
_api.RegisterMenuCategory("mycategory", Localizer?["category_name"] ?? "My Category");
};
```
---
## Menu Registration
### RegisterMenu (Basic)
Register a menu within a category.
```csharp
void RegisterMenu(
string categoryId,
string menuId,
string menuName,
Func<CCSPlayerController, object> menuFactory,
string? permission = null,
string? commandName = null
)
```
**Parameters:**
- `categoryId` - Category to add menu to
- `menuId` - Unique menu identifier
- `menuName` - Display name in menu
- `menuFactory` - Function that creates the menu
- `permission` - Required permission (optional)
- `commandName` - Command for permission override (optional, e.g., "css_god")
**Example:**
```csharp
_api.RegisterMenu(
"mycategory",
"mymenu",
"My Menu",
CreateMyMenu,
"@css/generic"
);
private object CreateMyMenu(CCSPlayerController player)
{
var menu = _api!.CreateMenuWithBack("My Menu", "mycategory", player);
// Add options...
return menu;
}
```
---
### RegisterMenu (with MenuContext) ⭐ RECOMMENDED
Register a menu with automatic context passing - **eliminates duplication!**
```csharp
void RegisterMenu(
string categoryId,
string menuId,
string menuName,
Func<CCSPlayerController, MenuContext, object> menuFactory,
string? permission = null,
string? commandName = null
)
```
**Parameters:**
- `categoryId` - Category to add menu to
- `menuId` - Unique menu identifier
- `menuName` - Display name in menu
- `menuFactory` - Function that receives player AND context
- `permission` - Required permission (optional)
- `commandName` - Command for permission override (optional)
**Example:**
```csharp
// ✅ NEW WAY - No duplication!
_api.RegisterMenu(
"fun",
"god",
"God Mode",
CreateGodMenu,
"@css/cheats",
"css_god"
);
private object CreateGodMenu(CCSPlayerController admin, MenuContext context)
{
// context contains: CategoryId, MenuId, MenuTitle, Permission, CommandName
return _api!.CreateMenuWithPlayers(
context, // ← Automatically uses "God Mode" and "fun"
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) => ToggleGod(admin, target)
);
}
// ❌ OLD WAY - Had to repeat "God Mode" and "fun"
private object CreateGodMenuOld(CCSPlayerController admin)
{
return _api!.CreateMenuWithPlayers(
"God Mode", // ← Repeated from RegisterMenu
"fun", // ← Repeated from RegisterMenu
admin,
filter,
action
);
}
```
**MenuContext Properties:**
```csharp
public class MenuContext
{
public string CategoryId { get; } // e.g., "fun"
public string MenuId { get; } // e.g., "god"
public string MenuTitle { get; } // e.g., "God Mode"
public string? Permission { get; } // e.g., "@css/cheats"
public string? CommandName { get; } // e.g., "css_god"
}
```
---
### UnregisterMenu
Remove a menu from a category.
```csharp
void UnregisterMenu(string categoryId, string menuId)
```
**Example:**
```csharp
public override void Unload(bool hotReload)
{
_api?.UnregisterMenu("mycategory", "mymenu");
}
```
---
## Menu Creation
### CreateMenuWithBack
Create a menu with automatic back button.
```csharp
object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)
```
**Parameters:**
- `title` - Menu title
- `categoryId` - Category for back button navigation
- `player` - Player viewing the menu
**Example:**
```csharp
var menu = _api!.CreateMenuWithBack("My Menu", "mycategory", admin);
_api.AddMenuOption(menu, "Option 1", _ => DoAction1());
_api.AddMenuOption(menu, "Option 2", _ => DoAction2());
return menu;
```
---
### CreateMenuWithBack (with Context) ⭐ RECOMMENDED
Create a menu using context - **no duplication!**
```csharp
object CreateMenuWithBack(MenuContext context, CCSPlayerController player)
```
**Example:**
```csharp
private object CreateMenu(CCSPlayerController admin, MenuContext context)
{
// ✅ Uses context.MenuTitle and context.CategoryId automatically
var menu = _api!.CreateMenuWithBack(context, admin);
_api.AddMenuOption(menu, "Option 1", _ => DoAction1());
return menu;
}
```
---
### CreateMenuWithPlayers
Create a menu showing a list of players.
```csharp
object CreateMenuWithPlayers(
string title,
string categoryId,
CCSPlayerController admin,
Func<CCSPlayerController, bool> filter,
Action<CCSPlayerController, CCSPlayerController> onSelect
)
```
**Parameters:**
- `title` - Menu title
- `categoryId` - Category for back button
- `admin` - Admin viewing menu
- `filter` - Function to filter which players to show
- `onSelect` - Action when player is selected (admin, selectedPlayer)
**Example:**
```csharp
return _api!.CreateMenuWithPlayers(
"Select Player",
"mycategory",
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) =>
{
// Do something with selected player
DoAction(admin, target);
}
);
```
---
### CreateMenuWithPlayers (with Context) ⭐ RECOMMENDED
```csharp
object CreateMenuWithPlayers(
MenuContext context,
CCSPlayerController admin,
Func<CCSPlayerController, bool> filter,
Action<CCSPlayerController, CCSPlayerController> onSelect
)
```
**Example:**
```csharp
private object CreatePlayerMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context, // ← Automatically uses correct title and category!
admin,
player => player.PawnIsAlive && admin.CanTarget(player),
(admin, target) => PerformAction(admin, target)
);
}
```
---
## Menu Options
### AddMenuOption
Add a clickable option to a menu.
```csharp
void AddMenuOption(
object menu,
string name,
Action<CCSPlayerController> action,
bool disabled = false,
string? permission = null
)
```
**Parameters:**
- `menu` - Menu to add option to
- `name` - Option display text
- `action` - Function called when selected
- `disabled` - Whether option is disabled (default: false)
- `permission` - Required permission to see option (optional)
**Example:**
```csharp
var menu = _api!.CreateMenuWithBack("Actions", "mycategory", admin);
_api.AddMenuOption(menu, "Heal Player", _ =>
{
target.SetHp(100);
});
_api.AddMenuOption(menu, "Admin Only Option", _ =>
{
// Admin action
}, false, "@css/root");
return menu;
```
---
### AddSubMenu
Add a submenu option that opens another menu.
```csharp
void AddSubMenu(
object menu,
string name,
Func<CCSPlayerController, object> subMenuFactory,
bool disabled = false,
string? permission = null
)
```
**Parameters:**
- `menu` - Parent menu
- `name` - Submenu option display text
- `subMenuFactory` - Function that creates the submenu
- `disabled` - Whether option is disabled (default: false)
- `permission` - Required permission (optional)
**Example:**
```csharp
var menu = _api!.CreateMenuWithBack("Main Menu", "mycategory", admin);
_api.AddSubMenu(menu, "Player Actions", admin =>
{
return CreatePlayerActionsMenu(admin);
});
_api.AddSubMenu(menu, "Server Settings", admin =>
{
return CreateServerSettingsMenu(admin);
}, false, "@css/root");
return menu;
```
---
## Opening Menus
### OpenMenu
Display a menu to a player.
```csharp
void OpenMenu(object menu, CCSPlayerController player)
```
**Example:**
```csharp
var menu = CreateMyMenu(player);
_api!.OpenMenu(menu, player);
```
**Note:** Usually menus open automatically when selected, but this can be used for direct opening.
---
## Complete Examples
### Simple Player Selection Menu
```csharp
private void RegisterMenus()
{
_api!.RegisterMenuCategory("actions", "Player Actions", "@css/generic");
_api.RegisterMenu(
"actions",
"slay",
"Slay Player",
CreateSlayMenu,
"@css/slay"
);
}
private object CreateSlayMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.PawnIsAlive && admin.CanTarget(player),
(admin, target) =>
{
target.PlayerPawn?.Value?.CommitSuicide(false, true);
admin.PrintToChat($"Slayed {target.PlayerName}");
}
);
}
```
---
### Nested Menu with Value Selection
```csharp
private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => p.PawnIsAlive && admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateHpValueMenu(admin, player);
});
}
return menu;
}
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Set HP: {target.PlayerName}", "mycategory", admin);
var hpValues = new[] { 1, 10, 50, 100, 200, 500 };
foreach (var hp in hpValues)
{
_api.AddMenuOption(menu, $"{hp} HP", _ =>
{
if (target.IsValid && target.PawnIsAlive)
{
target.PlayerPawn?.Value?.SetHealth(hp);
admin.PrintToChat($"Set {target.PlayerName} HP to {hp}");
}
});
}
return menu;
}
```
---
### Menu with Permissions
```csharp
private object CreateAdminMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
// Everyone with menu access sees this
_api.AddMenuOption(menu, "Basic Action", _ => DoBasicAction());
// Only root admins see this
_api.AddMenuOption(menu, "Dangerous Action", _ =>
{
DoDangerousAction();
}, false, "@css/root");
// Submenu with permission
_api.AddSubMenu(menu, "Advanced Options", admin =>
{
return CreateAdvancedMenu(admin);
}, false, "@css/root");
return menu;
}
```
---
### Dynamic Menu with Current State
```csharp
private object CreateToggleMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => admin.CanTarget(p));
foreach (var player in players)
{
// Show current state in option name
bool hasGod = GodPlayers.Contains(player.Slot);
string status = hasGod ? "✓ ON" : "✗ OFF";
_api.AddMenuOption(menu, $"{player.PlayerName} ({status})", _ =>
{
if (hasGod)
GodPlayers.Remove(player.Slot);
else
GodPlayers.Add(player.Slot);
// Recreate menu to show updated state
var newMenu = CreateToggleMenu(admin, context);
_api.OpenMenu(newMenu, admin);
});
}
return menu;
}
```
---
## Best Practices
### 1. Use MenuContext
```csharp
// ✅ Good - Uses context
_api.RegisterMenu("cat", "id", "Title", CreateMenu, "@css/generic");
private object CreateMenu(CCSPlayerController admin, MenuContext context)
{
return _api.CreateMenuWithPlayers(context, admin, filter, action);
}
// ❌ Bad - Duplicates title and category
_api.RegisterMenu("cat", "id", "Title", CreateMenuOld, "@css/generic");
private object CreateMenuOld(CCSPlayerController admin)
{
return _api.CreateMenuWithPlayers("Title", "cat", admin, filter, action);
}
```
### 2. Register in OnSimpleAdminReady
```csharp
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Also call directly for hot reload
private void RegisterMenus()
{
if (_menusRegistered) return;
_api!.RegisterMenuCategory("category", "Category Name");
_api.RegisterMenu("category", "menu", "Menu Name", CreateMenu);
_menusRegistered = true;
}
```
### 3. Always Unregister
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.UnregisterMenu("category", "menu");
_api.OnSimpleAdminReady -= RegisterMenus;
}
```
### 4. Validate Player State
```csharp
private object CreateMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.IsValid && // Player exists
!player.IsBot && // Not a bot
player.PawnIsAlive && // Alive
admin.CanTarget(player), // Can be targeted
(admin, target) =>
{
// Extra validation before action
if (!target.IsValid || !target.PawnIsAlive)
return;
DoAction(admin, target);
}
);
}
```
### 5. Use Translations for Menu Names
```csharp
_api.RegisterMenuCategory(
"mycategory",
Localizer?["category_name"] ?? "Default Name",
"@css/generic"
);
_api.RegisterMenu(
"mycategory",
"mymenu",
Localizer?["menu_name"] ?? "Default Menu",
CreateMenu
);
```
---
## Permission Override
The `commandName` parameter allows server admins to override menu permissions via CounterStrikeSharp's admin system.
**Example:**
```csharp
_api.RegisterMenu(
"fun",
"god",
"God Mode",
CreateGodMenu,
"@css/cheats", // Default permission
"css_god" // Command name for override
);
```
**Admin config can override:**
```json
{
"css_god": ["@css/vip"]
}
```
Now VIPs will see the God Mode menu instead of requiring @css/cheats!
---
## Common Patterns
### Player List with Actions
```csharp
private object CreatePlayerListMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
foreach (var player in _api.GetValidPlayers())
{
if (!admin.CanTarget(player)) continue;
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
var actionMenu = _api.CreateMenuWithBack($"Actions: {player.PlayerName}", context.CategoryId, admin);
_api.AddMenuOption(actionMenu, "Slay", _ => player.CommitSuicide());
_api.AddMenuOption(actionMenu, "Kick", _ => KickPlayer(player));
_api.AddMenuOption(actionMenu, "Ban", _ => BanPlayer(admin, player));
return actionMenu;
});
}
return menu;
}
```
### Category-Based Organization
```csharp
private void RegisterAllMenus()
{
// Player management category
_api!.RegisterMenuCategory("players", "Player Management", "@css/generic");
_api.RegisterMenu("players", "kick", "Kick Player", CreateKickMenu, "@css/kick");
_api.RegisterMenu("players", "ban", "Ban Player", CreateBanMenu, "@css/ban");
// Server management category
_api.RegisterMenuCategory("server", "Server Management", "@css/generic");
_api.RegisterMenu("server", "map", "Change Map", CreateMapMenu, "@css/changemap");
_api.RegisterMenu("server", "settings", "Settings", CreateSettingsMenu, "@css/root");
}
```
---
## Related APIs
- **[Commands API](commands)** - Command integration
- **[Penalties API](penalties)** - Issue penalties from menus
- **[Utilities API](utilities)** - Helper functions for menus

View File

@@ -0,0 +1,621 @@
---
sidebar_position: 1
---
# API Overview
Complete reference for the CS2-SimpleAdmin API (ICS2_SimpleAdminApi).
## Introduction
The CS2-SimpleAdmin API is exposed via the `ICS2_SimpleAdminApi` interface, accessible through CounterStrikeSharp's capability system.
---
## Getting the API
### Using Capability System
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
public class YourPlugin : 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!");
Unload(false);
return;
}
// API is ready to use
RegisterFeatures();
}
}
```
### Static Capability Reference
```csharp
// Alternative approach
var capability = ICS2_SimpleAdminApi.PluginCapability;
var api = capability?.Get();
```
---
## API Categories
The API is organized into logical categories:
| Category | Description | Learn More |
|----------|-------------|------------|
| **Commands** | Register/unregister commands, parse targets | [](commands) |
| **Menus** | Create admin menus with player selection | [](menus) |
| **Penalties** | Issue bans, mutes, gags, warnings | [](penalties) |
| **Events** | Subscribe to plugin events | [](events) |
| **Utilities** | Helper functions, player info, activity messages | [](utilities) |
---
## Quick Reference
### Command Management
```csharp
// Register command
_api.RegisterCommand(name, description, callback);
// Unregister command
_api.UnRegisterCommand(name);
// Parse player targets
var targets = _api.GetTarget(command);
// Log command
_api.LogCommand(caller, command);
```
**[Full Documentation →](commands)**
---
### Menu System
```csharp
// Register category
_api.RegisterMenuCategory(categoryId, categoryName, permission);
// Register menu
_api.RegisterMenu(categoryId, menuId, menuName, menuFactory, permission, commandName);
// Create menu with players
_api.CreateMenuWithPlayers(context, admin, filter, onSelect);
// Create menu with back button
_api.CreateMenuWithBack(context, admin);
// Add menu option
_api.AddMenuOption(menu, name, action, disabled, permission);
// Add submenu
_api.AddSubMenu(menu, name, subMenuFactory, disabled, permission);
// Open menu
_api.OpenMenu(menu, player);
// Unregister menu
_api.UnregisterMenu(categoryId, menuId);
```
**[Full Documentation →](menus)**
---
### Penalty Management
```csharp
// Issue penalty to online player
_api.IssuePenalty(player, admin, penaltyType, reason, duration);
// Issue penalty by SteamID
_api.IssuePenalty(steamId, admin, penaltyType, reason, duration);
// Get player info
var playerInfo = _api.GetPlayerInfo(player);
// Get mute status
var muteStatus = _api.GetPlayerMuteStatus(player);
```
**Penalty Types:**
- `PenaltyType.Ban` - Ban player
- `PenaltyType.Kick` - Kick player
- `PenaltyType.Gag` - Block text chat
- `PenaltyType.Mute` - Block voice chat
- `PenaltyType.Silence` - Block both
- `PenaltyType.Warn` - Issue warning
**[Full Documentation →](penalties)**
---
### Event System
```csharp
// Plugin ready event
_api.OnSimpleAdminReady += OnReady;
// Player penaltied
_api.OnPlayerPenaltied += OnPlayerPenaltied;
// Offline penalty added
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
// Admin activity
_api.OnAdminShowActivity += OnAdminActivity;
// Admin silent toggle
_api.OnAdminToggleSilent += OnAdminToggleSilent;
```
**[Full Documentation →](events)**
---
### Utility Functions
```csharp
// Get player info with penalties
var info = _api.GetPlayerInfo(player);
// Get database connection string
var connectionString = _api.GetConnectionString();
// Get server address
var serverAddress = _api.GetServerAddress();
// Get server ID
var serverId = _api.GetServerId();
// Get valid players
var players = _api.GetValidPlayers();
// Check if admin is silent
bool isSilent = _api.IsAdminSilent(player);
// Get all silent admins
var silentAdmins = _api.ListSilentAdminsSlots();
// Show admin activity
_api.ShowAdminActivity(messageKey, callerName, dontPublish, args);
// Show admin activity with custom translation
_api.ShowAdminActivityTranslated(translatedMessage, callerName, dontPublish);
// Show admin activity with module localizer (recommended)
_api.ShowAdminActivityLocalized(moduleLocalizer, messageKey, callerName, dontPublish, args);
```
**[Full Documentation →](utilities)**
---
## Common Patterns
### Basic Module Structure
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
namespace MyModule;
public class MyModule : BasePlugin
{
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
new("simpleadmin:api");
public override string ModuleName => "My Module";
public override string ModuleVersion => "1.0.0";
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
// Register features
RegisterCommands();
// Wait for SimpleAdmin ready
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Fallback for hot reload
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Cleanup
_api.UnRegisterCommand("css_mycommand");
_api.UnregisterMenu("category", "menu");
_api.OnSimpleAdminReady -= RegisterMenus;
}
}
```
---
### Command with Target Selection
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Parse targets
var targets = _api!.GetTarget(command);
if (targets == null) return;
// Filter valid players
var players = targets.Players
.Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
.ToList();
// Process each player
foreach (var player in players)
{
DoSomethingToPlayer(caller, player);
}
// Log command
_api.LogCommand(caller, command);
}
```
---
### Menu with Player Selection
```csharp
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
{
// Context contains categoryId, menuId, menuName, permission, commandName
return _api!.CreateMenuWithPlayers(
context, // Automatic title and category
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) =>
{
// Action when player selected
PerformAction(admin, target);
}
);
}
```
---
### Nested Menu
```csharp
private object CreatePlayerMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateActionMenu(admin, player);
});
}
return menu;
}
private object CreateActionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Actions for {target.PlayerName}", "category", admin);
_api.AddMenuOption(menu, "Action 1", _ => DoAction1(admin, target));
_api.AddMenuOption(menu, "Action 2", _ => DoAction2(admin, target));
return menu;
}
```
---
### Issue Penalty
```csharp
private void BanPlayer(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
{
// Issue ban
_api!.IssuePenalty(
target,
admin,
PenaltyType.Ban,
reason,
duration // minutes, 0 = permanent
);
// Show activity
if (admin == null || !_api.IsAdminSilent(admin))
{
_api.ShowAdminActivityLocalized(
Localizer,
"ban_message",
admin?.PlayerName,
false,
target.PlayerName,
duration
);
}
}
```
---
### Event Subscription
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
// Subscribe to events
_api.OnPlayerPenaltied += OnPlayerPenaltied;
}
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
Logger.LogInformation($"{player.PlayerName} received {type}: {reason} ({duration} min)");
// React to penalty
if (type == PenaltyType.Ban)
{
// Handle ban
}
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
}
```
---
## Best Practices
### 1. Always Check for Null
```csharp
if (_api == null)
{
Logger.LogError("API not available!");
return;
}
```
### 2. Use OnSimpleAdminReady Event
```csharp
_api.OnSimpleAdminReady += () =>
{
// Register menus only when SimpleAdmin is ready
RegisterMenus();
};
// Also call directly for hot reload case
RegisterMenus();
```
### 3. Clean Up on Unload
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister all commands
_api.UnRegisterCommand("css_mycommand");
// Unregister all menus
_api.UnregisterMenu("category", "menu");
// Unsubscribe all events
_api.OnSimpleAdminReady -= OnReady;
}
```
### 4. Validate Player State
```csharp
if (!player.IsValid || !player.PawnIsAlive)
{
return;
}
if (!caller.CanTarget(player))
{
return; // Immunity check
}
```
### 5. Use Per-Player Translations
```csharp
// Each player sees message in their configured language
_api.ShowAdminActivityLocalized(
Localizer, // Your module's localizer
"translation_key",
caller?.PlayerName,
false,
args
);
```
### 6. Log All Admin Actions
```csharp
_api.LogCommand(caller, command);
// or
_api.LogCommand(caller, $"css_mycommand {player.PlayerName}");
```
---
## API Versioning
The API uses semantic versioning:
- **Major** - Breaking changes
- **Minor** - New features, backwards compatible
- **Patch** - Bug fixes
**Current Version:** Check [GitHub Releases](https://github.com/daffyyyy/CS2-SimpleAdmin/releases)
---
## Thread Safety
The API is designed for single-threaded use within the CounterStrikeSharp game thread.
**Do NOT:**
- Call API methods from background threads
- Use async/await with API calls without proper synchronization
**Do:**
- Call API methods from event handlers
- Call API methods from commands
- Call API methods from timers
---
## Error Handling
The API uses exceptions for critical errors:
```csharp
try
{
_api.RegisterCommand("css_cmd", "Desc", callback);
}
catch (ArgumentException ex)
{
Logger.LogError($"Failed to register command: {ex.Message}");
}
```
**Common exceptions:**
- `ArgumentException` - Invalid arguments
- `InvalidOperationException` - Invalid state
- `KeyNotFoundException` - Player not found
---
## Performance Considerations
### Efficient Player Filtering
```csharp
// ✅ Good - single LINQ query
var players = _api.GetValidPlayers()
.Where(p => p.IsValid && admin.CanTarget(p))
.ToList();
// ❌ Bad - multiple iterations
var players = _api.GetValidPlayers();
players = players.Where(p => p.IsValid).ToList();
players = players.Where(p => admin.CanTarget(p)).ToList();
```
### Cache Expensive Operations
```csharp
// Cache menu creation if used multiple times
private object? _cachedMenu;
private object GetMenu(CCSPlayerController player)
{
if (_cachedMenu == null)
{
_cachedMenu = CreateMenu(player);
}
return _cachedMenu;
}
```
---
## Debugging
### Enable Detailed Logging
```csharp
Logger.LogInformation("Debug: API loaded");
Logger.LogWarning("Warning: Player not found");
Logger.LogError("Error: Failed to execute command");
```
### Check API Availability
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("❌ CS2-SimpleAdmin API not found!");
Logger.LogError("Make sure CS2-SimpleAdmin is installed and loaded.");
return;
}
Logger.LogInformation("✅ CS2-SimpleAdmin API loaded successfully");
}
```
---
## Next Steps
- **[Commands API](commands)** - Command registration and targeting
- **[Menus API](menus)** - Menu system details
- **[Penalties API](penalties)** - Penalty management
- **[Events API](events)** - Event subscription
- **[Utilities API](utilities)** - Helper functions
---
## Resources
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
- **[Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Reference implementation
- **[Module Development Guide](../module/getting-started)** - Create modules

View File

@@ -0,0 +1,610 @@
---
sidebar_position: 4
---
# Penalties API
Complete reference for issuing and managing player penalties.
## Penalty Types
```csharp
public enum PenaltyType
{
Ban, // Ban player from server
Kick, // Kick player from server
Gag, // Block text chat
Mute, // Block voice chat
Silence, // Block both text and voice
Warn // Issue warning
}
```
---
## Issue Penalties
### IssuePenalty (Online Player)
Issue a penalty to a currently connected player.
```csharp
void IssuePenalty(
CCSPlayerController player,
CCSPlayerController? admin,
PenaltyType penaltyType,
string reason,
int duration = -1
)
```
**Parameters:**
- `player` - Target player controller
- `admin` - Admin issuing penalty (null for console)
- `penaltyType` - Type of penalty
- `reason` - Reason for penalty
- `duration` - Duration in minutes (0 = permanent, -1 = default)
**Example:**
```csharp
// Ban player for 1 day
_api!.IssuePenalty(
player,
admin,
PenaltyType.Ban,
"Cheating",
1440 // 24 hours in minutes
);
// Permanent ban
_api.IssuePenalty(
player,
admin,
PenaltyType.Ban,
"Severe rule violation",
0
);
// Kick player
_api.IssuePenalty(
player,
admin,
PenaltyType.Kick,
"AFK"
);
// Gag for 30 minutes
_api.IssuePenalty(
player,
admin,
PenaltyType.Gag,
"Chat spam",
30
);
```
---
### IssuePenalty (Offline Player)
Issue a penalty to a player by SteamID (even if offline).
```csharp
void IssuePenalty(
SteamID steamid,
CCSPlayerController? admin,
PenaltyType penaltyType,
string reason,
int duration = -1
)
```
**Parameters:**
- `steamid` - Target player's SteamID
- `admin` - Admin issuing penalty (null for console)
- `penaltyType` - Type of penalty
- `reason` - Reason for penalty
- `duration` - Duration in minutes (0 = permanent, -1 = default)
**Example:**
```csharp
// Ban offline player
var steamId = new SteamID(76561198012345678);
_api!.IssuePenalty(
steamId,
admin,
PenaltyType.Ban,
"Ban evasion",
10080 // 7 days
);
// Mute offline player
_api.IssuePenalty(
steamId,
admin,
PenaltyType.Mute,
"Voice abuse",
1440
);
```
**Supported SteamID Formats:**
```csharp
// SteamID64
new SteamID(76561198012345678)
// Also works with SteamID string parsing
SteamID.FromString("STEAM_1:0:12345678")
SteamID.FromString("[U:1:12345678]")
```
---
## Get Player Information
### GetPlayerInfo
Get detailed player information including penalty counts.
```csharp
PlayerInfo GetPlayerInfo(CCSPlayerController player)
```
**Returns:** `PlayerInfo` object containing:
- `PlayerName` - Player's name
- `SteamId` - Steam ID (ulong)
- `IpAddress` - Player's IP address
- `Warnings` - Warning count
- `Bans` - Ban count
- `Mutes` - Mute count
- `Gags` - Gag count
**Example:**
```csharp
var playerInfo = _api!.GetPlayerInfo(player);
Console.WriteLine($"Player: {playerInfo.PlayerName}");
Console.WriteLine($"SteamID: {playerInfo.SteamId}");
Console.WriteLine($"Warnings: {playerInfo.Warnings}");
Console.WriteLine($"Total Bans: {playerInfo.Bans}");
// Check if player has penalties
if (playerInfo.Warnings >= 3)
{
_api.IssuePenalty(player, null, PenaltyType.Ban, "Too many warnings", 1440);
}
```
**Throws:**
- `KeyNotFoundException` - If player doesn't have a valid UserId
---
### GetPlayerMuteStatus
Get current mute/gag/silence status for a player.
```csharp
Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetPlayerMuteStatus(
CCSPlayerController player
)
```
**Returns:** Dictionary mapping penalty types to lists of active penalties
**Example:**
```csharp
var muteStatus = _api!.GetPlayerMuteStatus(player);
// Check if player is gagged
if (muteStatus.ContainsKey(PenaltyType.Gag))
{
var gagPenalties = muteStatus[PenaltyType.Gag];
foreach (var (endTime, duration, passed) in gagPenalties)
{
if (!passed)
{
var remaining = endTime - DateTime.UtcNow;
Console.WriteLine($"Gagged for {remaining.TotalMinutes:F0} more minutes");
}
}
}
// Check if player is muted
if (muteStatus.ContainsKey(PenaltyType.Mute))
{
Console.WriteLine("Player is currently muted");
}
// Check if player is silenced
if (muteStatus.ContainsKey(PenaltyType.Silence))
{
Console.WriteLine("Player is silenced (gag + mute)");
}
```
---
## Server Information
### GetConnectionString
Get the database connection string.
```csharp
string GetConnectionString()
```
**Example:**
```csharp
var connectionString = _api!.GetConnectionString();
// Use for custom database operations
```
---
### GetServerAddress
Get the server's IP address and port.
```csharp
string GetServerAddress()
```
**Example:**
```csharp
var serverAddress = _api!.GetServerAddress();
Console.WriteLine($"Server: {serverAddress}");
// Example output: "192.168.1.100:27015"
```
---
### GetServerId
Get the server's unique ID in the database.
```csharp
int? GetServerId()
```
**Returns:**
- `int` - Server ID if multi-server mode enabled
- `null` - If single-server mode
**Example:**
```csharp
var serverId = _api!.GetServerId();
if (serverId.HasValue)
{
Console.WriteLine($"Server ID: {serverId.Value}");
}
else
{
Console.WriteLine("Single-server mode");
}
```
---
## Complete Examples
### Ban with Validation
```csharp
private void BanPlayer(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
{
// Validate player
if (!target.IsValid)
{
admin?.PrintToChat("Invalid player!");
return;
}
// Check immunity
if (admin != null && !admin.CanTarget(target))
{
admin.PrintToChat($"You cannot ban {target.PlayerName} (higher immunity)!");
return;
}
// Get player info to check history
var playerInfo = _api!.GetPlayerInfo(target);
Logger.LogInformation(
$"{admin?.PlayerName ?? "Console"} banning {playerInfo.PlayerName} " +
$"(SteamID: {playerInfo.SteamId}, Previous bans: {playerInfo.Bans})"
);
// Issue ban
_api.IssuePenalty(target, admin, PenaltyType.Ban, reason, duration);
// Show activity
if (admin == null || !_api.IsAdminSilent(admin))
{
var durationText = duration == 0 ? "permanently" : $"for {duration} minutes";
Server.PrintToChatAll($"{admin?.PlayerName ?? "Console"} banned {target.PlayerName} {durationText}: {reason}");
}
}
```
---
### Progressive Punishment System
```csharp
private void HandlePlayerOffense(CCSPlayerController? admin, CCSPlayerController target, string reason)
{
var playerInfo = _api!.GetPlayerInfo(target);
// Progressive punishment based on warning count
if (playerInfo.Warnings == 0)
{
// First offense - warning
_api.IssuePenalty(target, admin, PenaltyType.Warn, reason);
target.PrintToChat("This is your first warning!");
}
else if (playerInfo.Warnings == 1)
{
// Second offense - gag for 30 minutes
_api.IssuePenalty(target, admin, PenaltyType.Gag, $"Second offense: {reason}", 30);
target.PrintToChat("Second warning! You are gagged for 30 minutes.");
}
else if (playerInfo.Warnings == 2)
{
// Third offense - 1 day ban
_api.IssuePenalty(target, admin, PenaltyType.Ban, $"Third offense: {reason}", 1440);
target.PrintToChat("Third offense! You are banned for 1 day.");
}
else
{
// More than 3 warnings - permanent ban
_api.IssuePenalty(target, admin, PenaltyType.Ban, $"Multiple offenses: {reason}", 0);
}
}
```
---
### Check Active Penalties Before Action
```csharp
private void AllowPlayerToChat(CCSPlayerController player)
{
var muteStatus = _api!.GetPlayerMuteStatus(player);
// Check if player is gagged
if (muteStatus.ContainsKey(PenaltyType.Gag))
{
player.PrintToChat("You are currently gagged and cannot use chat!");
return;
}
// Check if player is silenced (includes gag)
if (muteStatus.ContainsKey(PenaltyType.Silence))
{
player.PrintToChat("You are silenced and cannot communicate!");
return;
}
// Player can chat
ProcessChatMessage(player);
}
```
---
### Offline Player Ban
```csharp
private void BanOfflinePlayer(CCSPlayerController? admin, string steamIdString, int duration, string reason)
{
// Parse SteamID
if (!ulong.TryParse(steamIdString, out ulong steamId64))
{
admin?.PrintToChat("Invalid SteamID format!");
return;
}
var steamId = new SteamID(steamId64);
// Issue offline ban
_api!.IssuePenalty(steamId, admin, PenaltyType.Ban, reason, duration);
Logger.LogInformation(
$"{admin?.PlayerName ?? "Console"} banned offline player " +
$"(SteamID: {steamId64}) for {duration} minutes: {reason}"
);
admin?.PrintToChat($"Offline ban issued to SteamID {steamId64}");
}
```
---
### Multi-Account Detection
```csharp
[GameEventHandler]
public HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo info)
{
var player = @event.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
var playerInfo = _api!.GetPlayerInfo(player);
// Check if player has multiple accounts
if (playerInfo.Bans > 0)
{
// Notify admins
var admins = Utilities.GetPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, "@css/ban"));
foreach (var admin in admins)
{
admin.PrintToChat(
$"⚠ {player.PlayerName} has {playerInfo.Bans} previous ban(s)!"
);
}
}
return HookResult.Continue;
}
```
---
## Best Practices
### 1. Always Validate Players
```csharp
if (!target.IsValid || !target.PawnIsAlive)
{
return;
}
// Check immunity
if (admin != null && !admin.CanTarget(target))
{
admin.PrintToChat("Cannot target this player!");
return;
}
```
### 2. Provide Clear Reasons
```csharp
// ✅ Good - Specific reason
_api.IssuePenalty(player, admin, PenaltyType.Ban, "Aimbot detected in Round 12", 10080);
// ❌ Bad - Vague reason
_api.IssuePenalty(player, admin, PenaltyType.Ban, "cheating", 10080);
```
### 3. Log Penalty Actions
```csharp
_api.IssuePenalty(player, admin, PenaltyType.Ban, reason, duration);
Logger.LogInformation(
$"Penalty issued: {admin?.PlayerName ?? "Console"} -> {player.PlayerName} " +
$"| Type: {PenaltyType.Ban} | Duration: {duration}m | Reason: {reason}"
);
```
### 4. Handle Kick Separately
```csharp
// Kick doesn't need duration
_api.IssuePenalty(player, admin, PenaltyType.Kick, reason);
// NOT:
_api.IssuePenalty(player, admin, PenaltyType.Kick, reason, 0);
```
### 5. Check Active Penalties
```csharp
// Before issuing new penalty, check existing ones
var muteStatus = _api.GetPlayerMuteStatus(player);
if (muteStatus.ContainsKey(PenaltyType.Gag))
{
admin?.PrintToChat($"{player.PlayerName} is already gagged!");
return;
}
```
---
## Common Patterns
### Duration Helpers
```csharp
public static class PenaltyDurations
{
public const int OneHour = 60;
public const int OneDay = 1440;
public const int OneWeek = 10080;
public const int TwoWeeks = 20160;
public const int OneMonth = 43200;
public const int Permanent = 0;
}
// Usage
_api.IssuePenalty(player, admin, PenaltyType.Ban, reason, PenaltyDurations.OneWeek);
```
### Penalty History Display
```csharp
private void ShowPlayerHistory(CCSPlayerController admin, CCSPlayerController target)
{
var info = _api!.GetPlayerInfo(target);
admin.PrintToChat($"=== {info.PlayerName} History ===");
admin.PrintToChat($"Warnings: {info.Warnings}");
admin.PrintToChat($"Bans: {info.Bans}");
admin.PrintToChat($"Mutes: {info.Mutes}");
admin.PrintToChat($"Gags: {info.Gags}");
var muteStatus = _api.GetPlayerMuteStatus(target);
if (muteStatus.ContainsKey(PenaltyType.Gag))
admin.PrintToChat("Currently: GAGGED");
if (muteStatus.ContainsKey(PenaltyType.Mute))
admin.PrintToChat("Currently: MUTED");
if (muteStatus.ContainsKey(PenaltyType.Silence))
admin.PrintToChat("Currently: SILENCED");
}
```
---
## Error Handling
### Handle Invalid Players
```csharp
try
{
var playerInfo = _api!.GetPlayerInfo(player);
// Use playerInfo...
}
catch (KeyNotFoundException)
{
Logger.LogError($"Player info not found for {player?.PlayerName}");
return;
}
```
### Validate SteamID
```csharp
private bool TryParseSteamId(string input, out SteamID steamId)
{
steamId = default;
if (ulong.TryParse(input, out ulong steamId64))
{
steamId = new SteamID(steamId64);
return true;
}
return false;
}
```
---
## Related APIs
- **[Commands API](commands)** - Issue penalties from commands
- **[Menus API](menus)** - Issue penalties from menus
- **[Events API](events)** - React to penalty events
- **[Utilities API](utilities)** - Helper functions

View File

@@ -0,0 +1,585 @@
---
sidebar_position: 6
---
# Utilities API
Helper functions and utility methods for module development.
## Player Management
### GetValidPlayers
Get a list of all valid, connected players.
```csharp
List<CCSPlayerController> GetValidPlayers()
```
**Returns:** List of valid player controllers
**Example:**
```csharp
var players = _api!.GetValidPlayers();
foreach (var player in players)
{
Console.WriteLine($"Player: {player.PlayerName}");
}
// Filter for specific criteria
var alivePlayers = _api.GetValidPlayers()
.Where(p => p.PawnIsAlive)
.ToList();
var ctPlayers = _api.GetValidPlayers()
.Where(p => p.Team == CsTeam.CounterTerrorist)
.ToList();
```
**Note:** This method filters out invalid and bot players automatically.
---
## Admin Status
### IsAdminSilent
Check if an admin is in silent mode.
```csharp
bool IsAdminSilent(CCSPlayerController player)
```
**Parameters:**
- `player` - Player to check
**Returns:** `true` if player is in silent mode, `false` otherwise
**Example:**
```csharp
private void PerformAdminAction(CCSPlayerController admin, CCSPlayerController target)
{
// Do the action
DoAction(target);
// Only show activity if not silent
if (!_api!.IsAdminSilent(admin))
{
Server.PrintToChatAll($"{admin.PlayerName} performed action on {target.PlayerName}");
}
}
```
---
### ListSilentAdminsSlots
Get a list of player slots for all admins currently in silent mode.
```csharp
HashSet<int> ListSilentAdminsSlots()
```
**Returns:** HashSet of player slots
**Example:**
```csharp
var silentAdmins = _api!.ListSilentAdminsSlots();
Console.WriteLine($"Silent admins: {silentAdmins.Count}");
foreach (var slot in silentAdmins)
{
var player = Utilities.GetPlayerFromSlot(slot);
if (player != null)
{
Console.WriteLine($"- {player.PlayerName} (slot {slot})");
}
}
```
---
## Activity Messages
### ShowAdminActivity
Show an admin activity message to all players.
```csharp
void ShowAdminActivity(
string messageKey,
string? callerName = null,
bool dontPublish = false,
params object[] messageArgs
)
```
**Parameters:**
- `messageKey` - Translation key from SimpleAdmin's lang files
- `callerName` - Admin name (null for console)
- `dontPublish` - If true, don't trigger OnAdminShowActivity event
- `messageArgs` - Arguments for message formatting
**Example:**
```csharp
// Using SimpleAdmin's built-in translations
_api!.ShowAdminActivity(
"sa_admin_player_kick_message", // Translation key
admin?.PlayerName,
false,
player.PlayerName,
reason
);
```
**Limitations:**
- Only works with SimpleAdmin's own translation keys
- For module-specific messages, use `ShowAdminActivityLocalized`
---
### ShowAdminActivityTranslated
Show a pre-translated admin activity message.
```csharp
void ShowAdminActivityTranslated(
string translatedMessage,
string? callerName = null,
bool dontPublish = false
)
```
**Parameters:**
- `translatedMessage` - Already translated message
- `callerName` - Admin name
- `dontPublish` - If true, don't trigger event
**Example:**
```csharp
// Use when you've already translated the message
var message = Localizer?["my_action_message", player.PlayerName] ?? $"Action on {player.PlayerName}";
_api!.ShowAdminActivityTranslated(
message,
admin?.PlayerName,
false
);
```
**Use Case:**
- When you need custom message formatting
- When translation is already done
---
### ShowAdminActivityLocalized ⭐ RECOMMENDED
Show admin activity with per-player language support using module's localizer.
```csharp
void ShowAdminActivityLocalized(
object moduleLocalizer,
string messageKey,
string? callerName = null,
bool dontPublish = false,
params object[] messageArgs
)
```
**Parameters:**
- `moduleLocalizer` - Your module's `IStringLocalizer` instance
- `messageKey` - Translation key from your module's lang files
- `callerName` - Admin name
- `dontPublish` - If true, don't trigger event
- `messageArgs` - Message arguments
**Example:**
```csharp
// Each player sees message in their configured language!
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer, // Your module's localizer
"fun_admin_god_message", // From your lang/en.json
admin?.PlayerName,
false,
player.PlayerName
);
}
```
**lang/en.json:**
```json
{
"fun_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!"
}
```
**Why This is Best:**
- ✅ Each player sees message in their own language
- ✅ Uses your module's translations
- ✅ Supports color codes
- ✅ Per-player localization
---
## Complete Examples
### Action with Activity Message
```csharp
private void ToggleGodMode(CCSPlayerController? admin, CCSPlayerController target)
{
// Perform action
if (GodPlayers.Contains(target.Slot))
{
GodPlayers.Remove(target.Slot);
}
else
{
GodPlayers.Add(target.Slot);
}
// Show activity (respecting silent mode)
if (admin == null || !_api!.IsAdminSilent(admin))
{
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer,
"fun_admin_god_message",
admin?.PlayerName,
false,
target.PlayerName
);
}
}
// Log action
_api!.LogCommand(admin, $"css_god {target.PlayerName}");
}
```
---
### Broadcast to Non-Silent Admins
```csharp
private void NotifyAdmins(string message)
{
var silentAdmins = _api!.ListSilentAdminsSlots();
var players = _api.GetValidPlayers();
foreach (var player in players)
{
// Check if player is admin
if (!AdminManager.PlayerHasPermissions(player, "@css/generic"))
continue;
// Skip if admin is in silent mode
if (silentAdmins.Contains(player.Slot))
continue;
player.PrintToChat(message);
}
}
```
---
### Filter Players by Criteria
```csharp
private List<CCSPlayerController> GetTargetablePlayers(CCSPlayerController admin)
{
return _api!.GetValidPlayers()
.Where(p =>
p.IsValid &&
!p.IsBot &&
p.PawnIsAlive &&
admin.CanTarget(p))
.ToList();
}
private List<CCSPlayerController> GetAliveEnemies(CCSPlayerController player)
{
return _api!.GetValidPlayers()
.Where(p =>
p.Team != player.Team &&
p.PawnIsAlive)
.ToList();
}
private List<CCSPlayerController> GetAdmins()
{
return _api!.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, "@css/generic"))
.ToList();
}
```
---
## Best Practices
### 1. Use ShowAdminActivityLocalized
```csharp
// ✅ Good - Per-player language
_api.ShowAdminActivityLocalized(
Localizer,
"my_message_key",
admin?.PlayerName,
false,
args
);
// ❌ Bad - Single language for all
Server.PrintToChatAll($"{admin?.PlayerName} did something");
```
### 2. Respect Silent Mode
```csharp
// ✅ Good - Check silent mode
if (admin == null || !_api.IsAdminSilent(admin))
{
ShowActivity();
}
// ❌ Bad - Always show activity
ShowActivity(); // Ignores silent mode!
```
### 3. Validate Players from GetValidPlayers
```csharp
var players = _api.GetValidPlayers();
foreach (var player in players)
{
// Still good to check, especially for async operations
if (!player.IsValid) continue;
DoSomething(player);
}
```
### 4. Cache Silent Admin List if Checking Multiple Times
```csharp
// ✅ Good - Cache for multiple checks
var silentAdmins = _api.ListSilentAdminsSlots();
foreach (var admin in admins)
{
if (silentAdmins.Contains(admin.Slot)) continue;
NotifyAdmin(admin);
}
// ❌ Bad - Query for each admin
foreach (var admin in admins)
{
if (_api.IsAdminSilent(admin)) continue; // ← Repeated calls
NotifyAdmin(admin);
}
```
---
## Common Patterns
### Silent Mode Wrapper
```csharp
private void ShowActivityIfNotSilent(
CCSPlayerController? admin,
string messageKey,
params object[] args)
{
if (admin != null && _api!.IsAdminSilent(admin))
return;
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer,
messageKey,
admin?.PlayerName,
false,
args
);
}
}
// Usage
ShowActivityIfNotSilent(admin, "my_action", player.PlayerName);
```
---
### Get Online Admins
```csharp
private List<CCSPlayerController> GetOnlineAdmins(string permission = "@css/generic")
{
return _api!.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, permission))
.ToList();
}
// Usage
var admins = GetOnlineAdmins("@css/root");
foreach (var admin in admins)
{
admin.PrintToChat("Important admin message");
}
```
---
### Notify All Players Except Silent Admins
```csharp
private void BroadcastMessage(string message, bool excludeSilentAdmins = true)
{
var silentAdmins = excludeSilentAdmins
? _api!.ListSilentAdminsSlots()
: new HashSet<int>();
foreach (var player in _api.GetValidPlayers())
{
if (silentAdmins.Contains(player.Slot))
continue;
player.PrintToChat(message);
}
}
```
---
## Activity Message Formatting
### Color Codes in Messages
All activity messages support color codes:
```json
{
"my_message": "{lightred}Admin{default} banned {lightred}{0}{default} for {yellow}{1}{default}"
}
```
**Available Colors:**
- `{default}` - Default color
- `{white}` - White
- `{darkred}` - Dark red
- `{green}` - Green
- `{lightyellow}` - Light yellow
- `{lightblue}` - Light blue
- `{olive}` - Olive
- `{lime}` - Lime
- `{red}` - Red
- `{purple}` - Purple
- `{grey}` - Grey
- `{yellow}` - Yellow
- `{gold}` - Gold
- `{silver}` - Silver
- `{blue}` - Blue
- `{darkblue}` - Dark blue
- `{bluegrey}` - Blue grey
- `{magenta}` - Magenta
- `{lightred}` - Light red
- `{orange}` - Orange
---
### Message Arguments
```csharp
// lang/en.json
{
"ban_message": "{lightred}{0}{default} banned {lightred}{1}{default} for {yellow}{2}{default} minutes: {red}{3}"
}
// Code
_api.ShowAdminActivityLocalized(
Localizer,
"ban_message",
admin?.PlayerName,
false,
admin?.PlayerName, // {0}
target.PlayerName, // {1}
duration, // {2}
reason // {3}
);
```
---
## Performance Tips
### Minimize GetValidPlayers Calls
```csharp
// ✅ Good - Call once, filter multiple times
var allPlayers = _api.GetValidPlayers();
var alivePlayers = allPlayers.Where(p => p.PawnIsAlive).ToList();
var deadPlayers = allPlayers.Where(p => !p.PawnIsAlive).ToList();
// ❌ Bad - Multiple calls
var alivePlayers = _api.GetValidPlayers().Where(p => p.PawnIsAlive).ToList();
var deadPlayers = _api.GetValidPlayers().Where(p => !p.PawnIsAlive).ToList();
```
---
### Efficient Filtering
```csharp
// ✅ Good - Single LINQ query
var targets = _api.GetValidPlayers()
.Where(p => p.Team == CsTeam.Terrorist &&
p.PawnIsAlive &&
admin.CanTarget(p))
.ToList();
// ❌ Bad - Multiple iterations
var players = _api.GetValidPlayers();
players = players.Where(p => p.Team == CsTeam.Terrorist).ToList();
players = players.Where(p => p.PawnIsAlive).ToList();
players = players.Where(p => admin.CanTarget(p)).ToList();
```
---
## Troubleshooting
### Activity Messages Not Showing
**Check:**
1. Is `Localizer` not null?
2. Does translation key exist in lang files?
3. Is message correctly formatted?
4. Check `dontPublish` parameter
### Silent Mode Not Working
**Check:**
1. Is player actually in silent mode? (`css_hide` command)
2. Are you checking before showing activity?
3. Check slot vs player controller mismatch
---
## Related APIs
- **[Commands API](commands)** - Log commands
- **[Menus API](menus)** - Get players for menus
- **[Events API](events)** - Admin activity events
- **[Penalties API](penalties)** - Get player info

View File

@@ -0,0 +1,695 @@
---
sidebar_position: 8
---
# Plugin Architecture
Deep dive into CS2-SimpleAdmin's architecture and design patterns.
## Overview
CS2-SimpleAdmin follows a **layered architecture** with clear separation of concerns and well-defined responsibilities for each component.
---
## Architecture Layers
```
┌─────────────────────────────────────────┐
│ CounterStrikeSharp Integration Layer │ ← CS2_SimpleAdmin.cs
├─────────────────────────────────────────┤
│ Manager Layer │ ← /Managers/
│ • PermissionManager │
│ • BanManager │
│ • MuteManager │
│ • WarnManager │
│ • CacheManager │
│ • PlayerManager │
│ • ServerManager │
│ • DiscordManager │
├─────────────────────────────────────────┤
│ Database Layer │ ← /Database/
│ • IDatabaseProvider (Interface) │
│ • MySqlDatabaseProvider │
│ • SqliteDatabaseProvider │
│ • Migration System │
├─────────────────────────────────────────┤
│ Menu System │ ← /Menus/
│ • MenuManager (Singleton) │
│ • MenuBuilder (Factory) │
│ • Specific Menu Classes │
├─────────────────────────────────────────┤
│ Command System │ ← /Commands/
│ • RegisterCommands │
│ • Command Handlers (basebans, etc.) │
├─────────────────────────────────────────┤
│ Public API │ ← /Api/
│ • ICS2_SimpleAdminApi (Interface) │
│ • CS2_SimpleAdminApi (Implementation) │
└─────────────────────────────────────────┘
```
---
## Core Components
### 1. CounterStrikeSharp Integration Layer
**File:** `CS2_SimpleAdmin.cs`
**Responsibilities:**
- Plugin lifecycle management (Load/Unload)
- Event registration (`player_connect`, `player_disconnect`, etc.)
- Command routing
- Low-level game operations using `MemoryFunctionVoid`
- Timer management
**Key Methods:**
```csharp
public override void Load(bool hotReload)
public override void Unload(bool hotReload)
private HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo info)
private HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
```
---
### 2. Manager Layer
Each manager encapsulates specific domain logic:
#### PermissionManager
**File:** `/Managers/PermissionManager.cs`
**Responsibilities:**
- Load admin flags and groups from database
- Maintain in-memory `AdminCache` with lazy-loading
- Time-based cache expiry
- Immunity level management
**Key Patterns:**
- Caching for performance
- Lazy loading of admin data
- Periodic refresh
#### BanManager
**File:** `/Managers/BanManager.cs`
**Responsibilities:**
- Issue bans (SteamID, IP, or hybrid)
- Remove bans (unban)
- Handle ban expiration cleanup
- Multi-server ban synchronization
**Key Operations:**
```csharp
Task BanPlayer(...)
Task AddBanBySteamId(...)
Task RemoveBan(...)
```
#### MuteManager
**File:** `/Managers/MuteManager.cs`
**Responsibilities:**
- Three mute types: GAG (text), MUTE (voice), SILENCE (both)
- Duration-based mutes
- Expiration tracking
#### WarnManager
**File:** `/Managers/WarnManager.cs`
**Responsibilities:**
- Progressive warning system
- Auto-escalation to bans based on `WarnThreshold` config
- Warning history tracking
#### CacheManager
**File:** `/Managers/CacheManager.cs`
**Purpose:** Performance optimization layer
**Features:**
- In-memory ban cache with O(1) lookups by SteamID and IP
- Player IP history tracking for multi-account detection
- Reduces database queries on player join
**Data Structures:**
```csharp
Dictionary<ulong, BanInfo> _banCacheBySteamId
Dictionary<string, List<BanInfo>> _banCacheByIp
Dictionary<ulong, List<string>> _playerIpHistory
```
#### PlayerManager
**File:** `/Managers/PlayerManager.cs`
**Responsibilities:**
- Load player data on connect
- Check bans against cache
- Update IP history
- Semaphore limiting (max 5 concurrent loads)
**Key Pattern:**
```csharp
private readonly SemaphoreSlim _semaphore = new(5, 5);
public async Task LoadPlayerData(CCSPlayerController player)
{
await _semaphore.WaitAsync();
try
{
// Load player data
}
finally
{
_semaphore.Release();
}
}
```
#### ServerManager
**File:** `/Managers/ServerManager.cs`
**Responsibilities:**
- Load/register server metadata (IP, port, hostname, RCON)
- Multi-server mode support
- Server ID management
#### DiscordManager
**File:** `/Managers/DiscordManager.cs`
**Responsibilities:**
- Send webhook notifications for admin actions
- Configurable webhooks per penalty type
- Embed formatting with placeholders
---
### 3. Database Layer
**Files:** `/Database/`
**Provider Pattern** for database abstraction:
```csharp
public interface IDatabaseProvider
{
Task ExecuteAsync(string query, object? parameters = null);
Task<T> QueryFirstOrDefaultAsync<T>(string query, object? parameters = null);
Task<List<T>> QueryAsync<T>(string query, object? parameters = null);
// Query generation methods
string GetBanQuery(bool multiServer);
string GetMuteQuery(bool multiServer);
// ... more query methods
}
```
**Implementations:**
- `MySqlDatabaseProvider` - MySQL-specific SQL syntax
- `SqliteDatabaseProvider` - SQLite-specific SQL syntax
**Benefits:**
- Single codebase supports both MySQL and SQLite
- Easy to add new database providers
- Query methods accept `multiServer` boolean for scoping
**Migration System:**
**File:** `Database/Migration.cs`
- File-based migrations in `/Database/Migrations/{mysql,sqlite}/`
- Numbered files: `001_CreateTables.sql`, `002_AddColumn.sql`
- Tracking table: `sa_migrations`
- Auto-applies on plugin load
- Safe for multi-server environments
---
### 4. Menu System
**Files:** `/Menus/`
**MenuManager (Singleton Pattern):**
```csharp
public class MenuManager
{
public static MenuManager Instance { get; private set; }
private readonly Dictionary<string, MenuCategory> _categories = new();
private readonly Dictionary<string, Dictionary<string, MenuInfo>> _menus = new();
public void RegisterCategory(string id, string name, string permission);
public void RegisterMenu(string categoryId, string menuId, string name, ...);
public MenuBuilder CreateCategoryMenuPublic(MenuCategory category, CCSPlayerController player);
}
```
**MenuBuilder (Factory Pattern):**
```csharp
public class MenuBuilder
{
public MenuBuilder(string title);
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, ...);
public MenuBuilder AddSubMenu(string name, Func<CCSPlayerController, MenuBuilder> factory, ...);
public MenuBuilder WithBackAction(Action<CCSPlayerController> backAction);
public void OpenMenu(CCSPlayerController player);
}
```
**Specific Menu Classes:**
- `AdminMenu` - Main admin menu with categories
- `ManagePlayersMenu` - Player management menus
- `ManageServerMenu` - Server settings
- `DurationMenu` - Duration selection
- `ReasonMenu` - Reason selection
**Benefits:**
- Centralized menu management
- Permission-aware rendering
- Automatic back button handling
- Reusable menu components
---
### 5. Command System
**Files:** `/Commands/`
**Central Registration:**
**File:** `RegisterCommands.cs`
```csharp
public static class RegisterCommands
{
public static Dictionary<string, List<CommandDefinition>> _commandDefinitions = new();
public static void RegisterCommands(CS2_SimpleAdmin plugin)
{
// Load Commands.json
// Map commands to handler methods
// Register with CounterStrikeSharp
}
}
```
**Command Handlers:**
Organized by category:
- `basebans.cs` - Ban, unban, warn commands
- `basecomms.cs` - Gag, mute, silence commands
- `basecommands.cs` - Admin management, server commands
- `basechat.cs` - Chat commands (asay, csay, etc.)
- `playercommands.cs` - Player manipulation (slay, hp, etc.)
- `funcommands.cs` - Fun commands (god, noclip, etc.)
- `basevotes.cs` - Voting system
**Two-Tier Pattern:**
```csharp
// Entry command - parses arguments
[CommandHelper(2, "<#userid> <duration> [reason]")]
[RequiresPermissions("@css/ban")]
public void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = GetTarget(command);
int duration = ParseDuration(command.GetArg(2));
string reason = ParseReason(command);
foreach (var target in targets)
{
Ban(caller, target, duration, reason); // Core method
}
}
// Core method - database writes, events
private void Ban(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
{
// Write to database
BanManager.BanPlayer(target, admin, duration, reason);
// Update cache
CacheManager.AddBan(target);
// Trigger events
ApiInstance.OnPlayerPenaltiedEvent(target, admin, PenaltyType.Ban, reason, duration);
// Kick player
Server.ExecuteCommand($"kick {target.UserId}");
// Send Discord notification
DiscordManager.SendBanNotification(target, admin, duration, reason);
// Broadcast action
ShowAdminActivity("ban_message", admin?.PlayerName, target.PlayerName, duration, reason);
}
```
**Benefits:**
- Separation of parsing and execution
- Reusable core methods
- Consistent event triggering
- Easy to test
---
### 6. Public API
**Files:** `/Api/`
**Interface:** `ICS2_SimpleAdminApi.cs` (in CS2-SimpleAdminApi project)
**Implementation:** `CS2_SimpleAdminApi.cs`
**Capability System:**
```csharp
// In API interface
public static readonly PluginCapability<ICS2_SimpleAdminApi> PluginCapability = new("simpleadmin:api");
// In module
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
}
```
**Event Publishing:**
```csharp
// API exposes events
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, ...>? OnPlayerPenaltied;
// Core plugin triggers events
ApiInstance.OnPlayerPenaltiedEvent(player, admin, type, reason, duration, id);
// Modules subscribe
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// React to penalty
};
```
---
## Data Flow Patterns
### Player Join Flow
```
1. player_connect event
2. PlayerManager.LoadPlayerData()
3. Semaphore.WaitAsync() ← Max 5 concurrent
4. CacheManager.CheckBan(steamId, ip)
5a. BANNED → Kick player immediately
5b. CLEAN → Continue
6. Load active penalties from DB
7. Store in PlayersInfo dictionary
8. Update player IP history
```
### Ban Command Flow
```
1. OnBanCommand() ← Parse arguments
2. Ban() ← Core method
3. BanManager.BanPlayer() ← Write to DB
4. CacheManager.AddBan() ← Update cache
5. ApiInstance.OnPlayerPenaltiedEvent() ← Trigger event
6. Server.ExecuteCommand("kick") ← Kick player
7. DiscordManager.SendNotification() ← Discord webhook
8. ShowAdminActivity() ← Broadcast action
```
### Admin Permission Check Flow
```
1. Plugin Load
2. PermissionManager.LoadAdmins()
3. Build AdminCache ← SteamID → Flags/Immunity
4. Command Execution
5. RequiresPermissions attribute check
6. AdminManager.PlayerHasPermissions() ← Check cache
7a. HAS PERMISSION → Execute
7b. NO PERMISSION → Deny
```
---
## Design Patterns Used
### Singleton Pattern
```csharp
public class MenuManager
{
public static MenuManager Instance { get; private set; }
public static void Initialize(CS2_SimpleAdmin plugin)
{
Instance = new MenuManager(plugin);
}
}
```
**Used for:**
- MenuManager - Single menu registry
- Cache management - Single source of truth
### Factory Pattern
```csharp
public class MenuBuilder
{
public static MenuBuilder Create(string title) => new MenuBuilder(title);
public MenuBuilder AddOption(...) { /* ... */ return this; }
public MenuBuilder AddSubMenu(...) { /* ... */ return this; }
}
```
**Used for:**
- Menu creation
- Database provider creation
### Strategy Pattern
```csharp
public interface IDatabaseProvider
{
Task<List<BanInfo>> GetBans(bool multiServer);
}
public class MySqlDatabaseProvider : IDatabaseProvider { /* ... */ }
public class SqliteDatabaseProvider : IDatabaseProvider { /* ... */ }
```
**Used for:**
- Database abstraction
- Query generation per DB type
### Observer Pattern
```csharp
// Publisher
public event Action<PlayerInfo, ...>? OnPlayerPenaltied;
// Trigger
OnPlayerPenaltied?.Invoke(player, admin, type, reason, duration, id, sid);
// Subscribers
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// React
};
```
**Used for:**
- Event system
- Module communication
---
## Concurrency & Thread Safety
### Async/Await Patterns
All database operations use `async`/`await`:
```csharp
public async Task BanPlayer(...)
{
await _database.ExecuteAsync(query, parameters);
}
```
### Semaphore for Rate Limiting
```csharp
private readonly SemaphoreSlim _semaphore = new(5, 5);
public async Task LoadPlayerData(CCSPlayerController player)
{
await _semaphore.WaitAsync();
try
{
// Load data
}
finally
{
_semaphore.Release();
}
}
```
### Thread-Safe Collections
```csharp
private readonly ConcurrentDictionary<ulong, PlayerInfo> PlayersInfo = new();
```
---
## Memory Management
### In-Memory Caches
**AdminCache:**
```csharp
Dictionary<ulong, (List<string> Flags, int Immunity, DateTime Expiry)> AdminCache
```
**BanCache:**
```csharp
Dictionary<ulong, BanInfo> _banCacheBySteamId
Dictionary<string, List<BanInfo>> _banCacheByIp
```
**Benefits:**
- Reduces database load
- O(1) lookups
- TTL-based expiry
### Cleanup
```csharp
// On player disconnect
PlayersInfo.TryRemove(player.SteamID, out _);
// Periodic cache cleanup
AddTimer(3600f, CleanupExpiredCache, TimerFlags.REPEAT);
```
---
## Configuration System
### Multi-Level Configuration
1. **Main Config:** `CS2-SimpleAdmin.json`
2. **Commands Config:** `Commands.json`
3. **Module Configs:** Per-module JSON files
### Hot Reload Support
```csharp
public void OnConfigParsed(Config config)
{
Config = config;
// Reconfigure without restart
}
```
---
## Performance Optimizations
1. **Caching** - Minimize database queries
2. **Lazy Loading** - Load admin data on-demand
3. **Semaphore** - Limit concurrent operations
4. **Connection Pooling** - Reuse DB connections
5. **Indexed Queries** - Fast database lookups
6. **Memory Cleanup** - Remove disconnected player data
---
## Future Extensibility
### Plugin Capabilities
New modules can extend functionality:
```csharp
// New capability
var customCapability = new PluginCapability<ICustomFeature>("custom:feature");
Capabilities.RegisterPluginCapability(customCapability, () => _customFeature);
// Other plugins can use it
var feature = _customCapability.Get();
```
### Event-Driven Architecture
New events can be added without breaking changes:
```csharp
public event Action<NewEventArgs>? OnNewEvent;
```
---
## Testing Considerations
### Unit Testing
- Managers can be tested independently
- Mock `IDatabaseProvider` for testing
- Test command handlers with mock players
### Integration Testing
- Test on actual CS2 server
- Multi-server scenarios
- Database migration testing
---
## Related Documentation
- **[API Overview](api/overview)** - Public API details
- **[Module Development](module/getting-started)** - Create modules
- **[GitHub Source](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Browse code

View File

@@ -0,0 +1,379 @@
---
sidebar_position: 1
---
# Developer Introduction
Welcome to the CS2-SimpleAdmin developer documentation!
## Overview
This section contains technical documentation for developers who want to:
- Create modules using the CS2-SimpleAdmin API
- Contribute to the core plugin
- Integrate with CS2-SimpleAdmin from other plugins
- Understand the plugin architecture
---
## API Documentation
The CS2-SimpleAdmin API provides a rich set of features for module developers:
### Core Features
- **[Commands](api/commands)** - Register and manage commands
- **[Menus](api/menus)** - Create admin menus with player selection
- **[Penalties](api/penalties)** - Issue bans, mutes, gags, warnings
- **[Events](api/events)** - Subscribe to plugin events
- **[Utilities](api/utilities)** - Helper functions and player management
---
## Quick Links
### For Module Developers
- **[Module Development Guide](module/getting-started)** - Start creating modules
- **[Best Practices](module/best-practices)** - Write better code
- **[Examples](module/examples)** - Code examples and patterns
### For Core Contributors
- **[Architecture](architecture)** - Plugin structure and design
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
---
## Getting Started
### Prerequisites
- C# knowledge (intermediate level)
- .NET 8.0 SDK
- CounterStrikeSharp understanding
- CS2 dedicated server for testing
### Development Environment
**Recommended:**
- Visual Studio 2022 (Community or higher)
- VS Code with C# extension
- Git for version control
---
## CS2-SimpleAdminApi Interface
The main API interface provides all functionality:
```csharp
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
// Get the API
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;
}
// Use the API
_api.RegisterCommand("css_mycommand", "Description", OnMyCommand);
}
```
---
## API Categories
### Command Management
Register custom commands that integrate with CS2-SimpleAdmin:
```csharp
_api.RegisterCommand("css_mycommand", "Description", callback);
_api.UnRegisterCommand("css_mycommand");
_api.GetTarget(command); // Parse player targets
```
**[Learn more →](api/commands)**
---
### Menu System
Create interactive menus with automatic back button handling:
```csharp
// Register category
_api.RegisterMenuCategory("mycategory", "My Category", "@css/generic");
// Register menu
_api.RegisterMenu("mycategory", "mymenu", "My Menu", CreateMenu, "@css/generic");
// Create menu with players
_api.CreateMenuWithPlayers(context, admin, filter, onSelect);
```
**[Learn more →](api/menus)**
---
### Penalty System
Issue and manage player penalties:
```csharp
// Ban player
_api.IssuePenalty(player, admin, PenaltyType.Ban, "Reason", 1440);
// Offline ban
_api.IssuePenalty(steamId, admin, PenaltyType.Ban, "Reason", 0);
// Check penalties
var status = _api.GetPlayerMuteStatus(player);
```
**[Learn more →](api/penalties)**
---
### Event System
React to plugin events:
```csharp
_api.OnSimpleAdminReady += () => { /* Plugin ready */ };
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, serverId) =>
{
// Player received penalty
};
```
**[Learn more →](api/events)**
---
### Utility Functions
Helper functions for common tasks:
```csharp
// Get player info
var playerInfo = _api.GetPlayerInfo(player);
// Get valid players
var players = _api.GetValidPlayers();
// Check admin status
if (_api.IsAdminSilent(admin)) { /* ... */ }
// Show admin activity
_api.ShowAdminActivity("message_key", callerName, false, args);
```
**[Learn more →](api/utilities)**
---
## Code Examples
### Simple Command
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var target in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
// Do something with target
}
_api.LogCommand(caller, command);
}
```
### Simple Menu
```csharp
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) => DoAction(admin, target)
);
}
```
---
## Best Practices
### Error Handling
```csharp
if (_api == null)
{
Logger.LogError("API not available!");
return;
}
if (!player.IsValid || !player.PawnIsAlive)
{
return;
}
```
### Resource Cleanup
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister commands
_api.UnRegisterCommand("css_mycommand");
// Unregister menus
_api.UnregisterMenu("mycategory", "mymenu");
// Unsubscribe events
_api.OnSimpleAdminReady -= OnReady;
}
```
### Translations
```csharp
// Use per-player language support
_api.ShowAdminActivityLocalized(
Localizer,
"translation_key",
caller?.PlayerName,
false,
args
);
```
---
## Reference Implementation
The **Fun Commands Module** serves as a complete reference implementation demonstrating all API features:
- Command registration from config
- Menu creation with context
- Per-player translations
- Proper cleanup
- Code organization
**[View Source Code](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)**
---
## Architecture Overview
CS2-SimpleAdmin follows a layered architecture:
**Layers:**
1. **CounterStrikeSharp Integration** - Game event handling
2. **Manager Layer** - Business logic (Bans, Mutes, Permissions)
3. **Database Layer** - MySQL/SQLite with migrations
4. **Menu System** - MenuManager with factory pattern
5. **Command System** - Dynamic registration
6. **Public API** - ICS2_SimpleAdminApi interface
**[Learn more →](architecture)**
---
## Contributing
### Ways to Contribute
1. **Report Bugs** - [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
2. **Suggest Features** - [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
3. **Submit Pull Requests** - Code contributions
4. **Create Modules** - Extend functionality
5. **Improve Documentation** - Help others learn
### Development Workflow
1. Fork the repository
2. Create feature branch
3. Make changes
4. Test thoroughly
5. Submit pull request
---
## Resources
### Documentation
- **[API Reference](api/overview)** - Complete API documentation
- **[Module Development](module/getting-started)** - Create modules
- **[Architecture](architecture)** - Plugin design
### External Resources
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS framework
- **[CS2 Docs](https://developer.valvesoftware.com/wiki/Counter-Strike_2)** - Game documentation
### Community
- **[GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
- **[Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)** - Bug reports
- **[Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)** - Questions and ideas
---
## Support
### Getting Help
1. **Check Documentation** - Most questions answered here
2. **Search Issues** - Someone may have had same problem
3. **Ask in Discussions** - Community help
4. **Create Issue** - For bugs or feature requests
### Reporting Bugs
Include:
- CS2-SimpleAdmin version
- CounterStrikeSharp version
- Error messages
- Steps to reproduce
- Expected vs actual behavior
---
## Next Steps
### For New Developers
1. **[Read API Overview](api/overview)** - Understand available features
2. **[Study Examples](module/examples)** - Learn from code
3. **[Create First Module](module/getting-started)** - Get hands-on
### For Advanced Developers
1. **[Read Architecture](architecture)** - Deep dive into structure
2. **[Review Source Code](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Understand implementation
3. **[Contribute](https://github.com/daffyyyy/CS2-SimpleAdmin/pulls)** - Help improve the plugin

View File

@@ -0,0 +1,540 @@
---
sidebar_position: 2
---
# Best Practices
Guidelines for writing high-quality CS2-SimpleAdmin modules.
## Code Organization
### Use Partial Classes
Split your code into logical files:
```
MyModule/
├── MyModule.cs # Main class, initialization
├── Commands.cs # Command handlers
├── Menus.cs # Menu creation
├── Actions.cs # Core logic
└── Config.cs # Configuration
```
```csharp
// MyModule.cs
public partial class MyModule : BasePlugin, IPluginConfig<Config>
{
// Initialization
}
// Commands.cs
public partial class MyModule
{
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Command logic
}
}
// Menus.cs
public partial class MyModule
{
private object CreateMyMenu(CCSPlayerController player, MenuContext context)
{
// Menu logic
}
}
```
**Benefits:**
- ✅ Easy to navigate
- ✅ Logical separation
- ✅ Better maintainability
---
## Configuration
### Use Command Lists
Allow users to customize aliases:
```csharp
public class Config : IBasePluginConfig
{
// ✅ Good - List allows multiple aliases
public List<string> MyCommands { get; set; } = ["css_mycommand"];
// ❌ Bad - Single string
public string MyCommand { get; set; } = "css_mycommand";
}
```
**Usage:**
```csharp
foreach (var cmd in Config.MyCommands)
{
_api!.RegisterCommand(cmd, "Description", OnMyCommand);
}
```
### Provide Sensible Defaults
```csharp
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
// Good defaults
public bool EnableFeature { get; set; } = true;
public int MaxValue { get; set; } = 100;
public List<string> Commands { get; set; } = ["css_default"];
}
```
---
## API Usage
### Always Check for Null
```csharp
// ✅ Good
if (_api == null)
{
Logger.LogError("API not available!");
return;
}
_api.RegisterCommand(...);
// ❌ Bad
_api!.RegisterCommand(...); // Can crash if null
```
### Use OnSimpleAdminReady Pattern
```csharp
// ✅ Good - Handles both normal load and hot reload
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Also call directly
// ❌ Bad - Only works on normal load
_api.OnSimpleAdminReady += RegisterMenus;
```
### Always Clean Up
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister ALL commands
foreach (var cmd in Config.MyCommands)
{
_api.UnRegisterCommand(cmd);
}
// Unregister ALL menus
_api.UnregisterMenu("category", "menu");
// Unsubscribe ALL events
_api.OnSimpleAdminReady -= RegisterMenus;
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
}
```
---
## Player Validation
### Validate Before Acting
```csharp
// ✅ Good - Multiple checks
if (!player.IsValid)
{
Logger.LogWarning("Player is invalid!");
return;
}
if (!player.PawnIsAlive)
{
caller?.PrintToChat("Target must be alive!");
return;
}
if (admin != null && !admin.CanTarget(player))
{
admin.PrintToChat("Cannot target this player!");
return;
}
// Safe to proceed
DoAction(player);
```
### Check State Changes
```csharp
// ✅ Good - Validate in callback
_api.AddMenuOption(menu, "Action", _ =>
{
// Validate again - player state may have changed
if (!target.IsValid || !target.PawnIsAlive)
return;
DoAction(target);
});
// ❌ Bad - No validation in callback
_api.AddMenuOption(menu, "Action", _ =>
{
DoAction(target); // Might crash!
});
```
---
## Translations
### Use MenuContext for Menus
```csharp
// ✅ Good - No duplication
_api.RegisterMenu("cat", "id", "Title", CreateMenu, "@css/generic");
private object CreateMenu(CCSPlayerController admin, MenuContext context)
{
return _api.CreateMenuWithPlayers(context, admin, filter, action);
}
// ❌ Bad - Duplicates title and category
_api.RegisterMenu("cat", "id", "Title", CreateMenuOld, "@css/generic");
private object CreateMenuOld(CCSPlayerController admin)
{
return _api.CreateMenuWithPlayers("Title", "cat", admin, filter, action);
}
```
### Use Per-Player Translations
```csharp
// ✅ Good - Each player sees their language
if (Localizer != null)
{
_api.ShowAdminActivityLocalized(
Localizer,
"translation_key",
admin?.PlayerName,
false,
args
);
}
// ❌ Bad - Single language for all
Server.PrintToChatAll($"{admin?.PlayerName} did something");
```
### Provide English Fallbacks
```csharp
// ✅ Good - Fallback if translation missing
_api.RegisterMenuCategory(
"mycat",
Localizer?["category_name"] ?? "Default Category Name",
"@css/generic"
);
// ❌ Bad - No fallback
_api.RegisterMenuCategory(
"mycat",
Localizer["category_name"], // Crashes if no translation!
"@css/generic"
);
```
---
## Performance
### Cache Expensive Operations
```csharp
// ✅ Good - Cache on first access
private static Dictionary<int, string>? _itemCache;
private static Dictionary<int, string> GetItemCache()
{
if (_itemCache != null) return _itemCache;
// Build cache once
_itemCache = new Dictionary<int, string>();
// ... populate
return _itemCache;
}
// ❌ Bad - Rebuild every time
private Dictionary<int, string> GetItems()
{
var items = new Dictionary<int, string>();
// ... expensive operation
return items;
}
```
### Efficient LINQ Queries
```csharp
// ✅ Good - Single query
var players = _api.GetValidPlayers()
.Where(p => p.IsValid && !p.IsBot && p.PawnIsAlive)
.ToList();
// ❌ Bad - Multiple iterations
var players = _api.GetValidPlayers();
players = players.Where(p => p.IsValid).ToList();
players = players.Where(p => !p.IsBot).ToList();
players = players.Where(p => p.PawnIsAlive).ToList();
```
---
## Error Handling
### Log Errors
```csharp
// ✅ Good - Detailed logging
try
{
DoAction();
}
catch (Exception ex)
{
Logger.LogError($"Failed to perform action: {ex.Message}");
Logger.LogError($"Stack trace: {ex.StackTrace}");
}
// ❌ Bad - Silent failure
try
{
DoAction();
}
catch
{
// Ignore
}
```
### Graceful Degradation
```csharp
// ✅ Good - Continue with reduced functionality
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("SimpleAdmin API not found - limited functionality!");
// Module still loads, just without SimpleAdmin integration
return;
}
// ❌ Bad - Crash the entire module
_api = _pluginCapability.Get() ?? throw new Exception("No API!");
```
---
## Security
### Validate Admin Permissions
```csharp
// ✅ Good - Check permissions
[RequiresPermissions("@css/ban")]
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
// Already validated by attribute
}
// ❌ Bad - No permission check
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
// Anyone can use this!
}
```
### Check Immunity
```csharp
// ✅ Good - Check immunity
if (admin != null && !admin.CanTarget(target))
{
admin.PrintToChat($"Cannot target {target.PlayerName}!");
return;
}
// ❌ Bad - Ignore immunity
DoAction(target); // Can target higher immunity!
```
### Sanitize Input
```csharp
// ✅ Good - Validate and sanitize
private void OnSetValueCommand(CCSPlayerController? caller, CommandInfo command)
{
if (!int.TryParse(command.GetArg(1), out int value))
{
caller?.PrintToChat("Invalid number!");
return;
}
if (value < 0 || value > 1000)
{
caller?.PrintToChat("Value must be between 0 and 1000!");
return;
}
SetValue(value);
}
// ❌ Bad - No validation
private void OnSetValueCommand(CCSPlayerController? caller, CommandInfo command)
{
var value = int.Parse(command.GetArg(1)); // Can crash!
SetValue(value); // No range check!
}
```
---
## Documentation
### Comment Complex Logic
```csharp
// ✅ Good - Explain why, not what
// We need to check immunity twice because player state can change
// between menu creation and action execution
if (!admin.CanTarget(player))
{
return;
}
// ❌ Bad - States the obvious
// Check if admin can target player
if (!admin.CanTarget(player))
{
return;
}
```
### XML Documentation
```csharp
/// <summary>
/// Toggles god mode for the specified player.
/// </summary>
/// <param name="admin">Admin performing the action (null for console)</param>
/// <param name="target">Player to toggle god mode for</param>
/// <returns>True if god mode is now enabled, false otherwise</returns>
public bool ToggleGodMode(CCSPlayerController? admin, CCSPlayerController target)
{
// Implementation
}
```
---
## Testing
### Test Edge Cases
```csharp
// Test with:
// - Invalid players
// - Disconnected players
// - Players who changed teams
// - Null admins (console)
// - Silent admins
// - Players with higher immunity
```
### Test Hot Reload
```bash
# Server console
css_plugins reload YourModule
```
Make sure everything works after reload!
---
## Common Mistakes
### ❌ Forgetting to Unsubscribe
```csharp
public override void Unload(bool hotReload)
{
// Missing unsubscribe = memory leak!
// _api.OnSimpleAdminReady -= RegisterMenus; ← FORGOT THIS
}
```
### ❌ Not Checking API Availability
```csharp
// Crashes if SimpleAdmin not loaded!
_api.RegisterCommand(...); // ← No null check
```
### ❌ Hardcoding Strings
```csharp
// Bad - not translatable
player.PrintToChat("You have been banned!");
// Good - uses translations
var message = Localizer?["ban_message"] ?? "You have been banned!";
player.PrintToChat(message);
```
### ❌ Blocking Game Thread
```csharp
// Bad - blocks game thread
Thread.Sleep(5000);
// Good - use CounterStrikeSharp timers
AddTimer(5.0f, () => DoAction());
```
---
## Reference Implementation
Study the **Fun Commands Module** for best practices:
**[View Source](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)**
Shows:
- ✅ Proper code organization
- ✅ Configuration best practices
- ✅ Menu creation with context
- ✅ Per-player translations
- ✅ Proper cleanup
- ✅ Error handling
---
## Next Steps
- **[Examples](examples)** - More code examples
- **[API Reference](../api/overview)** - Full API documentation
- **[GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Browse source code

View File

@@ -0,0 +1,552 @@
---
sidebar_position: 3
---
# Code Examples
Practical examples for common module development scenarios.
## Complete Mini Module
A fully working minimal module:
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CS2_SimpleAdminApi;
namespace HelloModule;
public class HelloModule : BasePlugin, IPluginConfig<Config>
{
public override string ModuleName => "Hello Module";
public override string ModuleVersion => "1.0.0";
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
public Config Config { get; set; } = new();
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found!");
return;
}
// Register command
foreach (var cmd in Config.HelloCommands)
{
_api.RegisterCommand(cmd, "Say hello to a player", OnHelloCommand);
}
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnHelloCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
player.PrintToChat($"Hello {player.PlayerName}!");
caller?.PrintToChat($"Said hello to {player.PlayerName}");
}
_api.LogCommand(caller, command);
}
public void OnConfigParsed(Config config) => Config = config;
public override void Unload(bool hotReload)
{
if (_api == null) return;
foreach (var cmd in Config.HelloCommands)
{
_api.UnRegisterCommand(cmd);
}
}
}
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
public List<string> HelloCommands { get; set; } = ["css_hello"];
}
```
---
## Command Examples
### Simple Target Command
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/slay")]
private void OnSlayCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
if (player.PawnIsAlive)
{
player.PlayerPawn?.Value?.CommitSuicide(false, true);
caller?.PrintToChat($"Slayed {player.PlayerName}");
}
}
_api.LogCommand(caller, command);
}
```
### Command with Value Parameter
```csharp
[CommandHelper(2, "<#userid or name> <value>")]
[RequiresPermissions("@css/slay")]
private void OnSetHpCommand(CCSPlayerController? caller, CommandInfo command)
{
// Parse HP value
if (!int.TryParse(command.GetArg(2), out int hp) || hp < 1 || hp > 999)
{
caller?.PrintToChat("Invalid HP! Use 1-999");
return;
}
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && p.PawnIsAlive && caller!.CanTarget(p)))
{
player.PlayerPawn?.Value?.SetHealth(hp);
caller?.PrintToChat($"Set {player.PlayerName} HP to {hp}");
}
_api.LogCommand(caller, $"css_sethp {hp}");
}
```
### Command with Penalty
```csharp
[CommandHelper(1, "<#userid or name> [duration] [reason]")]
[RequiresPermissions("@css/ban")]
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
// Parse duration (default: 60 minutes)
int duration = 60;
if (command.ArgCount > 2)
{
int.TryParse(command.GetArg(2), out duration);
}
// Get reason (default: "Banned")
string reason = command.ArgCount > 3
? string.Join(" ", command.ArgString.Split(' ').Skip(2))
: "Banned";
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
_api.IssuePenalty(player, caller, PenaltyType.Ban, reason, duration);
caller?.PrintToChat($"Banned {player.PlayerName} for {duration} minutes");
}
_api.LogCommand(caller, command);
}
```
---
## Menu Examples
### Simple Player Selection Menu
```csharp
private void RegisterMenus()
{
_api!.RegisterMenuCategory("actions", "Player Actions", "@css/generic");
_api.RegisterMenu(
"actions",
"kick",
"Kick Player",
CreateKickMenu,
"@css/kick"
);
}
private object CreateKickMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) =>
{
Server.ExecuteCommand($"css_kick #{target.UserId} Kicked via menu");
admin.PrintToChat($"Kicked {target.PlayerName}");
}
);
}
```
### Nested Menu (Player → Action)
```csharp
private object CreatePlayerActionsMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
foreach (var player in _api.GetValidPlayers().Where(p => admin.CanTarget(p)))
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateActionSelectMenu(admin, player);
});
}
return menu;
}
private object CreateActionSelectMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Actions: {target.PlayerName}", "actions", admin);
_api.AddMenuOption(menu, "Slay", _ =>
{
if (target.IsValid && target.PawnIsAlive)
{
target.PlayerPawn?.Value?.CommitSuicide(false, true);
admin.PrintToChat($"Slayed {target.PlayerName}");
}
});
_api.AddMenuOption(menu, "Kick", _ =>
{
if (target.IsValid)
{
Server.ExecuteCommand($"css_kick #{target.UserId}");
}
});
_api.AddMenuOption(menu, "Ban", _ =>
{
if (target.IsValid)
{
_api.IssuePenalty(target, admin, PenaltyType.Ban, "Banned via menu", 1440);
}
});
return menu;
}
```
### Menu with Value Selection
```csharp
private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => p.PawnIsAlive && admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateHpValueMenu(admin, player);
});
}
return menu;
}
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Set HP: {target.PlayerName}", "actions", admin);
var hpValues = new[] { 1, 10, 50, 100, 200, 500 };
foreach (var hp in hpValues)
{
_api.AddMenuOption(menu, $"{hp} HP", _ =>
{
if (target.IsValid && target.PawnIsAlive)
{
target.PlayerPawn?.Value?.SetHealth(hp);
admin.PrintToChat($"Set {target.PlayerName} HP to {hp}");
}
});
}
return menu;
}
```
---
## Event Examples
### React to Bans
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
_api.OnPlayerPenaltied += OnPlayerPenaltied;
}
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
if (type != PenaltyType.Ban) return;
var adminName = admin?.PlayerName ?? "Console";
Logger.LogInformation($"Ban: {adminName} -> {player.PlayerName} ({duration}m): {reason}");
// Log to file
File.AppendAllText("bans.log",
$"[{DateTime.Now}] {player.PlayerName} banned by {adminName} for {duration}m: {reason}\n");
}
```
### Warning Escalation
```csharp
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
if (type != PenaltyType.Warn) return;
Logger.LogInformation($"{player.PlayerName} has {player.Warnings} warnings");
// Auto-ban at 3 warnings
if (player.Warnings >= 3)
{
var controller = Utilities.GetPlayers()
.FirstOrDefault(p => p.SteamID == player.SteamId);
if (controller != null)
{
_api!.IssuePenalty(
controller,
null,
PenaltyType.Ban,
"Automatic: 3 warnings",
1440 // 1 day
);
}
}
}
```
---
## Translation Examples
### Module with Translations
**lang/en.json:**
```json
{
"category_name": "My Module",
"menu_name": "My Action",
"action_message": "{lightred}{0}{default} performed action on {lightred}{1}{default}!",
"error_invalid_player": "{red}Error:{default} Invalid player!",
"success": "{green}Success!{default} Action completed."
}
```
**Code:**
```csharp
private void PerformAction(CCSPlayerController? admin, CCSPlayerController target)
{
// Perform action
DoSomething(target);
// Show activity with translation
if (admin == null || !_api!.IsAdminSilent(admin))
{
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer,
"action_message",
admin?.PlayerName,
false,
admin?.PlayerName ?? "Console",
target.PlayerName
);
}
}
// Send success message
admin?.PrintToChat(Localizer?["success"] ?? "Success!");
}
```
---
## Utility Examples
### Get Players by Team
```csharp
private List<CCSPlayerController> GetTeamPlayers(CsTeam team)
{
return _api!.GetValidPlayers()
.Where(p => p.Team == team)
.ToList();
}
// Usage
var ctPlayers = GetTeamPlayers(CsTeam.CounterTerrorist);
var tPlayers = GetTeamPlayers(CsTeam.Terrorist);
```
### Get Alive Players
```csharp
private List<CCSPlayerController> GetAlivePlayers()
{
return _api!.GetValidPlayers()
.Where(p => p.PawnIsAlive)
.ToList();
}
```
### Notify Admins
```csharp
private void NotifyAdmins(string message, string permission = "@css/generic")
{
var admins = _api!.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, permission));
foreach (var admin in admins)
{
admin.PrintToChat(message);
}
}
// Usage
NotifyAdmins("⚠ Important admin message", "@css/root");
```
---
## Timer Examples
### Delayed Action
```csharp
private void DelayedAction(CCSPlayerController player, float delay)
{
AddTimer(delay, () =>
{
if (player.IsValid && player.PawnIsAlive)
{
DoAction(player);
}
});
}
```
### Repeating Timer
```csharp
private void StartRepeatingAction()
{
AddTimer(1.0f, () =>
{
foreach (var player in _api!.GetValidPlayers())
{
if (player.PawnIsAlive)
{
UpdatePlayer(player);
}
}
}, TimerFlags.REPEAT);
}
```
---
## Configuration Examples
### Multiple Feature Toggles
```csharp
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
[JsonPropertyName("EnableFeature1")]
public bool EnableFeature1 { get; set; } = true;
[JsonPropertyName("EnableFeature2")]
public bool EnableFeature2 { get; set; } = false;
[JsonPropertyName("Feature1Commands")]
public List<string> Feature1Commands { get; set; } = ["css_feature1"];
[JsonPropertyName("Feature2Commands")]
public List<string> Feature2Commands { get; set; } = ["css_feature2"];
[JsonPropertyName("MaxValue")]
public int MaxValue { get; set; } = 100;
}
// Usage
private void RegisterCommands()
{
if (Config.EnableFeature1)
{
foreach (var cmd in Config.Feature1Commands)
{
_api!.RegisterCommand(cmd, "Feature 1", OnFeature1Command);
}
}
if (Config.EnableFeature2)
{
foreach (var cmd in Config.Feature2Commands)
{
_api!.RegisterCommand(cmd, "Feature 2", OnFeature2Command);
}
}
}
```
---
## Next Steps
- **[Best Practices](best-practices)** - Write better code
- **[Getting Started](getting-started)** - Create your first module
- **[API Reference](../api/overview)** - Full API documentation
- **[Fun Commands Source](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Complete reference implementation

View File

@@ -0,0 +1,282 @@
---
sidebar_position: 1
---
# Getting Started with Module Development
Step-by-step guide to creating your first CS2-SimpleAdmin module.
## Prerequisites
Before you begin:
- C# knowledge (intermediate level)
- .NET 8.0 SDK installed
- Visual Studio 2022 or VS Code
- Basic understanding of CounterStrikeSharp
- CS2 dedicated server for testing
---
## Step 1: Create Project
### Using .NET CLI
```bash
dotnet new classlib -n MyModule -f net8.0
cd MyModule
```
### Using Visual Studio
1. File → New → Project
2. Select "Class Library (.NET 8.0)"
3. Name: `MyModule`
4. Click Create
---
## Step 2: Add References
Edit `MyModule.csproj`:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="CounterStrikeSharp.API">
<HintPath>path/to/CounterStrikeSharp.API.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="CS2-SimpleAdminApi">
<HintPath>path/to/CS2-SimpleAdminApi.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
```
---
## Step 3: Create Main Plugin Class
Create `MyModule.cs`:
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CS2_SimpleAdminApi;
namespace MyModule;
public class MyModule : BasePlugin, IPluginConfig<Config>
{
public override string ModuleName => "My Module";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "Your Name";
public override string ModuleDescription => "My awesome module";
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
new("simpleadmin:api");
public Config Config { get; set; } = new();
public override void OnAllPluginsLoaded(bool hotReload)
{
// Get SimpleAdmin API
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found!");
return;
}
Logger.LogInformation("MyModule loaded successfully!");
// Register features
RegisterCommands();
// Register menus when ready
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Also call for hot reload
}
private void RegisterCommands()
{
if (_api == null) return;
foreach (var cmd in Config.MyCommands)
{
_api.RegisterCommand(cmd, "My command description", OnMyCommand);
}
}
private void RegisterMenus()
{
if (_api == null) return;
_api.RegisterMenuCategory("mymodule", "My Module", "@css/generic");
_api.RegisterMenu("mymodule", "mymenu", "My Menu", CreateMyMenu, "@css/generic");
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
player.PrintToChat($"Hello from MyModule!");
}
_api.LogCommand(caller, command);
}
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) =>
{
target.PrintToChat("You were selected!");
}
);
}
public void OnConfigParsed(Config config)
{
Config = config;
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister commands
foreach (var cmd in Config.MyCommands)
{
_api.UnRegisterCommand(cmd);
}
// Unregister menus
_api.UnregisterMenu("mymodule", "mymenu");
// Unsubscribe events
_api.OnSimpleAdminReady -= RegisterMenus;
}
}
```
---
## Step 4: Create Configuration
Create `Config.cs`:
```csharp
using CounterStrikeSharp.API.Core;
using System.Text.Json.Serialization;
public class Config : IBasePluginConfig
{
[JsonPropertyName("Version")]
public int Version { get; set; } = 1;
[JsonPropertyName("MyCommands")]
public List<string> MyCommands { get; set; } = ["css_mycommand"];
[JsonPropertyName("EnableFeature")]
public bool EnableFeature { get; set; } = true;
}
```
---
## Step 5: Build and Deploy
### Build
```bash
dotnet build -c Release
```
### Deploy
Copy files to server:
```
game/csgo/addons/counterstrikesharp/plugins/MyModule/
└── MyModule.dll
```
### Restart Server
```bash
# Server console
css_plugins reload
```
---
## Step 6: Test
1. Join your server
2. Open admin menu: `css_admin`
3. Look for "My Module" category
4. Test command: `css_mycommand @me`
---
## Next Steps
- **[Best Practices](best-practices)** - Write better code
- **[Examples](examples)** - More code examples
- **[API Reference](../api/overview)** - Full API documentation
- **[Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Reference implementation
---
## Common Issues
### API Not Found
**Error:** `CS2-SimpleAdmin API not found!`
**Solution:**
- Ensure CS2-SimpleAdmin is installed
- Check that CS2-SimpleAdminApi.dll is in shared folder
- Verify CS2-SimpleAdmin loads before your module
### Commands Not Working
**Check:**
- Command registered in `RegisterCommands()`
- Permission is correct
- Player has required permission
### Menu Not Showing
**Check:**
- `OnSimpleAdminReady` event subscribed
- Menu registered in category
- Permission is correct
- SimpleAdmin loaded successfully
---
## Resources
- **[Module Development Guide](../../modules/development)** - Detailed guide
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS framework

View File

@@ -0,0 +1,47 @@
---
sidebar_position: 1
---
# Tutorial Intro
Let's discover **Docusaurus in less than 5 minutes**.
## Getting Started
Get started by **creating a new site**.
Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**.
### What you'll need
- [Node.js](https://nodejs.org/en/download/) version 20.0 or above:
- When installing Node.js, you are recommended to check all checkboxes related to dependencies.
## Generate a new site
Generate a new Docusaurus site using the **classic template**.
The classic template will automatically be added to your project after you run the command:
```bash
npm init docusaurus@latest my-website classic
```
You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.
The command also installs all necessary dependencies you need to run Docusaurus.
## Start your site
Run the development server:
```bash
cd my-website
npm run start
```
The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there.
The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/.
Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes.

View File

@@ -0,0 +1,799 @@
---
sidebar_position: 3
---
# Module Development
Learn how to create your own CS2-SimpleAdmin modules.
## Introduction
Creating modules for CS2-SimpleAdmin allows you to extend the plugin's functionality while keeping your code separate and maintainable.
:::tip Reference Implementation
The **[Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** serves as a complete reference implementation. Study its code to learn best practices!
:::
---
## Prerequisites
### Knowledge Required
- C# programming (intermediate level)
- .NET 8.0
- CounterStrikeSharp basics
- Understanding of CS2-SimpleAdmin structure
### Tools Needed
- Visual Studio 2022 or VS Code
- .NET 8.0 SDK
- CS2 server for testing
---
## Quick Start
### 1. Create Project
```bash
dotnet new classlib -n YourModuleName -f net8.0
cd YourModuleName
```
### 2. Add References
Edit your `.csproj` file:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- CounterStrikeSharp -->
<Reference Include="CounterStrikeSharp.API">
<HintPath>path/to/CounterStrikeSharp.API.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- CS2-SimpleAdmin API -->
<Reference Include="CS2-SimpleAdminApi">
<HintPath>path/to/CS2-SimpleAdminApi.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
```
### 3. Create Main Plugin Class
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
namespace YourModuleName;
public class YourModule : BasePlugin, IPluginConfig<Config>
{
public override string ModuleName => "Your Module Name";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "Your Name";
public override string ModuleDescription => "Description";
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
public Config Config { get; set; } = new();
public override void OnAllPluginsLoaded(bool hotReload)
{
// Get SimpleAdmin API
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found!");
return;
}
// Register your commands and menus
RegisterCommands();
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Fallback for hot reload
}
public void OnConfigParsed(Config config)
{
Config = config;
}
private void RegisterCommands()
{
// Register commands here
}
private void RegisterMenus()
{
// Register menus here
}
}
```
---
## Module Structure
### Recommended File Organization
```
YourModuleName/
├── YourModule.cs # Main plugin class
├── Config.cs # Configuration
├── Commands.cs # Command handlers (partial class)
├── Menus.cs # Menu creation (partial class)
├── Actions.cs # Core logic (partial class)
├── lang/ # Translations
│ ├── en.json
│ ├── pl.json
│ └── ...
└── YourModuleName.csproj
```
### Using Partial Classes
Split your code for better organization:
```csharp
// YourModule.cs
public partial class YourModule : BasePlugin, IPluginConfig<Config>
{
// Plugin initialization
}
// Commands.cs
public partial class YourModule
{
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Command logic
}
}
// Menus.cs
public partial class YourModule
{
private object CreateMyMenu(CCSPlayerController player, MenuContext context)
{
// Menu creation
}
}
```
---
## Configuration
### Create Config Class
```csharp
using CounterStrikeSharp.API.Core;
using System.Text.Json.Serialization;
public class Config : IBasePluginConfig
{
[JsonPropertyName("Version")]
public int Version { get; set; } = 1;
[JsonPropertyName("MyCommands")]
public List<string> MyCommands { get; set; } = ["css_mycommand"];
[JsonPropertyName("EnableFeature")]
public bool EnableFeature { get; set; } = true;
[JsonPropertyName("MaxValue")]
public int MaxValue { get; set; } = 100;
}
```
### Config Best Practices
1. **Use command lists** - Allow users to add aliases or disable features
2. **Provide defaults** - Sensible default values
3. **Version your config** - Track config changes
4. **Document settings** - Clear property names
---
## Registering Commands
### Basic Command Registration
```csharp
private void RegisterCommands()
{
if (_api == null) return;
foreach (var cmd in Config.MyCommands)
{
_api.RegisterCommand(cmd, "Command description", OnMyCommand);
}
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Get target players
var targets = _api!.GetTarget(command);
if (targets == null) return;
// Filter for valid players
var players = targets.Players
.Where(p => p.IsValid && !p.IsBot)
.ToList();
// Process each player
foreach (var player in players)
{
if (caller!.CanTarget(player))
{
DoSomething(caller, player);
}
}
// Log the command
_api.LogCommand(caller, command);
}
```
### Command Cleanup
Always unregister commands when unloading:
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
foreach (var cmd in Config.MyCommands)
{
_api.UnRegisterCommand(cmd);
}
}
```
---
## Creating Menus
### Register Menu Category
```csharp
private void RegisterMenus()
{
if (_api == null || _menusRegistered) return;
// Register category
_api.RegisterMenuCategory(
"mycategory",
Localizer?["category_name"] ?? "My Category",
"@css/generic"
);
// Register menu
_api.RegisterMenu(
"mycategory",
"mymenu",
Localizer?["menu_name"] ?? "My Menu",
CreateMyMenu,
"@css/generic",
"css_mycommand" // For permission override
);
_menusRegistered = true;
}
```
### Menu with Player Selection (NEW API)
```csharp
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
{
// Context contains: CategoryId, MenuId, MenuTitle, Permission, CommandName
// No need to repeat "mycategory" and "My Menu" here!
return _api!.CreateMenuWithPlayers(
context, // ← Automatically uses menu title and category
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) => DoSomethingToPlayer(admin, target)
);
}
```
### Menu with Custom Options
```csharp
private object CreateValueSelectionMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var values = new[] { 10, 25, 50, 100, 200 };
foreach (var value in values)
{
_api.AddMenuOption(menu, $"{value} points", player =>
{
GivePoints(player, value);
});
}
return menu;
}
```
### Nested Menus
```csharp
private object CreatePlayerSelectionMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateValueMenu(admin, player);
});
}
return menu;
}
private object CreateValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Select value for {target.PlayerName}", "mycategory", admin);
// Add options...
return menu;
}
```
---
## Translations
### Create Translation Files
Create `lang/en.json`:
```json
{
"command_success": "{green}Success! {default}Action performed on {lightred}{0}",
"command_failed": "{red}Failed! {default}Could not perform action",
"menu_title": "My Custom Menu"
}
```
### Use Translations in Code
```csharp
// In commands
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Using module's own localizer for per-player language
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer,
"command_success",
caller?.PlayerName,
false,
target.PlayerName
);
}
}
```
### Multiple Language Support
Create files for each language:
- `lang/en.json` - English
- `lang/pl.json` - Polish
- `lang/ru.json` - Russian
- `lang/de.json` - German
- etc.
---
## Working with API
### Issue Penalties
```csharp
// Ban online player
_api!.IssuePenalty(
player,
admin,
PenaltyType.Ban,
"Cheating",
1440 // 1 day in minutes
);
// Ban offline player by SteamID
_api!.IssuePenalty(
new SteamID(76561198012345678),
admin,
PenaltyType.Ban,
"Ban evasion",
0 // Permanent
);
// Other penalty types
_api!.IssuePenalty(player, admin, PenaltyType.Gag, "Chat spam", 30);
_api!.IssuePenalty(player, admin, PenaltyType.Mute, "Mic spam", 60);
_api!.IssuePenalty(player, admin, PenaltyType.Silence, "Total abuse", 120);
_api!.IssuePenalty(player, admin, PenaltyType.Warn, "Rule break");
```
### Get Player Information
```csharp
// Get player info with penalty data
var playerInfo = _api!.GetPlayerInfo(player);
Console.WriteLine($"Player: {playerInfo.PlayerName}");
Console.WriteLine($"SteamID: {playerInfo.SteamId}");
Console.WriteLine($"Warnings: {playerInfo.Warnings}");
// Get player mute status
var muteStatus = _api!.GetPlayerMuteStatus(player);
if (muteStatus.ContainsKey(PenaltyType.Gag))
{
Console.WriteLine("Player is gagged");
}
```
### Check Admin Status
```csharp
// Check if admin is in silent mode
if (_api!.IsAdminSilent(admin))
{
// Don't broadcast this action
}
// Get all silent admins
var silentAdmins = _api!.ListSilentAdminsSlots();
```
---
## Events
### Subscribe to Events
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
// Subscribe to events
_api.OnSimpleAdminReady += OnSimpleAdminReady;
_api.OnPlayerPenaltied += OnPlayerPenaltied;
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
_api.OnAdminShowActivity += OnAdminShowActivity;
}
private void OnSimpleAdminReady()
{
Logger.LogInformation("SimpleAdmin is ready!");
RegisterMenus();
}
private void OnPlayerPenaltied(PlayerInfo player, PlayerInfo? admin,
PenaltyType type, string reason, int duration, int? penaltyId, int? serverId)
{
Logger.LogInformation($"{player.PlayerName} received {type} for {reason}");
}
private void OnPlayerPenaltiedAdded(SteamID steamId, PlayerInfo? admin,
PenaltyType type, string reason, int duration, int? penaltyId, int? serverId)
{
Logger.LogInformation($"Offline ban added to {steamId}");
}
private void OnAdminShowActivity(string messageKey, string? callerName,
bool dontPublish, object messageArgs)
{
// React to admin activity
}
```
### Unsubscribe on Unload
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.OnSimpleAdminReady -= OnSimpleAdminReady;
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
// ... unsubscribe all events
}
```
---
## Best Practices
### 1. Always Check for Null
```csharp
if (_api == null)
{
Logger.LogError("API not available!");
return;
}
```
### 2. Validate Player State
```csharp
if (!player.IsValid || !player.PawnIsAlive)
{
return;
}
```
### 3. Check Target Permissions
```csharp
if (!caller.CanTarget(target))
{
// caller can't target this player (immunity)
return;
}
```
### 4. Log Commands
```csharp
_api.LogCommand(caller, command);
// or
_api.LogCommand(caller, $"css_mycommand {player.PlayerName}");
```
### 5. Use Per-Player Translations
```csharp
// Each player sees message in their language!
_api.ShowAdminActivityLocalized(
Localizer,
"translation_key",
callerName,
false,
args
);
```
### 6. Clean Up Resources
```csharp
public override void Unload(bool hotReload)
{
// Unregister commands
// Unregister menus
// Unsubscribe events
// Dispose resources
}
```
---
## Common Patterns
### Player Targeting Helper
```csharp
private List<CCSPlayerController> GetTargets(CommandInfo command, CCSPlayerController? caller)
{
var targets = _api!.GetTarget(command);
if (targets == null) return new List<CCSPlayerController>();
return targets.Players
.Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
.ToList();
}
```
### Menu Context Pattern (NEW!)
```csharp
// ✅ NEW: Use context to avoid duplication
private object CreateMenu(CCSPlayerController player, MenuContext context)
{
// context.MenuTitle, context.CategoryId already set!
return _api!.CreateMenuWithPlayers(context, player, filter, action);
}
// ❌ OLD: Had to repeat title and category
private object CreateMenu(CCSPlayerController player)
{
return _api!.CreateMenuWithPlayers("My Menu", "mycategory", player, filter, action);
}
```
### Action with Activity Message
```csharp
private void DoAction(CCSPlayerController? caller, CCSPlayerController target)
{
// Perform action
// ...
// Show activity
if (caller == null || !_api!.IsAdminSilent(caller))
{
_api!.ShowAdminActivityLocalized(
Localizer,
"action_message",
caller?.PlayerName,
false,
target.PlayerName
);
}
// Log action
_api!.LogCommand(caller, $"css_action {target.PlayerName}");
}
```
---
## Testing Your Module
### 1. Build
```bash
dotnet build -c Release
```
### 2. Copy to Server
```
game/csgo/addons/counterstrikesharp/plugins/YourModuleName/
```
### 3. Test
- Start server
- Check console for load messages
- Test commands
- Test menus
- Check translations
### 4. Debug
Enable detailed logging:
```csharp
Logger.LogInformation("Debug: ...");
Logger.LogWarning("Warning: ...");
Logger.LogError("Error: ...");
```
---
## Example: Complete Mini-Module
Here's a complete working example:
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CS2_SimpleAdminApi;
namespace ExampleModule;
public class ExampleModule : BasePlugin, IPluginConfig<Config>
{
public override string ModuleName => "Example Module";
public override string ModuleVersion => "1.0.0";
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
public Config Config { get; set; } = new();
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found!");
return;
}
// Register command
if (Config.ExampleCommands.Count > 0)
{
foreach (var cmd in Config.ExampleCommands)
{
_api.RegisterCommand(cmd, "Example command", OnExampleCommand);
}
}
}
public void OnConfigParsed(Config config)
{
Config = config;
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnExampleCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var target in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
// Do something to target
caller?.PrintToChat($"Performed action on {target.PlayerName}");
}
_api.LogCommand(caller, command);
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
foreach (var cmd in Config.ExampleCommands)
{
_api.UnRegisterCommand(cmd);
}
}
}
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
public List<string> ExampleCommands { get; set; } = ["css_example"];
}
```
---
## Next Steps
- **[Study Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Complete reference
- **[Read API Documentation](../developer/api/overview)** - Full API reference
- **[Check Examples](../developer/module/examples)** - More code examples
---
## Resources
- **[CS2-SimpleAdmin GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS documentation
- **[Module Development Guide](../developer/module/getting-started)** - Detailed guide
---
## Need Help?
- **Issues:** [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
- **Discussions:** [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
- **Examples:** Study official modules for reference

View File

@@ -0,0 +1,691 @@
---
sidebar_position: 2
---
# Fun Commands Module
Add entertaining and powerful player manipulation commands to your server.
## Overview
The Fun Commands module extends CS2-SimpleAdmin with commands for god mode, noclip, freeze, respawn, weapon management, and player attribute modification.
**Module Name:** `CS2-SimpleAdmin_FunCommands`
---
## Features
- ⭐ God Mode - Make players invincible
- 👻 No Clip - Allow players to fly through walls
- 🧊 Freeze/Unfreeze - Freeze players in place
- 🔄 Respawn - Bring dead players back
- 🔫 Give Weapons - Provide any weapon to players
- 🗑️ Strip Weapons - Remove all weapons
- ❤️ Set HP - Modify player health
- ⚡ Set Speed - Change movement speed
- 🌙 Set Gravity - Modify gravity
- 💰 Set Money - Adjust player money
- 📏 Resize Player - Change player model size
---
## Installation
### Prerequisites
- CS2-SimpleAdmin installed and working
- CS2-SimpleAdminApi.dll in shared folder
### Install Steps
1. **Download** the module from releases
2. **Extract** to your server:
```
game/csgo/addons/counterstrikesharp/plugins/CS2-SimpleAdmin_FunCommands/
```
3. **Restart** your server or reload plugins:
```
css_plugins reload
```
4. **Verify** the module loaded:
- Check server console for load message
- Try `css_admin` and look for "Fun Commands" menu
---
## Commands
### God Mode
Toggle god mode (invincibility) for a player.
```bash
css_god <#userid or name>
css_godmode <#userid or name>
```
**Permission:** `@css/cheats`
**Examples:**
```bash
css_god #123
css_god PlayerName
css_god @all # Toggle god mode for everyone
```
**Effects:**
- Player takes no damage
- Toggles on/off with each use
---
### No Clip
Enable noclip mode (fly through walls).
```bash
css_noclip <#userid or name>
```
**Permission:** `@css/cheats`
**Examples:**
```bash
css_noclip #123
css_noclip PlayerName
```
**Effects:**
- Player can fly
- Can pass through walls
- Gravity disabled
- Toggles on/off with each use
---
### Freeze
Freeze a player in place.
```bash
css_freeze <#userid or name> [duration]
```
**Permission:** `@css/slay`
**Parameters:**
- `duration` - Freeze duration in seconds (optional, default: permanent until unfreeze)
**Examples:**
```bash
css_freeze #123 # Freeze permanently
css_freeze PlayerName 30 # Freeze for 30 seconds
css_freeze @t 10 # Freeze all terrorists for 10 seconds
```
**Effects:**
- Player cannot move
- Player cannot shoot
- Auto-unfreezes after duration (if specified)
---
### Unfreeze
Unfreeze a frozen player.
```bash
css_unfreeze <#userid or name>
```
**Permission:** `@css/slay`
**Examples:**
```bash
css_unfreeze #123
css_unfreeze PlayerName
css_unfreeze @all # Unfreeze everyone
```
---
### Respawn
Respawn a dead player at last death position.
```bash
css_respawn <#userid or name>
```
**Permission:** `@css/cheats`
**Examples:**
```bash
css_respawn #123
css_respawn PlayerName
css_respawn @dead # Respawn all dead players
```
**Effects:**
- Player spawns at death point
- Gets default weapons
- Joins their team
---
### Give Weapon
Give a weapon to a player.
```bash
css_give <#userid or name> <weapon>
```
**Permission:** `@css/cheats`
**Weapon names:**
**Rifles:**
- `weapon_ak47` or `ak47`
- `weapon_m4a1` or `m4a1`
- `weapon_m4a1_silencer` or `m4a1_silencer`
- `weapon_awp` or `awp`
- `weapon_aug` or `aug`
- `weapon_sg556` or `sg556`
- `weapon_ssg08` or `ssg08` (Scout)
- `weapon_g3sg1` or `g3sg1`
- `weapon_scar20` or `scar20`
**SMGs:**
- `weapon_mp5sd` or `mp5sd`
- `weapon_mp7` or `mp7`
- `weapon_mp9` or `mp9`
- `weapon_mac10` or `mac10`
- `weapon_p90` or `p90`
- `weapon_ump45` or `ump45`
- `weapon_bizon` or `bizon`
**Heavy:**
- `weapon_nova` or `nova`
- `weapon_xm1014` or `xm1014`
- `weapon_mag7` or `mag7`
- `weapon_sawedoff` or `sawedoff`
- `weapon_m249` or `m249`
- `weapon_negev` or `negev`
**Pistols:**
- `weapon_deagle` or `deagle`
- `weapon_elite` or `elite` (Dual Berettas)
- `weapon_fiveseven` or `fiveseven`
- `weapon_glock` or `glock`
- `weapon_hkp2000` or `hkp2000`
- `weapon_p250` or `p250`
- `weapon_usp_silencer` or `usp_silencer`
- `weapon_tec9` or `tec9`
- `weapon_cz75a` or `cz75a`
- `weapon_revolver` or `revolver`
**Grenades:**
- `weapon_flashbang` or `flashbang`
- `weapon_hegrenade` or `hegrenade`
- `weapon_smokegrenade` or `smokegrenade`
- `weapon_molotov` or `molotov`
- `weapon_incgrenade` or `incgrenade`
- `weapon_decoy` or `decoy`
**Equipment:**
- `weapon_knife` or `knife`
- `weapon_taser` or `taser`
- `item_defuser` or `defuser`
- `item_kevlar` or `kevlar`
- `item_assaultsuit` or `assaultsuit`
**Examples:**
```bash
css_give #123 awp
css_give PlayerName ak47
css_give @ct m4a1
css_give @all deagle
```
---
### Strip Weapons
Remove all weapons from a player.
```bash
css_strip <#userid or name>
```
**Permission:** `@css/slay`
**Examples:**
```bash
css_strip #123
css_strip PlayerName
css_strip @t # Disarm all terrorists
```
---
### Set HP
Set a player's health.
```bash
css_hp <#userid or name> <health>
```
**Permission:** `@css/slay`
**Parameters:**
- `health` - Health amount (1-999+)
**Examples:**
```bash
css_hp #123 100 # Full health
css_hp PlayerName 200 # 200 HP
css_hp @all 1 # 1 HP everyone
```
**Common values:**
- `1` - 1 HP (one-shot mode)
- `100` - Normal health
- `200` - Double health
- `500` - Tank mode
---
### Set Speed
Modify a player's movement speed.
```bash
css_speed <#userid or name> <speed>
```
**Permission:** `@css/slay`
**Parameters:**
- `speed` - Speed multiplier (0.1 - 10.0)
- `1.0` = Normal speed
- `2.0` = Double speed
- `0.5` = Half speed
**Examples:**
```bash
css_speed #123 1.5 # 50% faster
css_speed PlayerName 0.5 # Slow motion
css_speed @all 2.0 # Everyone fast
css_speed #123 1.0 # Reset to normal
```
**Common values:**
- `0.5` - Slow motion mode
- `1.0` - Normal (reset)
- `1.5` - Fast mode
- `2.0` - Super fast
- `3.0` - Extremely fast
---
### Set Gravity
Modify a player's gravity.
```bash
css_gravity <#userid or name> <gravity>
```
**Permission:** `@css/slay`
**Parameters:**
- `gravity` - Gravity multiplier (0.1 - 10.0)
- `1.0` = Normal gravity
- `0.5` = Moon jump
- `2.0` = Heavy
**Examples:**
```bash
css_gravity #123 0.5 # Moon jump
css_gravity PlayerName 0.1 # Super jump
css_gravity @all 2.0 # Heavy gravity
css_gravity #123 1.0 # Reset to normal
```
**Common values:**
- `0.1` - Super high jumps
- `0.5` - Moon gravity
- `1.0` - Normal (reset)
- `2.0` - Heavy/fast falling
---
### Set Money
Set a player's money amount.
```bash
css_money <#userid or name> <amount>
```
**Permission:** `@css/slay`
**Parameters:**
- `amount` - Money amount (0-65535)
**Examples:**
```bash
css_money #123 16000 # Max money
css_money PlayerName 0 # Remove all money
css_money @ct 10000 # Give all CTs $10,000
```
---
### Resize Player
Change a player's model size.
```bash
css_resize <#userid or name> <scale>
```
**Permission:** `@css/slay`
**Parameters:**
- `scale` - Size scale (0.1 - 10.0)
- `1.0` = Normal size
- `0.5` = Half size
- `2.0` = Double size
**Examples:**
```bash
css_resize #123 0.5 # Tiny player
css_resize PlayerName 2.0 # Giant player
css_resize #123 1.0 # Reset to normal
```
**Common values:**
- `0.5` - Tiny mode
- `1.0` - Normal (reset)
- `1.5` - Big
- `2.0` - Giant
---
## Configuration
Configuration file location:
```
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.json
```
### Default Configuration
```json
{
"Version": 1,
"GodCommands": ["css_god", "css_godmode"],
"NoclipCommands": ["css_noclip"],
"FreezeCommands": ["css_freeze"],
"UnfreezeCommands": ["css_unfreeze"],
"RespawnCommands": ["css_respawn"],
"GiveCommands": ["css_give"],
"StripCommands": ["css_strip"],
"HpCommands": ["css_hp"],
"SpeedCommands": ["css_speed"],
"GravityCommands": ["css_gravity"],
"MoneyCommands": ["css_money"],
"ResizeCommands": ["css_resize"]
}
```
### Customizing Commands
**Add aliases:**
```json
"GodCommands": ["css_god", "css_godmode", "css_immortal"]
```
**Disable feature:**
```json
"GodCommands": []
```
**Rename command:**
```json
"NoclipCommands": ["css_fly"]
```
---
## Admin Menu Integration
The module automatically adds a "Fun Commands" category to the admin menu with these options:
- God Mode
- No Clip
- Freeze
- Respawn
- Give Weapon
- Strip Weapons
- Set HP
- Set Speed
- Set Gravity
- Set Money
- Resize Player
**Access menu:**
```bash
css_admin # Navigate to "Fun Commands"
```
---
## Permission System
### Permission Override
Admins can override command permissions using CounterStrikeSharp's admin system.
**Example:**
If you want VIPs to use god mode:
1. **In admin config**, add permission override for `css_god`:
```json
{
"css_god": ["@css/vip"]
}
```
2. **VIPs will now see God Mode** in the menu
---
## Permissions Required
| Command | Default Permission | Description |
|---------|------------------|-------------|
| `css_god` | `@css/cheats` | God mode |
| `css_noclip` | `@css/cheats` | No clip |
| `css_freeze` | `@css/slay` | Freeze players |
| `css_unfreeze` | `@css/slay` | Unfreeze players |
| `css_respawn` | `@css/cheats` | Respawn players |
| `css_give` | `@css/cheats` | Give weapons |
| `css_strip` | `@css/slay` | Strip weapons |
| `css_hp` | `@css/slay` | Set health |
| `css_speed` | `@css/slay` | Set speed |
| `css_gravity` | `@css/slay` | Set gravity |
| `css_money` | `@css/slay` | Set money |
| `css_resize` | `@css/slay` | Resize player |
---
## Use Cases
### Fun Rounds
```bash
# Low gravity, high speed round
css_gravity @all 0.3
css_speed @all 1.5
# One-shot mode
css_hp @all 1
css_give @all deagle
# Tiny players
css_resize @all 0.5
```
### Admin Events
```bash
# Hide and seek (seekers)
css_speed @ct 1.5
css_hp @ct 200
# Hide and seek (hiders)
css_resize @t 0.5
css_speed @t 0.8
```
### Testing & Debug
```bash
# Test map navigation
css_noclip @me
css_god @me
# Test weapon balance
css_give @me awp
css_hp @me 100
```
---
## Best Practices
### Competitive Balance
1. **Don't use during serious matches** - Breaks game balance
2. **Announce fun rounds** - Let players know it's for fun
3. **Reset after use** - Return to normal settings
4. **Save for appropriate times** - End of night, special events
### Reset Commands
Always reset modifications after fun rounds:
```bash
css_speed @all 1.0
css_gravity @all 1.0
css_resize @all 1.0
```
### Permission Management
1. **Limit @css/cheats** - Only trusted admins
2. **@css/slay is safer** - For HP/speed/gravity
3. **Monitor usage** - Check logs for abuse
---
## Troubleshooting
### Speed/Gravity not persisting
**Solution:**
- These are maintained by a repeating timer
- If they reset, reapply them
- Check server console for timer errors
### God mode not working
**Check:**
- Is player alive?
- Check console for errors
- Try toggling off and on
### Can't give weapons
**Check:**
- Correct weapon name
- Player is alive
- Player has inventory space
### Noclip doesn't work
**Check:**
- Player must be alive
- sv_cheats doesn't need to be enabled
- Check console for errors
---
## Module Development
This module serves as a **reference implementation** for creating CS2-SimpleAdmin modules.
**Key concepts demonstrated:**
- Command registration from configuration
- Menu creation with SimpleAdmin API
- Per-player translation support
- Proper cleanup on module unload
- Code organization using partial classes
**[View source code](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** for implementation details.
---
## Translations
The module includes translations for 13 languages:
- English (en)
- Polish (pl)
- Russian (ru)
- Portuguese (pt)
- And 9 more...
Translation files location:
```
plugins/CS2-SimpleAdmin_FunCommands/lang/
```
---
## Related Documentation
- **[Player Commands](../user/commands/playercommands)** - Core player commands
- **[Module Development](development)** - Create your own modules
- **[API Reference](../developer/api/overview)** - CS2-SimpleAdmin API
---
## Version History
**v1.0.0** - Initial release
- God mode
- Noclip
- Freeze/Unfreeze
- Respawn
- Give/Strip weapons
- HP/Speed/Gravity/Money
- Resize player
- Admin menu integration
- 13 language support
---
## Support
**Issues:** [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
**Questions:** [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)

View File

@@ -0,0 +1,260 @@
---
sidebar_position: 1
---
# Modules Introduction
Extend CS2-SimpleAdmin functionality with powerful modules.
## What are Modules?
Modules are extensions that add new features to CS2-SimpleAdmin. They use the CS2-SimpleAdmin API to integrate seamlessly with the core plugin.
## Official Modules
### Fun Commands Module
Adds entertainment and player manipulation commands like god mode, noclip, freeze, and more.
**[Learn more →](funcommands)**
---
## Benefits of Modules
### 🔌 Easy Integration
- Built on CS2-SimpleAdmin API
- Automatic menu registration
- Command system integration
### 🎨 Feature Separation
- Keep core plugin lightweight
- Add only features you need
- Easy to enable/disable
### 🔧 Customizable
- Configure each module independently
- Disable unwanted commands
- Customize permissions
### 📦 Simple Installation
- Drop module files in folder
- Restart server
- Module auto-loads
---
## Installing Modules
### Standard Installation
1. **Download the module** from releases or build from source
2. **Extract to plugins folder:**
```
game/csgo/addons/counterstrikesharp/plugins/ModuleName/
```
3. **Restart server** or reload plugins:
```
css_plugins reload
```
4. **Configure** (if needed):
```
addons/counterstrikesharp/configs/plugins/ModuleName/
```
---
## Module Structure
Typical module structure:
```
plugins/
└── CS2-SimpleAdmin_ModuleName/
├── CS2-SimpleAdmin_ModuleName.dll
├── CS2-SimpleAdmin_ModuleName.json (config)
└── lang/ (translations)
├── en.json
├── pl.json
└── ...
```
---
## Module Configuration
Each module has its own configuration file:
```
addons/counterstrikesharp/configs/plugins/ModuleName/ModuleName.json
```
### Common Configuration Pattern
```json
{
"Version": 1,
"CommandName": ["css_command", "css_alias"],
"OtherSettings": {
"EnableFeature": true
}
}
```
**Key Features:**
- Command lists allow multiple aliases
- Empty command list = feature disabled
- Module-specific settings
---
## Available Modules
### Core Modules
| Module | Description | Status |
|--------|-------------|--------|
| **[Fun Commands](funcommands)** | God mode, noclip, freeze, speed, gravity | ✅ Official |
### Community Modules
Check the [GitHub repository](https://github.com/daffyyyy/CS2-SimpleAdmin) for community-contributed modules.
---
## Developing Modules
Want to create your own module?
**[See Module Development Guide →](development)**
**[See Developer Documentation →](../developer/intro)**
---
## Module vs Core Plugin
### When to use Core Plugin:
- Essential admin functions
- Punishment system
- Permission management
- Database operations
### When to use Modules:
- Optional features
- Server-specific functionality
- Experimental features
- Custom integrations
---
## Module Dependencies
### Required for All Modules:
- CS2-SimpleAdmin (core plugin)
- CS2-SimpleAdminApi.dll
### Module-Specific:
Check each module's documentation for specific requirements.
---
## Troubleshooting Modules
### Module doesn't load
**Check:**
1. Is CS2-SimpleAdmin loaded?
2. Is CS2-SimpleAdminApi.dll in shared folder?
3. Check server console for errors
4. Verify module files are complete
### Module commands not working
**Check:**
1. Is command enabled in module config?
2. Do you have required permissions?
3. Check Commands.json for conflicts
4. Verify module loaded successfully
### Module conflicts
**Check:**
- Multiple modules providing same command
- Check server console for warnings
- Disable conflicting module
---
## Best Practices
### Module Management
1. **Use only needed modules** - Don't overload server
2. **Keep modules updated** - Check for updates regularly
3. **Test before production** - Test modules on dev server first
4. **Review permissions** - Understand what each module can do
### Performance
1. **Monitor resource usage** - Some modules may impact performance
2. **Configure wisely** - Disable unused features
3. **Check logs** - Monitor for errors
---
## Module Updates
### Updating Modules
1. **Backup current version**
2. **Download new version**
3. **Replace files** in plugins folder
4. **Check configuration** - New config options may exist
5. **Restart server**
### Breaking Changes
Some updates may have breaking changes:
- Check module changelog
- Review new configuration options
- Test thoroughly
---
## Community Contributions
### Sharing Modules
Created a module? Share it with the community!
1. **Publish on GitHub**
2. **Document thoroughly**
3. **Provide examples**
4. **Include README**
### Using Community Modules
1. **Review code** - Ensure it's safe
2. **Check compatibility** - Verify CS2-SimpleAdmin version
3. **Test thoroughly** - Don't trust blindly
4. **Report issues** - Help improve modules
---
## Next Steps
- **[Explore Fun Commands Module](funcommands)** - Add entertainment features
- **[Learn Module Development](development)** - Create your own modules
- **[Read API Documentation](../developer/intro)** - Understand the API
---
## Need Help?
- **Issues** - [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
- **Discussions** - [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
- **Examples** - Check official modules for reference

View File

@@ -0,0 +1,8 @@
{
"label": "Tutorial - Basics",
"position": 2,
"link": {
"type": "generated-index",
"description": "5 minutes to learn the most important Docusaurus concepts."
}
}

View File

@@ -0,0 +1,23 @@
---
sidebar_position: 6
---
# Congratulations!
You have just learned the **basics of Docusaurus** and made some changes to the **initial template**.
Docusaurus has **much more to offer**!
Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**.
Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610)
## What's next?
- Read the [official documentation](https://docusaurus.io/)
- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config)
- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration)
- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout)
- Add a [search bar](https://docusaurus.io/docs/search)
- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase)
- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support)

View File

@@ -0,0 +1,34 @@
---
sidebar_position: 3
---
# Create a Blog Post
Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed...
## Create your first Post
Create a file at `blog/2021-02-28-greetings.md`:
```md title="blog/2021-02-28-greetings.md"
---
slug: greetings
title: Greetings!
authors:
- name: Joel Marcey
title: Co-creator of Docusaurus 1
url: https://github.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
- name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
tags: [greetings]
---
Congratulations, you have made your first post!
Feel free to play around and edit this post as much as you like.
```
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).

View File

@@ -0,0 +1,57 @@
---
sidebar_position: 2
---
# Create a Document
Documents are **groups of pages** connected through:
- a **sidebar**
- **previous/next navigation**
- **versioning**
## Create your first Doc
Create a Markdown file at `docs/hello.md`:
```md title="docs/hello.md"
# Hello
This is my **first Docusaurus document**!
```
A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello).
## Configure the Sidebar
Docusaurus automatically **creates a sidebar** from the `docs` folder.
Add metadata to customize the sidebar label and position:
```md title="docs/hello.md" {1-4}
---
sidebar_label: 'Hi!'
sidebar_position: 3
---
# Hello
This is my **first Docusaurus document**!
```
It is also possible to create your sidebar explicitly in `sidebars.js`:
```js title="sidebars.js"
export default {
tutorialSidebar: [
'intro',
// highlight-next-line
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
};
```

View File

@@ -0,0 +1,43 @@
---
sidebar_position: 1
---
# Create a Page
Add **Markdown or React** files to `src/pages` to create a **standalone page**:
- `src/pages/index.js``localhost:3000/`
- `src/pages/foo.md``localhost:3000/foo`
- `src/pages/foo/bar.js``localhost:3000/foo/bar`
## Create your first React Page
Create a file at `src/pages/my-react-page.js`:
```jsx title="src/pages/my-react-page.js"
import React from 'react';
import Layout from '@theme/Layout';
export default function MyReactPage() {
return (
<Layout>
<h1>My React page</h1>
<p>This is a React page</p>
</Layout>
);
}
```
A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page).
## Create your first Markdown Page
Create a file at `src/pages/my-markdown-page.md`:
```mdx title="src/pages/my-markdown-page.md"
# My Markdown page
This is a Markdown page
```
A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page).

View File

@@ -0,0 +1,31 @@
---
sidebar_position: 5
---
# Deploy your site
Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**).
It builds your site as simple **static HTML, JavaScript and CSS files**.
## Build your site
Build your site **for production**:
```bash
npm run build
```
The static files are generated in the `build` folder.
## Deploy your site
Test your production build locally:
```bash
npm run serve
```
The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/).
You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**).

View File

@@ -0,0 +1,152 @@
---
sidebar_position: 4
---
# Markdown Features
Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**.
## Front Matter
Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
```text title="my-doc.md"
// highlight-start
---
id: my-doc-id
title: My document title
description: My document description
slug: /my-custom-url
---
// highlight-end
## Markdown heading
Markdown text with [links](./hello.md)
```
## Links
Regular Markdown links are supported, using url paths or relative file paths.
```md
Let's see how to [Create a page](/create-a-page).
```
```md
Let's see how to [Create a page](./create-a-page.md).
```
**Result:** Let's see how to [Create a page](./create-a-page.md).
## Images
Regular Markdown images are supported.
You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`):
```md
![Docusaurus logo](/img/docusaurus.png)
```
![Docusaurus logo](/img/docusaurus.png)
You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them:
```md
![Docusaurus logo](./img/docusaurus.png)
```
## Code Blocks
Markdown code blocks are supported with Syntax highlighting.
````md
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return <h1>Hello, Docusaurus!</h1>;
}
```
````
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return <h1>Hello, Docusaurus!</h1>;
}
```
## Admonitions
Docusaurus has a special syntax to create admonitions and callouts:
```md
:::tip My tip
Use this awesome feature option
:::
:::danger Take care
This action is dangerous
:::
```
:::tip My tip
Use this awesome feature option
:::
:::danger Take care
This action is dangerous
:::
## MDX and React Components
[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**:
```jsx
export const Highlight = ({children, color}) => (
<span
style={{
backgroundColor: color,
borderRadius: '20px',
color: '#fff',
padding: '10px',
cursor: 'pointer',
}}
onClick={() => {
alert(`You clicked the color ${color} with label ${children}`)
}}>
{children}
</span>
);
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
```
export const Highlight = ({children, color}) => (
<span
style={{
backgroundColor: color,
borderRadius: '20px',
color: '#fff',
padding: '10px',
cursor: 'pointer',
}}
onClick={() => {
alert(`You clicked the color ${color} with label ${children}`);
}}>
{children}
</span>
);
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
This is <Highlight color="#1877F2">Facebook blue</Highlight> !

View File

@@ -0,0 +1,7 @@
{
"label": "Tutorial - Extras",
"position": 3,
"link": {
"type": "generated-index"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,55 @@
---
sidebar_position: 1
---
# Manage Docs Versions
Docusaurus can manage multiple versions of your docs.
## Create a docs version
Release a version 1.0 of your project:
```bash
npm run docusaurus docs:version 1.0
```
The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created.
Your docs now have 2 versions:
- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs
- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs**
## Add a Version Dropdown
To navigate seamlessly across versions, add a version dropdown.
Modify the `docusaurus.config.js` file:
```js title="docusaurus.config.js"
export default {
themeConfig: {
navbar: {
items: [
// highlight-start
{
type: 'docsVersionDropdown',
},
// highlight-end
],
},
},
};
```
The docs version dropdown appears in your navbar:
![Docs Version Dropdown](./img/docsVersionDropdown.png)
## Update an existing version
It is possible to edit versioned docs in their respective folder:
- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello`
- `docs/hello.md` updates `http://localhost:3000/docs/next/hello`

View File

@@ -0,0 +1,88 @@
---
sidebar_position: 2
---
# Translate your site
Let's translate `docs/intro.md` to French.
## Configure i18n
Modify `docusaurus.config.js` to add support for the `fr` locale:
```js title="docusaurus.config.js"
export default {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
};
```
## Translate a doc
Copy the `docs/intro.md` file to the `i18n/fr` folder:
```bash
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/
cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md
```
Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French.
## Start your localized site
Start your site on the French locale:
```bash
npm run start -- --locale fr
```
Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated.
:::caution
In development, you can only use one locale at a time.
:::
## Add a Locale Dropdown
To navigate seamlessly across languages, add a locale dropdown.
Modify the `docusaurus.config.js` file:
```js title="docusaurus.config.js"
export default {
themeConfig: {
navbar: {
items: [
// highlight-start
{
type: 'localeDropdown',
},
// highlight-end
],
},
},
};
```
The locale dropdown now appears in your navbar:
![Locale Dropdown](./img/localeDropdown.png)
## Build your localized site
Build your site for a specific locale:
```bash
npm run build -- --locale fr
```
Or build your site to include all the locales at once:
```bash
npm run build
```

View File

@@ -0,0 +1,262 @@
---
sidebar_position: 1
---
# Ban Commands
Commands for managing player bans.
## Ban Player
Ban a player currently on the server.
```bash
css_ban <#userid or name> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/ban`
**Examples:**
```bash
css_ban @all 60 "Timeout for everyone"
css_ban #123 1440 "Hacking - 1 day ban"
css_ban PlayerName 0 "Permanent ban for cheating"
css_ban @ct 30 "CT team timeout"
```
**Notes:**
- Time in minutes (0 = permanent)
- Supports player targeting (@all, @ct, @t, #userid, name)
- Reason is optional but recommended
---
## Add Ban (Offline Player)
Ban a player by SteamID even if they're not online.
```bash
css_addban <steamid> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/ban`
**Examples:**
```bash
css_addban STEAM_1:0:12345678 1440 "Ban evasion"
css_addban 76561198012345678 10080 "Hacking - 7 day ban"
css_addban STEAM_1:1:87654321 0 "Permanent ban"
```
**Supported SteamID formats:**
- SteamID64: `76561198012345678`
- SteamID: `STEAM_1:0:12345678`
- SteamID3: `[U:1:12345678]`
---
## Ban IP Address
Ban an IP address.
```bash
css_banip <ip> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/ban`
**Examples:**
```bash
css_banip 192.168.1.100 1440 "Ban evasion attempt"
css_banip 10.0.0.5 0 "Persistent troublemaker"
```
**Notes:**
- Useful for preventing ban evasion
- Can be combined with SteamID bans
- Check config for `BanType` setting (SteamID, IP, or Both)
---
## Unban Player
Remove a ban from a player.
```bash
css_unban <steamid or name or ip> [reason]
```
**Permission:** `@css/unban`
**Examples:**
```bash
css_unban 76561198012345678 "Appeal accepted"
css_unban STEAM_1:0:12345678 "Ban lifted"
css_unban 192.168.1.100 "Wrong person banned"
css_unban PlayerName "Mistake"
```
**Notes:**
- Works with SteamID, IP, or player name
- Unban reason is logged
- Can unban offline players
---
## Warn Player
Issue a warning to a player.
```bash
css_warn <#userid or name> [reason]
```
**Permission:** `@css/kick`
**Examples:**
```bash
css_warn #123 "Mic spam"
css_warn PlayerName "Language"
css_warn @all "Final warning"
```
**Notes:**
- Warnings can accumulate
- Auto-escalation to bans based on `WarnThreshold` config
- Example: 3 warnings = 1 hour ban, 4 warnings = 2 hour ban
**Warning Threshold Configuration:**
```json
"WarnThreshold": {
"3": "css_addban STEAMID64 60 \"3 warnings\"",
"4": "css_ban #USERID 120 \"4 warnings\""
}
```
---
## Unwarn Player
Remove a warning from a player.
```bash
css_unwarn <steamid or name>
```
**Permission:** `@css/kick`
**Examples:**
```bash
css_unwarn 76561198012345678
css_unwarn PlayerName
```
**Notes:**
- Removes the most recent warning
- Helps manage warning thresholds
- Can be used for offline players
---
## Permission Requirements
| Command | Required Permission | Description |
|---------|-------------------|-------------|
| `css_ban` | `@css/ban` | Ban online players |
| `css_addban` | `@css/ban` | Ban offline players by SteamID |
| `css_banip` | `@css/ban` | Ban IP addresses |
| `css_unban` | `@css/unban` | Remove bans |
| `css_warn` | `@css/kick` | Issue warnings |
| `css_unwarn` | `@css/kick` | Remove warnings |
## Ban Types
Configure ban behavior in `CS2-SimpleAdmin.json`:
```json
"BanType": 1
```
**Options:**
- `1` - SteamID only (default)
- `2` - IP only
- `3` - Both SteamID and IP
## Time Durations
Common time values:
| Duration | Minutes | Description |
|----------|---------|-------------|
| 1 minute | 1 | Very short timeout |
| 5 minutes | 5 | Short timeout |
| 15 minutes | 15 | Medium timeout |
| 1 hour | 60 | Standard timeout |
| 1 day | 1440 | Daily ban |
| 1 week | 10080 | Weekly ban |
| 2 weeks | 20160 | Bi-weekly ban |
| 1 month | 43200 | Monthly ban |
| Permanent | 0 | Never expires |
## Player Targeting
All ban commands support advanced targeting:
- `@all` - Target all players
- `@ct` - Target all Counter-Terrorists
- `@t` - Target all Terrorists
- `@spec` - Target all spectators
- `#123` - Target by userid
- `PlayerName` - Target by name (partial match)
## Best Practices
### Banning
1. **Always provide a reason** - Helps with appeals and record keeping
2. **Use appropriate durations** - Don't permaban for minor offenses
3. **Check ban history** - Use `css_who` to see if player has priors
4. **Consider warnings first** - Give players a chance to improve
### Warning System
1. **Be consistent** - Use warnings for minor offenses
2. **Configure thresholds** - Set up auto-escalation in config
3. **Communicate clearly** - Let players know why they're warned
4. **Review regularly** - Check warning history with `css_warns`
### Multi-Account Detection
When `CheckMultiAccountsByIp` is enabled:
- Plugin detects multiple accounts from same IP
- Sends Discord notifications if configured
- Helps identify ban evasion
## Troubleshooting
### Ban doesn't work
**Check:**
- Do you have `@css/ban` permission?
- Is the SteamID format correct?
- Check server console for errors
### Player rejoins after ban
**Check:**
- Is `MultiServerMode` enabled if using multiple servers?
- Is the database shared across servers?
- Check ban type configuration (SteamID vs IP)
### Warning threshold not working
**Check:**
- Is `WarnThreshold` configured correctly?
- Are the command formats correct in config?
- Check server console for execution errors
## Related Commands
- **[Communication Commands](basecomms)** - Mute, gag, silence
- **[Player Commands](playercommands)** - Kick, slay, etc.
- **[Base Commands](basecommands)** - Admin management

View File

@@ -0,0 +1,442 @@
---
sidebar_position: 4
---
# Chat Commands
Admin chat and messaging commands.
## Admin Chat
### Admin Say (Private)
Send a message to all online admins only.
```bash
css_asay <message>
```
**Permission:** `@css/chat`
**Features:**
- Only admins see the message
- Useful for admin coordination
- Colored differently from regular chat
**Examples:**
```bash
css_asay "Player is suspicious, keep an eye on them"
css_asay "I'm going AFK for 5 minutes"
css_asay "Need help with a situation"
```
**Message format:**
```
[ADMIN] YourName: message
```
---
## Public Announcements
### CSS Say (Colored)
Send a colored message to all players.
```bash
css_cssay <message>
```
**Permission:** `@css/chat`
**Features:**
- Colorful formatted message
- Visible to all players
- Stands out from regular chat
**Examples:**
```bash
css_cssay "Server will restart in 5 minutes!"
css_cssay "Welcome to our server!"
css_cssay "Rules: No cheating, be respectful"
```
---
### Say (With Prefix)
Send a message to all players with admin prefix.
```bash
css_say <message>
```
**Permission:** `@css/chat`
**Features:**
- Message shows with "(ADMIN)" prefix
- Visible to all players
- Authority message format
**Examples:**
```bash
css_say "Please be respectful in chat"
css_say "Cheating will result in permanent ban"
css_say "Type !rules for server rules"
```
**Message format:**
```
(ADMIN) YourName: message
```
---
### Private Say (Whisper)
Send a private message to a specific player.
```bash
css_psay <#userid or name> <message>
```
**Permission:** `@css/chat`
**Features:**
- Only the target player sees the message
- Useful for private warnings or help
- Doesn't clutter public chat
**Examples:**
```bash
css_psay #123 "Please stop mic spamming"
css_psay PlayerName "You need to join a team"
css_psay @all "This is a private message to everyone"
```
**Target receives:**
```
(ADMIN to you) AdminName: message
```
---
### Center Say
Display a message in the center of all players' screens.
```bash
css_csay <message>
```
**Permission:** `@css/chat`
**Features:**
- Large text in center of screen
- Impossible to miss
- Useful for important announcements
**Examples:**
```bash
css_csay "ROUND STARTS IN 10 SECONDS"
css_csay "FREEZE! Don't move!"
css_csay "Server restarting NOW"
```
**Display:**
- Shows in center of screen
- Large, bold text
- Auto-fades after a few seconds
---
### HUD Say
Display a message on players' HUD (screen overlay).
```bash
css_hsay <message>
```
**Permission:** `@css/chat`
**Features:**
- Message appears on screen overlay
- Less intrusive than center say
- Stays visible longer
**Examples:**
```bash
css_hsay "Tournament starting soon!"
css_hsay "New map voting available"
css_hsay "Visit our website: example.com"
```
---
## Usage Examples
### Announcements
```bash
# Server restart warning
css_cssay "Server will restart in 10 minutes! Save your progress!"
# Center screen countdown
css_csay "5"
# Wait...
css_csay "4"
css_csay "3"
css_csay "2"
css_csay "1"
css_csay "GO!"
```
### Player Communication
```bash
# Private warning
css_psay PlayerName "This is your first warning for chat spam"
# Public announcement
css_say "Everyone please be quiet for the next round"
# Admin coordination
css_asay "I'm spectating the suspicious player in T spawn"
```
### Event Management
```bash
# Tournament announcement
css_cssay "⚠ TOURNAMENT STARTING IN 5 MINUTES ⚠"
css_hsay "Teams, please get ready!"
css_csay "TOURNAMENT BEGINS NOW!"
```
---
## Color Codes
Many chat commands support color codes:
```
{default} - Default chat color
{white} - White
{darkred} - Dark red
{green} - Green
{lightyellow} - Light yellow
{lightblue} - Light blue
{olive} - Olive
{lime} - Lime
{red} - Red
{purple} - Purple
{grey} - Grey
{yellow} - Yellow
{gold} - Gold
{silver} - Silver
{blue} - Blue
{darkblue} - Dark blue
{bluegrey} - Blue grey
{magenta} - Magenta
{lightred} - Light red
{orange} - Orange
```
**Example:**
```bash
css_say "{red}WARNING: {default}No camping in spawn!"
```
---
## Message Targeting
Some commands support player targeting for private messages:
### Supported Targets
- `@all` - All players (private message to each)
- `@ct` - All Counter-Terrorists
- `@t` - All Terrorists
- `@spec` - All spectators
- `#123` - Specific userid
- `PlayerName` - Player by name
**Examples:**
```bash
# Private message to all CTs
css_psay @ct "Defend bombsite A this round"
# Private message to all terrorists
css_psay @t "Rush B with smoke and flash"
# Message to spectators
css_psay @spec "Type !join to play"
```
---
## Best Practices
### When to Use Each Command
**css_asay** (Admin Say):
- Admin coordination
- Discussing player behavior
- Planning admin actions
- Private admin discussions
**css_cssay** (Colored Say):
- Important server announcements
- Event notifications
- Eye-catching messages
- Server information
**css_say** (Say):
- General admin announcements
- Rule reminders
- Warnings to all players
- Admin presence
**css_psay** (Private Say):
- Private warnings
- Individual help
- Direct player communication
- Discretion needed
**css_csay** (Center Say):
- Emergency announcements
- Cannot-miss messages
- Round starts/events
- Countdowns
**css_hsay** (HUD Say):
- Persistent information
- Less urgent announcements
- Server info
- Website/Discord links
---
## Communication Guidelines
### Professional Communication
1. **Be clear and concise** - Don't spam long messages
2. **Use appropriate command** - Don't center-spam trivial messages
3. **Check for typos** - You represent the server
4. **Avoid excessive colors** - Can be hard to read
### Spam Prevention
1. **Don't overuse center say** - Very intrusive for players
2. **Space out announcements** - Don't flood chat
3. **Use HUD say for persistent info** - Less annoying
4. **Coordinate with other admins** - Avoid duplicate messages
### Effective Messaging
**Good Examples:**
```bash
css_cssay "🎯 New map voting system available! Type !mapvote"
css_psay PlayerName "Hey! Please enable your mic or use team chat"
css_asay "Checking player demos for possible aimbot"
```
**Poor Examples:**
```bash
css_csay "hi" # Don't use center say for trivial messages
css_cssay "a" "b" "c" "d" # Don't spam center messages
css_say "asdfasdfasdf" # Unprofessional
```
---
## Silent Mode
Admins can use silent mode to hide their activity:
```bash
css_hide # Toggle silent mode
```
When in silent mode:
- Chat messages still work
- Admin name might be hidden (depends on config)
- Useful for undercover moderation
---
## Configuration
### Show Activity Type
Controls how admin actions are displayed:
```json
"ShowActivityType": 2
```
**Options:**
- `0` - Hide all admin activity
- `1` - Anonymous ("An admin says...")
- `2` - Show name ("AdminName says...")
---
## Chat Restrictions
### Respecting Gags
Remember that:
- `css_asay` works even if admin is gagged (admin chat)
- Other commands respect communication penalties
- Can't use chat commands while silenced
### Permission Requirements
All chat commands require `@css/chat` permission:
```bash
css_addadmin STEAMID "Name" "@css/chat" 50 0
```
Or add to a group with chat permission:
```bash
css_addgroup "#moderators" "@css/chat,@css/kick" 50
```
---
## Troubleshooting
### Messages not showing
**Check:**
- Do you have `@css/chat` permission?
- Are you silenced/gagged?
- Check console for errors
- Verify player is connected (for css_psay)
### Colors not working
**Check:**
- Use correct color code syntax: `{red}`
- Some commands may not support colors
- Different chat systems handle colors differently
### Players can't see center/HUD messages
**Check:**
- CS2 client-side chat settings
- Conflicting HUD plugins
- Server console for errors
---
## Related Commands
- **[Communication Commands](basecomms)** - Gag, mute, silence players
- **[Base Commands](basecommands)** - Admin management
- **[Ban Commands](basebans)** - Player punishment

View File

@@ -0,0 +1,626 @@
---
sidebar_position: 3
---
# Base Commands
Core admin commands for server management and admin system.
## Player Information
### Show Penalties
View your own active penalties.
```bash
css_penalties
css_mypenalties
css_comms
```
**Permission:** None (all players)
**Shows:**
- Active bans
- Active communication restrictions (gag, mute, silence)
- Warning count
- Duration remaining
---
### Hide Penalty Notifications
Hide penalty notifications when you connect to the server.
```bash
css_hidecomms
```
**Permission:** `@css/kick`
**Notes:**
- Toggle on/off
- Admins won't see penalty notifications on join
- Useful for admin privacy
---
## Admin Menu
### Open Admin Menu
Opens the main admin menu interface.
```bash
css_admin
```
**Permission:** `@css/generic`
**Features:**
- Player management
- Server management
- Ban/kick/mute players via menu
- Map changing
- Custom server commands
---
### Admin Help
Print the admin help file.
```bash
css_adminhelp
```
**Permission:** `@css/generic`
**Shows:**
- Available commands for your permission level
- Command syntax
- Permission requirements
---
## Admin Management
### Add Admin
Add a new admin to the database.
```bash
css_addadmin <steamid> <name> <flags/groups> <immunity> <duration>
```
**Permission:** `@css/root`
**Parameters:**
- `steamid` - Player's SteamID (any format)
- `name` - Admin name (for identification)
- `flags/groups` - Permission flags or group name
- `immunity` - Immunity level (0-100, higher = more protection)
- `duration` - Duration in minutes (0 = permanent)
**Examples:**
```bash
# Add permanent admin with root access
css_addadmin 76561198012345678 "AdminName" "@css/root" 99 0
# Add moderator for 30 days
css_addadmin STEAM_1:0:12345678 "ModName" "@css/kick,@css/ban" 50 43200
# Add admin using group
css_addadmin 76561198012345678 "AdminName" "#moderators" 60 0
# Add admin to all servers (-g flag)
css_addadmin 76561198012345678 "AdminName" "@css/root" 99 0 -g
```
**Flags:**
- `-g` - Add to all servers (global admin)
---
### Delete Admin
Remove an admin from the database.
```bash
css_deladmin <steamid>
```
**Permission:** `@css/root`
**Examples:**
```bash
# Remove admin from current server
css_deladmin 76561198012345678
# Remove admin from all servers (-g flag)
css_deladmin 76561198012345678 -g
```
---
### Add Admin Group
Create a new admin group.
```bash
css_addgroup <group_name> <flags> <immunity>
```
**Permission:** `@css/root`
**Parameters:**
- `group_name` - Name of the group (e.g., "#moderators")
- `flags` - Permission flags for the group
- `immunity` - Default immunity level for group members
**Examples:**
```bash
# Create moderator group
css_addgroup "#moderators" "@css/kick,@css/ban,@css/chat" 50
# Create VIP group
css_addgroup "#vip" "@css/vip" 10
# Create global group (-g flag)
css_addgroup "#owner" "@css/root" 99 -g
```
**Flags:**
- `-g` - Create group on all servers
---
### Delete Admin Group
Remove an admin group from the database.
```bash
css_delgroup <group_name>
```
**Permission:** `@css/root`
**Examples:**
```bash
# Delete group from current server
css_delgroup "#moderators"
# Delete group from all servers (-g flag)
css_delgroup "#moderators" -g
```
---
### Reload Admins
Reload admin permissions from the database.
```bash
css_reloadadmins
```
**Permission:** `@css/root`
**When to use:**
- After adding/removing admins via database
- After modifying admin permissions
- After group changes
- Troubleshooting permission issues
**Note:** Admins are automatically reloaded periodically and on map change (if configured).
---
## Player Information
### Hide in Scoreboard
Toggle admin stealth mode (hide from scoreboard).
```bash
css_hide
css_stealth
```
**Permission:** `@css/kick`
**Features:**
- Hides you from the scoreboard
- Makes admin actions anonymous
- Useful for undercover moderation
---
### Who is This Player
Show detailed information about a player.
```bash
css_who <#userid or name>
```
**Permission:** `@css/generic`
**Shows:**
- Player name and SteamID
- IP address (if you have `@css/showip` permission)
- Connection time
- Active penalties
- Warning count
- Ban history
**Examples:**
```bash
css_who #123
css_who PlayerName
css_who @me
```
---
### Show Disconnected Players
Show recently disconnected players.
```bash
css_disconnected
css_last
css_last10 # Show last 10 (config value)
```
**Permission:** `@css/kick`
**Shows:**
- Player name
- SteamID
- Disconnect time
- Disconnect reason
**Configuration:**
```json
"DisconnectedPlayersHistoryCount": 10
```
---
### Show Warns for Player
Open warn list for a specific player.
```bash
css_warns <#userid or name>
```
**Permission:** `@css/kick`
**Shows:**
- All warnings for the player
- Warning reasons
- Admins who issued warnings
- Warning timestamps
- Total warning count
**Examples:**
```bash
css_warns #123
css_warns PlayerName
```
---
### Show Online Players
Show information about all online players.
```bash
css_players
```
**Permission:** `@css/generic`
**Shows:**
- List of all connected players
- UserIDs
- Names
- Teams
- Connection status
---
## Server Management
### Kick Player
Kick a player from the server.
```bash
css_kick <#userid or name> [reason]
```
**Permission:** `@css/kick`
**Examples:**
```bash
css_kick #123 "AFK"
css_kick PlayerName "Rule violation"
css_kick @spec "Cleaning spectators"
```
**Configuration:**
```json
"KickTime": 5
```
Delay in seconds before kicking (allows player to see the reason).
---
### Change Map
Change to a different map.
```bash
css_map <mapname>
css_changemap <mapname>
```
**Permission:** `@css/changemap`
**Examples:**
```bash
css_map de_dust2
css_changemap de_mirage
```
**Configuration:**
```json
"DefaultMaps": [
"de_dust2",
"de_mirage",
"de_inferno"
]
```
Maps in this list appear in the map change menu.
---
### Change Workshop Map
Change to a workshop map by ID or name.
```bash
css_wsmap <name or id>
css_changewsmap <name or id>
css_workshop <name or id>
```
**Permission:** `@css/changemap`
**Examples:**
```bash
css_wsmap 123456789
css_wsmap aim_map
```
**Configuration:**
```json
"WorkshopMaps": {
"aim_map": "123456789",
"surf_map": "987654321"
}
```
Maps configured here can be changed by name instead of ID.
---
### Change CVar
Change a server console variable.
```bash
css_cvar <cvar> <value>
```
**Permission:** `@css/cvar`
**Examples:**
```bash
css_cvar sv_cheats 1
css_cvar mp_roundtime 5
css_cvar mp_maxmoney 16000
```
**Warning:** This is a powerful command. Only grant to trusted admins.
---
### Execute RCON Command
Execute any command as the server.
```bash
css_rcon <command>
```
**Permission:** `@css/rcon`
**Examples:**
```bash
css_rcon status
css_rcon changelevel de_dust2
css_rcon sv_cheats 1
```
**Warning:** Extremely powerful command. Only grant to server owners.
**Configuration:**
```json
"DisableDangerousCommands": true
```
When enabled, prevents execution of dangerous commands via css_rcon.
---
### Restart Game
Restart the current game/round.
```bash
css_rr
css_rg
css_restart
css_restartgame
```
**Permission:** `@css/generic`
**Notes:**
- Restarts the current round
- Score is reset
- Players remain connected
---
## Permission Flags
Common permission flags used in CS2-SimpleAdmin:
| Flag | Description | Common Use |
|------|-------------|------------|
| `@css/generic` | Generic admin access | Basic admin menu, info commands |
| `@css/chat` | Chat management | Gag, mute, silence |
| `@css/kick` | Kick players | Kick, warnings, player info |
| `@css/ban` | Ban players | Ban, banip, addban |
| `@css/unban` | Unban players | Remove bans |
| `@css/permban` | Permanent bans | Issue permanent bans |
| `@css/changemap` | Change maps | Map changing |
| `@css/cvar` | Change cvars | Server variable modification |
| `@css/rcon` | Execute rcon | Full server control |
| `@css/root` | Root access | All permissions, admin management |
| `@css/slay` | Slay/respawn | Player manipulation |
| `@css/cheats` | Cheat commands | God mode, noclip, give weapons |
| `@css/showip` | View IPs | See player IP addresses |
---
## Immunity System
Immunity prevents lower-level admins from targeting higher-level admins.
**How it works:**
- Each admin has an immunity value (0-100)
- Higher immunity = more protection
- Admins can only target players with lower immunity
**Example:**
- Admin A has immunity 50
- Admin B has immunity 30
- Admin A can ban Admin B
- Admin B cannot ban Admin A
**Best Practice:**
- Owner: 99
- Senior admins: 80-90
- Moderators: 50-70
- Trial mods: 20-40
- Regular players: 0
---
## Configuration Options
### Reload Admins on Map Change
```json
"ReloadAdminsEveryMapChange": false
```
**Options:**
- `true` - Reload admin permissions every map change
- `false` - Only reload when explicitly requested (better performance)
### Show Activity Type
```json
"ShowActivityType": 2
```
**Options:**
- `0` - Hide all admin activity
- `1` - Show activity anonymously ("An admin banned PlayerName")
- `2` - Show admin name ("AdminName banned PlayerName")
---
## Best Practices
### Admin Management
1. **Use groups** - Easier to manage than individual permissions
2. **Set appropriate immunity** - Prevent abuse
3. **Time-limited admin** - For trial moderators
4. **Document changes** - Keep track of who has what permissions
### Permission Assignment
**Recommended hierarchy:**
```
Root (@css/root, immunity 99):
- Server owners only
Senior Admin (@css/ban,@css/kick,@css/chat,@css/changemap, immunity 80):
- Trusted long-term admins
Moderator (@css/kick,@css/chat, immunity 50):
- Regular moderators
Trial Mod (@css/kick, immunity 20):
- New moderators on probation
```
### Security
1. **Limit @css/rcon** - Only to server owner
2. **Limit @css/cvar** - Only to senior admins
3. **Monitor admin actions** - Review logs regularly
4. **Use time-limited admin** - For temporary staff
---
## Troubleshooting
### Admin permissions not working
**Check:**
1. Is admin correctly added with `css_addadmin`?
2. Run `css_reloadadmins`
3. Check database connection
4. Verify SteamID format
### Can't target another admin
**Check:**
- Your immunity level vs target's immunity
- You need equal or higher immunity to target
### Commands not available
**Check:**
- Your permission flags
- Commands.json for disabled commands
- Server console for errors
---
## Related Commands
- **[Ban Commands](basebans)** - Player punishment
- **[Communication Commands](basecomms)** - Chat/voice management
- **[Player Commands](playercommands)** - Player manipulation

View File

@@ -0,0 +1,396 @@
---
sidebar_position: 2
---
# Communication Commands
Commands for managing player communication (voice and text chat).
## Overview
CS2-SimpleAdmin provides three types of communication restrictions:
- **Gag** - Blocks text chat only
- **Mute** - Blocks voice chat only
- **Silence** - Blocks both text and voice chat
---
## Gag Commands
### Gag Player
Prevent a player from using text chat.
```bash
css_gag <#userid or name> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_gag #123 30 "Chat spam"
css_gag PlayerName 1440 "Advertising"
css_gag @all 5 "Everyone quiet for 5 minutes"
```
### Add Gag (Offline Player)
Gag a player by SteamID even if they're offline.
```bash
css_addgag <steamid> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_addgag 76561198012345678 60 "Chat abuse"
css_addgag STEAM_1:0:12345678 1440 "Spam"
```
### Ungag Player
Remove a gag from a player.
```bash
css_ungag <steamid or name> [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_ungag PlayerName "Appeal accepted"
css_ungag 76561198012345678 "Mistake"
```
---
## Mute Commands
### Mute Player
Prevent a player from using voice chat.
```bash
css_mute <#userid or name> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_mute #123 30 "Mic spam"
css_mute PlayerName 60 "Loud music"
css_mute @t 5 "T team timeout"
```
### Add Mute (Offline Player)
Mute a player by SteamID even if they're offline.
```bash
css_addmute <steamid> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_addmute 76561198012345678 120 "Voice abuse"
css_addmute STEAM_1:0:12345678 1440 "Mic spam"
```
### Unmute Player
Remove a mute from a player.
```bash
css_unmute <steamid or name> [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_unmute PlayerName "Behavior improved"
css_unmute 76561198012345678 "Time served"
```
---
## Silence Commands
### Silence Player
Block both text and voice chat from a player.
```bash
css_silence <#userid or name> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_silence #123 60 "Complete communication ban"
css_silence PlayerName 1440 "Severe abuse"
```
### Add Silence (Offline Player)
Silence a player by SteamID even if they're offline.
```bash
css_addsilence <steamid> [time in minutes/0 perm] [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_addsilence 76561198012345678 120 "Total communication ban"
css_addsilence STEAM_1:0:12345678 0 "Permanent silence"
```
### Unsilence Player
Remove a silence from a player.
```bash
css_unsilence <steamid or name> [reason]
```
**Permission:** `@css/chat`
**Examples:**
```bash
css_unsilence PlayerName "Punishment complete"
css_unsilence 76561198012345678 "Appeal granted"
```
---
## Permission Requirements
All communication commands require the `@css/chat` permission.
| Command | Action | Offline Support |
|---------|--------|----------------|
| `css_gag` | Block text chat | No |
| `css_addgag` | Block text chat | Yes |
| `css_ungag` | Remove text block | Yes |
| `css_mute` | Block voice chat | No |
| `css_addmute` | Block voice chat | Yes |
| `css_unmute` | Remove voice block | Yes |
| `css_silence` | Block both | No |
| `css_addsilence` | Block both | Yes |
| `css_unsilence` | Remove both blocks | Yes |
---
## Communication Penalty Types
### When to Use Each Type
**Gag (Text Only):**
- Chat spam
- Advertising in chat
- Offensive messages
- Spectator camera abuse messages
**Mute (Voice Only):**
- Mic spam
- Loud music/noise
- Voice abuse
- Excessive talking
**Silence (Both):**
- Severe abuse cases
- Players who switch between chat and voice to evade
- Complete communication bans
---
## Configuration Options
### UserMessage Gag Type
In `CS2-SimpleAdmin.json`:
```json
"UserMessageGagChatType": false
```
**Options:**
- `false` - Standard gag implementation (default)
- `true` - Alternative gag using UserMessage system
**Note:** Try switching this if gag commands don't work as expected.
### Notify Penalties on Connect
```json
"NotifyPenaltiesToAdminOnConnect": true
```
When enabled, admins see active communication penalties when they join:
```
[CS2-SimpleAdmin] PlayerName is gagged (30 minutes remaining)
[CS2-SimpleAdmin] PlayerName is muted (1 hour remaining)
```
---
## Checking Penalties
### View Own Penalties
Players can check their own communication penalties:
```bash
css_penalties
css_mypenalties
css_comms
```
Shows:
- Active gags, mutes, and silences
- Duration remaining
- Reason for penalty
- Admin who issued it
### Admin View of Penalties
Use the admin menu or player info command:
```bash
css_who <#userid or name>
```
Shows complete penalty history including communication restrictions.
---
## Time Durations
Common duration values:
| Duration | Minutes | Use Case |
|----------|---------|----------|
| 1 minute | 1 | Quick warning |
| 5 minutes | 5 | Minor spam |
| 15 minutes | 15 | Standard timeout |
| 30 minutes | 30 | Repeated offense |
| 1 hour | 60 | Moderate abuse |
| 6 hours | 360 | Serious abuse |
| 1 day | 1440 | Severe abuse |
| 1 week | 10080 | Extreme cases |
| Permanent | 0 | Reserved for worst cases |
---
## Player Targeting
All communication commands support advanced targeting:
- `@all` - Target all players
- `@ct` - Target all Counter-Terrorists
- `@t` - Target all Terrorists
- `@spec` - Target all spectators
- `#123` - Target by userid
- `PlayerName` - Target by name
**Examples:**
```bash
css_gag @all 1 "Quiet for one minute"
css_mute @t 5 "T team voice timeout"
css_silence @ct 10 "CT team complete silence"
```
---
## Best Practices
### Communication Management
1. **Start with warnings** - Not all chat issues need immediate gag
2. **Use appropriate durations** - Match severity to punishment
3. **Provide reasons** - Helps players understand what they did wrong
4. **Consider silence carefully** - Complete communication ban is harsh
### Gag vs Mute vs Silence
**Progressive Approach:**
1. Verbal warning
2. Gag or mute (specific to offense)
3. Longer gag/mute for repeat offense
4. Silence for continued abuse
5. Temporary ban for extreme cases
### Documentation
1. **Always provide reasons** - Required for appeals
2. **Be specific** - "Mic spam" not just "abuse"
3. **Keep records** - Use admin logs for repeat offenders
---
## Discord Integration
Communication penalties can send Discord notifications when configured:
```json
"DiscordPenaltyGagSettings": [...],
"DiscordPenaltyMuteSettings": [...],
"DiscordPenaltySilenceSettings": [...]
```
Notifications include:
- Player name and SteamID
- Penalty type and duration
- Reason provided
- Admin who issued it
---
## Troubleshooting
### Gag doesn't work
**Try:**
1. Switch `UserMessageGagChatType` in config
2. Ensure player is actually gagged (check with `css_who`)
3. Check for conflicting plugins
### Mute doesn't block voice
**Check:**
- Is sv_talk_enemy_dead configured correctly?
- Are there voice management plugins conflicting?
- Check server console for errors
### Penalties not persistent across maps
**Solution:**
- Penalties should persist automatically
- Check database connection
- Verify MultiServerMode if using multiple servers
### Player can't see their penalties
**Check:**
- Command aliases in Commands.json
- Ensure `css_penalties` is enabled
- Check player chat permissions
---
## Related Commands
- **[Ban Commands](basebans)** - For more serious offenses
- **[Player Commands](playercommands)** - Kick, team switch
- **[Base Commands](basecommands)** - Admin management

View File

@@ -0,0 +1,436 @@
---
sidebar_position: 6
---
# Vote Commands
Commands for creating polls and votes on your server.
## Create Vote
Create a custom poll for players to vote on.
```bash
css_vote <question> [option1] [option2] [option3] ...
```
**Permission:** `@css/generic`
**Parameters:**
- `question` - The question to ask players
- `option1, option2, ...` - Vote options (at least 2 required)
---
## Examples
### Simple Yes/No Vote
```bash
css_vote "Should we change map?" "Yes" "No"
```
**Player sees:**
```
Vote: Should we change map?
1. Yes
2. No
```
---
### Multiple Options
```bash
css_vote "Which map should we play next?" "de_dust2" "de_mirage" "de_inferno" "de_nuke"
```
**Player sees:**
```
Vote: Which map should we play next?
1. de_dust2
2. de_mirage
3. de_inferno
4. de_nuke
```
---
### Rule Vote
```bash
css_vote "Should we allow AWPs?" "Yes" "No" "Only one per team"
```
---
### Activity Vote
```bash
css_vote "What should we do?" "Surf" "Deathrun" "Competitive" "Fun Round"
```
---
## How Voting Works
### Player Participation
Players vote by:
1. Opening their chat
2. Typing the number of their choice
3. Or using vote menu (if available)
**Example:**
```
Player: 1 (votes for option 1)
Player: 2 (votes for option 2)
```
### Vote Duration
- Default vote time: ~30 seconds
- Vote timer shows on screen
- Results shown when vote ends
### Vote Results
After voting ends, results are displayed:
```
Vote Results:
1. Yes - 12 votes (60%)
2. No - 8 votes (40%)
Winner: Yes
```
---
## Use Cases
### Map Voting
```bash
css_vote "Next map?" "de_dust2" "de_mirage" "de_inferno"
```
### Rule Changes
```bash
css_vote "Enable friendly fire?" "Yes" "No"
css_vote "Restart round?" "Yes" "No"
```
### Player Punishment
```bash
css_vote "Ban PlayerName for cheating?" "Yes" "No"
css_vote "Kick AFK player?" "Yes" "No"
```
### Fun Rounds
```bash
css_vote "Fun round type?" "Knife only" "Deagle only" "Zeus only" "Normal"
```
### Server Settings
```bash
css_vote "Round time?" "2 minutes" "3 minutes" "5 minutes"
css_vote "Max players?" "10v10" "5v5" "7v7"
```
---
## Best Practices
### Question Clarity
**Good Questions:**
- Clear and concise
- Specific
- Easy to understand
**Examples:**
```bash
✅ css_vote "Change to de_dust2?" "Yes" "No"
❌ css_vote "Map?" "Yes" "No" # Unclear what map
✅ css_vote "Restart this round?" "Yes" "No"
❌ css_vote "Restart?" "Yes" "No" # Restart what?
```
### Option Limits
**Recommendations:**
- 2-5 options ideal
- Too many options confuse players
- Keep options brief
**Examples:**
```bash
✅ css_vote "Next map?" "dust2" "mirage" "inferno"
❌ css_vote "Next map?" "de_dust2" "de_mirage" "de_inferno" "de_nuke" "de_vertigo" "de_ancient" "de_anubis"
```
### Timing
**When to use votes:**
- End of round
- Between maps
- During downtime
- Not during active gameplay
**When NOT to use votes:**
- Mid-round
- During clutch situations
- Too frequently
### Vote Spam Prevention
Don't spam votes:
```bash
❌ Multiple votes in quick succession
❌ Overlapping votes
❌ Votes every round
```
Wait for current vote to finish before starting another.
---
## Vote Types
### Administrative Votes
**Map change:**
```bash
css_vote "Change map now?" "Yes" "No"
```
**Server restart:**
```bash
css_vote "Restart server?" "Yes" "No"
```
**Rule enforcement:**
```bash
css_vote "Kick PlayerName?" "Yes" "No"
```
### Gameplay Votes
**Weapon restrictions:**
```bash
css_vote "Disable AWP?" "Yes" "No"
```
**Team scramble:**
```bash
css_vote "Scramble teams?" "Yes" "No"
```
**Round rules:**
```bash
css_vote "Knife round first?" "Yes" "No"
```
### Event Votes
**Tournament:**
```bash
css_vote "Start tournament?" "Yes, start now" "Wait 5 minutes" "No, cancel"
```
**Custom game mode:**
```bash
css_vote "Game mode?" "Hide and Seek" "Gungame" "Surf" "Normal"
```
---
## Limitations
### Technical Limits
- Maximum ~10 options (depends on menu system)
- One vote at a time
- Requires active players to participate
### Permission Required
Only admins with `@css/generic` permission can start votes.
To grant permission:
```bash
css_addadmin STEAMID "Name" "@css/generic" 50 0
```
---
## Vote Results Handling
### Manual Enforcement
Votes don't automatically execute actions. Admins must:
1. **See the results**
2. **Manually execute the winning option**
**Example:**
```bash
# Start vote
css_vote "Change to de_dust2?" "Yes" "No"
# If "Yes" wins, manually change map
css_map de_dust2
```
### Why Manual?
- Prevents abuse
- Allows admin oversight
- Gives control over execution
---
## Advanced Usage
### Combining with Commands
Use votes to decide, then execute:
```bash
# Vote on map
css_vote "Next map?" "dust2" "mirage" "inferno"
# If dust2 wins:
css_map de_dust2
# Vote on player kick
css_vote "Kick PlayerName?" "Yes" "No"
# If Yes wins:
css_kick PlayerName "Voted to be kicked"
```
### Sequential Votes
Run multiple votes for complex decisions:
```bash
# First vote: Mode
css_vote "Game mode?" "Competitive" "Casual"
# If Competitive wins, second vote:
css_vote "Round time?" "2 min" "3 min" "5 min"
```
---
## Configuration
Check if vote commands are enabled in:
```
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/Commands.json
```
```json
{
"Commands": {
"css_vote": {
"Aliases": [
"css_vote"
]
}
}
}
```
To disable votes, remove all aliases:
```json
{
"Commands": {
"css_vote": {
"Aliases": []
}
}
}
```
---
## Troubleshooting
### Vote doesn't start
**Check:**
- Do you have `@css/generic` permission?
- Is command enabled in Commands.json?
- Are there at least 2 options?
### Players can't vote
**Check:**
- Vote menu is showing
- Players know how to vote (type number in chat)
- Vote hasn't already ended
### Vote results not showing
**Check:**
- Wait for vote to complete
- Check server console
- Ensure voting system is working
---
## Permission Requirements
| Command | Permission | Description |
|---------|------------|-------------|
| `css_vote` | `@css/generic` | Create votes/polls |
---
## Tips
### Effective Polling
1. **Ask clear questions** - No ambiguity
2. **Limit options** - 2-4 is ideal
3. **Time it right** - Between rounds
4. **Follow through** - Execute winning option
5. **Don't overuse** - Votes lose impact if spammed
### Community Engagement
Use votes to:
- Involve community in decisions
- Gauge player preferences
- Create democratic server atmosphere
- Get feedback on changes
### Example Scenarios
**New map test:**
```bash
css_vote "Try new map cs_office?" "Yes" "No"
```
**Event planning:**
```bash
css_vote "Tournament this weekend?" "Saturday" "Sunday" "No thanks"
```
**Rule feedback:**
```bash
css_vote "Keep no-AWP rule?" "Yes" "No" "Only limit to 2"
```
---
## Related Commands
- **[Base Commands](basecommands)** - Server management
- **[Chat Commands](basechat)** - Announcements
- **[Player Commands](playercommands)** - Player actions

View File

@@ -0,0 +1,516 @@
---
sidebar_position: 5
---
# Player Commands
Commands for managing and manipulating players on your server.
:::note
Many of these commands are included in the base plugin. For extended fun commands (god mode, noclip, freeze, etc.), see the [Fun Commands Module](../../modules/funcommands).
:::
## Player Management
### Slay Player
Kill a player instantly.
```bash
css_slay <#userid or name>
```
**Permission:** `@css/slay`
**Examples:**
```bash
css_slay #123
css_slay PlayerName
css_slay @ct # Slay all CTs
css_slay @t # Slay all terrorists
```
**Use cases:**
- Punishment for rule breaking
- Ending rounds quickly
- Removing camping players
---
### Slap Player
Slap a player, dealing damage and pushing them.
```bash
css_slap <#userid or name> [damage]
```
**Permission:** `@css/slay`
**Parameters:**
- `damage` - HP damage to deal (default: 0)
**Examples:**
```bash
css_slap #123 # Slap with no damage
css_slap PlayerName 10 # Slap for 10 HP damage
css_slap @all 5 # Slap everyone for 5 damage
```
**Effects:**
- Player is pushed in a random direction
- Optional damage dealt
- Makes slap sound
**Use cases:**
- Funny punishment
- Getting player attention
- Moving AFK players
---
## Player Attributes
### Set Player Health
Set a player's health points.
```bash
css_hp <#userid or name> <health>
```
**Permission:** `@css/slay`
**Examples:**
```bash
css_hp #123 100 # Set to full health
css_hp PlayerName 1 # Set to 1 HP
css_hp @ct 200 # Give all CTs 200 HP
```
**Valid range:** 1 - 999+
---
### Set Player Speed
Modify a player's movement speed.
```bash
css_speed <#userid or name> <speed>
```
**Permission:** `@css/slay`
**Parameters:**
- `speed` - Speed multiplier (1.0 = normal, 2.0 = double speed, 0.5 = half speed)
**Examples:**
```bash
css_speed #123 1.5 # 150% speed
css_speed PlayerName 0.5 # 50% speed (slow motion)
css_speed @all 2.0 # Double speed for everyone
css_speed #123 1.0 # Reset to normal speed
```
**Common values:**
- `0.5` - Slow motion
- `1.0` - Normal (default)
- `1.5` - Fast
- `2.0` - Very fast
- `3.0` - Extremely fast
**Note:** Speed persists across respawns until reset.
---
### Set Player Gravity
Modify a player's gravity.
```bash
css_gravity <#userid or name> <gravity>
```
**Permission:** `@css/slay`
**Parameters:**
- `gravity` - Gravity multiplier (1.0 = normal, 0.5 = moon jump, 2.0 = heavy)
**Examples:**
```bash
css_gravity #123 0.5 # Moon jump
css_gravity PlayerName 2.0 # Heavy gravity
css_gravity @all 0.1 # Super jump for everyone
css_gravity #123 1.0 # Reset to normal
```
**Common values:**
- `0.1` - Super high jumps
- `0.5` - Moon gravity
- `1.0` - Normal (default)
- `2.0` - Heavy/fast falling
**Note:** Gravity persists across respawns until reset.
---
### Set Player Money
Set a player's money amount.
```bash
css_money <#userid or name> <amount>
```
**Permission:** `@css/slay`
**Examples:**
```bash
css_money #123 16000 # Max money
css_money PlayerName 0 # Remove all money
css_money @ct 10000 # Give all CTs $10,000
```
**Valid range:** 0 - 65535 (CS2 engine limit)
---
## Team Management
### Switch Player Team
Move a player to a different team.
```bash
css_team <#userid or name> [ct/t/spec] [-k]
```
**Permission:** `@css/kick`
**Parameters:**
- `ct` - Counter-Terrorist team
- `t` - Terrorist team
- `spec` - Spectators
- `-k` - Kill player during switch (optional)
**Examples:**
```bash
css_team #123 ct # Move to CT
css_team PlayerName t # Move to T
css_team @spec t # Move all spectators to T
css_team #123 ct -k # Move to CT and kill
```
**Configuration:**
```json
"TeamSwitchType": 1
```
Determines team switch behavior.
---
### Rename Player
Temporarily rename a player.
```bash
css_rename <#userid or name> <new name>
```
**Permission:** `@css/kick`
**Examples:**
```bash
css_rename #123 "NewName"
css_rename PlayerName "RenamedPlayer"
css_rename @all "Everyone"
```
**Notes:**
- Rename is temporary (resets on reconnect)
- For permanent rename, use `css_prename`
---
### Permanent Rename
Permanently force a player's name.
```bash
css_prename <#userid or name> <new name>
```
**Permission:** `@css/ban`
**Examples:**
```bash
css_prename #123 "EnforcedName"
css_prename PlayerName "NewIdentity"
```
**Notes:**
- Name is enforced even after reconnect
- Stored in database
- Player cannot change it
- Useful for offensive names
---
## Weapon Management
### Give Weapon
Give a weapon to a player.
```bash
css_give <#userid or name> <weapon>
```
**Permission:** `@css/cheats`
**Weapon names:**
- Rifles: `ak47`, `m4a1`, `m4a1_silencer`, `aug`, `sg556`, `awp`
- SMGs: `mp5sd`, `mp7`, `mp9`, `p90`, `ump45`
- Heavy: `nova`, `xm1014`, `mag7`, `m249`, `negev`
- Pistols: `deagle`, `elite`, `fiveseven`, `glock`, `hkp2000`, `p250`, `tec9`, `usp_silencer`
- Grenades: `flashbang`, `hegrenade`, `smokegrenade`, `molotov`, `incgrenade`, `decoy`
- Equipment: `kevlar`, `assaultsuit`, `defuser`, `knife`
**Examples:**
```bash
css_give #123 awp
css_give PlayerName ak47
css_give @ct m4a1
css_give @all deagle
```
---
### Strip Weapons
Remove all weapons from a player.
```bash
css_strip <#userid or name>
```
**Permission:** `@css/slay`
**Examples:**
```bash
css_strip #123
css_strip PlayerName
css_strip @t # Disarm all terrorists
```
**Effects:**
- Removes all weapons
- Leaves player with knife only
- Removes grenades and equipment
---
## Teleportation
### Teleport to Player
Teleport yourself to another player.
```bash
css_tp <#userid or name>
css_tpto <#userid or name>
css_goto <#userid or name>
```
**Permission:** `@css/kick`
**Examples:**
```bash
css_tp #123
css_goto PlayerName
```
**Use cases:**
- Checking player behavior
- Admin help
- Spectating suspicious players
---
### Teleport Player to You
Bring a player to your location.
```bash
css_bring <#userid or name>
css_tphere <#userid or name>
```
**Permission:** `@css/kick`
**Examples:**
```bash
css_bring #123
css_tphere PlayerName
css_bring @all # Bring everyone to you
```
**Use cases:**
- Moving stuck players
- Gathering players
- Admin events
---
### Respawn Player
Respawn a dead player.
```bash
css_respawn <#userid or name>
```
**Permission:** `@css/cheats`
**Examples:**
```bash
css_respawn #123
css_respawn PlayerName
css_respawn @ct # Respawn all dead CTs
```
**Notes:**
- Player spawns at spawn point
- Equipped with default weapons
- Can break competitive balance
---
## Player Targeting
All player commands support advanced targeting:
### Target Syntax
- `@all` - All players
- `@ct` - All Counter-Terrorists
- `@t` - All Terrorists
- `@spec` - All spectators
- `@alive` - All alive players
- `@dead` - All dead players
- `@bot` - All bots
- `@human` - All human players
- `@me` - Yourself
- `#123` - Specific user ID
- `PlayerName` - By name (partial match supported)
### Multiple Targets
```bash
css_slay @ct # Kills all CTs
css_hp @all 200 # Give everyone 200 HP
css_speed @t 2.0 # Make all Ts fast
```
---
## Permission Requirements
| Command | Required Permission | Description |
|---------|-------------------|-------------|
| `css_slay` | `@css/slay` | Kill players |
| `css_slap` | `@css/slay` | Slap players |
| `css_hp` | `@css/slay` | Set health |
| `css_speed` | `@css/slay` | Modify speed |
| `css_gravity` | `@css/slay` | Modify gravity |
| `css_money` | `@css/slay` | Set money |
| `css_team` | `@css/kick` | Change team |
| `css_rename` | `@css/kick` | Temporary rename |
| `css_prename` | `@css/ban` | Permanent rename |
| `css_give` | `@css/cheats` | Give weapons |
| `css_strip` | `@css/slay` | Remove weapons |
| `css_tp` | `@css/kick` | Teleport to player |
| `css_bring` | `@css/kick` | Bring player |
| `css_respawn` | `@css/cheats` | Respawn players |
---
## Best Practices
### Punishment Commands
**Slay:**
- Use for rule violations
- Better than kick for minor issues
- Allows player to stay and learn
**Slap:**
- Lighter punishment
- Good for warnings
- Can be funny/entertaining
### Gameplay Modification
**HP/Speed/Gravity:**
- Use for events/fun rounds
- Don't abuse during competitive play
- Reset to normal after use
**Respawn:**
- Very disruptive to gameplay
- Use sparingly
- Good for fixing bugs/mistakes
### Team Management
**Team switching:**
- Balance teams fairly
- Don't abuse for winning
- Use `-k` flag for competitive integrity
---
## Configuration
### Team Switch Behavior
```json
"TeamSwitchType": 1
```
Controls how team switching works.
---
## Troubleshooting
### Speed/Gravity not persisting
**Solution:** These are maintained by a timer. If they reset:
- Check server console for errors
- Ensure plugin is loaded correctly
- Try reapplying the modification
### Can't teleport
**Check:**
- Target player is connected
- You have correct permissions
- Both players are valid
### Give weapon not working
**Check:**
- Weapon name is correct
- Player is alive
- Player has inventory space
---
## Related Commands
- **[Fun Commands Module](../../modules/funcommands)** - Extended fun commands (freeze, god mode, noclip)
- **[Ban Commands](basebans)** - Punishment commands
- **[Base Commands](basecommands)** - Server management

View File

@@ -0,0 +1,397 @@
---
sidebar_position: 3
---
# Configuration
Learn how to configure CS2-SimpleAdmin to suit your server's needs.
## Configuration File Location
The main configuration file is located at:
```
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json
```
## Configuration Structure
The configuration file is divided into several sections:
### Database Configuration
Configure your database connection:
```json
"DatabaseConfig": {
"DatabaseType": "SQLite",
"SqliteFilePath": "cs2-simpleadmin.sqlite",
"DatabaseHost": "",
"DatabasePort": 3306,
"DatabaseUser": "",
"DatabasePassword": "",
"DatabaseName": "",
"DatabaseSSlMode": "preferred"
}
```
**Database Types:**
- `SQLite` - Local database file (good for single server)
- `MySQL` - MySQL/MariaDB server (required for multi-server setups)
**MySQL Example:**
```json
"DatabaseConfig": {
"DatabaseType": "MySQL",
"DatabaseHost": "localhost",
"DatabasePort": 3306,
"DatabaseUser": "cs2admin",
"DatabasePassword": "your_password",
"DatabaseName": "cs2_simpleadmin",
"DatabaseSSlMode": "preferred"
}
```
### Other Settings
General plugin settings:
```json
"OtherSettings": {
"ShowActivityType": 2,
"TeamSwitchType": 1,
"KickTime": 5,
"BanType": 1,
"TimeMode": 1,
"DisableDangerousCommands": true,
"MaxBanDuration": 10080,
"MaxMuteDuration": 10080,
"ExpireOldIpBans": 0,
"ReloadAdminsEveryMapChange": false,
"DisconnectedPlayersHistoryCount": 10,
"NotifyPenaltiesToAdminOnConnect": true,
"ShowBanMenuIfNoTime": true,
"UserMessageGagChatType": false,
"CheckMultiAccountsByIp": true,
"AdditionalCommandsToLog": [],
"IgnoredIps": []
}
```
**Settings Explained:**
| Setting | Description | Default |
|---------|-------------|---------|
| `ShowActivityType` | How to display admin actions (0=hide, 1=anonymous, 2=show name) | 2 |
| `TeamSwitchType` | Team switch behavior | 1 |
| `KickTime` | Delay before kicking player (seconds) | 5 |
| `BanType` | Ban type (1=SteamID, 2=IP, 3=Both) | 1 |
| `TimeMode` | Time display mode | 1 |
| `DisableDangerousCommands` | Disable potentially dangerous commands | true |
| `MaxBanDuration` | Maximum ban duration in minutes (0=unlimited) | 10080 |
| `MaxMuteDuration` | Maximum mute duration in minutes (0=unlimited) | 10080 |
| `ExpireOldIpBans` | Auto-expire IP bans after X days (0=disabled) | 0 |
| `ReloadAdminsEveryMapChange` | Reload admin permissions on map change | false |
| `DisconnectedPlayersHistoryCount` | Number of disconnected players to track | 10 |
| `NotifyPenaltiesToAdminOnConnect` | Show penalties to admins when they connect | true |
| `ShowBanMenuIfNoTime` | Show ban menu even without time parameter | true |
| `UserMessageGagChatType` | Use UserMessage for gag (alternative chat blocking) | false |
| `CheckMultiAccountsByIp` | Detect multiple accounts from same IP | true |
| `AdditionalCommandsToLog` | Array of additional commands to log | [] |
| `IgnoredIps` | IPs to ignore in multi-account detection | [] |
### Metrics and Updates
```json
"EnableMetrics": true,
"EnableUpdateCheck": true
```
- `EnableMetrics` - Send anonymous usage statistics
- `EnableUpdateCheck` - Check for plugin updates on load
### Timezone
Set your server's timezone for accurate timestamps:
```json
"Timezone": "UTC"
```
See the [list of timezones](#timezone-list) below.
### Warning Thresholds
Configure automatic actions when players reach warning thresholds:
```json
"WarnThreshold": {
"998": "css_addban STEAMID64 60 \"3/4 Warn\"",
"999": "css_ban #USERID 120 \"4/4 Warn\""
}
```
**Example:** Automatically ban a player for 60 minutes when they receive their 3rd warning.
### Multi-Server Mode
Enable if you're running multiple servers with a shared database:
```json
"MultiServerMode": true
```
When enabled:
- Bans are shared across all servers
- Admin permissions can be global or server-specific
- Player data is synchronized
### Discord Integration
Send notifications to Discord webhooks:
```json
"Discord": {
"DiscordLogWebhook": "https://discord.com/api/webhooks/...",
"DiscordPenaltyBanSettings": [...],
"DiscordPenaltyMuteSettings": [...],
"DiscordPenaltyGagSettings": [...],
"DiscordPenaltySilenceSettings": [...],
"DiscordPenaltyWarnSettings": [...],
"DiscordAssociatedAccountsSettings": [...]
}
```
**Webhook Settings:**
Each penalty type can have its own webhook configuration:
```json
"DiscordPenaltyBanSettings": [
{
"name": "Color",
"value": "#FF0000"
},
{
"name": "Webhook",
"value": "https://discord.com/api/webhooks/YOUR_WEBHOOK_HERE"
},
{
"name": "ThumbnailUrl",
"value": "https://example.com/ban-icon.png"
},
{
"name": "ImageUrl",
"value": ""
},
{
"name": "Footer",
"value": "CS2-SimpleAdmin"
},
{
"name": "Time",
"value": "{relative}"
}
]
```
**Available Placeholders:**
- `{relative}` - Relative timestamp
- `{fixed}` - Fixed timestamp
### Map Configuration
Configure default maps and workshop maps:
```json
"DefaultMaps": [
"de_dust2",
"de_mirage",
"de_inferno"
],
"WorkshopMaps": {
"aim_map": "123456789",
"surf_map": "987654321"
}
```
### Custom Server Commands
Add custom commands to the admin menu:
```json
"CustomServerCommands": [
{
"Flag": "@css/root",
"DisplayName": "Reload Admins",
"Command": "css_reloadadmins"
},
{
"Flag": "@css/cheats",
"DisplayName": "Enable sv_cheats",
"Command": "sv_cheats 1"
}
]
```
### Menu Configuration
Configure menu appearance and options:
```json
"MenuConfig": {
"MenuType": "selectable",
"Durations": [
{ "name": "1 minute", "duration": 1 },
{ "name": "5 minutes", "duration": 5 },
{ "name": "15 minutes", "duration": 15 },
{ "name": "1 hour", "duration": 60 },
{ "name": "1 day", "duration": 1440 },
{ "name": "7 days", "duration": 10080 },
{ "name": "14 days", "duration": 20160 },
{ "name": "30 days", "duration": 43200 },
{ "name": "Permanent", "duration": 0 }
],
"BanReasons": [
"Hacking",
"Voice Abuse",
"Chat Abuse",
"Admin disrespect",
"Other"
],
"KickReasons": [
"Voice Abuse",
"Chat Abuse",
"Admin disrespect",
"Other"
],
"WarnReasons": [
"Voice Abuse",
"Chat Abuse",
"Admin disrespect",
"Other"
],
"MuteReasons": [
"Advertising",
"Spamming",
"Spectator camera abuse",
"Hate",
"Admin disrespect",
"Other"
],
"AdminFlags": [
{ "name": "Generic", "flag": "@css/generic" },
{ "name": "Chat", "flag": "@css/chat" },
{ "name": "Change Map", "flag": "@css/changemap" },
{ "name": "Slay", "flag": "@css/slay" },
{ "name": "Kick", "flag": "@css/kick" },
{ "name": "Ban", "flag": "@css/ban" },
{ "name": "Perm Ban", "flag": "@css/permban" },
{ "name": "Unban", "flag": "@css/unban" },
{ "name": "Show IP", "flag": "@css/showip" },
{ "name": "Cvar", "flag": "@css/cvar" },
{ "name": "Rcon", "flag": "@css/rcon" },
{ "name": "Root (all flags)", "flag": "@css/root" }
]
}
```
## Timezone List
<details>
<summary>Click to expand timezone list</summary>
```
UTC
America/New_York
America/Chicago
America/Denver
America/Los_Angeles
Europe/London
Europe/Paris
Europe/Berlin
Europe/Warsaw
Europe/Moscow
Asia/Tokyo
Asia/Shanghai
Asia/Dubai
Australia/Sydney
Pacific/Auckland
... (and many more)
```
For a complete list, see the info.txt file in the documentation folder.
</details>
## Commands Configuration
You can customize command aliases in:
```
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/Commands.json
```
Example:
```json
{
"Commands": {
"css_ban": {
"Aliases": [
"css_ban",
"css_ban2"
]
}
}
}
```
This allows you to:
- **Disable commands** - Remove all aliases from the array
- **Add aliases** - Add multiple command variations
- **Rename commands** - Change the command name while keeping functionality
## Best Practices
### Security
1. **Use MySQL in production** - SQLite is not suitable for multi-server setups
2. **Set MaxBanDuration** - Prevent accidental permanent bans
3. **Enable DisableDangerousCommands** - Protect against accidental server crashes
4. **Use strong database passwords** - If using MySQL
### Performance
1. **Set ReloadAdminsEveryMapChange to false** - Unless you frequently modify admin permissions
2. **Limit DisconnectedPlayersHistoryCount** - Reduce memory usage
3. **Use database indices** - Migrations create these automatically
### Multi-Server Setup
1. **Enable MultiServerMode** - Share data across servers
2. **Use MySQL** - Required for multi-server
3. **Configure server IDs** - Each server gets a unique ID automatically
4. **Test penalties** - Ensure bans work across all servers
## Troubleshooting
### Changes not taking effect
**Solution:** Reload the plugin or restart the server:
```
css_plugins reload CS2-SimpleAdmin
```
### Discord webhooks not working
**Solution:**
- Verify webhook URL is correct
- Check that the webhook is not deleted in Discord
- Ensure server has internet access
### TimeMode issues
**Solution:** Set your timezone correctly in the configuration
## Next Steps
- **[Learn admin commands](commands/basebans)** - Browse available commands
- **[Set up admins](#)** - Add your admin team
- **[Configure modules](../modules/intro)** - Extend functionality

View File

@@ -0,0 +1,188 @@
---
sidebar_position: 2
---
# Installation
This guide will help you install CS2-SimpleAdmin on your Counter-Strike 2 server.
## Prerequisites
Before installing CS2-SimpleAdmin, ensure you have the following dependencies installed:
### Required Dependencies
1. **[CounterStrikeSharp](https://github.com/roflmuffin/CounterStrikeSharp/)** (v1.0.340+)
- The core framework for CS2 server plugins
2. **[AnyBaseLibCS2](https://github.com/NickFox007/AnyBaseLibCS2)**
- Required by PlayerSettings
3. **[PlayerSettings](https://github.com/NickFox007/PlayerSettingsCS2)**
- Required by MenuManager
4. **[MenuManagerCS2](https://github.com/NickFox007/MenuManagerCS2)**
- Provides the menu system
### Database Requirements
You'll need either:
- **MySQL** server (recommended for production)
- **SQLite** (built-in, good for testing)
## Installation Steps
### 1. Download the Plugin
Download the latest release from the [GitHub Releases page](https://github.com/daffyyyy/CS2-SimpleAdmin/releases).
You can either:
- Download the pre-built release ZIP file
- Clone the repository and build from source
### 2. Extract Files
Extract the downloaded files to your server's CounterStrikeSharp directory:
```
game/csgo/addons/counterstrikesharp/plugins/CS2-SimpleAdmin/
```
Your directory structure should look like this:
```
csgo/
└── addons/
└── counterstrikesharp/
├── plugins/
│ └── CS2-SimpleAdmin/
│ ├── CS2-SimpleAdmin.dll
│ ├── lang/
│ └── ... (other files)
└── shared/
└── CS2-SimpleAdminApi/
└── CS2-SimpleAdminApi.dll
```
### 3. First Launch
Start your server. On the first launch, CS2-SimpleAdmin will:
1. Create a configuration file at:
```
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json
```
2. Create a database (if using SQLite):
```
addons/counterstrikesharp/plugins/CS2-SimpleAdmin/cs2-simpleadmin.sqlite
```
3. Apply database migrations automatically
### 4. Configure the Plugin
Edit the generated configuration file to match your server setup.
See the [Configuration Guide](configuration) for detailed information.
### 5. Restart Your Server
After editing the configuration, restart your server or reload the plugin:
```bash
css_plugins reload CS2-SimpleAdmin
```
## Building from Source
If you want to build CS2-SimpleAdmin from source:
### Prerequisites
- .NET 8.0 SDK
- Git
### Build Steps
1. **Clone the repository:**
```bash
git clone https://github.com/daffyyyy/CS2-SimpleAdmin.git
cd CS2-SimpleAdmin
```
2. **Restore dependencies:**
```bash
dotnet restore CS2-SimpleAdmin.sln
```
3. **Build the solution:**
```bash
dotnet build CS2-SimpleAdmin.sln -c Release
```
4. **Build output location:**
```
CS2-SimpleAdmin/bin/Release/net8.0/
CS2-SimpleAdminApi/bin/Release/net8.0/
```
5. **Copy to server:**
- Copy `CS2-SimpleAdmin.dll` and its dependencies to `plugins/CS2-SimpleAdmin/`
- Copy `CS2-SimpleAdminApi.dll` to `shared/CS2-SimpleAdminApi/`
## Verification
To verify the installation was successful:
1. **Check server console** for the plugin load message:
```
[CS2-SimpleAdmin] Plugin loaded successfully
```
2. **Run an admin command** in-game:
```
css_admin
```
3. **Check the logs** at:
```
addons/counterstrikesharp/logs/CS2-SimpleAdmin*.txt
```
## Troubleshooting
### Plugin doesn't load
**Solution:** Ensure all required dependencies are installed:
- CounterStrikeSharp (latest version)
- AnyBaseLibCS2
- PlayerSettings
- MenuManagerCS2
### Database connection errors
**Solution:**
- For MySQL: Verify database credentials in the config file
- For SQLite: Ensure the plugin has write permissions in its directory
### Commands not working
**Solution:**
- Check that you have admin permissions configured
- Verify the commands are enabled in `Commands.json`
- Check server console for error messages
## Next Steps
- **[Configure your plugin](configuration)** - Set up database, permissions, and features
- **[Learn the commands](commands/basebans)** - Browse available admin commands
- **[Add admins](#)** - Set up your admin team
## Need Help?
If you encounter issues:
1. Check the [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues) for similar problems
2. Review server logs for error messages
3. Ask for help on [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)

View File

@@ -0,0 +1,81 @@
---
sidebar_position: 1
---
# Introduction
Welcome to **CS2-SimpleAdmin** - a comprehensive administration plugin for Counter-Strike 2 servers built with C# (.NET 8.0) for CounterStrikeSharp.
## What is CS2-SimpleAdmin?
CS2-SimpleAdmin is a powerful server administration tool that provides comprehensive features for managing your Counter-Strike 2 server. It offers:
- **Player Management** - Ban, kick, mute, gag, and warn players
- **Admin System** - Flexible permission system with flags and groups
- **Multi-Server Support** - Manage multiple servers with a shared database
- **Discord Integration** - Send notifications to Discord webhooks
- **Menu System** - Easy-to-use admin menus
- **Extensive Commands** - Over 50 admin commands
- **Module Support** - Extend functionality with custom modules
## Key Features
### 🛡️ Comprehensive Penalty System
- **Bans** - Ban players by SteamID or IP address
- **Mutes** - Mute voice communication
- **Gags** - Gag text chat
- **Silence** - Block both voice and text
- **Warnings** - Progressive warning system with auto-escalation
### 👥 Flexible Admin System
- Permission-based access control using flags
- Admin groups for easy management
- Immunity levels to prevent abuse
- Server-specific or global admin assignments
### 🗄️ Database Support
- **MySQL** - For production environments
- **SQLite** - For quick setup and testing
- Automatic migration system
- Multi-server mode with shared data
### 🎮 User-Friendly Interface
- Interactive admin menus
- In-game admin panel
- Player selection menus
- Duration and reason selection
### 🔧 Extensibility
- Public API for module development
- Event system for custom integrations
- Command registration system
- Menu builder API
## Requirements
Before installing CS2-SimpleAdmin, make sure you have:
- [CounterStrikeSharp](https://github.com/roflmuffin/CounterStrikeSharp/) (v1.0.340+)
- [AnyBaseLibCS2](https://github.com/NickFox007/AnyBaseLibCS2)
- [PlayerSettings](https://github.com/NickFox007/PlayerSettingsCS2)
- [MenuManagerCS2](https://github.com/NickFox007/MenuManagerCS2)
- MySQL database (or use SQLite for testing)
## Quick Links
- **[Installation Guide](installation)** - Get started with CS2-SimpleAdmin
- **[Configuration](configuration)** - Learn how to configure the plugin
- **[Commands](commands/basebans)** - Browse all available commands
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code and releases
## Community & Support
Need help or want to contribute?
- **Issues** - Report bugs on [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
- **Discussions** - Ask questions on [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
- **Pull Requests** - Contribute improvements
## License
CS2-SimpleAdmin is open-source software. Check the [GitHub repository](https://github.com/daffyyyy/CS2-SimpleAdmin) for license details.

View File

@@ -0,0 +1,174 @@
// @ts-check
// `@type` JSDoc annotations allow editor autocompletion and type checking
// (when paired with `@ts-check`).
// There are various equivalent ways to declare your Docusaurus config.
// See: https://docusaurus.io/docs/api/docusaurus-config
import {themes as prismThemes} from 'prism-react-renderer';
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'CS2-SimpleAdmin',
tagline: 'Comprehensive administration plugin for Counter-Strike 2 servers',
favicon: 'img/favicon.ico',
// Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future
future: {
v4: true, // Improve compatibility with the upcoming Docusaurus v4
},
// Set the production url of your site here
url: 'https://cs2-simpleadmin.daffyy.dev',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'daffyyyy', // Usually your GitHub org/user name.
projectName: 'CS2-SimpleAdmin', // Usually your repo name.
onBrokenLinks: 'throw',
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: './sidebars.js',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
// editUrl:
// '',
},
blog: false, // Disable blog
theme: {
customCss: './src/css/custom.css',
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
metadata: [
{name: 'keywords', content: 'CS2, Counter-Strike 2, admin plugin, server management, bans, mutes, CounterStrikeSharp'},
{name: 'description', content: 'Comprehensive administration plugin for Counter-Strike 2 servers with ban management, multi-server support, and extensible API'},
{name: 'author', content: 'daffyyyy'},
{property: 'og:title', content: 'CS2-SimpleAdmin - Admin Plugin for Counter-Strike 2'},
{property: 'og:description', content: 'Comprehensive administration plugin for CS2 servers. Manage bans, mutes, warnings, and permissions with multi-server support.'},
{property: 'og:type', content: 'website'},
{property: 'og:url', content: 'https://cs2-simpleadmin.daffyy.dev'},
{property: 'og:image', content: 'https://cs2-simpleadmin.daffyy.dev/img/docusaurus-social-card.jpg'},
{name: 'twitter:card', content: 'summary_large_image'},
{name: 'twitter:title', content: 'CS2-SimpleAdmin - Admin Plugin for Counter-Strike 2'},
{name: 'twitter:description', content: 'Comprehensive administration plugin for CS2 servers with ban management and multi-server support.'},
{name: 'twitter:image', content: 'https://cs2-simpleadmin.daffyy.dev/img/docusaurus-social-card.jpg'},
],
colorMode: {
respectPrefersColorScheme: true,
},
navbar: {
title: 'CS2-SimpleAdmin',
logo: {
alt: 'CS2-SimpleAdmin Logo',
src: 'img/logo.svg',
},
items: [
{
type: 'docSidebar',
sidebarId: 'userSidebar',
position: 'left',
label: 'User Guide',
},
{
type: 'docSidebar',
sidebarId: 'modulesSidebar',
position: 'left',
label: 'Modules',
},
{
type: 'docSidebar',
sidebarId: 'developerSidebar',
position: 'left',
label: 'Developer',
},
{
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Documentation',
items: [
{
label: 'User Guide',
to: '/docs/user/intro',
},
{
label: 'Modules',
to: '/docs/modules/intro',
},
{
label: 'Developer',
to: '/docs/developer/intro',
},
],
},
{
title: 'Community',
items: [
{
label: 'GitHub Issues',
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin/issues',
},
{
label: 'GitHub Discussions',
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin/discussions',
},
],
},
{
title: 'More',
items: [
{
label: 'GitHub',
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin',
},
{
label: 'Releases',
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin/releases',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} CS2-SimpleAdmin. Built with Docusaurus.`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
additionalLanguages: ['csharp', 'json', 'bash'],
},
}),
};
export default config;

File diff suppressed because it is too large Load Diff

17981
CS2-SimpleAdmin-docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
{
"name": "cs-2-simple-admin-docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "3.9.2",
"@docusaurus/preset-classic": "3.9.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/types": "3.9.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=20.0"
}
}

View File

@@ -0,0 +1,81 @@
// @ts-check
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
@type {import('@docusaurus/plugin-content-docs').SidebarsConfig}
*/
const sidebars = {
userSidebar: [
'user/intro',
{
type: 'category',
label: 'Getting Started',
items: [
'user/installation',
'user/configuration',
],
},
{
type: 'category',
label: 'Commands',
items: [
'user/commands/basebans',
'user/commands/basecomms',
'user/commands/basecommands',
'user/commands/basechat',
'user/commands/playercommands',
'user/commands/basevotes',
],
},
],
modulesSidebar: [
'modules/intro',
{
type: 'category',
label: 'Official Modules',
items: [
'modules/funcommands',
],
},
'modules/development',
],
developerSidebar: [
'developer/intro',
{
type: 'category',
label: 'API Reference',
items: [
'developer/api/overview',
'developer/api/commands',
'developer/api/menus',
'developer/api/penalties',
'developer/api/events',
'developer/api/utilities',
],
},
{
type: 'category',
label: 'Module Development',
items: [
'developer/module/getting-started',
'developer/module/best-practices',
'developer/module/examples',
],
},
'developer/architecture',
],
};
export default sidebars;

View File

@@ -0,0 +1,64 @@
import clsx from 'clsx';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
const FeatureList = [
{
title: 'Comprehensive Admin Tools',
img: require('@site/static/img/index_1.png').default,
description: (
<>
Full suite of admin commands for managing players, bans, mutes, warnings,
and server settings. Everything you need to moderate your CS2 server.
</>
),
},
{
title: 'Multi-Server Support',
img: require('@site/static/img/index_2.png').default,
description: (
<>
Manage multiple servers with synchronized admin permissions and penalties.
Share bans, mutes, and admin groups across your entire server network.
</>
),
},
{
title: 'Extensible API',
img: require('@site/static/img/index_3.png').default,
description: (
<>
Build custom modules using the public API. Create your own commands,
menus, and integrate with CS2-SimpleAdmin's permission and penalty systems.
</>
),
},
];
function Feature({img, title, description}) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">
<img src={img} className={styles.featureSvg} alt={title} />
</div>
<div className="text--center padding-horiz--md">
<Heading as="h3">{title}</Heading>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures() {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}

View File

@@ -0,0 +1,30 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #ff8c00;
--ifm-color-primary-dark: #e67e00;
--ifm-color-primary-darker: #d97700;
--ifm-color-primary-darkest: #b36200;
--ifm-color-primary-light: #ff9a1a;
--ifm-color-primary-lighter: #ffa328;
--ifm-color-primary-lightest: #ffb54d;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #ff9500;
--ifm-color-primary-dark: #e68600;
--ifm-color-primary-darker: #cc7700;
--ifm-color-primary-darkest: #b36200;
--ifm-color-primary-light: #ffa31a;
--ifm-color-primary-lighter: #ffad33;
--ifm-color-primary-lightest: #ffc266;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

View File

@@ -0,0 +1,52 @@
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures';
import Heading from '@theme/Heading';
import styles from './index.module.css';
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<iframe
className={styles.videoBackground}
src="https://www.youtube.com/embed/4qEdIXLdxMo?autoplay=1&mute=1&loop=1&playlist=4qEdIXLdxMo&controls=0&showinfo=0&rel=0&modestbranding=1&playsinline=1"
title="Background Video"
frameBorder="0"
allow="autoplay; encrypted-media"
allowFullScreen
/>
<div className={styles.videoOverlay}></div>
<div className="container">
<Heading as="h1" className="hero__title">
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/user/intro">
Get Started
</Link>
</div>
</div>
</header>
);
}
export default function Home() {
const {siteConfig} = useDocusaurusContext();
return (
<Layout
title={`${siteConfig.title} - Admin Plugin for CS2`}
description="CS2-SimpleAdmin is a comprehensive administration plugin for Counter-Strike 2 servers. Manage bans, mutes, warnings, and permissions with multi-server support and extensible API.">
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}

View File

@@ -0,0 +1,97 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 8rem 0;
text-align: center;
position: relative;
overflow: hidden;
min-height: 600px;
display: flex;
align-items: center;
justify-content: center;
}
.videoBackground {
position: absolute;
top: 50%;
left: 50%;
width: 100vw;
height: 56.25vw; /* 16:9 Aspect Ratio */
min-height: 100vh;
min-width: 177.77vh; /* 16:9 Aspect Ratio */
transform: translate(-50%, -50%);
z-index: 0;
pointer-events: none;
}
.videoOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 1;
}
.heroBanner :global(.container) {
position: relative;
z-index: 999 !important;
}
.heroBanner :global(.hero__title) {
color: white !important;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.9);
font-size: 3.5rem;
font-weight: bold;
margin-bottom: 1rem;
}
.heroBanner :global(.hero__subtitle) {
color: white !important;
text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.9);
font-size: 1.5rem;
margin-bottom: 2rem;
}
.heroBanner .buttons {
position: relative;
z-index: 10;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 4rem 2rem;
min-height: 400px;
}
.videoBackground {
width: 200vw;
height: 112.5vw;
}
.heroBanner :global(.hero__title) {
font-size: 2rem;
}
.heroBanner :global(.hero__subtitle) {
font-size: 1.2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 10;
}
.buttons :global(.button) {
position: relative;
z-index: 10;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}

View File

@@ -0,0 +1,7 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

31
CS2-SimpleAdmin.sln Normal file
View File

@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34309.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CS2-SimpleAdmin", "CS2-SimpleAdmin\CS2-SimpleAdmin.csproj", "{CC7C3B4D-26C9-4DE7-B4E1-0864350468D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CS2-SimpleAdminApi", "CS2-SimpleAdminApi\CS2-SimpleAdminApi.csproj", "{8BEF0C35-7E4E-4BAF-B632-8584FAFCA922}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CC7C3B4D-26C9-4DE7-B4E1-0864350468D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC7C3B4D-26C9-4DE7-B4E1-0864350468D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC7C3B4D-26C9-4DE7-B4E1-0864350468D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC7C3B4D-26C9-4DE7-B4E1-0864350468D0}.Release|Any CPU.Build.0 = Release|Any CPU
{8BEF0C35-7E4E-4BAF-B632-8584FAFCA922}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BEF0C35-7E4E-4BAF-B632-8584FAFCA922}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BEF0C35-7E4E-4BAF-B632-8584FAFCA922}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BEF0C35-7E4E-4BAF-B632-8584FAFCA922}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {86114444-059F-4DB8-9A55-E6D1BB76238D}
EndGlobalSection
EndGlobal

Binary file not shown.

View File

@@ -0,0 +1,446 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Core.Translations;
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)
{
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)
{
return PlayerPenaltyManager.GetAllPlayerPenalties(player.Slot);
}
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltied;
public event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltiedAdded;
public event Action<string, string?, bool, object>? OnAdminShowActivity;
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);
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);
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)
{
switch (penaltyType)
{
case PenaltyType.Ban:
{
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason);
break;
}
case PenaltyType.Kick:
{
CS2_SimpleAdmin.Instance.Kick(admin, player, reason);
break;
}
case PenaltyType.Gag:
{
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
break;
}
case PenaltyType.Mute:
{
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
break;
}
case PenaltyType.Silence:
{
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
break;
}
case PenaltyType.Warn:
{
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
}
}
public void IssuePenalty(SteamID steamid, CCSPlayerController? admin, PenaltyType penaltyType, string reason,
int duration = -1)
{
switch (penaltyType)
{
case PenaltyType.Ban:
{
CS2_SimpleAdmin.Instance.AddBan(admin, steamid, duration, reason);
break;
}
case PenaltyType.Gag:
{
CS2_SimpleAdmin.Instance.AddGag(admin, steamid, duration, reason);
break;
}
case PenaltyType.Mute:
{
CS2_SimpleAdmin.Instance.AddMute(admin, steamid, duration, reason);
break;
}
case PenaltyType.Silence:
{
CS2_SimpleAdmin.Instance.AddSilence(admin, steamid, duration, reason);
break;
}
case PenaltyType.Warn:
{
CS2_SimpleAdmin.Instance.AddWarn(admin, steamid, duration, reason);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
}
}
public void LogCommand(CCSPlayerController? caller, string command)
{
Helper.LogCommand(caller, command);
}
public void LogCommand(CCSPlayerController? caller, CommandInfo command)
{
Helper.LogCommand(caller, command);
}
public bool IsAdminSilent(CCSPlayerController player)
{
return CS2_SimpleAdmin.SilentPlayers.Contains(player.Slot);
}
public HashSet<int> ListSilentAdminsSlots()
{
return CS2_SimpleAdmin.SilentPlayers;
}
public void RegisterCommand(string name, string? description, CommandInfo.CommandCallback callback)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Command name cannot be null or empty.", nameof(name));
ArgumentNullException.ThrowIfNull(callback);
var definition = new CommandDefinition(name, description ?? "No description", callback);
if (!RegisterCommands._commandDefinitions.TryGetValue(name, out var list))
{
list = new List<CommandDefinition>();
RegisterCommands._commandDefinitions[name] = list;
}
list.Add(definition);
}
public void UnRegisterCommand(string commandName)
{
var definitions = RegisterCommands._commandDefinitions[commandName];
if (definitions.Count == 0)
return;
foreach (var definition in definitions)
{
CS2_SimpleAdmin.Instance.RemoveCommand(commandName, definition.Callback);
}
}
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 RegisterMenuCategory(string categoryId, string categoryNameKey, string permission, object moduleLocalizer)
{
if (moduleLocalizer is not IStringLocalizer localizer)
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryNameKey, permission, localizer);
}
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 RegisterMenu(string categoryId, string menuId, string menuName,
Func<CCSPlayerController, MenuContext, object> menuFactory, string? permission = null, string? commandName = null)
{
Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuName, BuilderFactory, permission, commandName);
return;
MenuBuilder BuilderFactory(CCSPlayerController player)
{
var context = new MenuContext(categoryId, menuId, menuName, permission, commandName);
if (menuFactory(player, context) 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 RegisterMenu(string categoryId, string menuId, string menuNameKey,
Func<CCSPlayerController, MenuContext, object> menuFactory, string? permission, string? commandName, object moduleLocalizer)
{
if (moduleLocalizer is not IStringLocalizer localizer)
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuNameKey, BuilderFactory, permission, commandName, localizer);
return;
MenuBuilder BuilderFactory(CCSPlayerController player)
{
var context = new MenuContext(categoryId, menuId, menuNameKey, permission, commandName);
if (menuFactory(player, context) 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 object CreateMenuWithBack(MenuContext context, CCSPlayerController player)
{
// Get translated title if module has localizer
string title = context.MenuTitle;
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category))
{
// Check if this specific menu has a localizer
if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer))
{
using (new WithTemporaryCulture(player.GetLanguage()))
{
title = menuLocalizer[context.MenuTitle] ?? context.MenuTitle;
}
}
// Fallback to category localizer
else if (category.ModuleLocalizer != null)
{
using (new WithTemporaryCulture(player.GetLanguage()))
{
title = category.ModuleLocalizer[context.MenuTitle] ?? context.MenuTitle;
}
}
}
return CreateMenuWithBack(title, context.CategoryId, player);
}
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 object CreateMenuWithPlayers(MenuContext context, CCSPlayerController admin,
Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)
{
// Get translated title if module has localizer
string title = context.MenuTitle;
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category))
{
// Check if this specific menu has a localizer
if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer))
{
using (new WithTemporaryCulture(admin.GetLanguage()))
{
title = menuLocalizer[context.MenuTitle] ?? context.MenuTitle;
}
}
// Fallback to category localizer
else if (category.ModuleLocalizer != null)
{
using (new WithTemporaryCulture(admin.GetLanguage()))
{
title = category.ModuleLocalizer[context.MenuTitle] ?? context.MenuTitle;
}
}
}
return CreateMenuWithPlayers(title, context.CategoryId, admin, filter, onSelect);
}
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

@@ -0,0 +1,271 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
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;
namespace CS2_SimpleAdmin;
[MinimumApiVersion(300)]
public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdminConfig>
{
internal static CS2_SimpleAdmin Instance { get; private set; } = new();
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.8-beta-5";
public override void Load(bool hotReload)
{
Instance = this;
if (hotReload)
{
ServerLoaded = false;
_serverLoading = false;
CacheManager?.Dispose();
CacheManager = new CacheManager();
// OnGameServerSteamAPIActivated();
OnMapStart(string.Empty);
AddTimer(6.0f, () =>
{
if (DatabaseProvider == null) return;
PlayersInfo.Clear();
CachedPlayers.Clear();
BotPlayers.Clear();
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsHLTV).ToArray())
{
if (!player.IsBot)
PlayerManager.LoadPlayerData(player, true);
else
BotPlayers.Add(player);
};
});
PlayersTimer?.Kill();
PlayersTimer = null;
}
_cBasePlayerControllerSetPawnFunc = new MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>(GameData.GetSignature("CBasePlayerController_SetPawn"));
SimpleAdminApi = new Api.CS2_SimpleAdminApi();
Capabilities.RegisterPluginCapability(ICS2_SimpleAdminApi.PluginCapability, () => SimpleAdminApi);
PlayersTimer?.Kill();
PlayersTimer = null;
PlayerManager.CheckPlayersTimer();
Menus.MenuManager.Instance.InitializeDefaultCategories();
BasicMenu.Initialize();
}
public override void OnAllPluginsLoaded(bool hotReload)
{
try
{
MenuApi = MenuCapability.Get();
}
catch (Exception ex)
{
Logger.LogError("Unable to load required plugins ... \n{exception}", ex.Message);
Unload(false);
}
AddTimer(6.0f, () => ReloadAdmins(null));
RegisterEvents();
AddTimer(0.5f, RegisterCommands.InitializeCommands);
if (!CoreConfig.UnlockConCommands)
{
_logger?.LogError(
$"⚠️ Warning: 'UnlockConCommands' is disabled in core.json. " +
$"Players will not be automatically banned when kicked and will be able " +
$"to rejoin the server for 60 seconds. " +
$"To enable instant banning, set 'UnlockConCommands': true"
);
_logger?.LogError(
$"⚠️ Warning: 'UnlockConCommands' is disabled in core.json. " +
$"Players will not be automatically banned when kicked and will be able " +
$"to rejoin the server for 60 seconds. " +
$"To enable instant banning, set 'UnlockConCommands': true"
);
}
}
public void OnConfigParsed(CS2_SimpleAdminConfig config)
{
if (System.Diagnostics.Debugger.IsAttached)
Environment.FailFast(":(!");
Helper.UpdateConfig(config);
_logger = Logger;
Config = config;
bool missing = false;
var cssPath = Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp");
var pluginsPath = Path.Combine(cssPath, "plugins");
var sharedPath = Path.Combine(cssPath, "shared");
foreach (var plugin in _requiredPlugins)
{
var pluginDirPath = Path.Combine(pluginsPath, plugin);
var pluginDllPath = Path.Combine(pluginDirPath, $"{plugin}.dll");
if (!Directory.Exists(pluginDirPath))
{
_logger?.LogError(
$"❌ Plugin directory '{plugin}' missing at: {pluginDirPath}"
);
missing = true;
}
if (!File.Exists(pluginDllPath))
{
_logger?.LogError(
$"❌ Plugin DLL '{plugin}.dll' missing at: {pluginDllPath}"
);
missing = true;
}
}
foreach (var shared in _requiredShared)
{
var sharedDirPath = Path.Combine(sharedPath, shared);
var sharedDllPath = Path.Combine(sharedDirPath, $"{shared}.dll");
if (!Directory.Exists(sharedDirPath))
{
_logger?.LogError(
$"❌ Shared library directory '{shared}' missing at: {sharedDirPath}"
);
missing = true;
}
if (!File.Exists(sharedDllPath))
{
_logger?.LogError(
$"❌ Shared library DLL '{shared}.dll' missing at: {sharedDllPath}"
);
missing = true;
}
}
if (missing)
Server.ExecuteCommand($"css_plugins unload {ModuleName}");
Instance = this;
if (Config.DatabaseConfig.DatabaseType.Contains("mysql", StringComparison.CurrentCultureIgnoreCase))
{
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseHost) ||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseName) ||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseUser))
{
throw new Exception("[CS2-SimpleAdmin] You need to setup MySQL credentials in config!");
}
var builder = new MySqlConnectionStringBuilder()
{
Server = config.DatabaseConfig.DatabaseHost,
Database = config.DatabaseConfig.DatabaseName,
UserID = config.DatabaseConfig.DatabaseUser,
Password = config.DatabaseConfig.DatabasePassword,
Port = (uint)config.DatabaseConfig.DatabasePort,
SslMode = Enum.TryParse(config.DatabaseConfig.DatabaseSSlMode, true, out MySqlSslMode sslMode)
? sslMode
: MySqlSslMode.Preferred,
Pooling = true,
};
DbConnectionString = builder.ConnectionString;
DatabaseProvider = new MySqlDatabaseProvider(DbConnectionString);
}
else
{
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.SqliteFilePath))
{
throw new Exception("[CS2-SimpleAdmin] You need to specify SQLite file path in config!");
}
DatabaseProvider = new SqliteDatabaseProvider(ModuleDirectory + "/" + config.DatabaseConfig.SqliteFilePath);
}
var (success, exception) = Task.Run(() => DatabaseProvider.CheckConnectionAsync()).GetAwaiter().GetResult();
if (!success)
{
if (exception != null)
Logger.LogError("Problem with database connection! \n{exception}", exception);
Unload(false);
return;
}
Task.Run(() => DatabaseProvider.DatabaseMigrationAsync());
if (!Directory.Exists(ModuleDirectory + "/data"))
{
Directory.CreateDirectory(ModuleDirectory + "/data");
}
_localizer = Localizer;
if (!string.IsNullOrEmpty(Config.Discord.DiscordLogWebhook))
DiscordWebhookClientLog = new DiscordManager(Config.Discord.DiscordLogWebhook);
if (Config.EnableUpdateCheck)
Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, Logger));
PermissionManager = new PermissionManager(DatabaseProvider);
BanManager = new BanManager(DatabaseProvider);
MuteManager = new MuteManager(DatabaseProvider);
WarnManager = new WarnManager(DatabaseProvider);
}
internal static TargetResult? GetTarget(CommandInfo command, int argument = 1)
{
var matches = command.GetArgTargetResult(argument);
if (!matches.Any())
{
command.ReplyToCommand($"Target {command.GetArg(argument)} not found.");
return null;
}
if (command.GetArg(argument).StartsWith('@'))
return matches;
if (matches.Count() == 1)
return matches;
command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(argument)}\".");
return null;
}
public override void Unload(bool hotReload)
{
CacheManager?.Dispose();
CacheManager = null;
PlayersTimer?.Kill();
PlayersTimer = null;
UnregisterEvents();
if (hotReload)
PlayersInfo.Clear();
else
Server.ExecuteCommand($"css_plugins unload {ModuleDirectory}");
}
}

View File

@@ -0,0 +1,152 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CS2_SimpleAdmin</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<PublishTrimmed>true</PublishTrimmed>
<DebuggerSupport>false</DebuggerSupport>
<!-- <GenerateDependencyFile>false</GenerateDependencyFile>-->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.346">
<PrivateAssets>none</PrivateAssets>
<ExcludeAssets>runtime</ExcludeAssets>
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="MySqlConnector" Version="2.5.0-beta.1" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Linq.Async" Version="7.0.0-preview.1.g24680b5469" />
<PackageReference Include="ZLinq" Version="1.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CS2-SimpleAdminApi\CS2-SimpleAdminApi.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
<None Update="Database\Migrations\Mysql\001_CreateTables.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\002_CreateFlagsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\003_ChangeColumnsPosition.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\004_MoveOldFlagsToFlagsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\005_CreateUnbansTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\006_ServerGroupsFeature.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\007_ServerGroupsGlobal.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\008_OnlineTimeInPenalties.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\009_BanAllUsedIpAddress.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\010_CreateWarnsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\011_AddRconColumnToServersTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\012_AddUpdatedAtColumnToSaBansTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\013_AddNameColumnToSaPlayerIpsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\014_AddIndexesToMutesTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<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>
<None Update="Database\Migrations\Sqlite\002_CreateFlagsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\003_ChangeColumnsPosition.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\004_MoveOldFlagsToFlagsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\005_CreateUnbansTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\006_ServerGroupsFeature.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\007_ServerGroupsGlobal.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\008_OnlineTimeInPenalties.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\009_BanAllUsedIpAddress.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\010_CreateWarnsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\011_AddRconColumnToServersTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\012_AddUpdatedAtColumnToSaBansTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\013_AddNameColumnToSaPlayerIpsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\014_AddIndexesToMutesTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<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>
<None Update="Database\Migrations\*.sql" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<None Update="admin_help.txt" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Reference Include="MenuManagerApi">
<HintPath>3rd_party\MenuManagerApi.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,237 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.Logging;
namespace CS2_SimpleAdmin;
public static class RegisterCommands
{
internal static readonly Dictionary<string, IList<CommandDefinition>> _commandDefinitions =
new(StringComparer.InvariantCultureIgnoreCase);
private delegate void CommandCallback(CCSPlayerController? caller, CommandInfo.CommandCallback callback);
private static readonly string CommandsPath = Path.Combine(CS2_SimpleAdmin.ConfigDirectory, "Commands.json");
private static readonly List<CommandMapping> CommandMappings =
[
new("css_ban", CS2_SimpleAdmin.Instance.OnBanCommand),
new("css_addban", CS2_SimpleAdmin.Instance.OnAddBanCommand),
new("css_banip", CS2_SimpleAdmin.Instance.OnBanIpCommand),
new("css_unban", CS2_SimpleAdmin.Instance.OnUnbanCommand),
new("css_warn", CS2_SimpleAdmin.Instance.OnWarnCommand),
new("css_unwarn", CS2_SimpleAdmin.Instance.OnUnwarnCommand),
new("css_asay", CS2_SimpleAdmin.Instance.OnAdminToAdminSayCommand),
new("css_cssay", CS2_SimpleAdmin.Instance.OnAdminCustomSayCommand),
new("css_say", CS2_SimpleAdmin.Instance.OnAdminSayCommand),
new("css_psay", CS2_SimpleAdmin.Instance.OnAdminPrivateSayCommand),
new("css_csay", CS2_SimpleAdmin.Instance.OnAdminCenterSayCommand),
new("css_hsay", CS2_SimpleAdmin.Instance.OnAdminHudSayCommand),
new("css_penalties", CS2_SimpleAdmin.Instance.OnPenaltiesCommand),
new("css_admin", CS2_SimpleAdmin.Instance.OnAdminCommand),
new("css_adminhelp", CS2_SimpleAdmin.Instance.OnAdminHelpCommand),
new("css_addadmin", CS2_SimpleAdmin.Instance.OnAddAdminCommand),
new("css_deladmin", CS2_SimpleAdmin.Instance.OnDelAdminCommand),
new("css_addgroup", CS2_SimpleAdmin.Instance.OnAddGroup),
new("css_delgroup", CS2_SimpleAdmin.Instance.OnDelGroupCommand),
new("css_reloadadmins", CS2_SimpleAdmin.Instance.OnRelAdminCommand),
new("css_reloadbans", CS2_SimpleAdmin.Instance.OnRelBans),
new("css_hide", CS2_SimpleAdmin.Instance.OnHideCommand),
new("css_hidecomms", CS2_SimpleAdmin.Instance.OnHideCommsCommand),
new("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand),
new("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand),
new("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand),
new("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand),
new("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand),
new("css_map", CS2_SimpleAdmin.Instance.OnMapCommand),
new("css_wsmap", CS2_SimpleAdmin.Instance.OnWorkshopMapCommand),
new("css_cvar", CS2_SimpleAdmin.Instance.OnCvarCommand),
new("css_rcon", CS2_SimpleAdmin.Instance.OnRconCommand),
new("css_rr", CS2_SimpleAdmin.Instance.OnRestartCommand),
new("css_gag", CS2_SimpleAdmin.Instance.OnGagCommand),
new("css_addgag", CS2_SimpleAdmin.Instance.OnAddGagCommand),
new("css_ungag", CS2_SimpleAdmin.Instance.OnUngagCommand),
new("css_mute", CS2_SimpleAdmin.Instance.OnMuteCommand),
new("css_addmute", CS2_SimpleAdmin.Instance.OnAddMuteCommand),
new("css_unmute", CS2_SimpleAdmin.Instance.OnUnmuteCommand),
new("css_silence", CS2_SimpleAdmin.Instance.OnSilenceCommand),
new("css_addsilence", CS2_SimpleAdmin.Instance.OnAddSilenceCommand),
new("css_unsilence", CS2_SimpleAdmin.Instance.OnUnsilenceCommand),
new("css_vote", CS2_SimpleAdmin.Instance.OnVoteCommand),
new("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand),
new("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand),
new("css_team", CS2_SimpleAdmin.Instance.OnTeamCommand),
new("css_rename", CS2_SimpleAdmin.Instance.OnRenameCommand),
new("css_prename", CS2_SimpleAdmin.Instance.OnPrenameCommand),
new("css_tp", CS2_SimpleAdmin.Instance.OnGotoCommand),
new("css_bring", CS2_SimpleAdmin.Instance.OnBringCommand),
new("css_pluginsmanager", CS2_SimpleAdmin.Instance.OnPluginManagerCommand),
new("css_adminvoice", CS2_SimpleAdmin.Instance.OnAdminVoiceCommand)
];
/// <summary>
/// Initializes command registration.
/// If the commands config file does not exist, creates it and then recurses to register commands.
/// Otherwise, directly registers commands from the configuration.
/// </summary>
public static void InitializeCommands()
{
if (!File.Exists(CommandsPath))
{
CreateConfig();
InitializeCommands();
}
else
{
Register();
}
}
/// <summary>
/// Creates the default commands configuration JSON file with built-in commands and aliases.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
private static void CreateConfig()
{
var commands = new CommandsConfig
{
Commands = new Dictionary<string, Command>
{
{ "css_ban", new Command { Aliases = ["css_ban"] } },
{ "css_addban", new Command { Aliases = ["css_addban"] } },
{ "css_banip", new Command { Aliases = ["css_banip"] } },
{ "css_unban", new Command { Aliases = ["css_unban"] } },
{ "css_warn", new Command { Aliases = ["css_warn"] } },
{ "css_unwarn", new Command { Aliases = ["css_unwarn"] } },
{ "css_asay", new Command { Aliases = ["css_asay"] } },
{ "css_cssay", new Command { Aliases = ["css_cssay"] } },
{ "css_say", new Command { Aliases = ["css_say"] } },
{ "css_psay", new Command { Aliases = ["css_psay"] } },
{ "css_csay", new Command { Aliases = ["css_csay"] } },
{ "css_hsay", new Command { Aliases = ["css_hsay"] } },
{ "css_penalties", new Command { Aliases = ["css_penalties", "css_mypenalties", "css_comms"] } },
{ "css_admin", new Command { Aliases = ["css_admin"] } },
{ "css_adminhelp", new Command { Aliases = ["css_adminhelp"] } },
{ "css_addadmin", new Command { Aliases = ["css_addadmin"] } },
{ "css_deladmin", new Command { Aliases = ["css_deladmin"] } },
{ "css_addgroup", new Command { Aliases = ["css_addgroup"] } },
{ "css_delgroup", new Command { Aliases = ["css_delgroup"] } },
{ "css_reloadadmins", new Command { Aliases = ["css_reloadadmins"] } },
{ "css_reloadbans", new Command { Aliases = ["css_reloadbans"] } },
{ "css_hide", new Command { Aliases = ["css_hide", "css_stealth"] } },
{ "css_hidecomms", new Command { Aliases = ["css_hidecomms"] } },
{ "css_who", new Command { Aliases = ["css_who"] } },
{ "css_disconnected", new Command { Aliases = ["css_disconnected", "css_last"] } },
{ "css_warns", new Command { Aliases = ["css_warns"] } },
{ "css_players", new Command { Aliases = ["css_players"] } },
{ "css_kick", new Command { Aliases = ["css_kick"] } },
{ "css_map", new Command { Aliases = ["css_map", "css_changemap"] } },
{ "css_wsmap", new Command { Aliases = ["css_wsmap", "css_changewsmap", "css_workshop"] } },
{ "css_cvar", new Command { Aliases = ["css_cvar"] } },
{ "css_rcon", new Command { Aliases = ["css_rcon"] } },
{ "css_rr", new Command { Aliases = ["css_rr", "css_rg", "css_restart", "css_restartgame"] } },
{ "css_gag", new Command { Aliases = ["css_gag"] } },
{ "css_addgag", new Command { Aliases = ["css_addgag"] } },
{ "css_ungag", new Command { Aliases = ["css_ungag"] } },
{ "css_mute", new Command { Aliases = ["css_mute"] } },
{ "css_addmute", new Command { Aliases = ["css_addmute"] } },
{ "css_unmute", new Command { Aliases = ["css_unmute"] } },
{ "css_silence", new Command { Aliases = ["css_silence"] } },
{ "css_addsilence", new Command { Aliases = ["css_addsilence"] } },
{ "css_unsilence", new Command { Aliases = ["css_unsilence"] } },
{ "css_vote", new Command { Aliases = ["css_vote"] } },
{ "css_slay", new Command { Aliases = ["css_slay"] } },
{ "css_slap", new Command { Aliases = ["css_slap"] } },
{ "css_team", new Command { Aliases = ["css_team"] } },
{ "css_rename", new Command { Aliases = ["css_rename"] } },
{ "css_prename", new Command { Aliases = ["css_prename"] } },
{ "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"] } },
{ "css_adminvoice", new Command { Aliases = ["css_adminvoice", "css_listenall"] } }
}
};
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(commands, options);
File.WriteAllText(CommandsPath, json);
}
/// <summary>
/// Reads the command configuration JSON file and registers all commands and their aliases with their callbacks.
/// Also registers any custom commands previously stored.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
private static void Register()
{
var json = File.ReadAllText(CommandsPath);
var commandsConfig = JsonSerializer.Deserialize<CommandsConfig>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (commandsConfig?.Commands != null)
{
foreach (var command in commandsConfig.Commands)
{
if (command.Value.Aliases == null) continue;
CS2_SimpleAdmin._logger?.LogInformation(
$"Registering command: `{command.Key}` with aliases: `{string.Join(", ", command.Value.Aliases)}`");
var mapping = CommandMappings.FirstOrDefault(m => m.CommandKey == command.Key);
if (mapping == null || command.Value.Aliases.Length == 0) continue;
foreach (var alias in command.Value.Aliases)
{
CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback);
}
}
}
foreach (var (name, definitions) in _commandDefinitions)
{
foreach (var definition in definitions)
{
CS2_SimpleAdmin._logger?.LogInformation($"Registering custom command: `{name}`");
CS2_SimpleAdmin.Instance.AddCommand(name, definition.Description, definition.Callback);
}
}
}
/// <summary>
/// Represents the JSON configuration structure for commands.
/// </summary>
private class CommandsConfig
{
public Dictionary<string, Command>? Commands { get; init; }
}
/// <summary>
/// Represents a command definition containing a list of aliases.
/// </summary>
private class Command
{
public string[]? Aliases { get; init; }
}
/// <summary>
/// Maps a command key to its respective command callback handler.
/// </summary>
private class CommandMapping(string commandKey, CommandInfo.CommandCallback callback)
{
public string CommandKey { get; } = commandKey;
public CommandInfo.CommandCallback Callback { get; } = callback;
}
}

View File

@@ -0,0 +1,580 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.ValveConstants.Protobuf;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Handles the 'ban' command, allowing admins to ban one or more valid connected players.
/// </summary>
/// <param name="caller">The player issuing the ban command, or null for console.</param>
/// <param name="command">The command information including arguments.</param>
[RequiresPermissions("@css/ban")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
if (command.ArgCount < 2)
return;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, IsHLTV: false }).ToList();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{
return;
}
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Helper.ParsePenaltyTime(command.GetArg(2));
playersToTarget.ForEach(player =>
{
if (!caller.CanTarget(player)) return;
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_ban"] ?? "Ban"}: {player.PlayerName}", player,
ManagePlayersMenu.BanMenu);
return;
}
Ban(caller, player, time, reason, callerName, BanManager, command);
});
}
/// <summary>
/// Core logic to ban a specific player, scheduling database updates, notifications, and kicks.
/// </summary>
/// <param name="caller">The player issuing the ban, or null for console.</param>
/// <param name="player">The player to be banned.</param>
/// <param name="time">Ban duration in minutes; 0 means permanent.</param>
/// <param name="reason">Reason for the ban.</param>
/// <param name="callerName">Optional caller name string. If null, defaults to player name or console.</param>
/// <param name="banManager">Optional BanManager to handle ban persistence.</param>
/// <param name="command">Optional command info object for logging.</param>
/// <param name="silent">If true, suppresses command logging.</param>
internal void Ban(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, BanManager? banManager = null, CommandInfo? command = null, bool silent = false)
{
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidBan(caller, time)) return;
// Set default caller name if not provided
callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
// Get player and admin information
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle banning logic
Task.Run(async () =>
{
int? penaltyId = await BanManager.BanPlayer(playerInfo, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Ban, reason, time, penaltyId);
});
});
// Determine message keys and arguments based on ban time
var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = time == 0
? ("sa_player_ban_message_perm", "sa_admin_ban_message_perm",
[reason, "CALLER"],
["CALLER", player.PlayerName, reason])
: ("sa_player_ban_message_time", "sa_admin_ban_message_time",
new object[] { reason, time, "CALLER" },
new object[] { "CALLER", player.PlayerName, reason, time });
// Display center message to the player
Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs);
// Display admin activity message if necessary
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Schedule a kick timer
if (player.UserId.HasValue)
{
Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED, Config.OtherSettings.KickTime);
}
// Execute ban command if necessary
if (UnlockedCommands)
{
Server.ExecuteCommand($"banid 1 {new SteamID(player.SteamID).SteamId3}");
}
if (!silent)
{
if (command == null)
Helper.LogCommand(caller, $"css_ban {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
else
Helper.LogCommand(caller, command);
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Ban, _localizer);
}
/// <summary>
/// Adds a ban for a player by their SteamID, including offline bans.
/// </summary>
/// <param name="caller">The player issuing the ban command.</param>
/// <param name="steamid">SteamID of the player to ban.</param>
/// <param name="time">Ban duration in minutes (0 means permanent).</param>
/// <param name="reason">Reason for banning.</param>
/// <param name="banManager">Optional ban manager for database operations.</param>
internal void AddBan(CCSPlayerController? caller, SteamID steamid, int time, string reason, BanManager? banManager = null)
{
// Set default caller name if not provided
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Ban(caller, player, time, reason, callerName, silent: true);
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await BanManager.AddBanBySteamid(steamid.SteamId64, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Ban, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Ban, _localizer);
}
}
/// <summary>
/// Handles banning a player by specifying their SteamID via command.
/// </summary>
/// <param name="caller">The player issuing the command, or null if console.</param>
/// <param name="command">Command information including arguments (SteamID, time, reason).</param>
[RequiresPermissions("@css/ban")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
{
command.ReplyToCommand("Invalid SteamID64.");
return;
}
var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidBan(caller, time)) return;
var adminInfo = caller != null && caller.UserId.HasValue
? PlayersInfo[caller.SteamID]
: null;
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Ban(caller, player, time, reason, callerName, silent: true);
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
}
else
{
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await BanManager.AddBanBySteamid(steamid, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Ban, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Ban, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Ban has been added offline.");
}
Helper.LogCommand(caller, command);
if (UnlockedCommands)
Server.ExecuteCommand($"banid 1 {steamId.SteamId3}");
}
/// <summary>
/// Handles banning a player by their IP address, supporting offline banning if player is not online.
/// </summary>
/// <param name="caller">The player issuing the ban command.</param>
/// <param name="command">The command containing the IP, time, and reason arguments.</param>
[RequiresPermissions("@css/ban")]
[CommandHelper(minArgs: 1, usage: "<ip> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnBanIpCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
var ipAddress = command.GetArg(1);
if (!Helper.IsValidIp(ipAddress))
{
command.ReplyToCommand($"Invalid IP address.");
return;
}
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidBan(caller, time)) return;
var adminInfo = caller != null && caller.UserId.HasValue
? PlayersInfo[caller.SteamID]
: null;
var players = Helper.GetPlayerFromIp(ipAddress);
if (players.Count >= 1)
{
foreach (var player in players)
{
if (player == null || !player.IsValid) continue;
if (!caller.CanTarget(player))
return;
Ban(caller, player, time, reason, callerName, silent: true);
}
}
else
{
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
await BanManager.AddBanByIp(ipAddress, adminInfo, reason, time);
});
command.ReplyToCommand($"Player with ip {ipAddress} is not online. Ban has been added offline.");
}
Helper.LogCommand(caller, command);
}
/// <summary>
/// Checks whether the ban duration is valid based on the caller's permissions and configured limits.
/// </summary>
/// <param name="caller">The player issuing the ban command.</param>
/// <param name="duration">Requested ban duration in minutes.</param>
/// <returns>True if ban duration is valid; otherwise, false.</returns>
private bool CheckValidBan(CCSPlayerController? caller, int duration)
{
if (caller == null) return true;
var canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban");
if (duration <= 0 && !canPermBan)
{
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}");
return false;
}
if (duration <= Config.OtherSettings.MaxBanDuration || canPermBan) return true;
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_max_duration_exceeded", Config.OtherSettings.MaxBanDuration]}");
return false;
}
/// <summary>
/// Handles unbanning players by pattern (steamid, name, or IP).
/// </summary>
/// <param name="caller">The player issuing the unban command.</param>
/// <param name="command">Command containing target pattern and optional reason.</param>
[RequiresPermissions("@css/unban")]
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnbanCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
if (command.GetArg(1).Length <= 1)
{
command.ReplyToCommand($"Too short pattern to search.");
return;
}
var pattern = command.GetArg(1);
var reason = command.ArgCount >= 2
? string.Join(" ", Enumerable.Range(2, command.ArgCount - 2).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
Task.Run(async () => await BanManager.UnbanPlayer(pattern, callerSteamId, reason));
Helper.LogCommand(caller, command);
command.ReplyToCommand($"Unbanned player with pattern {pattern}.");
}
/// <summary>
/// Handles warning players, supporting multiple targets and warning durations.
/// </summary>
/// <param name="caller">The player issuing the warn command.</param>
/// <param name="command">The command containing target, time, and reason parameters.</param>
[RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnWarnCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null)
return;
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
if (command.ArgCount < 2)
return;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected && !player.IsHLTV).ToList();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{
return;
}
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
Warn(caller, player, time, reason, callerName, command);
}
});
}
/// <summary>
/// Issues a warning penalty to a specific player with optional duration and reason.
/// </summary>
/// <param name="caller">The player issuing the warning.</param>
/// <param name="player">The player to warn.</param>
/// <param name="time">Duration of the warning in minutes.</param>
/// <param name="reason">Reason for the warning.</param>
/// <param name="callerName">Optional display name of the caller.</param>
/// <param name="command">Optional command info for logging.</param>
internal void Warn(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null)
{
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidBan(caller, time)) return;
// Set default caller name if not provided
callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
// Freeze player pawn if alive
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
player.PlayerPawn?.Value?.Freeze();
AddTimer(5.0f, () => player.PlayerPawn?.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
// Get player and admin information
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle warning logic
Task.Run(async () =>
{
int? penaltyId = await WarnManager.WarnPlayer(playerInfo, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Warn, reason, time,
penaltyId);
});
// Check for warn thresholds and execute punish command if applicable
var totalWarns = await WarnManager.GetPlayerWarnsCount(player.SteamID);
if (Config.WarnThreshold.Count > 0)
{
string? punishCommand = null;
var lastKey = Config.WarnThreshold.Keys.Max();
if (totalWarns >= lastKey)
punishCommand = Config.WarnThreshold[lastKey];
else if (Config.WarnThreshold.TryGetValue(totalWarns, out var value))
punishCommand = value;
if (!string.IsNullOrEmpty(punishCommand))
{
await Server.NextWorldUpdateAsync(() =>
{
Server.ExecuteCommand(punishCommand.Replace("USERID", playerInfo.UserId.ToString()).Replace("STEAMID64", playerInfo.SteamId?.ToString()));
});
}
}
});
// Determine message keys and arguments based on warning time
var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = time == 0
? ("sa_player_warn_message_perm", "sa_admin_warn_message_perm",
new object[] { reason, "CALLER" },
new object[] { "CALLER", player.PlayerName, reason })
: ("sa_player_warn_message_time", "sa_admin_warn_message_time",
[reason, time, "CALLER"],
["CALLER", player.PlayerName, reason, time]);
// Display center message to the playser
Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs);
// Display admin activity message if necessary
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Log the warning command
if (command == null)
Helper.LogCommand(caller, $"css_warn {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
else
Helper.LogCommand(caller, command);
// Send Discord notification for the warning
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Warn, _localizer);
}
/// <summary>
/// Adds a warning to a player by their SteamID, including support for offline players.
/// </summary>
/// <param name="caller">The player issuing the warn command.</param>
/// <param name="steamid">SteamID of the player to warn.</param>
/// <param name="time">Warning duration in minutes.</param>
/// <param name="reason">Reason for the warning.</param>
/// <param name="warnManager">Optional warn manager instance.</param>
internal void AddWarn(CCSPlayerController? caller, SteamID steamid, int time, string reason, WarnManager? warnManager = null)
{
// Set default caller name if not provided
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Warn(caller, player, time, reason, callerName);
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await WarnManager.AddWarnBySteamid(steamid.SteamId64, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Warn, reason, time,
penaltyId);
});
// Check for warn thresholds and execute punish command if applicable
var totalWarns = await WarnManager.GetPlayerWarnsCount(steamid.SteamId64);
if (Config.WarnThreshold.Count > 0)
{
string? punishCommand = null;
var lastKey = Config.WarnThreshold.Keys.Max();
if (totalWarns >= lastKey)
punishCommand = Config.WarnThreshold[lastKey];
else if (Config.WarnThreshold.TryGetValue(totalWarns, out var value))
punishCommand = value;
if (!string.IsNullOrEmpty(punishCommand))
{
await Server.NextWorldUpdateAsync(() =>
{
Server.ExecuteCommand(punishCommand.Replace("STEAMID64", steamid.SteamId64.ToString()));
});
}
}
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Warn, _localizer);
}
}
/// <summary>
/// Handles removing a warning (unwarn) by a pattern string.
/// </summary>
/// <param name="caller">The player issuing the unwarn command.</param>
/// <param name="command">The command containing target pattern.</param>
[RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnwarnCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
if (command.GetArg(1).Length <= 1)
{
command.ReplyToCommand($"Too short pattern to search.");
return;
}
var pattern = command.GetArg(1);
Task.Run(async () => await WarnManager.UnwarnPlayer(pattern));
Helper.LogCommand(caller, command);
command.ReplyToCommand($"Unwarned player with pattern {pattern}.");
}
}

View File

@@ -0,0 +1,150 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using System.Text;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Sends a chat message only to admins that have chat permission.
/// The message is encoded properly to handle UTF-8 characters.
/// </summary>
/// <param name="caller">The admin player sending the message, or null for console.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminToAdminSayCommand(CCSPlayerController? caller, CommandInfo command)
{
Helper.LogCommand(caller, command);
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
foreach (var player in Helper.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
{
if (_localizer != null)
player.PrintToChat(_localizer["sa_adminchat_template_admin",
caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName,
utf8String]);
}
}
/// <summary>
/// Sends a custom chat message to all players with color tags processed.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminCustomSayCommand(CCSPlayerController? caller, CommandInfo command)
{
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0) return;
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
foreach (var player in Helper.GetValidPlayers())
{
player.PrintToChat(utf8String.ReplaceColorTags());
}
}
/// <summary>
/// Sends a chat message to all players with localization prefix and color tags handled.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminSayCommand(CCSPlayerController? caller, CommandInfo command)
{
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0) return;
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
foreach (var player in Helper.GetValidPlayers())
{
player.SendLocalizedMessage(_localizer,
"sa_adminsay_prefix",
utf8String.ReplaceColorTags());
}
}
/// <summary>
/// Sends a private chat message from the caller to the specified target player(s).
/// </summary>
/// <param name="caller">The admin or console sending the private message.</param>
/// <param name="command">The command input containing target and message.</param>
[CommandHelper(2, "<#userid or name> <message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminPrivateSayCommand(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();
//Helper.LogCommand(caller, command);
var range = command.GetArg(0).Length + command.GetArg(1).Length + 2;
var message = command.GetCommandString[range..];
var utf8BytesString = Encoding.UTF8.GetBytes(message);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
playersToTarget.ForEach(player =>
{
player.PrintToChat($"({callerName}) {utf8String}".ReplaceColorTags());
});
command.ReplyToCommand($" Private message sent!");
}
/// <summary>
/// Broadcasts a center-screen message to all players.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminCenterSayCommand(CCSPlayerController? caller, CommandInfo command)
{
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
Helper.PrintToCenterAll(utf8String.ReplaceColorTags());
}
/// <summary>
/// Sends a HUD alert message to all players.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminHudSayCommand(CCSPlayerController? caller, CommandInfo command)
{
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
VirtualFunctions.ClientPrintAll(
HudDestination.Alert,
utf8String.ReplaceColorTags(),
0, 0, 0, 0, 0);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,973 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Processes the 'gag' command, applying a muted penalty to target players with optional time and reason.
/// </summary>
/// <param name="caller">The player issuing the gag command or null for console.</param>
/// <param name="command">The command input containing targets, time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnGagCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
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();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{
return;
}
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Helper.ParsePenaltyTime(command.GetArg(2));
playersToTarget.ForEach(player =>
{
if (!caller!.CanTarget(player)) return;
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_gag"] ?? "Gag"}: {player.PlayerName}", player,
ManagePlayersMenu.GagMenu);
return;
}
Gag(caller, player, time, reason, callerName, command);
});
}
/// <summary>
/// Applies the gag penalty logic to an individual player, performing permission checks, notification, and logging.
/// </summary>
/// <param name="caller">The player issuing the gag.</param>
/// <param name="player">The player to gag.</param>
/// <param name="time">Duration of the gag in minutes, 0 is permanent.</param>
/// <param name="reason">Reason for the gag.</param>
/// <param name="callerName">Optional caller name for notifications.</param>
/// <param name="command">Optional command info for logging.</param>
/// <param name="silent">If true, suppresses logging notifications.</param>
internal void Gag(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false)
{
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return;
// Set default caller name if not provided
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle gag logic
Task.Run(async () =>
{
int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Gag, reason, time,
penaltyId);
});
});
// Add penalty to the player's penalty manager
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Gag, Time.ActualDateTime().AddMinutes(time), time);
// Determine message keys and arguments based on gag time (permanent or timed)
var (messageKey, activityMessageKey, playerArgs, adminActivityArgs) = time == 0
? ("sa_player_gag_message_perm", "sa_admin_gag_message_perm",
[reason, "CALLER"],
["CALLER", player.PlayerName, reason])
: ("sa_player_gag_message_time", "sa_admin_gag_message_time",
new object[] { reason, time, "CALLER" },
new object[] { "CALLER", player.PlayerName, reason, time });
// Display center message to the gagged player
Helper.DisplayCenterMessage(player, messageKey, callerName, playerArgs);
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Increment the player's total gags count
PlayersInfo[player.SteamID].TotalGags++;
// Log the gag command and send Discord notification
if (!silent)
{
if (command == null)
Helper.LogCommand(caller, $"css_gag {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
else
Helper.LogCommand(caller, command);
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Gag, _localizer);
}
/// <summary>
/// Adds a gag penalty to a player identified by SteamID, supporting offline players.
/// </summary>
/// <param name="caller">The player issuing the command or null for console.</param>
/// <param name="steamid">SteamID of the target player.</param>
/// <param name="time">Duration in minutes (0 for permanent).</param>
/// <param name="reason">Reason for the gag.</param>
internal void AddGag(CCSPlayerController? caller, SteamID steamid, int time, string reason)
{
// Set default caller name if not provided
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Gag(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64, adminInfo, reason, time, 3);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Gag, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Gag, _localizer);
}
}
/// <summary>
/// Handles the 'addgag' command, which adds a gag penalty to a player specified by SteamID.
/// </summary>
/// <param name="caller">The player issuing the command or null for console.</param>
/// <param name="command">Command input that includes SteamID, optional time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddGagCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
// Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Validate command arguments
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
// Validate and extract SteamID
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
{
command.ReplyToCommand("Invalid SteamID64.");
return;
}
var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidMute(caller, time)) return;
// Get player and admin info
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Attempt to match player based on SteamID
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
// Check if caller can target the player
if (!caller.CanTarget(player)) return;
// Perform the gag for an online player
Gag(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous gag operation for offline players
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Gag, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Gag, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Gag has been added offline.");
}
// Log the gag command and respond to the command
Helper.LogCommand(caller, command);
}
/// <summary>
/// Handles removing a gag penalty ('ungag') of a player, either by SteamID or pattern match.
/// </summary>
/// <param name="caller">The player issuing the ungag command or null for console.</param>
/// <param name="command">Command input containing SteamID or player name and optional reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUngagCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1);
var reason = command.ArgCount >= 2
? string.Join(" ", Enumerable.Range(2, command.ArgCount - 2).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
if (pattern.Length <= 1)
{
command.ReplyToCommand($"Too short pattern to search.");
return;
}
Helper.LogCommand(caller, command);
// Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{
var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
if (player != null && player.IsValid)
{
PlayerPenaltyManager.RemovePenaltiesByType(player.Slot, PenaltyType.Gag);
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(player.SteamID.ToString(), callerSteamId, reason);
});
command.ReplyToCommand($"Ungaged player {player.PlayerName}.");
return;
}
}
// If not a valid SteamID64, check by player name
var nameMatches = Helper.GetPlayerFromName(pattern);
var namePlayer = nameMatches.Count == 1 ? nameMatches.FirstOrDefault() : null;
if (namePlayer != null && namePlayer.IsValid)
{
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Gag);
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalGags > 0)
PlayersInfo[namePlayer.SteamID].TotalGags--;
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(namePlayer.SteamID.ToString(), callerSteamId, reason);
});
command.ReplyToCommand($"Ungaged player {namePlayer.PlayerName}.");
}
else
{
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(pattern, callerSteamId, reason);
});
command.ReplyToCommand($"Ungaged offline player with pattern {pattern}.");
}
}
/// <summary>
/// Processes the 'mute' command, applying a voice mute penalty to target players with optional time and reason.
/// </summary>
/// <param name="caller">The player issuing the mute command or null for console.</param>
/// <param name="command">The command input containing targets, time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnMuteCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
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();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{
return;
}
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Helper.ParsePenaltyTime(command.GetArg(2));
playersToTarget.ForEach(player =>
{
if (!caller!.CanTarget(player)) return;
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_mute"] ?? "Mute"}: {player.PlayerName}", player,
ManagePlayersMenu.MuteMenu);
return;
}
Mute(caller, player, time, reason, callerName, command);
});
}
/// <summary>
/// Applies the mute penalty logic to an individual player, handling permissions, notifications, logging, and countdown timers.
/// </summary>
/// <param name="caller">The player issuing the mute.</param>
/// <param name="player">The player to mute.</param>
/// <param name="time">Duration in minutes, 0 indicates permanent mute.</param>
/// <param name="reason">Reason for the mute.</param>
/// <param name="callerName">Optional caller name for notification messages.</param>
/// <param name="command">Optional command info for logging.</param>
/// <param name="silent">If true, suppresses some logging.</param>
internal void Mute(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false)
{
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return;
// Set default caller name if not provided
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Set player's voice flags to muted
player.VoiceFlags = VoiceFlags.Muted;
// Asynchronously handle mute logic
Task.Run(async () =>
{
int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time, 1);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Mute, reason, time,
penaltyId);
});
});
// Add penalty to the player's penalty manager
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Mute, Time.ActualDateTime().AddMinutes(time), time);
// Determine message keys and arguments based on mute time (permanent or timed)
var (messageKey, activityMessageKey, playerArgs, adminActivityArgs) = time == 0
? ("sa_player_mute_message_perm", "sa_admin_mute_message_perm",
[reason, "CALLER"],
["CALLER", player.PlayerName, reason])
: ("sa_player_mute_message_time", "sa_admin_mute_message_time",
new object[] { reason, time, "CALLER" },
new object[] { "CALLER", player.PlayerName, reason, time });
// Display center message to the muted player
Helper.DisplayCenterMessage(player, messageKey, callerName, playerArgs);
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Increment the player's total mutes count
PlayersInfo[player.SteamID].TotalMutes++;
// Log the mute command and send Discord notification
if (!silent)
{
if (command == null)
Helper.LogCommand(caller, $"css_mute {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
else
Helper.LogCommand(caller, command);
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Mute, _localizer);
}
/// <summary>
/// Handles the 'addmute' command that adds a mute penalty to a player specified by SteamID.
/// </summary>
/// <param name="caller">The player issuing the command or null for console.</param>
/// <param name="command">Command input includes SteamID, optional time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddMuteCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
// Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Validate command arguments
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
// Validate and extract SteamID
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
{
command.ReplyToCommand("Invalid SteamID64.");
return;
}
var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidMute(caller, time)) return;
// Get player and admin info
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Attempt to match player based on SteamID
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
// Check if caller can target the player
if (!caller.CanTarget(player)) return;
// Perform the mute for an online player
Mute(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous mute operation for offline players
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time, 1);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Mute, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Mute, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Mute has been added offline.");
}
// Log the mute command and respond to the command
Helper.LogCommand(caller, command);
}
/// <summary>
/// Asynchronously adds a mute penalty to a player by Steam ID. Handles both online and offline players.
/// </summary>
/// <param name="caller">The admin/player issuing the mute.</param>
/// <param name="steamid">The Steam ID of the player to mute.</param>
/// <param name="time">Duration of the mute in minutes.</param>
/// <param name="reason">Reason for the mute.</param>
/// <param name="muteManager">Optional mute manager instance for handling database ops.</param>
internal void AddMute(CCSPlayerController? caller, SteamID steamid, int time, string reason, MuteManager? muteManager = null)
{
// Set default caller name if not provided
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Mute(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64, adminInfo, reason, time, 1);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Mute, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Mute, _localizer);
}
}
/// <summary>
/// Handles the unmute command - removes mute penalty from player identified by SteamID or name.
/// Can target both online and offline players.
/// </summary>
/// <param name="caller">The admin/player issuing the unmute.</param>
/// <param name="command">The command arguments including target identifier and optional reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnmuteCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1);
var reason = command.ArgCount >= 2
? string.Join(" ", Enumerable.Range(2, command.ArgCount - 2).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
if (pattern.Length <= 1)
{
command.ReplyToCommand("Too short pattern to search.");
return;
}
Helper.LogCommand(caller, command);
// Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{
var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
if (player != null && player.IsValid)
{
PlayerPenaltyManager.RemovePenaltiesByType(player.Slot, PenaltyType.Mute);
player.VoiceFlags = VoiceFlags.Normal;
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(player.SteamID.ToString(), callerSteamId, reason, 1);
});
command.ReplyToCommand($"Unmuted player {player.PlayerName}.");
return;
}
}
// If not a valid SteamID64, check by player name
var nameMatches = Helper.GetPlayerFromName(pattern);
var namePlayer = nameMatches.Count == 1 ? nameMatches.FirstOrDefault() : null;
if (namePlayer != null && namePlayer.IsValid)
{
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Mute);
namePlayer.VoiceFlags = VoiceFlags.Normal;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalMutes > 0)
PlayersInfo[namePlayer.SteamID].TotalMutes--;
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(namePlayer.SteamID.ToString(), callerSteamId, reason, 1);
});
command.ReplyToCommand($"Unmuted player {namePlayer.PlayerName}.");
}
else
{
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(pattern, callerSteamId, reason, 1);
});
command.ReplyToCommand($"Unmuted offline player with pattern {pattern}.");
}
}
/// <summary>
/// Issue a 'silence' penalty to a player - disables voice communication.
/// Handles online and offline players, with duration and reason specified.
/// </summary>
/// <param name="caller">The admin/player issuing the silence.</param>
/// <param name="command">Command containing target, duration, and optional reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSilenceCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
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();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{
return;
}
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Helper.ParsePenaltyTime(command.GetArg(2));
playersToTarget.ForEach(player =>
{
if (!caller!.CanTarget(player)) return;
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_silence"] ?? "Silence"}: {player.PlayerName}", player,
ManagePlayersMenu.SilenceMenu);
return;
}
Silence(caller, player, time, reason, callerName, command);
});
}
/// <summary>
/// Applies silence logical processing for a player - updates database and notifies.
/// </summary>
/// <param name="caller">Admin/player applying the silence.</param>
/// <param name="player">Target player.</param>
/// <param name="time">Duration of silence.</param>
/// <param name="reason">Reason for silence.</param>
/// <param name="callerName">Optional name of silent admin or console.</param>
/// <param name="command">Optional command details for logging.</param>
/// <param name="silent">If true, suppresses logging notifications.</param>
internal void Silence(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false)
{
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return;
// Set default caller name if not provided
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle silence logic
Task.Run(async () =>
{
int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time, 2);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Silence, reason, time,
penaltyId);
});
});
// Add penalty to the player's penalty manager
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Silence, Time.ActualDateTime().AddMinutes(time), time);
player.VoiceFlags = VoiceFlags.Muted;
// Determine message keys and arguments based on silence time (permanent or timed)
var (messageKey, activityMessageKey, playerArgs, adminActivityArgs) = time == 0
? ("sa_player_silence_message_perm", "sa_admin_silence_message_perm",
[reason, "CALLER"],
["CALLER", player.PlayerName, reason])
: ("sa_player_silence_message_time", "sa_admin_silence_message_time",
new object[] { reason, time, "CALLER" },
new object[] { "CALLER", player.PlayerName, reason, time });
// Display center message to the silenced player
Helper.DisplayCenterMessage(player, messageKey, callerName, playerArgs);
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Increment the player's total silences count
PlayersInfo[player.SteamID].TotalSilences++;
// Log the silence command and send Discord notification
if (!silent)
{
if (command == null)
Helper.LogCommand(caller, $"css_silence {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
else
Helper.LogCommand(caller, command);
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Silence, _localizer);
}
/// <summary>
/// Handles the 'AddSilence' command, applying a silence penalty to a player specified by SteamID,
/// with support for offline player penalties.
/// </summary>
/// <param name="caller">The player/admin issuing the command.</param>
/// <param name="command">The command input containing SteamID, optional time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddSilenceCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
// Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Validate command arguments
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
// Validate and extract SteamID
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
{
command.ReplyToCommand("Invalid SteamID64.");
return;
}
var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidMute(caller, time)) return;
// Get player and admin info
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Attempt to match player based on SteamID
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
// Check if caller can target the player
if (!caller.CanTarget(player)) return;
// Perform the silence for an online player
Silence(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous silence operation for offline players
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time, 2);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Silence, reason,
time, penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Silence, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Silence has been added offline.");
}
// Log the silence command and respond to the command
Helper.LogCommand(caller, command);
}
/// <summary>
/// Adds a silence penalty to a player by Steam ID. Manages both online and offline player cases.
/// </summary>
/// <param name="caller">Admin/player initiating the silence.</param>
/// <param name="steamid">Steam ID of player.</param>
/// <param name="time">Duration of silence.</param>
/// <param name="reason">Reason for the penalty.</param>
/// <param name="muteManager">Optional mute manager for DB operations.</param>
internal void AddSilence(CCSPlayerController? caller, SteamID steamid, int time, string reason, MuteManager? muteManager = null)
{
// Set default caller name if not provided
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Silence(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64, adminInfo, reason, time, 2);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Silence, reason,
time, penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Silence, _localizer);
}
}
/// <summary>
/// Removes the silence penalty from a player, either by SteamID, name, or offline pattern.
/// Resets voice settings and updates notices accordingly.
/// </summary>
/// <param name="caller">Admin/player issuing the unsilence.</param>
/// <param name="command">Command arguments with target pattern and optional reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnsilenceCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1);
var reason = command.ArgCount >= 2
? string.Join(" ", Enumerable.Range(2, command.ArgCount - 2).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
if (pattern.Length <= 1)
{
command.ReplyToCommand("Too short pattern to search.");
return;
}
Helper.LogCommand(caller, command);
// Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{
var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
if (player != null && player.IsValid)
{
PlayerPenaltyManager.RemovePenaltiesByType(player.Slot, PenaltyType.Silence);
// Reset voice flags to normal
player.VoiceFlags = VoiceFlags.Normal;
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(player.SteamID.ToString(), callerSteamId, reason, 2); // Unmute by type 2 (silence)
});
command.ReplyToCommand($"Unsilenced player {player.PlayerName}.");
return;
}
}
// If not a valid SteamID64, check by player name
var nameMatches = Helper.GetPlayerFromName(pattern);
var namePlayer = nameMatches.Count == 1 ? nameMatches.FirstOrDefault() : null;
if (namePlayer != null && namePlayer.IsValid)
{
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Silence);
// Reset voice flags to normal
namePlayer.VoiceFlags = VoiceFlags.Normal;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalSilences > 0)
PlayersInfo[namePlayer.SteamID].TotalSilences--;
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(namePlayer.SteamID.ToString(), callerSteamId, reason, 2); // Unmute by type 2 (silence)
});
command.ReplyToCommand($"Unsilenced player {namePlayer.PlayerName}.");
}
else
{
Task.Run(async () =>
{
await MuteManager.UnmutePlayer(pattern, callerSteamId, reason, 2); // Unmute by type 2 (silence)
});
command.ReplyToCommand($"Unsilenced offline player with pattern {pattern}.");
}
}
/// <summary>
/// Validates mute penalty duration based on admin privileges and configured max duration.
/// </summary>
/// <param name="caller">Admin/player issuing the mute.</param>
/// <param name="duration">Requested duration in minutes.</param>
/// <returns>True if mute penalty duration is allowed; false otherwise.</returns>
private bool CheckValidMute(CCSPlayerController? caller, int duration)
{
if (caller == null) return true;
var canPermMute = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permmute");
if (duration <= 0 && !canPermMute)
{
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}");
return false;
}
if (duration <= Config.OtherSettings.MaxMuteDuration || canPermMute) return true;
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_max_duration_exceeded", Config.OtherSettings.MaxMuteDuration]}");
return false;
}
}

View File

@@ -0,0 +1,98 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Menu;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Handles the vote command, creates voting menu for players, and collects answers.
/// Displays results after timeout and resets voting state.
/// </summary>
/// <param name="caller">The player/admin who initiated the vote, or null for console.</param>
/// <param name="command">Command object containing question and options.</param>
[RequiresPermissions("@css/generic")]
[CommandHelper(minArgs: 2, usage: "<question> [... options ...]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnVoteCommand(CCSPlayerController? caller, CommandInfo command)
{
if (command.ArgCount < 2 || _localizer == null)
return;
Helper.LogCommand(caller, command);
VoteAnswers.Clear();
var question = command.GetArg(1);
var answersCount = command.ArgCount;
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
for (var i = 2; i <= answersCount - 1; i++)
{
VoteAnswers.Add(command.GetArg(i), 0);
}
foreach (var player in Helper.GetValidPlayers())
{
using (new WithTemporaryCulture(player.GetLanguage()))
{
IMenu? voteMenu = Helper.CreateMenu(_localizer["sa_admin_vote_menu_title", question]);
if (voteMenu == null)
return;
//ChatMenu voteMenu = new(_localizer!["sa_admin_vote_menu_title", question]);
for (var i = 2; i <= answersCount - 1; i++)
{
voteMenu.AddMenuOption(command.GetArg(i), Helper.HandleVotes);
}
voteMenu.PostSelectAction = PostSelectAction.Close;
Helper.PrintToCenterAll(_localizer["sa_admin_vote_message", caller == null ? _localizer["sa_console"] : caller.PlayerName, question]);
player.SendLocalizedMessage(_localizer,
"sa_admin_vote_message",
caller == null ? _localizer["sa_console"] : caller.PlayerName,
question);
voteMenu.Open(player);
//MenuManager.OpenChatMenu(player, voteMenu);
}
}
VoteInProgress = true;
}
if (VoteInProgress)
{
AddTimer(30, () =>
{
foreach (var player in Helper.GetValidPlayers())
{
if (_localizer != null)
player.SendLocalizedMessage(_localizer,
"sa_admin_vote_message_results",
question);
}
foreach (var (key, value) in VoteAnswers)
{
foreach (var player in Helper.GetValidPlayers())
{
if (_localizer != null)
player.SendLocalizedMessage(_localizer,
"sa_admin_vote_message_results_answer",
key,
value);
}
}
VoteAnswers.Clear();
VoteInProgress = false;
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
}
}

View File

@@ -0,0 +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)}");
// }
// }

View File

@@ -0,0 +1,625 @@
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Executes the 'slay' command, forcing the targeted players to commit suicide.
/// 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 OnSlayCommand(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 =>
{
Slay(caller, player, callerName, command);
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Performs the actual slay action on a player, with notification and logging.
/// </summary>
/// <param name="caller">Admin or console issuing the slay.</param>
/// <param name="player">Target player to slay.</param>
/// <param name="callerName">Optional name to display as the slayer.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void Slay(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Make the player commit suicide
player.CommitSuicide(false, true);
player.EmitSound("Player.Death");
// Determine message keys and arguments for the slay notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_slay_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_slay {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
/// <summary>
/// Applies damage as a slap effect to the targeted players.
/// </summary>
/// <param name="caller">The player/admin executing the slap command.</param>
/// <param name="command">The command including targets and optional damage value.</param>
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [damage]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSlapCommand(CCSPlayerController? caller, CommandInfo command)
{
var damage = 0;
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();
if (command.ArgCount >= 2)
{
int.TryParse(command.GetArg(2), out damage);
}
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
Slap(caller, player, damage, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Applies slap damage to a specific player with notifications and logging.
/// </summary>
/// <param name="caller">The player/admin applying the slap effect.</param>
/// <param name="player">The target player to slap.</param>
/// <param name="damage">The damage amount to apply.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void Slap(CCSPlayerController? caller, CCSPlayerController player, int damage, 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";
// Apply slap damage to the player
player.Pawn.Value?.Slap(damage);
player.EmitSound("Player.DamageFall");
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_slap {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {damage}");
// Determine message key and arguments for the slap notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_slap_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
if (_localizer != null)
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Changes the team of targeted players with optional kill on switch.
/// </summary>
/// <param name="caller">The player/admin issuing the command.</param>
/// <param name="command">The command containing targets, team info, and optional kill flag.</param>
[RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 2, usage: "<#userid or name> [<ct/tt/spec>] [-k]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnTeamCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var teamName = command.GetArg(2).ToLower();
string _teamName;
var teamNum = CsTeam.Spectator;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
switch (teamName)
{
case "ct":
case "counterterrorist":
teamNum = CsTeam.CounterTerrorist;
_teamName = "CT";
break;
case "t":
case "tt":
case "terrorist":
teamNum = CsTeam.Terrorist;
_teamName = "TT";
break;
case "swap":
_teamName = "SWAP";
break;
default:
teamNum = CsTeam.Spectator;
_teamName = "SPEC";
break;
}
var kill = command.GetArg(3).ToLower().Equals("-k");
playersToTarget.ForEach(player =>
{
ChangeTeam(caller, player, _teamName, teamNum, kill, command);
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Changes the team of a player with various conditions and logs the operation.
/// </summary>
/// <param name="caller">The player/admin issuing the change.</param>
/// <param name="player">The target player.</param>
/// <param name="teamName">Team name string.</param>
/// <param name="teamNum">Team enumeration value.</param>
/// <param name="kill">If true, kills player on team change.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void ChangeTeam(CCSPlayerController? caller, CCSPlayerController player, string teamName, CsTeam teamNum, bool kill, CommandInfo? command = null)
{
// Check if the player is valid and connected
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected)
return;
// Ensure the caller can target the player
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Change team based on the provided teamName and conditions
if (!teamName.Equals("swap", StringComparison.OrdinalIgnoreCase))
{
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && teamNum != CsTeam.Spectator && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
player.SwitchTeam(teamNum);
else
player.ChangeTeam(teamNum);
}
else
{
if (player.TeamNum != (byte)CsTeam.Spectator)
{
var _teamNum = (CsTeam)player.TeamNum == CsTeam.Terrorist ? CsTeam.CounterTerrorist : CsTeam.Terrorist;
teamName = _teamNum == CsTeam.Terrorist ? "TT" : "CT";
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
player.SwitchTeam(_teamNum);
else
player.ChangeTeam(_teamNum);
}
}
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_team {player.PlayerName} {teamName}");
// Determine message key and arguments for the team change notification
var activityMessageKey = "sa_admin_team_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, teamName };
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
/// <summary>
/// Renames targeted players to a new name.
/// </summary>
/// <param name="caller">The admin issuing the rename command.</param>
/// <param name="command">The command including targets and new name.</param>
[CommandHelper(1, "<#userid or name> <new name>")]
[RequiresPermissions("@css/kick")]
public void OnRenameCommand(CCSPlayerController? caller, CommandInfo command)
{
// Set default caller name if not provided
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get the new name from the command arguments
var newName = command.GetArg(2);
// Check if the new name is valid
if (string.IsNullOrEmpty(newName))
return;
// Retrieve the targets based on the command
var targets = GetTarget(command);
if (targets == null) return;
// Filter out valid players from the targets
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
// Log the command
Helper.LogCommand(caller, command);
// Process each player to rename
playersToTarget.ForEach(player =>
{
// Check if the player is connected and can be targeted
if (player.Connected != PlayerConnectedState.PlayerConnected || !caller!.CanTarget(player))
return;
// Determine message key and arguments for the rename notification
var activityMessageKey = "sa_admin_rename_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// Rename the player
player.Rename(newName);
});
}
/// <summary>
/// Renames permamently targeted players to a new name.
/// </summary>
/// <param name="caller">The admin issuing the pre-rename command.</param>
/// <param name="command">The command containing targets and new alias.</param>
[CommandHelper(1, "<#userid or name> <new name>")]
[RequiresPermissions("@css/ban")]
public void OnPrenameCommand(CCSPlayerController? caller, CommandInfo command)
{
// Set default caller name if not provided
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get the new name from the command arguments
var newName = command.GetArg(2);
// Retrieve the targets based on the command
var targets = GetTarget(command);
if (targets == null) return;
// Filter out valid players from the targets
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
// Log the command
Helper.LogCommand(caller, command);
// Process each player to rename
playersToTarget.ForEach(player =>
{
// Check if the player is connected and can be targeted
if (player.Connected != PlayerConnectedState.PlayerConnected || !caller!.CanTarget(player))
return;
// Determine message key and arguments for the rename notification
var activityMessageKey = "sa_admin_rename_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
// Display admin activity message to other players
if (caller != null && !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Determine if the new name is valid and update the renamed players list
if (!string.IsNullOrEmpty(newName))
{
RenamedPlayers[player.SteamID] = newName;
player.Rename(newName);
}
else
{
RenamedPlayers.Remove(player.SteamID);
}
});
}
/// <summary>
/// Teleports targeted player(s) to another player's location.
/// </summary>
/// <param name="caller">Admin issuing teleport command.</param>
/// <param name="command">Command containing teleport targets and destination.</param>
[CommandHelper(1, "<#userid or name> [#userid or name]")]
[RequiresPermissions("@css/kick")]
public void OnGotoCommand(CCSPlayerController? caller, CommandInfo command)
{
IEnumerable<CCSPlayerController> playersToTeleport;
CCSPlayerController? destinationPlayer;
var targets = GetTarget(command);
if (command.ArgCount < 3)
{
if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
return;
if (targets == null || targets.Count() != 1)
return;
destinationPlayer = targets.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null || !caller.CanTarget(destinationPlayer) || caller.PlayerPawn.Value == null)
return;
playersToTeleport = [caller];
}
else
{
var destination = GetTarget(command, 2);
if (targets == null || destination == null || destination.Count() != 1)
return;
destinationPlayer = destination.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null)
return;
playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p))
.ToList();
if (!playersToTeleport.Any())
return;
}
// Log command
Helper.LogCommand(caller, command);
foreach (var player in playersToTeleport)
{
if (player.PlayerPawn?.Value == null || destinationPlayer?.PlayerPawn?.Value == null)
continue;
player.TeleportPlayer(destinationPlayer);
player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
AddTimer(4, () =>
{
if (player is { IsValid: true, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
}
if (destinationPlayer.IsValid && destinationPlayer.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
}
});
if (caller != null && !SilentPlayers.Contains(caller.Slot) && _localizer != null)
{
Helper.ShowAdminActivity("sa_admin_tp_message", player.PlayerName, false, "CALLER", destinationPlayer.PlayerName);
}
}
}
/// <summary>
/// Brings targeted player(s) to the caller or specified destination player's location.
/// </summary>
/// <param name="caller">Player issuing the bring command.</param>
/// <param name="command">Command containing the destination and targets.</param>
[CommandHelper(1, "<#destination or name> [#userid or name...]")]
[RequiresPermissions("@css/kick")]
public void OnBringCommand(CCSPlayerController? caller, CommandInfo command)
{
IEnumerable<CCSPlayerController> playersToTeleport;
CCSPlayerController? destinationPlayer;
if (command.ArgCount < 3)
{
if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
return;
var targets = GetTarget(command);
if (targets == null || !targets.Any())
return;
destinationPlayer = caller;
playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p))
.ToList();
}
else
{
var destination = GetTarget(command);
if (destination == null || destination.Count() != 1)
return;
destinationPlayer = destination.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null)
return;
// Rest args = targets to teleport
var targets = GetTarget(command, 2);
if (targets == null || !targets.Any())
return;
playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p))
.ToList();
}
if (destinationPlayer == null || !playersToTeleport.Any())
return;
// Log command
Helper.LogCommand(caller, command);
foreach (var player in playersToTeleport)
{
if (player.PlayerPawn?.Value == null || destinationPlayer.PlayerPawn?.Value == null)
continue;
// Teleport
player.TeleportPlayer(destinationPlayer);
player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
AddTimer(4, () =>
{
if (player is { IsValid: true, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
}
if (destinationPlayer.IsValid && destinationPlayer.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
}
});
if (caller != null && !SilentPlayers.Contains(caller.Slot) && _localizer != null)
{
Helper.ShowAdminActivity("sa_admin_bring_message", player.PlayerName, false, "CALLER", destinationPlayer.PlayerName);
}
}
}
// [CommandHelper(1, "<#userid or name> [#userid or name]")]
// [RequiresPermissions("@css/kick")]
// public void OnBringCommand(CCSPlayerController? caller, CommandInfo command)
// {
// // Check if the caller is valid and has a live pawn
// if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
// return;
//
// // Get the target players
// var targets = GetTarget(command);
// if (targets == null || targets.Count() > 1) return;
//
// var playersToTarget = targets.Players
// .Where(player => player is { IsValid: true, IsHLTV: false })
// .ToList();
//
// // Log the command
// Helper.LogCommand(caller, command);
//
// // Process each player to teleport
// foreach (var player in playersToTarget.Where(player => player is { Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).Where(caller.CanTarget))
// {
// if (caller.PlayerPawn.Value == null || player.PlayerPawn.Value == null)
// continue;
//
// // Teleport the player to the caller and toggle noclip
// player.TeleportPlayer(caller);
// // caller.PlayerPawn.Value.ToggleNoclip();
//
// caller.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
// caller.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
//
// Utilities.SetStateChanged(caller, "CCollisionProperty", "m_CollisionGroup");
// Utilities.SetStateChanged(caller, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
//
// player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
// player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
//
// Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
// Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
//
// // Set a timer to toggle collision back after 4 seconds
// AddTimer(4, () =>
// {
// if (!player.IsValid || player.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
// return;
//
// caller.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
// caller.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
//
// Utilities.SetStateChanged(caller, "CCollisionProperty", "m_CollisionGroup");
// Utilities.SetStateChanged(caller, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
//
// player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
// player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
//
// Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
// Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
// });
//
// // Prepare message key and arguments for the bring notification
// var activityMessageKey = "sa_admin_bring_message";
// var adminActivityArgs = new object[] { "CALLER", player.PlayerName };
//
// // Show admin activity
// if (!SilentPlayers.Contains(caller.Slot) && _localizer != null)
// {
// Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, false, adminActivityArgs);
// }
// }
// }
}

325
CS2-SimpleAdmin/Config.cs Normal file
View File

@@ -0,0 +1,325 @@
using CounterStrikeSharp.API.Core;
using System.Text.Json.Serialization;
namespace CS2_SimpleAdmin;
public class DurationItem
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("duration")]
public int Duration { get; set; }
}
public class AdminFlag
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("flag")]
public required string Flag { get; set; }
}
public class DiscordPenaltySetting
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("value")]
public string? Value { get; set; } = "";
}
public class Discord
{
[JsonPropertyName("DiscordLogWebhook")]
public string DiscordLogWebhook { get; set; } = "";
[JsonPropertyName("DiscordPenaltyBanSettings")]
public DiscordPenaltySetting[] DiscordPenaltyBanSettings { get; set; } =
[
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordPenaltyMuteSettings")]
public DiscordPenaltySetting[] DiscordPenaltyMuteSettings { get; set; } =
[
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordPenaltyGagSettings")]
public DiscordPenaltySetting[] DiscordPenaltyGagSettings { get; set; } =
[
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordPenaltySilenceSettings")]
public DiscordPenaltySetting[] DiscordPenaltySilenceSettings { get; set; } =
[
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordPenaltyWarnSettings")]
public DiscordPenaltySetting[] DiscordPenaltyWarnSettings { get; set; } =
[
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordAssociatedAccountsSettings")]
public DiscordPenaltySetting[] DiscordAssociatedAccountsSettings { get; set; } =
[
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
}
public class CustomServerCommandData
{
[JsonPropertyName("Flag")]
public string Flag { get; set; } = "@css/generic";
[JsonPropertyName("DisplayName")]
public string DisplayName { get; set; } = "";
[JsonPropertyName("Command")]
public string Command { get; set; } = "";
[JsonPropertyName("ExecuteOnClient")]
public bool ExecuteOnClient { get; set; } = false;
}
public class MenuConfig
{
[JsonPropertyName("MenuType")] public string MenuType { get; set; } = "selectable";
[JsonPropertyName("Durations")]
public DurationItem[] Durations { get; set; } =
[
new() { Name = "1 minute", Duration = 1 },
new() { Name = "5 minutes", Duration = 5 },
new() { Name = "15 minutes", Duration = 15 },
new() { Name = "1 hour", Duration = 60 },
new() { Name = "1 day", Duration = 60 * 24 },
new() { Name = "7 days", Duration = 60 * 24 * 7 },
new() { Name = "14 days", Duration = 60 * 24 * 14 },
new() { Name = "30 days", Duration = 60 * 24 * 30 },
new() { Name = "Permanent", Duration = 0 }
];
[JsonPropertyName("BanReasons")]
public List<string> BanReasons { get; set; } =
[
"Hacking",
"Voice Abuse",
"Chat Abuse",
"Admin disrespect",
"Other"
];
[JsonPropertyName("KickReasons")]
public List<string> KickReasons { get; set; } =
[
"Voice Abuse",
"Chat Abuse",
"Admin disrespect",
"Other"
];
[JsonPropertyName("WarnReasons")]
public List<string> WarnReasons { get; set; } =
[
"Voice Abuse",
"Chat Abuse",
"Admin disrespect",
"Other"
];
[JsonPropertyName("MuteReasons")]
public List<string> MuteReasons { get; set; } =
[
"Advertising",
"Spamming",
"Spectator camera abuse",
"Hate",
"Admin disrespect",
"Other"
];
[JsonPropertyName("AdminFlags")]
public AdminFlag[] AdminFlags { get; set; } =
[
new() { Name = "Generic", Flag = "@css/generic" },
new() { Name = "Chat", Flag = "@css/chat" },
new() { Name = "Change Map", Flag = "@css/changemap" },
new() { Name = "Slay", Flag = "@css/slay" },
new() { Name = "Kick", Flag = "@css/kick" },
new() { Name = "Ban", Flag = "@css/ban" },
new() { Name = "Perm Ban", Flag = "@css/permban" },
new() { Name = "Unban", Flag = "@css/unban" },
new() { Name = "Show IP", Flag = "@css/showip" },
new() { Name = "Cvar", Flag = "@css/cvar" },
new() { Name = "Rcon", Flag = "@css/rcon" },
new() { Name = "Root (all flags)", Flag = "@css/root" }
];
}
public class OtherSettings
{
[JsonPropertyName("ShowActivityType")]
public int ShowActivityType { get; set; } = 2;
[JsonPropertyName("TeamSwitchType")]
public int TeamSwitchType { get; set; } = 1;
[JsonPropertyName("KickTime")]
public int KickTime { get; set; } = 5;
[JsonPropertyName("BanType")]
public int BanType { get; set; } = 1;
[JsonPropertyName("TimeMode")]
public int TimeMode { get; set; } = 1;
[JsonPropertyName("DisableDangerousCommands")]
public bool DisableDangerousCommands { get; set; } = true;
[JsonPropertyName("MaxBanDuration")]
public int MaxBanDuration { get; set; } = 60 * 24 * 7;
[JsonPropertyName("MaxMuteDuration")]
public int MaxMuteDuration { get; set; } = 60 * 24 * 7;
[JsonPropertyName("ExpireOldIpBans")]
public int ExpireOldIpBans { get; set; } = 0;
[JsonPropertyName("ReloadAdminsEveryMapChange")]
public bool ReloadAdminsEveryMapChange { get; set; } = false;
[JsonPropertyName("DisconnectedPlayersHistoryCount")]
public int DisconnectedPlayersHistoryCount { get; set; } = 10;
[JsonPropertyName("NotifyPenaltiesToAdminOnConnect")]
public bool NotifyPenaltiesToAdminOnConnect { get; set; } = true;
[JsonPropertyName("ShowBanMenuIfNoTime")]
public bool ShowBanMenuIfNoTime { get; set; } = true;
[JsonPropertyName("UserMessageGagChatType")]
public bool UserMessageGagChatType { get; set; } = false;
[JsonPropertyName("CheckMultiAccountsByIp")]
public bool CheckMultiAccountsByIp { get; set; } = true;
[JsonPropertyName("AdditionalCommandsToLog")]
public List<string> AdditionalCommandsToLog { get; set; } = new();
[JsonPropertyName("IgnoredIps")]
public List<string> IgnoredIps { get; set; } = new();
}
public class CS2_SimpleAdminConfig : BasePluginConfig
{
[JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 25;
[JsonPropertyName("DatabaseConfig")]
public DatabaseConfig DatabaseConfig { get; set; } = new();
[JsonPropertyName("OtherSettings")]
public OtherSettings OtherSettings { get; set; } = new();
[JsonPropertyName("EnableMetrics")]
public bool EnableMetrics { get; set; } = true;
[JsonPropertyName("EnableUpdateCheck")]
public bool EnableUpdateCheck { get; set; } = true;
[JsonPropertyName("Timezone")]
public string Timezone { get; set; } = "UTC";
[JsonPropertyName("WarnThreshold")]
public Dictionary<int, string> WarnThreshold { get; set; } = new()
{
{ 998, "css_addban STEAMID64 60 \"3/4 Warn\"" },
{ 999, "css_ban #USERID 120 \"4/4 Warn\"" },
};
[JsonPropertyName("MultiServerMode")]
public bool MultiServerMode { get; set; } = true;
[JsonPropertyName("Discord")]
public Discord Discord { get; set; } = new();
[JsonPropertyName("DefaultMaps")]
public List<string> DefaultMaps { get; set; } = new();
[JsonPropertyName("WorkshopMaps")]
public Dictionary<string, long?> WorkshopMaps { get; set; } = new();
[JsonPropertyName("CustomServerCommands")]
public List<CustomServerCommandData> CustomServerCommands { get; set; } = new();
[JsonPropertyName("MenuConfig")]
public MenuConfig MenuConfigs { get; set; } = new();
}
public class DatabaseConfig
{
[JsonPropertyName("DatabaseType")]
public string DatabaseType { get; set; } = "SQLite";
[JsonPropertyName("SqliteFilePath")]
public string SqliteFilePath { get; set; } = "cs2-simpleadmin.sqlite";
[JsonPropertyName("DatabaseHost")]
public string DatabaseHost { get; set; } = "";
[JsonPropertyName("DatabasePort")]
public int DatabasePort { get; set; } = 3306;
[JsonPropertyName("DatabaseUser")]
public string DatabaseUser { get; set; } = "";
[JsonPropertyName("DatabasePassword")]
public string DatabasePassword { get; set; } = "";
[JsonPropertyName("DatabaseName")]
public string DatabaseName { get; set; } = "";
[JsonPropertyName("DatabaseSSlMode")]
public string DatabaseSSlMode { get; set; } = "preferred";
}
public enum DatabaseType
{
MySQL,
SQLite
}

View File

@@ -0,0 +1,69 @@
using Microsoft.Extensions.Logging;
using MySqlConnector;
namespace CS2_SimpleAdmin.Database;
public class Database(string dbConnectionString)
{
public MySqlConnection GetConnection()
{
try
{
var connection = new MySqlConnection(dbConnectionString);
connection.Open();
// using var cmd = connection.CreateCommand();
// cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
// cmd.ExecuteNonQueryAsync();
return connection;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogCritical($"Unable to connect to database: {ex.Message}");
throw;
}
}
public async Task<MySqlConnection> GetConnectionAsync()
{
try
{
var connection = new MySqlConnection(dbConnectionString);
await connection.OpenAsync();
// await using var cmd = connection.CreateCommand();
// cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
// await cmd.ExecuteNonQueryAsync();
return connection;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogCritical($"Unable to connect to database: {ex.Message}");
throw;
}
}
// public async Task DatabaseMigration()
// {
// Migration migrator = new(this);
// await migrator.ExecuteMigrationsAsync();
// }
public bool CheckDatabaseConnection(out string? exception)
{
using var connection = GetConnection();
exception = null;
try
{
return connection.Ping();
}
catch (Exception ex)
{
exception = ex.Message;
return false;
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Data.Common;
namespace CS2_SimpleAdmin.Database;
public interface IDatabaseProvider
{
Task<DbConnection> CreateConnectionAsync();
Task<(bool Success, string? Exception)> CheckConnectionAsync();
Task DatabaseMigrationAsync();
// CacheManager
string GetBanSelectQuery(bool multiServer);
string GetIpHistoryQuery();
string GetBanUpdateQuery(bool multiServer);
// PlayerManager
string GetUpsertPlayerIpQuery();
// PermissionManager
string GetAdminsQuery();
string GetDeleteAdminQuery(bool globalDelete);
string GetAddAdminQuery();
string GetAddAdminFlagsQuery();
string GetUpdateAdminGroupQuery();
string GetGroupsQuery();
string GetGroupIdByNameQuery();
string GetAddGroupQuery();
string GetAddGroupFlagsQuery();
string GetAddGroupServerQuery();
string GetDeleteGroupQuery();
string GetDeleteOldAdminsQuery();
// BanManager
string GetAddBanQuery();
string GetAddBanBySteamIdQuery();
string GetAddBanByIpQuery();
string GetUnbanRetrieveBansQuery(bool multiServer);
string GetUnbanAdminIdQuery();
string GetInsertUnbanQuery(bool includeReason);
string GetUpdateBanStatusQuery();
string GetExpireBansQuery(bool multiServer);
string GetExpireIpBansQuery(bool multiServer);
// MuteManager
string GetAddMuteQuery(bool includePlayerName);
string GetIsMutedQuery(bool multiServer, int timeMode);
string GetMuteStatsQuery(bool multiServer);
string GetUpdateMutePassedQuery(bool multiServer);
string GetCheckExpiredMutesQuery(bool multiServer);
string GetRetrieveMutesQuery(bool multiServer);
string GetUnmuteAdminIdQuery();
string GetInsertUnmuteQuery(bool includeReason);
string GetUpdateMuteStatusQuery();
string GetExpireMutesQuery(bool multiServer, int timeMode);
// WarnManager
string GetAddWarnQuery(bool includePlayerName);
string GetPlayerWarnsQuery(bool multiServer, bool active);
string GetPlayerWarnsCountQuery(bool multiServer, bool active);
string GetUnwarnByIdQuery(bool multiServer);
string GetUnwarnLastQuery(bool multiServer);
string GetExpireWarnsQuery(bool multiServer);
}

View File

@@ -0,0 +1,105 @@
using System.Data.Common;
using Microsoft.Extensions.Logging;
namespace CS2_SimpleAdmin.Database;
public class Migration(string migrationsPath)
{
/// <summary>
/// Executes all migration scripts found in the configured migrations path that have not been applied yet.
/// Creates a migration tracking table if it does not exist.
/// Applies migration scripts in filename order and logs successes or failures.
/// </summary>
public async Task ExecuteMigrationsAsync()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
var files = Directory.GetFiles(migrationsPath, "*.sql").OrderBy(f => f).ToList();
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 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL
);
""";
}
else
{
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS sa_migrations (
id INT PRIMARY KEY AUTO_INCREMENT,
version VARCHAR(128) NOT NULL
);
""";
}
await cmd.ExecuteNonQueryAsync();
}
var lastAppliedVersion = await GetLastAppliedVersionAsync(connection);
foreach (var file in files)
{
var version = Path.GetFileNameWithoutExtension(file);
if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0)
continue;
try
{
var sqlScript = await File.ReadAllTextAsync(file);
await using (var cmdMigration = connection.CreateCommand())
{
cmdMigration.CommandText = sqlScript;
await cmdMigration.ExecuteNonQueryAsync();
}
await UpdateLastAppliedVersionAsync(connection, version);
CS2_SimpleAdmin._logger?.LogInformation($"Migration \"{version}\" successfully applied.");
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, $"Error applying migration \"{version}\".");
break;
}
}
}
/// <summary>
/// Retrieves the version string of the last applied migration from the database.
/// </summary>
/// <param name="connection">The open database connection.</param>
/// <returns>The version string of the last applied migration, or empty string if none.</returns>
private static async Task<string> GetLastAppliedVersionAsync(DbConnection connection)
{
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT version FROM sa_migrations ORDER BY id DESC LIMIT 1;";
var result = await cmd.ExecuteScalarAsync();
return result?.ToString() ?? string.Empty;
}
/// <summary>
/// Inserts a record tracking the successful application of a migration version.
/// </summary>
/// <param name="connection">The open database connection.</param>
/// <param name="version">The version string of the migration applied.</param>
private static async Task UpdateLastAppliedVersionAsync(DbConnection connection, string version)
{
await using var cmd = connection.CreateCommand();
cmd.CommandText = "INSERT INTO sa_migrations (version) VALUES (@Version);";
var param = cmd.CreateParameter();
param.ParameterName = "@Version";
param.Value = version;
cmd.Parameters.Add(param);
await cmd.ExecuteNonQueryAsync();
}
}

View File

@@ -0,0 +1,50 @@
CREATE TABLE IF NOT EXISTS `sa_bans` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`player_name` VARCHAR(128),
`player_steamid` VARCHAR(64),
`player_ip` VARCHAR(128),
`admin_steamid` VARCHAR(64) NOT NULL,
`admin_name` VARCHAR(128) NOT NULL,
`reason` VARCHAR(255) NOT NULL,
`duration` INT NOT NULL,
`ends` TIMESTAMP NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`server_id` INT NULL,
`status` ENUM('ACTIVE', 'UNBANNED', 'EXPIRED', '') NOT NULL DEFAULT 'ACTIVE'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `sa_mutes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`player_name` varchar(128) NULL,
`player_steamid` varchar(64) NOT NULL,
`admin_steamid` varchar(64) NOT NULL,
`admin_name` varchar(128) NOT NULL,
`reason` varchar(255) NOT NULL,
`duration` int(11) NOT NULL,
`ends` timestamp NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`type` enum('GAG','MUTE','SILENCE','') NOT NULL DEFAULT 'GAG',
`server_id` INT NULL,
`status` enum('ACTIVE','UNMUTED','EXPIRED','') NOT NULL DEFAULT 'ACTIVE',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `sa_admins` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`player_name` varchar(128) NOT NULL,
`player_steamid` varchar(64) NOT NULL,
`flags` TEXT NULL,
`immunity` int(11) NOT NULL DEFAULT 0,
`server_id` INT NULL,
`ends` timestamp NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `sa_servers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`hostname` varchar(128) NOT NULL,
`address` varchar(64) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `address` (`address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS `sa_admins_flags` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`admin_id` int(11) NOT NULL,
`flag` varchar(64) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`admin_id`) REFERENCES `sa_admins` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `sa_admins` CHANGE `flags` `flags` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL;

View File

@@ -0,0 +1,4 @@
ALTER TABLE `sa_bans` CHANGE `player_name` `player_name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL AFTER `id`;
ALTER TABLE `sa_mutes` CHANGE `player_name` `player_name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL AFTER `id`;
ALTER TABLE `sa_admins` CHANGE `player_name` `player_name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL AFTER `id`;
ALTER TABLE `sa_servers` CHANGE `hostname` `hostname` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL AFTER `id`;

View File

@@ -0,0 +1,36 @@
INSERT INTO sa_admins_flags (admin_id, flag)
SELECT
min_admins.admin_id,
TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(sa_admins.flags, ',', numbers.n), ',', -1)) AS flag
FROM (
SELECT MIN(id) AS admin_id, player_steamid, server_id
FROM sa_admins
WHERE player_steamid != 'Console'
GROUP BY player_steamid, server_id
) AS min_admins
JOIN sa_admins ON min_admins.player_steamid = sa_admins.player_steamid
JOIN (
SELECT 1 AS n UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5 UNION ALL
SELECT 6 UNION ALL
SELECT 7 UNION ALL
SELECT 8 UNION ALL
SELECT 9 UNION ALL
SELECT 10 UNION ALL
SELECT 11 UNION ALL
SELECT 12 UNION ALL
SELECT 13 UNION ALL
SELECT 14 UNION ALL
SELECT 15 UNION ALL
SELECT 16 UNION ALL
SELECT 17 UNION ALL
SELECT 18 UNION ALL
SELECT 19 UNION ALL
SELECT 20
) AS numbers
ON CHAR_LENGTH(sa_admins.flags) - CHAR_LENGTH(REPLACE(sa_admins.flags, ',', '')) >= numbers.n - 1
AND (min_admins.server_id = sa_admins.server_id OR (min_admins.server_id IS NULL AND sa_admins.server_id IS NULL))
WHERE sa_admins.id IS NOT NULL;

View File

@@ -0,0 +1,29 @@
CREATE TABLE IF NOT EXISTS `sa_unbans` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ban_id` int(11) NOT NULL,
`admin_id` int(11) NOT NULL DEFAULT 0,
`reason` varchar(255) NOT NULL DEFAULT 'Unknown',
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `sa_unmutes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mute_id` int(11) NOT NULL,
`admin_id` int(11) NOT NULL DEFAULT 0,
`reason` varchar(255) NOT NULL DEFAULT 'Unknown',
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT IGNORE INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`)
VALUES (0, 'Console', 'Console', '', '0', NULL, NULL, NOW());
UPDATE `sa_admins` SET `id` = 0 WHERE `id` = -1;
ALTER TABLE `sa_bans` ADD `unban_id` INT NULL AFTER `server_id`;
ALTER TABLE `sa_mutes` ADD `unmute_id` INT NULL AFTER `server_id`;
ALTER TABLE `sa_bans` ADD FOREIGN KEY (`unban_id`) REFERENCES `sa_unbans`(`id`) ON DELETE CASCADE;
ALTER TABLE `sa_mutes` ADD FOREIGN KEY (`unmute_id`) REFERENCES `sa_unmutes`(`id`) ON DELETE CASCADE;
ALTER TABLE `sa_unbans` ADD FOREIGN KEY (`admin_id`) REFERENCES `sa_admins`(`id`) ON DELETE CASCADE;
ALTER TABLE `sa_unmutes` ADD FOREIGN KEY (`admin_id`) REFERENCES `sa_admins`(`id`) ON DELETE CASCADE;

View File

@@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS `sa_groups` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`immunity` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `sa_groups_flags` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`group_id` int(11) NOT NULL,
`flag` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `sa_groups_servers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`group_id` int(11) NOT NULL,
`server_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) 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

@@ -0,0 +1 @@
ALTER TABLE `sa_groups_servers` CHANGE `server_id` `server_id` INT(11) NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE `sa_mutes` ADD `passed` INT NULL AFTER `duration`;

View File

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

View File

@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS `sa_warns` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`player_name` varchar(128) DEFAULT NULL,
`player_steamid` varchar(64) NOT NULL,
`admin_steamid` varchar(64) NOT NULL,
`admin_name` varchar(128) NOT NULL,
`reason` varchar(255) NOT NULL,
`duration` int(11) NOT NULL,
`ends` timestamp NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`server_id` int(11) DEFAULT NULL,
`status` enum('ACTIVE','EXPIRED','') NOT NULL DEFAULT 'ACTIVE',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@@ -0,0 +1 @@
ALTER TABLE `sa_servers` ADD `rcon_password` varchar(128) NULL AFTER `hostname`;

View File

@@ -0,0 +1 @@
ALTER TABLE `sa_bans` ADD COLUMN `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER `status`;

View File

@@ -0,0 +1,5 @@
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;
ALTER TABLE `sa_players_ips` ADD INDEX (used_at DESC);
ALTER TABLE `sa_players_ips` ADD `name` VARCHAR(64) NULL DEFAULT NULL AFTER `steamid`;

View File

@@ -0,0 +1,3 @@
ALTER TABLE sa_mutes ADD INDEX (player_steamid, status, ends);
ALTER TABLE sa_mutes ADD INDEX(player_steamid, status, server_id, duration);
ALTER TABLE sa_mutes ADD INDEX(player_steamid, type);

View File

@@ -0,0 +1,23 @@
ALTER TABLE `sa_bans` CHANGE `player_steamid` `player_steamid` BIGINT NULL DEFAULT NULL;
UPDATE `sa_bans`
SET admin_steamid = '0'
WHERE admin_steamid NOT REGEXP '^[0-9]+$';
ALTER TABLE `sa_bans` CHANGE `admin_steamid` `admin_steamid` BIGINT NOT NULL;
ALTER TABLE `sa_mutes` CHANGE `player_steamid` `player_steamid` BIGINT NULL DEFAULT NULL;
UPDATE `sa_mutes`
SET admin_steamid = '0'
WHERE admin_steamid NOT REGEXP '^[0-9]+$';
ALTER TABLE `sa_mutes` CHANGE `admin_steamid` `admin_steamid` BIGINT NOT NULL;
ALTER TABLE `sa_warns` CHANGE `player_steamid` `player_steamid` BIGINT NULL DEFAULT NULL;
UPDATE `sa_warns`
SET admin_steamid = '0'
WHERE admin_steamid NOT REGEXP '^[0-9]+$';
ALTER TABLE `sa_warns` CHANGE `admin_steamid` `admin_steamid` BIGINT NOT NULL;
UPDATE `sa_admins`
SET player_steamid = '0'
WHERE player_steamid NOT REGEXP '^[0-9]+$';
ALTER TABLE `sa_admins` CHANGE `player_steamid` `player_steamid` BIGINT NULL DEFAULT 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

@@ -0,0 +1,47 @@
CREATE TABLE IF NOT EXISTS `sa_bans` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`player_name` VARCHAR(128),
`player_steamid` VARCHAR(64),
`player_ip` VARCHAR(128),
`admin_steamid` VARCHAR(64) NOT NULL,
`admin_name` VARCHAR(128) NOT NULL,
`reason` VARCHAR(255) NOT NULL,
`duration` INTEGER NOT NULL,
`ends` TIMESTAMP NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`server_id` INTEGER NULL,
`status` TEXT NOT NULL DEFAULT 'ACTIVE'
);
CREATE TABLE IF NOT EXISTS `sa_mutes` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`player_name` VARCHAR(128) NULL,
`player_steamid` VARCHAR(64) NOT NULL,
`admin_steamid` VARCHAR(64) NOT NULL,
`admin_name` VARCHAR(128) NOT NULL,
`reason` VARCHAR(255) NOT NULL,
`duration` INTEGER NOT NULL,
`ends` TIMESTAMP NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`type` TEXT NOT NULL DEFAULT 'GAG',
`server_id` INTEGER NULL,
`status` TEXT NOT NULL DEFAULT 'ACTIVE'
);
CREATE TABLE IF NOT EXISTS `sa_admins` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`player_name` VARCHAR(128) NOT NULL,
`player_steamid` VARCHAR(64) NOT NULL,
`flags` TEXT NULL,
`immunity` INTEGER NOT NULL DEFAULT 0,
`server_id` INTEGER NULL,
`ends` TIMESTAMP NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `sa_servers` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`hostname` VARCHAR(128) NOT NULL,
`address` VARCHAR(64) NOT NULL,
UNIQUE (`address`)
);

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `sa_admins_flags` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`admin_id` INTEGER NOT NULL,
`flag` VARCHAR(64) NOT NULL,
FOREIGN KEY (`admin_id`) REFERENCES `sa_admins` (`id`) ON DELETE CASCADE
);

View File

@@ -0,0 +1,46 @@
INSERT INTO sa_admins_flags (admin_id, flag)
WITH RECURSIVE
min_admins AS (
SELECT MIN(id) AS admin_id, player_steamid, server_id
FROM sa_admins
WHERE player_steamid != 'Console'
GROUP BY player_steamid, server_id
),
split_flags AS (
SELECT
ma.admin_id,
sa.flags,
1 AS pos,
CASE
WHEN INSTR(sa.flags || ',', ',') = 0 THEN sa.flags
ELSE SUBSTR(sa.flags, 1, INSTR(sa.flags || ',', ',') - 1)
END AS flag,
CASE
WHEN INSTR(sa.flags || ',', ',') = 0 THEN ''
ELSE SUBSTR(sa.flags, INSTR(sa.flags || ',', ',') + 1)
END AS remaining
FROM min_admins ma
JOIN sa_admins sa ON ma.player_steamid = sa.player_steamid
AND (ma.server_id = sa.server_id OR (ma.server_id IS NULL AND sa.server_id IS NULL))
WHERE sa.flags IS NOT NULL AND sa.flags != ''
UNION ALL
SELECT
admin_id,
flags,
pos + 1,
CASE
WHEN INSTR(remaining || ',', ',') = 0 THEN remaining
ELSE SUBSTR(remaining, 1, INSTR(remaining || ',', ',') - 1)
END AS flag,
CASE
WHEN INSTR(remaining || ',', ',') = 0 THEN ''
ELSE SUBSTR(remaining, INSTR(remaining || ',', ',') + 1)
END AS remaining
FROM split_flags
WHERE remaining != ''
)
SELECT admin_id, TRIM(flag)
FROM split_flags
WHERE flag IS NOT NULL AND TRIM(flag) != '';

View File

@@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS `sa_unbans` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`ban_id` INTEGER NOT NULL,
`admin_id` INTEGER NOT NULL DEFAULT 0,
`reason` VARCHAR(255) NOT NULL DEFAULT 'Unknown',
`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `sa_unmutes` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`mute_id` INTEGER NOT NULL,
`admin_id` INTEGER NOT NULL DEFAULT 0,
`reason` VARCHAR(255) NOT NULL DEFAULT 'Unknown',
`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`)
VALUES (0, 'Console', 'Console', '', '0', NULL, NULL, CURRENT_TIMESTAMP);
UPDATE `sa_admins` SET `id` = 0 WHERE `id` = -1;
ALTER TABLE `sa_bans` ADD `unban_id` INTEGER NULL;
ALTER TABLE `sa_mutes` ADD `unmute_id` INTEGER NULL;

View File

@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS `sa_groups` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR(255) NOT NULL,
`immunity` INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS `sa_groups_flags` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`group_id` INTEGER NOT NULL,
`flag` VARCHAR(64) NOT NULL,
FOREIGN KEY (`group_id`) REFERENCES `sa_groups` (`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `sa_groups_servers` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`group_id` INTEGER NOT NULL,
`server_id` INTEGER NULL,
FOREIGN KEY (`group_id`) REFERENCES `sa_groups` (`id`) ON DELETE CASCADE
);
ALTER TABLE `sa_admins` ADD `group_id` INTEGER NULL;

Some files were not shown because too many files have changed in this diff Show More