// 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
*/

/* # 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.
* Add table for bans and kicks.
* Vip limit on characters.
*/


/// 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)

// SQL datatypes
#define SQL_INTERGER_LENGTH 10

// 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
	MODERATOR_CREW,			// 3
	ADMIN_CREW,				// 4
	MANAGEMENT_CREW,		// 5
	FOUNDER_PLAYER			// 6
}

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

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

// Environment settings	THESE SHOULD ALL BE READ FROM A CONFIG FILE!
static bool:scriptDebug = true;	// Debug setting
#define MODE_NAME "0.0a Build 4"
#define SERVER_NAME "Bone County RPG"
#define PG_HOST "127.0.0.1"
#define PG_ROLE "rpfw-dev"
#define PG_PASS "1234"
#define PG_DB "rpfw-dev"
#define PG_PORT 5432
#define DISCORD_HOME_GUILD_ID "666077037470941184"			// Emerald City Roleplay
#define DISCORD_ECHO_CHANNEL_ID "667396220402270228"		// #development
//#define DISCORD_ECHO_CHANNEL_ID "666078187079598080"		// #ecrp-echo
#define DISCORD_MAIN_CHANNEL_ID "667396220402270228"		// #development
//#define DISCORD_MAIN_CHANNEL_ID "667396220402270228"		// #development
#define DISCORD_ADMIN_ECHO_CHANNEL_ID "672841892169383936"	// #admin-echo
#define DISCORD_ADMIN_CHANNEL_ID "672841892169383936"		// #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

// 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: {DiscordSendChannelMessage(echoChannel, message);}				// Public echo message.
		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
