[Tutorial] MySQL Registration System [Threaded Queries(R33+) + Whirlpool]
#1

Introduction
First of all, it's been a long time since I didn't release anything on this forum. So today I'm gonna release something that is, I think pretty useful for those who wanna learn how to create a registration system with mysql threaded queries. Many people use non-threaded queries because they think it's harder to use threaded queries. But in fact, it's just way easier than you thought. So today, I will teach so you how to make a registration system using mysql threaded queries(version r33 and above). In this tutorial, we will save player's name, password, ip, admin level, vip level, money, and player's position.

Things that you need
- Mysql plugin - Download it and place the files in your server's directory.
- Whirlpool - Download it and place the file in your server's directory.
- xampp - Download it and install it in your pc.

Creating a new database and table
- Run xampp-control and you should see this.


- Click Start on Apache and MySQL. If you have skype opened, close it.


- Click Admin at MySQL to manage/create your database.


- Click on Databases to create a new database.


- Name your server database and click Create (In this tutorial will use "server" as our database)


- After you click the Create button, you will notice your database has been created by looking at left side of your screen. Click your database to manage your table's name and column.


- You will see this screen after you click your database. (More explanation in the image)


- After you click Go button, it will redirect to you the next page where you can manage your table's column.


Congratulations, you have created your database and table! Now;

Let's start scripting!

pawn Code:
//First, of course we need to include these files first
#include <a_samp> //Without this, we won't be able to use all samp functions/callbacks
#include <a_mysql> //Without this, we won't be able to use all mysql functions
pawn Code:
//Let's define our mysql settings
#define host    "localhost" //This will be your mysql host. Default for xampp is localhost
#define user    "root" //This will be your mysql username. Default for xampp is root
#define db      "server" //This is your database name. Remember we have created a database called server before.
#define pass    "" //This is your mysql password. In xampp, the password didn't set. So leave it empty.

//dialogs
#define dregister    6287 //dialog register id
#define dlogin        6288 // ^
pawn Code:
//Global variables. We will use them later
static
    mysql, //This variable will be used to manage our database
    Name[MAX_PLAYERS][24], //We will use this variable to store player's name.
    IP[MAX_PLAYERS][16] //We will use this variable to store player's ip.
    ;
pawn Code:
native WP_Hash(buffer[], len, const str[]); //whirlpool, for hashing our password
pawn Code:
//Now let's create an enumerator that holds player's information
enum PDATA //We name our enumerator as PDATA (which stands for PlayerDATA). You can name it however you want.
{
    ID, //Will be used later to store player's ID from database so we can use it anywhere later
    Password[129], //We will load player's password into this varible from database
    Admin, //We will load player's admin level from database into this variable so we can use it anywhere later.
    VIP, //We will load player's VIP level from database into this variable so we can use it anywhere later.
    Money, //We will load player's money from database into this variable so we can use it anywhere later.
    Float:posX, //We will load player's X position from database into this variable so we can use it anywhere later.
    Float:posY, //We will load player's Y position from database into this variable so we can use it anywhere later.
    Float:posZ //We will load player's Z from database into this variable so we can use it anywhere later.

}
new pInfo[MAX_PLAYERS][PDATA]; //Variable that stores enumerator above
pawn Code:
public OnGameModeInit()
{
    mysql_log(LOG_ERROR | LOG_WARNING | LOG_DEBUG); //Let's enable debugging so we can detect a problem(if there is)
    mysql = mysql_connect(host, user, db, pass); //This function will connect your server to database. Remember we have defined our host, username, database and password. It's time to use it here.
    if(mysql_errno(mysql) != 0) print("Could not connect to database!"); //This will tell if your connection to database is successful or not. If it's not, check your host, username, database and password. Make sure they all right.
    return 1;
}
pawn Code:
//Checking player's account if they are registered or not.
public OnPlayerConnect(playerid)
{
    new query[128]; //We use this variable to format our query
    GetPlayerName(playerid, Name[playerid], 24); //Getting player's name
    GetPlayerIp(playerid, IP[playerid], 16); //Getting layer's IP
    mysql_format(mysql, query, sizeof(query),"SELECT `Password`, `ID` FROM `players` WHERE `Username` = '%e' LIMIT 1", Name[playerid]);
    // - We use mysql_format instead of format because we can use an %e specifier. %e specifier escapes a string so we can avoid sql injection which means we don't have to use mysql_real_escape_string
    // - Formatting our query; SELECT `Password`, `ID` FROM `players` WHERE `Username`='%e' means we are selecting a Password and ID's column in the table that has player's name in Username column.
    // - LIMIT 1; we only need 1 result to be shown
    mysql_tquery(mysql, query, "OnAccountCheck", "i", playerid);
    //lets execute the formatted query and when the execution is done, a callback OnAccountCheck will be called
    //You can name the callback however you like

    return 1;
}
pawn Code:
//OnAccountCheck is a custom callback which means it has to be forwarded.
forward OnAccountCheck(playerid);

