[Tutorial] A complete login and registration system using y_ini:
#1

A complete login and registration system using y_ini:
Foreword

Before anything, read every single word in this thread, else you'll get everything wrong and you might even get a ton of errors, simply because you have not bothered to read anything. Before asking a question, read the thread once again. It could be an error from my side, but it could also be lack of your attention.

It's been a while since I decided to create a registration system using the y_ini include created by ******. I myself am not a fan of MySQL (maybe because I've not yet bothered to work with it) saving, so I've been using y_ini since it's release.

This login and registration system includes dialogs, used to get the user input for their password, age, gender. mSelection is used to allow the player to select his skin. This system also saves the user's cash and his position (x, y, z) on disconnect. To successfully have this system working, you will need:
y_ini: Download it from this thread. If you do not know how to use y_ini, read this thread.
mSelection: Download it from this thread.
Whirlpool: Download it from this thread.
As for the colors you'll see in this thread, I created a folder called "inc" in the Gamemodes folder, created colors.pwn in it, then on the main gamemode, in which we use this sytem we'll be creating, I added the line #include "../gamemodes/inc/colors.pwn". This is the color defines, incase you want to use the colors I used:

pawn Код:
#define COLOR_PURPLE    0xC2A2DAAA
#define COLOR_GRAD2     0xBFC0C2FF
#define COLOR_GRAD1     0xB4B5B7FF
#define COLOR_GRAD2     0xBFC0C2FF
#define COLOR_GREY      0xAFAFAFAA
#define COLOR_GRAD3     0xCBCCCEFF
#define COLOR_LIGHTBLUE 0x006FDD96
#define COLOR_GRAD4     0xD8D8D8FF
#define COLOR_FADE      0xC8C8C8C8
#define COLOR_FADE2     0xC8C8C8C8
#define COLOR_FADE3     0xAAAAAAAA
#define COLOR_FADE4     0x8C8C8C8C
#define COLOR_YELLOW    0xDABB3E00
#define COLOR_FADE5     0x6E6E6E6E
#define COLOR_GRAD5     0xE3E3E3FF
#define COLOR_FADE1     0xE6E6E6E6
#define COLOR_GRAD6     0xF0F0F0FF
#define NORMAL_COL  0xFFFFFF00
We'll use serverside cash, so ontop of your gamemode (below your includes), you'll need to add the code below. If you want to give money to a player, use GivePlayerCash. If you want to reset the player's cash (set it to $000000000), use ResetPlayerCash. And if you want to retrieve the ammount of cash the player has get, use GetPlayerCash.

pawn Код:
#define GivePlayerCash(%0,%1) SetPVarInt(%0,"Money",GetPlayerCash(%0)+%1),GivePlayerMoney(%0,%1)
#define ResetPlayerCash(%0) SetPVarInt(%0,"Money",0), ResetPlayerMoney(%0)
#define GetPlayerCash(%0) GetPVarInt(%0,"Money")
We'll also use these definitions for functions:

pawn Код:
#define function%0(%1) forward%0(%1); public%0(%1)
Tutorial
I. The includes
Ontop of your gamemode, you will need to include the includes you will be using for your server, as you might already know. For this tutorial, we will be using y_ini and mSelection, as indicated above. Therefore, you'll have to include them ontop of your script.

pawn Код:
#include <a_samp> //Mandatory include
#include <mSelection>
#include <YSI\y_ini>
II. The enumerator
Don't know what an enumerator is? Read this thread.

As I mentioned previously, just a few lines above, we'll be saving the user password, the user's cash, the user's skin, age, deaths, kills and position on disconnect. For that, we will use an enumerator (we'll use an enum to store our variables). If you don't know what an enumerator is, you should read the thread that I linked above.

You can call the enum(erator) whatever you want. Make sure the enum is ontop of everything.

