[Tutorial] Login/Register [Y_INI][WHIRLPOOL][UPDATE SERIES]
#1

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(playeridnamesizeof(name));
    
// Format USER_PATH with the name that we got with GetPlayerName.
    
format(strsizeof(str), USER_PATHname); // 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(playeridnamesizeof(name));
    
TogglePlayerSpectating(playeridtrue);
    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(playeridDIALOG_LOGINDIALOG_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(playeridDIALOG_REGISTERDIALOG_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(playeridname[], value[]);
public 
LoadPlayerData_PlayerData(playeridname[], 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(playeriddialogidresponselistiteminputtext[]) {
    switch(
dialogid) {
        case 
DIALOG_REGISTER: {
            if(!
responseKick(playerid);
            else {
                if(
isnull(inputtext)) {
                    
SendClientMessage(playerid, -1"You have to enter your desired password.");
                    return 
ShowPlayerDialog(playeridDIALOG_REGISTERDIALOG_STYLE_INPUT"Register""Welcome. This account is not registered.\n\nEnter your desired password below to register:""Register""Quit");
                }
                
WP_Hash(PlayerInfo[playerid][Password], 129inputtext);
                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(playeridfalse);
                return 
1;
            }
        }
        case 
DIALOG_LOGIN: {
            if(!
responseKick(playerid);
            else {
                new
                    
hashpass[129];
                
WP_Hash(hashpasssizeof(hashpass), inputtext);
                if(!
strcmp(hashpassPlayerInfo[playerid][Password])) {
                    
// The player has entered the correct password
                    
SetPlayerScore(playeridPlayerInfo[playerid][Score]);
                    
GivePlayerMoney(playeridPlayerInfo[playerid][Money]);
                    
SendClientMessage(playerid, -1"Welcome back! You have successfully logged in!");
                    
PlayerInfo[playerid][LoggedIn] = true;
                    
TogglePlayerSpectating(playeridfalse);
                }
                else {
                    
// The player has entered an incorrect password
                    
SendClientMessage(playerid, -1"You have entered an incorrect password.");
                    
ShowPlayerDialog(playeridDIALOG_LOGINDIALOG_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(playeridkilleridreason) {
    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!
Reply
#2

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
Reply
#3

Do a pastebin for lazy people
Reply
#4

Quote:
Originally Posted by Wizzard2H
View Post
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
View Post
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.
Reply
#5

Very useful tutorial, Thanks alot!
Reply
#6

Thx i will use it in my admin system its helpful 10/10 +rep
Reply
#7

Nicely done Andy, as always.
Keep it up!
Reply
#8

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!
Reply
#9

Thank you for a well explained tutorial! This helped me out
Reply
#10

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.
Reply
#11

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
Reply
#12

Quote:
Originally Posted by NealPeteros
View Post
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[]); 
Reply
#13

Nice tutorial , i will use this because I can't understand Register/Login System with Mysql
Reply
#14

** 1 month later **

How can I hash register system?
Reply
#15

That's what Whirlpool is for. It's in the tutorial.
Reply
#16

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.
Reply
#17

Quote:
Originally Posted by kasis223
View Post
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.
Reply
#18

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
Reply
#19

Also , I get a "wrong password error" when i enter a correct password.
Reply
#20

Quote:
Originally Posted by DragonZafiro
View Post
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
View Post
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).
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)