// Developers notes
/* # Coding guideline
* Add +1 at the end of array declarations to visually account for null bit.
* Always log first.
* Always notify discord last.
* Always wait for other queries to finish before initiating one: "sql_wait(sqlHandle);	// Wait for other queries to finish"
* Delete GVar strings as soon as possible, but at least before the player quits or the gamemode exits, as per: https://forum.sa-mp.com/showthread.php?t=151076
*/

/* # Style guide
* GLOBAL_CONSTANT
* local_variable
* someFunction
* SomeClass
* playerid  // Variable storing in-game player ID
* player_id // Variable storing database player ID
*/

/* # Adding a discord bot to your guild.
https://discordapp.com/oauth2/authorize?client_id=%CLIENT_ID%&scope=bot&permissions=3072
Where %CLEINT_ID% is the client id.
*/

/* # To do:
* Add geoiplib, to make connect messages fancier, and add a locate command.
* Show underscores as spaces and convert spaces in changename input to undescores before regex chack.
* Vip limit on characters.
* See why sql_insert_id wont work.
* Add all ADMIN_ECHO_CHANNEL messages to admin chat (not the real admin chat)
*/


/// Global definitions
// Colours
/*
#define COLOR_GREEN 0x33AA33AA
#define COLOR_YELLOW 0xFFFF00AA
#define COLOR_WHITE 0xFFFFFFAA
#define COLOR_BLUE 0x0000BBAA
#define COLOR_LIGHTBLUE 0x33CCFFAA
#define COLOR_ORANGE 0xFF9900AA
#define COLOR_RED 0xAA3333AA
#define COLOR_LIME 0x10F441AA
#define COLOR_NAVY 0x000080AA
#define COLOR_AQUA 0xF0F8FFAA
#define COLOR_CRIMSON 0xDC143CAA
#define COLOR_BISQUE 0xFFE4C4AA
#define COLOR_BLACK 0x000000AA
#define COLOR_CHARTREUSE 0x7FFF00AA
#define COLOR_BROWN 0XA52A2AAA
#define COLOR_CORAL 0xFF7F50AA
#define COLOR_GOLD 0xB8860BAA
#define COLOR_GREENYELLOW 0xADFF2FAA
#define COLOR_INDIGO 0x4B00B0AA
#define COLOR_IVORY 0xFFFF82AA
#define COLOR_LAWNGREEN 0x7CFC00AA
#define COLOR_SEAGREEN 0x20B2AAAA
#define COLOR_SEAGREEN 0x2E8B57AA
#define COLOR_LIMEGREEN 0x32CD32AA //<--- Dark lime
#define COLOR_MIDNIGHTBLUE 0X191970AA
#define COLOR_MAROON 0x800000AA
#define COLOR_OLIVE 0x808000AA
#define COLOR_ORANGERED 0xFF4500AA
#define COLOR_PINK 0xFFC0CBAA // - Light light pink
#define COLOR_SPRINGGREEN 0x00FF7FAA
#define COLOR_TOMATO 0xFF6347AA // - Tomato >:/ sounds wrong lol... well... :P
#define COLOR_YELLOWGREEN 0x9ACD32AA //- like military green
#define COLOR_MEDIUMAQUA 0x83BFBFAA
#define COLOR_MEDIUMMAGENTA 0x8B008BAA // dark magenta ^^
*/
#define COLOR_ACTIVEBORDER 0xB4B4B4FF
#define COLOR_ACTIVECAPTION 0x99B4D1FF
#define COLOR_ACTIVECAPTIONTEXT 0x000000FF
#define COLOR_ALICEBLUE 0xF0F8FFFF
#define COLOR_ANTIQUEWHITE 0xFAEBD7FF
#define COLOR_APPWORKSPACE 0xABABABFF
#define COLOR_AQUA 0x00FFFFFF
#define COLOR_AQUAMARINE 0x7FFFD4FF
#define COLOR_AZURE 0xF0FFFFFF
#define COLOR_BEIGE 0xF5F5DCFF
#define COLOR_BISQUE 0xFFE4C4FF
#define COLOR_BLACK 0x000000FF
#define COLOR_BLANCHEDALMOND 0xFFEBCDFF
#define COLOR_BLUE 0x0000FFFF
#define COLOR_BLUEVIOLET 0x8A2BE2FF
#define COLOR_BROWN 0xA52A2AFF
#define COLOR_BURLYWOOD 0xDEB887FF
#define COLOR_BUTTONFACE 0xF0F0F0FF
#define COLOR_BUTTONHIGHLIGHT 0xFFFFFFFF
#define COLOR_BUTTONSHADOW 0xA0A0A0FF
#define COLOR_CADETBLUE 0x5F9EA0FF
#define COLOR_CHARTREUSE 0x7FFF00FF
#define COLOR_CHOCOLATE 0xD2691EFF
#define COLOR_CONTROL 0xF0F0F0FF
#define COLOR_CONTROLDARK 0xA0A0A0FF
#define COLOR_CONTROLDARKDARK 0x696969FF
#define COLOR_CONTROLLIGHT 0xE3E3E3FF
#define COLOR_CONTROLLIGHTLIGHT 0xFFFFFFFF
#define COLOR_CONTROLTEXT 0x000000FF
#define COLOR_CORAL 0xFF7F50FF
#define COLOR_CORNFLOWERBLUE 0x6495EDFF
#define COLOR_CORNSILK 0xFFF8DCFF
#define COLOR_CRIMSON 0xDC143CFF
#define COLOR_CYAN 0x00FFFFFF
#define COLOR_DARKBLUE 0x00008BFF
#define COLOR_DARKCYAN 0x008B8BFF
#define COLOR_DARKGOLDENROD 0xB8860BFF
#define COLOR_DARKGRAY 0xA9A9A9FF
#define COLOR_DARKGREEN 0x006400FF
#define COLOR_DARKKHAKI 0xBDB76BFF
#define COLOR_DARKMAGENTA 0x8B008BFF
#define COLOR_DARKOLIVEGREEN 0x556B2FFF
#define COLOR_DARKORANGE 0xFF8C00FF
#define COLOR_DARKORCHID 0x9932CCFF
#define COLOR_DARKRED 0x8B0000FF
#define COLOR_DARKSALMON 0xE9967AFF
#define COLOR_DARKSEAGREEN 0x8FBC8BFF
#define COLOR_DARKSLATEBLUE 0x483D8BFF
#define COLOR_DARKSLATEGRAY 0x2F4F4FFF
#define COLOR_DARKTURQUOISE 0x00CED1FF
#define COLOR_DARKVIOLET 0x9400D3FF
#define COLOR_DEEPPINK 0xFF1493FF
#define COLOR_DEEPSKYBLUE 0x00BFFFFF
#define COLOR_DEFAULT_CHAT 0xFFFFFFFF
#define COLOR_DESKTOP 0x000000FF
#define COLOR_DIMGRAY 0x696969FF
#define COLOR_DODGERBLUE 0x1E90FFFF
#define COLOR_FIREBRICK 0xB22222FF
#define COLOR_FLBLUE 0x6495EDAA
#define COLOR_FLORALWHITE 0xFFFAF0FF
#define COLOR_FORESTGREEN 0x228B22FF
#define COLOR_GAINSBORO 0xDCDCDCFF
#define COLOR_GHOSTWHITE 0xF8F8FFFF
#define COLOR_GOLD 0xFFD700FF
#define COLOR_GOLDENROD 0xDAA520FF
#define COLOR_GRADIENTACTIVECAPTION 0xB9D1EAFF
#define COLOR_GRADIENTINACTIVECAPTION 0xD7E4F2FF
#define COLOR_GRAY 0x808080FF
#define COLOR_GRAYTEXT 0x808080FF
#define COLOR_GREEN 0x008000FF
#define COLOR_GREENYELLOW 0xADFF2FFF
#define COLOR_GREY 0xAFAFAFAA
#define COLOR_HIGHLIGHT 0x3399FFFF
#define COLOR_HIGHLIGHTTEXT 0xFFFFFFFF
#define COLOR_HONEYDEW 0xF0FFF0FF
#define COLOR_HOTPINK 0xFF69B4FF
#define COLOR_HOTTRACK 0x0066CCFF
#define COLOR_INACTIVEBORDER 0xF4F7FCFF
#define COLOR_INACTIVECAPTION 0xBFCDDBFF
#define COLOR_INACTIVECAPTIONTEXT 0x434E54FF
#define COLOR_INDIANRED 0xCD5C5CFF
#define COLOR_INDIGO 0x4B0082FF
#define COLOR_INFO 0xFFFFE1FF
#define COLOR_INFOTEXT 0x000000FF
#define COLOR_IVORY 0xFFFFF0FF
#define COLOR_KHAKI 0xF0E68CFF
#define COLOR_LAVENDER 0xE6E6FAFF
#define COLOR_LAVENDERBLUSH 0xFFF0F5FF
#define COLOR_LAWNGREEN 0x7CFC00FF
#define COLOR_LEMONCHIFFON 0xFFFACDFF
#define COLOR_LIGHTBLUE 0xADD8E6FF
#define COLOR_LIGHTCORAL 0xF08080FF
#define COLOR_LIGHTCYAN 0xE0FFFFFF
#define COLOR_LIGHTGOLDENRODYELLOW 0xFAFAD2FF
#define COLOR_LIGHTGRAY 0xD3D3D3FF
#define COLOR_LIGHTGREEN 0x90EE90FF
#define COLOR_LIGHTPINK 0xFFB6C1FF
#define COLOR_LIGHTSALMON 0xFFA07AFF
#define COLOR_LIGHTSEAGREEN 0x20B2AAFF
#define COLOR_LIGHTSKYBLUE 0x87CEFAFF
#define COLOR_LIGHTSLATEGRAY 0x778899FF
#define COLOR_LIGHTSTEELBLUE 0xB0C4DEFF
#define COLOR_LIGHTYELLOW 0xFFFFE0FF
#define COLOR_LIME 0x00FF00FF
#define COLOR_LIMEGREEN 0x32CD32FF
#define COLOR_LINEN 0xFAF0E6FF
#define COLOR_MAGENTA 0xFF00FFFF
#define COLOR_MAROON 0x800000FF
#define COLOR_MEDIUMAQUAMARINE 0x66CDAAFF
#define COLOR_MEDIUMBLUE 0x0000CDFF
#define COLOR_MEDIUMORCHID 0xBA55D3FF
#define COLOR_MEDIUMPURPLE 0x9370DBFF
#define COLOR_MEDIUMSEAGREEN 0x3CB371FF
#define COLOR_MEDIUMSLATEBLUE 0x7B68EEFF
#define COLOR_MEDIUMSPRINGGREEN 0x00FA9AFF
#define COLOR_MEDIUMTURQUOISE 0x48D1CCFF
#define COLOR_MEDIUMVIOLETRED 0xC71585FF
#define COLOR_MENU 0xF0F0F0FF
#define COLOR_MENUBAR 0xF0F0F0FF
#define COLOR_MENUHIGHLIGHT 0x3399FFFF
#define COLOR_MENUTEXT 0x000000FF
#define COLOR_MIDNIGHTBLUE 0x191970FF
#define COLOR_MINTCREAM 0xF5FFFAFF
#define COLOR_MISTYROSE 0xFFE4E1FF
#define COLOR_MOCCASIN 0xFFE4B5FF
#define COLOR_NAVAJOWHITE 0xFFDEADFF
#define COLOR_NAVY 0x000080FF
#define COLOR_OLDLACE 0xFDF5E6FF
#define COLOR_OLIVE 0x808000FF
#define COLOR_OLIVEDRAB 0x6B8E23FF
#define COLOR_ORANGE 0xFFA500FF
#define COLOR_ORANGERED 0xFF4500FF
#define COLOR_ORCHID 0xDA70D6FF
#define COLOR_PALEGOLDENROD 0xEEE8AAFF
#define COLOR_PALEGREEN 0x98FB98FF
#define COLOR_PALETURQUOISE 0xAFEEEEFF
#define COLOR_PALEVIOLETRED 0xDB7093FF
#define COLOR_PAPAYAWHIP 0xFFEFD5FF
#define COLOR_PEACHPUFF 0xFFDAB9FF
#define COLOR_PERU 0xCD853FFF
#define COLOR_PINK 0xFFC0CBFF
#define COLOR_PLUM 0xDDA0DDFF
#define COLOR_POWDERBLUE 0xB0E0E6FF
#define COLOR_PURPLE 0x800080FF
#define COLOR_RED 0xFF0000FF
#define COLOR_ROSYBROWN 0xBC8F8FFF
#define COLOR_ROYALBLUE 0x4169E1FF
#define COLOR_SADDLEBROWN 0x8B4513FF
#define COLOR_SALMON 0xFA8072FF
#define COLOR_SANDYBROWN 0xF4A460FF
#define COLOR_SCROLLBAR 0xC8C8C8FF
#define COLOR_SEAGREEN 0x2E8B57FF
#define COLOR_SEASHELL 0xFFF5EEFF
#define COLOR_SIENNA 0xA0522DFF
#define COLOR_SILVER 0xC0C0C0FF
#define COLOR_SKYBLUE 0x87CEEBFF
#define COLOR_SLATEBLUE 0x6A5ACDFF
#define COLOR_SLATEGRAY 0x708090FF
#define COLOR_SNOW 0xFFFAFAFF
#define COLOR_SPRINGGREEN 0x00FF7FFF
#define COLOR_STEELBLUE 0x4682B4FF
#define COLOR_TAN 0xD2B48CFF
#define COLOR_TEAL 0x008080FF
#define COLOR_THISTLE 0xD8BFD8FF
#define COLOR_TOMATO 0xFF6347FF
#define COLOR_TRANSPARENT 0xFFFFFF00
#define COLOR_TURQUOISE 0x40E0D0FF
#define COLOR_VIOLET 0xEE82EEFF
#define COLOR_WHEAT 0xF5DEB3FF
#define COLOR_WHITE 0xFFFFFFFF
#define COLOR_WHITESMOKE 0xF5F5F5FF
#define COLOR_WINDOW 0xFFFFFFFF
#define COLOR_WINDOWFRAME 0x646464FF
#define COLOR_WINDOWTEXT 0x000000FF
#define COLOR_YELLOW 0xFFFF00FF
#define COLOR_YELLOWGREEN 0x9ACD32FF
#define STEALTH_ORANGE 0xFF880000
#define STEALTH_OLIVE 0x66660000
#define STEALTH_GREEN 0x33DD1100
#define STEALTH_PINK 0xFF22EE00
#define STEALTH_BLUE 0x0077BB00

