├── .gitattributes ├── .gitignore ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── command-guess.inc ├── pawn.yaml └── test.pwn /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pwn linguist-language=Pawn 2 | *.inc linguist-language=Pawn 3 | 4 | * text=auto eol=lf 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Package only files 3 | # 4 | 5 | # Compiled Bytecode, precompiled output and assembly 6 | *.amx 7 | *.lst 8 | *.asm 9 | 10 | # Vendor directory for dependencies 11 | dependencies/ 12 | 13 | # Dependency versions lockfile 14 | pawn.lock 15 | 16 | 17 | # 18 | # Server/gamemode related files 19 | # 20 | 21 | # compiled settings file 22 | # keep `samp.json` file on version control 23 | # but make sure the `rcon_password` field is set externally 24 | # you can use the environment variable `SAMP_RCON_PASSWORD` to do this. 25 | server.cfg 26 | 27 | # Plugins directory 28 | plugins/ 29 | 30 | # binaries 31 | *.exe 32 | *.dll 33 | *.so 34 | announce 35 | samp03svr 36 | samp-npc 37 | 38 | # logs 39 | logs/ 40 | server_log.txt 41 | crashinfo.txt 42 | 43 | # Ban list 44 | samp.ban 45 | 46 | # 47 | # Common files 48 | # 49 | 50 | *.sublime-workspace 51 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build only", 6 | "type": "shell", 7 | "command": "sampctl package build", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "isBackground": false, 13 | "presentation": { 14 | "reveal": "silent", 15 | "panel": "dedicated" 16 | }, 17 | "problemMatcher": "$sampctl" 18 | }, 19 | { 20 | "label": "build watcher", 21 | "type": "shell", 22 | "command": "sampctl package build --watch", 23 | "group": "build", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "silent", 27 | "panel": "dedicated" 28 | }, 29 | "problemMatcher": "$sampctl" 30 | }, 31 | { 32 | "label": "run tests", 33 | "type": "shell", 34 | "command": "sampctl package run", 35 | "group": { 36 | "kind": "test", 37 | "isDefault": true 38 | }, 39 | "isBackground": true, 40 | "presentation": { 41 | "reveal": "silent", 42 | "panel": "dedicated" 43 | }, 44 | "problemMatcher": "$sampctl" 45 | }, 46 | { 47 | "label": "run tests watcher", 48 | "type": "shell", 49 | "command": "sampctl package run --watch", 50 | "group": "test", 51 | "isBackground": true, 52 | "presentation": { 53 | "reveal": "silent", 54 | "panel": "dedicated" 55 | }, 56 | "problemMatcher": "$sampctl" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kirima2nd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SA:MP Command Guess 2 | 3 | [![sampctl](https://img.shields.io/badge/sampctl-command--guess-2f2f2f.svg?style=for-the-badge)](https://github.com/Se8870/samp-command-guess) 4 | 5 | Just simple command guesser for SA-MP using Levenshtein Distance function. 6 | 7 | Will working with most of command processor (including y_commands and also discord command) 8 | 9 | ## Installation 10 | 11 | ### Using sampctl 12 | 13 | Simply install to your project: 14 | 15 | ```bash 16 | sampctl package install Se8870/samp-command-guess 17 | ``` 18 | 19 | Include in your code and begin using the library: 20 | 21 | ```pawn 22 | #include 23 | ``` 24 | 25 | ### Manual installation 26 | Simply download it from main repository, or go to [release](../../releases) page. 27 | 28 | Extract `command.guess.inc` inside the zip or tar.gz to your server directory (It should be inside `pawno/includes`) 29 | 30 | Include in your code and begin using the library: 31 | 32 | ```pawn 33 | #include 34 | ``` 35 | 36 | ## Function Lists 37 | 38 | ```pawn 39 | Command_Guess(output[], const cmdtext[], len = sizeof dest); 40 | ``` 41 | 42 | ## Usage 43 | 44 | ### With Y_CMD 45 | ```pawn 46 | #include 47 | #include 48 | #include 49 | 50 | main() 51 | { 52 | print("Script loaded"); 53 | 54 | Command_SetDeniedReturn(true); 55 | } 56 | 57 | public e_COMMAND_ERRORS:OnPlayerCommandReceived(playerid, cmdtext[], e_COMMAND_ERRORS:success) 58 | { 59 | if (success == COMMAND_UNDEFINED) 60 | { 61 | new 62 | guessCmd[32], 63 | dist = Command_Guess(guessCmd, cmdtext); 64 | 65 | if (dist < 3) 66 | { 67 | SendClientMessageEx(playerid, -1, "{FF0000}ERROR:{FFFFFF} \"%s\" is not found, did you mean \"%s\"?", cmdtext, guessCmd); 68 | } 69 | else 70 | { 71 | SendClientMessageEx(playerid, -1, "{FF0000}ERROR:{FFFFFF} \"%s\" is not found", cmdtext); 72 | } 73 | return COMMAND_SILENT; 74 | } 75 | return COMMAND_OK; 76 | } 77 | ``` 78 | 79 | ### With most command processor (I-ZCMD Example) 80 | ```pawn 81 | #include 82 | 83 | #include 84 | #include 85 | 86 | main() 87 | { 88 | print("Script loaded"); 89 | } 90 | public OnPlayerCommandPerformed(playerid, cmdtext[], success) 91 | { 92 | if (!success) 93 | { 94 | new 95 | guessCmd[32], 96 | dist = Command_Guess(guessCmd, cmdtext); 97 | 98 | if (dist < 3) 99 | { 100 | SendClientMessageEx(playerid, -1, "{FF0000}ERROR:{FFFFFF} \"%s\" is not found, did you mean \"%s\"?", cmdtext, guessCmd); 101 | } 102 | else 103 | { 104 | SendClientMessageEx(playerid, -1, "{FF0000}ERROR:{FFFFFF} \"%s\" is not found", cmdtext); 105 | } 106 | return 1; 107 | } 108 | return 1; 109 | } 110 | ``` 111 | 112 | ## Testing 113 | 114 | To test, simply run the following commands: 115 | 116 | ```bash 117 | sampctl package run 118 | ``` 119 | -------------------------------------------------------------------------------- /command-guess.inc: -------------------------------------------------------------------------------- 1 | /* 2 | command-guess.inc 3 | By: Kirima 4 | 5 | Credits: 6 | - Southclaws for Levenshtein Distance function 7 | - Zeex for amx_assembly 8 | */ 9 | 10 | #if defined _INC_guess_command 11 | #endinput 12 | #endif 13 | 14 | #define _INC_guess_command 15 | 16 | #if !defined _samp_included 17 | #error Please include a_samp before using this 18 | #endif 19 | 20 | #if !defined strcopy 21 | stock strcopy(dest[], const source[], maxlength=sizeof dest) 22 | return strcat((dest[0] = EOS, dest), source, maxlength); 23 | #endif 24 | 25 | #if defined _INC_y_commands 26 | #if !defined _INC_y_iterate 27 | #tryinclude 28 | #endif 29 | #if !defined _INC_y_iterate 30 | #tryinclude 31 | #endif 32 | #if !defined _INC_y_iterate 33 | #error "Cannot find y_iterate/y_foreach" 34 | #endif 35 | #else 36 | #if !defined AMX_HEADER_INC 37 | #tryinclude 38 | #endif 39 | #if !defined AMX_HEADER_INC 40 | #tryinclude 41 | #endif 42 | #if !defined AMX_HEADER_INC 43 | #tryinclude 44 | #endif 45 | #if !defined AMX_HEADER_INC 46 | #error "Cannot find amx_header.inc, please download it from https://github.com/Zeex/amx_assembly" 47 | #endif 48 | 49 | /* 50 | CGUESS_CMD_FUNC 51 | ------------- 52 | This pre-processor is used for finding the right length of one of these internal functions below 53 | You also can add another one using `#define CGUESS_CUSTOM_CMD_FUNC` before including `command_guess.inc` 54 | 55 | Example Code: 56 | 57 | ```pawn 58 | #define CGUESS_CUSTOM_CMD_FUNC "ahcmd_", "bonk_cmd", "loli_cmd@" 59 | #include 60 | ``` 61 | */ 62 | 63 | #if !defined CGUESS_CORE_CMD_FUNC 64 | #define CGUESS_CORE_CMD_FUNC "cmd", "cmd_", "dc_cmd", "dcmd", "dcmd_", "_dcmd@", "kcmd_", "_kcmd@" 65 | #endif 66 | 67 | #if !defined CGUESS_CUSTOM_CMD_FUNC 68 | #define CGUESS_CMD_FUNC CGUESS_CORE_CMD_FUNC 69 | #else 70 | #define CGUESS_CMD_FUNC CGUESS_CORE_CMD_FUNC, CGUESS_CUSTOM_CMD_FUNC 71 | #endif 72 | 73 | 74 | /* 75 | Command Internal Name 76 | ---------------------- 77 | This variable can be used for holding the command internal name and adding it for looping stuff. 78 | Each command processor have it's own unique internal name, so that's why i'm using this instead of 79 | Trying to use dumb method from 3000 BC. 80 | */ 81 | 82 | static stock g_arrCmdInternalName[][32 + 1] = {CGUESS_CMD_FUNC}; 83 | #endif 84 | 85 | /* 86 | CGUESS_MAX_CACHE_COMMAND 87 | ------------- 88 | This pre-processor will be used if there is no maximum commaands when using izcmd or simillar 89 | Because they're designed to bypass the limitation and decided to use the string that user 90 | input. 91 | */ 92 | 93 | #if !defined CGUESS_MAX_CACHE_COMMAND 94 | #if defined MAX_COMMANDS 95 | #define CGUESS_MAX_CACHE_COMMAND MAX_COMMANDS 96 | #else 97 | #define CGUESS_MAX_CACHE_COMMAND 500 98 | #endif 99 | #endif 100 | 101 | /* 102 | Command Storage 103 | ---------------- 104 | This variable can be used for holding the command names and adding it into index for looping stuff. 105 | Since i want to minimalize the usage of YSI and not everyone will use it, it is required to have 106 | `g_sRegisteredCommands` 107 | */ 108 | 109 | static stock 110 | g_sRegisteredCommands = -1, 111 | g_sCommandName[CGUESS_MAX_CACHE_COMMAND][32 + 1]; 112 | 113 | /** 114 | * This function will try to search available command and make a prediction to guess the right command. 115 | * Used for storing the results into your variable 116 | * The failed command that is used for guessing 117 | * Length of your output 118 | * Minimum distance between available command and the command from cmdtext 119 | */ 120 | 121 | /* 122 | This function will try to search available command and make a prediction to guess the right command. 123 | Returns: Minimum distance between available command and the command from cmdtext 124 | */ 125 | stock Command_Guess(output[], const cmdtext[], len = sizeof output) 126 | { 127 | new 128 | dist, 129 | min_dist = 0x7FFFFFFF, 130 | min_idx = -1 131 | ; 132 | 133 | for (new i = 0; i <= g_sRegisteredCommands; i ++) 134 | { 135 | dist = CGuess_GetDistance(cmdtext[1], g_sCommandName[i]); 136 | 137 | if (dist < min_dist) 138 | { 139 | min_dist = dist; 140 | min_idx = i; 141 | } 142 | } 143 | 144 | if (('A' <= cmdtext[0] <= 'Z') || ('a' <= cmdtext[0] <= 'z') || ('0' <= cmdtext[0] <= '9')) 145 | { 146 | strcopy(output, g_sCommandName[min_idx], len); 147 | } 148 | else 149 | { 150 | format(output, len, "%c%s", cmdtext[0], g_sCommandName[min_idx]); 151 | } 152 | return min_dist; 153 | } 154 | 155 | /** 156 | * This was used for getting the distance between two strings using Levenshtein Distance method 157 | * The first string 158 | * The second string 159 | * 160 | * The distance between two strings 161 | */ 162 | 163 | static CGuess_GetDistance(const a[], const b[]) { 164 | new 165 | aLength = strlen(a), 166 | bLength = strlen(b), 167 | cache[256], 168 | index = 0, 169 | bIndex = 0, 170 | distance, 171 | bDistance, 172 | result, 173 | code; 174 | 175 | if (!strcmp(a, b)) { 176 | return 0; 177 | } 178 | 179 | if (aLength == 0) { 180 | return bLength; 181 | } 182 | 183 | if (bLength == 0) { 184 | return aLength; 185 | } 186 | 187 | while (index < aLength) { 188 | cache[index] = index + 1; 189 | index++; 190 | } 191 | 192 | while (bIndex < bLength) { 193 | code = b[bIndex]; 194 | result = distance = bIndex++; 195 | index = -1; 196 | 197 | while (++index < aLength) { 198 | bDistance = code == a[index] ? distance : distance + 1; 199 | distance = cache[index]; 200 | 201 | cache[index] = result = distance > result 202 | ? bDistance > result 203 | ? result + 1 204 | : bDistance 205 | : bDistance > distance 206 | ? distance + 1 207 | : bDistance; 208 | } 209 | } 210 | 211 | return result; 212 | } 213 | 214 | /* 215 | Script Init 216 | ------------- 217 | This script init will try it's best to detect if the script using this was filterscript or gamemode 218 | for calling `CGuess_Init`. 219 | */ 220 | 221 | static stock 222 | g_sScriptIndicator = false; 223 | 224 | public OnGameModeInit() 225 | { 226 | if (!g_sScriptIndicator) 227 | { 228 | CGuess_Init(); 229 | g_sScriptIndicator = true; 230 | } 231 | #if defined CGuess_OnGameModeInit 232 | return CGuess_OnGameModeInit(); 233 | #else 234 | return 1; 235 | #endif 236 | } 237 | 238 | public OnFilterScriptInit() 239 | { 240 | if (!g_sScriptIndicator) 241 | { 242 | CGuess_Init(); 243 | g_sScriptIndicator = true; 244 | } 245 | #if defined CGuess_OnFilterScriptInit 246 | return CGuess_OnFilterScriptInit(); 247 | #else 248 | return 1; 249 | #endif 250 | } 251 | 252 | #if defined _ALS_OnGameModeInit 253 | #undef OnGameModeInit 254 | #else 255 | #define _ALS_OnGameModeInit 256 | #endif 257 | 258 | #define OnGameModeInit CGuess_OnGameModeInit 259 | #if defined CGuess_OnGameModeInit 260 | forward CGuess_OnGameModeInit(); 261 | #endif 262 | 263 | #if defined _ALS_OnFilterScriptInit 264 | #undef OnFilterScriptInit 265 | #else 266 | #define _ALS_OnFilterScriptInit 267 | #endif 268 | 269 | #define OnFilterScriptInit CGuess_OnFilterScriptInit 270 | #if defined CGuess_OnFilterScriptInit 271 | forward CGuess_OnFilterScriptInit(); 272 | #endif 273 | 274 | /** 275 | * This internal function was used for checking the command internal function name. 276 | * The command function name that is used for checking 277 | * 278 | * the size of internal function name length, or -1 if it's not found 279 | */ 280 | 281 | static stock CGuess_CheckInternalFunc(const commandName[]) 282 | { 283 | // strcmp check is case sensitive, to catch any weird names. 284 | for (new i = 0, j = sizeof(g_arrCmdInternalName); i < j; i ++) 285 | { 286 | new 287 | cmdLen = strlen(g_arrCmdInternalName[i]); 288 | 289 | if (!strcmp(commandName, g_arrCmdInternalName[i], true, cmdLen)) 290 | { 291 | return cmdLen + 1; 292 | } 293 | } 294 | return -1; 295 | } 296 | 297 | /** 298 | * This was used for caching the commands as many as possible 299 | * 300 | * The code may get slower depending on how many command did you have. 301 | * You can always increase `CGUESS_MAX_CACHE_COMMAND` by adding `#define CGUESS_MAX_CACHE_COMMAND (number)` 302 | * to cache more commands before including command-guess.inc 303 | * This function doesn't have any return types 304 | */ 305 | 306 | static CGuess_Init() 307 | { 308 | #if defined _INC_y_commands 309 | foreach (new i : Command()) 310 | { 311 | if (Command_IsValid(i)) 312 | { 313 | // this ensures for loop to be broken 314 | // and, no need to continue the loop. 315 | if ((g_sRegisteredCommands + 1) >= CGUESS_MAX_CACHE_COMMAND) 316 | break; 317 | 318 | strcopy(g_sCommandName[++g_sRegisteredCommands], Command_GetName(i), 32); 319 | } 320 | } 321 | #else 322 | new 323 | hdr[AMX_HDR]; 324 | 325 | GetAmxHeader(hdr); 326 | 327 | for (new i = 0, j = GetNumPublics(hdr); i < j; i ++) 328 | { 329 | new 330 | trimmedPos, 331 | tmpName[32 + 1]; 332 | 333 | GetPublicNameFromIndex(i, tmpName); 334 | 335 | if ((trimmedPos = CGuess_CheckInternalFunc(tmpName)) != -1) 336 | { 337 | // this ensures for loop to be broken 338 | // and, no need to continue the loop. 339 | if ((g_sRegisteredCommands + 1) >= CGUESS_MAX_CACHE_COMMAND) 340 | break; 341 | 342 | format(g_sCommandName[++g_sRegisteredCommands], 32, "%s", tmpName[trimmedPos]); 343 | } 344 | } 345 | #endif 346 | return 1; 347 | } 348 | -------------------------------------------------------------------------------- /pawn.yaml: -------------------------------------------------------------------------------- 1 | site: "" 2 | user: Kirima2nd 3 | repo: samp-command-guess 4 | entry: test.pwn 5 | output: test.amx 6 | dependencies: 7 | - sampctl/samp-stdlib 8 | - Zeex/amx_assembly 9 | dev_dependencies: 10 | - YashasSamaga/I-ZCMD 11 | - pawn-lang/YSI-Includes 12 | -------------------------------------------------------------------------------- /test.pwn: -------------------------------------------------------------------------------- 1 | #include 2 | //#include 3 | 4 | #include 5 | #include "command-guess.inc" 6 | 7 | main() 8 | { 9 | new 10 | guessedCmd[32], ret; 11 | 12 | print("Testing..."); 13 | 14 | ret = Command_Guess(guessedCmd, "/hlpe", sizeof(guessedCmd)); 15 | printf("Result: %s | Ret: %d", guessedCmd, ret); 16 | 17 | ret = Command_Guess(guessedCmd, "/dd", sizeof(guessedCmd)); 18 | printf("Result: %s | Ret: %d", guessedCmd, ret); 19 | 20 | ret = Command_Guess(guessedCmd, "/cixa", sizeof(guessedCmd)); 21 | printf("Result: %s | Ret: %d", guessedCmd, ret); 22 | 23 | ret = Command_Guess(guessedCmd, "/cm", sizeof(guessedCmd)); 24 | printf("Result: %s | Ret: %d", guessedCmd, ret); 25 | 26 | ret = Command_Guess(guessedCmd, "/cmmd", sizeof(guessedCmd)); 27 | printf("Result: %s | Ret: %d", guessedCmd, ret); 28 | } 29 | 30 | // Testing cmd 31 | CMD:help(playerid, cmdtext[]) return 1; 32 | CMD:cmds(playerid, cmdtext[]) return 1; 33 | CMD:cmd(playerid, cmdtext[]) return 1; 34 | CMD:cd(playerid, cmdtext[]) return 1; 35 | CMD:cc(playerid, cmdtext[]) return 1; 36 | CMD:cica(playerid, cmdtext[]) return 1; 37 | CMD:chalice(playerid, cmdtext[]) return 1; 38 | --------------------------------------------------------------------------------