Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
AndySedeyn - 02.01.2016
Introduction
Many tutorials promote bad written code or not the most optimised code and I've decided to take it upon myself to spend time updating these tutorials. The creator of the original tutorial is linked at the end of each tutorial. All code is rewritten by myself and tested by myself. It is recommended to read everything and not just recklessly copy the code (which I'm sure that many will do).
Notes
This tutorial assumes that you know where includes and plugins are installed. If not, you may refer to the following wikipage:
https://sampwiki.blast.hk/wiki/Scripting_Basics or a tutorial regarding that on the forum. This tutorial also assumes that you have a fairly basic understanding of scripting. I will not go in-depth with basic functions and variables. If you don't have a basic understanding of scripting, then don't bother creating your own login/register script.
Necessities
- The YSI library:
https://sampforum.blast.hk/showthread.php?tid=570883
- Whirlpool:
https://sampforum.blast.hk/showthread.php?tid=570945
- A 'Users' folder in 'scriptfiles'.
Script
To use the code from the libraries stated in the necessities section, we need to include them. We can include them using the 'include' directive:
PHP Code:
#include <a_samp>
#include <YSI\y_ini>
Since Whirlpool only defines one native function, you don't need an include for it. You simply place the following line after including all the necessary includes:
PHP Code:
native WP_Hash(buffer[], len, const str[]);
Now that we included the libraries, we are able to use the code to achieve a login/register script. We have to define a path to where a user's file is saved to. We will use this path when saving the user's data and to load their data. We can define a constant path with the 'define' directive. It is convenient to write definitions in full caps:
PHP Code:
#define USER_PATH "/Users/%s.ini"
'%s' is a format specifier for strings. The string value in this tutorial is the user's name with which they registered. More about specifiers and formatting strings:
https://sampwiki.blast.hk/wiki/Format
We need a function to replace the '%s' format specifier with the user's name, don't we? The following is a function without an initialiser because we don't need one:
PHP Code:
UserPath(playerid) {
// Declare our variables used in this function
new
str[36], // 'str' will be our variable used to format a string, the size of that string will never exceed 36 characters.
name[MAX_PLAYER_NAME]; // 'name' will be our variable used to store the player's name in the scope of this function. MAX_PLAYER_NAME is defined as 24.
// Get the player's name.
GetPlayerName(playerid, name, sizeof(name));
// Format USER_PATH with the name that we got with GetPlayerName.
format(str, sizeof(str), USER_PATH, name); // USER_PATH has been defined as: "/Users/%s.ini", %s will be replaced the player's name.
return str;
}
The PAWN precompiler replaces USER_PATH with the value that we defined earlier. USER_PATH in this case will be replaced by "/Users/%s.ini" and the '%s' will be formatted with the player's name that we retrieved with the GetPlayerName function. More about specifiers and formatting strings:
https://sampwiki.blast.hk/wiki/Format
Once that is done, we can start declaring an enum for the dialogs and the player's data used in this tutorial. We put our dialogs in an enum to avoid ID collision (note: it uses memory to store the dialog IDs). More about dialogs:
https://sampwiki.blast.hk/wiki/ShowPlayerDialog
To define an enum, we use the 'enum' initialiser. Every variable in an enum is referred to as an enumerator.
PHP Code:
enum {
DIALOG_LOGIN,
DIALOG_REGISTER
};
You can name the enum's name to whatever name you like as well as the names of the enumerators.
PHP Code:
enum E_PLAYER_DATA {
Password[129],
AdminLevel,
VIPLevel,
Money,
Score,
Kills,
Deaths,
bool:LoggedIn
};
new PlayerInfo[MAX_PLAYERS][E_PLAYER_DATA];
We declare a variable at the end of an enum to address the enumerators in the enum. Each enumerator has a memory address that we will use to store data in. More about enums and how they work:
https://sampforum.blast.hk/showthread.php?tid=318307 and
https://sampwiki.blast.hk/wiki/Keywords:Initialisers#enum
All right. We have the variables, we have set up the files necessary to save and load the user's data and we have the dialog IDs. We now have to use all this code. We start writing our code from the very beginning; when the player connects.
PHP Code:
public OnPlayerConnect(playerid) {
// Reset the variables to avoid data corruption
PlayerInfo[playerid][AdminLevel] = 0;
PlayerInfo[playerid][VIPLevel] = 0;
PlayerInfo[playerid][Money] = 0;
PlayerInfo[playerid][Score] = 0;
PlayerInfo[playerid][Kills] = 0;
PlayerInfo[playerid][Deaths] = 0;
PlayerInfo[playerid][LoggedIn] = false;
new
name[MAX_PLAYER_NAME]; // 'name' will be our variable used to store the player's name in the scope of this function. MAX_PLAYER_NAME is defined as 24.
GetPlayerName(playerid, name, sizeof(name));
TogglePlayerSpectating(playerid, true);
if(fexist(UserPath(playerid))) {
// This will check whether the user's file exists (he is registered).
// When it exists, run the following code:
INI_ParseFile(UserPath(playerid), "LoadPlayerData_PlayerData", .bExtra = true, .extra = playerid);
ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login", "Welcome back. This account is registered.\n\nEnter your password below to log in:", "Login", "Quit");
}
else {
// When the user's file doesn't exist (he isn't registered).
// When it doesn't exist, run the following code:
ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_INPUT, "Register", "Welcome. This account is not registered.\n\nEnter your desired password below to register:", "Register", "Quit");
}
return 1;
}
The code is pretty self-explanatory. INI_ParseFile is a function from the Y_INI include and calls a forwarded function to load the player's data. More about y_ini:
https://sampforum.blast.hk/showthread.php?tid=570957
We now have written code that calls a dialog and if the player is registered, a function too. We have to do something with that. We have to declare that function first:
PHP Code:
forward LoadPlayerData_PlayerData(playerid, name[], value[]);
public LoadPlayerData_PlayerData(playerid, name[], value[]) {
INI_String("Password", PlayerInfo[playerid][Password], 129);
INI_Int("AdminLevel", PlayerInfo[playerid][AdminLevel]);
INI_Int("VIPLevel", PlayerInfo[playerid][VIPLevel]);
INI_Int("Money", PlayerInfo[playerid][Money]);
INI_Int("Score", PlayerInfo[playerid][Score]);
INI_Int("Kills", PlayerInfo[playerid][Kills]);
INI_Int("Deaths", PlayerInfo[playerid][Deaths]);
return 1;
}
The functions with the 'INI_' prefix load the player's data and assigns an enumerator to it so we can use it throughout the script.
We now have to create our dialogs:
PHP Code:
public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) {
switch(dialogid) {
case DIALOG_REGISTER: {
if(!response) Kick(playerid);
else {
if(isnull(inputtext)) {
SendClientMessage(playerid, -1, "You have to enter your desired password.");
return ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_INPUT, "Register", "Welcome. This account is not registered.\n\nEnter your desired password below to register:", "Register", "Quit");
}
WP_Hash(PlayerInfo[playerid][Password], 129, inputtext);
new INI:file = INI_Open(UserPath(playerid));
INI_SetTag(file, "PlayerData");
INI_WriteString(file, "Password", PlayerInfo[playerid][Password]);
INI_WriteInt(file, "AdminLevel", 0);
INI_WriteInt(file, "VIPLevel", 0);
INI_WriteInt(file, "Money", 0);
INI_WriteInt(file, "Score", 0);
INI_WriteInt(file, "Kills", 0);
INI_WriteInt(file, "Deaths", 0);
INI_Close(file);
SendClientMessage(playerid, -1, "You have successfully registered.");
PlayerInfo[playerid][LoggedIn] = true;
TogglePlayerSpectating(playerid, false);
return 1;
}
}
case DIALOG_LOGIN: {
if(!response) Kick(playerid);
else {
new
hashpass[129];
WP_Hash(hashpass, sizeof(hashpass), inputtext);
if(!strcmp(hashpass, PlayerInfo[playerid][Password])) {
// The player has entered the correct password
SetPlayerScore(playerid, PlayerInfo[playerid][Score]);
GivePlayerMoney(playerid, PlayerInfo[playerid][Money]);
SendClientMessage(playerid, -1, "Welcome back! You have successfully logged in!");
PlayerInfo[playerid][LoggedIn] = true;
TogglePlayerSpectating(playerid, false);
}
else {
// The player has entered an incorrect password
SendClientMessage(playerid, -1, "You have entered an incorrect password.");
ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login", "Welcome back. This account is registered.\n\nEnter your password below to log in:", "Login", "Quit");
}
return 1;
}
}
}
return 0;
}
The code is pretty self-explanatory. We use a switch statement because it is faster than having a lot of if-statements. Though, you should only use switch statements where needed and not when there's only one if-statement. There are only two if-statements, but I'm sure you'll end up adding more dialogs and thus having a switch-statement is the best option here.
If you don't have ZCMD in your gamemode, then add this to your gamemode:
PHP Code:
#define isnull(%1) \
((!(%1[0])) || (((%1[0]) == '\1') && (!(%1[1]))))
The WP_Hash function is used to hash a certain string (the text that the user entered in the dialog), we immediately save the hashed string into the user's password enumerator to make sure that the password is in plain text for a minimal amount of time.
I return 0 in this callback because you might have a filterscript that uses OnDialogResponse. Returning 1 instead makes the callback unusable
I am a strong pioneer of saving data when it is changed and not when the player disconnects. The reason for that is because data might get corrupted when the player disconnects. For example: when the player gets killed, we want to add a death to their record and a kill to the killer's record:
PHP Code:
public OnPlayerDeath(playerid, killerid, reason) {
if(killerid != INVALID_PLAYER_ID) {
// We check whether the killer is a valid player
PlayerInfo[playerid][Deaths] ++; // ++ means +1
PlayerInfo[killerid][Kills] ++;
// Save the deaths
new INI:file = INI_Open(UserPath(playerid));
INI_SetTag(file, "PlayerData");
INI_WriteInt(file, "Deaths", PlayerInfo[playerid][Deaths]);
INI_Close(file);
// Save the kills
new INI:file2 = INI_Open(UserPath(killerid));
INI_SetTag(file2, "PlayerData");
INI_WriteInt(file2, "Kills", PlayerInfo[killerid][Kills]);
INI_Close(file2);
}
return 1;
}
We add a kill to the killer's data and a death to the player's data. We immediately save it to their files to avoid possible data corruption.
FAQ
I am able to log on with any password!
Change:
PHP Code:
INI_ParseFile(UserPath(playerid), "LoadPlayerData_%s", .bExtra = true, .extra = playerid);
To
PHP Code:
INI_ParseFile(UserPath(playerid), "LoadPlayerData_PlayerData", .bExtra = true, .extra = playerid);
The data which the placeholder '%s' is replacing, is the name of the file's tag. A huge thanks to Misiur:
http://forum.sa-mp.com/showpost.php?...2&postcount=13
Footnote
And there we go. You've successfully created your own secured login/register script using y_ini to save the player's data.
I've decided to just rewrite the tutorial and repost it instead of contacting newbienoob because otherwise he had to update the whole topic, which in my opinion is just worth a repost. I have also actively been adding and editing pages on the samp wiki and decided to do the same on the forum.
This tutorial is based on newbienoob's tutorial:
https://sampforum.blast.hk/showthread.php?tid=352703
The credits of mentioned URLs go to their respective creators.
I decided to update this tutorial first because it's one of the most popular tutorials on the SA:MP forum. If there's a tutorial that you think needs an update, then let me know!
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
Crystallize - 02.01.2016
You have to check if user has actually logged in in case they use force spawner and dialog hider... that could be done easy with a boolean
9/10
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
Joron - 02.01.2016
Do a pastebin
for lazy people
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
AndySedeyn - 02.01.2016
Quote:
Originally Posted by Wizzard2H
You have to check if user has actually logged in in case they use force spawner and dialog hider... that could be done easy with a boolean
9/10
|
That actually reminds me that the spawn button is still visible. Will add that in. Thank you very much.
Quote:
Originally Posted by Joron
Do a pastebin for lazy people
|
I've actually considered that but decided to not do that because I'm actually trying to teach readers something. The tutorial already spoon-feeds code and I'm generally against spoon-feeding code. I can already imagine most replies to this tutorial if I didn't include any working code. Thank you for the suggestion, though.
Topic updated:
- Added an enumerator that keeps track on the LoggedIn status of the player.
- All enumerators are reset under OnPlayerConnect to avoid data corruption.
- Added TogglePlayerSpectating under OnPlayerConnect to hide the 'spawn' button.
- Spectating is disabled when the player registers or logs in.
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
saffierr - 03.01.2016
Very useful tutorial, Thanks alot!
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
NourNN - 03.01.2016
Thx i will use it in my admin system its helpful 10/10 +rep
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
Michael B - 03.01.2016
Nicely done Andy, as always.
Keep it up!
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
-CaRRoT - 09.01.2016
This is actually great, its been so long since I've touched a PAWN Code and I was looking to get back into it, this was a great memory refresher.
Cheers!
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
Wiseco - 09.01.2016
Thank you for a well explained tutorial! This helped me out
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
AndySedeyn - 10.01.2016
Thank you for the positive feedback.
@partickgtr, I know of this method. Will have to test something before adding it to the tutorial. Thank you, though.
Topic updated
Being able to log on with any password is a known problem. There is an easy fix for this:
Change:
PHP Code:
INI_ParseFile(UserPath(playerid), "LoadPlayerData_%s", .bExtra = true, .extra = playerid);
To
PHP Code:
INI_ParseFile(UserPath(playerid), "LoadPlayerData_user", .bExtra = true, .extra = playerid);
I myself don't know what causes this. For some it works with %s and for others it works with _user. Someone who has more insight in the frame of y_ini might be able to provide an explanation for this.
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
NealPeteros - 11.01.2016
I got these errors
Quote:
C:\Users\4neals\Desktop\Samp\Main\gamemodes\FreeRo am.pwn(7) : error 001: expected token: ")", but found "const"
C:\Users\4neals\Desktop\Samp\Main\gamemodes\FreeRo am.pwn(7) : error 001: expected token: ";", but found "const"
|
On the native line under includes
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
AndySedeyn - 11.01.2016
Quote:
Originally Posted by NealPeteros
I got these errors
On the native line under includes
|
Good spot. Add a comma after 'len' and before 'const'. A typo when copying that line.
PHP Code:
native WP_Hash(buffer[], len, const str[]);
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
MicroKyrr - 11.01.2016
Nice tutorial , i will use this because I can't understand Register/Login System with Mysql
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
MicroKyrr - 28.02.2016
** 1 month later **
How can I hash register system?
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
AndySedeyn - 28.02.2016
That's what Whirlpool is for. It's in the tutorial.
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
Kasis - 03.03.2016
There is mistype i believe in enum: Scores
It gives errors. Anyways, just delete ''s'' in 3 places and no errors.
Great tutorial man. Thanks.
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
AndySedeyn - 04.03.2016
Quote:
Originally Posted by kasis223
There is mistype i believe in enum: Scores
It gives errors. Anyways, just delete ''s'' in 3 places and no errors.
Great tutorial man. Thanks.
|
Thank you for the spot. Fixed.
Respuesta: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
DragonZafiro - 12.03.2016
why the "INI_Load" and "INI_ParseFile" functions needs a lot of data size on compile?
when i use it, my final .amx gets this size:
when i remove it:
and yes, are that functions, took me a while to realize it was
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
MicroKyrr - 12.03.2016
Also , I get a "wrong password error" when i enter a correct password.
Re: Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES] -
Crayder - 12.03.2016
Quote:
Originally Posted by DragonZafiro
why the "INI_Load" and "INI_ParseFile" functions needs a lot of data size on compile?
when i use it, my final .amx gets this size:
when i remove it:
and yes, are that functions, took me a while to realize it was
|
That's allocated memory to be used to store the files for reading.
Quote:
Originally Posted by MicroKyrr
Also , I get a "wrong password error" when i enter a correct password.
|
You are likely just doing to hash wrong. However, INI files are not the best to use for user systems. They aren't secure and they waste a lot of space. Consider some SQL. It's easier than it looks, but bare in mind SQL is an entire language (literally, the L stands for Language).