// Color groups
#define COLOR_DEFAULT_COMMAND_OUTPUT 0xFFFFFFFF // White
#define COLOR_NOTICE 0xAFAFAFAA 				// Grey
#define COLOR_WARNING_MESSAGE 0xFFA500FF        // Orange? (Looks more yellow to me)
#define COLOR_ERROR_MESSAGE 0xFF0000FF        	// Red
#define COLOR_GLOBAL_CHAT 0xFFFFFFFF        	// White
#define COLOR_PM_CHAT 0xFFFF00FF        		// Yellow
#define COLOR_VIP_CHAT 0x800080FF        		// Purple
#define COLOR_CREW_CHAT 0xFF9900AA        		// Orange
#define COLOR_ADMIN_CHAT 0xB8860BAA        		// Gold

// SQL datatypes
#define SQL_INTERGER_LENGTH 10
#define TIMESTAMP_LENGTH 19	// 1999-01-08 04:05:06 [5 + 4 + 2 + 2 +2 + 2 + 2]
#define SQL_FLOAT_LENGTH 16
#define HASH_LENGTH 128
#define IP_LEGNTH 45
#define REASON_LENGTH 121

// Log levels
#define LOGLEVEL_CHAT -2
#define LOGLEVEL_COMMAND -1
#define LOGLEVEL_DEBUG 0
#define LOGLEVEL_INFO 1
#define LOGLEVEL_NOTICE 2
#define LOGLEVEL_WARNING 3
#define LOGLEVEL_ERROR 4
#define LOGLEVEL_CRITICAL 5
#define LOGLEVEL_PANIC 6

// Userlevels
enum{   // Noted values as comments, for database reference.
	UNREGISTERED_PLAYER,		// 0
	REGISTERED_PLAYER,			// 1
	REGULAR_PLAYER,				// 2
	VIP_PLAYER,             	// 3
	MODERATOR_CREW,				// 4
	VETERAN_CREW,				// 5    Inactive admins and management that are allowed to keep their rights.
	ADMIN_CREW,					// 6
	UNDERCOVER_ADMIN_CREW,  	// 7
	MANAGEMENT_CREW,			// 8
	UNDRECOVER_MANAGEMENT_CREW, // 9
	FOUNDER_PLAYER,				// 10
	UNDERCOVER_FOUNDER_PLAYER   // 11
}

// Discord channels
enum{
	ECHO_CHANNEL,
	MAIN_CHANNEL,
	ADMIN_ECHO_CHANNEL,
	ADMIN_CHANNEL,
	MANAGEMENT_CHANNEL
}

// Chats
enum{
    CHAT_LOCAL, // Has to be 0, else a gVar would have to be created OnJoin, this conserves memory when no chatmode is set (which will be the case most likely)
	CHAT_WHISPER,
	CHAT_LOW,
	CHAT_ACTION,
	CHAT_SHOUT,
	CHAT_OC,
	CHAT_GLOBAL,
	CHAT_CALL,
	CHAT_SMS,
	CHAT_RADIO,
	CHAT_PM,
	CHAT_GANG,
	CHAT_GANG_OC,
	CHAT_FACTION,
	CHAT_FACTION_OC,
	CHAT_VIP,
	CHAT_CREW,
	CHAT_ADMIN,
	CHAT_MANAGEMENT,
	CHAT_PARTYLINE
}

// Dialogs
enum{
	DIALOG_CHANGENAME,
	DIALOG_REGISTER,
	DIALOG_ACCOUNT_CREATED,
	DIALOG_LOGIN,
	DIALOG_CHANGE_USERNAME,
	DIALOG_CHARACTERS,
	DIALOG_LOGIN_FAILED,
	DIALOG_DELETE_CHARACTER
}

// Environment settings	THESE SHOULD ALL BE READ FROM A CONFIG FILE!
static bool:scriptDebug = true;	// Debug setting
#define MODE_NAME "0.0a Build 5"
#define SERVER_NAME "Bone County RPG"
#define PG_HOST "127.0.0.1"
#define PG_ROLE "rpfw-dev"
#define PG_PASS "nJd&1k$0fs"
#define PG_DB "rpfw-dev"
#define PG_PORT 5432
#define DISCORD_HOME_GUILD_ID "666077037470941184"			// Emerald City Roleplay
#define DISCORD_ECHO_CHANNEL_ID "677855051166777344"		// #bcrp-echo
//#define DISCORD_ECHO_CHANNEL_ID "666078187079598080"		// #ecrp-echo
#define DISCORD_MAIN_CHANNEL_ID "677855315898793984"		// #bcrp
//#define DISCORD_MAIN_CHANNEL_ID "667396220402270228"		// #development
#define DISCORD_ADMIN_ECHO_CHANNEL_ID "672841892169383936"	// #admin-echo
#define DISCORD_ADMIN_CHANNEL_ID "667396026058932236"		// #admins
#define DISCORD_MANAGEMENT_CHANNEL_ID "666091376240361492"	// #management

// Game-mode limits
#define MAX_CHARACTERS_PER_USER 20  // Maximum of MAX_CHARACTERS_PER_USER_DIGITS digits (Due to SQL query string length)
#define MAX_CHARACTERS_PER_USER_DIGITS 3    // TODO user sizeof() and possibly move to OnGamemodeInit

// SQL plugin
new SQL:sqlHandle;

// discord-connector
//static DCC_Guild:homeGuild;			// Discord guild controlled by game community.
static DCC_Channel:echoChannel;			// Public echo channel.
static DCC_Channel:mainChannel;			// Channel for communirty notifications.
static DCC_Channel:adminEchoChannel;	// Admin echo channel.
static DCC_Channel:adminChannel;		// Channel for admin notifications.
static DCC_Channel:managementChannel;	// Channel for management notifications.

DiscordEcho(const message[], messageLevel){ // Write to echo channels.
	switch(messageLevel)	// Output facilities.
    {
        case ECHO_CHANNEL: {                                                                // Public echo message
			DiscordSendChannelMessage(echoChannel, message);
			DiscordSendChannelMessage(adminEchoChannel, message);   // Also send to admin echo channel, so admin can see everything in one channel without needing to constantly switch. Also handy as log on conflicts/complaints.
		}
		case MAIN_CHANNEL: {DiscordSendChannelMessage(mainChannel, message);}				// Public notification
        case ADMIN_ECHO_CHANNEL: {DiscordSendChannelMessage(adminEchoChannel, message);}	// Admin echo message
		case ADMIN_CHANNEL: {DiscordSendChannelMessage(adminChannel, message);}				// Admin notification
        case MANAGEMENT_CHANNEL: {DiscordSendChannelMessage(managementChannel, message);}	// Management notification
    }

	return 0;
}

// Natives
native WP_Hash(buffer[], len, const str[]);	// https://forum.sa-mp.com/showthread.php?t=570945
//native Float:loadavg();	// https://forum.sa-mp.com/showthread.php?t=260206	LINUX ONLY

// Includes
#include <a_samp>	// https://sa-mp.com
#include <sql>	// https://github.com/udan11/samp-plugin-sql	(Fastest player in town and only one supporting postgreSQL) | Examples: https://pastebin.com/67y2nq2n https://github.com/udan11/samp-plugin-sql/issues/10
#include <sscanf2>	// Newest version: https://github.com/maddinat0r/sscanf/releases | Better readme: https://github.com/Y-Less/sscanf
#include <strlib>	// https://github.com/oscar-broman/strlib
#include <Pawn.Regex>	// https://github.com/urShadow/Pawn.Regex
#include <Pawn.CMD>	// https://github.com/urShadow/Pawn.CMD	(Fastest player in town)
#include <gvar>	// https://github.com/samp-incognito/samp-gvar-plugin
#include <discord-connector>	// https://github.com/maddinat0r/samp-discord-connector	(Only player in town)
//#include <streamer>	// https://github.com/samp-incognisto/samp-streamer-plugin	NOT USED YET


/// Middle-ware
// Logging
logger(const log_level, const message[]){	// Write to logging facility
	// Do not log commands or debug when script debugging is turned off.
	if(scriptDebug == false){   // Debug is off
		if(log_level == LOGLEVEL_COMMAND || log_level == LOGLEVEL_DEBUG){   // Command or debug message

			return 0;   // Stop and do not log
		}
	}
	
	// Messagelevel tag
	new human_readable_log_level[8 + 1];
	switch(log_level){	// Assign log level.
		case LOGLEVEL_CHAT: human_readable_log_level = "chat";
		case LOGLEVEL_COMMAND: human_readable_log_level = "command";
        case LOGLEVEL_DEBUG: human_readable_log_level = "debug";
        case LOGLEVEL_INFO: human_readable_log_level = "info";
        case LOGLEVEL_NOTICE: human_readable_log_level = "notice";
		case LOGLEVEL_WARNING: human_readable_log_level = "warning";
		case LOGLEVEL_ERROR: human_readable_log_level = "error";
		case LOGLEVEL_CRITICAL: human_readable_log_level = "critical";
		case LOGLEVEL_PANIC: human_readable_log_level = "panic";
    }
    
	printf("[%s] %s", human_readable_log_level, message);	// Print to STDOUT.

	return 0;
}

/*// SQL plugin (BROKEN, look at it again after I have more then a week of experience)
//forward sqlQuery(SQL:handle, query[]);
//public sqlQuery(SQL:handle, query[]){
sqlQuery(SQL:handle, query[]){
	//sql_wait(handle);	// Wait for all queries to finish.

	new Result:result = sql_query(handle, query);
	if(sql_error(result)){printf("SQL error");}	// Did not work during a test with a faulty statement.
	return result;
}*/

// discord-connector
DiscordSendChannelMessage(DCC_Channel:channel, const message[]){
	if(scriptDebug){    // Log middle-ware event in debugging mode only
		new channel_name[100 + 1];	// Default value from tutorial
		DCC_GetChannelName(channel, channel_name);
		new logMessage[26 + 100 + 2000 + 1];	// Discord max message length = 2000
		format(logMessage, sizeof(logMessage), "Send to discord channel %s: %s", channel_name, message);

		logger(LOGLEVEL_DEBUG, logMessage);	// Actually log the message
	}

	DCC_SendChannelMessage(channel, message);	// Call discord-connector	DISABLE THIS TO STOP ALL OUTGOING DISCORD MESSAGES
}