pawn Код:
enum pInfo //Change pInfo (playerInfo) to whatever you'd like, If you wish.
{
    pPass[129],
    pCash,
    pSex,
    pAge,
    Float:pPos_x,
    Float:pPos_y,
    Float:pPos_z,
    pSkin,
    pKills,
    pDeaths
}
new PlayerInfo[MAX_PLAYERS][pInfo];
I bet you noticed that there's something I didn't explain: new PlayerInfo[MAX_PLAYERS][pInfo]; We will be using this to later load, save and/or write something to the user's textfile (number of deaths, his age). You'll see the use of it very soon.
III. Showing the dialogs, creating user file if unexistent
So far, what have we got? We've included the includes we will be using for our system. We've created an enumerator, which will be used to store our variables. Now we need to start creating the visual stuff, if you get my point: the dialogs.

But before proceeding, we'll need to define the dialogs and the path that the user textfile will be created (in the Scriptfiles folder). In regards of the path where the user textfile will be created/stored, you'll need to create a folder in the 'Scriptfiles' folder, named after whatever you want (e.g. "Users", "Accounts"). The definitions are done ontop of the script, just below our includes.

I've created a folder called "Accounts" in the "Scriptfiles" folder (the folder that comes with the SA:MP server package). Incase you've named it different, just change the 'Accounts' text to the name of your folder (e.g. #define PATH "Users/%s.ini").

pawn Код:
#include <a_samp> //Mandatory include
#include <mSelection>
#include <YSI\y_ini>

#define DIALOG_REGISTER      1
#define DIALOG_LOGIN      2
#define DIALOG_AGE        3
#define DIALOG_SEX        4
#define PATH "Accounts/%s.ini"
Now that we have defined the dialogs and the path to store the user textfile in, we will need to create a function that will grab the path we are using to store our users textfiles into.

pawn Код:
stock UserPath(playerid)
{
    new string[128], playername[MAX_PLAYER_NAME];
    GetPlayerName(playerid,playername,sizeof(playername));
    format(string,sizeof(string),PATH,playername);
    return string;
}
We'll also need to create a function to load the user's data. Notice that pSex will be an integer and not a string. That's because we'll be defining "male" as 1 and "female" as 2.

pawn Код:
forward LoadUser_data(playerid,name[],value[]);
public LoadUser_data(playerid,name[],value[])
{
    INI_Int("Password",PlayerInfo[playerid][pPass]);
    INI_Int("Cash",PlayerInfo[playerid][pCash]);
    INI_Int("Sex",PlayerInfo[playerid][pSex]);
    INI_Int("Age",PlayerInfo[playerid][pAge]);
    INI_Float("Pos_x",PlayerInfo[playerid][pPos_x]);
    INI_Float("Pos_y",PlayerInfo[playerid][pPos_y]);
    INI_Float("Pos_z",PlayerInfo[playerid][pPos_z]);
    INI_Int("Skin",PlayerInfo[playerid][pSkin]);
    INI_Int("Kills",PlayerInfo[playerid][pKills]);
    INI_Int("Deaths",PlayerInfo[playerid][pDeaths]);
    return 1;
}
Now we can work on the OnPlayerConnect callback because we've got everything we need to work on it. As I told you just a few lines above, we'll check if the player's registered (then, login) or not (then, register, creating a path). Let's suppose my in-game name is Twizted.

pawn Код:
public OnPlayerConnect(playerid)
{
    if(fexist(UserPath(playerid))) //If there's a textfile for Twizted, then the user should login
    {
        INI_ParseFile(UserPath(playerid), "LoadUser_%s", .bExtra = true, .extra = playerid); //Loads the data from the user's textfile (password)
        ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD,"Login","Type your password below in order to login.","Login","Quit");
    }
    else //If the path for Twizted in the Scriptfiles/Accounts directory is non-existent, the user is prompted to register or quit
    {
        ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD,"Register","Type your password below in order to register a new account.","Register","Quit");
    }
    return 1;
}
IV. The dialogs responses
Woah, that was long! By now, you should just get up, walk around for five minutes and have a drink. There's more to come, and there's more you must follow in order to have a great system. Now, we'll be working on our OnDialogResponse callback. We'll work on a specific order on the dialogs. But before working on the callback, we'll need to create a variable and a boolean. We'll also need to define SECONDS, which we'll use to save each user's account. You'll understand what I mean.

