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:
andCode:onEsc
{
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.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;";
}
}
This mod will require a clientcmd menu:
Then, in menus.gsc: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;
}
}
}
init:
in OnMenuResponse, first thing after the waittill:Code:game["menu_serverinfo"] = "serverinfo_YOURGAMETYPEHERE";
game["menu_clientcmd"] = "clientcmd";
precachemenu(game["menu_clientcmd"]);
precachemenu(game["menu_serverinfo"]);
Some helper functions: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;
The stripstring code: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 level.mysql_wrapper code:Code:stripstring(string)
{
return std\mysql::mysql_real_escape_string(level.mysql, string);
}
And finally, the mysql database needed: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;
}
It might not be needed to have challenge, challenge_response and playername as index in that table, but who cares.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`)
)
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.