├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── img │ └── ColorsCS2.png └── workflows │ └── main.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ChangeLogs.md ├── CustomCommands.sln ├── CustomCommands ├── .gitignore ├── Commands.example.json ├── Commands │ ├── Others.example.json │ └── ServerCommands.example.json ├── CustomCommands.cs ├── CustomCommands.csproj ├── CustomCommandsConfig.cs ├── CustomCommandsServiceCollection.cs ├── Interfaces │ ├── ICooldownManager.cs │ ├── IEventManager.cs │ ├── ILoadJson.cs │ ├── IMessageManager.cs │ ├── IPluginGlobals.cs │ ├── IPluginUtilities.cs │ ├── IRegisterCommands.cs │ └── IReplaceTagsFunctions.cs ├── Model │ ├── CenterElement.cs │ ├── CommandsConfig.cs │ ├── CooldownTimer.cs │ └── Receiver.cs ├── Services │ ├── CooldownManager.cs │ ├── EventManager.cs │ ├── LoadJson.cs │ ├── MessageManager.cs │ ├── PluginGlobals.cs │ ├── PluginUtilities.cs │ ├── RegisterCommands.cs │ └── ReplaceTagsFunction.cs ├── lang │ ├── de.json │ └── en.json └── tests.md ├── Examples ├── CenterMessageWithColor.md ├── ClientCommands.md ├── Colors.md ├── CommandWithArguments.md ├── Cooldown.md ├── LanguageSupport.md ├── MulitpleAliasesForOneCommand.md ├── NewLines.md ├── Permissions.md ├── PrintTo.md ├── README.md ├── ServerCommands.md ├── SimpleCommands.md ├── Tags.md └── ToggleServerCommands.md ├── LICENSE └── README.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/img/ColorsCS2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrMagiic/CSS-CreateCustomCommands/c56df02f0e59f304d57d11aa4f8bf85f6308b3e2/.github/img/ColorsCS2.png -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: .NET 8 Build and Release 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: '8.0.x' 20 | 21 | - name: Restore dependencies 22 | run: dotnet restore 23 | 24 | - name: Build 25 | run: dotnet build ./CustomCommands/CustomCommands.csproj --configuration Release --no-restore 26 | 27 | - name: Publish 28 | run: dotnet publish ./CustomCommands/CustomCommands.csproj --configuration Release --no-build --output ./out 29 | 30 | - name: Zip artifacts 31 | run: | 32 | cd ./out 33 | zip -r CustomCommands.zip . 34 | 35 | - name: Create Release 36 | id: create_release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 40 | with: 41 | tag_name: ${{ github.event.pull_request.title }} 42 | release_name: ${{ github.event.pull_request.title }} 43 | body: ${{ github.event.pull_request.body }} 44 | draft: true 45 | prerelease: false 46 | 47 | 48 | - name: Upload Release Asset 49 | id: upload-release-asset 50 | uses: actions/upload-release-asset@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 53 | with: 54 | upload_url: ${{ steps.create_release.outputs.upload_url }} 55 | asset_path: ./out/CustomCommands.zip 56 | asset_name: CustomCommands.zip 57 | asset_content_type: application/zip 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Discord: herrmagic. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | You are welcome to contribute to this project! Simply create a pull request, and it will be promptly reviewed. 2 | -------------------------------------------------------------------------------- /ChangeLogs.md: -------------------------------------------------------------------------------- 1 | # Changelogs 2 | 3 | 4 | ## v3.0.0 5 | 6 | ### Features 7 | - **Config Option for Command Registration**: Added a new configuration option to disable automatic CSS command registration (e.g., `css_`). This allows you to create custom commands like `!ip` without conflicting with commands like `ip` in vanilla CS2. 8 | - **Client Command Execution**: Introduced support for executing client-side commands. Check out [`ClientCommands.md`](https://github.com/HerrMagiic/CSS-CreateCustomCommands/blob/main/Examples/ClientCommands.md) for more details. 9 | - **Client Command from Server**: Added functionality to send commands from the server to clients. Refer to [`ClientCommands.md`](https://github.com/HerrMagiic/CSS-CreateCustomCommands/blob/main/Examples/ClientCommands.md) for implementation instructions. 10 | - **Server Commands**: New server commands have been added. Example usages can be found in [`ServerCommands.md`](https://github.com/HerrMagiic/CSS-CreateCustomCommands/blob/main/Examples/ServerCommands.md). 11 | - **Updated Example Files**: The example files have been updated with more information and clearer descriptions, making it easier to use the plugin. 12 | - **Server Kick Example**: A server kick example has been added in [`ServerCommands.md`](https://github.com/HerrMagiic/CSS-CreateCustomCommands/blob/main/Examples/ServerCommands.md) to demonstrate kicking players. 13 | - **Random Tag Example**: A new example showcasing random tag functionality has been added. See [`Tags.md`](https://github.com/HerrMagiic/CSS-CreateCustomCommands/blob/main/Examples/Tags.md) for details. 14 | - **Color Tags for Prefixes**: Added the ability to apply color tags to command prefixes for more customization. 15 | 16 | ### Bug Fixes 17 | - **Fixed Newline in Language Tag**: Resolved an issue where newlines were not working correctly in language tags. 18 | - **Fixed Unicode characters not working**: Commands and messages now fully support Unicode, enabling broader character compatibility. 19 | 20 | ### Optimizations 21 | - **Code Optimizations**: Made small optimizations to improve the code's performance. 22 | - **Variable Refactoring**: Changed most variables to use `var` instead of specific types (like `int` or `string`) for cleaner and more flexible code. 23 | - **Async Command File Reading**: Command files are now read asynchronously, improving performance and responsiveness. 24 | -------------------------------------------------------------------------------- /CustomCommands.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34018.315 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomCommands", "CustomCommands\CustomCommands.csproj", "{CFD687D3-02AF-4F8B-B561-ED01C4B2C5D3}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {CFD687D3-02AF-4F8B-B561-ED01C4B2C5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {CFD687D3-02AF-4F8B-B561-ED01C4B2C5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {CFD687D3-02AF-4F8B-B561-ED01C4B2C5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {CFD687D3-02AF-4F8B-B561-ED01C4B2C5D3}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {F5F2148E-D3A3-4196-B97A-4E0941326365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {F5F2148E-D3A3-4196-B97A-4E0941326365}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {F5F2148E-D3A3-4196-B97A-4E0941326365}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {F5F2148E-D3A3-4196-B97A-4E0941326365}.Release|Any CPU.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ExtensibilityGlobals) = postSolution 27 | SolutionGuid = {EE21A19C-5459-44A8-B594-5ACEC2B2BA82} 28 | EndGlobalSection 29 | EndGlobal 30 | -------------------------------------------------------------------------------- /CustomCommands/.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | /bin 3 | /obj 4 | 5 | TestingCommands.json -------------------------------------------------------------------------------- /CustomCommands/Commands.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Title": "Discord", 4 | "Description": "Command for Discord", 5 | "Command": "discord", 6 | "Message": "{PREFIX}{GREEN}Discord: \n ", 7 | "PrintTo": 0 8 | }, 9 | { 10 | "Title": "Steam", 11 | "Description": "Command for SteamGroup", 12 | "Command": "steam,steamgroup,group", 13 | "Message": "SteamGroup: ", 14 | "CenterMessage": { 15 | "Message": "
Steam Group

https...
", 16 | "Time": 10 17 | }, 18 | "PrintTo": 7 19 | }, 20 | { 21 | "Title": "Enable Surf", 22 | "Command": "surf", 23 | "Message": [ 24 | "Surf is now:", 25 | "{GREEN}Enabled" 26 | ], 27 | "PrintTo": 0, 28 | "Description": "Command for Surf gamemode", 29 | "ServerCommands": [ 30 | "sv_cheats 1", 31 | "sv_falldamage_scale 0", 32 | "sv_party_mode 1", 33 | "mp_freezetime 1", 34 | "mp_round_restart_delay 2", 35 | "cl_ragdoll_gravity 0", 36 | "sv_accelerate 10", 37 | "sv_airaccelerate 1400", 38 | "sv_gravity 800.0", 39 | "say hello" 40 | ], 41 | "Permission": { 42 | "RequiresAllPermissions": false, 43 | "PermissionList": [ 44 | "@css/cvar", 45 | "@custom/permission", 46 | "#css/simple-admin" 47 | ] 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /CustomCommands/Commands/Others.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Title": "Discord", 4 | "Description": "Command for Discord", 5 | "Command": "discord", 6 | "Message": "{PREFIX}{GREEN}Discord: \n ", 7 | "PrintTo": 0 8 | }, 9 | { 10 | "Title": "Steam", 11 | "Description": "Command for SteamGroup", 12 | "Command": "steam,steamgroup,group", 13 | "Message": "SteamGroup: ", 14 | "CenterMessage": { 15 | "Message": "
Steam Group