pawn Код:
//Just below your other defines
#define SECONDS(%1) ((%1)*(1000))

//Your variables
new Logged[ MAX_PLAYERS ]; //Checks if the player is logged in.

//Your booleans
new bool:pNewlyRegged[MAX_PLAYERS]; //Checks if player registered
We'll also need to use Whirlpool hashing to hash the user's password as he responds to the registration dialog. The link to download this plugin is at the very start of the thread.

pawn Код:
//Below your includes
native WP_Hash(buffer[], len, const str[]);
We need a function that will be used to fix the spectate mode @ OnPlayerSpawn.

pawn Код:
forward SpectateFix(playerid);
public SpectateFix(playerid)
{
    SpawnPlayer(playerid);
    return true;
}
In the callback OnGamemodeInit(), we'll need to set a timer that will be used in our SaveAccounts function.

pawn Код:
public OnGameModeInit()
{
    //Anything else you might have in this callback
    skinlist = LoadModelSelectionMenu("skins.txt");
    SetTimer("SaveAccounts", SECONDS(13), 1);
    return 1;
}
We will need two functions to save the user's data.

pawn Код:
function SaveAccountStats(playerid)
{
    if(Logged[playerid] == 1)
    {
    new
        INI:File = INI_Open(UserPath(playerid))
    ;
    INI_SetTag(File,"data"); //Settings tags in y_ini is very important!

    PlayerInfo[playerid][pSkin] = GetPlayerSkin(playerid);
    PlayerInfo[playerid][pCash] = GetPlayerCash(playerid);
    new
        Float:x,
        Float:y,
        Float:z
    ;
    GetPlayerPos(playerid,x,y,z);
    PlayerInfo[playerid][pPos_x] = x;
    PlayerInfo[playerid][pPos_y] = y;
    PlayerInfo[playerid][pPos_z] = z;

    INI_WriteInt(File,"Cash",PlayerInfo[playerid][pCash]);
    INI_WriteInt(File,"Age",PlayerInfo[playerid][pAge]);
    INI_WriteInt(File,"Sex",PlayerInfo[playerid][pSex]);
    INI_WriteInt(File,"Skin",PlayerInfo[playerid][pSkin]);
    INI_WriteFloat(File,"Pos_x",PlayerInfo[playerid][pPos_x]);
    INI_WriteFloat(File,"Pos_y",PlayerInfo[playerid][pPos_y]);
    INI_WriteFloat(File,"Pos_z",PlayerInfo[playerid][pPos_z]);
    INI_WriteInt(File,"Deaths",PlayerInfo[playerid][pDeaths]);
    INI_WriteInt(File,"Kills",PlayerInfo[playerid][pKills]);

    INI_Close(File);
    }
    return 1;
}

function SaveAccounts()
{
    for(new i = 0; i < MAX_PLAYERS; i++)
    {
        if(IsPlayerConnected(i))
        {
            SaveAccountStats(i); //Will be used to save the account stats
        }
    }
}
Now we're ready to work on the OnDialogResponse callback. Throughout the script, there are comments you must read in order to understand what we're doing.