//Now once the query has been processed;
public OnAccountCheck(playerid)
{
    new rows, fields; //a variable that will be used to retrieve rows and fields in the database.
    cache_get_data(rows, fields, mysql);//let's get the rows and fields from the database.
    if(rows) //if there is row
    {//then
        cache_get_field_content(0, "PASS", pInfo[playerid][Password], mysql, 129);
        //we will load player's password into pInfo[playerid][Password] to be used in logging in
        pInfo[playerid][ID] = cache_get_field_content_int(0, "ID"); //now let's load player's ID into pInfo[playerid][ID] so we can use it later
        printf("%s", pInfo[playerid][Password]); //OPTIONAL: Just for debugging. If it didn't show your password, then there must be something wrong while getting player's password
        ShowPlayerDialog(playerid, dlogin, DIALOG_STYLE_INPUT, "Login", "In order to play, you need to login", "Login", "Quit"); //And since we found a result from the database, which means, there is an account; we will show a login dialog
    }
    else //if we didn't find any rows from the database, that means, no accounts were found
    {
        ShowPlayerDialog(playerid, dregister, DIALOG_STYLE_INPUT, "Register", "In order to play, you need to register.", "Register", "Quit");
        //So we show them a dialog register
    }
    return 1;
}
pawn Code:
//Now let's response to the login and register dialog
public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[])
{
    switch(dialogid)
    {
        case dlogin: //login dialog
        {
            if(!response) Kick(playerid); //if they clicked Quit, we will kick them
            new hpass[129]; //for password hashing
        new query[100]; // for formatting our query.
            WP_Hash(hpass, 129, inputtext); //hashing inputtext
            if(!strcmp(hpass, pInfo[playerid][Password])) //remember we have loaded player's password into this variable, pInfo[playerid][Password] earlier. Now let's use it to compare the hashed password with password that we load
            { //if the hashed password matches with the loaded password from database
                mysql_format(mysql, query, sizeof(query), "SELECT * FROM `players` WHERE `Username` = '%e' LIMIT 1", Name[playerid]);
                //let's format our query
                //We select all rows in the table that has your name and limit the result to 1
                mysql_tquery(mysql, query, "OnAccountLoad", "i", playerid);
                //lets execute the formatted query and when the execution is done, a callback OnAccountLoad will be called
                //You can name the callback however you like
            }
            else //if the hashed password didn't match with the loaded password(pInfo[playerid][Password])
            {
                //we tell them that they have inserted a wrong password
                ShowPlayerDialog(playerid, dlogin, DIALOG_STYLE_INPUT, "Login", "In order to play, you need to login\nWrong password!", "Login", "Quit");
            }
        }
        case dregister: //register dialog
        {
            if(!response) return Kick(playerid); //if they clicked Quit, we will kick them
            if(strlen(inputtext) < 6) return ShowPlayerDialog(playerid, dregister, DIALOG_STYLE_INPUT, "Register", "In order to play, you need to register.\nYour password must be at least 6 characters long!", "Register", "Quit");
            //strlen checks a lenght of a string. so if player types their password that is lower than 6, we tell them; Your password must be at least 6 characters long!
                new query[300];
            WP_Hash(pInfo[playerid][Password], 129, inputtext); //hashing inputtext
            mysql_format(mysql, query, sizeof(query), "INSERT INTO `players` (`Username`, `Password`, `IP`, `Admin`, `VIP`, `Money`, `PosX` ,`PosY`, `PosZ`) VALUES ('%e', '%s', '%s', 0, 0, 0, 0.0, 0.0, 0.0)", Name[playerid], pInfo[playerid][Password], IP[playerid]);
            //Now let's create a new row and insert player's information in it
            mysql_tquery(mysql, query, "OnAccountRegister", "i", playerid);
            //let's execute the query
        }
    }
    return 1;
}
pawn Code:
forward OnAccountLoad(playerid);
forward OnAccountRegister(playerid);
//let's load player's information
public OnAccountLoad(playerid)
{
    pInfo[playerid][Admin] = cache_get_field_content_int(0, "Admin"); //we're getting a field 4 from row 0. And since it's an integer, we use cache_get_row_int
    pInfo[playerid][VIP] = cache_get_field_content_int(0, "VIP"); //Above
    pInfo[playerid][Money] = cache_get_field_content_int(0, "Money");//Above
    pInfo[playerid][posX] = cache_get_field_content_float(0, "PosX");//Above. Since player's position is a float, we use cache_get_field_content_float
    pInfo[playerid][posY] = cache_get_field_content_float(0, "PosY");//Above
    pInfo[playerid][posZ] = cache_get_field_content_float(0, "PosZ");//Above
   
    GivePlayerMoney(playerid, pInfo[playerid][Money]);//Let's set their money
    //For player's position, set it once they spawn(OnPlayerSpawn)
    SendClientMessage(playerid, -1, "Successfully logged in"); //tell them that they have successfully logged in
    return 1;
}

