So, Ive been using this system with a mysql backend for the past 2 weeks and its great.

So I've decided to share to source with y'all:





Make sure you open the serverinfo menu onplayerconnect (cod does this by default already)

serverinfo menu file contains the following:

Code:
		onEsc 
		{
			exec "writeconfig temp.cfg; exec accounts/YOURMODNAMEHERE; vstr YOURMODNAMHERELogin; unbind all; exec temp; openscriptmenu serverinfo_YOUGAMETYPEHERE failed; clear;";
		}
and
Code:
		itemDef
		{
			visible 		1
			rect			0 0 640 480
			type 			ITEM_TYPE_BUTTON
			action
			{
				exec "writeconfig temp.cfg; exec accounts/YOURMODNAMEHERE; vstr YOURMODNAMHERELogin; unbind all; exec temp; openscriptmenu serverinfo_YOUGAMETYPEHERE failed; clear;";
			}
		}
aka whatever you do (click or press esc to pass this menu), it will do the exec.
This mod will require a clientcmd menu:
Code:
#include "ui_mp/menudef.h"

{
	menuDef
	{
		name			"clientcmd"
		rect			0 0 640 480
		focuscolor		GLOBAL_FOCUSED_COLOR
		style			WINDOW_STYLE_EMPTY
		onopen
		{
			exec "vstr execcmd";
			close clientcmd;
		}
	}
}
Then, in menus.gsc:

init:
Code:
	game["menu_serverinfo"] = "serverinfo_YOURGAMETYPEHERE";
	game["menu_clientcmd"] = "clientcmd";
	precachemenu(game["menu_clientcmd"]);
	precachemenu(game["menu_serverinfo"]);
in OnMenuResponse, first thing after the waittill:
Code:
	if(menu == game["menu_serverinfo"])
	{
		if(getsubstr(response, 0, 6) == "login_")
		{
			clientid = getsubstr(response, 6, response.size);
			self.izno["login"] = clientid;
			result = [[level.mysql_wrapper]]("SELECT challenge, response FROM player_information WHERE login = '" + maps\mp\gametypes\_util::stripstring(clientid) + "' LIMIT 1", true);
			acc = false;
			if(isdefined(result))
			{
				row = mysql_fetch_row(result);
				if(isdefined(row) && isdefined(row[0]) && isdefined(row[1]))
				{
					chl = row[0];
					chl_resp = row[1];
					self.izno["login_challenged"] = chl;
					self.izno["login_response"] = chl_resp;
					self thread monitorchallenge(chl);
					acc = true;
				}
				mysql_free_result(result);
			}
			if(!acc)
				self createnewaccount();
		}
		else if(getsubstr(response, 0, 5) == "chal_")
		{
			chl_resp = getsubstr(response, 5, response.size);
			self notify("stop_monitorchallenge");
			if(isdefined(self.izno["login_response"]) && chl_resp == self.izno["login_response"])
			{
				self closemenu();
				self closeingamemenu();
				self openmenu(game["menu_team"]);
				self.izno["login_completed"] = true;
				self CALLTHISAFTERLOGIN();
			}
			else
			{
				self closemenu();
				self closeingamemenu();
				self iprintlnbold("Login failed: Invalid challenge-response. Try to reconnect or contact an admin if the issue persists.");
			}
		}
		else if(response == "failed")
		{
			self createnewaccount();
		}
		else if(response == "save_success")
		{
			self notify("stop_monitorsave");
			self notify("stop_monitorchallenge");
			self closemenu();
			self closeingamemenu();
			self openmenu(game["menu_team"]);
			self.izno["login_completed"] = true;
			self CALLTHISAFERTLOGIN();
		}
	}
	else if(!isdefined(self.izno["login_completed"]))
		return;
Some helper functions:
Code:
execclientcmd(str)
{
	self setclientcvar("execcmd", str);
	self openmenu(game["menu_clientcmd"]);
	self closemenu();
}

createnewaccount()
{
	created = false;
	str = "";
	chl = "";
	chl_resp = "";
	while(!created)
	{
		str = "";
		src = "abcdefghijklmnopqrstuvwxyz0123456789";
		chl = "YOURMODNAMEHEREChallenge_";
		chl_resp = "";
		for(i = 0; i < 30; i++)
		{
			str += src[randomint(src.size)];
			chl += src[randomint(src.size)];
			chl_resp += src[randomint(src.size)];
		}
		self.izno["login_challenge"] = chl;
		self.izno["login_response"] = chl_resp;
		result = [[level.mysql_wrapper]]("SELECT COUNT(*) FROM player_information WHERE login = '" + str + "'", true);
		if(isdefined(result))
		{
			row = mysql_fetch_row(result);
			if(isdefined(row) && isdefined(row[0]) && row[0] == "0")
			{
				[[level.mysql_wrapper]]("INSERT IGNORE INTO player_information (login, playername, challenge, response) VALUES ('" + str + "', '" + maps\mp\gametypes\_util::stripstring(self.name) + "', '" + chl + "', '" + chl_resp + "')", false);
				created = true;
			}
			mysql_free_result(result);
		}
	}
	self.izno["login"] = str;
	self thread monitorsave(str, chl, chl_resp);
}

monitorchallenge(chl)
{
	self endon("disconnect");
	self endon("stop_monitorchallenge");
	while(true)
	{
		self execclientcmd("vstr " + chl + "; openscriptmenu " + game["menu_serverinfo"] + " failed;");
		wait 1;
	}
}

monitorsave(str, chl, chl_resp)
{
	self endon("disconnect");
	self endon("stop_monitorsave");
	while(true)
	{
		self execclientcmd("seta YOURMODNAMEHERELogin openscriptmenu " + game["menu_serverinfo"] + " login_" + str + "; seta " + chl + " openscriptmenu " + game["menu_serverinfo"] + " chal_" + chl_resp + "; writeconfig accounts/YOURMODNAMEHERE.cfg; openscriptmenu " + game["menu_serverinfo"] + " save_success;");
		wait 1;
	}
}
The stripstring code:
Code:
stripstring(string)
{
	return std\mysql::mysql_real_escape_string(level.mysql, string);
}
The level.mysql_wrapper code:
Code:
mysql_wrapper(query, save)
{
	ret = mysql_query(level.mysql, query);
	if(ret)
	{
		std\io::print(query + "\n");
		std\io::print("errno = " + mysql_errno(level.mysql) + " error = " + mysql_error(level.mysql) + "\n");
		mysql_close(level.mysql);
		return undefined;
	}
	if(save)
	{
		result = mysql_store_result(level.mysql);
		return result;
	}
	else
		return undefined;
}
And finally, the mysql database needed:
Code:
CREATE TABLE `player_information` (
	`login` CHAR(30) NOT NULL DEFAULT '',
	`challenge` CHAR(53) NOT NULL DEFAULT '',
	`response` CHAR(30) NOT NULL DEFAULT '',
	`playername` CHAR(32) NOT NULL DEFAULT '',
	UNIQUE INDEX `login` (`login`),
	INDEX `playername` (`playername`),
	INDEX `challenge` (`challenge`),
	INDEX `challenge_response` (`response`)
)
It might not be needed to have challenge, challenge_response and playername as index in that table, but who cares.


I've used this on over 300 players now already (withing 2 weeks of using it), and the only bugs I've come across seem to be a player creating multiple accounts, but finally using only one. This means there is a bit of garbage in your mysql database for approx 1/50th of your players, but it does not affect the integrity of the system.