[Tutorial] Building an advanced invite-only registration system [MySQL]
#1

BUILDING AN ADVANCED INVITE-ONLY REGISTRATION SYSTEM

Introduction

Hello everyone, my name is GiamPy and this is pretty much my first tutorial in sa-mp.com.
That being said, let's get ourself immediatly into the tutorial.

The purpose of this tutorial is to teach you some pretty cool stuff that may be created with MySQL and a simple registration system.

First of all, what is an invite-only registration system?
For example, all of you should know how closed-beta games usually work.

A closed beta game allows specific players (invited or chosen through an application system) to partecipate at the closed beta sessions. As it's a closed beta, not everyone is allowed to, and in order to avoid any kind of leaks using a single password to join the server, it's better to make this kind of system, which is an invite-only registration system by using serials (or however you want to call them).

This system uses MySQL so, before anything else, download the BlueG MySQL plugin from this URL.
If you don't know how MySQL works, this tutorial won't explain you the basics, there are plenty of tutorials about this in the forum.

Few months ago I have released a registration system in the SA-MP forums with BlueG's MySQL plugin, threaded and cached functions. We will use it for a faster and easier way and for the sake of the tutorial. You may find here in this URL (database file included). The plugin found in the package is not updated to the R20 yet so, feel free to do it.
Note: The pack is not updated to 0.3x so you might wanna update it as well.

Part 1: Database

First off, we need to create a database.
The name of the database doesn't really matter, but for the sake of the tutorial we'll call it keys.

The database keys will contain all the serials available to register an account in the server.

Begin making the following columns:
  1. keyID
  2. keyValue
  3. keyStatus
  4. keyUsedBy
  5. keyTS
keyID has to be set as autoincrement, it works as an identifier number for a serial.
keyValue is the actual serial, we won't need to hash them so we'll leave them as plaintext, this will allow us more easily to edit or see which serials have been used.
keyStatus will be used as a boolean value that will define if the serial as been used or not.
keyUsedBy is only made for log purposes, it will updated with the player that registered using that key.
keyTS will be the timestamp of when the serial has been used, obviously still for log purposes.

Part 2: Registration System

This is the OnPlayerConnect of my released script in SA-MP forums:

pawn Code:
public OnPlayerConnect(playerid)
{

    // We are creating the two variables playerName and escapedPlayerName assigning both of them MAX_PLAYER_NAME cells, which is 24 in the a_samp.inc (sa-mp native include).
    new
        playerName[MAX_PLAYER_NAME],
        escapedPlayerName[MAX_PLAYER_NAME];

    // GetPlayerName now stores inside the variable playerName the playerid's name.
    GetPlayerName(playerid, playerName, sizeof(playerName));
   
    // This is not really mandatory but you can't know. We're escaping the player's name and storing it into the variable escapedPlayerName.
    mysql_real_escape_string(playerName, escapedPlayerName);
   
    // We're resetting the player variables in order to avoid any kind of saving / loading issues.
    resetPlayerVariables(playerid);
       
    format(szQueryInput, sizeof(szQueryInput), "SELECT `playerName` FROM `playeraccounts` WHERE `playerName` = '%s' LIMIT 0,1", escapedPlayerName);
    mysql_function_query(connection, szQueryInput, true, "CheckPlayerAccount", "is", playerid, escapedPlayerName);
   
    format(szString, sizeof(szString), "[SERVER]: {FFFFFF}%s HAS JOINED THE SERVER\n", playerName);
    SendClientMessageToAll(COLOR_GREEN, szString);
   
    TogglePlayerSpectating(playerid, true); // THIS FUNCTION HIDES THE HUD AT THE LOGIN SCREEN
    return 1;
}
As you may see, the query is then continued in the thread CheckPlayerAccount which basically checks if the account exists or not:

pawn Code:
public CheckPlayerAccount(playerid, account[])
{
    new rows, fields;
    cache_get_data(rows, fields);
   
    if(!rows) {
        format(szString, sizeof(szString), "[SERVER]: {FFFFFF}Welcome in SERVER NAME, %s! Please, register an account to proceed.", account);
        SendClientMessage(playerid, COLOR_GREY, szString);

        format(szString, sizeof(szString), "{FFFFFF}Welcome, %s!", account);
        ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, szString, "{FFFFFF}Enter your password below:", "Register", "Cancel");
    }
    else
    {
        format(szString, sizeof(szString), "{FFFFFF}Welcome back, %s!", account);
        ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, szString, "{FFFFFF}Enter your password below:", "Login", "Cancel");
    }
   
    return 1;
}
The process will be made in this way:
  1. The account does not exist, we're not asking yet for a password. We ask FIRST the serial key in order to allow the player to register an account.
  2. Does the serial key exist and it is not defined as used? Perfect. We are going to allow the user to make an account but we're NOT YET claiming the serial as used. The player may leave immediatly before registering the account, causing the key to be wasted.
  3. We will then claim the key as used AFTER the account has been created.
Pretty simple, right?

We're going to define a new dialog by defining a new constant:

pawn Code:
#define         DIALOG_SERIAL_KEY       10003
The CheckPlayerAccount thread will be then edited in:

pawn Code:
public CheckPlayerAccount(playerid, account[])
{
    new rows, fields;
    cache_get_data(rows, fields);
   
    if(!rows) {
        format(szString, sizeof(szString), "[SERVER]: {FFFFFF}Welcome in SERVER NAME, %s! Please, insert a valid serial key to proceed.", account);
        SendClientMessage(playerid, COLOR_GREY, szString);

        format(szString, sizeof(szString), "{FFFFFF}Welcome, %s!", account);
        ShowPlayerDialog(playerid, DIALOG_SERIAL_KEY, DIALOG_STYLE_PASSWORD, szString, "{FFFFFF}Please, insert a valid serial key:", "Proceed", "Cancel");
    }
    else
    {
        format(szString, sizeof(szString), "{FFFFFF}Welcome back, %s!", account);
        ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, szString, "{FFFFFF}Enter your password below:", "Login", "Cancel");
    }
   
    return 1;
}
We have changed from DIALOG_REGISTER to DIALOG_SERIAL_KEY so we can check the serial key prompted on OnDialogResponse.

Part 3: DIALOG_SERIAL_KEY

This is the DIALOG_SERIAL_KEY that has to be placed under OnDialogResponse:

pawn Code:
case DIALOG_SERIAL_KEY: {

            if(response) {

                new
                    playerName[MAX_PLAYER_NAME];

                GetPlayerName(playerid, playerName, sizeof(playerName));

                format(szString, sizeof(szString), "{FF0000}Sorry, %s!", playerName);

                if(isnull(inputtext))
                    return ShowPlayerDialog(playerid, DIALOG_SERIAL_KEY, DIALOG_STYLE_INPUT, szString, "{FF0000}The input must not be empty.\n{FFFFFF}Insert a valid serial key to proceed:", "Proceed", "Cancel");

                format(szQueryInput, sizeof(szQueryInput), "SELECT `keyID`, `keyStatus` FROM `keys` WHERE `keyValue` = '%s' LIMIT 1", inputtext);
                mysql_function_query(connection, szQueryInput, true, "CheckingSerialKey", "is", playerid, inputtext);

            }
            else
            {
           
                SendClientMessage(playerid, COLOR_BRIGHTRED, "[SERVER]: {FFFFFF}You have to insert a valid serial key to register an account in SERVER NAME.");
                Kick(playerid);

            }
           
            return 1;
        }
The point of this code is querying the database to check if the serial key prompted in the input field exists in the datbase. You may see in the following part what I mean with it.

Part 4: Checking the Serial Prompted

We're now going to add the code that checks if the serial is either valid or not:

pawn Code:
forward CheckingSerialKey(playerid, serial_key[]);
public CheckingSerialKey(playerid, serial_key[])
{
    new rows, fields, keyID, isUsed;
    cache_get_data(rows, fields);
   
    // If the key exist, this code will be run.
    if(rows)
    {
        cache_get_field_content(0, "keyID", szQueryOutput), keyID = strval(szQueryOutput);
        cache_get_field_content(0, "keyStatus", szQueryOutput), isUsed = strval(szQueryOutput);
       
        // If the key exist BUT it has been used already, this code will be run.
        if(isUsed)
        {
            SendClientMessage(playerid, COLOR_BRIGHTRED, "[SERVER]: {FFFFFF}The serial key is already used.");
           
            // We're then going to allow him to insert another valid serial key.
            ShowPlayerDialog(playerid, DIALOG_SERIAL_KEY, DIALOG_STYLE_INPUT, szString, "{FFFFFF}Insert a valid serial key to proceed:", "Proceed", "Cancel");
            return 1;
        }

        // We are using a PVar because it's temporary and will be most likely deleted few seconds after.
        // This PVar saves the keyID that will be used to set the serial as "used" after the account is registered.
        SetPVarInt(playerid, "SerialKeyID", keyID);
       
        SendClientMessage(playerid, COLOR_GREEN, "[SERVER]: {FFFFFF}The serial key is valid, please register an account.");
        ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_INPUT, "{FFFFFF}Register", "{FFFFFF}Enter your password below:", "Register", "Cancel");
    }
    else
    {
        // The key does not exist, so the input dialog is requested once again.
        SendClientMessage(playerid, COLOR_BRIGHTRED, "[SERVER]: {FFFFFF}The serial key is invalid.");
        ShowPlayerDialog(playerid, DIALOG_SERIAL_KEY, DIALOG_STYLE_INPUT, szString, "{FFFFFF}Insert a valid serial key to proceed:", "Proceed", "Cancel");
    }
   
    return 1;
}
This will then be the new DIALOG_REGISTER:

pawn Code:
case DIALOG_REGISTER: {

            if(response) {

                new
                    playerName[MAX_PLAYER_NAME],
                    escapedPassword[129],
                    escapedPlayerName[MAX_PLAYER_NAME];

                GetPlayerName(playerid, playerName, sizeof(playerName));

                format(szString, sizeof(szString), "{FF0000}Sorry, %s!", playerName);

                if(!strlen(inputtext) || strlen(inputtext) > 24)
                    return ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_INPUT, szString, "{FF0000}The password must be at least 1 character and not over 24 characters.\n{FFFFFF}Enter your password below:", "Register", "Cancel");

                    // Whirpool hash, released by ******.
                WP_Hash(escapedPassword, sizeof(escapedPassword), inputtext);
                mysql_real_escape_string(playerName, escapedPlayerName);

                // The player has registered a serial key successfully, so we're going to set the serial key prompted earlier as used.
                // Also, as I said earlier, the name will be set in the "keyUsedBy" for log purposes.
                format(szQueryInput, sizeof(szQueryInput), "UPDATE `keys` SET `keyStatus` = '1', `keyUsedBy` = '%s' WHERE `keyID` = '%i'", playerName, GetPVarInt(playerid, "SerialKeyID"));
                mysql_function_query(connection, szQueryInput, false, "", "");
               
                // We're not deleting the "SerialKeyID" player variable because we don't need it anymore.
                DeletePVar(playerid, "SerialKeyID");

                format(szQueryInput, sizeof(szQueryInput), "INSERT INTO `playeraccounts` (playerName, playerPassword) VALUES('%s', '%s')", escapedPlayerName, escapedPassword);
                mysql_function_query(connection, szQueryInput, false, "", "");

                format(szString, sizeof(szString), "[SERVER]: {FFFFFF}Good job, %s! You are now a player of SERVER NAME!", playerName);
                SendClientMessage(playerid, COLOR_GREEN, szString);

                registerProcess[playerid] = 1; // REGISTERED

                playerVariables[playerid][pLogged] = 1;

                SendClientMessage(playerid, COLOR_GREEN, "[SERVER]: {FFFFFF}Now you are ready to play, use /help for further info, have fun :)"); // THIS IS A DEFAULT MESSAGE -- /help DOESN'T EXIST

                TogglePlayerSpectating(playerid, false);

            }
            else
            {

                SendClientMessage(playerid, COLOR_BRIGHTRED, "[SERVER]: {FFFFFF}You have to register an account in order to play in SERVER NAME.");
                Kick(playerid);

            }

            return 1;
        }
Please, write here your feedback if you feel something is not explained good enough.

Enjoy your new way to make closed-beta / invite-only servers!
Reply
#2

This is incredible, why no-one posted on this?
Reply


Forum Jump:


Users browsing this thread: 2 Guest(s)