[Tutorial] MySQL [R41-2] Registration System
#1

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.
pawn Код:
#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.
pawn Код:
#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..
pawn Код:
#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.
Код:
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.
Код:
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.
Код:
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.
Код:
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.
Код:
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.
Код:
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 failed to login. (%d)", pInfo[playerid][Name], pInfo[playerid][PasswordFails]);
				// Printing the message that someone has failed to login to his account.

				if (pInfo[playerid][PasswordFails] >= 3) // If the fails exceeded the limit we kick the player.
				{
					format(String, sizeof(String), "%s has been kicked Reason: {FF0000}(%d/3) Login fails.", 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.
Код:
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.
Код:
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.
Код:
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 ******.
  • 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 !
Reply
#2

OnPlayerDisconnect is called when gamemode exits. So you are calling it twice.
Reply
#3

I am not calling it twice, and this is the most appropriate way to save the player's data upon the server's closure otherwise players data would be simply lost just after the server closes.
Reply
#4

Quote:
Originally Posted by Variable™
Посмотреть сообщение
I am not calling it twice, and this is the most appropriate way to save the player's data upon the server's closure otherwise players data would be simply lost just after the server closes.
Gammix is right. OnPlayerDisconnect is called when the gamemode exits so you are saving the player's account twice.
Reply
#5

I won't talk too much, I have debugged what you have said above just to show that your statements are wrong. Well, I have made a message prints to the log once the server shuts down with the "/rcon exit" method.

My OnPlayerDisconnect 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] = 0;
	print("OnPlayerDisconnect has been called."); // Sending message once OnPlayerDisconnect is called.
	return 1;
}
The result of the code:
Код:
[13:40:00] [connection] 10.42.0.74:61008 requests connection cookie.
[13:40:01] [connection] incoming connection: 10.42.0.74:61008 id: 0
[13:40:01] [join] Variable has joined the server (0:10.42.0.74)
[13:40:12] RCON (In-Game): Player #0 (Variable) has logged in.
[13:40:14] RCON (In-Game): Player [Variable] sent command: exit
[13:40:14] --- Server Shutting Down.
[13:40:14] OnPlayerDisconnect has been called.
[13:40:15] [part] Variable has left the server (0:0)
You saw the message being sent twice in the log? Nope.
Hopefully you'll next time make sure of what you state before you do and thanks.

You can also try that by yourself.

=======================================

Would like to make sure the OnPlayerDisconnect doesn't get called twice? Here's another evidence!

When I removed the part from OnGameModeExit, the message didn't show anymore.