public OnAccountRegister(playerid)
{
    pInfo[playerid][ID] = cache_insert_id(); //loads the ID of the player in the variable once they registered.
    printf("New account registered. ID: %d", pInfo[playerid][ID]); //just for debugging.
    return 1;
}
pawn Code:
public OnPlayerDisconnect(playerid, reason)
{
    new query[128], Float:pos[3]; //query[128] is for formatting our query and Float:pos[3] is for getting and saving player's position
    GetPlayerPos(playerid, pos[0], pos[1], pos[2]); //let's get player's position when they leave your server
    mysql_format(mysql, query, sizeof(query), "UPDATE `players` SET `Admin`=%d, `VIP`=%d, `Money`=%d, `posX`=%f, `posY`=%f, `posZ`=%f WHERE `ID`=%d",\
    pInfo[playerid][Admin], pInfo[playerid][VIP], pInfo[playerid][Money], pos[0], pos[1], pos[2], pInfo[playerid][ID]);
    //We update the table(`players`) by getting player's admin level, vip level, money, and positions and save them in the database
    mysql_tquery(mysql, query, "", "");
    //let's execute the query.
    return 1;
}
pawn Code:
public OnPlayerSpawn(playerid)
{
    SetPlayerPos(playerid, pInfo[playerid][posX], pInfo[playerid][posZ], pInfo[playerid][posZ]);
    //Set player's position to the last saved position.
    return 1;
}
Thank you for reading my tutorial. If there's a mistake somewhere, please let me know.
NOTE: If you're having a problem, check your mysql logs.
Thanks to;
BlueG - MySQL plugin
AndreT - For his tutorial
****** - Whirlpool plugin
ReneG - For his mysql gamemode. I learned everything from his script
Reply
#2

Nice tutorial.
Reply
#3

Another tutorial from the troll.
Reply
#4