/// Game-mode
// Account functions
forward changeName(playerid, const name[]);
public changeName(playerid, const name[]){	// Check if name is valid and force change if needed.
	// Prevent regex error when comparing against null
	if(isempty(name)){  // No name entered
	    ShowPlayerDialog(playerid, DIALOG_CHANGENAME, DIALOG_STYLE_INPUT, "Character name", "You must enter a name.\n\nExamples:\n Jo_Bo\n Dingle_P._J._Berry\n Jackson_DeForest_Kelley\n MaryJo_Ann_LaFluer", "Change", "");	// Force RP name.
	    
	    return 0;
	}
	
	// Check name
	new Regex:r = Regex_New("^[A-Z][a-z]{1,}([A-Z][a-z]{1,})?(_([A-Z][a-z]{1,}([A-Z][a-z]{1,})?|[A-Z]\\.(_[A-Z]\\.)?))?_[A-Z][a-z]{1,}([A-Z][a-z]{1,})?$");	// Regex name filter
	new isValidName = Regex_Check(name, r); // Validate name to filter
	Regex_Delete(r);
	if(!isValidName){	// Invalid role-play name
		ShowPlayerDialog(playerid, DIALOG_CHANGENAME, DIALOG_STYLE_INPUT, "Character name", "Please pick a realistic name, separate first and last name with an underscore.\n\nExamples:\n Jo_Bo\n Dingle_P._J._Berry\n Jackson_DeForest_Kelley\n MaryJo_Ann_LaFluer", "Change", "");	// Force RP name.
	}
	else {	// Valid role-play name
	    // Check for name in use
		/*new escaped_name[MAX_PLAYER_NAME + 1];  // TODO should be more, to account for escape characters.
	    sql_escape_string(sqlHandle, name, escaped_name, sizeof(escaped_name));	// Escape player name   BROKEN argument type mismatch on argument 2, but name is a sting...*/
	    new character_query[40 + MAX_PLAYER_NAME + 1], Result:character_result = sql_query(sqlHandle, character_query);
		format(character_query, sizeof(character_query), "SELECT id FROM character WHERE name = '%s'", name);
		
        if(sql_num_rows(character_result) > 0){  // Name taken
            ShowPlayerDialog(playerid, DIALOG_CHANGENAME, DIALOG_STYLE_INPUT, "Character name", "Name already taken. Please pick an original name.", "Change", "");	// Force RP name.
        }
        else{   // Name free
            // Notify all players
			new message[11 + 4 + MAX_PLAYER_NAME + 1];

			format(message, sizeof(message), "* [%i] %s joined.", playerid, fromPlayerName(name));
			SendClientMessageToAll(COLOR_NOTICE, message);	// Notify all players
			DiscordEcho(message, ECHO_CHANNEL);	// Notify discord public echo
			
			new client_connect_username[MAX_PLAYER_NAME + 1], admin_message[39 + 1];
            GetGVarString("client_connect_username", client_connect_username, sizeof(client_connect_username), playerid);   // Get client connect name
		    if(GetPlayerState(playerid) == PLAYER_STATE_NONE){	// Not spawned: character creation (Creating a character without registering)
		        // Notify admins
	            
	            format(admin_message, sizeof(admin_message), "* [%i] %s temporary charcter, created by: %s", playerid, fromPlayerName(name), client_connect_username);
				//  TODO inform in-game adminchat
				DiscordEcho(admin_message, ADMIN_ECHO_CHANNEL);

	            // Do nothing let continue to spawn, after spawn character will be saved to database.
		    }
	    	else{   // Spawnedplayer: Creating permanent or renaming an existing permanent character
	    	    // Notify admins
	            format(admin_message, sizeof(admin_message), "* [%i] %s created by: %s", playerid, fromPlayerName(name), client_connect_username);
				//  TODO inform in-game adminchat
				DiscordEcho(admin_message, ADMIN_ECHO_CHANNEL);
				
                // Send to skin selection
                ForceClassSelection(playerid);
                TogglePlayerSpectating(playerid, true);
			    TogglePlayerSpectating(playerid, false);
			}
	    
			// Change name  (No way yet to change name ingame)
			/*if(GetGVarInt("userlevel", playerid) > 1){	// Registered player.
				// Get username
				new username[MAX_PLAYER_NAME + 1], message[75 + MAX_PLAYER_NAME + 1];
				GetGVarString("username", username, sizeof(username), playerid);

				// Update or create character name in database
				new callback[1];
				new Result:result;
				new query[51 + MAX_PLAYER_NAME + MAX_PLAYER_NAME + 1];
				format(query, sizeof(query), "UPDATE \"user\"(name) VALUES('%s') WHERE username == '%s'", name, username);
				sql_wait(sqlHandle);	// Wait for other queries to finish
				result = sql_query(sqlHandle, query, callback = "", "r");
				
				// Inform user
				format(message, sizeof(message), "SERVER: Your username remains unchanged, next time connect as: %s", client_connect_username);
				SendClientMessage(playerid, COLOR_WHITE, message);
			}*/

			SetPlayerName(playerid, name);	// Change name in-game
		}
	}
	
	return 0;
}

forward register(playerid);
public register(playerid){	// Register player in database
	if(GetPlayerState(playerid) == PLAYER_STATE_NONE){	// Not spawned
		SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "ERROR: You need to have spawned to register.");
	}
	else{	// Spawned player
		if(GetGVarInt("userlevel", playerid) != UNREGISTERED_PLAYER){	// Registered player.
			new message[35 + MAX_PLAYER_NAME + 1];
			format(message, sizeof(message), "ERROR: You are already registered"); //, %s.",
			SendClientMessage(playerid, COLOR_WHITE, message);
		}
		else{	// Unregistered player.
			ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Password", "Please pick a strong and safe password, that you are able to remember.", "Continue", "Cancel");	// Password prompt
		}
	}
}

forward createCharacterRecord(playerid, id);
public createCharacterRecord(playerid, id){ // TODO do not force for registered players
	new playername[MAX_PLAYER_NAME + 1], character_query[94 + SQL_INTERGER_LENGTH + MAX_PLAYER_NAME + 3 + 1];
	GetPlayerName(playerid, playername, sizeof(playername));
	format(character_query, sizeof(character_query), "INSERT INTO character(user_id, name, skin_id) VALUES(%i, '%s', %i)", id, playername, GetPlayerSkin(playerid));
	sql_query(sqlHandle, character_query);
}

forward authenticate(playerid);
public authenticate(playerid){
    ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Sign in", "Enter your password to log on", "Log in", "Cancel");	// Show password confirmation dialog
}

forward characterSelection(playerid);
public characterSelection(playerid){
	if(GetGVarInt("userlevel", playerid) < REGISTERED_PLAYER){  // Unregistered player (Or worse?)
	    SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "SERVER: Register first with: /my account register");
	}
	else{   // Registered player
	    print("Character selection registered player.");
		// If spawned, save current character
		if(GetPlayerState(playerid) != PLAYER_STATE_NONE){
		    savePlayerState(playerid);
		}

		new id = getUserID(playerid);
		new character_query[75 + SQL_INTERGER_LENGTH + MAX_CHARACTERS_PER_USER_DIGITS + 1];
		format(character_query, sizeof(character_query), "SELECT id, name FROM character WHERE user_id = %i LIMIT %i", id, MAX_CHARACTERS_PER_USER);
		new Result:character_result = sql_query(sqlHandle, character_query);

		new character_string[1024 + 1], character_array[MAX_CHARACTERS_PER_USER];
		for(new i = 0; i < sql_num_rows(character_result); i++){
			new name[MAX_PLAYER_NAME + 1];
			sql_get_field_assoc_ex(character_result, i, "name", name, sizeof(name));
			format(character_string, sizeof(character_string), "%s%s\n", character_string, name);
			character_array[i] = sql_get_field_assoc_int_ex(character_result, i, "id");
		}
		print(character_string);
		new character_array_string[MAX_CHARACTERS_PER_USER * SQL_INTERGER_LENGTH + 1];
		strfrombin(character_array_string, character_array);    // Convert array to string for use with GVar
		SetGVarString("character_array_string", character_array_string, playerid);

		print("About to show dialog");
		ShowPlayerDialog(playerid, DIALOG_CHARACTERS, DIALOG_STYLE_LIST, "Characters", character_string, "Spawn", "New");
	}
}

forward savePlayerState(playerid);
public savePlayerState(playerid){
	new playername[MAX_PLAYER_NAME + 1], escaped_playername[MAX_PLAYER_NAME + 1], character_query[183 + SQL_INTERGER_LENGTH + 3 + 3 + 1 + SQL_FLOAT_LENGTH + SQL_FLOAT_LENGTH + SQL_FLOAT_LENGTH + SQL_FLOAT_LENGTH + MAX_PLAYER_NAME + 1];    // Should be longer to allow for escaped characters
	GetPlayerName(playerid, playername, sizeof(playername));
	sql_escape_string(sqlHandle, playername, escaped_playername, sizeof(escaped_playername));	// Escape player name
	new Float:health, Float:armour, Float:x, Float:y, Float:z, Float:Angle;
	// Unrealiable TODO change to gvars
	GetPlayerHealth(playerid, health);
	GetPlayerArmour(playerid, armour);
	GetPlayerPos(playerid, x, y, z);
	GetPlayerFacingAngle(playerid, Angle);
	
	format(character_query, sizeof(character_query), "UPDATE character SET (cash, health, armour, jailed, pos_x, pos_y, pos_z, rotation) = (%i, '%f', '%f', %i, '%f', '%f', '%f', '%f') WHERE name = '%s'", GetPlayerMoney(playerid), health, armour, 0, x, y, z, Angle, escaped_playername);
	sql_query(sqlHandle, character_query);
}

forward kickPlayer(playerid, kickerid, const reason[], banned);
public kickPlayer(playerid, kickerid, const reason[], banned){	// Kick a player
    // Issuer of kick
    new kickername[MAX_PLAYER_NAME + 1], kicker_id;
    if(kickerid == -1){ // Not kicked by in-game player
        kickername = "SERVER";
        kicker_id = -1;
	}
	else{   //  Kicked by in-game player
	    GetPlayerName(kickerid, kickername, sizeof(kickername));
	    kicker_id = getUserID(kickerid);
	}

	// Action
	new action[16 + SQL_INTERGER_LENGTH + 1];
	if(banned){
	    if(banned < 1){   // Permanent ban
			action = "banned permanently";
		}
		else{   // Temporary ban
	        format(action, sizeof(action), "banned for %i days", banned);
		}
    }
    else{
        action = "kicked";
	}

	// Notify all players
	new playername[MAX_PLAYER_NAME + 1], message[16 + 4 + MAX_PLAYER_NAME + MAX_PLAYER_NAME + 16 + REASON_LENGTH + 1], adminMessage[17 + 4 + MAX_PLAYER_NAME + 16 + MAX_PLAYER_NAME + REASON_LENGTH + 1];	// Max samp chat message length 128 - 8 for "/kick ? ".
    GetPlayerName(playerid, playername, sizeof(playername));
	format(message, sizeof(message), "* [%i] %s %s, reason: %s", playerid, fromPlayerName(playername), action, reason);
	format(adminMessage, sizeof(adminMessage), "* [%i] %s %s %s, reason: %s", kickerid, strreplace(kickername, "_", " "), action, fromPlayerName(playername), reason);
	logger(LOGLEVEL_INFO, message);	// Log event
	SendClientMessageToAll(COLOR_NOTICE, message);	// Notify all players.
	DiscordEcho(message, ECHO_CHANNEL);
	//TODO notify all in-game admins
	DiscordEcho(adminMessage, ADMIN_ECHO_CHANNEL);

	// Inform player (Redundant, but some people are blind or stupid)
	new player_message[30 + 6 + MAX_PLAYER_NAME + REASON_LENGTH + 1];	// Max samp chat message length 128 - 8 for "/kick ? ".
	format(player_message, sizeof(player_message), "SERVER: You have been %s, reason: %s", action, strreplace(kickername, "_", " "), reason);
	SendClientMessage(playerid, COLOR_ERROR_MESSAGE, player_message);	// Notify player
	if(banned){
	    SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "You may appeal this ban via the forum or Discord.");
	}

	new ip_id = getIPID(playerid);
	// Crearte IP kick record
	new ip_query[61 + 4 + REASON_LENGTH + 4 + 1];
	format(ip_query, sizeof(ip_query), "INSERT INTO ip_kick(ip_id, reason, kicker_id) VALUES(%i, '%s', %i)", ip_id, reason, kicker_id);
	sql_query(sqlHandle, ip_query);

	// Get IP kick record
	new ip_id_query[61 + 4 + REASON_LENGTH + 4 + 1];
	format(ip_id_query, sizeof(ip_id_query), "SELECT id FROM ip_kick(ip_id) WHERE ip_id = %i ORDER BY id DESC LIMIT 1", ip_id, reason, kicker_id);
	new Result:ip_id_result = sql_query(sqlHandle, ip_id_query);
	new ip_kick_id = sql_get_field_assoc_int(Result:ip_id_result, "id");

	// User kick record
	new user_query[79 + 4 + REASON_LENGTH + 4 + 1];
	format(user_query, sizeof(user_query), "INSERT INTO user_kick(user_id, reason, kicker_id, ip_kick_id) VALUES(%i, '%s', %i, %i)", getUserID(playerid), reason, kicker_id, ip_kick_id);
	sql_query(sqlHandle, user_query);

	SetTimerEx("Kick", 50, false, "i", playerid);	// Give the message 50 milliseconds to reach the player before kicking.
}

