Hello,

I know there might have been similar tutorials but in that one I tried my best to make it doing the work at the best and simple for a good performance. Well, basically this tutorial stores players information for example, score and money, kills and deaths. This tutorial shows the basic look of the stuff, and I'm quite sure I've explained the codes briefly so that newbies could understand the job of each line written within the tutorial.

In this tutorial we will be saving:
Score.
Cash.
Kills.
Deaths.

Let's start..

1. Includes
Well, we won't need any include other than SA:MP and MySQL R41-2.
Code:
#include <a_samp>
#include <a_mysql>
#include <foreach>

2. Configuration
Now we should start off with our code, well you firstly should have a MySQL database, the details is what we need to care about as if they're invalid, everything done here wouldn't work.
Well, I prefer defining the MySQL details at the top of my gamemode so that if I change them I won't have to find the connection line and start replacing stuff while it can be so easy to manage it from the top part.

Code:
#define MYSQL_HOST "Your DB Host" // Change this to your MySQL Remote IP or "localhost".
#define MYSQL_USER "Your DB User" // Change this to your MySQL Database username.
#define MYSQL_PASS "Your DB Pass" // Change this to your MySQL Database password.
#define MYSQL_DATABASE "Your DB Name" // Change this to your MySQL Database name.
Okay, now you should replace the stuff above (inside the ""'s") with your own MySQL database details, don't expect that to work if you leave it like that.
Let's continue the configuration of our script now..

Code:
#define DIALOG_REGISTER (0)
#define DIALOG_LOGIN (1)

// Make sure the dialog IDs above do not match any dialog ID you're using in your
// gamemode otherwise they won't do their job properly.

new
MySQL: Database, Corrupt_Check[MAX_PLAYERS];

// Now creating an enumerator to store player's data for further use (as below).

enum ENUM_PLAYER_DATA
{
ID,
Name[25],

Password[65],
Salt[11],

PasswordFails,

Kills,
Deaths,

Score,
Cash,

Cache: Player_Cache,
bool:LoggedIn
}

new pInfo[MAX_PLAYERS][ENUM_PLAYER_DATA];
We have defined the dialogs as seen above, make sure the dialog IDs don't match any of your gamemode's dialogs otherwise it will not work or it will cause problems with your gamemode.
We have created an enumerator to store the player's information so we won't have to do that manually at each part we'd need stored information which is just shit.

3. OnGameModeInit
Read commented parts for good explanation of codes part by part.
Code:
public OnGameModeInit()
{
new MySQLOpt: option_id = mysql_init_options();
mysql_set_option(option_id, AUTO_RECONNECT, true); // We will set that option to automatically reconnect on timeouts.

Database = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DATABASE, option_id); // Setting up the "Database" handle on the given MySQL details above.

if(Database == MYSQL_INVALID_HANDLE || mysql_errno(Database) != 0) // Checking if the database connection is invalid to shutdown.
{
print("I couldn't connect to the MySQL server, closing."); // Printing a message to the log.

SendRconCommand("exit"); // Sending console command to shut down server.
return 1;
}

print("I have connected to the MySQL server."); // If the given MySQL details were all okay, this message prints to the log.

// Now, we will set up the information table of the player's information.

mysql_tquery(Database, "CREATE TABLE IF NOT EXISTS `PLAYERS` (`ID` int(11) NOT NULL AUTO_INCREMENT,`USERNAME` varchar(24) NOT NULL,`PASSWORD` char(65) NOT NULL,`SALT` char(11) NOT NULL,`SCORE` mediumint(7), `KILLS` mediumint(7), `CASH` mediumint(7) NOT NULL DEFAULT '0',`DEATHS` mediumint(7) NOT NULL DEFAULT '0', PRIMARY KEY (`ID`), UNIQUE KEY `USERNAME` (`USERNAME`))");

// So, this code is probably the only one which you haven't understood.
// Well, we firstly create a table only if not existing in the database which is "USERS".
// We create "ID" and set it as a primary key with auto increment to use it in retrieving information and many more uses.
// We create "USERNAME" and set it as a unique key, the USERNAME stores every player's name in the database so you can
// Control the players in offline mode and when a player leaves everything storted like kills, deaths, password and Saltion key
// Wouldn't be lost upon server's close or player's disconnection.
// We store kills, deaths, score and cash as written above so they might be useful for further use.

return 1;
}
4. OnGameModeExit
Read commented parts for good explanation of codes part by part.
Code:
public OnGameModeExit()
{
foreach(new i: Player)
{
if(IsPlayerConnected(i)) // Checking if the players stored in "i" are connected.
{
OnPlayerDisconnect(i, 1); // We do that so players wouldn't lose their data upon server's close.
}
}

mysql_close(Database); // Closing the database.
return 1;
}
5. OnPlayerConnect
Read commented parts for good explanation of codes part by part.
Code:
public OnPlayerConnect(playerid)
{
new DB_Query[115];

//Resetting player information.
pInfo[playerid][Kills] = 0;
pInfo[playerid][Deaths] = 0;
pInfo[playerid][PasswordFails] = 0;

GetPlayerName(playerid, pInfo[playerid][Name], MAX_PLAYER_NAME); // Getting the player's name.
Corrupt_Check[playerid]++;

mysql_format(Database, DB_Query, sizeof(DB_Query), "SELECT * FROM `PLAYERS` WHERE `USERNAME` = '%e' LIMIT 1", pInfo[playerid][Name]);
mysql_tquery(Database, DB_Query, "OnPlayerDataCheck", "ii", playerid, Corrupt_Check[playerid]);
return 1;
}
6. OnPlayerDataCheck
Read commented parts for good explanation of codes part by part.
Code:
forward public OnPlayerDataCheck(playerid, corrupt_check);
public OnPlayerDataCheck(playerid, corrupt_check)
{
if (corrupt_check != Corrupt_Check[playerid]) return Kick(playerid);
// You'd have asked already what's corrput_check and how it'd benefit me?
// Well basically MySQL query takes long, incase a player leaves while its not proceeded
// With ID 1 for example, then another player comes as ID 1 it'll basically corrupt the data
// So, once the query is done, the player will have the wrong data assigned for himself.

new String[150];

if(cache_num_rows() > 0)
{
// If the player exists, everything is okay and nothing is wrongly detected
// The player's password and Saltion key gets stored as seen below
// So we won't have to get a headache just to match player's password.

cache_get_value(0, "PASSWORD", pInfo[playerid][Password], 65);
cache_get_value(0, "SALT", pInfo[playerid][Salt], 11);

pInfo[playerid][Player_Cache] = cache_save();
// ^ Storing the cache ID of the player for further use later.

format(String, sizeof(String), "{FFFFFF}Welcome back, %s.\n\n{0099FF}This account is already registered.\n\
{0099FF}Please, input your password below to proceed to the game.\n\n", pInfo[playerid][Name]);
ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login System", String, "Login", "Leave");
}
else
{
format(String, sizeof(String), "{FFFFFF}Welcome %s.\n\n{0099FF}This account is not registered.\n\
{0099FF}Please, input your password below to proceed to the game.\n\n", pInfo[playerid][Name]);
ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Registration System", String, "Register", "Leave");
}
return 1;
}
7. OnPlayerDisconnect
Read commented parts for good explanation of codes part by part.
Code:
public OnPlayerDisconnect(playerid, reason)
{
Corrupt_Check[playerid]++;

new DB_Query[256];
//Running a query to save the player's data using the stored stuff.
mysql_format(Database, DB_Query, sizeof(DB_Query), "UPDATE `PLAYERS` SET `SCORE` = %d, `CASH` = %d, `KILLS` = %d, `DEATHS` = %d WHERE `ID` = %d LIMIT 1",
pInfo[playerid][Score], pInfo[playerid][Cash], pInfo[playerid][Kills], pInfo[playerid][Deaths], pInfo[playerid][ID]);

mysql_tquery(Database, DB_Query);

if(cache_is_valid(pInfo[playerid][Player_Cache])) //Checking if the player's cache ID is valid.
{
cache_delete(pInfo[playerid][Player_Cache]); // Deleting the cache.
pInfo[playerid][Player_Cache] = MYSQL_INVALID_CACHE; // Setting the stored player Cache as invalid.
}

pInfo[playerid][LoggedIn] = false;
print("OnPlayerDisconnect has been called."); // Sending message once OnPlayerDisconnect is called.
return 1;
}
8. OnDialogResponse
I guess the comments after the codes explains the codes enough at this section.
Code:
public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[])
{
switch (dialogid)
{
case DIALOG_LOGIN:
{
if(!response) return Kick(playerid);

new Salted_Key[65];
SHA256_PassHash(inputtext, pInfo[playerid][Salt], Salted_Key, 65);

if(strcmp(Salted_Key, pInfo[playerid][Password]) == 0)
{
// Now, password should be correct as well as the strings
// Matched with each other, so nothing is wrong until now.

// We will activate the cache of player to make use of it e.g.
// Retrieve their data.

cache_set_active(pInfo[playerid][Player_Cache]);

// Okay, we are retrieving the information now..
cache_get_value_int(0, "ID", pInfo[playerid][ID]);

cache_get_value_int(0, "KILLS", pInfo[playerid][Kills]);
cache_get_value_int(0, "DEATHS", pInfo[playerid][Deaths]);

cache_get_value_int(0, "SCORE", pInfo[playerid][Score]);
cache_get_value_int(0, "CASH", pInfo[playerid][Cash]);

SetPlayerScore(playerid, pInfo[playerid][Score]);

ResetPlayerMoney(playerid);
GivePlayerMoney(playerid, pInfo[playerid][Cash]);

// So, we have successfully retrieved data? Now deactivating the cache.

cache_delete(pInfo[playerid][Player_Cache]);
pInfo[playerid][Player_Cache] = MYSQL_INVALID_CACHE;

pInfo[playerid][LoggedIn] = true;
SendClientMessage(playerid, 0x00FF00FF, "Logged in to the account.");
}
else
{
new String[150];

pInfo[playerid][PasswordFails] += 1;
printf("%s has been ed to login. (%d)", pInfo[playerid][Name], pInfo[playerid][PasswordFails]);
// Printing the message that someone has ed to login to his account.

if (pInfo[playerid][PasswordFails] >= 3) // If the s exceeded the limit we kick the player.
{
format(String, sizeof(String), "%s has been kicked Reason: {FF0000}(%d/3) Login s.", pInfo[playerid][Name], pInfo[playerid][PasswordFails]);
SendClientMessageToAll(0x969696FF, String);
Kick(playerid);
}
else
{
// If the player didn't exceed the limits we send him a message that the password is wrong.
format(String, sizeof(String), "Wrong password, you have %d out of 3 tries.", pInfo[playerid][PasswordFails]);
SendClientMessage(playerid, 0xFF0000FF, String);

format(String, sizeof(String), "{FFFFFF}Welcome back, %s.\n\n{0099FF}This account is already registered.\n\
{0099FF}Please, input your password below to proceed to the game.\n\n", pInfo[playerid][Name]);
ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login System", String, "Login", "Leave");
}
}
}
case DIALOG_REGISTER:
{
if(!response) return Kick(playerid);

if(strlen(inputtext) <= 5 || strlen(inputtext) > 60)
{
// If the password length is less than or equal to 5 and more than 60
// It repeats the process and shows error message as seen below.

SendClientMessage(playerid, 0x969696FF, "Invalid password length, should be 5 - 60.");

new String[150];

format(String, sizeof(String), "{FFFFFF}Welcome %s.\n\n{0099FF}This account is not registered.\n\
{0099FF}Please, input your password below to proceed.\n\n", pInfo[playerid][Name]);
ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Registration System", String, "Register", "Leave");
}
else
{

// Salting the player's password using SHA256 for a better security.

for (new i = 0; i < 10; i++)
{
pInfo[playerid][Salt][i] = random(79) + 47;
}

pInfo[playerid][Salt][10] = 0;
SHA256_PassHash(inputtext, pInfo[playerid][Salt], pInfo[playerid][Password], 65);

new DB_Query[225];

// Storing player's information if everything goes right.
mysql_format(Database, DB_Query, sizeof(DB_Query), "INSERT INTO `PLAYERS` (`USERNAME`, `PASSWORD`, `SALT`, `SCORE`, `KILLS`, `CASH`, `DEATHS`)\
VALUES ('%e', '%s', '%e', '20', '0', '0', '0')", pInfo[playerid][Name], pInfo[playerid][Password], pInfo[playerid][Salt]);
mysql_tquery(Database, DB_Query, "OnPlayerRegister", "d", playerid);
}
}
}
return 1;
}
9. OnPlayerRegister
After the player completes the registration process this gets called.
Code:
forward public OnPlayerRegister(playerid);
public OnPlayerRegister(playerid)
{
// This gets called only when the player registers a new account.
SendClientMessage(playerid, 0x00FF00FF, "You are now registered and has been logged in.");
pInfo[playerid][LoggedIn] = true;
return 1;
}
10. OnPlayerDeath
Adding kills to the killer, deaths to the player and storing that.
Code:
public OnPlayerDeath(playerid, killerid, reason)
{
if(killerid != INVALID_PLAYER_ID) // Checking if the killer of the player is valid.
{
//Increasing the kills of the killer and the deaths of the player.
pInfo[killerid][Kills]++;
pInfo[playerid][Deaths]++;
}
return 1;
}
11. OnPlayerRequestSpawn
Here we will check if the player isn't logged in so he wouldn't simply spawn and evade the login part.
Code:
public OnPlayerRequestSpawn(playerid)
{
if(pInfo[playerid][LoggedIn] == false) return 0; // Ignoring the request incase player isn't logged in.
return 1;
}
Alright so, after reading this tutorial briefly I can guarantee that you are ready to create a MySQL R41-2 Registration and Login system without finding it difficult to understand what you are doing.

This script is making use of:
SHA256 - Encrypting passwords. (Implemented by SA:MP)
foreach - Originally developed by Y_Less.
MySQL R41-2 - Originally created by BlueG, development is now held by maddinat0r. ( https://github.com/pBlueG/SA-MP-MySQL/releases )

That's the end of the tutorial, if you'd like to have the whole script in one part, take it from this link http://pastebin.com/9zLjXyhY !