Nice, well explained. But in case if there's too much fields, it's better to use functions like cache_get_field_content, cache_get_field_content_int, cache_get_field_content_float, etc. So that the user who uses won't get confused in knowing the field ID.
Reply
#5

For update query, shouldn't we use this (?):

pawn Code:
mysql_format(mysql, query, sizeof(query), "UPDATE `players` SET `Admin`=%d, `VIP`=%d, `Money`=%d, `posX`=%f, `posY`=%f, `posZ`=%f WHERE `ID`=%d",\
    pInfo[playerid][Admin], pInfo[playerid][VIP], pInfo[playerid][Money], pos[0], pos[1], pos[2], pInfo[playerid][ID]);
mysql_query(mysql, query, false); // use_cache bool is not enabled, that means un-cached query.

/* native mysql_query(conhandle, query[], bool:use_cache = true); */
Also, you are escaping player name, why don't you escape password, lol?
Reply
#6

mysql_query and mysql_tquery are 2 different things. Also we're not escaping password because we're using whirlpool which hashes the password. So the password would be like;
Code:
B97DE512E91E3828B40D2B0FDCE9CEB3C4A71F9BEA8D88E75C4FA854DF36725FD2B52EB6544EDCACD6F8BEDDFEA403CB55AE31F03AD62A5EF54E42EE82C3FB35
But it's up to you to escape it or not.
Reply
#7

Perfect! It has eveything. From setting up XAMP to the scrip!
Reply
#8

How to get string for the database?

For int is cache_get_row_int(0, *);. For string?

Thanks for the tutorial.
Reply
#9

Quote:
Originally Posted by ]Rafaellos[
View Post
How to get string for the database?

For int is cache_get_row_int(0, *);. For string?

Thanks for the tutorial.
https://sampwiki.blast.hk/wiki/MySQL/R33#cache_get_row
Reply
#10

This looks good, just a couple remarks. It is common practice to write definitions in UPPERCASE and all other variables (ID, VIP) in lowercase or CamelCase. The other thing; I don't really consider it useful creating tables from within the script. This will most likely only ever be executed once in the lifetime of the server. Lastly, only select the fields you will actually need instead of using the '*' selector. In you account check function, for example, you only use the password.
Reply
#11

The better option would be to select all the data once and assign them if there are rows. In the login, you already got the password so compare it and you're done.

Few notes:
- Reset variables on connect.
- Instead of cache_get_data and storing the rows and fields to 2 variables, you could use directly cache_num_rows function:
pawn Code:
if (cache_num_rows()) // if (rows)
Also keep in mind about race condition attacks with threaded queries.

Quote:
Originally Posted by Lordz™
View Post
Nice, well explained. But in case if there's too much fields, it's better to use functions like cache_get_field_content, cache_get_field_content_int, cache_get_field_content_float, etc. So that the user who uses won't get confused in knowing the field ID.
Using the field's index instead of the name is faster, but yes - it depends on the user!
Reply
#12

Good tutorial! I only noticed two things (apart of these others mentioned already):
1. "mysql_debug" is only available in R34 or higher. You can write "mysql_log(LOG_DEBUG | LOG_ERROR | LOG_WARNING);" instead (phew, what a long line, I'll probably add 'LOG_ALL' in R35)
2. Like Konstantinos already said, the best option would be to select all the data once. But instead of changing your whole callback structure and assigning the data before actually logging in, you can use "cache_save" and "cache_set_active" (you can find a further explanation about these natives in the SA-MP wiki).
Reply
#13

Hello,

I tried to follow your tutorial but I encounter some errors:

Beta.pwn(69) : warning 217: loose indentation
Beta.pwn(119) : error 017: undefined symbol "Name"
Beta.pwn(119) : warning 215: expression has no effect
Beta.pwn(119) : error 001: expected token: ";", but found "]"
Beta.pwn(119) : error 029: invalid expression, assumed zero
Beta.pwn(119) : fatal error 107: too many error messages on one line

Pastbin: http://pastebin.com/JksS6uE9




How can I fix this ?


Thank you =)
Line 119: GetPlayerName(playerid, Name[playerid], 24); //Getting player's name
Reply
#14

I didn't read the entire tutorial and I saw that now by the above user's post. You've used char in the Name and IP arrays but you used them incorrect. You're supposed to use strpack/strunpack for it and it's good if you want to make less size but really not recommended into a tutorial.
Quote:
Originally Posted by anou1
View Post
Hello,

I tried to follow your tutorial but I encounter some errors:

Beta.pwn(69) : warning 217: loose indentation
Beta.pwn(119) : error 017: undefined symbol "Name"
Beta.pwn(119) : warning 215: expression has no effect
Beta.pwn(119) : error 001: expected token: ";", but found "]"
Beta.pwn(119) : error 029: invalid expression, assumed zero
Beta.pwn(119) : fatal error 107: too many error messages on one line

Pastbin: http://pastebin.com/JksS6uE9



How can I fix this ?


Thank you =)
Line 119: GetPlayerName(playerid, Name[playerid], 24); //Getting player's name
http://pastebin.com/yA3LU9pu
Reply
#15

