Creating A Simple Administration FilterScript
From SA-MP Wiki
This article is outdated. It may use methods or functions which no longer exist or which are deemed obsolete by the community. Caution is advised. |
Contents |
General Knowledge
Hello. I am Cueball (often aliased on IRC as 'PokeBall[NOT_AFK]', or something similar (anything with '[NOT_AFK]' in it is me :) )) and this guide will teach you how to make a simple administration filterscript that will work with all versions of SA:MP V0.2 (IE: 0.2.*). I will preface this tutorial by stating that you will need to have some knowledge and past experience with SAMP pawn scripting. This guide will be very in-depth, with the aim to have you understand every function used, and how to mirror this administration filterscript yourself with only a casual gaze over my work. I reccomend reading the following, as without these, you will most likely lose sight of our goal, and become very confused:
These are an absolute neccessity if you are to have any hope of understanding this guide. I will be reffering to them often, so if you have not previously read them, do so now!
Getting Pawned
Ok, let us begin this script by opening a new document in Pawno. The standard location for this is:
C:\Program Files\Rockstar Games\GTA San Andreas\SAMP\
This will be what I refer to when I say the 'server location', so if this is not correct in your case, just substitute my server location for yours.
Look inside the folder named 'pawno' and open up "pawno.exe". The program called 'pawno' will appear, and you will have a blank page with some functions in a scrollbar to the right. This is not yet a script, as no active documents are open. To open one, simply click the 'File' button, and then 'New Ctrl+N' option. (Or, just click the 'new page' button that looks like a blank bit of paper).
Your blank page will now have alot of writing on it, and may be a little confusing. After reading the pages listed above, you will know that those such as 'public OnFilterScriptInit()' are callbacks, and also that those like 'SetGameModeText("Blank Script");' are functions. You can start by deleting everything on the page by doing 'Right Click > Select All' and then pressing the 'backspace' key.
Ok, we have now started the script, so lets continue.
Firstly add the following lines to the top of your script:
#include <a_samp> #include <dini> #include <dutils>
These tell the script to include "a_samp.inc" and "dini.inc" which are located inside your server location's 'pawno' folder, and then inside 'include' folder. Clicking the 'compile' button (Method 1: Build > Compile. Method 2: pressing the 'F5' key.) will now prompt you to save your new admin script. I recommend calling it something under approximately 7 characters, to avoid errors, and annoying rename problems that may occur later. I have called my new script "admin", and saved it in your server location's 'filterscripts' folder.
Ok, with the script saved, a box will appear. This is the compiler, and it checks the script for errors and nasty warnings that will prevent our script from working. You should recieve the following error inside the compiler:
C:\Program Files\Rockstar Games\GTA San Andreas\SAMP\filterscripts\admin.pwn(2) : fatal error 100: cannot read from file: "dini" Compilation aborted.Pawn compiler 3.2.3664 Copyright (c) 1997-2006, ITB CompuPhase 1 Error.
This means we have a problem already, and that the compiler could not read from the file "dini.inc" which is inside the 'pawno\include' folder. This is most likely due to the fact that you do not have the file. To fix this, simply download these files from here and place them inside your server location's 'pawno\include' folder:
Now that they are there, compile once again, and you should get this inside your compiler:
Pawn compiler 3.2.3664 Copyright (c) 1997-2006, ITB CompuPhase
Congratulations, your script has just compiled correctly, and you have completed the first step towards our great, but very simple, administration filterscript.
Beginning The Script
Now, we will set a few things up that will make our script a lot easier to use, and understand. We will start by defining a few things.
Add the following defines underneath your "#include" lines:
#define FILTERSCRIPT #if defined FILTERSCRIPT #define dcmd(%1,%2,%3) if ((strcmp((%3)[1], #%1, true, (%2)) == 0) && ((((%3)[(%2) + 1] == 0) && (dcmd_%1(playerid, "")))||(((%3)[(%2) + 1] == 32) && (dcmd_%1(playerid, (%3)[(%2) + 2]))))) return 1 #define COLOUR_GREEN 0x33AA33AA #define COLOUR_RED 0xAA3333AA #define COLOUR_YELLOW 0xFFFF00AA #define COLOUR_LIGHTBLUE 0x33CCFFAA #define COLOUR_ORANGE 0xFF9900AA #define PlayerFile "AdminScript/Users/%s.ini" #define SettingFile "AdminScript/Settings/MainSettings.ini" #define CommandFile "AdminScript/Settings/Commands.ini"
These basically define our new administration script to be a filterscript, which affects the way things work. The "#if defined FILTERSCRIPT" line checks whether the script is a filterscript and if so, does all code until it finds an "#endif". They also define possibly the most important part of our script, 'dcmd', which will help us with our commands that are still to be written.
Explaining the concept of 'dcmd' to a new scripter with little knowledge of SAMP pawn scripting is pointless, so just take my word on it, that that one line is very useful and very necessary to our administration script.
The other lines after it just define 5 colours in hexadecimal format, which will make displaying messages to players in different colours much easier.
The final 3 lines define our file locations. Change these to suit your server, but you will need to have these folders inside your server location's 'scriptfiles' folder:
"AdminScript/Users" "AdminScript/Settings"
If you don't do that, your server will crash, and that would be bad, so I recommend leaving them as they are, and creating the folders now.
Compile your script now and you should get this in your compiler:
C:\Program Files\Rockstar Games\GTA San Andreas\SAMP\filterscripts\admin.pwn(19) : error 001: expected token: "#endif", but found "-end of file-" Pawn compiler 3.2.3664 Copyright (c) 1997-2006, ITB CompuPhase 1 Error.
As mentioned before, using "#if" requires an "#endif" statement, but we can not add that to our script at the moment.
Instead, using our knowledge of callbacks, we can use this: "public OnFilterScriptInit()". This means that when the admin filterscript is run in our server, this piece of code will be executed. Add the following lines to your script:
public OnFilterScriptInit() { print("\n****************************************"); print("* Admin Filterscript by your name here *"); print("****************************************\n"); return 1; } #endif
Obviously, replace "your name here" with your actual/SAMP name, and click compile. You should get this in your compiler once again.
Pawn compiler 3.2.3664 Copyright (c) 1997-2006, ITB CompuPhase
NOTE: THE "#endif" statement was used, and this removed the previous error about not finding the "#endif".
Also, your administration script will print in the server console (using the print() function we learnt about before) a friendly little message saying "Admin Filterscript by ~Cueball~" surrounded by asteriks (substitute "~Cueball~" with your name). We now have a working administration script, that both compiles without errors, AND works with the server console itself. However, the script is useless, as we have not done anything that will affect the user.
Enumeration
Let's begin the coding progress. Many of the tutorials on making an administration script work with level's as the primary objective of their script, but our's will be much better, and have far more options, and not just show you how to set a player's level.
We will now use the enumeration feature that is hardcoded into pawn itself. From reading about it in Scripting Basics, we know that using enumeration lets us store lots of data inside a cell of an array, rather than many separate array's. Start by adding this underneath the "#endif" line:
enum PLAYER_MAIN { PLAYER_NAME[MAX_PLAYER_NAME], PLAYER_IP[16], PLAYER_REGGED, PLAYER_PASS, PLAYER_LOGGED, PLAYER_LEVEL, PLAYER_WIRED, PLAYER_JAILED }
The enumeration we have just done stores 8 different bits of data inside one piece, called 'PLAYER_MAIN'. You can see that we now have a cell to store the player's name (which is much easier than constantly creating variable's and using GetPlayerName() after every callback/command). We also have a place to store the player's IP address, whether they are registered, logged in, their password, their level, and whether they are wired (muted) and jailed. This way we can keep track of everyone's progress by simply checking it inside their piece of code.
But hold on one second, as that was just an enumeration, no new variable has been created yet to include it. We can fix that by adding this line underneath our last line:
new gPlayerInfo[MAX_PLAYERS][PLAYER_MAIN];
Ok, we have just made an array, with 500 (the size of 'MAX_PLAYERS') different cells and each cell storing the enumeration we made. This means for every player in your server, we can tell their level, name, IP, etc. by checking it with the variable 'gPlayerInfo'.
Compile the script and you should have no errors, but a warning saying this:
C:\PROGRA~1\ROCKST~1\GTASAN~1\Samp\FILTER~1\admin.pwn(42) : warning 203: symbol is never used: "gPlayerInfo" Pawn compiler 3.2.3664 Copyright (c) 1997-2006, ITB CompuPhase 1 Warning.
Ignore this for now, as it is just telling us that our array has not been used yet. This will change as soon as we begin accessing and storing data inside it.
Accessing Our Array
With the above completed, we are ready to begin storing data inside our new array. Add the following lines underneath our enumerated variable:
public OnPlayerConnect(playerid) { new file[100],Name[MAX_PLAYER_NAME],Ip[16]; GetPlayerName(playerid,Name,sizeof(Name)); GetPlayerIp(playerid,Ip,sizeof(Ip)); format(file,sizeof(file),PlayerFile,Name); if(!dini_Exists(file)) { dini_Create(file); dini_Set(file,"Name",Name); dini_Set(file,"Ip",Ip); dini_IntSet(file,"Registered",-1); dini_IntSet(file,"Password",0); dini_IntSet(file,"Level",0); dini_IntSet(file,"Wired",0); dini_IntSet(file,"Jailed",0); SendClientMessage(playerid,COLOUR_ORANGE,"Your username is not recognized on this server. Please /register to continue."); } strcat(gPlayerInfo[playerid][PLAYER_NAME], dini_Get(file,"Name")); strcat(gPlayerInfo[playerid][PLAYER_IP], dini_Get(file,"Ip")); gPlayerInfo[playerid][PLAYER_REGGED] = dini_Int(file,"Registered"); gPlayerInfo[playerid][PLAYER_PASS] = dini_Int(file,"Password"); gPlayerInfo[playerid][PLAYER_LEVEL] = dini_Int(file,"Level"); gPlayerInfo[playerid][PLAYER_WIRED] = dini_Int(file,"Wired"); gPlayerInfo[playerid][PLAYER_JAILED] = dini_Int(file,"Jailed"); if(gPlayerInfo[playerid][PLAYER_REGGED] == 0) SendClientMessage(playerid,COLOUR_ORANGE,"You're username is recognised on this server, but you have not registered. Please /register to continue."); else if(gPlayerInfo[playerid][PLAYER_REGGED] == 1) SendClientMessage(playerid,COLOUR_ORANGE,"You're username is recognised on this server. Please /login to continue."); gPlayerInfo[playerid][PLAYER_REGGED] = 0; return 1; }
That is one big piece of code to digest, so I will try to explain it as best as I can:
- When a player connects, the server retreives his name and ip address, and stores his name into a string called 'file'.
- Then, if the file does not exist, ie: he has never been on your server (remember that the file is inside the 'Users' folder of the 'AdminScript' folder inside your server location's 'scriptfiles' folder), the server creates it using our dini_* functions, and writes the neccessary info
into it. He also recieves a message in orange (thanks to the COLOUR_ORANGE define we did earlier) telling him to '/register'.
You will notice that the "Registered" line is set to write it as '-1'. This is so that he does not see two messages when he connects. This will be explained in a few moments.
- Next, we use the strcat() function to store his name into our 'gPlayerInfo' array, and we get his name from the player's file.
- We do the same with his IP address, once again, getting it from his file. The next lines are just numbers, not strings, so we can just
use an equal sign (=) to store the data from the file into the various cells (Is he registered? His password, etc.). Using this method, if the player WAS jailed last time he was on your server, he will continue to be so when he spawns (this will be scripted soon).
- The next 3 lines before the last curly brace check whether he has connected before (by comparing with our 'gPlayerInfo'
variable) or whether he has registered his account (will be scripted later) and sends the corresponding message to him.
- Then, we make sure that when he connects next (and he has not registered yet), he will trigger this if line:
if(gPlayerInfo[playerid][PLAYER_REGGED] == 0) SendClientMessage(playerid,COLOUR_ORANGE,"You're username is recognised on this server, but you have not registered. Please /register to continue.");
- Finally, we 'return 1;' to tell the script and the server that that bit of code is over.
Compile your script and you should recieve no errors OR warnings. Congratulations, we are one step closer towards our very great, but simple administration script.
The Would-Be Problem
Now, we already have an unseen problem. If a player logs in with the same id as, say, an administrator who just left, their level's would be the same, as with their password, etc. The way we have set up our script to read and store into our array removes this problem, BUT, it is a good practise to get into, as it prevents other problems from occuring. Add the following lines underneath the last curly brace:
public OnPlayerDisconnect(playerid, reason) { new file[100]; format(file,sizeof(file),PlayerFile,gPlayerInfo[playerid][PLAYER_NAME]); dini_Set(file,"Name",gPlayerInfo[playerid][PLAYER_NAME]); dini_Set(file,"Ip",gPlayerInfo[playerid][PLAYER_IP]); dini_IntSet(file,"Registered",gPlayerInfo[playerid][PLAYER_REGGED]); dini_IntSet(file,"Password",gPlayerInfo[playerid][PLAYER_PASS]); dini_IntSet(file,"Level",gPlayerInfo[playerid][PLAYER_LEVEL]); dini_IntSet(file,"Wired",gPlayerInfo[playerid][PLAYER_WIRED]); dini_IntSet(file,"Jailed",gPlayerInfo[playerid][PLAYER_JAILED]); gPlayerInfo[playerid][PLAYER_NAME] = 0; gPlayerInfo[playerid][PLAYER_IP] = 0; gPlayerInfo[playerid][PLAYER_REGGED] = 0; gPlayerInfo[playerid][PLAYER_LOGGED] = 0; gPlayerInfo[playerid][PLAYER_PASS] = 0; gPlayerInfo[playerid][PLAYER_LEVEL] = 0; gPlayerInfo[playerid][PLAYER_WIRED] = 0; gPlayerInfo[playerid][PLAYER_JAILED] = 0; return 1; }
The first line does the same as in OnPlayerConnect(), but without the need to use GetPlayerName(), as we have already gotten this, and stored it inside our array (making things easier, and alot more efficient). The next 7 lines store the information from our array into a file.
The next 9 lines set our array's cells back to 0, which as stated before, is good practise to get into, as it prevents errors from occuring, and is also very useful. We then 'return 1;' to tell the script that that piece of code is over.
Compile the script, and you should recieve no errors OR warnings. We now have the requirements for our administration script, and have a working system to check, access, remove, and rely on, that will take care of itself.
Beginning The Commands
Ok, now the part of the script that I'm sure you have been waiting for, so if you have read all the previous sections, congratulations on making it this far. If not, do so now!
Add the following lines underneath your script:
public OnPlayerCommandText(playerid, cmdtext[]) { dcmd(register, 8, cmdtext); dcmd(login, 5, cmdtext); dcmd(logout, 6, cmdtext); dcmd(password, 8, cmdtext); return 0; }
Basically, this callback is called whenever a player types a message, and the first character is '/' (ie: a command). I will not be explaining how dcmd() works, but these are the basics of it:
- The first parameter is the command name.
- The second parameter is the length of the command (number of characters exclusing the '/').
- The final parameter is the string to compare with.
As stated, I will not be explaining in great detail how dcmd() works, but you should read about it on SA-MP's forums.
The 'return 0;' sends the 'SERVER: Unknown command' message for any unscripted commands. You should always return 0 in every OnPlayerCommandText callback you write, or else you can often cause broken commands.
Account Management
Here we have our four commands for registration, and account management:
Register
Copy the following into our script, underneath our last line:
dcmd_register(playerid, params[]) { if(gPlayerInfo[playerid][PLAYER_REGGED] == 1) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You have already registered!"); else if(!params[0]) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /register [password]"); /*else if(strlen(params) < gSettings[PASS_MIN] || strlen(params) > gSettings[PASS_MAX]) { new string[128]; format(string, sizeof(string), "ERROR: Password must be between %d and %d characters long!", gSettings[PASS_MIN], gSettings[PASS_MAX]); return SendClientMessage(playerid, COLOUR_ORANGE, string); }*/ else { new password = num_hash(params); gPlayerInfo[playerid][PLAYER_PASS] = password; gPlayerInfo[playerid][PLAYER_REGGED] = 1; gPlayerInfo[playerid][PLAYER_LOGGED] = 1; GetPlayerIp(playerid, gPlayerInfo[playerid][PLAYER_IP], 16); new string[128]; format(string, sizeof(string), "You have successfully registered your account with the password \'%s\'. You have been automatically logged in.", params); return SendClientMessage(playerid, COLOUR_LIGHTBLUE, string); } }
Now, lets go through that command:
- We first check if the player has already registered, by comparing with our 'gPlayerInfo' array. If he has, we send him an error message, and because we used 'return SendClientMessage', two things happen:
- The command finishes at that point.
- He doesn't recieve the 'SERVER: Unknown command' message.
- We then check if he did not supply a password, using '!params[0]'. params[0] is the first character of the password, so if he has not (hence the '!') given the first character of the password, we send him a message telling him how to use the command correctly. Once again, we 'return SendClientMessage' so he does not recieve the 'SERVER: Unknown command' message.
- You will notice the '/*' and '*/' before and after the next 'else if'. This means that we have commented that section. The script/server ignores all commented lines, so we can write code that will cause problems without having to worry. I have commented this section because we have not made our 'gSettings' array yet. When we do create it, I will instruct you to uncomment this section. If you do not understand this yet, don't worry, because all will be explained as soon as we make our 'gSettings' array.
- As this next section is the last and final piece for this command to use, we use the 'else' statement here, as opposed to 'else if'. For this section to be triggered, the player must not be registered, as well as having supplied a password. Lets check what we do next:
- We create a new variable (integer) called 'password' and assign it to the 'num_hash()' function we can find in our dutils.inc file.
- We set the 'PLAYER_PASS' cell of our 'gPlayerInfo[playerid]' array to 'password'. The 'num_hash()' function simply converts the given string (in this case, our password), to a numerival value, with which we then place in to our array cell.
- We set the 'PLAYER_REGGED' cell of our 'gPlayerInfo[playerid]' array to 1. This is so that if he types '/register [password]' again, he will trigger this line:
if(gPlayerInfo[playerid][PLAYER_REGGED] == 1) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You have already registered!");
- We set the 'PLAYER_LOGGED' cell of our 'gPlayerInfo[playerid]' array to 1, thus logging them in, and allowing them to use our commands that are yet to be scripted. We will also check this variable when we script our '/login' and '/logout' commands, using a similar check as with this command.
- We then get the player's ip address using the GetPlayerIp() function, and store it inside our 'PLAYER_IP' cell of 'gPlayerInfo[playerid]' array. We will check this later, so it is necessary to store this information now.
- Now we do 3 new things:
- We create a new string called 'string', and declare it to have a maximum size (max characters we can use in it) of 128.
- We then format our string using the format() function. Note that we have used an escape character for our single quote characters, and we have also used a format string specifier ('%s') meaning that we are going to replace that piece with a string that will be the next parameter of the format() function. Read about them at the format() page. What we are inserting is the characters that the player typed after '/register '.
- Finally we 'return SendClientMessage' signifying that this is the final piece of code, and also send him the newly-formatted string in lightblue (thanks to our 'COLOUR_LIGHTBLUE' define).
That was alot of code to digest, so if you did not understand it, please read through it again now!
Login
Copy this next command underneath our newly-scripted register command:
dcmd_login(playerid, params[]) { if(gPlayerInfo[playerid][PLAYER_REGGED] != 1) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You must register first to do that! Use /register [password] to register and login."); else if(gPlayerInfo[playerid][PLAYER_LOGGED] == 1) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You are already logged-in."); else if(!params[0]) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /login [password]"); else { new password = num_hash(params); if(gPlayerInfo[playerid][PLAYER_PASS] == password) { gPlayerInfo[playerid][PLAYER_LOGGED] = 1; GetPlayerIp(playerid, gPlayerInfo[playerid][PLAYER_IP], 16); return SendClientMessage(playerid, COLOUR_LIGHTBLUE, "You have successfully logged in to your account."); } else return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: Incorrect password."); } }
Now once again, I shall guide you through the coding process, to explain what we have just done:
- Using the same method as above (in our register command), we check if the player has not registered. If this is true, we once again 'return SendClientMessage', and let him know that he must register first.
- We then check if the player has already logged in. If this is true, we once again 'return SendClientMessage', and let him know that he has already logged in.
- Next, we check if he supplied a password using the same check we did for the register command. If he did not type anything after '/register ', it will trigger this message, and will stop the command and tell him how to correctly use the command.
- If none of the above sections were triggered, we move to the final section of code using our 'else' statement. Inside it, the following happens:
- We get the encrypt the password that the player typed, using our 'num_hash()' function (just like we did in our register command) and assign it to the newly-created variable 'password'.
- We check the variable 'password' against our 'PLAYER_PASS' cell of our 'gPlayerInfo[playerid]', and if they are the same, we do the following:
- We set the 'PLAYER_LOGGED' cell of our 'gPlayerInfo[playerid]' array to 1, thus logging them in.
- We then get the player's ip address using the GetPlayerIp() function, and store it inside our 'PLAYER_IP' cell of 'gPlayerInfo[playerid]' array. We will check this later, so it is necessary to store this information now.
- We finally tell the player that they have logged in using our 'return SendClientMessage' method (thus ending the command).
- Using the 'else' statement, this section of code is triggered if the above 'if' statement was not valid. To trigger this section, the password must not be correct, so we use the good-old 'return SendClientMessage' method to end the command, and tell the player that his specified password was incorrect.
Once again, a big explanation for something relatively simple. Hopefully you have understood everything so far, so if you have, go get something to eat, and have a little break because chances are, you will need it. Also, if you have not understood this command, read it again!
Logout
dcmd_logout(playerid, params[]) { #pragma unused params if(gPlayerInfo[playerid][PLAYER_REGGED] != 1) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You must register first to do that! Use /register [password] to register."); else if(gPlayerInfo[playerid][PLAYER_LOGGED] == 0) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You are already logged-out."); else { gPlayerInfo[playerid][PLAYER_LOGGED] = 0; return SendClientMessage(playerid, COLOUR_LIGHTBLUE, "You have successfully logged out of your account."); } }
This command is much shorter and easier to understand, so this explanantion won't be as big.
- We will not be using the 'params' variable in this command, so to stop a 'warning 203: symbol is never used: "params"' warning, we use '#pragma unused' on the params variable.
- We next check if the player has not registered. If so, we 'return SendClientMessage' once again, and let him know that he must register first.
- We next check if the player has not logged in. If true, we use the 'return SendClientMessage' method and tell him he has already logged out.
- The final section of this command means that we can use the 'else' statement. To get to this point, the player must have:
- Registered.
- Logged in.
- We set the 'PLAYER_LOGGED' cell of our 'gPlayerInfo[playerid]' array to 0, thus logging them out.
- We 'return SendClientMessage' the player, letting him know that he has been logged out successfully.
Relatively easy to understand, but if you didn't, read it again!
Password
dcmd_password(playerid, params[]) { if(gPlayerInfo[playerid][PLAYER_REGGED] != 1) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You must register first to do that! Use /register [password] to register and login."); else if(gPlayerInfo[playerid][PLAYER_LOGGED] == 0) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You must be logged-in to do that! Use /login [password] to login."); else { new tmp[30], tmp2[30], index; tmp = strtok(params, index); if(!strlen(tmp)) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /password [password] [new password]"); tmp2 = strtok(params, index); if(!strlen(tmp2)) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /password [password] [new password]"); new oldpassword = num_hash(tmp), newpassword = num_hash(tmp2); if(gPlayerInfo[playerid][PLAYER_PASS] == oldpassword) { if(oldpassword == newpassword) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: Your old password can not be the same as your new password."); /*else if(strlen(tmp2) < gSettings[PASS_MIN] || strlen(tmp2) > gSettings[PASS_MAX]) { new string[100]; format(string, sizeof(string), "ERROR: Your new password must be between %d and %d characters long!", gSettings[PASS_MIN], gSettings[PASS_MAX]); return SendClientMessage(playerid, COLOUR_ORANGE, string); }*/ gPlayerInfo[playerid][PLAYER_PASS] = newpassword; new string[128]; format(string, sizeof(string), "You have successfully changed your password from \'%s\' to \'%s\'.", tmp, tmp2); return SendClientMessage(playerid, COLOUR_LIGHTBLUE, string); } else return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: Incorrect password."); } }
By far the hardest command yet, so this explanation will be big.
- We first check if the player has not registered. Using the 'not' control structure, we can send the message with our trusty 'return SendClientMessage' method, letting him know that he must first register.
- Then, we check if the player has logged in or not, and if it is the latter, we send him a message telling him to do so.
- As this is the final path for the command to take, we use the 'else' statement:
- We create three new variables, two of which are strings, and the third is an integer. The strings have a maximum size of 30 characters, and these will be used to hold what the player types after '/password'. As our command is set up with two pieces of information to grab ('/password [password] [new password]'), we will be using strtok().
- We use the strtok() function to assign the [password] piece of the command to our string 'tmp'. We then check if the string length is 0 (using the 'not' control structure), and if it is 0 (ie: he did not supply his current password), we tell him the correct usage of the command.
- We then do the same thing with the [new password] piece of the command. After assigning it to our 'tmp2' string, we check the length, and like before, if it is 0, we once again tell him the correct usage of the command.
- Now we make two new variables, both integers, and make each one the hashed number of our command information ([password] & [new password]) using the num_hash() command that was included with our dutils.inc include.
- Now we check if the players current password (using our 'gPlayerInfo' array) is the same as what he typed in the game. If it is, we execute the following:
- We check if the new password he typed is exactly the same (because they are both integers now, we don't need to use strcmp() to compare them). If they are the same (ie: he typed this for example: '/password Cueball Cueball'), we tell him that he can not change his password to the same as it was before.
- The next piece is commented out, and this will stay like that until we create our 'gSettings' array. This commented piece ensures that his new password is not too short or too long, but leave it uncommented for now. I will instruct you to do so when we create the array.
- We set the players 'PLAYER_PASS' cell of the 'gPlayerInfo' array to the new password.
- We now create a new string, and format it. This is a very useful function which will help make our string show the player his old password as well as his new password. This tutorial will not explain how to use format(), but I will give you these links:
- We finally 'return SendClientMessage' and send the string (after formatting it with both his old and new passwords) in lightblue (thanks to the 'COLOUR_LIGHTBLUE' define).
- If the password he typed in the [password] piece of the command is not the same as his current password (ie: his current password is 'Cueball' and he types 'Violin'), we tell him to use his correct password.
Another command done (arguably the toughest so far, and possibly throughout the whole tutorial), I suggest you read it again if it did not make any sense. You will definately need to read about format(), as it is an essential function, and will be used in almost every script you make/use. Well done on coming this far.
Creating And Storing Our Settings
It is now time to create storage room for our settings. This will be done using both files and arrays, in exactly the same way we created our player files and enumerations.
Place the following lines underneath our player enumeration code:
We begin by enumerating to 'SETTINGS_MAIN':
enum SETTINGS_MAIN { POCKET_MONEY, JAIL_COMMANDS, ANNOUNCE_SECONDS, PASS_MIN, PASS_MAX } new gSettings[SETTINGS_MAIN];
We now have a storage area (ie: an array) to hold all the needed data in terms of settings. The default value for PAWN to use in variables is 0, so each one of those cells are now set to 0. We will now write the code needed to read from our settings files (the location of this file was written in our 'SettingFile' define):
WE WILL BE EDITING OUR PREVIOUS WORK IN THIS SECTION!
public OnFilterScriptInit() { print("\n****************************************"); print("* Admin Filterscript by your name here *"); print("****************************************\n"); if(!fexist(SettingFile)) { dini_Create(SettingFile); dini_IntSet(SettingFile, "PocketMoney", 3000); dini_IntSet(SettingFile, "JailCommands", 0); dini_IntSet(SettingFile, "AnnounceSeconds", 3); dini_IntSet(SettingFile, "PassMin", 3); dini_IntSet(SettingFile, "PassMax", 15); } gSettings[POCKET_MONEY] = dini_Int(SettingFile, "PocketMoney"); gSettings[JAIL_COMMANDS] = dini_Int(SettingFile, "JailCommands"); gSettings[ANNOUNCE_SECONDS] = dini_Int(SettingFile, "AnnounceSeconds"); gSettings[PASS_MIN] = dini_Int(SettingFile, "PassMin"); gSettings[PASS_MAX] = dini_Int(SettingFile, "PassMax"); return 1; } #endif
You can now replace your previous OnFilterScriptInit callback with that one.
Let me now guide you through what we have done:
- We store 5 different bits of information inside one piece (our enumeration 'SETTINGS_MAIN').
- We then create a new global array called 'gSettings', and set it to hold our enumeration.
- We then modify our OnFilterScriptInit callback:
- We first print three lines to the console, which as you should know, are now changed to suit your liking.
- We then use the fexist() function to check if our 'SettingFile' (remember our define) file exists. If it does not (hence the '!'), we execute the following code:
- We create the file using dini_Create().
- We then write 5 different lines and their values to the file. It will look like this inside the file when opened with an external program (such as notepad):
PocketMoney=3000
JailCommands=0
AnnounceSeconds=3
PassMin=3
PassMax=15
- We now know that the file must exist, so we can safely retrieve information from it:
- We store our information from the file into each cell of our 'gSettings' array, using the dini_Int() function.
- We finally 'return 1;' to let the server and script know that that segment of code is over.
Compile your script now and you should recieve no errors nor warnings. Congratulations, we can now continue on to make our existing commands much better.
Modifying Our Commands
With our newly created 'gSettings' array, we can now alter our account management commands.
Registration Modification
We shall begin with our register command.
dcmd_register(playerid, params[]) { if(gPlayerInfo[playerid][PLAYER_REGGED] == 1) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You have already registered!"); else if(!params[0]) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /register [password]"); else if(strlen(params) < gSettings[PASS_MIN] || strlen(params) > gSettings[PASS_MAX]) { new string[128]; format(string, sizeof(string), "ERROR: Password must be between %d and $d characters long!", gSettings[PASS_MIN], gSettings[PASS_MAX]); return SendClientMessage(playerid, COLOUR_ORANGE, string); } else { new password = num_hash(params); gPlayerInfo[playerid][PLAYER_PASS] = password; gPlayerInfo[playerid][PLAYER_REGGED] = 1; gPlayerInfo[playerid][PLAYER_LOGGED] = 1; GetPlayerIp(playerid, gPlayerInfo[playerid][PLAYER_IP], 16); new string[128]; format(string, sizeof(string), "You have successfully registered your account with the password \'%s\'. You have been automatically logged in.", params); return SendClientMessage(playerid, COLOUR_LIGHTBLUE, string); } }
If you did not pick up on what code was changed, allow me to tell you:
- The only piece of this command that was altered was the previously commented section cantaining this code:
else if(strlen(params) < gSettings[PASS_MIN] || strlen(params) > gSettings[PASS_MAX]) { new string[128]; format(string, sizeof(string), "ERROR: Password must be between %d and $d characters long!", gSettings[PASS_MIN], gSettings[PASS_MAX]); return SendClientMessage(playerid, COLOUR_ORANGE, string); }
- The code works like this:
- If the password supplied's length is less than the minimum length specified in our file, or if it is longer than the maximum length specified in our file, we do the following:
- We create a new variable (string), with a maximum length of 128 characters.
- We use the format() function on our 'string' variable, and insert inside it both the 'PASS_MIN' and 'PASS_MAX' values from our 'gSettings' array.
- Finally, we 'return SendClientMessage' once again, and let the player know that they must use a valid password length.
- If the password's length was valid, the code just moves on, and you can read about it here:
Registration
- If the password supplied's length is less than the minimum length specified in our file, or if it is longer than the maximum length specified in our file, we do the following:
Replace our existing 'dcmd_register' function/command with the above code! Do not just copy and paste it at the bottom of our script, or you will recieve errors!
Password Modification
dcmd_password(playerid, params[]) { if(gPlayerInfo[playerid][PLAYER_REGGED] != 1) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You must register first to do that! Use /register [password] to register and login."); else if(gPlayerInfo[playerid][PLAYER_LOGGED] == 0) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You must be logged-in to do that! Use /login [password] to login."); else { new tmp[30], tmp2[30], index; tmp = strtok(params, index); if(!strlen(tmp)) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /password [password] [new password]"); tmp2 = strtok(params, index); if(!strlen(tmp2)) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /password [password] [new password]"); new oldpassword = num_hash(tmp), newpassword = num_hash(tmp2); if(gPlayerInfo[playerid][PLAYER_PASS] == oldpassword) { if(oldpassword == newpassword) return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: Your old password can not be the same as your new password."); else if(strlen(tmp2) < gSettings[PASS_MIN] || strlen(tmp2) > gSettings[PASS_MAX]) { new string[100]; format(string, sizeof(string), "ERROR: Your new password must be between %d and %d characters long!", gSettings[PASS_MIN], gSettings[PASS_MAX]); return SendClientMessage(playerid, COLOUR_ORANGE, string); } gPlayerInfo[playerid][PLAYER_PASS] = newpassword; new string[128]; format(string, sizeof(string), "You have successfully changed your password from \'%s\' to \'%s\'.", tmp, tmp2); return SendClientMessage(playerid, COLOUR_LIGHTBLUE, string); } else return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: Incorrect password."); } }
Once again, we are modifying a previously-existing command, and I shall once again explain how it works (though it is extroadinarily similar to our registration modification done previously).
- We modify this section of code:
else if(strlen(tmp2) < gSettings[PASS_MIN] || strlen(tmp2) > gSettings[PASS_MAX]) { new string[100]; format(string, sizeof(string), "ERROR: Your new password must be between %d and %d characters long!", gSettings[PASS_MIN], gSettings[PASS_MAX]); return SendClientMessage(playerid, COLOUR_ORANGE, string); }
- We have simply un-commented it, so that the server will now use this piece of code, rather than ignoring it (as it would have done previously):
- If the length of our [new password] section of the command is either smaller than the minimum password length, or larger than the maximum password length (both set inside our settings file (and 'gSettings' array)), we execute the following piece of code:
- We create a new variable (string) with a maximum length of 100 characters.
- We then format() the variable, and insert our two password minimum/maximum lengths inside it.
- Finally, we 'return SendClientMessage' the string to the player, thus ending the command.
- If the players new password length is valid, we ignore this piece of code and continue on with the command. Read about it here:
Password
- If the length of our [new password] section of the command is either smaller than the minimum password length, or larger than the maximum password length (both set inside our settings file (and 'gSettings' array)), we execute the following piece of code:
Very easy to understand, just a simple uncommenting, but, like always, if you did not understand it, please read it again!
Setting Up For Commands
With our account management commands done, we can begin setting up the arrays which will hold the levels needed to make our administration commands work. Let us begin once again with the enumeration stage:
enum COMMANDS_MAIN { AKILL, ANNOUNCE, ARMOURALL, BAN, CARHP, EXPLODE, FLIP, GOTO, GETHERE, GIVEARMOUR, GIVEHEALTH, GIVEWEAPON, GOD, HEALALL, IMITATE, IP, KICK, MAXAMMO, PING, SETLEVEL, SETWANTED, TBAN, TIME, WEATHER } new gCommands[COMMANDS_MAIN];
You should know what this does by now. Place it under our 'gSettings' array, and let us once again modify our OnFilterScriptInit callback to make this array useful.
Setting Command Levels
public OnFilterScriptInit() { print("\n****************************************"); print("* Admin Filterscript by your name here *"); print("****************************************\n"); if(!fexist(SettingFile)) { dini_Create(SettingFile); dini_IntSet(SettingFile, "PocketMoney", 3000); dini_IntSet(SettingFile, "JailCommands", 0); dini_IntSet(SettingFile, "AnnounceSeconds", 3); dini_IntSet(SettingFile, "PassMin", 3); dini_IntSet(SettingFile, "PassMax", 15); } gSettings[POCKET_MONEY] = dini_Int(SettingFile, "PocketMoney"); gSettings[JAIL_COMMANDS] = dini_Int(SettingFile, "JailCommands"); gSettings[ANNOUNCE_SECONDS] = dini_Int(SettingFile, "AnnounceSeconds"); gSettings[PASS_MIN] = dini_Int(SettingFile, "PassMin"); gSettings[PASS_MAX] = dini_Int(SettingFile, "PassMax"); if(!fexist(CommandFile)) { dini_Create(CommandFile); dini_IntSet(CommandFile, "Akill", 6); dini_IntSet(CommandFile, "Announce", 5); dini_IntSet(CommandFile, "Armourall", 3); dini_IntSet(CommandFile, "Ban", 9); dini_IntSet(CommandFile, "Carhp", 4); dini_IntSet(CommandFile, "Explode", 5); dini_IntSet(CommandFile, "Goto", 4); dini_IntSet(CommandFile, "Gethere", 5); dini_IntSet(CommandFile, "Givearmour", 6); dini_IntSet(CommandFile, "Givehealth", 6); dini_IntSet(CommandFile, "Giveweapon", 7); dini_IntSet(CommandFile, "God", 10); dini_IntSet(CommandFile, "Healall", 7); dini_IntSet(CommandFile, "Imitate", 8); dini_IntSet(CommandFile, "Ip", 2); dini_IntSet(CommandFile, "Kick", 7); dini_IntSet(CommandFile, "Maxammo", 8); dini_IntSet(CommandFile, "Ping", 1); dini_IntSet(CommandFile, "Setlevel", 10); dini_IntSet(CommandFile, "Setwanted", 6); dini_IntSet(CommandFile, "Tban", 9); dini_IntSet(CommandFile, "Time", 3); dini_IntSet(CommandFile, "Weather", 3); } gCommands[AKILL] = dini_Int(CommandFile, "Akill"); gCommands[ANNOUNCE] = dini_Int(CommandFile, "Announce"); gCommands[ARMOURALL] = dini_Int(CommandFile, "Armourall"); gCommands[BAN] = dini_Int(CommandFile, "Ban"); gCommands[CARHP] = dini_Int(CommandFile, "Carhp"); gCommands[EXPLODE] = dini_Int(CommandFile, "Explode"); gCommands[GOTO] = dini_Int(CommandFile, "Goto"); gCommands[GETHERE] = dini_Int(CommandFile, "Gethere"); gCommands[GIVEARMOUR] = dini_Int(CommandFile, "Givearmour"); gCommands[GIVEHEALTH] = dini_Int(CommandFile, "Givehealth"); gCommands[GIVEWEAPON] = dini_Int(CommandFile, "Giveweapon"); gCommands[GOD] = dini_Int(CommandFile, "God"); gCommands[HEALALL] = dini_Int(CommandFile, "Healall"); gCommands[IMITATE] = dini_Int(CommandFile, "Imitate"); gCommands[IP] = dini_Int(CommandFile, "Ip"); gCommands[KICK] = dini_Int(CommandFile, "Kick"); gCommands[MAXAMMO] = dini_Int(CommandFile, "Maxammo"); gCommands[SETLEVEL] = dini_Int(CommandFile, "Setlevel"); gCommands[SETWANTED] = dini_Int(CommandFile, "Setwanted"); gCommands[TBAN] = dini_Int(CommandFile, "Tban"); gCommands[TIME] = dini_Int(CommandFile, "Time"); gCommands[WEATHER] = dini_Int(CommandFile, "Weather"); return 1; }
We have simply added in an if statement, and then stored values from a file inside our 'gSettings' array. This was done in exactly the same method as when we stored our settings information. Read about it here:
Why have we done this you ask? This was so that we can set access levels for our commands. This means you can have lead-admins, and then moderating admins, and so on. It provides a better way to manage your server than RCON. We will be checking these variables at each respective command.
Further Commands
We will now make use of our 'gCommands' array, and begin our commands. They will be split into the following:
The difference is that moderation commands are those that you would allow other users to use (such as /akill and /goto), whereas administration commands are those that manage the server, and not just the players using it.
Moderation Commands
Here are our moderation commands, which work with our level system.
Akill
dcmd_akill(playerid, params[]) { if(gPlayerInfo[playerid][PLAYER_LEVEL] < gCommands[AKILL]) { new string[100]; format(string, sizeof(string), "You must be administrator level %d to use that command!", gCommands[AKILL]); return SendClientMessage(playerid, COLOUR_ORANGE, string); } else if(!strlen(params)) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /akill [id | name]"); else { new id = (isNumeric(params)) ? strval(params) : GetPlayerId(params); if(IsPlayerConnected(id) && id != playerid) { SetPlayerHealth(id, 0.0); new string[128]; format(string, sizeof(string), "You have been admin-killed by administrator \'%s\'.", gPlayerInfo[playerid][PLAYER_NAME]); SendClientMessage(id, COLOUR_ORANGE, string); format(string, sizeof(string), "You have successfully admin-killed player \'%s\'.", gPlayerInfo[id][PLAYER_NAME]); return SendClientMessage(playerid, COLOUR_LIGHTBLUE, string); } else return SendClientMessage(playerid, COLOUR_ORANGE, "ERROR: You can not admin-kill yourself or a disconnected player."); } }
Here is how this command works:
- We first check if the players current level (using their 'PLAYER_LEVEL' cell of our 'gPlayerInfo[playerid]' array) is less than the commands set level (its cell in our 'gCommands' array). If this is true, we do the following:
- We first create a new string with a maximum of 100 characters.
- We then format it to include the current level of the command.
- Finally, we 'return SendClientMessage' to send the string to the player, thus ending the command.
- We now check if the player supplied an id or name to use the command on, by checking the length of 'params' ('params' was passed to the function by dcmd()), and if its length is 0 (hence the '!'), we tell the player the correct usage of the command.
- We can finally use the 'else' statement. The following happens now:
- We create a new integer, and set its value to be either:
- The value that the player typed if it was a number (not a name).
- The id that the name belongs to if it was a name (not an id).
- If the id typed is connected and it is not the same as the id who typed the command, we do the following:
- We set the id's health to 0 (0.0 is used because the function calls for a float).
- We create a new string with a maximum length of 128 characters.
- We format the string to hold the administrators name.
- We send the newly-formatted string to the id.
- We format the string to hold the ids name.
- We 'return SendClientMessage' the string to the administrator.
- If the id was not connected or it was the same as the players id, we 'return SendClientMessage' the player, telling them that they must use a valid id (also ending the command).
- We create a new integer, and set its value to be either:
You should know the drill by now, if you didn't understand it, read it again! Or, if you did, congratulations. We have now completed our first real 'moderation' command.
Announce
dcmd_announce(playerid, params[]) { if(gPlayerInfo[playerid][PLAYER_LEVEL] < gCommands[ANNOUNCE]) { new string[100]; format(string, sizeof(string), "You must be administrator level %d to use that command!", gCommands[ANNOUNCE]); return SendClientMessage(playerid, COLOUR_ORANGE, string); } else if(!strlen(params)) return SendClientMessage(playerid, COLOUR_ORANGE, "USAGE: /announce [message]"); else return GameTextForAll(params, gSettings[ANNOUNCE_SECONDS] * 1000, 3); }
This is a tiny command, and here is how it works:
- We first check the players level against the one we have in our file (and array). If it is lower, we:
- Create a string with a maximum length of 100 characters.
- We then format it to include the correct level.
- Finally we send the player the message.
- We now check if the player typed a message to announce. If not, we tell him the correct usage.
- Finally, we send the announcement using the GameTextForAll() function. The second parameter is the amount of milliseconds to show it for. We used our 'gSettings' array, because it holds the amount of seconds that you want the message shown for. As that was in seconds, and the function calls for milliseconds, we simply multiply the time by 1000 (there are 1000 milliseconds in 1 second). We then use a style on the message which shows it in the centre of the screen.
Another one down, and that could not have been easier.
WORK IN PROGRESS!!