forward banExpiration(days);
public banExpiration(days){
	if(days < 1){   // Permanent ban
	    return 0;
	}
	else{   // Temporary ban
	    new year, month, day, hour, minute, second, expires[TIMESTAMP_LENGTH + 1];
		getdate(year, month, day);
		gettime(hour, minute, second);
		day = day + days;
		format(expires, sizeof(expires), "%i-%i-%i %i:%i:%i", year, month, day, hour, minute, second);

		return year, month, day, hour, minute, second;
	}
}

forward banPlayer(playerid, bannerid, const reason[], days);
public banPlayer(playerid, bannerid, const reason[], days){	// Ban & kick a player
	// Issuer of ban
	new banner_id;
	if(bannerid < 0){   // Not banned by in-game player
	    banner_id = bannerid;
	}
	else{   // Banned by in-game player
	    banner_id = getUserID(bannerid);
	}

	new ip_id = getIPID(playerid);
	// Create IP ban record
    new ip_query[81 + SQL_INTERGER_LENGTH + TIMESTAMP_LENGTH + REASON_LENGTH + SQL_INTERGER_LENGTH + SQL_INTERGER_LENGTH + 1];
    format(ip_query, sizeof(ip_query), "INSERT INTO ip_ban(ip_id, expires, reason, banner_id) VALUES(%i, %s, %s, %i) WHERE id = %i", ip_id, banExpiration(days), reason, banner_id, getUserID(playerid));
	sql_query(sqlHandle, ip_query);

	// Get IP ban record ID
	new ban_id_query[61 + MAX_PLAYER_NAME + 1];
	format(ban_id_query, sizeof(ban_id_query), "SELECT id FROM ip_ban WHERE ip_id = %i ORDER BY id DESC LIMIT 1", ip_id);
	new Result:ban_id_result = sql_query(sqlHandle, ban_id_query);
 	new ip_ban_id = sql_get_field_assoc_int(ban_id_result, "id");

	// User ban record (As banned players get only kicked, noneed to check for active ban)
	if(GetGVarInt("userlevel", playerid) > 1){  // Registered player
		new user_query[97 + SQL_INTERGER_LENGTH + TIMESTAMP_LENGTH + REASON_LENGTH + SQL_INTERGER_LENGTH + SQL_INTERGER_LENGTH + 1];
		format(user_query, sizeof(user_query), "INSERT INTO user_ban(user_id, expires, reason, banner_id, ip_ban_id) VALUES(%i, %s, %s, %i, %i) WHERE id = %i", ip_id, banExpiration(days), reason, banner_id, ip_ban_id);
		sql_query(sqlHandle, user_query);
	}

	// Kick player
	kickPlayer(playerid, bannerid, reason, days);
}


// Database functions
forward getIPID(playerid);
public getIPID(playerid){
    new player_ip[IP_LEGNTH + 1], query[37 + IP_LEGNTH + 1]; // Create varaibles
    GetPlayerIp(playerid, player_ip, sizeof(player_ip));    // Polulate player_ip variable
	format(query, sizeof(query), "SELECT id FROM ip WHERE address = '%s'", player_ip);   // Format query string
	new Result:result = sql_query(sqlHandle, query);    // Execute query
	
	return sql_get_field_assoc_int(result, "id");  // Get id from result
}

forward getUserID(playerid);
public getUserID(playerid){
	// SQL escape username
    new client_connect_username[MAX_PLAYER_NAME + 1], escaped_username[MAX_PLAYER_NAME + 1]; // TODO escaped_username should be longer to account for escape characters (Also increate database column size!)
	GetGVarString("client_connect_username", client_connect_username, sizeof(client_connect_username), playerid);   // Get client connect name
	sql_escape_string(sqlHandle, client_connect_username, escaped_username, sizeof(escaped_username));	// Escape player name

	// Get user ID
	new userid_query[47 + MAX_PLAYER_NAME + 1];
	format(userid_query, sizeof(userid_query), "SELECT id FROM \"user\" WHERE name = '%s'", escaped_username);
	new Result:result = sql_query(sqlHandle, userid_query);

	return sql_get_field_assoc_int(result, "id");
}


// Foo functions    TODO: Rename this description.
forward deleteAllGVars(playerid);
public deleteAllGVars(playerid){
    // Delete GVars as per https://forum.sa-mp.com/showthread.php?t=151076
	DeleteGVar("client_connect_username", playerid);    // From OnPlayerConnect
	DeleteGVar("userlevel", playerid);  				// From OnPlayerConnect
	DeleteGVar("hash", playerid);       				// From DIALOG_REGISTER
	DeleteGVar("character_array_string", playerid);		// From OnDialogResponse
	DeleteGVar("authentication_count", playerid);       // From DIALOG_LOGIN
	DeleteGVar("chatmode", playerid);                   // FROM cmd:my
}


// Chat functions
fromPlayerName(const name[]);
fromPlayerName(const name[]){ // Convert name from player name
	new output[MAX_PLAYER_NAME + 1];
	strfromliteral(output, name);
	strreplace(output, "_", " ");

	return output;
}