Thank you, I changed something now it work for my pastbin but I have those errors:


Beta.pwn(352) : warning 225: unreachable code
Beta.pwn(352) : error 029: invalid expression, assumed zero
Beta.pwn(352) : error 004: function "OnAccountCheck" is not implemented
Beta.pwn(353) : warning 217: loose indentation
Beta.pwn(379) : warning 225: unreachable code
Beta.pwn(379) : error 029: invalid expression, assumed zero
Beta.pwn(379) : error 004: function "OnAccountLoad" is not implemented
Beta.pwn(401) : error 030: compound statement not closed at the end of file (started at line 34

I did a 2 forward for OnAccountCheck and OnAccountLoad but what I have done wrong ?
Could u please explain me why ? And how can I solve this ?

Thank you

New Pastbin:http://pastebin.com/71Jz3VB7



EDIT: Sorry I forget a "}"
Reply
#16

Thank you guys. I'll keep in mind to update this later.
Reply
#17

I dont know for you, but for me it doesnt save users data.

Every time I have to register again.
Reply
#18

Quote:
Originally Posted by anou1
View Post
I dont know for you, but for me it doesnt save users data.

Every time I have to register again.
Check if it is even querying data to the database. Also enable mysql_log and check the mysql debug.
Reply
#19

I found this error in mysql_log:

Code:
[20:02:42] [ERROR] CMySQLQuery::Execute[] - (error #1064) You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1
[20:02:49] [DEBUG] mysql_format - connection: 1, len: 128, format: "UPDATE `joueurs` SET `Admin`=%d, `VIP`=%d, `Argent`=%d, `posX`=%f, `posY`=%f, `posZ`=%f WHERE `ID`=%d"
[20:02:49] [ERROR] mysql_format - destination size is too small
Reply
#20

Quote:
Originally Posted by Vince
View Post
This looks good, just a couple remarks. It is common practice to write definitions in UPPERCASE and all other variables (ID, VIP) in lowercase or CamelCase. The other thing; I don't really consider it useful creating tables from within the script. This will most likely only ever be executed once in the lifetime of the server. Lastly, only select the fields you will actually need instead of using the '*' selector. In you account check function, for example, you only use the password.
If some gamemode uses many tables, it would be nice to see the script creating them automatically.
Would you feel fine if a downloaded gamemode needs 10 tables, where every table has more than 10 columns, to create them by hand?
Should the author of the gamemode write a manual about how the tables should be named, which column has which name, content-type (int, float, text, byte, ...), instead of adding a line of code which will be used only once, as you said?

I'd rather have the gamemode create them in such a case, even if that code is executed only once.
Then everyone is certain they have the proper table-name and table-structure and don't have to fiddle around trying to get all tables created manually and testing if the script works using your manually created table.
I created a table with 35 columns a few days ago and it took a while to set it up.



Nice tutorial btw, I'll be using this.
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)