// 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.
		print("past DIALOG_CHANGENAME");
	}
	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[999 + MAX_PLAYER_NAME + 1];
		format(character_query, sizeof(character_query), "SELECT id FROM character WHERE name = '%s'", name);
		new Result:character_result = sql_query(sqlHandle, character_query);
        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
	    	// Rename or character creation
		    if(GetPlayerState(playerid) == PLAYER_STATE_NONE){	// Not spawned: character creation
	            // Do nothing let continue to spawn, after spawn character will be saved to database.
		    }
	    	else{   // Spawned: renaming existing character
	    
			}
	    
		// Inform player & change name
		/*if(GetGVarInt("userlevel", playerid) > 1){	// Registered player.
			// Get username
			new username[MAX_PLAYER_NAME], 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");
			if(sql_error(result)){	// Untested.
				printf("SQL error");
				return 0;
			}

			// Inform user
			format(message, sizeof(message), "SERVER: Your username remains unchanged, next time connect as: %s", 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
		// Get username
		//new username[MAX_PLAYER_NAME + 1];
		//GetGVarString("username", username, sizeof(username), playerid);

		if(GetGVarInt("userlevel", playerid) != UNREGISTERED_PLAYER){	// Registered player.
			printf("3");
			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 getIPID(playerid);
public getIPID(playerid){
    new player_ip[45 + 1], query[37 + 45 + 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 kickPlayer(playerid, kickerid, const reason[]);
public kickPlayer(playerid, kickerid, const reason[]){	// Kick a player	TODO table for all kicks
	new playername[MAX_PLAYER_NAME +1 ], kickername[MAX_PLAYER_NAME +1];
    GetPlayerName(playerid, playername, sizeof(playername));
    GetPlayerName(kickerid, kickername, sizeof(kickername));
	
	// Notify all players
	new message[25 + 4 + MAX_PLAYER_NAME + 120 + 1];	// Max samp chat message length 128 - 8 for "/kick ? ".
	format(message, sizeof(message), "* [%i] %s kicked %s, reason: %s", kickerid, kickername, playername, reason);
	SendClientMessageToAll(COLOR_NOTICE, message);	// Notify all players.

	// Inform player (Redundant, but some people are blind or stupid)
	new playerMessage[25 + 4 + MAX_PLAYER_NAME + 120 + 1];	// Max samp chat message length 128 - 8 for "/kick ? ".
	format(message, sizeof(message), "You have been kicked by %s, reason: %s", kickerName, reason);
	SendClientMessage(playerid, COLOR_RED, playerMessage);	// Notify player

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

	DiscordEcho(message, ECHO_CHANNEL);	// Notify discord public echo
}

forward createCharacterRecord(playerid, id);
public createCharacterRecord(playerid, id){
	new playername[MAX_PLAYER_NAME + 1], character_query[94 + 10 + MAX_PLAYER_NAME + 3 + 1];
	GetPlayerName(playerid, playername, sizeof(playername));
	format(character_query, sizeof(character_query), "INSERT INTO character(character_id, name, skin_id) VALUES(%i, '%s', %i)", id, playername, GetPlayerSkin(playerid));
	sql_query(sqlHandle, character_query);
}

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");
}

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

forward characterSelection(playerid);
public characterSelection(playerid){
	// If spawned, save current character
	if(GetPlayerState(playerid) != PLAYER_STATE_NONE){
	    savePlayerState(playerid);
	}
	
	new id = getUserID(playerid);
	new character_query[87 + SQL_INTERGER_LENGTH + MAX_CHARACTERS_PER_USER_DIGITS + 1];
	format(character_query, sizeof(character_query), "SELECT id, name FROM character WHERE character_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");
	}

	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);

	ShowPlayerDialog(playerid, DIALOG_CHARACTERS, DIALOG_STYLE_LIST, "Characters", character_string, "Spawn", "New");
}

forward savePlayerState(playerid);
public savePlayerState(playerid){
	new playername[MAX_PLAYER_NAME], escaped_playername[MAX_PLAYER_NAME + 1], character_query[187 + 10 + 3 + 3 + 1 + 16 + 16 + 16 + 16 + 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, armor, 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 deleteAllGVars(playerid);
public deleteAllGVars(playerid){
    // Delete GVars as per https://forum.sa-mp.com/showthread.php?t=151076
	//DeleteGVar("IPID", playerid);   					// From OnPlayerConnect
	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
}

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

        return 0;
    }

    return 1;
}

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, "/cmds /my /v /p");

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

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");
	}
	
	// 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{   // Invalid value
			printf("else isequals register");
			SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Usage: /my account <value>");
			SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "Values: register");
		}
	}
	else{   // Invalid option
		printf("else !strcmp account");
		SendClientMessage(playerid, COLOR_DEFAULT_COMMAND_OUTPUT, "/my options: account");
	}
    return 0;
}

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

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

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


// a_samp events
public OnGameModeInit(){
	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 channels
	//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.
	
	// Notify Discord
	new message[36 + 22 + 1];
	format(message, sizeof(message), "* Global game-mode initialization: v%s", MODE_NAME);
	DiscordEcho(message, ECHO_CHANNEL);
	//DiscordEcho(message, MAIN_CHANNEL);   // 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(){
	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
	
	// 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
        }
	}

	return 1;
}

public OnPlayerRequestClass(playerid, classid){	// Skin selection before spawn.
	if(scriptDebug){
		new playerName[MAX_PLAYER_NAME], message[21 + MAX_PLAYER_NAME + 1];
		GetPlayerName(playerid, playerName, sizeof(playerName));
		format(message, sizeof(message), "* [%d] %s Class selection.", playerid, 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], playerip[45 + 1];

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

	// Global connection notification
	new admin_message[22 + MAX_PLAYER_NAME + 45 + 1];
	format(admin_message, sizeof(admin_message), "* [%d] %s (IP:%s) connected.", playerid, playername);

	Logger(LOGLEVEL_INFO, admin_message);	// Log event
    //SendClientMessageToAll(COLOR_NOTICE, adminMessage);	// Notify all players.	CHANGE TO ADMINS.
	DiscordEcho(admin_message, ADMIN_ECHO_CHANNEL);	// Notify Discord admin echo.

	// Create IP record or update connection attempts
	new ip_query[109 + 45 + 1]; // 45 for IPv6
	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 ban_query[40 + 45 + 1];	// 45 for IPv6
	format(ban_query, sizeof(ban_query), "SELECT is_banned FROM ip WHERE address = '%s'", playerip);
	new Result:ban_result = sql_query(sqlHandle, ban_query);
	new banned = sql_get_field_assoc_int(ban_result, "is_banned");
	printf("isbanned: %i", banned);
	// TODO kick if IP is banned.

    // 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 and DIALOG_LOGIN
	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){
    // Save character
    savePlayerState(playerid);
	
	deleteAllGVars(playerid);   // Delete GVars as per https://forum.sa-mp.com/showthread.php?t=151076
	
	new playername[MAX_PLAYER_NAME], message[39 + 4 + MAX_PLAYER_NAME + 1];
	GetPlayerName(playerid, playername, sizeof(playername));
    switch(reason)
    {
        case 0: format(message, sizeof(message), "* [%d] %s disconnected. (Lost Connection)", playerid, playername);
        case 1: format(message, sizeof(message), "* [%d] %s disconnected. (Leaving)", playerid, playername);
        case 2: format(message, sizeof(message), "* [%d] %s disconnected. (Kicked)", playerid, playername); // Leave this in place for RCON kicks.
    }

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

public OnPlayerSpawn(playerid){
	if(scriptDebug){	// Log event in case of debugging.
		new playerName[MAX_PLAYER_NAME], message[MAX_PLAYER_NAME + 14 + 1];
		GetPlayerName(playerid, playerName, sizeof(playerName));
		format(message, sizeof(message), "* [%d] %s spawned.", playerid, 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){	// WORK TO FINISH: Paste correct DiscordEcho messages in limetless script.
	new playerName[MAX_PLAYER_NAME], message[15 + MAX_PLAYER_NAME + 1];
    GetPlayerName(playerid, playerName, sizeof(playerName));
	format(message, sizeof(message), "* [%d] %s died.", playerid, playerName);	// Add killerid & reason.

	Logger(LOGLEVEL_DEBUG, message);	// Log event
	DiscordEcho(message, 2);	// Notify Discord admin echo.

	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 playerName[MAX_PLAYER_NAME], message[5 + MAX_PLAYER_NAME + 128 + 1];	// 128 = samp text input limit.
    GetPlayerName(playerid, playerName, sizeof(playerName));
	format(message, sizeof(message), "[%d] %s: %s", playerid, playerName, text);

	Logger(LOGLEVEL_CHAT, message);	// Log event.	NOT NEEDED FOR LIMITLESS: limitless already outputs local chat, but not /b /g /pm etc.
	DiscordEcho(message, 2);	// Notify Discord admins echo.

	return 1;
}

public OnPlayerCommandText(playerid, cmdtext[]){
	// Make conditional on scriptDebug, but not as long we base for limetless.
	new playerName[MAX_PLAYER_NAME], 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, 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[]){
	new message[8 + 128 + 1];	// Max samp message legnth = 128.
	format(message, sizeof(message), "* RCON: %s", cmd);

	Logger(LOGLEVEL_NOTICE, message);	// Log event

	return 1;
}

public OnPlayerRequestSpawn(playerid){  // After picking a skin in class selection.
	if(scriptDebug){	// Only lig spawns when debuggins script.
		new playerName[MAX_PLAYER_NAME], message[14 + MAX_PLAYER_NAME + 1];
		GetPlayerName(playerid, playerName, sizeof(playerName));
		format(message, sizeof(message), "* [%d] %s Chose a character.", playerid, playerName);

		Logger(LOGLEVEL_DEBUG, message);	// Log event
	}

	if(GetGVarInt("userlevel", playerid) > UNREGISTERED_PLAYER){    // Registered player
	    createCharacterRecord(playerid, getUserID(playerid));	// Save character
	}

	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 + 45 + 1];	// IPv6 max length = 45
	if(success){
		format(message, sizeof(message), "* [%s] authenticated via RCON.", ip);
	}
	else{
		format(message, sizeof(message), "* [%s] invalid RCON authentication.", ip);
	}

	Logger(1, message);	// Log event
	DiscordEcho(message, 4);	// Notify Discord management.

	return 1;
}

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

public OnPlayerStreamIn(playerid, forplayerid){
	new playerName[MAX_PLAYER_NAME], forPlayerName[MAX_PLAYER_NAME], 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, playerName, forplayerid, forPlayerName);

	Logger(1, 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:{
			changeName(playerid, inputtext);
		}

		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])(?=.*[!@#\\$%\\^&\\*\\(\\)\\-\\_=+[{\\]}\\\\|;:'\",<.>\\/?]).{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 special character (!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?).\n * 8 characters.", "Register", "Cancel");	// Show password requirements.
				}
				else {  // Secure password
					// Hash password
					new buffer[129], hash[129];
					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[45 + 1], escaped_username[MAX_PLAYER_NAME + 1];	// 45 for IPv6
							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[80 + MAX_PLAYER_NAME + 129 + 45 + 1];	// 45 for IPv6.
							format(user_query, sizeof(user_query), "INSERT INTO \"user\"(name, password, last_address) VALUES('%s', '%s', '%s')", escaped_username, hash, playerIP);
							//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 + 10 + 10 + 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);
						}
					}
				}
			}
			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[71 + MAX_PLAYER_NAME + 1], hash[129];
				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[128 + 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);
						printf("auth count %i", authentication_count);
						ShowPlayerDialog(playerid, DIALOG_LOGIN_FAILED, DIALOG_STYLE_MSGBOX, "Authenticaion failed ", "Invalid password.", "", "");
						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
				    // bankick if banned
				    
				    // Set userlevel
        			new userlevel = sql_get_field_assoc_int(user_result, "level");
					SetGVarInt("userlevel", userlevel, playerid);
		
					// Get character names
					characterSelection(playerid);
					/*new id = sql_get_field_assoc_int(user_result, "id");
					new character_query[87 + SQL_INTERGER_LENGTH + MAX_CHARACTERS_PER_USER_DIGITS + 1];
					format(character_query, sizeof(character_query), "SELECT id, name FROM character WHERE character_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");
				    }
				    
				    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);
					
				    ShowPlayerDialog(playerid, DIALOG_CHARACTERS, DIALOG_STYLE_LIST, "Characters", character_string, "Spawn", "New");*/
				}
			}
			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
				    // check if taken
				    // Get user record
					new escaped_username[MAX_PLAYER_NAME + 1], user_query[999 + 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[999 + MAX_PLAYER_NAME + 1], name[24 + 1];
				format(character_query, sizeof(character_query), "SELECT name, skin_id, cash, armor, 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 armor = sql_get_field_assoc_int(character_result, "armor");
				new health = sql_get_field_assoc_int(character_result, "health");
				new pos_x = sql_get_field_assoc_int(character_result, "pos_x");
				new pos_y = sql_get_field_assoc_int(character_result, "pos_y");
				new pos_z = sql_get_field_assoc_int(character_result, "pos_z");
				new rotation = sql_get_field_assoc_int(character_result, "rotation");
				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);
			}
			else{	// New character
			    // character cap
			    new id = getUserID(playerid);
				new character_query[62 + 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{
			        changeName(playerid, "");	// Pick name and save character
				}
                
			}
		}
        case DIALOG_LOGIN_FAILED:{
			ShowPlayerDialog(playerid, DIALOG_LOGIN_FAILED, DIALOG_STYLE_MSGBOX, "Authenticaion failed ", "Please wait...", "", "");
		}
	}
	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);

	// 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(" RPFW");
	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");
}