forward sendToChat(playerid, target, text[], receiver_id);
public sendToChat(playerid, target, text[], receiver_id){
    strtrim(text);  // Trim edge whitespaces
	if(isempty(text)){
	    return 0;   // Fail if text is empty
	}
    new playername[MAX_PLAYER_NAME + 1];
	GetPlayerName(playerid, playername, sizeof(playername));
	
	new chat_format, chat_color, chat_range, chat_userlevel, chat_character[1 + 1], chat_name[13 + 1], discord_channel;
	switch(target){
	    case CHAT_WHISPER:{
			chat_format = 2;
			chat_range = 3;
			chat_name = "whispers";
			chat_color = COLOR_DEFAULT_COMMAND_OUTPUT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_LOW:{
			chat_format = 2;
			chat_range = 10;
			chat_name = "speaks softly";
			chat_color = COLOR_DEFAULT_COMMAND_OUTPUT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
        case CHAT_LOCAL:{
            chat_format = 1;
            chat_range = 20;
			chat_color = COLOR_DEFAULT_COMMAND_OUTPUT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_SHOUT:{
		    chat_format = 2;
		    chat_range = 50;
			chat_name = "shouts";
			chat_color = COLOR_DEFAULT_COMMAND_OUTPUT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_ACTION:{
		    chat_format = 0;
		    chat_range = 20;
			chat_color = COLOR_DEFAULT_COMMAND_OUTPUT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
        case CHAT_OC:{
	        chat_format = 3;
            chat_range = 20;
            chat_character = "'";
            chat_name = "OOC";
			chat_color = COLOR_DEFAULT_COMMAND_OUTPUT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
  		case CHAT_GLOBAL:{
	  		chat_format = 3;
			chat_character = "`";
			chat_name = "Global";
			chat_color = COLOR_DEFAULT_COMMAND_OUTPUT;
			discord_channel = ECHO_CHANNEL;
		}
		case CHAT_GANG:{
		    chat_format = 3;
			chat_character = "~";
			chat_name = "Gang";
			chat_color = COLOR_CREW_CHAT;
			// TODO: Output to gang Discord channel
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_GANG_OC:{
		    chat_format = 3;
			chat_character = "$";
			chat_name = "Gang OC";
			chat_color = COLOR_CREW_CHAT;
			// TODO: Output to gang Discord channel
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_FACTION:{
		    chat_format = 3;
			chat_character = "!";
			chat_name = "Faction";
			chat_color = COLOR_CREW_CHAT;
			// TODO: Output to faction Discord channel
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_FACTION_OC:{
		    chat_format = 3;
			chat_character = "%";
			chat_name = "Faction OC";
			chat_color = COLOR_CREW_CHAT;
			// TODO: Output to faction Discord channel
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_CREW:{
		    chat_format = 3;
			chat_character = "@";
			chat_name = "Crew";
			chat_color = COLOR_CREW_CHAT;
			chat_userlevel = MODERATOR_CREW;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_ADMIN:{
		    chat_format = 3;
			chat_character = "#";
			chat_name = "Admin";
			chat_color = COLOR_ADMIN_CHAT;
			chat_userlevel = ADMIN_CREW;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_MANAGEMENT:{
		    chat_format = 3;
			chat_character = "&";
			chat_name = "Management";
			chat_color = COLOR_ADMIN_CHAT;
			chat_userlevel = MANAGEMENT_CREW;
			discord_channel = MANAGEMENT_CHANNEL;
		}
		case CHAT_VIP:{
		    chat_format = 3;
			chat_character = "^";
			chat_name = "VIP";
			chat_color = COLOR_VIP_CHAT;
			chat_userlevel = VIP_PLAYER;
			discord_channel = ADMIN_ECHO_CHANNEL;
			//TODO: Add VIP discord channel.
		}
		case CHAT_PARTYLINE:{
		    chat_format = -1;
			chat_character = "*";
			chat_color = COLOR_DEFAULT_COMMAND_OUTPUT;
			chat_userlevel = ADMIN_CREW;
			discord_channel = ECHO_CHANNEL;
		}
		case CHAT_PM:{
		    chat_format = 3;
		    chat_character = ">";
		    chat_name = "PM";
			chat_color = COLOR_PM_CHAT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_CALL:{
		    chat_format = 3;
		    chat_character = "+";
		    chat_name = "Call";
			chat_color = COLOR_PM_CHAT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_SMS:{
		    chat_format = 3;
		    chat_character = "-";
		    chat_name = "SMS";
			chat_color = COLOR_PM_CHAT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
		case CHAT_RADIO:{
		    chat_format = 3;
		    chat_character = "=";
		    chat_name = "Radio";
			chat_color = COLOR_PM_CHAT;
			discord_channel = ADMIN_ECHO_CHANNEL;
		}
	}
	
	// Format chat message
	new message[9 + 1 + sizeof(chat_name) + 4 + MAX_PLAYER_NAME + 128 + 1];
	switch(chat_format){
	    case -1: format(message, sizeof(message), "%s %s", chat_character, text);
		case 0: format(message, sizeof(message), "[%i] %s %s", playerid, fromPlayerName(playername), text);
		case 1: format(message, sizeof(message), "[%i] %s: %s", playerid, fromPlayerName(playername), text);
		case 2: format(message, sizeof(message), "[%i] %s %s: %s", playerid, fromPlayerName(playername), chat_name, text);
		case 3: format(message, sizeof(message), "%s (%s) [%i] %s: %s", chat_character, chat_name, playerid, fromPlayerName(playername), text);
	}
	
	// Authorisation
	if(chat_userlevel && chat_userlevel > GetGVarInt("userlevel", playerid)){    // User not privilged to read chat
	    if(target == CHAT_CREW){    // Show the user the message was sent to crew chat.
	        SendClientMessage(playerid, chat_color, message);
		}
		else{
		    SendClientMessage(playerid, COLOR_CREW_CHAT, "ERROR: You are not authorized to speak in this chat.");

		    return 0;   // Fail to send the message
		}
	}
	
    // Send TODO: Add checks for muted.
    if(chat_range){ // Ranged chats
        logger(LOGLEVEL_CHAT, message);	// Log event
        
        // Show message to players in range
        new Float:x, Float:y, Float:z;
		GetPlayerPos(playerid, x, y, z);
		for(new recipient_id, a = GetMaxPlayers(); recipient_id < a; recipient_id++){
			if(IsPlayerConnected(receiver_id)){
				new Float:distance = GetPlayerDistanceFromPoint(recipient_id, x, y, z);
				if(distance <= chat_range){  // Player nearby
		    		SendClientMessage(recipient_id, chat_color, message);
				}
			}
		}

    	DiscordEcho(message, discord_channel);
	}
	else{   // Global chats
	    if(target == CHAT_GLOBAL){  // Public chat
	        logger(LOGLEVEL_CHAT, message);	// Log event
     		SendClientMessageToAll(chat_color, message);
   		}
   		else if(target == CHAT_GANG || target == CHAT_GANG_OC || target == CHAT_FACTION || target == CHAT_FACTION_OC || target == CHAT_CALL || target == CHAT_SMS || target == CHAT_RADIO ){
			// TODO
			// TODO sendToAdmins();
			// TODO send to specific discord channel
		}
		else if(target == CHAT_PM){
		    new output[sizeof(message)];
			strdel(message, 0, 1);
		    format(output, sizeof(output), "<%s", message);
			SendClientMessage(receiver_id, chat_color, output);
			
			new receipt[12 + 1 + sizeof(chat_name) + 4 + MAX_PLAYER_NAME + 128 + 1];
			format(receipt, sizeof(receipt), "%s (%s) to [%i] %s: %s", chat_character, chat_name, receiver_id, fromPlayerName(playername), text);
			SendClientMessage(playerid, chat_color, receipt);

			new admin_message[15 + 1 + sizeof(chat_name) + 4 + MAX_PLAYER_NAME + 4 + 128 + 1];
			format(admin_message, sizeof(admin_message), "%s (%s) [%i] to %s [%i]: %s", chat_character, chat_name, playerid, fromPlayerName(playername), receiver_id, text);
			logger(LOGLEVEL_CHAT, admin_message);	// Log event
			
			sendToAdmins(chat_color, admin_message);
			DiscordEcho(message, discord_channel);
		}
		
		else{   // Chat with select users
			for(new recipient_id, a = GetMaxPlayers(); recipient_id < a; recipient_id++){
				if(IsPlayerConnected(recipient_id)){
					if(GetGVarInt("userlevel", recipient_id) >= chat_userlevel){    // Privileged
					    logger(LOGLEVEL_CHAT, message);	// Log event
					    
			  			SendClientMessage(recipient_id, chat_color, message);
			  			DiscordEcho(message, discord_channel);
					}
				}
			}
		}
	}

	return 1;
}

forward sendToAdmins(chat_color, const message[]);
public sendToAdmins(chat_color, const message[]){
    for(new recipient_id, a = GetMaxPlayers(); recipient_id < a; recipient_id++){
		if(IsPlayerConnected(recipient_id)){
			if(GetGVarInt("userlevel", recipient_id) >= ADMIN_CREW){    // Privileged
				SendClientMessage(recipient_id, chat_color, message);
			}
		}
	}
}

forward sendPM(playerid, text[], recipient_id);
public sendPM(playerid, text[], recipient_id){
	if (recipient_id == INVALID_PLAYER_ID){
		SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "ERROR: Player not found.");
	}
	else{
		sendToChat(playerid, CHAT_PM, text, recipient_id);
	}
}


// Commands
public OnPlayerCommandPerformed(playerid, cmd[], params[], result, flags){
    if(result == -1){
        SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "SERVER: Unknown command.");

        return 0;
    }

    return 1;
}
public PC_OnInit(){ // TODO
	return 1;	// Remove this once stuff is in place.
}

// Command commands
cmd:help(playerid, params[]){
    SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "SERVER: Ask questions in the chat, on Discord or the forum.\nTo list all commands: /cmds");

    return 1;
}
alias:help("h");

cmd:cmds(playerid, params[]){
	SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Command help syntax: \"/\" = start of cmd, \"()\" = cmd aliasses, \"|\" = or, \"<>\" = required parameter, \"[]\" = optional parameter, \",\" = next item.");
	SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Command help syntax example: /command (/alias | /other_alias) <parameter> [optional_parameter], /next_command");
	SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "General commands: /help (/h), /cmds (/cmd | /commands), /my (/myself | /mine), /v (/vehivle | /veh), /p (/player)");
	SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Chat commands: /me (/emote | /action), /w (/whisper), /low (/soft), /l (/local), /s (/shout | /scream), /o (' | /oc | /ooc)");
	SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Chat commands: /g (` | /global | /public), /gc (~) /fc (!), /vc (&), /cc (@), /my chatmode <value>");

    return 0;
}
alias:cmds("commands", "cmd");

// User commands
cmd:register(playerid, params[]){
	register(playerid);
}

cmd:my(playerid, params[]){
	// No option specified
	if(strlen(params) < 1){
		SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Usage: /my <option> <value>");
		return SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Options: account chatmode");
	}
	
	// Options
	if(!strcmp(params, "account", true, 7)){    // /my account
		strdel(params, 0, 8);	// Remove first 8 characters, "account ", from the string.

		// Values
		if (isequal(params, "register", .ignorecase = true)){   // /my account register
			register(playerid);	// Call public register function
		}
		else if (isequal(params, "characters", .ignorecase = true)){   // /my account characters
			characterSelection(playerid);
		}
		else if (isequal(params, "deletecharacter", .ignorecase = true)){   // /my account characters
			ShowPlayerDialog(playerid, DIALOG_DELETE_CHARACTER, DIALOG_STYLE_MSGBOX, "Charater deletion", "You are about to permanently delete this charater and all it's assets.\n This can NOT be undone.", "Destroy", "Abort");
		}
		else{   // Invalid value
			SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Usage: /my account <value>");
			SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Values: register characters deletecharacter");
		}
	}
	else if(!strcmp(params, "chatmode", true, 8)){    // /my chatmode
		strdel(params, 0, 9);	// Remove first 9 characters, "account ", from the string.

        // Values
        if (isequal(params, "whisper", .ignorecase = true)){
            SetGVarInt("chatmode", CHAT_WHISPER, playerid);
		}
		else if (isequal(params, "low", .ignorecase = true)){
            SetGVarInt("chatmode", CHAT_LOW, playerid);
		}
		else if (isequal(params, "local", .ignorecase = true)){
			SetGVarInt("chatmode", CHAT_LOCAL, playerid);
		}
        else if (isequal(params, "shout", .ignorecase = true)){
            SetGVarInt("chatmode", CHAT_SHOUT, playerid);
		}
		else if (isequal(params, "oc", .ignorecase = true)){
			SetGVarInt("chatmode", CHAT_OC, playerid);
		}
		else if (isequal(params, "global", .ignorecase = true)){
			SetGVarInt("chatmode", CHAT_GLOBAL, playerid);
		}
		else if (isequal(params, "gang", .ignorecase = true)){
		    SetGVarInt("chatmode", CHAT_GANG, playerid);
		}
		else if (isequal(params, "faction", .ignorecase = true)){
		    SetGVarInt("chatmode", CHAT_FACTION, playerid);
		}
		else if (isequal(params, "vip", .ignorecase = true)){
		    SetGVarInt("chatmode", CHAT_VIP, playerid);
		}
		else if (isequal(params, "crew", .ignorecase = true)){
		    SetGVarInt("chatmode", CHAT_CREW, playerid);
		}
		else if (isequal(params, "admin", .ignorecase = true)){
            SetGVarInt("chatmode", CHAT_ADMIN, playerid);
		}
        else if (isequal(params, "management", .ignorecase = true)){
            SetGVarInt("chatmode", CHAT_MANAGEMENT, playerid);
		}
		else{   // Invalid value
			printf("else isequals register");
			SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Usage: /my chatmode <value>");
			SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Values: whisper low local shout oc global gang faction vip crew admin management");
		}
	}
	else{   // Invalid option
		printf("else !strcmp account");
		SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "/my options: account chatmode");
	}
	return 0;
}
alias:my("myself", "mine");

// Chat commands
cmd:w(playerid, params[]){
    sendToChat(playerid, CHAT_WHISPER, params, 0);
    
    return 1;
}
alias:w("whisper");
cmd:low(playerid, params[]){
    sendToChat(playerid, CHAT_LOW, params, 0);

    return 1;
}
alias:low("soft");
cmd:l(playerid, params[]){
    sendToChat(playerid, CHAT_LOCAL, params, 0);

    return 1;
}
alias:l("local");
cmd:me(playerid, params[]){
    sendToChat(playerid, CHAT_ACTION, params, 0);

    return 1;
}
alias:me("emote", "action");
cmd:s(playerid, params[]){
	sendToChat(playerid, CHAT_SHOUT, params, 0);

    return 1;
}
alias:s("shout", "scream");
cmd:o(playerid, params[]){
    sendToChat(playerid, CHAT_OC, params, 0);

    return 1;
}
alias:o("oc", "ooc");
cmd:g(playerid, params[]){
    sendToChat(playerid, CHAT_GLOBAL, params, 0);

    return 1;
}
alias:g("global", "public");
cmd:gc(playerid, params[]){
    sendToChat(playerid, CHAT_GANG, params, 0);

    return 1;
}
cmd:fc(playerid, params[]){
    sendToChat(playerid, CHAT_FACTION, params, 0);

    return 1;
}
cmd:vc(playerid, params[]){
    sendToChat(playerid, CHAT_VIP, params, 0);

    return 1;
}
cmd:cc(playerid, params[]){
    sendToChat(playerid, CHAT_CREW, params, 0);

    return 1;
}

cmd:pm(playerid, params[]){
    new recipient_id, message[123 + 1];
	if (sscanf(params, "u s", recipient_id, message)){
		SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Usage: /pm <playerid> <message>");
	}
	else{
		sendPM(playerid, message, recipient_id);
	}

    return 1;
}
alias:pm("msg", "dm");

// Vehicle commands
cmd:v(playerid, params[]){
    SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "health/armor [AMOUNT]");
    return 1;
}
alias:v("vehicle", "veh");

// Player commands
cmd:p(playerid, params[]){
    SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "health/armor [AMOUNT]");
    return 1;
}
alias:p("player");


// a_samp events
public OnGameModeInit(){
	new message[36 + 22 + 1];
	format(message, sizeof(message), "* Global game-mode initialization: v%s", MODE_NAME);
	logger(LOGLEVEL_NOTICE, "* Global game-mode initialization.");	// Log event.

	// Hardcoded settings
	ShowPlayerMarkers(PLAYER_MARKERS_MODE_STREAMED);	// Player radar blip markers only visible to nearby players.

	// Set mode name
	SetGameModeText(SERVER_NAME);

	// SQL log level
	if(scriptDebug){
		sql_debug(LOG_ALL, LOG_ALL);	// Log everything everywhere.
	}
	else{
	    sql_debug(LOG_INFO, LOG_WARNING);    // Loglevel info for file and worning for console.
	}

	// Connect to database
	sqlHandle = SQL:sql_connect(SQL_HANDLER_POSTGRESQL, PG_HOST, PG_ROLE, PG_PASS, PG_DB, PG_PORT);
	printf("sqlconnection = %d", _:sqlHandle);
	if(!sql_ping(sqlHandle)){
		print( " + Database connection" );
	}
    else{
		print( "Database connection failed" );
	}

    // Initialize Discord
	//homeGuild = DCC_GetGuildId(DISCORD_HOME_GUILD_ID);	// Set home guild ID.	NOT NEEDED FOR NOW AND BROKEN
	echoChannel = DCC_FindChannelById(DISCORD_ECHO_CHANNEL_ID);	// Set main echo channel ID.
	mainChannel = DCC_FindChannelById(DISCORD_MAIN_CHANNEL_ID);	// Set main notification channel ID.
	adminEchoChannel = DCC_FindChannelById(DISCORD_ADMIN_ECHO_CHANNEL_ID);	// Set admin echo channel ID.
	adminChannel = DCC_FindChannelById(DISCORD_ADMIN_CHANNEL_ID);	// Set admin notification channel ID.
	managementChannel = DCC_FindChannelById(DISCORD_MANAGEMENT_CHANNEL_ID);	// Set management notification channel ID.
	
	DiscordEcho(message, ECHO_CHANNEL); // Notify Discord
	//DiscordEcho(message, MAIN_CHANNEL);   // TODO Enable after we are stable
	

	// Hobo's with a cane (0 ammo value makes them lose the cane as soon as they switch weapon)
	// Only homeless skins, as players should slowly class up in society.
	AddPlayerClass(134, -184.7607, 950.5010, 16.7740, 358.3032, 15, 0, 0, 0, 0, false);		// Fort Carson West boulevard right curb.
	AddPlayerClass(10, -184.7607, 950.5010, 16.7740, 358.3032, 15, 0, 0, 0, 0, false);		// Fort Carson West boulevard right curb.
	AddPlayerClass(78, 111.0115, 1189.2029, 18.1627, 89.0095, 15, 0, 0, 0, 0, false);		// Fort Carson South boulevard left curb.
	AddPlayerClass(129, 111.0115, 1189.2029, 18.1627, 89.0095, 15, 0, 0, 0, 0, false);		// Fort Carson South boulevard left curb.
	AddPlayerClass(162, -109.4227, 1242.4860, 16.8223, 183.5798, 15, 0, 0, 0, 0, false);	// Fort Carson East boulevard left curb.
	AddPlayerClass(77, -109.4227, 1242.4860, 16.8223, 183.5798, 15, 0, 0, 0, 0, false);		// Fort Carson East boulevard left curb.
	AddPlayerClass(79, -201.5379, 948.1683, 15.9131, 359.9720, 15, 0, 0, 0, 0, false); 		// Fort Carson West boulevard left curb.
	AddPlayerClass(196, -201.5379, 948.1683, 15.9131, 359.9720, 15, 0, 0, 0, 0, false); 	// Fort Carson West boulevard left curb.
	AddPlayerClass(239, 62.4694, 1205.0531, 18.8153, 89.9380, 15, 0, 0, 0, 0, false); 		// Fort Carson South boulevard right curb.
	AddPlayerClass(89, 62.4694, 1205.0531, 18.8153, 89.9380, 15, 0, 0, 0, 0, false); 		// Fort Carson South boulevard right curb.
	AddPlayerClass(135, -126.0831, 1242.5745, 18.6138, 183.2986, 15, 0, 0, 0, 0, false); 	// Fort Carson East boulevard right curb.
	AddPlayerClass(197, -126.0831, 1242.5745, 18.6138, 183.2986, 15, 0, 0, 0, 0, false); 	// Fort Carson East boulevard right curb.

	return 1;
}

public OnGameModeExit(){
	// Cycle every player
	for(new playerid, a = GetMaxPlayers(); playerid < a; playerid++){
		if(IsPlayerConnected(playerid))
		{
			deleteAllGVars(playerid);   // Delete GVars as per https://forum.sa-mp.com/showthread.php?t=151076

			// Set name back to username
			new client_connect_username[MAX_PLAYER_NAME + 1];
			GetGVarString("client_connect_username", client_connect_username, sizeof(client_connect_username), playerid);
			//SetPlayerName(playerid, client_connect_username);	// Change name in-game back to username, for login after restart  PROBLEM: Crashses the server on GMX.
			// TODO think of somthing for the usernames, kickign every player, or accapting character names as usersnames.
        }
	}

    logger(LOGLEVEL_NOTICE, "* Global game-mode termination.");	// Log event
	sql_wait(sqlHandle);	// Wait for queries to finish.
	sql_disconnect(sqlHandle);	// Disconnect from database.
	DiscordEcho("* Global game-mode termination.", ECHO_CHANNEL);	// Notify discord
	//DiscordEcho("* Global game-mode termination.", MAIN_CHANNEL);	// Enable when stable
	
	return 1;
}

public OnPlayerRequestClass(playerid, classid){	// Skin selection before spawn.
	if(scriptDebug){
		new playername[MAX_PLAYER_NAME + 1], message[22 + 4 + MAX_PLAYER_NAME + 1];
		GetPlayerName(playerid, playername, sizeof(playername));
		format(message, sizeof(message), "* [%i] %s Class selection.", playerid, fromPlayerName(playername));

		logger(LOGLEVEL_DEBUG, message);	// Log event.
	}

	SetPlayerPos(playerid, -185.5514, 944.2042, 15.9337);	// In front of Fort Carson city limits sign.
	SetPlayerFacingAngle(playerid, 182.7345);	// Charater looks toward the camera.
	SetPlayerCameraPos(playerid, -185.5514, 939.0957, 15.6594);	// Further in front of the Fort Carson city limits sign.
	SetPlayerCameraLookAt(playerid, -185.5514, 944.2042, 15.9337);	// In front of Fort Carson city limits sign.

	return 1;   // Must return one, or skin selection breaks
}

public OnPlayerConnect(playerid){
	// Create variables
	new playername[MAX_PLAYER_NAME + 1], playerip[IP_LEGNTH + 1];

	// Populate variables
	GetPlayerName(playerid, playername, sizeof(playername));
	GetPlayerIp(playerid, playerip, sizeof(playerip));

	// Global connection notification
	new admin_message[23 + MAX_PLAYER_NAME + IP_LEGNTH + 1];
	format(admin_message, sizeof(admin_message), "* [%d] %s (IP: %s) connected.", playerid, fromPlayerName(playername), playerip);
	logger(LOGLEVEL_INFO, admin_message);	// Log event
    sendToAdmins(COLOR_NOTICE, admin_message);	// Notify all admins.
	DiscordEcho(admin_message, ADMIN_ECHO_CHANNEL);	// Notify Discord admin echo.

	// Create IP record or update connection attempts
	new ip_query[109 + IP_LEGNTH + 1];
	format(ip_query, sizeof(ip_query), "INSERT INTO ip(address) VALUES('%s') ON CONFLICT (address) DO UPDATE SET connections = ip.connections+1", playerip);
	sql_query(sqlHandle, ip_query);
	
	// Check if IP is banned
	new ip_id = getIPID(playerid);
	
	new ban_query[72 + 1];
	format(ban_query, sizeof(ban_query), "SELECT id, reason FROM ip_ban WHERE id = %i AND expires > NOW()::timestamp", ip_id);
	new Result:ban_result = sql_query(sqlHandle, ban_query);
	if(sql_num_rows(ban_result) > 0){  // Banned
	    new kick_message[8 + REASON_LENGTH + 1];
	    format(kick_message, sizeof(kick_message), "Banned: %s", sql_get_field_assoc_int(ban_result, "reason"));
	    SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "You may appeal your ban via the forum or Discord.");
	    kickPlayer(playerid, -1, kick_message, 1);
	}

    // Get user record
	new escaped_username[MAX_PLAYER_NAME + 1], user_query[47 + MAX_PLAYER_NAME + 1];    // Should be longer to allow for escaped characters
	sql_escape_string(sqlHandle, playername, escaped_username, sizeof(escaped_username));	// Escape player name
	format(user_query, sizeof(user_query), "SELECT id FROM \"user\" WHERE name = '%s'", escaped_username);
	//new Result:result = sqlQuery(sqlHandle, user_query);  // Middleware broken.
	new Result:result = sql_query(sqlHandle, user_query);
	
	SetGVarString("client_connect_username", playername, playerid); // Used by register, DIALOG_LOGIN, getUserID & OnGameModeExit
	
	if(sql_num_rows(result) == 0){  // Unkown user
		SetGVarInt("userlevel", UNREGISTERED_PLAYER, playerid);	// Set userlevel unregistered
		changeName(playerid, playername);	// Check, force and set role-play name
	}
	else{   // Known user
		ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Sign in", "Enter your password to log on", "Log in", "Cancel");	// Show password confirmation dialog
	}

	//return 1;
}

public OnPlayerDisconnect(playerid, reason){
    savePlayerState(playerid);  // Save character
	deleteAllGVars(playerid);   // Delete GVars as per https://forum.sa-mp.com/showthread.php?t=151076
	
	new playername[MAX_PLAYER_NAME + 1], message[40 + 4 + MAX_PLAYER_NAME + 1];
	GetPlayerName(playerid, playername, sizeof(playername));
    switch(reason){
		case 0: format(message, sizeof(message), "* [%i] %s disconnected. (Lost Connection)", playerid, fromPlayerName(playername));
		case 1: format(message, sizeof(message), "* [%i] %s disconnected. (Leaving)", playerid, fromPlayerName(playername));
		case 2: format(message, sizeof(message), "* [%i] %s disconnected. (Kicked)", playerid, fromPlayerName(playername)); // Leave this in place for RCON kicks.
	}

 	logger(LOGLEVEL_INFO, message);	// Log event
	SendClientMessageToAll(COLOR_NOTICE, message);	// Notify all players.
	DiscordEcho(message, ECHO_CHANNEL);	// Notify discord.
	
	return 1;
}

public OnPlayerSpawn(playerid){
	if(scriptDebug){	// Log event in case of debugging.
		new playername[MAX_PLAYER_NAME + 1], message[MAX_PLAYER_NAME + 14 + 1];
		GetPlayerName(playerid, playername, sizeof(playername));
		format(message, sizeof(message), "* [%i] %s spawned.", playerid, fromPlayerName(playername));

		logger(LOGLEVEL_DEBUG, message);	// Log event.
	}

	if(GetGVarInt("userlevel", playerid) == UNREGISTERED_PLAYER){	// Unregistered player.
		SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "SERVER: To reserve your name, save your character, statistics and money, type: /register");
	}

	return 1;
}

public OnPlayerDeath(playerid, killerid, reason){
	new playername[MAX_PLAYER_NAME + 1], message[15 + MAX_PLAYER_NAME + 1];
    GetPlayerName(playerid, playername, sizeof(playername));
	format(message, sizeof(message), "* [%d] %s died.", playerid, fromPlayerName(playername));	// TODO Add killerid & reason.

	logger(LOGLEVEL_DEBUG, message);	// Log event
	DiscordEcho(message, ADMIN_ECHO_CHANNEL);	// Notify Discord admin echo.
	
	//ResetPlayerMoney(playerid);   TODO test if required
	//SpawnPlayer(playerid);

	return 1;
}

public OnVehicleSpawn(vehicleid)	// TODO for 0.0a
{
	return 1;
}

public OnVehicleDeath(vehicleid, killerid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerText(playerid, text[]){
	new shortcut_message[128 + 1];
	strfromliteral(shortcut_message, text);
	strdel(shortcut_message, 0, 1); // Remove chat character from text
	switch(strgetfirstc(text)){
		case '`': {
		    if(GetGVarInt("chatmode", playerid) == CHAT_LOCAL){
		        sendToChat(playerid, CHAT_GLOBAL, shortcut_message, 0);
			}
			else if (GetGVarInt("chatmode", playerid) == CHAT_GLOBAL){
				sendToChat(playerid, CHAT_LOCAL, shortcut_message, 0);
			}
			else{
			    sendToChat(playerid, CHAT_GLOBAL, shortcut_message, 0);
			}
		}
		case '~': {sendToChat(playerid, CHAT_GANG, shortcut_message, 0);}
		case '!': {sendToChat(playerid, CHAT_FACTION, shortcut_message, 0);}
		case '@': {sendToChat(playerid, CHAT_CREW, shortcut_message, 0);}
		case '#': {sendToChat(playerid, CHAT_ADMIN, shortcut_message, 0);}
		case '$': {sendToChat(playerid, CHAT_GANG_OC, shortcut_message, 0);}
		case '%': {sendToChat(playerid, CHAT_FACTION_OC, shortcut_message, 0);}
		case '^': {sendToChat(playerid, CHAT_MANAGEMENT, shortcut_message, 0);}
		//case '$': {sendToChat(playerid, CHAT_MANAGEMENT, shortcut_message[]);}
		case '&': {sendToChat(playerid, CHAT_VIP, shortcut_message, 0);}
		case '*': {sendToChat(playerid, CHAT_PARTYLINE, shortcut_message, 0);}
		case '+': {sendToChat(playerid, CHAT_CALL, shortcut_message, 0);}
		case '-': {sendToChat(playerid, CHAT_SMS, shortcut_message, 0);}
		case '=': {sendToChat(playerid, CHAT_RADIO, shortcut_message, 0);}
		case '>': {
		    new recipient_id, message[125 + 1];
    		if (sscanf(shortcut_message, "u s", recipient_id, message)){
				SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Usage: > <playerid> <message>");
			}
			else{
				sendPM(playerid, message, recipient_id);
			}
		}
		default:{
			new	chatmode = GetGVarInt("chatmode", playerid);
			if(!chatmode){  // Default is local chat
				sendToChat(playerid, CHAT_LOCAL, text, 0);
			}
			else{   // Send to preferred chat
			    printf("chatmode: %i", chatmode);
			    sendToChat(playerid, chatmode, text, 0);
			}
		}
 	}

	return 0;   // Return 1 for default behavior, return 0 to disable default output.
}

public OnPlayerCommandText(playerid, cmdtext[]){
	if(scriptDebug){	// Log event in case of debugging.
		new playername[MAX_PLAYER_NAME + 1], message[5 + 4 + MAX_PLAYER_NAME + 128 + 1]; // 128 = samp text input limit.
    	GetPlayerName(playerid, playername, sizeof(playername));
		format(message, sizeof(message), "[%d] %s: %s", playerid, fromPlayerName(playername), cmdtext);

		logger(LOGLEVEL_COMMAND, message);	// Log event
	}

	return 0;
}

public OnPlayerEnterVehicle(playerid, vehicleid, ispassenger)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerExitVehicle(playerid, vehicleid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerStateChange(playerid, newstate, oldstate) // TODO for 0.0a
{
	return 1;
}

public OnPlayerEnterCheckpoint(playerid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerLeaveCheckpoint(playerid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerEnterRaceCheckpoint(playerid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerLeaveRaceCheckpoint(playerid)	// TODO for 0.0a
{
	return 1;
}

public OnRconCommand(cmd[]){    // The website and some cronjobs do RCON commands. TODO create filter not to show some commands to preven spam
	new message[8 + 128 + 1];	// Max samp message legnth = 128.
	format(message, sizeof(message), "* RCON: %s", cmd);

	logger(LOGLEVEL_NOTICE, message);	// Log event
	DiscordEcho(message, MANAGEMENT_CHANNEL);

	return 1;
}

public OnPlayerRequestSpawn(playerid){  // After picking a skin in class selection.
	new playername[MAX_PLAYER_NAME + 1], message[14 + MAX_PLAYER_NAME + 1];
	GetPlayerName(playerid, playername, sizeof(playername));

	if(scriptDebug){	// Only log spawns when debuggins script.
		format(message, sizeof(message), "* [%d] %s Chose a character.", playerid, fromPlayerName(playername));
		logger(LOGLEVEL_DEBUG, message);	// Log event
	}

	if(GetGVarInt("userlevel", playerid) > UNREGISTERED_PLAYER){    // Registered player
	    createCharacterRecord(playerid, getUserID(playerid));	// Save character
	    
	    new client_connect_username[MAX_PLAYER_NAME + 1], admin_message[25 + 4 + MAX_PLAYER_NAME + MAX_PLAYER_NAME + 1];
	    GetGVarString("client_connect_username", client_connect_username, sizeof(client_connect_username), playerid);   // Get client connect name
	    format(admin_message, sizeof(admin_message), "[%i] %s has been created by %s.", playerid, fromPlayerName(playername), client_connect_username);
	    DiscordEcho(admin_message, ADMIN_ECHO_CHANNEL);
	}

	return 1;
}

public OnObjectMoved(objectid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerObjectMoved(playerid, objectid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerPickUpPickup(playerid, pickupid)	// TODO for 0.0a
{
	return 1;
}

public OnVehicleMod(playerid, vehicleid, componentid)	// TODO for 0.0a
{
	return 1;
}

public OnVehiclePaintjob(playerid, vehicleid, paintjobid)	// TODO for 0.0a
{
	return 1;
}

public OnVehicleRespray(playerid, vehicleid, color1, color2)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerSelectedMenuRow(playerid, row)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerExitedMenu(playerid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerInteriorChange(playerid, newinteriorid, oldinteriorid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerKeyStateChange(playerid, newkeys, oldkeys)	// TODO for 0.0a
{
	return 1;
}

public OnRconLoginAttempt(ip[], password[], success){
	new message[30 + IP_LEGNTH + 1];
	if(success){
		format(message, sizeof(message), "* [%s] authenticated via RCON.", ip);
	}
	else{
		format(message, sizeof(message), "* [%s] invalid RCON authentication.", ip);
	}

	logger(LOGLEVEL_NOTICE, message);	// Log event
	DiscordEcho(message, MANAGEMENT_CHANNEL);	// Notify Discord management.

	return 1;
}

public OnPlayerUpdate(playerid)	// Don't use for now.
{
	return 1;
}

public OnPlayerStreamIn(playerid, forplayerid){
	new playername[MAX_PLAYER_NAME + 1], forPlayerName[MAX_PLAYER_NAME + 1], message[30 + 4 + MAX_PLAYER_NAME + 4 + MAX_PLAYER_NAME + 1];
    GetPlayerName(playerid, playername, sizeof(playername));
	GetPlayerName(forplayerid, forPlayerName, sizeof(forPlayerName));
	format(message, sizeof(message), "* [%d] %s is now streamed in for [%d] %s.", playerid, fromPlayerName(playername), forplayerid, strreplace(forPlayerName, "_", " "));

	logger(LOGLEVEL_DEBUG, message);	// Log event

	/*format(message, sizeof(message), "[%d] is now streamed in for you.", playerid);
    SendClientMessage(forplayerid, 0xFFFFFFFF, string);*/

	// Is this when they reach eachoters drawdistance, or when they sync?

	return 1;
}

public OnPlayerStreamOut(playerid, forplayerid)	// TODO for 0.0a
{
	return 1;
}

public OnVehicleStreamIn(vehicleid, forplayerid)	// TODO for 0.0a
{
	return 1;
}

public OnVehicleStreamOut(vehicleid, forplayerid)	// TODO for 0.0a
{
	return 1;
}

public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]){
	switch(dialogid){

		case DIALOG_CHANGENAME:{
			if(GetGVarInt("userlevel", playerid) > UNREGISTERED_PLAYER){ // Registered player
			    if(!response){  // User aborted
			        return 0;   // Allow escaping dialogm without forcing to continue
				}
			}
			
			changeName(playerid, inputtext);    // Forced name check and change
		}

		case DIALOG_REGISTER:{
			if(response){ // If they clicked 'Yes' or pressed enter
				// Check password complexity
				new Regex:r = Regex_New("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[0-9!@#\\$%\\^&\\*\\(\\)\\-\\_=+[{\\]}\\\\|;:'\",<.>\\/?]).{8,}$");	// Regex password filter
				new isSecurePassword = Regex_Check(inputtext, r); // Validate name to filter
				Regex_Delete(r);
				if(!isSecurePassword){   // Insecure password
				    ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Insecure password", "Enter a secure password, containg at least:\n\n * A lowercase letter (a-z)\n * A capiral letter (A-Z)\n * A number (0-9).\n * A character that is not a letter (0-9!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?).\n * 8 characters.", "Register", "Cancel");	// Show password requirements.
				}
				else {  // Secure password
					// Hash password
					new buffer[HASH_LENGTH + 1], hash[HASH_LENGTH + 1];
					WP_Hash(buffer, sizeof(buffer), inputtext);
					GetGVarString("hash", hash, sizeof(hash), playerid);

					if(isempty(hash)){	// First password dialog
						SetGVarString("hash", buffer, playerid);	// Save password has a GVar
						ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Password confirmation", "Repeat your password to confirm.", "Register", "Cancel");	// Show password confirmation dialog
					}
					else{	// Password confirmation
						if(!isequal(hash, buffer)){	// Password does not match confirmation
					    	DeleteGVar("hash", playerid);
					    
							SendClientMessage(playerid, COLOR_RED, "ERROR: Password does not match password confirmation.");
							ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Password", "Enter the same password twice.", "Continue", "Cancel");	// Password prompt
						}
						else{	// Password matches confirmation
						 	DeleteGVar("hash", playerid);
					 	
							new client_connect_username[MAX_PLAYER_NAME + 1], playerIP[IP_LEGNTH + 1], escaped_username[MAX_PLAYER_NAME + 1];
							GetGVarString("client_connect_username", client_connect_username, sizeof(client_connect_username), playerid);
							GetPlayerIp(playerid, playerIP, sizeof(playerIP));
							sql_escape_string(sqlHandle, client_connect_username, escaped_username, sizeof(escaped_username));

							// Create user record
							new user_query[61 + MAX_PLAYER_NAME + HASH_LENGTH + 1 + IP_LEGNTH + 1];
							format(user_query, sizeof(user_query), "INSERT INTO \"user\"(name, password) VALUES('%s', '%s')", escaped_username, hash);
							/*new Result:user_result = sql_query(sqlHandle, user_query);
							//new id = sql_insert_id(user_result);   // Broken, always returns 0.
							// sql_insert_id workaround*/
							sql_query(sqlHandle, user_query);
							
							new id = getUserID(playerid);  // Get ID user user record
							
							SetGVarInt("userlevel", REGISTERED_PLAYER, playerid);

							// Create user_ip relation table record
							new user_ip_query[50 + SQL_INTERGER_LENGTH + SQL_INTERGER_LENGTH + 1 ];
							format(user_ip_query, sizeof(user_ip_query), "INSERT INTO user_ip(id, id) VALUES(%i, %i)", getIPID(playerid), id);
							sql_query(sqlHandle, user_ip_query);

							createCharacterRecord(playerid, id);	// Save character
							
							// Inform user
							new dialogMessage[135 + MAX_PLAYER_NAME + 1], message[31 + MAX_PLAYER_NAME + 1];
							format(dialogMessage, sizeof(dialogMessage), "To login as any of your characters, connect as: \nRemember your username carefully, as you have to use it to connect with SA-MP client!", client_connect_username);
							ShowPlayerDialog(playerid, DIALOG_ACCOUNT_CREATED, DIALOG_STYLE_MSGBOX, "Account created", dialogMessage, "Play", "");
							format(message, sizeof(message), "SERVER: Remember your username, %s!", client_connect_username);
							SendClientMessage(playerid, COLOR_WARNING_MESSAGE, message);
							
							// send discord admin echo
							new admin_message[30 + 4 + MAX_PLAYER_NAME + 1];
							format(admin_message, sizeof(admin_message), "[%i] %s has registered an account.", playerid, client_connect_username);
							DiscordEcho(message, ADMIN_ECHO_CHANNEL);
						}
					}
				}
			}
			else{ // Pressed ESC or clicked cancel
				DeleteGVar("hash", playerid);
			}
		}
		case DIALOG_LOGIN:{
			if(response){ // If they clicked 'Yes' or pressed enter
				// Escape username
				new client_connect_username[MAX_PLAYER_NAME + 1], escaped_username[MAX_PLAYER_NAME + 1];
				GetGVarString("client_connect_username", client_connect_username, sizeof(client_connect_username), playerid);
				sql_escape_string(sqlHandle, client_connect_username, escaped_username, sizeof(escaped_username));

				// Get account
				new user_query[82 + MAX_PLAYER_NAME + 1], hash[HASH_LENGTH + 1];
				format(user_query, sizeof(user_query), "SELECT id, password, level FROM \"user\" WHERE name = '%s'", escaped_username);
				new Result:user_result = sql_query(sqlHandle, user_query);
				sql_get_field_assoc_ex(user_result, 0, "password", hash, sizeof(hash));

				// Compare hashes
				new buffer[HASH_LENGTH + 1];
				WP_Hash(buffer, sizeof(buffer), inputtext);
				if (!isequal(buffer, hash)){ //	Hashes don't match
					// Brute-force protection
						new authentication_count = GetGVarInt("authentication_count", playerid);
						authentication_count++;
						SetGVarInt("authentication_count", authentication_count, playerid);
						ShowPlayerDialog(playerid, DIALOG_LOGIN_FAILED, DIALOG_STYLE_MSGBOX, "Authenticaion failed", "Invalid password.", "Wait", "");
						switch(authentication_count){
							case 0: SetTimerEx("authenticate", 3000, false, "i", playerid);	// Return to authentication workflow in 3 second.
							case 1: SetTimerEx("authenticate", 5000, false, "i", playerid);	// Return to authentication workflow in 5 seconds.
							case 2: SetTimerEx("authenticate", 7000, false, "i", playerid);	// Return to authentication workflow in 7 seconds.
							default: ShowPlayerDialog(playerid, DIALOG_CHANGE_USERNAME, DIALOG_STYLE_INPUT, "Username", "Pick an unregistered username.", "Change", "");  // After 4 failed authentication attempts
						}
				}
				else{	// Hashes match
					// Check bans
					/*new ip_query[45 + IP_LEGNTH + 1];
					format(ip_query, sizeof(ip_query), "SELECT ip_id, reason FROM ip WHERE address = '%s'", playerip);
					new Result:ip_result = sql_query(sqlHandle, ip_query);
					new ip_id = sql_get_field_assoc_int(ip_result, "ip_id");*/
					new ban_query[79 + SQL_INTERGER_LENGTH + 1];
					format(ban_query, sizeof(ban_query), "SELECT id, reason FROM user_ban WHERE user_id = %i AND expires > NOW()::timestamp", getUserID(playerid));
					new Result:ban_result = sql_query(sqlHandle, ban_query);
					if(sql_num_rows(ban_result) > 0){  // Banned
					    SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "You may appeal your ban via the forum or Discord.");
					    new reason[121 + 1];
						sql_get_field_assoc(ban_result, "reason", reason, sizeof(reason));
					    banPlayer(playerid, -1, reason, 10);
					}
					
				    // Set userlevel
        			new userlevel = sql_get_field_assoc_int(user_result, "level");
					SetGVarInt("userlevel", userlevel, playerid);
		
					// Get character names
					characterSelection(playerid);
				}
			}
			else{	// Pressed ESC or clicked cancel
				ShowPlayerDialog(playerid, DIALOG_CHANGE_USERNAME, DIALOG_STYLE_INPUT, "Username", "Pick an unregistered username.", "Change", "");
			}
		}
		case DIALOG_CHANGE_USERNAME:{
			if(response){ // If they clicked 'Yes' or pressed enter
				new Regex:r = Regex_New("^[0-9a-zA-Z\\[\\]\\(\\)\\$@._=]{1,}$");	// Regex name filter
				new isValidName = Regex_Check(inputtext, r);
				Regex_Delete(r);
				if(isValidName){	// Valid name
				    // Get user record
					new escaped_username[MAX_PLAYER_NAME + 1], user_query[36 + MAX_PLAYER_NAME + 1];
					sql_escape_string(sqlHandle, inputtext, escaped_username, sizeof(escaped_username));	// Escape player name
					format(user_query, sizeof(user_query), "SELECT id FROM \"user\" WHERE name = '%s'", escaped_username);
					//new Result:result = sqlQuery(sqlHandle, user_query);  // Middleware broken.
					new Result:result = sql_query(sqlHandle, user_query);
					
					if(sql_num_rows(result) > 0){  // Username taken
					    ShowPlayerDialog(playerid, DIALOG_CHANGE_USERNAME, DIALOG_STYLE_INPUT, "Username", "Pick an unregistered username.", "Change", "");
					}
					else{   // Username free
					    SetGVarString("client_connect_username", inputtext, playerid); // Used by register and DIALOG_LOGIN
					    SetPlayerName(playerid, inputtext);	// Change name in-game

						return 1;   // Do nothing to let player continue class selection
					}
				}
				else{	// Invalid name
					ShowPlayerDialog(playerid, DIALOG_CHANGE_USERNAME, DIALOG_STYLE_INPUT, "Username", "Invalid name. Names may only contain numbers (0-9), letters (a-z) & (A-Z) and special characters ([]()$@._=)", "Change", "");
				}
			}
			else{	// Pick another username
			    ShowPlayerDialog(playerid, DIALOG_CHANGE_USERNAME, DIALOG_STYLE_INPUT, "Username", "Pick an unregistered username.", "Change", "");
			}
		}
		case DIALOG_CHARACTERS:{
			if(response){ // Spawn as character
			    new character_array_string[MAX_CHARACTERS_PER_USER * SQL_INTERGER_LENGTH + 1], character_array[MAX_CHARACTERS_PER_USER];
       			GetGVarString("character_array_string", character_array_string, sizeof(character_array_string), playerid);
       			DeleteGVar("character_array_string");
       			
       			strtobin(character_array, character_array_string);
			    
				new character_query[100 + MAX_PLAYER_NAME + 1], name[24 + 1];
				format(character_query, sizeof(character_query), "SELECT name, skin_id, cash, armour, health, pos_x, pos_y, pos_z, rotation FROM character WHERE id = %i", character_array[listitem]);
				new Result:character_result = sql_query(sqlHandle, character_query);
				sql_get_field_assoc(character_result, "name", name, sizeof(name));
				new skin_id = sql_get_field_assoc_int(character_result, "skin_id");
				new cash = sql_get_field_assoc_int(character_result, "cash");
				new Float:armour = sql_get_field_assoc_int(character_result, "armour");
				new Float:health = sql_get_field_assoc_int(character_result, "health");
				new Float:pos_x = sql_get_field_assoc_int(character_result, "pos_x");
				new Float:pos_y = sql_get_field_assoc_int(character_result, "pos_y");
				new Float:pos_z = sql_get_field_assoc_int(character_result, "pos_z");
				new Float:rotation = sql_get_field_assoc_int(character_result, "rotation");
				
				// Temporary bug workaround TODO find cause
				if(pos_x == 0 && pos_y == 0 && pos_z == 0){ // Sometimes this happens. Due to unreliable OnPlayerDisconnect() & OnGamemodeExit(), due to character creation, or due to (forgot the other possiblity, but had a strong hunch)?
				    pos_x = -144.0328;
					pos_y = 1225.0564;
					pos_z = 19.8992;
					rotation = 175.5507;
				}
				SetSpawnInfo(playerid, 0, skin_id, pos_x, pos_y, pos_z, rotation, 0, 0, 0, 0, 0, 0);
                //SetSpawnInfo(playerid, 0, skin_id, -144.0328, 1225.0564, 19.8992, 175.5507, 0, 0, 0, 0, 0, 0 );
                
                SetPlayerName(playerid, name);	// Change name in-game
                SpawnPlayer(playerid);
                ResetPlayerMoney(playerid);
                GivePlayerMoney(playerid, cash);
                SetPlayerHealth(playerid, health);
                SetPlayerArmour(playerid, armour);
                
                // Notify all players
				new message[16 + 4 + MAX_PLAYER_NAME + 1];
				format(message, sizeof(message), "* [%i] %s joined.", playerid, fromPlayerName(name));
				SendClientMessageToAll(COLOR_NOTICE, message);	// Notify all players.
				DiscordEcho(message, ECHO_CHANNEL);	// Notify discord public echo
                
			}
			else{	// New character
			    // character cap
			    new id = getUserID(playerid);
				new character_query[60 + SQL_INTERGER_LENGTH + 1];
				format(character_query, sizeof(character_query), "SELECT id FROM character WHERE character_id = %i", id);
				new Result:character_result = sql_query(sqlHandle, character_query);
				printf("%i", sql_num_rows(character_result));
				if(sql_num_rows(character_result) >= MAX_CHARACTERS_PER_USER){  // At or over character limit
				    SendClientMessage(playerid, COLOR_WARNING_MESSAGE, "SERVER: Maximum characters reached, can not create another.");
				    characterSelection(playerid);
				}
			    else{
			        ShowPlayerDialog(playerid, DIALOG_CHANGENAME, DIALOG_STYLE_INPUT, "Character name", "Pick a name.\n\nExamples:\n Jo_Bo\n Dingle_P._J._Berry\n Jackson_DeForest_Kelley\n MaryJo_Ann_LaFluer", "Change", "");	// Force RP name.
			        // changeName(playerid, "");	// Pick name and save character ANNOYING this forces new character creation, no way out.
				}
                
			}
		}
        case DIALOG_LOGIN_FAILED:{
			ShowPlayerDialog(playerid, DIALOG_LOGIN_FAILED, DIALOG_STYLE_MSGBOX, "Authenticaion failed", "Please wait...", "Wait", "");
		}
		case DIALOG_DELETE_CHARACTER:{
			if(response){ // Delete character
				new playername[MAX_PLAYER_NAME + 1];
				GetPlayerName(playerid, playername, sizeof(playername));
				
				// Checkcharacter amount
				new id = getUserID(playerid), character_query[75 + SQL_INTERGER_LENGTH + MAX_CHARACTERS_PER_USER_DIGITS + 1];
				format(character_query, sizeof(character_query), "SELECT id, name FROM character WHERE user_id = %i LIMIT %i", id, MAX_CHARACTERS_PER_USER);
				new Result:character_result = sql_query(sqlHandle, character_query);
                if(sql_num_rows(character_result) < 2){  // 1 character or less
                    SendClientMessage(playerid, COLOR_WARNING_MESSAGE, "SERVER: You can not have less then one character.\nCreate another character,before deleting this one.");
                
				}
			    else{   // More then 1 character
					// delete character record
					new delete_query[50 + MAX_PLAYER_NAME + SQL_INTERGER_LENGTH + 1];
					format(delete_query, sizeof(delete_query), "DELETE FROM character WHERE name = '%s' AND user_id = %i", playername, getUserID(playerid));
					sql_query(sqlHandle, delete_query);
			
					// infordm admin echo
					new client_connect_username[MAX_PLAYER_NAME + 1], message[28 + 4 + MAX_PLAYER_NAME + MAX_PLAYER_NAME + 1];
					GetGVarString("client_connect_username", client_connect_username, sizeof(client_connect_username), playerid);   // Get client connect name
					format(message, sizeof(message), "* [%i] %s has been deleted by it's user: %s", playerid, fromPlayerName(playername), client_connect_username);
					DiscordEcho(message, ADMIN_ECHO_CHANNEL);
					// TODO send to in-game adminchat
					
					characterSelection(playerid);
				}
			}
		}
	}
	return 0; // MUST return 0 here, just like OnPlayerCommandText.
}


public OnPlayerClickPlayer(playerid, clickedplayerid, source)	// TODO for 0.0a
{
	return 1;
}
public OnEnterExitModShop(playerid, enterexit, interiorid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerGiveDamage(playerid, damagedid, Float: amount, weaponid)	// TODO for 0.0a
{
	return 1;
}

public OnPlayerTakeDamage(playerid, issuerid, Float: amount, weaponid)	// TODO for 0.0a
{
	return 1;
}


// discord-connector events
public DCC_OnMessageCreate(DCC_Message:message){

	// Originating Discord channel
	new DCC_Channel:channel;
	DCC_GetMessageChannel(message, channel);

	// Originating Discord user
	new DCC_User:author;
	DCC_GetMessageAuthor(message, author);

	// Message content
	new str[256];
	new command[32], params[128];
	DCC_GetMessageContent(message, str);
	sscanf(str, "s[32]s[128]", command, params);    // This string is to small some times, and throws an error BROKEN TODO

	// Ignore bots
	new bool:isBot;
	DCC_IsUserBot(author, isBot);
	if(isBot){

		return 1;
	}

	/*// Beyond this point, don't respond to commands on foreign guilds.
	if(guild != homeGuild){

		return 1;
	}*/

	// Beyond this point, only respond to privilaged channels.
	if(channel != adminEchoChannel && channel != adminChannel && channel != managementChannel){

		return 1;
	}


	//  Test command.
	if(!strcmp(command, "!test", true)){
		DiscordSendChannelMessage(channel, "Works.");
		print("Some said !test in the echo channel.");
	}

	return 1;
}


// Entrypoint
main(){
	// Credits
	print("\n*----------------------------------*");
	print("TPFW");
	new message[21 + 20 + 1];
	format(message, sizeof(message), " Role-play framework v%s", MODE_NAME);
	print(message);
	//printf("System load average: %f",loadavg());	// Linux only   TODO: test
	print("*----------------------------------*\n");
}