https...
", 16 | "Time": 10 17 | }, 18 | "PrintTo": 7 19 | } 20 | ] -------------------------------------------------------------------------------- /CustomCommands/Commands/ServerCommands.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Title": "Fast restart", 4 | "Command": "rg", 5 | "Message": [ 6 | "Restarting game..." 7 | ], 8 | "PrintTo": 0, 9 | "Description": "restarting game", 10 | "ServerCommands": [ 11 | "mp_restartgame 1" 12 | ], 13 | "Permission": { 14 | "RequiresAllPermissions": false, 15 | "PermissionList": [ 16 | "@css/cvar", 17 | "@custom/permission", 18 | "#css/simple-admin" 19 | ] 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /CustomCommands/CustomCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CounterStrikeSharp.API.Core.Attributes; 3 | using CustomCommands.Interfaces; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace CustomCommands; 7 | 8 | [MinimumApiVersion(213)] 9 | public partial class CustomCommands : BasePlugin, IPluginConfig 10 | { 11 | public override string ModuleName => "CustomCommands"; 12 | public override string ModuleVersion => "3.0.0"; 13 | public override string ModuleAuthor => "HerrMagic"; 14 | public override string ModuleDescription => "Create your own commands per config"; 15 | 16 | public CustomCommandsConfig Config { get; set; } = new(); 17 | private readonly IRegisterCommands _registerCommands; 18 | private readonly IPluginGlobals _pluginGlobals; 19 | private readonly ILoadJson _loadJson; 20 | private readonly IEventManager _eventManager; 21 | private readonly IReplaceTagsFunctions _replaceTagsFunctions; 22 | 23 | public CustomCommands(IRegisterCommands RegisterCommands, ILogger Logger, 24 | IPluginGlobals PluginGlobals, ILoadJson LoadJson, IEventManager EventManager, IReplaceTagsFunctions ReplaceTagsFunctions) 25 | { 26 | this.Logger = Logger; 27 | _registerCommands = RegisterCommands; 28 | _pluginGlobals = PluginGlobals; 29 | _loadJson = LoadJson; 30 | _eventManager = EventManager; 31 | _replaceTagsFunctions = ReplaceTagsFunctions; 32 | } 33 | 34 | public void OnConfigParsed(CustomCommandsConfig config) 35 | { 36 | Config = config; 37 | } 38 | 39 | public override void Load(bool hotReload) 40 | { 41 | if (!Config.IsPluginEnabled) 42 | { 43 | Logger.LogInformation($"{Config.LogPrefix} {ModuleName} is disabled"); 44 | return; 45 | } 46 | 47 | Logger.LogInformation( 48 | $"{ModuleName} loaded!"); 49 | 50 | _pluginGlobals.Config = Config; 51 | Config.Prefix = _replaceTagsFunctions.ReplaceColorTags(Config.Prefix); 52 | 53 | var comms = Task.Run(async () => await _loadJson.GetCommandsFromJsonFiles(ModuleDirectory)).Result; 54 | 55 | if (comms == null) 56 | { 57 | Logger.LogError("No commands found please create a config file"); 58 | return; 59 | } 60 | 61 | _eventManager.RegisterListeners(); 62 | 63 | if (comms != null) 64 | { 65 | _pluginGlobals.CustomCommands = comms; 66 | 67 | _registerCommands.CheckForDuplicateCommands(); 68 | _registerCommands.ConvertingCommandsForRegister(); 69 | 70 | // Add commands from the JSON file to the server 71 | foreach (var cmd in _pluginGlobals.CustomCommands) 72 | _registerCommands.AddCommands(cmd); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /CustomCommands/CustomCommands.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | none 15 | runtime 16 | compile; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | Always 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /CustomCommands/CustomCommandsConfig.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CounterStrikeSharp.API.Modules.Utils; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace CustomCommands; 6 | 7 | public class CustomCommandsConfig : BasePluginConfig 8 | { 9 | public override int Version { get; set; } = 2; 10 | 11 | [JsonPropertyName("IsPluginEnabled")] 12 | public bool IsPluginEnabled { get; set; } = true; 13 | 14 | [JsonPropertyName("LogPrefix")] 15 | public string LogPrefix { get; set; } = "CSSharp"; 16 | 17 | [JsonPropertyName("Prefix")] 18 | public string Prefix { get; set; } = $"[{ChatColors.Yellow}Info{ChatColors.Default}] "; 19 | 20 | [JsonPropertyName("RegisterCommandsAsCSSFramework")] 21 | public bool RegisterCommandsAsCSSFramework { get; set; } = true; 22 | } -------------------------------------------------------------------------------- /CustomCommands/CustomCommandsServiceCollection.cs: -------------------------------------------------------------------------------- 1 | 2 | using CounterStrikeSharp.API.Core; 3 | using CustomCommands.Interfaces; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace CustomCommands; 7 | 8 | public class CustomCommandsServiceCollection : IPluginServiceCollection 9 | { 10 | public void ConfigureServices(IServiceCollection services) 11 | { 12 | // Scans the interface in the CustomCommands.Interfaces namespace and adds the classes that implement the interface to the service collection automatically 13 | services.Scan(scan => scan 14 | .FromAssemblyOf() 15 | .AddClasses() 16 | .AsImplementedInterfaces() 17 | .WithSingletonLifetime() 18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /CustomCommands/Interfaces/ICooldownManager.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CustomCommands.Model; 3 | 4 | namespace CustomCommands.Interfaces; 5 | 6 | public interface ICooldownManager 7 | { 8 | bool IsCommandOnCooldown(CCSPlayerController player, Commands cmd); 9 | void AddToCooldownList(bool isGlobal, int playerID, Guid commandID, int cooldownTime); 10 | bool IsCommandOnCooldownWithCondition(Func predicate, CCSPlayerController player, Commands cmd); 11 | void SetCooldown(CCSPlayerController player, Commands cmd); 12 | } 13 | -------------------------------------------------------------------------------- /CustomCommands/Interfaces/IEventManager.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | 3 | namespace CustomCommands.Interfaces; 4 | 5 | public interface IEventManager 6 | { 7 | HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo _); 8 | void RegisterListeners(); 9 | } -------------------------------------------------------------------------------- /CustomCommands/Interfaces/ILoadJson.cs: -------------------------------------------------------------------------------- 1 | using CustomCommands.Model; 2 | 3 | namespace CustomCommands.Interfaces; 4 | 5 | public interface ILoadJson 6 | { 7 | /// 8 | /// Retrieves a list of commands from JSON files located in the specified path. 9 | /// 10 | /// The path where the JSON files are located. 11 | /// A list of commands. 12 | Task> GetCommandsFromJsonFiles(string path); 13 | 14 | /// 15 | /// Checks if an .example file exists in the specified path and creates a default config file if it doesn't exist. 16 | /// 17 | /// The path to check for the example file. 18 | void CheckForExampleFile(string path); 19 | 20 | /// 21 | /// Checks if the JSON file has a valid syntax. 22 | /// 23 | /// The path to the JSON file. 24 | /// The json string. 25 | /// True if the JSON syntax is valid, false otherwise. 26 | bool IsValidJsonSyntax(string json, string path); 27 | 28 | /// 29 | /// Validates the list of commands loaded from a JSON file. 30 | /// 31 | /// The list of commands to validate. 32 | /// The path of the JSON file. 33 | /// True if all commands are valid, false otherwise. 34 | bool ValidateObject(List? comms, string path); 35 | 36 | /// 37 | /// Logs the details of a command. 38 | /// 39 | /// The command to log. 40 | void LogCommandDetails(Commands comms); 41 | 42 | /// 43 | /// Validates the PrintTo property of the Commands object and checks if the required message properties are set based on the PrintTo value. 44 | /// 45 | /// The Commands object to validate. 46 | /// True if the PrintTo property is valid and the required message properties are set; otherwise, false. 47 | bool PrintToCheck(Commands comms); 48 | 49 | /// 50 | /// Validates a dynamic message by checking if it is a string or an array. 51 | /// 52 | /// The dynamic message to validate. 53 | /// True if the message is a string or an array, otherwise false. 54 | bool ValidateMessage(dynamic message); 55 | } -------------------------------------------------------------------------------- /CustomCommands/Interfaces/IMessageManager.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CustomCommands.Model; 3 | 4 | namespace CustomCommands.Interfaces; 5 | 6 | public interface IMessageManager 7 | { 8 | /// 9 | /// Sends a message based on the specified command and target receiver. 10 | /// 11 | /// The player who triggered the command. 12 | /// The command containing the message and target receiver. 13 | void SendMessage(CCSPlayerController player, Commands cmd); 14 | void PrintToCenterClient(CCSPlayerController player, Commands cmd); 15 | void PrintToAllCenter(Commands cmd); 16 | void PrintToChatAndCenter(Receiver receiver, CCSPlayerController player, Commands cmd); 17 | void PrintToChatAndAllCenter(Receiver receiver, CCSPlayerController player, Commands cmd); 18 | void PrintToChat(Receiver printToChat, CCSPlayerController player, dynamic message); 19 | void PrintToChatClient(CCSPlayerController player, string[] msg); 20 | void PrintToChatServer(string[] msg); 21 | } 22 | -------------------------------------------------------------------------------- /CustomCommands/Interfaces/IPluginGlobals.cs: -------------------------------------------------------------------------------- 1 | using CustomCommands.Model; 2 | 3 | namespace CustomCommands.Interfaces; 4 | public interface IPluginGlobals 5 | { 6 | List centerClientOn { get; set; } 7 | CenterServerElement centerServerOn { get; set; } 8 | CustomCommandsConfig Config { get; set; } 9 | List CooldownTimer { get; set; } 10 | List CustomCommands { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /CustomCommands/Interfaces/IPluginUtilities.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CustomCommands.Model; 3 | 4 | namespace CustomCommands.Interfaces; 5 | 6 | public interface IPluginUtilities 7 | { 8 | /// 9 | /// Adds the css_ prefix to each alias. 10 | /// This will help cs# tell this command belongs to the framework. 11 | /// 12 | /// 13 | /// 14 | string[] AddCSSTagsToAliases(List input); 15 | 16 | /// 17 | /// Splits a string by comma, semicolon, or whitespace characters. 18 | /// 19 | /// The string to be split. 20 | /// An array of strings containing the split substrings. 21 | string[] SplitStringByCommaOrSemicolon(string str); 22 | 23 | /// 24 | /// Executes the server commands from the command object 25 | /// 26 | /// 27 | /// 28 | void ExecuteServerCommands(Commands cmd, CCSPlayerController player); 29 | 30 | /// 31 | /// Issue the specified command to the specified client (mimics that client typing the command at the console). 32 | /// Note: Only works for some commands, marked with the FCVAR_CLIENT_CAN_EXECUTE flag (not many). 33 | /// 34 | /// 35 | /// 36 | void ExecuteClientCommands(Commands cmd, CCSPlayerController player); 37 | 38 | /// 39 | /// Issue the specified command directly from the server (mimics the server executing the command with the given player context). 40 | /// Works with server commands like `kill`, `explode`, `noclip`, etc. 41 | /// 42 | /// 43 | /// 44 | void ExecuteClientCommandsFromServer(Commands cmd, CCSPlayerController player); 45 | 46 | /// 47 | /// Checks if the player has the required permissions to execute the command 48 | /// 49 | /// 50 | /// 51 | /// 52 | bool RequiresPermissions(CCSPlayerController player, Permission permissions); 53 | } 54 | -------------------------------------------------------------------------------- /CustomCommands/Interfaces/IRegisterCommands.cs: -------------------------------------------------------------------------------- 1 | using CustomCommands.Model; 2 | 3 | namespace CustomCommands.Interfaces; 4 | 5 | public interface IRegisterCommands 6 | { 7 | /// 8 | /// Adds custom commands to the plugin. 9 | /// 10 | /// The command to add. 11 | void AddCommands(Commands cmd); 12 | 13 | /// 14 | /// Checks for duplicate commands in the provided list and removes them. 15 | /// 16 | void CheckForDuplicateCommands(); 17 | 18 | /// 19 | /// Converts custom commands stored in the global plugin state by processing each command string, 20 | /// splitting it by commas or semicolons, and adjusting the command structure. 21 | /// 22 | /// The method clones each command, assigns a new unique ID, and processes its arguments. 23 | /// Each split command is then added back to the global list with updated formatting. 24 | /// 25 | void ConvertingCommandsForRegister(); 26 | } 27 | -------------------------------------------------------------------------------- /CustomCommands/Interfaces/IReplaceTagsFunctions.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | 3 | namespace CustomCommands.Interfaces; 4 | 5 | public interface IReplaceTagsFunctions 6 | { 7 | /// 8 | /// Replaces tags in the input array with their corresponding values. 9 | /// 10 | /// The array of strings containing tags to be replaced. 11 | /// The CCSPlayerController object used for tag replacement. 12 | /// The array of strings with tags replaced. 13 | string[] ReplaceTags(dynamic input, CCSPlayerController player); 14 | 15 | /// 16 | /// Replaces language tags in the input string with the corresponding localized value. 17 | /// Language tags are defined within curly braces, e.g. "{LANG=LocalizerTag}". 18 | /// If a language tag is found, it is replaced with the localized value from the CustomCommands plugin's Localizer. 19 | /// If the localized value is not found, a default message is returned. 20 | /// 21 | /// The input string to process. 22 | /// The input string with language tags replaced with localized values. 23 | string ReplaceLanguageTags(string input); 24 | 25 | /// 26 | /// Replaces tags in the input string with corresponding values based on the provided player information. 27 | /// 28 | /// The input string containing tags to be replaced. 29 | /// The CCSPlayerController object representing the player. 30 | /// A boolean value indicating whether to replace the {PLAYERNAME} tag. Default is true. 31 | /// The modified string with replaced tags. 32 | string ReplaceMessageTags(string input, CCSPlayerController player, bool safety = true); 33 | string ReplaceColorTags(string input); 34 | 35 | /// 36 | /// Splits the input into an array of strings. If the input is a string, it will be split by newlines. If the input is an array, each element will be split by newlines. 37 | /// 38 | /// This should be a string[] or a string 39 | /// An array of strings representing the lines of the input. 40 | List WrappedLine(dynamic message); 41 | 42 | string ReplaceRandomTags(string message); 43 | } -------------------------------------------------------------------------------- /CustomCommands/Model/CenterElement.cs: -------------------------------------------------------------------------------- 1 | namespace CustomCommands.Model; 2 | 3 | public class CenterClientElement 4 | { 5 | public int ClientId { get; set; } = -1; 6 | public string Message { get; set; } = ""; 7 | } 8 | public class CenterServerElement 9 | { 10 | public string Message { get; set; } = ""; 11 | public bool IsRunning { get; set; } = false; 12 | } -------------------------------------------------------------------------------- /CustomCommands/Model/CommandsConfig.cs: -------------------------------------------------------------------------------- 1 | namespace CustomCommands.Model; 2 | 3 | public class Commands : ICloneable 4 | { 5 | public Guid ID { get; set; } = Guid.NewGuid(); 6 | public string? Argument { get; set; } 7 | public string Title { get; set; } = ""; 8 | public string Description { get; set; } = "Description"; 9 | public string Command { get; set; } = ""; 10 | public dynamic Cooldown { get; set; } = 0; 11 | public dynamic Message { get; set; } = ""; 12 | public CenterElement CenterMessage { get; set; } = new(); 13 | public Sender PrintTo { get; set; } = Sender.ClientChat; 14 | public List ServerCommands { get; set; } = new(); 15 | public List ClientCommands { get; set; } = new(); 16 | public List ClientCommandsFromServer { get; set; } = new(); 17 | public Permission? Permission { get; set; } = new(); 18 | public bool IsRegisterable { get; set; } = true; 19 | 20 | public object Clone() 21 | { 22 | return new Commands() 23 | { 24 | ID = ID, 25 | Argument = Argument, 26 | Title = Title, 27 | Description = Description, 28 | Command = Command, 29 | Cooldown = Cooldown, 30 | Message = Message, 31 | CenterMessage = CenterMessage, 32 | PrintTo = PrintTo, 33 | ServerCommands = new List(ServerCommands), 34 | ClientCommands = new List(ClientCommands), 35 | ClientCommandsFromServer = new List(ClientCommandsFromServer), 36 | Permission = Permission?.Clone() as Permission, 37 | IsRegisterable = IsRegisterable 38 | }; 39 | } 40 | } 41 | public class Cooldown 42 | { 43 | public int CooldownTime { get; set; } = 0; 44 | public bool IsGlobal { get; set; } = false; 45 | public string CooldownMessage { get; set; } = ""; 46 | } 47 | public class Permission : ICloneable 48 | { 49 | public bool RequiresAllPermissions { get; set; } = false; 50 | public List PermissionList { get; set; } = new(); 51 | 52 | public object Clone() 53 | { 54 | return MemberwiseClone(); 55 | } 56 | 57 | } 58 | public class CenterElement 59 | { 60 | public string Message { get; set; } = ""; 61 | public int Time { get; set; } = 1; 62 | } 63 | public enum Sender 64 | { 65 | ClientChat = 0, 66 | AllChat = 1, 67 | ClientCenter = 2, 68 | AllCenter = 3, 69 | ClientChatClientCenter = 4, 70 | ClientChatAllCenter = 5, 71 | AllChatClientCenter = 6, 72 | AllChatAllCenter = 7 73 | } 74 | -------------------------------------------------------------------------------- /CustomCommands/Model/CooldownTimer.cs: -------------------------------------------------------------------------------- 1 | namespace CustomCommands.Model; 2 | 3 | public class CooldownTimer 4 | { 5 | public bool IsGlobal { get; set; } = false; 6 | public int PlayerID { get; set; } 7 | public required Guid CommandID { get; set; } 8 | public required DateTime CooldownTime { get; set; } 9 | } -------------------------------------------------------------------------------- /CustomCommands/Model/Receiver.cs: -------------------------------------------------------------------------------- 1 | namespace CustomCommands.Model; 2 | 3 | public enum Receiver 4 | { 5 | Client = 0, 6 | Server = 1 7 | } -------------------------------------------------------------------------------- /CustomCommands/Services/CooldownManager.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using CounterStrikeSharp.API.Core; 3 | using CustomCommands.Model; 4 | 5 | namespace CustomCommands.Interfaces; 6 | 7 | public class CooldownManager : ICooldownManager 8 | { 9 | public IPluginGlobals _pluginGlobals { get; } 10 | public IReplaceTagsFunctions _replaceTagsFunctions { get; } 11 | 12 | public CooldownManager(IPluginGlobals PluginGlobals, IReplaceTagsFunctions ReplaceTagsFunctions) 13 | { 14 | _pluginGlobals = PluginGlobals; 15 | _replaceTagsFunctions = ReplaceTagsFunctions; 16 | } 17 | 18 | /// 19 | /// Checks if the command is on cooldown 20 | /// 21 | /// 22 | /// 23 | /// 24 | public bool IsCommandOnCooldown(CCSPlayerController player, Commands cmd) 25 | { 26 | // Check global cooldown 27 | if (IsCommandOnCooldownWithCondition(x => x.IsGlobal == true && x.CommandID == cmd.ID, player, cmd)) 28 | return true; 29 | 30 | // Check player cooldown 31 | if (IsCommandOnCooldownWithCondition(x => x.PlayerID == player.UserId && x.CommandID == cmd.ID, player, cmd)) 32 | return true; 33 | 34 | return false; 35 | } 36 | 37 | /// 38 | /// Checks if a command is on cooldown based on a given condition. 39 | /// 40 | /// The condition to check for each cooldown timer. 41 | /// The player controller. 42 | /// The command. 43 | /// True if the command is on cooldown, false otherwise. 44 | public bool IsCommandOnCooldownWithCondition(Func predicate, CCSPlayerController player, Commands cmd) 45 | { 46 | var index = _pluginGlobals.CooldownTimer.FindIndex(x => predicate(x) && x.CooldownTime > DateTime.Now); 47 | 48 | if (index != -1) 49 | { 50 | var totalSeconds = (double)_pluginGlobals.CooldownTimer[index].CooldownTime.Subtract(DateTime.Now).TotalSeconds; 51 | var totalSecondsRounded = (int)Math.Round(totalSeconds); 52 | var timeleft = totalSecondsRounded.ToString(); 53 | var message = ""; 54 | 55 | // This is ugly as fuck 56 | try 57 | { 58 | var cooldown = JsonSerializer.Deserialize(cmd.Cooldown.GetRawText()); 59 | Console.WriteLine(cooldown.CooldownMessage); 60 | string[] replaceTimeleft = {cooldown.CooldownMessage.Replace("{TIMELEFT}", timeleft)}; 61 | message = _replaceTagsFunctions.ReplaceTags(replaceTimeleft, player)[0]; 62 | } 63 | catch (JsonException) 64 | { 65 | message = $"This command is for {timeleft} seconds on cooldown"; 66 | } 67 | 68 | player.PrintToChat($"{_pluginGlobals.Config.Prefix}{message}"); 69 | 70 | return true; 71 | } 72 | 73 | return false; 74 | } 75 | 76 | /// 77 | /// Adds the command to the cooldown list 78 | /// 79 | /// 80 | /// 81 | /// 82 | /// 83 | public void AddToCooldownList(bool isGlobal, int playerID, Guid commandID, int cooldownTime) 84 | { 85 | var timer = new CooldownTimer() { 86 | IsGlobal = isGlobal, 87 | CommandID = commandID, 88 | CooldownTime = DateTime.Now.AddSeconds(cooldownTime) 89 | }; 90 | 91 | if (isGlobal) 92 | { 93 | int index = _pluginGlobals.CooldownTimer.FindIndex(x => 94 | x.IsGlobal == true 95 | && x.CommandID == commandID); 96 | 97 | if (index != -1) 98 | _pluginGlobals.CooldownTimer[index].CooldownTime = timer.CooldownTime; 99 | else 100 | _pluginGlobals.CooldownTimer.Add(timer); 101 | } 102 | else 103 | { 104 | timer.PlayerID = playerID; 105 | int index = _pluginGlobals.CooldownTimer.FindIndex(x => 106 | x.PlayerID == playerID 107 | && x.CommandID == commandID); 108 | if (index != -1) 109 | _pluginGlobals.CooldownTimer[index].CooldownTime = timer.CooldownTime; 110 | else 111 | _pluginGlobals.CooldownTimer.Add(timer); 112 | } 113 | } 114 | 115 | /// 116 | /// Sets the cooldown for the command 117 | /// 118 | /// Need to add the player if the Cooldown is only for a specific player 119 | /// 120 | public void SetCooldown(CCSPlayerController player, Commands cmd) 121 | { 122 | if (cmd.Cooldown is JsonElement jsonElement) 123 | { 124 | switch (jsonElement.ValueKind) 125 | { 126 | case JsonValueKind.Number: 127 | 128 | var cooldown = cmd.Cooldown.GetInt32(); 129 | if (cooldown == 0) 130 | break; 131 | 132 | AddToCooldownList(false, player.UserId ?? 0, cmd.ID, cooldown); 133 | break; 134 | 135 | case JsonValueKind.Object: 136 | 137 | var cooldownObject = JsonSerializer.Deserialize(cmd.Cooldown.GetRawText()); 138 | AddToCooldownList(cooldownObject.IsGlobal, player.UserId ?? 0, cmd.ID, cooldownObject.CooldownTime); 139 | break; 140 | 141 | default: 142 | break; 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /CustomCommands/Services/EventManager.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Core.Attributes.Registration; 4 | using CounterStrikeSharp.API.Core.Plugin; 5 | using CustomCommands.Interfaces; 6 | 7 | namespace CustomCommands.Services; 8 | 9 | public class EventManager : IEventManager 10 | { 11 | private readonly IPluginGlobals _pluginGlobals; 12 | private readonly PluginContext _pluginContext; 13 | private readonly IReplaceTagsFunctions _replaceTagsFunctions; 14 | 15 | public EventManager(IPluginGlobals PluginGlobals, IPluginContext PluginContext, IReplaceTagsFunctions ReplaceTagsFunctions) 16 | { 17 | _pluginGlobals = PluginGlobals; 18 | _pluginContext = (PluginContext as PluginContext)!; 19 | _replaceTagsFunctions = ReplaceTagsFunctions; 20 | } 21 | 22 | [GameEventHandler] 23 | public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo _) 24 | { 25 | _pluginGlobals.centerClientOn.RemoveAll(p => p.ClientId == @event.Userid.UserId); 26 | _pluginGlobals.CooldownTimer.RemoveAll(p => p.PlayerID == @event.Userid.UserId); 27 | 28 | return HookResult.Continue; 29 | } 30 | 31 | public void RegisterListeners() 32 | { 33 | var plugin = (_pluginContext.Plugin as CustomCommands)!; 34 | 35 | // Register the OnTick event for PrintToCenterHtml duration 36 | plugin.RegisterListener(() => 37 | { 38 | // Client Print To Center 39 | if(_pluginGlobals.centerClientOn != null && _pluginGlobals.centerClientOn.Count !> 0 ) 40 | { 41 | foreach (var player in _pluginGlobals.centerClientOn) 42 | { 43 | var targetPlayer = Utilities.GetPlayerFromUserid(player.ClientId); 44 | if (player != null && targetPlayer != null) 45 | { 46 | targetPlayer.PrintToCenterHtml(player.Message, 1); 47 | } 48 | } 49 | } 50 | 51 | // Server Print To Center 52 | if (_pluginGlobals.centerServerOn.IsRunning) 53 | { 54 | Utilities.GetPlayers().ForEach(controller => 55 | { 56 | if (!controller.IsValid || controller.SteamID == 0) return; 57 | 58 | var message = _replaceTagsFunctions.ReplaceLanguageTags(_pluginGlobals.centerServerOn.Message); 59 | message = _replaceTagsFunctions.ReplaceMessageTags(message, controller); 60 | controller.PrintToCenterHtml(message, 1); 61 | }); 62 | } 63 | }); 64 | 65 | plugin.RegisterListener(() => 66 | { 67 | _pluginGlobals.centerClientOn.Clear(); 68 | _pluginGlobals.CooldownTimer.Clear(); 69 | }); 70 | } 71 | } -------------------------------------------------------------------------------- /CustomCommands/Services/LoadJson.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | using CustomCommands.Interfaces; 4 | using CustomCommands.Model; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace CustomCommands.Services; 8 | 9 | public class LoadJson : ILoadJson 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public LoadJson(ILogger Logger) 14 | { 15 | _logger = Logger; 16 | } 17 | 18 | public async Task> GetCommandsFromJsonFiles(string path) 19 | { 20 | var comms = new List(); 21 | 22 | var defaultconfigpath = Path.Combine(path, "Commands.json"); 23 | 24 | CheckForExampleFile(path); 25 | 26 | var pathofcommands = Path.Combine(path, "Commands"); 27 | 28 | var files = new List(); 29 | 30 | if (Directory.Exists(pathofcommands)) 31 | files.AddRange(Directory.GetFiles(pathofcommands, "*.json", SearchOption.AllDirectories)); 32 | 33 | // Check if the default config file exists in plugins/CustomCommands 34 | if (File.Exists(defaultconfigpath)) 35 | { 36 | files.Add(defaultconfigpath); 37 | _logger.LogInformation("Found default config file."); 38 | } 39 | else if (!File.Exists(defaultconfigpath) && files.Count == 0) 40 | { 41 | _logger.LogWarning("No Config file found. Please create plugins/CustomCommands/Commands.json or in plugins/CustomCommands/Commands/.json"); 42 | return comms; 43 | } 44 | 45 | // Create a list of tasks to handle each file asynchronously 46 | var tasks = files.Select(async file => 47 | { 48 | var json = string.Empty; 49 | 50 | // Read Unicode Characters asynchronously 51 | using (var sr = new StreamReader(file, Encoding.UTF8)) 52 | json = await sr.ReadToEndAsync(); 53 | 54 | // Validate the JSON file 55 | if (!IsValidJsonSyntax(json, file)) 56 | return; 57 | 58 | // Deserialize and validate the commands 59 | var commands = JsonSerializer.Deserialize>(json); 60 | if (ValidateObject(commands, file)) 61 | { 62 | lock (comms) // Ensure thread-safety while adding to the shared list 63 | { 64 | comms.AddRange(commands!); 65 | } 66 | } 67 | }); 68 | 69 | await Task.WhenAll(tasks); 70 | 71 | return comms; 72 | } 73 | 74 | public void CheckForExampleFile(string path) 75 | { 76 | if (Directory.Exists(Path.Combine(path, "Commands"))) 77 | { 78 | var files = Directory.GetFiles(Path.Combine(path, "Commands"), "*.json", SearchOption.AllDirectories); 79 | if (files.Length > 0) 80 | return; 81 | } 82 | 83 | var defaultconfigpath = Path.Combine(path, "Commands.json"); 84 | var exampleconfigpath = Path.Combine(path, "Commands.example.json"); 85 | if (!File.Exists(defaultconfigpath)) 86 | { 87 | File.Copy(exampleconfigpath, defaultconfigpath); 88 | _logger.LogInformation("Created default config file."); 89 | } 90 | } 91 | 92 | public bool IsValidJsonSyntax(string json, string path) 93 | { 94 | try 95 | { 96 | var document = JsonDocument.Parse(json); 97 | return true; 98 | } 99 | catch (JsonException ex) 100 | { 101 | _logger.LogError($"Invalid JSON syntax in {path}. Please check the docs on how to create a valid JSON file"); 102 | _logger.LogError(ex.Message); 103 | return false; 104 | } 105 | } 106 | 107 | public bool ValidateObject(List? comms, string path) 108 | { 109 | if (comms == null) 110 | { 111 | _logger.LogError($"Invalid JSON format in {path}. Please check the docs on how to create a valid JSON file"); 112 | return false; 113 | } 114 | var commandstatus = true; 115 | for (int i = 0; i < comms.Count; i++) 116 | { 117 | commandstatus = true; 118 | // Title 119 | if (string.IsNullOrEmpty(comms[i].Title)) 120 | { 121 | _logger.LogWarning($"Title not set in {path}. Title is not required but recommended"); 122 | commandstatus = false; 123 | } 124 | // Description 125 | if (string.IsNullOrEmpty(comms[i].Description)) 126 | { 127 | _logger.LogWarning($"Description not set in {path}. Description is not required but recommended. This will be shown in the help command"); 128 | commandstatus = false; 129 | } 130 | // Command 131 | if (string.IsNullOrEmpty(comms[i].Command)) 132 | { 133 | _logger.LogError($"Command not set in {path}"); 134 | commandstatus = false; 135 | } 136 | if (!PrintToCheck(comms[i])) 137 | commandstatus = false; 138 | 139 | if (!commandstatus) 140 | { 141 | _logger.LogError($"Command {comms[i].Command} will not be loaded. Index: {i}"); 142 | LogCommandDetails(comms[i]); 143 | } 144 | } 145 | if (!commandstatus) 146 | return false; 147 | return true; 148 | } 149 | 150 | public void LogCommandDetails(Commands comms) 151 | { 152 | _logger.LogInformation($"-- Title: {comms.Title}"); 153 | _logger.LogInformation($"-- Description: {comms.Description}"); 154 | _logger.LogInformation($"-- Command: {comms.Command}"); 155 | _logger.LogInformation($"-- Message: {comms.Message}"); 156 | _logger.LogInformation($"-- CenterMessage: {comms.CenterMessage.Message}"); 157 | _logger.LogInformation($"-- CenterMessageTime: {comms.CenterMessage.Time}"); 158 | _logger.LogInformation($"-- PrintTo: {comms.PrintTo}"); 159 | _logger.LogInformation($"-- ServerCommands: {JsonSerializer.Serialize(comms.ServerCommands)}"); 160 | _logger.LogInformation($"-- PermissionList: {JsonSerializer.Serialize(comms.Permission)}"); 161 | _logger.LogInformation("--------------------------------------------------"); 162 | } 163 | 164 | public bool PrintToCheck(Commands comms) 165 | { 166 | if (comms.PrintTo == Sender.ClientChat || comms.PrintTo == Sender.AllChat) 167 | { 168 | 169 | if (!ValidateMessage(comms.Message)) 170 | { 171 | _logger.LogError($"Message not set but needs to be set because PrintTo is set to {comms.PrintTo}"); 172 | return false; 173 | } 174 | } 175 | else if (comms.PrintTo == Sender.ClientCenter || comms.PrintTo == Sender.AllCenter) 176 | { 177 | if (string.IsNullOrEmpty(comms.CenterMessage.Message)) 178 | { 179 | _logger.LogError($"CenterMessage is not set but needs to be set because PrintTo is set to {comms.PrintTo}"); 180 | return false; 181 | } 182 | } 183 | else 184 | { 185 | if (!ValidateMessage(comms.Message) && string.IsNullOrEmpty(comms.CenterMessage.Message)) 186 | { 187 | _logger.LogError($"Message and CenterMessage are not set but needs to be set because PrintTo is set to {comms.PrintTo}"); 188 | return false; 189 | } 190 | } 191 | return true; 192 | } 193 | 194 | public bool ValidateMessage(dynamic message) 195 | { 196 | if (message is JsonElement jsonElement) 197 | { 198 | if (jsonElement.ValueKind == JsonValueKind.String) 199 | return true; 200 | 201 | if (jsonElement.ValueKind == JsonValueKind.Array) 202 | return true; 203 | 204 | return false; 205 | } 206 | return false; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /CustomCommands/Services/MessageManager.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Core.Plugin; 4 | using CustomCommands.Interfaces; 5 | using CustomCommands.Model; 6 | 7 | namespace CustomCommands.Services; 8 | public class MessageManager : IMessageManager 9 | { 10 | private readonly IPluginGlobals _pluginGlobals; 11 | private readonly IReplaceTagsFunctions _replaceTagsFunctions; 12 | private readonly PluginContext _pluginContext; 13 | 14 | public MessageManager(IPluginGlobals PluginGlobals, IReplaceTagsFunctions ReplaceTagsFunctions, IPluginContext PluginContext) 15 | { 16 | _pluginGlobals = PluginGlobals; 17 | _replaceTagsFunctions = ReplaceTagsFunctions; 18 | _pluginContext = (PluginContext as PluginContext)!; 19 | } 20 | 21 | public void SendMessage(CCSPlayerController player, Commands cmd) 22 | { 23 | switch (cmd.PrintTo) 24 | { 25 | case Sender.ClientChat: 26 | PrintToChat(Receiver.Client, player, cmd.Message); 27 | break; 28 | case Sender.AllChat: 29 | PrintToChat(Receiver.Server, player, cmd.Message); 30 | break; 31 | case Sender.ClientCenter: 32 | PrintToCenterClient(player, cmd); 33 | break; 34 | case Sender.AllCenter: 35 | PrintToAllCenter(cmd); 36 | break; 37 | case Sender.ClientChatClientCenter: 38 | PrintToChatAndCenter(Receiver.Client, player, cmd); 39 | break; 40 | case Sender.ClientChatAllCenter: 41 | PrintToChatAndAllCenter(Receiver.Client, player, cmd); 42 | break; 43 | case Sender.AllChatClientCenter: 44 | PrintToChatAndCenter(Receiver.Server, player, cmd); 45 | break; 46 | case Sender.AllChatAllCenter: 47 | PrintToChatAndAllCenter(Receiver.Server, player, cmd); 48 | break; 49 | default: 50 | break; 51 | } 52 | } 53 | 54 | public void PrintToCenterClient(CCSPlayerController player, Commands cmd) 55 | { 56 | var context = (_pluginContext.Plugin as CustomCommands)!; 57 | var message = _replaceTagsFunctions.ReplaceLanguageTags(cmd.CenterMessage.Message); 58 | message = _replaceTagsFunctions.ReplaceMessageTags(message, player); 59 | 60 | var CenterClientElement = new CenterClientElement 61 | { 62 | ClientId = player.UserId!.Value, 63 | Message = message 64 | }; 65 | _pluginGlobals.centerClientOn.Add(CenterClientElement); 66 | context.AddTimer(cmd.CenterMessage.Time, () => _pluginGlobals.centerClientOn.Remove(CenterClientElement)); 67 | } 68 | 69 | public void PrintToAllCenter(Commands cmd) 70 | { 71 | var context = (_pluginContext.Plugin as CustomCommands)!; 72 | _pluginGlobals.centerServerOn.Message = cmd.CenterMessage.Message; 73 | _pluginGlobals.centerServerOn.IsRunning = true; 74 | 75 | context.AddTimer(cmd.CenterMessage.Time, () => 76 | { 77 | _pluginGlobals.centerServerOn.IsRunning = false; 78 | }); 79 | } 80 | 81 | public void PrintToChatAndCenter(Receiver receiver, CCSPlayerController player, Commands cmd) 82 | { 83 | PrintToChat(receiver, player, cmd.Message); 84 | PrintToCenterClient(player, cmd); 85 | } 86 | 87 | public void PrintToChatAndAllCenter(Receiver receiver, CCSPlayerController player, Commands cmd) 88 | { 89 | PrintToChat(receiver, player, cmd.Message); 90 | PrintToAllCenter(cmd); 91 | } 92 | 93 | public void PrintToChat(Receiver printToChat, CCSPlayerController player, dynamic message) 94 | { 95 | var msg = _replaceTagsFunctions.ReplaceTags(message, player); 96 | 97 | switch (printToChat) 98 | { 99 | case Receiver.Client: 100 | PrintToChatClient(player, msg); 101 | break; 102 | case Receiver.Server: 103 | PrintToChatServer(msg); 104 | break; 105 | default: 106 | break; 107 | } 108 | } 109 | 110 | public void PrintToChatClient(CCSPlayerController player, string[] msg) 111 | { 112 | foreach (var line in msg) 113 | player.PrintToChat(line); 114 | } 115 | 116 | public void PrintToChatServer(string[] msg) 117 | { 118 | foreach (var line in msg) 119 | Server.PrintToChatAll(line); 120 | } 121 | } -------------------------------------------------------------------------------- /CustomCommands/Services/PluginGlobals.cs: -------------------------------------------------------------------------------- 1 | using CustomCommands.Interfaces; 2 | using CustomCommands.Model; 3 | 4 | namespace CustomCommands.Services; 5 | public class PluginGlobals : IPluginGlobals 6 | { 7 | /// 8 | /// List of clients that have a message printed to their center. 9 | /// 10 | public List centerClientOn { get; set; } = new(); 11 | /// 12 | /// The message that is printed to all players' center. 13 | /// 14 | public CenterServerElement centerServerOn { get; set; } = new(); 15 | /// 16 | /// The configuration for the plugin. 17 | /// 18 | public CustomCommandsConfig Config { get; set; } = new(); 19 | /// 20 | /// List of cooldown timers for each player. 21 | /// 22 | public List CooldownTimer { get; set; } = new(); 23 | /// 24 | /// List of custom commands. 25 | /// 26 | public List CustomCommands { get; set; } = new(); 27 | } 28 | -------------------------------------------------------------------------------- /CustomCommands/Services/PluginUtilities.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using CounterStrikeSharp.API; 3 | using CounterStrikeSharp.API.Core; 4 | using CounterStrikeSharp.API.Modules.Admin; 5 | using CounterStrikeSharp.API.Modules.Cvars; 6 | using CustomCommands.Interfaces; 7 | using CustomCommands.Model; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace CustomCommands.Services; 11 | 12 | public partial class PluginUtilities : IPluginUtilities 13 | { 14 | private readonly IPluginGlobals _pluginGlobals; 15 | private readonly IReplaceTagsFunctions _replaceTagsFunctions; 16 | private readonly ILogger _logger; 17 | 18 | public PluginUtilities(IPluginGlobals PluginGlobals, IReplaceTagsFunctions ReplaceTagsFunctions, 19 | ILogger Logger) 20 | { 21 | _pluginGlobals = PluginGlobals; 22 | _replaceTagsFunctions = ReplaceTagsFunctions; 23 | _logger = Logger; 24 | } 25 | 26 | public string[] AddCSSTagsToAliases(List input) 27 | { 28 | for (int i = 0; i < input.Count; i++) 29 | { 30 | if (!input[i].StartsWith("css_")) 31 | input[i] = "css_" + input[i]; 32 | } 33 | return input.ToArray(); 34 | } 35 | 36 | [GeneratedRegex("[,;]")] 37 | private static partial Regex SplitStringByCommaOrSemicolonRegex(); 38 | 39 | public string[] SplitStringByCommaOrSemicolon(string str) 40 | { 41 | return SplitStringByCommaOrSemicolonRegex().Split(str) 42 | .Where(s => !string.IsNullOrEmpty(s)) 43 | .ToArray(); 44 | } 45 | 46 | public void ExecuteServerCommands(Commands cmd, CCSPlayerController player) 47 | { 48 | if (cmd.ServerCommands.Count == 0) 49 | return; 50 | 51 | foreach (var serverCommand in cmd.ServerCommands) 52 | { 53 | // If the command starts with "toggle" we want to toggle the cvar 54 | if (serverCommand.StartsWith("toggle")) 55 | { 56 | HandleToggleCommand(serverCommand); 57 | continue; 58 | } 59 | 60 | 61 | Server.ExecuteCommand(_replaceTagsFunctions.ReplaceMessageTags(serverCommand, player)); 62 | } 63 | } 64 | 65 | public void ExecuteClientCommands(Commands cmd, CCSPlayerController player) 66 | { 67 | if (cmd.ClientCommands.Count == 0) 68 | return; 69 | 70 | foreach (var clientCommand in cmd.ClientCommands) 71 | { 72 | player.ExecuteClientCommand(_replaceTagsFunctions.ReplaceMessageTags(clientCommand, player)); 73 | } 74 | } 75 | 76 | public void ExecuteClientCommandsFromServer(Commands cmd, CCSPlayerController player) 77 | { 78 | if (cmd.ClientCommandsFromServer.Count == 0) 79 | return; 80 | 81 | foreach (var clientCommandsFromServer in cmd.ClientCommandsFromServer) 82 | { 83 | player.ExecuteClientCommandFromServer(_replaceTagsFunctions.ReplaceMessageTags(clientCommandsFromServer, player)); 84 | } 85 | } 86 | 87 | /// 88 | /// Handles the toggle command 89 | /// 90 | /// 91 | private void HandleToggleCommand(string serverCommand) 92 | { 93 | var commandWithoutToggle = serverCommand.Replace("toggle ", ""); 94 | var commandCvar = ConVar.Find(commandWithoutToggle); 95 | 96 | if (commandCvar != null) 97 | { 98 | if (commandCvar.GetPrimitiveValue()) 99 | Server.ExecuteCommand($"{commandWithoutToggle} 0"); 100 | else 101 | Server.ExecuteCommand($"{commandWithoutToggle} 1"); 102 | } 103 | else 104 | { 105 | _logger.LogError($"Couldn't toggle {commandWithoutToggle}. Please check if this command is toggleable"); 106 | } 107 | } 108 | 109 | public bool RequiresPermissions(CCSPlayerController player, Permission permissions) 110 | { 111 | if (!permissions.RequiresAllPermissions) 112 | { 113 | foreach (var permission in permissions.PermissionList) 114 | { 115 | if (AdminManager.PlayerHasPermissions(player, new string[] { permission })) 116 | return true; 117 | } 118 | player.PrintToChat($"{_pluginGlobals.Config.Prefix}You don't have the required permissions to execute this command"); 119 | return false; 120 | } 121 | else 122 | { 123 | if (!AdminManager.PlayerHasPermissions(player, permissions.PermissionList.ToArray())) 124 | { 125 | player.PrintToChat($"{_pluginGlobals.Config.Prefix}You don't have the required permissions to execute this command"); 126 | return false; 127 | } 128 | return true; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /CustomCommands/Services/RegisterCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core.Plugin; 2 | using CounterStrikeSharp.API.Modules.Commands; 3 | using CustomCommands.Interfaces; 4 | using CustomCommands.Model; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace CustomCommands.Services; 8 | public class RegisterCommands : IRegisterCommands 9 | { 10 | private readonly ILogger _logger; 11 | private readonly IMessageManager _messageManager; 12 | private readonly IPluginGlobals _pluginGlobals; 13 | private readonly PluginContext _pluginContext; 14 | private readonly IPluginUtilities _pluginUtilities; 15 | private readonly ICooldownManager _cooldownManager; 16 | 17 | public RegisterCommands(ILogger Logger, IMessageManager MessageManager, 18 | IPluginGlobals PluginGlobals, IPluginContext PluginContext, 19 | IPluginUtilities PluginUtilities, ICooldownManager CooldownManager) 20 | { 21 | _logger = Logger; 22 | _messageManager = MessageManager; 23 | _pluginGlobals = PluginGlobals; 24 | _pluginContext = (PluginContext as PluginContext)!; 25 | _pluginUtilities = PluginUtilities; 26 | _cooldownManager = CooldownManager; 27 | } 28 | 29 | public void AddCommands(Commands cmd) 30 | { 31 | if (!cmd.IsRegisterable) 32 | return; 33 | 34 | var context = (_pluginContext.Plugin as CustomCommands)!; 35 | 36 | context.AddCommand(cmd.Command, cmd.Description, 37 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 38 | (player, info) => 39 | { 40 | if (player == null) 41 | return; 42 | 43 | var command = cmd; 44 | 45 | // Check if the command has arguments and if it does, check if the command exists and is the right one 46 | if (info.ArgCount > 1) 47 | { 48 | var findcommand = _pluginGlobals.CustomCommands.Find(x => x.Command == command.Command && x.Argument == info.ArgString); 49 | // Check if the command is the right one with the right arguments 50 | if (findcommand is null) 51 | { 52 | player.PrintToChat("This command requires no Arguments or you added to many"); 53 | return; 54 | } 55 | 56 | if (!findcommand.Argument!.Equals(info.ArgString)) 57 | { 58 | player.PrintToChat("Wrong Arguments"); 59 | return; 60 | } 61 | 62 | command = findcommand; 63 | } 64 | 65 | // This will exit the command if the command has arguments but the client didn't provide any 66 | if (info.ArgCount <= 1 && !string.IsNullOrEmpty(command.Argument!)) 67 | { 68 | player.PrintToChat("This command requires Arguments"); 69 | return; 70 | } 71 | 72 | // Check if the player has the permission to use the command 73 | if (command!.Permission?.PermissionList.Count > 0 && command.Permission != null) 74 | if (!_pluginUtilities.RequiresPermissions(player, command.Permission)) 75 | return; 76 | 77 | // Check if the command is on cooldown 78 | if(_cooldownManager.IsCommandOnCooldown(player, command)) return; 79 | 80 | // Set the cooldown for the command if it has a cooldown set 81 | _cooldownManager.SetCooldown(player, command); 82 | 83 | // Sending the message to the player 84 | _messageManager.SendMessage(player, command); 85 | 86 | // Execute the client commands 87 | _pluginUtilities.ExecuteClientCommands(command, player); 88 | 89 | // Execute the client commands from the server 90 | _pluginUtilities.ExecuteClientCommandsFromServer(command, player); 91 | 92 | // Execute the server commands 93 | _pluginUtilities.ExecuteServerCommands(command, player); 94 | }); 95 | } 96 | 97 | public void CheckForDuplicateCommands() 98 | { 99 | var comms = _pluginGlobals.CustomCommands; 100 | var duplicateCommands = new List(); 101 | var commandNames = new List(); 102 | 103 | foreach (var cmd in comms) 104 | { 105 | string[] aliases = cmd.Command.Split(','); 106 | 107 | foreach (var alias in aliases) 108 | { 109 | if (commandNames.Contains(alias.ToLower())) 110 | { 111 | duplicateCommands.Add(cmd); 112 | continue; 113 | } 114 | commandNames.Add(alias.ToLower()); 115 | } 116 | } 117 | 118 | if (duplicateCommands.Count == 0) 119 | return; 120 | 121 | // Log the duplicate commands 122 | _logger.LogError($"------------------------------------------------------------------------"); 123 | _logger.LogError($"{_pluginGlobals.Config.LogPrefix} Duplicate commands found, removing them from the list. Please check your config file for duplicate commands and remove them."); 124 | for (int i = 0; i < comms.Count; i++) 125 | { 126 | if(duplicateCommands.Contains(comms[i])) 127 | { 128 | _logger.LogError($"{_pluginGlobals.Config.LogPrefix} Duplicate command found index {i+1}: "); 129 | _logger.LogError($"{_pluginGlobals.Config.LogPrefix} - {comms[i].Title} "); 130 | _logger.LogError($"{_pluginGlobals.Config.LogPrefix} - {comms[i].Description}"); 131 | _logger.LogError($"{_pluginGlobals.Config.LogPrefix} - {comms[i].Command}"); 132 | continue; 133 | } 134 | 135 | comms.Add(comms[i]); 136 | } 137 | _logger.LogError($"------------------------------------------------------------------------"); 138 | } 139 | 140 | public void ConvertingCommandsForRegister() 141 | { 142 | var newCmds = new List(); 143 | 144 | foreach (var cmd in _pluginGlobals.CustomCommands) 145 | { 146 | var splitCommands = _pluginUtilities.SplitStringByCommaOrSemicolon(cmd.Command); 147 | splitCommands = _pluginUtilities.AddCSSTagsToAliases(splitCommands.ToList()); 148 | 149 | foreach (var split in splitCommands) 150 | { 151 | var args = split.Split(' '); 152 | 153 | if(args.Length == 1) 154 | { 155 | var newCmd = cmd.Clone() as Commands; 156 | newCmd!.ID = Guid.NewGuid(); 157 | newCmd.Command = split.Trim(); 158 | newCmds.Add(newCmd); 159 | } 160 | else if (args.Length > 1) 161 | { 162 | var newCmd = cmd.Clone() as Commands; 163 | 164 | if (newCmds.Any(p => p.Command.Contains(args[0]))) 165 | newCmd!.IsRegisterable = false; 166 | 167 | newCmd!.ID = Guid.NewGuid(); 168 | newCmd.Command = args[0].Trim(); 169 | args[0] = ""; 170 | newCmd.Argument = string.Join(" ", args).Trim(); 171 | newCmds.Add(newCmd); 172 | } 173 | } 174 | } 175 | 176 | _pluginGlobals.CustomCommands = newCmds; 177 | } 178 | } -------------------------------------------------------------------------------- /CustomCommands/Services/ReplaceTagsFunction.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.RegularExpressions; 3 | using CounterStrikeSharp.API; 4 | using CounterStrikeSharp.API.Core; 5 | using CounterStrikeSharp.API.Modules.Cvars; 6 | using CounterStrikeSharp.API.Modules.Entities; 7 | using CounterStrikeSharp.API.Modules.Utils; 8 | using CustomCommands.Interfaces; 9 | using Microsoft.Extensions.Logging; 10 | using CounterStrikeSharp.API.Core.Plugin; 11 | 12 | namespace CustomCommands.Services; 13 | public partial class ReplaceTagsFunctions : IReplaceTagsFunctions 14 | { 15 | private readonly IPluginGlobals _pluginGlobals; 16 | private readonly PluginContext _pluginContext; 17 | private readonly ILogger _logger; 18 | 19 | private static readonly Random _random = new Random(); 20 | 21 | public ReplaceTagsFunctions(IPluginGlobals PluginGlobals, IPluginContext PluginContext, 22 | ILogger Logger) 23 | { 24 | _pluginGlobals = PluginGlobals; 25 | _pluginContext = (PluginContext as PluginContext)!; 26 | _logger = Logger; 27 | } 28 | 29 | public string[] ReplaceTags(dynamic input, CCSPlayerController player) 30 | { 31 | var output = WrappedLine(input); 32 | 33 | for (int i = 0; i < output.Count; i++) 34 | output[i] = ReplaceLanguageTags(output[i]); 35 | 36 | output = WrappedLine(output.ToArray()); 37 | 38 | for (int i = 0; i < output.Count; i++) 39 | { 40 | output[i] = ReplaceMessageTags(output[i], player, false); 41 | output[i] = ReplaceRandomTags(output[i]); 42 | output[i] = ReplaceColorTags(output[i]); 43 | } 44 | 45 | return output.ToArray(); 46 | } 47 | 48 | [GeneratedRegex(@"\{LANG=(.*?)\}")] 49 | private static partial Regex ReplaceLanguageTagsRegex(); 50 | 51 | public string ReplaceLanguageTags(string input) 52 | { 53 | // Use Regex to find matches 54 | var match = ReplaceLanguageTagsRegex().Match(input); 55 | 56 | // Check if a match is found 57 | if (match.Success) 58 | { 59 | // Return the group captured in the regex, which is the string after "=" 60 | var lang = match.Groups[1].Value; 61 | var context = (_pluginContext.Plugin as CustomCommands)!; 62 | 63 | return input.Replace(match.Value, context.Localizer[lang] ?? " not found>"); 64 | } 65 | else 66 | { 67 | // Return the original string if no match is found 68 | return input; 69 | } 70 | } 71 | 72 | /// 73 | /// Use regex to find the RNDNO tag pattern {RNDNO={min, max}} 74 | /// 75 | /// 76 | [GeneratedRegex(@"\{RNDNO=\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)\)\}")] 77 | private static partial Regex ReplaceRandomTagsRegex(); 78 | 79 | public string ReplaceRandomTags(string message) 80 | { 81 | // Replace all occurrences of the RNDNO tag in the message 82 | var match = ReplaceRandomTagsRegex().Match(message); 83 | 84 | // Check if the match is successful 85 | if (!match.Success) 86 | { 87 | return message; // Return original message if no match is found 88 | } 89 | 90 | // Extract min and max from the regex match groups 91 | string minStr = match.Groups[1].Value; 92 | string maxStr = match.Groups[2].Value; 93 | 94 | // Check for empty strings 95 | if (string.IsNullOrWhiteSpace(minStr) || string.IsNullOrWhiteSpace(maxStr)) 96 | { 97 | return message; // Return original message if min or max is empty 98 | } 99 | 100 | // Determine if the min and max are integers or floats 101 | bool isMinFloat = float.TryParse(minStr, out float minFloat); 102 | bool isMaxFloat = float.TryParse(maxStr, out float maxFloat); 103 | 104 | if (isMinFloat && isMaxFloat) 105 | { 106 | // Generate a random float between min and max (inclusive) 107 | float randomFloat = (float)(_random.NextDouble() * (maxFloat - minFloat) + minFloat); 108 | 109 | // Determine the maximum precision from the min and max values 110 | int maxDecimalPlaces = Math.Max(GetDecimalPlaces(minStr), GetDecimalPlaces(maxStr)); 111 | 112 | // Use the determined precision to format the float 113 | message = message.Replace(match.Value, randomFloat.ToString($"F{maxDecimalPlaces}")); 114 | } 115 | else if (int.TryParse(minStr, out int min) && int.TryParse(maxStr, out int max)) 116 | { 117 | /// Generate a random integer between min and max (inclusive) 118 | int randomValue = _random.Next(min, max + 1); // max is exclusive, so add 1 119 | message = message.Replace(match.Value, randomValue.ToString()); 120 | } 121 | else 122 | { 123 | // If neither min nor max is valid, return the original message 124 | return message; 125 | } 126 | 127 | return message; 128 | } 129 | 130 | // Method to get the number of decimal places in a number string 131 | private static int GetDecimalPlaces(string numberStr) 132 | { 133 | int decimalIndex = numberStr.IndexOf('.'); 134 | if (decimalIndex == -1) 135 | { 136 | return 0; // No decimal point, return 0 137 | } 138 | return numberStr.Length - decimalIndex - 1; // Count digits after the decimal point 139 | } 140 | 141 | public string ReplaceMessageTags(string input, CCSPlayerController player, bool safety = true) 142 | { 143 | var steamId = new SteamID(player.SteamID); 144 | 145 | Dictionary replacements = new() 146 | { 147 | {"{PREFIX}", _pluginGlobals.Config.Prefix ?? ""}, 148 | {"{MAP}", NativeAPI.GetMapName() ?? ""}, 149 | {"{TIME}", DateTime.Now.ToString("HH:mm:ss") ?? "