Код:
public OnGameModeExit()
{
/*    for(new i = 0, j = GetPlayerPoolSize(); i <= j; i++) // Getting the highest player ID in the game.
    {
		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;
}
Same stuff at OnPlayerDisconnect, as in this 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] = 0;
	print("OnPlayerDisconnect has been called."); // Sending message once OnPlayerDisconnect is called.
	return 1;
}
The result from the log:
Код:
[13:57:34] [connection] 10.42.0.74:61529 requests connection cookie.
[13:57:35] [connection] incoming connection: 10.42.0.74:61529 id: 0
[13:57:35] [join] Variable has joined the server (0:10.42.0.74)
[13:57:49] RCON (In-Game): Player #0 (Variable) has logged in.
[13:57:52] RCON (In-Game): Player [Variable] sent command: exit
[13:57:52] --- Server Shutting Down.
[13:57:52] [part] Variable has left the server (0:0)
Seen the log message? Nothing has been called as the message didn't print to the log, that means its not even called. So, if I don't use my way, no one's stats will be saved once server closes.

For "/rcon gmx" method
  • If you kick at OnGameModeExit it'll call OnPlayerDisconnect, if you don't do anything at OnGameModeExit, once you close the server, no one's stats will be saved and will just save if you RESTART server only.
Which means, my solution is the finest for now.
Reply
#6

Here are things that you need to look on:
  • Use foreach
  • Make global variables static so as same memory won't be allocated for these varaibles on each execution of abstract machine
  • Also make other local variables static that need not be created again and again.
  • Make a global variable (putting it in enum would be nice for readability) for holding names so as they are intialised during connection and can be used through out the script without creating another.
  • Use bool tags for variables that need to treat as boolean in logic wise so as to increase readability.
  • Normalise your database.
Reply
#7

Tutorial has been updated. Check it out!
Reply
#8

playerid in OnPlayerDeath is always valid, You don't need to check if the killerid is not invalid to increase deaths, move it out (only deaths).

Good job by the way, Well explained.
Reply
#9

Good job...
Can you explain and add SPAWN system too..
Like giving a spawn point for new registrations and then if player logged in he will spanw at last location as well player whole data being updated every second?
Reply
#10

Sure, I will do that within the next version, (Expect that today/tomorrow).
Reply
#11

Hmm i might update my password with hash and salt.
only have hash.

My code is Different from this tho

Good tut.
Reply
#12

Well explained, thank you for this tutorial.
Reply
#13

Quote:
Originally Posted by Variable™
Посмотреть сообщение
I won't talk too much, I have debugged what you have said above just to show that your statements are wrong. Well, I have made a message prints to the log once the server shuts down with the "/rcon exit" method.

My OnPlayerDisconnect 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] = 0;
	print("OnPlayerDisconnect has been called."); // Sending message once OnPlayerDisconnect is called.
	return 1;
}
The result of the code:
Код:
[13:40:00] [connection] 10.42.0.74:61008 requests connection cookie.
[13:40:01] [connection] incoming connection: 10.42.0.74:61008 id: 0
[13:40:01] [join] Variable has joined the server (0:10.42.0.74)
[13:40:12] RCON (In-Game): Player #0 (Variable) has logged in.
[13:40:14] RCON (In-Game): Player [Variable] sent command: exit
[13:40:14] --- Server Shutting Down.
[13:40:14] OnPlayerDisconnect has been called.
[13:40:15] [part] Variable has left the server (0:0)
You saw the message being sent twice in the log? Nope.
Hopefully you'll next time make sure of what you state before you do and thanks.

You can also try that by yourself.

=======================================

Would like to make sure the OnPlayerDisconnect doesn't get called twice? Here's another evidence!

When I removed the part from OnGameModeExit, the message didn't show anymore.

Код:
public OnGameModeExit()
{
/*    for(new i = 0, j = GetPlayerPoolSize(); i <= j; i++) // Getting the highest player ID in the game.
    {
		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;
}
Same stuff at OnPlayerDisconnect, as in this 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] = 0;
	print("OnPlayerDisconnect has been called."); // Sending message once OnPlayerDisconnect is called.
	return 1;
}
The result from the log:
Код:
[13:57:34] [connection] 10.42.0.74:61529 requests connection cookie.
[13:57:35] [connection] incoming connection: 10.42.0.74:61529 id: 0
[13:57:35] [join] Variable has joined the server (0:10.42.0.74)
[13:57:49] RCON (In-Game): Player #0 (Variable) has logged in.
[13:57:52] RCON (In-Game): Player [Variable] sent command: exit
[13:57:52] --- Server Shutting Down.
[13:57:52] [part] Variable has left the server (0:0)
Seen the log message? Nothing has been called as the message didn't print to the log, that means its not even called. So, if I don't use my way, no one's stats will be saved once server closes.

For "/rcon gmx" method
  • If you kick at OnGameModeExit it'll call OnPlayerDisconnect, if you don't do anything at OnGameModeExit, once you close the server, no one's stats will be saved and will just save if you RESTART server only.
Which means, my solution is the finest for now.
That might only be the case for "/rcon exit", cause it is generally called when gamemode exits. Even at "changemode" or "gmx", OnPlayerDisconnect is called properly. I would rather hook "/rcon exit" with saving player data than doing it always when gamemode exits.
But anyways gamemode exit happens only once or twice in a long time period unless your mode requires gamemode switching for new rounds/games, where this could be concern for developers.
Reply
#14

I don't see why you save a cache for later use.

When a player connects, just load his ID in the database that matches his name and load the hashed password and salt, store all 3 (password, salt and ID) in the array pInfo and ShowPlayerDialog to input a password.
Then in that callback (OnDialogResponse), check if the hashed input is the same as the stored hashed password.
If so, send a new query to load the remaining data and store that as well in your pInfo array.

No need to save cache and having to remember to delete it later on.



Also, saving everything inside OnPlayerDisconnect is also questionable.
With so little data, it won't matter that much.
But once you have a huge gamemode, you'll want to split up datasaving.



In my gamemode, I have so much data that my query would be at least 5000 characters long when I would save everything at once in one huge query upon logging out.

Just save whatever changes using a small query and save it immediately.

A player gets money? Save it.
A player receives a scorepoint? Save it.

A player buys a vehicle or a house? Save the money with one query, and update the housedata with a second query.

I'm having a full-blown auction system in my gamemode to auction houses and vehicles to other players, all controlled via one single menu built with textdraws (my vehicle-menu).

Players can place bids on houses and the system also has an automatic system linked to it.
Once a house is put up for auction, the house will automatically transferred to the highest bidder who has the money for it after 7 days, even when both parties (seller and buyer) are offline.
If no bids were there, or nobody has the money for it, the house goes to the bank.
Also vehicles can be browsed that are put up for auction.

House-owners also need to pay a weekly maintenance fee to keep their houses and vehicles.
If they can't afford it, the house is automatically put up for auction along with the vehicles linked to that house.

Doing such a system with queries only inside OnPlayerDisconnect is mindblowing and practically impossible to do.

If I would only save whenever the server shuts down, the server would need several thousand huge queries to update every house on the server,
every house-vehicle (which could be several tens of thousands), just in case something was edited.

Just save whatever needs to be saved at the moment it changes, and use small queries to do it.

Nothing as easy as a function like this:
PHP Code:
// This function is used to give (or take) money to/from the player
Player_GiveMoney(playeridamount)
{
    
// Setup local variables
    
new query[128];
    
// Add the given Money to the player's account
    
APlayerData[playerid][Money] = APlayerData[playerid][Money] + amount;
    
// Also update the client immediately instead of waiting for the GlobalTimer1000 to update it
    
ResetPlayerMoney(playerid);
    
GivePlayerMoney(playeridAPlayerData[playerid][Money]);
    
// Update money for this player in the player's account in MySQL
    
mysql_format(SQL_dbquerysizeof(query), "UPDATE playerdata SET Money = '%i' WHERE UserID = '%i'"APlayerData[playerid][Money], APlayerData[playerid][UserID]);
    
mysql_tquery(SQL_dbquery"""");
    return 
1;

Whenever a player gets money or pays money, his account in the database has also been updated at once.

That way, you can also lose the Corrupt_Check thing, whatever it does.
The data was already saved while playing, logging in fast after a player disconnects won't corrupt data as there is nothing saved in there.



What if your server suddenly crashes? Then nothing would be saved for hours.
You'll lose hours of progress of your players, who in turn will not be happy because they need to spend hours again to get back what was lost.


The only thing I'm saving in my OnPlayerDisconnect callback, is the fuel for all my house-vehicles, as fuel changes every second while driving, this would mean one query per second per vehicle and is too much at once.
And for the rest of the code in there, is to delete the house-vehicles from the server's memory when a player logs out and clearing variables.



When creating an account, add some default values for values to shorten the INSERT query as well upon registering.
Since score, kills, deaths and cash are 0 to begin with, you can simply have MySQL fill in those for you automatically when your table has default values of 0 assigned to those columns.



Also, clearing an array with an enum is much easier than clearing every variable one by one.
You can clear them all at once for a single player.

Instead of doing:
PHP Code:
//Resetting player information.
pInfo[playerid][Kills] = 0;
pInfo[playerid][Deaths] = 0;
pInfo[playerid][PasswordFails] = 0
You can do:
PHP Code:
new clean[ENUM_PLAYER_DATA];
pInfo[playerid] = clean
Not much of a space-saver here, but you never have to edit that code again when you add a variable to the enum.
So less likely to forget to clear one.
Reply
#15

Learned from it a lot....But can you explain ORM....I just need to know some basics of it.
Reply
#16

Will make another tutorial for the use of ORM in a new thread soon!
Reply
#17

Anyone else having the issue that when you restart the server, Players can log into the accounts using any password?
Reply
#18

What you basically did is copy example login-system-cache from mysql github and changed some variables name and released it as a tutorial.
https://raw.githubusercontent.com/pB...stem-cache.pwn
Reply
#19

Quote:
Originally Posted by Sabur
View Post
What you basically did is copy example login-system-cache from mysql github and changed some variables name and released it as a tutorial.
https://raw.githubusercontent.com/pB...stem-cache.pwn
Quite funny that people without a sufficient enough experience are arguing for nothing, who don't even know that it doesn't mean someone is copying pieces of codes in case of similarities. You can't just say that an admin script is like all the others because both have /ban command, pal. My code is quite different and just a small similarity which doesn't mean that I copied it.

@TheLeech: Impossible, make sure you have properly copied and assigned the codes as explained above.
Reply
#20

Thanks, I had quite the fun reading this, useful, brief and knowledge-expanding, +REP for your hard work,
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)