pawn Код:
public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[])
{
    if(dialogid == DIALOG_AGE)
    {
        TogglePlayerSpectating(playerid, false);

        if(!response)
        {
            Kick(playerid); //If the player clicks the second button
        }
        else
        {
            if(strlen(inputtext))
            {
                new age = strval(inputtext);
                if(age > 100 || age < 16) //If the age doesn't meet the requirements (change to whatever you want)
                {
                    ShowPlayerDialog(playerid, DIALOG_AGE, DIALOG_STYLE_INPUT, "Age","How old is your character?\nYour character's age must be between 16 and 100.","Proceed","Quit");
                }
                else //If the player types a age that meets the requirements
                {
                    PlayerInfo[playerid][pAge] = age; //Sets the player's age
                    new
                        string[ 64 ]
                    ;
                    format(string, sizeof(string), "{3685BA}SERVER: {FFFFFF}You are %d years old.",age);
                    SendClientMessage(playerid, -1, string);
                    GivePlayerCash(playerid, 15000); //Gives a $15,000 ammount of starting cash
                    SaveAccountStats(playerid); //Saves the player's account stats
                    SpawnPlayer(playerid); //Spawns the player
                    SetTimer("SpectateFix", 350, false); //Fixes a bug that needs this timer in order to successfully use mSelection
                }
            }
            else
            {
                return 0;
            }
        }
    }
    if(dialogid == DIALOG_SEX)
    {
        if(response)
        {
            PlayerInfo[playerid][pSex] = 1; // Male = 1
            SendClientMessage(playerid, -1, "{3685BA}SERVER: {FFFFFF}You have set your character's gender. You are a male.");
            ShowPlayerDialog(playerid, DIALOG_AGE, DIALOG_STYLE_INPUT, "Age","How old is your character?\nYour character's age must be between 16 and 100.","Proceed","Quit"); //After the sex selection, you're sent to the age selection dialog
        }
        else
        {
            PlayerInfo[playerid][pSex] = 2; //Female =  2
            SendClientMessage(playerid, -1, "{3685BA}SERVER: {FFFFFF}You have set your character's gender. You are a female.");
            ShowPlayerDialog(playerid, DIALOG_AGE, DIALOG_STYLE_INPUT, "Age","How old is your character?\nYour character's age must be between 16 and 100.","Proceed","Quit");//After the sex selection, you're sent to the age selection dialog
        }
    }
    switch( dialogid )
    {
        case DIALOG_REGISTER:
        {
            if (!response) return Kick(playerid); //If the player clicks the second button
            if(response) //If he clicks the first button
            {
                if(!strlen(inputtext)) return ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_INPUT, "Registering","You have entered an invalid password.\nType your password below to register a new account.","Register","Quit"); //This happens if the user has input no password. You can add limits here.
                new buf[129]; //Now we'll use our Whirlpool hashing
                new INI:File = INI_Open(UserPath(playerid)); //Opening the user file
                INI_SetTag(File,"data"); //Settings tags
                WP_Hash(buf, sizeof(buf), inputtext); //Hashing the inputtext (password)
                INI_WriteString(File,"Password", buf); //Writes a string, the password, hashed
                INI_WriteInt(File,"Cash",0);
                INI_WriteInt(File,"Skin",0);
                INI_WriteInt(File,"Sex",0);
                INI_WriteInt(File,"Age",0);
                INI_WriteFloat(File,"Pos_x",0);
                INI_WriteFloat(File,"Pos_y",0);
                INI_WriteFloat(File,"Pos_z",0);
                INI_WriteInt(File,"Deaths",0);
                INI_WriteInt(File,"Kills",0);
                INI_Close(File);

                pNewlyRegged[playerid] = true; //Changing the value of our boolean
               
                ShowPlayerDialog(playerid, DIALOG_SEX, DIALOG_STYLE_MSGBOX, "Sex","What is the gender of your character?","Male","Female"); //After the registration, you must select your gender
            }
        }
        case DIALOG_LOGIN:
        {
            if ( !response ) return Kick ( playerid ); //If the player clicks the second button
            if( response )
            {
                new hashpass[129];
                WP_Hash(hashpass,sizeof(hashpass),inputtext);
                if(!strcmp(hashpass, PlayerInfo[playerid][pPass], false))  //Compares the inputtext to the player's pass, pPass.
                {
                    INI_ParseFile(UserPath(playerid), "LoadUser_%s", .bExtra = true, .extra = playerid);
                    TogglePlayerSpectating(playerid, false);
                    SetTimerEx("UnsetFirstSpawn", 5000, false, "i", playerid); //We're using this for other purposes, to fix a bug in skin selection (OnPlayerRequestClass)
                    Logged[ playerid ] = 1; //giving the 1 ("true") value to our variable "Logged"
                    GivePlayerCash(playerid, PlayerInfo[playerid][pCash]); //Gives the player the cash he has
                    SetSpawnInfo(playerid, 0 /*Because the player has no team*/, PlayerInfo[playerid][pSkin], PlayerInfo[playerid][pPos_x], PlayerInfo[playerid][pPos_y], PlayerInfo[playerid][pPos_z], 1.0, -1, -1, -1, -1, -1, -1); //Sets the player's spawn information
                }
                else
                {
                    ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_INPUT,"Login","You have entered an incorrect password.\nType your password below to login.","Login","Quit"); //If the player has entered an invalid password, he's prompted to retype his password
                }
                return 1;
            }
        }
    }
    return 1;
}
V. OnPlayerSpawn (skin selection menu, player positioning)
Another long paragraph. Time for another drink, maybe?

We've completed the dialog responses. Well, most of them. We still need to work on our skin selection, after the spawn. You should've set up mSelection by now (atleast the part of including it, adding it to the pawno/include folder and adding the skins.txt file to the Scriptfiles folder). If you haven't, do it now.

And to have no errors while working on the skin selection, you must add the following line ontop of your skin (remember, always below your includes).

pawn Код:
new skinlist = mS_INVALID_LISTID;
Now we can work on the OnPlayerSpawn callback. Read the comments throughout the script. But we'll need to create a function called RandomSpawnLoc(playerid); in order to randomly set the player's position if he has just registered.

pawn Код:
stock RandomSpawnLoc(playerid)
{
    switch(random(3))
    {
        case 0: // ls
        {
            SetPlayerPos(playerid, 1642.1678, -2240.5181, 13.4952);
            SendClientMessage(playerid, -1, "{3685BA}SERVER: {FFFFFF}You have spawned in Los Santos! This where you start your life.");
        }
       
        case 1: // sf
        {
            SetPlayerPos(playerid, -1409.0891, -306.1781, 14.1484);
            SendClientMessage(playerid, -1, "{3685BA}SERVER: {FFFFFF}You have spawned in San Fierro! This where you start your life.");
        }
       
        case 2: // lv
        {
            SetPlayerPos(playerid, 1689.2460, 1446.9261, 10.7672);
            SendClientMessage(playerid, -1, "{3685BA}SERVER: {FFFFFF}You have spawned in Las Venturas! This where you start your life.");
        }
    }
    return 1;
}
pawn Код:
public OnPlayerSpawn(playerid)
{
    if(pNewlyRegged[playerid]) //If the player is a newly registered player
    {
        ShowModelSelectionMenu(playerid, skinlist, "Skin Selection"); //Shows the skin selection menu for the player
    }
   
    if(PlayerInfo[playerid][pPos_x] == 0 && PlayerInfo[playerid][pPos_y] == 0) //Pretty much means that if the player isn't registered, his position is set to the position indicated in RandomSpawnLoc function
    {
        RandomSpawnLoc(playerid); //Sets player position randomly
        Logged[playerid] = 1; //Changes the value of our variable "Logged" to 1("true").
    }
    else //If the player logged in
    {
        SetPlayerPos(playerid, PlayerInfo[playerid][pPos_x], PlayerInfo[playerid][pPos_y], PlayerInfo[playerid][pPos_z]); //Sets the player position to his last position
        Logged[playerid] = 1;
    }
    return 1;
}
VI. Player selecting skin
The last paragraph wasn't so long, thankfully. And we're almost done here. Now we need to work on the the mSelection callback called OnPlayerModelSelection.

pawn Код:
public OnPlayerModelSelection(playerid, response, listid, modelid)
{
    if(listid == skinlist)
    {
        if(response)
        {
            new string[129];
            format(string, sizeof(string), "{3685BA}SERVER: {FFFFFF}You have set your skin to ID %d!", modelid);
            SendClientMessage(playerid, -1, string);
            SetPlayerSkin(playerid, modelid);
            new INI:File = INI_Open(UserPath(playerid));
            INI_SetTag(File,"data");
            INI_WriteInt(File,"Skin",modelid);
            INI_Close(File);
        }

        else
        {
            SendClientMessage(playerid, -1, "{3685BA}SERVER: {FFFFFF}You must select a skin.");
            ShowModelSelectionMenu(playerid, skinlist, "Skin Selection");
            return 1;
        }
    }
    return 1;
}
VII. OnPlayerDeath: Kills and Deaths count
This is most likely to be considered the easiest part of this tutorial. We want to increase the kills of the killerid and increase the deaths of the playerid. Therefore, what we have to add under the callback is simple:

pawn Код:
public OnPlayerDeath( playerid, killerid, reason )
{
    PlayerInfo[playerid][pDeaths] ++; // +1 death every time the player dies.
    if( killerid != INVALID_PLAYER_ID )
    { // If the killerid is a valid player
        PlayerInfo[killerid][pKills] ++; // +1 Kill every time the killerid a valid player
    }
    //Anything else you might have in this callback
    return 1;
}
VIII. OnPlayerDisconnect: Saving the player's stats
Yes, you know this is the end. It's been a long journey, right? Well, not really If you've just copied the code between the tags, but if you've read everything, you're the man. Thanks for the time.

Remember we created a function called SaveAccountStats(playerid); on step IV? Well, we will use that to save the user's stats when he disconnects from your server (well, you certainly do not wish this to happen too often!). You just call the function inside the callback and you're done!

pawn Код:
public OnPlayerDisconnect(playerid, reason)
{
    SaveAccountStats(playerid); //Calls the function, saves the stats
    return 1;
}
The End
This is the end of this big tutorial. I've certainly spent 3 to 4 hours writing this out, but luckily having the help of other threads which saved some time. I hope that you understood everything, and if you haven't, let me know. If you come across any errors, please, let me know. But as I said at the very first lines, read every single word in this thread, else you'll get everything wrong and you might even get a ton of errors, simply because you have not bothered to read anything. Before asking a question, read the thread once again. It could be an error from my side, but it could also be lack of your attention. I hope this has been a precious help to your gamemode.
Reply
#2

Good tutorial.
Reply
#3

Thank you, Affan.

I edited the 'Foreword', as I forgot to add the colors include and the serverside cash.
Reply
#4

Good Job +REP
Reply
#5

In LoadUser_data, password must be a string, not an integer.
Reply
#6

Quote:
Originally Posted by LivingLikeYouDo
Посмотреть сообщение
In LoadUser_data, password must be a string, not an integer.
Thanks for noticing. That's something I had not seen.
Reply
#7

Good job, +REP.
Reply
#8

Quote:
Originally Posted by Twizted
Посмотреть сообщение
Thanks for noticing. That's something I had not seen.
No worries, mistakes happens.
But also, I think in DIALOG_REGISTER, you should set the max cells for the password's hash to 129, like this;
Код:
INI_WriteString(File,"Password", buf, 129);
But I don't think that it is such a big deal, but I saw some players come through problems because of this.

Anyways, good job as always! I wish I could've been able to reputate you, but still, heads up!

Hope to see more!
Reply
#9

LivingLikeYouDo: It is set to 129 characters, because just above it, we have new buf[129], so that restricts the maximum ammount of cells.
Reply
#10

Nice Tut. Thanks Dude.
Reply
#11

Thanks a lot, iFarbod, I'm glad it was useful.
Reply
#12

Added final touches, everything's working. Make sure to read everything throughout. I was looking at the code once more and tested it, and ran across a problem. Your enumerator needs to be located before any custom function or standard SAMP function (after the defines, preferably). Also, INI_String didn't work, so you have to write 'Password' as a string and load it as an integer (thanks to Kush on this thread).
Reply
#13

I have a problem, when I reconnect and login, the server sends me to select a skin again and no save the skin in the account.
Reply
#14

Good tutorial.

Nice job! Keep it up
Reply
#15

Quote:
Originally Posted by Lorataco99
View Post
I have a problem, when I reconnect and login, the server sends me to select a skin again and no save the skin in the account.
Any help?
Reply
#16

'PlayerInfo[playerid][pCash] = GetPlayerCash(playerid);'

GetPlayerCash isn't implemented, good tutorial though, thank you.
Reply
#17

Nice tut
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)